Skip to content

Commit

Permalink
Merge pull request #168 from chainbound/fix/bolt-client
Browse files Browse the repository at this point in the history
chore: add client signature test
  • Loading branch information
merklefruit authored Jul 29, 2024
2 parents 0d1f0be + db4430a commit 1cf27d6
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 33 deletions.
12 changes: 6 additions & 6 deletions bolt-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ This is a simple CLI tool to interact with Bolt.

1. Prepare the environment variables (either in a `.env` file, or as CLI arguments):

- `RPC_URL` or `--rpc-url`: the URL of the Bolt RPC server (default: Chainbound's RPC)
- `PRIVATE_KEY` or `--private-key`: the private key of the account to send transactions from
- `NONCE_OFFSET` or `--nonce-offset`: the offset to add to the account's nonce (default: 0)
- `BLOB` or `--blob`: bool flag to send a blob-carrying transaction (default: false)
- `BOLT_RPC_URL` or `--rpc-url`: the URL of the Bolt RPC server (default: Chainbound's RPC)
- `BOLT_PRIVATE_KEY` or `--private-key`: the private key of the account to send transactions from
- `BOLT_NONCE_OFFSET` or `--nonce-offset`: the offset to add to the account's nonce (default: 0)
- `--blob`: bool flag to send a blob-carrying transaction (default: false)

**Optionally**, you can use the following flags to fetch the lookahead data from the beacon chain directly
instead of relying on the RPC server:

- `--use-registry`: bool flag to fetch data from a local node instead of the RPC_URL (default: false)
- `--registry-address`: the address of the bolt-registry contract
- `--beacon-client-url`: the URL of the CL node to use
- `BOLT_REGISTRY_ADDRESS` or `--registry-address`: the address of the bolt-registry contract
- `BOLT_BEACON_CLIENT_URL` or `--beacon-client-url`: the URL of the CL node to use

1. Run the CLI tool with the desired command and arguments, if any.

Expand Down
86 changes: 64 additions & 22 deletions bolt-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use alloy::{
eips::eip2718::Encodable2718,
hex,
network::{EthereumWallet, TransactionBuilder},
primitives::Address,
primitives::{Address, B256},
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
};
use beacon_api_client::mainnet::Client as BeaconApiClient;
use clap::Parser;
use eyre::{bail, Result};
use serde_json::{json, Value};
use tracing::info;
use url::Url;

Expand All @@ -24,7 +25,7 @@ struct Opts {
#[clap(
short = 'p',
long,
default_value = "http://135.181.191.125:8015/",
default_value = "http://135.181.191.125:8015/rpc",
env = "BOLT_RPC_URL"
)]
rpc_url: Url,
Expand All @@ -35,11 +36,14 @@ struct Opts {
#[clap(short, long, default_value_t = 0, env = "BOLT_NONCE_OFFSET")]
nonce_offset: u64,
/// Flag for generating a blob tx instead of a regular tx
#[clap(short = 'B', long, default_value_t = false)]
#[clap(long, default_value_t = false)]
blob: bool,
/// Number of transactions to send in a sequence
#[clap(short, long, default_value_t = 1)]
count: u64,
/// Flag for sending all "count" transactions in a single bundle
#[clap(long, default_value_t = false)]
bundle: bool,

/// Flag for using the registry to fetch the lookahead
#[clap(short, long, default_value_t = false, requires_ifs([("true", "registry_address"), ("true", "beacon_client_url")]))]
Expand All @@ -65,7 +69,7 @@ async fn main() -> Result<()> {
info!("starting bolt-client");

let _ = dotenvy::dotenv();
let opts = Opts::parse();
let mut opts = Opts::parse();

let wallet: PrivateKeySigner = opts.private_key.parse().expect("invalid private key");
let transaction_signer: EthereumWallet = wallet.clone().into();
Expand All @@ -80,38 +84,77 @@ async fn main() -> Result<()> {
let duties = get_proposer_duties(&beacon_api_client, curr_slot, curr_slot / 32).await?;
match registry.next_preconfer_from_registry(duties).await {
Ok(Some((endpoint, slot))) => (Url::parse(&endpoint)?, slot),
Ok(None) => bail!("no next preconfer slot found"),
Ok(None) => bail!("no next preconfer slot found, try again later"),
Err(e) => bail!("error fetching next preconfer slot from registry: {:?}", e),
}
} else {
// TODO: remove "cbOnly=true"
let url =
opts.rpc_url.join("proposers/lookahead?onlyActive=true&onlyFuture=true&cbOnly=true")?;
let lookahead_response = reqwest::get(url).await?.json::<serde_json::Value>().await?;
opts.rpc_url.join("proposers/lookahead?activeOnly=true&futureOnly=true&cbOnly=true")?;
let lookahead_response = reqwest::get(url).await?.json::<Value>().await?;
if lookahead_response.as_array().unwrap_or(&vec![]).is_empty() {
bail!("no bolt proposer found in lookahead, try again later");
}
let next_preconfer_slot = lookahead_response[0].get("slot").unwrap().as_u64().unwrap();
(opts.rpc_url.join("/rpc")?, next_preconfer_slot)
(opts.rpc_url, next_preconfer_slot)
};

let mut tx = if opts.blob { generate_random_blob_tx() } else { generate_random_tx() };
tx.set_from(sender);
tx.set_chain_id(provider.get_chain_id().await?);
tx.set_nonce(provider.get_transaction_count(sender).await? + opts.nonce_offset);
let mut txs_rlp = Vec::with_capacity(opts.count as usize);
let mut tx_hashes = Vec::with_capacity(opts.count as usize);
for _ in 0..opts.count {
let mut tx = if opts.blob { generate_random_blob_tx() } else { generate_random_tx() };
tx.set_from(sender);
tx.set_chain_id(provider.get_chain_id().await?);
tx.set_nonce(provider.get_transaction_count(sender).await? + opts.nonce_offset);

// Set the nonce offset for the next transaction
opts.nonce_offset += 1;

let tx_signed = tx.build(&transaction_signer).await?;
let tx_hash = tx_signed.tx_hash();
let tx_rlp = hex::encode(tx_signed.encoded_2718());

if opts.bundle {
// store transactions in a bundle to send them all at once
txs_rlp.push(tx_rlp);
tx_hashes.push(*tx_hash);
} else {
// Send rpc requests singularly for each transaction
send_rpc_request(
vec![tx_rlp.clone()],
vec![*tx_hash],
target_slot,
target_sidecar_url.clone(),
&wallet,
)
.await?;
}
}

let tx_signed = tx.build(&transaction_signer).await?;
let tx_hash = tx_signed.tx_hash();
let tx_rlp = hex::encode(tx_signed.encoded_2718());
if opts.bundle {
send_rpc_request(txs_rlp, tx_hashes, target_slot, target_sidecar_url, &wallet).await?;
}

Ok(())
}

async fn send_rpc_request(
txs_rlp: Vec<String>,
tx_hashes: Vec<B256>,
target_slot: u64,
target_sidecar_url: Url,
wallet: &PrivateKeySigner,
) -> Result<()> {
let request = prepare_rpc_request(
"bolt_requestInclusion",
vec![serde_json::json!({
json!({
"slot": target_slot,
"txs": vec![tx_rlp],
})],
"txs": txs_rlp,
}),
);

info!("Transaction hash: {}", tx_hash);

let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;
info!(?tx_hashes, target_slot, %target_sidecar_url);
let signature = sign_request(tx_hashes, target_slot, wallet).await?;

let response = reqwest::Client::new()
.post(target_sidecar_url)
Expand All @@ -126,6 +169,5 @@ async fn main() -> Result<()> {
// strip out long series of zeros in the response (to avoid spamming blob contents)
let response = response.replace(&"0".repeat(32), ".").replace(&".".repeat(4), "");
info!("Response: {:?}", response);

Ok(())
}
49 changes: 44 additions & 5 deletions bolt-client/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ pub fn generate_random_blob_tx() -> TransactionRequest {
.with_blob_sidecar(sidecar)
}

pub fn prepare_rpc_request(method: &str, params: Vec<Value>) -> Value {
pub fn prepare_rpc_request(method: &str, params: Value) -> Value {
serde_json::json!({
"id": "1",
"jsonrpc": "2.0",
"method": method,
"params": params,
"params": vec![params],
})
}

Expand All @@ -68,7 +68,7 @@ pub async fn get_proposer_duties(
}

pub async fn sign_request(
tx_hashes: Vec<&B256>,
tx_hashes: Vec<B256>,
target_slot: u64,
wallet: &PrivateKeySigner,
) -> eyre::Result<String> {
Expand All @@ -89,7 +89,12 @@ pub async fn sign_request(
mod tests {
use std::str::FromStr;

use alloy::{primitives::B256, signers::local::PrivateKeySigner};
use alloy::{
primitives::{keccak256, Signature, B256},
signers::local::PrivateKeySigner,
};

use crate::sign_request;

#[tokio::test]
async fn test_sign_request() -> eyre::Result<()> {
Expand All @@ -98,12 +103,46 @@ mod tests {
B256::from_str("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")?;
let target_slot = 42;

let signature = super::sign_request(vec![&tx_hash], target_slot, &wallet).await?;
let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;
let parts: Vec<&str> = signature.split(':').collect();

assert_eq!(parts.len(), 2);
assert_eq!(parts[0], wallet.address().to_string());
assert_eq!(parts[1].len(), 130);
Ok(())
}

#[tokio::test]
async fn test_verify_signature() -> eyre::Result<()> {
// Randomly generated private key
let private_key = "0xfa4c3c87627a58684fb519f7b01a31ef31e56f414e8aa56a15f574381a5a7a9c";
let tx_hash = "0x6938dbd0649ce26af79b0cca677b493257bd87c17d25ff717feba33c8b3920b3";
let expected_signature = "0x10386a2aF29854954645C9710A038AcF4B2F1752:0x8db9bbcc1db5257c80138bd1df0185305918dbc8a607f63458ea885a6ccce5177a73417d693953b9f5c017a927e9c8acbf24c05b09a55f1f3fa83db57931ed9e1c";
let target_slot = 254464;

let wallet = PrivateKeySigner::from_str(private_key)?;
let tx_hash = B256::from_str(tx_hash)?;

let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;

assert_eq!(signature, expected_signature);

let expected_signer = expected_signature.split(':').next().unwrap();
let expected_sig = expected_signature.split(':').last().unwrap();
let sig = Signature::from_str(expected_sig)?;

// recompute the prehash again
let digest = {
let mut data = Vec::new();
data.extend_from_slice(tx_hash.as_slice());
data.extend_from_slice(target_slot.to_le_bytes().as_slice());
keccak256(data)
};

let recovered_address = sig.recover_address_from_prehash(&digest)?;
assert_eq!(recovered_address, wallet.address());
assert_eq!(recovered_address.to_string(), expected_signer);

Ok(())
}
}

0 comments on commit 1cf27d6

Please sign in to comment.