-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: send preconfirmation requests to the rpc
- Loading branch information
1 parent
b2ad8fe
commit cc8db38
Showing
9 changed files
with
1,219 additions
and
24 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,142 @@ | ||
use eyre::Result; | ||
use alloy::{ | ||
eips::eip2718::Encodable2718, | ||
network::{EthereumWallet, TransactionBuilder}, | ||
primitives::{keccak256, B256, U256}, | ||
providers::{ProviderBuilder, SendableTx}, | ||
rpc::types::TransactionRequest, | ||
signers::{local::PrivateKeySigner, Signer}, | ||
}; | ||
use eyre::{bail, Context, Result}; | ||
use rand::Rng; | ||
use reqwest::Url; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::Value; | ||
use tracing::info; | ||
|
||
use crate::cli::SendCommand; | ||
|
||
/// Path to the lookahead endpoint on the Bolt RPC server. | ||
const BOLT_LOOKAHEAD_PATH: &str = "proposers/lookahead?activeOnly=true&futureOnly=true"; | ||
|
||
impl SendCommand { | ||
/// Run the `send` command. | ||
pub async fn run(self) -> Result<()> { | ||
let wallet: PrivateKeySigner = self.private_key.parse().wrap_err("invalid private key")?; | ||
let transaction_signer = EthereumWallet::from(wallet.clone()); | ||
|
||
let provider = ProviderBuilder::new() | ||
.with_recommended_fillers() | ||
.wallet(transaction_signer) | ||
.on_http(self.bolt_rpc_url.clone()); | ||
|
||
// Fetch the lookahead info from the Bolt RPC server | ||
let lookahead_url = self.bolt_rpc_url.join(BOLT_LOOKAHEAD_PATH)?; | ||
let lookahead_res = reqwest::get(lookahead_url).await?.json::<Vec<LookaheadSlot>>().await?; | ||
if lookahead_res.is_empty() { | ||
println!("no bolt proposer found in the lookahead, try again later 🥲"); | ||
return Ok(()); | ||
} | ||
|
||
// Extract the next preconfirmer slot from the lookahead info | ||
let next_preconfirmer_slot = lookahead_res[0].slot; | ||
info!("Next preconfirmer slot: {}", next_preconfirmer_slot); | ||
|
||
// generate a simple self-transfer of ETH | ||
let random_data = rand::thread_rng().gen::<[u8; 32]>(); | ||
let req = TransactionRequest::default() | ||
.with_to(wallet.address()) | ||
.with_value(U256::from(100_000)) | ||
.with_input(random_data); | ||
|
||
let raw_tx = match provider.fill(req).await? { | ||
SendableTx::Builder(_) => bail!("expected a raw transaction"), | ||
SendableTx::Envelope(raw) => raw.encoded_2718(), | ||
}; | ||
let tx_hash = B256::from(keccak256(&raw_tx)); | ||
|
||
send_rpc_request( | ||
vec![hex::encode(&raw_tx)], | ||
vec![tx_hash], | ||
next_preconfirmer_slot, | ||
self.bolt_rpc_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", | ||
serde_json::json!({ | ||
"slot": target_slot, | ||
"txs": txs_rlp, | ||
}), | ||
); | ||
|
||
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) | ||
.header("content-type", "application/json") | ||
.header("x-bolt-signature", signature) | ||
.body(serde_json::to_string(&request)?) | ||
.send() | ||
.await?; | ||
|
||
let response = response.text().await?; | ||
|
||
// 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(()) | ||
} | ||
|
||
async fn sign_request( | ||
tx_hashes: Vec<B256>, | ||
target_slot: u64, | ||
wallet: &PrivateKeySigner, | ||
) -> eyre::Result<String> { | ||
let digest = { | ||
let mut data = Vec::new(); | ||
let hashes = tx_hashes.iter().map(|hash| hash.as_slice()).collect::<Vec<_>>().concat(); | ||
data.extend_from_slice(&hashes); | ||
data.extend_from_slice(target_slot.to_le_bytes().as_slice()); | ||
keccak256(data) | ||
}; | ||
|
||
let signature = hex::encode(wallet.sign_hash(&digest).await?.as_bytes()); | ||
|
||
Ok(format!("{}:0x{}", wallet.address(), signature)) | ||
} | ||
|
||
fn prepare_rpc_request(method: &str, params: Value) -> Value { | ||
serde_json::json!({ | ||
"id": "1", | ||
"jsonrpc": "2.0", | ||
"method": method, | ||
"params": vec![params], | ||
}) | ||
} | ||
|
||
/// Info about a specific slot in the beacon chain lookahead. | ||
#[derive(Debug, Clone, Serialize, Deserialize)] | ||
pub struct LookaheadSlot { | ||
/// Slot number in the beacon chain | ||
pub slot: u64, | ||
/// Validator index that will propose in this slot | ||
pub validator_index: u64, | ||
/// Validator pubkey that will propose in this slot | ||
pub validator_pubkey: String, | ||
/// Optional URL of the Bolt sidecar associated with the proposer | ||
pub sidecar_url: Option<String>, | ||
} |
Oops, something went wrong.