diff --git a/Cargo.toml b/Cargo.toml index e1245ca2403..466f3930153 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,10 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [workspace.dependencies] +alloy-consensus = { version = "0.1.0", path = "crates/consensus" } +alloy-eips = { version = "0.1.0", path = "crates/eips" } alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" } -alloy-networks = { version = "0.1.0", path = "crates/networks" } +alloy-network = { version = "0.1.0", path = "crates/network" } alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } @@ -37,11 +39,11 @@ alloy-sol-types = { version = "0.5.1", default-features = false, features = ["st alloy-rlp = "0.3" # crypto -elliptic-curve = { version = "0.13.5", default-features = false, features = ["std"] } -generic-array = { version = "0.14.7", default-features = false, features = ["std"] } -k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "std"] } -sha2 = { version = "0.10.8", default-features = false, features = ["std"] } -spki = { version = "0.7.2", default-features = false, features = ["std"] } +elliptic-curve = { version = "0.13", default-features = false, features = ["std"] } +generic-array = { version = "0.14", default-features = false, features = ["std"] } +k256 = { version = "0.13", default-features = false, features = ["ecdsa", "std"] } +sha2 = { version = "0.10", default-features = false, features = ["std"] } +spki = { version = "0.7", default-features = false, features = ["std"] } # async async-trait = "0.1.74" @@ -57,13 +59,13 @@ tower = { version = "0.4.13", features = ["util"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" -tempfile = "3.8" - +# misc auto_impl = "1.1" base64 = "0.21" bimap = "0.6" home = "0.5" itertools = "0.12" +once_cell = "1.17" pin-project = "1.1" rand = "0.8.5" reqwest = { version = "0.11.18", default-features = false } @@ -78,7 +80,13 @@ serde_json = "1.0" ## misc-testing arbitrary = "1.3" assert_matches = "1.5" -similar-asserts = "1.5" proptest = "1.4" proptest-derive = "0.4" serial_test = "2.0" +similar-asserts = "1.5" +tempfile = "3.8" + +[patch.crates-io] +alloy-primitives = { git = "https://github.com/alloy-rs/core" } +alloy-sol-types = { git = "https://github.com/alloy-rs/core" } +alloy-sol-macro = { git = "https://github.com/alloy-rs/core" } diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml new file mode 100644 index 00000000000..9dc275f0579 --- /dev/null +++ b/crates/consensus/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "alloy-consensus" +description = "Ethereum consensus interface" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-network.workspace = true +alloy-primitives = { workspace = true, features = ["rlp"] } +alloy-rlp.workspace = true +alloy-eips.workspace = true + +# arbitrary +arbitrary = { workspace = true, features = ["derive"], optional = true } + +[dev-dependencies] +# arbitrary +arbitrary = { workspace = true, features = ["derive"] } + +[features] +k256 = ["alloy-primitives/k256", "alloy-network/k256"] +arbitrary = ["dep:arbitrary", "alloy-eips/arbitrary"] diff --git a/crates/consensus/README.md b/crates/consensus/README.md new file mode 100644 index 00000000000..cd4a01083a0 --- /dev/null +++ b/crates/consensus/README.md @@ -0,0 +1,27 @@ +# alloy-consensus + +Consensus types for the Ethereum blockchain. + +This crate contains constants, types, and functions for implementing Ethereum +EL consensus and communication. This includes headers, blocks, transactions, +eip2718 envelopes, eip2930, eip4844, and more. The types in this crate +implement many of the traits found in [alloy_network]. + +In general a type belongs in this crate if it is committed to in the EL block +header. This includes: + +- transactions +- blocks +- headers +- receipts +- [EIP-2718] envelopes. + +[alloy-network]: ../network +[EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + +## Provenance + +Much of this code was ported from [reth-primitives] as part of ongoing alloy +migrations. + +[reth-primitives]: https://github.com/paradigmxyz/reth/tree/main/crates/primitives diff --git a/crates/consensus/src/constants.rs b/crates/consensus/src/constants.rs new file mode 100644 index 00000000000..764ca25fc86 --- /dev/null +++ b/crates/consensus/src/constants.rs @@ -0,0 +1,72 @@ +//! Ethereum protocol-related constants +use alloy_primitives::{address, b256, Address, B256}; + +/// The first four bytes of the call data for a function call specifies the function to be called. +pub const SELECTOR_LEN: usize = 4; + +/// Maximum extra data size in a block after genesis +pub const MAXIMUM_EXTRA_DATA_SIZE: usize = 32; + +/// Multiplier for converting gwei to wei. +pub const GWEI_TO_WEI: u64 = 1_000_000_000; + +/// Multiplier for converting finney (milliether) to wei. +pub const FINNEY_TO_WEI: u128 = (GWEI_TO_WEI as u128) * 1_000_000; + +/// Multiplier for converting ether to wei. +pub const ETH_TO_WEI: u128 = FINNEY_TO_WEI * 1000; + +/// Multiplier for converting mgas to gas. +pub const MGAS_TO_GAS: u64 = 1_000_000u64; + +/// The Ethereum mainnet genesis hash. +pub const MAINNET_GENESIS_HASH: B256 = + b256!("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"); + +/// Goerli genesis hash. +pub const GOERLI_GENESIS_HASH: B256 = + b256!("bf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a"); + +/// Sepolia genesis hash. +pub const SEPOLIA_GENESIS_HASH: B256 = + b256!("25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9"); + +/// Holesky genesis hash. +pub const HOLESKY_GENESIS_HASH: B256 = + b256!("ff9006519a8ce843ac9c28549d24211420b546e12ce2d170c77a8cca7964f23d"); + +/// Testnet genesis hash. +pub const DEV_GENESIS_HASH: B256 = + b256!("2f980576711e3617a5e4d83dd539548ec0f7792007d505a3d2e9674833af2d7c"); + +/// Optimism goerli genesis hash. +pub const GOERLI_OP_GENESIS: B256 = + b256!("c1fc15cd51159b1f1e5cbc4b82e85c1447ddfa33c52cf1d98d14fba0d6354be1"); + +/// Base goerli genesis hash. +pub const GOERLI_BASE_GENESIS: B256 = + b256!("a3ab140f15ea7f7443a4702da64c10314eb04d488e72974e02e2d728096b4f76"); + +/// Keccak256 over empty array. +pub const KECCAK_EMPTY: B256 = + b256!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + +/// Ommer root of empty list. +pub const EMPTY_OMMER_ROOT_HASH: B256 = + b256!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + +/// Root hash of an empty trie. +pub const EMPTY_ROOT_HASH: B256 = + b256!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + +/// Transactions root of empty receipts set. +pub const EMPTY_RECEIPTS: B256 = EMPTY_ROOT_HASH; + +/// Transactions root of empty transactions set. +pub const EMPTY_TRANSACTIONS: B256 = EMPTY_ROOT_HASH; + +/// Withdrawals root of empty withdrawals set. +pub const EMPTY_WITHDRAWALS: B256 = EMPTY_ROOT_HASH; + +/// The address for the beacon roots contract defined in EIP-4788. +pub const BEACON_ROOTS_ADDRESS: Address = address!("000F3df6D732807Ef1319fB7B8bB8522d0Beac02"); diff --git a/crates/consensus/src/header.rs b/crates/consensus/src/header.rs new file mode 100644 index 00000000000..8a0c5561bdf --- /dev/null +++ b/crates/consensus/src/header.rs @@ -0,0 +1,464 @@ +use alloy_eips::{ + eip1559::{calc_next_block_base_fee, BaseFeeParams}, + eip4844::{calc_blob_gasprice, calc_excess_blob_gas}, +}; +use alloy_network::Sealable; +use alloy_primitives::{b256, keccak256, Address, BlockNumber, Bloom, Bytes, B256, B64, U256}; +use alloy_rlp::{ + length_of_length, Buf, BufMut, Decodable, Encodable, EMPTY_LIST_CODE, EMPTY_STRING_CODE, +}; +use std::mem; + +/// Ommer root of empty list. +pub const EMPTY_OMMER_ROOT_HASH: B256 = + b256!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + +/// Root hash of an empty trie. +pub const EMPTY_ROOT_HASH: B256 = + b256!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + +/// Ethereum Block header +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Header { + /// The Keccak 256-bit hash of the parent + /// block’s header, in its entirety; formally Hp. + pub parent_hash: B256, + /// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho. + pub ommers_hash: B256, + /// The 160-bit address to which all fees collected from the successful mining of this block + /// be transferred; formally Hc. + pub beneficiary: Address, + /// The Keccak 256-bit hash of the root node of the state trie, after all transactions are + /// executed and finalisations applied; formally Hr. + pub state_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with each + /// transaction in the transactions list portion of the block; formally Ht. + pub transactions_root: B256, + /// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts + /// of each transaction in the transactions list portion of the block; formally He. + pub receipts_root: B256, + /// The Keccak 256-bit hash of the withdrawals list portion of this block. + /// + pub withdrawals_root: Option, + /// The Bloom filter composed from indexable information (logger address and log topics) + /// contained in each log entry from the receipt of each transaction in the transactions list; + /// formally Hb. + pub logs_bloom: Bloom, + /// A scalar value corresponding to the difficulty level of this block. This can be calculated + /// from the previous block’s difficulty level and the timestamp; formally Hd. + pub difficulty: U256, + /// A scalar value equal to the number of ancestor blocks. The genesis block has a number of + /// zero; formally Hi. + pub number: BlockNumber, + /// A scalar value equal to the current limit of gas expenditure per block; formally Hl. + pub gas_limit: u64, + /// A scalar value equal to the total gas used in transactions in this block; formally Hg. + pub gas_used: u64, + /// A scalar value equal to the reasonable output of Unix’s time() at this block’s inception; + /// formally Hs. + pub timestamp: u64, + /// A 256-bit hash which, combined with the + /// nonce, proves that a sufficient amount of computation has been carried out on this block; + /// formally Hm. + pub mix_hash: B256, + /// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of + /// computation has been carried out on this block; formally Hn. + pub nonce: u64, + /// A scalar representing EIP1559 base fee which can move up or down each block according + /// to a formula which is a function of gas used in parent block and gas target + /// (block gas limit divided by elasticity multiplier) of parent block. + /// The algorithm results in the base fee per gas increasing when blocks are + /// above the gas target, and decreasing when blocks are below the gas target. The base fee per + /// gas is burned. + pub base_fee_per_gas: Option, + /// The total amount of blob gas consumed by the transactions within the block, added in + /// EIP-4844. + pub blob_gas_used: Option, + /// A running total of blob gas consumed in excess of the target, prior to the block. Blocks + /// with above-target blob gas consumption increase this value, blocks with below-target blob + /// gas consumption decrease it (bounded at 0). This was added in EIP-4844. + pub excess_blob_gas: Option, + /// The hash of the parent beacon block's root is included in execution blocks, as proposed by + /// EIP-4788. + /// + /// This enables trust-minimized access to consensus state, supporting staking pools, bridges, + /// and more. + /// + /// The beacon roots contract handles root storage, enhancing Ethereum's functionalities. + pub parent_beacon_block_root: Option, + /// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or + /// fewer; formally Hx. + pub extra_data: Bytes, +} + +impl Default for Header { + fn default() -> Self { + Header { + parent_hash: Default::default(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Default::default(), + state_root: EMPTY_ROOT_HASH, + transactions_root: EMPTY_ROOT_HASH, + receipts_root: EMPTY_ROOT_HASH, + logs_bloom: Default::default(), + difficulty: Default::default(), + number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: Default::default(), + mix_hash: Default::default(), + nonce: 0, + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + } + } +} + +impl Sealable for Header { + fn hash(&self) -> B256 { + self.hash_slow() + } +} + +impl Header { + // TODO: re-enable + + // /// Returns the parent block's number and hash + // pub fn parent_num_hash(&self) -> BlockNumHash { + // BlockNumHash { number: self.number.saturating_sub(1), hash: self.parent_hash } + // } + + /// Heavy function that will calculate hash of data and will *not* save the change to metadata. + /// + /// Use [`Header::seal_slow`] and unlock if you need the hash to be persistent. + pub fn hash_slow(&self) -> B256 { + let mut out = Vec::::new(); + self.encode(&mut out); + keccak256(&out) + } + + /// Checks if the header is empty - has no transactions and no ommers + pub fn is_empty(&self) -> bool { + let txs_and_ommers_empty = self.transaction_root_is_empty() && self.ommers_hash_is_empty(); + if let Some(withdrawals_root) = self.withdrawals_root { + txs_and_ommers_empty && withdrawals_root == EMPTY_ROOT_HASH + } else { + txs_and_ommers_empty + } + } + + /// Check if the ommers hash equals to empty hash list. + pub fn ommers_hash_is_empty(&self) -> bool { + self.ommers_hash == EMPTY_OMMER_ROOT_HASH + } + + /// Check if the transaction root equals to empty root. + pub fn transaction_root_is_empty(&self) -> bool { + self.transactions_root == EMPTY_ROOT_HASH + } + + // TODO: re-enable + + // /// Converts all roots in the header to a [BlockBodyRoots] struct. + // pub fn body_roots(&self) -> BlockBodyRoots { + // BlockBodyRoots { + // tx_root: self.transactions_root, + // ommers_hash: self.ommers_hash, + // withdrawals_root: self.withdrawals_root, + // } + // } + + /// Returns the blob fee for _this_ block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None + pub fn blob_fee(&self) -> Option { + self.excess_blob_gas.map(calc_blob_gasprice) + } + + /// Returns the blob fee for the next block according to the EIP-4844 spec. + /// + /// Returns `None` if `excess_blob_gas` is None. + /// + /// See also [Self::next_block_excess_blob_gas] + pub fn next_block_blob_fee(&self) -> Option { + self.next_block_excess_blob_gas().map(calc_blob_gasprice) + } + + /// Calculate base fee for next block according to the EIP-1559 spec. + /// + /// Returns a `None` if no base fee is set, no EIP-1559 support + pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option { + Some(calc_next_block_base_fee( + self.gas_used, + self.gas_limit, + self.base_fee_per_gas?, + base_fee_params, + )) + } + + /// Calculate excess blob gas for the next block according to the EIP-4844 + /// spec. + /// + /// Returns a `None` if no excess blob gas is set, no EIP-4844 support + pub fn next_block_excess_blob_gas(&self) -> Option { + Some(calc_excess_blob_gas(self.excess_blob_gas?, self.blob_gas_used?)) + } + + /// Calculate a heuristic for the in-memory size of the [Header]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // parent hash + mem::size_of::() + // ommers hash + mem::size_of::
() + // beneficiary + mem::size_of::() + // state root + mem::size_of::() + // transactions root + mem::size_of::() + // receipts root + mem::size_of::>() + // withdrawals root + mem::size_of::() + // logs bloom + mem::size_of::() + // difficulty + mem::size_of::() + // number + mem::size_of::() + // gas limit + mem::size_of::() + // gas used + mem::size_of::() + // timestamp + mem::size_of::() + // mix hash + mem::size_of::() + // nonce + mem::size_of::>() + // base fee per gas + mem::size_of::>() + // blob gas used + mem::size_of::>() + // excess blob gas + mem::size_of::>() + // parent beacon block root + self.extra_data.len() // extra data + } + + fn header_payload_length(&self) -> usize { + let mut length = 0; + length += self.parent_hash.length(); + length += self.ommers_hash.length(); + length += self.beneficiary.length(); + length += self.state_root.length(); + length += self.transactions_root.length(); + length += self.receipts_root.length(); + length += self.logs_bloom.length(); + length += self.difficulty.length(); + length += U256::from(self.number).length(); + length += U256::from(self.gas_limit).length(); + length += U256::from(self.gas_used).length(); + length += self.timestamp.length(); + length += self.extra_data.length(); + length += self.mix_hash.length(); + length += B64::new(self.nonce.to_be_bytes()).length(); + + if let Some(base_fee) = self.base_fee_per_gas { + length += U256::from(base_fee).length(); + } else if self.withdrawals_root.is_some() + || self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + length += 1; // EMPTY LIST CODE + } + + if let Some(root) = self.withdrawals_root { + length += root.length(); + } else if self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + length += 1; // EMPTY STRING CODE + } + + if let Some(blob_gas_used) = self.blob_gas_used { + length += U256::from(blob_gas_used).length(); + } else if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + length += 1; // EMPTY LIST CODE + } + + if let Some(excess_blob_gas) = self.excess_blob_gas { + length += U256::from(excess_blob_gas).length(); + } else if self.parent_beacon_block_root.is_some() { + length += 1; // EMPTY LIST CODE + } + + // Encode parent beacon block root length. If new fields are added, the above pattern will + // need to be repeated and placeholder length added. Otherwise, it's impossible to + // tell _which_ fields are missing. This is mainly relevant for contrived cases + // where a header is created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(parent_beacon_block_root) = self.parent_beacon_block_root { + length += parent_beacon_block_root.length(); + } + + length + } +} + +impl Encodable for Header { + fn encode(&self, out: &mut dyn BufMut) { + let list_header = + alloy_rlp::Header { list: true, payload_length: self.header_payload_length() }; + list_header.encode(out); + self.parent_hash.encode(out); + self.ommers_hash.encode(out); + self.beneficiary.encode(out); + self.state_root.encode(out); + self.transactions_root.encode(out); + self.receipts_root.encode(out); + self.logs_bloom.encode(out); + self.difficulty.encode(out); + U256::from(self.number).encode(out); + U256::from(self.gas_limit).encode(out); + U256::from(self.gas_used).encode(out); + self.timestamp.encode(out); + self.extra_data.encode(out); + self.mix_hash.encode(out); + B64::new(self.nonce.to_be_bytes()).encode(out); + + // Encode base fee. Put empty list if base fee is missing, + // but withdrawals root is present. + if let Some(ref base_fee) = self.base_fee_per_gas { + U256::from(*base_fee).encode(out); + } else if self.withdrawals_root.is_some() + || self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode withdrawals root. Put empty string if withdrawals root is missing, + // but blob gas used is present. + if let Some(ref root) = self.withdrawals_root { + root.encode(out); + } else if self.blob_gas_used.is_some() + || self.excess_blob_gas.is_some() + || self.parent_beacon_block_root.is_some() + { + out.put_u8(EMPTY_STRING_CODE); + } + + // Encode blob gas used. Put empty list if blob gas used is missing, + // but excess blob gas is present. + if let Some(ref blob_gas_used) = self.blob_gas_used { + U256::from(*blob_gas_used).encode(out); + } else if self.excess_blob_gas.is_some() || self.parent_beacon_block_root.is_some() { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode excess blob gas. Put empty list if excess blob gas is missing, + // but parent beacon block root is present. + if let Some(ref excess_blob_gas) = self.excess_blob_gas { + U256::from(*excess_blob_gas).encode(out); + } else if self.parent_beacon_block_root.is_some() { + out.put_u8(EMPTY_LIST_CODE); + } + + // Encode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders added. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if let Some(ref parent_beacon_block_root) = self.parent_beacon_block_root { + parent_beacon_block_root.encode(out); + } + } + + fn length(&self) -> usize { + let mut length = 0; + length += self.header_payload_length(); + length += length_of_length(length); + length + } +} + +impl Decodable for Header { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let rlp_head = alloy_rlp::Header::decode(buf)?; + if !rlp_head.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let started_len = buf.len(); + let mut this = Self { + parent_hash: Decodable::decode(buf)?, + ommers_hash: Decodable::decode(buf)?, + beneficiary: Decodable::decode(buf)?, + state_root: Decodable::decode(buf)?, + transactions_root: Decodable::decode(buf)?, + receipts_root: Decodable::decode(buf)?, + logs_bloom: Decodable::decode(buf)?, + difficulty: Decodable::decode(buf)?, + number: U256::decode(buf)?.to::(), + gas_limit: U256::decode(buf)?.to::(), + gas_used: U256::decode(buf)?.to::(), + timestamp: Decodable::decode(buf)?, + extra_data: Decodable::decode(buf)?, + mix_hash: Decodable::decode(buf)?, + nonce: u64::from_be_bytes(B64::decode(buf)?.0), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + }; + + if started_len - buf.len() < rlp_head.payload_length { + if buf.first().map(|b| *b == EMPTY_LIST_CODE).unwrap_or_default() { + buf.advance(1) + } else { + this.base_fee_per_gas = Some(U256::decode(buf)?.to::()); + } + } + + // Withdrawals root for post-shanghai headers + if started_len - buf.len() < rlp_head.payload_length { + if buf.first().map(|b| *b == EMPTY_STRING_CODE).unwrap_or_default() { + buf.advance(1) + } else { + this.withdrawals_root = Some(Decodable::decode(buf)?); + } + } + + // Blob gas used and excess blob gas for post-cancun headers + if started_len - buf.len() < rlp_head.payload_length { + if buf.first().map(|b| *b == EMPTY_LIST_CODE).unwrap_or_default() { + buf.advance(1) + } else { + this.blob_gas_used = Some(U256::decode(buf)?.to::()); + } + } + + if started_len - buf.len() < rlp_head.payload_length { + if buf.first().map(|b| *b == EMPTY_LIST_CODE).unwrap_or_default() { + buf.advance(1) + } else { + this.excess_blob_gas = Some(U256::decode(buf)?.to::()); + } + } + + // Decode parent beacon block root. If new fields are added, the above pattern will need to + // be repeated and placeholders decoded. Otherwise, it's impossible to tell _which_ + // fields are missing. This is mainly relevant for contrived cases where a header is + // created at random, for example: + // * A header is created with a withdrawals root, but no base fee. Shanghai blocks are + // post-London, so this is technically not valid. However, a tool like proptest would + // generate a block like this. + if started_len - buf.len() < rlp_head.payload_length { + this.parent_beacon_block_root = Some(B256::decode(buf)?); + } + + let consumed = started_len - buf.len(); + if consumed != rlp_head.payload_length { + return Err(alloy_rlp::Error::ListLengthMismatch { + expected: rlp_head.payload_length, + got: consumed, + }); + } + Ok(this) + } +} diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs new file mode 100644 index 00000000000..d3fa574e811 --- /dev/null +++ b/crates/consensus/src/lib.rs @@ -0,0 +1,29 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod constants; + +mod header; +pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; + +mod receipt; +pub use receipt::{Receipt, ReceiptEnvelope, ReceiptWithBloom}; + +mod transaction; +pub use transaction::{TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType}; + +pub use alloy_network::TxKind; diff --git a/crates/consensus/src/receipt/envelope.rs b/crates/consensus/src/receipt/envelope.rs new file mode 100644 index 00000000000..f2114ca4986 --- /dev/null +++ b/crates/consensus/src/receipt/envelope.rs @@ -0,0 +1,152 @@ +use crate::{Receipt, ReceiptWithBloom, TxType}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +/// Receipt envelope, as defined in [EIP-2718]. +/// +/// This enum distinguishes between tagged and untagged legacy receipts, as the +/// in-protocol merkle tree may commit to EITHER 0-prefixed or raw. Therefore +/// we must ensure that encoding returns the precise byte-array that was +/// decoded, preserving the presence or absence of the `TransactionType` flag. +/// +/// Transaction receipt payloads are specified in their respective EIPs. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReceiptEnvelope { + /// Receipt envelope with no type flag. + Legacy(ReceiptWithBloom), + /// Receipt envelope with type flag 0. + TaggedLegacy(ReceiptWithBloom), + /// Receipt envelope with type flag 1, containing a [EIP-2930] receipt. + /// + /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + Eip2930(ReceiptWithBloom), + /// Receipt envelope with type flag 2, containing a [EIP-1559] receipt. + /// + /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + Eip1559(ReceiptWithBloom), +} + +impl ReceiptEnvelope { + /// Return the [`TxType`] of the inner receipt. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) | Self::TaggedLegacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + } + } + + /// Return the inner receipt with bloom. Currently this is infallible, + /// however, future receipt types may be added. + pub const fn as_receipt_with_bloom(&self) -> Option<&ReceiptWithBloom> { + match self { + Self::Legacy(t) | Self::TaggedLegacy(t) | Self::Eip2930(t) | Self::Eip1559(t) => { + Some(t) + } + } + } + + /// Return the inner receipt. Currently this is infallible, however, future + /// receipt types may be added. + pub const fn as_receipt(&self) -> Option<&Receipt> { + match self { + Self::Legacy(t) | Self::TaggedLegacy(t) | Self::Eip2930(t) | Self::Eip1559(t) => { + Some(&t.receipt) + } + } + } + + /// Get the length of the inner receipt in the 2718 encoding. + pub fn inner_length(&self) -> usize { + self.as_receipt_with_bloom().unwrap().length() + } + + /// Calculate the length of the rlp payload of the network encoded receipt. + pub fn rlp_payload_length(&self) -> usize { + let length = self.as_receipt_with_bloom().unwrap().length(); + match self { + Self::Legacy(_) => length, + _ => length + 1, + } + } +} + +impl Encodable for ReceiptEnvelope { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.network_encode(out) + } + + fn length(&self) -> usize { + let mut payload_length = self.rlp_payload_length(); + if !self.is_legacy() { + payload_length += length_of_length(payload_length); + } + payload_length + } +} + +impl Decodable for ReceiptEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Self::network_decode(buf) { + Ok(t) => Ok(t), + Err(Eip2718Error::RlpError(e)) => Err(e), + Err(_) => Err(alloy_rlp::Error::Custom("Unexpected type")), + } + } +} + +impl Encodable2718 for ReceiptEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::TaggedLegacy(_) => Some(TxType::Legacy as u8), + Self::Eip2930(_) => Some(TxType::Eip2930 as u8), + Self::Eip1559(_) => Some(TxType::Eip1559 as u8), + } + } + + fn encode_2718_len(&self) -> usize { + self.inner_length() + !self.is_legacy() as usize + } + + fn encode_2718(&self, out: &mut dyn BufMut) { + match self.type_flag() { + None => {} + Some(ty) => out.put_u8(ty), + } + self.as_receipt_with_bloom().unwrap().encode(out); + } +} + +impl Decodable2718 for ReceiptEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { + let receipt = Decodable::decode(buf)?; + match ty.try_into()? { + TxType::Legacy => Ok(Self::TaggedLegacy(receipt)), + TxType::Eip2930 => Ok(Self::Eip2930(receipt)), + TxType::Eip1559 => Ok(Self::Eip1559(receipt)), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Result { + Ok(Self::Legacy(Decodable::decode(buf)?)) + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for ReceiptEnvelope { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let tx_type = u.int_in_range(-1..=2)?; + let receipt = Receipt::arbitrary(u)?.with_bloom(); + + match tx_type { + -1 => Ok(Self::Legacy(receipt)), + 0 => Ok(Self::TaggedLegacy(receipt)), + 1 => Ok(Self::Eip2930(receipt)), + 2 => Ok(Self::Eip1559(receipt)), + _ => unreachable!(), + } + } +} diff --git a/crates/consensus/src/receipt/mod.rs b/crates/consensus/src/receipt/mod.rs new file mode 100644 index 00000000000..a1e4de4dcfa --- /dev/null +++ b/crates/consensus/src/receipt/mod.rs @@ -0,0 +1,112 @@ +mod envelope; +pub use envelope::ReceiptEnvelope; + +mod receipts; +pub use receipts::{Receipt, ReceiptWithBloom}; + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::{address, b256, bytes, hex, Bytes, Log, LogData}; + use alloy_rlp::{Decodable, Encodable}; + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn encode_legacy_receipt() { + let expected = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"); + + let mut data = vec![]; + let receipt = + ReceiptEnvelope::Legacy(ReceiptWithBloom { + receipt: Receipt { + cumulative_gas_used: 0x1u64, + logs: vec![Log { + address: address!("0000000000000000000000000000000000000011"), + data: LogData::new_unchecked( + vec![ + b256!("000000000000000000000000000000000000000000000000000000000000dead"), + b256!("000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + ), + }], + success: false, + }, + bloom: [0; 256].into(), + }); + + receipt.network_encode(&mut data); + + // check that the rlp length equals the length of the expected rlp + assert_eq!(receipt.length(), expected.len()); + assert_eq!(data, expected); + } + + // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 + #[test] + fn decode_legacy_receipt() { + let data = hex!("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"); + + // EIP658Receipt + let expected = + ReceiptWithBloom { + receipt: Receipt { + cumulative_gas_used: 0x1u64, + logs: vec![Log { + address: address!("0000000000000000000000000000000000000011"), + data: LogData::new_unchecked( + vec![ + b256!("000000000000000000000000000000000000000000000000000000000000dead"), + b256!("000000000000000000000000000000000000000000000000000000000000beef"), + ], + bytes!("0100ff"), + ), + }], + success: false, + }, + bloom: [0; 256].into(), + }; + + let receipt = ReceiptWithBloom::decode(&mut &data[..]).unwrap(); + assert_eq!(receipt, expected); + } + + #[test] + fn gigantic_receipt() { + let receipt = Receipt { + cumulative_gas_used: 16747627, + success: true, + logs: vec![ + Log { + address: address!("4bf56695415f725e43c3e04354b604bcfb6dfb6e"), + data: LogData::new_unchecked( + vec![b256!( + "c69dc3d7ebff79e41f525be431d5cd3cc08f80eaf0f7819054a726eeb7086eb9" + )], + Bytes::from(vec![1; 0xffffff]), + ), + }, + Log { + address: address!("faca325c86bf9c2d5b413cd7b90b209be92229c2"), + data: LogData::new_unchecked( + vec![b256!( + "8cca58667b1e9ffa004720ac99a3d61a138181963b294d270d91c53d36402ae2" + )], + Bytes::from(vec![1; 0xffffff]), + ), + }, + ], + } + .with_bloom(); + + let mut data = vec![]; + + receipt.encode(&mut data); + let decoded = ReceiptWithBloom::decode(&mut &data[..]).unwrap(); + + // receipt.clone().to_compact(&mut data); + // let (decoded, _) = Receipt::from_compact(&data[..], data.len()); + assert_eq!(decoded, receipt); + } +} diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipt/receipts.rs new file mode 100644 index 00000000000..5e9022cb9cc --- /dev/null +++ b/crates/consensus/src/receipt/receipts.rs @@ -0,0 +1,188 @@ +use alloy_primitives::{Bloom, Log}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +/// Receipt containing result of transaction execution. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Receipt { + /// If transaction is executed successfully. + /// + /// This is the `statusCode` + pub success: bool, + /// Gas used + pub cumulative_gas_used: u64, + /// Log send from contracts. + pub logs: Vec, +} + +impl Receipt { + /// Calculates [`Log`]'s bloom filter. this is slow operation and [ReceiptWithBloom] can + /// be used to cache this value. + pub fn bloom_slow(&self) -> Bloom { + self.logs.iter().collect() + } + + /// Calculates the bloom filter for the receipt and returns the [ReceiptWithBloom] container + /// type. + pub fn with_bloom(self) -> ReceiptWithBloom { + self.into() + } +} + +impl alloy_network::Receipt for 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 + } +} + +/// [`Receipt`] with calculated bloom filter. +/// +/// This convenience type allows us to lazily calculate the bloom filter for a +/// receipt, similar to [`Sealed`]. +/// +/// [`Sealed`]: ::alloy_network::Sealed +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct ReceiptWithBloom { + /// The receipt. + pub receipt: Receipt, + /// The bloom filter. + pub bloom: Bloom, +} + +impl From for ReceiptWithBloom { + fn from(receipt: Receipt) -> Self { + let bloom = receipt.bloom_slow(); + ReceiptWithBloom { receipt, bloom } + } +} + +impl ReceiptWithBloom { + /// Create new [ReceiptWithBloom] + pub const fn new(receipt: Receipt, bloom: Bloom) -> Self { + Self { receipt, bloom } + } + + /// Consume the structure, returning only the receipt + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_receipt(self) -> Receipt { + self.receipt + } + + /// Consume the structure, returning the receipt and the bloom filter + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_components(self) -> (Receipt, Bloom) { + (self.receipt, self.bloom) + } + + fn payload_len(&self) -> usize { + self.receipt.success.length() + + self.receipt.cumulative_gas_used.length() + + self.bloom.length() + + self.receipt.logs.length() + } + + /// Returns the rlp header for the receipt payload. + fn receipt_rlp_header(&self) -> alloy_rlp::Header { + alloy_rlp::Header { list: true, payload_length: self.payload_len() } + } + + /// Encodes the receipt data. + fn encode_fields(&self, out: &mut dyn BufMut) { + self.receipt_rlp_header().encode(out); + self.receipt.success.encode(out); + self.receipt.cumulative_gas_used.encode(out); + self.bloom.encode(out); + self.receipt.logs.encode(out); + } + + /// Decodes the receipt payload + fn decode_receipt(buf: &mut &[u8]) -> alloy_rlp::Result { + let b: &mut &[u8] = &mut &**buf; + let rlp_head = alloy_rlp::Header::decode(b)?; + if !rlp_head.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let started_len = b.len(); + + let success = Decodable::decode(b)?; + let cumulative_gas_used = Decodable::decode(b)?; + let bloom = Decodable::decode(b)?; + let logs = Decodable::decode(b)?; + + let receipt = Receipt { success, cumulative_gas_used, logs }; + + let this = Self { receipt, bloom }; + let consumed = started_len - b.len(); + if consumed != rlp_head.payload_length { + return Err(alloy_rlp::Error::ListLengthMismatch { + expected: rlp_head.payload_length, + got: consumed, + }); + } + *buf = *b; + Ok(this) + } +} + +impl alloy_network::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 + } +} + +impl alloy_rlp::Encodable for ReceiptWithBloom { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.receipt.success.length() + + self.receipt.cumulative_gas_used.length() + + self.bloom.length() + + self.receipt.logs.length(); + payload_length + length_of_length(payload_length) + } +} + +impl alloy_rlp::Decodable for ReceiptWithBloom { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::decode_receipt(buf) + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for Receipt { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let success = bool::arbitrary(u)?; + let cumulative_gas_used = u64::arbitrary(u)?; + let logs = u.arbitrary_iter()?.take(4).collect::, _>>()?; + Ok(Self { success, cumulative_gas_used, logs }) + } +} diff --git a/crates/consensus/src/transaction/eip1559.rs b/crates/consensus/src/transaction/eip1559.rs new file mode 100644 index 00000000000..e3ac6c67bf0 --- /dev/null +++ b/crates/consensus/src/transaction/eip1559.rs @@ -0,0 +1,376 @@ +use crate::{TxKind, TxType}; +use alloy_eips::eip2930::AccessList; +use alloy_network::{Signed, Transaction}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use std::mem; + +/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip1559 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: u64, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasFeeCap` + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasTipCap` + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip1559 { + /// Returns the effective gas price for the given `base_fee`. + pub const fn effective_gas_price(&self, base_fee: Option) -> u128 { + match base_fee { + None => self.max_fee_per_gas, + Some(base_fee) => { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128); + if tip > self.max_priority_fee_per_gas { + self.max_priority_fee_per_gas + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas + } + } + } + } + + /// Decodes the inner [TxEip1559] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.max_fee_per_gas.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn alloy_rlp::BufMut, + ) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub(crate) const fn tx_type(&self) -> TxType { + TxType::Eip1559 + } + + /// Calculates a heuristic for the in-memory size of the [TxEip1559] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Encodes the legacy transaction in RLP for signing. + pub(crate) fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub(crate) fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub(crate) fn signature_hash(&self) -> B256 { + let mut buf = Vec::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + +impl Encodable for TxEip1559 { + fn encode(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxEip1559 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) + } +} + +impl Transaction for TxEip1559 { + type Signature = Signature; + // type Receipt = ReceiptWithBloom; + + fn into_signed(self, signature: Signature) -> Signed { + let mut buf = vec![]; + buf.put_u8(TxType::Eip1559 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + Signed::new_unchecked(self, signature, hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + TxEip1559::encode_with_signature(self, signature, out) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn signature_hash(&self) -> B256 { + TxEip1559::signature_hash(self) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + None + } + + fn set_gas_price(&mut self, price: U256) { + let _ = price; + } +} + +#[cfg(all(test, feature = "k256"))] +mod tests { + use super::TxEip1559; + use crate::TxKind; + use alloy_eips::eip2930::AccessList; + use alloy_network::Transaction; + use alloy_primitives::{address, b256, hex, Address, Signature, B256, U256}; + use alloy_rlp::Encodable; + + #[test] + fn recover_signer_eip1559() { + let signer: Address = address!("dd6b8b3dc6b7ad97db52f08a275ff4483e024cea"); + let hash: B256 = b256!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0"); + + let tx = TxEip1559 { + chain_id: 1, + nonce: 0x42, + gas_limit: 44386, + to: TxKind::Call( address!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6")), + value: U256::from(0_u64), + input: hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(), + max_fee_per_gas: 0x4a817c800, + max_priority_fee_per_gas: 0x3b9aca00, + access_list: AccessList::default(), + }; + + let sig = Signature::from_scalars_and_parity( + b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"), + b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"), + false, + ) + .unwrap(); + + assert_eq!( + tx.signature_hash(), + hex!("0d5688ac3897124635b6cf1bc0e29d6dfebceebdc10a54d74f2ef8b56535b682") + ); + + dbg!({ + let mut buf = vec![]; + tx.encode(&mut buf); + alloy_primitives::hex::encode(&buf) + }); + + dbg!(alloy_primitives::hex::encode(tx.signature_hash())); + + let signed_tx = tx.into_signed(sig); + assert_eq!(*signed_tx.hash(), hash, "Expected same hash"); + assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass."); + } +} diff --git a/crates/consensus/src/transaction/eip2930.rs b/crates/consensus/src/transaction/eip2930.rs new file mode 100644 index 00000000000..9726199d2d8 --- /dev/null +++ b/crates/consensus/src/transaction/eip2930.rs @@ -0,0 +1,354 @@ +use crate::{TxKind, TxType}; +use alloy_eips::eip2930::AccessList; +use alloy_network::{Signed, Transaction}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use std::mem; + +/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip2930 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: ChainId, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip2930 { + /// Calculates a heuristic for the in-memory size of the [TxEip2930] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() // input + } + + /// Decodes the inner [TxEip2930] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `gas_price` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_price: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature(&self, signature: &Signature, out: &mut dyn BufMut) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type. + pub const fn tx_type(&self) -> TxType { + TxType::Eip2930 + } + + /// Encodes the legacy transaction in RLP for signing. + pub(crate) fn encode_for_signing(&self, out: &mut dyn BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub(crate) fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub(crate) fn signature_hash(&self) -> B256 { + let mut buf = Vec::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + +impl Encodable for TxEip2930 { + fn encode(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxEip2930 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) + } +} + +impl Transaction for TxEip2930 { + type Signature = Signature; + // type Receipt = ReceiptWithBloom; + + fn into_signed(self, signature: Signature) -> Signed { + let mut buf = vec![]; + buf.put_u8(TxType::Eip2930 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + Signed::new_unchecked(self, signature, hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + self.encode_with_signature(signature, out) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + dbg!(alloy_primitives::hex::encode(&buf)); + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn signature_hash(&self) -> B256 { + TxEip2930::signature_hash(self) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) + } + + fn set_gas_price(&mut self, price: U256) { + if let Ok(price) = price.try_into() { + self.gas_price = price; + } + } +} + +#[cfg(test)] +mod tests { + use super::TxEip2930; + use crate::{TxEnvelope, TxKind}; + use alloy_network::{Signed, Transaction}; + use alloy_primitives::{Address, Bytes, Signature, U256}; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn test_decode_create() { + // tests that a contract creation tx encodes and decodes properly + let request = TxEip2930 { + chain_id: 1u64, + nonce: 0, + gas_price: 1, + gas_limit: 2, + to: TxKind::Create, + value: U256::from(3_u64), + input: Bytes::from(vec![1, 2]), + access_list: Default::default(), + }; + let signature = Signature::test_signature(); + + let tx = request.into_signed(signature); + + let mut encoded = Vec::new(); + tx.encode(&mut encoded); + assert_eq!(encoded.len(), tx.length()); + + let decoded = Signed::decode(&mut &*encoded).unwrap(); + assert_eq!(decoded, tx); + } + + #[test] + fn test_decode_call() { + let request = TxEip2930 { + chain_id: 1u64, + nonce: 0, + gas_price: 1, + gas_limit: 2, + to: TxKind::Call(Address::default()), + value: U256::from(3_u64), + input: Bytes::from(vec![1, 2]), + access_list: Default::default(), + }; + + let signature = Signature::test_signature(); + + let tx = request.into_signed(signature); + + let envelope = TxEnvelope::Eip2930(tx); + + let mut encoded = Vec::new(); + envelope.encode(&mut encoded); + assert_eq!(encoded.len(), envelope.length()); + + assert_eq!( + alloy_primitives::hex::encode(&encoded), + "b86401f8610180010294000000000000000000000000000000000000000003820102c080a0840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565a025e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1" + ); + + let decoded = TxEnvelope::decode(&mut encoded.as_ref()).unwrap(); + assert_eq!(decoded, envelope); + } +} diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs new file mode 100644 index 00000000000..090a73ba5e8 --- /dev/null +++ b/crates/consensus/src/transaction/envelope.rs @@ -0,0 +1,182 @@ +use crate::{TxEip1559, TxEip2930, TxLegacy}; +use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}; +use alloy_network::Signed; +use alloy_rlp::{length_of_length, Decodable, Encodable}; + +/// Ethereum `TransactionType` flags as specified in EIPs [2718], [1559], and +/// [2930]. +/// +/// [2718]: https://eips.ethereum.org/EIPS/eip-2718 +/// [1559]: https://eips.ethereum.org/EIPS/eip-1559 +/// [2930]: https://eips.ethereum.org/EIPS/eip-2930 +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub enum TxType { + /// Wrapped legacy transaction type. + Legacy = 0, + /// EIP-2930 transaction type. + Eip2930 = 1, + /// EIP-1559 transaction type. + Eip1559 = 2, +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for TxType { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(match u.int_in_range(0..=2)? { + 0 => TxType::Legacy, + 1 => TxType::Eip2930, + 2 => TxType::Eip1559, + _ => unreachable!(), + }) + } +} + +impl TryFrom for TxType { + type Error = Eip2718Error; + + fn try_from(value: u8) -> Result { + match value { + // SAFETY: repr(u8) with explicit discriminant + ..=2 => Ok(unsafe { std::mem::transmute(value) }), + _ => Err(Eip2718Error::UnexpectedType(value)), + } + } +} + +/// The Ethereum [EIP-2718] Transaction Envelope. +/// +/// # Note: +/// +/// This enum distinguishes between tagged and untagged legacy transactions, as +/// the in-protocol merkle tree may commit to EITHER 0-prefixed or raw. +/// Therefore we must ensure that encoding returns the precise byte-array that +/// was decoded, preserving the presence or absence of the `TransactionType` +/// flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TxEnvelope { + /// An untagged [`TxLegacy`]. + Legacy(Signed), + /// A [`TxLegacy`] tagged with type 0. + TaggedLegacy(Signed), + /// A [`TxEip2930`]. + Eip2930(Signed), + /// A [`TxEip1559`]. + Eip1559(Signed), +} + +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip2930(v) + } +} + +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip1559(v) + } +} + +impl TxEnvelope { + /// Return the [`TxType`] of the inner txn. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) | Self::TaggedLegacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + } + } + + /// Return the length of the inner txn. + pub fn inner_length(&self) -> usize { + match self { + Self::Legacy(t) | Self::TaggedLegacy(t) => t.length(), + Self::Eip2930(t) => t.length(), + Self::Eip1559(t) => t.length(), + } + } + + /// Return the RLP payload length of the network-serialized wrapper + fn rlp_payload_length(&self) -> usize { + if let Self::Legacy(t) = self { + return t.length(); + } + // length of inner tx body + let inner_length = self.inner_length(); + // with tx type byte + inner_length + 1 + } +} + +impl Encodable for TxEnvelope { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.network_encode(out) + } + + fn length(&self) -> usize { + let mut payload_length = self.rlp_payload_length(); + if !self.is_legacy() { + payload_length += length_of_length(payload_length); + } + payload_length + } +} + +impl Decodable for TxEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + match Self::network_decode(buf) { + Ok(t) => Ok(t), + Err(Eip2718Error::RlpError(e)) => Err(e), + Err(_) => Err(alloy_rlp::Error::Custom("Unexpected type")), + } + } +} + +impl Decodable2718 for TxEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result { + match ty.try_into()? { + TxType::Legacy => Ok(Self::TaggedLegacy(Decodable::decode(buf)?)), + TxType::Eip2930 => Ok(Self::Eip2930(Decodable::decode(buf)?)), + TxType::Eip1559 => Ok(Self::Eip1559(Decodable::decode(buf)?)), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Result { + Ok(TxEnvelope::Legacy(Decodable::decode(buf)?)) + } +} + +impl Encodable2718 for TxEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::TaggedLegacy(_) => Some(TxType::Legacy as u8), + Self::Eip2930(_) => Some(TxType::Eip2930 as u8), + Self::Eip1559(_) => Some(TxType::Eip1559 as u8), + } + } + + fn encode_2718_len(&self) -> usize { + self.inner_length() + !self.is_legacy() as usize + } + + fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + TxEnvelope::Legacy(tx) => tx.encode(out), + TxEnvelope::TaggedLegacy(tx) => { + out.put_u8(TxType::Legacy as u8); + tx.encode(out); + } + TxEnvelope::Eip2930(tx) => { + out.put_u8(TxType::Eip2930 as u8); + tx.encode(out); + } + TxEnvelope::Eip1559(tx) => { + out.put_u8(TxType::Eip2930 as u8); + tx.encode(out); + } + } + } +} diff --git a/crates/consensus/src/transaction/legacy.rs b/crates/consensus/src/transaction/legacy.rs new file mode 100644 index 00000000000..1227efef6cb --- /dev/null +++ b/crates/consensus/src/transaction/legacy.rs @@ -0,0 +1,339 @@ +use crate::TxKind; +use alloy_network::{Signed, Transaction}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result}; +use std::mem; + +/// Legacy transaction. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxLegacy { + /// Added as EIP-155: Simple replay attack protection + pub chain_id: Option, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxLegacy { + /// The EIP-2718 transaction type. + pub const TX_TYPE: isize = 0; + + /// Calculates a heuristic for the in-memory size of the [TxLegacy] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::>() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.to.size() + // to + mem::size_of::() + // value + self.input.len() // input + } + + /// Outputs the length of the transaction's fields, without a RLP header or length of the + /// eip155 fields. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.nonce.length(); + len += self.gas_price.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header or + /// eip155 fields. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.nonce.encode(out); + self.gas_price.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash. + pub fn encode_with_signature(&self, signature: &Signature, out: &mut dyn alloy_rlp::BufMut) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.write_rlp_vrs(out); + } + + /// Output the length of the RLP signed transaction encoding. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } + + /// Encodes EIP-155 arguments into the desired buffer. Only encodes values + /// for legacy transactions. + pub(crate) fn encode_eip155_signing_fields(&self, out: &mut dyn BufMut) { + // if this is a legacy transaction without a chain ID, it must be pre-EIP-155 + // and does not need to encode the chain ID for the signature hash encoding + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes + id.encode(out); + 0x00u8.encode(out); + 0x00u8.encode(out); + } + } + + /// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy + /// transactions. + pub(crate) fn eip155_fields_len(&self) -> usize { + if let Some(id) = self.chain_id { + // EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain + // ID to get the length of all 3 fields + // len(chain_id) + (0x00) + (0x00) + id.length() + 2 + } else { + // this is either a pre-EIP-155 legacy transaction or a typed transaction + 0 + } + } + + /// Encodes the legacy transaction in RLP for signing, including the EIP-155 fields if possible. + pub(crate) fn encode_for_signing(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() + self.eip155_fields_len() } + .encode(out); + self.encode_fields(out); + self.encode_eip155_signing_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction, including the length + /// of the EIP-155 fields if possible. + pub(crate) fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + // See [`Self::encode_for_signing`] for more information on the encoding format. + pub fn signature_hash(&self) -> B256 { + let mut buf = Vec::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } + + /// Decode the RLP fields of the transaction, without decoding an RLP + /// header. + pub(crate) fn decode_fields(data: &mut &[u8]) -> Result { + Ok(TxLegacy { + nonce: Decodable::decode(data)?, + gas_price: Decodable::decode(data)?, + gas_limit: Decodable::decode(data)?, + to: Decodable::decode(data)?, + value: Decodable::decode(data)?, + input: Decodable::decode(data)?, + chain_id: None, + }) + } +} + +impl Encodable for TxLegacy { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_for_signing(out) + } + + fn length(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxLegacy { + fn decode(data: &mut &[u8]) -> Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); + + let transaction_payload_len = header.payload_length; + + if transaction_payload_len > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + let mut transaction = Self::decode_fields(data)?; + + // If we still have data, it should be an eip-155 encoded chain_id + if !data.is_empty() { + transaction.chain_id = Some(Decodable::decode(data)?); + let _: U256 = Decodable::decode(data)?; // r + let _: U256 = Decodable::decode(data)?; // s + } + + let decoded = remaining_len - data.len(); + if decoded != transaction_payload_len { + return Err(alloy_rlp::Error::UnexpectedLength); + } + + Ok(transaction) + } +} + +impl Transaction for TxLegacy { + type Signature = Signature; + // type Receipt = ReceiptWithBloom; + + fn into_signed(self, signature: Signature) -> Signed { + let mut buf = vec![]; + self.encode_with_signature(&signature, &mut buf); + let hash = keccak256(&buf); + Signed::new_unchecked(self, signature, hash) + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + self.encode_with_signature(signature, out); + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + let tx = Self::decode_fields(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn signature_hash(&self) -> B256 { + let mut out: Vec = vec![]; + self.encode_for_signing(&mut out); + keccak256(&out) + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, data: Bytes) { + self.input = data; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn chain_id(&self) -> Option { + self.chain_id + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = Some(chain_id); + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, gas_limit: u64) { + self.gas_limit = gas_limit; + } + + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) + } + + fn set_gas_price(&mut self, price: U256) { + if let Ok(price) = price.try_into() { + self.gas_price = price; + } + } +} + +#[cfg(test)] +mod tests { + #[test] + #[cfg(feature = "k256")] + fn recover_signer_legacy() { + use crate::{TxKind, TxLegacy}; + use alloy_network::Transaction; + use alloy_primitives::{b256, hex, Address, Signature, B256, U256}; + + let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into(); + let hash: B256 = + hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into(); + + let tx = TxLegacy { + chain_id: Some(1), + nonce: 0x18, + gas_price: 0xfa56ea00, + gas_limit: 119902, + to: TxKind::Call( hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()), + value: U256::from(0x1c6bf526340000u64), + input: hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(), + }; + + let sig = Signature::from_scalars_and_parity( + b256!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031"), + b256!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5"), + 37, + ) + .unwrap(); + + let signed_tx = tx.into_signed(sig); + + assert_eq!(*signed_tx.hash(), hash, "Expected same hash"); + assert_eq!(signed_tx.recover_signer().unwrap(), signer, "Recovering signer should pass."); + } +} diff --git a/crates/consensus/src/transaction/mod.rs b/crates/consensus/src/transaction/mod.rs new file mode 100644 index 00000000000..ec5b99d0c9d --- /dev/null +++ b/crates/consensus/src/transaction/mod.rs @@ -0,0 +1,11 @@ +mod eip1559; +pub use eip1559::TxEip1559; + +mod eip2930; +pub use eip2930::TxEip2930; + +mod legacy; +pub use legacy::TxLegacy; + +mod envelope; +pub use envelope::{TxEnvelope, TxType}; diff --git a/crates/eips/Cargo.toml b/crates/eips/Cargo.toml new file mode 100644 index 00000000000..0466594e2c5 --- /dev/null +++ b/crates/eips/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "alloy-eips" +description = "Ethereum Improvement Proprosal (EIP) implementations" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["rlp"] } +alloy-rlp = { workspace = true, features = ["derive"] } + +thiserror.workspace = true + +# serde +serde = { workspace = true, optional = true } + +# arbitrary +arbitrary = { workspace = true, features = ["derive"], optional = true } +proptest = { workspace = true, optional = true } +proptest-derive = { workspace = true, optional = true } + +[dev-dependencies] +arbitrary = { workspace = true, features = ["derive"] } +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] +serde = ["dep:serde", "alloy-primitives/serde"] +arbitrary = ["dep:arbitrary", "dep:proptest-derive", "dep:proptest", "alloy-primitives/arbitrary"] diff --git a/crates/eips/README.md b/crates/eips/README.md new file mode 100644 index 00000000000..bc45b7ea908 --- /dev/null +++ b/crates/eips/README.md @@ -0,0 +1,11 @@ +# alloy-eips + +Contains constants, helpers, and basic data structures for consensus EIPs. + +## Current support + +- EIP-1559 +- EIP-2718 +- EIP-2930 +- EIP-4788 +- EIP-4844 diff --git a/crates/eips/src/eip1559/basefee.rs b/crates/eips/src/eip1559/basefee.rs new file mode 100644 index 00000000000..02696d1295b --- /dev/null +++ b/crates/eips/src/eip1559/basefee.rs @@ -0,0 +1,23 @@ +use crate::eip1559::constants::{ + DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, DEFAULT_ELASTICITY_MULTIPLIER, +}; + +/// BaseFeeParams contains the config parameters that control block base fee computation +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BaseFeeParams { + /// The base_fee_max_change_denominator from EIP-1559 + pub max_change_denominator: u64, + /// The elasticity multiplier from EIP-1559 + pub elasticity_multiplier: u64, +} + +impl BaseFeeParams { + /// Get the base fee parameters for Ethereum mainnet + pub const fn ethereum() -> BaseFeeParams { + BaseFeeParams { + max_change_denominator: DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, + elasticity_multiplier: DEFAULT_ELASTICITY_MULTIPLIER, + } + } +} diff --git a/crates/eips/src/eip1559/constants.rs b/crates/eips/src/eip1559/constants.rs new file mode 100644 index 00000000000..de36f1132ba --- /dev/null +++ b/crates/eips/src/eip1559/constants.rs @@ -0,0 +1,30 @@ +use alloy_primitives::U256; + +/// The default Ethereum block gas limit. +/// +/// TODO: This should be a chain spec parameter. +/// See . +pub const ETHEREUM_BLOCK_GAS_LIMIT: u64 = 30_000_000; + +/// The minimum tx fee below which the txpool will reject the transaction. +/// +/// Configured to `7` WEI which is the lowest possible value of base fee under mainnet EIP-1559 +/// parameters. `BASE_FEE_MAX_CHANGE_DENOMINATOR` +/// is `8`, or 12.5%. Once the base fee has dropped to `7` WEI it cannot decrease further because +/// 12.5% of 7 is less than 1. +/// +/// Note that min base fee under different 1559 parameterizations may differ, but there's no +/// signifant harm in leaving this setting as is. +pub const MIN_PROTOCOL_BASE_FEE: u64 = 7; + +/// Same as [MIN_PROTOCOL_BASE_FEE] but as a U256. +pub const MIN_PROTOCOL_BASE_FEE_U256: U256 = U256::from_limbs([7u64, 0, 0, 0]); + +/// Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; + +/// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8; + +/// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) +pub const DEFAULT_ELASTICITY_MULTIPLIER: u64 = 2; diff --git a/crates/eips/src/eip1559/helpers.rs b/crates/eips/src/eip1559/helpers.rs new file mode 100644 index 00000000000..a7d622face5 --- /dev/null +++ b/crates/eips/src/eip1559/helpers.rs @@ -0,0 +1,172 @@ +use crate::eip1559::BaseFeeParams; + +/// Calculate the base fee for the next block based on the EIP-1559 specification. +/// +/// This function calculates the base fee for the next block according to the rules defined in the +/// EIP-1559. EIP-1559 introduces a new transaction pricing mechanism that includes a +/// fixed-per-block network fee that is burned and dynamically adjusts block sizes to handlez +/// transient congestion. +/// +/// For each block, the base fee per gas is determined by the gas used in the parent block and the +/// target gas (the block gas limit divided by the elasticity multiplier). The algorithm increases +/// the base fee when blocks are congested and decreases it when they are under the target gas +/// usage. The base fee per gas is always burned. +/// +/// Parameters: +/// - `gas_used`: The gas used in the current block. +/// - `gas_limit`: The gas limit of the current block. +/// - `base_fee`: The current base fee per gas. +/// - `base_fee_params`: Base fee parameters such as elasticity multiplier and max change +/// denominator. +/// +/// Returns: +/// The calculated base fee for the next block as a `u64`. +/// +/// For more information, refer to the [EIP-1559 spec](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md). +pub fn calc_next_block_base_fee( + gas_used: u64, + gas_limit: u64, + base_fee: u64, + base_fee_params: BaseFeeParams, +) -> u64 { + // Calculate the target gas by dividing the gas limit by the elasticity multiplier. + let gas_target = gas_limit / base_fee_params.elasticity_multiplier; + + match gas_used.cmp(&gas_target) { + // If the gas used in the current block is equal to the gas target, the base fee remains the + // same (no increase). + core::cmp::Ordering::Equal => base_fee, + // If the gas used in the current block is greater than the gas target, calculate a new + // increased base fee. + core::cmp::Ordering::Greater => { + // Calculate the increase in base fee based on the formula defined by EIP-1559. + base_fee + + (core::cmp::max( + // Ensure a minimum increase of 1. + 1, + base_fee as u128 * (gas_used - gas_target) as u128 + / (gas_target as u128 * base_fee_params.max_change_denominator as u128), + ) as u64) + } + // If the gas used in the current block is less than the gas target, calculate a new + // decreased base fee. + core::cmp::Ordering::Less => { + // Calculate the decrease in base fee based on the formula defined by EIP-1559. + base_fee.saturating_sub( + (base_fee as u128 * (gas_target - gas_used) as u128 + / (gas_target as u128 * base_fee_params.max_change_denominator as u128)) + as u64, + ) + } + } +} + +#[cfg(test)] +mod tests { + use crate::eip1559::constants::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; + + use super::*; + + #[test] + fn min_protocol_sanity() { + assert_eq!(MIN_PROTOCOL_BASE_FEE_U256.to::(), MIN_PROTOCOL_BASE_FEE); + } + + #[test] + fn calculate_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1125000000, 1083333333, 1053571428, 1179939062, 1116028649, 918084097, 1063811730, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + BaseFeeParams::ethereum(), + ) + ); + } + } + + #[cfg(feature = "optimism")] + #[test] + fn calculate_optimism_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1100000048, 1080000000, 1065714297, 1167067046, 1128881311, 1028254188, 1098203452, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + crate::BaseFeeParams::optimism(), + ) + ); + } + } + + #[cfg(feature = "optimism")] + #[test] + fn calculate_optimism_goerli_base_fee_success() { + let base_fee = [ + 1000000000, 1000000000, 1000000000, 1072671875, 1059263476, 1049238967, 1049238967, 0, + 1, 2, + ]; + let gas_used = [ + 10000000, 10000000, 10000000, 9000000, 10001000, 0, 10000000, 10000000, 10000000, + 10000000, + ]; + let gas_limit = [ + 10000000, 12000000, 14000000, 10000000, 14000000, 2000000, 18000000, 18000000, + 18000000, 18000000, + ]; + let next_base_fee = [ + 1180000000, 1146666666, 1122857142, 1244299375, 1189416692, 1028254188, 1144836295, 1, + 2, 3, + ]; + + for i in 0..base_fee.len() { + assert_eq!( + next_base_fee[i], + calc_next_block_base_fee( + gas_used[i], + gas_limit[i], + base_fee[i], + crate::BaseFeeParams::optimism_goerli(), + ) + ); + } + } +} diff --git a/crates/eips/src/eip1559/mod.rs b/crates/eips/src/eip1559/mod.rs new file mode 100644 index 00000000000..793d2d070c5 --- /dev/null +++ b/crates/eips/src/eip1559/mod.rs @@ -0,0 +1,15 @@ +//! [EIP-1559] constants, helpers, and types. +//! +//! [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 + +mod basefee; +pub use basefee::BaseFeeParams; + +mod constants; +pub use constants::{ + DEFAULT_BASE_FEE_MAX_CHANGE_DENOMINATOR, DEFAULT_ELASTICITY_MULTIPLIER, + ETHEREUM_BLOCK_GAS_LIMIT, INITIAL_BASE_FEE, MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256, +}; + +mod helpers; +pub use helpers::calc_next_block_base_fee; diff --git a/crates/eips/src/eip2718.rs b/crates/eips/src/eip2718.rs new file mode 100644 index 00000000000..360f3f73b8e --- /dev/null +++ b/crates/eips/src/eip2718.rs @@ -0,0 +1,162 @@ +//! [EIP-2718] traits. +//! +//! [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + +use alloy_primitives::{keccak256, Sealed, B256}; +use alloy_rlp::{BufMut, Header}; + +// https://eips.ethereum.org/EIPS/eip-2718#transactiontype-only-goes-up-to-0x7f +const TX_TYPE_BYTE_MAX: u8 = 0x7f; + +/// [EIP-2718] decoding errors. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +#[derive(thiserror::Error, Debug)] +pub enum Eip2718Error { + /// Rlp error from [`alloy_rlp`]. + #[error(transparent)] + RlpError(#[from] alloy_rlp::Error), + /// Got an unexpected type flag while decoding. + #[error("Unexpected type flag. Got {0}.")] + UnexpectedType(u8), + /// Some other error occurred. + #[error(transparent)] + Custom(#[from] Box), +} + +/// Decoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction +/// or a receipt with a type flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Decodable2718: Sized { + /// Extract the type byte from the buffer, if any. The type byte is the + /// first byte, provided that that first byte is 0x7f or lower. + fn extract_type_byte(buf: &mut &[u8]) -> Option { + buf.first().copied().filter(|b| *b <= TX_TYPE_BYTE_MAX) + } + + /// Decode the appropriate variant, based on the type flag. + /// + /// This function is invoked by [`Self::decode_2718`] with the type byte, and the tail of the + /// buffer. + /// + /// ## Note + /// + /// This should be a simple match block that invokes an inner type's RLP decoder. + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result; + + /// Decode the default variant. + /// + /// This function is invoked by [`Self::decode_2718`] when no type byte can be extracted. + fn fallback_decode(buf: &mut &[u8]) -> Result; + + /// Decode an EIP-2718 transaction into a concrete instance + fn decode_2718(buf: &mut &[u8]) -> Result { + Self::extract_type_byte(buf) + .map(|ty| Self::typed_decode(ty, &mut &buf[1..])) + .unwrap_or_else(|| Self::fallback_decode(buf)) + } + + /// Decode an EIP-2718 transaction in the network format. + /// + /// The network format is the RLP encoded string consisting of the + /// type-flag prepneded to an opaque inner encoding. The inner encoding is + /// RLP for all current Ethereum transaction types, but may not be in future + /// versions of the protocol. + fn network_decode(buf: &mut &[u8]) -> Result { + let h_decode = &mut *buf; + let h = Header::decode(h_decode)?; + + if h.list { + return Self::fallback_decode(buf); + } else { + *buf = h_decode; + } + + let pre_len = buf.len(); + if pre_len == 0 || pre_len < h.payload_length { + return Err(alloy_rlp::Error::InputTooShort.into()); + } + let ty = buf[0]; + let buf = &mut &buf[1..]; + let tx = Self::typed_decode(ty, buf)?; + + if buf.len() != pre_len - h.payload_length { + return Err(alloy_rlp::Error::UnexpectedLength.into()); + } + + Ok(tx) + } +} + +/// Encoding trait for [EIP-2718] envelopes. These envelopes wrap a transaction +/// or a receipt with a type flag. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Encodable2718: Sized + Send + Sync + 'static { + /// Return the type flag (if any). + /// + /// This should return `None` for the default (legacy) variant of the + /// envelope. + fn type_flag(&self) -> Option; + + /// True if the envelope is the legacy variant. + fn is_legacy(&self) -> bool { + matches!(self.type_flag(), None | Some(0)) + } + + /// The length of the 2718 encoded envelope. This is the length of the type + /// flag + the length of the inner transaction RLP. + fn encode_2718_len(&self) -> usize; + + /// Encode the transaction according to [EIP-2718] rules. First a 1-byte + /// type flag in the range 0x0-0x7f, then the body of the transaction. + /// + /// This implementation uses RLP for the transaction body. Non-standard + /// users can override this to use some other serialization scheme. + /// + /// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 + fn encode_2718(&self, out: &mut dyn BufMut); + + /// Encode the transaction according to [EIP-2718] rules. First a 1-byte + /// type flag in the range 0x0-0x7f, then the body of the transaction. + /// + /// This is a convenience method for encoding into a vec, and returning the + /// vec. + fn encoded_2718(&self) -> Vec { + let mut out = vec![]; + self.encode_2718(&mut out); + out + } + + /// Compute the hash as committed to in the MPT trie. + fn trie_hash(&self) -> B256 { + keccak256(self.encoded_2718()) + } + + /// Seal the encodable, by encoding and hashing it. + fn seal(self) -> Sealed { + let hash = self.trie_hash(); + Sealed::new_unchecked(self, hash) + } + + /// Return the network encoding. For non-legacy items, this is the RLP + /// encoding of the bytestring of the 2718 encoding. For legacy items it is + /// simply the legacy encoding. + fn network_encode(&self, out: &mut dyn BufMut) { + if !self.is_legacy() { + Header { list: false, payload_length: self.encode_2718_len() }.encode(out); + } + + self.encode_2718(out); + } +} + +/// An [EIP-2718] envelope, blanket implemented for types that impl +/// [`Encodable2718`] and [`Decodable2718`]. This envelope is a wrapper around +/// a transaction, or a receipt, or any other type that is differentiated by an +/// EIP-2718 transaction type. +/// +/// [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +pub trait Eip2718Envelope: Decodable2718 + Encodable2718 {} +impl Eip2718Envelope for T where T: Decodable2718 + Encodable2718 {} diff --git a/crates/eips/src/eip2930.rs b/crates/eips/src/eip2930.rs new file mode 100644 index 00000000000..2f7b7af8e96 --- /dev/null +++ b/crates/eips/src/eip2930.rs @@ -0,0 +1,93 @@ +//! [EIP-2930] types. +//! +//! [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 + +use alloy_primitives::{Address, B256, U256}; +use alloy_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; +use std::mem; + +/// A list of addresses and storage keys that the transaction plans to access. +/// Accesses outside the list are possible, but become more expensive. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodable, RlpEncodable)] +#[cfg_attr( + any(test, feature = "arbitrary"), + derive(proptest_derive::Arbitrary, arbitrary::Arbitrary) +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct AccessListItem { + /// Account addresses that would be loaded at the start of execution + pub address: Address, + /// Keys of storage that would be loaded at the start of execution + #[cfg_attr( + any(test, feature = "arbitrary"), + proptest( + strategy = "proptest::collection::vec(proptest::arbitrary::any::(), 0..=20)" + ) + )] + pub storage_keys: Vec, +} + +impl AccessListItem { + /// Calculates a heuristic for the in-memory size of the [AccessListItem]. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::
() + self.storage_keys.capacity() * mem::size_of::() + } +} + +/// AccessList as defined in EIP-2930 +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodableWrapper, RlpEncodableWrapper)] +#[cfg_attr( + any(test, feature = "arbitrary"), + derive(proptest_derive::Arbitrary, arbitrary::Arbitrary) +)] +pub struct AccessList( + #[cfg_attr( + any(test, feature = "arbitrary"), + proptest( + strategy = "proptest::collection::vec(proptest::arbitrary::any::(), 0..=20)" + ) + )] + pub Vec, +); + +impl AccessList { + /// Converts the list into a vec, expected by revm + pub fn flattened(&self) -> Vec<(Address, Vec)> { + self.flatten().collect() + } + + /// Consumes the type and converts the list into a vec, expected by revm + pub fn into_flattened(self) -> Vec<(Address, Vec)> { + self.into_flatten().collect() + } + + /// Consumes the type and returns an iterator over the list's addresses and storage keys. + pub fn into_flatten(self) -> impl Iterator)> { + self.0.into_iter().map(|item| { + ( + item.address, + item.storage_keys.into_iter().map(|slot| U256::from_be_bytes(slot.0)).collect(), + ) + }) + } + + /// Returns an iterator over the list's addresses and storage keys. + pub fn flatten(&self) -> impl Iterator)> + '_ { + self.0.iter().map(|item| { + ( + item.address, + item.storage_keys.iter().map(|slot| U256::from_be_bytes(slot.0)).collect(), + ) + }) + } + + /// Calculates a heuristic for the in-memory size of the [AccessList]. + #[inline] + pub fn size(&self) -> usize { + // take into account capacity + self.0.iter().map(AccessListItem::size).sum::() + + self.0.capacity() * mem::size_of::() + } +} diff --git a/crates/eips/src/eip4788.rs b/crates/eips/src/eip4788.rs new file mode 100644 index 00000000000..42aa14b394d --- /dev/null +++ b/crates/eips/src/eip4788.rs @@ -0,0 +1,9 @@ +//! [EIP-4788] constants. +//! +//! [EIP-4788]: https://eips.ethereum.org/EIPS/eip-4788 + +use alloy_primitives::{address, Address}; + +/// The caller to be used when calling the EIP-4788 beacon roots contract at the beginning of the +/// block. +pub const SYSTEM_ADDRESS: Address = address!("fffffffffffffffffffffffffffffffffffffffe"); diff --git a/crates/eips/src/eip4844.rs b/crates/eips/src/eip4844.rs new file mode 100644 index 00000000000..a27c2005f65 --- /dev/null +++ b/crates/eips/src/eip4844.rs @@ -0,0 +1,179 @@ +//! [EIP-4844] constants and helpers. +//! +//! [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + +/// Size a single field element in bytes. +pub const FIELD_ELEMENT_BYTES: u64 = 32; + +/// How many field elements are stored in a single data blob. +pub const FIELD_ELEMENTS_PER_BLOB: u64 = 4096; + +/// Gas consumption of a single data blob. +pub const DATA_GAS_PER_BLOB: u64 = 131_072u64; // 32*4096 = 131072 == 2^17 == 0x20000 + +/// Maximum data gas for data blobs in a single block. +pub const MAX_DATA_GAS_PER_BLOCK: u64 = 786_432u64; // 0xC0000 = 6 * 0x20000 + +/// Target data gas for data blobs in a single block. +pub const TARGET_DATA_GAS_PER_BLOCK: u64 = 393_216u64; // 0x60000 = 3 * 0x20000 + +/// Maximum number of data blobs in a single block. +pub const MAX_BLOBS_PER_BLOCK: usize = (MAX_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) as usize; // 786432 / 131072 = 6 + +/// Target number of data blobs in a single block. +pub const TARGET_BLOBS_PER_BLOCK: u64 = TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB; // 393216 / 131072 = 3 + +/// Determines the maximum rate of change for blob fee +pub const BLOB_GASPRICE_UPDATE_FRACTION: u64 = 3_338_477u64; // 3338477 + +/// Minimum gas price for a data blob +pub const BLOB_TX_MIN_BLOB_GASPRICE: u128 = 1u128; + +/// Commitment version of a KZG commitment +pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; + +/// Calculates the `excess_blob_gas` from the parent header's `blob_gas_used` and `excess_blob_gas`. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`calc_excess_blob_gas`). +#[inline] +pub const fn calc_excess_blob_gas(parent_excess_blob_gas: u64, parent_blob_gas_used: u64) -> u64 { + (parent_excess_blob_gas + parent_blob_gas_used).saturating_sub(TARGET_DATA_GAS_PER_BLOCK) +} + +/// Calculates the blob gas price from the header's excess blob gas field. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`get_blob_gasprice`). +#[inline] +pub fn calc_blob_gasprice(excess_blob_gas: u64) -> u128 { + fake_exponential( + BLOB_TX_MIN_BLOB_GASPRICE as u64, + excess_blob_gas, + BLOB_GASPRICE_UPDATE_FRACTION, + ) +} + +/// Approximates `factor * e ** (numerator / denominator)` using Taylor expansion. +/// +/// This is used to calculate the blob price. +/// +/// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) +/// (`fake_exponential`). +/// +/// # Panics +/// +/// This function panics if `denominator` is zero. +#[inline] +fn fake_exponential(factor: u64, numerator: u64, denominator: u64) -> u128 { + assert_ne!(denominator, 0, "attempt to divide by zero"); + let factor = factor as u128; + let numerator = numerator as u128; + let denominator = denominator as u128; + + let mut i = 1; + let mut output = 0; + let mut numerator_accum = factor * denominator; + while numerator_accum > 0 { + output += numerator_accum; + + // Denominator is asserted as not zero at the start of the function. + numerator_accum = (numerator_accum * numerator) / (denominator * i); + i += 1; + } + output / denominator +} + +#[cfg(test)] +mod tests { + use super::*; + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L27 + #[test] + fn test_calc_excess_blob_gas() { + for t @ &(excess, blobs, expected) in &[ + // The excess blob gas should not increase from zero if the used blob + // slots are below - or equal - to the target. + (0, 0, 0), + (0, 1, 0), + (0, TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB, 0), + // If the target blob gas is exceeded, the excessBlobGas should increase + // by however much it was overshot + (0, (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 1, DATA_GAS_PER_BLOB), + (1, (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 1, DATA_GAS_PER_BLOB + 1), + (1, (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) + 2, 2 * DATA_GAS_PER_BLOB + 1), + // The excess blob gas should decrease by however much the target was + // under-shot, capped at zero. + ( + TARGET_DATA_GAS_PER_BLOCK, + TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB, + TARGET_DATA_GAS_PER_BLOCK, + ), + ( + TARGET_DATA_GAS_PER_BLOCK, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 1, + TARGET_DATA_GAS_PER_BLOCK - DATA_GAS_PER_BLOB, + ), + ( + TARGET_DATA_GAS_PER_BLOCK, + (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 2, + TARGET_DATA_GAS_PER_BLOCK - (2 * DATA_GAS_PER_BLOB), + ), + (DATA_GAS_PER_BLOB - 1, (TARGET_DATA_GAS_PER_BLOCK / DATA_GAS_PER_BLOB) - 1, 0), + ] { + let actual = calc_excess_blob_gas(excess, blobs * DATA_GAS_PER_BLOB); + assert_eq!(actual, expected, "test: {t:?}"); + } + } + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L60 + #[test] + fn test_calc_blob_fee() { + let blob_fee_vectors = &[ + (0, 1), + (2314057, 1), + (2314058, 2), + (10 * 1024 * 1024, 23), + // calc_blob_gasprice approximates `e ** (excess_blob_gas / + // BLOB_GASPRICE_UPDATE_FRACTION)` using Taylor expansion + // + // to roughly find where boundaries will be hit: + // 2 ** bits = e ** (excess_blob_gas / BLOB_GASPRICE_UPDATE_FRACTION) + // excess_blob_gas = ln(2 ** bits) * BLOB_GASPRICE_UPDATE_FRACTION + (148099578, 18446739238971471609), // output is just below the overflow + (148099579, 18446744762204311910), // output is just after the overflow + (161087488, 902580055246494526580), + ]; + + for &(excess, expected) in blob_fee_vectors { + let actual = calc_blob_gasprice(excess); + assert_eq!(actual, expected, "test: {excess}"); + } + } + + // https://github.com/ethereum/go-ethereum/blob/28857080d732857030eda80c69b9ba2c8926f221/consensus/misc/eip4844/eip4844_test.go#L78 + #[test] + fn fake_exp() { + for t @ &(factor, numerator, denominator, expected) in &[ + (1u64, 0u64, 1u64, 1u128), + (38493, 0, 1000, 38493), + (0, 1234, 2345, 0), + (1, 2, 1, 6), // approximate 7.389 + (1, 4, 2, 6), + (1, 3, 1, 16), // approximate 20.09 + (1, 6, 2, 18), + (1, 4, 1, 49), // approximate 54.60 + (1, 8, 2, 50), + (10, 8, 2, 542), // approximate 540.598 + (11, 8, 2, 596), // approximate 600.58 + (1, 5, 1, 136), // approximate 148.4 + (1, 5, 2, 11), // approximate 12.18 + (2, 5, 2, 23), // approximate 24.36 + (1, 50000000, 2225652, 5709098764), + (1, 380928, BLOB_GASPRICE_UPDATE_FRACTION, 1), + ] { + let actual = fake_exponential(factor, numerator, denominator); + assert_eq!(actual, expected, "test: {t:?}"); + } + } +} diff --git a/crates/eips/src/lib.rs b/crates/eips/src/lib.rs new file mode 100644 index 00000000000..5935ff12ec3 --- /dev/null +++ b/crates/eips/src/lib.rs @@ -0,0 +1,30 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod eip1559; +pub use eip1559::calc_next_block_base_fee; + +pub mod eip2718; + +pub mod eip2930; + +pub mod eip4788; + +pub mod eip4844; +pub use eip4844::{calc_blob_gasprice, calc_excess_blob_gas}; + +pub mod merge; diff --git a/crates/eips/src/merge.rs b/crates/eips/src/merge.rs new file mode 100644 index 00000000000..026440baa49 --- /dev/null +++ b/crates/eips/src/merge.rs @@ -0,0 +1,46 @@ +//! Constants related to the beacon chain consensus. + +use std::time::Duration; + +/// An EPOCH is a series of 32 slots. +pub const EPOCH_SLOTS: u64 = 32; + +/// The duration of a slot in seconds. +/// +/// This is the time period of 12 seconds in which a randomly chosen validator has time to propose a +/// block. +pub const SLOT_DURATION_SECS: u64 = 12; + +/// An EPOCH is a series of 32 slots (~6.4min). +pub const EPOCH_DURATION_SECS: u64 = EPOCH_SLOTS * SLOT_DURATION_SECS; + +/// The duration of a slot in seconds. +/// +/// This is the time period of 12 seconds in which a randomly chosen validator has time to propose a +/// block. +pub const SLOT_DURATION: Duration = Duration::from_secs(SLOT_DURATION_SECS); + +/// An EPOCH is a series of 32 slots (~6.4min). +pub const EPOCH_DURATION: Duration = Duration::from_secs(EPOCH_DURATION_SECS); + +/// The default block nonce in the beacon consensus +pub const BEACON_NONCE: u64 = 0u64; + +/// The number of blocks to unwind during a reorg that already became a part of canonical chain. +/// +/// In reality, the node can end up in this particular situation very rarely. It would happen only +/// if the node process is abruptly terminated during ongoing reorg and doesn't boot back up for +/// long period of time. +/// +/// Unwind depth of `3` blocks significantly reduces the chance that the reorged block is kept in +/// the database. +pub const BEACON_CONSENSUS_REORG_UNWIND_DEPTH: u64 = 3; + +/// Max seconds from current time allowed for blocks, before they're considered future blocks. +/// +/// This is only used when checking whether or not the timestamp for pre-merge blocks is in the +/// future. +/// +/// See: +/// +pub const ALLOWED_FUTURE_BLOCK_TIME_SECONDS: u64 = 15; diff --git a/crates/json-rpc/src/common.rs b/crates/json-rpc/src/common.rs index d2a0f47b7b6..39a99558945 100644 --- a/crates/json-rpc/src/common.rs +++ b/crates/json-rpc/src/common.rs @@ -1,6 +1,5 @@ -use std::fmt::Display; - use serde::{de::Visitor, Deserialize, Serialize}; +use std::fmt::Display; /// A JSON-RPC 2.0 ID object. This may be a number, a string, or null. /// diff --git a/crates/json-rpc/src/error.rs b/crates/json-rpc/src/error.rs index f72122b668d..d398e0a46e8 100644 --- a/crates/json-rpc/src/error.rs +++ b/crates/json-rpc/src/error.rs @@ -1,6 +1,5 @@ -use serde_json::value::RawValue; - use crate::{ErrorPayload, RpcReturn}; +use serde_json::value::RawValue; /// An RPC error. #[derive(thiserror::Error, Debug)] diff --git a/crates/json-rpc/src/result.rs b/crates/json-rpc/src/result.rs index 4ef5b45dac5..c75f8a1cf66 100644 --- a/crates/json-rpc/src/result.rs +++ b/crates/json-rpc/src/result.rs @@ -1,8 +1,6 @@ -use std::borrow::Borrow; - use crate::{Response, ResponsePayload, RpcError, RpcReturn}; - use serde_json::value::RawValue; +use std::borrow::Borrow; /// The result of a JSON-RPC request. /// diff --git a/crates/networks/Cargo.toml b/crates/network/Cargo.toml similarity index 68% rename from crates/networks/Cargo.toml rename to crates/network/Cargo.toml index 75b68dc4efc..51ecf79cbc8 100644 --- a/crates/networks/Cargo.toml +++ b/crates/network/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "alloy-networks" +name = "alloy-network" description = "Ethereum blockchain RPC behavior abstraction" version.workspace = true @@ -12,6 +12,11 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-eips = { workspace = true, features = ["serde"] } alloy-json-rpc.workspace = true alloy-primitives.workspace = true alloy-rlp.workspace = true +serde = { workspace = true, features = ["derive"] } + +[features] +k256 = ["alloy-primitives/k256"] diff --git a/crates/networks/README.md b/crates/network/README.md similarity index 95% rename from crates/networks/README.md rename to crates/network/README.md index 9a37e995551..e9ff1d6f6fe 100644 --- a/crates/networks/README.md +++ b/crates/network/README.md @@ -22,8 +22,8 @@ networking. The core model is as follows: ## Usage This crate is not intended to be used directly. It is used by the -[alloy-providers] library to modify the input and output types of the RPC -methods. +[alloy-providers] library and reth to modify the input and output types of the +RPC methods. This crate will primarily be used by blockchain maintainers to add bespoke RPC types to the Alloy provider. This is done by implementing the `Network` trait, diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs new file mode 100644 index 00000000000..c7042b550d0 --- /dev/null +++ b/crates/network/src/lib.rs @@ -0,0 +1,81 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +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::{Eip1559Transaction, Signed, Transaction, TxKind}; + +mod receipt; +pub use receipt::Receipt; + +pub use alloy_eips::eip2718; + +/// A list of transactions, either hydrated or hashes. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum TransactionList { + /// Hashes only. + Hashes(Vec), + /// Hydrated tx objects. + Hydrated(Vec), + /// Special case for uncle response + Uncled, +} + +/// A block response +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct BlockResponse { + #[serde(flatten)] + header: N::HeaderResponse, + transactions: TransactionList, +} + +/// Captures type info for network-specific RPC requests/responses. +pub trait Network: Sized + Send + Sync + 'static { + #[doc(hidden)] + /// Asserts that this trait can only be implemented on a ZST. + const __ASSERT_ZST: () = { + assert!(std::mem::size_of::() == 0, "Network must be a ZST"); + }; + + // -- Consensus types -- + + /// The network transaction envelope type. + type TxEnvelope: Eip2718Envelope; + /// The network receipt envelope type. + type ReceiptEnvelope: Eip2718Envelope; + /// The network header type. + type Header; + + // -- JSON RPC types -- + + /// The JSON body of a transaction request. + type TransactionRequest: RpcObject + Transaction; // + TransactionBuilder + /// The JSON body of a transaction response. + type TransactionResponse: RpcObject; + /// The JSON body of a transaction receipt. + type ReceiptResponse: RpcObject; + /// The JSON body of a header response, as flattened into + /// [`BlockResponse`]. + type HeaderResponse: RpcObject; +} diff --git a/crates/network/src/receipt.rs b/crates/network/src/receipt.rs new file mode 100644 index 00000000000..2648cce6623 --- /dev/null +++ b/crates/network/src/receipt.rs @@ -0,0 +1,23 @@ +use alloy_primitives::{Bloom, Log}; + +/// Receipt is the result of a transaction execution. +pub trait Receipt { + /// Returns true if the transaction was successful. + fn success(&self) -> bool; + + /// Returns the bloom filter for the logs in the receipt. This operation + /// may be expensive. + fn bloom(&self) -> Bloom; + + /// Returns the bloom filter for the logs in the receipt, if it is cheap to + /// compute. + fn bloom_cheap(&self) -> Option { + None + } + + /// Returns the cumulative gas used in the block after this transaction was executed. + fn cumulative_gas_used(&self) -> u64; + + /// Returns the logs emitted by this transaction. + fn logs(&self) -> &[Log]; +} diff --git a/crates/network/src/sealed.rs b/crates/network/src/sealed.rs new file mode 100644 index 00000000000..86d5c85a3b9 --- /dev/null +++ b/crates/network/src/sealed.rs @@ -0,0 +1,68 @@ +use alloy_primitives::B256; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// A consensus hashable item, with its memoized hash. +/// +/// We do not implement +pub struct Sealed { + /// The inner item + inner: T, + /// Its hash. + seal: B256, +} + +impl core::ops::Deref for Sealed { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner() + } +} + +impl Sealed { + /// Instantiate without performing the hash. This should be used carefully. + pub const fn new_unchecked(inner: T, seal: B256) -> Self { + Self { inner, seal } + } + + /// Decompose into parts. + #[allow(clippy::missing_const_for_fn)] // false positive + pub fn into_parts(self) -> (T, B256) { + (self.inner, self.seal) + } + + /// Get the inner item. + #[inline(always)] + pub const fn inner(&self) -> &T { + &self.inner + } + + /// Get the hash. + #[inline(always)] + pub const fn seal(&self) -> B256 { + self.seal + } + + /// Geth the hash (alias for [`Self::seal`]). + #[inline(always)] + pub const fn hash(&self) -> B256 { + self.seal() + } +} + +/// Sealeable objects. +pub trait Sealable: Sized { + /// Calculate the seal hash, this may be slow. + fn hash(&self) -> B256; + + /// Seal the object by calculating the hash. This may be slow. + fn seal_slow(self) -> Sealed { + let seal = self.hash(); + Sealed::new_unchecked(self, seal) + } + + /// Instantiate an unchecked seal. This should be used with caution. + fn seal_unchecked(self, seal: B256) -> Sealed { + Sealed::new_unchecked(self, seal) + } +} diff --git a/crates/network/src/transaction/common.rs b/crates/network/src/transaction/common.rs new file mode 100644 index 00000000000..42bf4d89486 --- /dev/null +++ b/crates/network/src/transaction/common.rs @@ -0,0 +1,72 @@ +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 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) + } + } +} diff --git a/crates/network/src/transaction/mod.rs b/crates/network/src/transaction/mod.rs new file mode 100644 index 00000000000..eafceed815a --- /dev/null +++ b/crates/network/src/transaction/mod.rs @@ -0,0 +1,103 @@ +use alloy_primitives::{Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{BufMut, Encodable}; + +mod common; +pub use common::TxKind; + +mod signed; +pub use signed::Signed; + +/// Represents a minimal EVM transaction. +pub trait Transaction: std::any::Any + Encodable + Send + Sync + 'static { + /// The signature type for this transaction. + /// + /// This is usually [`alloy_primitives::Signature`], however, it may be different for future + /// EIP-2718 transaction types, or in other networks. For example, in Optimism, the deposit + /// transaction signature is the unit type `()`. + type Signature; + + /// Convert to a signed transaction by adding a signature and computing the + /// hash. + fn into_signed(self, signature: Signature) -> Signed + where + Self: Sized; + + /// Encode with a signature. This encoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut); + + /// Decode a signed transaction. This decoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + /// + /// This MUST be the inverse of [`Transaction::encode_signed`]. + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> + where + Self: Sized; + + /// Calculate the signing hash for the transaction. + fn signature_hash(&self) -> B256; + + /// Get `data`. + fn input(&self) -> &[u8]; + /// Get `data`. + fn input_mut(&mut self) -> &mut Bytes; + /// Set `data`. + fn set_input(&mut self, data: Bytes); + + /// Get `to`. + fn to(&self) -> TxKind; + /// Set `to`. + fn set_to(&mut self, to: TxKind); + + /// Get `value`. + fn value(&self) -> U256; + /// Set `value`. + fn set_value(&mut self, value: U256); + + /// Get `chain_id`. + fn chain_id(&self) -> Option; + /// Set `chain_id`. + fn set_chain_id(&mut self, chain_id: ChainId); + + /// Get `nonce`. + fn nonce(&self) -> u64; + /// Set `nonce`. + fn set_nonce(&mut self, nonce: u64); + + /// Get `gas_limit`. + fn gas_limit(&self) -> u64; + /// Set `gas_limit`. + fn set_gas_limit(&mut self, limit: u64); + + /// Get `gas_price`. + fn gas_price(&self) -> Option; + /// Set `gas_price`. + fn set_gas_price(&mut self, price: U256); +} + +// TODO: Remove in favor of dyn trait upcasting (1.76+) +#[doc(hidden)] +impl dyn Transaction { + pub fn __downcast_ref(&self) -> Option<&T> { + if std::any::Any::type_id(self) == std::any::TypeId::of::() { + unsafe { Some(&*(self as *const _ as *const T)) } + } else { + None + } + } +} + +/// Captures getters and setters common across EIP-1559 transactions across all networks +pub trait Eip1559Transaction: Transaction { + /// Get `max_priority_fee_per_gas`. + #[doc(alias = "max_tip")] + fn max_priority_fee_per_gas(&self) -> U256; + /// Set `max_priority_fee_per_gas`. + #[doc(alias = "set_max_tip")] + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: U256); + + /// Get `max_fee_per_gas`. + fn max_fee_per_gas(&self) -> U256; + /// Set `max_fee_per_gas`. + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: U256); +} diff --git a/crates/network/src/transaction/signed.rs b/crates/network/src/transaction/signed.rs new file mode 100644 index 00000000000..fbb70b761db --- /dev/null +++ b/crates/network/src/transaction/signed.rs @@ -0,0 +1,85 @@ +use crate::Transaction; +use alloy_primitives::{Signature, B256}; +use alloy_rlp::BufMut; + +/// A transaction with a signature and hash seal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Signed { + tx: T, + signature: Sig, + hash: B256, +} + +impl std::ops::Deref for Signed { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.tx + } +} + +impl Signed { + /// Returns a reference to the transaction. + pub const fn tx(&self) -> &T { + &self.tx + } + + /// Returns a reference to the signature. + pub const fn signature(&self) -> &Sig { + &self.signature + } + + /// Returns a reference to the transaction hash. + pub const fn hash(&self) -> &B256 { + &self.hash + } +} + +impl Signed { + /// Instantiate from a transaction and signature. Does not verify the signature. + pub const fn new_unchecked(tx: T, signature: Signature, hash: B256) -> Self { + Self { tx, signature, hash } + } + + /// Calculate the signing hash for the transaction. + pub fn signature_hash(&self) -> B256 { + self.tx.signature_hash() + } + + /// Output the signed RLP for the transaction. + pub fn encode_signed(&self, out: &mut dyn BufMut) { + self.tx.encode_signed(&self.signature, out); + } + + /// Produce the RLP encoded signed transaction. + pub fn rlp_signed(&self) -> Vec { + let mut buf = vec![]; + self.encode_signed(&mut buf); + buf + } +} + +impl alloy_rlp::Encodable for Signed { + fn encode(&self, out: &mut dyn BufMut) { + self.tx.encode_signed(&self.signature, out) + } + + // TODO: impl length +} + +impl alloy_rlp::Decodable for Signed { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + T::decode_signed(buf) + } +} + +#[cfg(feature = "k256")] +impl Signed { + /// Recover the signer of the transaction + pub fn recover_signer( + &self, + ) -> Result { + let sighash = self.tx.signature_hash(); + self.signature.recover_address_from_prehash(sighash) + } +} diff --git a/crates/networks/src/lib.rs b/crates/networks/src/lib.rs deleted file mode 100644 index aa64945d240..00000000000 --- a/crates/networks/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -#![doc = include_str!("../README.md")] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", - html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" -)] -#![warn( - missing_copy_implementations, - missing_debug_implementations, - missing_docs, - unreachable_pub, - clippy::missing_const_for_fn, - rustdoc::all -)] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] -#![deny(unused_must_use, rust_2018_idioms)] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] - -use alloy_json_rpc::RpcObject; - -/// Captures type info for network-specific RPC requests/responses. -pub trait Network: Sized + Send + Sync + 'static { - #[doc(hidden)] - /// Asserts that this trait can only be implemented on a ZST. - const __ASSERT_ZST: () = { - assert!(std::mem::size_of::() == 0, "Network must be a ZST"); - }; - - /// The JSON body of a transaction request. - type TransactionRequest: Transaction; - - /// The JSON body of a transaction receipt. - type Receipt: Receipt; - - /// The JSON body of a transaction response. - type TransactionResponse: Transaction; -} - -/// Captures getters and setters common across transactions and -/// transaction-like objects across all networks. -pub trait Transaction: - alloy_rlp::Encodable + alloy_rlp::Decodable + RpcObject + Clone + Sized + 'static -{ - /// Sets the gas price of the transaction. - fn set_gas(&mut self, gas: alloy_primitives::U256); -} - -/// Captures getters and setters common across EIP-1559 transactions across all networks -pub trait Eip1559Transaction: Transaction {} - -/// Captures getters and setters common across receipts across all networks -pub trait Receipt: RpcObject + 'static {} diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 820d35dde7b..8fcdfc9369f 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-networks.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true alloy-rpc-client.workspace = true alloy-rpc-types.workspace = true diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index b1b486a632e..d69a7ad359a 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,5 +1,5 @@ use crate::{NetworkRpcClient, Provider}; -use alloy_networks::Network; +use alloy_network::Network; use alloy_rpc_client::RpcClient; use alloy_transport::Transport; use std::marker::PhantomData; diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index dbd39f52343..9001de90dac 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -16,7 +16,7 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use alloy_networks::{Network, Transaction}; +use alloy_network::{Network, Transaction}; use alloy_primitives::Address; use alloy_rpc_client::RpcClient; use alloy_transport::{BoxTransport, Transport, TransportResult}; @@ -109,14 +109,17 @@ pub trait Provider: Send + Sync { /// Send a transaction to the network. /// /// The transaction type is defined by the network. - async fn send_transaction(&self, tx: &N::TransactionRequest) -> TransportResult { + async fn send_transaction( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { self.inner().send_transaction(tx).await } async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { let gas = self.estimate_gas(&*tx).await; - gas.map(|gas| tx.set_gas(gas)) + gas.map(|gas| tx.set_gas_limit(gas.try_into().unwrap())) } } @@ -149,7 +152,10 @@ impl Provider for NetworkRpcClient .await } - async fn send_transaction(&self, tx: &N::TransactionRequest) -> TransportResult { + async fn send_transaction( + &self, + tx: &N::TransactionRequest, + ) -> TransportResult { self.prepare("eth_sendTransaction", Cow::Borrowed(tx)).await } } @@ -157,7 +163,7 @@ impl Provider for NetworkRpcClient #[cfg(test)] mod test { use crate::Provider; - use alloy_networks::Network; + use alloy_network::Network; // checks that `Provider` is object-safe fn __compile_check() -> Box> { diff --git a/crates/rpc-client/src/batch.rs b/crates/rpc-client/src/batch.rs index b154c81bfa2..5f682e78fa7 100644 --- a/crates/rpc-client/src/batch.rs +++ b/crates/rpc-client/src/batch.rs @@ -1,5 +1,4 @@ use crate::RpcClient; - use alloy_json_rpc::{ transform_response, try_deserialize_ok, Id, Request, RequestPacket, ResponsePacket, RpcParam, RpcReturn, SerializedRequest, diff --git a/crates/rpc-client/tests/it/ipc.rs b/crates/rpc-client/tests/it/ipc.rs index 2a8f15c5e58..6de72a1459f 100644 --- a/crates/rpc-client/tests/it/ipc.rs +++ b/crates/rpc-client/tests/it/ipc.rs @@ -1,10 +1,9 @@ -use std::borrow::Cow; - use alloy_primitives::U64; use alloy_pubsub::PubSubFrontend; use alloy_rpc_client::{ClientBuilder, RpcCall, RpcClient}; use alloy_transport_ipc::IpcConnect; use ethers_core::utils::{Geth, GethInstance}; +use std::borrow::Cow; use tempfile::NamedTempFile; async fn connect() -> (RpcClient, GethInstance) { diff --git a/crates/rpc-trace-types/src/common.rs b/crates/rpc-trace-types/src/common.rs index 1ce139eee0c..638cb62f203 100644 --- a/crates/rpc-trace-types/src/common.rs +++ b/crates/rpc-trace-types/src/common.rs @@ -1,4 +1,4 @@ -//! Types used by tracing backends +//! Types used by tracing backends. use alloy_primitives::TxHash; use serde::{Deserialize, Serialize}; diff --git a/crates/rpc-types/src/eth/log.rs b/crates/rpc-types/src/eth/log.rs index f7bce9b5a55..3ca85568233 100644 --- a/crates/rpc-types/src/eth/log.rs +++ b/crates/rpc-types/src/eth/log.rs @@ -26,11 +26,11 @@ pub struct Log { pub removed: bool, } -impl TryFrom for alloy_primitives::Log { +impl TryFrom for alloy_primitives::LogData { type Error = LogError; fn try_from(value: Log) -> Result { - alloy_primitives::Log::new(value.topics, value.data).ok_or(LogError::TooManyTopics) + alloy_primitives::LogData::new(value.topics, value.data).ok_or(LogError::TooManyTopics) } } diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 1fca83f9119..a0c391a3e18 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{hex, Address, B256}; +use alloy_primitives::{hex, Address, ChainId, B256}; use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; use aws_sdk_kms::{ @@ -11,7 +11,7 @@ use aws_sdk_kms::{ types::{MessageType, SigningAlgorithmSpec}, Client, }; -use k256::ecdsa::{self, RecoveryId, VerifyingKey}; +use k256::ecdsa::{self, VerifyingKey}; use std::fmt; /// Amazon Web Services Key Management Service (AWS KMS) Ethereum signer. @@ -37,7 +37,7 @@ use std::fmt; /// let client = aws_sdk_kms::Client::new(&config); /// /// let key_id = "...".to_string(); -/// let chain_id = 1; +/// let chain_id = Some(1); /// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); /// /// let message = vec![0, 1, 2, 3]; @@ -49,10 +49,10 @@ use std::fmt; #[derive(Clone)] pub struct AwsSigner { kms: Client, - chain_id: u64, key_id: String, pubkey: VerifyingKey, address: Address, + chain_id: Option, } impl fmt::Debug for AwsSigner { @@ -96,20 +96,9 @@ pub enum AwsSignerError { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { #[instrument(err)] - #[allow(clippy::blocks_in_conditions)] - async fn sign_hash(&self, hash: &B256) -> Result { - self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) - } - - #[cfg(TODO)] // TODO: TypedTransaction - #[instrument(err)] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - tx_with_chain.set_chain_id(chain_id); - - let sighash = tx_with_chain.sighash(); - self.sign_digest_with_eip155(sighash, chain_id).await + #[allow(clippy::blocks_in_conditions)] // tracing::instrument on async fn + async fn sign_hash(&self, hash: B256) -> Result { + self.sign_digest_inner(hash).await.map_err(alloy_signer::Error::other) } #[inline] @@ -118,12 +107,12 @@ impl Signer for AwsSigner { } #[inline] - fn chain_id(&self) -> u64 { + fn chain_id(&self) -> Option { self.chain_id } #[inline] - fn set_chain_id(&mut self, chain_id: u64) { + fn set_chain_id(&mut self, chain_id: Option) { self.chain_id = chain_id; } } @@ -136,7 +125,7 @@ impl AwsSigner { pub async fn new( kms: Client, key_id: String, - chain_id: u64, + chain_id: Option, ) -> Result { let resp = request_get_pubkey(&kms, key_id.clone()).await?; let pubkey = decode_pubkey(resp)?; @@ -159,27 +148,24 @@ impl AwsSigner { pub async fn sign_digest_with_key( &self, key_id: String, - digest: &B256, + digest: B256, ) -> Result { request_sign_digest(&self.kms, key_id, digest).await.and_then(decode_signature) } /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: &B256) -> Result { + pub async fn sign_digest(&self, digest: B256) -> Result { self.sign_digest_with_key(self.key_id.clone(), digest).await } - /// Sign a digest with this signer's key and add the eip155 `v` value - /// corresponding to the input chain_id + /// Sign a digest with this signer's key and applies EIP-155. #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] - async fn sign_digest_with_eip155( - &self, - digest: &B256, - chain_id: u64, - ) -> Result { + async fn sign_digest_inner(&self, digest: B256) -> Result { let sig = self.sign_digest(digest).await?; let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); - sig.apply_eip155(chain_id); + if let Some(chain_id) = self.chain_id { + sig = sig.with_chain_id(chain_id); + } Ok(sig) } } @@ -196,7 +182,7 @@ async fn request_get_pubkey( async fn request_sign_digest( kms: &Client, key_id: String, - digest: &B256, + digest: B256, ) -> Result { kms.sign() .key_id(key_id) @@ -226,15 +212,15 @@ fn decode_signature(resp: SignOutput) -> Result Signature { - let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); + let signature = Signature::from_signature_and_parity(sig, false).unwrap(); if check_candidate(&signature, hash, pubkey) { return signature; } - signature.set_v(1); + let signature = signature.with_parity(true); if check_candidate(&signature, hash, pubkey) { return signature; } @@ -243,7 +229,7 @@ fn sig_from_digest_bytes_trial_recovery( } /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { +fn check_candidate(signature: &Signature, hash: B256, pubkey: &VerifyingKey) -> bool { signature.recover_from_prehash(hash).map(|key| key == *pubkey).unwrap_or(false) } @@ -258,8 +244,7 @@ mod tests { let config = aws_config::load_defaults(BehaviorVersion::latest()).await; let client = aws_sdk_kms::Client::new(&config); - let chain_id = 1; - let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); + let signer = AwsSigner::new(client, key_id, Some(1)).await.unwrap(); let message = vec![0, 1, 2, 3]; diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index a9d8b45edaf..f7a31951f81 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -18,7 +18,6 @@ alloy-signer.workspace = true async-trait.workspace = true coins-ledger = { version = "0.9.1", default-features = false } futures-util.workspace = true -k256.workspace = true semver.workspace = true thiserror.workspace = true tracing.workspace = true @@ -27,6 +26,7 @@ tracing.workspace = true alloy-sol-types = { workspace = true, optional = true } [dev-dependencies] +alloy-consensus.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } serial_test.workspace = true diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 3b51731a84d..ef051ef30da 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -1,8 +1,8 @@ //! Ledger Ethereum app wrapper. use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; -use alloy_primitives::{hex, Address, B256}; -use alloy_signer::{Result, Signature, Signer}; +use alloy_primitives::{hex, Address, ChainId, B256}; +use alloy_signer::{Result, SignableTx, Signature, Signer, TransactionExt}; use async_trait::async_trait; use coins_ledger::{ common::{APDUCommand, APDUData}, @@ -23,24 +23,14 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; pub struct LedgerSigner { transport: Mutex, derivation: DerivationType, - pub(crate) chain_id: u64, + pub(crate) chain_id: Option, pub(crate) address: Address, } -impl std::fmt::Display for LedgerSigner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "LedgerApp. Key at index {} with address {:?} on chain_id {}", - self.derivation, self.address, self.chain_id - ) - } -} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { - async fn sign_hash(&self, _hash: &B256) -> Result { + async fn sign_hash(&self, _hash: B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) @@ -57,10 +47,18 @@ impl Signer for LedgerSigner { .map_err(alloy_signer::Error::other) } - #[cfg(TODO)] // TODO: TypedTransaction #[inline] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - self.sign_tx(&tx).await.map_err(alloy_signer::Error::other) + async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { + let chain_id = self.chain_id(); + if let Some(chain_id) = chain_id { + tx.set_chain_id_checked(chain_id)?; + } + let rlp = tx.rlp_encode(); + let mut sig = self.sign_tx_rlp(&rlp).await.map_err(alloy_signer::Error::other)?; + if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { + sig = sig.with_chain_id(chain_id); + } + Ok(sig) } #[cfg(feature = "eip712")] @@ -79,12 +77,12 @@ impl Signer for LedgerSigner { } #[inline] - fn chain_id(&self) -> u64 { + fn chain_id(&self) -> Option { self.chain_id } #[inline] - fn set_chain_id(&mut self, chain_id: u64) { + fn set_chain_id(&mut self, chain_id: Option) { self.chain_id = chain_id; } } @@ -98,11 +96,14 @@ impl LedgerSigner { /// # async fn foo() -> Result<(), Box> { /// use alloy_signer_ledger::{HDPath, Ledger}; /// - /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; + /// let ledger = Ledger::new(HDPath::LedgerLive(0), Some(1)).await?; /// # Ok(()) /// # } /// ``` - pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + pub async fn new( + derivation: DerivationType, + chain_id: Option, + ) -> Result { let transport = Ledger::init().await?; let address = Self::get_address_with_path_transport(&transport, &derivation).await?; @@ -177,42 +178,13 @@ impl LedgerSigner { Ok(version) } - /// Signs an Ethereum transaction (requires confirmation on the ledger) - #[cfg(TODO)] // TODO: TypedTransaction - pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } + /// Signs an Ethereum transaction's RLP bytes (requires confirmation on the ledger). + /// + /// Note that this does not apply EIP-155. + pub async fn sign_tx_rlp(&self, tx_rlp: &[u8]) -> Result { let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(tx_with_chain.rlp().as_ref()); - - let mut signature = self.sign_payload(INS::SIGN, &payload).await?; - - // modify `v` value of signature to match EIP-155 for chains with large chain ID - // The logic is derived from Ledger's library - // https://github.com/LedgerHQ/ledgerjs/blob/e78aac4327e78301b82ba58d63a72476ecb842fc/packages/hw-app-eth/src/Eth.ts#L300 - let eip155_chain_id = self.chain_id * 2 + 35; - if eip155_chain_id + 1 > 255 { - let one_byte_chain_id = eip155_chain_id % 256; - let ecc_parity = if signature.v > one_byte_chain_id { - signature.v - one_byte_chain_id - } else { - one_byte_chain_id - signature.v - }; - - signature.v = match tx { - TypedTransaction::Eip2930(_) | TypedTransaction::Eip1559(_) => { - (ecc_parity % 2 != 1) as u64 - } - TypedTransaction::Legacy(_) => eip155_chain_id + ecc_parity, - #[cfg(feature = "optimism")] - TypedTransaction::DepositTransaction(_) => 0, - }; - } - - Ok(signature) + payload.extend_from_slice(tx_rlp); + self.sign_payload(INS::SIGN, &payload).await } #[cfg(feature = "eip712")] @@ -280,7 +252,7 @@ impl LedgerSigner { return Err(LedgerError::ShortResponse { got: data.len(), expected: 65 }); } - let sig = Signature::from_bytes(&data[1..], data[0] as u64)?; + let sig = Signature::from_bytes_and_parity(&data[1..], data[0] as u64)?; debug!(?sig, "Received signature from device"); Ok(sig) } @@ -309,6 +281,7 @@ impl LedgerSigner { #[cfg(test)] mod tests { use super::*; + use alloy_primitives::{address, U256}; const DTYPE: DerivationType = DerivationType::LedgerLive(0); @@ -317,7 +290,7 @@ mod tests { } async fn init_ledger() -> LedgerSigner { - match LedgerSigner::new(DTYPE, 1).await { + match LedgerSigner::new(DTYPE, Some(1)).await { Ok(ledger) => ledger, Err(e) => panic!("{e:?}\n{e}"), } @@ -345,22 +318,25 @@ mod tests { #[tokio::test] #[serial_test::serial] #[ignore] - #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_tx() { let ledger = init_ledger().await; // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); - let tx_req = TransactionRequest::new() - .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) - .gas(1000000) - .gas_price(400e9 as u64) - .nonce(5) - .data(data) - .value(alloy_primitives::utils::parse_ether(100).unwrap()) - .into(); - let tx = ledger.sign_transaction(&tx_req).await.unwrap(); + let mut tx = alloy_consensus::TxLegacy { + nonce: 0, + gas_price: 400e9 as u128, + gas_limit: 1000000, + to: alloy_consensus::TxKind::Call(address!("2ed7afa17473e17ac59908f088b4371d28585476")), + input: data.into(), + value: U256::from(100e18 as u128), + chain_id: None, + }; + let sighash = tx.signature_hash(); + let sig = ledger.sign_transaction(&mut tx).await.unwrap(); + assert_eq!(tx.chain_id, None); + assert_eq!(sig.recover_address_from_prehash(sighash).unwrap(), my_address()); } #[tokio::test] diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index e25a27d423e..4d7f8d947ac 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -5,7 +5,6 @@ #![allow(clippy::upper_case_acronyms)] use alloy_primitives::hex; -use k256::ecdsa; use std::fmt; use thiserror::Error; @@ -45,9 +44,9 @@ pub enum LedgerError { /// [`semver`] error. #[error(transparent)] SemVerError(#[from] semver::Error), - /// [`ecdsa`] error. + /// Signature Error #[error(transparent)] - Ecdsa(#[from] ecdsa::Error), + SignatureError(#[from] alloy_primitives::SignatureError), /// Thrown when trying to sign using EIP-712 with an incompatible Ledger Ethereum app. #[error("Ledger Ethereum app requires at least version {0}")] UnsupportedAppVersion(&'static str), diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index b2f47165cea..f1e73c8779b 100644 --- a/crates/signer-trezor/Cargo.toml +++ b/crates/signer-trezor/Cargo.toml @@ -12,6 +12,8 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true @@ -22,7 +24,6 @@ protobuf = "=3.2.0" async-trait.workspace = true semver.workspace = true thiserror.workspace = true -k256.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index dfc91a19e73..330eade994c 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,6 +1,8 @@ use super::types::{DerivationType, TrezorError}; -use alloy_primitives::{hex, Address, B256, U256}; -use alloy_signer::{Result, Signature, Signer}; +use alloy_consensus::TxEip1559; +use alloy_network::{Transaction, TxKind}; +use alloy_primitives::{hex, Address, ChainId, Parity, B256, U256}; +use alloy_signer::{Result, SignableTx, Signature, Signer, TransactionExt}; use async_trait::async_trait; use std::fmt; use trezor_client::client::Trezor; @@ -18,7 +20,7 @@ const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, - pub(crate) chain_id: u64, + pub(crate) chain_id: Option, pub(crate) address: Address, } @@ -35,7 +37,8 @@ impl fmt::Debug for TrezorSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { - async fn sign_hash(&self, _hash: &B256) -> Result { + #[inline] + async fn sign_hash(&self, _hash: B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) @@ -43,13 +46,23 @@ impl Signer for TrezorSigner { #[inline] async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message_(message).await.map_err(alloy_signer::Error::other) + self.sign_message_inner(message).await.map_err(alloy_signer::Error::other) } - #[cfg(TODO)] // TODO: TypedTransaction #[inline] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - self.sign_tx(tx).await + async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { + // TODO: the Trezor Ethereum sign transaction protobufs don't require a chain ID, but the + // trezor-client API does not reflect this. + // https://github.com/trezor/trezor-firmware/pull/3482 + let chain_id = if let Some(chain_id) = self.chain_id { + tx.set_chain_id_checked(chain_id)?; + chain_id + } else { + tx.chain_id().ok_or(TrezorError::MissingChainId).map_err(alloy_signer::Error::other)? + }; + let mut sig = self.sign_tx_inner(tx, chain_id).await.map_err(alloy_signer::Error::other)?; + sig = sig.with_chain_id(chain_id); + Ok(sig) } #[inline] @@ -58,12 +71,12 @@ impl Signer for TrezorSigner { } #[inline] - fn chain_id(&self) -> u64 { + fn chain_id(&self) -> Option { self.chain_id } #[inline] - fn set_chain_id(&mut self, chain_id: u64) { + fn set_chain_id(&mut self, chain_id: Option) { self.chain_id = chain_id; } } @@ -71,7 +84,10 @@ impl Signer for TrezorSigner { impl TrezorSigner { /// Instantiates a new Trezor signer. #[instrument(ret)] - pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + pub async fn new( + derivation: DerivationType, + chain_id: Option, + ) -> Result { let mut signer = Self { derivation: derivation.clone(), chain_id, @@ -105,7 +121,7 @@ impl TrezorSigner { let mut client = trezor_client::unique(false)?; client.init_device(None)?; - let features = client.features().ok_or(TrezorError::FeaturesError)?; + let features = client.features().ok_or(TrezorError::Features)?; let version = semver::Version::new( features.major_version() as u64, features.minor_version() as u64, @@ -140,59 +156,81 @@ impl TrezorSigner { Ok(address_str.parse()?) } - /// Signs an Ethereum transaction (requires confirmation on the Trezor) - #[cfg(TODO)] // TODO: TypedTransaction - pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { + /// Signs an Ethereum transaction (requires confirmation on the Trezor). + /// + /// Does not apply EIP-155. + async fn sign_tx_inner( + &self, + tx: &dyn Transaction, + chain_id: ChainId, + ) -> Result { let mut client = self.get_client()?; + let path = Self::convert_path(&self.derivation); - let arr_path = Self::convert_path(&self.derivation); + let nonce = tx.nonce(); + let nonce = u64_to_trezor(nonce); - let transaction = TrezorTransaction::load(tx)?; + let gas_price = U256::ZERO; + let gas_price = u256_to_trezor(gas_price); - let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + let gas_limit = tx.gas_limit(); + let gas_limit = u64_to_trezor(gas_limit); - let signature = match tx { - TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx( - arr_path, - transaction.nonce, - transaction.gas_price, - transaction.gas, - transaction.to, - transaction.value, - transaction.data, - chain_id, - )?, - TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx( - arr_path, - transaction.nonce, - transaction.gas, - transaction.to, - transaction.value, - transaction.data, - chain_id, - transaction.max_fee_per_gas, - transaction.max_priority_fee_per_gas, - transaction.access_list, - )?, - #[cfg(feature = "optimism")] - TypedTransaction::DepositTransaction(tx) => { - trezor_client::client::Signature { r: 0.into(), s: 0.into(), v: 0 } - } + let to = match tx.to() { + TxKind::Call(to) => address_to_trezor(&to), + TxKind::Create => String::new(), }; - Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + let value = tx.value(); + let value = u256_to_trezor(value); + + let data = tx.input().to_vec(); + + // TODO: Uncomment in 1.76 + /* + let signature = if let Some(tx) = (tx as &dyn std::any::Any).downcast_ref::() { + */ + let signature = if let Some(tx) = tx.__downcast_ref::() { + let max_gas_fee = tx.max_fee_per_gas; + let max_gas_fee = u128_to_trezor(max_gas_fee); + + let max_priority_fee = tx.max_priority_fee_per_gas; + let max_priority_fee = u128_to_trezor(max_priority_fee); + + let access_list = tx + .access_list + .0 + .iter() + .map(|item| trezor_client::client::AccessListItem { + address: address_to_trezor(&item.address), + storage_keys: item.storage_keys.iter().map(|key| key.to_vec()).collect(), + }) + .collect(); + + client.ethereum_sign_eip1559_tx( + path, + nonce, + gas_limit, + to, + value, + data, + chain_id, + max_gas_fee, + max_priority_fee, + access_list, + ) + } else { + client.ethereum_sign_tx(path, nonce, gas_price, gas_limit, to, value, data, chain_id) + }?; + signature_from_trezor(signature) } #[instrument(skip(message), fields(message=hex::encode(message)), ret)] - async fn sign_message_(&self, message: &[u8]) -> Result { + async fn sign_message_inner(&self, message: &[u8]) -> Result { let mut client = self.get_client()?; let apath = Self::convert_path(&self.derivation); - let signature = client.ethereum_sign_message(message.into(), apath)?; - - let r = U256::from_limbs(signature.r.0); - let s = U256::from_limbs(signature.s.0); - Signature::from_scalars(r.into(), s.into(), signature.v).map_err(Into::into) + signature_from_trezor(signature) } // helper which converts a derivation path to [u32] @@ -214,6 +252,31 @@ impl TrezorSigner { } } +fn u64_to_trezor(x: u64) -> Vec { + let bytes = x.to_be_bytes(); + bytes[x.leading_zeros() as usize / 8..].to_vec() +} + +fn u128_to_trezor(x: u128) -> Vec { + let bytes = x.to_be_bytes(); + bytes[x.leading_zeros() as usize / 8..].to_vec() +} + +fn u256_to_trezor(x: U256) -> Vec { + let bytes = x.to_be_bytes::<32>(); + bytes[x.leading_zeros() / 8..].to_vec() +} + +fn address_to_trezor(x: &Address) -> String { + format!("{x:?}") +} + +fn signature_from_trezor(x: trezor_client::client::Signature) -> Result { + let s = U256::from_limbs(x.s.0); + let r = U256::from_limbs(x.r.0); + Signature::from_rs_and_parity(r, s, Parity::Eip155(x.v)).map_err(Into::into) +} + #[cfg(test)] mod tests { use super::*; @@ -224,7 +287,7 @@ mod tests { // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default trezor derivation path - let trezor = TrezorSigner::new(DerivationType::TrezorLive(1), 1).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(1), Some(1)).await.unwrap(); assert_eq!( trezor.get_address().await.unwrap(), address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), @@ -238,7 +301,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_message() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), Some(1)).await.unwrap(); let message = "hello world"; let sig = trezor.sign_message(message.as_bytes()).await.unwrap(); let addr = trezor.get_address().await.unwrap(); diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index c1a0e9fe297..7ab3315b737 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -2,10 +2,9 @@ //! //! [Official Docs](https://docs.trezor.io/trezor-firmware/index.html) -use alloy_primitives::{hex, B256, U256}; +use alloy_primitives::hex; use std::fmt; use thiserror::Error; -use trezor_client::client::AccessListItem as Trezor_AccessListItem; /// Trezor wallet type. #[derive(Clone, Debug)] @@ -40,115 +39,17 @@ pub enum TrezorError { /// Thrown when converting a semver requirement. #[error(transparent)] Semver(#[from] semver::Error), - /// [`ecdsa`](k256::ecdsa) error. + /// Signature Error #[error(transparent)] - Ecdsa(#[from] k256::ecdsa::Error), + SignatureError(#[from] alloy_primitives::SignatureError), /// Thrown when trying to sign an EIP-712 struct with an incompatible Trezor Ethereum app /// version. #[error("Trezor Ethereum app requires at least version {0:?}")] UnsupportedFirmwareVersion(String), - /// No ENS support. - #[error("Trezor does not support ENS")] - NoEnsSupport, + /// Need to provide a chain ID for EIP-155 signing. + #[error("missing Trezor signer chain ID")] + MissingChainId, /// Could not retrieve device features. #[error("could not retrieve device features")] - FeaturesError, -} - -/// Trezor transaction. -#[allow(dead_code)] -pub(crate) struct TrezorTransaction { - pub(crate) nonce: Vec, - pub(crate) gas: Vec, - pub(crate) gas_price: Vec, - pub(crate) value: Vec, - pub(crate) to: String, - pub(crate) data: Vec, - pub(crate) max_fee_per_gas: Vec, - pub(crate) max_priority_fee_per_gas: Vec, - pub(crate) access_list: Vec, -} - -impl TrezorTransaction { - #[allow(dead_code)] - fn to_trimmed_big_endian(value: &U256) -> Vec { - let trimmed_value = B256::from(*value); - trimmed_value[value.leading_zeros() / 8..].to_vec() - } - - #[cfg(TODO)] // TODO: TypedTransaction - pub fn load(tx: &TypedTransaction) -> Result { - let to: String = match tx.to() { - Some(v) => match v { - NameOrAddress::Name(_) => return Err(TrezorError::NoEnsSupport), - NameOrAddress::Address(value) => hex::encode_prefixed(value), - }, - // Contract Creation - None => "".to_string(), - }; - - let nonce = tx.nonce().map_or(vec![], Self::to_trimmed_big_endian); - let gas = tx.gas().map_or(vec![], Self::to_trimmed_big_endian); - let gas_price = tx.gas_price().map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); - let value = tx.value().map_or(vec![], Self::to_trimmed_big_endian); - let data = tx.data().map_or(vec![], |v| v.to_vec()); - - match tx { - TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => Ok(Self { - nonce, - gas, - gas_price, - value, - to, - data, - max_fee_per_gas: vec![], - max_priority_fee_per_gas: vec![], - access_list: vec![], - }), - TypedTransaction::Eip1559(eip1559_tx) => { - let max_fee_per_gas = - eip1559_tx.max_fee_per_gas.map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); - - let max_priority_fee_per_gas = eip1559_tx - .max_priority_fee_per_gas - .map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); - - let mut access_list: Vec = Vec::new(); - for item in &eip1559_tx.access_list.0 { - let address: String = hex::encode_prefixed(item.address); - let mut storage_keys: Vec> = Vec::new(); - - for key in &item.storage_keys { - storage_keys.push(key.as_bytes().to_vec()) - } - - access_list.push(Trezor_AccessListItem { address, storage_keys }) - } - - Ok(Self { - nonce, - gas, - gas_price, - value, - to, - data, - max_fee_per_gas, - max_priority_fee_per_gas, - access_list, - }) - } - #[cfg(feature = "optimism")] - TypedTransaction::DepositTransaction(_) => Ok(Self { - nonce, - gas, - gas_price, - value, - to, - data, - max_fee_per_gas: vec![], - max_priority_fee_per_gas: vec![], - access_list: vec![], - }), - } - } + Features, } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 53a5eb2d7f7..5e075a1b49b 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -12,9 +12,8 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-primitives.workspace = true -# TODO -# alloy-rpc-types.workspace = true +alloy-network.workspace = true +alloy-primitives = { workspace = true, features = ["k256"] } auto_impl.workspace = true elliptic-curve.workspace = true @@ -37,6 +36,7 @@ coins-bip39 = { version = "0.8.7", default-features = false, optional = true } yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } [dev-dependencies] +alloy-consensus.workspace = true assert_matches.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 24b8fa36be3..1da4a180e0b 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { #[error("operation `{0}` is not supported by the signer")] UnsupportedOperation(UnsupportedSignerOperation), /// Mismatch between provided transaction chain ID and signer chain ID. - #[error("transaction chain ID ({tx}) does not match the signer's ({signer})")] + #[error("transaction-provided chain ID ({tx}) does not match the signer's ({signer})")] TransactionChainIdMismatch { /// The signer's chain ID. signer: u64, @@ -26,6 +26,9 @@ pub enum Error { /// [`hex`](mod@hex) error. #[error(transparent)] HexError(#[from] hex::FromHexError), + /// Signature error. + #[error(transparent)] + SignatureError(#[from] alloy_primitives::SignatureError), /// Generic error. #[error(transparent)] Other(#[from] Box), diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 989bd20c534..ab60a30af41 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -18,11 +18,8 @@ mod error; pub use error::{Error, Result, UnsupportedSignerOperation}; -mod signature; -pub use signature::Signature; - mod signer; -pub use signer::{Signer, SignerSync}; +pub use signer::{SignableTx, Signer, SignerSync, TransactionExt}; mod wallet; #[cfg(feature = "mnemonic")] @@ -31,6 +28,8 @@ pub use wallet::{Wallet, WalletError}; pub mod utils; +pub use alloy_primitives::Signature; + #[cfg(feature = "yubihsm")] pub use yubihsm; diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs deleted file mode 100644 index e60f38deabf..00000000000 --- a/crates/signer/src/signature.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::utils::{public_key_to_address, to_eip155_v}; -use alloy_primitives::{eip191_hash_message, hex, Address, B256}; -use elliptic_curve::NonZeroScalar; -use k256::{ - ecdsa::{self, RecoveryId, VerifyingKey}, - Secp256k1, -}; -use std::str::FromStr; - -/// An Ethereum ECDSA signature. -/// -/// This is a wrapper around [`ecdsa::Signature`] and a [`RecoveryId`] to provide public key -/// recovery functionality. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Signature { - /// The inner ECDSA signature. - inner: ecdsa::Signature, - /// The recovery ID. - recid: RecoveryId, -} - -impl<'a> TryFrom<&'a [u8]> for Signature { - type Error = ecdsa::Error; - - /// Parses a raw signature which is expected to be 65 bytes long where - /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value - /// and the final byte is the `v` value in 'Electrum' notation. - fn try_from(bytes: &'a [u8]) -> Result { - if bytes.len() != 65 { - return Err(ecdsa::Error::new()); - } - Self::from_bytes(&bytes[..64], bytes[64] as u64) - } -} - -impl FromStr for Signature { - type Err = ecdsa::Error; - - fn from_str(s: &str) -> Result { - match hex::decode(s) { - Ok(bytes) => Self::try_from(&bytes[..]), - Err(e) => Err(ecdsa::Error::from_source(e)), - } - } -} - -impl From<&Signature> for [u8; 65] { - #[inline] - fn from(value: &Signature) -> [u8; 65] { - value.as_bytes() - } -} - -impl From for [u8; 65] { - #[inline] - fn from(value: Signature) -> [u8; 65] { - value.as_bytes() - } -} - -impl From<&Signature> for Vec { - #[inline] - fn from(value: &Signature) -> Vec { - value.as_bytes().to_vec() - } -} - -impl From for Vec { - #[inline] - fn from(value: Signature) -> Vec { - value.as_bytes().to_vec() - } -} - -impl Signature { - /// Creates a new [`Signature`] from the given ECDSA signature and recovery ID. - /// - /// Normalizes the signature into "low S" form as described in - /// [BIP 0062: Dealing with Malleability][1]. - /// - /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki - #[inline] - pub fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { - let mut sig = Self::new_not_normalized(inner, recid); - sig.normalize_s(); - sig - } - - /// Creates a new signature from the given inner signature and recovery ID, without normalizing - /// it into "low S" form. - #[inline] - pub const fn new_not_normalized(inner: ecdsa::Signature, recid: RecoveryId) -> Self { - Self { inner, recid } - } - - /// Normalizes the signature into "low S" form as described in - /// [BIP 0062: Dealing with Malleability][1]. - /// - /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki - #[inline] - pub fn normalize_s(&mut self) { - // Normalize into "low S" form. See: - // - https://github.com/RustCrypto/elliptic-curves/issues/988 - // - https://github.com/bluealloy/revm/pull/870 - if let Some(normalized) = self.inner.normalize_s() { - self.inner = normalized; - self.recid = RecoveryId::from_byte(self.recid.to_byte() ^ 1).unwrap(); - } - } - - /// Parses a signature from a byte slice. - #[inline] - pub fn from_bytes(bytes: &[u8], v: u64) -> Result { - let inner = ecdsa::Signature::from_slice(bytes)?; - let recid = normalize_v(v); - Ok(Self::new(inner, recid)) - } - - /// Creates a [`Signature`] from the serialized `r` and `s` scalar values, which comprise the - /// ECDSA signature, alongside a `v` value, used to determine the recovery ID. - /// - /// See [`ecdsa::Signature::from_scalars`] for more details. - #[inline] - pub fn from_scalars(r: B256, s: B256, v: u64) -> Result { - let inner = ecdsa::Signature::from_scalars(r.0, s.0)?; - let recid = normalize_v(v); - Ok(Self::new(inner, recid)) - } - - /// Returns the inner ECDSA signature. - #[inline] - pub const fn inner(&self) -> &ecdsa::Signature { - &self.inner - } - - /// Returns the inner ECDSA signature. - #[inline] - pub fn inner_mut(&mut self) -> &mut ecdsa::Signature { - &mut self.inner - } - - /// Returns the inner ECDSA signature. - #[inline] - pub const fn into_inner(self) -> ecdsa::Signature { - self.inner - } - - /// Returns the recovery ID. - #[inline] - pub const fn recid(&self) -> RecoveryId { - self.recid - } - - #[doc(hidden)] - #[deprecated(note = "use `Signature::recid` instead")] - pub const fn recovery_id(&self) -> RecoveryId { - self.recid - } - - /// Returns the `r` component of this signature. - #[inline] - pub fn r(&self) -> NonZeroScalar { - self.inner.r() - } - - /// Returns the `s` component of this signature. - #[inline] - pub fn s(&self) -> NonZeroScalar { - self.inner.s() - } - - /// Returns the recovery ID as a `u8`. - #[inline] - pub const fn v(&self) -> u8 { - self.recid.to_byte() - } - - /// Returns the byte-array representation of this signature. - /// - /// The first 32 bytes are the `r` value, the second 32 bytes the `s` value - /// and the final byte is the `v` value in 'Electrum' notation. - #[inline] - pub fn as_bytes(&self) -> [u8; 65] { - let mut sig = [0u8; 65]; - sig[..32].copy_from_slice(self.r().to_bytes().as_ref()); - sig[32..64].copy_from_slice(self.s().to_bytes().as_ref()); - sig[64] = self.recid.to_byte(); - sig - } - - /// Sets the recovery ID. - #[inline] - pub fn set_recid(&mut self, recid: RecoveryId) { - self.recid = recid; - } - - /// Sets the recovery ID by normalizing a `v` value. - #[inline] - pub fn set_v(&mut self, v: u64) { - self.set_recid(normalize_v(v)); - } - - /// Modifies the recovery ID by applying [EIP-155] to a `v` value. - /// - /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 - #[inline] - pub fn apply_eip155(&mut self, chain_id: u64) { - self.set_v(to_eip155_v(self.recid.to_byte(), chain_id)); - } - - /// Recovers an [`Address`] from this signature and the given message by first prefixing and - /// hashing the message according to [EIP-191](eip191_hash_message). - #[inline] - pub fn recover_address_from_msg>( - &self, - msg: T, - ) -> Result { - self.recover_from_msg(msg).map(|pubkey| public_key_to_address(&pubkey)) - } - - /// Recovers an [`Address`] from this signature and the given prehashed message. - #[inline] - pub fn recover_address_from_prehash(&self, prehash: &B256) -> Result { - self.recover_from_prehash(prehash).map(|pubkey| public_key_to_address(&pubkey)) - } - - /// Recovers a [`VerifyingKey`] from this signature and the given message by first prefixing and - /// hashing the message according to [EIP-191](eip191_hash_message). - #[inline] - pub fn recover_from_msg>(&self, msg: T) -> Result { - self.recover_from_prehash(&eip191_hash_message(msg)) - } - - /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. - #[inline] - pub fn recover_from_prehash(&self, prehash: &B256) -> Result { - VerifyingKey::recover_from_prehash(prehash.as_slice(), &self.inner, self.recid) - } -} - -/// Normalizes a `v` value, respecting raw, legacy, and EIP-155 values. -/// -/// This function covers the entire u64 range, producing v-values as follows: -/// - 0-26 - raw/bare. 0-3 are legal. In order to ensure that all values are covered, we also handle -/// 4-26 here by returning v % 4. -/// - 27-34 - legacy. 27-30 are legal. By legacy bitcoin convention range 27-30 signals uncompressed -/// pubkeys, while 31-34 signals compressed pubkeys. We do not respect the compression convention. -/// All Ethereum keys are uncompressed. -/// - 35+ - EIP-155. By EIP-155 convention, `v = 35 + CHAIN_ID * 2 + 0/1` We return (v-1 % 2) here. -/// -/// NB: raw and legacy support values 2, and 3, while EIP-155 does not. -/// Recovery values of 2 and 3 are unlikely to occur in practice. In the vanishingly unlikely event -/// that you encounter an EIP-155 signature with a recovery value of 2 or 3, you should normalize -/// out of band. -#[inline] -const fn normalize_v(v: u64) -> RecoveryId { - let byte = match v { - // Case 1: raw/bare - 0..=26 => (v % 4) as u8, - // Case 2: non-EIP-155 v value - 27..=34 => ((v - 27) % 4) as u8, - // Case 3: EIP-155 V value - 35.. => ((v - 1) % 2) as u8, - }; - debug_assert!(byte <= RecoveryId::MAX); - match RecoveryId::from_byte(byte) { - Some(recid) => recid, - None => unsafe { core::hint::unreachable_unchecked() }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{address, b256}; - use std::str::FromStr; - - #[test] - #[cfg(TODO)] // TODO: Transaction - fn can_recover_tx_sender() { - // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f - let tx_rlp = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap(); - let tx: Transaction = rlp::decode(&tx_rlp).unwrap(); - assert_eq!(tx.rlp(), tx_rlp); - assert_eq!( - tx.hash, - "0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f".parse().unwrap() - ); - assert_eq!(tx.transaction_type, Some(2.into())); - let expected = Address::from_str("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5").unwrap(); - assert_eq!(tx.recover_from().unwrap(), expected); - } - - #[test] - fn can_recover_tx_sender_not_normalized() { - let sig = Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); - let hash = b256!("5eb4f5a33c621f32a8622d5f943b6b102994dfe4e5aebbefe69bb1b2aa0fc93e"); - let expected = address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e"); - assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), expected); - } - - #[test] - fn recover_web3_signature() { - // test vector taken from: - // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign - let signature = Signature::from_str( - "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" - ).expect("could not parse signature"); - let expected = address!("2c7536E3605D9C16a7a3D7b1898e529396a65c23"); - assert_eq!(signature.recover_address_from_msg("Some data").unwrap(), expected); - } - - #[test] - fn signature_from_str() { - let s1 = Signature::from_str( - "0xaa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" - ).expect("could not parse 0x-prefixed signature"); - - let s2 = Signature::from_str( - "aa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" - ).expect("could not parse non-prefixed signature"); - - assert_eq!(s1, s2); - } -} diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 319f3b31cfa..709a1236005 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -1,11 +1,49 @@ -use crate::{Result, Signature}; -use alloy_primitives::{eip191_hash_message, Address, B256}; +use crate::Result; +use alloy_network::Transaction; +use alloy_primitives::{eip191_hash_message, Address, ChainId, Signature, B256}; use async_trait::async_trait; use auto_impl::auto_impl; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; +/// A signable transaction. +pub type SignableTx = dyn Transaction; + +/// Extension trait for utilities for signable transactions. +/// +/// This trait is implemented for all types that implement [`Transaction`] with [`Signature`] as the +/// signature associated type. +pub trait TransactionExt: Transaction { + /// Encode the transaction. + fn rlp_encode(&self) -> Vec { + let mut out = Vec::new(); + self.encode(&mut out); + out + } + + /// Set `chain_id` if it is not already set. Checks that the provided `chain_id` matches the + /// existing `chain_id` if it is already set. + fn set_chain_id_checked(&mut self, chain_id: ChainId) -> Result<()> { + match self.chain_id() { + Some(tx_chain_id) => { + if tx_chain_id != chain_id { + return Err(crate::Error::TransactionChainIdMismatch { + signer: chain_id, + tx: tx_chain_id, + }); + } + } + None => { + self.set_chain_id(chain_id); + } + } + Ok(()) + } +} + +impl> TransactionExt for T {} + /// Asynchronous Ethereum signer. /// /// All provided implementations rely on [`sign_hash`](Signer::sign_hash). A signer may not always @@ -13,27 +51,43 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing /// methods directly. /// +/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay +/// protection. +/// +/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in +/// [`sign_transaction`](Self::sign_transaction), and to the resulting signature in all the methods. +/// If `chain_id` is None, [EIP-155] should not be applied. +/// /// Synchronous signers should implement both this trait and [`SignerSync`]. +/// +/// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[auto_impl(&mut, Box)] pub trait Signer: Send + Sync { /// Signs the given hash. - async fn sign_hash(&self, hash: &B256) -> Result; + async fn sign_hash(&self, hash: B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_hash(&eip191_hash_message(message)).await + self.sign_hash(eip191_hash_message(message)).await } /// Signs the transaction. - #[cfg(TODO)] // TODO: TypedTransaction #[inline] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - self.sign_hash(&message.sighash()).await + async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { + let chain_id = self.chain_id(); + if let Some(chain_id) = chain_id { + tx.set_chain_id_checked(chain_id)?; + } + let mut sig = self.sign_hash(tx.signature_hash()).await?; + if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { + sig = sig.with_chain_id(chain_id); + } + Ok(sig) } /// Encodes and signs the typed data according to [EIP-712]. @@ -49,23 +103,23 @@ pub trait Signer: Send + Sync { where Self: Sized, { - self.sign_hash(&payload.eip712_signing_hash(domain)).await + self.sign_hash(payload.eip712_signing_hash(domain)).await } /// Returns the signer's Ethereum Address. fn address(&self) -> Address; /// Returns the signer's chain ID. - fn chain_id(&self) -> u64; + fn chain_id(&self) -> Option; /// Sets the signer's chain ID. - fn set_chain_id(&mut self, chain_id: u64); + fn set_chain_id(&mut self, chain_id: Option); /// Sets the signer's chain ID and returns `self`. #[inline] #[must_use] #[auto_impl(keep_default_for(&mut, Box))] - fn with_chain_id(mut self, chain_id: u64) -> Self + fn with_chain_id(mut self, chain_id: Option) -> Self where Self: Sized, { @@ -81,26 +135,42 @@ pub trait Signer: Send + Sync { /// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing /// methods directly. /// +/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay +/// protection. +/// +/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in +/// [`sign_transaction_sync`](Self::sign_transaction_sync), and to the resulting signature in all +/// the methods. If `chain_id` is None, [EIP-155] should not be applied. +/// /// Synchronous signers should also implement [`Signer`], as they are always able to by delegating /// the asynchronous methods to the synchronous ones. +/// +/// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[auto_impl(&, &mut, Box, Rc, Arc)] pub trait SignerSync { /// Signs the given hash. - fn sign_hash_sync(&self, hash: &B256) -> Result; + fn sign_hash_sync(&self, hash: B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] fn sign_message_sync(&self, message: &[u8]) -> Result { - self.sign_hash_sync(&eip191_hash_message(message)) + self.sign_hash_sync(eip191_hash_message(message)) } /// Signs the transaction. - #[cfg(TODO)] // TODO: TypedTransaction #[inline] - fn sign_transaction_sync(&self, message: &TypedTransaction) -> Result { - self.sign_hash_sync(&message.sighash()) + fn sign_transaction_sync(&self, tx: &mut SignableTx) -> Result { + let chain_id = self.chain_id_sync(); + if let Some(chain_id) = chain_id { + tx.set_chain_id_checked(chain_id)?; + } + let mut sig = self.sign_hash_sync(tx.signature_hash())?; + if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { + sig = sig.with_chain_id(chain_id); + } + Ok(sig) } /// Encodes and signs the typed data according to [EIP-712]. @@ -116,8 +186,11 @@ pub trait SignerSync { where Self: Sized, { - self.sign_hash_sync(&payload.eip712_signing_hash(domain)) + self.sign_hash_sync(payload.eip712_signing_hash(domain)) } + + /// Returns the signer's chain ID. + fn chain_id_sync(&self) -> Option; } #[cfg(test)] @@ -155,7 +228,7 @@ mod tests { async fn test_unsized_unimplemented_signer(s: &S) { assert_matches!( - s.sign_hash(&B256::ZERO).await, + s.sign_hash(B256::ZERO).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); @@ -164,13 +237,12 @@ mod tests { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - #[cfg(TODO)] // TODO: TypedTransaction - assert!(s.sign_transaction(&Default::default()).await.is_err()); + assert!(s.sign_transaction(&mut alloy_consensus::TxLegacy::default()).await.is_err()); } fn test_unsized_unimplemented_signer_sync(s: &S) { assert_matches!( - s.sign_hash_sync(&B256::ZERO), + s.sign_hash_sync(B256::ZERO), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); @@ -179,8 +251,7 @@ mod tests { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - #[cfg(TODO)] // TODO: TypedTransaction - assert!(s.sign_transaction_sync(&Default::default()).is_err()); + assert!(s.sign_transaction_sync(&mut alloy_consensus::TxLegacy::default()).is_err()); } struct UnimplementedSigner; @@ -188,27 +259,29 @@ mod tests { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for UnimplementedSigner { - async fn sign_hash(&self, _hash: &B256) -> Result { + async fn sign_hash(&self, _hash: B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } fn address(&self) -> Address { - unimplemented!() + Address::ZERO } - fn chain_id(&self) -> u64 { - unimplemented!() + fn chain_id(&self) -> Option { + None } - fn set_chain_id(&mut self, _chain_id: u64) { - unimplemented!() - } + fn set_chain_id(&mut self, _chain_id: Option) {} } impl SignerSync for UnimplementedSigner { - fn sign_hash_sync(&self, _hash: &B256) -> Result { + fn sign_hash_sync(&self, _hash: B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } + + fn chain_id_sync(&self) -> Option { + None + } } test_unimplemented_signer(&UnimplementedSigner).await; diff --git a/crates/signer/src/utils.rs b/crates/signer/src/utils.rs index 6dbed0d0ec0..542fe199055 100644 --- a/crates/signer/src/utils.rs +++ b/crates/signer/src/utils.rs @@ -7,12 +7,6 @@ use k256::{ AffinePoint, }; -/// Applies [EIP-155](https://eips.ethereum.org/EIPS/eip-155). -#[inline] -pub const fn to_eip155_v(v: u8, chain_id: u64) -> u64 { - (v as u64) + 35 + chain_id * 2 -} - /// Converts an ECDSA private key to its corresponding Ethereum Address. #[inline] pub fn secret_key_to_address(secret_key: &SigningKey) -> Address { diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index e07e12105cd..2157c344b50 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -162,8 +162,7 @@ impl MnemonicBuilder { let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref(); let signer = SigningKey::from_bytes(&key.to_bytes())?; let address = secret_key_to_address(&signer); - - Ok(Wallet:: { signer, address, chain_id: 1 }) + Ok(Wallet:: { signer, address, chain_id: None }) } } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 1bc067625e3..f03922ad60a 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -1,5 +1,5 @@ -use crate::{Result, Signature, Signer, SignerSync}; -use alloy_primitives::{Address, B256}; +use crate::{Result, Signer, SignerSync}; +use alloy_primitives::{Address, ChainId, Signature, B256}; use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; use std::fmt; @@ -35,7 +35,7 @@ mod yubi; /// /// // Optionally, the wallet's chain id can be set, in order to use EIP-155 /// // replay protection with different chains -/// let wallet = wallet.with_chain_id(1337u64); +/// let wallet = wallet.with_chain_id(Some(1337)); /// /// // The wallet can be used to sign messages /// let message = b"hello"; @@ -55,14 +55,14 @@ pub struct Wallet { /// The wallet's address. pub(crate) address: Address, /// The wallet's chain ID (for EIP-155). - pub(crate) chain_id: u64, + pub(crate) chain_id: Option, } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { #[inline] - async fn sign_hash(&self, hash: &B256) -> Result { + async fn sign_hash(&self, hash: B256) -> Result { self.sign_hash_sync(hash) } @@ -72,28 +72,37 @@ impl + Send + Sync> Signer for } #[inline] - fn chain_id(&self) -> u64 { + fn chain_id(&self) -> Option { self.chain_id } #[inline] - fn set_chain_id(&mut self, chain_id: u64) { + fn set_chain_id(&mut self, chain_id: Option) { self.chain_id = chain_id; } } impl> SignerSync for Wallet { #[inline] - fn sign_hash_sync(&self, hash: &B256) -> Result { + fn sign_hash_sync(&self, hash: B256) -> Result { let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; - Ok(Signature::new(recoverable_sig, recovery_id)) + let mut sig = Signature::from_signature_and_parity(recoverable_sig, recovery_id)?; + if let Some(chain_id) = self.chain_id { + sig = sig.with_chain_id(chain_id); + } + Ok(sig) + } + + #[inline] + fn chain_id_sync(&self) -> Option { + self.chain_id } } impl + Send + Sync> Wallet { /// Construct a new wallet with an external [`PrehashSigner`]. #[inline] - pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { + pub const fn new_with_signer(signer: D, address: Address, chain_id: Option) -> Self { Wallet { signer, address, chain_id } } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 6f5e22d3d0f..7391a9ee65f 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -41,7 +41,7 @@ impl Wallet { #[inline] fn new_pk(signer: SigningKey) -> Self { let address = secret_key_to_address(&signer); - Self { signer, address, chain_id: 1 } + Self::new_with_signer(signer, address, None) } } @@ -134,8 +134,9 @@ impl FromStr for Wallet { #[cfg(test)] mod tests { use super::*; - use crate::{LocalWallet, SignerSync}; - use alloy_primitives::address; + use crate::{LocalWallet, Result, SignableTx, Signer, SignerSync}; + use alloy_consensus::TxLegacy; + use alloy_primitives::{address, ChainId, Signature, U256}; #[cfg(feature = "keystore")] use {std::path::Path, tempfile::tempdir}; @@ -219,102 +220,83 @@ mod tests { assert_eq!(recovered, address); // if provided with a hash, it will skip hashing - let recovered2 = signature.recover_address_from_prehash(&hash).unwrap(); + let recovered2 = signature.recover_address_from_prehash(hash).unwrap(); assert_eq!(recovered2, address); } #[tokio::test] - #[cfg(TODO)] // TODO: TypedTransaction async fn signs_tx() { - // retrieved test vector from: - // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx: TypedTransaction = TransactionRequest { - from: None, - to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), - value: Some(1_000_000_000.into()), - gas: Some(2_000_000.into()), - nonce: Some(0.into()), - gas_price: Some(21_000_000_000u128.into()), - data: None, - chain_id: Some(U64::one()), + 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) } - .into(); - let wallet: Wallet = - "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); - let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64()); - let sig = wallet.sign_transaction(&tx).await.unwrap(); - let sighash = tx.sighash(); - sig.verify(sighash, wallet.address).unwrap(); - } + async fn sign_dyn_tx_test( + tx: &mut SignableTx, + chain_id: Option, + ) -> Result { + let mut wallet: Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + wallet.set_chain_id(chain_id); - #[tokio::test] - #[cfg(TODO)] // TODO: TypedTransaction - async fn signs_tx_empty_chain_id() { - // retrieved test vector from: - // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx: TypedTransaction = TransactionRequest { - from: None, - to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), - value: Some(1_000_000_000.into()), - gas: Some(2_000_000.into()), - nonce: Some(0.into()), - gas_price: Some(21_000_000_000u128.into()), - data: None, - chain_id: None, + 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) } - .into(); - let wallet: Wallet = - "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); - let wallet = wallet.with_chain_id(1u64); - - // this should populate the tx chain_id as the signer's chain_id (1) before signing - let sig = wallet.sign_transaction(&tx).await.unwrap(); - - // since we initialize with None we need to re-set the chain_id for the sighash to be - // correct - let mut tx = tx; - tx.set_chain_id(1); - let sighash = tx.sighash(); - sig.verify(sighash, wallet.address).unwrap(); - } - #[test] - #[cfg(TODO)] // TODO: TypedTransaction - fn signs_tx_empty_chain_id_sync() { - let chain_id = 1337u64; // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx: TypedTransaction = TransactionRequest { - from: None, - to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), - value: Some(1_000_000_000u64.into()), - gas: Some(2_000_000u64.into()), - nonce: Some(0u64.into()), - gas_price: Some(21_000_000_000u128.into()), - data: None, + let mut tx = TxLegacy { + to: alloy_consensus::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, - } - .into(); - let wallet: Wallet = - "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); - let wallet = wallet.with_chain_id(chain_id); - - // this should populate the tx chain_id as the signer's chain_id (1337) before signing and - // normalize the v - let sig = wallet.sign_transaction_sync(&tx).unwrap(); - - // ensure correct v given the chain - first extract recid - let recid = (sig.v - 35) % 2; - // eip155 check - assert_eq!(sig.v, chain_id * 2 + 35 + recid); - - // since we initialize with None we need to re-set the chain_id for the sighash to be - // correct - let mut tx = tx; - tx.set_chain_id(chain_id); - let sighash = tx.sighash(); - sig.verify(sighash, wallet.address).unwrap(); + }; + 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 = crate::Error::TransactionChainIdMismatch { signer: 1, tx: 2 }; + assert_eq!(error.to_string(), expected_error.to_string()); } #[test] @@ -346,7 +328,7 @@ mod tests { let foo_bar = FooBar { foo: I256::try_from(10u64).unwrap(), bar: U256::from(20u64), - fizz: b"fizz".into(), + fizz: b"fizz".to_vec(), buzz: keccak256("buzz"), far: String::from("space"), out: Address::ZERO, @@ -354,8 +336,8 @@ mod tests { let wallet = Wallet::random(); let hash = foo_bar.eip712_signing_hash(&domain); let sig = wallet.sign_typed_data_sync(&foo_bar, &domain).unwrap(); - assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), wallet.address()); - assert_eq!(wallet.sign_hash_sync(&hash).unwrap(), sig); + assert_eq!(sig.recover_address_from_prehash(hash).unwrap(), wallet.address()); + assert_eq!(wallet.sign_hash_sync(hash).unwrap(), sig); } #[test] diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index e430d27aaa6..6a02fe30f27 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -59,7 +59,7 @@ impl From> for Wallet> { let bytes = pubkey.as_bytes(); debug_assert_eq!(bytes[0], 0x04); let address = raw_public_key_to_address(&bytes[1..]); - Self::new_with_signer(signer, address, 1) + Self::new_with_signer(signer, address, None) } } diff --git a/crates/transport-ipc/Cargo.toml b/crates/transport-ipc/Cargo.toml index 0ee14eb4aba..ec99dbe0965 100644 --- a/crates/transport-ipc/Cargo.toml +++ b/crates/transport-ipc/Cargo.toml @@ -1,5 +1,7 @@ [package] name = "alloy-transport-ipc" +description = "IPC transport implementation" + version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,8 +11,6 @@ homepage.workspace = true repository.workspace = true exclude.workspace = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] alloy-json-rpc.workspace = true alloy-transport.workspace = true @@ -20,7 +20,7 @@ futures.workspace = true pin-project.workspace = true serde_json.workspace = true tokio.workspace = true -tokio-util = { workspace = true, features = ["io", "compat"]} +tokio-util = { workspace = true, features = ["io", "compat"] } tracing.workspace = true bytes = "1.5.0" diff --git a/crates/transport-ipc/src/lib.rs b/crates/transport-ipc/src/lib.rs index fa94430b20a..e73ad5a5910 100644 --- a/crates/transport-ipc/src/lib.rs +++ b/crates/transport-ipc/src/lib.rs @@ -15,6 +15,13 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use bytes::{Buf, BytesMut}; +use futures::{ready, AsyncRead, AsyncWriteExt, StreamExt}; +use interprocess::local_socket::{tokio::LocalSocketStream, ToLocalSocketName}; +use std::task::Poll::Ready; +use tokio::select; +use tokio_util::compat::FuturesAsyncReadCompatExt; + mod connect; pub use connect::IpcConnect; @@ -23,13 +30,6 @@ pub mod mock; #[cfg(feature = "mock")] pub use mock::MockIpcServer; -use bytes::{Buf, BytesMut}; -use futures::{ready, AsyncRead, AsyncWriteExt, StreamExt}; -use interprocess::local_socket::{tokio::LocalSocketStream, ToLocalSocketName}; -use std::task::Poll::Ready; -use tokio::select; -use tokio_util::compat::FuturesAsyncReadCompatExt; - type Result = std::result::Result; /// An IPC backend task. diff --git a/crates/transport/src/utils.rs b/crates/transport/src/utils.rs index 07727f0cd81..96cc56b45e0 100644 --- a/crates/transport/src/utils.rs +++ b/crates/transport/src/utils.rs @@ -1,3 +1,4 @@ +use crate::{TransportError, TransportResult}; use serde::Serialize; use serde_json::{ self, @@ -6,8 +7,6 @@ use serde_json::{ use std::future::Future; use url::Url; -use crate::{TransportError, TransportResult}; - /// Convert to a `Box` from a `Serialize` type, mapping the error /// to a `TransportError`. pub fn to_json_raw_value(s: &S) -> TransportResult>