From 56f16ea6b94305a23d49efe186d78fe246766ee3 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 30 Jun 2025 16:25:48 -0400 Subject: [PATCH 01/24] "wip" --- crates/constants/src/chains/pecorino.rs | 9 +- crates/constants/src/chains/test_utils.rs | 9 +- crates/constants/src/lib.rs | 3 + crates/constants/src/types/host.rs | 5 + crates/constants/src/types/mod.rs | 6 + crates/constants/src/types/tokens.rs | 20 +- crates/evm/src/driver.rs | 364 ++++----------------- crates/evm/src/lib.rs | 9 +- crates/evm/src/macros.rs | 34 ++ crates/evm/src/{sys_log.rs => sys/logs.rs} | 45 +-- crates/evm/src/sys/mod.rs | 12 + crates/evm/src/sys/native.rs | 121 +++++++ crates/evm/src/sys/token.rs | 185 +++++++++++ crates/evm/src/sys/transact.rs | 143 ++++++++ crates/extract/src/extracted.rs | 59 +--- crates/test-utils/tests/evm.rs | 63 +++- 16 files changed, 679 insertions(+), 408 deletions(-) create mode 100644 crates/evm/src/macros.rs rename crates/evm/src/{sys_log.rs => sys/logs.rs} (56%) create mode 100644 crates/evm/src/sys/mod.rs create mode 100644 crates/evm/src/sys/native.rs create mode 100644 crates/evm/src/sys/token.rs create mode 100644 crates/evm/src/sys/transact.rs diff --git a/crates/constants/src/chains/pecorino.rs b/crates/constants/src/chains/pecorino.rs index 1ed22672..75121535 100644 --- a/crates/constants/src/chains/pecorino.rs +++ b/crates/constants/src/chains/pecorino.rs @@ -28,6 +28,8 @@ pub const HOST_USDC: Address = address!("0x885F8DB528dC8a38aA3DDad9D3F619746B4a6 pub const HOST_USDT: Address = address!("0x7970D259D4a96764Fa9B23FF0715A35f06f52D1A"); /// WBTC token for the Pecorino testnet host chain. pub const HOST_WBTC: Address = address!("0x9aeDED4224f3dD31aD8A0B1FcD05E2d7829283a7"); +/// WETH token for the Pecorino testnet host chain. +pub const HOST_WETH: Address = Address::ZERO; /// USDC token for the Pecorino testnet RU chain. pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e2"); @@ -35,6 +37,8 @@ pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e pub const RU_USDT: Address = address!("0xF34326d3521F1b07d1aa63729cB14A372f8A737C"); /// WBTC token for the Pecorino testnet RU chain. pub const RU_WBTC: Address = address!("0xE3d7066115f7d6b65F88Dff86288dB4756a7D733"); +/// WETH token for the Pecorino testnet RU chain. +pub const RU_WETH: Address = Address::ZERO; /// Name for the network. pub const RU_NAME: &str = "Pecorino"; @@ -50,10 +54,11 @@ pub const BASE_FEE_RECIPIENT: Address = address!("0xe0eDA3701D44511ce419344A4CeD /// Host system tokens for Pecorino. pub const HOST_TOKENS: PredeployTokens = - crate::PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC); + crate::PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC, HOST_WETH); /// RU system tokens for Pecorino. -pub const RU_TOKENS: PredeployTokens = crate::PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC); +pub const RU_TOKENS: PredeployTokens = + crate::PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC, HOST_WETH); /// The URL of the Transaction Cache endpoint. pub const TX_CACHE_URL: &str = "https://transactions.pecorino.signet.sh"; diff --git a/crates/constants/src/chains/test_utils.rs b/crates/constants/src/chains/test_utils.rs index 7a0554b4..866e22e1 100644 --- a/crates/constants/src/chains/test_utils.rs +++ b/crates/constants/src/chains/test_utils.rs @@ -31,6 +31,8 @@ pub const HOST_USDC: Address = Address::repeat_byte(0x55); pub const HOST_USDT: Address = Address::repeat_byte(0x66); /// Test address for predeployed WBTC pub const HOST_WBTC: Address = Address::repeat_byte(0x77); +/// Test address for predeployed WETH +pub const HOST_WETH: Address = Address::repeat_byte(0x88); /// Test address for predeployed USDC pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e2"); @@ -38,6 +40,8 @@ pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e pub const RU_USDT: Address = address!("0xF34326d3521F1b07d1aa63729cB14A372f8A737C"); /// Test address for predeployed WBTC pub const RU_WBTC: Address = address!("0xE3d7066115f7d6b65F88Dff86288dB4756a7D733"); +/// Test address for predeployed WETH +pub const RU_WETH: Address = Address::repeat_byte(0x99); /// Name for the network. pub const RU_NAME: &str = "Test Rollup"; @@ -51,10 +55,11 @@ pub const RU_PASSAGE: Address = address!("0xB043BdD3d91376A76078c361bb82496Fdb80 pub const BASE_FEE_RECIPIENT: Address = Address::repeat_byte(0xab); /// Host system tokens. -pub const HOST_TOKENS: PredeployTokens = PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC); +pub const HOST_TOKENS: PredeployTokens = + PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC, HOST_WETH); /// RU system tokens. -pub const RU_TOKENS: PredeployTokens = PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC); +pub const RU_TOKENS: PredeployTokens = PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC, RU_WETH); /// The URL of the Transaction Cache endpoint. pub const TX_CACHE_URL: &str = "localhost:8080/txcache"; diff --git a/crates/constants/src/lib.rs b/crates/constants/src/lib.rs index 322fe700..121fbfcf 100644 --- a/crates/constants/src/lib.rs +++ b/crates/constants/src/lib.rs @@ -27,3 +27,6 @@ pub use types::{ PredeployTokens, RollupConstants, SignetConstants, SignetEnvironmentConstants, SignetSystemConstants, MINTER_ADDRESS, }; + +/// Placeholder address for ETH. +pub const ETH_ADDRESS: alloy::primitives::Address = alloy::primitives::Address::repeat_byte(0xee); diff --git a/crates/constants/src/types/host.rs b/crates/constants/src/types/host.rs index 38332c0b..6c660d6c 100644 --- a/crates/constants/src/types/host.rs +++ b/crates/constants/src/types/host.rs @@ -124,6 +124,11 @@ impl HostConstants { pub const fn tokens(&self) -> PredeployTokens { self.tokens } + + /// Return true if the address is an approved USD token. + pub const fn is_usd(&self, address: Address) -> bool { + address.const_eq(&self.tokens.usdc()) + } } impl FromStr for HostConstants { diff --git a/crates/constants/src/types/mod.rs b/crates/constants/src/types/mod.rs index b12d837c..ef435b92 100644 --- a/crates/constants/src/types/mod.rs +++ b/crates/constants/src/types/mod.rs @@ -94,6 +94,12 @@ impl SignetSystemConstants { self.host.is_system_contract(address) } + /// True if the address is a host USD that can be used to mint rollup + /// native asset. + pub const fn is_host_usd(&self, address: Address) -> bool { + self.host.is_usd(address) + } + /// Get the Order contract address for the given chain id. pub const fn orders_for(&self, chain_id: u64) -> Option
{ if chain_id == self.host_chain_id() { diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index 762922e7..d3358aba 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -1,5 +1,7 @@ use alloy::primitives::Address; +use crate::ETH_ADDRESS; + /// Rollup pre-deploy tokens. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[non_exhaustive] @@ -10,6 +12,8 @@ pub enum PermissionedToken { Usdt, /// WBTC Wbtc, + /// ETH or WETH + Weth, } /// Rollup configuration pre-deploy tokens. @@ -22,12 +26,14 @@ pub struct PredeployTokens { usdt: Address, /// WBTC. wbtc: Address, + /// ETH or WETH + weth: Address, } impl PredeployTokens { /// Create a new pre-deploy tokens configuration. - pub const fn new(usdc: Address, usdt: Address, wbtc: Address) -> Self { - Self { usdc, usdt, wbtc } + pub const fn new(usdc: Address, usdt: Address, wbtc: Address, weth: Address) -> Self { + Self { usdc, usdt, wbtc, weth } } /// Get the hard-coded pecorino host tokens. @@ -60,6 +66,8 @@ impl PredeployTokens { Some(PermissionedToken::Usdt) } else if address.const_eq(&self.wbtc) { Some(PermissionedToken::Wbtc) + } else if address.const_eq(&self.weth) || address.const_eq(Ð_ADDRESS) { + Some(PermissionedToken::Weth) } else { None } @@ -73,6 +81,8 @@ impl PredeployTokens { Some(PermissionedToken::Usdt) } else if address == self.wbtc { Some(PermissionedToken::Wbtc) + } else if address == self.weth || address == ETH_ADDRESS { + Some(PermissionedToken::Weth) } else { None } @@ -94,6 +104,7 @@ impl PredeployTokens { PermissionedToken::Usdc => self.usdc, PermissionedToken::Usdt => self.usdt, PermissionedToken::Wbtc => self.wbtc, + PermissionedToken::Weth => self.weth, } } @@ -111,4 +122,9 @@ impl PredeployTokens { pub const fn wbtc(&self) -> Address { self.wbtc } + + /// Get the address of the WETH token. + pub const fn weth(&self) -> Address { + self.weth + } } diff --git a/crates/evm/src/driver.rs b/crates/evm/src/driver.rs index 3089fb75..02f10bb6 100644 --- a/crates/evm/src/driver.rs +++ b/crates/evm/src/driver.rs @@ -1,6 +1,7 @@ use crate::{ - orders::SignetInspector, BlockResult, EvmNeedsTx, EvmTransacted, ExecutionOutcome, RunTxResult, - SignetLayered, BASE_GAS, + orders::SignetInspector, + sys::{MintNative, MintToken, TransactSysLog}, + BlockResult, EvmNeedsTx, EvmTransacted, ExecutionOutcome, RunTxResult, SignetLayered, }; use alloy::{ consensus::{ @@ -16,7 +17,7 @@ use signet_types::{ primitives::{BlockBody, RecoveredBlock, SealedBlock, SealedHeader, TransactionSigned}, AggregateFills, MarketError, }; -use signet_zenith::{Passage, Transactor, MINTER_ADDRESS}; +use signet_zenith::{Transactor, MINTER_ADDRESS}; use std::collections::{HashSet, VecDeque}; use tracing::{debug, debug_span, warn}; use trevm::{ @@ -34,78 +35,8 @@ use trevm::{ trevm_try, BlockDriver, BlockOutput, Tx, }; -macro_rules! run_tx { - ($self:ident, $trevm:ident, $tx:expr, $sender:expr) => {{ - let trevm = $trevm.fill_tx($tx); - - let _guard = tracing::trace_span!("run_tx", block_env = ?trevm.block(), tx = ?$tx, tx_env = ?trevm.tx(), spec_id = ?trevm.spec_id()).entered(); - - match trevm.run() { - Ok(t) => { - tracing::debug!("evm executed successfully"); - ControlFlow::Keep(t) - }, - Err(e) => { - if e.is_transaction_error() { - tracing::debug!( - err = %e.as_transaction_error().unwrap(), - "Discarding outcome due to execution error" - ); - ControlFlow::Discard(e.discard_error()) - } else { - return Err(e.err_into()); - } - } - } - }}; -} - -macro_rules! run_tx_early_return { - ($self:ident, $trevm:ident, $tx:expr, $sender:expr) => { - match run_tx!($self, $trevm, $tx, $sender) { - ControlFlow::Discard(t) => return Ok(t), - ControlFlow::Keep(t) => t, - } - }; -} - -/// Shim to impl [`Tx`] for [`Passage::EnterToken`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct EnterTokenFiller<'a, 'b, R> { - /// The extracted event for the enter token event. - pub enter_token: &'a ExtractedEvent<'b, R, Passage::EnterToken>, - /// The nonce of the transaction. - pub nonce: u64, - /// The address of the token being minted. - pub token: Address, -} - -impl Tx for EnterTokenFiller<'_, '_, R> { - fn fill_tx_env(&self, tx_env: &mut TxEnv) { - self.enter_token.event.fill_tx_env(tx_env); - tx_env.kind = TransactTo::Call(self.token); - tx_env.nonce = self.nonce; - } -} - -/// Shim to impl [`Tx`] for [`Transactor::Transact`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TransactFiller<'a, 'b, R> { - /// The extracted event for the transact event. - pub transact: &'a ExtractedEvent<'b, R, Transactor::Transact>, - /// The nonce of the transaction. - pub nonce: u64, -} - -impl Tx for TransactFiller<'_, '_, R> { - fn fill_tx_env(&self, tx_env: &mut TxEnv) { - self.transact.event.fill_tx_env(tx_env); - tx_env.nonce = self.nonce; - } -} - /// Used internally to signal that the transaction should be discarded. -enum ControlFlow +pub(crate) enum ControlFlow where Db: Database + DatabaseCommit, Insp: Inspector>, @@ -265,7 +196,7 @@ impl Tx for FillShim<'_> { #[derive(Debug)] pub struct SignetDriver<'a, 'b, C: Extractable> { /// The block extracts. - extracts: &'a Extracts<'b, C>, + pub(crate) extracts: &'a Extracts<'b, C>, /// Parent rollup block. parent: SealedHeader, @@ -281,10 +212,10 @@ pub struct SignetDriver<'a, 'b, C: Extractable> { to_process: VecDeque, /// Transactions that have been processed. - processed: Vec, + pub(crate) processed: Vec, /// Receipts and senders. - output: BlockOutput, + pub(crate) output: BlockOutput, /// Payable gas used in the block. payable_gas_used: u64, @@ -414,18 +345,6 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { } } - /// Make a receipt for an enter. - fn make_enter_receipt( - &self, - enter: &ExtractedEvent<'_, C::Receipt, Passage::Enter>, - ) -> alloy::consensus::Receipt { - let cumulative_gas_used = self.cumulative_gas_used().saturating_add(BASE_GAS as u64); - - let sys_log = crate::sys_log::Enter::from(enter).into(); - - alloy::consensus::Receipt { status: true.into(), cumulative_gas_used, logs: vec![sys_log] } - } - /// Construct a block header for DB and evm execution. fn construct_header(&self) -> Header { Header { @@ -461,7 +380,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// This path is used by /// - [`TransactionSigned`] objects /// - [`Transact`] events - fn check_fills_and_accept( + pub(crate) fn check_fills_and_accept( &mut self, mut trevm: EvmTransacted, tx: TransactionSigned, @@ -486,7 +405,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { if let ExecutionResult::Success { logs, .. } = trevm.result_mut_unchecked() { if let Some(extract) = extract { // Push the sys_log to the outcome - let sys_log = crate::sys_log::Transact::from(extract).into(); + let sys_log = TransactSysLog::from(extract).into(); logs.push(sys_log); } } @@ -506,7 +425,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// - [`TransactionSigned`] objects /// - [`Transact`] events /// - [`Enter`] events - fn accept_tx( + pub(crate) fn accept_tx( &mut self, trevm: EvmTransacted, tx: TransactionSigned, @@ -587,9 +506,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Ok(trevm) } - /// Credit enters to the recipients. This is done in the middle of the - /// block, between transactions and transact events. - fn credit_enters( + fn run_mints_inner( &mut self, mut trevm: EvmNeedsTx, ) -> RunTxResult @@ -597,206 +514,72 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Db: Database + DatabaseCommit, Insp: Inspector>, { + // Load the nonce once, we'll write it at the end + let minter_nonce = + trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); + + // Some setup for logging + let _span = debug_span!( + "signet_evm::evm::run_mints", + enters = self.extracts.enters.len(), + enter_tokens = self.extracts.enter_tokens.len(), + minter_nonce + ); let mut eth_minted = U256::ZERO; - let mut accts: HashSet
= HashSet::with_capacity(self.extracts.enters.len()); + let mut eth_accts = HashSet::with_capacity(self.extracts.enters.len()); + let mut usd_minted = U256::ZERO; + let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); - // Increment the nonce for the minter address by the number of enters. - // Doing it this way is slightly more efficient than incrementing the - // nonce in the loop. - let nonce = - trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); + let eth_token = self.constants.rollup().tokens().weth(); + + for (i, e) in self.extracts.enters.iter().enumerate() { + let mint = MintToken::from_enter(minter_nonce + i as u64, eth_token, e); + trevm = self.execute_mint_token(trevm, &mint)?; + eth_minted += e.event.amount; + eth_accts.insert(e.event.recipient()); + } + + // Use a new base nonce for the enter_tokens + let minter_nonce = minter_nonce + self.extracts.enters.len() as u64; + + for (i, e) in self.extracts.enter_tokens.iter().enumerate() { + let nonce = minter_nonce + i as u64; + if self.constants.is_host_usd(e.event.token) { + // USDC is handled as a native mint + let mint = MintNative::new(nonce, e); + trevm = self.mint_native(trevm, &mint)?; + usd_minted += e.event.amount; + usd_accts.insert(e.event.recipient()); + } else { + // All other tokens are non-native mints + let ru_token_addr = self + .constants + .rollup_token_from_host_address(e.event.token) + .expect("token enters must be permissioned"); + let mint = MintToken::from_enter_token(nonce, ru_token_addr, e); + trevm = self.execute_mint_token(trevm, &mint)?; + } + } + + // Update the minter nonce. + let minter_nonce = minter_nonce + self.extracts.enter_tokens.len() as u64; trevm_try!( - trevm - .try_set_nonce_unchecked(MINTER_ADDRESS, nonce + self.extracts.enters.len() as u64) - .map_err(EVMError::Database), + trevm.try_set_nonce_unchecked(MINTER_ADDRESS, minter_nonce).map_err(EVMError::Database), trevm ); - for enter in self.extracts.enters.iter() { - let recipient = enter.recipient(); - let amount = enter.amount(); - - // Increase the balance - trevm_try!( - trevm.try_increase_balance_unchecked(recipient, amount).map_err(EVMError::Database), - trevm - ); - - // push receipt and transaction to the block - self.processed.push(enter.make_transaction(nonce)); - self.output.push_result( - ReceiptEnvelope::Eip1559(self.make_enter_receipt(enter).into()), - MINTER_ADDRESS, - ); - - // Tracking for logging - accts.insert(recipient); - eth_minted += amount; - } - debug!( - accounts_touched = accts.len(), %eth_minted, - enters_count = self.extracts.enters.len(), - "Crediting enters" + eth_accts_touched = %eth_accts.len(), + %usd_minted, + usd_accts_touched = %usd_accts.len(), + "Minting completed" ); Ok(trevm) } - /// Execute an [`EnterToken`] event. - /// - /// [`EnterToken`]: signet_zenith::Passage::EnterToken - fn execute_enter_token( - &mut self, - mut trevm: EvmNeedsTx, - idx: usize, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - let _span = { - let e = &self.extracts.enter_tokens[idx]; - debug_span!("signet::evm::execute_enter_token", idx, host_tx = %e.tx_hash(), log_index = e.log_index).entered() - }; - - // Get the rollup token address from the host token address. - let ru_token_addr = self - .constants - .rollup_token_from_host_address(self.extracts.enter_tokens[idx].event.token) - .expect("token enters must be permissioned"); - - // Load the nonce as well - let nonce = - trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); - - let extract = &self.extracts.enter_tokens[idx]; - - let filler = EnterTokenFiller { enter_token: extract, nonce, token: ru_token_addr }; - let mut t = run_tx_early_return!(self, trevm, &filler, MINTER_ADDRESS); - - // push a sys_log to the outcome - if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { - let sys_log = crate::sys_log::EnterToken::from(extract).into(); - logs.push(sys_log) - } - let tx = extract.make_transaction(nonce, ru_token_addr); - - // No need to check AggregateFills. This call cannot result in orders. - Ok(self.accept_tx(t, tx)) - } - - /// Execute all [`EnterToken`] events. - fn execute_all_enter_tokens( - &mut self, - mut trevm: EvmNeedsTx, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - trevm = trevm.try_with_cfg(&DisableGasChecks, |trevm| { - trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { - for i in 0..self.extracts.enter_tokens.len() { - trevm = self.execute_enter_token(trevm, i)?; - } - Ok(trevm) - }) - })?; - Ok(trevm) - } - - /// Execute a [`Transactor::Transact`] event. - /// - /// This function does the following: - /// - Run the transaction. - /// - Check the aggregate fills. - /// - Debit the sender's account for unused gas. - /// - Create a receipt. - /// - Create a transaction and push it to the block. - /// - /// [`Transactor::Transact`]: signet_zenith::Transactor::Transact - fn execute_transact_event( - &mut self, - mut trevm: EvmNeedsTx, - idx: usize, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - let _span = { - let e = &self.extracts.transacts[idx]; - debug_span!("execute_transact_event", idx, - host_tx = %e.tx_hash(), - log_index = e.log_index, - sender = %e.event.sender, - gas_limit = e.event.gas(), - ) - .entered() - }; - - let sender = self.extracts.transacts[idx].event.sender; - let nonce = trevm_try!(trevm.try_read_nonce(sender).map_err(EVMError::Database), trevm); - - let transact = &self.extracts.transacts[idx]; - let to_execute = TransactFiller { transact, nonce }; - - let mut t = run_tx_early_return!(self, trevm, &to_execute, sender); - - { - // NB: This is a little sensitive. - // Although the EVM performs a check on the balance of the sender, - // to ensure they can pay the full price, that check may be - // invalidated by transaction execution. As a result, we have to - // perform the same check here, again. - let gas_used = t.result().gas_used(); - - // Set gas used to the transact gas limit - match t.result_mut_unchecked() { - ExecutionResult::Success { gas_used, .. } - | ExecutionResult::Revert { gas_used, .. } - | ExecutionResult::Halt { gas_used, .. } => { - *gas_used = if transact.gas() >= u64::MAX as u128 { - u64::MAX - } else { - transact.gas() as u64 - } - } - } - - let unused_gas = transact.gas.saturating_sub(U256::from(gas_used)); - let base_fee = t.block().basefee; - let to_debit = U256::from(base_fee) * unused_gas; - - debug!(%base_fee, gas_used, %unused_gas, %to_debit, "Debiting unused transact gas"); - - let acct = t - .result_and_state_mut_unchecked() - .state - .get_mut(&transact.sender) - .expect("sender account must be in state, as it is touched by definition"); - - match acct.info.balance.checked_sub(to_debit) { - // If the balance is sufficient, debit the account. - Some(balance) => acct.info.balance = balance, - // If the balance is insufficient, discard the transaction. - None => { - debug!("Discarding transact outcome due to insufficient balance to pay for unused transact gas"); - return Ok(t.reject()); - } - } - } - - // Convert the transact event into a transaction, and then check the - // aggregate fills - let tx = transact.make_transaction(nonce); - self.check_fills_and_accept(t, tx, Some(transact)) - } - - /// Execute all transact events. - fn execute_all_transacts( + fn run_all_mints( &mut self, trevm: EvmNeedsTx, ) -> RunTxResult @@ -804,11 +587,8 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Db: Database + DatabaseCommit, Insp: Inspector>, { - trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { - for i in 0..self.extracts.transacts.len() { - trevm = self.execute_transact_event(trevm, i)?; - } - Ok(trevm) + trevm.try_with_cfg(&DisableGasChecks, |trevm| { + trevm.try_with_cfg(&DisableNonceCheck, |trevm| self.run_mints_inner(trevm)) }) } @@ -913,13 +693,9 @@ where // Transaction gas is metered, and pays basefee trevm = self.execute_all_transactions(trevm)?; - // Credit enters to the recipients. - // Enter gas is unmetered, and does not pay basefee - trevm = self.credit_enters(trevm)?; - - // Run the enter token events. - // Enter token gas is unmetered, and does not pay basefee - trevm = self.execute_all_enter_tokens(trevm)?; + // Run all Enter and EnterToken events + // Gas is unmetered, and does not pay basefee + trevm = self.run_all_mints(trevm)?; // Run the transact events. // Transact gas is metered, and pays basefee diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 9be4eb94..68432034 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -12,10 +12,14 @@ //! Signet EVM +#[macro_use] +mod macros; + mod aliases; pub use aliases::*; mod driver; +pub(crate) use driver::ControlFlow; pub use driver::SignetDriver; mod journal; @@ -41,9 +45,8 @@ use trevm::{ TrevmBuilder, }; -mod sys_log; - -pub(crate) const BASE_GAS: usize = 21_000; +/// System structs and types. +pub mod sys; /// Create a new EVM with the given database. pub fn signet_evm( diff --git a/crates/evm/src/macros.rs b/crates/evm/src/macros.rs new file mode 100644 index 00000000..94d17280 --- /dev/null +++ b/crates/evm/src/macros.rs @@ -0,0 +1,34 @@ +macro_rules! run_tx { + ($self:ident, $trevm:ident, $tx:expr, $sender:expr) => {{ + let trevm = $trevm.fill_tx($tx); + + let _guard = tracing::trace_span!("run_tx", block_env = ?trevm.block(), tx = ?$tx, tx_env = ?trevm.tx(), spec_id = ?trevm.spec_id()).entered(); + + match trevm.run() { + Ok(t) => { + tracing::debug!("evm executed successfully"); + ControlFlow::Keep(t) + }, + Err(e) => { + if e.is_transaction_error() { + tracing::debug!( + err = %e.as_transaction_error().unwrap(), + "Discarding outcome due to execution error" + ); + ControlFlow::Discard(e.discard_error()) + } else { + return Err(e.err_into()); + } + } + } + }}; +} + +macro_rules! run_tx_early_return { + ($self:ident, $trevm:ident, $tx:expr, $sender:expr) => { + match run_tx!($self, $trevm, $tx, $sender) { + ControlFlow::Discard(t) => return Ok(t), + ControlFlow::Keep(t) => t, + } + }; +} diff --git a/crates/evm/src/sys_log.rs b/crates/evm/src/sys/logs.rs similarity index 56% rename from crates/evm/src/sys_log.rs rename to crates/evm/src/sys/logs.rs index e6cdffb3..40a96f54 100644 --- a/crates/evm/src/sys_log.rs +++ b/crates/evm/src/sys/logs.rs @@ -1,20 +1,20 @@ use alloy::{consensus::TxReceipt, primitives::Log, sol_types::SolEvent}; use signet_extract::ExtractedEvent; -use signet_zenith::{Passage, Transactor, MINTER_ADDRESS}; +use signet_zenith::{Transactor, MINTER_ADDRESS}; alloy::sol! { - event Enter( + event MintNative( bytes32 indexed txHash, uint64 indexed logIndex, address indexed recipient, uint256 amount, ); - event EnterToken( + event MintToken( bytes32 indexed txHash, uint64 indexed logIndex, address indexed recipient, - address token, + address hostToken, uint256 amount, ); @@ -28,37 +28,20 @@ alloy::sol! { ); } -impl> From<&ExtractedEvent<'_, R, Passage::Enter>> for Enter { - fn from(event: &ExtractedEvent<'_, R, Passage::Enter>) -> Self { - Enter { - recipient: event.event.rollupRecipient, - txHash: event.tx_hash(), - logIndex: event.log_index as u64, - amount: event.amount(), - } - } -} - -impl From for Log { - fn from(value: Enter) -> Self { +impl From for Log { + fn from(value: MintNative) -> Self { Log { address: MINTER_ADDRESS, data: value.encode_log_data() } } } -impl> From<&ExtractedEvent<'_, R, Passage::EnterToken>> for EnterToken { - fn from(event: &ExtractedEvent<'_, R, Passage::EnterToken>) -> Self { - EnterToken { - recipient: event.event.rollupRecipient, - txHash: event.tx_hash(), - logIndex: event.log_index as u64, - token: event.token(), - amount: event.amount(), - } +impl From for Log { + fn from(value: MintToken) -> Self { + Log { address: MINTER_ADDRESS, data: value.encode_log_data() } } } -impl From for Log { - fn from(value: EnterToken) -> Self { +impl From for Log { + fn from(value: Transact) -> Self { Log { address: MINTER_ADDRESS, data: value.encode_log_data() } } } @@ -75,9 +58,3 @@ impl> From<&ExtractedEvent<'_, R, Transactor::Transact>> } } } - -impl From for Log { - fn from(value: Transact) -> Self { - Log { address: MINTER_ADDRESS, data: value.encode_log_data() } - } -} diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs new file mode 100644 index 00000000..8f96e5e9 --- /dev/null +++ b/crates/evm/src/sys/mod.rs @@ -0,0 +1,12 @@ +mod logs; +pub use logs::{ + MintNative as MintNativeSysLog, MintToken as MintTokenSysLog, Transact as TransactSysLog, +}; + +mod native; +pub use native::MintNative; + +mod token; +pub use token::MintToken; + +mod transact; diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs new file mode 100644 index 00000000..0544c93c --- /dev/null +++ b/crates/evm/src/sys/native.rs @@ -0,0 +1,121 @@ +use crate::{sys::MintNativeSysLog, EvmNeedsTx, RunTxResult, SignetDriver}; +use alloy::{ + consensus::{ReceiptEnvelope, TxEip1559, TxReceipt}, + primitives::{Address, Log, U256}, +}; +use signet_extract::{Extractable, ExtractedEvent}; +use signet_types::{ + constants::MINTER_ADDRESS, + primitives::{Transaction, TransactionSigned}, + MagicSig, +}; +use signet_zenith::Passage; +use trevm::{ + helpers::Ctx, + revm::{context::result::EVMError, Database, DatabaseCommit, Inspector}, + trevm_try, MIN_TRANSACTION_GAS, +}; + +/// System transaction to mint native tokens. +#[derive(Debug, Clone, Copy)] +pub struct MintNative { + /// The address that will receive the minted tokens. + recipient: Address, + /// The amount of native tokens to mint. + amount: U256, + + /// The magic signature for the mint. + magic_sig: MagicSig, + + /// The nonce of the mint transaction. + nonce: u64, + /// The rollup chain ID. + rollup_chain_id: u64, +} + +impl MintNative { + /// Create a new [`MintNative`] instance from an [`ExtractedEvent`] + /// containing a [`Passage::EnterToken`] event. + pub fn new>( + nonce: u64, + event: &ExtractedEvent<'_, R, Passage::EnterToken>, + ) -> Self { + Self { + recipient: event.event.recipient(), + amount: event.event.amount(), + magic_sig: event.magic_sig(), + nonce, + rollup_chain_id: event.rollup_chain_id(), + } + } + + /// Create a new [`Log`] for the [`MintNative`] operation. + pub const fn to_log(&self) -> MintNativeSysLog { + MintNativeSysLog { + txHash: self.magic_sig.txid, + logIndex: self.magic_sig.event_idx as u64, + recipient: self.recipient, + amount: self.amount, + } + } + + /// Convert the [`MintNative`] instance into a [`TransactionSigned`]. + pub fn to_transaction(&self) -> TransactionSigned { + TransactionSigned::new_unhashed( + Transaction::Eip1559(TxEip1559 { + chain_id: self.rollup_chain_id, + nonce: self.nonce, + gas_limit: MIN_TRANSACTION_GAS, + max_fee_per_gas: 0, + max_priority_fee_per_gas: 0, + to: self.recipient.into(), + value: self.amount, + access_list: Default::default(), + input: Default::default(), + }), + self.magic_sig.into(), + ) + } +} + +impl<'a, 'b, C> SignetDriver<'a, 'b, C> +where + C: Extractable, +{ + fn mint_native_receipt(&self, mint: &MintNative) -> ReceiptEnvelope { + let cumulative_gas_used = self.cumulative_gas_used().saturating_add(MIN_TRANSACTION_GAS); + + ReceiptEnvelope::Eip1559( + alloy::consensus::Receipt { + status: true.into(), + cumulative_gas_used, + logs: vec![mint.to_log().into()], + } + .with_bloom(), + ) + } + + pub(crate) fn mint_native( + &mut self, + mut trevm: EvmNeedsTx, + mint: &MintNative, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + // Increase the balance + trevm_try!( + trevm + .try_increase_balance_unchecked(mint.recipient, mint.amount) + .map_err(EVMError::Database), + trevm + ); + + // push receipt and transaction to the block + self.processed.push(mint.to_transaction()); + self.output.push_result(self.mint_native_receipt(mint), MINTER_ADDRESS); + + Ok(trevm) + } +} diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs new file mode 100644 index 00000000..5b81efd9 --- /dev/null +++ b/crates/evm/src/sys/token.rs @@ -0,0 +1,185 @@ +use crate::{sys::MintTokenSysLog, ControlFlow, EvmNeedsTx, RunTxResult, SignetDriver}; +use alloy::{ + consensus::{TxEip1559, TxReceipt}, + primitives::{Address, Log, U256}, + sol_types::SolCall, +}; +use signet_extract::{Extractable, ExtractedEvent}; +use signet_types::{ + constants::MINTER_ADDRESS, + primitives::{Transaction, TransactionSigned}, + MagicSig, +}; +use signet_zenith::Passage; +use tracing::debug_span; +use trevm::{ + helpers::Ctx, + revm::{ + context::{result::ExecutionResult, TransactTo, TransactionType, TxEnv}, + Database, DatabaseCommit, Inspector, + }, + MIN_TRANSACTION_GAS, +}; + +/// System transaction to mint tokens. +#[derive(Debug, Clone, Copy)] +pub struct MintToken { + /// The address that will receive the minted tokens. + recipient: Address, + /// The amount of tokens to mint. + amount: U256, + /// The token being minted. + token: Address, + /// The corresponding token on the host. + host_token: Address, + + /// The magic signature for the mint. + magic_sig: MagicSig, + + /// The nonce of the mint transaction. + nonce: u64, + /// The rollup chain ID. + rollup_chain_id: u64, +} + +impl trevm::Tx for MintToken { + fn fill_tx_env(&self, tx_env: &mut TxEnv) { + let TxEnv { + tx_type, + caller, + gas_limit, + gas_price, + kind, + value, + data, + nonce, + chain_id, + access_list, + gas_priority_fee, + blob_hashes, + max_fee_per_blob_gas, + authorization_list, + } = tx_env; + + *tx_type = TransactionType::Custom as u8; + *caller = MINTER_ADDRESS; + *gas_limit = 1_000_000; + *gas_price = 0; + *kind = TransactTo::Call(self.token); + *value = U256::ZERO; + *data = self.mint_call().abi_encode().into(); + *nonce = self.nonce; + *chain_id = Some(self.rollup_chain_id); + *access_list = Default::default(); + *gas_priority_fee = Some(0); + blob_hashes.clear(); + *max_fee_per_blob_gas = 0; + authorization_list.clear(); + } +} + +impl MintToken { + /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] + /// containing a [`Passage::EnterToken`] event. + pub fn from_enter_token>( + nonce: u64, + token: Address, + event: &ExtractedEvent<'_, R, Passage::EnterToken>, + ) -> Self { + Self { + recipient: event.event.recipient(), + amount: event.event.amount(), + token, + host_token: event.event.token, + magic_sig: event.magic_sig(), + nonce, + rollup_chain_id: event.rollup_chain_id(), + } + } + + /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] + /// containing a [`Passage::Enter`] event. + pub fn from_enter>( + nonce: u64, + token: Address, + event: &ExtractedEvent<'_, R, Passage::Enter>, + ) -> Self { + Self { + recipient: event.event.recipient(), + amount: event.event.amount(), + token, + host_token: Address::repeat_byte(0xee), + magic_sig: event.magic_sig(), + nonce, + rollup_chain_id: event.rollup_chain_id(), + } + } + + /// Create the ABI-encoded call for the mint operation. + const fn mint_call(&self) -> signet_zenith::mintCall { + signet_zenith::mintCall { amount: self.amount, to: self.recipient } + } + + /// Create a new [`Log`] for the [`MintToken`] operation. + const fn to_log(self) -> MintTokenSysLog { + MintTokenSysLog { + txHash: self.magic_sig.txid, + logIndex: self.magic_sig.event_idx as u64, + recipient: self.recipient, + amount: self.amount, + hostToken: self.host_token, // TODO: this needs to be the HOST token + } + } + + /// Convert the [`MintToken`] instance into a [`TransactionSigned`]. + pub fn to_transaction(self) -> TransactionSigned { + let input = self.mint_call().abi_encode().into(); + + TransactionSigned::new_unhashed( + Transaction::Eip1559(TxEip1559 { + chain_id: self.rollup_chain_id, + nonce: self.nonce, + gas_limit: MIN_TRANSACTION_GAS, + max_fee_per_gas: 0, + max_priority_fee_per_gas: 0, + // NB: set to the address of the token contract. + to: self.token.into(), + value: U256::ZERO, + access_list: Default::default(), + input, // NB: set to the ABI-encoded input for the `mint` function, which dictates the amount and recipient. + }), + self.magic_sig.into(), + ) + } +} + +impl<'a, 'b, C> SignetDriver<'a, 'b, C> +where + C: Extractable, +{ + /// Execute a [`MintToken`], triggered by either a [`Passage::Enter`] or a + /// [`Passage::EnterToken`]. + pub(crate) fn execute_mint_token( + &mut self, + trevm: EvmNeedsTx, + mint: &MintToken, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + let _span = debug_span!("signet::evm::Execute_mint_token", host_tx = %mint.magic_sig.txid, log_index = mint.magic_sig.event_idx).entered(); + + // Run the transaction. + let mut t = run_tx_early_return!(self, trevm, mint, MINTER_ADDRESS); + + // push a sys_log to the outcome + if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { + logs.push(mint.to_log().into()); + } + + // No need to check AggregateFills. This call cannot result in orders. + let tx = mint.to_transaction(); + Ok(self.accept_tx(t, tx)) + } +} diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs new file mode 100644 index 00000000..1a6e9961 --- /dev/null +++ b/crates/evm/src/sys/transact.rs @@ -0,0 +1,143 @@ +use crate::{ControlFlow, EvmNeedsTx, RunTxResult, SignetDriver}; +use alloy::primitives::U256; +use signet_extract::{Extractable, ExtractedEvent}; +use signet_zenith::Transactor; +use tracing::{debug, debug_span}; +use trevm::{ + fillers::DisableNonceCheck, + helpers::Ctx, + revm::{ + context::{ + result::{EVMError, ExecutionResult}, + TxEnv, + }, + Database, DatabaseCommit, Inspector, + }, + trevm_try, Tx, +}; + +/// Shim to impl [`Tx`] for [`Transactor::Transact`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TransactFiller<'a, 'b, R> { + /// The extracted event for the transact event. + pub transact: &'a ExtractedEvent<'b, R, Transactor::Transact>, + /// The nonce of the transaction. + pub nonce: u64, +} + +impl Tx for TransactFiller<'_, '_, R> { + fn fill_tx_env(&self, tx_env: &mut TxEnv) { + self.transact.event.fill_tx_env(tx_env); + tx_env.nonce = self.nonce; + } +} + +impl<'a, 'b, C> SignetDriver<'a, 'b, C> +where + C: Extractable, +{ + /// Execute a [`Transactor::Transact`] event. + /// + /// This function does the following: + /// - Run the transaction. + /// - Check the aggregate fills. + /// - Debit the sender's account for unused gas. + /// - Create a receipt. + /// - Create a transaction and push it to the block. + /// + /// [`Transactor::Transact`]: signet_zenith::Transactor::Transact + fn execute_transact_event( + &mut self, + mut trevm: EvmNeedsTx, + idx: usize, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + let _span = { + let e = &self.extracts.transacts[idx]; + debug_span!("execute_transact_event", idx, + host_tx = %e.tx_hash(), + log_index = e.log_index, + sender = %e.event.sender, + gas_limit = e.event.gas(), + ) + .entered() + }; + + let sender = self.extracts.transacts[idx].event.sender; + let nonce = trevm_try!(trevm.try_read_nonce(sender).map_err(EVMError::Database), trevm); + + let transact = &self.extracts.transacts[idx]; + let to_execute = TransactFiller { transact, nonce }; + + let mut t = run_tx_early_return!(self, trevm, &to_execute, sender); + + { + // NB: This is a little sensitive. + // Although the EVM performs a check on the balance of the sender, + // to ensure they can pay the full price, that check may be + // invalidated by transaction execution. As a result, we have to + // perform the same check here, again. + let gas_used = t.result().gas_used(); + + // Set gas used to the transact gas limit + match t.result_mut_unchecked() { + ExecutionResult::Success { gas_used, .. } + | ExecutionResult::Revert { gas_used, .. } + | ExecutionResult::Halt { gas_used, .. } => { + *gas_used = if transact.gas() >= u64::MAX as u128 { + u64::MAX + } else { + transact.gas() as u64 + } + } + } + + let unused_gas = transact.gas.saturating_sub(U256::from(gas_used)); + let base_fee = t.block().basefee; + let to_debit = U256::from(base_fee) * unused_gas; + + debug!(%base_fee, gas_used, %unused_gas, %to_debit, "Debiting unused transact gas"); + + let acct = t + .result_and_state_mut_unchecked() + .state + .get_mut(&transact.sender) + .expect("sender account must be in state, as it is touched by definition"); + + match acct.info.balance.checked_sub(to_debit) { + // If the balance is sufficient, debit the account. + Some(balance) => acct.info.balance = balance, + // If the balance is insufficient, discard the transaction. + None => { + debug!("Discarding transact outcome due to insufficient balance to pay for unused transact gas"); + return Ok(t.reject()); + } + } + } + + // Convert the transact event into a transaction, and then check the + // aggregate fills + let tx = transact.make_transaction(nonce); + self.check_fills_and_accept(t, tx, Some(transact)) + } + + /// Execute all transact events. + pub(crate) fn execute_all_transacts( + &mut self, + trevm: EvmNeedsTx, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { + for i in 0..self.extracts.transacts.len() { + trevm = self.execute_transact_event(trevm, i)?; + } + Ok(trevm) + }) + } +} diff --git a/crates/extract/src/extracted.rs b/crates/extract/src/extracted.rs index 5fe9f6ea..856f49a3 100644 --- a/crates/extract/src/extracted.rs +++ b/crates/extract/src/extracted.rs @@ -1,8 +1,7 @@ use crate::Events; use alloy::{ consensus::{TxEip1559, TxReceipt}, - primitives::{Address, Log, TxHash, U256}, - sol_types::SolCall, + primitives::{Log, TxHash, U256}, }; use signet_types::{ primitives::{Transaction, TransactionSigned}, @@ -10,9 +9,6 @@ use signet_types::{ }; use signet_zenith::{Passage, RollupOrders, Transactor, Zenith}; -/// Basic gas cost for a transaction. -const BASE_TX_GAS_COST: u64 = 21_000; - /// A single event extracted from the host chain. /// /// This struct contains a reference to the transaction that caused the event, @@ -242,30 +238,6 @@ impl> ExtractedEvent<'_, R, Passage::Enter> { pub fn magic_sig(&self) -> MagicSig { MagicSig { ty: MagicSigInfo::Enter, txid: self.tx_hash(), event_idx: self.log_index } } - - /// Get the reth transaction signature for the enter event. - fn signature(&self) -> alloy::primitives::Signature { - self.magic_sig().into() - } - - /// Make the transaction that corresponds to this enter event, using the - /// provided nonce. - pub fn make_transaction(&self, nonce: u64) -> TransactionSigned { - TransactionSigned::new_unhashed( - Transaction::Eip1559(TxEip1559 { - chain_id: self.rollup_chain_id(), - nonce, - gas_limit: BASE_TX_GAS_COST, - max_fee_per_gas: 0, - max_priority_fee_per_gas: 0, - to: self.rollupRecipient.into(), - value: self.amount, - access_list: Default::default(), - input: Default::default(), - }), - self.signature(), - ) - } } impl> ExtractedEvent<'_, R, Passage::EnterToken> { @@ -273,35 +245,6 @@ impl> ExtractedEvent<'_, R, Passage::EnterToken> { pub fn magic_sig(&self) -> MagicSig { MagicSig { ty: MagicSigInfo::EnterToken, txid: self.tx_hash(), event_idx: self.log_index } } - - /// Get the reth transaction signature for the enter token event. - fn signature(&self) -> alloy::primitives::Signature { - self.magic_sig().into() - } - - /// Make the transaction that corresponds to this enter token event, - /// using the provided nonce. - pub fn make_transaction(&self, nonce: u64, token: Address) -> TransactionSigned { - let input = signet_zenith::mintCall { amount: self.amount(), to: self.rollupRecipient } - .abi_encode() - .into(); - - TransactionSigned::new_unhashed( - Transaction::Eip1559(TxEip1559 { - chain_id: self.rollup_chain_id(), - nonce, - gas_limit: BASE_TX_GAS_COST, - max_fee_per_gas: 0, - max_priority_fee_per_gas: 0, - // NB: set to the address of the token contract. - to: token.into(), - value: U256::ZERO, - access_list: Default::default(), - input, // NB: set to the ABI-encoded input for the `mint` function, which dictates the amount and recipient. - }), - self.signature(), - ) - } } impl ExtractedEvent<'_, R, Zenith::BlockSubmitted> { diff --git a/crates/test-utils/tests/evm.rs b/crates/test-utils/tests/evm.rs index 599fcd28..7f23e550 100644 --- a/crates/test-utils/tests/evm.rs +++ b/crates/test-utils/tests/evm.rs @@ -5,12 +5,16 @@ use alloy::{ }, primitives::{Address, U256}, signers::{local::PrivateKeySigner, Signature}, + sol_types::SolEvent, }; use signet_constants::SignetSystemConstants; -use signet_evm::SignetDriver; +use signet_evm::{ + sys::{MintNative, MintToken, MintTokenSysLog}, + SignetDriver, +}; use signet_extract::{Extractable, ExtractedEvent, Extracts}; use signet_test_utils::{ - chain::{fake_block, Chain, RU_CHAIN_ID}, + chain::{fake_block, Chain, HOST_USDC, RU_CHAIN_ID, RU_WETH}, evm::test_signet_evm, specs::{make_wallet, sign_tx_with_key_pair, simple_send}, }; @@ -193,21 +197,32 @@ fn test_an_enter() { let mut driver = context.driver(&mut extracts, vec![]); // Run the EVM - let mut trevm = context.trevm().drive_block(&mut driver).unwrap(); + let _trevm = context.trevm().drive_block(&mut driver).unwrap(); let (sealed_block, receipts) = driver.finish(); + let expected_tx = MintToken::from_enter(0, RU_WETH, &extracts.enters[0]).to_transaction(); + assert_eq!(sealed_block.senders.len(), 1); - assert_eq!( - sealed_block.block.body.transactions().collect::>(), - vec![&extracts.enters[0].make_transaction(0)] - ); + assert_eq!(sealed_block.block.body.transactions().collect::>(), vec![&expected_tx]); assert_eq!(receipts.len(), 1); - assert_eq!(trevm.read_balance(user), U256::from(100)); - assert_eq!(trevm.read_nonce(user), 0); + + let ReceiptEnvelope::Eip1559(ref receipt) = receipts[0] else { + panic!("expected 1559 receipt") + }; + let mint_log = receipt.receipt.logs.last().unwrap(); + + let decoded = MintTokenSysLog::decode_log(mint_log).unwrap(); + + assert_eq!(decoded.address, MINTER_ADDRESS); + assert_eq!(decoded.recipient, user); + assert_eq!(decoded.amount, U256::from(100)); + assert_eq!(decoded.hostToken, Address::repeat_byte(0xee)); } #[test] fn test_a_transact() { + tracing_subscriber::fmt::init(); + let mut context = TestEnv::new(); let sender = Address::repeat_byte(1); let recipient = Address::repeat_byte(2); @@ -222,6 +237,13 @@ fn test_a_transact() { amount: U256::from(ETH_TO_WEI), }; + let enter_token = signet_zenith::Passage::EnterToken { + rollupChainId: U256::from(RU_CHAIN_ID), + rollupRecipient: sender, + token: HOST_USDC, + amount: U256::from(ETH_TO_WEI), + }; + let transact = signet_zenith::Transactor::Transact { rollupChainId: U256::from(RU_CHAIN_ID), sender, @@ -232,7 +254,7 @@ fn test_a_transact() { maxFeePerGas: U256::from(GWEI_TO_WEI), }; - // Setup the driver + // Setup extraction outputs let block = context.next_block(); let mut extracts = Extracts::::empty(&block); extracts.enters.push(ExtractedEvent { @@ -241,6 +263,12 @@ fn test_a_transact() { log_index: 0, event: enter, }); + extracts.enter_tokens.push(ExtractedEvent { + tx: &fake_tx, + receipt: &fake_receipt, + log_index: 0, + event: enter_token, + }); extracts.transacts.push(ExtractedEvent { tx: &fake_tx, receipt: &fake_receipt, @@ -248,18 +276,27 @@ fn test_a_transact() { event: transact, }); + // Setup the driver let mut driver = context.driver(&mut extracts, vec![]); // Run the EVM let mut trevm = context.trevm().drive_block(&mut driver).unwrap(); let (sealed_block, receipts) = driver.finish(); - assert_eq!(sealed_block.senders, vec![MINTER_ADDRESS, sender]); + // Transactions for the block should be: + // 1. MintToken for the enter event + // 2. MintNative for the enter token event + // 3. Transact for the transact event + let expected_tx_0 = MintToken::from_enter(0, RU_WETH, &extracts.enters[0]).to_transaction(); + let expected_tx_1 = MintNative::new(1, &extracts.enter_tokens[0]).to_transaction(); + let expected_tx_2 = extracts.transacts[0].make_transaction(0); + + assert_eq!(sealed_block.senders, vec![MINTER_ADDRESS, MINTER_ADDRESS, sender]); assert_eq!( sealed_block.block.body.transactions().collect::>(), - vec![&extracts.enters[0].make_transaction(0), &extracts.transacts[0].make_transaction(0)] + vec![&expected_tx_0, &expected_tx_1, &expected_tx_2] ); - assert_eq!(receipts.len(), 2); + assert_eq!(receipts.len(), 3); assert_eq!(trevm.read_balance(recipient), U256::from(100)); } From 2e5b152ddcc64930affb380bb501acb905b34c0e Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Jul 2025 11:42:18 -0400 Subject: [PATCH 02/24] refactor: introduce SysOutput, SysAction, and SysTx traits --- crates/evm/src/driver.rs | 50 ++++++++++++++++++++++++--- crates/evm/src/sys/mod.rs | 61 ++++++++++++++++++++++++++++++++ crates/evm/src/sys/native.rs | 67 ++++++++++++++++++------------------ crates/evm/src/sys/token.rs | 50 ++++++++------------------- 4 files changed, 154 insertions(+), 74 deletions(-) diff --git a/crates/evm/src/driver.rs b/crates/evm/src/driver.rs index 02f10bb6..56ba6ba4 100644 --- a/crates/evm/src/driver.rs +++ b/crates/evm/src/driver.rs @@ -1,6 +1,6 @@ use crate::{ orders::SignetInspector, - sys::{MintNative, MintToken, TransactSysLog}, + sys::{MintNative, MintToken, SysAction, SysTx, TransactSysLog}, BlockResult, EvmNeedsTx, EvmTransacted, ExecutionOutcome, RunTxResult, SignetLayered, }; use alloy::{ @@ -506,6 +506,48 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Ok(trevm) } + fn apply_sys_action( + &mut self, + mut trevm: EvmNeedsTx, + action: &S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: SysAction, + { + // Run the system action. + trevm_try!(action.apply(&mut trevm), trevm); + // push receipt and transaction to the block + self.processed.push(action.produce_transaction()); + self.output + .push_result(action.produce_receipt(self.cumulative_gas_used()), action.sender()); + + Ok(trevm) + } + + fn apply_sys_transaction( + &mut self, + trevm: EvmNeedsTx, + sys_tx: &S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: SysTx, + { + // Run the transaction. + let mut t = run_tx_early_return!(self, trevm, sys_tx, MINTER_ADDRESS); + + // push a sys_log to the outcome + if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { + logs.push(sys_tx.produce_log().into()); + } + + let tx = sys_tx.produce_transaction(); + Ok(self.accept_tx(t, tx)) + } + fn run_mints_inner( &mut self, mut trevm: EvmNeedsTx, @@ -534,7 +576,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { for (i, e) in self.extracts.enters.iter().enumerate() { let mint = MintToken::from_enter(minter_nonce + i as u64, eth_token, e); - trevm = self.execute_mint_token(trevm, &mint)?; + trevm = self.apply_sys_transaction(trevm, &mint)?; eth_minted += e.event.amount; eth_accts.insert(e.event.recipient()); } @@ -547,7 +589,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { if self.constants.is_host_usd(e.event.token) { // USDC is handled as a native mint let mint = MintNative::new(nonce, e); - trevm = self.mint_native(trevm, &mint)?; + trevm = self.apply_sys_action(trevm, &mint)?; usd_minted += e.event.amount; usd_accts.insert(e.event.recipient()); } else { @@ -557,7 +599,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { .rollup_token_from_host_address(e.event.token) .expect("token enters must be permissioned"); let mint = MintToken::from_enter_token(nonce, ru_token_addr, e); - trevm = self.execute_mint_token(trevm, &mint)?; + trevm = self.apply_sys_transaction(trevm, &mint)?; } } diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index 8f96e5e9..5843327e 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -10,3 +10,64 @@ mod token; pub use token::MintToken; mod transact; + +use alloy::{ + consensus::ReceiptEnvelope, + primitives::{Address, Log}, +}; +use core::fmt; +use signet_types::primitives::TransactionSigned; +use trevm::{ + helpers::Ctx, + revm::{context::result::EVMError, Database, DatabaseCommit, Inspector}, + Trevm, Tx, +}; + +/// Produce a transaction from a system action. This will be ingested into the +/// block during EVM execution. +pub trait SysOutput: fmt::Debug + Clone { + /// Convert the system action into a transaction that can be appended to a + /// block by the [`SignetDriver`]. + fn produce_transaction(&self) -> TransactionSigned; + + /// Produce a log for the system action. This will be appended to the end + /// of the receipt, and + fn produce_log(&self) -> Log; + + /// Get the address of the sender of the system action. This is typically + /// the [`MINTER_ADDRESS`] for minting actions, or the address of the + /// system contract caller for other actions. + fn sender(&self) -> Address; +} + +/// System transactions run on the EVM as a transaction, and are subject to the +/// same rules and constraints as regular transactions. They may run arbitrary +/// execution, have gas limits, and can revert if they fail. They must satisfy +/// the system market constraints on Orders. +/// +/// They are distinct from [`SysAction`], which are not run as transactions, +/// but rather apply changes to the state directly without going through the +/// transaction processing pipeline. +pub trait SysTx: SysOutput + Tx {} + +impl SysTx for T where T: SysOutput + Tx {} + +/// System actions are operations that apply changes to the EVM state without +/// going through the transaction processing pipeline. They are not run as +/// transactions, and do not have gas limits or revert semantics. They are +/// typically used for operations that need to be applied directly to the state, +/// such as modifying balances. +pub trait SysAction: SysOutput { + /// Apply the system action to the EVM state. + fn apply( + &self, + evm: &mut Trevm, + ) -> Result<(), EVMError> + where + Db: Database + DatabaseCommit, + Insp: Inspector>; + + /// Produce a receipt for the system action. This receipt will be + /// accumulated in the block object during EVM execution. + fn produce_receipt(&self, cumulative_gas_used: u64) -> ReceiptEnvelope; +} diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index 0544c93c..c8c69dcd 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -1,9 +1,9 @@ -use crate::{sys::MintNativeSysLog, EvmNeedsTx, RunTxResult, SignetDriver}; +use crate::sys::{MintNativeSysLog, SysAction, SysOutput}; use alloy::{ consensus::{ReceiptEnvelope, TxEip1559, TxReceipt}, primitives::{Address, Log, U256}, }; -use signet_extract::{Extractable, ExtractedEvent}; +use signet_extract::ExtractedEvent; use signet_types::{ constants::MINTER_ADDRESS, primitives::{Transaction, TransactionSigned}, @@ -13,7 +13,7 @@ use signet_zenith::Passage; use trevm::{ helpers::Ctx, revm::{context::result::EVMError, Database, DatabaseCommit, Inspector}, - trevm_try, MIN_TRANSACTION_GAS, + Trevm, MIN_TRANSACTION_GAS, }; /// System transaction to mint native tokens. @@ -78,44 +78,43 @@ impl MintNative { } } -impl<'a, 'b, C> SignetDriver<'a, 'b, C> -where - C: Extractable, -{ - fn mint_native_receipt(&self, mint: &MintNative) -> ReceiptEnvelope { - let cumulative_gas_used = self.cumulative_gas_used().saturating_add(MIN_TRANSACTION_GAS); +impl SysOutput for MintNative { + fn produce_transaction(&self) -> TransactionSigned { + self.to_transaction() + } - ReceiptEnvelope::Eip1559( - alloy::consensus::Receipt { - status: true.into(), - cumulative_gas_used, - logs: vec![mint.to_log().into()], - } - .with_bloom(), - ) + fn produce_log(&self) -> Log { + self.to_log().into() + } + + fn sender(&self) -> Address { + MINTER_ADDRESS } +} - pub(crate) fn mint_native( - &mut self, - mut trevm: EvmNeedsTx, - mint: &MintNative, - ) -> RunTxResult +impl SysAction for MintNative { + fn apply( + &self, + evm: &mut Trevm, + ) -> Result<(), EVMError> where Db: Database + DatabaseCommit, Insp: Inspector>, { - // Increase the balance - trevm_try!( - trevm - .try_increase_balance_unchecked(mint.recipient, mint.amount) - .map_err(EVMError::Database), - trevm - ); - - // push receipt and transaction to the block - self.processed.push(mint.to_transaction()); - self.output.push_result(self.mint_native_receipt(mint), MINTER_ADDRESS); + // Increase the balance of the recipient + evm.try_increase_balance_unchecked(self.recipient, self.amount) + .map(drop) + .map_err(EVMError::Database) + } - Ok(trevm) + fn produce_receipt(&self, cumulative_gas_used: u64) -> ReceiptEnvelope { + ReceiptEnvelope::Eip1559( + alloy::consensus::Receipt { + status: true.into(), + cumulative_gas_used: cumulative_gas_used.saturating_add(MIN_TRANSACTION_GAS), + logs: vec![self.to_log().into()], + } + .with_bloom(), + ) } } diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index 5b81efd9..4f298b8e 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -1,23 +1,18 @@ -use crate::{sys::MintTokenSysLog, ControlFlow, EvmNeedsTx, RunTxResult, SignetDriver}; +use crate::sys::{MintTokenSysLog, SysOutput}; use alloy::{ consensus::{TxEip1559, TxReceipt}, primitives::{Address, Log, U256}, sol_types::SolCall, }; -use signet_extract::{Extractable, ExtractedEvent}; +use signet_extract::ExtractedEvent; use signet_types::{ constants::MINTER_ADDRESS, primitives::{Transaction, TransactionSigned}, MagicSig, }; use signet_zenith::Passage; -use tracing::debug_span; use trevm::{ - helpers::Ctx, - revm::{ - context::{result::ExecutionResult, TransactTo, TransactionType, TxEnv}, - Database, DatabaseCommit, Inspector, - }, + revm::context::{TransactTo, TransactionType, TxEnv}, MIN_TRANSACTION_GAS, }; @@ -127,12 +122,12 @@ impl MintToken { logIndex: self.magic_sig.event_idx as u64, recipient: self.recipient, amount: self.amount, - hostToken: self.host_token, // TODO: this needs to be the HOST token + hostToken: self.host_token, } } /// Convert the [`MintToken`] instance into a [`TransactionSigned`]. - pub fn to_transaction(self) -> TransactionSigned { + pub fn to_transaction(&self) -> TransactionSigned { let input = self.mint_call().abi_encode().into(); TransactionSigned::new_unhashed( @@ -153,33 +148,16 @@ impl MintToken { } } -impl<'a, 'b, C> SignetDriver<'a, 'b, C> -where - C: Extractable, -{ - /// Execute a [`MintToken`], triggered by either a [`Passage::Enter`] or a - /// [`Passage::EnterToken`]. - pub(crate) fn execute_mint_token( - &mut self, - trevm: EvmNeedsTx, - mint: &MintToken, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - let _span = debug_span!("signet::evm::Execute_mint_token", host_tx = %mint.magic_sig.txid, log_index = mint.magic_sig.event_idx).entered(); - - // Run the transaction. - let mut t = run_tx_early_return!(self, trevm, mint, MINTER_ADDRESS); +impl SysOutput for MintToken { + fn produce_transaction(&self) -> TransactionSigned { + self.to_transaction() + } - // push a sys_log to the outcome - if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { - logs.push(mint.to_log().into()); - } + fn produce_log(&self) -> Log { + self.to_log().into() + } - // No need to check AggregateFills. This call cannot result in orders. - let tx = mint.to_transaction(); - Ok(self.accept_tx(t, tx)) + fn sender(&self) -> Address { + MINTER_ADDRESS } } From 2748de3bf3e5160139ab6890d2fed13d9c919fae Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Jul 2025 14:20:45 -0400 Subject: [PATCH 03/24] fix: remove unused TX impls --- crates/evm/src/driver.rs | 166 +----------- crates/evm/src/lib.rs | 1 - crates/evm/src/sys/driver.rs | 458 ++++++++++++++++++++++++++++++++ crates/evm/src/sys/mod.rs | 71 ++++- crates/evm/src/sys/native.rs | 15 +- crates/evm/src/sys/token.rs | 26 +- crates/evm/src/sys/transact.rs | 226 +++++++--------- crates/extract/src/extracted.rs | 58 ++-- crates/test-utils/tests/evm.rs | 10 +- crates/zenith/src/trevm.rs | 94 +------ 10 files changed, 713 insertions(+), 412 deletions(-) create mode 100644 crates/evm/src/sys/driver.rs diff --git a/crates/evm/src/driver.rs b/crates/evm/src/driver.rs index 56ba6ba4..d9aa7c8c 100644 --- a/crates/evm/src/driver.rs +++ b/crates/evm/src/driver.rs @@ -1,7 +1,6 @@ use crate::{ - orders::SignetInspector, - sys::{MintNative, MintToken, SysAction, SysTx, TransactSysLog}, - BlockResult, EvmNeedsTx, EvmTransacted, ExecutionOutcome, RunTxResult, SignetLayered, + orders::SignetInspector, BlockResult, EvmNeedsTx, EvmTransacted, ExecutionOutcome, RunTxResult, + SignetLayered, }; use alloy::{ consensus::{ @@ -11,17 +10,15 @@ use alloy::{ eips::eip1559::{BaseFeeParams, INITIAL_BASE_FEE as EIP1559_INITIAL_BASE_FEE}, primitives::{Address, Bloom, U256}, }; -use signet_extract::{Extractable, ExtractedEvent, Extracts}; +use signet_extract::{Extractable, Extracts}; use signet_types::{ constants::SignetSystemConstants, primitives::{BlockBody, RecoveredBlock, SealedBlock, SealedHeader, TransactionSigned}, AggregateFills, MarketError, }; -use signet_zenith::{Transactor, MINTER_ADDRESS}; -use std::collections::{HashSet, VecDeque}; -use tracing::{debug, debug_span, warn}; +use std::collections::VecDeque; +use tracing::{debug, debug_span, info_span, warn}; use trevm::{ - fillers::{DisableGasChecks, DisableNonceCheck}, helpers::Ctx, revm::{ context::{ @@ -202,7 +199,7 @@ pub struct SignetDriver<'a, 'b, C: Extractable> { parent: SealedHeader, /// Rollup constants, including pre-deploys - constants: SignetSystemConstants, + pub(crate) constants: SignetSystemConstants, /// The working context is a clone of the block's [`AggregateFills`] that /// is updated progessively as the block is evaluated. @@ -379,12 +376,11 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// /// This path is used by /// - [`TransactionSigned`] objects - /// - [`Transact`] events + /// - [`Transactor::Transact`] events pub(crate) fn check_fills_and_accept( &mut self, mut trevm: EvmTransacted, tx: TransactionSigned, - extract: Option<&ExtractedEvent<'_, C::Receipt, Transactor::Transact>>, ) -> RunTxResult where Db: Database + DatabaseCommit, @@ -402,14 +398,6 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { return Ok(trevm.reject()); } - if let ExecutionResult::Success { logs, .. } = trevm.result_mut_unchecked() { - if let Some(extract) = extract { - // Push the sys_log to the outcome - let sys_log = TransactSysLog::from(extract).into(); - logs.push(sys_log); - } - } - // We track this separately from the cumulative gas used. Enters and // EnterTokens are not payable, so we don't want to include their gas // usage in the payable gas used. @@ -423,8 +411,10 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// /// This path is used by /// - [`TransactionSigned`] objects - /// - [`Transact`] events - /// - [`Enter`] events + /// - [`Transactor::Transact`] events + /// - [`Passage::Enter`] events + /// + /// [`Passage::Enter`]: signet_zenith::Passage::Enter pub(crate) fn accept_tx( &mut self, trevm: EvmTransacted, @@ -482,7 +472,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { s.record("sender", sender.to_string()); // Run the tx, returning from this function if there is a tx error let t = run_tx_early_return!(self, trevm, &FillShim(&tx, sender), sender); - trevm = self.check_fills_and_accept(t, tx, None)?; + trevm = self.check_fills_and_accept(t, tx)?; } else { warn!("Failed to recover signer for transaction"); } @@ -506,134 +496,6 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Ok(trevm) } - fn apply_sys_action( - &mut self, - mut trevm: EvmNeedsTx, - action: &S, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - S: SysAction, - { - // Run the system action. - trevm_try!(action.apply(&mut trevm), trevm); - // push receipt and transaction to the block - self.processed.push(action.produce_transaction()); - self.output - .push_result(action.produce_receipt(self.cumulative_gas_used()), action.sender()); - - Ok(trevm) - } - - fn apply_sys_transaction( - &mut self, - trevm: EvmNeedsTx, - sys_tx: &S, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - S: SysTx, - { - // Run the transaction. - let mut t = run_tx_early_return!(self, trevm, sys_tx, MINTER_ADDRESS); - - // push a sys_log to the outcome - if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { - logs.push(sys_tx.produce_log().into()); - } - - let tx = sys_tx.produce_transaction(); - Ok(self.accept_tx(t, tx)) - } - - fn run_mints_inner( - &mut self, - mut trevm: EvmNeedsTx, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - // Load the nonce once, we'll write it at the end - let minter_nonce = - trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); - - // Some setup for logging - let _span = debug_span!( - "signet_evm::evm::run_mints", - enters = self.extracts.enters.len(), - enter_tokens = self.extracts.enter_tokens.len(), - minter_nonce - ); - let mut eth_minted = U256::ZERO; - let mut eth_accts = HashSet::with_capacity(self.extracts.enters.len()); - let mut usd_minted = U256::ZERO; - let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); - - let eth_token = self.constants.rollup().tokens().weth(); - - for (i, e) in self.extracts.enters.iter().enumerate() { - let mint = MintToken::from_enter(minter_nonce + i as u64, eth_token, e); - trevm = self.apply_sys_transaction(trevm, &mint)?; - eth_minted += e.event.amount; - eth_accts.insert(e.event.recipient()); - } - - // Use a new base nonce for the enter_tokens - let minter_nonce = minter_nonce + self.extracts.enters.len() as u64; - - for (i, e) in self.extracts.enter_tokens.iter().enumerate() { - let nonce = minter_nonce + i as u64; - if self.constants.is_host_usd(e.event.token) { - // USDC is handled as a native mint - let mint = MintNative::new(nonce, e); - trevm = self.apply_sys_action(trevm, &mint)?; - usd_minted += e.event.amount; - usd_accts.insert(e.event.recipient()); - } else { - // All other tokens are non-native mints - let ru_token_addr = self - .constants - .rollup_token_from_host_address(e.event.token) - .expect("token enters must be permissioned"); - let mint = MintToken::from_enter_token(nonce, ru_token_addr, e); - trevm = self.apply_sys_transaction(trevm, &mint)?; - } - } - - // Update the minter nonce. - let minter_nonce = minter_nonce + self.extracts.enter_tokens.len() as u64; - trevm_try!( - trevm.try_set_nonce_unchecked(MINTER_ADDRESS, minter_nonce).map_err(EVMError::Database), - trevm - ); - - debug!( - %eth_minted, - eth_accts_touched = %eth_accts.len(), - %usd_minted, - usd_accts_touched = %usd_accts.len(), - "Minting completed" - ); - - Ok(trevm) - } - - fn run_all_mints( - &mut self, - trevm: EvmNeedsTx, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - trevm.try_with_cfg(&DisableGasChecks, |trevm| { - trevm.try_with_cfg(&DisableNonceCheck, |trevm| self.run_mints_inner(trevm)) - }) - } - /// Clear the balance of the rollup passage. This is run at the end of the /// block, after all transactions, enters, and transact events have been /// processed. It ensures that ETH sent to the rollup passage is burned, @@ -709,8 +571,8 @@ where } fn run_txns(&mut self, mut trevm: EvmNeedsTx) -> RunTxResult { - let _span = debug_span!( - "run_txns", + let _span = info_span!( + "SignetDriver::run_txns", txn_count = self.to_process.len(), enter_count = self.extracts.enters.len(), enter_token_count = self.extracts.enter_tokens.len(), diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 68432034..29019773 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -19,7 +19,6 @@ mod aliases; pub use aliases::*; mod driver; -pub(crate) use driver::ControlFlow; pub use driver::SignetDriver; mod journal; diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs new file mode 100644 index 00000000..902e97a4 --- /dev/null +++ b/crates/evm/src/sys/driver.rs @@ -0,0 +1,458 @@ +#![allow(dead_code)] // NB: future proofing. + +use crate::{ + driver::ControlFlow, + sys::{ + MeteredSysTx, MintNative, MintToken, SysAction, SysOutput, TransactSysTx, UnmeteredSysTx, + }, + EvmNeedsTx, RunTxResult, SignetDriver, +}; +use alloy::primitives::{map::HashSet, U256}; +use signet_extract::Extractable; +use signet_zenith::MINTER_ADDRESS; +use tracing::{debug, debug_span}; +use trevm::{ + fillers::{DisableGasChecks, DisableNonceCheck}, + helpers::Ctx, + revm::{ + context::result::{EVMError, ExecutionResult}, + Database, DatabaseCommit, Inspector, + }, + trevm_try, +}; + +/// Populate the nonce for a system output. +fn populate_nonce_from_trevm( + trevm: &mut EvmNeedsTx, + sys_output: &mut S, +) -> Result<(), EVMError> +where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: SysOutput, +{ + // If the sys_output already has a nonce, we don't need to populate it. + if sys_output.has_nonce() { + return Ok(()); + } + + // Read the nonce from the database and populate it in the sys_output. + trevm + .try_read_nonce(sys_output.sender()) + .map(|nonce| sys_output.populate_nonce(nonce)) + .map_err(EVMError::Database) +} + +impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { + /// Apply a [`SysAction`] to the EVM state. + /// + /// This will do the following: + /// - Run the system action, allowing direct EVM state changes. + /// - Produce the transaction using [`SysOutput::produce_transaction`]. + /// - Produce the syslog eipt using [`SysOutput::produce_log`]. + /// - Produce a receipt containing the gas used and logs. + /// - Push the resulting transaction to the block. + /// - Push the resulting receipt to the output. + /// + /// [`SysAction`]s have the following properties: + /// - DO NOT pay for gas. + /// - DO update the nonce of the [`SysOutput::sender`] sender. + /// - DO NOT run the EVM. + /// + /// See the [`SysAction`] trait documentation for more details. + /// + /// [`SysOutput::sender`]: crate::sys::SysOutput::sender + /// [`SysOutput::produce_log`]: crate::sys::SysOutput::produce_log + /// [`SysOutput::produce_transaction`]: crate::sys::SysOutput::produce_transaction + pub(crate) fn apply_sys_action_single( + &mut self, + mut trevm: EvmNeedsTx, + mut action: S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: SysAction, + { + // Populate the nonce for the action. + trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut action), trevm); + + // Run the system action. + trevm_try!(action.apply(&mut trevm), trevm); + // push receipt and transaction to the block + self.processed.push(action.produce_transaction()); + self.output + .push_result(action.produce_receipt(self.cumulative_gas_used()), action.sender()); + + Ok(trevm) + } + + /// Apply a series of [`SysAction`]s to the EVM state. + pub(crate) fn apply_sys_actions( + &mut self, + mut trevm: EvmNeedsTx, + sys_actions: impl IntoIterator, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: SysAction, + { + for action in sys_actions { + trevm = self.apply_sys_action_single(trevm, action)?; + } + Ok(trevm) + } + + /// Inner logic for applying a [`UnmeteredSysTx`] to the EVM state. + /// + /// This function expects that gas and nonce checks are already disabled + /// in the EVM, and will not re-enable them. + pub(crate) fn apply_unmetered_sys_transaction_inner( + &mut self, + mut trevm: EvmNeedsTx, + mut sys_tx: S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: UnmeteredSysTx, + { + // Populate the nonce for the action. + trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); + + // Run the transaction. + let mut t = run_tx_early_return!(self, trevm, &sys_tx, MINTER_ADDRESS); + + // push a sys_log to the outcome + if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { + logs.push(sys_tx.produce_log()); + } + + let tx = sys_tx.produce_transaction(); + Ok(self.accept_tx(t, tx)) + } + + /// Apply a [`UnmeteredSysTx`] to the EVM state. + /// + /// When applying many system transactions, it is recommended to use + /// [`Self::apply_unmetered_sys_transactions`] instead, as it will disable + /// gas and nonce checks only once, rather than for each transaction. + /// + /// This will do the following: + /// - Disable gas and nonce checks in the EVM. + /// - Run the system transaction in the EVM as + /// [`Self::apply_unmetered_sys_transaction_inner`]. + /// - Re-enable gas and nonce checks in the EVM. + /// + /// [`UnmeteredSysTx`]s have the following properties: + /// - DO NOT pay for gas. + /// - DO update the nonce of the [`SysOutput::sender`]. + /// - DO run the EVM. + /// + /// [`SysOutput::sender`]: crate::sys::SysOutput::sender + pub(crate) fn apply_unmetered_sys_transaction_single( + &mut self, + trevm: EvmNeedsTx, + sys_tx: S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: UnmeteredSysTx, + { + trevm.try_with_cfg(&DisableGasChecks, |trevm| { + trevm.try_with_cfg(&DisableNonceCheck, |trevm| { + self.apply_unmetered_sys_transaction_inner(trevm, sys_tx) + }) + }) + } + + /// Apply a series of [`UnmeteredSysTx`]s to the EVM state. + /// + /// This will do the following: + /// - Disable gas and nonce checks in the EVM. + /// - Run each system transaction in the EVM as + /// [`Self::apply_unmetered_sys_transaction_inner`]. + /// - Re-enable gas and nonce checks in the EVM. + /// + /// /// [`UnmeteredSysTx`]s have the following properties: + /// - DO NOT pay for gas. + /// - DO update the nonce of the [`SysOutput::sender`]. + /// - DO run the EVM. + /// + /// [`SysOutput::sender`]: crate::sys::SysOutput::sender + pub(crate) fn apply_unmetered_sys_transactions( + &mut self, + trevm: EvmNeedsTx, + sys_txs: impl IntoIterator, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: UnmeteredSysTx, + { + trevm.try_with_cfg(&DisableGasChecks, |trevm| { + trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { + for sys_tx in sys_txs { + // Populate the nonce for the transaction + trevm = self.apply_unmetered_sys_transaction_inner(trevm, sys_tx)?; + } + Ok(trevm) + }) + }) + } + + /// Apply a [`MeteredSysTx`] to the EVM state. + /// + /// This will do the following: + /// - Run the system transaction in the EVM. + /// - Double-check that the sender has enough balance to pay for the unused + /// gas. + /// - Produce the transaction using [`SysOutput::produce_transaction`]. + /// - Produce a syslog using [`SysOutput::produce_log`]. + /// - Push the syslog to the outcome. + /// - Invoke [`Self::check_fills_and_accept`] to check the fills and + /// accept the transaction and receipt. + /// + /// [`MeteredSysTx`]s have the following properties: + /// - DO pay for gas, INCLUDING unused gas. + /// - DO update the nonce of the [`SysOutput::sender`]. + /// - DO run the EVM. + /// + /// [`SysOutput::produce_transaction`]: crate::sys::SysOutput::produce_transaction + /// [`SysOutput::sender`]: crate::sys::SysOutput::sender + /// [`SysOutput::produce_log`]: crate::sys::SysOutput::produce_log + pub(crate) fn apply_metered_sys_transaction_single( + &mut self, + mut trevm: EvmNeedsTx, + mut sys_tx: S, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: MeteredSysTx, + { + // Populate the nonce for the action. + trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); + + let mut t = run_tx_early_return!(self, trevm, &sys_tx, sys_tx.sender()); + + { + // NB: This is a little sensitive. + // Although the EVM performs a check on the balance of the sender, + // to ensure they can pay the full price, that check may be + // invalidated by transaction execution. As a result, we have to + // perform the same check here, again. + let gas_used = t.result().gas_used(); + + // Set gas used to the transact gas limit + match t.result_mut_unchecked() { + ExecutionResult::Success { ref mut gas_used, .. } + | ExecutionResult::Revert { ref mut gas_used, .. } + | ExecutionResult::Halt { ref mut gas_used, .. } => { + *gas_used = if sys_tx.gas_limit() >= u64::MAX as u128 { + u64::MAX + } else { + sys_tx.gas_limit() as u64 + } + } + } + + let unused_gas = sys_tx.gas_limit().saturating_sub(gas_used as u128); + let base_fee = t.block().basefee as u128; + let to_debit = base_fee * unused_gas; + + debug!(%base_fee, gas_used, %unused_gas, %to_debit, "Debiting unused transact gas"); + + let acct = t + .result_and_state_mut_unchecked() + .state + .get_mut(&sys_tx.sender()) + .expect("sender account must be in state, as it is touched by definition"); + + match acct.info.balance.checked_sub(U256::from(to_debit)) { + // If the balance is sufficient, debit the account. + Some(balance) => acct.info.balance = balance, + // If the balance is insufficient, discard the transaction. + None => { + debug!("Discarding metered sys tx outcome due to insufficient balance to pay for unused gas"); + return Ok(t.reject()); + } + } + } + + // push a sys_log to the outcome + if let ExecutionResult::Success { logs, .. } = t.result_mut_unchecked() { + logs.push(sys_tx.produce_log()); + } + + let tx = sys_tx.produce_transaction(); + self.check_fills_and_accept(t, tx) + } + + /// Apply a series of [`MeteredSysTx`]s to the EVM state. + /// + /// This will do the following: + /// - Run each system transaction in the EVM using + /// [`Self::apply_metered_sys_transaction_single`]. + /// + /// [`MeteredSysTx`]s have the following properties: + /// - DO pay for gas, INCLUDING unused gas. + /// - DO update the nonce of the [`SysOutput::sender`]. + /// - DO run the EVM. + /// + /// [`SysOutput::sender`]: crate::sys::SysOutput::sender + fn apply_metered_sys_transactions( + &mut self, + mut trevm: EvmNeedsTx, + sys_txs: impl IntoIterator, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: MeteredSysTx, + { + for mut sys_tx in sys_txs { + let span = tracing::debug_span!( + "SignetDriver::apply_metered_sys_transactions", + sender = %sys_tx.sender(), + gas_limit = sys_tx.gas_limit(), + callee = ?sys_tx.callee(), + ); + if tracing::enabled!(tracing::Level::TRACE) { + span.record("input", format!("{}", &sys_tx.input())); + } + let _enter = span.entered(); + + let nonce = trevm_try!( + trevm.try_read_nonce(sys_tx.sender()).map_err(EVMError::Database), + trevm + ); + sys_tx.populate_nonce(nonce); + debug!(nonce, "Applying metered sys tx"); + trevm = self.apply_metered_sys_transaction_single(trevm, sys_tx)?; + } + Ok(trevm) + } + + /// Execute all [`Transactor::Transact`] extracts from the block via + /// [`Self::apply_metered_sys_transactions`]. + pub(crate) fn execute_all_transacts( + &mut self, + trevm: EvmNeedsTx, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + let _span = tracing::debug_span!( + "SignetDriver::execute_all_transacts", + count = self.extracts.transacts.len() + ) + .entered(); + let transacts = self.extracts.transacts.iter().map(TransactSysTx::new); + self.apply_metered_sys_transactions(trevm, transacts) + } + + /// Run all mints, including enters and enter tokens. + /// + /// This could be implemented more-simply using + /// [`Self::apply_unmetered_sys_transactions`] and + /// [`Self::apply_sys_actions`], + /// + /// This is special cased as follows: + /// - Nonce lookups are done ONCE as the sender is known to be identical + /// for all mints. + /// - Details are collected for tracing purposes. + /// - Nonce setting is done at the end, after all mints are processed. + fn run_mints_inner( + &mut self, + mut trevm: EvmNeedsTx, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + // Load the nonce once, we'll write it at the end + let minter_nonce = + trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); + + // Some setup for logging + let _span = debug_span!( + "signet_evm::evm::run_mints", + enters = self.extracts.enters.len(), + enter_tokens = self.extracts.enter_tokens.len(), + minter_nonce + ); + let mut eth_minted = U256::ZERO; + let mut eth_accts = HashSet::with_capacity(self.extracts.enters.len()); + let mut usd_minted = U256::ZERO; + let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); + + let eth_token = self.constants.rollup().tokens().weth(); + + for (i, e) in self.extracts.enters.iter().enumerate() { + let mut mint = MintToken::from_enter(eth_token, e); + mint.populate_nonce(minter_nonce + i as u64); + trevm = self.apply_unmetered_sys_transaction_inner(trevm, mint)?; + + eth_minted += e.event.amount; + eth_accts.insert(e.event.recipient()); + } + + // Use a new base nonce for the enter_tokens + let minter_nonce = minter_nonce + self.extracts.enters.len() as u64; + + for (i, e) in self.extracts.enter_tokens.iter().enumerate() { + let nonce = minter_nonce + i as u64; + if self.constants.is_host_usd(e.event.token) { + // USDC is handled as a native mint + let mut mint = MintNative::new(e); + mint.populate_nonce(nonce); + trevm = self.apply_sys_action_single(trevm, mint)?; + usd_minted += e.event.amount; + usd_accts.insert(e.event.recipient()); + } else { + // All other tokens are non-native mints + let ru_token_addr = self + .constants + .rollup_token_from_host_address(e.event.token) + .expect("token enters must be permissioned"); + let mut mint = MintToken::from_enter_token(ru_token_addr, e); + mint.populate_nonce(nonce); + trevm = self.apply_unmetered_sys_transaction_inner(trevm, mint)?; + } + } + + // Update the minter nonce. + let minter_nonce = minter_nonce + self.extracts.enter_tokens.len() as u64; + trevm_try!( + trevm.try_set_nonce_unchecked(MINTER_ADDRESS, minter_nonce).map_err(EVMError::Database), + trevm + ); + + debug!( + %eth_minted, + eth_accts_touched = %eth_accts.len(), + %usd_minted, + usd_accts_touched = %usd_accts.len(), + "Minting completed" + ); + + Ok(trevm) + } + + pub(crate) fn run_all_mints( + &mut self, + trevm: EvmNeedsTx, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + { + trevm.try_with_cfg(&DisableGasChecks, |trevm| { + trevm.try_with_cfg(&DisableNonceCheck, |trevm| self.run_mints_inner(trevm)) + }) + } +} diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index 5843327e..d1c02cb1 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -1,3 +1,5 @@ +mod driver; + mod logs; pub use logs::{ MintNative as MintNativeSysLog, MintToken as MintTokenSysLog, Transact as TransactSysLog, @@ -10,10 +12,11 @@ mod token; pub use token::MintToken; mod transact; +pub use transact::TransactSysTx; use alloy::{ consensus::ReceiptEnvelope, - primitives::{Address, Log}, + primitives::{Address, Bytes, Log, TxKind}, }; use core::fmt; use signet_types::primitives::TransactionSigned; @@ -26,6 +29,26 @@ use trevm::{ /// Produce a transaction from a system action. This will be ingested into the /// block during EVM execution. pub trait SysOutput: fmt::Debug + Clone { + /// Check if the system action has a nonce. This is typically used to + /// determine if the nonce should be populated by the Evm during + /// transaction processing. + fn has_nonce(&self) -> bool; + + /// Populate the nonce for the transaction. This is typically used to + /// ensure that the transaction is unique. It will be called by the Evm + /// during transaction processing to set the nonce for the transaction. + fn populate_nonce(&mut self, nonce: u64); + + /// Set the nonce for the transaction. This is a convenience method that + /// calls [`Self::populate_nonce`] with the given nonce. + fn with_nonce(mut self, nonce: u64) -> Self + where + Self: Sized, + { + self.populate_nonce(nonce); + self + } + /// Convert the system action into a transaction that can be appended to a /// block by the [`SignetDriver`]. fn produce_transaction(&self) -> TransactionSigned; @@ -40,18 +63,12 @@ pub trait SysOutput: fmt::Debug + Clone { fn sender(&self) -> Address; } -/// System transactions run on the EVM as a transaction, and are subject to the -/// same rules and constraints as regular transactions. They may run arbitrary -/// execution, have gas limits, and can revert if they fail. They must satisfy -/// the system market constraints on Orders. +/// A transaction that is run on the EVM, and may or may not pay gas. /// -/// They are distinct from [`SysAction`], which are not run as transactions, -/// but rather apply changes to the state directly without going through the -/// transaction processing pipeline. +/// See [`MeteredSysTx`] and [`UnmeteredSysTx`] for more specific +/// transaction types. pub trait SysTx: SysOutput + Tx {} -impl SysTx for T where T: SysOutput + Tx {} - /// System actions are operations that apply changes to the EVM state without /// going through the transaction processing pipeline. They are not run as /// transactions, and do not have gas limits or revert semantics. They are @@ -71,3 +88,37 @@ pub trait SysAction: SysOutput { /// accumulated in the block object during EVM execution. fn produce_receipt(&self, cumulative_gas_used: u64) -> ReceiptEnvelope; } + +/// System transactions run on the EVM as a transaction, but do not pay gas and +/// cannot produce Orders. They are run as transactions, but are not subject to +/// the same rules and constraints as regular transactions. They CAN revert, +/// and CAN halt. They are typically used for operations that need to be run as +/// transactions, but should not pay gas. E.g. minting tokens or performing +/// system-level operations that do not require gas payment. +pub trait UnmeteredSysTx: SysOutput + Tx {} + +/// System transactions run on the EVM as a transaction, and are subject to the +/// same rules and constraints as regular transactions. They may run arbitrary +/// execution, have gas limits, and can revert if they fail. They must satisfy +/// the system market constraints on Orders. +/// +/// They are distinct from [`UnmeteredSysTx`], which are run as transactions, +/// but do not pay gas and cannot produce Orders. +/// +/// They are distinct from [`SysAction`], which are not run as transactions, +/// but rather apply changes to the state directly without going through the +/// transaction processing pipeline. +pub trait MeteredSysTx: SysOutput + Tx { + /// Get the gas limit for the transaction. This is the maximum amount of + /// gas that the transaction is allowed to consume. + /// + /// Metered system transactions ALWAYS consume all gas. + fn gas_limit(&self) -> u128; + + /// Get the callee address for the transaction. + fn callee(&self) -> TxKind; + + /// Get the input data for the transaction. This is the calldata that is + /// passed to the callee when the transaction is executed. + fn input(&self) -> Bytes; +} diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index c8c69dcd..0bf42ca8 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -28,7 +28,7 @@ pub struct MintNative { magic_sig: MagicSig, /// The nonce of the mint transaction. - nonce: u64, + nonce: Option, /// The rollup chain ID. rollup_chain_id: u64, } @@ -37,14 +37,13 @@ impl MintNative { /// Create a new [`MintNative`] instance from an [`ExtractedEvent`] /// containing a [`Passage::EnterToken`] event. pub fn new>( - nonce: u64, event: &ExtractedEvent<'_, R, Passage::EnterToken>, ) -> Self { Self { recipient: event.event.recipient(), amount: event.event.amount(), magic_sig: event.magic_sig(), - nonce, + nonce: None, rollup_chain_id: event.rollup_chain_id(), } } @@ -64,7 +63,7 @@ impl MintNative { TransactionSigned::new_unhashed( Transaction::Eip1559(TxEip1559 { chain_id: self.rollup_chain_id, - nonce: self.nonce, + nonce: self.nonce.expect("must be set"), gas_limit: MIN_TRANSACTION_GAS, max_fee_per_gas: 0, max_priority_fee_per_gas: 0, @@ -79,6 +78,14 @@ impl MintNative { } impl SysOutput for MintNative { + fn has_nonce(&self) -> bool { + self.nonce.is_some() + } + + fn populate_nonce(&mut self, nonce: u64) { + self.nonce = Some(nonce) + } + fn produce_transaction(&self) -> TransactionSigned { self.to_transaction() } diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index 4f298b8e..a4ce5ed8 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -1,4 +1,4 @@ -use crate::sys::{MintTokenSysLog, SysOutput}; +use crate::sys::{MintTokenSysLog, SysOutput, SysTx, UnmeteredSysTx}; use alloy::{ consensus::{TxEip1559, TxReceipt}, primitives::{Address, Log, U256}, @@ -32,7 +32,7 @@ pub struct MintToken { magic_sig: MagicSig, /// The nonce of the mint transaction. - nonce: u64, + nonce: Option, /// The rollup chain ID. rollup_chain_id: u64, } @@ -63,7 +63,7 @@ impl trevm::Tx for MintToken { *kind = TransactTo::Call(self.token); *value = U256::ZERO; *data = self.mint_call().abi_encode().into(); - *nonce = self.nonce; + *nonce = self.nonce.expect("must be set"); *chain_id = Some(self.rollup_chain_id); *access_list = Default::default(); *gas_priority_fee = Some(0); @@ -77,7 +77,6 @@ impl MintToken { /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] /// containing a [`Passage::EnterToken`] event. pub fn from_enter_token>( - nonce: u64, token: Address, event: &ExtractedEvent<'_, R, Passage::EnterToken>, ) -> Self { @@ -87,7 +86,7 @@ impl MintToken { token, host_token: event.event.token, magic_sig: event.magic_sig(), - nonce, + nonce: None, rollup_chain_id: event.rollup_chain_id(), } } @@ -95,7 +94,6 @@ impl MintToken { /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] /// containing a [`Passage::Enter`] event. pub fn from_enter>( - nonce: u64, token: Address, event: &ExtractedEvent<'_, R, Passage::Enter>, ) -> Self { @@ -105,7 +103,7 @@ impl MintToken { token, host_token: Address::repeat_byte(0xee), magic_sig: event.magic_sig(), - nonce, + nonce: None, rollup_chain_id: event.rollup_chain_id(), } } @@ -133,7 +131,7 @@ impl MintToken { TransactionSigned::new_unhashed( Transaction::Eip1559(TxEip1559 { chain_id: self.rollup_chain_id, - nonce: self.nonce, + nonce: self.nonce.expect("must be set"), gas_limit: MIN_TRANSACTION_GAS, max_fee_per_gas: 0, max_priority_fee_per_gas: 0, @@ -149,6 +147,10 @@ impl MintToken { } impl SysOutput for MintToken { + fn populate_nonce(&mut self, nonce: u64) { + self.nonce = Some(nonce); + } + fn produce_transaction(&self) -> TransactionSigned { self.to_transaction() } @@ -160,4 +162,12 @@ impl SysOutput for MintToken { fn sender(&self) -> Address { MINTER_ADDRESS } + + fn has_nonce(&self) -> bool { + self.nonce.is_some() + } } + +impl SysTx for MintToken {} + +impl UnmeteredSysTx for MintToken {} diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 1a6e9961..78b74f90 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -1,143 +1,117 @@ -use crate::{ControlFlow, EvmNeedsTx, RunTxResult, SignetDriver}; -use alloy::primitives::U256; -use signet_extract::{Extractable, ExtractedEvent}; -use signet_zenith::Transactor; -use tracing::{debug, debug_span}; -use trevm::{ - fillers::DisableNonceCheck, - helpers::Ctx, - revm::{ - context::{ - result::{EVMError, ExecutionResult}, - TxEnv, - }, - Database, DatabaseCommit, Inspector, - }, - trevm_try, Tx, +use crate::sys::{MeteredSysTx, SysOutput, SysTx, TransactSysLog}; +use alloy::{ + consensus::{EthereumTxEnvelope, Transaction}, + primitives::{Address, Log, TxKind, U256}, }; +use core::fmt; +use signet_extract::ExtractedEvent; +use signet_types::{primitives::TransactionSigned, MagicSig}; +use signet_zenith::Transactor; +use trevm::{revm::context::TxEnv, Tx}; /// Shim to impl [`Tx`] for [`Transactor::Transact`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct TransactFiller<'a, 'b, R> { - /// The extracted event for the transact event. - pub transact: &'a ExtractedEvent<'b, R, Transactor::Transact>, +#[derive(PartialEq, Eq)] +pub struct TransactSysTx { + tx: TransactionSigned, + /// The nonce of the transaction. - pub nonce: u64, + nonce: Option, + + /// The magic sig. Memoized here to make it a little simpler to + /// access. Also available on the [`MagicSig`] in the transaction above. + magic_sig: MagicSig, +} + +impl<'a, R> From<&ExtractedEvent<'a, R, Transactor::Transact>> for TransactSysTx { + fn from(transact: &ExtractedEvent<'a, R, Transactor::Transact>) -> Self { + Self::new(transact) + } +} + +impl TransactSysTx { + /// Instantiate a new [`TransactFiller`]. + pub fn new(transact: &ExtractedEvent<'_, R, Transactor::Transact>) -> Self { + let magic_sig = transact.magic_sig(); + let tx = transact.make_transaction(0); + Self { tx, nonce: None, magic_sig } + } + + /// Create a [`TransactSysLog`] from the filler. + pub fn to_log(&self) -> TransactSysLog { + TransactSysLog { + txHash: self.magic_sig.txid, + logIndex: self.magic_sig.event_idx as u64, + sender: self.sender(), + value: self.tx.value(), + gas: U256::from(self.tx.gas_limit()), + maxFeePerGas: U256::from(self.tx.max_fee_per_gas()), + } + } +} + +// NB: manual impl because of incorrect auto-derive bound on `R: Debug` +impl fmt::Debug for TransactSysTx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TransactFiller") + .field("transact", &self.tx) + .field("magic_sig", &self.magic_sig) + .finish() + } } -impl Tx for TransactFiller<'_, '_, R> { +// NB: manual impl because of incorrect auto-derive bound on `R: Clone` +impl Clone for TransactSysTx { + fn clone(&self) -> Self { + Self { tx: self.tx.clone(), nonce: self.nonce, magic_sig: self.magic_sig } + } +} + +impl Tx for TransactSysTx { fn fill_tx_env(&self, tx_env: &mut TxEnv) { - self.transact.event.fill_tx_env(tx_env); - tx_env.nonce = self.nonce; + self.tx.as_eip1559().unwrap().fill_tx_env(tx_env); + tx_env.caller = self.magic_sig.sender(); } } -impl<'a, 'b, C> SignetDriver<'a, 'b, C> -where - C: Extractable, -{ - /// Execute a [`Transactor::Transact`] event. - /// - /// This function does the following: - /// - Run the transaction. - /// - Check the aggregate fills. - /// - Debit the sender's account for unused gas. - /// - Create a receipt. - /// - Create a transaction and push it to the block. - /// - /// [`Transactor::Transact`]: signet_zenith::Transactor::Transact - fn execute_transact_event( - &mut self, - mut trevm: EvmNeedsTx, - idx: usize, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - let _span = { - let e = &self.extracts.transacts[idx]; - debug_span!("execute_transact_event", idx, - host_tx = %e.tx_hash(), - log_index = e.log_index, - sender = %e.event.sender, - gas_limit = e.event.gas(), - ) - .entered() +impl SysOutput for TransactSysTx { + fn has_nonce(&self) -> bool { + self.nonce.is_some() + } + + fn populate_nonce(&mut self, nonce: u64) { + // NB: we have to set the nonce on the tx as well. + let EthereumTxEnvelope::Eip1559(signed) = &mut self.tx else { + unreachable!("new sets this to 1559"); }; + signed.tx_mut().nonce = nonce; + self.nonce = Some(nonce); + } - let sender = self.extracts.transacts[idx].event.sender; - let nonce = trevm_try!(trevm.try_read_nonce(sender).map_err(EVMError::Database), trevm); - - let transact = &self.extracts.transacts[idx]; - let to_execute = TransactFiller { transact, nonce }; - - let mut t = run_tx_early_return!(self, trevm, &to_execute, sender); - - { - // NB: This is a little sensitive. - // Although the EVM performs a check on the balance of the sender, - // to ensure they can pay the full price, that check may be - // invalidated by transaction execution. As a result, we have to - // perform the same check here, again. - let gas_used = t.result().gas_used(); - - // Set gas used to the transact gas limit - match t.result_mut_unchecked() { - ExecutionResult::Success { gas_used, .. } - | ExecutionResult::Revert { gas_used, .. } - | ExecutionResult::Halt { gas_used, .. } => { - *gas_used = if transact.gas() >= u64::MAX as u128 { - u64::MAX - } else { - transact.gas() as u64 - } - } - } - - let unused_gas = transact.gas.saturating_sub(U256::from(gas_used)); - let base_fee = t.block().basefee; - let to_debit = U256::from(base_fee) * unused_gas; - - debug!(%base_fee, gas_used, %unused_gas, %to_debit, "Debiting unused transact gas"); - - let acct = t - .result_and_state_mut_unchecked() - .state - .get_mut(&transact.sender) - .expect("sender account must be in state, as it is touched by definition"); - - match acct.info.balance.checked_sub(to_debit) { - // If the balance is sufficient, debit the account. - Some(balance) => acct.info.balance = balance, - // If the balance is insufficient, discard the transaction. - None => { - debug!("Discarding transact outcome due to insufficient balance to pay for unused transact gas"); - return Ok(t.reject()); - } - } - } + fn produce_transaction(&self) -> TransactionSigned { + self.tx.clone() + } + + fn produce_log(&self) -> Log { + self.to_log().into() + } + + fn sender(&self) -> Address { + self.magic_sig.sender() + } +} + +impl SysTx for TransactSysTx {} + +impl MeteredSysTx for TransactSysTx { + fn gas_limit(&self) -> u128 { + self.tx.gas_limit() as u128 + } - // Convert the transact event into a transaction, and then check the - // aggregate fills - let tx = transact.make_transaction(nonce); - self.check_fills_and_accept(t, tx, Some(transact)) + fn callee(&self) -> TxKind { + self.tx.kind() } - /// Execute all transact events. - pub(crate) fn execute_all_transacts( - &mut self, - trevm: EvmNeedsTx, - ) -> RunTxResult - where - Db: Database + DatabaseCommit, - Insp: Inspector>, - { - trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { - for i in 0..self.extracts.transacts.len() { - trevm = self.execute_transact_event(trevm, i)?; - } - Ok(trevm) - }) + fn input(&self) -> alloy::primitives::Bytes { + self.tx.input().clone() } } diff --git a/crates/extract/src/extracted.rs b/crates/extract/src/extracted.rs index 856f49a3..a1aa9a2f 100644 --- a/crates/extract/src/extracted.rs +++ b/crates/extract/src/extracted.rs @@ -1,3 +1,5 @@ +use core::fmt; + use crate::Events; use alloy::{ consensus::{TxEip1559, TxReceipt}, @@ -16,7 +18,7 @@ use signet_zenith::{Passage, RollupOrders, Transactor, Zenith}; /// receipt's logs, and the extracted event itself. /// /// Events may be either the enum type [`Events`], or a specific event type. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Copy, PartialEq, Eq)] pub struct ExtractedEvent<'a, R, E = Events> { /// The transaction that caused the event pub tx: &'a TransactionSigned, @@ -28,11 +30,35 @@ pub struct ExtractedEvent<'a, R, E = Events> { pub event: E, } -impl std::ops::Deref for ExtractedEvent<'_, R, E> +impl fmt::Debug for ExtractedEvent<'_, R, E> where - R: TxReceipt, - E: Into, + E: Into + fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExtractedEvent") + .field("tx", &self.tx) + .field("log_index", &self.log_index) + .field("event", &self.event) + .finish_non_exhaustive() + } +} + +// NB: manual impl because of incorrect auto-derive bound on `R: Clone` +impl Clone for ExtractedEvent<'_, R, E> +where + E: Clone, { + fn clone(&self) -> Self { + ExtractedEvent { + tx: self.tx, + receipt: self.receipt, + log_index: self.log_index, + event: self.event.clone(), + } + } +} + +impl std::ops::Deref for ExtractedEvent<'_, R, E> { type Target = E; fn deref(&self) -> &Self::Target { @@ -40,26 +66,24 @@ where } } -impl ExtractedEvent<'_, R, E> -where - R: TxReceipt, - E: Into, -{ +impl ExtractedEvent<'_, R, E> { /// Get the transaction hash of the extracted event. pub fn tx_hash(&self) -> TxHash { *self.tx.hash() } +} +impl ExtractedEvent<'_, R, E> +where + R: TxReceipt, +{ /// Borrow the raw log from the receipt. pub fn raw_log(&self) -> &Log { &self.receipt.logs()[self.log_index] } } -impl<'a, R> ExtractedEvent<'a, R, Events> -where - R: TxReceipt, -{ +impl<'a, R> ExtractedEvent<'a, R, Events> { /// True if the event is an [`Passage::EnterToken`]. pub const fn is_enter_token(&self) -> bool { self.event.is_enter_token() @@ -197,12 +221,12 @@ where } } -impl> ExtractedEvent<'_, R, Transactor::Transact> { +impl ExtractedEvent<'_, R, Transactor::Transact> { /// Create a magic signature for the transact event, containing sender /// information. pub fn magic_sig(&self) -> MagicSig { MagicSig { - ty: MagicSigInfo::Transact { sender: self.sender() }, + ty: MagicSigInfo::Transact { sender: self.event.sender() }, txid: self.tx_hash(), event_idx: self.log_index, } @@ -233,14 +257,14 @@ impl> ExtractedEvent<'_, R, Transactor::Transact> { } } -impl> ExtractedEvent<'_, R, Passage::Enter> { +impl ExtractedEvent<'_, R, Passage::Enter> { /// Get the magic signature for the enter event. pub fn magic_sig(&self) -> MagicSig { MagicSig { ty: MagicSigInfo::Enter, txid: self.tx_hash(), event_idx: self.log_index } } } -impl> ExtractedEvent<'_, R, Passage::EnterToken> { +impl ExtractedEvent<'_, R, Passage::EnterToken> { /// Get the magic signature for the enter token event. pub fn magic_sig(&self) -> MagicSig { MagicSig { ty: MagicSigInfo::EnterToken, txid: self.tx_hash(), event_idx: self.log_index } diff --git a/crates/test-utils/tests/evm.rs b/crates/test-utils/tests/evm.rs index 7f23e550..08f23768 100644 --- a/crates/test-utils/tests/evm.rs +++ b/crates/test-utils/tests/evm.rs @@ -9,7 +9,7 @@ use alloy::{ }; use signet_constants::SignetSystemConstants; use signet_evm::{ - sys::{MintNative, MintToken, MintTokenSysLog}, + sys::{MintNative, MintToken, MintTokenSysLog, SysOutput}, SignetDriver, }; use signet_extract::{Extractable, ExtractedEvent, Extracts}; @@ -200,7 +200,8 @@ fn test_an_enter() { let _trevm = context.trevm().drive_block(&mut driver).unwrap(); let (sealed_block, receipts) = driver.finish(); - let expected_tx = MintToken::from_enter(0, RU_WETH, &extracts.enters[0]).to_transaction(); + let expected_tx = + MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).to_transaction(); assert_eq!(sealed_block.senders.len(), 1); assert_eq!(sealed_block.block.body.transactions().collect::>(), vec![&expected_tx]); @@ -287,8 +288,9 @@ fn test_a_transact() { // 1. MintToken for the enter event // 2. MintNative for the enter token event // 3. Transact for the transact event - let expected_tx_0 = MintToken::from_enter(0, RU_WETH, &extracts.enters[0]).to_transaction(); - let expected_tx_1 = MintNative::new(1, &extracts.enter_tokens[0]).to_transaction(); + let expected_tx_0 = + MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).to_transaction(); + let expected_tx_1 = MintNative::new(&extracts.enter_tokens[0]).with_nonce(1).to_transaction(); let expected_tx_2 = extracts.transacts[0].make_transaction(0); assert_eq!(sealed_block.senders, vec![MINTER_ADDRESS, MINTER_ADDRESS, sender]); diff --git a/crates/zenith/src/trevm.rs b/crates/zenith/src/trevm.rs index 98af4ddb..c99345e7 100644 --- a/crates/zenith/src/trevm.rs +++ b/crates/zenith/src/trevm.rs @@ -1,95 +1,9 @@ -use crate::{Passage::EnterToken, Transactor, Zenith}; -use alloy::{ - primitives::{Address, U256}, - rlp::BufMut, - rpc::types::AccessList, - sol_types::SolCall, -}; -use trevm::{ - journal::{JournalDecode, JournalDecodeError, JournalEncode}, - revm::context::{TransactTo, TransactionType, TxEnv}, - Tx, -}; +use crate::Zenith; +use alloy::rlp::BufMut; +use trevm::journal::{JournalDecode, JournalDecodeError, JournalEncode}; const ZENITH_HEADER_BYTES: usize = 32 + 32 + 32 + 20 + 32; -impl Tx for Transactor::Transact { - fn fill_tx_env(&self, tx_env: &mut TxEnv) { - // destructuring here means that any changes to the fields will result - // in breaking changes here, ensuring that they never silently add new - // fields - let TxEnv { - tx_type, - caller, - gas_limit, - gas_price, - kind, - value, - data, - nonce, - chain_id, - access_list, - gas_priority_fee, - blob_hashes, - max_fee_per_blob_gas, - authorization_list, - } = tx_env; - // TransactionType::Custom disables tx pre-validation - *tx_type = TransactionType::Custom as u8; - *caller = self.sender; - *gas_limit = self.gas.as_limbs()[0]; - *gas_price = self.maxFeePerGas.saturating_to(); - *gas_priority_fee = Some(0); - *kind = TransactTo::Call(self.to); - *value = self.value; - *data = self.data.clone(); - *chain_id = Some(self.rollup_chain_id()); - *nonce = 0; - *access_list = Default::default(); - blob_hashes.clear(); - *max_fee_per_blob_gas = 0; - authorization_list.clear(); - } -} - -impl Tx for EnterToken { - fn fill_tx_env(&self, tx_env: &mut TxEnv) { - let TxEnv { - tx_type, - caller, - gas_limit, - gas_price, - kind, - value, - data, - nonce, - chain_id, - access_list, - gas_priority_fee, - blob_hashes, - max_fee_per_blob_gas, - authorization_list, - } = tx_env; - // TransactionType::Custom disables tx pre-validation - *tx_type = TransactionType::Custom as u8; - *caller = crate::MINTER_ADDRESS; - *gas_limit = 1_000_000; - *gas_price = 0; - // This is deliberately not set, as it is not known by the event. - *kind = Address::ZERO.into(); - *value = U256::ZERO; - *data = - crate::mintCall { amount: self.amount(), to: self.rollupRecipient }.abi_encode().into(); - *nonce = 0; - *chain_id = Some(self.rollup_chain_id()); - *access_list = AccessList::default(); - *gas_priority_fee = Some(0); - blob_hashes.clear(); - *max_fee_per_blob_gas = 0; - authorization_list.clear(); - } -} - impl JournalEncode for Zenith::BlockHeader { fn serialized_size(&self) -> usize { ZENITH_HEADER_BYTES @@ -121,7 +35,7 @@ impl JournalDecode for Zenith::BlockHeader { #[cfg(test)] mod test { use super::*; - use alloy::primitives::B256; + use alloy::primitives::{Address, B256, U256}; fn roundtrip(expected: &T) { let enc = JournalEncode::encoded(expected); From 58be6f9612811a9bc300820309bdb6f47a32dda7 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Jul 2025 16:07:35 -0400 Subject: [PATCH 04/24] chore: improve visibility --- crates/evm/src/sys/native.rs | 13 +++++++++++-- crates/evm/src/sys/token.rs | 2 +- crates/evm/src/sys/transact.rs | 2 +- crates/test-utils/tests/evm.rs | 7 ++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index 0bf42ca8..ea8fc8f1 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -33,6 +33,15 @@ pub struct MintNative { rollup_chain_id: u64, } +impl From<&ExtractedEvent<'_, R, Passage::EnterToken>> for MintNative +where + R: TxReceipt, +{ + fn from(event: &ExtractedEvent<'_, R, Passage::EnterToken>) -> Self { + Self::new(event) + } +} + impl MintNative { /// Create a new [`MintNative`] instance from an [`ExtractedEvent`] /// containing a [`Passage::EnterToken`] event. @@ -49,7 +58,7 @@ impl MintNative { } /// Create a new [`Log`] for the [`MintNative`] operation. - pub const fn to_log(&self) -> MintNativeSysLog { + const fn to_log(&self) -> MintNativeSysLog { MintNativeSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, @@ -59,7 +68,7 @@ impl MintNative { } /// Convert the [`MintNative`] instance into a [`TransactionSigned`]. - pub fn to_transaction(&self) -> TransactionSigned { + fn to_transaction(&self) -> TransactionSigned { TransactionSigned::new_unhashed( Transaction::Eip1559(TxEip1559 { chain_id: self.rollup_chain_id, diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index a4ce5ed8..4a61b683 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -125,7 +125,7 @@ impl MintToken { } /// Convert the [`MintToken`] instance into a [`TransactionSigned`]. - pub fn to_transaction(&self) -> TransactionSigned { + pub(crate) fn to_transaction(&self) -> TransactionSigned { let input = self.mint_call().abi_encode().into(); TransactionSigned::new_unhashed( diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 78b74f90..4880834b 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -37,7 +37,7 @@ impl TransactSysTx { } /// Create a [`TransactSysLog`] from the filler. - pub fn to_log(&self) -> TransactSysLog { + fn to_log(&self) -> TransactSysLog { TransactSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, diff --git a/crates/test-utils/tests/evm.rs b/crates/test-utils/tests/evm.rs index 08f23768..86feee21 100644 --- a/crates/test-utils/tests/evm.rs +++ b/crates/test-utils/tests/evm.rs @@ -201,7 +201,7 @@ fn test_an_enter() { let (sealed_block, receipts) = driver.finish(); let expected_tx = - MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).to_transaction(); + MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).produce_transaction(); assert_eq!(sealed_block.senders.len(), 1); assert_eq!(sealed_block.block.body.transactions().collect::>(), vec![&expected_tx]); @@ -289,8 +289,9 @@ fn test_a_transact() { // 2. MintNative for the enter token event // 3. Transact for the transact event let expected_tx_0 = - MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).to_transaction(); - let expected_tx_1 = MintNative::new(&extracts.enter_tokens[0]).with_nonce(1).to_transaction(); + MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).produce_transaction(); + let expected_tx_1 = + MintNative::new(&extracts.enter_tokens[0]).with_nonce(1).produce_transaction(); let expected_tx_2 = extracts.transacts[0].make_transaction(0); assert_eq!(sealed_block.senders, vec![MINTER_ADDRESS, MINTER_ADDRESS, sender]); From 1b641a6ac56ab7f3168e35e634be8ace37616a6e Mon Sep 17 00:00:00 2001 From: James Date: Tue, 8 Jul 2025 16:17:44 -0400 Subject: [PATCH 05/24] lint: clippy --- crates/evm/src/sys/native.rs | 10 +++++----- crates/evm/src/sys/token.rs | 8 ++++---- crates/evm/src/sys/transact.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index ea8fc8f1..64ddda77 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -58,7 +58,7 @@ impl MintNative { } /// Create a new [`Log`] for the [`MintNative`] operation. - const fn to_log(&self) -> MintNativeSysLog { + const fn make_sys_log(&self) -> MintNativeSysLog { MintNativeSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, @@ -68,7 +68,7 @@ impl MintNative { } /// Convert the [`MintNative`] instance into a [`TransactionSigned`]. - fn to_transaction(&self) -> TransactionSigned { + fn make_transaction(&self) -> TransactionSigned { TransactionSigned::new_unhashed( Transaction::Eip1559(TxEip1559 { chain_id: self.rollup_chain_id, @@ -96,11 +96,11 @@ impl SysOutput for MintNative { } fn produce_transaction(&self) -> TransactionSigned { - self.to_transaction() + self.make_transaction() } fn produce_log(&self) -> Log { - self.to_log().into() + self.make_sys_log().into() } fn sender(&self) -> Address { @@ -128,7 +128,7 @@ impl SysAction for MintNative { alloy::consensus::Receipt { status: true.into(), cumulative_gas_used: cumulative_gas_used.saturating_add(MIN_TRANSACTION_GAS), - logs: vec![self.to_log().into()], + logs: vec![self.make_sys_log().into()], } .with_bloom(), ) diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index 4a61b683..b03fe0fd 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -114,7 +114,7 @@ impl MintToken { } /// Create a new [`Log`] for the [`MintToken`] operation. - const fn to_log(self) -> MintTokenSysLog { + const fn make_sys_log(self) -> MintTokenSysLog { MintTokenSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, @@ -125,7 +125,7 @@ impl MintToken { } /// Convert the [`MintToken`] instance into a [`TransactionSigned`]. - pub(crate) fn to_transaction(&self) -> TransactionSigned { + fn make_transaction(&self) -> TransactionSigned { let input = self.mint_call().abi_encode().into(); TransactionSigned::new_unhashed( @@ -152,11 +152,11 @@ impl SysOutput for MintToken { } fn produce_transaction(&self) -> TransactionSigned { - self.to_transaction() + self.make_transaction() } fn produce_log(&self) -> Log { - self.to_log().into() + self.make_sys_log().into() } fn sender(&self) -> Address { diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 4880834b..f8579b21 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -37,7 +37,7 @@ impl TransactSysTx { } /// Create a [`TransactSysLog`] from the filler. - fn to_log(&self) -> TransactSysLog { + fn make_sys_log(&self) -> TransactSysLog { TransactSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, @@ -92,7 +92,7 @@ impl SysOutput for TransactSysTx { } fn produce_log(&self) -> Log { - self.to_log().into() + self.make_sys_log().into() } fn sender(&self) -> Address { From 63e650706cfbe0b5c13339bc61dfd7e3cb433ce6 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 10:33:41 -0400 Subject: [PATCH 06/24] fix: remove unnecessary bound --- crates/evm/src/sys/logs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/evm/src/sys/logs.rs b/crates/evm/src/sys/logs.rs index 40a96f54..06987c4f 100644 --- a/crates/evm/src/sys/logs.rs +++ b/crates/evm/src/sys/logs.rs @@ -1,4 +1,4 @@ -use alloy::{consensus::TxReceipt, primitives::Log, sol_types::SolEvent}; +use alloy::{primitives::Log, sol_types::SolEvent}; use signet_extract::ExtractedEvent; use signet_zenith::{Transactor, MINTER_ADDRESS}; @@ -46,7 +46,7 @@ impl From for Log { } } -impl> From<&ExtractedEvent<'_, R, Transactor::Transact>> for Transact { +impl From<&ExtractedEvent<'_, R, Transactor::Transact>> for Transact { fn from(event: &ExtractedEvent<'_, R, Transactor::Transact>) -> Self { Transact { sender: event.event.sender, From 0c79bea98da54d9b87cd29c7e199a6872dd90205 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 10:40:19 -0400 Subject: [PATCH 07/24] refactor: rename SysOutput to SysBase --- crates/evm/src/sys/driver.rs | 16 +++++++--------- crates/evm/src/sys/mod.rs | 25 ++++++++++++++----------- crates/evm/src/sys/native.rs | 6 +++--- crates/evm/src/sys/token.rs | 6 +++--- crates/evm/src/sys/transact.rs | 8 ++++---- crates/test-utils/tests/evm.rs | 2 +- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 902e97a4..a070323b 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -2,9 +2,7 @@ use crate::{ driver::ControlFlow, - sys::{ - MeteredSysTx, MintNative, MintToken, SysAction, SysOutput, TransactSysTx, UnmeteredSysTx, - }, + sys::{MeteredSysTx, MintNative, MintToken, SysAction, SysBase, TransactSysTx, UnmeteredSysTx}, EvmNeedsTx, RunTxResult, SignetDriver, }; use alloy::primitives::{map::HashSet, U256}; @@ -29,7 +27,7 @@ fn populate_nonce_from_trevm( where Db: Database + DatabaseCommit, Insp: Inspector>, - S: SysOutput, + S: SysBase, { // If the sys_output already has a nonce, we don't need to populate it. if sys_output.has_nonce() { @@ -38,7 +36,7 @@ where // Read the nonce from the database and populate it in the sys_output. trevm - .try_read_nonce(sys_output.sender()) + .try_read_nonce(sys_output.evm_sender()) .map(|nonce| sys_output.populate_nonce(nonce)) .map_err(EVMError::Database) } @@ -82,7 +80,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { // push receipt and transaction to the block self.processed.push(action.produce_transaction()); self.output - .push_result(action.produce_receipt(self.cumulative_gas_used()), action.sender()); + .push_result(action.produce_receipt(self.cumulative_gas_used()), action.evm_sender()); Ok(trevm) } @@ -268,7 +266,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let acct = t .result_and_state_mut_unchecked() .state - .get_mut(&sys_tx.sender()) + .get_mut(&sys_tx.evm_sender()) .expect("sender account must be in state, as it is touched by definition"); match acct.info.balance.checked_sub(U256::from(to_debit)) { @@ -316,7 +314,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { for mut sys_tx in sys_txs { let span = tracing::debug_span!( "SignetDriver::apply_metered_sys_transactions", - sender = %sys_tx.sender(), + sender = %sys_tx.evm_sender(), gas_limit = sys_tx.gas_limit(), callee = ?sys_tx.callee(), ); @@ -326,7 +324,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let _enter = span.entered(); let nonce = trevm_try!( - trevm.try_read_nonce(sys_tx.sender()).map_err(EVMError::Database), + trevm.try_read_nonce(sys_tx.evm_sender()).map_err(EVMError::Database), trevm ); sys_tx.populate_nonce(nonce); diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index d1c02cb1..c5395bdf 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -26,9 +26,10 @@ use trevm::{ Trevm, Tx, }; -/// Produce a transaction from a system action. This will be ingested into the -/// block during EVM execution. -pub trait SysOutput: fmt::Debug + Clone { +/// [`SysBase`] is the root trait for all system actions and transactions. It +/// provides the basic functionality that the [`SignetDriver`] needs to process +/// system actions and transactions. +pub trait SysBase: fmt::Debug + Clone { /// Check if the system action has a nonce. This is typically used to /// determine if the nonce should be populated by the Evm during /// transaction processing. @@ -57,24 +58,26 @@ pub trait SysOutput: fmt::Debug + Clone { /// of the receipt, and fn produce_log(&self) -> Log; - /// Get the address of the sender of the system action. This is typically - /// the [`MINTER_ADDRESS`] for minting actions, or the address of the - /// system contract caller for other actions. - fn sender(&self) -> Address; + /// Get the address that the Signet EVM considers to be the sender of the + /// system action. This is typically the [`MINTER_ADDRESS`] for token or + /// native asset mints, and the host-chain user address for transact events. + /// + /// [`MINTER_ADDRESS`]: signet_types::constants::MINTER_ADDRESS + fn evm_sender(&self) -> Address; } /// A transaction that is run on the EVM, and may or may not pay gas. /// /// See [`MeteredSysTx`] and [`UnmeteredSysTx`] for more specific /// transaction types. -pub trait SysTx: SysOutput + Tx {} +pub trait SysTx: SysBase + Tx {} /// System actions are operations that apply changes to the EVM state without /// going through the transaction processing pipeline. They are not run as /// transactions, and do not have gas limits or revert semantics. They are /// typically used for operations that need to be applied directly to the state, /// such as modifying balances. -pub trait SysAction: SysOutput { +pub trait SysAction: SysBase { /// Apply the system action to the EVM state. fn apply( &self, @@ -95,7 +98,7 @@ pub trait SysAction: SysOutput { /// and CAN halt. They are typically used for operations that need to be run as /// transactions, but should not pay gas. E.g. minting tokens or performing /// system-level operations that do not require gas payment. -pub trait UnmeteredSysTx: SysOutput + Tx {} +pub trait UnmeteredSysTx: SysBase + Tx {} /// System transactions run on the EVM as a transaction, and are subject to the /// same rules and constraints as regular transactions. They may run arbitrary @@ -108,7 +111,7 @@ pub trait UnmeteredSysTx: SysOutput + Tx {} /// They are distinct from [`SysAction`], which are not run as transactions, /// but rather apply changes to the state directly without going through the /// transaction processing pipeline. -pub trait MeteredSysTx: SysOutput + Tx { +pub trait MeteredSysTx: SysBase + Tx { /// Get the gas limit for the transaction. This is the maximum amount of /// gas that the transaction is allowed to consume. /// diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index 64ddda77..fe5e0c03 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -1,4 +1,4 @@ -use crate::sys::{MintNativeSysLog, SysAction, SysOutput}; +use crate::sys::{MintNativeSysLog, SysAction, SysBase}; use alloy::{ consensus::{ReceiptEnvelope, TxEip1559, TxReceipt}, primitives::{Address, Log, U256}, @@ -86,7 +86,7 @@ impl MintNative { } } -impl SysOutput for MintNative { +impl SysBase for MintNative { fn has_nonce(&self) -> bool { self.nonce.is_some() } @@ -103,7 +103,7 @@ impl SysOutput for MintNative { self.make_sys_log().into() } - fn sender(&self) -> Address { + fn evm_sender(&self) -> Address { MINTER_ADDRESS } } diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index b03fe0fd..b913dbdd 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -1,4 +1,4 @@ -use crate::sys::{MintTokenSysLog, SysOutput, SysTx, UnmeteredSysTx}; +use crate::sys::{MintTokenSysLog, SysBase, SysTx, UnmeteredSysTx}; use alloy::{ consensus::{TxEip1559, TxReceipt}, primitives::{Address, Log, U256}, @@ -146,7 +146,7 @@ impl MintToken { } } -impl SysOutput for MintToken { +impl SysBase for MintToken { fn populate_nonce(&mut self, nonce: u64) { self.nonce = Some(nonce); } @@ -159,7 +159,7 @@ impl SysOutput for MintToken { self.make_sys_log().into() } - fn sender(&self) -> Address { + fn evm_sender(&self) -> Address { MINTER_ADDRESS } diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index f8579b21..8ecfc850 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -1,4 +1,4 @@ -use crate::sys::{MeteredSysTx, SysOutput, SysTx, TransactSysLog}; +use crate::sys::{MeteredSysTx, SysBase, SysTx, TransactSysLog}; use alloy::{ consensus::{EthereumTxEnvelope, Transaction}, primitives::{Address, Log, TxKind, U256}, @@ -41,7 +41,7 @@ impl TransactSysTx { TransactSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, - sender: self.sender(), + sender: self.evm_sender(), value: self.tx.value(), gas: U256::from(self.tx.gas_limit()), maxFeePerGas: U256::from(self.tx.max_fee_per_gas()), @@ -73,7 +73,7 @@ impl Tx for TransactSysTx { } } -impl SysOutput for TransactSysTx { +impl SysBase for TransactSysTx { fn has_nonce(&self) -> bool { self.nonce.is_some() } @@ -95,7 +95,7 @@ impl SysOutput for TransactSysTx { self.make_sys_log().into() } - fn sender(&self) -> Address { + fn evm_sender(&self) -> Address { self.magic_sig.sender() } } diff --git a/crates/test-utils/tests/evm.rs b/crates/test-utils/tests/evm.rs index 86feee21..dffed330 100644 --- a/crates/test-utils/tests/evm.rs +++ b/crates/test-utils/tests/evm.rs @@ -9,7 +9,7 @@ use alloy::{ }; use signet_constants::SignetSystemConstants; use signet_evm::{ - sys::{MintNative, MintToken, MintTokenSysLog, SysOutput}, + sys::{MintNative, MintToken, MintTokenSysLog, SysBase}, SignetDriver, }; use signet_extract::{Extractable, ExtractedEvent, Extracts}; From c00389d1d792a88a7df77c70ecb1723dc946abc1 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 10:44:47 -0400 Subject: [PATCH 08/24] feat: add max_fee_per_gas and max_fee to MeteredSysTx --- crates/evm/src/sys/mod.rs | 21 ++++++++++++++++++++- crates/evm/src/sys/transact.rs | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index c5395bdf..45f6212b 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -16,7 +16,7 @@ pub use transact::TransactSysTx; use alloy::{ consensus::ReceiptEnvelope, - primitives::{Address, Bytes, Log, TxKind}, + primitives::{Address, Bytes, Log, TxKind, U256}, }; use core::fmt; use signet_types::primitives::TransactionSigned; @@ -118,6 +118,25 @@ pub trait MeteredSysTx: SysBase + Tx { /// Metered system transactions ALWAYS consume all gas. fn gas_limit(&self) -> u128; + /// Get the max fee per gas for the transaction. This is the maximum + /// amount of gas that the transaction is willing to pay for each unit of + /// gas consumed. + /// + /// Metered system transactions ALWAYS consume all gas and NEVER pay a tip. + fn max_fee_per_gas(&self) -> u128; + + /// Get the precise total fee for the transaction. This is the product of + /// [`MeteredSysTx::gas_limit`] and [`MeteredSysTx::max_fee_per_gas`]. This + /// is distinct from the actual fee paid, which may be less than this. The + /// actual fee paid is the product of [`MeteredSysTx::gas_limit`] and the + /// current block's basefee. + /// + /// Metered system transactions ALWAYS consume all gas and NEVER pay a tip, + /// so the maximum fee they will pay is known up front. + fn max_fee(&self) -> U256 { + U256::from(self.gas_limit()) * U256::from(self.max_fee_per_gas()) + } + /// Get the callee address for the transaction. fn callee(&self) -> TxKind; diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 8ecfc850..80585718 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -107,6 +107,10 @@ impl MeteredSysTx for TransactSysTx { self.tx.gas_limit() as u128 } + fn max_fee_per_gas(&self) -> u128 { + self.tx.max_fee_per_gas() as u128 + } + fn callee(&self) -> TxKind { self.tx.kind() } From d9c9be3367e238ac033bcd99d47e4230d5d05c68 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 12:27:15 -0400 Subject: [PATCH 09/24] refactor: remove pecorino --- crates/constants/src/chains/mod.rs | 2 - crates/constants/src/chains/pecorino.rs | 92 ------ crates/constants/src/chains/test_utils.rs | 42 ++- crates/constants/src/lib.rs | 7 +- crates/constants/src/types/chains.rs | 3 - crates/constants/src/types/environment.rs | 6 - crates/constants/src/types/host.rs | 22 +- crates/constants/src/types/mod.rs | 49 +-- crates/constants/src/types/rollup.rs | 14 +- crates/constants/src/types/tokens.rs | 365 +++++++++++++++++----- crates/evm/src/sys/driver.rs | 2 +- crates/extract/src/extractor.rs | 2 +- crates/rpc/examples/order.rs | 13 +- crates/rpc/src/ctx.rs | 6 +- crates/sim/src/env.rs | 2 +- crates/test-utils/src/specs/host_spec.rs | 8 +- crates/test-utils/src/specs/ru_spec.rs | 6 - crates/tx-cache/Cargo.toml | 1 - crates/tx-cache/src/client.rs | 13 - 19 files changed, 363 insertions(+), 292 deletions(-) delete mode 100644 crates/constants/src/chains/pecorino.rs diff --git a/crates/constants/src/chains/mod.rs b/crates/constants/src/chains/mod.rs index d4742966..75854d38 100644 --- a/crates/constants/src/chains/mod.rs +++ b/crates/constants/src/chains/mod.rs @@ -1,4 +1,2 @@ -pub mod pecorino; - #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; diff --git a/crates/constants/src/chains/pecorino.rs b/crates/constants/src/chains/pecorino.rs deleted file mode 100644 index 75121535..00000000 --- a/crates/constants/src/chains/pecorino.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Constants for the Pecorino testnet. - -use crate::{ - HostConstants, PredeployTokens, RollupConstants, SignetConstants, SignetEnvironmentConstants, - SignetSystemConstants, -}; -use alloy::primitives::{address, Address}; -use std::borrow::Cow; - -/// Name for the host chain. -pub const HOST_NAME: &str = "Pecorino Host"; -/// Chain ID for the Pecorino testnet host chain. -pub const HOST_CHAIN_ID: u64 = 3151908; -/// Deployment height for the Pecorino testnet host chain. -pub const DEPLOY_HEIGHT: u64 = 149984; -/// `Zenith` contract address for the Pecorino testnet host chain. -pub const HOST_ZENITH: Address = address!("0xbe45611502116387211D28cE493D6Fb3d192bc4E"); -/// `Orders` contract address for the Pecorino testnet host chain. -pub const HOST_ORDERS: Address = address!("0x4E8cC181805aFC307C83298242271142b8e2f249"); -/// `Passage` contract address for the Pecorino testnet host chain. -pub const HOST_PASSAGE: Address = address!("0xd553C4CA4792Af71F4B61231409eaB321c1Dd2Ce"); -/// `Transactor` contract address for the Pecorino testnet host chain. -pub const HOST_TRANSACTOR: Address = address!("0x1af3A16857C28917Ab2C4c78Be099fF251669200"); - -/// USDC token for the Pecorino testnet host chain. -pub const HOST_USDC: Address = address!("0x885F8DB528dC8a38aA3DDad9D3F619746B4a6A81"); -/// USDT token for the Pecorino testnet host chain. -pub const HOST_USDT: Address = address!("0x7970D259D4a96764Fa9B23FF0715A35f06f52D1A"); -/// WBTC token for the Pecorino testnet host chain. -pub const HOST_WBTC: Address = address!("0x9aeDED4224f3dD31aD8A0B1FcD05E2d7829283a7"); -/// WETH token for the Pecorino testnet host chain. -pub const HOST_WETH: Address = Address::ZERO; - -/// USDC token for the Pecorino testnet RU chain. -pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e2"); -/// USDT token for the Pecorino testnet RU chain. -pub const RU_USDT: Address = address!("0xF34326d3521F1b07d1aa63729cB14A372f8A737C"); -/// WBTC token for the Pecorino testnet RU chain. -pub const RU_WBTC: Address = address!("0xE3d7066115f7d6b65F88Dff86288dB4756a7D733"); -/// WETH token for the Pecorino testnet RU chain. -pub const RU_WETH: Address = Address::ZERO; - -/// Name for the network. -pub const RU_NAME: &str = "Pecorino"; -/// Chain ID for the Pecorino testnet RU chain. -pub const RU_CHAIN_ID: u64 = 14174; -/// `Orders` contract address for the Pecorino testnet RU chain. -pub const RU_ORDERS: Address = address!("0xC2D3Dac6B115564B10329697195656459BFb2c74"); -/// `Passage` contract address for the Pecorino testnet RU chain. -/// This is currently a dummy value and will be replaced with the actual Passage contract address in the future. -pub const RU_PASSAGE: Address = Address::repeat_byte(0xff); -/// Base fee recipient address for the Pecorino testnet RU chain. -pub const BASE_FEE_RECIPIENT: Address = address!("0xe0eDA3701D44511ce419344A4CeD30B52c9Ba231"); - -/// Host system tokens for Pecorino. -pub const HOST_TOKENS: PredeployTokens = - crate::PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC, HOST_WETH); - -/// RU system tokens for Pecorino. -pub const RU_TOKENS: PredeployTokens = - crate::PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC, HOST_WETH); - -/// The URL of the Transaction Cache endpoint. -pub const TX_CACHE_URL: &str = "https://transactions.pecorino.signet.sh"; - -/// Host system constants for Pecorino. -pub const HOST: HostConstants = crate::HostConstants::new( - HOST_CHAIN_ID, - DEPLOY_HEIGHT, - HOST_ZENITH, - HOST_ORDERS, - HOST_PASSAGE, - HOST_TRANSACTOR, - HOST_TOKENS, -); - -/// RU system constants for Pecorino. -pub const ROLLUP: RollupConstants = - crate::RollupConstants::new(RU_CHAIN_ID, RU_ORDERS, RU_PASSAGE, BASE_FEE_RECIPIENT, RU_TOKENS); - -/// Signet system constants for Pecorino. -pub const PECORINO_SYS: SignetSystemConstants = crate::SignetSystemConstants::new(HOST, ROLLUP); - -/// Signet environment constants for Pecorino. -pub const PECORINO_ENV: SignetEnvironmentConstants = SignetEnvironmentConstants::new( - Cow::Borrowed(HOST_NAME), - Cow::Borrowed(RU_NAME), - Cow::Borrowed(TX_CACHE_URL), -); - -/// Signet constants for Pecorino. -pub const PECORINO: SignetConstants = SignetConstants::new(PECORINO_SYS, PECORINO_ENV); diff --git a/crates/constants/src/chains/test_utils.rs b/crates/constants/src/chains/test_utils.rs index 866e22e1..e37d0f85 100644 --- a/crates/constants/src/chains/test_utils.rs +++ b/crates/constants/src/chains/test_utils.rs @@ -1,8 +1,11 @@ //! Constants for local testnet chains. use crate::{ - HostConstants, PredeployTokens, RollupConstants, SignetConstants, SignetEnvironmentConstants, - SignetSystemConstants, + types::{ + HostConstants, HostTokens, HostUsdRecord, RollupConstants, RollupTokens, SignetConstants, + SignetEnvironmentConstants, SignetSystemConstants, + }, + UsdRecords, }; use alloy::primitives::{address, Address}; use std::borrow::Cow; @@ -25,23 +28,33 @@ pub const HOST_PASSAGE: Address = Address::repeat_byte(0x33); /// Test address for the host transactor pub const HOST_TRANSACTOR: Address = Address::repeat_byte(0x44); -/// Test address for predeployed USDC +/// Test address for host USDC pub const HOST_USDC: Address = Address::repeat_byte(0x55); -/// Test address for predeployed USDT +/// Test address for host USDT pub const HOST_USDT: Address = Address::repeat_byte(0x66); -/// Test address for predeployed WBTC +/// Test address for host WBTC pub const HOST_WBTC: Address = Address::repeat_byte(0x77); -/// Test address for predeployed WETH +/// Test address for host WETH pub const HOST_WETH: Address = Address::repeat_byte(0x88); -/// Test address for predeployed USDC -pub const RU_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e2"); -/// Test address for predeployed USDT -pub const RU_USDT: Address = address!("0xF34326d3521F1b07d1aa63729cB14A372f8A737C"); +/// Test record for host USDC. +pub const USDC_RECORD: HostUsdRecord = HostUsdRecord::new(HOST_USDC, Cow::Borrowed("USDC"), 6); + +/// Test record for host USDT. +pub const USDT_RECORD: HostUsdRecord = HostUsdRecord::new(HOST_USDT, Cow::Borrowed("USDT"), 12); + +/// Test records for host USD tokens. +pub const HOST_USDS: UsdRecords = { + let mut records = UsdRecords::new(); + records.push(USDC_RECORD); + records.push(USDT_RECORD); + records +}; + /// Test address for predeployed WBTC -pub const RU_WBTC: Address = address!("0xE3d7066115f7d6b65F88Dff86288dB4756a7D733"); +pub const RU_WBTC: Address = Address::repeat_byte(0x99); /// Test address for predeployed WETH -pub const RU_WETH: Address = Address::repeat_byte(0x99); +pub const RU_WETH: Address = Address::repeat_byte(0xaa); /// Name for the network. pub const RU_NAME: &str = "Test Rollup"; @@ -55,11 +68,10 @@ pub const RU_PASSAGE: Address = address!("0xB043BdD3d91376A76078c361bb82496Fdb80 pub const BASE_FEE_RECIPIENT: Address = Address::repeat_byte(0xab); /// Host system tokens. -pub const HOST_TOKENS: PredeployTokens = - PredeployTokens::new(HOST_USDC, HOST_USDT, HOST_WBTC, HOST_WETH); +pub const HOST_TOKENS: HostTokens = HostTokens::new(HOST_USDS, HOST_WBTC, HOST_WETH); /// RU system tokens. -pub const RU_TOKENS: PredeployTokens = PredeployTokens::new(RU_USDC, RU_USDT, RU_WBTC, RU_WETH); +pub const RU_TOKENS: RollupTokens = RollupTokens::new(RU_WBTC, RU_WETH); /// The URL of the Transaction Cache endpoint. pub const TX_CACHE_URL: &str = "localhost:8080/txcache"; diff --git a/crates/constants/src/lib.rs b/crates/constants/src/lib.rs index 121fbfcf..f2af77de 100644 --- a/crates/constants/src/lib.rs +++ b/crates/constants/src/lib.rs @@ -17,15 +17,14 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] mod chains; -pub use chains::pecorino; #[cfg(any(test, feature = "test-utils"))] pub use chains::test_utils; mod types; pub use types::{ - ConfigError, HostConstants, KnownChains, PairedHeights, ParseChainError, PermissionedToken, - PredeployTokens, RollupConstants, SignetConstants, SignetEnvironmentConstants, - SignetSystemConstants, MINTER_ADDRESS, + ConfigError, HostConstants, HostPermitted, HostTokens, HostUsdRecord, KnownChains, + PairedHeights, ParseChainError, RollupConstants, RollupPermitted, RollupTokens, + SignetConstants, SignetEnvironmentConstants, SignetSystemConstants, UsdRecords, MINTER_ADDRESS, }; /// Placeholder address for ETH. diff --git a/crates/constants/src/types/chains.rs b/crates/constants/src/types/chains.rs index 139f07ab..92563d1c 100644 --- a/crates/constants/src/types/chains.rs +++ b/crates/constants/src/types/chains.rs @@ -14,8 +14,6 @@ pub enum ParseChainError { /// Known chains for the Signet system. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum KnownChains { - /// Pecorino chain. - Pecorino, /// Test chain. #[cfg(any(test, feature = "test-utils"))] Test, @@ -27,7 +25,6 @@ impl FromStr for KnownChains { fn from_str(s: &str) -> Result { let s = s.trim().to_lowercase(); match s.as_str() { - "pecorino" => Ok(Self::Pecorino), #[cfg(any(test, feature = "test-utils"))] "test" => Ok(Self::Test), _ => Err(ParseChainError::ChainNotSupported(s)), diff --git a/crates/constants/src/types/environment.rs b/crates/constants/src/types/environment.rs index 9968fd79..7eecbefd 100644 --- a/crates/constants/src/types/environment.rs +++ b/crates/constants/src/types/environment.rs @@ -22,11 +22,6 @@ impl SignetEnvironmentConstants { Self { host_name, rollup_name, transaction_cache } } - /// Get the hard-coded pecorino rollup constants. - pub const fn pecorino() -> Self { - crate::chains::pecorino::PECORINO_ENV - } - /// Get the hard-coded local test rollup constants. #[cfg(any(test, feature = "test-utils"))] pub const fn test() -> Self { @@ -55,7 +50,6 @@ impl FromStr for SignetEnvironmentConstants { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), #[cfg(any(test, feature = "test-utils"))] KnownChains::Test => Ok(Self::test()), } diff --git a/crates/constants/src/types/host.rs b/crates/constants/src/types/host.rs index 6c660d6c..4469389d 100644 --- a/crates/constants/src/types/host.rs +++ b/crates/constants/src/types/host.rs @@ -1,4 +1,4 @@ -use crate::types::{ConfigError, KnownChains, ParseChainError, PredeployTokens}; +use crate::types::{ConfigError, HostTokens, KnownChains, ParseChainError}; use alloy::{genesis::Genesis, primitives::Address}; use serde_json::Value; use std::str::FromStr; @@ -8,7 +8,7 @@ use std::str::FromStr; /// These are system constants which may vary between chains, and are used to /// determine the behavior of the chain, such as which contracts the Signet /// node should listen to, and the addresses of system-priveleged tokens. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct HostConstants { /// Host chain ID. @@ -24,7 +24,7 @@ pub struct HostConstants { /// Host address for the transactor contract transactor: Address, /// Host chain tokens that are predeployed on the rollup. - tokens: PredeployTokens, + tokens: HostTokens, } impl std::fmt::Display for HostConstants { @@ -46,16 +46,11 @@ impl HostConstants { orders: Address, passage: Address, transactor: Address, - tokens: PredeployTokens, + tokens: HostTokens, ) -> Self { Self { chain_id, deploy_height, zenith, orders, passage, transactor, tokens } } - /// Get the hard-coded pecorino host constants. - pub const fn pecorino() -> Self { - crate::chains::pecorino::HOST - } - /// Get the hard-coded local test host constants. #[cfg(any(test, feature = "test-utils"))] pub const fn test() -> Self { @@ -121,13 +116,13 @@ impl HostConstants { } /// Get the host tokens. - pub const fn tokens(&self) -> PredeployTokens { - self.tokens + pub const fn tokens(&self) -> &HostTokens { + &self.tokens } /// Return true if the address is an approved USD token. - pub const fn is_usd(&self, address: Address) -> bool { - address.const_eq(&self.tokens.usdc()) + pub fn is_usd(&self, address: Address) -> bool { + self.tokens.is_usd(address) } } @@ -137,7 +132,6 @@ impl FromStr for HostConstants { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), #[cfg(any(test, feature = "test-utils"))] KnownChains::Test => Ok(Self::test()), } diff --git a/crates/constants/src/types/mod.rs b/crates/constants/src/types/mod.rs index ef435b92..a5f1c459 100644 --- a/crates/constants/src/types/mod.rs +++ b/crates/constants/src/types/mod.rs @@ -14,7 +14,9 @@ mod chains; pub use chains::{KnownChains, ParseChainError}; mod tokens; -pub use tokens::{PermissionedToken, PredeployTokens}; +pub use tokens::{ + HostPermitted, HostTokens, HostUsdRecord, RollupPermitted, RollupTokens, UsdRecords, +}; mod environment; pub use environment::SignetEnvironmentConstants; @@ -31,7 +33,7 @@ use std::str::FromStr; /// information about the host and rollup state. These constants are used to /// determine the behavior of the chain, such as which contracts the Signet /// node should listen to, and the addresses of system-priveleged tokens. -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct SignetSystemConstants { /// Host constants. host: HostConstants, @@ -45,11 +47,6 @@ impl SignetSystemConstants { Self { host, rollup } } - /// Get the hard-coded pecorino system constants. - pub const fn pecorino() -> Self { - crate::chains::pecorino::PECORINO_SYS - } - /// Get the hard-coded local test constants. #[cfg(any(test, feature = "test-utils"))] pub const fn test() -> Self { @@ -65,8 +62,8 @@ impl SignetSystemConstants { } /// Get the host addresses. - pub const fn host(&self) -> HostConstants { - self.host + pub const fn host(&self) -> &HostConstants { + &self.host } /// Get the rollup addresses. @@ -96,7 +93,7 @@ impl SignetSystemConstants { /// True if the address is a host USD that can be used to mint rollup /// native asset. - pub const fn is_host_usd(&self, address: Address) -> bool { + pub fn is_host_usd(&self, address: Address) -> bool { self.host.is_usd(address) } @@ -189,12 +186,6 @@ impl SignetSystemConstants { U256::from(self.ru_chain_id()) } - /// `True` if the address is a host token corresponding to a pre-deployed - /// token on the rollup. - pub const fn const_is_host_token(&self, address: Address) -> bool { - self.host.tokens().const_is_token(address) - } - /// `True` if the address is a host token corresponding to a pre-deployed /// token on the rollup. pub fn is_host_token(&self, address: Address) -> bool { @@ -202,19 +193,16 @@ impl SignetSystemConstants { } /// `True` if the address is a pre-deployed token on the rollup. - pub const fn const_is_rollup_token(&self, address: Address) -> bool { - self.rollup.tokens().const_is_token(address) - } - /// `True` if the address is a pre-deployed token on the rollup. - pub fn is_rollup_token(&self, address: Address) -> bool { + pub const fn is_rollup_token(&self, address: Address) -> bool { self.rollup.tokens().is_token(address) } /// Get the rollup token address corresponding to a host address. /// /// Returns `None` if the address is not a pre-deployed token. - pub fn rollup_token_from_host_address(&self, host_address: Address) -> Option
{ - self.host.tokens().token_for(host_address).map(|t| self.rollup.tokens().address_for(t)) + pub fn rollup_address_from_host_address(&self, host_address: Address) -> Option
{ + let perm = self.host.tokens().token_for(host_address)?; + Some(self.rollup.tokens().address_for(perm.into())) } /// Get the minter address. @@ -229,7 +217,6 @@ impl FromStr for SignetSystemConstants { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), #[cfg(any(test, feature = "test-utils"))] KnownChains::Test => Ok(Self::test()), } @@ -254,11 +241,6 @@ impl SignetConstants { Self { system, environment } } - /// Get the hard-coded pecorino rollup constants. - pub const fn pecorino() -> Self { - crate::chains::pecorino::PECORINO - } - /// Get the hard-coded local test rollup constants. #[cfg(any(test, feature = "test-utils"))] pub const fn test() -> Self { @@ -266,13 +248,13 @@ impl SignetConstants { } /// Get the system constants. - pub const fn system(&self) -> SignetSystemConstants { - self.system + pub const fn system(&self) -> &SignetSystemConstants { + &self.system } /// Get the host constants. - pub const fn host(&self) -> HostConstants { - self.system.host + pub const fn host(&self) -> &HostConstants { + &self.system.host } /// Get the rollup constants. @@ -292,7 +274,6 @@ impl FromStr for SignetConstants { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), #[cfg(any(test, feature = "test-utils"))] KnownChains::Test => Ok(Self::test()), } diff --git a/crates/constants/src/types/rollup.rs b/crates/constants/src/types/rollup.rs index 20a2de14..6b22ae77 100644 --- a/crates/constants/src/types/rollup.rs +++ b/crates/constants/src/types/rollup.rs @@ -1,4 +1,4 @@ -use crate::types::{ConfigError, KnownChains, ParseChainError, PredeployTokens}; +use crate::types::{ConfigError, KnownChains, ParseChainError, RollupTokens}; use alloy::{ genesis::Genesis, primitives::{address, Address}, @@ -28,7 +28,7 @@ pub struct RollupConstants { /// Address of the base fee recipient. base_fee_recipient: Address, /// Address of the pre-deployed tokens. - tokens: PredeployTokens, + tokens: RollupTokens, } impl RollupConstants { @@ -38,16 +38,11 @@ impl RollupConstants { orders: Address, passage: Address, base_fee_recipient: Address, - tokens: PredeployTokens, + tokens: RollupTokens, ) -> Self { Self { chain_id, orders, passage, base_fee_recipient, tokens } } - /// Get the hard-coded pecorino rollup constants. - pub const fn pecorino() -> Self { - crate::chains::pecorino::ROLLUP - } - /// Get the hard-coded local test rollup constants. #[cfg(any(test, feature = "test-utils"))] pub const fn test() -> Self { @@ -97,7 +92,7 @@ impl RollupConstants { } /// Get the address of the pre-deployed tokens. - pub const fn tokens(&self) -> PredeployTokens { + pub const fn tokens(&self) -> RollupTokens { self.tokens } @@ -113,7 +108,6 @@ impl FromStr for RollupConstants { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), #[cfg(any(test, feature = "test-utils"))] KnownChains::Test => Ok(Self::test()), } diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index d3358aba..f74147f5 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -1,11 +1,12 @@ -use alloy::primitives::Address; - use crate::ETH_ADDRESS; +use alloy::primitives::Address; +use serde::ser::SerializeSeq; +use std::{borrow::Cow, mem::MaybeUninit}; -/// Rollup pre-deploy tokens. +/// Permissioned tokens for the host chain. #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[non_exhaustive] -pub enum PermissionedToken { +pub enum HostPermitted { /// USDC Usdc, /// USDT @@ -16,115 +17,329 @@ pub enum PermissionedToken { Weth, } -/// Rollup configuration pre-deploy tokens. -#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PredeployTokens { - /// USDC. - usdc: Address, - /// USDT. - usdt: Address, - /// WBTC. - wbtc: Address, - /// ETH or WETH - weth: Address, +/// Permissioned tokens for the rollup. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum RollupPermitted { + /// USD (Native Asset) + USD, + /// WBTC + Wbtc, + /// WETH + Weth, } -impl PredeployTokens { - /// Create a new pre-deploy tokens configuration. - pub const fn new(usdc: Address, usdt: Address, wbtc: Address, weth: Address) -> Self { - Self { usdc, usdt, wbtc, weth } +impl From for RollupPermitted { + fn from(value: HostPermitted) -> Self { + match value { + HostPermitted::Usdc => RollupPermitted::USD, + HostPermitted::Usdt => RollupPermitted::USD, + HostPermitted::Wbtc => RollupPermitted::Wbtc, + HostPermitted::Weth => RollupPermitted::Weth, + } } +} - /// Get the hard-coded pecorino host tokens. - pub const fn pecorino_host() -> Self { - crate::chains::pecorino::HOST_TOKENS +/// A host-chain USD record, which contains the address of the token and its +/// decimals./s +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize, Hash)] +pub struct HostUsdRecord { + /// Host chain address of the USD token. + address: Address, + /// Name of the USD token. + ticker: Cow<'static, str>, + /// Decimals of the USD token. + decimals: u8, +} + +impl HostUsdRecord { + /// Create a new host USD record. + pub const fn new(address: Address, ticker: Cow<'static, str>, decimals: u8) -> Self { + Self { address, ticker, decimals } } - /// Get the hard-coded local test rollup tokens. - pub const fn pecorino_rollup() -> Self { - crate::chains::pecorino::RU_TOKENS + /// Get the address of the USD token. + pub const fn address(&self) -> Address { + self.address } - /// Get the hard-coded local test host tokens. - #[cfg(any(test, feature = "test-utils"))] - pub const fn test_host() -> Self { - crate::chains::test_utils::HOST_TOKENS + /// Get the ticker of the USD token. + pub fn ticker(&self) -> &str { + &self.ticker } - /// Get the hard-coded local test rollup tokens. - #[cfg(any(test, feature = "test-utils"))] - pub const fn test_rollup() -> Self { - crate::chains::test_utils::RU_TOKENS + /// Get the decimals of the USD token. + pub const fn decimals(&self) -> u8 { + self.decimals } +} - /// Get the token for the given address. - pub const fn const_token_for(&self, address: Address) -> Option { - if address.const_eq(&self.usdc) { - Some(PermissionedToken::Usdc) - } else if address.const_eq(&self.usdt) { - Some(PermissionedToken::Usdt) - } else if address.const_eq(&self.wbtc) { - Some(PermissionedToken::Wbtc) - } else if address.const_eq(&self.weth) || address.const_eq(Ð_ADDRESS) { - Some(PermissionedToken::Weth) - } else { - None - } +/// Host tokens configuration for the rollup. +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct HostTokens { + usds: UsdRecords, + wbtc: Address, + weth: Address, +} + +impl HostTokens { + /// Instantiate a new host tokens configuration. + pub const fn new(usds: UsdRecords, wbtc: Address, weth: Address) -> Self { + Self { usds, wbtc, weth } + } + + /// Get the USD record for the given address, if it is a USD token. + pub fn usd_record(&self, address: Address) -> Option<&HostUsdRecord> { + self.usds.iter().find(|record| record.address == address) } - /// Get the token for the given address. - pub fn token_for(&self, address: Address) -> Option { - if address == self.usdc { - Some(PermissionedToken::Usdc) - } else if address == self.usdt { - Some(PermissionedToken::Usdt) - } else if address == self.wbtc { - Some(PermissionedToken::Wbtc) + /// Check if the address is an approved USD token. + pub fn is_usd(&self, address: Address) -> bool { + self.usd_record(address).is_some() + } + + /// Get the decimals for the given address, if the address is a USD token. + pub fn decimals_for(&self, address: Address) -> Option { + self.usd_record(address).map(|r| r.decimals) + } + + /// Returns true if the token is a permitted host token. + pub fn is_host_token(&self, address: Address) -> bool { + address == self.wbtc || address == self.weth || self.usd_record(address).is_some() + } + + /// Get the [`HostPermitted`] for the given address, if it is a + /// permitted token. + pub fn token_for(&self, address: Address) -> Option { + if address == self.wbtc { + Some(HostPermitted::Wbtc) } else if address == self.weth || address == ETH_ADDRESS { - Some(PermissionedToken::Weth) + Some(HostPermitted::Weth) + } else if let Some(record) = self.usd_record(address) { + match record.ticker.as_ref() { + "USDC" => Some(HostPermitted::Usdc), + "USDT" => Some(HostPermitted::Usdt), + _ => None, + } } else { None } } - /// True if the address is a token. - pub const fn const_is_token(&self, address: Address) -> bool { - self.const_token_for(address).is_some() - } - - /// True if the address is a token. + /// Check if the address is a permitted token. pub fn is_token(&self, address: Address) -> bool { self.token_for(address).is_some() } /// Get the address for the given token. - pub const fn address_for(&self, token: PermissionedToken) -> Address { + pub fn address_for(&self, token: HostPermitted) -> Address { match token { - PermissionedToken::Usdc => self.usdc, - PermissionedToken::Usdt => self.usdt, - PermissionedToken::Wbtc => self.wbtc, - PermissionedToken::Weth => self.weth, + HostPermitted::Usdc => self + .usds + .iter() + .find_map( + |record| if record.ticker == "USDC" { Some(record.address) } else { None }, + ) + .expect("must contain USDC"), + HostPermitted::Usdt => self + .usds + .iter() + .find_map( + |record| if record.ticker == "USDT" { Some(record.address) } else { None }, + ) + .expect("must contain USDT"), + HostPermitted::Wbtc => self.wbtc, + HostPermitted::Weth => self.weth, } } - /// Get the address of the USDC token. - pub const fn usdc(&self) -> Address { - self.usdc + /// Get the host address for USDC. + pub fn usdc(&self) -> Address { + self.address_for(HostPermitted::Usdc) + } + + /// Get the host address for USDT. + pub fn usdt(&self) -> Address { + self.address_for(HostPermitted::Usdt) + } + + /// Get the host address for WBTC. + pub fn wbtc(&self) -> Address { + self.address_for(HostPermitted::Wbtc) + } + + /// Get the host address for WETH. + pub fn weth(&self) -> Address { + self.address_for(HostPermitted::Weth) } +} + +/// Rollup tokens configuration for the rollup. +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct RollupTokens { + wbtc: Address, + weth: Address, +} - /// Get the address of the USDT token. - pub const fn usdt(&self) -> Address { - self.usdt +impl RollupTokens { + /// Instantiate a new rollup tokens configuration. + pub const fn new(wbtc: Address, weth: Address) -> Self { + Self { wbtc, weth } } - /// Get the address of the WBTC token. + /// Get the address of the WBTC token on the rollup chain. pub const fn wbtc(&self) -> Address { self.wbtc } - /// Get the address of the WETH token. + /// Get the address of the WETH token on the rollup chain. pub const fn weth(&self) -> Address { self.weth } + + /// Get the [`RollupPermitted`] for the given address, if it is a + /// permitted token. + pub const fn token_for(&self, address: Address) -> Option { + if address.const_eq(&self.wbtc) { + Some(RollupPermitted::Wbtc) + } else if address.const_eq(&self.weth) || address.const_eq(Ð_ADDRESS) { + Some(RollupPermitted::Weth) + } else { + None + } + } + + /// Check if the provided address is a predeployed token contract on the + /// rollup. + pub const fn is_token(&self, address: Address) -> bool { + self.token_for(address).is_some() + } + + /// Get the address for the given token. + pub const fn address_for(&self, token: RollupPermitted) -> Address { + match token { + RollupPermitted::Wbtc => self.wbtc, + RollupPermitted::Weth => self.weth, + RollupPermitted::USD => ETH_ADDRESS, + } + } +} + +const USD_RECORD_CAPACITY: usize = 5; + +/// A set of USD records, which can hold up to 5 records. 5 is chosen as a +/// reasonable limit for the number of USD tokens that can be predeployed on +/// the host chain. This limit is arbitrary but should be sufficient for most +/// use cases, and can be adjusted trivially if needed. +#[derive(Debug)] +pub struct UsdRecords { + len: usize, + data: [MaybeUninit; USD_RECORD_CAPACITY], +} + +impl Clone for UsdRecords { + fn clone(&self) -> Self { + let mut data = [const { MaybeUninit::uninit() }; USD_RECORD_CAPACITY]; + for (idx, item) in self.iter().enumerate() { + data[idx].write(item.clone()); + } + + Self { len: self.len, data } + } +} + +impl PartialEq for UsdRecords { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +impl Eq for UsdRecords {} + +impl UsdRecords { + /// Create a new empty set of USD records. + pub const fn new() -> Self { + Self { len: 0, data: [const { MaybeUninit::uninit() }; USD_RECORD_CAPACITY] } + } + + /// Get the number of USD records in the set. + pub const fn len(&self) -> usize { + self.len + } + + /// Get the maximum capacity of the set. + pub const fn capacity(&self) -> usize { + USD_RECORD_CAPACITY + } + + /// Check if the set is empty. + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Push a new USD record into the set, if there is space. Panic otherwise + pub const fn push(&mut self, record: HostUsdRecord) { + if self.len < self.capacity() { + self.data[self.len].write(record); + self.len += 1; + } else { + panic!("Cannot push more than 5 USD records"); + } + } + + /// Get the record at the specified index, if it exists. + pub const fn get(&self, index: usize) -> Option { + if index < self.len { + // SAFETY: We ensure that the data is initialized when pushing records. + Some(unsafe { self.data[index].assume_init_read() }) + } else { + None + } + } + + /// Get a slice of the USD records. + pub fn as_slice(&self) -> &[HostUsdRecord] { + // SAFETY: We ensure that the data is initialized when pushing records. + unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const HostUsdRecord, self.len) } + } + + /// Get an iterator over the USD records. + pub fn iter(&self) -> impl Iterator { + self.data[..self.len].iter().map(|init| { + // SAFETY: We ensure that the data is initialized when pushing records. + unsafe { init.assume_init_ref() } + }) + } +} + +impl serde::Serialize for UsdRecords { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.len))?; + + for element in self.iter() { + seq.serialize_element(element)?; + } + + seq.end() + } +} + +impl<'de> serde::Deserialize<'de> for UsdRecords { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let vec: Vec = Vec::deserialize(deserializer)?; + if vec.len() > 5 { + return Err(serde::de::Error::custom("Too many USD records")); + } + let mut records = UsdRecords::new(); + for record in vec { + records.push(record); + } + Ok(records) + } } diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index a070323b..a8832d34 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -415,7 +415,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { // All other tokens are non-native mints let ru_token_addr = self .constants - .rollup_token_from_host_address(e.event.token) + .rollup_address_from_host_address(e.event.token) .expect("token enters must be permissioned"); let mut mint = MintToken::from_enter_token(ru_token_addr, e); mint.populate_nonce(nonce); diff --git a/crates/extract/src/extractor.rs b/crates/extract/src/extractor.rs index 4deab74c..f22485fd 100644 --- a/crates/extract/src/extractor.rs +++ b/crates/extract/src/extractor.rs @@ -12,7 +12,7 @@ use signet_types::constants::SignetSystemConstants; /// represented as [`ExtractedEvent`] objects containing [`Events`]. One /// [`Extracts`] will be produced for each block in the input chain, provided /// that Signet was deployed at that height. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct Extractor { constants: SignetSystemConstants, } diff --git a/crates/rpc/examples/order.rs b/crates/rpc/examples/order.rs index 0e2ad7d1..f85923f3 100644 --- a/crates/rpc/examples/order.rs +++ b/crates/rpc/examples/order.rs @@ -1,10 +1,11 @@ use alloy::{ + consensus::constants::GWEI_TO_WEI, primitives::{uint, U256}, signers::Signer, }; use chrono::Utc; use eyre::Error; -use signet_constants::SignetConstants; +use signet_constants::{SignetConstants, ETH_ADDRESS}; use signet_tx_cache::client::TxCache; use signet_types::UnsignedOrder; use signet_zenith::RollupOrders::{Input, Order, Output}; @@ -61,10 +62,14 @@ where /// Get an example Order which swaps 1 USDC on the rollup for 1 USDC on the host. fn example_order(&self) -> Order { - // input is 1 USDC on the rollup - let input = Input { token: self.constants.rollup().tokens().usdc(), amount: ONE_USDC }; + let amount = U256::from(GWEI_TO_WEI); - // output is 1 USDC on the host chain + // input is 1 USD on the rollup + let input = Input { token: ETH_ADDRESS, amount }; + + // output is 1 USDC on the host chain. + // NB: decimals are important! USDC has 6 decimals, while Signet's USD + // native asset has 18. let output = Output { token: self.constants.host().tokens().usdc(), amount: ONE_USDC, diff --git a/crates/rpc/src/ctx.rs b/crates/rpc/src/ctx.rs index 95ce2ee0..7ce57ea5 100644 --- a/crates/rpc/src/ctx.rs +++ b/crates/rpc/src/ctx.rs @@ -172,7 +172,7 @@ where let db = self.signet.state_provider_database(height)?; - let mut trevm = signet_evm::signet_evm(db, self.signet.constants) + let mut trevm = signet_evm::signet_evm(db, self.signet.constants.clone()) .fill_cfg(&self.signet) .fill_block(block); @@ -264,8 +264,8 @@ where } /// Access the signet constants - pub const fn constants(&self) -> SignetSystemConstants { - self.constants + pub const fn constants(&self) -> &SignetSystemConstants { + &self.constants } /// Access the signet DB diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 4db08282..8f562025 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -251,7 +251,7 @@ where let inspector = Layered::new(TimeLimit::new(self.finish_by - Instant::now()), Insp::default()); - Ok(signet_evm::signet_evm_with_inspector(db, inspector, self.constants)) + Ok(signet_evm::signet_evm_with_inspector(db, inspector, self.constants.clone())) } } diff --git a/crates/test-utils/src/specs/host_spec.rs b/crates/test-utils/src/specs/host_spec.rs index f3cb97df..437d8a4a 100644 --- a/crates/test-utils/src/specs/host_spec.rs +++ b/crates/test-utils/src/specs/host_spec.rs @@ -59,7 +59,7 @@ pub struct HostBlockSpec { impl Clone for HostBlockSpec { fn clone(&self) -> Self { Self { - constants: self.constants, + constants: self.constants.clone(), receipts: self.receipts.clone(), ru_block: self.ru_block.clone(), sidecar: self.sidecar.clone(), @@ -90,11 +90,6 @@ impl HostBlockSpec { } } - /// Make a new block spec with pecorino constants. - pub const fn pecorino() -> Self { - Self::new(SignetSystemConstants::pecorino()) - } - /// Make a new block spec with test constants. pub const fn test() -> Self { Self::new(SignetSystemConstants::test()) @@ -417,7 +412,6 @@ impl FromStr for HostBlockSpec { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), KnownChains::Test => Ok(Self::test()), } } diff --git a/crates/test-utils/src/specs/ru_spec.rs b/crates/test-utils/src/specs/ru_spec.rs index f95e464d..e92d0060 100644 --- a/crates/test-utils/src/specs/ru_spec.rs +++ b/crates/test-utils/src/specs/ru_spec.rs @@ -43,11 +43,6 @@ impl RuBlockSpec { Self { constants, tx: vec![], gas_limit: None, reward_address: None } } - /// Create a new empty RU block spec with the pecorino constants. - pub const fn pecorino() -> Self { - Self::new(SignetSystemConstants::pecorino()) - } - /// Create a new empty RU block spec with the test constants. pub const fn test() -> Self { Self::new(SignetSystemConstants::test()) @@ -152,7 +147,6 @@ impl FromStr for RuBlockSpec { fn from_str(s: &str) -> Result { let chain: KnownChains = s.parse()?; match chain { - KnownChains::Pecorino => Ok(Self::pecorino()), KnownChains::Test => Ok(Self::test()), } } diff --git a/crates/tx-cache/Cargo.toml b/crates/tx-cache/Cargo.toml index 3e39a626..15c47f58 100644 --- a/crates/tx-cache/Cargo.toml +++ b/crates/tx-cache/Cargo.toml @@ -13,7 +13,6 @@ repository.workspace = true [dependencies] signet-bundle.workspace = true signet-types.workspace = true -signet-constants.workspace = true alloy.workspace = true diff --git a/crates/tx-cache/src/client.rs b/crates/tx-cache/src/client.rs index 89e61058..91a7a61a 100644 --- a/crates/tx-cache/src/client.rs +++ b/crates/tx-cache/src/client.rs @@ -6,7 +6,6 @@ use alloy::consensus::TxEnvelope; use eyre::Error; use serde::{de::DeserializeOwned, Serialize}; use signet_bundle::SignetEthBundle; -use signet_constants::pecorino; use signet_types::SignedOrder; use tracing::{instrument, warn}; @@ -42,18 +41,6 @@ impl TxCache { Ok(Self::new(url)) } - /// Connect to the Pecorino tx cache. - pub fn pecorino() -> Self { - Self::new_from_string(pecorino::TX_CACHE_URL).expect("pecorino tx cache URL invalid") - } - - /// Connect to the Pecornio tx cache, using a specific [`Client`]. - pub fn pecorino_with_client(client: reqwest::Client) -> Self { - let url = - reqwest::Url::parse(pecorino::TX_CACHE_URL).expect("pecorino tx cache URL invalid"); - Self::new_with_client(url, client) - } - /// Get the client used to send requests pub const fn client(&self) -> &reqwest::Client { &self.client From dac7b22db064b5c0d71de6f50635d20c51311b78 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 12:38:50 -0400 Subject: [PATCH 10/24] fix: account for decimals in minting --- crates/constants/src/types/host.rs | 10 +++++++- crates/constants/src/types/mod.rs | 5 ++++ crates/evm/src/sys/driver.rs | 4 +-- crates/evm/src/sys/native.rs | 41 ++++++++++++++++++++---------- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/crates/constants/src/types/host.rs b/crates/constants/src/types/host.rs index 4469389d..e6950c27 100644 --- a/crates/constants/src/types/host.rs +++ b/crates/constants/src/types/host.rs @@ -1,4 +1,7 @@ -use crate::types::{ConfigError, HostTokens, KnownChains, ParseChainError}; +use crate::{ + types::{ConfigError, HostTokens, KnownChains, ParseChainError}, + HostUsdRecord, +}; use alloy::{genesis::Genesis, primitives::Address}; use serde_json::Value; use std::str::FromStr; @@ -120,6 +123,11 @@ impl HostConstants { &self.tokens } + /// Get the host USD record for the given address, if it is a host USD. + pub fn usd_record(&self, address: Address) -> Option<&HostUsdRecord> { + self.tokens.usd_record(address) + } + /// Return true if the address is an approved USD token. pub fn is_usd(&self, address: Address) -> bool { self.tokens.is_usd(address) diff --git a/crates/constants/src/types/mod.rs b/crates/constants/src/types/mod.rs index a5f1c459..6b13ce2c 100644 --- a/crates/constants/src/types/mod.rs +++ b/crates/constants/src/types/mod.rs @@ -97,6 +97,11 @@ impl SignetSystemConstants { self.host.is_usd(address) } + /// Get the host USD record for the given address, if it is a host USD. + pub fn host_usd_record(&self, address: Address) -> Option<&HostUsdRecord> { + self.host.usd_record(address) + } + /// Get the Order contract address for the given chain id. pub const fn orders_for(&self, chain_id: u64) -> Option
{ if chain_id == self.host_chain_id() { diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index a8832d34..d4871e0b 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -404,9 +404,9 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { for (i, e) in self.extracts.enter_tokens.iter().enumerate() { let nonce = minter_nonce + i as u64; - if self.constants.is_host_usd(e.event.token) { + if let Some(record) = self.constants.host_usd_record(e.event.token) { // USDC is handled as a native mint - let mut mint = MintNative::new(e); + let mut mint = MintNative::new(e, record.decimals()); mint.populate_nonce(nonce); trevm = self.apply_sys_action_single(trevm, mint)?; usd_minted += e.event.amount; diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index fe5e0c03..db7b8db3 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -16,13 +16,19 @@ use trevm::{ Trevm, MIN_TRANSACTION_GAS, }; +const ETH_DECIMALS: u8 = 18; + /// System transaction to mint native tokens. #[derive(Debug, Clone, Copy)] pub struct MintNative { /// The address that will receive the minted tokens. recipient: Address, + + /// The host USD record for the mint. + decimals: u8, + /// The amount of native tokens to mint. - amount: U256, + host_amount: U256, /// The magic signature for the mint. magic_sig: MagicSig, @@ -33,24 +39,17 @@ pub struct MintNative { rollup_chain_id: u64, } -impl From<&ExtractedEvent<'_, R, Passage::EnterToken>> for MintNative -where - R: TxReceipt, -{ - fn from(event: &ExtractedEvent<'_, R, Passage::EnterToken>) -> Self { - Self::new(event) - } -} - impl MintNative { /// Create a new [`MintNative`] instance from an [`ExtractedEvent`] /// containing a [`Passage::EnterToken`] event. pub fn new>( event: &ExtractedEvent<'_, R, Passage::EnterToken>, + decimals: u8, ) -> Self { Self { recipient: event.event.recipient(), - amount: event.event.amount(), + decimals, + host_amount: event.event.amount(), magic_sig: event.magic_sig(), nonce: None, rollup_chain_id: event.rollup_chain_id(), @@ -63,7 +62,7 @@ impl MintNative { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, recipient: self.recipient, - amount: self.amount, + amount: self.host_amount, } } @@ -77,13 +76,27 @@ impl MintNative { max_fee_per_gas: 0, max_priority_fee_per_gas: 0, to: self.recipient.into(), - value: self.amount, + value: self.host_amount, access_list: Default::default(), input: Default::default(), }), self.magic_sig.into(), ) } + + /// Get the amount of native tokens to mint, adjusted for the decimals of + /// the host USD record. + pub fn mint_amount(&self) -> U256 { + if self.decimals > ETH_DECIMALS { + let divisor_exp = self.decimals - ETH_DECIMALS; + let divisor = U256::from(10u64).pow(U256::from(divisor_exp)); + self.host_amount / divisor + } else { + let multiplier_exp = ETH_DECIMALS - self.decimals; + let multiplier = U256::from(10u64).pow(U256::from(multiplier_exp)); + self.host_amount * multiplier + } + } } impl SysBase for MintNative { @@ -118,7 +131,7 @@ impl SysAction for MintNative { Insp: Inspector>, { // Increase the balance of the recipient - evm.try_increase_balance_unchecked(self.recipient, self.amount) + evm.try_increase_balance_unchecked(self.recipient, self.mint_amount()) .map(drop) .map_err(EVMError::Database) } From 04788d0b4fc0e5a477ce6992154164048c1643d9 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 14:53:19 -0400 Subject: [PATCH 11/24] fix: host_amount vs mint_amount --- crates/evm/src/sys/native.rs | 66 ++++++++++++++++++++++++++------ crates/test-utils/tests/evm.rs | 69 +++++++++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 21 deletions(-) diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index db7b8db3..51b1d566 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -57,12 +57,12 @@ impl MintNative { } /// Create a new [`Log`] for the [`MintNative`] operation. - const fn make_sys_log(&self) -> MintNativeSysLog { + fn make_sys_log(&self) -> MintNativeSysLog { MintNativeSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, recipient: self.recipient, - amount: self.host_amount, + amount: self.mint_amount(), } } @@ -76,7 +76,7 @@ impl MintNative { max_fee_per_gas: 0, max_priority_fee_per_gas: 0, to: self.recipient.into(), - value: self.host_amount, + value: self.mint_amount(), access_list: Default::default(), input: Default::default(), }), @@ -87,15 +87,8 @@ impl MintNative { /// Get the amount of native tokens to mint, adjusted for the decimals of /// the host USD record. pub fn mint_amount(&self) -> U256 { - if self.decimals > ETH_DECIMALS { - let divisor_exp = self.decimals - ETH_DECIMALS; - let divisor = U256::from(10u64).pow(U256::from(divisor_exp)); - self.host_amount / divisor - } else { - let multiplier_exp = ETH_DECIMALS - self.decimals; - let multiplier = U256::from(10u64).pow(U256::from(multiplier_exp)); - self.host_amount * multiplier - } + let decimals = self.decimals; + adjust_decimals(self.host_amount, decimals, ETH_DECIMALS) } } @@ -147,3 +140,52 @@ impl SysAction for MintNative { ) } } + +/// Adjust the amount of tokens based on the decimals of the host USD record +/// and the target decimals. +/// +/// This is done by either dividing or multiplying the host amount +/// by a power of 10, depending on whether the host decimals are greater than +/// or less than the target decimals. +fn adjust_decimals(amount: U256, decimals: u8, target_decimals: u8) -> U256 { + if target_decimals == 0 || decimals == 0 { + // If target decimals is 0, return the host amount unchanged + return amount; + } + + if decimals > target_decimals { + let divisor_exp = decimals - target_decimals; + let divisor = U256::from(10u64).pow(U256::from(divisor_exp)); + amount / divisor + } else { + let multiplier_exp = target_decimals.checked_sub(decimals).unwrap_or_default(); + let multiplier = U256::from(10u64).pow(U256::from(multiplier_exp)); + amount * multiplier + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::uint; + + #[test] + fn mint_amount_math() { + uint! { + assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 18), 10_U256.pow(18_U256)); + assert_eq!(adjust_decimals(10_U256.pow(18_U256), 18, 6), 10_U256.pow(6_U256)); + + assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 12), 10_U256.pow(12_U256)); + assert_eq!(adjust_decimals(10_U256.pow(12_U256), 12, 6), 10_U256.pow(6_U256)); + + assert_eq!(adjust_decimals(10_U256.pow(18_U256), 18, 12), 10_U256.pow(12_U256)); + assert_eq!(adjust_decimals(10_U256.pow(12_U256), 12, 18), 10_U256.pow(18_U256)); + + assert_eq!(adjust_decimals(10_U256.pow(6_U256), 6, 0), 10_U256.pow(6_U256)); + + assert_eq!(adjust_decimals(10_U256.pow(18_U256), 3, 6), 10_U256.pow(21_U256)); + assert_eq!(adjust_decimals(10_U256.pow(21_U256), 6, 3), 10_U256.pow(18_U256)); + assert_eq!(adjust_decimals(10_U256.pow(18_U256), 6, 3), 10_U256.pow(15_U256)); + } + } +} diff --git a/crates/test-utils/tests/evm.rs b/crates/test-utils/tests/evm.rs index dffed330..d1487fd7 100644 --- a/crates/test-utils/tests/evm.rs +++ b/crates/test-utils/tests/evm.rs @@ -6,6 +6,7 @@ use alloy::{ primitives::{Address, U256}, signers::{local::PrivateKeySigner, Signature}, sol_types::SolEvent, + uint, }; use signet_constants::SignetSystemConstants; use signet_evm::{ @@ -14,7 +15,9 @@ use signet_evm::{ }; use signet_extract::{Extractable, ExtractedEvent, Extracts}; use signet_test_utils::{ - chain::{fake_block, Chain, HOST_USDC, RU_CHAIN_ID, RU_WETH}, + chain::{ + fake_block, Chain, HOST_USDC, HOST_USDT, RU_CHAIN_ID, RU_WETH, USDC_RECORD, USDT_RECORD, + }, evm::test_signet_evm, specs::{make_wallet, sign_tx_with_key_pair, simple_send}, }; @@ -227,6 +230,7 @@ fn test_a_transact() { let mut context = TestEnv::new(); let sender = Address::repeat_byte(1); let recipient = Address::repeat_byte(2); + let third_party = Address::repeat_byte(3); // Set up a couple fake events let fake_tx = fake_tx(); @@ -245,6 +249,20 @@ fn test_a_transact() { amount: U256::from(ETH_TO_WEI), }; + let enter_token_2 = signet_zenith::Passage::EnterToken { + rollupChainId: U256::from(RU_CHAIN_ID), + rollupRecipient: third_party, + token: HOST_USDT, + amount: uint!(1_000_000_000_000_U256), + }; + + let enter_token_3 = signet_zenith::Passage::EnterToken { + rollupChainId: U256::from(RU_CHAIN_ID), + rollupRecipient: third_party, + token: HOST_USDC, + amount: U256::from(1_000_000), + }; + let transact = signet_zenith::Transactor::Transact { rollupChainId: U256::from(RU_CHAIN_ID), sender, @@ -270,6 +288,18 @@ fn test_a_transact() { log_index: 0, event: enter_token, }); + extracts.enter_tokens.push(ExtractedEvent { + tx: &fake_tx, + receipt: &fake_receipt, + log_index: 0, + event: enter_token_2, + }); + extracts.enter_tokens.push(ExtractedEvent { + tx: &fake_tx, + receipt: &fake_receipt, + log_index: 0, + event: enter_token_3, + }); extracts.transacts.push(ExtractedEvent { tx: &fake_tx, receipt: &fake_receipt, @@ -288,19 +318,40 @@ fn test_a_transact() { // 1. MintToken for the enter event // 2. MintNative for the enter token event // 3. Transact for the transact event - let expected_tx_0 = - MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0).produce_transaction(); - let expected_tx_1 = - MintNative::new(&extracts.enter_tokens[0]).with_nonce(1).produce_transaction(); - let expected_tx_2 = extracts.transacts[0].make_transaction(0); + // 4. MintNative for the second enter token event + // 5. MintNative for the third enter token event + let expected_sys_0 = MintToken::from_enter(RU_WETH, &extracts.enters[0]).with_nonce(0); + let expected_tx_0 = expected_sys_0.produce_transaction(); + + let expected_sys_1 = + MintNative::new(&extracts.enter_tokens[0], USDC_RECORD.decimals()).with_nonce(1); + let expected_tx_1 = expected_sys_1.produce_transaction(); + + let expected_sys_2 = + MintNative::new(&extracts.enter_tokens[1], USDT_RECORD.decimals()).with_nonce(2); + let expected_tx_2 = expected_sys_2.produce_transaction(); + + let expected_sys_3 = + MintNative::new(&extracts.enter_tokens[2], USDC_RECORD.decimals()).with_nonce(3); + let expected_tx_3 = expected_sys_3.produce_transaction(); + + let expected_tx_4 = extracts.transacts[0].make_transaction(0); - assert_eq!(sealed_block.senders, vec![MINTER_ADDRESS, MINTER_ADDRESS, sender]); + assert_eq!( + sealed_block.senders, + vec![MINTER_ADDRESS, MINTER_ADDRESS, MINTER_ADDRESS, MINTER_ADDRESS, sender] + ); assert_eq!( sealed_block.block.body.transactions().collect::>(), - vec![&expected_tx_0, &expected_tx_1, &expected_tx_2] + vec![&expected_tx_0, &expected_tx_1, &expected_tx_2, &expected_tx_3, &expected_tx_4] ); - assert_eq!(receipts.len(), 3); + assert_eq!(receipts.len(), 5); assert_eq!(trevm.read_balance(recipient), U256::from(100)); + + let inbound_usdt = expected_sys_2.mint_amount(); + let inbound_usdc = expected_sys_3.mint_amount(); + let expected_third_party_balance = inbound_usdc + inbound_usdt; + assert_eq!(trevm.read_balance(third_party), expected_third_party_balance); } fn fake_tx() -> TransactionSigned { From 249b806615bc27dbb48d17682b3fd6927b92a090 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 9 Jul 2025 14:54:24 -0400 Subject: [PATCH 12/24] lint: clippy --- crates/constants/src/types/tokens.rs | 8 +++++++- crates/evm/src/sys/transact.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index f74147f5..a05247ea 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -256,6 +256,12 @@ impl PartialEq for UsdRecords { impl Eq for UsdRecords {} +impl Default for UsdRecords { + fn default() -> Self { + Self::new() + } +} + impl UsdRecords { /// Create a new empty set of USD records. pub const fn new() -> Self { @@ -298,7 +304,7 @@ impl UsdRecords { } /// Get a slice of the USD records. - pub fn as_slice(&self) -> &[HostUsdRecord] { + pub const fn as_slice(&self) -> &[HostUsdRecord] { // SAFETY: We ensure that the data is initialized when pushing records. unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const HostUsdRecord, self.len) } } diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 80585718..596aafaa 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -108,7 +108,7 @@ impl MeteredSysTx for TransactSysTx { } fn max_fee_per_gas(&self) -> u128 { - self.tx.max_fee_per_gas() as u128 + self.tx.max_fee_per_gas() } fn callee(&self) -> TxKind { From 9dbc01dfda396bf084f6d49b6078dfd2674e0878 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 10:11:08 -0400 Subject: [PATCH 13/24] refactor: move callee and input into systx --- crates/evm/src/sys/mod.rs | 29 ++++++++++++++--------------- crates/evm/src/sys/token.rs | 34 +++++++++++++++++++++++++++------- crates/evm/src/sys/transact.rs | 20 ++++++++++---------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index 45f6212b..5d1bccf3 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -66,12 +66,6 @@ pub trait SysBase: fmt::Debug + Clone { fn evm_sender(&self) -> Address; } -/// A transaction that is run on the EVM, and may or may not pay gas. -/// -/// See [`MeteredSysTx`] and [`UnmeteredSysTx`] for more specific -/// transaction types. -pub trait SysTx: SysBase + Tx {} - /// System actions are operations that apply changes to the EVM state without /// going through the transaction processing pipeline. They are not run as /// transactions, and do not have gas limits or revert semantics. They are @@ -98,8 +92,20 @@ pub trait SysAction: SysBase { /// and CAN halt. They are typically used for operations that need to be run as /// transactions, but should not pay gas. E.g. minting tokens or performing /// system-level operations that do not require gas payment. -pub trait UnmeteredSysTx: SysBase + Tx {} +pub trait UnmeteredSysTx: SysBase + SysTx {} + +/// A transaction that is run on the EVM, and may or may not pay gas. +/// +/// See [`MeteredSysTx`] and [`UnmeteredSysTx`] for more specific +/// transaction types. +pub trait SysTx: SysBase + Tx { + /// Get the callee address for the transaction. + fn callee(&self) -> TxKind; + /// Get the input data for the transaction. This is the calldata that is + /// passed to the callee when the transaction is executed. + fn input(&self) -> Bytes; +} /// System transactions run on the EVM as a transaction, and are subject to the /// same rules and constraints as regular transactions. They may run arbitrary /// execution, have gas limits, and can revert if they fail. They must satisfy @@ -111,7 +117,7 @@ pub trait UnmeteredSysTx: SysBase + Tx {} /// They are distinct from [`SysAction`], which are not run as transactions, /// but rather apply changes to the state directly without going through the /// transaction processing pipeline. -pub trait MeteredSysTx: SysBase + Tx { +pub trait MeteredSysTx: SysBase + SysTx { /// Get the gas limit for the transaction. This is the maximum amount of /// gas that the transaction is allowed to consume. /// @@ -136,11 +142,4 @@ pub trait MeteredSysTx: SysBase + Tx { fn max_fee(&self) -> U256 { U256::from(self.gas_limit()) * U256::from(self.max_fee_per_gas()) } - - /// Get the callee address for the transaction. - fn callee(&self) -> TxKind; - - /// Get the input data for the transaction. This is the calldata that is - /// passed to the callee when the transaction is executed. - fn input(&self) -> Bytes; } diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index b913dbdd..b3a9cdcb 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -1,7 +1,9 @@ +use std::sync::OnceLock; + use crate::sys::{MintTokenSysLog, SysBase, SysTx, UnmeteredSysTx}; use alloy::{ consensus::{TxEip1559, TxReceipt}, - primitives::{Address, Log, U256}, + primitives::{Address, Bytes, Log, TxKind, U256}, sol_types::SolCall, }; use signet_extract::ExtractedEvent; @@ -17,7 +19,7 @@ use trevm::{ }; /// System transaction to mint tokens. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct MintToken { /// The address that will receive the minted tokens. recipient: Address, @@ -35,6 +37,9 @@ pub struct MintToken { nonce: Option, /// The rollup chain ID. rollup_chain_id: u64, + + /// The ABI-encoded call for the mint operation./s + encoded_call: OnceLock, } impl trevm::Tx for MintToken { @@ -62,7 +67,7 @@ impl trevm::Tx for MintToken { *gas_price = 0; *kind = TransactTo::Call(self.token); *value = U256::ZERO; - *data = self.mint_call().abi_encode().into(); + *data = self.encoded_call().clone(); *nonce = self.nonce.expect("must be set"); *chain_id = Some(self.rollup_chain_id); *access_list = Default::default(); @@ -88,6 +93,7 @@ impl MintToken { magic_sig: event.magic_sig(), nonce: None, rollup_chain_id: event.rollup_chain_id(), + encoded_call: OnceLock::new(), } } @@ -105,16 +111,22 @@ impl MintToken { magic_sig: event.magic_sig(), nonce: None, rollup_chain_id: event.rollup_chain_id(), + encoded_call: OnceLock::new(), } } /// Create the ABI-encoded call for the mint operation. - const fn mint_call(&self) -> signet_zenith::mintCall { + pub const fn mint_call(&self) -> signet_zenith::mintCall { signet_zenith::mintCall { amount: self.amount, to: self.recipient } } + /// Get the ABI-encoded call for the mint operation, lazily initialized. + pub fn encoded_call(&self) -> &Bytes { + self.encoded_call.get_or_init(|| self.mint_call().abi_encode().into()) + } + /// Create a new [`Log`] for the [`MintToken`] operation. - const fn make_sys_log(self) -> MintTokenSysLog { + const fn make_sys_log(&self) -> MintTokenSysLog { MintTokenSysLog { txHash: self.magic_sig.txid, logIndex: self.magic_sig.event_idx as u64, @@ -126,7 +138,7 @@ impl MintToken { /// Convert the [`MintToken`] instance into a [`TransactionSigned`]. fn make_transaction(&self) -> TransactionSigned { - let input = self.mint_call().abi_encode().into(); + let input = self.encoded_call().clone(); TransactionSigned::new_unhashed( Transaction::Eip1559(TxEip1559 { @@ -168,6 +180,14 @@ impl SysBase for MintToken { } } -impl SysTx for MintToken {} +impl SysTx for MintToken { + fn callee(&self) -> TxKind { + self.token.into() + } + + fn input(&self) -> Bytes { + self.encoded_call().clone() + } +} impl UnmeteredSysTx for MintToken {} diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 596aafaa..2ba2f474 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -1,7 +1,7 @@ use crate::sys::{MeteredSysTx, SysBase, SysTx, TransactSysLog}; use alloy::{ consensus::{EthereumTxEnvelope, Transaction}, - primitives::{Address, Log, TxKind, U256}, + primitives::{Address, Bytes, Log, TxKind, U256}, }; use core::fmt; use signet_extract::ExtractedEvent; @@ -100,7 +100,15 @@ impl SysBase for TransactSysTx { } } -impl SysTx for TransactSysTx {} +impl SysTx for TransactSysTx { + fn callee(&self) -> TxKind { + self.tx.kind() + } + + fn input(&self) -> Bytes { + self.tx.input().clone() + } +} impl MeteredSysTx for TransactSysTx { fn gas_limit(&self) -> u128 { @@ -110,12 +118,4 @@ impl MeteredSysTx for TransactSysTx { fn max_fee_per_gas(&self) -> u128 { self.tx.max_fee_per_gas() } - - fn callee(&self) -> TxKind { - self.tx.kind() - } - - fn input(&self) -> alloy::primitives::Bytes { - self.tx.input().clone() - } } From 4eaed3032a164782e083153703262b77fa356e15 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 11:12:15 -0400 Subject: [PATCH 14/24] refactor: use iterator in mints_inner --- crates/evm/src/sys/driver.rs | 74 +++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index d4871e0b..9c5e5eb0 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -106,6 +106,10 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// /// This function expects that gas and nonce checks are already disabled /// in the EVM, and will not re-enable them. + /// + /// # Panics + /// + /// If the EVM is not configured to disable gas, basefee, and nonce checks. pub(crate) fn apply_unmetered_sys_transaction_inner( &mut self, mut trevm: EvmNeedsTx, @@ -116,6 +120,10 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Insp: Inspector>, S: UnmeteredSysTx, { + debug_assert!(trevm.inner().cfg.disable_balance_check); + debug_assert!(trevm.inner().cfg.disable_base_fee); + debug_assert!(trevm.inner().cfg.disable_nonce_check); + // Populate the nonce for the action. trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); @@ -131,6 +139,38 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Ok(self.accept_tx(t, tx)) } + /// Apply a series of [`UnmeteredSysTx`]s to the EVM state. + /// + /// This will do the following: + /// - Run each system transaction in the EVM as + /// [`Self::apply_unmetered_sys_transaction_inner`]. + /// + /// This function expects that gas and nonce checks are already disabled + /// in the EVM, and will not re-enable them. + /// + /// # Panics + /// + /// If the EVM is not configured to disable gas, basefee, and nonce checks. + pub(crate) fn apply_unmetered_sys_transactions_inner( + &mut self, + mut trevm: EvmNeedsTx, + sys_txs: impl IntoIterator, + ) -> RunTxResult + where + Db: Database + DatabaseCommit, + Insp: Inspector>, + S: UnmeteredSysTx, + { + debug_assert!(trevm.inner().cfg.disable_balance_check); + debug_assert!(trevm.inner().cfg.disable_base_fee); + debug_assert!(trevm.inner().cfg.disable_nonce_check); + + for sys_tx in sys_txs { + trevm = self.apply_unmetered_sys_transaction_inner(trevm, sys_tx)?; + } + Ok(trevm) + } + /// Apply a [`UnmeteredSysTx`] to the EVM state. /// /// When applying many system transactions, it is recommended to use @@ -191,12 +231,8 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { S: UnmeteredSysTx, { trevm.try_with_cfg(&DisableGasChecks, |trevm| { - trevm.try_with_cfg(&DisableNonceCheck, |mut trevm| { - for sys_tx in sys_txs { - // Populate the nonce for the transaction - trevm = self.apply_unmetered_sys_transaction_inner(trevm, sys_tx)?; - } - Ok(trevm) + trevm.try_with_cfg(&DisableNonceCheck, |trevm| { + self.apply_unmetered_sys_transactions_inner(trevm, sys_txs) }) }) } @@ -372,6 +408,8 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Db: Database + DatabaseCommit, Insp: Inspector>, { + let eth_token = self.constants.rollup().tokens().weth(); + // Load the nonce once, we'll write it at the end let minter_nonce = trevm_try!(trevm.try_read_nonce(MINTER_ADDRESS).map_err(EVMError::Database), trevm); @@ -388,20 +426,30 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let mut usd_minted = U256::ZERO; let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); - let eth_token = self.constants.rollup().tokens().weth(); - - for (i, e) in self.extracts.enters.iter().enumerate() { + let eth_mints = self.extracts.enters.iter().enumerate().map(|(i, e)| { + eth_accts.insert(e.event.recipient()); + eth_minted = eth_minted.saturating_add(e.event.amount); let mut mint = MintToken::from_enter(eth_token, e); mint.populate_nonce(minter_nonce + i as u64); - trevm = self.apply_unmetered_sys_transaction_inner(trevm, mint)?; + mint + }); - eth_minted += e.event.amount; - eth_accts.insert(e.event.recipient()); - } + trevm = self.apply_unmetered_sys_transactions_inner(trevm, eth_mints)?; // Use a new base nonce for the enter_tokens let minter_nonce = minter_nonce + self.extracts.enters.len() as u64; + // NB: I would love to refactor this to use filter and the batch apply + // fns however + // - The filters would require cloning the `SignetSystemConstants` to + // avoid borrowchecker issues. + // - It would result in 2 iterations over the enter tokens, one for + // tokens and one for USDs. + // + // Instead, we iterate over the enter tokens, minting USDs and tokens + // singly. It might be worth revisiting this to make it so native asset + // is always minted first. + for (i, e) in self.extracts.enter_tokens.iter().enumerate() { let nonce = minter_nonce + i as u64; if let Some(record) = self.constants.host_usd_record(e.event.token) { From bf39b8e663d784792f32e09af8cfc760183cc88a Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 11:23:00 -0400 Subject: [PATCH 15/24] nit: move variable declaration down --- crates/evm/src/sys/driver.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 9c5e5eb0..78611a08 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -423,8 +423,6 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { ); let mut eth_minted = U256::ZERO; let mut eth_accts = HashSet::with_capacity(self.extracts.enters.len()); - let mut usd_minted = U256::ZERO; - let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); let eth_mints = self.extracts.enters.iter().enumerate().map(|(i, e)| { eth_accts.insert(e.event.recipient()); @@ -438,6 +436,8 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { // Use a new base nonce for the enter_tokens let minter_nonce = minter_nonce + self.extracts.enters.len() as u64; + let mut usd_minted = U256::ZERO; + let mut usd_accts = HashSet::with_capacity(self.extracts.enter_tokens.len()); // NB: I would love to refactor this to use filter and the batch apply // fns however From 3126ed88f9e79d5d7a7dedc604b429aa61663c94 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 11:34:30 -0400 Subject: [PATCH 16/24] refactor: add with nonce methods --- crates/evm/src/sys/driver.rs | 12 ++++-------- crates/evm/src/sys/native.rs | 11 +++++++++++ crates/evm/src/sys/token.rs | 26 +++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 78611a08..3951e4e6 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -427,9 +427,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let eth_mints = self.extracts.enters.iter().enumerate().map(|(i, e)| { eth_accts.insert(e.event.recipient()); eth_minted = eth_minted.saturating_add(e.event.amount); - let mut mint = MintToken::from_enter(eth_token, e); - mint.populate_nonce(minter_nonce + i as u64); - mint + MintToken::from_enter_with_nonce(eth_token, e, minter_nonce + i as u64) }); trevm = self.apply_unmetered_sys_transactions_inner(trevm, eth_mints)?; @@ -454,8 +452,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let nonce = minter_nonce + i as u64; if let Some(record) = self.constants.host_usd_record(e.event.token) { // USDC is handled as a native mint - let mut mint = MintNative::new(e, record.decimals()); - mint.populate_nonce(nonce); + let mint = MintNative::new_with_nonce(e, record.decimals(), nonce); trevm = self.apply_sys_action_single(trevm, mint)?; usd_minted += e.event.amount; usd_accts.insert(e.event.recipient()); @@ -464,9 +461,8 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { let ru_token_addr = self .constants .rollup_address_from_host_address(e.event.token) - .expect("token enters must be permissioned"); - let mut mint = MintToken::from_enter_token(ru_token_addr, e); - mint.populate_nonce(nonce); + .expect("unpermissioned tokens must be filtered during extraction"); + let mint = MintToken::from_enter_token_with_nonce(ru_token_addr, e, nonce); trevm = self.apply_unmetered_sys_transaction_inner(trevm, mint)?; } } diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index 51b1d566..89abcc95 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -56,6 +56,17 @@ impl MintNative { } } + /// Create a new [`MintNative`] instance with a nonce. + pub fn new_with_nonce>( + event: &ExtractedEvent<'_, R, Passage::EnterToken>, + decimals: u8, + nonce: u64, + ) -> Self { + let mut mint = Self::new(event, decimals); + mint.populate_nonce(nonce); + mint + } + /// Create a new [`Log`] for the [`MintNative`] operation. fn make_sys_log(&self) -> MintNativeSysLog { MintNativeSysLog { diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index b3a9cdcb..041e072f 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -38,7 +38,7 @@ pub struct MintToken { /// The rollup chain ID. rollup_chain_id: u64, - /// The ABI-encoded call for the mint operation./s + /// The ABI-encoded call for the mint operation. encoded_call: OnceLock, } @@ -97,6 +97,18 @@ impl MintToken { } } + /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] + /// containing a [`Passage::EnterToken`] event, with a specified nonce. + pub fn from_enter_token_with_nonce>( + token: Address, + event: &ExtractedEvent<'_, R, Passage::EnterToken>, + nonce: u64, + ) -> Self { + let mut mint = Self::from_enter_token(token, event); + mint.populate_nonce(nonce); + mint + } + /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] /// containing a [`Passage::Enter`] event. pub fn from_enter>( @@ -115,6 +127,18 @@ impl MintToken { } } + /// Create a new [`MintToken`] instance from an [`ExtractedEvent`] + /// containing a [`Passage::Enter`] event, with a specified nonce. + pub fn from_enter_with_nonce>( + token: Address, + event: &ExtractedEvent<'_, R, Passage::Enter>, + nonce: u64, + ) -> Self { + let mut mint = Self::from_enter(token, event); + mint.populate_nonce(nonce); + mint + } + /// Create the ABI-encoded call for the mint operation. pub const fn mint_call(&self) -> signet_zenith::mintCall { signet_zenith::mintCall { amount: self.amount, to: self.recipient } From 4cdc660d8ea489649f284768ef81af9a26f32ac4 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 12:09:53 -0400 Subject: [PATCH 17/24] refactor: add some good tracing spans --- crates/evm/src/sys/driver.rs | 47 ++++++++++++++++++++++------------ crates/evm/src/sys/mod.rs | 23 +++++++++++++++++ crates/evm/src/sys/native.rs | 10 +++++++- crates/evm/src/sys/token.rs | 15 +++++++++++ crates/evm/src/sys/transact.rs | 23 ++++++++++++++++- 5 files changed, 99 insertions(+), 19 deletions(-) diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 3951e4e6..0187ce4c 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -72,6 +72,14 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Insp: Inspector>, S: SysAction, { + let _guard = debug_span!( + "SignetDriver::apply_sys_action_single", + action = S::name(), + details = action.description(), + sender = %action.evm_sender(), + ) + .entered(); + // Populate the nonce for the action. trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut action), trevm); @@ -124,6 +132,15 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { debug_assert!(trevm.inner().cfg.disable_base_fee); debug_assert!(trevm.inner().cfg.disable_nonce_check); + let _guard = debug_span!( + "SignetDriver::apply_unmetered_sys_transaction_inner", + action = S::name(), + details = sys_tx.description(), + sender = %sys_tx.evm_sender(), + callee = ?sys_tx.callee(), + ) + .entered(); + // Populate the nonce for the action. trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); @@ -267,6 +284,18 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Insp: Inspector>, S: MeteredSysTx, { + let _guard = debug_span!( + "SignetDriver::apply_metered_sys_transaction_single", + action = S::name(), + details = sys_tx.description(), + sender = %sys_tx.evm_sender(), + callee = ?sys_tx.callee(), + value = %sys_tx.value(), + gas_limit = %sys_tx.gas_limit(), + max_fee_per_gas = %sys_tx.max_fee_per_gas(), + ) + .entered(); + // Populate the nonce for the action. trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); @@ -348,23 +377,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { S: MeteredSysTx, { for mut sys_tx in sys_txs { - let span = tracing::debug_span!( - "SignetDriver::apply_metered_sys_transactions", - sender = %sys_tx.evm_sender(), - gas_limit = sys_tx.gas_limit(), - callee = ?sys_tx.callee(), - ); - if tracing::enabled!(tracing::Level::TRACE) { - span.record("input", format!("{}", &sys_tx.input())); - } - let _enter = span.entered(); - - let nonce = trevm_try!( - trevm.try_read_nonce(sys_tx.evm_sender()).map_err(EVMError::Database), - trevm - ); - sys_tx.populate_nonce(nonce); - debug!(nonce, "Applying metered sys tx"); + trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); trevm = self.apply_metered_sys_transaction_single(trevm, sys_tx)?; } Ok(trevm) diff --git a/crates/evm/src/sys/mod.rs b/crates/evm/src/sys/mod.rs index 5d1bccf3..dc626356 100644 --- a/crates/evm/src/sys/mod.rs +++ b/crates/evm/src/sys/mod.rs @@ -29,7 +29,25 @@ use trevm::{ /// [`SysBase`] is the root trait for all system actions and transactions. It /// provides the basic functionality that the [`SignetDriver`] needs to process /// system actions and transactions. +/// +/// The [`fmt::Display`] impl is used for tracing, and should be a short +/// pub trait SysBase: fmt::Debug + Clone { + /// Get the name of the system action or transaction. This is used for + /// tracing, and should be short, descriptive, and unique for each + /// system action or transaction type. + fn name() -> &'static str; + + /// Get a short description of the system action or transaction. This + /// should be a concise, human-readable string that describes the + /// system action or transaction. + /// + /// E.g. + /// - "Mint 100 USD to 0xabcd..." + /// - "Transact 0.5 ETH from 0xabcd... to 0xef01... with input data + /// 0x1234..." + fn description(&self) -> String; + /// Check if the system action has a nonce. This is typically used to /// determine if the nonce should be populated by the Evm during /// transaction processing. @@ -105,6 +123,11 @@ pub trait SysTx: SysBase + Tx { /// Get the input data for the transaction. This is the calldata that is /// passed to the callee when the transaction is executed. fn input(&self) -> Bytes; + + /// Get the value of the transaction. This is the amount of native + /// asset that is being transferred to the callee when the transaction is + /// executed. + fn value(&self) -> U256; } /// System transactions run on the EVM as a transaction, and are subject to the /// same rules and constraints as regular transactions. They may run arbitrary diff --git a/crates/evm/src/sys/native.rs b/crates/evm/src/sys/native.rs index 89abcc95..58685eb4 100644 --- a/crates/evm/src/sys/native.rs +++ b/crates/evm/src/sys/native.rs @@ -1,7 +1,7 @@ use crate::sys::{MintNativeSysLog, SysAction, SysBase}; use alloy::{ consensus::{ReceiptEnvelope, TxEip1559, TxReceipt}, - primitives::{Address, Log, U256}, + primitives::{utils::format_ether, Address, Log, U256}, }; use signet_extract::ExtractedEvent; use signet_types::{ @@ -104,6 +104,14 @@ impl MintNative { } impl SysBase for MintNative { + fn name() -> &'static str { + "MintNative" + } + + fn description(&self) -> String { + format!("Mint {} native tokens to {}", format_ether(self.mint_amount()), self.recipient) + } + fn has_nonce(&self) -> bool { self.nonce.is_some() } diff --git a/crates/evm/src/sys/token.rs b/crates/evm/src/sys/token.rs index 041e072f..4b0b88b1 100644 --- a/crates/evm/src/sys/token.rs +++ b/crates/evm/src/sys/token.rs @@ -183,6 +183,17 @@ impl MintToken { } impl SysBase for MintToken { + fn name() -> &'static str { + "MintToken" + } + + fn description(&self) -> String { + format!( + "Mint {} tokens to {} on token contract {}", + self.amount, self.recipient, self.token + ) + } + fn populate_nonce(&mut self, nonce: u64) { self.nonce = Some(nonce); } @@ -212,6 +223,10 @@ impl SysTx for MintToken { fn input(&self) -> Bytes { self.encoded_call().clone() } + + fn value(&self) -> U256 { + U256::ZERO + } } impl UnmeteredSysTx for MintToken {} diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index 2ba2f474..d68dd343 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -1,7 +1,8 @@ use crate::sys::{MeteredSysTx, SysBase, SysTx, TransactSysLog}; use alloy::{ consensus::{EthereumTxEnvelope, Transaction}, - primitives::{Address, Bytes, Log, TxKind, U256}, + hex, + primitives::{utils::format_ether, Address, Bytes, Log, TxKind, U256}, }; use core::fmt; use signet_extract::ExtractedEvent; @@ -74,6 +75,22 @@ impl Tx for TransactSysTx { } impl SysBase for TransactSysTx { + fn name() -> &'static str { + "TransactSysTx" + } + + fn description(&self) -> String { + format!( + "Transact from {} to {} with value {} and {} bytes of input data: `0x{}{}`", + self.magic_sig.sender(), + self.tx.to().expect("creates not allowed"), + format_ether(self.tx.value()), + self.tx.input().len(), + self.tx.input().chunks(4).next().map(hex::encode).unwrap_or_default(), + if self.tx.input().len() > 4 { "..." } else { "" }, + ) + } + fn has_nonce(&self) -> bool { self.nonce.is_some() } @@ -108,6 +125,10 @@ impl SysTx for TransactSysTx { fn input(&self) -> Bytes { self.tx.input().clone() } + + fn value(&self) -> U256 { + self.tx.value() + } } impl MeteredSysTx for TransactSysTx { From c0f5f36238e2942ad169f449217dbad7b2834812 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 12:12:17 -0400 Subject: [PATCH 18/24] doc: update NB --- crates/evm/src/sys/transact.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/evm/src/sys/transact.rs b/crates/evm/src/sys/transact.rs index d68dd343..ab466d3c 100644 --- a/crates/evm/src/sys/transact.rs +++ b/crates/evm/src/sys/transact.rs @@ -96,7 +96,9 @@ impl SysBase for TransactSysTx { } fn populate_nonce(&mut self, nonce: u64) { - // NB: we have to set the nonce on the tx as well. + // NB: we have to set the nonce on the tx as well. Setting the nonce on + // the TX will change its hash, but will not invalidate the magic sig + // (as it's not a real signature). let EthereumTxEnvelope::Eip1559(signed) = &mut self.tx else { unreachable!("new sets this to 1559"); }; From 11bc6a9a883a9d20dafc11391d6206c23424b945 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 12:23:29 -0400 Subject: [PATCH 19/24] test: add USD record ser test --- crates/constants/src/types/tokens.rs | 36 ++++++++++++++++++++++++++++ crates/evm/src/sys/driver.rs | 3 +-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index a05247ea..96bd242f 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -349,3 +349,39 @@ impl<'de> serde::Deserialize<'de> for UsdRecords { Ok(records) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn usd_records_serde() { + let json = r#"[ + {"address": "0x0000000000000000000000000000000000000001", "ticker": "USDC", "decimals": 6}, + {"address": "0x0000000000000000000000000000000000000002", "ticker": "USDT", "decimals": 12}, + {"address": "0x0000000000000000000000000000000000000003", "ticker": "DAI", "decimals": 18} + ]"#; + + let mut records = UsdRecords::new(); + records.push(HostUsdRecord { + address: Address::with_last_byte(0x01), + ticker: "USDC".into(), + decimals: 6, + }); + records.push(HostUsdRecord { + address: Address::with_last_byte(0x02), + ticker: "USDT".into(), + decimals: 12, + }); + records.push(HostUsdRecord { + address: Address::with_last_byte(0x03), + ticker: "DAI".into(), + decimals: 18, + }); + + let records: UsdRecords = serde_json::from_str(json).unwrap(); + let serialized = serde_json::to_string(&records).unwrap(); + let deserialized: UsdRecords = serde_json::from_str(&serialized).unwrap(); + assert_eq!(records, deserialized); + } +} diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 0187ce4c..4ab9f264 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -376,8 +376,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { Insp: Inspector>, S: MeteredSysTx, { - for mut sys_tx in sys_txs { - trevm_try!(populate_nonce_from_trevm(&mut trevm, &mut sys_tx), trevm); + for sys_tx in sys_txs { trevm = self.apply_metered_sys_transaction_single(trevm, sys_tx)?; } Ok(trevm) From 0b80ebea20b84c95c9df8084bc0d7ccb27b4e748 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 10 Jul 2025 12:24:17 -0400 Subject: [PATCH 20/24] test: expand usd records test --- crates/constants/src/types/tokens.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index 96bd242f..a13c41f6 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -382,6 +382,12 @@ mod test { let records: UsdRecords = serde_json::from_str(json).unwrap(); let serialized = serde_json::to_string(&records).unwrap(); let deserialized: UsdRecords = serde_json::from_str(&serialized).unwrap(); + assert_eq!(records, deserialized); + assert_eq!(records.len(), 3); + assert_eq!(deserialized.len(), 3); + assert!(records.iter().any(|r| r.ticker == "USDC")); + assert!(records.iter().any(|r| r.ticker == "USDT")); + assert!(records.iter().any(|r| r.ticker == "DAI")); } } From 3216db10834654e30f9b7cec0ac3fcb4088bc835 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 11 Jul 2025 13:02:15 -0400 Subject: [PATCH 21/24] chore: PascalCase --- crates/constants/src/types/tokens.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index a13c41f6..b4379bb1 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -1,5 +1,6 @@ use crate::ETH_ADDRESS; use alloy::primitives::Address; +use core::fmt; use serde::ser::SerializeSeq; use std::{borrow::Cow, mem::MaybeUninit}; @@ -22,7 +23,7 @@ pub enum HostPermitted { #[non_exhaustive] pub enum RollupPermitted { /// USD (Native Asset) - USD, + Usd, /// WBTC Wbtc, /// WETH @@ -32,8 +33,8 @@ pub enum RollupPermitted { impl From for RollupPermitted { fn from(value: HostPermitted) -> Self { match value { - HostPermitted::Usdc => RollupPermitted::USD, - HostPermitted::Usdt => RollupPermitted::USD, + HostPermitted::Usdc => RollupPermitted::Usd, + HostPermitted::Usdt => RollupPermitted::Usd, HostPermitted::Wbtc => RollupPermitted::Wbtc, HostPermitted::Weth => RollupPermitted::Weth, } @@ -220,7 +221,7 @@ impl RollupTokens { match token { RollupPermitted::Wbtc => self.wbtc, RollupPermitted::Weth => self.weth, - RollupPermitted::USD => ETH_ADDRESS, + RollupPermitted::Usd => ETH_ADDRESS, } } } @@ -231,12 +232,20 @@ const USD_RECORD_CAPACITY: usize = 5; /// reasonable limit for the number of USD tokens that can be predeployed on /// the host chain. This limit is arbitrary but should be sufficient for most /// use cases, and can be adjusted trivially if needed. -#[derive(Debug)] pub struct UsdRecords { len: usize, data: [MaybeUninit; USD_RECORD_CAPACITY], } +impl fmt::Debug for UsdRecords { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UsdRecords") + .field("len", &self.len) + .field("data", &self.as_slice()) + .finish() + } +} + impl Clone for UsdRecords { fn clone(&self) -> Self { let mut data = [const { MaybeUninit::uninit() }; USD_RECORD_CAPACITY]; From f543c64341e175244a9986f344f880317ba4c0ba Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Jul 2025 13:54:01 -0400 Subject: [PATCH 22/24] doc: improve one --- crates/constants/src/types/host.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/constants/src/types/host.rs b/crates/constants/src/types/host.rs index e6950c27..1f0c488b 100644 --- a/crates/constants/src/types/host.rs +++ b/crates/constants/src/types/host.rs @@ -26,7 +26,9 @@ pub struct HostConstants { passage: Address, /// Host address for the transactor contract transactor: Address, - /// Host chain tokens that are predeployed on the rollup. + /// Host chain tokens that are special-cased on the rollup. This includes + /// USD tokens for the native asset, and permissioned tokens for bridged + /// assets. tokens: HostTokens, } From e68bd0b60c4afe67c9f5427b2d286d33f29cfd90 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Jul 2025 16:46:05 -0400 Subject: [PATCH 23/24] fix: is_host_token includes ether --- crates/constants/src/types/tokens.rs | 7 ++++++- crates/evm/src/sys/driver.rs | 2 +- crates/rpc/examples/order.rs | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index b4379bb1..3321f1ca 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -99,6 +99,11 @@ impl HostTokens { self.usd_record(address).is_some() } + /// Check if the address is WETH or ETH. + pub fn is_eth(&self, address: Address) -> bool { + address == self.weth || address == ETH_ADDRESS + } + /// Get the decimals for the given address, if the address is a USD token. pub fn decimals_for(&self, address: Address) -> Option { self.usd_record(address).map(|r| r.decimals) @@ -106,7 +111,7 @@ impl HostTokens { /// Returns true if the token is a permitted host token. pub fn is_host_token(&self, address: Address) -> bool { - address == self.wbtc || address == self.weth || self.usd_record(address).is_some() + address == self.wbtc || self.is_eth(address) || self.usd_record(address).is_some() } /// Get the [`HostPermitted`] for the given address, if it is a diff --git a/crates/evm/src/sys/driver.rs b/crates/evm/src/sys/driver.rs index 4ab9f264..55a90a7f 100644 --- a/crates/evm/src/sys/driver.rs +++ b/crates/evm/src/sys/driver.rs @@ -47,7 +47,7 @@ impl<'a, 'b, C: Extractable> SignetDriver<'a, 'b, C> { /// This will do the following: /// - Run the system action, allowing direct EVM state changes. /// - Produce the transaction using [`SysOutput::produce_transaction`]. - /// - Produce the syslog eipt using [`SysOutput::produce_log`]. + /// - Produce the syslog for the receipt using [`SysOutput::produce_log`]. /// - Produce a receipt containing the gas used and logs. /// - Push the resulting transaction to the block. /// - Push the resulting receipt to the output. diff --git a/crates/rpc/examples/order.rs b/crates/rpc/examples/order.rs index f85923f3..d20c50d6 100644 --- a/crates/rpc/examples/order.rs +++ b/crates/rpc/examples/order.rs @@ -62,6 +62,7 @@ where /// Get an example Order which swaps 1 USDC on the rollup for 1 USDC on the host. fn example_order(&self) -> Order { + // The native asset on the rollup has 18 decimals. let amount = U256::from(GWEI_TO_WEI); // input is 1 USD on the rollup From c296cc4e294f5b9ccb93938398542e85e1241b49 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Jul 2025 16:51:09 -0400 Subject: [PATCH 24/24] fix: native token address --- crates/constants/src/lib.rs | 5 +++-- crates/constants/src/types/tokens.rs | 10 +++++----- crates/rpc/examples/order.rs | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/constants/src/lib.rs b/crates/constants/src/lib.rs index f2af77de..eafd0e3e 100644 --- a/crates/constants/src/lib.rs +++ b/crates/constants/src/lib.rs @@ -27,5 +27,6 @@ pub use types::{ SignetConstants, SignetEnvironmentConstants, SignetSystemConstants, UsdRecords, MINTER_ADDRESS, }; -/// Placeholder address for ETH. -pub const ETH_ADDRESS: alloy::primitives::Address = alloy::primitives::Address::repeat_byte(0xee); +/// Placeholder address for the native token of the current chain. By convention this is `0xee...`. +pub const NATIVE_TOKEN_ADDRESS: alloy::primitives::Address = + alloy::primitives::Address::repeat_byte(0xee); diff --git a/crates/constants/src/types/tokens.rs b/crates/constants/src/types/tokens.rs index 3321f1ca..4fea97eb 100644 --- a/crates/constants/src/types/tokens.rs +++ b/crates/constants/src/types/tokens.rs @@ -1,4 +1,4 @@ -use crate::ETH_ADDRESS; +use crate::NATIVE_TOKEN_ADDRESS; use alloy::primitives::Address; use core::fmt; use serde::ser::SerializeSeq; @@ -101,7 +101,7 @@ impl HostTokens { /// Check if the address is WETH or ETH. pub fn is_eth(&self, address: Address) -> bool { - address == self.weth || address == ETH_ADDRESS + address == self.weth || address == NATIVE_TOKEN_ADDRESS } /// Get the decimals for the given address, if the address is a USD token. @@ -119,7 +119,7 @@ impl HostTokens { pub fn token_for(&self, address: Address) -> Option { if address == self.wbtc { Some(HostPermitted::Wbtc) - } else if address == self.weth || address == ETH_ADDRESS { + } else if address == self.weth || address == NATIVE_TOKEN_ADDRESS { Some(HostPermitted::Weth) } else if let Some(record) = self.usd_record(address) { match record.ticker.as_ref() { @@ -208,7 +208,7 @@ impl RollupTokens { pub const fn token_for(&self, address: Address) -> Option { if address.const_eq(&self.wbtc) { Some(RollupPermitted::Wbtc) - } else if address.const_eq(&self.weth) || address.const_eq(Ð_ADDRESS) { + } else if address.const_eq(&self.weth) || address.const_eq(&NATIVE_TOKEN_ADDRESS) { Some(RollupPermitted::Weth) } else { None @@ -226,7 +226,7 @@ impl RollupTokens { match token { RollupPermitted::Wbtc => self.wbtc, RollupPermitted::Weth => self.weth, - RollupPermitted::Usd => ETH_ADDRESS, + RollupPermitted::Usd => NATIVE_TOKEN_ADDRESS, } } } diff --git a/crates/rpc/examples/order.rs b/crates/rpc/examples/order.rs index d20c50d6..2551f8fc 100644 --- a/crates/rpc/examples/order.rs +++ b/crates/rpc/examples/order.rs @@ -5,7 +5,7 @@ use alloy::{ }; use chrono::Utc; use eyre::Error; -use signet_constants::{SignetConstants, ETH_ADDRESS}; +use signet_constants::{SignetConstants, NATIVE_TOKEN_ADDRESS}; use signet_tx_cache::client::TxCache; use signet_types::UnsignedOrder; use signet_zenith::RollupOrders::{Input, Order, Output}; @@ -66,7 +66,7 @@ where let amount = U256::from(GWEI_TO_WEI); // input is 1 USD on the rollup - let input = Input { token: ETH_ADDRESS, amount }; + let input = Input { token: NATIVE_TOKEN_ADDRESS, amount }; // output is 1 USDC on the host chain. // NB: decimals are important! USDC has 6 decimals, while Signet's USD