diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 19d0326bc696..d3fa574e8119 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -24,4 +24,6 @@ mod receipt; pub use receipt::{Receipt, ReceiptEnvelope, ReceiptWithBloom}; mod transaction; -pub use transaction::{TxEip1559, TxEip2930, TxEnvelope, TxKind, TxLegacy, TxType}; +pub use transaction::{TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType}; + +pub use alloy_network::TxKind; diff --git a/crates/consensus/src/transaction/eip1559.rs b/crates/consensus/src/transaction/eip1559.rs index ad8372d066a6..42b34afcbfc1 100644 --- a/crates/consensus/src/transaction/eip1559.rs +++ b/crates/consensus/src/transaction/eip1559.rs @@ -1,4 +1,4 @@ -use crate::{ReceiptWithBloom, TxKind, TxType}; +use crate::{TxKind, TxType}; use alloy_eips::eip2930::AccessList; use alloy_network::{Signed, Transaction}; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; @@ -231,7 +231,7 @@ impl Decodable for TxEip1559 { impl Transaction for TxEip1559 { type Signature = Signature; - type Receipt = ReceiptWithBloom; + // type Receipt = ReceiptWithBloom; fn into_signed(self, signature: Signature) -> Signed { let mut buf = vec![]; @@ -274,6 +274,14 @@ impl Transaction for TxEip1559 { 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 } @@ -305,6 +313,14 @@ impl Transaction for TxEip1559 { 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"))] diff --git a/crates/consensus/src/transaction/eip2930.rs b/crates/consensus/src/transaction/eip2930.rs index 814ac0eb3d80..9726199d2d8c 100644 --- a/crates/consensus/src/transaction/eip2930.rs +++ b/crates/consensus/src/transaction/eip2930.rs @@ -1,4 +1,4 @@ -use crate::{transaction::TxKind, ReceiptWithBloom, TxType}; +use crate::{TxKind, TxType}; use alloy_eips::eip2930::AccessList; use alloy_network::{Signed, Transaction}; use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; @@ -192,7 +192,7 @@ impl Decodable for TxEip2930 { impl Transaction for TxEip2930 { type Signature = Signature; - type Receipt = ReceiptWithBloom; + // type Receipt = ReceiptWithBloom; fn into_signed(self, signature: Signature) -> Signed { let mut buf = vec![]; @@ -236,6 +236,14 @@ impl Transaction for TxEip2930 { 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 } @@ -267,13 +275,22 @@ impl Transaction for TxEip2930 { 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::{transaction::TxKind, TxEnvelope}; + use crate::{TxEnvelope, TxKind}; use alloy_network::{Signed, Transaction}; use alloy_primitives::{Address, Bytes, Signature, U256}; use alloy_rlp::{Decodable, Encodable}; diff --git a/crates/consensus/src/transaction/legacy.rs b/crates/consensus/src/transaction/legacy.rs index 4f32eb673c19..766ada86c4c6 100644 --- a/crates/consensus/src/transaction/legacy.rs +++ b/crates/consensus/src/transaction/legacy.rs @@ -1,4 +1,4 @@ -use crate::{transaction::TxKind, ReceiptWithBloom}; +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}; @@ -208,7 +208,7 @@ impl Decodable for TxLegacy { impl Transaction for TxLegacy { type Signature = Signature; - type Receipt = ReceiptWithBloom; + // type Receipt = ReceiptWithBloom; fn into_signed(self, signature: Signature) -> Signed { let mut buf = vec![]; @@ -250,6 +250,14 @@ impl Transaction for TxLegacy { 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 } @@ -281,6 +289,16 @@ impl Transaction for TxLegacy { 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)] @@ -288,7 +306,7 @@ mod tests { #[test] #[cfg(feature = "k256")] fn recover_signer_legacy() { - use crate::transaction::{TxKind, TxLegacy}; + use crate::{TxKind, TxLegacy}; use alloy_network::Transaction; use alloy_primitives::{b256, hex, Address, Signature, B256, U256}; diff --git a/crates/consensus/src/transaction/mod.rs b/crates/consensus/src/transaction/mod.rs index 4259d4383859..ec5b99d0c9d4 100644 --- a/crates/consensus/src/transaction/mod.rs +++ b/crates/consensus/src/transaction/mod.rs @@ -1,6 +1,3 @@ -mod common; -pub use common::TxKind; - mod eip1559; pub use eip1559::TxEip1559; diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs index 885067212530..c7042b550d0c 100644 --- a/crates/network/src/lib.rs +++ b/crates/network/src/lib.rs @@ -23,7 +23,7 @@ mod sealed; pub use sealed::{Sealable, Sealed}; mod transaction; -pub use transaction::{Eip1559Transaction, Signed, Transaction}; +pub use transaction::{Eip1559Transaction, Signed, Transaction, TxKind}; mod receipt; pub use receipt::Receipt; diff --git a/crates/consensus/src/transaction/common.rs b/crates/network/src/transaction/common.rs similarity index 97% rename from crates/consensus/src/transaction/common.rs rename to crates/network/src/transaction/common.rs index 802acb2f7c48..42bf4d894865 100644 --- a/crates/consensus/src/transaction/common.rs +++ b/crates/network/src/transaction/common.rs @@ -35,7 +35,7 @@ impl TxKind { /// Calculates a heuristic for the in-memory size of this object. #[inline] - pub(crate) const fn size(self) -> usize { + pub const fn size(self) -> usize { std::mem::size_of::() } } diff --git a/crates/network/src/transaction/mod.rs b/crates/network/src/transaction/mod.rs index a88e2d6abe60..66e0bdfc73be 100644 --- a/crates/network/src/transaction/mod.rs +++ b/crates/network/src/transaction/mod.rs @@ -1,12 +1,14 @@ -use crate::Receipt; use alloy_primitives::{Bytes, ChainId, Signature, B256, U256}; -use alloy_rlp::{BufMut, Decodable, Encodable}; +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: Encodable + Decodable + Send + Sync + 'static { +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 @@ -14,9 +16,6 @@ pub trait Transaction: Encodable + Decodable + Send + Sync + 'static { /// transaction signature is the unit type `()`. type Signature; - /// The receipt type for this transaction. - type Receipt: Receipt; - /// Convert to a signed transaction by adding a signature and computing the /// hash. fn into_signed(self, signature: Signature) -> Signed @@ -45,6 +44,11 @@ pub trait Transaction: Encodable + Decodable + Send + Sync + 'static { /// 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`. @@ -64,6 +68,11 @@ pub trait Transaction: Encodable + Decodable + Send + Sync + 'static { 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); } /// Captures getters and setters common across EIP-1559 transactions across all networks diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index 7be31ebef5ca..f1e73c8779b5 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 diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index ced843d825ff..da8103c5f57c 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, ChainId, 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}; use async_trait::async_trait; use std::fmt; use trezor_client::client::Trezor; @@ -35,6 +37,7 @@ 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 { + #[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) } #[inline] - #[cfg(TODO)] async fn sign_transaction(&self, tx: &mut dyn SignableTx) -> Result { - self.sign_tx(tx).await + // 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] @@ -108,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, @@ -143,59 +156,78 @@ impl TrezorSigner { Ok(address_str.parse()?) } - /// Signs an Ethereum transaction (requires confirmation on the Trezor) - #[cfg(TODO)] - async fn sign_tx(&self, tx: &mut dyn SignableTx) -> 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().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(); + + let tx_any = tx as &dyn std::any::Any; + let signature = if let Some(tx) = tx_any.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) + }?; + convert_signature(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_and_parity(r.into(), s.into(), signature.v).map_err(Into::into) + convert_signature(signature) } // helper which converts a derivation path to [u32] @@ -217,6 +249,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 convert_signature(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::*; diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index b2dd4639df9e..7ab3315b737f 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -5,7 +5,6 @@ use alloy_primitives::hex; use std::fmt; use thiserror::Error; -use trezor_client::client::AccessListItem as Trezor_AccessListItem; /// Trezor wallet type. #[derive(Clone, Debug)] @@ -47,101 +46,10 @@ pub enum TrezorError { /// version. #[error("Trezor Ethereum app requires at least version {0:?}")] UnsupportedFirmwareVersion(String), + /// 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 { - #[cfg(TODO)] - fn to_trimmed_big_endian(value: &U256) -> Vec { - let trimmed_value = B256::from(*value); - trimmed_value[value.leading_zeros() / 8..].to_vec() - } - - #[cfg(TODO)] - pub fn load(tx: &TypedTransaction) -> Result { - let to: String = match tx.to() { - TxKind::Call(address) => hex::encode_prefixed(address), - TxKind::Create => String::new(), - }; - - 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/src/signer.rs b/crates/signer/src/signer.rs index 15acc5eea35e..515af2d90d80 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -9,21 +9,15 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Trait for a signable transaction. /// -/// This trait allows us to generically sign transactions without knowing the -/// receipt type, and is blanket implemented for transactions which our signer -/// trait can produce signatures for. -pub trait SignableTx: Send + Sync + 'static { +/// This trait is implemented for all types that implement [`Transaction`] with [`Signature`] as the +/// signature associated type. +pub trait SignableTx: Transaction { /// Encode the transaction. - fn rlp_encode(&self) -> Vec; - - /// Calculate the signing hash for the transaction. - fn signature_hash(&self) -> B256; - - /// Returns the chain ID. Used for EIP-155 signing. - fn chain_id(&self) -> Option; - - /// Sets the chain ID. Used for EIP-155 signing. - fn set_chain_id(&mut self, chain_id: ChainId); + 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. @@ -45,25 +39,7 @@ pub trait SignableTx: Send + Sync + 'static { } } -impl> SignableTx for T { - fn rlp_encode(&self) -> Vec { - let mut out = Vec::new(); - self.encode(&mut out); - out - } - - fn signature_hash(&self) -> B256 { - Transaction::signature_hash(self) - } - - fn chain_id(&self) -> Option { - Transaction::chain_id(self) - } - - fn set_chain_id(&mut self, chain_id: ChainId) { - Transaction::set_chain_id(self, chain_id) - } -} +impl> SignableTx for T {} /// Asynchronous Ethereum signer. ///