diff --git a/crates/bundle/src/call/ty.rs b/crates/bundle/src/call/ty.rs index 7074c11..23fc108 100644 --- a/crates/bundle/src/call/ty.rs +++ b/crates/bundle/src/call/ty.rs @@ -350,4 +350,131 @@ mod test { assert_eq!(resp, deserialized); } + + /// Generate test vectors for TypeScript SDK. + /// + /// Run with: `cargo t -p signet-bundle -- --ignored --nocapture` + #[test] + #[ignore] + fn generate_call_bundle_vectors() { + let vectors = vec![ + ( + "minimal", + SignetCallBundle { + bundle: EthCallBundle { + txs: vec![b"\x02\xf8test_tx_1".into()], + block_number: 12345678, + state_block_number: BlockNumberOrTag::Number(12345677), + ..Default::default() + }, + }, + ), + ( + "with_overrides", + SignetCallBundle { + bundle: EthCallBundle { + txs: vec![b"\x02\xf8test_tx_1".into()], + block_number: 12345678, + state_block_number: BlockNumberOrTag::Number(12345677), + timestamp: Some(1700000000), + gas_limit: Some(30000000), + base_fee: Some(1000000000), + ..Default::default() + }, + }, + ), + ( + "with_coinbase", + SignetCallBundle { + bundle: EthCallBundle { + txs: vec![b"\x02\xf8test_tx_1".into()], + block_number: 12345678, + state_block_number: BlockNumberOrTag::Latest, + coinbase: Some(Address::repeat_byte(0x42)), + timeout: Some(5), + ..Default::default() + }, + }, + ), + ]; + + let output: Vec<_> = vectors + .into_iter() + .map(|(name, bundle)| { + serde_json::json!({ + "name": name, + "bundle": bundle, + }) + }) + .collect(); + + println!("// SignetCallBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap()); + + // Also generate response vectors + let response_vectors = vec![ + ( + "minimal_response", + SignetCallBundleResponse::from(EthCallBundleResponse { + bundle_hash: B256::repeat_byte(0xaa), + bundle_gas_price: U256::from(1000000000u64), + coinbase_diff: U256::from(100000000000000u64), + eth_sent_to_coinbase: U256::from(50000000000000u64), + gas_fees: U256::from(50000000000000u64), + results: vec![EthCallBundleTransactionResult { + coinbase_diff: U256::from(100000000000000u64), + eth_sent_to_coinbase: U256::from(50000000000000u64), + from_address: Address::repeat_byte(0x11), + gas_fees: U256::from(50000000000000u64), + gas_price: U256::from(1000000000u64), + gas_used: 21000, + to_address: Some(Address::repeat_byte(0x22)), + tx_hash: B256::repeat_byte(0xbb), + value: Some(Bytes::from(b"result_data")), + revert: None, + }], + state_block_number: 12345677, + total_gas_used: 21000, + }), + ), + ( + "reverted_response", + SignetCallBundleResponse::from(EthCallBundleResponse { + bundle_hash: B256::repeat_byte(0xcc), + bundle_gas_price: U256::from(1000000000u64), + coinbase_diff: U256::from(0u64), + eth_sent_to_coinbase: U256::from(0u64), + gas_fees: U256::from(21000000000000u64), + results: vec![EthCallBundleTransactionResult { + coinbase_diff: U256::from(0u64), + eth_sent_to_coinbase: U256::from(0u64), + from_address: Address::repeat_byte(0x33), + gas_fees: U256::from(21000000000000u64), + gas_price: U256::from(1000000000u64), + gas_used: 21000, + to_address: Some(Address::repeat_byte(0x44)), + tx_hash: B256::repeat_byte(0xdd), + value: None, + revert: Some(Bytes::from(b"execution reverted")), + }], + state_block_number: 12345677, + total_gas_used: 21000, + }), + ), + ]; + + let response_output: Vec<_> = response_vectors + .into_iter() + .map(|(name, resp)| { + serde_json::json!({ + "name": name, + "response": resp, + }) + }) + .collect(); + + println!( + "\n// SignetCallBundleResponse vectors\n{}", + serde_json::to_string_pretty(&response_output).unwrap() + ); + } } diff --git a/crates/bundle/src/send/bundle.rs b/crates/bundle/src/send/bundle.rs index bef4a86..ecd2879 100644 --- a/crates/bundle/src/send/bundle.rs +++ b/crates/bundle/src/send/bundle.rs @@ -283,4 +283,105 @@ mod test { assert!(deserialized.host_txs.is_empty()); } + + /// Generate test vectors for TypeScript SDK. + /// + /// Run with: `cargo t -p signet-bundle -- --ignored --nocapture` + #[test] + #[ignore] + fn generate_eth_bundle_vectors() { + use alloy::primitives::Address; + + let vectors = vec![ + ( + "minimal", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8test_tx_1".into()], + block_number: 12345678, + ..Default::default() + }, + vec![], + ), + ), + ( + "with_timestamps", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8test_tx_1".into()], + block_number: 12345678, + min_timestamp: Some(1700000000), + max_timestamp: Some(1700003600), + ..Default::default() + }, + vec![], + ), + ), + ( + "with_reverting_hashes", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8test_tx_1".into(), b"\x02\xf8test_tx_2".into()], + block_number: 12345678, + reverting_tx_hashes: vec![B256::repeat_byte(0xab), B256::repeat_byte(0xcd)], + ..Default::default() + }, + vec![], + ), + ), + ( + "with_host_txs", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8rollup_tx".into()], + block_number: 12345678, + ..Default::default() + }, + vec![b"\x02\xf8host_tx_1".into(), b"\x02\xf8host_tx_2".into()], + ), + ), + ( + "full_bundle", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8tx_1".into(), b"\x02\xf8tx_2".into()], + block_number: 12345678, + min_timestamp: Some(1700000000), + max_timestamp: Some(1700003600), + reverting_tx_hashes: vec![B256::repeat_byte(0xef)], + dropping_tx_hashes: vec![B256::repeat_byte(0x11)], + refund_percent: Some(90), + refund_recipient: Some(Address::repeat_byte(0x22)), + refund_tx_hashes: vec![B256::repeat_byte(0x33)], + ..Default::default() + }, + vec![b"\x02\xf8host_tx".into()], + ), + ), + ( + "replacement_bundle", + SignetEthBundle::new( + EthSendBundle { + txs: vec![b"\x02\xf8replacement_tx".into()], + block_number: 12345678, + replacement_uuid: Some("550e8400-e29b-41d4-a716-446655440000".to_owned()), + ..Default::default() + }, + vec![], + ), + ), + ]; + + let output: Vec<_> = vectors + .into_iter() + .map(|(name, bundle)| { + serde_json::json!({ + "name": name, + "bundle": bundle, + }) + }) + .collect(); + + println!("// SignetEthBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap()); + } } diff --git a/crates/test-utils/tests/fill_behavior.rs b/crates/test-utils/tests/fill_behavior.rs new file mode 100644 index 0000000..bffd20e --- /dev/null +++ b/crates/test-utils/tests/fill_behavior.rs @@ -0,0 +1,618 @@ +//! Integration tests verifying fill-handling behavior across three drivers: +//! +//! 1. **Call Bundle** (`SignetBundleDriver`) → OUTPUT missing fills (no validation, just report) +//! 2. **Send Bundle** (`SignetEthBundleDriver`) → ERROR on missing fills (stop execution) +//! 3. **Block Driver** (`SignetDriver`) → DROP TXN on missing fills (reject tx, continue block) +//! +//! These tests use the same input scenario with varying fill states to verify each +//! driver behaves correctly. + +use alloy::{ + consensus::{constants::ETH_TO_WEI, Header, ReceiptEnvelope, TxEnvelope, TypedTransaction}, + eips::BlockNumberOrTag, + primitives::{keccak256, Address, U256}, + signers::local::PrivateKeySigner, + uint, +}; +use signet_bundle::{ + BundleInspector, SignetBundleDriver, SignetCallBundle, SignetEthBundle, SignetEthBundleDriver, + SignetEthBundleError, +}; +use signet_constants::test_utils::{HOST_CHAIN_ID, HOST_WBTC, HOST_WETH, RU_CHAIN_ID}; +use signet_constants::SignetSystemConstants; +use signet_evm::{EvmNeedsTx, SignetDriver}; +use signet_extract::{Extractable, ExtractedEvent, Extracts}; +use signet_test_utils::{ + chain::{fake_block, Chain, RU_ORDERS}, + evm::test_signet_evm_with_inspector, + specs::{sign_tx_with_key_pair, simple_bundle, simple_call, simple_send}, + users::{TEST_SIGNERS, TEST_USERS}, +}; +use signet_types::{ + primitives::{SealedHeader, TransactionSigned}, + AggregateFills, +}; +use signet_zenith::HostOrders::{initiateCall, Filled, Input, Output}; +use std::{borrow::Cow, sync::LazyLock, time::Duration}; +use tokio::time::Instant; +use trevm::BundleError; +use trevm::{ + inspectors::{Layered, TimeLimit}, + revm::{database::InMemoryDB, inspector::NoOpInspector}, + BundleDriver, NoopBlock, +}; + +// ============================================================================= +// Test Constants & Fixtures +// ============================================================================= + +static SENDER_WALLET: LazyLock<&PrivateKeySigner> = LazyLock::new(|| &TEST_SIGNERS[0]); +static ORDERER: LazyLock
= LazyLock::new(|| TEST_USERS[1]); +static ORDERER_WALLET: LazyLock<&PrivateKeySigner> = LazyLock::new(|| &TEST_SIGNERS[1]); + +/// Recipient for tx_0 (simple send before order) +const TX_0_RECIPIENT: Address = Address::repeat_byte(0x31); +/// Recipient for tx_2 (simple send after order) +const TX_2_RECIPIENT: Address = Address::repeat_byte(0x32); + +/// Input amount for the order (100 ETH in wei) +const INPUT_AMOUNT: U256 = uint!(100_000_000_000_000_000_000_U256); +/// Full output WBTC amount (100 units) +const OUTPUT_WBTC: U256 = uint!(100_U256); +/// Full output WETH amount (200 units) +const OUTPUT_WETH: U256 = uint!(200_U256); +/// Partial output WBTC amount (50 units - half of full) +const PARTIAL_WBTC: U256 = uint!(50_U256); +/// Partial output WETH amount (100 units - half of full) +const PARTIAL_WETH: U256 = uint!(100_U256); + +// ============================================================================= +// EVM Setup Functions +// ============================================================================= + +/// Create a host EVM for fill simulation (no inspector needed) +fn host_evm() -> EvmNeedsTx { + test_signet_evm_with_inspector(NoOpInspector).fill_block(&NoopBlock) +} + +/// Create a bundle EVM with time-limited inspector for send bundle tests +fn bundle_evm() -> EvmNeedsTx { + let inspector: BundleInspector<_> = + Layered::new(TimeLimit::new(Duration::from_secs(5)), NoOpInspector); + test_signet_evm_with_inspector(inspector).fill_block(&NoopBlock) +} + +/// Create a call bundle EVM with signet layered inspector +fn call_bundle_evm() -> signet_evm::EvmNeedsTx> { + let inspector = Layered::new(TimeLimit::new(Duration::from_secs(5)), NoOpInspector); + test_signet_evm_with_inspector(inspector).fill_block(&NoopBlock) +} + +// ============================================================================= +// Fill Fixtures +// ============================================================================= + +/// Create full fills that completely satisfy the order outputs +fn full_fills() -> Filled { + Filled { + outputs: vec![ + Output { + token: HOST_WBTC, + amount: OUTPUT_WBTC, + recipient: TX_0_RECIPIENT, + chainId: RU_CHAIN_ID as u32, + }, + Output { + token: HOST_WETH, + amount: OUTPUT_WETH, + recipient: TX_2_RECIPIENT, + chainId: RU_CHAIN_ID as u32, + }, + ], + } +} + +/// Create partial fills that only provide half of the required outputs +fn partial_fills() -> Filled { + Filled { + outputs: vec![ + Output { + token: HOST_WBTC, + amount: PARTIAL_WBTC, + recipient: TX_0_RECIPIENT, + chainId: RU_CHAIN_ID as u32, + }, + Output { + token: HOST_WETH, + amount: PARTIAL_WETH, + recipient: TX_2_RECIPIENT, + chainId: RU_CHAIN_ID as u32, + }, + ], + } +} + +/// Create aggregate fills from a Filled event +fn aggregate_from_filled(filled: &Filled) -> AggregateFills { + let mut agg = AggregateFills::new(); + agg.add_fill(HOST_CHAIN_ID, filled); + agg +} + +// ============================================================================= +// Transaction & Bundle Fixtures +// ============================================================================= + +/// Create an order transaction that requires fills +fn simple_order(nonce: u64) -> TypedTransaction { + simple_call( + RU_ORDERS, + &initiateCall { + deadline: U256::MAX, + inputs: vec![Input { token: Address::ZERO, amount: INPUT_AMOUNT }], + outputs: vec![ + Output { + token: HOST_WBTC, + amount: OUTPUT_WBTC, + recipient: TX_0_RECIPIENT, + chainId: HOST_CHAIN_ID as u32, + }, + Output { + token: HOST_WETH, + amount: OUTPUT_WETH, + recipient: TX_2_RECIPIENT, + chainId: HOST_CHAIN_ID as u32, + }, + ], + }, + INPUT_AMOUNT, + nonce, + RU_CHAIN_ID, + ) +} + +/// Create a test bundle with: +/// - tx_0: Simple ETH send to TX_0_RECIPIENT +/// - tx_1: Order transaction requiring fills +/// - tx_2: Simple ETH send to TX_2_RECIPIENT +fn order_bundle() -> SignetEthBundle { + let tx_0 = simple_send(TX_0_RECIPIENT, U256::ONE, 0, RU_CHAIN_ID); + let tx_1 = simple_order(0); + let tx_2 = simple_send(TX_2_RECIPIENT, U256::ONE, 1, RU_CHAIN_ID); + + let tx_0 = sign_tx_with_key_pair(&SENDER_WALLET, tx_0); + let tx_1 = sign_tx_with_key_pair(&ORDERER_WALLET, tx_1); + let tx_2 = sign_tx_with_key_pair(&SENDER_WALLET, tx_2); + + simple_bundle(vec![tx_0, tx_1, tx_2], vec![], 0) +} + +/// Create a SignetCallBundle from a SignetEthBundle for call bundle tests +fn to_call_bundle(bundle: &SignetEthBundle) -> SignetCallBundle { + SignetCallBundle { + bundle: alloy::rpc::types::mev::EthCallBundle { + txs: bundle.txs().to_vec(), + block_number: 0, + state_block_number: BlockNumberOrTag::Number(1), + timestamp: None, + gas_limit: None, + difficulty: None, + base_fee: None, + transaction_index: None, + coinbase: None, + timeout: None, + }, + } +} + +// ============================================================================= +// Call Bundle Tests +// ============================================================================= +// +// Call bundle (SignetBundleDriver) performs NO fill validation. +// It simply reports detected fills and orders in the response. + +mod call_bundle { + use super::*; + + /// Test that call bundle executes all transactions and reports detected orders. + /// + /// Call bundle (SignetBundleDriver) performs NO fill validation - it simply + /// reports detected orders in the response. This test verifies that behavior. + #[test] + fn reports_orders_without_validation() { + let trevm = call_bundle_evm(); + + let bundle = order_bundle(); + let call_bundle = to_call_bundle(&bundle); + + let mut driver = SignetBundleDriver::new(&call_bundle); + + // Run the bundle - should succeed regardless of fill state + let _trevm = driver.run_bundle(trevm).expect("call bundle should succeed"); + + let response = driver.into_response(); + + // Call bundle should have detected the order outputs + assert!(!response.orders.outputs.is_empty(), "call bundle should detect order outputs"); + + // All three transactions should have been executed + assert_eq!(response.results.len(), 3, "all transactions should execute in call bundle"); + } +} + +// ============================================================================= +// Send Bundle Tests +// ============================================================================= +// +// Send bundle (SignetEthBundleDriver) validates fills and ERRORS on missing fills. +// Transactions marked as revertible are dropped instead of causing errors. + +mod send_bundle { + use super::*; + + /// Test that send bundle succeeds when fills are complete. + #[test] + fn succeeds_with_valid_fills() { + let trevm = bundle_evm(); + let initial_balance = trevm.read_balance_ref(*ORDERER); + + // Set up complete fills + let filled = full_fills(); + let agg_fills = aggregate_from_filled(&filled); + + let bundle = order_bundle(); + let bundle = bundle.try_to_recovered().unwrap(); + + let mut driver = SignetEthBundleDriver::new_with_fill_state( + &bundle, + host_evm(), + Instant::now() + Duration::from_secs(5), + Cow::Owned(agg_fills), + ); + + // Should succeed with valid fills + let trevm = driver.run_bundle(trevm).expect("send bundle should succeed with valid fills"); + + // Verify all transactions executed + let post_balance = trevm.read_balance_ref(*ORDERER); + assert_eq!(trevm.read_balance_ref(TX_0_RECIPIENT), U256::ONE); + assert!(post_balance < initial_balance - INPUT_AMOUNT); + assert_eq!(trevm.read_balance_ref(TX_2_RECIPIENT), U256::ONE); + } + + /// Test that send bundle errors on partial fills (insufficient). + #[test] + fn errors_on_partial_fills() { + let trevm = bundle_evm(); + let initial_balance = trevm.read_balance_ref(*ORDERER); + + // Set up partial fills (insufficient) + let filled = partial_fills(); + let agg_fills = aggregate_from_filled(&filled); + + let bundle = order_bundle(); + let bundle = bundle.try_to_recovered().unwrap(); + + let mut driver = SignetEthBundleDriver::new_with_fill_state( + &bundle, + host_evm(), + Instant::now() + Duration::from_secs(5), + Cow::Owned(agg_fills), + ); + + // Should error due to insufficient fills + let (err, trevm) = + driver.run_bundle(trevm).expect_err("should error on partial fills").take_err(); + assert!( + matches!(err, SignetEthBundleError::Bundle(BundleError::BundleReverted)), + "expected BundleReverted error, got {:?}", + err + ); + + // tx_0 executed, tx_1 (order) failed validation, tx_2 not executed + assert_eq!(trevm.read_balance_ref(TX_0_RECIPIENT), U256::ONE); + assert_eq!(trevm.read_balance_ref(*ORDERER), initial_balance); + assert_eq!(trevm.read_balance_ref(TX_2_RECIPIENT), U256::ZERO); + } + + /// Test that send bundle errors when no fills are provided. + #[test] + fn errors_on_missing_fills() { + let trevm = bundle_evm(); + let initial_balance = trevm.read_balance_ref(*ORDERER); + + // No fills provided + let bundle = order_bundle(); + let bundle = bundle.try_to_recovered().unwrap(); + + let mut driver = SignetEthBundleDriver::new( + &bundle, + host_evm(), + Instant::now() + Duration::from_secs(5), + ); + + // Should error due to missing fills + let (err, trevm) = + driver.run_bundle(trevm).expect_err("should error on missing fills").take_err(); + assert!( + matches!(err, SignetEthBundleError::Bundle(BundleError::BundleReverted)), + "expected BundleReverted error, got {:?}", + err + ); + + // tx_0 executed, tx_1 (order) failed, tx_2 not executed + assert_eq!(trevm.read_balance_ref(TX_0_RECIPIENT), U256::ONE); + assert_eq!(trevm.read_balance_ref(*ORDERER), initial_balance); + assert_eq!(trevm.read_balance_ref(TX_2_RECIPIENT), U256::ZERO); + } + + /// Test that send bundle drops revertible tx and continues when fills are missing. + #[test] + fn drops_revertible_on_missing() { + let trevm = bundle_evm(); + let initial_balance = trevm.read_balance_ref(*ORDERER); + + let mut bundle = order_bundle(); + + // Mark the order transaction (tx_1) as revertible + let hash = keccak256(&bundle.txs()[1]); + bundle.bundle.reverting_tx_hashes.push(hash); + + let bundle = bundle.try_to_recovered().unwrap(); + let mut driver = SignetEthBundleDriver::new( + &bundle, + host_evm(), + Instant::now() + Duration::from_secs(5), + ); + + // Should succeed - order tx dropped but bundle continues + let trevm = driver.run_bundle(trevm).expect("should succeed when revertible tx dropped"); + + // tx_0 and tx_2 executed, tx_1 (order) was dropped + assert_eq!(trevm.read_balance_ref(TX_0_RECIPIENT), U256::ONE); + assert_eq!(trevm.read_balance_ref(*ORDERER), initial_balance); + assert_eq!(trevm.read_balance_ref(TX_2_RECIPIENT), U256::ONE); + } +} + +// ============================================================================= +// Block Driver Tests +// ============================================================================= +// +// Block driver (SignetDriver) validates fills and DROPS invalid transactions. +// The block continues processing after a dropped transaction. + +mod block_driver { + use super::*; + + /// Test environment for block driver tests + struct BlockDriverEnv { + wallets: Vec, + nonces: [u64; 10], + sequence: u64, + } + + impl BlockDriverEnv { + fn new() -> Self { + let wallets = (1..=10).map(signet_test_utils::specs::make_wallet).collect::>(); + Self { wallets, nonces: [0; 10], sequence: 1 } + } + + fn trevm(&self) -> signet_evm::EvmNeedsBlock { + let mut trevm = signet_test_utils::evm::test_signet_evm(); + for wallet in &self.wallets { + let address = wallet.address(); + // Need 1000 ETH to cover order value (100 ETH) plus gas fees + trevm.test_set_balance(address, U256::from(ETH_TO_WEI * 1000)); + } + trevm + } + + fn next_block(&mut self) -> signet_types::primitives::RecoveredBlock { + let block = fake_block(self.sequence); + self.sequence += 1; + block + } + + fn signed_simple_send(&mut self, from: usize, to: Address, amount: U256) -> TxEnvelope { + let wallet = &self.wallets[from]; + let tx = simple_send(to, amount, self.nonces[from], RU_CHAIN_ID); + let tx = sign_tx_with_key_pair(wallet, tx); + self.nonces[from] += 1; + tx + } + + fn signed_order(&mut self, from: usize) -> TxEnvelope { + let wallet = &self.wallets[from]; + let tx = simple_order(self.nonces[from]); + let tx = sign_tx_with_key_pair(wallet, tx); + self.nonces[from] += 1; + tx + } + + fn driver<'a, 'b, C: Extractable>( + &self, + extracts: &'a mut Extracts<'b, C>, + txns: Vec, + ) -> SignetDriver<'a, 'b, C> { + let header = Header { gas_limit: 30_000_000, ..Default::default() }; + SignetDriver::new( + extracts, + Default::default(), + txns.into(), + SealedHeader::new(header), + SignetSystemConstants::test(), + ) + } + } + + /// Create a fake transaction for use in extracts + fn fake_tx() -> TransactionSigned { + use alloy::{consensus::TxEip1559, signers::Signature}; + let tx = TxEip1559::default(); + let signature = Signature::test_signature(); + TransactionSigned::new_unhashed(tx.into(), signature) + } + + /// Test that block driver accepts all transactions when fills are valid. + #[test] + fn accepts_with_valid_fills() { + let mut ctx = BlockDriverEnv::new(); + let orderer = ctx.wallets[1].address(); + + // Create transactions: send, order, send + let tx_0 = ctx.signed_simple_send(0, TX_0_RECIPIENT, U256::from(100)); + let tx_1 = ctx.signed_order(1); // Uses wallet 1 (orderer) + let tx_2 = ctx.signed_simple_send(0, TX_2_RECIPIENT, U256::from(100)); + + // Set up the block with fills + let block = ctx.next_block(); + // Use Extracts::new with proper chain IDs so fills are keyed correctly + let mut extracts = Extracts::::new(HOST_CHAIN_ID, &block, RU_CHAIN_ID, 1); + + // Add a fill event to the extracts + let fake_tx = fake_tx(); + let fake_receipt = ReceiptEnvelope::Eip1559(Default::default()); + let filled = full_fills(); + + extracts.ingest_event(ExtractedEvent { + tx: &fake_tx, + receipt: &fake_receipt, + log_index: 0, + event: signet_extract::Events::Filled(signet_zenith::RollupOrders::Filled { + outputs: filled + .outputs + .iter() + .map(|o| signet_zenith::RollupOrders::Output { + token: o.token, + amount: o.amount, + recipient: o.recipient, + chainId: o.chainId, + }) + .collect(), + }), + }); + + let mut driver = ctx.driver( + &mut extracts, + vec![tx_0.clone().into(), tx_1.clone().into(), tx_2.clone().into()], + ); + + // Run the block + let mut trevm = ctx.trevm().drive_block(&mut driver).unwrap(); + let (sealed_block, receipts) = driver.finish(); + + // All transactions should be processed + assert_eq!( + sealed_block.block.body.transactions().count(), + 3, + "all 3 transactions should be in the block" + ); + assert_eq!(receipts.len(), 3, "should have 3 receipts"); + + // Verify balances + assert_eq!(trevm.read_balance(TX_0_RECIPIENT), U256::from(100)); + assert_eq!(trevm.read_balance(TX_2_RECIPIENT), U256::from(100)); + // Orderer's balance should have decreased (spent INPUT_AMOUNT + gas) + assert!(trevm.read_balance(orderer) < U256::from(ETH_TO_WEI * 1000) - INPUT_AMOUNT); + } + + /// Test that block driver drops order tx on partial fills but processes others. + #[test] + fn drops_tx_on_partial_fills() { + let mut ctx = BlockDriverEnv::new(); + let orderer = ctx.wallets[1].address(); + + // Create transactions: send, order, send + let tx_0 = ctx.signed_simple_send(0, TX_0_RECIPIENT, U256::from(100)); + let tx_1 = ctx.signed_order(1); + let tx_2 = ctx.signed_simple_send(0, TX_2_RECIPIENT, U256::from(100)); + + let block = ctx.next_block(); + // Use Extracts::new with proper chain IDs so fills are keyed correctly + let mut extracts = Extracts::::new(HOST_CHAIN_ID, &block, RU_CHAIN_ID, 1); + + // Add partial fills (insufficient) + let fake_tx = fake_tx(); + let fake_receipt = ReceiptEnvelope::Eip1559(Default::default()); + let filled = partial_fills(); + + extracts.ingest_event(ExtractedEvent { + tx: &fake_tx, + receipt: &fake_receipt, + log_index: 0, + event: signet_extract::Events::Filled(signet_zenith::RollupOrders::Filled { + outputs: filled + .outputs + .iter() + .map(|o| signet_zenith::RollupOrders::Output { + token: o.token, + amount: o.amount, + recipient: o.recipient, + chainId: o.chainId, + }) + .collect(), + }), + }); + + let mut driver = ctx.driver( + &mut extracts, + vec![tx_0.clone().into(), tx_1.clone().into(), tx_2.clone().into()], + ); + + // Run the block + let mut trevm = ctx.trevm().drive_block(&mut driver).unwrap(); + let (sealed_block, receipts) = driver.finish(); + + // Order tx should be dropped, other 2 should succeed + assert_eq!( + sealed_block.block.body.transactions().count(), + 2, + "order tx should be dropped, only 2 transactions in block" + ); + assert_eq!(receipts.len(), 2, "should have 2 receipts"); + + // tx_0 and tx_2 should have executed + assert_eq!(trevm.read_balance(TX_0_RECIPIENT), U256::from(100)); + assert_eq!(trevm.read_balance(TX_2_RECIPIENT), U256::from(100)); + // Orderer's balance should be unchanged (order tx was dropped) + assert_eq!(trevm.read_balance(orderer), U256::from(ETH_TO_WEI * 1000)); + } + + /// Test that block driver drops order tx when no fills are provided. + #[test] + fn drops_tx_on_missing_fills() { + let mut ctx = BlockDriverEnv::new(); + + // Create transactions: send, order, send + let tx_0 = ctx.signed_simple_send(0, TX_0_RECIPIENT, U256::from(100)); + let tx_1 = ctx.signed_order(1); + let tx_2 = ctx.signed_simple_send(0, TX_2_RECIPIENT, U256::from(100)); + + let block = ctx.next_block(); + // Use Extracts::new with proper chain IDs (no fills added - empty context) + let mut extracts = Extracts::::new(HOST_CHAIN_ID, &block, RU_CHAIN_ID, 1); + + let mut driver = ctx.driver( + &mut extracts, + vec![tx_0.clone().into(), tx_1.clone().into(), tx_2.clone().into()], + ); + + // Run the block + let mut trevm = ctx.trevm().drive_block(&mut driver).unwrap(); + let (sealed_block, receipts) = driver.finish(); + + // Order tx should be dropped, other 2 should succeed + assert_eq!( + sealed_block.block.body.transactions().count(), + 2, + "order tx should be dropped when no fills, only 2 transactions in block" + ); + assert_eq!(receipts.len(), 2, "should have 2 receipts"); + + // tx_0 and tx_2 should have executed + assert_eq!(trevm.read_balance(TX_0_RECIPIENT), U256::from(100)); + assert_eq!(trevm.read_balance(TX_2_RECIPIENT), U256::from(100)); + } +}