Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: commitment test utils
Browse files Browse the repository at this point in the history
merklefruit committed Jul 15, 2024
1 parent db7d643 commit 3386bf3
Showing 3 changed files with 107 additions and 26 deletions.
7 changes: 2 additions & 5 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
@@ -78,11 +78,8 @@ async fn main() -> eyre::Result<()> {
}
};

let sender = match execution_state
.check_commitment_validity(&request)
.await
{
Ok(sender) => { sender },
let sender = match execution_state.check_commitment_validity(&request).await {
Ok(sender) => sender,
Err(e) => {
tracing::error!("Failed to commit request: {:?}", e);
let _ = response_tx.send(Err(ApiError::Custom(e.to_string())));
81 changes: 62 additions & 19 deletions bolt-sidecar/src/state/execution.rs
Original file line number Diff line number Diff line change
@@ -19,6 +19,9 @@ pub enum ValidationError {
/// The transaction fee is too low to cover the maximum base fee.
#[error("Transaction fee is too low, need {0} gwei to cover the maximum base fee")]
BaseFeeTooLow(u128),
/// The base fee calculation incurred an error.
#[error("Invalid base fee calculation")]
InvalidBaseFeeCalc,
/// The transaction nonce is too low.
#[error("Transaction nonce too low")]
NonceTooLow,
@@ -66,24 +69,20 @@ impl ValidationError {
pub struct ExecutionState<C> {
/// The latest block number.
block_number: u64,

/// The latest slot number.
slot: u64,

/// The base fee at the head block.
basefee: u128,
/// The cached account states. This should never be read directly.
/// These only contain the canonical account states at the head block,
/// not the intermediate states.
account_states: HashMap<Address, AccountState>,

/// The block templates by target SLOT NUMBER.
/// We have multiple block templates because in rare cases we might have multiple
/// proposal duties for a single lookahead.
block_templates: HashMap<Slot, BlockTemplate>,

/// The maximum number of commitments allowed per slot.
max_commitments_per_slot: NonZero<usize>,

/// The state fetcher client.
client: C,
}
@@ -117,6 +116,7 @@ impl<C: StateFetcher> ExecutionState<C> {
}

/// Validates the commitment request against state (historical + intermediate).
///
/// NOTE: This function only simulates against execution state, it does not consider
/// timing or proposer slot targets.
///
@@ -139,22 +139,23 @@ impl<C: StateFetcher> ExecutionState<C> {
}
}

let sender = req.tx.recover_signer().ok_or(ValidationError::Internal(
"Failed to recover signer from transaction".to_string(),
))?;
let sender = req
.tx
.recover_signer()
.ok_or(ValidationError::RecoverSigner)?;

tracing::debug!(%sender, target_slot = req.slot, "Trying to commit inclusion request to block template");

// Check if the max_fee_per_gas would cover the maximum possible basefee.
let slot_diff = req.slot - self.slot;
let slot_diff = req.slot.saturating_sub(self.slot);

// Calculate the max possible basefee given the slot diff
let max_basefee = calculate_max_basefee(self.basefee, slot_diff)
.ok_or(reject_internal("Overflow calculating max basefee"))?;
.ok_or(ValidationError::InvalidBaseFeeCalc)?;

// Validate the base fee
if !req.validate_basefee(max_basefee) {
return Err(ValidationError::BaseFeeTooLow(max_basefee as u128));
return Err(ValidationError::BaseFeeTooLow(max_basefee));
}

// If we have the account state, use it here
@@ -165,11 +166,13 @@ impl<C: StateFetcher> ExecutionState<C> {
} else {
tracing::debug!(address = %sender, "Unknown account state");
// If we don't have the account state, we need to fetch it
let account_state = self
.client
.get_account_state(&sender, None)
.await
.map_err(|e| reject_internal(&e.to_string()))?;
let account_state =
self.client
.get_account_state(&sender, None)
.await
.map_err(|e| {
ValidationError::Internal(format!("Failed to fetch account state: {:?}", e))
})?;

tracing::debug!(address = %sender, "Fetched account state: {account_state:?}");

@@ -217,7 +220,6 @@ impl<C: StateFetcher> ExecutionState<C> {
) -> Result<(), TransportError> {
self.slot = slot;

// TODO: invalidate any state that we don't need anymore (will be based on block template)
let accounts = self.account_states.keys().collect::<Vec<_>>();
let update = self.client.get_state_update(accounts, block_number).await?;

@@ -293,6 +295,47 @@ pub struct StateUpdate {
pub block_number: u64,
}

fn reject_internal(reason: &str) -> ValidationError {
ValidationError::Internal(reason.to_string())
#[cfg(test)]
mod tests {
use std::num::NonZero;

use reqwest::Url;

use crate::{
state::{fetcher::StateFetcher, ExecutionState, StateClient, ValidationError},
test_util::{create_signed_commitment_request, default_test_transaction, launch_anvil},
};

#[tokio::test]
async fn test_check_commitment_validity() -> eyre::Result<()> {
let anvil = launch_anvil();
let client = StateClient::new(Url::parse(&anvil.endpoint())?);

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).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 mut tx = default_test_transaction(*sender, None);

// Base fee too low
tx.gas_price = Some(0);
let req = create_signed_commitment_request(tx.clone(), sender_pk, slot + 1).await?;
let res = state.check_commitment_validity(&req).await;
assert!(matches!(res, Err(ValidationError::BaseFeeTooLow(_))));

// Nonce too high
tx.gas_price = Some(1_000_000_000);
tx.nonce = Some(100);
let req = create_signed_commitment_request(tx, sender_pk, slot + 1).await?;
let res = state.check_commitment_validity(&req).await;
assert!(matches!(res, Err(ValidationError::NonceTooHigh)));

Ok(())
}
}
45 changes: 43 additions & 2 deletions bolt-sidecar/src/test_util.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
use alloy_network::TransactionBuilder;
use alloy_eips::eip2718::Encodable2718;
use alloy_network::{EthereumWallet, TransactionBuilder};
use alloy_node_bindings::{Anvil, AnvilInstance};
use alloy_primitives::{Address, U256};
use alloy_primitives::{keccak256, Address, B256, U256};
use alloy_rpc_types::TransactionRequest;
use alloy_signer::{
k256::{ecdsa::SigningKey as K256SigningKey, SecretKey as K256SecretKey},
Signer,
};
use alloy_signer_local::PrivateKeySigner;
use blst::min_pk::SecretKey;
use reth_primitives::TransactionSigned;
use secp256k1::Message;

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

@@ -130,3 +138,36 @@ impl SignableECDSA for TestSignableData {
Message::from_digest_slice(as_32.as_slice()).expect("valid message")
}
}

/// 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,
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_signed_reth = TransactionSigned::decode_enveloped(&mut raw_encoded.as_slice())?;

let tx_hash = tx_signed_reth.hash();

let message_digest = {
let mut data = Vec::new();
data.extend_from_slice(&slot.to_le_bytes());
data.extend_from_slice(tx_hash.as_slice());
B256::from(keccak256(data))
};

let signature = signer.sign_hash(&message_digest).await?;

Ok(CommitmentRequest::Inclusion(InclusionRequest {
tx: tx_signed_reth,
slot,
signature,
}))
}

0 comments on commit 3386bf3

Please sign in to comment.