From b487e5ecba0e7d796ecd850c6bef51de0f1eb173 Mon Sep 17 00:00:00 2001 From: tsite Date: Mon, 17 Nov 2025 23:36:21 -0500 Subject: [PATCH 1/8] hardfork: derive l1 block number from the settlement chain --- synd-mchain/src/methods/eth_methods.rs | 10 +++++++++- .../src/appchains/arbitrum/arbitrum_adapter.rs | 7 +++++-- synd-withdrawals/synd-enclave/enclave/accumulator.go | 3 ++- synd-withdrawals/synd-enclave/enclave/server.go | 3 +++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/synd-mchain/src/methods/eth_methods.rs b/synd-mchain/src/methods/eth_methods.rs index ea915c579..6db8de051 100644 --- a/synd-mchain/src/methods/eth_methods.rs +++ b/synd-mchain/src/methods/eth_methods.rs @@ -31,6 +31,9 @@ use std::{ time::UNIX_EPOCH, }; +/// hardfork timestamp +pub const HARDFORK_TS: u64 = 1764565200; + /// `eth_subscribe` #[allow(clippy::unwrap_used)] pub async fn eth_subscribe( @@ -190,8 +193,13 @@ pub fn eth_get_block_by_hash( let (hash, _): (FixedBytes<32>, bool) = p.parse()?; let number = u64::from_be_bytes(hash[hash.len() - 8..].try_into().map_err(to_err)?); let block = db.get_block(number)?; + let l1_block_number = if block.timestamp < HARDFORK_TS { + block.slot.seq_block_number + } else { + block.slot.set_block_number + }; Ok(alloy::rpc::types::Block { - header: create_header(number, block.slot.seq_block_number, block.timestamp), + header: create_header(number, l1_block_number, block.timestamp), ..Default::default() }) } diff --git a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs index 841304a94..9f8495c6f 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs @@ -22,7 +22,7 @@ use contract_bindings::synd::i_bridge::IBridge::MessageDelivered; use eyre::Result; use shared::types::{BlockBuilder, DelayedMsgsData, PartialBlock}; use std::collections::HashMap; -use synd_mchain::db::DelayedMessage; +use synd_mchain::{db::DelayedMessage, methods::eth_methods::HARDFORK_TS}; use thiserror::Error; use tracing::{debug, error, info, trace}; @@ -143,11 +143,14 @@ impl ArbitrumAdapter { .map(|x|x.1).collect::>() ); + let block_number = + if block.block_ref.timestamp < HARDFORK_TS { block.block_ref.timestamp } else { 0 }; + Ok(( mb_transactions.len() as u64, self.build_batch_txn( mb_transactions.into_iter().map(|x| x.0).collect(), - block.block_ref.number, + block_number, block.block_ref.timestamp, )?, )) diff --git a/synd-withdrawals/synd-enclave/enclave/accumulator.go b/synd-withdrawals/synd-enclave/enclave/accumulator.go index b540dce00..449d36b11 100644 --- a/synd-withdrawals/synd-enclave/enclave/accumulator.go +++ b/synd-withdrawals/synd-enclave/enclave/accumulator.go @@ -98,6 +98,7 @@ func buildL2MessageSegment(txs [][]byte) ([]byte, error) { } const TX_PER_BLOCK = 100 +const HARDFORK_TS = 1764565200 func buildBatch(txs [][]byte, ts uint64, blockNum uint64) ([]byte, error) { var data []byte @@ -114,7 +115,7 @@ func buildBatch(txs [][]byte, ts uint64, blockNum uint64) ([]byte, error) { data = append(data, segment...) } - if blockNum != 0 { + if blockNum != 0 && ts < HARDFORK_TS { segment, err := rlp.EncodeToBytes(blockNum) if err != nil { return nil, err diff --git a/synd-withdrawals/synd-enclave/enclave/server.go b/synd-withdrawals/synd-enclave/enclave/server.go index c4cf7c2b4..2a5215ff6 100644 --- a/synd-withdrawals/synd-enclave/enclave/server.go +++ b/synd-withdrawals/synd-enclave/enclave/server.go @@ -413,6 +413,9 @@ func processMessage(msg []byte, blockNum uint64, ts uint64) ([]byte, error) { if _, ok := allowedMsgs[msg[0]]; !ok { return nil, fmt.Errorf("unexpected message: type %d", msg[0]) } + if ts >= HARDFORK_TS { + blockNum = binary.BigEndian.Uint64(msg[33:41]) + } if msg[0] == arbostypes.L1MessageType_BatchPostingReport { requestId := msg[teetypes.DelayedMessageRequestIdOffset : teetypes.DelayedMessageRequestIdOffset+32] msg = make([]byte, teetypes.DelayedMessageDataOffset) From 877c8939a9c1cf5fd3a60ccb26ccc9f1f1a8ade6 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Mon, 24 Nov 2025 12:48:11 +0000 Subject: [PATCH 2/8] misc refactor and fix --- synd-mchain/src/db.rs | 17 +++++++++++++++-- synd-mchain/src/methods/common.rs | 9 ++------- synd-mchain/src/methods/eth_methods.rs | 3 --- .../src/appchains/arbitrum/arbitrum_adapter.rs | 15 ++++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/synd-mchain/src/db.rs b/synd-mchain/src/db.rs index c8487e5ab..542e674c4 100644 --- a/synd-mchain/src/db.rs +++ b/synd-mchain/src/db.rs @@ -110,6 +110,10 @@ pub struct Block { pub slot: Slot, } +/// timestamp for the hardfork where L1 block number changes from being derived from the seq chain +/// to the set chain +pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1764565200; + impl Block { /// The delayed message accumulator pub fn after_message_acc(&self) -> FixedBytes<32> { @@ -119,6 +123,15 @@ impl Block { pub const fn after_message_count(&self) -> u64 { self.before_message_count + self.messages.len() as u64 } + + /// l1 block number for this mchain block + pub const fn l1_block_number(&self) -> u64 { + if self.timestamp < L1_BLOCK_NUM_HARDFORK_TS { + self.slot.seq_block_number + } else { + self.slot.set_block_number + } + } } /// `rocksdb` implements the key-value trait @@ -360,13 +373,13 @@ pub trait ArbitrumDB { }; let mut inbox_acc = block.before_message_acc; let offset = self.get_migration_offset(); + let l1_block_number = block.l1_block_number(); for (i, (msg, acc)) in block.messages.iter_mut().enumerate() { - let l1_block_num = block.slot.seq_block_number; let message_hash = keccak256( ( [msg.kind], msg.sender, - l1_block_num, + l1_block_number, mblock.timestamp, U256::from(block.before_message_count + i as u64), msg.base_fee_l1, diff --git a/synd-mchain/src/methods/common.rs b/synd-mchain/src/methods/common.rs index 7dc83c441..1b701aaf6 100644 --- a/synd-mchain/src/methods/common.rs +++ b/synd-mchain/src/methods/common.rs @@ -1,6 +1,6 @@ //! Shared functions and constants for the `synd-mchain` RPC server methods -use crate::{db::Block, methods::eth_methods::L1_BLOCK_NUM_HARDFORK_TS}; +use crate::db::Block; use alloy::primitives::{address, Address, FixedBytes, U256}; use jsonrpsee::{ server::SubscriptionSink, @@ -36,18 +36,13 @@ pub fn create_log( /// Helper function to create a mock header object pub fn create_header(batch_count: u64, offset: u64, block: &Block) -> alloy::rpc::types::Header { - let l1_block_number = if block.timestamp < L1_BLOCK_NUM_HARDFORK_TS { - block.slot.seq_block_number - } else { - block.slot.set_block_number - }; alloy::rpc::types::Header { inner: alloy::consensus::Header { number: batch_count + offset, base_fee_per_gas: Some(1), extra_data: FixedBytes::<32>::ZERO.into(), #[allow(clippy::unwrap_used)] - mix_hash: U256::from(l1_block_number) + mix_hash: U256::from(block.l1_block_number()) .checked_shl(64) .unwrap() .checked_add(U256::from(1)) diff --git a/synd-mchain/src/methods/eth_methods.rs b/synd-mchain/src/methods/eth_methods.rs index a1638e4b7..358577b18 100644 --- a/synd-mchain/src/methods/eth_methods.rs +++ b/synd-mchain/src/methods/eth_methods.rs @@ -31,9 +31,6 @@ use std::{ time::UNIX_EPOCH, }; -/// hardfork timestamp -pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1764565200; - /// `eth_subscribe` #[allow(clippy::unwrap_used)] pub async fn eth_subscribe( diff --git a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs index cb5c22105..7204f7a26 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs @@ -22,7 +22,7 @@ use contract_bindings::synd::i_bridge::IBridge::MessageDelivered; use eyre::Result; use shared::types::{BlockBuilder, DelayedMsgsData, PartialBlock}; use std::collections::HashMap; -use synd_mchain::{db::DelayedMessage, methods::eth_methods::L1_BLOCK_NUM_HARDFORK_TS}; +use synd_mchain::db::{DelayedMessage, L1_BLOCK_NUM_HARDFORK_TS}; use thiserror::Error; use tracing::{debug, error, info, trace}; @@ -143,9 +143,10 @@ impl ArbitrumAdapter { .map(|x|x.1).collect::>() ); - let block_number = if block.block_ref.timestamp < L1_BLOCK_NUM_HARDFORK_TS { - block.block_ref.timestamp + let l1_block_number = if block.block_ref.timestamp < L1_BLOCK_NUM_HARDFORK_TS { + block.block_ref.number } else { + // TODO 0 }; @@ -153,7 +154,7 @@ impl ArbitrumAdapter { mb_transactions.len() as u64, self.build_batch_txn( mb_transactions.into_iter().map(|x| x.0).collect(), - block_number, + l1_block_number, block.block_ref.timestamp, )?, )) @@ -252,7 +253,7 @@ impl ArbitrumAdapter { fn build_batch_txn( &self, txs: Vec, - mchain_block_number: u64, + l1_block_number: u64, mchain_timestamp: u64, ) -> Result { debug!("Sequenced transactions: {:?}", txs); @@ -271,7 +272,7 @@ impl ArbitrumAdapter { if block.len() >= TX_PER_BLOCK || (!block.is_empty() && size > MAX_L2_MESSAGE_SIZE) { messages.push(BatchMessage::L2(L1IncomingMessage { header: L1IncomingMessageHeader { - block_number: mchain_block_number, + block_number: l1_block_number, timestamp: mchain_timestamp, }, l2_msg: block, @@ -287,7 +288,7 @@ impl ArbitrumAdapter { if !block.is_empty() { messages.push(BatchMessage::L2(L1IncomingMessage { header: L1IncomingMessageHeader { - block_number: mchain_block_number, + block_number: l1_block_number, timestamp: mchain_timestamp, }, l2_msg: block, From b772a50a0f9dd06e1a8228a855c22e5a617d4b0f Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Tue, 25 Nov 2025 12:58:29 +0000 Subject: [PATCH 3/8] refactor batch building to take a l1 block as input --- Cargo.lock | 2 + shared/src/types.rs | 2 +- synd-chain-ingestor/src/client.rs | 30 +- synd-chain-ingestor/tests/integration_test.rs | 14 +- synd-mchain/src/db.rs | 3 +- .../bin/synd-translator/src/spawn.rs | 16 +- synd-translator/crates/common/src/types.rs | 18 +- .../appchains/arbitrum/arbitrum_adapter.rs | 275 +++++------------- .../src/appchains/shared/mod.rs | 2 - .../src/appchains/shared/rollup_adapter.rs | 42 +-- .../shared/sequencing_transaction_parser.rs | 121 +++----- .../crates/synd-slotter/Cargo.toml | 9 +- .../crates/synd-slotter/src/batch.rs | 143 +++++++++ .../crates/synd-slotter/src/lib.rs | 1 + .../crates/synd-slotter/src/slotter.rs | 156 +++++++--- 15 files changed, 450 insertions(+), 384 deletions(-) create mode 100644 synd-translator/crates/synd-slotter/src/batch.rs diff --git a/Cargo.lock b/Cargo.lock index 7605a4467..34f891999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7798,10 +7798,12 @@ dependencies = [ "alloy", "async-trait", "common", + "contract-bindings", "ctor", "eyre", "prometheus-client", "shared", + "synd-block-builder", "synd-chain-ingestor", "synd-mchain", "thiserror 2.0.17", diff --git a/shared/src/types.rs b/shared/src/types.rs index 2a135b98a..e0f3193ec 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -35,7 +35,7 @@ pub type DelayedMsgsData = HashMap; #[async_trait] pub trait BlockBuilder: Send { /// Process a single slot - fn build_block(&self, block: &PartialBlock, msgs_data: DelayedMsgsData) -> eyre::Result; + fn build_block(&self, block: PartialBlock, msgs_data: DelayedMsgsData) -> eyre::Result; } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] diff --git a/synd-chain-ingestor/src/client.rs b/synd-chain-ingestor/src/client.rs index 1bcc5cf97..c1ec18464 100644 --- a/synd-chain-ingestor/src/client.rs +++ b/synd-chain-ingestor/src/client.rs @@ -200,7 +200,7 @@ struct BlockStream< > { stream: Pin>>>, buffer: VecDeque, - block_builder: Arc, + block_builder: B, indexed_block_number: u64, inbox_addr: Option
, client: EthClient, @@ -225,7 +225,7 @@ impl< { fn new( stream: S, - block_builder: Arc, + block_builder: B, start_block: u64, client: EthClient, init_data: (Vec
, u64), @@ -313,6 +313,26 @@ impl< B: BlockBuilder + Sync, > BlockStreamT for BlockStream { + /// Receives the next block from the stream once a block with timestamp >= the provided + /// timestamp has arrived. + /// + /// This function: + /// 1. Processes the initial message on first call (converting init data into batched requests) + /// 2. Executes queued init requests or polls the stream for new block data + /// 3. Builds blocks using the block builder with delayed message data + /// 4. Manages a buffer of blocks, handling reorgs by updating stale blocks in-place + /// 5. Returns the oldest block in the buffer once its timestamp meets the requirement + /// + /// # Arguments + /// * `timestamp` - The minimum timestamp threshold for the returned block + /// + /// # Returns + /// The next block with `block.timestamp >= timestamp`, pulled from the back of the buffer + /// + /// # Errors + /// - If the stream closes unexpectedly + /// - If a reorg affects a block that has already been removed from the buffer + /// - If block building or delayed message processing fails #[allow(clippy::unwrap_used)] async fn recv(&mut self, timestamp: u64) -> eyre::Result { let mut responses = vec![]; @@ -340,7 +360,7 @@ impl< self.inbox_addr, ) .await?; - let block = self.block_builder.build_block(&partial_block, delayed_msgs_data)?; + let block = self.block_builder.build_block(partial_block, delayed_msgs_data)?; let block_number = block.block_ref().number; assert!( block_number <= self.indexed_block_number, @@ -498,7 +518,7 @@ pub trait IngestorProvider: Sync { &self, start_block: u64, addresses: Vec
, - block_builder: Arc + Sync + 'static>, + block_builder: impl BlockBuilder + Sync + 'static, client: EthClient, inbox_addr: Option
, ) -> Result, ClientError> { @@ -598,7 +618,7 @@ impl IngestorProvider for IngestorProviderImpl { &self, start_block: u64, addresses: Vec
, - block_builder: Arc + Sync + 'static>, + block_builder: impl BlockBuilder + Sync + 'static, client: EthClient, inbox_addr: Option
, ) -> Result, ClientError> { diff --git a/synd-chain-ingestor/tests/integration_test.rs b/synd-chain-ingestor/tests/integration_test.rs index cc6741ef9..55044c96d 100644 --- a/synd-chain-ingestor/tests/integration_test.rs +++ b/synd-chain-ingestor/tests/integration_test.rs @@ -2,7 +2,7 @@ use common::types::SequencingBlock; use shared::types::{BlockBuilder, PartialBlock}; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use synd_chain_ingestor::{ client::{BlockStreamT, IngestorProvider, IngestorProviderConfig, IngestorProviderImpl}, eth_client::EthClient, @@ -28,7 +28,7 @@ mod tests { impl BlockBuilder for MockBlockBuilder { fn build_block( &self, - block: &PartialBlock, + block: PartialBlock, _msgs_data: DelayedMsgsData, ) -> eyre::Result { Ok(SequencingBlock { @@ -127,9 +127,8 @@ mod tests { ) .await; - let mut block_stream = client - .get_blocks(start_block, vec![], Arc::new(MockBlockBuilder), eth_client, None) - .await?; + let mut block_stream = + client.get_blocks(start_block, vec![], MockBlockBuilder, eth_client, None).await?; for _ in 0..post_init_blocks { mine_block(&anvil.provider, 10).await?; @@ -184,9 +183,8 @@ mod tests { ) .await; - let mut block_stream = client - .get_blocks(start_block, vec![], Arc::new(MockBlockBuilder), eth_client, None) - .await?; + let mut block_stream = + client.get_blocks(start_block, vec![], MockBlockBuilder, eth_client, None).await?; for _ in 0..post_init_blocks { mine_block(&anvil.provider, 10).await?; diff --git a/synd-mchain/src/db.rs b/synd-mchain/src/db.rs index 542e674c4..5796688b6 100644 --- a/synd-mchain/src/db.rs +++ b/synd-mchain/src/db.rs @@ -112,7 +112,8 @@ pub struct Block { /// timestamp for the hardfork where L1 block number changes from being derived from the seq chain /// to the set chain -pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1764565200; +/// 5 Jan 2026 +pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1767571200; impl Block { /// The delayed message accumulator diff --git a/synd-translator/bin/synd-translator/src/spawn.rs b/synd-translator/bin/synd-translator/src/spawn.rs index 2f3fa8ef1..e7114ee5c 100644 --- a/synd-translator/bin/synd-translator/src/spawn.rs +++ b/synd-translator/bin/synd-translator/src/spawn.rs @@ -8,13 +8,14 @@ use shared::{ service_start_utils::{start_http_server_with_aux_handlers, MetricsState}, tracing::SpanKind, }; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use synd_block_builder::appchains::arbitrum::arbitrum_adapter::ArbitrumAdapter; use synd_chain_ingestor::{ client::{IngestorProvider, IngestorProviderConfig, IngestorProviderImpl}, eth_client::EthClient, }; use synd_mchain::client::{MProvider, MchainProvider}; +use synd_slotter::slotter; use tracing::{error, instrument, log::info}; use url::Url; @@ -89,9 +90,7 @@ async fn start_slotter(config: &TranslatorConfig, metrics: &TranslatorMetrics) - settlement_config.start_block = state.settlement_block.number; } - let arbitrum_adapter = Arc::new(ArbitrumAdapter::new(&config.block_builder)); - - let adapter = arbitrum_adapter.clone(); + let arbitrum_adapter = ArbitrumAdapter::new(&config.block_builder); let seq_urls = sequencing_client .get_urls() @@ -112,8 +111,8 @@ async fn start_slotter(config: &TranslatorConfig, metrics: &TranslatorMetrics) - let sequencing = sequencing_client .get_blocks( sequencing_config.start_block, - adapter.sequencer_addresses(), - adapter, + arbitrum_adapter.sequencer_addresses(), + arbitrum_adapter.clone(), seq_client, None, ) @@ -139,7 +138,7 @@ async fn start_slotter(config: &TranslatorConfig, metrics: &TranslatorMetrics) - .get_blocks( settlement_config.start_block, arbitrum_adapter.settlement_addresses(), - arbitrum_adapter, + arbitrum_adapter.clone(), set_client, inbox_address, ) @@ -147,10 +146,11 @@ async fn start_slotter(config: &TranslatorConfig, metrics: &TranslatorMetrics) - let settlement_delay = config.settlement_delay; - Ok(synd_slotter::slotter::run( + Ok(slotter::run( settlement_delay.unwrap(), sequencing, settlement, + arbitrum_adapter, &mchain, &metrics.slotter, ) diff --git a/synd-translator/crates/common/src/types.rs b/synd-translator/crates/common/src/types.rs index b7cb1331d..db9c335d2 100644 --- a/synd-translator/crates/common/src/types.rs +++ b/synd-translator/crates/common/src/types.rs @@ -1,18 +1,12 @@ //! Types module for `synd-translator` -use alloy::primitives::{Bytes, FixedBytes}; -use shared::types::{BlockRef, GetBlockRef}; +use alloy::primitives::FixedBytes; +use shared::types::{BlockRef, GetBlockRef, PartialBlock}; use strum_macros::Display; use synd_mchain::db::DelayedMessage; #[allow(missing_docs)] -#[derive(Debug, Default, Clone)] -pub struct SequencingBlock { - pub block_ref: BlockRef, - pub parent_hash: FixedBytes<32>, - pub batch: Bytes, - pub tx_count: u64, -} +pub type SequencingBlock = PartialBlock; #[allow(missing_docs)] #[derive(Debug, Default, Clone)] @@ -39,12 +33,6 @@ impl From for &'static str { } } -impl GetBlockRef for SequencingBlock { - fn block_ref(&self) -> &BlockRef { - &self.block_ref - } -} - impl GetBlockRef for SettlementBlock { fn block_ref(&self) -> &BlockRef { &self.block_ref diff --git a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs index 7204f7a26..442f6fc59 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs @@ -9,12 +9,12 @@ use crate::{ arbitrum::batch::{ Batch, BatchMessage, L1IncomingMessage, L1IncomingMessageHeader, MAX_L2_MESSAGE_SIZE, }, - shared::{RollupAdapter, SequencingTransactionParser}, + shared::{sequencing_transaction_parser::get_event_transactions, RollupAdapter}, }, config::BlockBuilderConfig, }; use alloy::{ - primitives::{Address, Bytes, Log, U256}, + primitives::{Address, Bytes, Log, TxHash, U256}, sol_types::SolEvent as _, }; use common::types::{SequencingBlock, SettlementBlock}; @@ -22,9 +22,9 @@ use contract_bindings::synd::i_bridge::IBridge::MessageDelivered; use eyre::Result; use shared::types::{BlockBuilder, DelayedMsgsData, PartialBlock}; use std::collections::HashMap; -use synd_mchain::db::{DelayedMessage, L1_BLOCK_NUM_HARDFORK_TS}; +use synd_mchain::db::DelayedMessage; use thiserror::Error; -use tracing::{debug, error, info, trace}; +use tracing::{debug, error, trace}; // Limit the number of tx per a block so that they have enough gas to be included most of the time. // Each tx can use 320k gas on average given the default block gas limit of 32 million. @@ -95,8 +95,8 @@ impl std::fmt::Display for L1MessageType { #[derive(Debug, Clone)] /// Builder for constructing Arbitrum blocks from transactions pub struct ArbitrumAdapter { - /// Transaction parser for sequencing chain - pub transaction_parser: SequencingTransactionParser, + /// sequencing contract + pub sequencing_contract_address: Address, /// Settlement chain address pub bridge_address: Address, @@ -106,8 +106,67 @@ pub struct ArbitrumAdapter { } impl RollupAdapter for ArbitrumAdapter { - fn transaction_parser(&self) -> &SequencingTransactionParser { - &self.transaction_parser + fn get_event_transactions(&self, eth_log: &Log) -> Result> { + get_event_transactions(eth_log, &self.sequencing_contract_address) + .map_err(|e| eyre::eyre!("{}", e)) + } + + /// Builds a batch of transactions into an Arbitrum batch + /// note: this must mirror the logic in the enclave go code + /// for building batches. + #[allow(clippy::cognitive_complexity)] + fn build_batch_bytes( + &self, + txs: Vec, + l1_block_number: u64, + mchain_timestamp: u64, + ) -> Result { + debug!("Sequenced transactions: {:?}", txs); + + let mut messages = vec![]; + let mut block = vec![]; + // Start with the batch header byte - see l2_msg_to_bytes in batch.rs for more + // infomation. + let mut size = 1; + for tx in txs { + // When multiple txs are included in the block, then each tx is prefixed a uint64 + // value indicating with the size of the tx. + // See l2_msg_to_bytes in batch.rs for more infomation. + let tx_size = 8 + tx.len(); + size += tx_size; + if block.len() >= TX_PER_BLOCK || (!block.is_empty() && size > MAX_L2_MESSAGE_SIZE) { + messages.push(BatchMessage::L2(L1IncomingMessage { + header: L1IncomingMessageHeader { + block_number: l1_block_number, + timestamp: mchain_timestamp, + }, + l2_msg: block, + })); + block = vec![]; + // When multiple transactions are in the block, then the batch of transactions + // is prefixed with a batch header byte. + // See l2_msg_to_bytes in batch.rs for more infomation. + size = 1 + tx_size; + } + block.push(tx); + } + if !block.is_empty() { + messages.push(BatchMessage::L2(L1IncomingMessage { + header: L1IncomingMessageHeader { + block_number: l1_block_number, + timestamp: mchain_timestamp, + }, + l2_msg: block, + })); + } + + let batch = Batch(messages); + debug!("New Batch: {:?}", batch); + + // Encode the batch data + let encoded_batch = batch.encode()?; + + Ok(encoded_batch) } } @@ -119,47 +178,12 @@ impl ArbitrumAdapter { #[allow(clippy::unwrap_used)] //it's okay to unwrap here because we know the config is valid pub const fn new(config: &BlockBuilderConfig) -> Self { Self { - transaction_parser: SequencingTransactionParser::new( - config.sequencing_contract_address.unwrap(), - ), + sequencing_contract_address: config.sequencing_contract_address.unwrap(), bridge_address: config.arbitrum_bridge_address.unwrap(), inbox_address: config.arbitrum_inbox_address.unwrap(), } } - /// Builds a batch from a sequencing block - pub fn build_batch(&self, block: &PartialBlock) -> Result<(u64, Bytes)> { - let mb_transactions = self.parse_block_to_mbtxs(block); - - if mb_transactions.is_empty() { - return Ok((0, Default::default())); - } - - info!( - slot = %block.block_ref.number, - "Processing sequencer transactions: {:?}", - mb_transactions - .iter() - .map(|x|x.1).collect::>() - ); - - let l1_block_number = if block.block_ref.timestamp < L1_BLOCK_NUM_HARDFORK_TS { - block.block_ref.number - } else { - // TODO - 0 - }; - - Ok(( - mb_transactions.len() as u64, - self.build_batch_txn( - mb_transactions.into_iter().map(|x| x.0).collect(), - l1_block_number, - block.block_ref.timestamp, - )?, - )) - } - /// Processes settlement chain receipts into delayed messages pub fn process_delayed_messages( &self, @@ -209,7 +233,7 @@ impl ArbitrumAdapter { /// Sequencer log addresses used for building batches pub fn sequencer_addresses(&self) -> Vec
{ - vec![self.transaction_parser.sequencing_contract_address] + vec![self.sequencing_contract_address] } /// Settlement log addresses used for building delayed messages @@ -246,64 +270,6 @@ impl ArbitrumAdapter { }) } - /// Builds a batch of transactions into an Arbitrum batch - /// note: this must mirror the logic in the enclave go code - /// for building batches. - #[allow(clippy::cognitive_complexity)] - fn build_batch_txn( - &self, - txs: Vec, - l1_block_number: u64, - mchain_timestamp: u64, - ) -> Result { - debug!("Sequenced transactions: {:?}", txs); - - let mut messages = vec![]; - let mut block = vec![]; - // Start with the batch header byte - see l2_msg_to_bytes in batch.rs for more - // infomation. - let mut size = 1; - for tx in txs { - // When multiple txs are included in the block, then each tx is prefixed a uint64 - // value indicating with the size of the tx. - // See l2_msg_to_bytes in batch.rs for more infomation. - let tx_size = 8 + tx.len(); - size += tx_size; - if block.len() >= TX_PER_BLOCK || (!block.is_empty() && size > MAX_L2_MESSAGE_SIZE) { - messages.push(BatchMessage::L2(L1IncomingMessage { - header: L1IncomingMessageHeader { - block_number: l1_block_number, - timestamp: mchain_timestamp, - }, - l2_msg: block, - })); - block = vec![]; - // When multiple transactions are in the block, then the batch of transactions - // is prefixed with a batch header byte. - // See l2_msg_to_bytes in batch.rs for more infomation. - size = 1 + tx_size; - } - block.push(tx); - } - if !block.is_empty() { - messages.push(BatchMessage::L2(L1IncomingMessage { - header: L1IncomingMessageHeader { - block_number: l1_block_number, - timestamp: mchain_timestamp, - }, - l2_msg: block, - })); - } - - let batch = Batch(messages); - debug!("New Batch: {:?}", batch); - - // Encode the batch data - let encoded_batch = batch.encode()?; - - Ok(encoded_batch) - } - /// Should ignore delayed message. Ignores `Initialize` & `BatchPostingReport` message types. pub fn should_ignore_delayed_message(kind: &L1MessageType) -> bool { // Always ignore Initialize & BatchPostingReport message types. @@ -320,33 +286,27 @@ impl ArbitrumAdapter { impl BlockBuilder for ArbitrumAdapter { fn build_block( &self, - block: &PartialBlock, + block: PartialBlock, msgs_data: DelayedMsgsData, ) -> Result { assert!( msgs_data.is_empty(), "delayed messages found on sequencing block: {block:?}, {msgs_data:?}" ); - let (tx_count, batch) = self.build_batch(block)?; - Ok(SequencingBlock { - block_ref: block.block_ref.clone(), - parent_hash: block.parent_hash, - tx_count, - batch, - }) + Ok(block) } } impl BlockBuilder for ArbitrumAdapter { fn build_block( &self, - block: &PartialBlock, + block: PartialBlock, msgs_data: DelayedMsgsData, ) -> Result { Ok(SettlementBlock { block_ref: block.block_ref.clone(), parent_hash: block.parent_hash, - messages: self.process_delayed_messages(block, msgs_data)?, + messages: self.process_delayed_messages(&block, msgs_data)?, }) } } @@ -354,16 +314,8 @@ impl BlockBuilder for ArbitrumAdapter { #[cfg(test)] mod tests { use super::*; - use crate::appchains::shared::sequencing_transaction_parser::L2MessageKind; - use alloy::{ - eips::Encodable2718, - network::{EthereumWallet, TransactionBuilder as _}, - primitives::{hex, keccak256, FixedBytes}, - rpc::types::TransactionRequest, - signers::local::PrivateKeySigner, - }; + use alloy::primitives::{hex, keccak256, FixedBytes}; use assert_matches::assert_matches; - use contract_bindings::synd::syndicate_sequencing_chain::SyndicateSequencingChain::TransactionProcessed; use std::str::FromStr; fn test_config() -> BlockBuilderConfig { @@ -381,86 +333,11 @@ mod tests { } } - #[tokio::test] - async fn test_parse_tx() { - let sequencing_contract_address = test_config().sequencing_contract_address.unwrap(); - let tx = TransactionRequest::default() - .with_to(Address::ZERO) - .with_nonce(0) - .with_gas_limit(0) - .with_max_fee_per_gas(0) - .with_max_priority_fee_per_gas(0) - .build(&EthereumWallet::from(PrivateKeySigner::random())) - .await - .unwrap(); - let mut encoded_tx = tx.encoded_2718(); - encoded_tx.splice(0..0, vec![L2MessageKind::SignedTx as u8]); - let block = PartialBlock { - logs: vec![ - // empty tx - Log { - address: sequencing_contract_address, - data: TransactionProcessed { - sender: Default::default(), - data: Default::default(), - } - .encode_log_data(), - }, - // invalid txs - Log { - address: sequencing_contract_address, - data: TransactionProcessed { - sender: Default::default(), - data: vec![L2MessageKind::SignedTx as u8].into(), - } - .encode_log_data(), - }, - Log { - address: sequencing_contract_address, - data: TransactionProcessed { - sender: Default::default(), - data: vec![L2MessageKind::SignedTx as u8, 0].into(), - } - .encode_log_data(), - }, - // valid tx - Log { - address: sequencing_contract_address, - data: TransactionProcessed { - sender: Default::default(), - data: encoded_tx.clone().into(), - } - .encode_log_data(), - }, - ], - ..Default::default() - }; - // parse mbtxs - let txs = ArbitrumAdapter::new(&test_config()).parse_block_to_mbtxs(&block); - assert_eq!(txs, vec![(encoded_tx.into(), *tx.hash())]) - } - - #[test] - fn test_new_builder() { - let dummy_contract_addr = Address::from_str("0x1234000000000000000000000000000000000000") - .expect("Invalid address format"); - let config = BlockBuilderConfig { - sequencing_contract_address: Some(dummy_contract_addr), - arbitrum_bridge_address: Some(dummy_contract_addr), - arbitrum_inbox_address: Some(dummy_contract_addr), - ..Default::default() - }; - - let builder = ArbitrumAdapter::new(&config); - let parser = builder.transaction_parser(); - assert!(!std::ptr::eq(parser, std::ptr::null()), "Transaction parser should not be null"); - } - #[tokio::test] async fn test_build_batch_empty_txs() { let builder = ArbitrumAdapter::new(&test_config()); let txs = vec![]; - let batch = builder.build_batch_txn(txs, 0, 0).unwrap(); + let batch = builder.build_batch_bytes(txs, 0, 0).unwrap(); // For empty batch, should create BatchMessage::Delayed let expected_batch = Batch(vec![]); @@ -476,7 +353,7 @@ mod tests { hex!("1234").into(), // Sample transaction data hex!("5678").into(), ]; - let batch = builder.build_batch_txn(txs.clone(), 0, 0).unwrap(); + let batch = builder.build_batch_bytes(txs.clone(), 0, 0).unwrap(); // For non-empty batch, should create BatchMessage::L2 let expected_batch = Batch(vec![BatchMessage::L2(L1IncomingMessage { diff --git a/synd-translator/crates/synd-block-builder/src/appchains/shared/mod.rs b/synd-translator/crates/synd-block-builder/src/appchains/shared/mod.rs index f0ba2bcac..ad2c200d6 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/shared/mod.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/shared/mod.rs @@ -3,5 +3,3 @@ pub mod rollup_adapter; pub use rollup_adapter::RollupAdapter; pub mod sequencing_transaction_parser; - -pub use sequencing_transaction_parser::SequencingTransactionParser; diff --git a/synd-translator/crates/synd-block-builder/src/appchains/shared/rollup_adapter.rs b/synd-translator/crates/synd-block-builder/src/appchains/shared/rollup_adapter.rs index 17ead5d0b..58c2f3955 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/shared/rollup_adapter.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/shared/rollup_adapter.rs @@ -3,42 +3,24 @@ //! This module provides the core [`RollupAdapter`] trait that defines how //! different rollup implementations can construct and process their blocks. -use crate::appchains::shared::SequencingTransactionParser; -use alloy::primitives::{Bytes, TxHash}; +use alloy::primitives::{Bytes, Log, TxHash}; use async_trait::async_trait; -use shared::types::PartialBlock; use std::marker::{Send, Sync}; -use tracing::warn; /// Trait for rollup-specific block builders that construct batches from sequencer transactions /// and delayed messages from settlement ones. #[async_trait] pub trait RollupAdapter: Send + Sync { - /// Parses a sequencing chain block into a batch. - /// - /// Uses the associated transaction parser to extract transactions - /// from the logs within block receipts. - /// - /// # Arguments - /// * `input` - A block along with its associated receipts. - /// - /// # Returns - /// A vector of extracted transaction data containing the raw `Bytes` and tx hash - fn parse_block_to_mbtxs(&self, input: &PartialBlock) -> Vec<(Bytes, TxHash)> { - input - .logs - .iter() - .filter_map(|log| match self.transaction_parser().get_event_transactions(log) { - Ok(txs) => Some(txs), - Err(e) => { - warn!("Failed to get event transactions from log: {:?}, error: {:?}", log, e); - None - } - }) - .flatten() - .collect() - } + /// Decodes the event data into a vector of transactions + fn get_event_transactions(&self, eth_log: &Log) -> eyre::Result>; - /// Provides access to the transaction parser used by the block builder. - fn transaction_parser(&self) -> &SequencingTransactionParser; + /// constructs a batch of sequenced transaction to be added to mchain in a format that's + /// compatible with the rollup node + /// NOTE: this must mirror the logic of the TEE enclave + fn build_batch_bytes( + &self, + txs: Vec, + l1_block_number: u64, + mchain_timestamp: u64, + ) -> eyre::Result; // TODO txhash is not used and should be rm'd } diff --git a/synd-translator/crates/synd-block-builder/src/appchains/shared/sequencing_transaction_parser.rs b/synd-translator/crates/synd-block-builder/src/appchains/shared/sequencing_transaction_parser.rs index 44204757b..8e379e941 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/shared/sequencing_transaction_parser.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/shared/sequencing_transaction_parser.rs @@ -7,7 +7,7 @@ use crate::appchains::arbitrum::batch::MAX_L2_MESSAGE_SIZE; use alloy::{ consensus::{transaction::RlpEcdsaDecodableTx as _, TxEip1559, TxEip2930, TxEip7702, TxLegacy}, eips::eip2718::Eip2718Error, - primitives::{keccak256, Address, Bytes, Log, TxHash}, + primitives::{Address, Bytes, Log, TxHash}, sol_types::SolEvent, }; use contract_bindings::synd::syndicate_sequencing_chain::SyndicateSequencingChain::TransactionProcessed; @@ -52,13 +52,6 @@ pub enum SequencingParserError { DecompressionError(String), } -/// The parser for appchain transactions -#[derive(Debug, Clone)] -pub struct SequencingTransactionParser { - /// The address of the sequencing contract - pub sequencing_contract_address: Address, -} - /// See `arbos/parse_l2.go` for details. #[derive(Debug)] #[allow(missing_docs)] @@ -152,67 +145,61 @@ fn decompress_transactions(data: &[u8]) -> Result, Sequenci .map_err(|e| SequencingParserError::DecompressionError(e.to_string())) } -impl SequencingTransactionParser { - /// Creates a new `SequencingTransactionParser` - pub const fn new(sequencing_contract_address: Address) -> Self { - Self { sequencing_contract_address } - } - - /// Checks if a log is a `TransactionProcessed` event - pub fn is_log_transaction_processed(&self, eth_log: &Log) -> bool { - eth_log.address == self.sequencing_contract_address && - eth_log - .topics() - .first() - .is_some_and(|t| *t == keccak256(TransactionProcessed::SIGNATURE.as_bytes())) +/// Decodes the event data into a vector of typed transactions +fn decode_event_data(data: &Bytes) -> Result, SequencingParserError> { + if data.is_empty() { + return Err(SequencingParserError::NoDataProvided); } - /// Decodes the event data into a vector of typed transactions - pub fn decode_event_data(data: &Bytes) -> Result, SequencingParserError> { - if data.is_empty() { - return Err(SequencingParserError::NoDataProvided); - } - - match data[0].try_into().map_err(|_| SequencingParserError::UnexpectedDataType)? { - L2MessageKind::Batch => decompress_transactions(&data[1..]), - L2MessageKind::SignedTx => Ok(vec![( - data.clone(), - signed_tx_hash(&data[1..]) - .map_err(|e| SequencingParserError::InvalidTxData(e.to_string()))?, - )]), - // The sequencing contract ensures that unsigned transactions are valid - L2MessageKind::UnsignedUserTx | L2MessageKind::ContractTx => { - if data.len() > MAX_L2_MESSAGE_SIZE { - return Err(SequencingParserError::InvalidTxData( - "dropping tx greater than the max l2 message size".to_string(), - )); - } - // TODO(SEQ-1370): compute tx hash for unsigned txs - Ok(vec![(data.clone(), Default::default())]) + match data[0].try_into().map_err(|_| SequencingParserError::UnexpectedDataType)? { + L2MessageKind::Batch => decompress_transactions(&data[1..]), + L2MessageKind::SignedTx => Ok(vec![( + data.clone(), + signed_tx_hash(&data[1..]) + .map_err(|e| SequencingParserError::InvalidTxData(e.to_string()))?, + )]), + // The sequencing contract ensures that unsigned transactions are valid + L2MessageKind::UnsignedUserTx | L2MessageKind::ContractTx => { + if data.len() > MAX_L2_MESSAGE_SIZE { + return Err(SequencingParserError::InvalidTxData( + "dropping tx greater than the max l2 message size".to_string(), + )); } + // TODO(SEQ-1370): compute tx hash for unsigned txs + Ok(vec![(data.clone(), Default::default())]) } } +} - /// Decodes the event data into a vector of transactions - pub fn get_event_transactions( - &self, - eth_log: &Log, - ) -> Result, SequencingParserError> { - if !self.is_log_transaction_processed(eth_log) { - return Err(SequencingParserError::InvalidLogEvent); - } - let decoded_event = TransactionProcessed::decode_log_data_validate(ð_log.data) - .map_err(|_e| SequencingParserError::DynSolEventCreation)?; +/// Checks if a log is a `TransactionProcessed` event +fn is_log_transaction_processed(eth_log: &Log, sequencing_contract: &Address) -> bool { + eth_log.address == *sequencing_contract && + eth_log.topics().first().is_some_and(|t| *t == TransactionProcessed::SIGNATURE_HASH) +} - // Decode the transactions - Self::decode_event_data(&decoded_event.data) +/// Decodes the event data into a vector of transactions +pub fn get_event_transactions( + eth_log: &Log, + sequencing_contract: &Address, +) -> eyre::Result, SequencingParserError> { + if !is_log_transaction_processed(eth_log, sequencing_contract) { + return Err(SequencingParserError::InvalidLogEvent); } + let decoded_event = TransactionProcessed::decode_log_data_validate(ð_log.data) + .map_err(|_e| SequencingParserError::DynSolEventCreation)?; + + // Decode the transactions + decode_event_data(&decoded_event.data) } #[cfg(test)] mod tests { use super::*; - use alloy::{hex, primitives::B256, sol_types::SolValue}; + use alloy::{ + hex, + primitives::{keccak256, B256}, + sol_types::SolValue, + }; const DUMMY_TXN_VALUE: &[u8] = &[L2MessageKind::UnsignedUserTx as u8]; @@ -228,40 +215,27 @@ mod tests { Log::new_unchecked(contract_address, topics, Bytes::from(DUMMY_TXN_VALUE.abi_encode())) } - #[tokio::test] - async fn test_new_parser() { - let contract_address: Address = - "0x000000000000000000000000000000000000abcd".parse().unwrap(); - - let parser = SequencingTransactionParser::new(contract_address); - assert_eq!(parser.sequencing_contract_address, contract_address); - } - #[tokio::test] async fn test_is_log_transaction_processed() { let contract_address: Address = "0x000000000000000000000000000000000000abcd".parse().unwrap(); - let parser: SequencingTransactionParser = - SequencingTransactionParser::new(contract_address); - let log = generate_valid_test_log(contract_address); - assert!(parser.is_log_transaction_processed(&log)); + assert!(is_log_transaction_processed(&log, &contract_address)); let unrelated_contract_address: Address = "0x110000000000000000000000000000000000abcd".parse().unwrap(); let unrelated_log = generate_valid_test_log(unrelated_contract_address); - assert!(!parser.is_log_transaction_processed(&unrelated_log)); + assert!(!is_log_transaction_processed(&unrelated_log, &contract_address)); } #[tokio::test] async fn test_get_event_transactions_valid_log() { let contract_address: Address = "0x000000000000000000000000000000000000abcd".parse().unwrap(); - let parser = SequencingTransactionParser::new(contract_address); let log = generate_valid_test_log(contract_address); - let result = parser.get_event_transactions(&log); + let result = get_event_transactions(&log, &contract_address); assert!(result.is_ok()); let transactions = result.unwrap(); assert_eq!(transactions.len(), 1); @@ -272,13 +246,12 @@ mod tests { async fn test_get_event_transactions_invalid_log() { let contract_address: Address = "0x000000000000000000000000000000000000abcd".parse().unwrap(); - let parser = SequencingTransactionParser::new(contract_address); let unrelated_contract_address: Address = "0x110000000000000000000000000000000000abcd".parse().unwrap(); let log = generate_valid_test_log(unrelated_contract_address); - let result = parser.get_event_transactions(&log); + let result = get_event_transactions(&log, &contract_address); assert!(result.is_err()); assert_eq!(result.unwrap_err(), SequencingParserError::InvalidLogEvent); } diff --git a/synd-translator/crates/synd-slotter/Cargo.toml b/synd-translator/crates/synd-slotter/Cargo.toml index 2655ef0de..d1965b66a 100644 --- a/synd-translator/crates/synd-slotter/Cargo.toml +++ b/synd-translator/crates/synd-slotter/Cargo.toml @@ -13,6 +13,7 @@ alloy = { workspace = true } common = { workspace = true } prometheus-client = { workspace = true } shared = { workspace = true } +synd-block-builder = { workspace = true } synd-chain-ingestor = { workspace = true } synd-mchain = { workspace = true } thiserror = { workspace = true } @@ -20,9 +21,11 @@ tokio = { workspace = true, features = ["macros", "rt", "sync", "t tracing = { workspace = true } [dev-dependencies] -async-trait = { workspace = true } -ctor = { workspace = true } -eyre = { workspace = true } +alloy = { workspace = true, features = ["sol-types"] } +async-trait = { workspace = true } +contract-bindings = { workspace = true } +ctor = { workspace = true } +eyre = { workspace = true } [lints] workspace = true diff --git a/synd-translator/crates/synd-slotter/src/batch.rs b/synd-translator/crates/synd-slotter/src/batch.rs new file mode 100644 index 000000000..4881769bc --- /dev/null +++ b/synd-translator/crates/synd-slotter/src/batch.rs @@ -0,0 +1,143 @@ +//! contains batch parsing logic used by the slotter + +use crate::slotter::SlotterError; +use alloy::primitives::{Bytes, TxHash}; +use shared::types::{BlockRef, PartialBlock}; +use synd_block_builder::appchains::shared::RollupAdapter; +use tracing::{info, warn}; + +/// builds a batch from a sequencing block, rollup adapter and l1 block ref +pub fn build_batch( + seq_block: &PartialBlock, + rollup_adapter: &impl RollupAdapter, + l1_block: &BlockRef, +) -> Result<(u64, Bytes), SlotterError> { + let mb_transactions = parse_block_to_txs(seq_block, rollup_adapter); + + if mb_transactions.is_empty() { + return Ok((0, Default::default())); + } + + info!( + slot = seq_block.block_ref.number, + "Processing sequencer transactions: {:?}", + mb_transactions.iter().map(|x| x.1).collect::>() + ); + Ok(( + mb_transactions.len() as u64, + rollup_adapter + .build_batch_bytes( + mb_transactions.into_iter().map(|x| x.0).collect(), + l1_block.number, + l1_block.timestamp, + ) + .map_err(|e| SlotterError::BuildBatchError(e.to_string()))?, + )) +} + +/// Parses a sequencing chain block into a batch. +/// +/// extracts transactions from the block logs +fn parse_block_to_txs( + seq_block: &PartialBlock, + rollup_adapter: &impl RollupAdapter, +) -> Vec<(Bytes, TxHash)> { + // TODO txHash return value is completely unused, should be removed + seq_block + .logs + .iter() + .filter_map(|log| match rollup_adapter.get_event_transactions(log) { + Ok(txs) => Some(txs), + Err(e) => { + warn!("Failed to get event transactions from log: {:?}, error: {:?}", log, e); + None + } + }) + .flatten() + .collect() +} + +#[cfg(test)] +mod tests { + + use super::*; + use alloy::{ + eips::Encodable2718, + network::{EthereumWallet, TransactionBuilder as _}, + primitives::{address, Address, Log}, + rpc::types::TransactionRequest, + signers::local::PrivateKeySigner, + sol_types::SolEvent, + }; + use contract_bindings::synd::syndicate_sequencing_chain::SyndicateSequencingChain::TransactionProcessed; + use synd_block_builder::appchains::{ + arbitrum::arbitrum_adapter::ArbitrumAdapter, + shared::sequencing_transaction_parser::L2MessageKind, + }; + + #[tokio::test] + async fn test_parse_tx() { + let sequencing_contract_address = address!("0x0000000000000000000000000000000000000123"); + let tx = TransactionRequest::default() + .with_to(Address::ZERO) + .with_nonce(0) + .with_gas_limit(0) + .with_max_fee_per_gas(0) + .with_max_priority_fee_per_gas(0) + .build(&EthereumWallet::from(PrivateKeySigner::random())) + .await + .unwrap(); + let mut encoded_tx = tx.encoded_2718(); + encoded_tx.splice(0..0, vec![L2MessageKind::SignedTx as u8]); + let block = PartialBlock { + logs: vec![ + // empty tx + Log { + address: sequencing_contract_address, + data: TransactionProcessed { + sender: Default::default(), + data: Default::default(), + } + .encode_log_data(), + }, + // invalid txs + Log { + address: sequencing_contract_address, + data: TransactionProcessed { + sender: Default::default(), + data: vec![L2MessageKind::SignedTx as u8].into(), + } + .encode_log_data(), + }, + Log { + address: sequencing_contract_address, + data: TransactionProcessed { + sender: Default::default(), + data: vec![L2MessageKind::SignedTx as u8, 0].into(), + } + .encode_log_data(), + }, + // valid tx + Log { + address: sequencing_contract_address, + data: TransactionProcessed { + sender: Default::default(), + data: encoded_tx.clone().into(), + } + .encode_log_data(), + }, + ], + ..Default::default() + }; + // parse mbtxs + let rollup_adapter = + ArbitrumAdapter::new(&synd_block_builder::config::BlockBuilderConfig { + mchain_ws_url: String::new(), + sequencing_contract_address: Some(sequencing_contract_address), + arbitrum_bridge_address: Some(Address::ZERO), + arbitrum_inbox_address: Some(Address::ZERO), + }); + let txs = parse_block_to_txs(&block, &rollup_adapter); + assert_eq!(txs, vec![(encoded_tx.into(), *tx.hash())]) + } +} diff --git a/synd-translator/crates/synd-slotter/src/lib.rs b/synd-translator/crates/synd-slotter/src/lib.rs index c38478c47..6e6e42fde 100644 --- a/synd-translator/crates/synd-slotter/src/lib.rs +++ b/synd-translator/crates/synd-slotter/src/lib.rs @@ -1,4 +1,5 @@ //! `Synd-slotter` crate for `synd-translator` +pub mod batch; pub mod metrics; pub mod slotter; diff --git a/synd-translator/crates/synd-slotter/src/slotter.rs b/synd-translator/crates/synd-slotter/src/slotter.rs index 65d582dd0..032adaa8a 100644 --- a/synd-translator/crates/synd-slotter/src/slotter.rs +++ b/synd-translator/crates/synd-slotter/src/slotter.rs @@ -1,16 +1,17 @@ //! Slotter module for `synd-translator` -use crate::metrics::SlotterMetrics; +use crate::{batch::build_batch, metrics::SlotterMetrics}; use alloy::primitives::FixedBytes; use common::types::{Chain, SequencingBlock, SettlementBlock}; use shared::tracing::SpanKind; +use synd_block_builder::appchains::shared::RollupAdapter; use synd_chain_ingestor::client::BlockStreamT; use synd_mchain::{ client::MchainProvider, - db::{ArbitrumBatch, MBlock, Slot}, + db::{ArbitrumBatch, MBlock, Slot, L1_BLOCK_NUM_HARDFORK_TS}, }; use thiserror::Error; -use tracing::{error, info, instrument, trace}; +use tracing::{info, instrument, trace}; /// Ingests blocks from the sequencing and settlement chains, slots them into slots, and sends the /// slots to the slot processor to generate `synd-mchain` blocks. @@ -25,7 +26,8 @@ pub async fn run( settlement_delay: u64, mut sequencing: impl BlockStreamT + Send, mut settlement: impl BlockStreamT + Send, - provider: &impl MchainProvider, + rollup_adapter: impl RollupAdapter, + mchain: &impl MchainProvider, metrics: &SlotterMetrics, ) -> Result<(), SlotterError> { info!("Starting Slotter"); @@ -45,9 +47,10 @@ pub async fn run( metrics.record_last_processed_block(seq_block.block_ref.number, Chain::Sequencing); metrics.update_chain_timestamp_lag(seq_block.block_ref.timestamp, Chain::Sequencing); + let timestamp = seq_block.block_ref.timestamp; let mut mblock = MBlock { - timestamp: seq_block.block_ref.timestamp, + timestamp, slot: Slot { seq_block_number: seq_block.block_ref.number, seq_block_hash: seq_block.block_ref.hash, @@ -57,17 +60,14 @@ pub async fn run( payload: None, }; - let mut messages = vec![]; + let mut delayed_msgs = vec![]; let mut blocks_per_slot: u64 = 1; - let slot_end_ts = if seq_block.block_ref.timestamp >= settlement_delay { - seq_block.block_ref.timestamp - settlement_delay + 1 - } else { - Default::default() - }; - while set_block.block_ref.timestamp < slot_end_ts { + let slot_end_ts = seq_block.block_ref.timestamp.saturating_sub(settlement_delay); + + while set_block.block_ref.timestamp <= slot_end_ts { blocks_per_slot += 1; - messages.append(&mut set_block.messages); + delayed_msgs.append(&mut set_block.messages); set_block = settlement .recv(slot_end_ts) .await @@ -76,15 +76,22 @@ pub async fn run( metrics.update_chain_timestamp_lag(set_block.block_ref.timestamp, Chain::Settlement); } - if seq_block.tx_count > 0 || !messages.is_empty() { - mblock.payload = Some(ArbitrumBatch::new(seq_block.batch, messages)); + let l1_block = if timestamp < L1_BLOCK_NUM_HARDFORK_TS { + &seq_block.block_ref + } else { + &set_block.block_ref + }; + let (tx_count, sequenced_batch) = build_batch(&seq_block, &rollup_adapter, l1_block)?; + + if tx_count > 0 || !delayed_msgs.is_empty() { + mblock.payload = Some(ArbitrumBatch::new(sequenced_batch, delayed_msgs)); } mblock.slot.set_block_hash = set_block.block_ref.hash; mblock.slot.set_block_number = set_block.block_ref.number; trace!("Processing slot {:?}", mblock.slot); let time = std::time::Instant::now(); - provider + mchain .add_batch(&mblock) .await .map_err(|e| SlotterError::SlotProcessorError(e.to_string()))?; @@ -92,7 +99,7 @@ pub async fn run( info!( "Sent slot {} ({} seq, {} set) with timestamp {} in {:?}", mblock.slot.seq_block_number, - seq_block.tx_count, + tx_count, payload.delayed_messages.len(), mblock.timestamp, time.elapsed() @@ -114,18 +121,34 @@ pub enum SlotterError { /// restarted and attempt to reconcile the mchain state with the real world #[error("{0} chain ingestor error: {1}")] IngestorError(Chain, String), + + /// Error happened when building a batch from a list of txs - SHOULD NOT HAPPEN + #[error("error when building a batch: {0}")] + BuildBatchError(String), } #[cfg(test)] mod tests { use super::*; use crate::{metrics::SlotterMetrics, slotter::run}; - use alloy::primitives::{Address, Bytes, FixedBytes, U256}; + use alloy::{ + eips::Encodable2718, + network::{EthereumWallet, TransactionBuilder}, + primitives::{address, Address, Bytes, FixedBytes, Log, TxHash, U256}, + rpc::types::TransactionRequest, + signers::local::PrivateKeySigner, + sol_types::SolEvent, + }; use async_trait::async_trait; use common::types::{SequencingBlock, SettlementBlock}; + use contract_bindings::synd::syndicate_sequencing_chain::SyndicateSequencingChain::TransactionProcessed; use prometheus_client::registry::Registry; use shared::types::BlockRef; use std::sync::{Arc, Mutex}; + use synd_block_builder::appchains::{ + arbitrum::arbitrum_adapter::ArbitrumAdapter, + shared::sequencing_transaction_parser::L2MessageKind, + }; use synd_chain_ingestor::client::BlockStreamT; use synd_mchain::{ client::{ClientError, DeserializeOwned, MchainProvider, ToRpcParams}, @@ -191,17 +214,31 @@ mod tests { } } - fn create_seq_block( - number: u64, - timestamp: u64, - tx_count: u64, - batch_data: &[u8], - ) -> SequencingBlock { + const SEQUENCING_CONTRACT_ADDRESS: Address = + address!("0x0000000000000000000000000000000000000123"); + + fn create_rollup_adapter() -> ArbitrumAdapter { + ArbitrumAdapter::new(&synd_block_builder::config::BlockBuilderConfig { + mchain_ws_url: String::new(), + sequencing_contract_address: Some(SEQUENCING_CONTRACT_ADDRESS), + arbitrum_bridge_address: Some(Address::ZERO), + arbitrum_inbox_address: Some(Address::ZERO), + }) + } + + fn create_seq_block(number: u64, timestamp: u64, txs: Vec) -> SequencingBlock { SequencingBlock { block_ref: BlockRef { number, timestamp, hash: FixedBytes::from([number as u8; 32]) }, parent_hash: FixedBytes::from([(number - 1) as u8; 32]), - batch: Bytes::from(batch_data.to_vec()), - tx_count, + logs: txs + .iter() + .map(|tx| Log { + address: SEQUENCING_CONTRACT_ADDRESS, + data: TransactionProcessed { sender: Default::default(), data: tx.clone() } + .encode_log_data(), + }) + .collect(), + log_tx_hashes: vec![], } } @@ -228,6 +265,21 @@ mod tests { } } + async fn dummy_tx() -> Bytes { + let tx = TransactionRequest::default() + .with_to(Address::ZERO) + .with_nonce(0) + .with_gas_limit(0) + .with_max_fee_per_gas(0) + .with_max_priority_fee_per_gas(0) + .build(&EthereumWallet::from(PrivateKeySigner::random())) + .await + .unwrap(); + let mut encoded_tx = tx.encoded_2718(); + encoded_tx.splice(0..0, vec![L2MessageKind::SignedTx as u8]); + encoded_tx.into() + } + #[async_trait] impl BlockStreamT for TestBlockStream { async fn recv(&mut self, _timestamp: u64) -> eyre::Result { @@ -249,7 +301,7 @@ mod tests { let metrics = SlotterMetrics::new(&mut Registry::default()); // Create one sequencing block at timestamp 100 - let seq_blocks = vec![create_seq_block(1, 100, 5, b"seq_batch_1")]; + let seq_blocks = vec![create_seq_block(1, 100, vec![dummy_tx().await])]; // Create settlement blocks: // - Block at timestamp 89 (will be included in slot) @@ -265,8 +317,15 @@ mod tests { let mchain_provider_clone = mchain_provider.clone(); // Run slotter in a separate task let handle = tokio::spawn(async move { - let _ = run(settlement_delay, sequencing, settlement, &mchain_provider_clone, &metrics) - .await; + let _ = run( + settlement_delay, + sequencing, + settlement, + create_rollup_adapter(), + &mchain_provider_clone, + &metrics, + ) + .await; }); // Wait for the expected number of blocks to be processed @@ -293,7 +352,7 @@ mod tests { let metrics = SlotterMetrics::new(&mut Registry::default()); // Create one sequencing block at timestamp 200 - let seq_blocks = vec![create_seq_block(1, 200, 2, b"seq_batch")]; + let seq_blocks = vec![create_seq_block(1, 200, vec![dummy_tx().await])]; // Create multiple settlement blocks that should fit in the slot // slot_end_ts = 200 - 100 + 1 = 101 @@ -310,8 +369,15 @@ mod tests { let mchain_provider_clone = mchain_provider.clone(); let handle = tokio::spawn(async move { - let _ = run(settlement_delay, sequencing, settlement, &mchain_provider_clone, &metrics) - .await; + let _ = run( + settlement_delay, + sequencing, + settlement, + create_rollup_adapter(), + &mchain_provider_clone, + &metrics, + ) + .await; }); mchain_provider.wait_for_blocks().await; @@ -334,7 +400,7 @@ mod tests { let metrics = SlotterMetrics::new(&mut Registry::default()); // Sequencing block with no transactions - let seq_blocks = vec![create_seq_block(1, 100, 0, b"")]; + let seq_blocks = vec![create_seq_block(1, 100, vec![])]; // Settlement blocks with no messages let set_blocks = vec![create_set_block(1, 89, vec![]), create_set_block(2, 91, vec![])]; @@ -344,8 +410,15 @@ mod tests { let mchain_provider_clone = mchain_provider.clone(); let handle = tokio::spawn(async move { - let _ = run(settlement_delay, sequencing, settlement, &mchain_provider_clone, &metrics) - .await; + let _ = run( + settlement_delay, + sequencing, + settlement, + create_rollup_adapter(), + &mchain_provider_clone, + &metrics, + ) + .await; }); mchain_provider.wait_for_blocks().await; @@ -367,7 +440,7 @@ mod tests { let metrics = SlotterMetrics::new(&mut Registry::default()); // Sequencing block with no transactions - let seq_blocks = vec![create_seq_block(1, 100, 0, b"")]; + let seq_blocks = vec![create_seq_block(1, 100, vec![])]; // Settlement blocks with delayed messages let message1 = DelayedMessage { @@ -394,8 +467,15 @@ mod tests { let mchain_provider_clone = mchain_provider.clone(); let handle = tokio::spawn(async move { - let _ = run(settlement_delay, sequencing, settlement, &mchain_provider_clone, &metrics) - .await; + let _ = run( + settlement_delay, + sequencing, + settlement, + create_rollup_adapter(), + &mchain_provider_clone, + &metrics, + ) + .await; }); mchain_provider.wait_for_blocks().await; From 9e0a8a643c6f92341734b2eb9e5b3926640ad8c9 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Mon, 1 Dec 2025 11:50:53 +0000 Subject: [PATCH 4/8] WIP --- Cargo.lock | 1 + synd-mchain/src/db.rs | 3 +- synd-mchain/src/server.rs | 3 +- synd-migration-cli/src/migration.rs | 11 +- .../appchains/arbitrum/arbitrum_adapter.rs | 25 ++--- .../crates/synd-slotter/Cargo.toml | 1 + .../crates/synd-slotter/src/batch.rs | 19 ++-- .../crates/synd-slotter/src/slotter.rs | 105 +++++++++++++++++- .../synd-enclave/enclave/accumulator.go | 37 +++--- .../synd-enclave/enclave/server.go | 6 +- .../synd-enclave/enclave/verify.go | 35 ++++-- .../synd-enclave/teetypes/types.go | 9 +- synd-withdrawals/synd-proposer/go.mod | 2 + synd-withdrawals/synd-proposer/go.sum | 9 ++ synd-withdrawals/synd-proposer/pkg/helpers.go | 2 + .../tests/e2e/e2e_tests_withdrawals.rs | 7 ++ 16 files changed, 206 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34f891999..15f40fd61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7797,6 +7797,7 @@ version = "0.1.0" dependencies = [ "alloy", "async-trait", + "brotli", "common", "contract-bindings", "ctor", diff --git a/synd-mchain/src/db.rs b/synd-mchain/src/db.rs index 5796688b6..ace4c3c6c 100644 --- a/synd-mchain/src/db.rs +++ b/synd-mchain/src/db.rs @@ -9,7 +9,7 @@ use jsonrpsee::types::{error::INTERNAL_ERROR_CODE, ErrorObjectOwned}; use rocksdb::{DBWithThreadMode, ThreadMode}; use serde::{Deserialize, Serialize}; use std::fmt; -use tracing::{debug, trace}; +use tracing::{debug, info, trace}; /// VERSION must be bumped whenever a breaking change is made const VERSION: u64 = 4; @@ -478,6 +478,7 @@ pub trait ArbitrumDB { }, true, )?; + info!("migration applied: {params:?}"); Ok(()) } diff --git a/synd-mchain/src/server.rs b/synd-mchain/src/server.rs index 924a4bb95..580f5ac0f 100644 --- a/synd-mchain/src/server.rs +++ b/synd-mchain/src/server.rs @@ -25,7 +25,7 @@ use jsonrpsee::RpcModule; #[cfg(not(test))] use std::time::SystemTime; use std::{collections::VecDeque, sync::Mutex, time::UNIX_EPOCH}; -use tracing::error; +use tracing::{error, info}; // 000b00800203 corresponds to a batch containing a single delayed message const EMPTY_BATCH: Bytes = Bytes::from_static(&alloy::hex!("000b00800203")); @@ -60,6 +60,7 @@ pub fn start_mchain( let batch = ArbitrumBatch::new(EMPTY_BATCH, vec![init_msg]); db.add_batch(MBlock { payload: Some(batch), ..Default::default() }).unwrap(); if let Some(migration_params) = migration_params { + info!("applying migration: {migration_params:?}"); db.appchain_migration(migration_params).unwrap(); finalized_batch_count = db.get_state().batch_count; } diff --git a/synd-migration-cli/src/migration.rs b/synd-migration-cli/src/migration.rs index 5ae2b3c7f..d58f0d57f 100644 --- a/synd-migration-cli/src/migration.rs +++ b/synd-migration-cli/src/migration.rs @@ -3,7 +3,7 @@ use alloy::{ primitives::B256, rlp::{Decodable, RlpDecodable}, }; -use eyre::{eyre, Context, Result}; +use eyre::{eyre, Result}; use rocksdb::{Options, DB}; use serde::{Deserialize, Serialize}; use std::path::Path; @@ -186,12 +186,15 @@ pub async fn get_migration_data(nitro_db_path: &Path) -> Result<(RollupState, Ve // Open the database with read-write access if we're modifying, read-only otherwise let mut opts = Options::default(); opts.create_if_missing(false); - let db = - DB::open_for_read_only(&opts, &chaindata_path, false).context("Failed to open database")?; + let db = DB::open_for_read_only(&opts, &chaindata_path, false) + .inspect_err(|e| eprintln!("couldn't open l1chaindata DB: {e}")) + .unwrap(); // Also open the arbitrumdata database which contains Arbitrum-specific state let arb_db_path = nitro_db_path.join("arbitrumdata"); - let arb_db = DB::open_for_read_only(&opts, &arb_db_path, false).unwrap(); + let arb_db = DB::open_for_read_only(&opts, &arb_db_path, false) + .inspect_err(|e| eprintln!("couldn't open l1chaindata DB: {e}")) + .unwrap(); // Get the rollup state let rollup_state = get_rollup_state(&db, &arb_db)?; diff --git a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs index 442f6fc59..313c5a2c7 100644 --- a/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs +++ b/synd-translator/crates/synd-block-builder/src/appchains/arbitrum/arbitrum_adapter.rs @@ -30,7 +30,7 @@ use tracing::{debug, error, trace}; // Each tx can use 320k gas on average given the default block gas limit of 32 million. // Eventually once the translator uses nitro to simulate tx execution, transactions can be slotted // into blocks based on gas usage instead. -const TX_PER_BLOCK: usize = 100; +const MAX_TXS_PER_BLOCK: usize = 100; #[allow(missing_docs)] // self-documenting #[derive(Debug, Error)] @@ -114,6 +114,7 @@ impl RollupAdapter for ArbitrumAdapter { /// Builds a batch of transactions into an Arbitrum batch /// note: this must mirror the logic in the enclave go code /// for building batches. + /// NOTE: this must mirror the logic of the TEE enclave #[allow(clippy::cognitive_complexity)] fn build_batch_bytes( &self, @@ -124,7 +125,7 @@ impl RollupAdapter for ArbitrumAdapter { debug!("Sequenced transactions: {:?}", txs); let mut messages = vec![]; - let mut block = vec![]; + let mut batch_txs = vec![]; // Start with the batch header byte - see l2_msg_to_bytes in batch.rs for more // infomation. let mut size = 1; @@ -134,39 +135,37 @@ impl RollupAdapter for ArbitrumAdapter { // See l2_msg_to_bytes in batch.rs for more infomation. let tx_size = 8 + tx.len(); size += tx_size; - if block.len() >= TX_PER_BLOCK || (!block.is_empty() && size > MAX_L2_MESSAGE_SIZE) { + if batch_txs.len() >= MAX_TXS_PER_BLOCK || + (!batch_txs.is_empty() && size > MAX_L2_MESSAGE_SIZE) + { messages.push(BatchMessage::L2(L1IncomingMessage { header: L1IncomingMessageHeader { block_number: l1_block_number, timestamp: mchain_timestamp, }, - l2_msg: block, + l2_msg: batch_txs, })); - block = vec![]; + batch_txs = vec![]; // When multiple transactions are in the block, then the batch of transactions // is prefixed with a batch header byte. // See l2_msg_to_bytes in batch.rs for more infomation. size = 1 + tx_size; } - block.push(tx); + batch_txs.push(tx); } - if !block.is_empty() { + if !batch_txs.is_empty() { messages.push(BatchMessage::L2(L1IncomingMessage { header: L1IncomingMessageHeader { block_number: l1_block_number, timestamp: mchain_timestamp, }, - l2_msg: block, + l2_msg: batch_txs, })); } let batch = Batch(messages); debug!("New Batch: {:?}", batch); - - // Encode the batch data - let encoded_batch = batch.encode()?; - - Ok(encoded_batch) + batch.encode() } } diff --git a/synd-translator/crates/synd-slotter/Cargo.toml b/synd-translator/crates/synd-slotter/Cargo.toml index d1965b66a..3245098ca 100644 --- a/synd-translator/crates/synd-slotter/Cargo.toml +++ b/synd-translator/crates/synd-slotter/Cargo.toml @@ -23,6 +23,7 @@ tracing = { workspace = true } [dev-dependencies] alloy = { workspace = true, features = ["sol-types"] } async-trait = { workspace = true } +brotli = { workspace = true } contract-bindings = { workspace = true } ctor = { workspace = true } eyre = { workspace = true } diff --git a/synd-translator/crates/synd-slotter/src/batch.rs b/synd-translator/crates/synd-slotter/src/batch.rs index 4881769bc..94860150e 100644 --- a/synd-translator/crates/synd-slotter/src/batch.rs +++ b/synd-translator/crates/synd-slotter/src/batch.rs @@ -2,7 +2,7 @@ use crate::slotter::SlotterError; use alloy::primitives::{Bytes, TxHash}; -use shared::types::{BlockRef, PartialBlock}; +use shared::types::PartialBlock; use synd_block_builder::appchains::shared::RollupAdapter; use tracing::{info, warn}; @@ -10,27 +10,24 @@ use tracing::{info, warn}; pub fn build_batch( seq_block: &PartialBlock, rollup_adapter: &impl RollupAdapter, - l1_block: &BlockRef, + l1_block_number: u64, + timestamp: u64, ) -> Result<(u64, Bytes), SlotterError> { - let mb_transactions = parse_block_to_txs(seq_block, rollup_adapter); + let txs = parse_block_to_txs(seq_block, rollup_adapter); - if mb_transactions.is_empty() { + if txs.is_empty() { return Ok((0, Default::default())); } info!( slot = seq_block.block_ref.number, "Processing sequencer transactions: {:?}", - mb_transactions.iter().map(|x| x.1).collect::>() + txs.iter().map(|x| x.1).collect::>() ); Ok(( - mb_transactions.len() as u64, + txs.len() as u64, rollup_adapter - .build_batch_bytes( - mb_transactions.into_iter().map(|x| x.0).collect(), - l1_block.number, - l1_block.timestamp, - ) + .build_batch_bytes(txs.into_iter().map(|x| x.0).collect(), l1_block_number, timestamp) .map_err(|e| SlotterError::BuildBatchError(e.to_string()))?, )) } diff --git a/synd-translator/crates/synd-slotter/src/slotter.rs b/synd-translator/crates/synd-slotter/src/slotter.rs index 032adaa8a..2dfa21872 100644 --- a/synd-translator/crates/synd-slotter/src/slotter.rs +++ b/synd-translator/crates/synd-slotter/src/slotter.rs @@ -76,12 +76,16 @@ pub async fn run( metrics.update_chain_timestamp_lag(set_block.block_ref.timestamp, Chain::Settlement); } - let l1_block = if timestamp < L1_BLOCK_NUM_HARDFORK_TS { - &seq_block.block_ref + let l1_block_number = if timestamp < L1_BLOCK_NUM_HARDFORK_TS { + seq_block.block_ref.number + } else if delayed_msgs.is_empty() { + 0 } else { - &set_block.block_ref + set_block.block_ref.number }; - let (tx_count, sequenced_batch) = build_batch(&seq_block, &rollup_adapter, l1_block)?; + + let (tx_count, sequenced_batch) = + build_batch(&seq_block, &rollup_adapter, l1_block_number, timestamp)?; if tx_count > 0 || !delayed_msgs.is_empty() { mblock.payload = Some(ArbitrumBatch::new(sequenced_batch, delayed_msgs)); @@ -134,7 +138,7 @@ mod tests { use alloy::{ eips::Encodable2718, network::{EthereumWallet, TransactionBuilder}, - primitives::{address, Address, Bytes, FixedBytes, Log, TxHash, U256}, + primitives::{address, Address, Bytes, FixedBytes, Log, U256}, rpc::types::TransactionRequest, signers::local::PrivateKeySigner, sol_types::SolEvent, @@ -490,4 +494,95 @@ mod tests { Ok(()) } + + /// Helper to extract L1 block number and timestamp from batch headers + fn extract_batch_headers(batch_data: &Bytes) -> eyre::Result<(u64, u64)> { + use alloy::rlp::{Decodable, Header as RlpHeader}; + + let mut decompressed = Vec::new(); + brotli::BrotliDecompress(&mut &batch_data[1..], &mut decompressed)?; + + let mut buf = &decompressed[..]; + let mut header_block_number = 0; + let mut header_timestamp = 0; + + while !buf.is_empty() { + let header = RlpHeader::decode(&mut buf)?; + let segment_data = &buf[..header.payload_length]; + buf = &buf[header.payload_length..]; + + if !segment_data.is_empty() { + match segment_data[0] { + 4 => { + // BatchSegmentKind::AdvanceL1BlockNumber + let mut segment_buf = &segment_data[1..]; + header_block_number += u64::decode(&mut segment_buf)?; + } + 3 => { + // BatchSegmentKind::AdvanceTimestamp + let mut segment_buf = &segment_data[1..]; + header_timestamp += u64::decode(&mut segment_buf)?; + } + _ => {} + } + } + } + Ok((header_block_number, header_timestamp)) + } + + #[tokio::test] + async fn test_hardfork_timestamp_changes_batch_header_source() -> eyre::Result<()> { + let settlement_delay = 10; + let dummy = dummy_tx().await; + + // Test before and after hardfork + for (test_name, timestamp, expected_is_settlement) in [ + ("before_hardfork", L1_BLOCK_NUM_HARDFORK_TS - 1, false), + ("after_hardfork", L1_BLOCK_NUM_HARDFORK_TS, true), + ] { + let mchain_provider = MockMchainProvider::new(1); + let metrics = SlotterMetrics::new(&mut Registry::default()); + + let seq_block_num = 1000; + let set_block_num = 2000; + let set_timestamp = timestamp - settlement_delay + 1; + + let seq_blocks = vec![create_seq_block(seq_block_num, timestamp, vec![dummy.clone()])]; + let set_blocks = vec![ + create_set_block(set_block_num, timestamp - settlement_delay, vec![]), + create_set_block(set_block_num + 1, set_timestamp, vec![]), + ]; + + let mchain_clone = mchain_provider.clone(); + let handle = tokio::spawn(async move { + let _ = run( + settlement_delay, + TestBlockStream::new(seq_blocks), + TestBlockStream::new(set_blocks), + create_rollup_adapter(), + &mchain_clone, + &metrics, + ) + .await; + }); + + mchain_provider.wait_for_blocks().await; + drop(handle); + + let blocks = mchain_provider.get_blocks(); + let (block_num, ts) = + extract_batch_headers(&blocks[0].payload.as_ref().unwrap().batch_data)?; + + let (expected_block, expected_ts) = if expected_is_settlement { + (set_block_num + 1, set_timestamp) + } else { + (seq_block_num, timestamp) + }; + + assert_eq!(block_num, expected_block, "{}: wrong block number", test_name); + assert_eq!(ts, expected_ts, "{}: wrong timestamp", test_name); + } + + Ok(()) + } } diff --git a/synd-withdrawals/synd-enclave/enclave/accumulator.go b/synd-withdrawals/synd-enclave/enclave/accumulator.go index 449d36b11..fdde2bbf2 100644 --- a/synd-withdrawals/synd-enclave/enclave/accumulator.go +++ b/synd-withdrawals/synd-enclave/enclave/accumulator.go @@ -98,13 +98,12 @@ func buildL2MessageSegment(txs [][]byte) ([]byte, error) { } const TX_PER_BLOCK = 100 -const HARDFORK_TS = 1764565200 -func buildBatch(txs [][]byte, ts uint64, blockNum uint64) ([]byte, error) { +func buildBatch(txs [][]byte, l1BlockNum uint64, l1BlockTimestamp uint64) ([]byte, error) { var data []byte - if ts != 0 { - segment, err := rlp.EncodeToBytes(ts) + if l1BlockTimestamp != 0 { + segment, err := rlp.EncodeToBytes(l1BlockTimestamp) if err != nil { return nil, err } @@ -115,8 +114,10 @@ func buildBatch(txs [][]byte, ts uint64, blockNum uint64) ([]byte, error) { data = append(data, segment...) } - if blockNum != 0 && ts < HARDFORK_TS { - segment, err := rlp.EncodeToBytes(blockNum) + // TODO is this correct? why not apply this segment after the hardfork? + // if l1BlockNum != 0 && l1BlockTimestamp < L1_BLOCK_NUM_HARDFORK_TS { + if l1BlockNum != 0 { + segment, err := rlp.EncodeToBytes(l1BlockNum) if err != nil { return nil, err } @@ -127,24 +128,24 @@ func buildBatch(txs [][]byte, ts uint64, blockNum uint64) ([]byte, error) { data = append(data, segment...) } - var block [][]byte + var batchTxs [][]byte size := 1 for _, tx := range txs { txSize := len(tx) + 8 size += txSize - if len(block) >= TX_PER_BLOCK || (len(block) > 0 && size > arbostypes.MaxL2MessageSize) { - segment, err := buildL2MessageSegment(block) + if len(batchTxs) >= TX_PER_BLOCK || (len(batchTxs) > 0 && size > arbostypes.MaxL2MessageSize) { + segment, err := buildL2MessageSegment(batchTxs) if err != nil { return nil, err } data = append(data, segment...) - block = nil + batchTxs = nil size = 1 + txSize } - block = append(block, tx) + batchTxs = append(batchTxs, tx) } - if len(block) > 0 { - segment, err := buildL2MessageSegment(block) + if len(batchTxs) > 0 { + segment, err := buildL2MessageSegment(batchTxs) if err != nil { return nil, err } @@ -175,11 +176,11 @@ func init() { TransactionProcessedEvent = abi.Events["TransactionProcessed"] } -func (s *SyndicateAccumulator) ProcessBlock(block *types.Block, receipts types.Receipts) error { - if s.BlockNum > 0 && s.BlockNum+1 != block.NumberU64() { +func (s *SyndicateAccumulator) ProcessBlock(seqBlock *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error { + if s.BlockNum > 0 && s.BlockNum+1 != seqBlock.NumberU64() { return errors.New("unexpected block number") } - s.BlockNum = block.NumberU64() + s.BlockNum = seqBlock.NumberU64() var txs [][]byte for _, receipt := range receipts { for _, log := range receipt.Logs { @@ -202,13 +203,13 @@ func (s *SyndicateAccumulator) ProcessBlock(block *types.Block, receipts types.R var data []byte if len(txs) > 0 { var err error - data, err = buildBatch(txs, block.Time(), block.NumberU64()) + data, err = buildBatch(txs, l1BlockNum, timestamp) if err != nil { return err } } s.Batches = append(s.Batches, teetypes.SyndicateBatch{ - Timestamp: block.Time(), + Timestamp: seqBlock.Time(), Data: data, }) return nil diff --git a/synd-withdrawals/synd-enclave/enclave/server.go b/synd-withdrawals/synd-enclave/enclave/server.go index 2a5215ff6..fcb208dec 100644 --- a/synd-withdrawals/synd-enclave/enclave/server.go +++ b/synd-withdrawals/synd-enclave/enclave/server.go @@ -413,7 +413,7 @@ func processMessage(msg []byte, blockNum uint64, ts uint64) ([]byte, error) { if _, ok := allowedMsgs[msg[0]]; !ok { return nil, fmt.Errorf("unexpected message: type %d", msg[0]) } - if ts >= HARDFORK_TS { + if ts >= L1_BLOCK_NUM_HARDFORK_TS { blockNum = binary.BigEndian.Uint64(msg[33:41]) } if msg[0] == arbostypes.L1MessageType_BatchPostingReport { @@ -460,10 +460,6 @@ func parseAppBatches(input *teetypes.VerifyAppchainInput) ([][]byte, error) { return nil, errors.New("must include at least one delayed message") } - fmt.Println("input.DelayedMessage length", len(input.DelayedMessages)) - // fmt.Println("input.DelayedMessages", input.DelayedMessages) - fmt.Println("input.StartDelayedMessagesAccumulator", input.StartDelayedMessagesAccumulator) - // verify delayed messages startIndex, err := validateDelayedMessages(input.DelayedMessages) if err != nil { diff --git a/synd-withdrawals/synd-enclave/enclave/verify.go b/synd-withdrawals/synd-enclave/enclave/verify.go index 92a6bdef0..4b828ee6e 100644 --- a/synd-withdrawals/synd-enclave/enclave/verify.go +++ b/synd-withdrawals/synd-enclave/enclave/verify.go @@ -6,6 +6,7 @@ package enclave import ( "bytes" "context" + "encoding/binary" "encoding/json" "errors" "fmt" @@ -23,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/triedb" "github.com/SyndicateProtocol/synd-appchains/synd-enclave/enclave/wavmio" + "github.com/SyndicateProtocol/synd-appchains/synd-enclave/teetypes" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" @@ -67,11 +69,13 @@ func readMessage(ctx context.Context, wavm *wavmio.Wavm, delayedMessagesRead uin return msg, nil } +const L1_BLOCK_NUM_HARDFORK_TS = 1767571200 + func Verify( ctx context.Context, data wavmio.ValidationInput, processor interface { - ProcessBlock(*types.Block, types.Receipts) error + ProcessBlock(seqBlock *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error }, ) (_ *execution.MessageResult, err error) { if data.BlockHash == (common.Hash{}) { @@ -154,22 +158,22 @@ func Verify( chainContext := wavmio.WavmChainContext{ChainConfig: chainConfig, Wavm: wavm} - block, receipts, err := arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, header, statedb, chainContext, false, core.NewMessageRecordingContext([]rawdb.WasmTarget{rawdb.LocalTarget()})) + seq_block, receipts, err := arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, header, statedb, chainContext, false, core.NewMessageRecordingContext([]rawdb.WasmTarget{rawdb.LocalTarget()})) if err != nil { return nil, err } - if block.NumberU64() != header.Number.Uint64()+1 { - return nil, fmt.Errorf("unexpected block number: got %d, expected %d", block.NumberU64(), header.Number.Uint64()+1) + if seq_block.NumberU64() != header.Number.Uint64()+1 { + return nil, fmt.Errorf("unexpected block number: got %d, expected %d", seq_block.NumberU64(), header.Number.Uint64()+1) } - header = block.Header() + header = seq_block.Header() bytes, err := rlp.EncodeToBytes(header) if err != nil { return nil, fmt.Errorf("error RLP encoding header: %v", err) } wavm.Preimages[arbutil.Keccak256PreimageType][crypto.Keccak256Hash(bytes)] = bytes - result, err := statedb.Commit(block.NumberU64(), true, false) + result, err := statedb.Commit(seq_block.NumberU64(), true, false) if err != nil { return nil, err } @@ -177,8 +181,25 @@ func Verify( return nil, fmt.Errorf("bad commit root hash expected %v, got %v", header.Root, result) } + // NOTE: l1BlockNum hardfork logic must match slotter.rs + l1BlockNum := uint64(0) + if seq_block.Time() < L1_BLOCK_NUM_HARDFORK_TS { + l1BlockNum = seq_block.NumberU64() + } else { + // Get settlement block number from latest delayed message if available + if len(data.Messages) > 0 { + lastMsg := data.Messages[len(data.Messages)-1] + if len(lastMsg) < teetypes.DelayedMessageBlockNumberOffset+8 { + return nil, errors.New("delayed message too short to contain block number") + } + l1BlockNum = binary.BigEndian.Uint64( + lastMsg[teetypes.DelayedMessageBlockNumberOffset : teetypes.DelayedMessageBlockNumberOffset+8], + ) + } + } + if processor != nil { - if err := processor.ProcessBlock(block, receipts); err != nil { + if err := processor.ProcessBlock(seq_block, receipts, l1BlockNum, seq_block.Time()); err != nil { return nil, err } } diff --git a/synd-withdrawals/synd-enclave/teetypes/types.go b/synd-withdrawals/synd-enclave/teetypes/types.go index facb82f79..a419263df 100644 --- a/synd-withdrawals/synd-enclave/teetypes/types.go +++ b/synd-withdrawals/synd-enclave/teetypes/types.go @@ -29,10 +29,11 @@ var ( // field offsets into the serialized arbostypes.L1IncomingMessage struct const ( - DelayedMessageSenderOffset = 13 - DelayedMessageTimestampOffset = 41 - DelayedMessageRequestIdOffset = 49 - DelayedMessageDataOffset = 113 + DelayedMessageSenderOffset = 13 + DelayedMessageBlockNumberOffset = 33 + DelayedMessageTimestampOffset = 41 + DelayedMessageRequestIdOffset = 49 + DelayedMessageDataOffset = 113 ) // Wrapper around the teemodule.TeeTrustedInput to define the Hash method diff --git a/synd-withdrawals/synd-proposer/go.mod b/synd-withdrawals/synd-proposer/go.mod index f4dc0a595..a810082a0 100644 --- a/synd-withdrawals/synd-proposer/go.mod +++ b/synd-withdrawals/synd-proposer/go.mod @@ -130,6 +130,8 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703 // indirect + github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 // indirect github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect diff --git a/synd-withdrawals/synd-proposer/go.sum b/synd-withdrawals/synd-proposer/go.sum index 6fe92ef31..c70bd0a49 100644 --- a/synd-withdrawals/synd-proposer/go.sum +++ b/synd-withdrawals/synd-proposer/go.sum @@ -250,6 +250,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= @@ -376,6 +377,10 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703 h1:oTi0zYvHo1sfk5sevGc4LrfgpLYB6cIhP/HllCUGcZ8= +github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703/go.mod h1:ycRhVmo6wegyEl6WN+zXOHUTJvB0J2tiuH88q/McTK8= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -677,6 +682,8 @@ golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUU golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -775,7 +782,9 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/synd-withdrawals/synd-proposer/pkg/helpers.go b/synd-withdrawals/synd-proposer/pkg/helpers.go index 717f853c2..69161e688 100644 --- a/synd-withdrawals/synd-proposer/pkg/helpers.go +++ b/synd-withdrawals/synd-proposer/pkg/helpers.go @@ -432,6 +432,8 @@ func GetDelayedMessages( return common.Hash{}, nil, false, errors.Wrap(err, "failed to get block by hash") } + // TODO revisit this + // Override the block number with the L1 block number // It is used during contract execution in nitro rollups l1BlockNum := types.DeserializeHeaderExtraInformation(block.Header()).L1BlockNumber diff --git a/test-framework/tests/e2e/e2e_tests_withdrawals.rs b/test-framework/tests/e2e/e2e_tests_withdrawals.rs index 734a3adac..e65bb88a5 100644 --- a/test-framework/tests/e2e/e2e_tests_withdrawals.rs +++ b/test-framework/tests/e2e/e2e_tests_withdrawals.rs @@ -636,6 +636,13 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu proposer_instance.kill(); enclave_server_instance.kill(); + // TODO + // - progress timestamp to be after L1_BLOCK_NUM_HARDFORK_TS + // - sequence a tx + // - assert l1_block_num on the rollup matches the expected value + // - make a new withdrawal + // - assert the new withdrawal passes + Ok(()) }, ) From 47787c902bdd4e0f230bfc8dc5242d9c4aa4047d Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Mon, 1 Dec 2025 20:50:32 +0000 Subject: [PATCH 5/8] use timestamp from slots with delayd messages, testing WIP --- shared/test-utils/src/docker.rs | 1 + .../crates/synd-slotter/src/slotter.rs | 23 +++-- .../src/components/configuration.rs | 2 + .../src/components/test_components.rs | 7 +- .../tests/e2e/e2e_tests_withdrawals.rs | 99 ++++++++++++++++++- 5 files changed, 121 insertions(+), 11 deletions(-) diff --git a/shared/test-utils/src/docker.rs b/shared/test-utils/src/docker.rs index 733399f1f..a4b59b9e7 100644 --- a/shared/test-utils/src/docker.rs +++ b/shared/test-utils/src/docker.rs @@ -372,6 +372,7 @@ pub async fn launch_nitro_node(args: NitroNodeArgs) -> Result { .arg("--execution.tx-pre-checker.strictness=20") .arg("--ensure-rollup-deployment=false") .arg("--init.validate-genesis-assertion=false") + .arg("--execution.sequencer.max-acceptable-timestamp-delta=8760h") // 1 year .arg(format!( "--chain.info-json={}", nitro_chain_info_json(NitroChainInfoArgs { diff --git a/synd-translator/crates/synd-slotter/src/slotter.rs b/synd-translator/crates/synd-slotter/src/slotter.rs index 2dfa21872..d58665b9c 100644 --- a/synd-translator/crates/synd-slotter/src/slotter.rs +++ b/synd-translator/crates/synd-slotter/src/slotter.rs @@ -81,7 +81,7 @@ pub async fn run( } else if delayed_msgs.is_empty() { 0 } else { - set_block.block_ref.number + set_block.block_ref.number - 1 }; let (tx_count, sequenced_batch) = @@ -547,9 +547,19 @@ mod tests { let set_block_num = 2000; let set_timestamp = timestamp - settlement_delay + 1; + // Create a delayed message for settlement blocks to ensure l1_block_number uses + // set_block.block_ref.number after hardfork (not 0). + // Must be in a settlement block with timestamp <= slot_end_ts to be processed. + let delayed_msg = DelayedMessage { + kind: 0, + sender: Address::ZERO, + data: Bytes::from("test_message"), + base_fee_l1: U256::from(1000), + }; + let seq_blocks = vec![create_seq_block(seq_block_num, timestamp, vec![dummy.clone()])]; let set_blocks = vec![ - create_set_block(set_block_num, timestamp - settlement_delay, vec![]), + create_set_block(set_block_num, timestamp - settlement_delay, vec![delayed_msg]), create_set_block(set_block_num + 1, set_timestamp, vec![]), ]; @@ -573,11 +583,10 @@ mod tests { let (block_num, ts) = extract_batch_headers(&blocks[0].payload.as_ref().unwrap().batch_data)?; - let (expected_block, expected_ts) = if expected_is_settlement { - (set_block_num + 1, set_timestamp) - } else { - (seq_block_num, timestamp) - }; + // After hardfork, block number comes from settlement chain (when delayed_msgs present), + // but timestamp always comes from sequencing chain + let expected_block = if expected_is_settlement { set_block_num } else { seq_block_num }; + let expected_ts = timestamp; assert_eq!(block_num, expected_block, "{}: wrong block number", test_name); assert_eq!(ts, expected_ts, "{}: wrong timestamp", test_name); diff --git a/test-framework/src/components/configuration.rs b/test-framework/src/components/configuration.rs index 02373de38..a3a8aca7d 100644 --- a/test-framework/src/components/configuration.rs +++ b/test-framework/src/components/configuration.rs @@ -40,6 +40,7 @@ pub struct ConfigurationOptions { pub maestro_finalization_duration: Option, pub maestro_finalization_checker_interval: Option, pub close_challenge_interval: Duration, + pub initial_l1_timestamp: Option, } impl Default for ConfigurationOptions { @@ -59,6 +60,7 @@ impl Default for ConfigurationOptions { maestro_finalization_duration: None, maestro_finalization_checker_interval: None, close_challenge_interval: Duration::from_secs(1), + initial_l1_timestamp: None, } } } diff --git a/test-framework/src/components/test_components.rs b/test-framework/src/components/test_components.rs index 763866392..9f6e266ce 100644 --- a/test-framework/src/components/test_components.rs +++ b/test-framework/src/components/test_components.rs @@ -157,9 +157,10 @@ impl TestComponents { BaseChainsType::Anvil | BaseChainsType::PreLoaded(_) => None, BaseChainsType::Nitro | BaseChainsType::NitroWithEigenda => { let info = start_anvil(1).await?; - // avoid "latest L1 block is old" error log from nitro - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs(); - info.provider.evm_mine(Some(MineOptions::Timestamp(Some(now)))).await?; + let timestamp = options.initial_l1_timestamp.unwrap_or_else(|| { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() + }); + info.provider.evm_mine(Some(MineOptions::Timestamp(Some(timestamp)))).await?; info.provider.anvil_set_auto_mine(true).await?; //auto-mine enabled info.provider.anvil_set_block_timestamp_interval(1).await?; Some(info) diff --git a/test-framework/tests/e2e/e2e_tests_withdrawals.rs b/test-framework/tests/e2e/e2e_tests_withdrawals.rs index e65bb88a5..d8e78818e 100644 --- a/test-framework/tests/e2e/e2e_tests_withdrawals.rs +++ b/test-framework/tests/e2e/e2e_tests_withdrawals.rs @@ -25,6 +25,7 @@ use eyre::Result; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, time::Duration}; use synd_block_builder::appchains::shared::sequencing_transaction_parser::L2MessageKind; +use synd_mchain::db::L1_BLOCK_NUM_HARDFORK_TS; use test_framework::components::{ configuration::{BaseChainsType, ConfigurationOptions}, proposer::ProposerConfig, @@ -35,6 +36,7 @@ use test_utils::{ docker::{launch_enclave_server, start_component}, nitro_chain::{ apply_l1_to_l2_alias, execute_withdrawal, init_withdrawal_tx, ExecuteWithdrawalParams, + NitroBlock, }, port_manager::PortManager, wait_until, @@ -67,7 +69,7 @@ sol! { function number() return uint256; } -#[allow(clippy::unwrap_used)] +#[allow(clippy::unwrap_used, clippy::large_stack_frames)] async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Result<()> { let close_challenge_interval = Duration::from_secs(1); let settlement_delay = 2; @@ -77,6 +79,8 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu rollup_owner: test_account1().address, settlement_delay, close_challenge_interval, + // 1h before hardfork timestamp + // initial_l1_timestamp: Some(L1_BLOCK_NUM_HARDFORK_TS - 3600), ..Default::default() }, |components| async move { @@ -643,6 +647,99 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu // - make a new withdrawal // - assert the new withdrawal passes + + // Progress L1 timestamp to be after L1_BLOCK_NUM_HARDFORK_TS + assert!( + l1_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .unwrap().header.timestamp < L1_BLOCK_NUM_HARDFORK_TS ); + + // Progress time to just after the hardfork + l1_provider + .evm_mine(Some(MineOptions::Options { + timestamp: Some(L1_BLOCK_NUM_HARDFORK_TS + 10), + blocks: Some(1), + })) + .await?; + + // Get the current settlement chain block number to verify later + let settlement_block_before = components + .settlement_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .unwrap() + .header + .number; + + /// TODO breaking here... continue + + // Sequence a transaction + let tx_receipt = components + .sequence_tx(b"test_tx_after_hardfork", 0, true) + .await? + .unwrap(); + + // Get the appchain block and verify l1_block_number + let block: NitroBlock = components + .appchain_provider + .raw_request( + "eth_getBlockByHash".into(), + (tx_receipt.block_hash.unwrap(), false), + ) + .await?; + + // After hardfork, l1_block_number should come from settlement chain + assert!( + block.l1_block_number >= U256::from(settlement_block_before), + "l1_block_number should be from settlement chain after hardfork. Expected >= {}, got {}", + settlement_block_before, + block.l1_block_number + ); + + // Make a new withdrawal + let withdrawal_value = parse_ether("0.05")?; + let to_address = address!("0x0000000000000000000000000000000000000003"); + let tx = + init_withdrawal_tx(to_address, withdrawal_value, &components.appchain_provider) + .await?; + + let receipt = components.sequence_tx(tx.encoded_2718().as_slice(), 0, true).await?; + let appchain_block_hash_to_prove = receipt.unwrap().block_hash.unwrap(); + + // Wait for the withdrawal root to be posted + wait_until!( + rollup_core + .NodeConfirmed_filter() + .query() + .await? + .iter() + .any(|event| event.0.blockHash == appchain_block_hash_to_prove), + Duration::from_secs(20 * 60) + ); + + // Execute the withdrawal + execute_withdrawal(ExecuteWithdrawalParams { + to_address, + withdrawal_value, + appchain_block_hash_to_prove, + bridge_address: components.appchain_deployment.bridge, + settlement_provider: &components.settlement_provider, + appchain_provider: &components.appchain_provider, + l2_sender: components.appchain_provider.default_signer_address(), + send_root_size: 3, // We've made 3 withdrawals total now + withdrawal_position: 0, + }) + .await; + + // Assert the withdrawal passed + let balance_after = components.settlement_provider.get_balance(to_address).await?; + assert_eq!(balance_after, withdrawal_value); + + // Final cleanup + proposer_instance.kill(); + enclave_server_instance.kill(); + Ok(()) }, ) From a69a9a3d9f6f4983a61b65c01d962a5b3a4ae145 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Fri, 5 Dec 2025 18:34:01 +0000 Subject: [PATCH 6/8] WIP testing --- shared/test-utils/src/docker.rs | 28 +- synd-chain-ingestor/tests/integration_test.rs | 2 + synd-mchain/src/db.rs | 14 +- .../crates/synd-slotter/src/slotter.rs | 31 +- .../synd-enclave/enclave/verify.go | 14 +- .../src/components/configuration.rs | 4 +- .../src/components/test_components.rs | 15 +- .../tests/e2e/e2e_tests_migration.rs | 6 + .../tests/e2e/e2e_tests_withdrawals.rs | 604 ++++++++++-------- 9 files changed, 437 insertions(+), 281 deletions(-) diff --git a/shared/test-utils/src/docker.rs b/shared/test-utils/src/docker.rs index a4b59b9e7..711d310a3 100644 --- a/shared/test-utils/src/docker.rs +++ b/shared/test-utils/src/docker.rs @@ -17,6 +17,7 @@ use eyre::Result; use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder}; use redis::aio::ConnectionManager; use std::{ + collections::HashMap, env, future::Future, path::Path, @@ -125,6 +126,7 @@ impl Drop for E2EProcess { pub async fn start_component( executable_name: &str, api_port: u16, + env_vars: HashMap, args: Vec, cargs: Vec, ) -> Result { @@ -162,6 +164,7 @@ pub async fn start_component( let mut docker = if let Ok(tag) = tag { E2EProcess::new( Command::new("docker") + .envs(env_vars) .arg("run") .arg("--init") .arg("--rm") @@ -184,6 +187,7 @@ pub async fn start_component( .env_remove("CARGO_PKG_VERSION_PRE") .env_remove("CARGO_MANIFEST_LINKS") .current_dir(env!("CARGO_WORKSPACE_DIR")) + .envs(env_vars) .arg("run"); if needs_rocksdb { @@ -217,6 +221,7 @@ pub async fn start_mchain( appchain_chain_id: u64, finality_delay: u64, migration_params: Option, + l1_block_num_hardfork_ts: Option, config_manager_rpc_url: Option, config_manager_address: Option
, ) -> Result<(String, E2EProcess, MProvider)> { @@ -258,8 +263,22 @@ pub async fn start_mchain( args.extend(vec!["--config-manager-address".to_string(), address.to_string()]); } - let docker = - start_component("synd-mchain", port, args, vec!["--datadir".to_string(), tmp_dir]).await?; + let mut env_vars = HashMap::new(); + if let Some(custom_l1_block_num_hardfork_ts) = l1_block_num_hardfork_ts { + env_vars.insert( + "L1_BLOCK_NUM_HARDFORK_TS".to_string(), + custom_l1_block_num_hardfork_ts.to_string(), + ); + } + + let docker = start_component( + "synd-mchain", + port, + env_vars, + args, + vec!["--datadir".to_string(), tmp_dir], + ) + .await?; let url = format!("ws://localhost:{port}"); let mchain = MProvider::new(&url).await?; Ok((url, docker, mchain)) @@ -445,7 +464,9 @@ pub async fn start_valkey() -> Result<(E2EProcess, String)> { Ok((valkey, valkey_url)) } -pub async fn launch_enclave_server() -> Result<(E2EProcess, String, Address)> { +pub async fn launch_enclave_server( + env_vars: HashMap, +) -> Result<(E2EProcess, String, Address)> { info!("launching enclave server"); let project_root = env!("CARGO_WORKSPACE_DIR"); @@ -492,6 +513,7 @@ pub async fn launch_enclave_server() -> Result<(E2EProcess, String, Address)> { let port = PortManager::instance().next_port().await; let docker = E2EProcess::new( Command::new("docker") + .envs(env_vars) .arg("run") .arg("--init") .arg("--rm") diff --git a/synd-chain-ingestor/tests/integration_test.rs b/synd-chain-ingestor/tests/integration_test.rs index 55044c96d..1ff8ca364 100644 --- a/synd-chain-ingestor/tests/integration_test.rs +++ b/synd-chain-ingestor/tests/integration_test.rs @@ -21,6 +21,7 @@ use tracing::info; mod tests { use super::*; use shared::types::DelayedMsgsData; + use std::collections::HashMap; use url::Url; struct MockBlockBuilder; @@ -79,6 +80,7 @@ mod tests { let sequencing_chain_ingestor = start_component( "synd-chain-ingestor", seq_chain_ingestor_cfg.port, + HashMap::new(), seq_chain_ingestor_cfg.cli_args(), Default::default(), ) diff --git a/synd-mchain/src/db.rs b/synd-mchain/src/db.rs index ace4c3c6c..cb32cd46b 100644 --- a/synd-mchain/src/db.rs +++ b/synd-mchain/src/db.rs @@ -8,7 +8,7 @@ use jsonrpsee::types::{error::INTERNAL_ERROR_CODE, ErrorObjectOwned}; #[cfg(feature = "rocksdb")] use rocksdb::{DBWithThreadMode, ThreadMode}; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::{env, fmt}; use tracing::{debug, info, trace}; /// VERSION must be bumped whenever a breaking change is made @@ -115,6 +115,14 @@ pub struct Block { /// 5 Jan 2026 pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1767571200; +/// gets the timestamp for the `l1_block_number` hardfork (supports env var override for testing +/// purposes) +#[allow(clippy::expect_used)] +pub fn get_l1_block_num_hardfork_ts() -> u64 { + env::var("L1_BLOCK_NUM_HARDFORK_TS") + .map_or(L1_BLOCK_NUM_HARDFORK_TS, |val| val.parse().expect("invalid timestamp provided")) +} + impl Block { /// The delayed message accumulator pub fn after_message_acc(&self) -> FixedBytes<32> { @@ -126,8 +134,8 @@ impl Block { } /// l1 block number for this mchain block - pub const fn l1_block_number(&self) -> u64 { - if self.timestamp < L1_BLOCK_NUM_HARDFORK_TS { + pub fn l1_block_number(&self) -> u64 { + if self.timestamp < get_l1_block_num_hardfork_ts() { self.slot.seq_block_number } else { self.slot.set_block_number diff --git a/synd-translator/crates/synd-slotter/src/slotter.rs b/synd-translator/crates/synd-slotter/src/slotter.rs index d58665b9c..397ec7e70 100644 --- a/synd-translator/crates/synd-slotter/src/slotter.rs +++ b/synd-translator/crates/synd-slotter/src/slotter.rs @@ -8,10 +8,10 @@ use synd_block_builder::appchains::shared::RollupAdapter; use synd_chain_ingestor::client::BlockStreamT; use synd_mchain::{ client::MchainProvider, - db::{ArbitrumBatch, MBlock, Slot, L1_BLOCK_NUM_HARDFORK_TS}, + db::{get_l1_block_num_hardfork_ts, ArbitrumBatch, MBlock, Slot}, }; use thiserror::Error; -use tracing::{info, instrument, trace}; +use tracing::{debug, info, instrument, trace}; /// Ingests blocks from the sequencing and settlement chains, slots them into slots, and sends the /// slots to the slot processor to generate `synd-mchain` blocks. @@ -44,6 +44,7 @@ pub async fn run( .recv(0) .await .map_err(|e| SlotterError::IngestorError(Chain::Sequencing, e.to_string()))?; + trace!("got seq_block: {seq_block:?}"); metrics.record_last_processed_block(seq_block.block_ref.number, Chain::Sequencing); metrics.update_chain_timestamp_lag(seq_block.block_ref.timestamp, Chain::Sequencing); @@ -62,27 +63,34 @@ pub async fn run( let mut delayed_msgs = vec![]; - let mut blocks_per_slot: u64 = 1; + let mut set_blocks_per_slot: u64 = 1; let slot_end_ts = seq_block.block_ref.timestamp.saturating_sub(settlement_delay); + // TODO is it okay to be 0 when there are no delayed msgs? + let mut set_block_num_in_slot = 0u64; + while set_block.block_ref.timestamp <= slot_end_ts { - blocks_per_slot += 1; - delayed_msgs.append(&mut set_block.messages); + set_blocks_per_slot += 1; + if !set_block.messages.is_empty() { + delayed_msgs.append(&mut set_block.messages); + set_block_num_in_slot = set_block.block_ref.number; + } + set_block = settlement .recv(slot_end_ts) .await .map_err(|e| SlotterError::IngestorError(Chain::Settlement, e.to_string()))?; + trace!("got set_block: {set_block:?}"); metrics.record_last_processed_block(set_block.block_ref.number, Chain::Settlement); metrics.update_chain_timestamp_lag(set_block.block_ref.timestamp, Chain::Settlement); } - let l1_block_number = if timestamp < L1_BLOCK_NUM_HARDFORK_TS { + let l1_block_number = if timestamp < get_l1_block_num_hardfork_ts() { seq_block.block_ref.number - } else if delayed_msgs.is_empty() { - 0 } else { - set_block.block_ref.number - 1 + set_block_num_in_slot }; + debug!("using l1_block_number: {l1_block_number}"); let (tx_count, sequenced_batch) = build_batch(&seq_block, &rollup_adapter, l1_block_number, timestamp)?; @@ -93,7 +101,7 @@ pub async fn run( mblock.slot.set_block_hash = set_block.block_ref.hash; mblock.slot.set_block_number = set_block.block_ref.number; - trace!("Processing slot {:?}", mblock.slot); + debug!("Processing slot {:?}", mblock.slot); let time = std::time::Instant::now(); mchain .add_batch(&mblock) @@ -108,8 +116,9 @@ pub async fn run( mblock.timestamp, time.elapsed() ); + debug!("slot payload: {payload:?}"); } - metrics.record_blocks_per_slot(blocks_per_slot); + metrics.record_blocks_per_slot(set_blocks_per_slot); metrics.record_last_slot(mblock.slot.seq_block_number); } } diff --git a/synd-withdrawals/synd-enclave/enclave/verify.go b/synd-withdrawals/synd-enclave/enclave/verify.go index 4b828ee6e..da6a1fe70 100644 --- a/synd-withdrawals/synd-enclave/enclave/verify.go +++ b/synd-withdrawals/synd-enclave/enclave/verify.go @@ -10,6 +10,8 @@ import ( "encoding/json" "errors" "fmt" + "os" + "strconv" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -71,6 +73,16 @@ func readMessage(ctx context.Context, wavm *wavmio.Wavm, delayedMessagesRead uin const L1_BLOCK_NUM_HARDFORK_TS = 1767571200 +// getL1BlockNumHardforkTS returns the hardfork timestamp, supporting env var override for testing +func getL1BlockNumHardforkTS() uint64 { + if val := os.Getenv("L1_BLOCK_NUM_HARDFORK_TS"); val != "" { + if ts, err := strconv.ParseUint(val, 10, 64); err == nil { + return ts + } + } + return L1_BLOCK_NUM_HARDFORK_TS +} + func Verify( ctx context.Context, data wavmio.ValidationInput, @@ -183,7 +195,7 @@ func Verify( // NOTE: l1BlockNum hardfork logic must match slotter.rs l1BlockNum := uint64(0) - if seq_block.Time() < L1_BLOCK_NUM_HARDFORK_TS { + if seq_block.Time() < getL1BlockNumHardforkTS() { l1BlockNum = seq_block.NumberU64() } else { // Get settlement block number from latest delayed message if available diff --git a/test-framework/src/components/configuration.rs b/test-framework/src/components/configuration.rs index a3a8aca7d..f2e3622e5 100644 --- a/test-framework/src/components/configuration.rs +++ b/test-framework/src/components/configuration.rs @@ -40,7 +40,7 @@ pub struct ConfigurationOptions { pub maestro_finalization_duration: Option, pub maestro_finalization_checker_interval: Option, pub close_challenge_interval: Duration, - pub initial_l1_timestamp: Option, + pub l1_block_num_hardfork_ts: Option, } impl Default for ConfigurationOptions { @@ -60,7 +60,7 @@ impl Default for ConfigurationOptions { maestro_finalization_duration: None, maestro_finalization_checker_interval: None, close_challenge_interval: Duration::from_secs(1), - initial_l1_timestamp: None, + l1_block_num_hardfork_ts: None, } } } diff --git a/test-framework/src/components/test_components.rs b/test-framework/src/components/test_components.rs index 9f6e266ce..7c0e5eb1a 100644 --- a/test-framework/src/components/test_components.rs +++ b/test-framework/src/components/test_components.rs @@ -117,7 +117,6 @@ pub struct TestComponents { pub const SEQUENCING_CHAIN_ID: u64 = 15; pub const SETTLEMENT_CHAIN_ID: u64 = 31337; -pub const GENESIS_TIMESTAMP: u64 = 1756209109; // some time after EPOCH_START_TIME (see GasAggregator.sol) #[allow(clippy::unwrap_used)] impl TestComponents { @@ -157,10 +156,6 @@ impl TestComponents { BaseChainsType::Anvil | BaseChainsType::PreLoaded(_) => None, BaseChainsType::Nitro | BaseChainsType::NitroWithEigenda => { let info = start_anvil(1).await?; - let timestamp = options.initial_l1_timestamp.unwrap_or_else(|| { - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() - }); - info.provider.evm_mine(Some(MineOptions::Timestamp(Some(timestamp)))).await?; info.provider.anvil_set_auto_mine(true).await?; //auto-mine enabled info.provider.anvil_set_block_timestamp_interval(1).await?; Some(info) @@ -274,7 +269,7 @@ impl TestComponents { } BaseChainsType::PreLoaded(_) => { // disable gas tracking, otherwise there will be an underflow error with anvil - // timstamp 0 + // timestamp 0 let _ = sequencing_contract.disableGasTracking().send().await?; mine_block(&seq_provider, 0).await?; } @@ -439,6 +434,7 @@ impl TestComponents { options.appchain_chain_id, options.finality_delay, None, + options.l1_block_num_hardfork_ts, Some(set_rpc_ws_url.clone()), Some(config_manager_address), ) @@ -457,6 +453,7 @@ impl TestComponents { let sequencing_chain_ingestor = start_component( "synd-chain-ingestor", seq_chain_ingestor_cfg.port, + HashMap::new(), seq_chain_ingestor_cfg.cli_args(), Default::default(), ) @@ -474,6 +471,7 @@ impl TestComponents { let settlement_chain_ingestor = start_component( "synd-chain-ingestor", set_chain_ingestor_cfg.port, + HashMap::new(), set_chain_ingestor_cfg.cli_args(), Default::default(), ) @@ -499,6 +497,9 @@ impl TestComponents { let translator = start_component( "synd-translator", translator_config.port, + options.l1_block_num_hardfork_ts.map_or_else(HashMap::new, |ts| { + HashMap::from([("L1_BLOCK_NUM_HARDFORK_TS".to_string(), ts.to_string())]) + }), translator_config.cli_args(), vec![], ) @@ -593,6 +594,7 @@ impl TestComponents { "synd-maestro", // `/health` is proxied to RPC method maestro_config.port, + HashMap::new(), maestro_config.cli_args(), Default::default(), ) @@ -612,6 +614,7 @@ impl TestComponents { let batch_sequencer_instance = start_component( "synd-batch-sequencer", batch_sequencer_config.port, + HashMap::new(), batch_sequencer_config.cli_args(), Default::default(), ) diff --git a/test-framework/tests/e2e/e2e_tests_migration.rs b/test-framework/tests/e2e/e2e_tests_migration.rs index 1465a8cec..98a0b8c11 100644 --- a/test-framework/tests/e2e/e2e_tests_migration.rs +++ b/test-framework/tests/e2e/e2e_tests_migration.rs @@ -115,6 +115,7 @@ async fn spin_up_syndicate_stack( appchain_chain_id, opt.finality_delay, None, + None, Some(settlement_rpc_url.clone()), Some(config_manager_address), ) @@ -132,6 +133,7 @@ async fn spin_up_syndicate_stack( let sequencing_chain_ingestor = start_component( "synd-chain-ingestor", seq_chain_ingestor_cfg.port, + HashMap::new(), seq_chain_ingestor_cfg.cli_args(), Default::default(), ) @@ -149,6 +151,7 @@ async fn spin_up_syndicate_stack( let settlement_chain_ingestor = start_component( "synd-chain-ingestor", set_chain_ingestor_cfg.port, + HashMap::new(), set_chain_ingestor_cfg.cli_args(), Default::default(), ) @@ -174,6 +177,7 @@ async fn spin_up_syndicate_stack( let translator = start_component( "synd-translator", translator_config.port, + HashMap::new(), translator_config.cli_args(), vec![], ) @@ -212,6 +216,7 @@ async fn spin_up_syndicate_stack( "synd-maestro", // `/health` is proxied to RPC method maestro_config.port, + HashMap::new(), maestro_config.cli_args(), Default::default(), ) @@ -228,6 +233,7 @@ async fn spin_up_syndicate_stack( let batch_sequencer = start_component( "synd-batch-sequencer", batch_sequencer_config.port, + HashMap::new(), batch_sequencer_config.cli_args(), Default::default(), ) diff --git a/test-framework/tests/e2e/e2e_tests_withdrawals.rs b/test-framework/tests/e2e/e2e_tests_withdrawals.rs index d8e78818e..89744f13d 100644 --- a/test-framework/tests/e2e/e2e_tests_withdrawals.rs +++ b/test-framework/tests/e2e/e2e_tests_withdrawals.rs @@ -11,11 +11,11 @@ use alloy::{ rpc::types::{ anvil::MineOptions, trace::geth::{GethDebugTracingOptions, GethTrace}, - TransactionReceipt, TransactionRequest, + TransactionRequest, }, signers::local::PrivateKeySigner, sol, - sol_types::{SolCall, SolValue}, + sol_types::SolValue, }; use contract_bindings::synd::{ assertion_poster::AssertionPoster, i_bridge::IBridge, i_inbox::IInbox, @@ -23,8 +23,11 @@ use contract_bindings::synd::{ }; use eyre::Result; use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, time::Duration}; -use synd_block_builder::appchains::shared::sequencing_transaction_parser::L2MessageKind; +use std::{ + collections::HashMap, + fmt::Debug, + time::{Duration, SystemTime}, +}; use synd_mchain::db::L1_BLOCK_NUM_HARDFORK_TS; use test_framework::components::{ configuration::{BaseChainsType, ConfigurationOptions}, @@ -53,7 +56,8 @@ fn init() { #[tokio::test] async fn e2e_tee_withdrawal() -> Result<()> { // use eigenda - e2e_tee_withdrawal_basic_flow(BaseChainsType::NitroWithEigenda).await?; + // TODO uncomment + // e2e_tee_withdrawal_basic_flow(BaseChainsType::NitroWithEigenda).await?; // use calldata e2e_tee_withdrawal_basic_flow(BaseChainsType::Nitro).await?; Ok(()) @@ -69,24 +73,29 @@ sol! { function number() return uint256; } +// NOTE: Custom timestamp for L1 anvil cannot be used in this test. This is because nitro uses the +// system clock time in sequencer mode and it will get very confused if we set a custom timestamp #[allow(clippy::unwrap_used, clippy::large_stack_frames)] async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Result<()> { let close_challenge_interval = Duration::from_secs(1); let settlement_delay = 2; + // Set it to 30m after current clock time + let l1_block_num_hardfork_ts = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() + 30 * 60; TestComponents::run( &ConfigurationOptions { base_chains_type, rollup_owner: test_account1().address, settlement_delay, close_challenge_interval, - // 1h before hardfork timestamp - // initial_l1_timestamp: Some(L1_BLOCK_NUM_HARDFORK_TS - 3600), + l1_block_num_hardfork_ts: Some(l1_block_num_hardfork_ts), ..Default::default() }, |components| async move { // Simulate L1 block production (this helps to alleviate race // conditions when enclave/proposer are building and on slower machines) let l1_provider = components.l1_provider.as_ref().unwrap(); + let l1_provider_clone = l1_provider.clone(); let _task = tokio::spawn(async move { loop { @@ -269,8 +278,12 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu assert!(receipt.status()); // start enclave and proposer, obtain the tee public key - let (mut enclave_server_instance, enclave_rpc_url, tee_public_key) = - launch_enclave_server().await?; + let (_enclave_server_instance, enclave_rpc_url, tee_public_key) = + launch_enclave_server(HashMap::from([( + "L1_BLOCK_NUM_HARDFORK_TS".to_string(), + l1_block_num_hardfork_ts.to_string(), + )])) + .await?; // deposit funds for the proposer to use on the settlement chain let tx = IInbox::new( @@ -339,9 +352,10 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu let is_valid = key_mgr.isKeyValid(tee_public_key).call().await?; assert!(is_valid); - let mut proposer_instance = start_component( + let _proposer_instance = start_component( "synd-proposer", proposer_config.port, + HashMap::new(), proposer_config.cli_args(), Default::default(), ) @@ -359,133 +373,138 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu Duration::from_millis(500) ); - // send 101 valid txs plus some invalid ones to trigger the block splitting code which - // does not require the nitro fork to be enabled - let latest = components.appchain_provider.get_block_number().await?; - let alias_address = apply_l1_to_l2_alias(test_account1().address); - let dummy_tx = vec![L2MessageKind::SignedTx as u8, 0xc0]; - let mut txs = vec![]; - for _ in 0..100 { - txs.push(dummy_tx.clone().into()); - } - for i in 0..101 { - txs.push( - TransactionRequest::default() - .with_to(alias_address) - .with_value(parse_ether("0.001")?) - .with_nonce(i) - .with_gas_limit(100_000) - .with_chain_id(components.appchain_chain_id) - .with_max_fee_per_gas(100000000) - .with_max_priority_fee_per_gas(0) - .build(components.sequencing_provider.wallet()) - .await? - .encoded_2718() - .into(), - ); - if i % 10 == 0 { - txs.push(dummy_tx.clone().into()); - } - } - for _ in 0..100 { - txs.push(dummy_tx.clone().into()); - } - components.sequence_batch(txs, 0).await?; - wait_until!( - components.appchain_provider.get_balance(alias_address).await? >= - parse_ether("0.101")?, - Duration::from_secs(10) - ); - assert_eq!(components.appchain_provider.get_block_number().await?, latest + 2); - let block_1 = - components.appchain_provider.get_block_by_number((latest + 1).into()).await?; - assert_eq!(block_1.map(|x| x.transactions.len()), Some(101)); - let block_2 = - components.appchain_provider.get_block_by_number((latest + 2).into()).await?; - assert_eq!(block_2.map(|x| x.transactions.len()), Some(2)); - assert_eq!( - components.appchain_provider.get_balance(alias_address).await?, - parse_ether("0.101")? - ); - - assert_eq!(components.appchain_provider.get_block_number().await.unwrap(), 0xb); - - // Deploy a stylus contract - let stylus_tx = TransactionRequest::default() - .with_deploy_code(STYLUS_COUNTER_SMARTCACHE) - .with_nonce(101) - .with_gas_limit(10_000_000) - .with_chain_id(components.appchain_chain_id) - .with_max_fee_per_gas(100000000) - .with_max_priority_fee_per_gas(0) - .build(components.sequencing_provider.wallet()) - .await? - .encoded_2718(); - - let stylus = components - .sequence_tx(&stylus_tx, 0, true) - .await? - .unwrap() - .contract_address - .unwrap(); - - // Activate the stylus contract - let active_tx = TransactionRequest::default() - // ArbWasm - .with_to(address!("0x0000000000000000000000000000000000000071")) - .input(activateProgramCall { program: stylus }.abi_encode().into()) - .with_value(parse_ether("0.5").unwrap()) - .with_nonce(102) - .with_gas_limit(10_000_000) - .with_chain_id(components.appchain_chain_id) - .with_max_fee_per_gas(100000000) - .with_max_priority_fee_per_gas(0) - .build(components.sequencing_provider.wallet()) - .await? - .encoded_2718(); - - components.sequence_tx(&active_tx, 0, false).await?; - - // Set a value on the stylus contract - let set_value_tx = TransactionRequest::default() - .with_to(stylus) - .input( - setNumberSmartcacheArbitrumMainnetNetworkCall { new_number: U256::from(34) } - .abi_encode() - .into(), - ) - .with_nonce(103) - .with_gas_limit(1_000_000) - .with_chain_id(components.appchain_chain_id) - .with_max_fee_per_gas(100000000) - .with_max_priority_fee_per_gas(0) - .build(components.sequencing_provider.wallet()) - .await? - .encoded_2718(); - - components.sequence_tx(&set_value_tx, 0, true).await?.unwrap(); - - // Wait for the blocks to be mined - wait_until!( - components.appchain_provider.get_block_number().await.unwrap() == 0xe, - Duration::from_secs(10) - ); - - // check the value - assert_eq!( - U256::try_from_be_slice( - &components - .appchain_provider - .call( - TransactionRequest::default() - .with_to(stylus) - .input(numberCall {}.abi_encode().into()), - ) - .await?, - ) - .unwrap(), - U256::from(34) - ); + // TODO uncomment, just to speed up testing + /* + + // send 101 valid txs plus some invalid ones to trigger the block splitting code which + // does not require the nitro fork to be enabled + let latest = components.appchain_provider.get_block_number().await?; + let alias_address = apply_l1_to_l2_alias(test_account1().address); + let dummy_tx = vec![L2MessageKind::SignedTx as u8, 0xc0]; + let mut txs = vec![]; + for _ in 0..100 { + txs.push(dummy_tx.clone().into()); + } + for i in 0..101 { + txs.push( + TransactionRequest::default() + .with_to(alias_address) + .with_value(parse_ether("0.001")?) + .with_nonce(i) + .with_gas_limit(100_000) + .with_chain_id(components.appchain_chain_id) + .with_max_fee_per_gas(100000000) + .with_max_priority_fee_per_gas(0) + .build(components.sequencing_provider.wallet()) + .await? + .encoded_2718() + .into(), + ); + if i % 10 == 0 { + txs.push(dummy_tx.clone().into()); + } + } + for _ in 0..100 { + txs.push(dummy_tx.clone().into()); + } + components.sequence_batch(txs, 0).await?; + wait_until!( + components.appchain_provider.get_balance(alias_address).await? >= + parse_ether("0.101")?, + Duration::from_secs(10) + ); + assert_eq!(components.appchain_provider.get_block_number().await?, latest + 2); + let block_1 = + components.appchain_provider.get_block_by_number((latest + 1).into()).await?; + assert_eq!(block_1.map(|x| x.transactions.len()), Some(101)); + let block_2 = + components.appchain_provider.get_block_by_number((latest + 2).into()).await?; + assert_eq!(block_2.map(|x| x.transactions.len()), Some(2)); + assert_eq!( + components.appchain_provider.get_balance(alias_address).await?, + parse_ether("0.101")? + ); + + assert_eq!(components.appchain_provider.get_block_number().await.unwrap(), 0xb); + + // Deploy a stylus contract + let stylus_tx = TransactionRequest::default() + .with_deploy_code(STYLUS_COUNTER_SMARTCACHE) + .with_nonce(101) + .with_gas_limit(10_000_000) + .with_chain_id(components.appchain_chain_id) + .with_max_fee_per_gas(100000000) + .with_max_priority_fee_per_gas(0) + .build(components.sequencing_provider.wallet()) + .await? + .encoded_2718(); + + let stylus = components + .sequence_tx(&stylus_tx, 0, true) + .await? + .unwrap() + .contract_address + .unwrap(); + + // Activate the stylus contract + let active_tx = TransactionRequest::default() + // ArbWasm + .with_to(address!("0x0000000000000000000000000000000000000071")) + .input(activateProgramCall { program: stylus }.abi_encode().into()) + .with_value(parse_ether("0.5").unwrap()) + .with_nonce(102) + .with_gas_limit(10_000_000) + .with_chain_id(components.appchain_chain_id) + .with_max_fee_per_gas(100000000) + .with_max_priority_fee_per_gas(0) + .build(components.sequencing_provider.wallet()) + .await? + .encoded_2718(); + + components.sequence_tx(&active_tx, 0, false).await?; + + // Set a value on the stylus contract + let set_value_tx = TransactionRequest::default() + .with_to(stylus) + .input( + setNumberSmartcacheArbitrumMainnetNetworkCall { new_number: U256::from(34) } + .abi_encode() + .into(), + ) + .with_nonce(103) + .with_gas_limit(1_000_000) + .with_chain_id(components.appchain_chain_id) + .with_max_fee_per_gas(100000000) + .with_max_priority_fee_per_gas(0) + .build(components.sequencing_provider.wallet()) + .await? + .encoded_2718(); + + components.sequence_tx(&set_value_tx, 0, true).await?.unwrap(); + + // Wait for the blocks to be mined + wait_until!( + components.appchain_provider.get_block_number().await.unwrap() == 0xe, + Duration::from_secs(10) + ); + + // check the value + assert_eq!( + U256::try_from_be_slice( + &components + .appchain_provider + .call( + TransactionRequest::default() + .with_to(stylus) + .input(numberCall {}.abi_encode().into()), + ) + .await?, + ) + .unwrap(), + U256::from(34) + ); + + */ // send a contract tx to trigger the nitro fork code. #[cfg(false)] @@ -561,110 +580,121 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu let balance_after = components.settlement_provider.get_balance(to_address).await?; assert_eq!(balance_after, withdrawal_value); - // lets withdraw using sendL2MessageFromOrigin - let withdrawal_value = parse_ether("0.5")?; - let to_address = address!("0x0000000000000000000000000000000000000002"); - let withdraw_from_origin_tx = - init_withdrawal_tx(to_address, withdrawal_value, &components.appchain_provider) - .await?; - let tx_hash = withdraw_from_origin_tx.hash(); - let mut raw_tx_with_prefix = withdraw_from_origin_tx.encoded_2718(); - raw_tx_with_prefix.insert(0, L2MessageKind::SignedTx as u8); - - let nonce = components - .settlement_provider - .get_transaction_count(components.settlement_provider.default_signer_address()) - .await?; - assert!(inbox - .sendL2MessageFromOrigin(raw_tx_with_prefix.into()) - .nonce(nonce) - .send() - .await? - .get_receipt() - .await? - .status()); - - let mut receipt: Option = None; - wait_until!( - { - // send a dummy tx so that the sequencing chain progresses and the deposit is - // slotted in - components.sequence_tx(b"dummy_tx", 0, false).await?; - receipt = - components.appchain_provider.get_transaction_receipt(*tx_hash).await?; - receipt.is_some() - }, - Duration::from_secs(60), - Duration::from_millis(500) - ); - let receipt = receipt.unwrap(); - assert!(receipt.status()); - - // wait for the sendroot to be updated - let appchain_block_hash_to_prove = receipt.block_hash.unwrap(); - wait_until!( - rollup_core - .NodeConfirmed_filter() - .query() - .await? - .iter() - .any(|event| event.0.blockHash == appchain_block_hash_to_prove), - Duration::from_secs(10 * 60) - ); - - // topic 3 of the L2ToL1Tx event is the withdrawal position - let withdrawal_position: u64 = - U256::from_be_bytes(receipt.logs()[1].clone().topics()[3].into()) - .try_into() - .unwrap(); - - // finish the withdrawal on the settlement chain - execute_withdrawal(ExecuteWithdrawalParams { - to_address, - withdrawal_value, - appchain_block_hash_to_prove, - bridge_address: components.appchain_deployment.bridge, - settlement_provider: &components.settlement_provider, - appchain_provider: &components.appchain_provider, - l2_sender: components.appchain_provider.default_signer_address(), - send_root_size: 2, - withdrawal_position, - }) - .await; - - // Assert new balance is equal to withdrawal amount - let balance_after = components.settlement_provider.get_balance(to_address).await?; - assert_eq!(balance_after, withdrawal_value); - - // Cleanup: kill the instances - proposer_instance.kill(); - enclave_server_instance.kill(); - - // TODO - // - progress timestamp to be after L1_BLOCK_NUM_HARDFORK_TS + // TODO uncomment, just to speed up testing + /* + + // lets withdraw using sendL2MessageFromOrigin + let withdrawal_value = parse_ether("0.5")?; + let to_address = address!("0x0000000000000000000000000000000000000002"); + let withdraw_from_origin_tx = + init_withdrawal_tx(to_address, withdrawal_value, &components.appchain_provider) + .await?; + let tx_hash = withdraw_from_origin_tx.hash(); + let mut raw_tx_with_prefix = withdraw_from_origin_tx.encoded_2718(); + raw_tx_with_prefix.insert(0, L2MessageKind::SignedTx as u8); + + let nonce = components + .settlement_provider + .get_transaction_count(components.settlement_provider.default_signer_address()) + .await?; + assert!(inbox + .sendL2MessageFromOrigin(raw_tx_with_prefix.into()) + .nonce(nonce) + .send() + .await? + .get_receipt() + .await? + .status()); + + let mut receipt: Option = None; + wait_until!( + { + // send a dummy tx so that the sequencing chain progresses and the deposit is + // slotted in + components.sequence_tx(b"dummy_tx", 0, false).await?; + receipt = + components.appchain_provider.get_transaction_receipt(*tx_hash).await?; + receipt.is_some() + }, + Duration::from_secs(60), + Duration::from_millis(500) + ); + let receipt = receipt.unwrap(); + assert!(receipt.status()); + + // wait for the sendroot to be updated + let appchain_block_hash_to_prove = receipt.block_hash.unwrap(); + wait_until!( + rollup_core + .NodeConfirmed_filter() + .query() + .await? + .iter() + .any(|event| event.0.blockHash == appchain_block_hash_to_prove), + Duration::from_secs(10 * 60) + ); + + // topic 3 of the L2ToL1Tx event is the withdrawal position + let withdrawal_position: u64 = + U256::from_be_bytes(receipt.logs()[1].clone().topics()[3].into()) + .try_into() + .unwrap(); + + // finish the withdrawal on the settlement chain + execute_withdrawal(ExecuteWithdrawalParams { + to_address, + withdrawal_value, + appchain_block_hash_to_prove, + bridge_address: components.appchain_deployment.bridge, + settlement_provider: &components.settlement_provider, + appchain_provider: &components.appchain_provider, + l2_sender: components.appchain_provider.default_signer_address(), + send_root_size: 2, + withdrawal_position, + }) + .await; + + // Assert new balance is equal to withdrawal amount + let balance_after = components.settlement_provider.get_balance(to_address).await?; + assert_eq!(balance_after, withdrawal_value); + + */ + + // Testing l1 block number HARDFORK below + // - progress timestamp past L1_BLOCK_NUM_HARDFORK_TS // - sequence a tx // - assert l1_block_num on the rollup matches the expected value // - make a new withdrawal // - assert the new withdrawal passes + // + // TODO rm all debug prints + println!("POTATO"); - - // Progress L1 timestamp to be after L1_BLOCK_NUM_HARDFORK_TS + // Progress L1 timestamp to be after l1_block_num_hardfork_ts assert!( - l1_provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await? - .unwrap().header.timestamp < L1_BLOCK_NUM_HARDFORK_TS ); + l1_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .unwrap() + .header + .timestamp < + l1_block_num_hardfork_ts + ); - // Progress time to just after the hardfork - l1_provider - .evm_mine(Some(MineOptions::Options { - timestamp: Some(L1_BLOCK_NUM_HARDFORK_TS + 10), - blocks: Some(1), - })) - .await?; + //NOTE: settlement block height must be increased to be higher than seq block height, + //otherwise appchain nitro will think there was a reorg + //(this shouldn't be a problem in mainnet/testnet because the settlement chains used + //have a lot more activity) - // Get the current settlement chain block number to verify later - let settlement_block_before = components + let seq_block_height = components + .sequencing_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .number; + let settlement_block_height = components .settlement_provider .get_block_by_number(BlockNumberOrTag::Latest) .await? @@ -672,29 +702,94 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .header .number; - /// TODO breaking here... continue + // let's make it so the settlement block is 10 blocks ahead of sequencing chain + // let seq_blocks_to_mine = (seq_block_height + 10) - settlement_block_height; + // TODO should be less than 1000 blocks, but its currently failing to include the + // past-hardfork timestamp in the deposit settlement block otherwise, need to + // figure it out + let seq_blocks_to_mine = 1000u64; //(seq_block_height + 10) - settlement_block_height; + let nonce = components + .settlement_provider + .get_transaction_count(test_account1().address) + .await?; + for i in 0..seq_blocks_to_mine { + let tx = TransactionRequest::default() + .with_to(test_account1().address) + .with_value(U256::ZERO) + .with_nonce(nonce + i) + .with_gas_limit(21_000) + .with_max_fee_per_gas(100_000_000) + .with_max_priority_fee_per_gas(0); + + let _ = components.settlement_provider.send_transaction(tx).await.unwrap(); + } + + let l1_block_pre_fork = + l1_provider.get_block_by_number(BlockNumberOrTag::Latest).await?.unwrap(); + println!("TOMATO l1 pre-fork: {:?}", l1_block_pre_fork); - // Sequence a transaction - let tx_receipt = components - .sequence_tx(b"test_tx_after_hardfork", 0, true) - .await? - .unwrap(); + // Progress time to just after the hardfork + l1_provider + .evm_mine(Some(MineOptions::Timestamp(Some(l1_block_num_hardfork_ts + 10)))) + .await?; - // Get the appchain block and verify l1_block_number - let block: NitroBlock = components - .appchain_provider - .raw_request( - "eth_getBlockByHash".into(), - (tx_receipt.block_hash.unwrap(), false), + println!( + "TOMATO l1 post-fork: {:?}", + l1_provider.get_block_by_number(BlockNumberOrTag::Latest).await?.unwrap() + ); + + // make a new deposit so a new delayed message is included and the l1_block_number is + // updated + let receipt = inbox + .depositEth() + .value(parse_ether("1")?) + .nonce( + components + .settlement_provider + .get_transaction_count(test_account1().address) + .await?, ) - .await?; + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + assert!(receipt.status()); - // After hardfork, l1_block_number should come from settlement chain - assert!( - block.l1_block_number >= U256::from(settlement_block_before), - "l1_block_number should be from settlement chain after hardfork. Expected >= {}, got {}", - settlement_block_before, - block.l1_block_number + println!("TOMATO receipt_block_num: {:?}", receipt.block_number); + println!("TOMATO hardfork timestamp: {l1_block_num_hardfork_ts}"); + + wait_until!( + { + // Sequence dummy transactions until we see the deposit and the updated + // l1_block_number + let _ = components.sequence_tx(b"test_tx_after_hardfork", 0, false).await; + + let block: NitroBlock = components + .appchain_provider + .raw_request("eth_getBlockByNumber".into(), ("latest", false)) + .await + .unwrap(); + + println!( + "TOMATO nitro block: {block:?}, l1_block_number: {}", + block.l1_block_number + ); + + // TODO note that this check is not `==`. The appchain nitro will apply a few + // numbers on top of the reported l1_block_number. + // example: in a test run translator used l1_block_num 1102, but nitro decided + // to use 1109 + // + // Need to investigate if this can cause issues (like appchain assuming a reorg + // happened if in a given interval more sequencing blocks have been produced + // than settlement blocks) + + block.l1_block_number >= U256::from(receipt.block_number.unwrap()) + }, + Duration::from_secs(60), + Duration::from_millis(500) ); // Make a new withdrawal @@ -736,9 +831,8 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu let balance_after = components.settlement_provider.get_balance(to_address).await?; assert_eq!(balance_after, withdrawal_value); - // Final cleanup - proposer_instance.kill(); - enclave_server_instance.kill(); + // TODO mine a bunch of sequencing blocks (no settlement) + // assert nothing bad happens and l1_block_number is still fine Ok(()) }, From 46af77c46ae4d8725234b95b1076cb62c10d5188 Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Thu, 11 Dec 2025 09:57:10 +0000 Subject: [PATCH 7/8] WIP --- .../synd-enclave/enclave/server.go | 36 +++++--- .../synd-enclave/enclave/verify.go | 1 + .../tests/e2e/e2e_tests_withdrawals.rs | 86 ++++++++++++++----- 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/synd-withdrawals/synd-enclave/enclave/server.go b/synd-withdrawals/synd-enclave/enclave/server.go index fcb208dec..716cfc82b 100644 --- a/synd-withdrawals/synd-enclave/enclave/server.go +++ b/synd-withdrawals/synd-enclave/enclave/server.go @@ -413,9 +413,6 @@ func processMessage(msg []byte, blockNum uint64, ts uint64) ([]byte, error) { if _, ok := allowedMsgs[msg[0]]; !ok { return nil, fmt.Errorf("unexpected message: type %d", msg[0]) } - if ts >= L1_BLOCK_NUM_HARDFORK_TS { - blockNum = binary.BigEndian.Uint64(msg[33:41]) - } if msg[0] == arbostypes.L1MessageType_BatchPostingReport { requestId := msg[teetypes.DelayedMessageRequestIdOffset : teetypes.DelayedMessageRequestIdOffset+32] msg = make([]byte, teetypes.DelayedMessageDataOffset) @@ -485,24 +482,37 @@ func parseAppBatches(input *teetypes.VerifyAppchainInput) ([][]byte, error) { msgCount := uint64(len(input.DelayedMessages)) var i uint64 var batches [][]byte - blockNum := input.VerifySequencingChainOutput.SequencingBlockNumber - uint64(len(input.VerifySequencingChainOutput.Batches)) + seqBlockNum := input.VerifySequencingChainOutput.SequencingBlockNumber - uint64(len(input.VerifySequencingChainOutput.Batches)) for _, batch := range input.VerifySequencingChainOutput.Batches { - blockNum++ - var hasDelayedMessage bool + seqBlockNum++ + delayedMsgsInBatch := make([]uint64, 0, len(input.DelayedMessages)+1) + // collect all the delayed messages in the batch for i < msgCount { timestamp := binary.BigEndian.Uint64(input.DelayedMessages[i][teetypes.DelayedMessageTimestampOffset : teetypes.DelayedMessageTimestampOffset+8]) if timestamp+input.Config.SettlementDelay > batch.Timestamp { break } - var err error - input.DelayedMessages[i], err = processMessage(input.DelayedMessages[i], blockNum, batch.Timestamp) - if err != nil { - return nil, fmt.Errorf("failed to process delayed message: %w", err) - } + delayedMsgsInBatch = append(delayedMsgsInBatch, i) i++ - hasDelayedMessage = true } - if hasDelayedMessage || len(batch.Data) > 0 { + + if len(delayedMsgsInBatch) > 0 || len(batch.Data) > 0 { + blockNum := uint64(0) + if batch.Timestamp < getL1BlockNumHardforkTS() { + blockNum = seqBlockNum + } else if len(delayedMsgsInBatch) > 0 { + // settlement block number of the last delayed msg in the batch + lastDelayedMsgIndex := delayedMsgsInBatch[len(delayedMsgsInBatch)-1] + blockNum = binary.BigEndian.Uint64(input.DelayedMessages[lastDelayedMsgIndex][teetypes.DelayedMessageBlockNumberOffset : teetypes.DelayedMessageBlockNumberOffset+8]) + } + for _, delayedMsgIdx := range delayedMsgsInBatch { + var err error + input.DelayedMessages[delayedMsgIdx], err = processMessage(input.DelayedMessages[delayedMsgIdx], blockNum, batch.Timestamp) + if err != nil { + return nil, fmt.Errorf("failed to process delayed message: %w", err) + } + + } batches = append(batches, buildArbBatch(startIndex+i, batch.Data)) } batch.Data = nil diff --git a/synd-withdrawals/synd-enclave/enclave/verify.go b/synd-withdrawals/synd-enclave/enclave/verify.go index da6a1fe70..491e73dad 100644 --- a/synd-withdrawals/synd-enclave/enclave/verify.go +++ b/synd-withdrawals/synd-enclave/enclave/verify.go @@ -77,6 +77,7 @@ const L1_BLOCK_NUM_HARDFORK_TS = 1767571200 func getL1BlockNumHardforkTS() uint64 { if val := os.Getenv("L1_BLOCK_NUM_HARDFORK_TS"); val != "" { if ts, err := strconv.ParseUint(val, 10, 64); err == nil { + log.Warn("L1_BLOCK_NUM_HARDFORK_TS override", "value", ts) return ts } } diff --git a/test-framework/tests/e2e/e2e_tests_withdrawals.rs b/test-framework/tests/e2e/e2e_tests_withdrawals.rs index 89744f13d..25ab89e74 100644 --- a/test-framework/tests/e2e/e2e_tests_withdrawals.rs +++ b/test-framework/tests/e2e/e2e_tests_withdrawals.rs @@ -694,7 +694,7 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .unwrap() .header .number; - let settlement_block_height = components + let set_block_height = components .settlement_provider .get_block_by_number(BlockNumberOrTag::Latest) .await? @@ -702,26 +702,24 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .header .number; - // let's make it so the settlement block is 10 blocks ahead of sequencing chain - // let seq_blocks_to_mine = (seq_block_height + 10) - settlement_block_height; - // TODO should be less than 1000 blocks, but its currently failing to include the - // past-hardfork timestamp in the deposit settlement block otherwise, need to - // figure it out - let seq_blocks_to_mine = 1000u64; //(seq_block_height + 10) - settlement_block_height; - let nonce = components - .settlement_provider - .get_transaction_count(test_account1().address) - .await?; - for i in 0..seq_blocks_to_mine { - let tx = TransactionRequest::default() - .with_to(test_account1().address) - .with_value(U256::ZERO) - .with_nonce(nonce + i) - .with_gas_limit(21_000) - .with_max_fee_per_gas(100_000_000) - .with_max_priority_fee_per_gas(0); - - let _ = components.settlement_provider.send_transaction(tx).await.unwrap(); + // let's make it so the settlement block is at least 10 blocks ahead of sequencing chain + if set_block_height < seq_block_height + 10 { + let seq_blocks_to_mine = (seq_block_height + 10) - set_block_height; + let nonce = components + .settlement_provider + .get_transaction_count(test_account1().address) + .await?; + for i in 0..seq_blocks_to_mine { + let tx = TransactionRequest::default() + .with_to(test_account1().address) + .with_value(U256::ZERO) + .with_nonce(nonce + i) + .with_gas_limit(21_000) + .with_max_fee_per_gas(100_000_000) + .with_max_priority_fee_per_gas(0); + + let _ = components.settlement_provider.send_transaction(tx).await.unwrap(); + } } let l1_block_pre_fork = @@ -738,6 +736,49 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu l1_provider.get_block_by_number(BlockNumberOrTag::Latest).await?.unwrap() ); + // keep mining blocks until the timestamp increase is seen on the base chains + wait_until!( + { + let tx = TransactionRequest::default() + .with_to(test_account1().address) + .with_value(U256::ZERO) + .with_nonce( + components + .settlement_provider + .get_transaction_count(test_account1().address) + .await?, + ) + .with_gas_limit(21_000) + .with_max_fee_per_gas(100_000_000) + .with_max_priority_fee_per_gas(0); + + let _ = components.settlement_provider.send_transaction(tx).await.unwrap(); + components.sequence_tx(b"dummy_tx", 0, false).await?; + + let set_ts = components + .settlement_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .timestamp; + let seq_ts = components + .sequencing_provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .timestamp; + + set_ts >= l1_block_num_hardfork_ts && seq_ts >= l1_block_num_hardfork_ts + }, + Duration::from_secs(60), + Duration::from_millis(500) + ); + print!("TOMATO, base chains ready"); + // make a new deposit so a new delayed message is included and the l1_block_number is // updated let receipt = inbox @@ -810,7 +851,8 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .await? .iter() .any(|event| event.0.blockHash == appchain_block_hash_to_prove), - Duration::from_secs(20 * 60) + Duration::from_secs(20 * 60), + Duration::from_millis(500) ); // Execute the withdrawal From 3c050d7ba918dd461db9a533bbb4606a60b8281f Mon Sep 17 00:00:00 2001 From: Jorge Silva Date: Fri, 12 Dec 2025 20:55:00 +0000 Subject: [PATCH 8/8] WIP --- shared/test-utils/src/docker.rs | 39 ++++------- synd-mchain/src/client.rs | 4 +- synd-mchain/src/db.rs | 40 ++++------- synd-mchain/src/methods/common.rs | 2 +- synd-mchain/src/server.rs | 2 +- .../crates/synd-slotter/src/slotter.rs | 42 ++++++++--- .../tests/tests/integration_tests.rs | 25 ++++--- .../synd-enclave/enclave/accumulator.go | 17 ++--- .../synd-enclave/enclave/server.go | 35 +++++----- .../synd-enclave/enclave/verify.go | 55 +++++++++------ .../synd-enclave/enclave/wavmio/wavm.go | 9 +-- .../synd-enclave/teetypes/types.go | 13 ++-- synd-withdrawals/synd-proposer/pkg/helpers.go | 70 ++++++++++--------- .../synd-proposer/pkg/proposer.go | 3 +- .../tests/e2e/e2e_tests_withdrawals.rs | 18 +++-- 15 files changed, 211 insertions(+), 163 deletions(-) diff --git a/shared/test-utils/src/docker.rs b/shared/test-utils/src/docker.rs index 711d310a3..770a87342 100644 --- a/shared/test-utils/src/docker.rs +++ b/shared/test-utils/src/docker.rs @@ -162,19 +162,14 @@ pub async fn start_component( tag = Ok("dev".to_string()); } let mut docker = if let Ok(tag) = tag { - E2EProcess::new( - Command::new("docker") - .envs(env_vars) - .arg("run") - .arg("--init") - .arg("--rm") - .arg("--net=host") - .arg(format!( - "ghcr.io/syndicateprotocol/syndicate-appchains/{executable_name}:{tag}" - )) - .args(args), - executable_name, - ) + let mut cmd = Command::new("docker"); + cmd.arg("run").arg("--init").arg("--rm").arg("--net=host"); + for (key, value) in &env_vars { + cmd.arg("-e").arg(format!("{key}={value}")); + } + cmd.arg(format!("ghcr.io/syndicateprotocol/syndicate-appchains/{executable_name}:{tag}")) + .args(args); + E2EProcess::new(&mut cmd, executable_name) } else { let mut cmd = Command::new("cargo"); // ring has a custom build.rs script that rebuilds whenever certain environment @@ -511,17 +506,13 @@ pub async fn launch_enclave_server( } let port = PortManager::instance().next_port().await; - let docker = E2EProcess::new( - Command::new("docker") - .envs(env_vars) - .arg("run") - .arg("--init") - .arg("--rm") - .arg("-p") - .arg(format!("{port}:1234")) - .arg(image_name), - "enclave-server", - )?; + let mut cmd = Command::new("docker"); + cmd.arg("run").arg("--init").arg("--rm"); + for (key, value) in env_vars { + cmd.arg("-e").arg(format!("{key}={value}")); + } + cmd.arg("-p").arg(format!("{port}:1234")).arg(image_name); + let docker = E2EProcess::new(&mut cmd, "enclave-server")?; let enclave_rpc_url = format!("http://localhost:{port}"); diff --git a/synd-mchain/src/client.rs b/synd-mchain/src/client.rs index 222b2152c..be41c7168 100644 --- a/synd-mchain/src/client.rs +++ b/synd-mchain/src/client.rs @@ -501,7 +501,7 @@ mod tests { assert_eq!(mchain.get_block_number().await, 2); mchain .add_batch(&MBlock { - payload: Some(ArbitrumBatch::new(Default::default(), vec![empty.clone()])), + payload: Some(ArbitrumBatch::new(Default::default(), vec![empty.clone()], 0)), slot: Slot { seq_block_number: 2, ..Default::default() }, timestamp: 0, }) @@ -510,7 +510,7 @@ mod tests { assert_eq!(mchain.get_block_number().await, 3); mchain .add_batch(&MBlock { - payload: Some(ArbitrumBatch::new(Default::default(), vec![empty; 2])), + payload: Some(ArbitrumBatch::new(Default::default(), vec![empty; 2], 0)), slot: Slot { seq_block_number: 3, ..Default::default() }, timestamp: 0, }) diff --git a/synd-mchain/src/db.rs b/synd-mchain/src/db.rs index cb32cd46b..f0d1b660f 100644 --- a/synd-mchain/src/db.rs +++ b/synd-mchain/src/db.rs @@ -64,12 +64,18 @@ pub struct ArbitrumBatch { pub batch_data: Bytes, /// The delayed messages included in this batch pub delayed_messages: Vec, + /// the l1 block number + pub l1_block_number: u64, } impl ArbitrumBatch { /// Creates a new [`ArbitrumBatch`] - pub const fn new(batch_data: Bytes, delayed_messages: Vec) -> Self { - Self { batch_data, delayed_messages } + pub const fn new( + batch_data: Bytes, + delayed_messages: Vec, + l1_block_number: u64, + ) -> Self { + Self { batch_data, delayed_messages, l1_block_number } } } @@ -108,19 +114,9 @@ pub struct Block { pub before_message_count: u64, /// reorg data pub slot: Slot, -} - -/// timestamp for the hardfork where L1 block number changes from being derived from the seq chain -/// to the set chain -/// 5 Jan 2026 -pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1767571200; - -/// gets the timestamp for the `l1_block_number` hardfork (supports env var override for testing -/// purposes) -#[allow(clippy::expect_used)] -pub fn get_l1_block_num_hardfork_ts() -> u64 { - env::var("L1_BLOCK_NUM_HARDFORK_TS") - .map_or(L1_BLOCK_NUM_HARDFORK_TS, |val| val.parse().expect("invalid timestamp provided")) + // TODO this is a breaking chain, need to update MCHAIN version + /// the l1 block number + pub l1_block_number: u64, } impl Block { @@ -132,15 +128,6 @@ impl Block { pub const fn after_message_count(&self) -> u64 { self.before_message_count + self.messages.len() as u64 } - - /// l1 block number for this mchain block - pub fn l1_block_number(&self) -> u64 { - if self.timestamp < get_l1_block_num_hardfork_ts() { - self.slot.seq_block_number - } else { - self.slot.set_block_number - } - } } /// `rocksdb` implements the key-value trait @@ -369,6 +356,7 @@ pub trait ArbitrumDB { }; let batch = arbitrum_batch.batch_data; let messages = arbitrum_batch.delayed_messages; + let l1_block_number = arbitrum_batch.l1_block_number; let mut block = Block { timestamp: mblock.timestamp, @@ -379,16 +367,16 @@ pub trait ArbitrumDB { before_message_acc: state.message_acc, messages: messages.iter().map(|x| (x.to_owned(), FixedBytes::ZERO)).collect(), after_batch_acc: Default::default(), + l1_block_number, }; let mut inbox_acc = block.before_message_acc; let offset = self.get_migration_offset(); - let l1_block_number = block.l1_block_number(); for (i, (msg, acc)) in block.messages.iter_mut().enumerate() { let message_hash = keccak256( ( [msg.kind], msg.sender, - l1_block_number, + block.l1_block_number, mblock.timestamp, U256::from(block.before_message_count + i as u64), msg.base_fee_l1, diff --git a/synd-mchain/src/methods/common.rs b/synd-mchain/src/methods/common.rs index 1b701aaf6..feea00fab 100644 --- a/synd-mchain/src/methods/common.rs +++ b/synd-mchain/src/methods/common.rs @@ -42,7 +42,7 @@ pub fn create_header(batch_count: u64, offset: u64, block: &Block) -> alloy::rpc base_fee_per_gas: Some(1), extra_data: FixedBytes::<32>::ZERO.into(), #[allow(clippy::unwrap_used)] - mix_hash: U256::from(block.l1_block_number()) + mix_hash: U256::from(block.l1_block_number) .checked_shl(64) .unwrap() .checked_add(U256::from(1)) diff --git a/synd-mchain/src/server.rs b/synd-mchain/src/server.rs index 580f5ac0f..cdfe21127 100644 --- a/synd-mchain/src/server.rs +++ b/synd-mchain/src/server.rs @@ -57,7 +57,7 @@ pub fn start_mchain( let mut pending_ts: VecDeque = Default::default(); let mut finalized_batch_count = 1u64; if db.get_state().batch_count == 0 { - let batch = ArbitrumBatch::new(EMPTY_BATCH, vec![init_msg]); + let batch = ArbitrumBatch::new(EMPTY_BATCH, vec![init_msg], 0); db.add_batch(MBlock { payload: Some(batch), ..Default::default() }).unwrap(); if let Some(migration_params) = migration_params { info!("applying migration: {migration_params:?}"); diff --git a/synd-translator/crates/synd-slotter/src/slotter.rs b/synd-translator/crates/synd-slotter/src/slotter.rs index 397ec7e70..d2c1b1867 100644 --- a/synd-translator/crates/synd-slotter/src/slotter.rs +++ b/synd-translator/crates/synd-slotter/src/slotter.rs @@ -1,14 +1,14 @@ //! Slotter module for `synd-translator` use crate::{batch::build_batch, metrics::SlotterMetrics}; -use alloy::primitives::FixedBytes; +use alloy::{hex, primitives::FixedBytes}; use common::types::{Chain, SequencingBlock, SettlementBlock}; use shared::tracing::SpanKind; use synd_block_builder::appchains::shared::RollupAdapter; use synd_chain_ingestor::client::BlockStreamT; use synd_mchain::{ client::MchainProvider, - db::{get_l1_block_num_hardfork_ts, ArbitrumBatch, MBlock, Slot}, + db::{ArbitrumBatch, MBlock, Slot}, }; use thiserror::Error; use tracing::{debug, info, instrument, trace}; @@ -66,14 +66,21 @@ pub async fn run( let mut set_blocks_per_slot: u64 = 1; let slot_end_ts = seq_block.block_ref.timestamp.saturating_sub(settlement_delay); - // TODO is it okay to be 0 when there are no delayed msgs? - let mut set_block_num_in_slot = 0u64; + let mut last_delayed_msg_consumed_set_block_num = 0u64; while set_block.block_ref.timestamp <= slot_end_ts { set_blocks_per_slot += 1; if !set_block.messages.is_empty() { - delayed_msgs.append(&mut set_block.messages); - set_block_num_in_slot = set_block.block_ref.number; + delayed_msgs.append(&mut set_block.messages.clone()); + // TODO change this so it reads from the message itself + println!( + "TOMATO took set_num from this msg: {}", + hex::encode( + #[allow(clippy::unwrap_used)] + set_block.messages.last().unwrap().data.as_ref() + ) + ); + last_delayed_msg_consumed_set_block_num = set_block.block_ref.number; } set_block = settlement @@ -88,7 +95,12 @@ pub async fn run( let l1_block_number = if timestamp < get_l1_block_num_hardfork_ts() { seq_block.block_ref.number } else { - set_block_num_in_slot + if last_delayed_msg_consumed_set_block_num != 0 { + info!( + "TOMATO slotter l1_bloc_num after fork {last_delayed_msg_consumed_set_block_num}, hardfork_ts: {}", get_l1_block_num_hardfork_ts() + ); + } + last_delayed_msg_consumed_set_block_num }; debug!("using l1_block_number: {l1_block_number}"); @@ -96,7 +108,8 @@ pub async fn run( build_batch(&seq_block, &rollup_adapter, l1_block_number, timestamp)?; if tx_count > 0 || !delayed_msgs.is_empty() { - mblock.payload = Some(ArbitrumBatch::new(sequenced_batch, delayed_msgs)); + mblock.payload = + Some(ArbitrumBatch::new(sequenced_batch, delayed_msgs, l1_block_number)); } mblock.slot.set_block_hash = set_block.block_ref.hash; mblock.slot.set_block_number = set_block.block_ref.number; @@ -123,6 +136,19 @@ pub async fn run( } } +/// timestamp for the hardfork where L1 block number changes from being derived from the seq chain +/// to the set chain +/// 5 Jan 2026 +pub const L1_BLOCK_NUM_HARDFORK_TS: u64 = 1767571200; + +/// gets the timestamp for the `l1_block_number` hardfork (supports env var override for testing +/// purposes) +#[allow(clippy::expect_used)] +pub fn get_l1_block_num_hardfork_ts() -> u64 { + std::env::var("L1_BLOCK_NUM_HARDFORK_TS") + .map_or(L1_BLOCK_NUM_HARDFORK_TS, |val| val.parse().expect("invalid timestamp provided")) +} + /// Slotter Errors #[derive(Debug, Error, PartialEq, Eq)] pub enum SlotterError { diff --git a/synd-translator/tests/tests/integration_tests.rs b/synd-translator/tests/tests/integration_tests.rs index 50e7a6e8d..7d2af1539 100644 --- a/synd-translator/tests/tests/integration_tests.rs +++ b/synd-translator/tests/tests/integration_tests.rs @@ -55,7 +55,8 @@ fn deposit_eth(src: Address, dest: Address, value: U256) -> DelayedMessage { async fn arb_owner_test() -> Result<()> { // Start the appchain's node let appchain_owner = address!("0x0000000000000000000000000000000000000001"); - let (mchain_url, _mchain, _) = start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + let (mchain_url, _mchain, _) = + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, chain_owner: appchain_owner, @@ -84,7 +85,7 @@ async fn no_l1_fees_test() -> Result<()> { const ARB_GAS_INFO_CONTRACT_ADDRESS: Address = address!("0x000000000000000000000000000000000000006c"); let (mchain_url, _mchain, mchain) = - start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, chain_owner: Address::ZERO, @@ -114,6 +115,7 @@ async fn no_l1_fees_test() -> Result<()> { payload: Some(ArbitrumBatch::new( arbitrum::batch::Batch(vec![arbitrum::batch::BatchMessage::Delayed]).encode()?, vec![msg.clone()], + 0, )), timestamp: 100, slot: Slot { seq_block_number: 1, ..Default::default() }, @@ -124,6 +126,7 @@ async fn no_l1_fees_test() -> Result<()> { payload: Some(ArbitrumBatch::new( arbitrum::batch::Batch(vec![arbitrum::batch::BatchMessage::Delayed]).encode()?, vec![msg], + 0, )), timestamp: 200, slot: Slot { seq_block_number: 2, ..Default::default() }, @@ -140,7 +143,7 @@ async fn no_l1_fees_test() -> Result<()> { #[tokio::test] async fn test_nitro_batch() -> Result<()> { let (mchain_url, _mchain, mchain) = - start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, @@ -168,6 +171,7 @@ async fn test_nitro_batch() -> Result<()> { payload: Some(ArbitrumBatch::new( arbitrum::batch::Batch(vec![arbitrum::batch::BatchMessage::Delayed]).encode()?, vec![deposit_eth(Address::ZERO, addr, parse_ether("1")?)], + 0, )), timestamp: 0, slot: Slot { seq_block_number: 1, ..Default::default() }, @@ -200,7 +204,7 @@ async fn test_nitro_batch() -> Result<()> { )]); mchain .add_batch(&MBlock { - payload: Some(ArbitrumBatch::new(batch.encode()?, Default::default())), + payload: Some(ArbitrumBatch::new(batch.encode()?, Default::default(), 0)), slot: Slot { seq_block_number: 2, ..Default::default() }, ..Default::default() }) @@ -224,7 +228,7 @@ async fn test_nitro_batch() -> Result<()> { #[tokio::test] async fn test_nitro_batch_two_tx() -> Result<()> { let (mchain_url, _mchain, mchain) = - start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, chain_owner: Address::ZERO, @@ -250,6 +254,7 @@ async fn test_nitro_batch_two_tx() -> Result<()> { payload: Some(ArbitrumBatch::new( arbitrum::batch::Batch(vec![arbitrum::batch::BatchMessage::Delayed]).encode()?, vec![deposit_eth(Address::ZERO, addr, parse_ether("1")?)], + 0, )), timestamp: 0, slot: Slot { seq_block_number: 1, ..Default::default() }, @@ -300,7 +305,7 @@ async fn test_nitro_batch_two_tx() -> Result<()> { )]); mchain .add_batch(&MBlock { - payload: Some(ArbitrumBatch::new(batch.encode()?, Default::default())), + payload: Some(ArbitrumBatch::new(batch.encode()?, Default::default(), 0)), slot: Slot { seq_block_number: 2, ..Default::default() }, timestamp: 0, }) @@ -324,7 +329,7 @@ async fn test_nitro_batch_two_tx() -> Result<()> { #[tokio::test] async fn test_nitro_end_of_block_tx() -> Result<()> { let (mchain_url, _mchain, mchain) = - start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, chain_owner: Address::ZERO, @@ -356,6 +361,7 @@ async fn test_nitro_end_of_block_tx() -> Result<()> { }; 3 ], + 0, )), timestamp: 0, slot: Slot { seq_block_number: 1, ..Default::default() }, @@ -369,7 +375,7 @@ async fn test_nitro_end_of_block_tx() -> Result<()> { #[tokio::test] async fn test_nitro_delayed_message_after_batch() -> Result<()> { let (mchain_url, _mchain, mchain) = - start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None).await?; + start_mchain(APPCHAIN_CHAIN_ID, 0, None, None, None, None).await?; let chain_info = launch_nitro_node(NitroNodeArgs { chain_id: APPCHAIN_CHAIN_ID, chain_owner: Address::ZERO, @@ -395,6 +401,7 @@ async fn test_nitro_delayed_message_after_batch() -> Result<()> { payload: Some(ArbitrumBatch::new( arbitrum::batch::Batch(vec![]).encode()?, vec![msg.clone()], + 0, )), timestamp: 0, slot: Slot { seq_block_number: 1, ..Default::default() }, @@ -422,7 +429,7 @@ async fn test_nitro_delayed_message_after_batch() -> Result<()> { let msg: DelayedMessage = deposit_eth(Address::ZERO, TEST_ADDR, U256::from(1)); mchain .add_batch(&MBlock { - payload: Some(ArbitrumBatch::new(batch.encode()?, vec![msg])), + payload: Some(ArbitrumBatch::new(batch.encode()?, vec![msg], 0)), timestamp: 0, slot: Slot { seq_block_number: 2, ..Default::default() }, }) diff --git a/synd-withdrawals/synd-enclave/enclave/accumulator.go b/synd-withdrawals/synd-enclave/enclave/accumulator.go index fdde2bbf2..4cd4013ba 100644 --- a/synd-withdrawals/synd-enclave/enclave/accumulator.go +++ b/synd-withdrawals/synd-enclave/enclave/accumulator.go @@ -161,9 +161,9 @@ func buildBatch(txs [][]byte, l1BlockNum uint64, l1BlockTimestamp uint64) ([]byt } type SyndicateAccumulator struct { - Address common.Address - Batches []teetypes.SyndicateBatch - BlockNum uint64 + Address common.Address + Batches []teetypes.SyndicateBatch + SeqBlockNum uint64 } var TransactionProcessedEvent abi.Event @@ -176,11 +176,11 @@ func init() { TransactionProcessedEvent = abi.Events["TransactionProcessed"] } -func (s *SyndicateAccumulator) ProcessBlock(seqBlock *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error { - if s.BlockNum > 0 && s.BlockNum+1 != seqBlock.NumberU64() { +func (s *SyndicateAccumulator) ProcessBlock(block *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error { + if s.SeqBlockNum > 0 && s.SeqBlockNum+1 != block.NumberU64() { return errors.New("unexpected block number") } - s.BlockNum = seqBlock.NumberU64() + s.SeqBlockNum = block.NumberU64() var txs [][]byte for _, receipt := range receipts { for _, log := range receipt.Logs { @@ -209,8 +209,9 @@ func (s *SyndicateAccumulator) ProcessBlock(seqBlock *types.Block, receipts type } } s.Batches = append(s.Batches, teetypes.SyndicateBatch{ - Timestamp: seqBlock.Time(), - Data: data, + Timestamp: block.Time(), + Data: data, + L1BlockNumber: l1BlockNum, }) return nil } diff --git a/synd-withdrawals/synd-enclave/enclave/server.go b/synd-withdrawals/synd-enclave/enclave/server.go index 716cfc82b..6f7d70c98 100644 --- a/synd-withdrawals/synd-enclave/enclave/server.go +++ b/synd-withdrawals/synd-enclave/enclave/server.go @@ -382,7 +382,7 @@ func (s *Server) VerifySequencingChain(ctx context.Context, input teetypes.Verif Messages: input.DelayedMessages, } - data, err = Verify(ctx, blockVerifierInput, &acc) + data, err = Verify(ctx, blockVerifierInput, &acc, false) if err != nil { return nil, fmt.Errorf("failed to verify sequencing chain: %w", err) } @@ -391,7 +391,7 @@ func (s *Server) VerifySequencingChain(ctx context.Context, input teetypes.Verif output := teetypes.VerifySequencingChainOutput{ L1BatchAcc: l1BatchAcc, SequencingBlockHash: data.BlockHash, - SequencingBlockNumber: acc.BlockNum, + SequencingBlockNumber: acc.SeqBlockNum, Batches: acc.Batches, Signature: []byte{}, } @@ -409,7 +409,7 @@ var allowedMsgs = map[byte]struct{}{ arbostypes.L1MessageType_BatchPostingReport: {}, } -func processMessage(msg []byte, blockNum uint64, ts uint64) ([]byte, error) { +func processMessage(msg []byte, l1BlockNum uint64, ts uint64) ([]byte, error) { if _, ok := allowedMsgs[msg[0]]; !ok { return nil, fmt.Errorf("unexpected message: type %d", msg[0]) } @@ -419,7 +419,7 @@ func processMessage(msg []byte, blockNum uint64, ts uint64) ([]byte, error) { copy(msg[teetypes.DelayedMessageRequestIdOffset:teetypes.DelayedMessageRequestIdOffset+32], requestId) msg[0] = arbostypes.L1MessageType_EndOfBlock } - binary.BigEndian.PutUint64(msg[33:41], blockNum) + binary.BigEndian.PutUint64(msg[33:41], l1BlockNum) binary.BigEndian.PutUint64(msg[41:49], ts) return msg, nil } @@ -482,9 +482,7 @@ func parseAppBatches(input *teetypes.VerifyAppchainInput) ([][]byte, error) { msgCount := uint64(len(input.DelayedMessages)) var i uint64 var batches [][]byte - seqBlockNum := input.VerifySequencingChainOutput.SequencingBlockNumber - uint64(len(input.VerifySequencingChainOutput.Batches)) for _, batch := range input.VerifySequencingChainOutput.Batches { - seqBlockNum++ delayedMsgsInBatch := make([]uint64, 0, len(input.DelayedMessages)+1) // collect all the delayed messages in the batch for i < msgCount { @@ -497,17 +495,21 @@ func parseAppBatches(input *teetypes.VerifyAppchainInput) ([][]byte, error) { } if len(delayedMsgsInBatch) > 0 || len(batch.Data) > 0 { - blockNum := uint64(0) + // Compute l1BlockNum based on hardfork logic to match the slotter + var l1BlockNum uint64 if batch.Timestamp < getL1BlockNumHardforkTS() { - blockNum = seqBlockNum + // Pre-hardfork: use sequencing block number from batch + l1BlockNum = batch.L1BlockNumber } else if len(delayedMsgsInBatch) > 0 { - // settlement block number of the last delayed msg in the batch + // Post-hardfork: use settlement block number of last delayed message in batch lastDelayedMsgIndex := delayedMsgsInBatch[len(delayedMsgsInBatch)-1] - blockNum = binary.BigEndian.Uint64(input.DelayedMessages[lastDelayedMsgIndex][teetypes.DelayedMessageBlockNumberOffset : teetypes.DelayedMessageBlockNumberOffset+8]) + l1BlockNum = input.DelayedMessagesBlockNumbers[lastDelayedMsgIndex] } + // l1BlockNum stays 0 if post-hardfork with no delayed messages + log.Info("TOMATO parseAppBatches", "batch.Timestamp", batch.Timestamp, "len(delayedMsgsInBatch)", len(delayedMsgsInBatch), "l1BlockNum", l1BlockNum) for _, delayedMsgIdx := range delayedMsgsInBatch { var err error - input.DelayedMessages[delayedMsgIdx], err = processMessage(input.DelayedMessages[delayedMsgIdx], blockNum, batch.Timestamp) + input.DelayedMessages[delayedMsgIdx], err = processMessage(input.DelayedMessages[delayedMsgIdx], l1BlockNum, batch.Timestamp) if err != nil { return nil, fmt.Errorf("failed to process delayed message: %w", err) } @@ -539,12 +541,13 @@ func (s *Server) VerifyAppchain(ctx context.Context, input teetypes.VerifyAppcha } if len(batches) > 0 { blockVerifierInput := wavmio.ValidationInput{ - BlockHash: input.TrustedInput.AppStartBlockHash, - PreimageData: input.PreimageData, - Batches: batches, - Messages: input.DelayedMessages, + BlockHash: input.TrustedInput.AppStartBlockHash, + PreimageData: input.PreimageData, + Batches: batches, + Messages: input.DelayedMessages, + MessagesBlockNum: input.DelayedMessagesBlockNumbers, } - result, err = Verify(ctx, blockVerifierInput, nil) + result, err = Verify(ctx, blockVerifierInput, nil, true) if err != nil { return nil, err } diff --git a/synd-withdrawals/synd-enclave/enclave/verify.go b/synd-withdrawals/synd-enclave/enclave/verify.go index 491e73dad..32d1e0eb5 100644 --- a/synd-withdrawals/synd-enclave/enclave/verify.go +++ b/synd-withdrawals/synd-enclave/enclave/verify.go @@ -6,7 +6,6 @@ package enclave import ( "bytes" "context" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -14,6 +13,7 @@ import ( "strconv" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" @@ -26,7 +26,6 @@ import ( "github.com/ethereum/go-ethereum/triedb" "github.com/SyndicateProtocol/synd-appchains/synd-enclave/enclave/wavmio" - "github.com/SyndicateProtocol/synd-appchains/synd-enclave/teetypes" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" @@ -88,8 +87,9 @@ func Verify( ctx context.Context, data wavmio.ValidationInput, processor interface { - ProcessBlock(seqBlock *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error + ProcessBlock(block *types.Block, receipts types.Receipts, l1BlockNum uint64, timestamp uint64) error }, + appchain bool, ) (_ *execution.MessageResult, err error) { if data.BlockHash == (common.Hash{}) { return nil, errors.New("genesis block verification unsupported") @@ -115,6 +115,10 @@ func Verify( db := state.NewDatabase(triedb.NewDatabase(rawdb.WrapDatabaseWithWasm(rawdb.NewDatabase(&PreimageDb{wavm: wavm, memDb: memorydb.New()}), memorydb.New()), nil), nil) + // TODO it seems arbitrum uses the nonce in the block header to indicate the number of delayed messages read. this is a bit obscure, should find docs that lay this out + startDelayedMessagesRead := header.Nonce.Uint64() + prevDelayedMessagesRead := startDelayedMessagesRead + for wavm.GetInboxPosition() < batchCount { if err = ctx.Err(); err != nil { return nil, err @@ -171,22 +175,22 @@ func Verify( chainContext := wavmio.WavmChainContext{ChainConfig: chainConfig, Wavm: wavm} - seq_block, receipts, err := arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, header, statedb, chainContext, false, core.NewMessageRecordingContext([]rawdb.WasmTarget{rawdb.LocalTarget()})) + block, receipts, err := arbos.ProduceBlock(message.Message, message.DelayedMessagesRead, header, statedb, chainContext, false, core.NewMessageRecordingContext([]rawdb.WasmTarget{rawdb.LocalTarget()})) if err != nil { return nil, err } - if seq_block.NumberU64() != header.Number.Uint64()+1 { - return nil, fmt.Errorf("unexpected block number: got %d, expected %d", seq_block.NumberU64(), header.Number.Uint64()+1) + if block.NumberU64() != header.Number.Uint64()+1 { + return nil, fmt.Errorf("unexpected block number: got %d, expected %d", block.NumberU64(), header.Number.Uint64()+1) } - header = seq_block.Header() + header = block.Header() bytes, err := rlp.EncodeToBytes(header) if err != nil { return nil, fmt.Errorf("error RLP encoding header: %v", err) } wavm.Preimages[arbutil.Keccak256PreimageType][crypto.Keccak256Hash(bytes)] = bytes - result, err := statedb.Commit(seq_block.NumberU64(), true, false) + result, err := statedb.Commit(block.NumberU64(), true, false) if err != nil { return nil, err } @@ -194,25 +198,36 @@ func Verify( return nil, fmt.Errorf("bad commit root hash expected %v, got %v", header.Root, result) } - // NOTE: l1BlockNum hardfork logic must match slotter.rs + // NOTE: l1BlockNum hardfork logic must match slotter.rs and server.go:parseAppBatches l1BlockNum := uint64(0) - if seq_block.Time() < getL1BlockNumHardforkTS() { - l1BlockNum = seq_block.NumberU64() + if !appchain { + l1BlockNum = block.NumberU64() } else { - // Get settlement block number from latest delayed message if available - if len(data.Messages) > 0 { - lastMsg := data.Messages[len(data.Messages)-1] - if len(lastMsg) < teetypes.DelayedMessageBlockNumberOffset+8 { - return nil, errors.New("delayed message too short to contain block number") + // apply the l1_block_number hardfork logic only to derive the apchain + hardforkTS := getL1BlockNumHardforkTS() + log.Info("TOMATO verify.go hardfork check", "seq_block.Time()", block.Time(), "hardforkTS", hardforkTS, "seq_block.NumberU64()", block.NumberU64(), "message.DelayedMessagesRead", message.DelayedMessagesRead, "prevDelayedMessagesRead", prevDelayedMessagesRead, "startDelayedMessagesRead", startDelayedMessagesRead, "len(data.Messages)", len(data.Messages)) + if block.Time() < hardforkTS { + l1BlockNum = block.NumberU64() + log.Info("TOMATO verify.go pre-hardfork", "l1BlockNum", l1BlockNum) + } else { + // Get settlement block number from the last delayed message consumed in THIS block + // Only if this block consumed new delayed messages (DelayedMessagesRead increased) + if message.DelayedMessagesRead > prevDelayedMessagesRead && len(data.Messages) > 0 { + lastMsgIdx := message.DelayedMessagesRead - 1 - startDelayedMessagesRead + if lastMsgIdx < uint64(len(data.Messages)) { + lastMsg := data.Messages[lastMsgIdx] + l1BlockNum = data.MessagesBlockNum[lastMsgIdx] + log.Info("TOMATO verify.go post-hardfork got l1_bloc_num", "l1BlockNum", l1BlockNum, "delayedMsgsRead", message.DelayedMessagesRead, "delayedMsg", hexutil.Encode(lastMsg)) + } + } else { + log.Info("TOMATO verify.go post-hardfork NO delayed messages for this block") } - l1BlockNum = binary.BigEndian.Uint64( - lastMsg[teetypes.DelayedMessageBlockNumberOffset : teetypes.DelayedMessageBlockNumberOffset+8], - ) } + prevDelayedMessagesRead = message.DelayedMessagesRead } if processor != nil { - if err := processor.ProcessBlock(seq_block, receipts, l1BlockNum, seq_block.Time()); err != nil { + if err := processor.ProcessBlock(block, receipts, l1BlockNum, block.Time()); err != nil { return nil, err } } diff --git a/synd-withdrawals/synd-enclave/enclave/wavmio/wavm.go b/synd-withdrawals/synd-enclave/enclave/wavmio/wavm.go index 9590a76cd..3c6e69674 100644 --- a/synd-withdrawals/synd-enclave/enclave/wavmio/wavm.go +++ b/synd-withdrawals/synd-enclave/enclave/wavmio/wavm.go @@ -62,10 +62,11 @@ func (w *Wavm) GetBlockHeaderByHash(hash common.Hash) (*types.Header, error) { } type ValidationInput struct { - BlockHash common.Hash - PreimageData map[arbutil.PreimageType][][]byte - Batches [][]byte - Messages [][]byte + BlockHash common.Hash + PreimageData map[arbutil.PreimageType][][]byte + Batches [][]byte + Messages [][]byte + MessagesBlockNum []uint64 } func New(data ValidationInput) (*Wavm, error) { diff --git a/synd-withdrawals/synd-enclave/teetypes/types.go b/synd-withdrawals/synd-enclave/teetypes/types.go index a419263df..183cb6aa2 100644 --- a/synd-withdrawals/synd-enclave/teetypes/types.go +++ b/synd-withdrawals/synd-enclave/teetypes/types.go @@ -78,9 +78,11 @@ type VerifySequencingChainInput struct { } type VerifyAppchainInput struct { - TrustedInput TrustedInput - Config Config - DelayedMessages [][]byte + TrustedInput TrustedInput + Config Config + DelayedMessages [][]byte + DelayedMessagesBlockNumbers []uint64 + // get this from the first delayed message event, based on AppStartBlock.Nonce() StartDelayedMessagesAccumulator common.Hash VerifySequencingChainOutput VerifySequencingChainOutput @@ -102,8 +104,9 @@ func (c *Config) Hash() common.Hash { } type SyndicateBatch struct { - Timestamp uint64 - Data []byte + Timestamp uint64 + Data []byte + L1BlockNumber uint64 } type VerifySequencingChainOutput struct { diff --git a/synd-withdrawals/synd-proposer/pkg/helpers.go b/synd-withdrawals/synd-proposer/pkg/helpers.go index 69161e688..b578c81a8 100644 --- a/synd-withdrawals/synd-proposer/pkg/helpers.go +++ b/synd-withdrawals/synd-proposer/pkg/helpers.go @@ -228,14 +228,14 @@ func GetDelayedMessages( start uint64, endAcc common.Hash, settlesToArbitrumRollup bool, -) (common.Hash, [][]byte, bool, error) { +) (common.Hash, [][]byte, []uint64, bool, error) { endBlock, err := c.BlockNumber(ctx) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to get block number") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to get block number") } acc, end, err := GetMessageAcc(ctx, c, bridge, 0) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to get message account data") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to get message account data") } if acc != endAcc { @@ -248,14 +248,14 @@ func GetDelayedMessages( [][]common.Hash{{messageDeliveredEventHash}, nil, {endAcc}}, 1) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } if len(logs) != 1 { - return common.Hash{}, nil, false, fmt.Errorf("unexpected number of logs found: got %d, expected 1", len(logs)) + return common.Hash{}, nil, nil, false, fmt.Errorf("unexpected number of logs found: got %d, expected 1", len(logs)) } end = logs[0].Topics[1].Big().Uint64() if end == 0 { - return common.Hash{}, nil, false, errors.New("unexpected message index 0 found") + return common.Hash{}, nil, nil, false, errors.New("unexpected message index 0 found") } end-- endBlock = logs[0].BlockNumber @@ -268,7 +268,7 @@ func GetDelayedMessages( dummy = true } else { if start > end { - return common.Hash{}, nil, false, fmt.Errorf("start message %d is after end %d", start, end) + return common.Hash{}, nil, nil, false, fmt.Errorf("start message %d is after end %d", start, end) } if start < end { indexes = append(indexes, common.BigToHash(big.NewInt(int64(start)))) @@ -283,21 +283,21 @@ func GetDelayedMessages( [][]common.Hash{{messageDeliveredEventHash}, indexes}, uint64(len(indexes))) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } if len(logs) != len(indexes) { - return common.Hash{}, nil, false, + return common.Hash{}, nil, nil, false, fmt.Errorf("unexpected number of logs found: got %d, expected %d", len(logs), len(indexes)) } ibridge, err := bridgegen.NewBridge(bridge, c) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } seqInbox, err := ibridge.SequencerInbox(&bind.CallOpts{Context: ctx}) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } addrs := []common.Address{bridge, seqInbox} @@ -310,13 +310,13 @@ func GetDelayedMessages( break } - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } addrs = append(addrs, inbox) i++ } if i == 0 { - return common.Hash{}, nil, false, errors.New("no inbox addresses found") + return common.Hash{}, nil, nil, false, errors.New("no inbox addresses found") } logs, err = getLogs(ctx, c, logs[0].BlockNumber, logs[len(logs)-1].BlockNumber, @@ -325,7 +325,7 @@ func GetDelayedMessages( {messageDeliveredEventHash, inboxMessageDeliveredEventHash, inboxMessageDeliveredFromOriginEventHash}, }, 0) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } if len(logs)%2 != 0 { @@ -333,20 +333,21 @@ func GetDelayedMessages( fmt.Println("LOG: ", log.Topics[0], log.TxHash, log.Address, log.Topics[1].Big().Uint64()) } - return common.Hash{}, nil, false, fmt.Errorf("even number of logs expected: got %d", len(logs)) + return common.Hash{}, nil, nil, false, fmt.Errorf("even number of logs expected: got %d", len(logs)) } iinbox, err := bridgegen.NewIDelayedMessageProvider(common.Address{}, c) if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } var msgs [][]byte + var msgsBlockNumbers []uint64 var prevAcc *common.Hash for i := 0; i < len(logs); i += 2 { msgDeliveredLog, err := ibridge.ParseMessageDelivered(logs[i]) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to parse message delivered log") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to parse message delivered log") } inboxLog := logs[i+1] @@ -358,7 +359,7 @@ func GetDelayedMessages( case inboxMessageDeliveredEventHash: dataLog, err := iinbox.ParseInboxMessageDelivered(inboxLog) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to parse message delivered log") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to parse message delivered log") } logData = dataLog.Data logBlockNum = dataLog.Raw.BlockNumber @@ -367,37 +368,37 @@ func GetDelayedMessages( case inboxMessageDeliveredFromOriginEventHash: dataLog, err := iinbox.ParseInboxMessageDeliveredFromOrigin(inboxLog) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to parse message delivered from origin log") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to parse message delivered from origin log") } // fetch the tx from the event tx, _, err := c.TransactionByHash(ctx, inboxLog.TxHash) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, fmt.Sprintf("failed to get tx by hash %s", inboxLog.TxHash.String())) + return common.Hash{}, nil, nil, false, errors.Wrap(err, fmt.Sprintf("failed to get tx by hash %s", inboxLog.TxHash.String())) } if len(tx.Data()) < 4 { - return common.Hash{}, nil, false, errors.New("tx data too short") + return common.Hash{}, nil, nil, false, errors.New("tx data too short") } if l2MessageFromOriginCallSelector.Cmp(common.BytesToHash(tx.Data()[:4])) != 0 { - return common.Hash{}, nil, false, errors.New("invalid function selector") + return common.Hash{}, nil, nil, false, errors.New("invalid function selector") } args := make(map[string]interface{}) err = l2MessageFromOriginCallABI.Inputs.UnpackIntoMap(args, tx.Data()[4:]) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to parse inputs of sendL2MessageFromOrigin") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to parse inputs of sendL2MessageFromOrigin") } var ok bool logData, ok = args["messageData"].([]byte) if !ok { - return common.Hash{}, nil, false, errors.New("failed to cast messageData to []byte") + return common.Hash{}, nil, nil, false, errors.New("failed to cast messageData to []byte") } logBlockNum = dataLog.Raw.BlockNumber logMsgIndex = dataLog.MessageNum } if msgDeliveredLog.MessageIndex.Cmp(logMsgIndex) != 0 { - return common.Hash{}, nil, false, errors.New("event log msg index mismatch") + return common.Hash{}, nil, nil, false, errors.New("event log msg index mismatch") } if msgDeliveredLog.Raw.BlockNumber != logBlockNum { - return common.Hash{}, nil, false, errors.New("event log block number mismatch") + return common.Hash{}, nil, nil, false, errors.New("event log block number mismatch") } // skip events prior to the start one if msgDeliveredLog.MessageIndex.Cmp(big.NewInt(int64(start))) != 0 { @@ -429,29 +430,32 @@ func GetDelayedMessages( if settlesToArbitrumRollup { block, err := c.BlockByHash(ctx, msgDeliveredLog.Raw.BlockHash) if err != nil { - return common.Hash{}, nil, false, errors.Wrap(err, "failed to get block by hash") + return common.Hash{}, nil, nil, false, errors.Wrap(err, "failed to get block by hash") } - // TODO revisit this - // Override the block number with the L1 block number - // It is used during contract execution in nitro rollups + // this is necessary to match the on-chain accumulator. + // because nitro rollups solidity contracts get the l1_block_num as the return value of `block.number`, + // we need this data patch in order for the block numbers used to calculate the accumulators to match l1BlockNum := types.DeserializeHeaderExtraInformation(block.Header()).L1BlockNumber + log.Info("TOMATO helpers.go", "msgIndex", msgDeliveredLog.MessageIndex, "settlementBlockNum", block.NumberU64(), + "l1BlockNum", l1BlockNum, "originalBlockNum", msg.Header.BlockNumber) msg.Header.BlockNumber = l1BlockNum } data, err := msg.Serialize() if err != nil { - return common.Hash{}, nil, false, err + return common.Hash{}, nil, nil, false, err } + msgsBlockNumbers = append(msgsBlockNumbers, msgDeliveredLog.Raw.BlockNumber) msgs = append(msgs, data) } if start != end+1 || prevAcc == nil { - return common.Hash{}, nil, false, fmt.Errorf("missing message: got %d, expected %d", start, end+1) + return common.Hash{}, nil, nil, false, fmt.Errorf("missing message: got %d, expected %d", start, end+1) } - return *prevAcc, msgs, dummy, nil + return *prevAcc, msgs, msgsBlockNumbers, dummy, nil } func getNumBatches(batches []teetypes.SyndicateBatch, dmsgs [][]byte, setDelay uint64) uint64 { diff --git a/synd-withdrawals/synd-proposer/pkg/proposer.go b/synd-withdrawals/synd-proposer/pkg/proposer.go index 0e05441c9..b05034719 100644 --- a/synd-withdrawals/synd-proposer/pkg/proposer.go +++ b/synd-withdrawals/synd-proposer/pkg/proposer.go @@ -475,7 +475,7 @@ func (p *Proposer) Prove( } // get delayed messages - startAcc, msgs, isDummy, err := GetDelayedMessages( + startAcc, msgs, msgsBlockNumbers, isDummy, err := GetDelayedMessages( ctx, p.SettlementClient, p.Config.AppchainBridgeAddress, @@ -509,6 +509,7 @@ func (p *Proposer) Prove( TrustedInput: *trustedInput, Config: p.Config.EnclaveConfig, DelayedMessages: msgs, + DelayedMessagesBlockNumbers: msgsBlockNumbers, StartDelayedMessagesAccumulator: startAcc, VerifySequencingChainOutput: seqOutput, AppStartBlockHeader: *header, diff --git a/test-framework/tests/e2e/e2e_tests_withdrawals.rs b/test-framework/tests/e2e/e2e_tests_withdrawals.rs index 25ab89e74..77d6531b5 100644 --- a/test-framework/tests/e2e/e2e_tests_withdrawals.rs +++ b/test-framework/tests/e2e/e2e_tests_withdrawals.rs @@ -28,7 +28,6 @@ use std::{ fmt::Debug, time::{Duration, SystemTime}, }; -use synd_mchain::db::L1_BLOCK_NUM_HARDFORK_TS; use test_framework::components::{ configuration::{BaseChainsType, ConfigurationOptions}, proposer::ProposerConfig, @@ -777,7 +776,7 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu Duration::from_secs(60), Duration::from_millis(500) ); - print!("TOMATO, base chains ready"); + println!("TOMATO, base chains ready"); // make a new deposit so a new delayed message is included and the l1_block_number is // updated @@ -798,7 +797,7 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .unwrap(); assert!(receipt.status()); - println!("TOMATO receipt_block_num: {:?}", receipt.block_number); + println!("TOMATO receipt: {:?}", receipt); println!("TOMATO hardfork timestamp: {l1_block_num_hardfork_ts}"); wait_until!( @@ -814,8 +813,8 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu .unwrap(); println!( - "TOMATO nitro block: {block:?}, l1_block_number: {}", - block.l1_block_number + "TOMATO nitro: #{} {}, l1_bloc_num: {}", + block.number, block.hash, block.l1_block_number ); // TODO note that this check is not `==`. The appchain nitro will apply a few @@ -827,6 +826,15 @@ async fn e2e_tee_withdrawal_basic_flow(base_chains_type: BaseChainsType) -> Resu // happened if in a given interval more sequencing blocks have been produced // than settlement blocks) + if block.l1_block_number >= U256::from(receipt.block_number.unwrap()) { + let set_block: NitroBlock = components + .settlement_provider + .raw_request("eth_getBlockByNumber".into(), ("latest", false)) + .await + .unwrap(); + println!("TOMATO set block: {set_block:?}") + } + block.l1_block_number >= U256::from(receipt.block_number.unwrap()) }, Duration::from_secs(60),