From 8c5c484d628a359728ce9f90cf47b7d611cf697b Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Thu, 7 Mar 2024 16:19:33 +0100 Subject: [PATCH] feat: `Ethereum` network --- crates/network/Cargo.toml | 8 +- crates/network/src/ethereum/mod.rs | 108 +++++++++++++++++ crates/network/src/ethereum/receipt.rs | 43 +++++++ crates/network/src/ethereum/signer.rs | 141 +++++++++++++++++++++++ crates/network/src/lib.rs | 14 ++- crates/network/src/transaction/common.rs | 91 --------------- 6 files changed, 308 insertions(+), 97 deletions(-) create mode 100644 crates/network/src/ethereum/mod.rs create mode 100644 crates/network/src/ethereum/receipt.rs create mode 100644 crates/network/src/ethereum/signer.rs delete mode 100644 crates/network/src/transaction/common.rs diff --git a/crates/network/Cargo.toml b/crates/network/Cargo.toml index 51ecf79cbc8..d77e48cabfb 100644 --- a/crates/network/Cargo.toml +++ b/crates/network/Cargo.toml @@ -12,11 +12,15 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true alloy-eips = { workspace = true, features = ["serde"] } alloy-json-rpc.workspace = true alloy-primitives.workspace = true -alloy-rlp.workspace = true +alloy-rpc-types.workspace = true +alloy-signer.workspace = true +async-trait.workspace = true serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true [features] -k256 = ["alloy-primitives/k256"] +k256 = ["alloy-primitives/k256", "alloy-consensus/k256"] diff --git a/crates/network/src/ethereum/mod.rs b/crates/network/src/ethereum/mod.rs new file mode 100644 index 00000000000..8899488b161 --- /dev/null +++ b/crates/network/src/ethereum/mod.rs @@ -0,0 +1,108 @@ +use crate::{BuilderResult, Network, NetworkSigner, TransactionBuilder}; + +mod receipt; +mod signer; +use alloy_primitives::{Address, TxKind, U256, U64}; +pub use signer::EthereumSigner; + +/// Types for a mainnet-like Ethereum network. +#[derive(Debug, Clone, Copy)] +pub struct Ethereum; + +impl Network for Ethereum { + type TxEnvelope = alloy_consensus::TxEnvelope; + + type UnsignedTx = alloy_consensus::TypedTransaction; + + type ReceiptEnvelope = alloy_consensus::ReceiptEnvelope; + + type Header = alloy_consensus::Header; + + type TransactionRequest = alloy_rpc_types::transaction::TransactionRequest; + + type TransactionResponse = alloy_rpc_types::Transaction; + + type ReceiptResponse = alloy_rpc_types::TransactionReceipt; + + type HeaderResponse = alloy_rpc_types::Header; +} + +impl TransactionBuilder for alloy_rpc_types::TransactionRequest { + fn chain_id(&self) -> Option { + self.chain_id + } + + fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { + self.chain_id = Some(chain_id); + } + + fn nonce(&self) -> Option { + self.nonce + } + + fn set_nonce(&mut self, nonce: U64) { + self.nonce = Some(nonce); + } + + fn input(&self) -> Option<&alloy_primitives::Bytes> { + self.input.input() + } + + fn set_input(&mut self, input: alloy_primitives::Bytes) { + self.input.input = Some(input); + } + + fn to(&self) -> Option { + self.to.map(TxKind::Call).or(Some(TxKind::Create)) + } + + fn from(&self) -> Option
{ + self.from + } + + fn set_from(&mut self, from: Address) { + self.from = Some(from); + } + + fn set_to(&mut self, to: alloy_primitives::TxKind) { + match to { + TxKind::Create => self.to = None, + TxKind::Call(to) => self.to = Some(to), + } + } + + fn value(&self) -> Option { + self.value + } + + fn set_value(&mut self, value: alloy_primitives::U256) { + self.value = Some(value) + } + + fn gas_price(&self) -> Option { + todo!() + } + + fn set_gas_price(&mut self, gas_price: U256) { + todo!() + } + + fn gas_limit(&self) -> Option { + self.gas + } + + fn set_gas_limit(&mut self, gas_limit: U256) { + self.gas = Some(gas_limit); + } + + fn build_unsigned(self) -> BuilderResult<::UnsignedTx> { + todo!() + } + + fn build>( + self, + signer: &S, + ) -> BuilderResult<::TxEnvelope> { + todo!() + } +} diff --git a/crates/network/src/ethereum/receipt.rs b/crates/network/src/ethereum/receipt.rs new file mode 100644 index 00000000000..6e47ff80da1 --- /dev/null +++ b/crates/network/src/ethereum/receipt.rs @@ -0,0 +1,43 @@ +use crate::Receipt; +use alloy_consensus::ReceiptWithBloom; +use alloy_primitives::{Bloom, Log}; + +impl Receipt for alloy_consensus::Receipt { + fn success(&self) -> bool { + self.success + } + + fn bloom(&self) -> Bloom { + self.bloom_slow() + } + + fn cumulative_gas_used(&self) -> u64 { + self.cumulative_gas_used + } + + fn logs(&self) -> &[Log] { + &self.logs + } +} + +impl Receipt for ReceiptWithBloom { + fn success(&self) -> bool { + self.receipt.success + } + + fn bloom(&self) -> Bloom { + self.bloom + } + + fn bloom_cheap(&self) -> Option { + Some(self.bloom) + } + + fn cumulative_gas_used(&self) -> u64 { + self.receipt.cumulative_gas_used + } + + fn logs(&self) -> &[Log] { + &self.receipt.logs + } +} diff --git a/crates/network/src/ethereum/signer.rs b/crates/network/src/ethereum/signer.rs new file mode 100644 index 00000000000..1b72ac0dc8e --- /dev/null +++ b/crates/network/src/ethereum/signer.rs @@ -0,0 +1,141 @@ +use super::Ethereum; +use crate::{NetworkSigner, TxSigner}; +use alloy_consensus::{SignableTransaction, TxEnvelope, TypedTransaction}; +use alloy_signer::Signature; +use async_trait::async_trait; + +/// A signer capable of signing any transaction for the Ethereum network. +pub struct EthereumSigner(Box + Sync>); + +impl std::fmt::Debug for EthereumSigner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("EthereumSigner").finish() + } +} + +impl From for EthereumSigner +where + S: TxSigner + Sync + 'static, +{ + fn from(signer: S) -> Self { + Self(Box::new(signer)) + } +} + +impl EthereumSigner { + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result { + self.0.sign_transaction(tx).await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NetworkSigner for EthereumSigner { + async fn sign(&self, tx: TypedTransaction) -> alloy_signer::Result { + match tx { + TypedTransaction::Legacy(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip2930(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip1559(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip4844(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + } + } +} + +#[cfg(test)] +mod test { + use alloy_consensus::{SignableTransaction, TxLegacy}; + use alloy_primitives::{address, ChainId, Signature, U256}; + use alloy_signer::{k256, Result, Signer, TxSigner, TxSignerSync}; + + #[tokio::test] + async fn signs_tx() { + async fn sign_tx_test(tx: &mut TxLegacy, chain_id: Option) -> Result { + let mut before = tx.clone(); + let sig = sign_dyn_tx_test(tx, chain_id).await?; + if let Some(chain_id) = chain_id { + assert_eq!(tx.chain_id, Some(chain_id), "chain ID was not set"); + before.chain_id = Some(chain_id); + } + assert_eq!(*tx, before); + Ok(sig) + } + + async fn sign_dyn_tx_test( + tx: &mut dyn SignableTransaction, + chain_id: Option, + ) -> Result { + let mut wallet: alloy_signer::Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + wallet.set_chain_id(chain_id); + + let sig = wallet.sign_transaction_sync(tx)?; + let sighash = tx.signature_hash(); + assert_eq!(sig.recover_address_from_prehash(&sighash).unwrap(), wallet.address()); + + let sig_async = wallet.sign_transaction(tx).await.unwrap(); + assert_eq!(sig_async, sig); + + Ok(sig) + } + + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let mut tx = TxLegacy { + to: alloy_primitives::TxKind::Call(address!( + "F0109fC8DF283027b6285cc889F5aA624EaC1F55" + )), + value: U256::from(1_000_000_000), + gas_limit: 2_000_000, + nonce: 0, + gas_price: 21_000_000_000, + input: Default::default(), + chain_id: None, + }; + let sig_none = sign_tx_test(&mut tx, None).await.unwrap(); + + tx.chain_id = Some(1); + let sig_1 = sign_tx_test(&mut tx, None).await.unwrap(); + let expected = "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa6825".parse().unwrap(); + assert_eq!(sig_1, expected); + assert_ne!(sig_1, sig_none); + + tx.chain_id = Some(2); + let sig_2 = sign_tx_test(&mut tx, None).await.unwrap(); + assert_ne!(sig_2, sig_1); + assert_ne!(sig_2, sig_none); + + // Sets chain ID. + tx.chain_id = None; + let sig_none_none = sign_tx_test(&mut tx, None).await.unwrap(); + assert_eq!(sig_none_none, sig_none); + + tx.chain_id = None; + let sig_none_1 = sign_tx_test(&mut tx, Some(1)).await.unwrap(); + assert_eq!(sig_none_1, sig_1); + + tx.chain_id = None; + let sig_none_2 = sign_tx_test(&mut tx, Some(2)).await.unwrap(); + assert_eq!(sig_none_2, sig_2); + + // Errors on mismatch. + tx.chain_id = Some(2); + let error = sign_tx_test(&mut tx, Some(1)).await.unwrap_err(); + let expected_error = alloy_signer::Error::TransactionChainIdMismatch { signer: 1, tx: 2 }; + assert_eq!(error.to_string(), expected_error.to_string()); + } +} diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs index 53f7c8f040f..a4193e2e3fd 100644 --- a/crates/network/src/lib.rs +++ b/crates/network/src/lib.rs @@ -19,9 +19,6 @@ use alloy_eips::eip2718::Eip2718Envelope; use alloy_json_rpc::RpcObject; use alloy_primitives::B256; -mod sealed; -pub use sealed::{Sealable, Sealed}; - mod transaction; pub use transaction::{ BuilderResult, NetworkSigner, TransactionBuilder, TransactionBuilderError, TxSigner, @@ -33,6 +30,9 @@ pub use receipt::Receipt; pub use alloy_eips::eip2718; +mod ethereum; +pub use ethereum::{Ethereum, EthereumSigner}; + /// A list of transactions, either hydrated or hashes. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] @@ -54,6 +54,8 @@ pub struct BlockResponse { } /// Captures type info for network-specific RPC requests/responses. +// todo: block responses are ethereum only, so we need to include this in here too, or make `Block` +// generic over tx/header type pub trait Network: Sized + Send + Sync + 'static { #[doc(hidden)] /// Asserts that this trait can only be implemented on a ZST. @@ -65,6 +67,10 @@ pub trait Network: Sized + Send + Sync + 'static { /// The network transaction envelope type. type TxEnvelope: Eip2718Envelope; + + /// An enum over the various transaction types. + type UnsignedTx; + /// The network receipt envelope type. type ReceiptEnvelope: Eip2718Envelope; /// The network header type. @@ -73,7 +79,7 @@ pub trait Network: Sized + Send + Sync + 'static { // -- JSON RPC types -- /// The JSON body of a transaction request. - type TransactionRequest: RpcObject + Transaction; // + TransactionBuilder + type TransactionRequest: RpcObject + TransactionBuilder + std::fmt::Debug; /// The JSON body of a transaction response. type TransactionResponse: RpcObject; /// The JSON body of a transaction receipt. diff --git a/crates/network/src/transaction/common.rs b/crates/network/src/transaction/common.rs deleted file mode 100644 index 45d637a655f..00000000000 --- a/crates/network/src/transaction/common.rs +++ /dev/null @@ -1,91 +0,0 @@ -use alloy_primitives::Address; -use alloy_rlp::{Buf, BufMut, Decodable, Encodable, EMPTY_STRING_CODE}; - -/// The `to` field of a transaction. Either a target address, or empty for a -/// contract creation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub enum TxKind { - /// A transaction that creates a contract. - #[default] - Create, - /// A transaction that calls a contract or transfer. - Call(Address), -} - -impl From> for TxKind { - /// Creates a `TxKind::Call` with the `Some` address, `None` otherwise. - #[inline] - fn from(value: Option
) -> Self { - match value { - None => TxKind::Create, - Some(addr) => TxKind::Call(addr), - } - } -} - -impl From
for TxKind { - /// Creates a `TxKind::Call` with the given address. - #[inline] - fn from(value: Address) -> Self { - TxKind::Call(value) - } -} - -impl TxKind { - /// Returns the address of the contract that will be called or will receive the transfer. - pub const fn to(self) -> Option
{ - match self { - TxKind::Create => None, - TxKind::Call(to) => Some(to), - } - } - - /// Returns true if the transaction is a contract creation. - #[inline] - pub const fn is_create(self) -> bool { - matches!(self, TxKind::Create) - } - - /// Returns true if the transaction is a contract call. - #[inline] - pub const fn is_call(self) -> bool { - matches!(self, TxKind::Call(_)) - } - - /// Calculates a heuristic for the in-memory size of this object. - #[inline] - pub const fn size(self) -> usize { - std::mem::size_of::() - } -} - -impl Encodable for TxKind { - fn encode(&self, out: &mut dyn BufMut) { - match self { - TxKind::Call(to) => to.encode(out), - TxKind::Create => out.put_u8(EMPTY_STRING_CODE), - } - } - fn length(&self) -> usize { - match self { - TxKind::Call(to) => to.length(), - TxKind::Create => 1, // EMPTY_STRING_CODE is a single byte - } - } -} - -impl Decodable for TxKind { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - if let Some(&first) = buf.first() { - if first == EMPTY_STRING_CODE { - buf.advance(1); - Ok(TxKind::Create) - } else { - let addr =
::decode(buf)?; - Ok(TxKind::Call(addr)) - } - } else { - Err(alloy_rlp::Error::InputTooShort) - } - } -}