From 590ce831f8cf7eac0ba6a1a5e51d581a29d018ae Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Fri, 24 Jan 2025 15:22:47 +0100 Subject: [PATCH 1/8] chore(sidecar): get_or_fetch_account_states utils --- bolt-sidecar/src/state/execution.rs | 38 +++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index c702e9946..5ed98eef0 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -231,6 +231,23 @@ impl ExecutionState { self.basefee } + /// Get the canonical account state at the head of the block for the given address from the + /// [AccountStateCache]. If not available, it's fetched from the EL client, added to the cache + /// and returned. + pub async fn get_or_fetch_account_state( + &mut self, + address: Address, + ) -> Result { + match self.account_states.get(&address) { + Some(account) => Ok(*account), + None => { + let account = self.client.get_account_state(&address, None).await?; + self.account_states.insert(address, account); + Ok(account) + } + } + } + /// Validates the commitment request against state (historical + intermediate). /// /// NOTE: This function only simulates against execution state, it does not consider @@ -345,24 +362,9 @@ impl ExecutionState { return Err(ValidationError::SlotTooLow(highest_slot_for_account)); } - let account_state = match self.account_states.get(sender).copied() { - Some(account) => account, - None => { - // Fetch the account state from the client if it does not exist - let account = match self.client.get_account_state(sender, None).await { - Ok(account) => account, - Err(err) => { - return Err(ValidationError::Internal(format!( - "Error fetching account state: {:?}", - err - ))) - } - }; - - self.account_states.insert(*sender, account); - account - } - }; + let account_state = self.get_or_fetch_account_state(*sender).await.map_err(|e| { + ValidationError::Internal(format!("Error fetching account state: {:?}", e)) + })?; debug!(?account_state, ?nonce_diff, ?balance_diff, "Validating transaction"); From 7e0304e821471b37bfc5d23826051f34300a757d Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Fri, 24 Jan 2025 15:29:27 +0100 Subject: [PATCH 2/8] fix(sidecar): remove misleading comment This map can be read, just know that it contains the canonical account state at the head of the block. --- bolt-sidecar/src/state/execution.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 5ed98eef0..e5fbca4b4 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -144,7 +144,7 @@ pub struct ExecutionState { basefee: u128, /// The blob basefee at the head block. blob_basefee: u128, - /// The cached account states. This should never be read directly. These only contain the + /// The cached account states. These only contain the /// canonical account states at the head block, not the intermediate states. /// /// INVARIANT: the entries are modified only when receiving a new head. From a9a0b9371082a40ef48accf73fcc1c29b220a1e0 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Fri, 24 Jan 2025 16:51:22 +0100 Subject: [PATCH 3/8] chore(sidecar): deprecate trait fn that is implemented in alloy now --- bolt-sidecar/src/primitives/transaction.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/bolt-sidecar/src/primitives/transaction.rs b/bolt-sidecar/src/primitives/transaction.rs index a32f6f623..af150e1a1 100644 --- a/bolt-sidecar/src/primitives/transaction.rs +++ b/bolt-sidecar/src/primitives/transaction.rs @@ -5,7 +5,7 @@ use alloy::{ }, eips::eip2718::{Decodable2718, Encodable2718}, hex, - primitives::{Address, U256}, + primitives::Address, }; use reth_primitives::TransactionSigned; use serde::{de, ser::SerializeSeq}; @@ -14,9 +14,6 @@ use std::{borrow::Cow, fmt}; /// Trait that exposes additional information on transaction types that don't already do it /// by themselves (e.g. [`PooledTransaction`]). pub trait TransactionExt { - /// Returns the value of the transaction. - fn value(&self) -> U256; - /// Returns the blob sidecar of the transaction, if any. fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar>; @@ -25,16 +22,6 @@ pub trait TransactionExt { } impl TransactionExt for PooledTransaction { - fn value(&self) -> U256 { - match self { - Self::Legacy(transaction) => transaction.tx().value, - Self::Eip1559(transaction) => transaction.tx().value, - Self::Eip2930(transaction) => transaction.tx().value, - Self::Eip4844(transaction) => transaction.tx().tx().value, - Self::Eip7702(transaction) => transaction.tx().value, - } - } - fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> { match self { Self::Eip4844(transaction) => Some(&transaction.tx().sidecar), From ec45f730dbdca28197f12f4d0deb1388187e01b1 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 27 Jan 2025 16:48:23 +0100 Subject: [PATCH 4/8] fix(sidecar): don't run doc test --- bolt-sidecar/src/chain_io/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bolt-sidecar/src/chain_io/utils.rs b/bolt-sidecar/src/chain_io/utils.rs index f8b3b2973..6e1ef9442 100644 --- a/bolt-sidecar/src/chain_io/utils.rs +++ b/bolt-sidecar/src/chain_io/utils.rs @@ -48,7 +48,7 @@ fn pubkey_hash_digest(key: &BlsPublicKey) -> B512 { /// /// Example usage: /// -/// ```rust no_run +/// ```rust ignore /// sol! { /// library ErrorLib { /// error SomeError(uint256 code); From e1b2a823a3a8fa0bd0fcc05ec9992f22ed21af64 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 27 Jan 2025 16:49:27 +0100 Subject: [PATCH 5/8] feat(sidecar): move existing and new diffs-related types to their own file --- bolt-sidecar/src/primitives/diffs.rs | 98 ++++++++++++++++++++++++++++ bolt-sidecar/src/primitives/mod.rs | 3 + 2 files changed, 101 insertions(+) create mode 100644 bolt-sidecar/src/primitives/diffs.rs diff --git a/bolt-sidecar/src/primitives/diffs.rs b/bolt-sidecar/src/primitives/diffs.rs new file mode 100644 index 000000000..222a1babc --- /dev/null +++ b/bolt-sidecar/src/primitives/diffs.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use alloy::primitives::{Address, U256}; + +/// StateDiff tracks the intermediate changes to the state according to the block template. +#[derive(Debug, Default)] +pub struct StateDiff { + /// Map of diffs per address. Each diff is a tuple of the nonce and balance diff + /// that should be applied to the current state. + pub(crate) diffs: HashMap, +} + +impl StateDiff { + /// Returns a tuple of the nonce and balance diff for the given address. + /// The nonce diff should be added to the current nonce, the balance diff should be subtracted + /// from the current balance. + pub fn get_diff(&self, address: &Address) -> Option { + self.diffs.get(address).copied() + } +} + +/// AccountDiff tracks the changes to an account's nonce and balance. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct AccountDiff { + /// The nonce of the account. + nonce: u64, + /// The balance diff of the account. + balance: BalanceDiff, +} + +impl AccountDiff { + /// Creates a new account diff with the given nonce and balance diff. + pub fn new(nonce: u64, balance: BalanceDiff) -> Self { + Self { nonce, balance } + } + + /// Returns the nonce diff of the account. + pub fn nonce(&self) -> u64 { + self.nonce + } + + /// Returns the balance diff of the account. + pub fn balance(&self) -> BalanceDiff { + self.balance + } +} + +/// A balance diff is a tuple of consisting of a balance increase and a balance decrease. +/// +/// An `increase` should be _added_ to the current balance, while a `decrease` should be _subtracted_. +/// +/// Example: +/// ```rs +/// let balance = U256::from(100); +/// let balance_diff = BalanceDiff::new(U256::from(50), U256::from(10)); +/// assert_eq!(balance_diff.apply(balance), U256::from(140)); +/// ``` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct BalanceDiff { + /// The balance increase. + increase: U256, + /// The balance decrease. + decrease: U256, +} + +impl BalanceDiff { + /// Creates a new balance diff with the given increase and decrease. + pub fn new(increase: U256, decrease: U256) -> Self { + Self { increase, decrease } + } + + /// Returns the increase of the balance diff. + pub fn increase(&self) -> U256 { + self.increase + } + + /// Returns the decrease of the balance diff. + pub fn decrease(&self) -> U256 { + self.decrease + } + + /// Applies the balance diff to the given balance. + pub fn apply(&self, balance: U256) -> U256 { + balance.saturating_add(self.increase).saturating_sub(self.decrease) + } +} + +/// A trait for applying a balance diff to a U256 balance. +pub trait BalanceDiffApplier { + /// Applies the balance diff to the given balance. + fn apply_diff(&self, diff: BalanceDiff) -> U256; +} + +impl BalanceDiffApplier for U256 { + fn apply_diff(&self, diff: BalanceDiff) -> U256 { + self.saturating_add(diff.increase).saturating_sub(diff.decrease) + } +} diff --git a/bolt-sidecar/src/primitives/mod.rs b/bolt-sidecar/src/primitives/mod.rs index 22b2a156a..937cb0813 100644 --- a/bolt-sidecar/src/primitives/mod.rs +++ b/bolt-sidecar/src/primitives/mod.rs @@ -45,6 +45,9 @@ pub mod signature; /// JSON-RPC helper types and functions. pub mod jsonrpc; +/// Types and utilties relates to calculating account diffs. +pub mod diffs; + /// An alias for a Beacon Chain slot number pub type Slot = u64; From c4966c6f6b3e41ba7e2cd6114d4340fab30a3db9 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 27 Jan 2025 16:50:22 +0100 Subject: [PATCH 6/8] feat(sidecar): track balance increase in state diffs; use new diffs types --- bolt-sidecar/src/builder/template.rs | 96 ++++++++++++++++++++-------- bolt-sidecar/src/state/execution.rs | 72 +++++++++++---------- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/bolt-sidecar/src/builder/template.rs b/bolt-sidecar/src/builder/template.rs index 36d227da6..64bd9a8c4 100644 --- a/bolt-sidecar/src/builder/template.rs +++ b/bolt-sidecar/src/builder/template.rs @@ -7,12 +7,14 @@ use ethereum_consensus::{ deneb::mainnet::{Blob, BlobsBundle}, }; use reth_primitives::TransactionSigned; -use std::collections::HashMap; use tracing::warn; use crate::{ common::transactions::max_transaction_cost, - primitives::{AccountState, FullTransaction, SignedConstraints, TransactionExt}, + primitives::{ + diffs::{AccountDiff, BalanceDiff, StateDiff}, + AccountState, FullTransaction, SignedConstraints, TransactionExt, + }, }; /// A block template that serves as a fallback block, but is also used @@ -34,7 +36,7 @@ pub struct BlockTemplate { impl BlockTemplate { /// Return the state diff of the block template. - pub fn get_diff(&self, address: &Address) -> Option<(u64, U256)> { + pub fn get_diff(&self, address: &Address) -> Option { self.state_diff.get_diff(address) } @@ -123,14 +125,46 @@ impl BlockTemplate { pub fn add_constraints(&mut self, constraints: SignedConstraints) { for constraint in &constraints.message.transactions { let max_cost = max_transaction_cost(constraint); + + // Increase the nonce and decrease the balance of the sender self.state_diff .diffs .entry(*constraint.sender().expect("recovered sender")) - .and_modify(|(nonce, balance)| { - *nonce += 1; - *balance += max_cost; + .and_modify(|diff| { + *diff = AccountDiff::new( + diff.nonce().saturating_add(1), + BalanceDiff::new( + diff.balance().increase(), + diff.balance().decrease().saturating_add(max_cost), + ), + ) }) - .or_insert((1, max_cost)); + .or_insert(AccountDiff::new(1, BalanceDiff::new(U256::ZERO, max_cost))); + + // If there is an ETH transfer and it's not a contract creation, increase the balance + // of the recipient so that it can send inclusion requests on this preconfirmed state. + let value = constraint.tx.value(); + if value.is_zero() { + continue; + } + let Some(recipient) = constraint.to() else { continue }; + + self.state_diff + .diffs + .entry(recipient) + .and_modify(|diff| { + *diff = AccountDiff::new( + diff.nonce().saturating_add(1), + BalanceDiff::new( + diff.balance().increase().saturating_add(constraint.tx.value()), + diff.balance().decrease(), + ), + ) + }) + .or_insert(AccountDiff::new( + 0, + BalanceDiff::new(constraint.tx.value(), U256::ZERO), + )); } self.signed_constraints_list.push(constraints); @@ -141,13 +175,38 @@ impl BlockTemplate { let constraints = self.signed_constraints_list.remove(index); for constraint in &constraints.message.transactions { + let max_cost = max_transaction_cost(constraint); + self.state_diff .diffs .entry(*constraint.sender().expect("recovered sender")) - .and_modify(|(nonce, balance)| { - *nonce = nonce.saturating_sub(1); - *balance -= max_transaction_cost(constraint); + .and_modify(|diff| { + *diff = AccountDiff::new( + diff.nonce().saturating_sub(1), + BalanceDiff::new( + diff.balance().increase(), + diff.balance().decrease().saturating_sub(max_cost), + ), + ) }); + + // If there is an ETH transfer and it's not a contract creation, remove the balance + // increase of the recipient. + let value = constraint.tx.value(); + if value.is_zero() { + continue; + } + let Some(recipient) = constraint.to() else { continue }; + + self.state_diff.diffs.entry(recipient).and_modify(|diff| { + *diff = AccountDiff::new( + diff.nonce().saturating_sub(1), + BalanceDiff::new( + diff.balance().increase().saturating_sub(constraint.tx.value()), + diff.balance().decrease(), + ), + ) + }); } } @@ -196,20 +255,3 @@ impl BlockTemplate { } } } - -/// StateDiff tracks the intermediate changes to the state according to the block template. -#[derive(Debug, Default)] -pub struct StateDiff { - /// Map of diffs per address. Each diff is a tuple of the nonce and balance diff - /// that should be applied to the current state. - pub(crate) diffs: HashMap, -} - -impl StateDiff { - /// Returns a tuple of the nonce and balance diff for the given address. - /// The nonce diff should be added to the current nonce, the balance diff should be subtracted - /// from the current balance. - pub fn get_diff(&self, address: &Address) -> Option<(u64, U256)> { - self.diffs.get(address).copied() - } -} diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index e5fbca4b4..7021db053 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -9,14 +9,16 @@ use thiserror::Error; use tracing::{debug, error, trace, warn}; use crate::{ - builder::BlockTemplate, + builder::template::BlockTemplate, common::{ score_cache::ScoreCache, transactions::{calculate_max_basefee, max_transaction_cost, validate_transaction}, }, config::limits::LimitsOpts, primitives::{ - signature::SignatureError, AccountState, InclusionRequest, SignedConstraints, Slot, + diffs::{AccountDiff, BalanceDiff, BalanceDiffApplier}, + signature::SignatureError, + AccountState, InclusionRequest, SignedConstraints, Slot, }, state::pricing, telemetry::ApiMetrics, @@ -354,7 +356,7 @@ impl ExecutionState { for tx in &req.txs { let sender = tx.sender().expect("Recovered sender"); - let (nonce_diff, balance_diff, highest_slot_for_account) = + let (account_diff, highest_slot_for_account) = compute_diffs(&self.block_templates, sender); if target_slot < highest_slot_for_account { @@ -366,7 +368,7 @@ impl ExecutionState { ValidationError::Internal(format!("Error fetching account state: {:?}", e)) })?; - debug!(?account_state, ?nonce_diff, ?balance_diff, "Validating transaction"); + debug!(?account_state, ?account_diff, "Validating transaction"); let sender_nonce_diff = bundle_nonce_diff_map.entry(sender).or_insert(0); let sender_balance_diff = bundle_balance_diff_map.entry(sender).or_insert(U256::ZERO); @@ -376,14 +378,13 @@ impl ExecutionState { let account_state_with_diffs = AccountState { transaction_count: account_state .transaction_count - .saturating_add(nonce_diff) + .saturating_add(account_diff.nonce()) .saturating_add(*sender_nonce_diff), - balance: account_state - .balance - .saturating_sub(balance_diff) + balance: account_diff + .balance() + .apply(account_state.balance) .saturating_sub(*sender_balance_diff), - has_code: account_state.has_code, }; @@ -524,11 +525,12 @@ impl ExecutionState { template.retain(address, expected_account_state); // Update the account state with the remaining state diff for the next iteration. - if let Some((nonce_diff, balance_diff)) = template.get_diff(&address) { + if let Some(account_diff) = template.get_diff(&address) { // Nonce will always be increased - expected_account_state.transaction_count += nonce_diff; - // Balance will always be decreased - expected_account_state.balance -= balance_diff; + expected_account_state.transaction_count += account_diff.nonce(); + // Re-apply balance diffs + expected_account_state.balance = + expected_account_state.balance.apply_diff(account_diff.balance()) } } } @@ -583,21 +585,26 @@ pub struct StateUpdate { fn compute_diffs( block_templates: &HashMap, sender: &Address, -) -> (u64, U256, u64) { +) -> (AccountDiff, u64) { block_templates.iter().fold( - (0, U256::ZERO, 0), - |(nonce_diff_acc, balance_diff_acc, highest_slot), (slot, block_template)| { - let (nonce_diff, balance_diff, current_slot) = block_template + (AccountDiff::default(), 0), + |(diff_acc, highest_slot), (slot, block_template)| { + let (diff, current_slot) = block_template .get_diff(sender) - .map(|(nonce, balance)| (nonce, balance, *slot)) - .unwrap_or((0, U256::ZERO, 0)); + .map(|diff| (diff, *slot)) + .unwrap_or((AccountDiff::default(), 0)); // This might be noisy but it is a critical part in validation logic and // hard to debug. - trace!(?nonce_diff, ?balance_diff, ?slot, ?sender, "found diffs"); + trace!(account_diff = ?diff, ?slot, ?sender, "found diffs"); ( - nonce_diff_acc + nonce_diff, - balance_diff_acc.saturating_add(balance_diff), + AccountDiff::new( + diff_acc.nonce() + diff.nonce(), + BalanceDiff::new( + diff_acc.balance().increase().saturating_add(diff.balance().increase()), + diff_acc.balance().decrease().saturating_add(diff.balance().decrease()), + ), + ), u64::max(highest_slot, current_slot), ) }, @@ -608,7 +615,7 @@ fn compute_diffs( mod tests { use super::*; use crate::{ - builder::template::StateDiff, config::limits::DEFAULT_MAX_COMMITTED_GAS, + config::limits::DEFAULT_MAX_COMMITTED_GAS, primitives::diffs::StateDiff, signer::local::LocalSigner, }; use std::{num::NonZero, str::FromStr, time::Duration}; @@ -636,10 +643,10 @@ mod tests { let block_templates = HashMap::new(); let sender = Address::random(); - let (nonce_diff, balance_diff, highest_slot) = compute_diffs(&block_templates, &sender); + let (account_diff, highest_slot) = compute_diffs(&block_templates, &sender); - assert_eq!(nonce_diff, 0); - assert_eq!(balance_diff, U256::ZERO); + assert_eq!(account_diff.nonce(), 0); + assert_eq!(account_diff.balance().decrease(), U256::ZERO); assert_eq!(highest_slot, 0); } @@ -650,7 +657,8 @@ mod tests { let nonce = 1; let balance_diff = U256::from(2); let mut diffs = HashMap::new(); - diffs.insert(sender, (nonce, balance_diff)); + let account_diff = AccountDiff::new(nonce, BalanceDiff::new(U256::ZERO, balance_diff)); + diffs.insert(sender, account_diff); // Insert StateDiff entry let state_diff = StateDiff { diffs }; @@ -660,10 +668,10 @@ mod tests { let block_template = BlockTemplate { state_diff, signed_constraints_list: vec![] }; block_templates.insert(10, block_template); - let (nonce_diff, balance_diff, highest_slot) = compute_diffs(&block_templates, &sender); + let (computed_account_diff, highest_slot) = compute_diffs(&block_templates, &sender); - assert_eq!(nonce_diff, 1); - assert_eq!(balance_diff, U256::from(2)); + assert_eq!(computed_account_diff.nonce(), 1); + assert_eq!(computed_account_diff.balance().decrease(), U256::from(2)); assert_eq!(highest_slot, 10); } @@ -715,7 +723,7 @@ mod tests { // Insert a constraint diff for slot 11 let mut diffs = HashMap::new(); - diffs.insert(*sender, (1, U256::ZERO)); + diffs.insert(*sender, AccountDiff::new(1, BalanceDiff::default())); state.block_templates.insert( 11, BlockTemplate { state_diff: StateDiff { diffs }, signed_constraints_list: vec![] }, @@ -748,7 +756,7 @@ mod tests { // Insert a constraint diff for slot 9 to simulate nonce increment let mut diffs = HashMap::new(); - diffs.insert(*sender, (1, U256::ZERO)); + diffs.insert(*sender, AccountDiff::new(1, BalanceDiff::default())); state.block_templates.insert( 9, BlockTemplate { state_diff: StateDiff { diffs }, signed_constraints_list: vec![] }, From 042f1384bc65d8bb331e16d4e2fbdfd552070afe Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 27 Jan 2025 16:07:35 +0100 Subject: [PATCH 7/8] test(sidecar): balance increase --- .../src/api/commitments/server/mod.rs | 4 +- bolt-sidecar/src/state/execution.rs | 132 ++++++++++++++---- bolt-sidecar/src/test_util.rs | 12 +- 3 files changed, 114 insertions(+), 34 deletions(-) diff --git a/bolt-sidecar/src/api/commitments/server/mod.rs b/bolt-sidecar/src/api/commitments/server/mod.rs index e424ef16e..cb7c60246 100644 --- a/bolt-sidecar/src/api/commitments/server/mod.rs +++ b/bolt-sidecar/src/api/commitments/server/mod.rs @@ -206,7 +206,7 @@ mod test { let sk = SecretKey::random(&mut rand::thread_rng()); let signer = PrivateKeySigner::from(sk.clone()); let tx = default_test_transaction(signer.address(), None); - let req = create_signed_inclusion_request(&[tx], &sk, 12).await.unwrap(); + let req = create_signed_inclusion_request(&[tx], &sk.to_bytes(), 12).await.unwrap(); let payload = json!({ "jsonrpc": "2.0", @@ -249,7 +249,7 @@ mod test { let sk = SecretKey::random(&mut rand::thread_rng()); let signer = PrivateKeySigner::from(sk.clone()); let tx = default_test_transaction(signer.address(), None); - let req = create_signed_inclusion_request(&[tx], &sk, 12).await.unwrap(); + let req = create_signed_inclusion_request(&[tx], &sk.to_bytes(), 12).await.unwrap(); let sig = req.signature.unwrap().to_hex(); diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 7021db053..26542070a 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -628,6 +628,7 @@ mod tests { providers::{network::TransactionBuilder, Provider, ProviderBuilder}, signers::local::PrivateKeySigner, }; + use alloy_node_bindings::WEI_IN_ETHER; use fetcher::{StateClient, StateFetcher}; use tracing::info; @@ -638,6 +639,20 @@ mod tests { test_util::{create_signed_inclusion_request, default_test_transaction, launch_anvil}, }; + fn add_constraint( + request: InclusionRequest, + state: &mut ExecutionState, + signer: &LocalSigner, + target_slot: u64, + ) -> eyre::Result<()> { + let message = ConstraintsMessage::build(Default::default(), request.clone()); + let signature = signer.sign_commit_boost_root(message.digest())?; + let signed_constraints = SignedConstraints { message, signature }; + state.add_constraint(target_slot, signed_constraints); + + Ok(()) + } + #[test] fn test_compute_diff_no_templates() { let block_templates = HashMap::new(); @@ -693,7 +708,7 @@ mod tests { let tx = default_test_transaction(*sender, None); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(state.validate_request(&mut request).await.is_ok()); @@ -719,7 +734,7 @@ mod tests { // Create a transaction with a nonce that is too high let tx = default_test_transaction(*sender, Some(1)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; // Insert a constraint diff for slot 11 let mut diffs = HashMap::new(); @@ -765,7 +780,7 @@ mod tests { // Create a transaction with a nonce that is too low let tx = default_test_transaction(*sender, Some(0)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -777,7 +792,7 @@ mod tests { // Create a transaction with a nonce that is too high let tx = default_test_transaction(*sender, Some(2)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -807,7 +822,7 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_value(uint!(11_000_U256 * Uint::from(ETH_TO_WEI))); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -841,7 +856,7 @@ mod tests { // burn the balance let tx = default_test_transaction(*sender, Some(0)).with_value(uint!(balance_to_burn)); - let request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; let tx_bytes = request.txs.first().unwrap().encoded_2718(); let _ = client.inner().send_raw_transaction(tx_bytes.into()).await?; @@ -852,7 +867,7 @@ mod tests { // create a new transaction and request a preconfirmation for it let tx = default_test_transaction(*sender, Some(1)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; let validation = state.validate_request(&mut request).await; assert!(validation.is_ok(), "Validation failed: {validation:?}"); @@ -864,7 +879,7 @@ mod tests { // create a new transaction and request a preconfirmation for it let tx = default_test_transaction(*sender, Some(2)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; // this should fail because the balance is insufficient as we spent // all of it on the previous preconfirmation @@ -879,6 +894,66 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_balance_increase() -> eyre::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let anvil = launch_anvil(); + let client = StateClient::new(anvil.endpoint_url()); + + let mut state = ExecutionState::new(client.clone(), LimitsOpts::default()).await?; + + let sender = anvil.addresses().first().unwrap(); + let sender_pk = anvil.keys().first().unwrap(); + let signer = LocalSigner::random(); + + // initialize the state by updating the head once + let slot = client.get_head().await?; + state.update_head(None, slot).await?; + let target_slot = 10; + + let recipient_sk = PrivateKeySigner::random(); + let recipient_pk = recipient_sk.address(); + + // Create a transfer of 1 ETH to the recipient + let tx = default_test_transaction(*sender, Some(0)) + .with_to(recipient_pk) + .with_value(WEI_IN_ETHER); + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; + + let validation = state.validate_request(&mut request).await; + assert!(validation.is_ok(), "Validation failed: {validation:?}"); + + add_constraint(request.clone(), &mut state, &signer, target_slot)?; + + // Now the sender should have enough balance to send a transaction + let tx = default_test_transaction(recipient_pk, Some(0)) + .with_value(WEI_IN_ETHER.div_ceil(U256::from(2))); + let mut request = + create_signed_inclusion_request(&[tx], recipient_sk.to_bytes().as_slice(), 10).await?; + + let validation_result = state.validate_request(&mut request).await; + + add_constraint(request, &mut state, &signer, target_slot)?; + + assert!(validation_result.is_ok(), "validation failed: {validation_result:?}"); + + // The recipient cannot afford a second transfer of 0.5 ETH + let tx = default_test_transaction(recipient_pk, Some(1)) + .with_value(WEI_IN_ETHER.div_ceil(U256::from(2))); + let mut request = + create_signed_inclusion_request(&[tx], recipient_sk.to_bytes().as_slice(), 10).await?; + + let validation_result = state.validate_request(&mut request).await; + assert!( + matches!(validation_result, Err(ValidationError::InsufficientBalance)), + "Expected InsufficientBalance error, got {:?}", + validation_result + ); + + Ok(()) + } + #[tokio::test] async fn test_invalid_inclusion_request_basefee() -> eyre::Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -903,7 +978,7 @@ mod tests { .with_max_fee_per_gas(basefee - 1) .with_max_priority_fee_per_gas(basefee / 2); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -935,7 +1010,7 @@ mod tests { let tx = default_test_transaction(*sender, None).with_gas_limit(6_000_000); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -965,7 +1040,7 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_max_priority_fee_per_gas(GWEI_TO_WEI as u128 / 2); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -976,7 +1051,7 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_max_priority_fee_per_gas(4 * GWEI_TO_WEI as u128); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(state.validate_request(&mut request).await.is_ok()); @@ -1008,7 +1083,7 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_gas_price(max_base_fee + GWEI_TO_WEI as u128 / 2); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -1019,7 +1094,7 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_gas_price(max_base_fee + 4 * GWEI_TO_WEI as u128); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(state.validate_request(&mut request).await.is_ok()); @@ -1051,7 +1126,8 @@ mod tests { let tx = default_test_transaction(*sender, None) .with_gas_price(max_base_fee + 4 * GWEI_TO_WEI as u128); - let mut request = create_signed_inclusion_request(&[tx.clone(), tx], sender_pk, 10).await?; + let mut request = + create_signed_inclusion_request(&[tx.clone(), tx], &sender_pk.to_bytes(), 10).await?; let response = state.validate_request(&mut request).await; println!("{response:?}"); @@ -1086,7 +1162,8 @@ mod tests { let signed = tx.clone().build(&signer).await?; let target_slot = 10; - let mut request = create_signed_inclusion_request(&[tx], sender_pk, target_slot).await?; + let mut request = + create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), target_slot).await?; let inclusion_request = request.clone(); assert!(state.validate_request(&mut request).await.is_ok()); @@ -1134,7 +1211,8 @@ mod tests { let tx = default_test_transaction(*sender, None); let target_slot = 10; - let mut request = create_signed_inclusion_request(&[tx], sender_pk, target_slot).await?; + let mut request = + create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), target_slot).await?; let inclusion_request = request.clone(); assert!(state.validate_request(&mut request).await.is_ok()); @@ -1178,7 +1256,8 @@ mod tests { .with_gas_limit(limits.max_committed_gas_per_slot.get() - 1); let target_slot = 10; - let mut request = create_signed_inclusion_request(&[tx], sender_pk, target_slot).await?; + let mut request = + create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), target_slot).await?; let inclusion_request = request.clone(); let validation = state.validate_request(&mut request).await; @@ -1196,7 +1275,7 @@ mod tests { // This tx will exceed the committed gas limit let tx = default_test_transaction(*sender, Some(1)); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, 10).await?; + let mut request = create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -1226,7 +1305,8 @@ mod tests { let tx2 = default_test_transaction(*sender, Some(1)); let tx3 = default_test_transaction(*sender, Some(2)); - let mut request = create_signed_inclusion_request(&[tx1, tx2, tx3], sender_pk, 10).await?; + let mut request = + create_signed_inclusion_request(&[tx1, tx2, tx3], &sender_pk.to_bytes(), 10).await?; assert!(state.validate_request(&mut request).await.is_ok()); @@ -1253,7 +1333,8 @@ mod tests { let tx2 = default_test_transaction(*sender, Some(1)); let tx3 = default_test_transaction(*sender, Some(3)); // wrong nonce, should be 2 - let mut request = create_signed_inclusion_request(&[tx1, tx2, tx3], sender_pk, 10).await?; + let mut request = + create_signed_inclusion_request(&[tx1, tx2, tx3], &sender_pk.to_bytes(), 10).await?; assert!(matches!( state.validate_request(&mut request).await, @@ -1284,7 +1365,8 @@ mod tests { let tx3 = default_test_transaction(*sender, Some(2)) .with_value(uint!(11_000_U256 * Uint::from(ETH_TO_WEI))); - let mut request = create_signed_inclusion_request(&[tx1, tx2, tx3], sender_pk, 10).await?; + let mut request = + create_signed_inclusion_request(&[tx1, tx2, tx3], &sender_pk.to_bytes(), 10).await?; let validation_result = state.validate_request(&mut request).await; assert!( @@ -1319,7 +1401,8 @@ mod tests { let target_slot = 32; let tx = default_test_transaction(*sender, None).with_gas_price(ETH_TO_WEI / 1_000_000); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, target_slot).await?; + let mut request = + create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), target_slot).await?; let inclusion_request = request.clone(); let request_validation = state.validate_request(&mut request).await; @@ -1340,7 +1423,8 @@ mod tests { // 2. Send an inclusion request at `slot + 1` with `target_slot`. let tx = default_test_transaction(*sender, Some(1)).with_gas_price(ETH_TO_WEI / 1_000_000); - let mut request = create_signed_inclusion_request(&[tx], sender_pk, target_slot).await?; + let mut request = + create_signed_inclusion_request(&[tx], &sender_pk.to_bytes(), target_slot).await?; let inclusion_request = request.clone(); let request_validation = state.validate_request(&mut request).await; diff --git a/bolt-sidecar/src/test_util.rs b/bolt-sidecar/src/test_util.rs index 656e5f8c1..71264881e 100644 --- a/bolt-sidecar/src/test_util.rs +++ b/bolt-sidecar/src/test_util.rs @@ -5,11 +5,7 @@ use alloy::{ network::{EthereumWallet, TransactionBuilder}, primitives::{Address, U256}, rpc::types::TransactionRequest, - signers::{ - k256::{ecdsa::SigningKey as K256SigningKey, SecretKey as K256SecretKey}, - local::PrivateKeySigner, - Signer, - }, + signers::{k256::ecdsa::SigningKey as K256SigningKey, local::PrivateKeySigner, Signer}, }; use alloy_node_bindings::{Anvil, AnvilInstance}; use blst::min_pk::SecretKey; @@ -106,7 +102,7 @@ pub(crate) async fn get_test_config() -> Option { Some(opts) } -/// Launch a local instance of the Anvil test chain. +/// Launch a local instance of the Anvil test chain with 1 second block time pub(crate) fn launch_anvil() -> AnvilInstance { Anvil::new().block_time(1).chain_id(1337).spawn() } @@ -155,10 +151,10 @@ impl SignableECDSA for TestSignableData { /// from the given transaction, private key of the sender, and slot. pub(crate) async fn create_signed_inclusion_request( txs: &[TransactionRequest], - sk: &K256SecretKey, + sk: &[u8], slot: u64, ) -> eyre::Result { - let sk = K256SigningKey::from_slice(sk.to_bytes().as_slice())?; + let sk = K256SigningKey::from_slice(sk)?; let signer = PrivateKeySigner::from_signing_key(sk.clone()); let wallet = EthereumWallet::from(signer.clone()); From 54cb18b3e2ef5abc509d595f42650880023f2737 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Mon, 27 Jan 2025 16:33:16 +0100 Subject: [PATCH 8/8] test(sidecar): balance increase dropped, chained preconfs dropped --- bolt-sidecar/src/state/execution.rs | 138 ++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/bolt-sidecar/src/state/execution.rs b/bolt-sidecar/src/state/execution.rs index 26542070a..09cef784b 100644 --- a/bolt-sidecar/src/state/execution.rs +++ b/bolt-sidecar/src/state/execution.rs @@ -894,6 +894,8 @@ mod tests { Ok(()) } + /// Tests that a balance increase allows the recipient to send a transaction using preconfirmed + /// state. #[tokio::test] async fn test_balance_increase() -> eyre::Result<()> { let _ = tracing_subscriber::fmt::try_init(); @@ -954,6 +956,142 @@ mod tests { Ok(()) } + /// Tests that a balance increase is dropped if the preconfirmation is cancelled. + #[tokio::test] + async fn test_balance_increase_dropped() -> eyre::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let anvil = launch_anvil(); + let client = StateClient::new(anvil.endpoint_url()); + + let mut state = ExecutionState::new(client.clone(), LimitsOpts::default()).await?; + + let sender = anvil.addresses().first().unwrap(); + let sender_pk = anvil.keys().first().unwrap(); + let signer = LocalSigner::random(); + + // initialize the state by updating the head once + let slot = client.get_head().await?; + state.update_head(None, slot).await?; + let target_slot = slot; + + let recipient_sk = PrivateKeySigner::random(); + let recipient_pk = recipient_sk.address(); + + // Create a transfer of 1 ETH to the recipient + let tx = default_test_transaction(*sender, Some(0)) + .with_to(recipient_pk) + .with_value(WEI_IN_ETHER); + let mut request = + create_signed_inclusion_request(&[tx.clone()], &sender_pk.to_bytes(), 10).await?; + + let validation = state.validate_request(&mut request).await; + assert!(validation.is_ok(), "Validation failed: {validation:?}"); + + add_constraint(request.clone(), &mut state, &signer, target_slot)?; + + // Send a cancel tx + let tx = default_test_transaction(*sender, Some(0)) + .with_to(*sender) + .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas.unwrap() + 1); + + let _ = client.inner().send_transaction(tx).await?; + + // Wait 1s, update the head to include the cancel tx; the constraint should be dropped + tokio::time::sleep(Duration::from_secs(1)).await; + state.update_head(None, slot + 1).await?; + + // Now the recipient should not have balance + let tx = default_test_transaction(recipient_pk, Some(0)).with_value(U256::from(1)); + let mut request = create_signed_inclusion_request( + &[tx], + recipient_sk.to_bytes().as_slice(), + target_slot + 1, + ) + .await?; + + let validation_result = state.validate_request(&mut request).await; + + assert!( + matches!(validation_result, Err(ValidationError::InsufficientBalance)), + "Expected InsufficientBalance error, got {:?}", + validation_result + ); + + Ok(()) + } + + /// Tests that a chained preconfirmation is dropped if the previous one is cancelled. + #[tokio::test] + async fn test_chained_preconfs_dropped() -> eyre::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let anvil = launch_anvil(); + let client = StateClient::new(anvil.endpoint_url()); + + let mut state = ExecutionState::new(client.clone(), LimitsOpts::default()).await?; + + let sender = anvil.addresses().first().unwrap(); + let sender_pk = anvil.keys().first().unwrap(); + let signer = LocalSigner::random(); + + // initialize the state by updating the head once + let slot = client.get_head().await?; + state.update_head(None, slot).await?; + let target_slot = slot + 2; + + let recipient_sk = PrivateKeySigner::random(); + let recipient_pk = recipient_sk.address(); + + // Create a transfer of 1 ETH to the recipient + let tx_1 = default_test_transaction(*sender, Some(0)) + .with_to(recipient_pk) + .with_value(WEI_IN_ETHER); + let mut request = + create_signed_inclusion_request(&[tx_1.clone()], &sender_pk.to_bytes(), 10).await?; + + let validation = state.validate_request(&mut request).await; + assert!(validation.is_ok(), "Validation failed: {validation:?}"); + + add_constraint(request.clone(), &mut state, &signer, target_slot)?; + + // Now the sender should have enough balance to send a transaction + let tx_2 = default_test_transaction(recipient_pk, Some(0)) + .with_value(WEI_IN_ETHER.div_ceil(U256::from(2))); + let mut request = + create_signed_inclusion_request(&[tx_2], recipient_sk.to_bytes().as_slice(), 10) + .await?; + + let validation_result = state.validate_request(&mut request).await; + + add_constraint(request, &mut state, &signer, target_slot)?; + + assert!(validation_result.is_ok(), "validation failed: {validation_result:?}"); + + // Cancel the first preconfirmation request so that both preconfs are dropped. + + // Send a cancel tx + let tx_3 = default_test_transaction(*sender, Some(0)) + .with_to(*sender) + .with_max_priority_fee_per_gas(tx_1.max_priority_fee_per_gas.unwrap() + 1); + + let _ = client.inner().send_transaction(tx_3).await?; + + // Wait 1s, update the head to include the cancel tx; the constraint should be dropped + tokio::time::sleep(Duration::from_secs(1)).await; + state.update_head(None, slot + 1).await?; + + // Check that both preconfs have been dropped + + let template = state.block_templates.get(&target_slot).unwrap(); + assert!( + template.signed_constraints_list.is_empty(), + "block template should be empty, but got: {template:?}" + ); + + Ok(()) + } + #[tokio::test] async fn test_invalid_inclusion_request_basefee() -> eyre::Result<()> { let _ = tracing_subscriber::fmt::try_init();