├── rustfmt.toml ├── .arcconfig ├── README.md ├── .editorconfig ├── .gitignore ├── src ├── account.rs ├── log.rs ├── lib.rs ├── enveloped.rs ├── header.rs ├── transaction │ ├── eip1559.rs │ ├── signature.rs │ ├── eip2930.rs │ ├── legacy.rs │ ├── eip7702.rs │ └── mod.rs ├── util.rs ├── block.rs └── receipt.rs ├── Cargo.toml ├── .github └── workflows │ └── ci.yml └── LICENSE /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | merge_derives = false 3 | -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "phabricator.uri" : "https://source.that.world/" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ethereum-rs 2 | 3 | Apache-2 licensed common Ethereum structs shared by crates. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.rs] 4 | indent_style=tab 5 | indent_size=tab 6 | tab_width=4 7 | end_of_line=lf 8 | charset=utf-8 9 | trim_trailing_whitespace=true 10 | insert_final_newline=true 11 | 12 | [*.toml] 13 | indent_style=tab 14 | indent_size=tab 15 | tab_width=4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target 4 | 5 | # Generated by Nix 6 | **/result 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # IDEA files 16 | .idea/ -------------------------------------------------------------------------------- /src/account.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::{H256, U256}; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq)] 4 | #[derive(rlp::RlpEncodable, rlp::RlpDecodable)] 5 | #[cfg_attr( 6 | feature = "with-scale", 7 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 8 | )] 9 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 10 | pub struct Account { 11 | pub nonce: U256, 12 | pub balance: U256, 13 | pub storage_root: H256, 14 | pub code_hash: H256, 15 | } 16 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use ethereum_types::{H160, H256}; 4 | 5 | use crate::Bytes; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | #[derive(rlp::RlpEncodable, rlp::RlpDecodable)] 9 | #[cfg_attr( 10 | feature = "with-scale", 11 | derive( 12 | scale_codec::Encode, 13 | scale_codec::Decode, 14 | scale_codec::DecodeWithMemTracking, 15 | scale_info::TypeInfo 16 | ) 17 | )] 18 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 19 | pub struct Log { 20 | pub address: H160, 21 | pub topics: Vec, 22 | pub data: Bytes, 23 | } 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | extern crate alloc; 4 | 5 | mod account; 6 | mod block; 7 | mod enveloped; 8 | mod header; 9 | mod log; 10 | mod receipt; 11 | mod transaction; 12 | pub mod util; 13 | 14 | // Alias for `Vec`. This type alias is necessary for rlp-derive to work correctly. 15 | type Bytes = alloc::vec::Vec; 16 | 17 | pub use crate::account::Account; 18 | pub use crate::block::*; 19 | pub use crate::enveloped::*; 20 | pub use crate::header::{Header, PartialHeader}; 21 | pub use crate::log::Log; 22 | pub use crate::receipt::*; 23 | pub use crate::transaction::*; 24 | -------------------------------------------------------------------------------- /src/enveloped.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | 3 | /// DecoderError for typed transactions. 4 | #[derive(Clone, Debug, Eq, PartialEq)] 5 | pub enum EnvelopedDecoderError { 6 | UnknownTypeId, 7 | Payload(T), 8 | } 9 | 10 | impl From for EnvelopedDecoderError { 11 | fn from(e: T) -> Self { 12 | Self::Payload(e) 13 | } 14 | } 15 | 16 | /// Encodable typed transactions. 17 | pub trait EnvelopedEncodable { 18 | /// Convert self to an owned vector. 19 | fn encode(&self) -> BytesMut { 20 | let type_id = self.type_id(); 21 | 22 | let mut out = BytesMut::new(); 23 | if let Some(type_id) = type_id { 24 | assert!(type_id <= 0x7f); 25 | out.extend_from_slice(&[type_id]); 26 | } 27 | 28 | out.extend_from_slice(&self.encode_payload()[..]); 29 | out 30 | } 31 | 32 | /// Type Id of the transaction. 33 | fn type_id(&self) -> Option; 34 | 35 | /// Encode inner payload. 36 | fn encode_payload(&self) -> BytesMut; 37 | } 38 | 39 | /// Decodable typed transactions. 40 | pub trait EnvelopedDecodable: Sized { 41 | /// Inner payload decoder error. 42 | type PayloadDecoderError; 43 | 44 | /// Decode raw bytes to a Self type. 45 | fn decode(bytes: &[u8]) -> Result>; 46 | } 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ethereum" 3 | version = "0.18.2" 4 | license = "Apache-2.0" 5 | authors = ["Wei Tang "] 6 | description = "Core block and transaction types for Ethereum." 7 | repository = "https://github.com/rust-blockchain/ethereum" 8 | keywords = ["no_std", "ethereum"] 9 | edition = "2021" 10 | 11 | [dependencies] 12 | bytes = { version = "1.0", default-features = false } 13 | ethereum-types = { version = "0.15", default-features = false, features = ["rlp", "codec"] } 14 | hash-db = { version = "0.16", default-features = false } 15 | hash256-std-hasher = { version = "0.15", default-features = false } 16 | k256 = { version = "0.13", default-features = false, features = ["ecdsa", "sha256"] } 17 | rlp = { version = "0.6", default-features = false, features = ["derive"] } 18 | sha3 = { version = "0.10", default-features = false } 19 | trie-root = { version = "0.18", default-features = false } 20 | 21 | scale-codec = { package = "parity-scale-codec", version = "3.2", default-features = false, features = ["derive"], optional = true } 22 | scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } 23 | serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } 24 | 25 | [dev-dependencies] 26 | hash-db15 = { package = "hash-db", version = "0.15.2" } 27 | hex-literal = "0.4.1" 28 | rand = "0.8" 29 | triehash = "0.8.4" 30 | 31 | [features] 32 | default = ["std"] 33 | with-scale = ["scale-codec", "scale-info", "ethereum-types/codec"] 34 | with-serde = ["serde", "ethereum-types/serialize"] 35 | std = [ 36 | "bytes/std", 37 | "ethereum-types/std", 38 | "hash-db/std", 39 | "hash256-std-hasher/std", 40 | "k256/std", 41 | "rlp/std", 42 | "sha3/std", 43 | "trie-root/std", 44 | "scale-codec?/std", 45 | "scale-info?/std", 46 | "serde?/std", 47 | ] 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: 5 | - master 6 | 7 | name: CI 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: check 23 | 24 | build: 25 | name: Build 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | override: true 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: build 37 | args: --all --release --all-features 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: build 41 | args: --all --release --no-default-features 42 | 43 | test: 44 | name: Test Suite 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | profile: minimal 51 | toolchain: stable 52 | override: true 53 | - uses: actions-rs/cargo@v1 54 | with: 55 | command: test 56 | 57 | fmt: 58 | name: Rustfmt 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | - run: rustup component add rustfmt 68 | - uses: actions-rs/cargo@v1 69 | with: 70 | command: fmt 71 | args: --all -- --check 72 | 73 | clippy: 74 | name: Clippy 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v2 78 | - uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: stable 82 | override: true 83 | - run: rustup component add clippy 84 | - uses: actions-rs/cargo@v1 85 | with: 86 | command: clippy 87 | args: -- -D warnings 88 | -------------------------------------------------------------------------------- /src/header.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::{Bloom, H160, H256, H64, U256}; 2 | use sha3::{Digest, Keccak256}; 3 | 4 | use crate::Bytes; 5 | 6 | /// Ethereum header definition. 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | #[derive(rlp::RlpEncodable, rlp::RlpDecodable)] 9 | #[cfg_attr( 10 | feature = "with-scale", 11 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 12 | )] 13 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 14 | pub struct Header { 15 | pub parent_hash: H256, 16 | pub ommers_hash: H256, 17 | pub beneficiary: H160, 18 | pub state_root: H256, 19 | pub transactions_root: H256, 20 | pub receipts_root: H256, 21 | pub logs_bloom: Bloom, 22 | pub difficulty: U256, 23 | pub number: U256, 24 | pub gas_limit: U256, 25 | pub gas_used: U256, 26 | pub timestamp: u64, 27 | pub extra_data: Bytes, 28 | pub mix_hash: H256, 29 | pub nonce: H64, 30 | } 31 | 32 | impl Header { 33 | #[must_use] 34 | pub fn new(partial_header: PartialHeader, ommers_hash: H256, transactions_root: H256) -> Self { 35 | Self { 36 | parent_hash: partial_header.parent_hash, 37 | ommers_hash, 38 | beneficiary: partial_header.beneficiary, 39 | state_root: partial_header.state_root, 40 | transactions_root, 41 | receipts_root: partial_header.receipts_root, 42 | logs_bloom: partial_header.logs_bloom, 43 | difficulty: partial_header.difficulty, 44 | number: partial_header.number, 45 | gas_limit: partial_header.gas_limit, 46 | gas_used: partial_header.gas_used, 47 | timestamp: partial_header.timestamp, 48 | extra_data: partial_header.extra_data, 49 | mix_hash: partial_header.mix_hash, 50 | nonce: partial_header.nonce, 51 | } 52 | } 53 | 54 | #[must_use] 55 | pub fn hash(&self) -> H256 { 56 | H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) 57 | } 58 | } 59 | 60 | /// Partial header definition without ommers hash and transactions root. 61 | #[derive(Clone, Debug, PartialEq, Eq)] 62 | pub struct PartialHeader { 63 | pub parent_hash: H256, 64 | pub beneficiary: H160, 65 | pub state_root: H256, 66 | pub receipts_root: H256, 67 | pub logs_bloom: Bloom, 68 | pub difficulty: U256, 69 | pub number: U256, 70 | pub gas_limit: U256, 71 | pub gas_used: U256, 72 | pub timestamp: u64, 73 | pub extra_data: Bytes, 74 | pub mix_hash: H256, 75 | pub nonce: H64, 76 | } 77 | 78 | impl From
for PartialHeader { 79 | fn from(header: Header) -> PartialHeader { 80 | Self { 81 | parent_hash: header.parent_hash, 82 | beneficiary: header.beneficiary, 83 | state_root: header.state_root, 84 | receipts_root: header.receipts_root, 85 | logs_bloom: header.logs_bloom, 86 | difficulty: header.difficulty, 87 | number: header.number, 88 | gas_limit: header.gas_limit, 89 | gas_used: header.gas_used, 90 | timestamp: header.timestamp, 91 | extra_data: header.extra_data, 92 | mix_hash: header.mix_hash, 93 | nonce: header.nonce, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/transaction/eip1559.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::{H256, U256}; 2 | use rlp::{DecoderError, Rlp, RlpStream}; 3 | use sha3::{Digest, Keccak256}; 4 | 5 | use crate::Bytes; 6 | 7 | pub use super::eip2930::{AccessList, TransactionAction, TransactionSignature}; 8 | 9 | #[derive(Clone, Debug, PartialEq, Eq)] 10 | #[cfg_attr( 11 | feature = "with-scale", 12 | derive( 13 | scale_codec::Encode, 14 | scale_codec::Decode, 15 | scale_codec::DecodeWithMemTracking, 16 | scale_info::TypeInfo 17 | ) 18 | )] 19 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 20 | pub struct EIP1559Transaction { 21 | pub chain_id: u64, 22 | pub nonce: U256, 23 | pub max_priority_fee_per_gas: U256, 24 | pub max_fee_per_gas: U256, 25 | pub gas_limit: U256, 26 | pub action: TransactionAction, 27 | pub value: U256, 28 | pub input: Bytes, 29 | pub access_list: AccessList, 30 | pub signature: TransactionSignature, 31 | } 32 | 33 | impl EIP1559Transaction { 34 | pub fn hash(&self) -> H256 { 35 | let encoded = rlp::encode(self); 36 | let mut out = alloc::vec![0; 1 + encoded.len()]; 37 | out[0] = 2; 38 | out[1..].copy_from_slice(&encoded); 39 | H256::from_slice(Keccak256::digest(&out).as_ref()) 40 | } 41 | 42 | pub fn to_message(self) -> EIP1559TransactionMessage { 43 | EIP1559TransactionMessage { 44 | chain_id: self.chain_id, 45 | nonce: self.nonce, 46 | max_priority_fee_per_gas: self.max_priority_fee_per_gas, 47 | max_fee_per_gas: self.max_fee_per_gas, 48 | gas_limit: self.gas_limit, 49 | action: self.action, 50 | value: self.value, 51 | input: self.input, 52 | access_list: self.access_list, 53 | } 54 | } 55 | } 56 | 57 | impl rlp::Encodable for EIP1559Transaction { 58 | fn rlp_append(&self, s: &mut RlpStream) { 59 | s.begin_list(12); 60 | s.append(&self.chain_id); 61 | s.append(&self.nonce); 62 | s.append(&self.max_priority_fee_per_gas); 63 | s.append(&self.max_fee_per_gas); 64 | s.append(&self.gas_limit); 65 | s.append(&self.action); 66 | s.append(&self.value); 67 | s.append(&self.input); 68 | s.append_list(&self.access_list); 69 | s.append(&self.signature.odd_y_parity()); 70 | s.append(&U256::from_big_endian(&self.signature.r()[..])); 71 | s.append(&U256::from_big_endian(&self.signature.s()[..])); 72 | } 73 | } 74 | 75 | impl rlp::Decodable for EIP1559Transaction { 76 | fn decode(rlp: &Rlp) -> Result { 77 | if rlp.item_count()? != 12 { 78 | return Err(DecoderError::RlpIncorrectListLen); 79 | } 80 | 81 | Ok(Self { 82 | chain_id: rlp.val_at(0)?, 83 | nonce: rlp.val_at(1)?, 84 | max_priority_fee_per_gas: rlp.val_at(2)?, 85 | max_fee_per_gas: rlp.val_at(3)?, 86 | gas_limit: rlp.val_at(4)?, 87 | action: rlp.val_at(5)?, 88 | value: rlp.val_at(6)?, 89 | input: rlp.val_at(7)?, 90 | access_list: rlp.list_at(8)?, 91 | signature: { 92 | let odd_y_parity = rlp.val_at(9)?; 93 | let r = H256::from(rlp.val_at::(10)?.to_big_endian()); 94 | let s = H256::from(rlp.val_at::(11)?.to_big_endian()); 95 | TransactionSignature::new(odd_y_parity, r, s) 96 | .ok_or(DecoderError::Custom("Invalid transaction signature format"))? 97 | }, 98 | }) 99 | } 100 | } 101 | 102 | #[derive(Clone, Debug, PartialEq, Eq)] 103 | pub struct EIP1559TransactionMessage { 104 | pub chain_id: u64, 105 | pub nonce: U256, 106 | pub max_priority_fee_per_gas: U256, 107 | pub max_fee_per_gas: U256, 108 | pub gas_limit: U256, 109 | pub action: TransactionAction, 110 | pub value: U256, 111 | pub input: Bytes, 112 | pub access_list: AccessList, 113 | } 114 | 115 | impl EIP1559TransactionMessage { 116 | pub fn hash(&self) -> H256 { 117 | let encoded = rlp::encode(self); 118 | let mut out = alloc::vec![0; 1 + encoded.len()]; 119 | out[0] = 2; 120 | out[1..].copy_from_slice(&encoded); 121 | H256::from_slice(Keccak256::digest(&out).as_ref()) 122 | } 123 | } 124 | 125 | impl rlp::Encodable for EIP1559TransactionMessage { 126 | fn rlp_append(&self, s: &mut RlpStream) { 127 | s.begin_list(9); 128 | s.append(&self.chain_id); 129 | s.append(&self.nonce); 130 | s.append(&self.max_priority_fee_per_gas); 131 | s.append(&self.max_fee_per_gas); 132 | s.append(&self.gas_limit); 133 | s.append(&self.action); 134 | s.append(&self.value); 135 | s.append(&self.input); 136 | s.append_list(&self.access_list); 137 | } 138 | } 139 | 140 | impl From for EIP1559TransactionMessage { 141 | fn from(t: EIP1559Transaction) -> Self { 142 | t.to_message() 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/transaction/signature.rs: -------------------------------------------------------------------------------- 1 | use ethereum_types::H256; 2 | 3 | // ECDSA signature validation constants for secp256k1 curve 4 | 5 | /// Minimum valid value for signature components r and s (must be >= 1) 6 | pub const SIGNATURE_LOWER_BOUND: H256 = H256([ 7 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 8 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 9 | ]); 10 | 11 | /// Maximum valid value for signature components r and s (must be < secp256k1 curve order) 12 | /// This is the secp256k1 curve order: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 13 | pub const SIGNATURE_UPPER_BOUND: H256 = H256([ 14 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 15 | 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41, 16 | ]); 17 | 18 | /// Maximum value for low-s signature enforcement (half of curve order) 19 | /// This is used to prevent signature malleability 20 | /// Value: 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 21 | pub const SIGNATURE_LOW_S_BOUND: H256 = H256([ 22 | 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 23 | 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, 0x1b, 0x20, 0xa0, 24 | ]); 25 | 26 | /// Validates that a signature component (r or s) is within valid range 27 | /// 28 | /// A valid signature component must satisfy: 29 | /// - Greater than or equal to 1 (SIGNATURE_LOWER_BOUND) 30 | /// - Less than the secp256k1 curve order (SIGNATURE_UPPER_BOUND) 31 | #[inline] 32 | pub fn is_valid_signature_component(component: &H256) -> bool { 33 | *component >= SIGNATURE_LOWER_BOUND && *component < SIGNATURE_UPPER_BOUND 34 | } 35 | 36 | /// Checks if the s component satisfies the low-s requirement 37 | /// 38 | /// The low-s requirement helps prevent signature malleability by requiring 39 | /// that s <= n/2 where n is the curve order. 40 | #[inline] 41 | pub fn is_low_s(s: &H256) -> bool { 42 | *s <= SIGNATURE_LOW_S_BOUND 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | use ethereum_types::U256; 49 | 50 | /// Helper function to convert H256 to U256 for arithmetic operations in tests 51 | #[inline] 52 | fn h256_to_u256(h: &H256) -> U256 { 53 | U256::from_big_endian(h.as_bytes()) 54 | } 55 | 56 | /// Helper function to convert U256 to H256 57 | #[inline] 58 | fn u256_to_h256(u: U256) -> H256 { 59 | H256::from(u.to_big_endian()) 60 | } 61 | 62 | #[test] 63 | fn test_low_s_bound_is_half_curve_order() { 64 | // SIGNATURE_LOW_S_BOUND should be exactly n/2 where n is the curve order 65 | let n = h256_to_u256(&SIGNATURE_UPPER_BOUND); 66 | let expected_half_n = u256_to_h256(n / 2); 67 | 68 | assert_eq!( 69 | SIGNATURE_LOW_S_BOUND, expected_half_n, 70 | "SIGNATURE_LOW_S_BOUND must be exactly half of the curve order" 71 | ); 72 | } 73 | 74 | #[test] 75 | fn test_signature_bounds() { 76 | // Lower bound is 1 77 | assert_eq!(SIGNATURE_LOWER_BOUND, H256::from_low_u64_be(1)); 78 | 79 | // Verify that 0 is invalid 80 | assert!(!is_valid_signature_component(&H256::zero())); 81 | 82 | // Verify that 1 is valid (minimum) 83 | assert!(is_valid_signature_component(&H256::from_low_u64_be(1))); 84 | 85 | // Verify that curve_order - 1 is valid (maximum) 86 | let max_valid = u256_to_h256(h256_to_u256(&SIGNATURE_UPPER_BOUND) - 1); 87 | assert!(is_valid_signature_component(&max_valid)); 88 | 89 | // Verify that curve_order itself is invalid 90 | assert!(!is_valid_signature_component(&SIGNATURE_UPPER_BOUND)); 91 | 92 | // Verify that values above curve_order are invalid 93 | let above_max = u256_to_h256(h256_to_u256(&SIGNATURE_UPPER_BOUND) + 1); 94 | assert!(!is_valid_signature_component(&above_max)); 95 | } 96 | 97 | #[test] 98 | fn test_low_s_validation() { 99 | // s = 0 is invalid (below lower bound) 100 | assert!(!is_valid_signature_component(&H256::zero())); 101 | 102 | // s = 1 satisfies low-s requirement 103 | assert!(is_low_s(&u256_to_h256(U256::one()))); 104 | 105 | // s = low_s_bound satisfies low-s requirement (boundary) 106 | assert!(is_low_s(&SIGNATURE_LOW_S_BOUND)); 107 | 108 | // s = low_s_bound + 1 does NOT satisfy low-s requirement 109 | let above_low_s = u256_to_h256(h256_to_u256(&SIGNATURE_LOW_S_BOUND) + 1); 110 | assert!(!is_low_s(&above_low_s)); 111 | 112 | // s = curve_order - 1 is valid but does NOT satisfy low-s 113 | let high_s = u256_to_h256(h256_to_u256(&SIGNATURE_UPPER_BOUND) - 1); 114 | assert!(is_valid_signature_component(&high_s)); 115 | assert!(!is_low_s(&high_s)); 116 | } 117 | 118 | #[test] 119 | fn test_boundary_conditions() { 120 | // Test exact boundary values 121 | assert_eq!(h256_to_u256(&SIGNATURE_LOWER_BOUND), U256::one()); 122 | 123 | // Ensure low-s bound is exactly half the curve order (curve_order / 2) 124 | // Note: The curve order is odd, so half_order * 2 + 1 = curve_order 125 | let curve_order = h256_to_u256(&SIGNATURE_UPPER_BOUND); 126 | let half_order = h256_to_u256(&SIGNATURE_LOW_S_BOUND); 127 | assert_eq!(curve_order / 2, half_order); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for Ethereum. 2 | 3 | use alloc::vec::Vec; 4 | 5 | use ethereum_types::H256; 6 | use hash256_std_hasher::Hash256StdHasher; 7 | use hash_db::Hasher; 8 | use sha3::{Digest, Keccak256}; 9 | use trie_root::Value as TrieStreamValue; 10 | 11 | /// Concrete `Hasher` impl for the Keccak-256 hash 12 | #[derive(Default, Debug, Clone, PartialEq, Eq)] 13 | pub struct KeccakHasher; 14 | impl Hasher for KeccakHasher { 15 | type Out = H256; 16 | type StdHasher = Hash256StdHasher; 17 | const LENGTH: usize = 32; 18 | 19 | fn hash(x: &[u8]) -> Self::Out { 20 | H256::from_slice(Keccak256::digest(x).as_ref()) 21 | } 22 | } 23 | 24 | /// Concrete `TrieStream` impl for the ethereum trie. 25 | #[derive(Default)] 26 | pub struct Hash256RlpTrieStream { 27 | stream: rlp::RlpStream, 28 | } 29 | 30 | impl trie_root::TrieStream for Hash256RlpTrieStream { 31 | fn new() -> Self { 32 | Self { 33 | stream: rlp::RlpStream::new(), 34 | } 35 | } 36 | 37 | fn append_empty_data(&mut self) { 38 | self.stream.append_empty_data(); 39 | } 40 | 41 | fn begin_branch( 42 | &mut self, 43 | _maybe_key: Option<&[u8]>, 44 | _maybe_value: Option, 45 | _has_children: impl Iterator, 46 | ) { 47 | // an item for every possible nibble/suffix 48 | // + 1 for data 49 | self.stream.begin_list(17); 50 | } 51 | 52 | fn append_empty_child(&mut self) { 53 | self.stream.append_empty_data(); 54 | } 55 | 56 | fn end_branch(&mut self, value: Option) { 57 | match value { 58 | Some(value) => match value { 59 | TrieStreamValue::Inline(value) => self.stream.append(&value), 60 | TrieStreamValue::Node(value) => self.stream.append(&value), 61 | }, 62 | None => self.stream.append_empty_data(), 63 | }; 64 | } 65 | 66 | fn append_leaf(&mut self, key: &[u8], value: TrieStreamValue) { 67 | self.stream.begin_list(2); 68 | self.stream.append_iter(hex_prefix_encode(key, true)); 69 | match value { 70 | TrieStreamValue::Inline(value) => self.stream.append(&value), 71 | TrieStreamValue::Node(value) => self.stream.append(&value), 72 | }; 73 | } 74 | 75 | fn append_extension(&mut self, key: &[u8]) { 76 | self.stream.begin_list(2); 77 | self.stream.append_iter(hex_prefix_encode(key, false)); 78 | } 79 | 80 | fn append_substream(&mut self, other: Self) { 81 | let out = other.out(); 82 | match out.len() { 83 | 0..=31 => self.stream.append_raw(&out, 1), 84 | _ => self.stream.append(&H::hash(&out).as_ref()), 85 | }; 86 | } 87 | 88 | fn out(self) -> Vec { 89 | self.stream.out().freeze().into() 90 | } 91 | } 92 | 93 | // Copy from `triehash` crate. 94 | /// Hex-prefix Notation. First nibble has flags: oddness = 2^0 & termination = 2^1. 95 | /// 96 | /// The "termination marker" and "leaf-node" specifier are completely equivalent. 97 | /// 98 | /// Input values are in range `[0, 0xf]`. 99 | /// 100 | /// ```markdown 101 | /// [0,0,1,2,3,4,5] 0x10012345 // 7 > 4 102 | /// [0,1,2,3,4,5] 0x00012345 // 6 > 4 103 | /// [1,2,3,4,5] 0x112345 // 5 > 3 104 | /// [0,0,1,2,3,4] 0x00001234 // 6 > 3 105 | /// [0,1,2,3,4] 0x101234 // 5 > 3 106 | /// [1,2,3,4] 0x001234 // 4 > 3 107 | /// [0,0,1,2,3,4,5,T] 0x30012345 // 7 > 4 108 | /// [0,0,1,2,3,4,T] 0x20001234 // 6 > 4 109 | /// [0,1,2,3,4,5,T] 0x20012345 // 6 > 4 110 | /// [1,2,3,4,5,T] 0x312345 // 5 > 3 111 | /// [1,2,3,4,T] 0x201234 // 4 > 3 112 | /// ``` 113 | fn hex_prefix_encode(nibbles: &[u8], leaf: bool) -> impl Iterator + '_ { 114 | let inlen = nibbles.len(); 115 | let oddness_factor = inlen % 2; 116 | 117 | let first_byte = { 118 | let mut bits = ((inlen as u8 & 1) + (2 * leaf as u8)) << 4; 119 | if oddness_factor == 1 { 120 | bits += nibbles[0]; 121 | } 122 | bits 123 | }; 124 | core::iter::once(first_byte).chain( 125 | nibbles[oddness_factor..] 126 | .chunks(2) 127 | .map(|ch| ch[0] << 4 | ch[1]), 128 | ) 129 | } 130 | 131 | /// Generates a trie root hash for a vector of key-value tuples 132 | pub fn trie_root(input: I) -> H256 133 | where 134 | I: IntoIterator, 135 | K: AsRef<[u8]> + Ord, 136 | V: AsRef<[u8]>, 137 | { 138 | trie_root::trie_root::(input, None) 139 | } 140 | 141 | /// Generates a key-hashed (secure) trie root hash for a vector of key-value tuples. 142 | pub fn sec_trie_root(input: I) -> H256 143 | where 144 | I: IntoIterator, 145 | K: AsRef<[u8]>, 146 | V: AsRef<[u8]>, 147 | { 148 | trie_root::sec_trie_root::(input, None) 149 | } 150 | 151 | /// Generates a trie root hash for a vector of values 152 | pub fn ordered_trie_root(input: I) -> H256 153 | where 154 | I: IntoIterator, 155 | V: AsRef<[u8]>, 156 | { 157 | trie_root::trie_root::( 158 | input 159 | .into_iter() 160 | .enumerate() 161 | .map(|(i, v)| (rlp::encode(&i), v)), 162 | None, 163 | ) 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use ethereum_types::H256; 169 | use hash256_std_hasher::Hash256StdHasher; 170 | use hex_literal::hex; 171 | use sha3::{Digest, Keccak256}; 172 | 173 | #[derive(Default, Debug, Clone, PartialEq, Eq)] 174 | struct KeccakHasher15; 175 | impl hash_db15::Hasher for KeccakHasher15 { 176 | type Out = H256; 177 | type StdHasher = Hash256StdHasher; 178 | const LENGTH: usize = 32; 179 | 180 | fn hash(x: &[u8]) -> Self::Out { 181 | H256::from_slice(Keccak256::digest(x).as_ref()) 182 | } 183 | } 184 | 185 | #[test] 186 | fn test_trie_root() { 187 | let v = vec![ 188 | ("doe", "reindeer"), 189 | ("dog", "puppy"), 190 | ("dogglesworth", "cat"), 191 | ]; 192 | let root = hex!("8aad789dff2f538bca5d8ea56e8abe10f4c7ba3a5dea95fea4cd6e7c3a1168d3"); 193 | 194 | let before = triehash::trie_root::(v.clone()); 195 | assert_eq!(before.0, root); 196 | 197 | let after = super::trie_root::<_, _, _>(v); 198 | assert_eq!(after.0, root); 199 | } 200 | 201 | #[test] 202 | fn test_sec_trie_root() { 203 | let v = vec![ 204 | ("doe", "reindeer"), 205 | ("dog", "puppy"), 206 | ("dogglesworth", "cat"), 207 | ]; 208 | let root = hex!("d4cd937e4a4368d7931a9cf51686b7e10abb3dce38a39000fd7902a092b64585"); 209 | 210 | let before = triehash::sec_trie_root::(v.clone()); 211 | assert_eq!(before.0, root); 212 | 213 | let after = super::sec_trie_root::<_, _, _>(v); 214 | assert_eq!(after.0, root); 215 | } 216 | 217 | #[test] 218 | fn test_ordered_trie_root() { 219 | let v = &["doe", "reindeer"]; 220 | let root = hex!("e766d5d51b89dc39d981b41bda63248d7abce4f0225eefd023792a540bcffee3"); 221 | 222 | let before = triehash::ordered_trie_root::(v); 223 | assert_eq!(before.0, root); 224 | 225 | let after = super::ordered_trie_root::<_, _>(v); 226 | assert_eq!(after.0, root); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/block.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use ethereum_types::H256; 4 | use rlp::{DecoderError, Rlp, RlpStream}; 5 | use sha3::{Digest, Keccak256}; 6 | 7 | use crate::{ 8 | enveloped::{EnvelopedDecodable, EnvelopedEncodable}, 9 | header::{Header, PartialHeader}, 10 | transaction::{TransactionAny, TransactionV0, TransactionV1, TransactionV2, TransactionV3}, 11 | util::ordered_trie_root, 12 | }; 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | #[cfg_attr( 16 | feature = "with-scale", 17 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 18 | )] 19 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 20 | pub struct Block { 21 | pub header: Header, 22 | pub transactions: Vec, 23 | pub ommers: Vec
, 24 | } 25 | 26 | impl rlp::Encodable for Block { 27 | fn rlp_append(&self, s: &mut RlpStream) { 28 | s.begin_list(3); 29 | s.append(&self.header); 30 | s.append_list::, _>( 31 | &self 32 | .transactions 33 | .iter() 34 | .map(|tx| EnvelopedEncodable::encode(tx).to_vec()) 35 | .collect::>(), 36 | ); 37 | s.append_list(&self.ommers); 38 | } 39 | } 40 | 41 | impl rlp::Decodable for Block { 42 | fn decode(rlp: &Rlp) -> Result { 43 | Ok(Self { 44 | header: rlp.val_at(0)?, 45 | transactions: rlp 46 | .list_at::>(1)? 47 | .into_iter() 48 | .map(|raw_tx| { 49 | EnvelopedDecodable::decode(&raw_tx) 50 | .map_err(|_| DecoderError::Custom("decode enveloped transaction failed")) 51 | }) 52 | .collect::, _>>()?, 53 | ommers: rlp.list_at(2)?, 54 | }) 55 | } 56 | } 57 | 58 | impl Block { 59 | pub fn new(partial_header: PartialHeader, transactions: Vec, ommers: Vec
) -> Self { 60 | let ommers_hash = 61 | H256::from_slice(Keccak256::digest(&rlp::encode_list(&ommers)[..]).as_ref()); 62 | let transactions_root = ordered_trie_root( 63 | transactions 64 | .iter() 65 | .map(|r| EnvelopedEncodable::encode(r).freeze()), 66 | ); 67 | 68 | Self { 69 | header: Header::new(partial_header, ommers_hash, transactions_root), 70 | transactions, 71 | ommers, 72 | } 73 | } 74 | } 75 | 76 | pub type BlockV0 = Block; 77 | pub type BlockV1 = Block; 78 | pub type BlockV2 = Block; 79 | pub type BlockV3 = Block; 80 | pub type BlockAny = Block; 81 | 82 | impl From for Block 83 | where 84 | T: From + From, 85 | { 86 | fn from(t: BlockV0) -> Self { 87 | Self { 88 | header: t.header, 89 | transactions: t.transactions.into_iter().map(|t| t.into()).collect(), 90 | ommers: t.ommers, 91 | } 92 | } 93 | } 94 | 95 | impl From for BlockV2 { 96 | fn from(t: BlockV1) -> Self { 97 | Self { 98 | header: t.header, 99 | transactions: t.transactions.into_iter().map(|t| t.into()).collect(), 100 | ommers: t.ommers, 101 | } 102 | } 103 | } 104 | 105 | impl From for BlockV3 { 106 | fn from(t: BlockV2) -> Self { 107 | Self { 108 | header: t.header, 109 | transactions: t.transactions.into_iter().map(|t| t.into()).collect(), 110 | ommers: t.ommers, 111 | } 112 | } 113 | } 114 | 115 | impl From for BlockV3 { 116 | fn from(t: BlockV1) -> Self { 117 | Self { 118 | header: t.header, 119 | transactions: t.transactions.into_iter().map(|t| t.into()).collect(), 120 | ommers: t.ommers, 121 | } 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | use crate::transaction::{ 129 | eip2930, eip7702::AuthorizationListItem, legacy::TransactionAction, EIP7702Transaction, 130 | TransactionV3, 131 | }; 132 | use ethereum_types::{H160, H256, U256}; 133 | 134 | const ONE: H256 = H256([ 135 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136 | 0, 1, 137 | ]); 138 | 139 | #[test] 140 | fn block_v3_with_eip7702_transaction() { 141 | // Create an EIP-7702 transaction 142 | let eip7702_tx = TransactionV3::EIP7702(EIP7702Transaction { 143 | chain_id: 1, 144 | nonce: U256::from(1), 145 | max_priority_fee_per_gas: U256::from(1_000_000_000), 146 | max_fee_per_gas: U256::from(2_000_000_000), 147 | gas_limit: U256::from(21000), 148 | destination: TransactionAction::Call(H160::zero()), 149 | value: U256::zero(), 150 | data: vec![], 151 | access_list: vec![], 152 | authorization_list: vec![AuthorizationListItem { 153 | chain_id: 1, 154 | address: H160::zero(), 155 | nonce: U256::zero(), 156 | signature: eip2930::MalleableTransactionSignature { 157 | odd_y_parity: false, 158 | r: ONE, 159 | s: ONE, 160 | }, 161 | }], 162 | signature: eip2930::TransactionSignature::new(false, ONE, ONE).unwrap(), 163 | }); 164 | 165 | // Create a block with the EIP-7702 transaction 166 | let partial_header = PartialHeader { 167 | parent_hash: H256::zero(), 168 | beneficiary: H160::zero(), 169 | state_root: H256::zero(), 170 | receipts_root: H256::zero(), 171 | logs_bloom: ethereum_types::Bloom::zero(), 172 | difficulty: U256::zero(), 173 | number: U256::zero(), 174 | gas_limit: U256::from(1_000_000), 175 | gas_used: U256::zero(), 176 | timestamp: 0, 177 | extra_data: vec![], 178 | mix_hash: H256::zero(), 179 | nonce: ethereum_types::H64::zero(), 180 | }; 181 | 182 | let block = BlockV3::new(partial_header, vec![eip7702_tx.clone()], vec![]); 183 | 184 | // Verify the block can be encoded and decoded 185 | let encoded = rlp::encode(&block); 186 | let decoded: BlockV3 = rlp::decode(&encoded).unwrap(); 187 | 188 | assert_eq!(block, decoded); 189 | assert_eq!(decoded.transactions.len(), 1); 190 | 191 | // Verify the transaction is preserved correctly 192 | match &decoded.transactions[0] { 193 | TransactionV3::EIP7702(tx) => { 194 | assert_eq!(tx.chain_id, 1); 195 | assert_eq!(tx.authorization_list.len(), 1); 196 | } 197 | _ => panic!("Expected EIP7702 transaction"), 198 | } 199 | } 200 | 201 | #[test] 202 | fn block_v2_to_v3_conversion() { 203 | use crate::transaction::{EIP1559Transaction, TransactionV2}; 204 | 205 | // Create a BlockV2 with EIP1559 transaction 206 | let eip1559_tx = TransactionV2::EIP1559(EIP1559Transaction { 207 | chain_id: 1, 208 | nonce: U256::from(1), 209 | max_priority_fee_per_gas: U256::from(1_000_000_000), 210 | max_fee_per_gas: U256::from(2_000_000_000), 211 | gas_limit: U256::from(21000), 212 | action: TransactionAction::Call(H160::zero()), 213 | value: U256::zero(), 214 | input: vec![], 215 | access_list: vec![], 216 | signature: eip2930::TransactionSignature::new(false, ONE, ONE).unwrap(), 217 | }); 218 | 219 | let partial_header = PartialHeader { 220 | parent_hash: H256::zero(), 221 | beneficiary: H160::zero(), 222 | state_root: H256::zero(), 223 | receipts_root: H256::zero(), 224 | logs_bloom: ethereum_types::Bloom::zero(), 225 | difficulty: U256::zero(), 226 | number: U256::zero(), 227 | gas_limit: U256::from(1_000_000), 228 | gas_used: U256::zero(), 229 | timestamp: 0, 230 | extra_data: vec![], 231 | mix_hash: H256::zero(), 232 | nonce: ethereum_types::H64::zero(), 233 | }; 234 | 235 | let block_v2 = BlockV2::new(partial_header, vec![eip1559_tx], vec![]); 236 | let block_v3: BlockV3 = block_v2.into(); 237 | 238 | // Verify conversion worked 239 | assert_eq!(block_v3.transactions.len(), 1); 240 | match &block_v3.transactions[0] { 241 | TransactionV3::EIP1559(_) => {} // Expected 242 | _ => panic!("Expected EIP1559 transaction in V3"), 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/transaction/eip2930.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use ethereum_types::{Address, H256, U256}; 4 | use rlp::{DecoderError, Rlp, RlpStream}; 5 | use sha3::{Digest, Keccak256}; 6 | 7 | use super::signature; 8 | use crate::Bytes; 9 | 10 | pub use super::legacy::TransactionAction; 11 | 12 | #[derive(Clone, Debug, PartialEq, Eq)] 13 | #[cfg_attr( 14 | feature = "with-scale", 15 | derive( 16 | scale_info::TypeInfo, 17 | scale_codec::Encode, 18 | scale_codec::Decode, 19 | scale_codec::DecodeWithMemTracking 20 | ) 21 | )] 22 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 23 | pub struct MalleableTransactionSignature { 24 | pub odd_y_parity: bool, 25 | pub r: H256, 26 | pub s: H256, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, Eq)] 30 | #[cfg_attr( 31 | feature = "with-scale", 32 | derive( 33 | scale_info::TypeInfo, 34 | scale_codec::Encode, 35 | scale_codec::DecodeWithMemTracking 36 | ) 37 | )] 38 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize))] 39 | pub struct TransactionSignature { 40 | odd_y_parity: bool, 41 | r: H256, 42 | s: H256, 43 | } 44 | 45 | impl TransactionSignature { 46 | #[must_use] 47 | pub fn new(odd_y_parity: bool, r: H256, s: H256) -> Option { 48 | let is_valid = signature::is_valid_signature_component(&r) 49 | && signature::is_valid_signature_component(&s); 50 | 51 | if is_valid { 52 | Some(Self { odd_y_parity, r, s }) 53 | } else { 54 | None 55 | } 56 | } 57 | 58 | #[must_use] 59 | pub fn odd_y_parity(&self) -> bool { 60 | self.odd_y_parity 61 | } 62 | 63 | #[must_use] 64 | pub fn r(&self) -> &H256 { 65 | &self.r 66 | } 67 | 68 | #[must_use] 69 | pub fn s(&self) -> &H256 { 70 | &self.s 71 | } 72 | 73 | #[must_use] 74 | pub fn is_low_s(&self) -> bool { 75 | signature::is_low_s(&self.s) 76 | } 77 | } 78 | 79 | #[cfg(feature = "with-scale")] 80 | impl scale_codec::Decode for TransactionSignature { 81 | fn decode(value: &mut I) -> Result { 82 | let unchecked = MalleableTransactionSignature::decode(value)?; 83 | match Self::new(unchecked.odd_y_parity, unchecked.r, unchecked.s) { 84 | Some(signature) => Ok(signature), 85 | None => Err(scale_codec::Error::from("Invalid signature")), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(feature = "with-serde")] 91 | impl<'de> serde::Deserialize<'de> for TransactionSignature { 92 | fn deserialize(deserializer: D) -> Result 93 | where 94 | D: serde::de::Deserializer<'de>, 95 | { 96 | let unchecked = MalleableTransactionSignature::deserialize(deserializer)?; 97 | Ok( 98 | TransactionSignature::new(unchecked.odd_y_parity, unchecked.r, unchecked.s) 99 | .ok_or(serde::de::Error::custom("invalid signature"))?, 100 | ) 101 | } 102 | } 103 | 104 | #[derive(Clone, Debug, PartialEq, Eq)] 105 | #[cfg_attr( 106 | feature = "with-scale", 107 | derive( 108 | scale_codec::Encode, 109 | scale_codec::Decode, 110 | scale_codec::DecodeWithMemTracking, 111 | scale_info::TypeInfo 112 | ) 113 | )] 114 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 115 | pub struct AccessListItem { 116 | pub address: Address, 117 | pub storage_keys: Vec, 118 | } 119 | 120 | impl rlp::Encodable for AccessListItem { 121 | fn rlp_append(&self, s: &mut RlpStream) { 122 | s.begin_list(2); 123 | s.append(&self.address); 124 | s.append_list(&self.storage_keys); 125 | } 126 | } 127 | 128 | impl rlp::Decodable for AccessListItem { 129 | fn decode(rlp: &Rlp) -> Result { 130 | Ok(Self { 131 | address: rlp.val_at(0)?, 132 | storage_keys: rlp.list_at(1)?, 133 | }) 134 | } 135 | } 136 | 137 | pub type AccessList = Vec; 138 | 139 | #[derive(Clone, Debug, PartialEq, Eq)] 140 | #[cfg_attr( 141 | feature = "with-scale", 142 | derive( 143 | scale_codec::Encode, 144 | scale_codec::Decode, 145 | scale_codec::DecodeWithMemTracking, 146 | scale_info::TypeInfo 147 | ) 148 | )] 149 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 150 | pub struct EIP2930Transaction { 151 | pub chain_id: u64, 152 | pub nonce: U256, 153 | pub gas_price: U256, 154 | pub gas_limit: U256, 155 | pub action: TransactionAction, 156 | pub value: U256, 157 | pub input: Bytes, 158 | pub access_list: AccessList, 159 | pub signature: TransactionSignature, 160 | } 161 | 162 | impl EIP2930Transaction { 163 | pub fn hash(&self) -> H256 { 164 | let encoded = rlp::encode(self); 165 | let mut out = alloc::vec![0; 1 + encoded.len()]; 166 | out[0] = 1; 167 | out[1..].copy_from_slice(&encoded); 168 | H256::from_slice(Keccak256::digest(&out).as_ref()) 169 | } 170 | 171 | pub fn to_message(self) -> EIP2930TransactionMessage { 172 | EIP2930TransactionMessage { 173 | chain_id: self.chain_id, 174 | nonce: self.nonce, 175 | gas_price: self.gas_price, 176 | gas_limit: self.gas_limit, 177 | action: self.action, 178 | value: self.value, 179 | input: self.input, 180 | access_list: self.access_list, 181 | } 182 | } 183 | } 184 | 185 | impl rlp::Encodable for EIP2930Transaction { 186 | fn rlp_append(&self, s: &mut RlpStream) { 187 | s.begin_list(11); 188 | s.append(&self.chain_id); 189 | s.append(&self.nonce); 190 | s.append(&self.gas_price); 191 | s.append(&self.gas_limit); 192 | s.append(&self.action); 193 | s.append(&self.value); 194 | s.append(&self.input); 195 | s.append_list(&self.access_list); 196 | s.append(&self.signature.odd_y_parity()); 197 | s.append(&U256::from_big_endian(&self.signature.r()[..])); 198 | s.append(&U256::from_big_endian(&self.signature.s()[..])); 199 | } 200 | } 201 | 202 | impl rlp::Decodable for EIP2930Transaction { 203 | fn decode(rlp: &Rlp) -> Result { 204 | if rlp.item_count()? != 11 { 205 | return Err(DecoderError::RlpIncorrectListLen); 206 | } 207 | 208 | Ok(Self { 209 | chain_id: rlp.val_at(0)?, 210 | nonce: rlp.val_at(1)?, 211 | gas_price: rlp.val_at(2)?, 212 | gas_limit: rlp.val_at(3)?, 213 | action: rlp.val_at(4)?, 214 | value: rlp.val_at(5)?, 215 | input: rlp.val_at(6)?, 216 | access_list: rlp.list_at(7)?, 217 | signature: { 218 | let odd_y_parity = rlp.val_at(8)?; 219 | let r = H256::from(rlp.val_at::(9)?.to_big_endian()); 220 | let s = H256::from(rlp.val_at::(10)?.to_big_endian()); 221 | TransactionSignature::new(odd_y_parity, r, s) 222 | .ok_or(DecoderError::Custom("Invalid transaction signature format"))? 223 | }, 224 | }) 225 | } 226 | } 227 | 228 | #[derive(Clone, Debug, PartialEq, Eq)] 229 | pub struct EIP2930TransactionMessage { 230 | pub chain_id: u64, 231 | pub nonce: U256, 232 | pub gas_price: U256, 233 | pub gas_limit: U256, 234 | pub action: TransactionAction, 235 | pub value: U256, 236 | pub input: Bytes, 237 | pub access_list: AccessList, 238 | } 239 | 240 | impl EIP2930TransactionMessage { 241 | pub fn hash(&self) -> H256 { 242 | let encoded = rlp::encode(self); 243 | let mut out = alloc::vec![0; 1 + encoded.len()]; 244 | out[0] = 1; 245 | out[1..].copy_from_slice(&encoded); 246 | H256::from_slice(Keccak256::digest(&out).as_ref()) 247 | } 248 | } 249 | 250 | impl rlp::Encodable for EIP2930TransactionMessage { 251 | fn rlp_append(&self, s: &mut RlpStream) { 252 | s.begin_list(8); 253 | s.append(&self.chain_id); 254 | s.append(&self.nonce); 255 | s.append(&self.gas_price); 256 | s.append(&self.gas_limit); 257 | s.append(&self.action); 258 | s.append(&self.value); 259 | s.append(&self.input); 260 | s.append_list(&self.access_list); 261 | } 262 | } 263 | 264 | impl From for EIP2930TransactionMessage { 265 | fn from(t: EIP2930Transaction) -> Self { 266 | t.to_message() 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/transaction/legacy.rs: -------------------------------------------------------------------------------- 1 | use core::ops::Deref; 2 | 3 | use ethereum_types::{H160, H256, U256}; 4 | use rlp::{DecoderError, Rlp, RlpStream}; 5 | use sha3::{Digest, Keccak256}; 6 | 7 | use super::signature; 8 | use crate::Bytes; 9 | 10 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 11 | #[cfg_attr( 12 | feature = "with-scale", 13 | derive( 14 | scale_codec::Encode, 15 | scale_codec::Decode, 16 | scale_codec::DecodeWithMemTracking, 17 | scale_info::TypeInfo 18 | ) 19 | )] 20 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 21 | pub enum TransactionAction { 22 | Call(H160), 23 | Create, 24 | } 25 | 26 | impl rlp::Encodable for TransactionAction { 27 | fn rlp_append(&self, s: &mut RlpStream) { 28 | match self { 29 | Self::Call(address) => { 30 | s.encoder().encode_value(&address[..]); 31 | } 32 | Self::Create => s.encoder().encode_value(&[]), 33 | } 34 | } 35 | } 36 | 37 | impl rlp::Decodable for TransactionAction { 38 | fn decode(rlp: &Rlp) -> Result { 39 | if rlp.is_empty() { 40 | if rlp.is_data() { 41 | Ok(TransactionAction::Create) 42 | } else { 43 | Err(DecoderError::RlpExpectedToBeData) 44 | } 45 | } else { 46 | Ok(TransactionAction::Call(rlp.as_val()?)) 47 | } 48 | } 49 | } 50 | 51 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 52 | #[cfg_attr( 53 | feature = "with-scale", 54 | derive( 55 | scale_codec::Encode, 56 | scale_codec::Decode, 57 | scale_codec::DecodeWithMemTracking, 58 | scale_info::TypeInfo 59 | ) 60 | )] 61 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 62 | pub struct TransactionRecoveryId(pub u64); 63 | 64 | impl Deref for TransactionRecoveryId { 65 | type Target = u64; 66 | 67 | fn deref(&self) -> &u64 { 68 | &self.0 69 | } 70 | } 71 | 72 | impl TransactionRecoveryId { 73 | pub fn standard(self) -> u8 { 74 | if self.0 == 27 || self.0 == 28 || self.0 > 36 { 75 | ((self.0 - 1) % 2) as u8 76 | } else { 77 | 4 78 | } 79 | } 80 | 81 | pub fn chain_id(self) -> Option { 82 | if self.0 > 36 { 83 | Some((self.0 - 35) / 2) 84 | } else { 85 | None 86 | } 87 | } 88 | } 89 | 90 | #[derive(Clone, Debug, PartialEq, Eq)] 91 | #[cfg_attr( 92 | feature = "with-scale", 93 | derive(scale_info::TypeInfo, scale_codec::DecodeWithMemTracking) 94 | )] 95 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize))] 96 | pub struct TransactionSignature { 97 | v: TransactionRecoveryId, 98 | r: H256, 99 | s: H256, 100 | } 101 | 102 | impl TransactionSignature { 103 | #[must_use] 104 | pub fn new(v: u64, r: H256, s: H256) -> Option { 105 | let v = TransactionRecoveryId(v); 106 | let is_valid = v.standard() <= 1 107 | && signature::is_valid_signature_component(&r) 108 | && signature::is_valid_signature_component(&s); 109 | 110 | if is_valid { 111 | Some(Self { v, r, s }) 112 | } else { 113 | None 114 | } 115 | } 116 | 117 | #[must_use] 118 | pub fn v(&self) -> u64 { 119 | self.v.0 120 | } 121 | 122 | #[must_use] 123 | pub fn standard_v(&self) -> u8 { 124 | self.v.standard() 125 | } 126 | 127 | #[must_use] 128 | pub fn chain_id(&self) -> Option { 129 | self.v.chain_id() 130 | } 131 | 132 | #[must_use] 133 | pub fn r(&self) -> &H256 { 134 | &self.r 135 | } 136 | 137 | #[must_use] 138 | pub fn s(&self) -> &H256 { 139 | &self.s 140 | } 141 | 142 | #[must_use] 143 | pub fn is_low_s(&self) -> bool { 144 | signature::is_low_s(&self.s) 145 | } 146 | } 147 | 148 | #[cfg(feature = "with-scale")] 149 | impl scale_codec::Encode for TransactionSignature { 150 | fn size_hint(&self) -> usize { 151 | scale_codec::Encode::size_hint(&(self.v.0, self.r, self.s)) 152 | } 153 | 154 | fn using_encoded R>(&self, f: F) -> R { 155 | scale_codec::Encode::using_encoded(&(self.v.0, self.r, self.s), f) 156 | } 157 | } 158 | 159 | #[cfg(feature = "with-scale")] 160 | impl scale_codec::Decode for TransactionSignature { 161 | fn decode(value: &mut I) -> Result { 162 | let (v, r, s) = scale_codec::Decode::decode(value)?; 163 | match Self::new(v, r, s) { 164 | Some(signature) => Ok(signature), 165 | None => Err(scale_codec::Error::from("Invalid signature")), 166 | } 167 | } 168 | } 169 | 170 | #[cfg(feature = "with-serde")] 171 | #[derive(serde::Deserialize)] 172 | struct TransactionSignatureUnchecked { 173 | v: u64, 174 | r: H256, 175 | s: H256, 176 | } 177 | 178 | #[cfg(feature = "with-serde")] 179 | impl<'de> serde::Deserialize<'de> for TransactionSignature { 180 | fn deserialize(deserializer: D) -> Result 181 | where 182 | D: serde::de::Deserializer<'de>, 183 | { 184 | let unchecked = TransactionSignatureUnchecked::deserialize(deserializer)?; 185 | 186 | Ok( 187 | TransactionSignature::new(unchecked.v, unchecked.r, unchecked.s) 188 | .ok_or(serde::de::Error::custom("invalid signature"))?, 189 | ) 190 | } 191 | } 192 | 193 | #[derive(Clone, Debug, PartialEq, Eq)] 194 | #[cfg_attr( 195 | feature = "with-scale", 196 | derive( 197 | scale_codec::Encode, 198 | scale_codec::Decode, 199 | scale_codec::DecodeWithMemTracking, 200 | scale_info::TypeInfo 201 | ) 202 | )] 203 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 204 | pub struct LegacyTransaction { 205 | pub nonce: U256, 206 | pub gas_price: U256, 207 | pub gas_limit: U256, 208 | pub action: TransactionAction, 209 | pub value: U256, 210 | pub input: Bytes, 211 | pub signature: TransactionSignature, 212 | } 213 | 214 | impl LegacyTransaction { 215 | pub fn hash(&self) -> H256 { 216 | H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) 217 | } 218 | 219 | pub fn to_message(self) -> LegacyTransactionMessage { 220 | LegacyTransactionMessage { 221 | nonce: self.nonce, 222 | gas_price: self.gas_price, 223 | gas_limit: self.gas_limit, 224 | action: self.action, 225 | value: self.value, 226 | input: self.input, 227 | chain_id: self.signature.chain_id(), 228 | } 229 | } 230 | } 231 | 232 | impl rlp::Encodable for LegacyTransaction { 233 | fn rlp_append(&self, s: &mut RlpStream) { 234 | s.begin_list(9); 235 | s.append(&self.nonce); 236 | s.append(&self.gas_price); 237 | s.append(&self.gas_limit); 238 | s.append(&self.action); 239 | s.append(&self.value); 240 | s.append(&self.input); 241 | s.append(&self.signature.v.0); 242 | s.append(&U256::from_big_endian(&self.signature.r[..])); 243 | s.append(&U256::from_big_endian(&self.signature.s[..])); 244 | } 245 | } 246 | 247 | impl rlp::Decodable for LegacyTransaction { 248 | fn decode(rlp: &Rlp) -> Result { 249 | if rlp.item_count()? != 9 { 250 | return Err(DecoderError::RlpIncorrectListLen); 251 | } 252 | 253 | let v = rlp.val_at(6)?; 254 | let r = H256::from(rlp.val_at::(7)?.to_big_endian()); 255 | let s = H256::from(rlp.val_at::(8)?.to_big_endian()); 256 | let signature = TransactionSignature::new(v, r, s) 257 | .ok_or(DecoderError::Custom("Invalid transaction signature format"))?; 258 | 259 | Ok(Self { 260 | nonce: rlp.val_at(0)?, 261 | gas_price: rlp.val_at(1)?, 262 | gas_limit: rlp.val_at(2)?, 263 | action: rlp.val_at(3)?, 264 | value: rlp.val_at(4)?, 265 | input: rlp.val_at(5)?, 266 | signature, 267 | }) 268 | } 269 | } 270 | 271 | #[derive(Clone, Debug, PartialEq, Eq)] 272 | pub struct LegacyTransactionMessage { 273 | pub nonce: U256, 274 | pub gas_price: U256, 275 | pub gas_limit: U256, 276 | pub action: TransactionAction, 277 | pub value: U256, 278 | pub input: Bytes, 279 | pub chain_id: Option, 280 | } 281 | 282 | impl LegacyTransactionMessage { 283 | pub fn hash(&self) -> H256 { 284 | H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) 285 | } 286 | } 287 | 288 | impl rlp::Encodable for LegacyTransactionMessage { 289 | fn rlp_append(&self, s: &mut RlpStream) { 290 | if let Some(chain_id) = self.chain_id { 291 | s.begin_list(9); 292 | s.append(&self.nonce); 293 | s.append(&self.gas_price); 294 | s.append(&self.gas_limit); 295 | s.append(&self.action); 296 | s.append(&self.value); 297 | s.append(&self.input); 298 | s.append(&chain_id); 299 | s.append(&0_u8); 300 | s.append(&0_u8); 301 | } else { 302 | s.begin_list(6); 303 | s.append(&self.nonce); 304 | s.append(&self.gas_price); 305 | s.append(&self.gas_limit); 306 | s.append(&self.action); 307 | s.append(&self.value); 308 | s.append(&self.input); 309 | } 310 | } 311 | } 312 | 313 | impl From for LegacyTransactionMessage { 314 | fn from(t: LegacyTransaction) -> Self { 315 | t.to_message() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/receipt.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use bytes::BytesMut; 4 | use ethereum_types::{Bloom, H256, U256}; 5 | use rlp::{Decodable, DecoderError, Rlp}; 6 | 7 | use crate::{ 8 | enveloped::{EnvelopedDecodable, EnvelopedDecoderError, EnvelopedEncodable}, 9 | log::Log, 10 | }; 11 | 12 | #[derive(Clone, Debug, PartialEq, Eq)] 13 | #[derive(rlp::RlpEncodable, rlp::RlpDecodable)] 14 | #[cfg_attr( 15 | feature = "with-scale", 16 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 17 | )] 18 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 19 | pub struct FrontierReceiptData { 20 | pub state_root: H256, 21 | pub used_gas: U256, 22 | pub logs_bloom: Bloom, 23 | pub logs: Vec, 24 | } 25 | 26 | #[derive(Clone, Debug, PartialEq, Eq)] 27 | #[derive(rlp::RlpEncodable, rlp::RlpDecodable)] 28 | #[cfg_attr( 29 | feature = "with-scale", 30 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 31 | )] 32 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 33 | pub struct EIP658ReceiptData { 34 | pub status_code: u8, 35 | pub used_gas: U256, 36 | pub logs_bloom: Bloom, 37 | pub logs: Vec, 38 | } 39 | 40 | pub type EIP2930ReceiptData = EIP658ReceiptData; 41 | 42 | pub type EIP1559ReceiptData = EIP658ReceiptData; 43 | 44 | pub type EIP7702ReceiptData = EIP658ReceiptData; 45 | 46 | pub type ReceiptV0 = FrontierReceiptData; 47 | 48 | impl EnvelopedEncodable for ReceiptV0 { 49 | fn type_id(&self) -> Option { 50 | None 51 | } 52 | fn encode_payload(&self) -> BytesMut { 53 | rlp::encode(self) 54 | } 55 | } 56 | 57 | impl EnvelopedDecodable for ReceiptV0 { 58 | type PayloadDecoderError = DecoderError; 59 | 60 | fn decode(bytes: &[u8]) -> Result> { 61 | Ok(rlp::decode(bytes)?) 62 | } 63 | } 64 | 65 | pub type ReceiptV1 = EIP658ReceiptData; 66 | 67 | impl EnvelopedEncodable for ReceiptV1 { 68 | fn type_id(&self) -> Option { 69 | None 70 | } 71 | fn encode_payload(&self) -> BytesMut { 72 | rlp::encode(self) 73 | } 74 | } 75 | 76 | impl EnvelopedDecodable for ReceiptV1 { 77 | type PayloadDecoderError = DecoderError; 78 | 79 | fn decode(bytes: &[u8]) -> Result> { 80 | Ok(rlp::decode(bytes)?) 81 | } 82 | } 83 | 84 | #[derive(Clone, Debug, PartialEq, Eq)] 85 | #[cfg_attr( 86 | feature = "with-scale", 87 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 88 | )] 89 | #[cfg_attr( 90 | feature = "with-serde", 91 | derive(serde::Serialize, serde::Deserialize), 92 | serde(untagged) 93 | )] 94 | pub enum ReceiptV2 { 95 | /// Legacy receipt type 96 | Legacy(EIP658ReceiptData), 97 | /// EIP-2930 receipt type 98 | EIP2930(EIP2930ReceiptData), 99 | } 100 | 101 | impl EnvelopedEncodable for ReceiptV2 { 102 | fn type_id(&self) -> Option { 103 | match self { 104 | Self::Legacy(_) => None, 105 | Self::EIP2930(_) => Some(1), 106 | } 107 | } 108 | 109 | fn encode_payload(&self) -> BytesMut { 110 | match self { 111 | Self::Legacy(r) => rlp::encode(r), 112 | Self::EIP2930(r) => rlp::encode(r), 113 | } 114 | } 115 | } 116 | 117 | impl EnvelopedDecodable for ReceiptV2 { 118 | type PayloadDecoderError = DecoderError; 119 | 120 | fn decode(bytes: &[u8]) -> Result> { 121 | if bytes.is_empty() { 122 | return Err(EnvelopedDecoderError::UnknownTypeId); 123 | } 124 | 125 | let first = bytes[0]; 126 | 127 | let rlp = Rlp::new(bytes); 128 | if rlp.is_list() { 129 | return Ok(Self::Legacy(Decodable::decode(&rlp)?)); 130 | } 131 | 132 | let s = &bytes[1..]; 133 | 134 | if first == 0x01 { 135 | return Ok(Self::EIP2930(rlp::decode(s)?)); 136 | } 137 | 138 | Err(DecoderError::Custom("invalid receipt type").into()) 139 | } 140 | } 141 | 142 | impl From for EIP658ReceiptData { 143 | fn from(v2: ReceiptV2) -> Self { 144 | match v2 { 145 | ReceiptV2::Legacy(r) => r, 146 | ReceiptV2::EIP2930(r) => r, 147 | } 148 | } 149 | } 150 | 151 | #[derive(Clone, Debug, PartialEq, Eq)] 152 | #[cfg_attr( 153 | feature = "with-scale", 154 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 155 | )] 156 | #[cfg_attr( 157 | feature = "with-serde", 158 | derive(serde::Serialize, serde::Deserialize), 159 | serde(untagged) 160 | )] 161 | pub enum ReceiptV3 { 162 | /// Legacy receipt type 163 | Legacy(EIP658ReceiptData), 164 | /// EIP-2930 receipt type 165 | EIP2930(EIP2930ReceiptData), 166 | /// EIP-1559 receipt type 167 | EIP1559(EIP1559ReceiptData), 168 | } 169 | 170 | impl EnvelopedEncodable for ReceiptV3 { 171 | fn type_id(&self) -> Option { 172 | match self { 173 | Self::Legacy(_) => None, 174 | Self::EIP2930(_) => Some(1), 175 | Self::EIP1559(_) => Some(2), 176 | } 177 | } 178 | 179 | fn encode_payload(&self) -> BytesMut { 180 | match self { 181 | Self::Legacy(r) => rlp::encode(r), 182 | Self::EIP2930(r) => rlp::encode(r), 183 | Self::EIP1559(r) => rlp::encode(r), 184 | } 185 | } 186 | } 187 | 188 | impl EnvelopedDecodable for ReceiptV3 { 189 | type PayloadDecoderError = DecoderError; 190 | 191 | fn decode(bytes: &[u8]) -> Result> { 192 | if bytes.is_empty() { 193 | return Err(EnvelopedDecoderError::UnknownTypeId); 194 | } 195 | 196 | let first = bytes[0]; 197 | 198 | let rlp = Rlp::new(bytes); 199 | if rlp.is_list() { 200 | return Ok(Self::Legacy(Decodable::decode(&rlp)?)); 201 | } 202 | 203 | let s = &bytes[1..]; 204 | 205 | if first == 0x01 { 206 | return Ok(Self::EIP2930(rlp::decode(s)?)); 207 | } 208 | 209 | if first == 0x02 { 210 | return Ok(Self::EIP1559(rlp::decode(s)?)); 211 | } 212 | 213 | Err(DecoderError::Custom("invalid receipt type").into()) 214 | } 215 | } 216 | 217 | impl From for EIP658ReceiptData { 218 | fn from(v3: ReceiptV3) -> Self { 219 | match v3 { 220 | ReceiptV3::Legacy(r) => r, 221 | ReceiptV3::EIP2930(r) => r, 222 | ReceiptV3::EIP1559(r) => r, 223 | } 224 | } 225 | } 226 | 227 | #[derive(Clone, Debug, PartialEq, Eq)] 228 | #[cfg_attr( 229 | feature = "with-scale", 230 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 231 | )] 232 | #[cfg_attr( 233 | feature = "with-serde", 234 | derive(serde::Serialize, serde::Deserialize), 235 | serde(untagged) 236 | )] 237 | pub enum ReceiptV4 { 238 | /// Legacy receipt type 239 | Legacy(EIP658ReceiptData), 240 | /// EIP-2930 receipt type 241 | EIP2930(EIP2930ReceiptData), 242 | /// EIP-1559 receipt type 243 | EIP1559(EIP1559ReceiptData), 244 | /// EIP-7702 receipt type 245 | EIP7702(EIP7702ReceiptData), 246 | } 247 | 248 | impl EnvelopedEncodable for ReceiptV4 { 249 | fn type_id(&self) -> Option { 250 | match self { 251 | Self::Legacy(_) => None, 252 | Self::EIP2930(_) => Some(1), 253 | Self::EIP1559(_) => Some(2), 254 | Self::EIP7702(_) => Some(4), 255 | } 256 | } 257 | 258 | fn encode_payload(&self) -> BytesMut { 259 | match self { 260 | Self::Legacy(r) => rlp::encode(r), 261 | Self::EIP2930(r) => rlp::encode(r), 262 | Self::EIP1559(r) => rlp::encode(r), 263 | Self::EIP7702(r) => rlp::encode(r), 264 | } 265 | } 266 | } 267 | 268 | impl EnvelopedDecodable for ReceiptV4 { 269 | type PayloadDecoderError = DecoderError; 270 | 271 | fn decode(bytes: &[u8]) -> Result> { 272 | if bytes.is_empty() { 273 | return Err(EnvelopedDecoderError::UnknownTypeId); 274 | } 275 | 276 | let first = bytes[0]; 277 | 278 | let rlp = Rlp::new(bytes); 279 | if rlp.is_list() { 280 | return Ok(Self::Legacy(Decodable::decode(&rlp)?)); 281 | } 282 | 283 | let s = &bytes[1..]; 284 | 285 | if first == 0x01 { 286 | return Ok(Self::EIP2930(rlp::decode(s)?)); 287 | } 288 | 289 | if first == 0x02 { 290 | return Ok(Self::EIP1559(rlp::decode(s)?)); 291 | } 292 | 293 | if first == 0x04 { 294 | return Ok(Self::EIP7702(rlp::decode(s)?)); 295 | } 296 | 297 | Err(DecoderError::Custom("invalid receipt type").into()) 298 | } 299 | } 300 | 301 | impl From for EIP658ReceiptData { 302 | fn from(v3: ReceiptV4) -> Self { 303 | match v3 { 304 | ReceiptV4::Legacy(r) => r, 305 | ReceiptV4::EIP2930(r) => r, 306 | ReceiptV4::EIP1559(r) => r, 307 | ReceiptV4::EIP7702(r) => r, 308 | } 309 | } 310 | } 311 | 312 | #[derive(Clone, Debug, PartialEq, Eq)] 313 | #[cfg_attr( 314 | feature = "with-scale", 315 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 316 | )] 317 | #[cfg_attr( 318 | feature = "with-serde", 319 | derive(serde::Serialize, serde::Deserialize), 320 | serde(untagged) 321 | )] 322 | pub enum ReceiptAny { 323 | /// Frontier receipt type 324 | Frontier(FrontierReceiptData), 325 | /// EIP658 receipt type 326 | EIP658(EIP658ReceiptData), 327 | /// EIP-2930 receipt type 328 | EIP2930(EIP2930ReceiptData), 329 | /// EIP-1559 receipt type 330 | EIP1559(EIP1559ReceiptData), 331 | /// EIP-7702 receipt type 332 | EIP7702(EIP7702ReceiptData), 333 | } 334 | 335 | impl EnvelopedEncodable for ReceiptAny { 336 | fn type_id(&self) -> Option { 337 | match self { 338 | Self::Frontier(_) => None, 339 | Self::EIP658(_) => None, 340 | Self::EIP2930(_) => Some(1), 341 | Self::EIP1559(_) => Some(2), 342 | Self::EIP7702(_) => Some(4), 343 | } 344 | } 345 | 346 | fn encode_payload(&self) -> BytesMut { 347 | match self { 348 | Self::Frontier(r) => rlp::encode(r), 349 | Self::EIP658(r) => rlp::encode(r), 350 | Self::EIP2930(r) => rlp::encode(r), 351 | Self::EIP1559(r) => rlp::encode(r), 352 | Self::EIP7702(r) => rlp::encode(r), 353 | } 354 | } 355 | } 356 | 357 | impl EnvelopedDecodable for ReceiptAny { 358 | type PayloadDecoderError = DecoderError; 359 | 360 | fn decode(bytes: &[u8]) -> Result> { 361 | if bytes.is_empty() { 362 | return Err(EnvelopedDecoderError::UnknownTypeId); 363 | } 364 | 365 | let first = bytes[0]; 366 | 367 | let rlp = Rlp::new(bytes); 368 | if rlp.is_list() { 369 | if rlp.item_count()? == 4 { 370 | let first = rlp.at(0)?; 371 | if first.is_data() && first.data()?.len() <= 1 { 372 | return Ok(Self::Frontier(Decodable::decode(&rlp)?)); 373 | } else { 374 | return Ok(Self::EIP658(Decodable::decode(&rlp)?)); 375 | } 376 | } 377 | 378 | return Err(DecoderError::RlpIncorrectListLen.into()); 379 | } 380 | 381 | let s = &bytes[1..]; 382 | 383 | if first == 0x01 { 384 | return Ok(Self::EIP2930(rlp::decode(s)?)); 385 | } 386 | 387 | if first == 0x02 { 388 | return Ok(Self::EIP1559(rlp::decode(s)?)); 389 | } 390 | 391 | if first == 0x04 { 392 | return Ok(Self::EIP7702(rlp::decode(s)?)); 393 | } 394 | 395 | Err(DecoderError::Custom("invalid receipt type").into()) 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/transaction/eip7702.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use ethereum_types::{Address, H256, U256}; 4 | use k256::ecdsa::{RecoveryId, Signature, VerifyingKey}; 5 | use rlp::{DecoderError, Rlp, RlpStream}; 6 | use sha3::{Digest, Keccak256}; 7 | 8 | use crate::Bytes; 9 | 10 | pub use super::eip2930::{ 11 | AccessList, MalleableTransactionSignature, TransactionAction, TransactionSignature, 12 | }; 13 | 14 | /// Error type for EIP-7702 authorization signature recovery 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 17 | pub enum AuthorizationError { 18 | /// Invalid signature format 19 | InvalidSignature, 20 | /// Invalid recovery ID 21 | InvalidRecoveryId, 22 | /// Signature recovery failed 23 | RecoveryFailed, 24 | /// Invalid public key format 25 | InvalidPublicKey, 26 | } 27 | 28 | /// EIP-7702 transaction type as defined in the specification 29 | pub const SET_CODE_TX_TYPE: u8 = 0x04; 30 | 31 | /// EIP-7702 authorization message magic prefix 32 | pub const AUTHORIZATION_MAGIC: u8 = 0x05; 33 | 34 | #[derive(Clone, Debug, PartialEq, Eq)] 35 | #[cfg_attr( 36 | feature = "with-scale", 37 | derive( 38 | scale_codec::Encode, 39 | scale_codec::Decode, 40 | scale_codec::DecodeWithMemTracking, 41 | scale_info::TypeInfo 42 | ) 43 | )] 44 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 45 | pub struct AuthorizationListItem { 46 | pub chain_id: u64, 47 | pub address: Address, 48 | pub nonce: U256, 49 | pub signature: MalleableTransactionSignature, 50 | } 51 | 52 | impl rlp::Encodable for AuthorizationListItem { 53 | fn rlp_append(&self, s: &mut RlpStream) { 54 | s.begin_list(6); 55 | s.append(&self.chain_id); 56 | s.append(&self.address); 57 | s.append(&self.nonce); 58 | s.append(&self.signature.odd_y_parity); 59 | s.append(&U256::from_big_endian(&self.signature.r[..])); 60 | s.append(&U256::from_big_endian(&self.signature.s[..])); 61 | } 62 | } 63 | 64 | impl rlp::Decodable for AuthorizationListItem { 65 | fn decode(rlp: &Rlp) -> Result { 66 | if rlp.item_count()? != 6 { 67 | return Err(DecoderError::RlpIncorrectListLen); 68 | } 69 | 70 | Ok(Self { 71 | chain_id: rlp.val_at(0)?, 72 | address: rlp.val_at(1)?, 73 | nonce: rlp.val_at(2)?, 74 | signature: { 75 | let odd_y_parity = rlp.val_at(3)?; 76 | let r = H256::from(rlp.val_at::(4)?.to_big_endian()); 77 | let s = H256::from(rlp.val_at::(5)?.to_big_endian()); 78 | MalleableTransactionSignature { odd_y_parity, r, s } 79 | }, 80 | }) 81 | } 82 | } 83 | 84 | impl AuthorizationListItem { 85 | /// Check and get the signature. 86 | /// 87 | /// This checks that the signature is not malleable, but does not otherwise check or recover 88 | /// the public key. 89 | pub fn signature(&self) -> Option { 90 | TransactionSignature::new( 91 | self.signature.odd_y_parity, 92 | self.signature.r, 93 | self.signature.s, 94 | ) 95 | } 96 | 97 | /// Recover the authorizing address from the authorization signature according to EIP-7702 98 | pub fn authorizing_address(&self) -> Result { 99 | // Create the authorization message hash according to EIP-7702 100 | let message_hash = self.authorization_message_hash(); 101 | 102 | let sigv = self 103 | .signature() 104 | .ok_or(AuthorizationError::InvalidSignature)?; 105 | 106 | // Create signature from r and s components 107 | let mut signature_bytes = [0u8; 64]; 108 | signature_bytes[0..32].copy_from_slice(&sigv.r()[..]); 109 | signature_bytes[32..64].copy_from_slice(&sigv.s()[..]); 110 | 111 | // Create the signature and recovery ID 112 | let signature = Signature::from_bytes(&signature_bytes.into()) 113 | .map_err(|_| AuthorizationError::InvalidSignature)?; 114 | 115 | let recovery_id = RecoveryId::try_from(if sigv.odd_y_parity() { 1u8 } else { 0u8 }) 116 | .map_err(|_| AuthorizationError::InvalidRecoveryId)?; 117 | 118 | // Recover the verifying key using VerifyingKey::recover_from_prehash 119 | // message_hash is already a 32-byte Keccak256 hash, so we use recover_from_prehash 120 | let verifying_key = 121 | VerifyingKey::recover_from_prehash(message_hash.as_bytes(), &signature, recovery_id) 122 | .map_err(|_| AuthorizationError::RecoveryFailed)?; 123 | 124 | // Convert public key to Ethereum address 125 | Self::verifying_key_to_address(&verifying_key) 126 | } 127 | 128 | /// Create the authorization message hash according to EIP-7702 129 | pub fn authorization_message_hash(&self) -> H256 { 130 | // EIP-7702 authorization message format: 131 | // MAGIC || rlp([chain_id, address, nonce]) 132 | let mut message = alloc::vec![AUTHORIZATION_MAGIC]; 133 | 134 | // RLP encode the authorization tuple 135 | let mut rlp_stream = RlpStream::new_list(3); 136 | rlp_stream.append(&self.chain_id); 137 | rlp_stream.append(&self.address); 138 | rlp_stream.append(&self.nonce); 139 | message.extend_from_slice(&rlp_stream.out()); 140 | 141 | // Return keccak256 hash of the complete message 142 | H256::from_slice(Keccak256::digest(&message).as_ref()) 143 | } 144 | 145 | /// Convert VerifyingKey to Ethereum address 146 | fn verifying_key_to_address( 147 | verifying_key: &VerifyingKey, 148 | ) -> Result { 149 | // Convert public key to bytes (uncompressed format, skip the 0x04 prefix) 150 | let pubkey_point = verifying_key.to_encoded_point(false); 151 | let pubkey_bytes = pubkey_point.as_bytes(); 152 | 153 | // pubkey_bytes is 65 bytes: [0x04, x_coord (32 bytes), y_coord (32 bytes)] 154 | // We want just the x and y coordinates (64 bytes total) 155 | if pubkey_bytes.len() >= 65 && pubkey_bytes[0] == 0x04 { 156 | let pubkey_coords = &pubkey_bytes[1..65]; 157 | // Ethereum address is the last 20 bytes of keccak256(pubkey) 158 | let hash = Keccak256::digest(pubkey_coords); 159 | Ok(Address::from_slice(&hash[12..])) 160 | } else { 161 | Err(AuthorizationError::InvalidPublicKey) 162 | } 163 | } 164 | } 165 | 166 | pub type AuthorizationList = Vec; 167 | 168 | #[derive(Clone, Debug, PartialEq, Eq)] 169 | #[cfg_attr( 170 | feature = "with-scale", 171 | derive( 172 | scale_codec::Encode, 173 | scale_codec::Decode, 174 | scale_codec::DecodeWithMemTracking, 175 | scale_info::TypeInfo 176 | ) 177 | )] 178 | #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] 179 | pub struct EIP7702Transaction { 180 | pub chain_id: u64, 181 | pub nonce: U256, 182 | pub max_priority_fee_per_gas: U256, 183 | pub max_fee_per_gas: U256, 184 | pub gas_limit: U256, 185 | pub destination: TransactionAction, 186 | pub value: U256, 187 | pub data: Bytes, 188 | pub access_list: AccessList, 189 | pub authorization_list: AuthorizationList, 190 | pub signature: TransactionSignature, 191 | } 192 | 193 | impl EIP7702Transaction { 194 | pub fn hash(&self) -> H256 { 195 | let encoded = rlp::encode(self); 196 | let mut out = alloc::vec![0; 1 + encoded.len()]; 197 | out[0] = SET_CODE_TX_TYPE; 198 | out[1..].copy_from_slice(&encoded); 199 | H256::from_slice(Keccak256::digest(&out).as_ref()) 200 | } 201 | 202 | pub fn to_message(self) -> EIP7702TransactionMessage { 203 | EIP7702TransactionMessage { 204 | chain_id: self.chain_id, 205 | nonce: self.nonce, 206 | max_priority_fee_per_gas: self.max_priority_fee_per_gas, 207 | max_fee_per_gas: self.max_fee_per_gas, 208 | gas_limit: self.gas_limit, 209 | destination: self.destination, 210 | value: self.value, 211 | data: self.data, 212 | access_list: self.access_list, 213 | authorization_list: self.authorization_list, 214 | } 215 | } 216 | } 217 | 218 | impl rlp::Encodable for EIP7702Transaction { 219 | fn rlp_append(&self, s: &mut RlpStream) { 220 | s.begin_list(13); 221 | s.append(&self.chain_id); 222 | s.append(&self.nonce); 223 | s.append(&self.max_priority_fee_per_gas); 224 | s.append(&self.max_fee_per_gas); 225 | s.append(&self.gas_limit); 226 | s.append(&self.destination); 227 | s.append(&self.value); 228 | s.append(&self.data); 229 | s.append_list(&self.access_list); 230 | s.append_list(&self.authorization_list); 231 | s.append(&self.signature.odd_y_parity()); 232 | s.append(&U256::from_big_endian(&self.signature.r()[..])); 233 | s.append(&U256::from_big_endian(&self.signature.s()[..])); 234 | } 235 | } 236 | 237 | impl rlp::Decodable for EIP7702Transaction { 238 | fn decode(rlp: &Rlp) -> Result { 239 | if rlp.item_count()? != 13 { 240 | return Err(DecoderError::RlpIncorrectListLen); 241 | } 242 | 243 | Ok(Self { 244 | chain_id: rlp.val_at(0)?, 245 | nonce: rlp.val_at(1)?, 246 | max_priority_fee_per_gas: rlp.val_at(2)?, 247 | max_fee_per_gas: rlp.val_at(3)?, 248 | gas_limit: rlp.val_at(4)?, 249 | destination: rlp.val_at(5)?, 250 | value: rlp.val_at(6)?, 251 | data: rlp.val_at(7)?, 252 | access_list: rlp.list_at(8)?, 253 | authorization_list: rlp.list_at(9)?, 254 | signature: { 255 | let odd_y_parity = rlp.val_at(10)?; 256 | let r = H256::from(rlp.val_at::(11)?.to_big_endian()); 257 | let s = H256::from(rlp.val_at::(12)?.to_big_endian()); 258 | TransactionSignature::new(odd_y_parity, r, s) 259 | .ok_or(DecoderError::Custom("Invalid transaction signature format"))? 260 | }, 261 | }) 262 | } 263 | } 264 | 265 | #[derive(Clone, Debug, PartialEq, Eq)] 266 | pub struct EIP7702TransactionMessage { 267 | pub chain_id: u64, 268 | pub nonce: U256, 269 | pub max_priority_fee_per_gas: U256, 270 | pub max_fee_per_gas: U256, 271 | pub gas_limit: U256, 272 | pub destination: TransactionAction, 273 | pub value: U256, 274 | pub data: Bytes, 275 | pub access_list: AccessList, 276 | pub authorization_list: AuthorizationList, 277 | } 278 | 279 | impl EIP7702TransactionMessage { 280 | pub fn hash(&self) -> H256 { 281 | let encoded = rlp::encode(self); 282 | let mut out = alloc::vec![0; 1 + encoded.len()]; 283 | out[0] = SET_CODE_TX_TYPE; 284 | out[1..].copy_from_slice(&encoded); 285 | H256::from_slice(Keccak256::digest(&out).as_ref()) 286 | } 287 | } 288 | 289 | impl rlp::Encodable for EIP7702TransactionMessage { 290 | fn rlp_append(&self, s: &mut RlpStream) { 291 | s.begin_list(10); 292 | s.append(&self.chain_id); 293 | s.append(&self.nonce); 294 | s.append(&self.max_priority_fee_per_gas); 295 | s.append(&self.max_fee_per_gas); 296 | s.append(&self.gas_limit); 297 | s.append(&self.destination); 298 | s.append(&self.value); 299 | s.append(&self.data); 300 | s.append_list(&self.access_list); 301 | s.append_list(&self.authorization_list); 302 | } 303 | } 304 | 305 | impl From for EIP7702TransactionMessage { 306 | fn from(t: EIP7702Transaction) -> Self { 307 | t.to_message() 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | use ethereum_types::{Address, H256, U256}; 315 | 316 | #[test] 317 | fn test_authorizing_address_with_real_signature() { 318 | use k256::ecdsa::SigningKey; 319 | use k256::elliptic_curve::SecretKey; 320 | 321 | // Use a fixed test private key for deterministic testing 322 | let private_key_bytes = [ 323 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 324 | 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 325 | 0x1d, 0x1e, 0x1f, 0x20, 326 | ]; 327 | 328 | let secret_key = 329 | SecretKey::from_bytes(&private_key_bytes.into()).expect("Invalid private key"); 330 | let signing_key = SigningKey::from(secret_key); 331 | let verifying_key = signing_key.verifying_key(); 332 | 333 | // Create authorization data 334 | let chain_id = 1u64; 335 | let address = Address::from_slice(&[0x42u8; 20]); 336 | let nonce = U256::zero(); 337 | 338 | // Create the EIP-7702 authorization message hash 339 | let mut message = alloc::vec![AUTHORIZATION_MAGIC]; 340 | let mut rlp_stream = RlpStream::new_list(3); 341 | rlp_stream.append(&chain_id); 342 | rlp_stream.append(&address); 343 | rlp_stream.append(&nonce); 344 | message.extend_from_slice(&rlp_stream.out()); 345 | let message_hash = H256::from_slice(Keccak256::digest(&message).as_ref()); 346 | 347 | // Sign the message hash 348 | let (signature, recovery_id) = signing_key 349 | .sign_prehash_recoverable(message_hash.as_bytes()) 350 | .expect("Failed to sign message"); 351 | 352 | // Extract signature components 353 | let signature_bytes = signature.to_bytes(); 354 | let r = H256::from_slice(&signature_bytes[0..32]); 355 | let s = H256::from_slice(&signature_bytes[32..64]); 356 | let y_parity = recovery_id.is_y_odd(); 357 | 358 | // Create AuthorizationListItem with real signature 359 | let auth_item = AuthorizationListItem { 360 | chain_id, 361 | address, 362 | nonce, 363 | signature: MalleableTransactionSignature { 364 | odd_y_parity: y_parity, 365 | r, 366 | s, 367 | }, 368 | }; 369 | 370 | // Recover the authorizing address 371 | let recovered_address = auth_item 372 | .authorizing_address() 373 | .expect("Failed to recover authorizing address"); 374 | 375 | // Convert the original verifying key to an Ethereum address for comparison 376 | let expected_address = AuthorizationListItem::verifying_key_to_address(&verifying_key) 377 | .expect("Failed to convert verifying key to address"); 378 | 379 | // Verify that the recovered address matches the original signer 380 | assert_eq!(recovered_address, expected_address); 381 | assert_ne!(recovered_address, Address::zero()); 382 | 383 | // For deterministic testing, verify specific expected values 384 | // This ensures the implementation is working correctly with known inputs 385 | assert_eq!( 386 | expected_address, 387 | Address::from_slice(&hex_literal::hex!( 388 | "6370ef2f4db3611d657b90667de398a2cc2a370c" 389 | )) 390 | ); 391 | } 392 | 393 | #[test] 394 | fn test_authorizing_address_error_handling() { 395 | // Test with invalid signature components (zero values are invalid in ECDSA) 396 | assert!(TransactionSignature::new( 397 | false, 398 | H256::zero(), // Invalid r value (r cannot be zero) 399 | H256::zero(), // Invalid s value (s cannot be zero) 400 | ) 401 | .is_none()); 402 | 403 | // Test with values that are too high (greater than secp256k1 curve order) 404 | // secp256k1 curve order is FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 405 | assert!(TransactionSignature::new( 406 | false, 407 | // Use maximum possible values which exceed the curve order 408 | H256::from_slice(&[0xFF; 32]), 409 | H256::from_slice(&[0xFF; 32]), 410 | ) 411 | .is_none()); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/transaction/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod eip1559; 2 | pub mod eip2930; 3 | pub mod eip7702; 4 | pub mod legacy; 5 | mod signature; 6 | 7 | use bytes::BytesMut; 8 | use ethereum_types::H256; 9 | use rlp::{DecoderError, Rlp}; 10 | 11 | pub use self::{ 12 | eip1559::{EIP1559Transaction, EIP1559TransactionMessage}, 13 | eip2930::{AccessList, AccessListItem, EIP2930Transaction, EIP2930TransactionMessage}, 14 | eip7702::{ 15 | AuthorizationList, AuthorizationListItem, EIP7702Transaction, EIP7702TransactionMessage, 16 | }, 17 | legacy::{LegacyTransaction, LegacyTransactionMessage, TransactionAction}, 18 | }; 19 | use crate::enveloped::{EnvelopedDecodable, EnvelopedDecoderError, EnvelopedEncodable}; 20 | 21 | pub type TransactionV0 = LegacyTransaction; 22 | 23 | impl EnvelopedEncodable for TransactionV0 { 24 | fn type_id(&self) -> Option { 25 | None 26 | } 27 | fn encode_payload(&self) -> BytesMut { 28 | rlp::encode(self) 29 | } 30 | } 31 | 32 | impl EnvelopedDecodable for TransactionV0 { 33 | type PayloadDecoderError = DecoderError; 34 | 35 | fn decode(bytes: &[u8]) -> Result> { 36 | Ok(rlp::decode(bytes)?) 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug, PartialEq, Eq)] 41 | #[cfg_attr( 42 | feature = "with-scale", 43 | derive(scale_codec::Encode, scale_codec::Decode, scale_info::TypeInfo) 44 | )] 45 | #[cfg_attr( 46 | feature = "with-serde", 47 | derive(serde::Serialize, serde::Deserialize), 48 | serde(untagged) 49 | )] 50 | pub enum TransactionV1 { 51 | /// Legacy transaction type 52 | Legacy(LegacyTransaction), 53 | /// EIP-2930 transaction 54 | EIP2930(EIP2930Transaction), 55 | } 56 | 57 | impl TransactionV1 { 58 | pub fn hash(&self) -> H256 { 59 | match self { 60 | TransactionV1::Legacy(t) => t.hash(), 61 | TransactionV1::EIP2930(t) => t.hash(), 62 | } 63 | } 64 | } 65 | 66 | impl EnvelopedEncodable for TransactionV1 { 67 | fn type_id(&self) -> Option { 68 | match self { 69 | Self::Legacy(_) => None, 70 | Self::EIP2930(_) => Some(1), 71 | } 72 | } 73 | 74 | fn encode_payload(&self) -> BytesMut { 75 | match self { 76 | Self::Legacy(tx) => rlp::encode(tx), 77 | Self::EIP2930(tx) => rlp::encode(tx), 78 | } 79 | } 80 | } 81 | 82 | impl EnvelopedDecodable for TransactionV1 { 83 | type PayloadDecoderError = DecoderError; 84 | 85 | fn decode(bytes: &[u8]) -> Result> { 86 | if bytes.is_empty() { 87 | return Err(EnvelopedDecoderError::UnknownTypeId); 88 | } 89 | 90 | let first = bytes[0]; 91 | 92 | let rlp = Rlp::new(bytes); 93 | if rlp.is_list() { 94 | return Ok(Self::Legacy(rlp.as_val()?)); 95 | } 96 | 97 | let s = &bytes[1..]; 98 | 99 | if first == 0x01 { 100 | return Ok(Self::EIP2930(rlp::decode(s)?)); 101 | } 102 | 103 | Err(DecoderError::Custom("invalid tx type").into()) 104 | } 105 | } 106 | 107 | #[derive(Clone, Debug, PartialEq, Eq)] 108 | #[cfg_attr( 109 | feature = "with-scale", 110 | derive( 111 | scale_codec::Encode, 112 | scale_codec::Decode, 113 | scale_codec::DecodeWithMemTracking, 114 | scale_info::TypeInfo 115 | ) 116 | )] 117 | #[cfg_attr( 118 | feature = "with-serde", 119 | derive(serde::Serialize, serde::Deserialize), 120 | serde(untagged) 121 | )] 122 | pub enum TransactionV2 { 123 | /// Legacy transaction type 124 | Legacy(LegacyTransaction), 125 | /// EIP-2930 transaction 126 | EIP2930(EIP2930Transaction), 127 | /// EIP-1559 transaction 128 | EIP1559(EIP1559Transaction), 129 | } 130 | 131 | impl TransactionV2 { 132 | pub fn hash(&self) -> H256 { 133 | match self { 134 | TransactionV2::Legacy(t) => t.hash(), 135 | TransactionV2::EIP2930(t) => t.hash(), 136 | TransactionV2::EIP1559(t) => t.hash(), 137 | } 138 | } 139 | } 140 | 141 | impl EnvelopedEncodable for TransactionV2 { 142 | fn type_id(&self) -> Option { 143 | match self { 144 | Self::Legacy(_) => None, 145 | Self::EIP2930(_) => Some(1), 146 | Self::EIP1559(_) => Some(2), 147 | } 148 | } 149 | 150 | fn encode_payload(&self) -> BytesMut { 151 | match self { 152 | Self::Legacy(tx) => rlp::encode(tx), 153 | Self::EIP2930(tx) => rlp::encode(tx), 154 | Self::EIP1559(tx) => rlp::encode(tx), 155 | } 156 | } 157 | } 158 | 159 | impl EnvelopedDecodable for TransactionV2 { 160 | type PayloadDecoderError = DecoderError; 161 | 162 | fn decode(bytes: &[u8]) -> Result> { 163 | if bytes.is_empty() { 164 | return Err(EnvelopedDecoderError::UnknownTypeId); 165 | } 166 | 167 | let first = bytes[0]; 168 | 169 | let rlp = Rlp::new(bytes); 170 | if rlp.is_list() { 171 | return Ok(Self::Legacy(rlp.as_val()?)); 172 | } 173 | 174 | let s = &bytes[1..]; 175 | 176 | if first == 0x01 { 177 | return Ok(Self::EIP2930(rlp::decode(s)?)); 178 | } 179 | 180 | if first == 0x02 { 181 | return Ok(Self::EIP1559(rlp::decode(s)?)); 182 | } 183 | 184 | Err(DecoderError::Custom("invalid tx type").into()) 185 | } 186 | } 187 | 188 | impl From for TransactionV1 { 189 | fn from(t: LegacyTransaction) -> Self { 190 | TransactionV1::Legacy(t) 191 | } 192 | } 193 | 194 | impl From for TransactionV2 { 195 | fn from(t: LegacyTransaction) -> Self { 196 | TransactionV2::Legacy(t) 197 | } 198 | } 199 | 200 | impl From for TransactionV2 { 201 | fn from(t: TransactionV1) -> Self { 202 | match t { 203 | TransactionV1::Legacy(t) => TransactionV2::Legacy(t), 204 | TransactionV1::EIP2930(t) => TransactionV2::EIP2930(t), 205 | } 206 | } 207 | } 208 | 209 | #[derive(Clone, Debug, PartialEq, Eq)] 210 | #[cfg_attr( 211 | feature = "with-scale", 212 | derive( 213 | scale_codec::Encode, 214 | scale_codec::Decode, 215 | scale_codec::DecodeWithMemTracking, 216 | scale_info::TypeInfo 217 | ) 218 | )] 219 | #[cfg_attr( 220 | feature = "with-serde", 221 | derive(serde::Serialize, serde::Deserialize), 222 | serde(untagged) 223 | )] 224 | pub enum TransactionV3 { 225 | /// Legacy transaction type 226 | Legacy(LegacyTransaction), 227 | /// EIP-2930 transaction 228 | EIP2930(EIP2930Transaction), 229 | /// EIP-1559 transaction 230 | EIP1559(EIP1559Transaction), 231 | /// EIP-7702 transaction 232 | EIP7702(EIP7702Transaction), 233 | } 234 | 235 | impl TransactionV3 { 236 | pub fn hash(&self) -> H256 { 237 | match self { 238 | TransactionV3::Legacy(t) => t.hash(), 239 | TransactionV3::EIP2930(t) => t.hash(), 240 | TransactionV3::EIP1559(t) => t.hash(), 241 | TransactionV3::EIP7702(t) => t.hash(), 242 | } 243 | } 244 | } 245 | 246 | impl EnvelopedEncodable for TransactionV3 { 247 | fn type_id(&self) -> Option { 248 | match self { 249 | Self::Legacy(_) => None, 250 | Self::EIP2930(_) => Some(1), 251 | Self::EIP1559(_) => Some(2), 252 | Self::EIP7702(_) => Some(4), 253 | } 254 | } 255 | 256 | fn encode_payload(&self) -> BytesMut { 257 | match self { 258 | Self::Legacy(tx) => rlp::encode(tx), 259 | Self::EIP2930(tx) => rlp::encode(tx), 260 | Self::EIP1559(tx) => rlp::encode(tx), 261 | Self::EIP7702(tx) => rlp::encode(tx), 262 | } 263 | } 264 | } 265 | 266 | impl EnvelopedDecodable for TransactionV3 { 267 | type PayloadDecoderError = DecoderError; 268 | 269 | fn decode(bytes: &[u8]) -> Result> { 270 | if bytes.is_empty() { 271 | return Err(EnvelopedDecoderError::UnknownTypeId); 272 | } 273 | 274 | let first = bytes[0]; 275 | 276 | let rlp = Rlp::new(bytes); 277 | if rlp.is_list() { 278 | return Ok(Self::Legacy(rlp.as_val()?)); 279 | } 280 | 281 | let s = &bytes[1..]; 282 | 283 | if first == 0x01 { 284 | return Ok(Self::EIP2930(rlp::decode(s)?)); 285 | } 286 | 287 | if first == 0x02 { 288 | return Ok(Self::EIP1559(rlp::decode(s)?)); 289 | } 290 | 291 | if first == 0x04 { 292 | return Ok(Self::EIP7702(rlp::decode(s)?)); 293 | } 294 | 295 | Err(DecoderError::Custom("invalid tx type").into()) 296 | } 297 | } 298 | 299 | impl From for TransactionV3 { 300 | fn from(t: LegacyTransaction) -> Self { 301 | TransactionV3::Legacy(t) 302 | } 303 | } 304 | 305 | impl From for TransactionV3 { 306 | fn from(t: TransactionV1) -> Self { 307 | match t { 308 | TransactionV1::Legacy(t) => TransactionV3::Legacy(t), 309 | TransactionV1::EIP2930(t) => TransactionV3::EIP2930(t), 310 | } 311 | } 312 | } 313 | 314 | impl From for TransactionV3 { 315 | fn from(t: TransactionV2) -> Self { 316 | match t { 317 | TransactionV2::Legacy(t) => TransactionV3::Legacy(t), 318 | TransactionV2::EIP2930(t) => TransactionV3::EIP2930(t), 319 | TransactionV2::EIP1559(t) => TransactionV3::EIP1559(t), 320 | } 321 | } 322 | } 323 | 324 | pub type TransactionAny = TransactionV3; 325 | 326 | #[cfg(test)] 327 | mod tests { 328 | use super::{ 329 | eip2930::{self, AccessListItem}, 330 | eip7702::AuthorizationListItem, 331 | legacy::{self, TransactionAction}, 332 | EIP1559Transaction, EIP2930Transaction, EIP7702Transaction, EnvelopedDecodable, 333 | TransactionV0, TransactionV1, TransactionV2, TransactionV3, 334 | }; 335 | use crate::enveloped::*; 336 | use ethereum_types::U256; 337 | use hex_literal::hex; 338 | 339 | #[test] 340 | fn can_decode_raw_transaction() { 341 | let bytes = hex!("f901e48080831000008080b90196608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507fc68045c3c562488255b55aa2c4c7849de001859ff0d8a36a75c2d5ed80100fb660405180806020018281038252600d8152602001807f48656c6c6f2c20776f726c64210000000000000000000000000000000000000081525060200191505060405180910390a160cf806100c76000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80638da5cb5b14602d575b600080fd5b60336075565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff168156fea265627a7a72315820fae816ad954005c42bea7bc7cb5b19f7fd5d3a250715ca2023275c9ca7ce644064736f6c634300050f003278a04cab43609092a99cf095d458b61b47189d1bbab64baed10a0fd7b7d2de2eb960a011ab1bcda76dfed5e733219beb83789f9887b2a7b2e61759c7c90f7d40403201"); 342 | 343 | ::decode(&bytes).unwrap(); 344 | ::decode(&bytes).unwrap(); 345 | ::decode(&bytes).unwrap(); 346 | ::decode(&bytes).unwrap(); 347 | } 348 | 349 | #[test] 350 | fn transaction_v0() { 351 | let tx = TransactionV0 { 352 | nonce: 12.into(), 353 | gas_price: 20_000_000_000_u64.into(), 354 | gas_limit: 21000.into(), 355 | action: TransactionAction::Call( 356 | hex!("727fc6a68321b754475c668a6abfb6e9e71c169a").into(), 357 | ), 358 | value: U256::from(10) * 1_000_000_000 * 1_000_000_000, 359 | input: hex!("a9059cbb000000000213ed0f886efd100b67c7e4ec0a85a7d20dc971600000000000000000000015af1d78b58c4000").into(), 360 | signature: legacy::TransactionSignature::new(38, hex!("be67e0a07db67da8d446f76add590e54b6e92cb6b8f9835aeb67540579a27717").into(), hex!("2d690516512020171c1ec870f6ff45398cc8609250326be89915fb538e7bd718").into()).unwrap(), 361 | }; 362 | 363 | assert_eq!( 364 | tx, 365 | ::decode(&tx.encode()).unwrap() 366 | ); 367 | } 368 | 369 | #[test] 370 | fn transaction_v1() { 371 | let tx = TransactionV1::EIP2930(EIP2930Transaction { 372 | chain_id: 5, 373 | nonce: 7.into(), 374 | gas_price: 30_000_000_000_u64.into(), 375 | gas_limit: 5_748_100_u64.into(), 376 | action: TransactionAction::Call( 377 | hex!("811a752c8cd697e3cb27279c330ed1ada745a8d7").into(), 378 | ), 379 | value: U256::from(2) * 1_000_000_000 * 1_000_000_000, 380 | input: hex!("6ebaf477f83e051589c1188bcc6ddccd").into(), 381 | access_list: vec![ 382 | AccessListItem { 383 | address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), 384 | storage_keys: vec![ 385 | hex!("0000000000000000000000000000000000000000000000000000000000000003") 386 | .into(), 387 | hex!("0000000000000000000000000000000000000000000000000000000000000007") 388 | .into(), 389 | ], 390 | }, 391 | AccessListItem { 392 | address: hex!("bb9bc244d798123fde783fcc1c72d3bb8c189413").into(), 393 | storage_keys: vec![], 394 | }, 395 | ], 396 | signature: eip2930::TransactionSignature::new( 397 | false, 398 | hex!("36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0").into(), 399 | hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), 400 | ) 401 | .unwrap(), 402 | }); 403 | 404 | assert_eq!( 405 | tx, 406 | ::decode(&tx.encode()).unwrap() 407 | ); 408 | } 409 | 410 | #[test] 411 | fn transaction_v2() { 412 | let tx = TransactionV2::EIP1559(EIP1559Transaction { 413 | chain_id: 5, 414 | nonce: 7.into(), 415 | max_priority_fee_per_gas: 10_000_000_000_u64.into(), 416 | max_fee_per_gas: 30_000_000_000_u64.into(), 417 | gas_limit: 5_748_100_u64.into(), 418 | action: TransactionAction::Call( 419 | hex!("811a752c8cd697e3cb27279c330ed1ada745a8d7").into(), 420 | ), 421 | value: U256::from(2) * 1_000_000_000 * 1_000_000_000, 422 | input: hex!("6ebaf477f83e051589c1188bcc6ddccd").into(), 423 | access_list: vec![ 424 | AccessListItem { 425 | address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), 426 | storage_keys: vec![ 427 | hex!("0000000000000000000000000000000000000000000000000000000000000003") 428 | .into(), 429 | hex!("0000000000000000000000000000000000000000000000000000000000000007") 430 | .into(), 431 | ], 432 | }, 433 | AccessListItem { 434 | address: hex!("bb9bc244d798123fde783fcc1c72d3bb8c189413").into(), 435 | storage_keys: vec![], 436 | }, 437 | ], 438 | signature: eip2930::TransactionSignature::new( 439 | false, 440 | hex!("36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0").into(), 441 | hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), 442 | ) 443 | .unwrap(), 444 | }); 445 | 446 | assert_eq!( 447 | tx, 448 | ::decode(&tx.encode()).unwrap() 449 | ); 450 | } 451 | 452 | #[test] 453 | fn transaction_v3() { 454 | let tx = TransactionV3::EIP7702(EIP7702Transaction { 455 | chain_id: 5, 456 | nonce: 7.into(), 457 | max_priority_fee_per_gas: 10_000_000_000_u64.into(), 458 | max_fee_per_gas: 30_000_000_000_u64.into(), 459 | gas_limit: 5_748_100_u64.into(), 460 | destination: TransactionAction::Call( 461 | hex!("811a752c8cd697e3cb27279c330ed1ada745a8d7").into(), 462 | ), 463 | value: U256::from(2) * 1_000_000_000 * 1_000_000_000, 464 | data: hex!("6ebaf477f83e051589c1188bcc6ddccd").into(), 465 | access_list: vec![AccessListItem { 466 | address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), 467 | storage_keys: vec![hex!( 468 | "0000000000000000000000000000000000000000000000000000000000000003" 469 | ) 470 | .into()], 471 | }], 472 | authorization_list: vec![AuthorizationListItem { 473 | chain_id: 5, 474 | address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), 475 | nonce: 1.into(), 476 | signature: eip2930::MalleableTransactionSignature { 477 | odd_y_parity: false, 478 | r: hex!("36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0") 479 | .into(), 480 | s: hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094") 481 | .into(), 482 | }, 483 | }], 484 | signature: eip2930::TransactionSignature::new( 485 | false, 486 | hex!("36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0").into(), 487 | hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), 488 | ) 489 | .unwrap(), 490 | }); 491 | 492 | assert_eq!( 493 | tx, 494 | ::decode(&tx.encode()).unwrap() 495 | ); 496 | } 497 | } 498 | --------------------------------------------------------------------------------