Skip to content

Commit

Permalink
Merge pull request #64 from chainbound/feat/merge-rpc-types
Browse files Browse the repository at this point in the history
Feat: merge RPC commitment types to avoid duplicates
  • Loading branch information
merklefruit authored Jun 5, 2024
2 parents 2f5b01c + 8764b86 commit 57e8f94
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 158 deletions.
2 changes: 1 addition & 1 deletion bolt-sidecar/src/client/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use alloy_transport::TransportResult;
use alloy_transport_http::Http;
use reqwest::{Client, Url};

use crate::types::AccountState;
use crate::primitives::AccountState;

/// An HTTP-based JSON-RPC client that supports batching. Implements all methods that are relevant
/// to Bolt state.
Expand Down
9 changes: 5 additions & 4 deletions bolt-sidecar/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use alloy_consensus::TxEnvelope;
use alloy_primitives::U256;

use crate::{
primitives::{AccountState, TxInfo},
state::ValidationError,
types::{transaction::TxInfo, AccountState},
};

/// Calculates the max_basefee `slot_diff` blocks in the future given a current basefee (in gwei).
Expand All @@ -28,7 +29,7 @@ pub fn calculate_max_basefee(current: u128, block_diff: u64) -> Option<u128> {
}

/// Calculates the max transaction cost (gas + value) in wei.
pub fn max_transaction_cost<T: TxInfo>(transaction: &T) -> U256 {
pub fn max_transaction_cost(transaction: &TxEnvelope) -> U256 {
let gas_limit = transaction.gas_limit();

let fee_cap = transaction
Expand All @@ -44,9 +45,9 @@ pub fn max_transaction_cost<T: TxInfo>(transaction: &T) -> U256 {
/// This function validates a transaction against an account state. It checks 2 things:
/// 1. The nonce of the transaction must be higher than the account's nonce, but not higher than current + 1.
/// 2. The balance of the account must be higher than the transaction's max cost.
pub fn validate_transaction<T: TxInfo>(
pub fn validate_transaction(
account_state: &AccountState,
transaction: &T,
transaction: &TxEnvelope,
) -> Result<(), ValidationError> {
// Check if the nonce is correct (should be the same as the transaction count)
if transaction.nonce() < account_state.transaction_count {
Expand Down
39 changes: 19 additions & 20 deletions bolt-sidecar/src/json_rpc/api.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
use std::{num::NonZeroUsize, str::FromStr, sync::Arc};
use std::{num::NonZeroUsize, sync::Arc};

use alloy_consensus::TxEnvelope;
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::Signature;
use parking_lot::RwLock;
use secp256k1::SecretKey;
use serde_json::Value;
use thiserror::Error;
use tracing::info;

use super::{mevboost::MevBoostClient, types::InclusionRequestParams};
use super::mevboost::MevBoostClient;
use crate::{
crypto::{SignableECDSA, Signer},
crypto::Signer,
json_rpc::types::{BatchedSignedConstraints, ConstraintsMessage, SignedConstraints},
types::Slot,
primitives::{CommitmentRequest, Slot},
};

/// Default size of the api request cache (implemented as a LRU).
Expand Down Expand Up @@ -57,7 +54,7 @@ pub trait CommitmentsRpc {
/// sidecar's validator identity.
pub struct JsonRpcApi {
/// A cache of commitment requests.
cache: Arc<RwLock<lru::LruCache<Slot, Vec<InclusionRequestParams>>>>,
cache: Arc<RwLock<lru::LruCache<Slot, Vec<CommitmentRequest>>>>,
/// The signer for this sidecar.
signer: Signer,
/// The client for the MEV-Boost sidecar.
Expand Down Expand Up @@ -86,17 +83,16 @@ impl CommitmentsRpc for JsonRpcApi {
));
};

let params = serde_json::from_value::<InclusionRequestParams>(params)?;
let params = serde_json::from_value::<CommitmentRequest>(params)?;
let params = params
.as_inclusion_request()
.ok_or_else(|| ApiError::Custom("request must be an inclusion request".to_string()))?;
info!(?params, "received inclusion commitment request");

// parse the raw transaction bytes
let hex_decoded_tx = hex::decode(params.message.tx.trim_start_matches("0x"))?;
let transaction = TxEnvelope::decode_2718(&mut hex_decoded_tx.as_slice())?;
let tx_sender = transaction.recover_signer()?;
let tx_sender = params.tx.recover_signer()?;

// validate the user's signature
let user_sig = Signature::from_str(params.signature.trim_start_matches("0x"))?;
let signer_address = user_sig.recover_address_from_msg(params.message.digest().as_ref())?;
let signer_address = params.signature.recover_address_from_msg(params.digest())?;

// TODO: relax this check to allow for external signers to request commitments
// about transactions that they did not sign themselves
Expand All @@ -109,20 +105,23 @@ impl CommitmentsRpc for JsonRpcApi {
{
// check for duplicate requests and update the cache if necessary
let mut cache = self.cache.write();
if let Some(commitments) = cache.get_mut(&params.message.slot) {
if commitments.iter().any(|p| p == &params) {
if let Some(commitments) = cache.get_mut(&params.slot) {
if commitments
.iter()
.any(|p| p.as_inclusion_request().is_some_and(|i| i == params))
{
return Err(ApiError::DuplicateRequest);
}

commitments.push(params.clone());
commitments.push(params.clone().into());
} else {
cache.put(params.message.slot, vec![params.clone()]);
cache.put(params.slot, vec![params.clone().into()]);
}
} // Drop the lock

// parse the request into constraints and sign them with the sidecar signer
// TODO: get the validator index from somewhere
let constraints = ConstraintsMessage::build(0, params.message.slot, params.clone());
let constraints = ConstraintsMessage::build(0, params.slot, params.clone());
let constraints_sig = self.signer.sign_ecdsa(&constraints).to_string();
let signed_constraints: BatchedSignedConstraints = vec![SignedConstraints {
message: constraints,
Expand Down
50 changes: 10 additions & 40 deletions bolt-sidecar/src/json_rpc/types.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
use alloy_consensus::TxEnvelope;
use alloy_primitives::keccak256;
use secp256k1::Message;
use serde::{Deserialize, Serialize};

use crate::{crypto::SignableECDSA, types::Slot};

/// The API parameters to request an inclusion commitment for a given slot.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InclusionRequestParams {
#[serde(flatten)]
pub message: InclusionRequestMessage,
pub signature: String,
}

/// The message to request an inclusion commitment for a given slot.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InclusionRequestMessage {
pub slot: Slot,
pub tx: String,
pub index: Option<u64>,
}

/// What users have to sign to request an inclusion commitment.
/// We use the [SignableECDSA] trait to abstract over the signature verification step.
impl SignableECDSA for InclusionRequestMessage {
fn digest(&self) -> Message {
let mut data = Vec::new();
data.extend_from_slice(&self.slot.to_le_bytes());
data.extend_from_slice(self.tx.as_bytes());
data.extend_from_slice(&self.index.unwrap_or(0).to_le_bytes());

let hash = keccak256(data).0;
Message::from_digest_slice(&hash).expect("digest")
}
}
use crate::{crypto::SignableECDSA, primitives::InclusionRequest};

/// The inclusion request transformed into an explicit list of signed constraints
/// that need to be forwarded to the PBS pipeline to inform block production.
Expand All @@ -54,7 +23,7 @@ pub struct ConstraintsMessage {
}

impl ConstraintsMessage {
pub fn build(validator_index: u64, slot: u64, request: InclusionRequestParams) -> Self {
pub fn build(validator_index: u64, slot: u64, request: InclusionRequest) -> Self {
let constraints = vec![Constraint::from(request)];
Self {
validator_index,
Expand All @@ -66,24 +35,25 @@ impl ConstraintsMessage {

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Constraint {
pub tx: String,
pub tx: TxEnvelope,
pub index: Option<u64>,
}

impl Constraint {
// TODO: actually use SSZ encoding here
pub fn as_bytes(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(self.tx.as_bytes());
data.extend_from_slice(self.tx.tx_hash().as_slice());
data.extend_from_slice(&self.index.unwrap_or(0).to_le_bytes());
data
}
}

impl From<InclusionRequestParams> for Constraint {
fn from(params: InclusionRequestParams) -> Self {
impl From<InclusionRequest> for Constraint {
fn from(params: InclusionRequest) -> Self {
Self {
tx: params.message.tx,
index: params.message.index,
tx: params.tx,
index: None,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion bolt-sidecar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
mod client;
mod common;
mod crypto;
mod primitives;
mod state;
mod template;
mod types;

/// Configuration and command-line argument parsing for the sidecar
pub mod config;
Expand Down
132 changes: 132 additions & 0 deletions bolt-sidecar/src/primitives/commitment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::str::FromStr;

use alloy_consensus::TxEnvelope;
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::Signature;
use serde::{de, Deserialize, Deserializer, Serialize};

use super::transaction::TxInfo;

/// Commitment requests sent by users or RPC proxies to the sidecar.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum CommitmentRequest {
/// Request of inclusion of a transaction at a specific slot.
Inclusion(InclusionRequest),
}

impl CommitmentRequest {
pub fn as_inclusion_request(&self) -> Option<&InclusionRequest> {
#[allow(irrefutable_let_patterns)] // TODO: remove when we add more variants
if let CommitmentRequest::Inclusion(req) = self {
Some(req)
} else {
None
}
}
}

/// Request to include a transaction at a specific slot.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InclusionRequest {
/// The consensus slot number at which the transaction should be included.
pub slot: u64,
/// The transaction to be included.
#[serde(deserialize_with = "deserialize_tx_envelope")]
pub tx: TxEnvelope,
/// The signature over the "slot" and "tx" fields by the user.
/// A valid signature is the only proof that the user actually requested
/// this specific commitment to be included at the given slot.
#[serde(deserialize_with = "deserialize_from_str")]
pub signature: Signature,
}

impl InclusionRequest {
/// Validates the transaction fee against a minimum basefee.
/// Returns true if the fee is greater than or equal to the min, false otherwise.
pub fn validate_basefee(&self, min: u128) -> bool {
if let Some(max_fee) = self.tx.max_fee_per_gas() {
if max_fee < min {
return false;
}
} else if let Some(fee) = self.tx.gas_price() {
if fee < min {
return false;
}
} else {
unreachable!("Transaction must have a fee");
}

true
}
}

fn deserialize_tx_envelope<'de, D>(deserializer: D) -> Result<TxEnvelope, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let s = hex::decode(s.trim_start_matches("0x")).map_err(de::Error::custom)?;
TxEnvelope::decode_2718(&mut s.as_slice()).map_err(de::Error::custom)
}

fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: std::fmt::Display,
{
let s = String::deserialize(deserializer)?;
T::from_str(&s).map_err(de::Error::custom)
}

impl InclusionRequest {
// TODO: actually use SSZ encoding here
pub fn digest(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&self.slot.to_le_bytes());
data.extend_from_slice(self.tx.tx_hash().as_slice());
data
}
}

impl From<InclusionRequest> for CommitmentRequest {
fn from(req: InclusionRequest) -> Self {
CommitmentRequest::Inclusion(req)
}
}

#[cfg(test)]
mod tests {
use super::{CommitmentRequest, InclusionRequest};

#[test]
fn test_deserialize_inclusion_request() {
let json_req = r#"{
"tx": "0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"signature": "0xb8623aae262785bd31d0cc6e368a9b9ab5361002edd58ece424ef5dde0544b32472d954da3f34ca9c2c2201393f9b83cdc959bd416c0af96fe3e0962a08cb92101",
"slot": 1
}"#;

let req: InclusionRequest = serde_json::from_str(json_req).unwrap();
assert_eq!(req.slot, 1);
}

#[test]
fn test_deserialize_commitment_request() {
let json_req = r#"{
"tx": "0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"signature": "0xb8623aae262785bd31d0cc6e368a9b9ab5361002edd58ece424ef5dde0544b32472d954da3f34ca9c2c2201393f9b83cdc959bd416c0af96fe3e0962a08cb92101",
"slot": 1
}"#;

let req: CommitmentRequest = serde_json::from_str(json_req).unwrap();

#[allow(irrefutable_let_patterns)]
if let CommitmentRequest::Inclusion(req) = req {
assert_eq!(req.slot, 1);
} else {
panic!("Expected Inclusion request");
}
}
}
35 changes: 35 additions & 0 deletions bolt-sidecar/src/primitives/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use alloy_primitives::{Bytes, TxHash, U256};

pub mod commitment;
pub use commitment::{CommitmentRequest, InclusionRequest};

pub mod transaction;
pub use transaction::TxInfo;

/// An alias for a Beacon Chain slot number
pub type Slot = u64;

/// An enum representing all possible (signed) commitments.
#[derive(Debug)]
pub enum Commitment {
/// Inclusion commitment, accepted and signed by the proposer
/// through this sidecar's signer.
Inclusion(InclusionCommitment),
}

#[derive(Debug)]
pub struct InclusionCommitment {
pub slot: Slot,
pub tx_hash: TxHash,
pub raw_tx: Bytes,
// TODO:
pub signature: Bytes,
}

/// Minimal account state needed for commitment validation.
#[derive(Debug, Clone, Copy)]
pub struct AccountState {
/// The nonce of the account. This is the number of transactions sent from this account
pub transaction_count: u64,
pub balance: U256,
}
File renamed without changes.
Loading

0 comments on commit 57e8f94

Please sign in to comment.