Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sidecar): process pending bundle diffs #169

Merged
merged 4 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bolt-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async fn main() -> Result<()> {
} else {
// Send rpc requests singularly for each transaction
send_rpc_request(
vec![tx_rlp.clone()],
vec![tx_rlp],
vec![*tx_hash],
target_slot,
target_sidecar_url.clone(),
Expand Down
8 changes: 6 additions & 2 deletions bolt-sidecar/src/api/commitments/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ 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_commitment_request(tx, &sk, 12).await.unwrap();
let req = create_signed_commitment_request(&[tx], &sk, 12)
.await
.unwrap();

let payload = json!({
"jsonrpc": "2.0",
Expand Down Expand Up @@ -325,7 +327,9 @@ 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_commitment_request(tx, &sk, 12).await.unwrap();
let req = create_signed_commitment_request(&[tx], &sk, 12)
.await
.unwrap();

let sig = req.signature().unwrap().to_hex();

Expand Down
9 changes: 5 additions & 4 deletions bolt-sidecar/src/primitives/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ pub struct ConstraintsMessage {
impl ConstraintsMessage {
/// Builds a constraints message from an inclusion request and metadata
pub fn build(validator_index: u64, request: InclusionRequest) -> Self {
let mut constraints = Vec::with_capacity(request.txs.len());
for tx in request.txs {
constraints.push(Constraint::from_transaction(tx, None));
}
let constraints = request
.txs
.into_iter()
.map(|tx| Constraint::from_transaction(tx, None))
.collect();

Self {
validator_index,
Expand Down
158 changes: 139 additions & 19 deletions bolt-sidecar/src/state/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use thiserror::Error;

use crate::{
builder::BlockTemplate,
common::{calculate_max_basefee, validate_transaction},
common::{calculate_max_basefee, max_transaction_cost, validate_transaction},
config::Limits,
primitives::{AccountState, CommitmentRequest, SignedConstraints, Slot},
};
Expand All @@ -37,7 +37,7 @@ pub enum ValidationError {
#[error("Transaction nonce too low. Expected {0}, got {1}")]
NonceTooLow(u64, u64),
/// The transaction nonce is too high.
#[error("Transaction nonce too high")]
#[error("Transaction nonce too high. Expected {0}, got {1}")]
NonceTooHigh(u64, u64),
/// The sender account is a smart contract and has code.
#[error("Account has code")]
Expand Down Expand Up @@ -273,7 +273,18 @@ impl<C: StateFetcher> ExecutionState<C> {
return Err(ValidationError::SlotTooLow(self.slot));
}

for tx in &req.txs {
// Validate each transaction in the request against the account state,
// keeping track of the nonce and balance diffs, including:
// - any existing state in the account trie
// - any previously committed transactions
// - any previous transaction in the same request
//
// NOTE: it's also possible for a request to contain multiple transactions
// from different senders, in this case each sender will have its own nonce
// and balance diffs that will be applied to the account state.
let mut bundle_nonce_diff_map = HashMap::new();
let mut bundle_balance_diff_map = HashMap::new();
for tx in req.txs.iter() {
let sender = tx.sender().expect("Recovered sender");

// From previous preconfirmations requests retrieve
Expand Down Expand Up @@ -326,12 +337,30 @@ impl<C: StateFetcher> ExecutionState<C> {
}
};

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);

// Apply the diffs to this account according to the info fetched from the templates
// and the current bundle diffs for this sender.
let account_state_with_diffs = AccountState {
transaction_count: account_state.transaction_count.saturating_add(nonce_diff),
balance: account_state.balance.saturating_sub(balance_diff),
transaction_count: account_state
.transaction_count
.saturating_add(nonce_diff)
.saturating_add(*sender_nonce_diff),

balance: account_state
.balance
.saturating_sub(balance_diff)
.saturating_sub(*sender_balance_diff),

// TODO(nico): what if this changes in the middle of the request?
has_code: account_state.has_code,
merklefruit marked this conversation as resolved.
Show resolved Hide resolved
};

// Increase the bundle nonce and balance diffs for this sender for the next iteration
*sender_nonce_diff += 1;
*sender_balance_diff += max_transaction_cost(tx);

// Validate the transaction against the account state with existing diffs
validate_transaction(&account_state_with_diffs, tx)?;

Expand Down Expand Up @@ -501,7 +530,7 @@ mod tests {

let tx = default_test_transaction(*sender, None);

let mut request = create_signed_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(state
.validate_commitment_request(&mut request)
Expand Down Expand Up @@ -530,7 +559,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

// Insert a constraint diff for slot 11
let mut diffs = HashMap::new();
Expand Down Expand Up @@ -582,7 +611,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand All @@ -594,7 +623,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand Down Expand Up @@ -624,7 +653,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand Down Expand Up @@ -658,7 +687,7 @@ mod tests {

// burn the balance
let tx = default_test_transaction(*sender, Some(0)).with_value(uint!(balance_to_burn));
let request = create_signed_commitment_request(tx, sender_pk, 10).await?;
let request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;
let tx_bytes = request
.as_inclusion_request()
.unwrap()
Expand All @@ -675,7 +704,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(state
.validate_commitment_request(&mut request)
Expand All @@ -689,7 +718,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_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

// this should fail because the balance is insufficient as we spent
// all of it on the previous preconfirmation
Expand Down Expand Up @@ -724,7 +753,7 @@ mod tests {
.with_max_fee_per_gas(basefee - 1)
.with_max_priority_fee_per_gas(basefee / 2);

let mut request = create_signed_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand Down Expand Up @@ -756,7 +785,7 @@ mod tests {

let tx = default_test_transaction(*sender, None).with_gas_limit(6_000_000);

let mut request = create_signed_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand Down Expand Up @@ -791,7 +820,7 @@ mod tests {
let signed = tx.clone().build(&signer).await?;

let target_slot = 10;
let mut request = create_signed_commitment_request(tx, sender_pk, target_slot).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, target_slot).await?;
let inclusion_request = request.as_inclusion_request().unwrap().clone();

assert!(state
Expand Down Expand Up @@ -855,7 +884,7 @@ mod tests {
let tx = default_test_transaction(*sender, None);

let target_slot = 10;
let mut request = create_signed_commitment_request(tx, sender_pk, target_slot).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, target_slot).await?;
let inclusion_request = request.as_inclusion_request().unwrap().clone();

assert!(state
Expand Down Expand Up @@ -910,7 +939,7 @@ mod tests {
let tx = default_test_transaction(*sender, None).with_gas_limit(4_999_999);

let target_slot = 10;
let mut request = create_signed_commitment_request(tx, sender_pk, target_slot).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, target_slot).await?;
let inclusion_request = request.as_inclusion_request().unwrap().clone();

assert!(state
Expand All @@ -936,7 +965,7 @@ mod tests {
// This tx will exceed the committed gas limit
let tx = default_test_transaction(*sender, Some(1));

let mut request = create_signed_commitment_request(tx, sender_pk, 10).await?;
let mut request = create_signed_commitment_request(&[tx], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Expand All @@ -945,4 +974,95 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn test_valid_bundle_inclusion_request() -> 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(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();

// initialize the state by updating the head once
let slot = client.get_head().await?;
state.update_head(None, slot).await?;

let tx1 = default_test_transaction(*sender, Some(0));
let tx2 = default_test_transaction(*sender, Some(1));
let tx3 = default_test_transaction(*sender, Some(2));

let mut request = create_signed_commitment_request(&[tx1, tx2, tx3], sender_pk, 10).await?;

assert!(state
.validate_commitment_request(&mut request)
.await
.is_ok());

Ok(())
}

#[tokio::test]
async fn test_invalid_bundle_inclusion_request_nonce() -> 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(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();

// initialize the state by updating the head once
let slot = client.get_head().await?;
state.update_head(None, slot).await?;

let tx1 = default_test_transaction(*sender, Some(0));
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_commitment_request(&[tx1, tx2, tx3], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Err(ValidationError::NonceTooHigh(2, 3))
));

Ok(())
}

#[tokio::test]
async fn test_invalid_bundle_inclusion_request_balance() -> 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(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();

// initialize the state by updating the head once
let slot = client.get_head().await?;
state.update_head(None, slot).await?;

let tx1 = default_test_transaction(*sender, Some(0));
let tx2 = default_test_transaction(*sender, Some(1));
let tx3 = default_test_transaction(*sender, Some(2))
.with_value(uint!(11_000_U256 * Uint::from(ETH_TO_WEI)));

let mut request = create_signed_commitment_request(&[tx1, tx2, tx3], sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&mut request).await,
Err(ValidationError::InsufficientBalance)
));

Ok(())
}
}
17 changes: 10 additions & 7 deletions bolt-sidecar/src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use secp256k1::Message;

use crate::{
crypto::{ecdsa::SignableECDSA, SignableBLS},
primitives::{CommitmentRequest, InclusionRequest},
primitives::{CommitmentRequest, FullTransaction, InclusionRequest},
Config,
};

Expand Down Expand Up @@ -144,20 +144,23 @@ impl SignableECDSA for TestSignableData {
/// Create a valid signed commitment request for testing purposes
/// from the given transaction, private key of the sender, and slot.
pub(crate) async fn create_signed_commitment_request(
tx: TransactionRequest,
txs: &[TransactionRequest],
sk: &K256SecretKey,
slot: u64,
) -> eyre::Result<CommitmentRequest> {
let sk = K256SigningKey::from_slice(sk.to_bytes().as_slice())?;
let signer = PrivateKeySigner::from_signing_key(sk.clone());
let wallet = EthereumWallet::from(signer.clone());

let tx_signed = tx.build(&wallet).await?;
let raw_encoded = tx_signed.encoded_2718();
let tx_pooled = PooledTransactionsElement::decode_enveloped(&mut raw_encoded.as_slice())?;

let mut full_txs = Vec::with_capacity(txs.len());
for tx in txs {
let tx_signed = tx.clone().build(&wallet).await?;
let raw_encoded = tx_signed.encoded_2718();
let tx_pooled = PooledTransactionsElement::decode_enveloped(&mut raw_encoded.as_slice())?;
full_txs.push(FullTransaction::from(tx_pooled));
}
let mut request = InclusionRequest {
txs: vec![tx_pooled.into()],
txs: full_txs,
slot,
signature: None,
signer: None,
Expand Down
Loading