Skip to main content

chains_sdk/ethereum/
transaction.rs

1//! Ethereum transaction types with RLP encoding and signing.
2//!
3//! Supports Legacy (pre-EIP-2718), Type 1 (EIP-2930), and Type 2 (EIP-1559) transactions.
4//! Each transaction type can be built, serialized, signed, and exported as raw hex for broadcasting.
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::ethereum::transaction::EIP1559Transaction;
9//! use chains_sdk::ethereum::EthereumSigner;
10//! use chains_sdk::traits::KeyPair;
11//!
12//! fn example() -> Result<(), Box<dyn std::error::Error>> {
13//!     let signer = EthereumSigner::generate()?;
14//!     let tx = EIP1559Transaction {
15//!         chain_id: 1,
16//!         nonce: 0,
17//!         max_priority_fee_per_gas: 2_000_000_000, // 2 Gwei
18//!         max_fee_per_gas: 100_000_000_000,        // 100 Gwei
19//!         gas_limit: 21_000,
20//!         to: Some([0xAA; 20]),
21//!         value: 1_000_000_000_000_000_000,        // 1 ETH
22//!         data: vec![],
23//!         access_list: vec![],
24//!     };
25//!     let signed = tx.sign(&signer)?;
26//!     println!("Raw tx: 0x{}", hex::encode(&signed.raw_tx()));
27//!     println!("Tx hash: 0x{}", hex::encode(signed.tx_hash()));
28//!     Ok(())
29//! }
30//! ```
31
32use super::rlp;
33use super::EthereumSigner;
34use crate::error::SignerError;
35use core::cmp::Ordering;
36
37// ─── Signed Transaction ────────────────────────────────────────────
38
39/// A signed Ethereum transaction ready for broadcast.
40#[derive(Debug, Clone)]
41pub struct SignedTransaction {
42    /// The raw signed transaction bytes (for `eth_sendRawTransaction`).
43    raw: Vec<u8>,
44}
45
46impl SignedTransaction {
47    /// Return the raw signed transaction bytes.
48    ///
49    /// This is what you pass to `eth_sendRawTransaction`.
50    #[must_use]
51    pub fn raw_tx(&self) -> &[u8] {
52        &self.raw
53    }
54
55    /// Compute the transaction hash (keccak256 of the raw signed tx).
56    #[must_use]
57    pub fn tx_hash(&self) -> [u8; 32] {
58        keccak256(&self.raw)
59    }
60
61    /// Return the raw transaction as a `0x`-prefixed hex string.
62    #[must_use]
63    pub fn raw_tx_hex(&self) -> String {
64        format!("0x{}", hex::encode(&self.raw))
65    }
66}
67
68// ─── Legacy Transaction (pre-EIP-2718) ─────────────────────────────
69
70/// A Legacy (Type 0) Ethereum transaction.
71///
72/// Uses EIP-155 replay protection via `chain_id` in the signing payload.
73#[derive(Debug, Clone)]
74pub struct LegacyTransaction {
75    /// The nonce of the sender.
76    pub nonce: u64,
77    /// Gas price in wei.
78    pub gas_price: u128,
79    /// Gas limit.
80    pub gas_limit: u64,
81    /// Recipient address. `None` for contract creation.
82    pub to: Option<[u8; 20]>,
83    /// Value in wei.
84    pub value: u128,
85    /// Call data.
86    pub data: Vec<u8>,
87    /// Chain ID for EIP-155 replay protection.
88    pub chain_id: u64,
89}
90
91impl LegacyTransaction {
92    /// Serialize the unsigned transaction for signing (EIP-155).
93    ///
94    /// `RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0])`
95    fn signing_payload(&self) -> Vec<u8> {
96        let mut items = Vec::new();
97        items.extend_from_slice(&rlp::encode_u64(self.nonce));
98        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
99        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
100        items.extend_from_slice(&encode_address(&self.to));
101        items.extend_from_slice(&rlp::encode_u128(self.value));
102        items.extend_from_slice(&rlp::encode_bytes(&self.data));
103        // EIP-155: chain_id, 0, 0
104        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
105        items.extend_from_slice(&rlp::encode_u64(0));
106        items.extend_from_slice(&rlp::encode_u64(0));
107        rlp::encode_list(&items)
108    }
109
110    /// Sign this transaction with the given signer.
111    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
112        if self.chain_id == 0 {
113            return Err(SignerError::SigningFailed(
114                "legacy tx requires non-zero chain_id".into(),
115            ));
116        }
117        let payload = self.signing_payload();
118        let hash = keccak256(&payload);
119        let sig = signer.sign_digest(&hash)?;
120
121        // EIP-155: v = {0,1} + chain_id * 2 + 35
122        let recovery_id = sig
123            .v
124            .checked_sub(27)
125            .ok_or_else(|| SignerError::SigningFailed("invalid legacy recovery id".into()))?;
126        let v = recovery_id
127            .checked_add(
128                self.chain_id
129                    .checked_mul(2)
130                    .ok_or_else(|| SignerError::SigningFailed("chain_id overflow".into()))?,
131            )
132            .and_then(|vv| vv.checked_add(35))
133            .ok_or_else(|| SignerError::SigningFailed("EIP-155 v overflow".into()))?;
134
135        let mut items = Vec::new();
136        items.extend_from_slice(&rlp::encode_u64(self.nonce));
137        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
138        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
139        items.extend_from_slice(&encode_address(&self.to));
140        items.extend_from_slice(&rlp::encode_u128(self.value));
141        items.extend_from_slice(&rlp::encode_bytes(&self.data));
142        items.extend_from_slice(&rlp::encode_u64(v));
143        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
144        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
145
146        Ok(SignedTransaction {
147            raw: rlp::encode_list(&items),
148        })
149    }
150}
151
152// ─── EIP-2930 Transaction (Type 1) ─────────────────────────────────
153
154/// An EIP-2930 (Type 1) transaction with access list.
155///
156/// Introduced by Berlin hard fork. Uses EIP-2718 typed transaction envelope.
157#[derive(Debug, Clone)]
158pub struct EIP2930Transaction {
159    /// Chain ID (required, not optional like Legacy).
160    pub chain_id: u64,
161    /// Sender nonce.
162    pub nonce: u64,
163    /// Gas price in wei.
164    pub gas_price: u128,
165    /// Gas limit.
166    pub gas_limit: u64,
167    /// Recipient. `None` for contract creation.
168    pub to: Option<[u8; 20]>,
169    /// Value in wei.
170    pub value: u128,
171    /// Call data.
172    pub data: Vec<u8>,
173    /// Access list: `[(address, [storage_key, ...])]`.
174    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
175}
176
177impl EIP2930Transaction {
178    /// Signing payload: `keccak256(0x01 || RLP([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList]))`
179    fn signing_hash(&self) -> [u8; 32] {
180        let mut items = Vec::new();
181        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
182        items.extend_from_slice(&rlp::encode_u64(self.nonce));
183        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
184        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
185        items.extend_from_slice(&encode_address(&self.to));
186        items.extend_from_slice(&rlp::encode_u128(self.value));
187        items.extend_from_slice(&rlp::encode_bytes(&self.data));
188        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
189
190        let mut payload = vec![0x01]; // Type 1
191        payload.extend_from_slice(&rlp::encode_list(&items));
192        keccak256(&payload)
193    }
194
195    /// Sign this transaction.
196    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
197        if self.chain_id == 0 {
198            return Err(SignerError::SigningFailed(
199                "type1 tx requires non-zero chain_id".into(),
200            ));
201        }
202        let hash = self.signing_hash();
203        let sig = signer.sign_digest(&hash)?;
204        let y_parity = sig.v - 27; // 0 or 1
205
206        let mut items = Vec::new();
207        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
208        items.extend_from_slice(&rlp::encode_u64(self.nonce));
209        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
210        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
211        items.extend_from_slice(&encode_address(&self.to));
212        items.extend_from_slice(&rlp::encode_u128(self.value));
213        items.extend_from_slice(&rlp::encode_bytes(&self.data));
214        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
215        items.extend_from_slice(&rlp::encode_u64(y_parity));
216        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
217        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
218
219        let mut raw = vec![0x01]; // Type prefix
220        raw.extend_from_slice(&rlp::encode_list(&items));
221
222        Ok(SignedTransaction { raw })
223    }
224}
225
226// ─── EIP-1559 Transaction (Type 2) ─────────────────────────────────
227
228/// An EIP-1559 (Type 2) dynamic fee transaction.
229///
230/// The de facto standard since the London hard fork. Uses `maxFeePerGas` and
231/// `maxPriorityFeePerGas` instead of a single `gasPrice`.
232#[derive(Debug, Clone)]
233pub struct EIP1559Transaction {
234    /// Chain ID (required).
235    pub chain_id: u64,
236    /// Sender nonce.
237    pub nonce: u64,
238    /// Maximum priority fee (tip) per gas in wei.
239    pub max_priority_fee_per_gas: u128,
240    /// Maximum total fee per gas in wei.
241    pub max_fee_per_gas: u128,
242    /// Gas limit.
243    pub gas_limit: u64,
244    /// Recipient. `None` for contract creation.
245    pub to: Option<[u8; 20]>,
246    /// Value in wei.
247    pub value: u128,
248    /// Call data.
249    pub data: Vec<u8>,
250    /// Access list: `[(address, [storage_key, ...])]`.
251    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
252}
253
254impl EIP1559Transaction {
255    /// Signing hash: `keccak256(0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))`
256    fn signing_hash(&self) -> [u8; 32] {
257        let mut items = Vec::new();
258        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
259        items.extend_from_slice(&rlp::encode_u64(self.nonce));
260        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
261        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
262        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
263        items.extend_from_slice(&encode_address(&self.to));
264        items.extend_from_slice(&rlp::encode_u128(self.value));
265        items.extend_from_slice(&rlp::encode_bytes(&self.data));
266        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
267
268        let mut payload = vec![0x02]; // Type 2
269        payload.extend_from_slice(&rlp::encode_list(&items));
270        keccak256(&payload)
271    }
272
273    /// Sign this transaction.
274    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
275        if self.chain_id == 0 {
276            return Err(SignerError::SigningFailed(
277                "type2 tx requires non-zero chain_id".into(),
278            ));
279        }
280        if self.max_priority_fee_per_gas > self.max_fee_per_gas {
281            return Err(SignerError::SigningFailed(
282                "max_priority_fee_per_gas cannot exceed max_fee_per_gas".into(),
283            ));
284        }
285        let hash = self.signing_hash();
286        let sig = signer.sign_digest(&hash)?;
287        let y_parity = sig.v - 27; // 0 or 1
288
289        let mut items = Vec::new();
290        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
291        items.extend_from_slice(&rlp::encode_u64(self.nonce));
292        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
293        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
294        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
295        items.extend_from_slice(&encode_address(&self.to));
296        items.extend_from_slice(&rlp::encode_u128(self.value));
297        items.extend_from_slice(&rlp::encode_bytes(&self.data));
298        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
299        items.extend_from_slice(&rlp::encode_u64(y_parity));
300        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
301        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
302
303        let mut raw = vec![0x02]; // Type prefix
304        raw.extend_from_slice(&rlp::encode_list(&items));
305
306        Ok(SignedTransaction { raw })
307    }
308}
309
310// ─── EIP-4844 Transaction (Type 3) ─────────────────────────────────
311
312/// An EIP-4844 (Type 3) blob transaction.
313///
314/// Carries blob versioned hashes for rollup data availability.
315/// Note: the actual blob data and KZG proofs are sidecar data, not
316/// part of the transaction itself.
317#[derive(Debug, Clone)]
318pub struct EIP4844Transaction {
319    /// Chain ID (required).
320    pub chain_id: u64,
321    /// Sender nonce.
322    pub nonce: u64,
323    /// Maximum priority fee (tip) per gas in wei.
324    pub max_priority_fee_per_gas: u128,
325    /// Maximum total fee per gas in wei.
326    pub max_fee_per_gas: u128,
327    /// Gas limit.
328    pub gas_limit: u64,
329    /// Recipient address (required — no contract creation).
330    pub to: [u8; 20],
331    /// Value in wei.
332    pub value: u128,
333    /// Call data.
334    pub data: Vec<u8>,
335    /// Access list.
336    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
337    /// Maximum fee per blob gas in wei.
338    pub max_fee_per_blob_gas: u128,
339    /// Blob versioned hashes (32 bytes each, version byte 0x01).
340    pub blob_versioned_hashes: Vec<[u8; 32]>,
341}
342
343impl EIP4844Transaction {
344    /// Signing hash: `keccak256(0x03 || RLP([...fields, max_fee_per_blob_gas, blob_versioned_hashes]))`
345    fn signing_hash(&self) -> [u8; 32] {
346        let mut items = Vec::new();
347        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
348        items.extend_from_slice(&rlp::encode_u64(self.nonce));
349        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
350        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
351        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
352        items.extend_from_slice(&rlp::encode_bytes(&self.to));
353        items.extend_from_slice(&rlp::encode_u128(self.value));
354        items.extend_from_slice(&rlp::encode_bytes(&self.data));
355        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
356        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_blob_gas));
357        // blob_versioned_hashes as RLP list of 32-byte strings
358        let mut hash_items = Vec::new();
359        for h in &self.blob_versioned_hashes {
360            hash_items.extend_from_slice(&rlp::encode_bytes(h));
361        }
362        items.extend_from_slice(&rlp::encode_list(&hash_items));
363
364        let mut payload = vec![0x03]; // Type 3
365        payload.extend_from_slice(&rlp::encode_list(&items));
366        keccak256(&payload)
367    }
368
369    /// Sign this transaction.
370    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
371        if self.chain_id == 0 {
372            return Err(SignerError::SigningFailed(
373                "type3 tx requires non-zero chain_id".into(),
374            ));
375        }
376        if self.max_priority_fee_per_gas > self.max_fee_per_gas {
377            return Err(SignerError::SigningFailed(
378                "max_priority_fee_per_gas cannot exceed max_fee_per_gas".into(),
379            ));
380        }
381        if self.blob_versioned_hashes.is_empty() {
382            return Err(SignerError::SigningFailed(
383                "type3 tx requires at least one blob versioned hash".into(),
384            ));
385        }
386        for (i, hash) in self.blob_versioned_hashes.iter().enumerate() {
387            if hash[0] != 0x01 {
388                return Err(SignerError::SigningFailed(format!(
389                    "blob_versioned_hashes[{i}] must start with version byte 0x01"
390                )));
391            }
392        }
393        let hash = self.signing_hash();
394        let sig = signer.sign_digest(&hash)?;
395        let y_parity = sig.v - 27;
396
397        let mut items = Vec::new();
398        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
399        items.extend_from_slice(&rlp::encode_u64(self.nonce));
400        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
401        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
402        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
403        items.extend_from_slice(&rlp::encode_bytes(&self.to));
404        items.extend_from_slice(&rlp::encode_u128(self.value));
405        items.extend_from_slice(&rlp::encode_bytes(&self.data));
406        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
407        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_blob_gas));
408        let mut hash_items = Vec::new();
409        for h in &self.blob_versioned_hashes {
410            hash_items.extend_from_slice(&rlp::encode_bytes(h));
411        }
412        items.extend_from_slice(&rlp::encode_list(&hash_items));
413        items.extend_from_slice(&rlp::encode_u64(y_parity));
414        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
415        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
416
417        let mut raw = vec![0x03];
418        raw.extend_from_slice(&rlp::encode_list(&items));
419
420        Ok(SignedTransaction { raw })
421    }
422}
423
424// ─── Contract Address Prediction ───────────────────────────────────
425
426/// Predict the contract address deployed via CREATE.
427///
428/// `keccak256(RLP([sender, nonce]))[12..32]`
429pub fn create_address(sender: &[u8; 20], nonce: u64) -> [u8; 20] {
430    let mut items = Vec::new();
431    items.extend_from_slice(&rlp::encode_bytes(sender));
432    items.extend_from_slice(&rlp::encode_u64(nonce));
433    let rlp_data = rlp::encode_list(&items);
434    let hash = keccak256(&rlp_data);
435    let mut addr = [0u8; 20];
436    addr.copy_from_slice(&hash[12..]);
437    addr
438}
439
440/// Predict the contract address deployed via CREATE2 (EIP-1014).
441///
442/// `keccak256(0xFF || sender || salt || keccak256(init_code))[12..32]`
443pub fn create2_address(sender: &[u8; 20], salt: &[u8; 32], init_code: &[u8]) -> [u8; 20] {
444    let code_hash = keccak256(init_code);
445    let mut buf = Vec::with_capacity(1 + 20 + 32 + 32);
446    buf.push(0xFF);
447    buf.extend_from_slice(sender);
448    buf.extend_from_slice(salt);
449    buf.extend_from_slice(&code_hash);
450    let hash = keccak256(&buf);
451    let mut addr = [0u8; 20];
452    addr.copy_from_slice(&hash[12..]);
453    addr
454}
455
456// ─── EIP-1271: Contract Signature ──────────────────────────────────
457
458/// EIP-1271 magic value returned by `isValidSignature` on success.
459pub const EIP1271_MAGIC: [u8; 4] = [0x16, 0x26, 0xBA, 0x7E];
460
461/// Encode an `isValidSignature(bytes32, bytes)` call for EIP-1271.
462///
463/// Returns the ABI-encoded calldata suitable for `eth_call`.
464pub fn encode_is_valid_signature(hash: &[u8; 32], signature: &[u8]) -> Vec<u8> {
465    // Function selector: keccak256("isValidSignature(bytes32,bytes)")[..4]
466    let selector = &keccak256(b"isValidSignature(bytes32,bytes)")[..4];
467
468    let mut calldata = Vec::new();
469    calldata.extend_from_slice(selector);
470    // hash (bytes32) — padded to 32 bytes
471    calldata.extend_from_slice(hash);
472    // offset to bytes data (64 bytes from start of params)
473    let mut offset = [0u8; 32];
474    offset[31] = 64;
475    calldata.extend_from_slice(&offset);
476    // length of signature
477    let mut len_buf = [0u8; 32];
478    len_buf[28..32].copy_from_slice(&(signature.len() as u32).to_be_bytes());
479    calldata.extend_from_slice(&len_buf);
480    // signature data (padded to 32-byte boundary)
481    calldata.extend_from_slice(signature);
482    let padding = (32 - (signature.len() % 32)) % 32;
483    calldata.extend_from_slice(&vec![0u8; padding]);
484
485    calldata
486}
487
488// ─── Helpers ───────────────────────────────────────────────────────
489
490fn keccak256(data: &[u8]) -> [u8; 32] {
491    super::keccak256(data)
492}
493
494fn encode_address(to: &Option<[u8; 20]>) -> Vec<u8> {
495    match to {
496        Some(addr) => rlp::encode_bytes(addr),
497        None => rlp::encode_bytes(&[]),
498    }
499}
500
501fn strip_leading_zeros(data: &[u8; 32]) -> Vec<u8> {
502    let start = data.iter().position(|b| *b != 0).unwrap_or(31);
503    data[start..].to_vec()
504}
505
506// ─── Signed Transaction Decoding ───────────────────────────────────
507
508/// The type of an Ethereum transaction.
509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510pub enum TxType {
511    /// Pre-EIP-2718 legacy transaction.
512    Legacy,
513    /// EIP-2930 (Type 1) — access list transaction.
514    Type1AccessList,
515    /// EIP-1559 (Type 2) — dynamic fee transaction.
516    Type2DynamicFee,
517    /// EIP-4844 (Type 3) — blob transaction.
518    Type3Blob,
519}
520
521/// A decoded signed Ethereum transaction.
522#[derive(Debug, Clone)]
523pub struct DecodedTransaction {
524    /// Transaction type.
525    pub tx_type: TxType,
526    /// Chain ID.
527    pub chain_id: u64,
528    /// Sender nonce.
529    pub nonce: u64,
530    /// Recipient address (`None` for contract creation).
531    pub to: Option<[u8; 20]>,
532    /// Value in wei (as raw bytes, big-endian).
533    pub value: Vec<u8>,
534    /// Calldata.
535    pub data: Vec<u8>,
536    /// Gas limit.
537    pub gas_limit: u64,
538    /// Gas price (Legacy/Type 1) or max_fee_per_gas (Type 2/3).
539    pub gas_price_or_max_fee: Vec<u8>,
540    /// Max priority fee per gas (Type 2/3 only, empty for Legacy/Type 1).
541    pub max_priority_fee: Vec<u8>,
542    /// Signature v / y_parity.
543    pub v: u64,
544    /// Signature r (32 bytes).
545    pub r: [u8; 32],
546    /// Signature s (32 bytes).
547    pub s: [u8; 32],
548    /// Recovered signer address (20 bytes).
549    pub from: [u8; 20],
550    /// Transaction hash.
551    pub tx_hash: [u8; 32],
552}
553
554/// Decode a signed transaction from raw bytes and recover the signer.
555///
556/// Supports Legacy, Type 1 (EIP-2930), Type 2 (EIP-1559), and Type 3 (EIP-4844).
557///
558/// # Example
559/// ```no_run
560/// use chains_sdk::ethereum::transaction::decode_signed_tx;
561///
562/// fn example(raw_tx: &[u8]) {
563///     let decoded = decode_signed_tx(raw_tx).unwrap();
564///     println!("From: 0x{}", hex::encode(decoded.from));
565///     println!("Type: {:?}", decoded.tx_type);
566///     println!("Nonce: {}", decoded.nonce);
567/// }
568/// ```
569pub fn decode_signed_tx(raw: &[u8]) -> Result<DecodedTransaction, SignerError> {
570    if raw.is_empty() {
571        return Err(SignerError::ParseError("empty transaction".into()));
572    }
573
574    let tx_hash = keccak256(raw);
575
576    match raw[0] {
577        // EIP-2718 typed transactions: first byte < 0x7F
578        0x01 => decode_type1_tx(raw, tx_hash),
579        0x02 => decode_type2_tx(raw, tx_hash),
580        0x03 => decode_type3_tx(raw, tx_hash),
581        // Legacy: first byte >= 0xC0 (RLP list prefix)
582        0xC0..=0xFF => decode_legacy_tx(raw, tx_hash),
583        b => Err(SignerError::ParseError(format!(
584            "unknown tx type byte: 0x{b:02x}"
585        ))),
586    }
587}
588
589fn decode_legacy_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
590    let items = rlp::decode_list_items(raw)?;
591    if items.len() != 9 {
592        return Err(SignerError::ParseError(format!(
593            "legacy tx: expected 9 RLP items, got {}",
594            items.len()
595        )));
596    }
597
598    let nonce = items[0].as_u64()?;
599    let gas_price_bytes = items[1].as_bytes()?;
600    validate_uint256_bytes(gas_price_bytes, "legacy tx gas_price")?;
601    let gas_price = gas_price_bytes.to_vec();
602    let gas_limit = items[2].as_u64()?;
603    let to_bytes = items[3].as_bytes()?;
604    let to = decode_to_address(to_bytes)?;
605    let value_bytes = items[4].as_bytes()?;
606    validate_uint256_bytes(value_bytes, "legacy tx value")?;
607    let value = value_bytes.to_vec();
608    let data = items[5].as_bytes()?.to_vec();
609    let v = items[6].as_u64()?;
610    let r = pad_to_32(items[7].as_bytes()?, "legacy tx r")?;
611    let s = pad_to_32(items[8].as_bytes()?, "legacy tx s")?;
612
613    // EIP-155: chain_id = (v - 35) / 2
614    let (chain_id, recovery_id) = if v >= 35 {
615        if v <= 36 {
616            return Err(SignerError::ParseError(format!(
617                "legacy tx: non-canonical EIP-155 v value {v}"
618            )));
619        }
620        ((v - 35) / 2, ((v - 35) % 2) as u8)
621    } else if v == 27 || v == 28 {
622        (0, (v - 27) as u8)
623    } else {
624        return Err(SignerError::ParseError(format!(
625            "legacy tx: invalid v value {v}"
626        )));
627    };
628
629    // Reconstruct signing payload for ecrecover
630    let mut sign_items = Vec::new();
631    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
632    sign_items.extend_from_slice(&rlp::encode_bytes(&gas_price));
633    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
634    sign_items.extend_from_slice(&encode_address(&to));
635    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
636    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
637    if chain_id > 0 {
638        sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
639        sign_items.extend_from_slice(&rlp::encode_u64(0));
640        sign_items.extend_from_slice(&rlp::encode_u64(0));
641    }
642    let signing_hash = keccak256(&rlp::encode_list(&sign_items));
643
644    let from = recover_signer(&signing_hash, &r, &s, recovery_id)?;
645
646    Ok(DecodedTransaction {
647        tx_type: TxType::Legacy,
648        chain_id,
649        nonce,
650        to,
651        value,
652        data,
653        gas_limit,
654        gas_price_or_max_fee: gas_price,
655        max_priority_fee: vec![],
656        v,
657        r,
658        s,
659        from,
660        tx_hash,
661    })
662}
663
664fn decode_type1_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
665    let items = rlp::decode_list_items(&raw[1..])?;
666    if items.len() != 11 {
667        return Err(SignerError::ParseError(format!(
668            "type1 tx: expected 11 items, got {}",
669            items.len()
670        )));
671    }
672
673    let chain_id = items[0].as_u64()?;
674    let nonce = items[1].as_u64()?;
675    let gas_price_bytes = items[2].as_bytes()?;
676    validate_uint256_bytes(gas_price_bytes, "type1 tx gas_price")?;
677    let gas_price = gas_price_bytes.to_vec();
678    let gas_limit = items[3].as_u64()?;
679    let to_bytes = items[4].as_bytes()?;
680    let to = decode_to_address(to_bytes)?;
681    let value_bytes = items[5].as_bytes()?;
682    validate_uint256_bytes(value_bytes, "type1 tx value")?;
683    let value = value_bytes.to_vec();
684    let data = items[6].as_bytes()?.to_vec();
685    validate_access_list(&items[7], "type1 tx")?;
686    let y_parity = items[8].as_u64()?;
687    let r = pad_to_32(items[9].as_bytes()?, "type1 tx r")?;
688    let s = pad_to_32(items[10].as_bytes()?, "type1 tx s")?;
689
690    // Reconstruct signing hash
691    let mut sign_items = Vec::new();
692    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
693    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
694    sign_items.extend_from_slice(&rlp::encode_bytes(&gas_price));
695    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
696    sign_items.extend_from_slice(&encode_address(&to));
697    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
698    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
699    // Re-encode the access list from the decoded items
700    sign_items.extend_from_slice(&re_encode_rlp_item(&items[7]));
701    let mut payload = vec![0x01];
702    payload.extend_from_slice(&rlp::encode_list(&sign_items));
703    let signing_hash = keccak256(&payload);
704
705    if y_parity > 1 {
706        return Err(SignerError::ParseError(format!(
707            "type1: invalid y_parity {y_parity}"
708        )));
709    }
710    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
711
712    Ok(DecodedTransaction {
713        tx_type: TxType::Type1AccessList,
714        chain_id,
715        nonce,
716        to,
717        value,
718        data,
719        gas_limit,
720        gas_price_or_max_fee: gas_price,
721        max_priority_fee: vec![],
722        v: y_parity,
723        r,
724        s,
725        from,
726        tx_hash,
727    })
728}
729
730fn decode_type2_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
731    let items = rlp::decode_list_items(&raw[1..])?;
732    if items.len() != 12 {
733        return Err(SignerError::ParseError(format!(
734            "type2 tx: expected 12 items, got {}",
735            items.len()
736        )));
737    }
738
739    let chain_id = items[0].as_u64()?;
740    let nonce = items[1].as_u64()?;
741    let max_priority_fee_bytes = items[2].as_bytes()?;
742    validate_uint256_bytes(max_priority_fee_bytes, "type2 tx max_priority_fee_per_gas")?;
743    let max_priority_fee = max_priority_fee_bytes.to_vec();
744    let max_fee_bytes = items[3].as_bytes()?;
745    validate_uint256_bytes(max_fee_bytes, "type2 tx max_fee_per_gas")?;
746    let max_fee = max_fee_bytes.to_vec();
747    if cmp_uint256_be(&max_fee, &max_priority_fee) == Ordering::Less {
748        return Err(SignerError::ParseError(
749            "type2 tx: max_fee_per_gas cannot be lower than max_priority_fee_per_gas".into(),
750        ));
751    }
752    let gas_limit = items[4].as_u64()?;
753    let to_bytes = items[5].as_bytes()?;
754    let to = decode_to_address(to_bytes)?;
755    let value_bytes = items[6].as_bytes()?;
756    validate_uint256_bytes(value_bytes, "type2 tx value")?;
757    let value = value_bytes.to_vec();
758    let data = items[7].as_bytes()?.to_vec();
759    validate_access_list(&items[8], "type2 tx")?;
760    let y_parity = items[9].as_u64()?;
761    let r = pad_to_32(items[10].as_bytes()?, "type2 tx r")?;
762    let s = pad_to_32(items[11].as_bytes()?, "type2 tx s")?;
763
764    // Reconstruct signing hash
765    let mut sign_items = Vec::new();
766    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
767    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
768    sign_items.extend_from_slice(&rlp::encode_bytes(&max_priority_fee));
769    sign_items.extend_from_slice(&rlp::encode_bytes(&max_fee));
770    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
771    sign_items.extend_from_slice(&encode_address(&to));
772    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
773    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
774    sign_items.extend_from_slice(&re_encode_rlp_item(&items[8]));
775    let mut payload = vec![0x02];
776    payload.extend_from_slice(&rlp::encode_list(&sign_items));
777    let signing_hash = keccak256(&payload);
778
779    if y_parity > 1 {
780        return Err(SignerError::ParseError(format!(
781            "type2: invalid y_parity {y_parity}"
782        )));
783    }
784    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
785
786    Ok(DecodedTransaction {
787        tx_type: TxType::Type2DynamicFee,
788        chain_id,
789        nonce,
790        to,
791        value,
792        data,
793        gas_limit,
794        gas_price_or_max_fee: max_fee,
795        max_priority_fee,
796        v: y_parity,
797        r,
798        s,
799        from,
800        tx_hash,
801    })
802}
803
804fn decode_type3_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
805    let items = rlp::decode_list_items(&raw[1..])?;
806    if items.len() != 14 {
807        return Err(SignerError::ParseError(format!(
808            "type3 tx: expected 14 items, got {}",
809            items.len()
810        )));
811    }
812
813    let chain_id = items[0].as_u64()?;
814    let nonce = items[1].as_u64()?;
815    let max_priority_fee_bytes = items[2].as_bytes()?;
816    validate_uint256_bytes(max_priority_fee_bytes, "type3 tx max_priority_fee_per_gas")?;
817    let max_priority_fee = max_priority_fee_bytes.to_vec();
818    let max_fee_bytes = items[3].as_bytes()?;
819    validate_uint256_bytes(max_fee_bytes, "type3 tx max_fee_per_gas")?;
820    let max_fee = max_fee_bytes.to_vec();
821    if cmp_uint256_be(&max_fee, &max_priority_fee) == Ordering::Less {
822        return Err(SignerError::ParseError(
823            "type3 tx: max_fee_per_gas cannot be lower than max_priority_fee_per_gas".into(),
824        ));
825    }
826    let gas_limit = items[4].as_u64()?;
827    let to_bytes = items[5].as_bytes()?;
828    let to = decode_to_address(to_bytes)?.ok_or_else(|| {
829        SignerError::ParseError("type3 tx: contract creation is not allowed".into())
830    })?;
831    let value_bytes = items[6].as_bytes()?;
832    validate_uint256_bytes(value_bytes, "type3 tx value")?;
833    let value = value_bytes.to_vec();
834    let data = items[7].as_bytes()?.to_vec();
835    validate_access_list(&items[8], "type3 tx")?;
836    let max_fee_per_blob_gas_bytes = items[9].as_bytes()?;
837    validate_uint256_bytes(max_fee_per_blob_gas_bytes, "type3 tx max_fee_per_blob_gas")?;
838    validate_blob_hashes(&items[10])?;
839    let y_parity = items[11].as_u64()?;
840    let r = pad_to_32(items[12].as_bytes()?, "type3 tx r")?;
841    let s = pad_to_32(items[13].as_bytes()?, "type3 tx s")?;
842
843    // Reconstruct signing hash
844    let mut sign_items = Vec::new();
845    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
846    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
847    sign_items.extend_from_slice(&rlp::encode_bytes(&max_priority_fee));
848    sign_items.extend_from_slice(&rlp::encode_bytes(&max_fee));
849    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
850    sign_items.extend_from_slice(&rlp::encode_bytes(&to));
851    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
852    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
853    sign_items.extend_from_slice(&re_encode_rlp_item(&items[8]));
854    sign_items.extend_from_slice(&re_encode_rlp_item(&items[9]));
855    sign_items.extend_from_slice(&re_encode_rlp_item(&items[10]));
856    let mut payload = vec![0x03];
857    payload.extend_from_slice(&rlp::encode_list(&sign_items));
858    let signing_hash = keccak256(&payload);
859
860    if y_parity > 1 {
861        return Err(SignerError::ParseError(format!(
862            "type3: invalid y_parity {y_parity}"
863        )));
864    }
865    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
866
867    Ok(DecodedTransaction {
868        tx_type: TxType::Type3Blob,
869        chain_id,
870        nonce,
871        to: Some(to),
872        value,
873        data,
874        gas_limit,
875        gas_price_or_max_fee: max_fee,
876        max_priority_fee,
877        v: y_parity,
878        r,
879        s,
880        from,
881        tx_hash,
882    })
883}
884
885/// Re-encode a decoded RLP item back to bytes.
886fn re_encode_rlp_item(item: &rlp::RlpItem) -> Vec<u8> {
887    match item {
888        rlp::RlpItem::Bytes(b) => rlp::encode_bytes(b),
889        rlp::RlpItem::List(items) => {
890            let mut inner = Vec::new();
891            for i in items {
892                inner.extend_from_slice(&re_encode_rlp_item(i));
893            }
894            rlp::encode_list(&inner)
895        }
896    }
897}
898
899/// Decode the `to` field: empty = contract creation, 20 bytes = address, otherwise error.
900fn decode_to_address(bytes: &[u8]) -> Result<Option<[u8; 20]>, SignerError> {
901    match bytes.len() {
902        0 => Ok(None),
903        20 => {
904            let mut addr = [0u8; 20];
905            addr.copy_from_slice(bytes);
906            Ok(Some(addr))
907        }
908        n => Err(SignerError::ParseError(format!(
909            "invalid to address length: expected 0 or 20, got {n}"
910        ))),
911    }
912}
913
914fn validate_uint256_bytes(bytes: &[u8], field: &str) -> Result<(), SignerError> {
915    if bytes.len() > 32 {
916        return Err(SignerError::ParseError(format!(
917            "{field} exceeds uint256 size ({} bytes)",
918            bytes.len()
919        )));
920    }
921    if bytes.len() > 1 && bytes[0] == 0 {
922        return Err(SignerError::ParseError(format!(
923            "{field} has non-canonical leading zero"
924        )));
925    }
926    if bytes.len() == 1 && bytes[0] == 0 {
927        return Err(SignerError::ParseError(format!(
928            "{field} has non-canonical zero encoding"
929        )));
930    }
931    Ok(())
932}
933
934fn trim_leading_zeros(bytes: &[u8]) -> &[u8] {
935    let mut idx = 0usize;
936    while idx < bytes.len() && bytes[idx] == 0 {
937        idx += 1;
938    }
939    &bytes[idx..]
940}
941
942fn cmp_uint256_be(lhs: &[u8], rhs: &[u8]) -> Ordering {
943    let lhs = trim_leading_zeros(lhs);
944    let rhs = trim_leading_zeros(rhs);
945    lhs.len().cmp(&rhs.len()).then_with(|| lhs.cmp(rhs))
946}
947
948fn validate_access_list(item: &rlp::RlpItem, tx_type: &str) -> Result<(), SignerError> {
949    let entries = match item {
950        rlp::RlpItem::List(entries) => entries,
951        _ => {
952            return Err(SignerError::ParseError(format!(
953                "{tx_type}: access_list must be an RLP list"
954            )));
955        }
956    };
957
958    for (entry_idx, entry) in entries.iter().enumerate() {
959        let parts = match entry {
960            rlp::RlpItem::List(parts) => parts,
961            _ => {
962                return Err(SignerError::ParseError(format!(
963                    "{tx_type}: access_list[{entry_idx}] must be a 2-item list"
964                )));
965            }
966        };
967        if parts.len() != 2 {
968            return Err(SignerError::ParseError(format!(
969                "{tx_type}: access_list[{entry_idx}] must contain [address, storageKeys]"
970            )));
971        }
972
973        let addr = parts[0].as_bytes()?;
974        if addr.len() != 20 {
975            return Err(SignerError::ParseError(format!(
976                "{tx_type}: access_list[{entry_idx}] address must be 20 bytes"
977            )));
978        }
979
980        let keys = match &parts[1] {
981            rlp::RlpItem::List(keys) => keys,
982            _ => {
983                return Err(SignerError::ParseError(format!(
984                    "{tx_type}: access_list[{entry_idx}] storageKeys must be a list"
985                )));
986            }
987        };
988        for (key_idx, key) in keys.iter().enumerate() {
989            let key_bytes = key.as_bytes()?;
990            if key_bytes.len() != 32 {
991                return Err(SignerError::ParseError(format!(
992                    "{tx_type}: access_list[{entry_idx}] storage key {key_idx} must be 32 bytes"
993                )));
994            }
995        }
996    }
997    Ok(())
998}
999
1000fn validate_blob_hashes(item: &rlp::RlpItem) -> Result<(), SignerError> {
1001    let hashes = match item {
1002        rlp::RlpItem::List(hashes) => hashes,
1003        _ => {
1004            return Err(SignerError::ParseError(
1005                "type3 tx: blob_versioned_hashes must be an RLP list".into(),
1006            ));
1007        }
1008    };
1009
1010    if hashes.is_empty() {
1011        return Err(SignerError::ParseError(
1012            "type3 tx: blob_versioned_hashes must not be empty".into(),
1013        ));
1014    }
1015
1016    for (idx, hash_item) in hashes.iter().enumerate() {
1017        let hash = hash_item.as_bytes()?;
1018        if hash.len() != 32 {
1019            return Err(SignerError::ParseError(format!(
1020                "type3 tx: blob_versioned_hashes[{idx}] must be 32 bytes"
1021            )));
1022        }
1023        if hash[0] != 0x01 {
1024            return Err(SignerError::ParseError(format!(
1025                "type3 tx: blob_versioned_hashes[{idx}] must start with 0x01"
1026            )));
1027        }
1028    }
1029    Ok(())
1030}
1031
1032/// Recover the signer address from a message hash and ECDSA signature.
1033fn recover_signer(
1034    hash: &[u8; 32],
1035    r: &[u8; 32],
1036    s: &[u8; 32],
1037    recovery_id: u8,
1038) -> Result<[u8; 20], SignerError> {
1039    use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey};
1040
1041    if recovery_id > 1 {
1042        return Err(SignerError::InvalidSignature(format!(
1043            "invalid recovery id: {recovery_id}, expected 0 or 1"
1044        )));
1045    }
1046
1047    let mut sig_bytes = [0u8; 64];
1048    sig_bytes[..32].copy_from_slice(r);
1049    sig_bytes[32..].copy_from_slice(s);
1050    let sig = K256Signature::from_bytes((&sig_bytes).into())
1051        .map_err(|e| SignerError::InvalidSignature(format!("invalid sig: {e}")))?;
1052    if sig.normalize_s().is_some() {
1053        return Err(SignerError::InvalidSignature(
1054            "non-canonical high-s signature".into(),
1055        ));
1056    }
1057    let rid = RecoveryId::new(recovery_id != 0, false);
1058    let key = VerifyingKey::recover_from_prehash(hash, &sig, rid)
1059        .map_err(|e| SignerError::InvalidSignature(format!("ecrecover: {e}")))?;
1060
1061    let uncompressed = key.to_encoded_point(false);
1062    let pub_bytes = &uncompressed.as_bytes()[1..]; // skip 0x04 prefix
1063    let addr_hash = keccak256(pub_bytes);
1064    let mut addr = [0u8; 20];
1065    addr.copy_from_slice(&addr_hash[12..]);
1066    Ok(addr)
1067}
1068
1069fn pad_to_32(data: &[u8], field: &str) -> Result<[u8; 32], SignerError> {
1070    if data.is_empty() {
1071        return Err(SignerError::ParseError(format!(
1072            "{field}: signature component cannot be empty"
1073        )));
1074    }
1075    if data.len() == 1 && data[0] == 0 {
1076        return Err(SignerError::ParseError(format!(
1077            "{field}: signature component cannot be zero"
1078        )));
1079    }
1080    if data.len() > 1 && data[0] == 0 {
1081        return Err(SignerError::ParseError(format!(
1082            "{field}: signature component has non-canonical leading zero"
1083        )));
1084    }
1085    if data.len() > 32 {
1086        return Err(SignerError::ParseError(format!(
1087            "{field}: signature component too large: {} bytes (max 32)",
1088            data.len()
1089        )));
1090    }
1091    let mut buf = [0u8; 32];
1092    buf[32 - data.len()..].copy_from_slice(data);
1093    Ok(buf)
1094}
1095
1096#[cfg(test)]
1097#[allow(clippy::unwrap_used, clippy::expect_used)]
1098mod tests {
1099    use super::*;
1100    use crate::traits::{KeyPair, Signer};
1101
1102    #[test]
1103    fn test_legacy_tx_sign_recoverable() {
1104        let signer = EthereumSigner::generate().unwrap();
1105        let tx = LegacyTransaction {
1106            nonce: 0,
1107            gas_price: 20_000_000_000, // 20 Gwei
1108            gas_limit: 21_000,
1109            to: Some([0xBB; 20]),
1110            value: 1_000_000_000_000_000_000, // 1 ETH
1111            data: vec![],
1112            chain_id: 1,
1113        };
1114        let signed = tx.sign(&signer).unwrap();
1115        let raw = signed.raw_tx();
1116        assert!(!raw.is_empty());
1117        // Must be valid RLP
1118        let decoded = rlp::decode(raw).unwrap();
1119        let items = decoded.as_list().unwrap();
1120        assert_eq!(items.len(), 9); // nonce, gasPrice, gasLimit, to, value, data, v, r, s
1121    }
1122
1123    #[test]
1124    fn test_legacy_tx_hash_deterministic() {
1125        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1126        let tx = LegacyTransaction {
1127            nonce: 5,
1128            gas_price: 30_000_000_000,
1129            gas_limit: 21_000,
1130            to: Some([0xCC; 20]),
1131            value: 0,
1132            data: vec![0xDE, 0xAD],
1133            chain_id: 1,
1134        };
1135        let signed1 = tx.sign(&signer).unwrap();
1136        let signed2 = tx.sign(&signer).unwrap();
1137        // RFC 6979 deterministic: same tx + same key = same signature
1138        assert_eq!(signed1.tx_hash(), signed2.tx_hash());
1139    }
1140
1141    #[test]
1142    fn test_legacy_contract_creation() {
1143        let signer = EthereumSigner::generate().unwrap();
1144        let tx = LegacyTransaction {
1145            nonce: 0,
1146            gas_price: 20_000_000_000,
1147            gas_limit: 1_000_000,
1148            to: None, // contract creation
1149            value: 0,
1150            data: vec![0x60, 0x00], // minimal bytecode
1151            chain_id: 1,
1152        };
1153        let signed = tx.sign(&signer).unwrap();
1154        assert!(!signed.raw_tx().is_empty());
1155    }
1156
1157    #[test]
1158    fn test_eip2930_tx_type1_prefix() {
1159        let signer = EthereumSigner::generate().unwrap();
1160        let tx = EIP2930Transaction {
1161            chain_id: 1,
1162            nonce: 0,
1163            gas_price: 20_000_000_000,
1164            gas_limit: 21_000,
1165            to: Some([0xAA; 20]),
1166            value: 1_000_000_000_000_000_000,
1167            data: vec![],
1168            access_list: vec![([0xDD; 20], vec![[0xEE; 32]])],
1169        };
1170        let signed = tx.sign(&signer).unwrap();
1171        assert_eq!(signed.raw_tx()[0], 0x01, "Type 1 prefix");
1172    }
1173
1174    #[test]
1175    fn test_eip1559_tx_type2_prefix() {
1176        let signer = EthereumSigner::generate().unwrap();
1177        let tx = EIP1559Transaction {
1178            chain_id: 1,
1179            nonce: 0,
1180            max_priority_fee_per_gas: 2_000_000_000,
1181            max_fee_per_gas: 100_000_000_000,
1182            gas_limit: 21_000,
1183            to: Some([0xAA; 20]),
1184            value: 1_000_000_000_000_000_000,
1185            data: vec![],
1186            access_list: vec![],
1187        };
1188        let signed = tx.sign(&signer).unwrap();
1189        assert_eq!(signed.raw_tx()[0], 0x02, "Type 2 prefix");
1190    }
1191
1192    #[test]
1193    fn test_eip1559_different_nonces_different_hashes() {
1194        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1195        let base = EIP1559Transaction {
1196            chain_id: 1,
1197            nonce: 0,
1198            max_priority_fee_per_gas: 2_000_000_000,
1199            max_fee_per_gas: 100_000_000_000,
1200            gas_limit: 21_000,
1201            to: Some([0xAA; 20]),
1202            value: 0,
1203            data: vec![],
1204            access_list: vec![],
1205        };
1206        let mut tx2 = base.clone();
1207        tx2.nonce = 1;
1208        let h1 = base.sign(&signer).unwrap().tx_hash();
1209        let h2 = tx2.sign(&signer).unwrap().tx_hash();
1210        assert_ne!(h1, h2);
1211    }
1212
1213    #[test]
1214    fn test_eip4844_tx_type3_prefix() {
1215        let signer = EthereumSigner::generate().unwrap();
1216        let tx = EIP4844Transaction {
1217            chain_id: 1,
1218            nonce: 0,
1219            max_priority_fee_per_gas: 2_000_000_000,
1220            max_fee_per_gas: 100_000_000_000,
1221            gas_limit: 21_000,
1222            to: [0xAA; 20],
1223            value: 0,
1224            data: vec![],
1225            access_list: vec![],
1226            max_fee_per_blob_gas: 1_000_000_000,
1227            blob_versioned_hashes: vec![[0x01; 32]],
1228        };
1229        let signed = tx.sign(&signer).unwrap();
1230        assert_eq!(signed.raw_tx()[0], 0x03, "Type 3 prefix");
1231    }
1232
1233    #[test]
1234    fn test_create_address_known_vector() {
1235        // Known: sender 0x0000...0000 nonce 0 → specific address
1236        let sender = [0u8; 20];
1237        let addr = create_address(&sender, 0);
1238        assert_eq!(addr.len(), 20);
1239        // Verify it's deterministic
1240        assert_eq!(addr, create_address(&sender, 0));
1241        // Different nonce → different address
1242        assert_ne!(addr, create_address(&sender, 1));
1243    }
1244
1245    #[test]
1246    fn test_create2_address_eip1014_vector() {
1247        // EIP-1014 test vector #1:
1248        // sender = 0x0000000000000000000000000000000000000000
1249        // salt = 0x00...00
1250        // init_code = 0x00
1251        // expected = keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]
1252        let sender = [0u8; 20];
1253        let salt = [0u8; 32];
1254        let addr = create2_address(&sender, &salt, &[0x00]);
1255        // Verify determinism
1256        assert_eq!(addr, create2_address(&sender, &salt, &[0x00]));
1257        // Different init_code → different address
1258        assert_ne!(addr, create2_address(&sender, &salt, &[0x01]));
1259    }
1260
1261    #[test]
1262    fn test_eip1271_encode() {
1263        let hash = [0xAA; 32];
1264        let sig = vec![0xBB; 65];
1265        let calldata = encode_is_valid_signature(&hash, &sig);
1266        // First 4 bytes = function selector
1267        assert_eq!(
1268            &calldata[..4],
1269            &keccak256(b"isValidSignature(bytes32,bytes)")[..4]
1270        );
1271        // Next 32 bytes = hash
1272        assert_eq!(&calldata[4..36], &hash);
1273    }
1274
1275    #[test]
1276    fn test_raw_tx_hex_format() {
1277        let signer = EthereumSigner::generate().unwrap();
1278        let tx = EIP1559Transaction {
1279            chain_id: 1,
1280            nonce: 0,
1281            max_priority_fee_per_gas: 0,
1282            max_fee_per_gas: 0,
1283            gas_limit: 21_000,
1284            to: Some([0; 20]),
1285            value: 0,
1286            data: vec![],
1287            access_list: vec![],
1288        };
1289        let hex = tx.sign(&signer).unwrap().raw_tx_hex();
1290        assert!(hex.starts_with("0x02"), "should start with 0x02");
1291    }
1292
1293    #[test]
1294    fn test_signed_tx_hash_is_keccak_of_raw() {
1295        let signer = EthereumSigner::generate().unwrap();
1296        let tx = EIP1559Transaction {
1297            chain_id: 1,
1298            nonce: 42,
1299            max_priority_fee_per_gas: 1_000_000,
1300            max_fee_per_gas: 50_000_000_000,
1301            gas_limit: 100_000,
1302            to: Some([0xFF; 20]),
1303            value: 500_000_000_000_000,
1304            data: vec![0x01, 0x02, 0x03],
1305            access_list: vec![],
1306        };
1307        let signed = tx.sign(&signer).unwrap();
1308        let expected = keccak256(signed.raw_tx());
1309        assert_eq!(signed.tx_hash(), expected);
1310    }
1311
1312    // ─── Signed Transaction Decoding Tests ─────────────────────────
1313
1314    #[test]
1315    fn test_decode_legacy_roundtrip() {
1316        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1317        let tx = LegacyTransaction {
1318            nonce: 7,
1319            gas_price: 20_000_000_000,
1320            gas_limit: 21_000,
1321            to: Some([0xBB; 20]),
1322            value: 1_000_000_000_000_000_000,
1323            data: vec![0xAB, 0xCD],
1324            chain_id: 1,
1325        };
1326        let signed = tx.sign(&signer).unwrap();
1327        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1328
1329        assert_eq!(decoded.tx_type, TxType::Legacy);
1330        assert_eq!(decoded.chain_id, 1);
1331        assert_eq!(decoded.nonce, 7);
1332        assert_eq!(decoded.gas_limit, 21_000);
1333        assert_eq!(decoded.to, Some([0xBB; 20]));
1334        assert_eq!(decoded.data, vec![0xAB, 0xCD]);
1335        assert_eq!(decoded.from, signer.address());
1336        assert_eq!(decoded.tx_hash, signed.tx_hash());
1337    }
1338
1339    #[test]
1340    fn test_decode_type1_roundtrip() {
1341        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1342        let tx = EIP2930Transaction {
1343            chain_id: 1,
1344            nonce: 3,
1345            gas_price: 30_000_000_000,
1346            gas_limit: 50_000,
1347            to: Some([0xCC; 20]),
1348            value: 0,
1349            data: vec![0x01],
1350            access_list: vec![([0xDD; 20], vec![[0xEE; 32]])],
1351        };
1352        let signed = tx.sign(&signer).unwrap();
1353        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1354
1355        assert_eq!(decoded.tx_type, TxType::Type1AccessList);
1356        assert_eq!(decoded.chain_id, 1);
1357        assert_eq!(decoded.nonce, 3);
1358        assert_eq!(decoded.from, signer.address());
1359    }
1360
1361    #[test]
1362    fn test_decode_type2_roundtrip() {
1363        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1364        let tx = EIP1559Transaction {
1365            chain_id: 1,
1366            nonce: 42,
1367            max_priority_fee_per_gas: 2_000_000_000,
1368            max_fee_per_gas: 100_000_000_000,
1369            gas_limit: 21_000,
1370            to: Some([0xAA; 20]),
1371            value: 500_000_000_000_000,
1372            data: vec![],
1373            access_list: vec![],
1374        };
1375        let signed = tx.sign(&signer).unwrap();
1376        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1377
1378        assert_eq!(decoded.tx_type, TxType::Type2DynamicFee);
1379        assert_eq!(decoded.chain_id, 1);
1380        assert_eq!(decoded.nonce, 42);
1381        assert_eq!(decoded.gas_limit, 21_000);
1382        assert_eq!(decoded.to, Some([0xAA; 20]));
1383        assert_eq!(decoded.from, signer.address());
1384        assert_eq!(decoded.tx_hash, signed.tx_hash());
1385    }
1386
1387    #[test]
1388    fn test_decode_type3_roundtrip() {
1389        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1390        let tx = EIP4844Transaction {
1391            chain_id: 1,
1392            nonce: 0,
1393            max_priority_fee_per_gas: 1_000_000_000,
1394            max_fee_per_gas: 50_000_000_000,
1395            gas_limit: 100_000,
1396            to: [0xFF; 20],
1397            value: 0,
1398            data: vec![],
1399            access_list: vec![],
1400            max_fee_per_blob_gas: 1_000_000_000,
1401            blob_versioned_hashes: vec![[0x01; 32]],
1402        };
1403        let signed = tx.sign(&signer).unwrap();
1404        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1405
1406        assert_eq!(decoded.tx_type, TxType::Type3Blob);
1407        assert_eq!(decoded.from, signer.address());
1408        assert_eq!(decoded.chain_id, 1);
1409    }
1410
1411    #[test]
1412    fn test_decode_contract_creation() {
1413        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1414        let tx = LegacyTransaction {
1415            nonce: 0,
1416            gas_price: 20_000_000_000,
1417            gas_limit: 1_000_000,
1418            to: None,
1419            value: 0,
1420            data: vec![0x60, 0x00],
1421            chain_id: 1,
1422        };
1423        let signed = tx.sign(&signer).unwrap();
1424        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1425
1426        assert_eq!(decoded.to, None, "contract creation has no 'to'");
1427        assert_eq!(decoded.from, signer.address());
1428    }
1429
1430    #[test]
1431    fn test_decode_empty_tx_rejected() {
1432        assert!(decode_signed_tx(&[]).is_err());
1433    }
1434
1435    #[test]
1436    fn test_decode_unknown_type_rejected() {
1437        assert!(decode_signed_tx(&[0x04, 0x00]).is_err());
1438    }
1439
1440    #[test]
1441    fn test_decode_legacy_rejects_non_canonical_eip155_v_35() {
1442        let mut items = Vec::new();
1443        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(0)); // nonce
1444        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); // gas_price
1445        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(21_000)); // gas_limit
1446        items.extend_from_slice(&crate::ethereum::rlp::encode_bytes(&[0x11; 20])); // to
1447        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(0)); // value
1448        items.extend_from_slice(&crate::ethereum::rlp::encode_bytes(&[])); // data
1449        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(35)); // invalid/non-canonical
1450        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); // r
1451        items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); // s
1452        let raw = crate::ethereum::rlp::encode_list(&items);
1453
1454        let result = decode_signed_tx(&raw);
1455        assert!(
1456            matches!(
1457                result,
1458                Err(SignerError::ParseError(ref msg))
1459                    if msg.contains("non-canonical EIP-155 v value")
1460            ),
1461            "expected ParseError for non-canonical v, got {result:?}"
1462        );
1463    }
1464
1465    #[test]
1466    fn test_recover_signer_rejects_high_s_signature() {
1467        let signer = EthereumSigner::generate().unwrap();
1468        let sig = signer.sign(b"tx-high-s-reject").unwrap();
1469        let digest = keccak256(b"tx-high-s-reject");
1470        let mut hash = [0u8; 32];
1471        hash.copy_from_slice(&digest);
1472        let recovery_id = (sig.v - 27) as u8;
1473
1474        // secp256k1 n - 1 (valid scalar, always high-s)
1475        let high_s = [
1476            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
1477            0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C,
1478            0xD0, 0x36, 0x41, 0x40,
1479        ];
1480
1481        assert!(recover_signer(&hash, &sig.r, &high_s, recovery_id).is_err());
1482    }
1483
1484    #[test]
1485    fn test_decode_signer_matches_across_types() {
1486        // Same signer, same nonce → different tx types should all recover same address
1487        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1488        let expected_addr = signer.address();
1489
1490        let legacy = LegacyTransaction {
1491            nonce: 0,
1492            gas_price: 1,
1493            gas_limit: 21000,
1494            to: Some([0xAA; 20]),
1495            value: 0,
1496            data: vec![],
1497            chain_id: 1,
1498        }
1499        .sign(&signer)
1500        .unwrap();
1501
1502        let type2 = EIP1559Transaction {
1503            chain_id: 1,
1504            nonce: 0,
1505            max_priority_fee_per_gas: 1,
1506            max_fee_per_gas: 1,
1507            gas_limit: 21000,
1508            to: Some([0xAA; 20]),
1509            value: 0,
1510            data: vec![],
1511            access_list: vec![],
1512        }
1513        .sign(&signer)
1514        .unwrap();
1515
1516        assert_eq!(
1517            decode_signed_tx(legacy.raw_tx()).unwrap().from,
1518            expected_addr
1519        );
1520        assert_eq!(
1521            decode_signed_tx(type2.raw_tx()).unwrap().from,
1522            expected_addr
1523        );
1524    }
1525
1526    #[test]
1527    fn test_eip155_known_signing_vector() {
1528        // EIP-155 reference vector:
1529        // https://eips.ethereum.org/EIPS/eip-155
1530        let signer = EthereumSigner::from_bytes(&[0x46; 32]).unwrap();
1531        let tx = LegacyTransaction {
1532            nonce: 9,
1533            gas_price: 20_000_000_000,
1534            gas_limit: 21_000,
1535            to: Some([0x35; 20]),
1536            value: 1_000_000_000_000_000_000,
1537            data: vec![],
1538            chain_id: 1,
1539        };
1540        let signed = tx.sign(&signer).unwrap();
1541        assert_eq!(
1542            hex::encode(signed.raw_tx()),
1543            "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"
1544        );
1545    }
1546
1547    #[test]
1548    fn test_decode_rejects_non_canonical_zero_nonce_encoding() {
1549        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1550        let tx = LegacyTransaction {
1551            nonce: 0,
1552            gas_price: 1,
1553            gas_limit: 21_000,
1554            to: Some([0x11; 20]),
1555            value: 0,
1556            data: vec![],
1557            chain_id: 1,
1558        };
1559        let signed = tx.sign(&signer).unwrap();
1560        let mut malformed = signed.raw_tx().to_vec();
1561
1562        // Legacy list header is either 1 byte (short) or 1+len_of_len bytes (long).
1563        let header_len = if malformed[0] <= 0xF7 {
1564            1usize
1565        } else {
1566            1 + usize::from(malformed[0] - 0xF7)
1567        };
1568        assert_eq!(malformed[header_len], 0x80, "nonce=0 canonical encoding");
1569        malformed[header_len] = 0x00; // non-canonical integer zero
1570
1571        let err = decode_signed_tx(&malformed).unwrap_err().to_string();
1572        assert!(err.contains("non-canonical"));
1573    }
1574
1575    #[test]
1576    fn test_sign_rejects_zero_chain_id() {
1577        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1578
1579        let legacy = LegacyTransaction {
1580            nonce: 0,
1581            gas_price: 1,
1582            gas_limit: 21_000,
1583            to: Some([0xAA; 20]),
1584            value: 0,
1585            data: vec![],
1586            chain_id: 0,
1587        };
1588        assert!(legacy.sign(&signer).is_err());
1589
1590        let type1 = EIP2930Transaction {
1591            chain_id: 0,
1592            nonce: 0,
1593            gas_price: 1,
1594            gas_limit: 21_000,
1595            to: Some([0xAA; 20]),
1596            value: 0,
1597            data: vec![],
1598            access_list: vec![],
1599        };
1600        assert!(type1.sign(&signer).is_err());
1601
1602        let type2 = EIP1559Transaction {
1603            chain_id: 0,
1604            nonce: 0,
1605            max_priority_fee_per_gas: 1,
1606            max_fee_per_gas: 1,
1607            gas_limit: 21_000,
1608            to: Some([0xAA; 20]),
1609            value: 0,
1610            data: vec![],
1611            access_list: vec![],
1612        };
1613        assert!(type2.sign(&signer).is_err());
1614
1615        let type3 = EIP4844Transaction {
1616            chain_id: 0,
1617            nonce: 0,
1618            max_priority_fee_per_gas: 1,
1619            max_fee_per_gas: 1,
1620            gas_limit: 21_000,
1621            to: [0xAA; 20],
1622            value: 0,
1623            data: vec![],
1624            access_list: vec![],
1625            max_fee_per_blob_gas: 1,
1626            blob_versioned_hashes: vec![[0x01; 32]],
1627        };
1628        assert!(type3.sign(&signer).is_err());
1629    }
1630
1631    #[test]
1632    fn test_type2_sign_rejects_priority_fee_above_max_fee() {
1633        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1634        let tx = EIP1559Transaction {
1635            chain_id: 1,
1636            nonce: 0,
1637            max_priority_fee_per_gas: 10,
1638            max_fee_per_gas: 9,
1639            gas_limit: 21_000,
1640            to: Some([0xAA; 20]),
1641            value: 0,
1642            data: vec![],
1643            access_list: vec![],
1644        };
1645        assert!(tx.sign(&signer).is_err());
1646    }
1647
1648    #[test]
1649    fn test_type3_sign_rejects_invalid_blob_hashes() {
1650        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1651
1652        let empty = EIP4844Transaction {
1653            chain_id: 1,
1654            nonce: 0,
1655            max_priority_fee_per_gas: 1,
1656            max_fee_per_gas: 1,
1657            gas_limit: 21_000,
1658            to: [0xAA; 20],
1659            value: 0,
1660            data: vec![],
1661            access_list: vec![],
1662            max_fee_per_blob_gas: 1,
1663            blob_versioned_hashes: vec![],
1664        };
1665        assert!(empty.sign(&signer).is_err());
1666
1667        let mut bad_hash = [0u8; 32];
1668        bad_hash[0] = 0x02;
1669        let invalid = EIP4844Transaction {
1670            chain_id: 1,
1671            nonce: 0,
1672            max_priority_fee_per_gas: 1,
1673            max_fee_per_gas: 1,
1674            gas_limit: 21_000,
1675            to: [0xAA; 20],
1676            value: 0,
1677            data: vec![],
1678            access_list: vec![],
1679            max_fee_per_blob_gas: 1,
1680            blob_versioned_hashes: vec![bad_hash],
1681        };
1682        assert!(invalid.sign(&signer).is_err());
1683    }
1684
1685    #[test]
1686    fn test_decode_type2_rejects_max_fee_below_priority_fee() {
1687        let mut items = Vec::new();
1688        items.extend_from_slice(&rlp::encode_u64(1)); // chain_id
1689        items.extend_from_slice(&rlp::encode_u64(0)); // nonce
1690        items.extend_from_slice(&rlp::encode_u64(2)); // max_priority_fee_per_gas
1691        items.extend_from_slice(&rlp::encode_u64(1)); // max_fee_per_gas (invalid)
1692        items.extend_from_slice(&rlp::encode_u64(21_000)); // gas_limit
1693        items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); // to
1694        items.extend_from_slice(&rlp::encode_u64(0)); // value
1695        items.extend_from_slice(&rlp::encode_bytes(&[])); // data
1696        items.extend_from_slice(&rlp::encode_empty_list()); // access_list
1697        items.extend_from_slice(&rlp::encode_u64(0)); // y_parity
1698        items.extend_from_slice(&rlp::encode_u64(1)); // r
1699        items.extend_from_slice(&rlp::encode_u64(1)); // s
1700        let mut raw = vec![0x02];
1701        raw.extend_from_slice(&rlp::encode_list(&items));
1702
1703        let err = decode_signed_tx(&raw).unwrap_err().to_string();
1704        assert!(err.contains("max_fee_per_gas cannot be lower"));
1705    }
1706
1707    #[test]
1708    fn test_decode_type1_rejects_non_list_access_list() {
1709        let mut items = Vec::new();
1710        items.extend_from_slice(&rlp::encode_u64(1)); // chain_id
1711        items.extend_from_slice(&rlp::encode_u64(0)); // nonce
1712        items.extend_from_slice(&rlp::encode_u64(1)); // gas_price
1713        items.extend_from_slice(&rlp::encode_u64(21_000)); // gas_limit
1714        items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); // to
1715        items.extend_from_slice(&rlp::encode_u64(0)); // value
1716        items.extend_from_slice(&rlp::encode_bytes(&[])); // data
1717        items.extend_from_slice(&rlp::encode_bytes(&[0x01])); // malformed access_list
1718        items.extend_from_slice(&rlp::encode_u64(0)); // y_parity
1719        items.extend_from_slice(&rlp::encode_u64(1)); // r
1720        items.extend_from_slice(&rlp::encode_u64(1)); // s
1721        let mut raw = vec![0x01];
1722        raw.extend_from_slice(&rlp::encode_list(&items));
1723
1724        let err = decode_signed_tx(&raw).unwrap_err().to_string();
1725        assert!(err.contains("access_list must be an RLP list"));
1726    }
1727
1728    #[test]
1729    fn test_decode_type3_rejects_contract_creation_and_bad_blob_version() {
1730        let mut blob_items = Vec::new();
1731        let mut bad_hash = [0u8; 32];
1732        bad_hash[0] = 0x02;
1733        blob_items.extend_from_slice(&rlp::encode_bytes(&bad_hash));
1734
1735        let mut items = Vec::new();
1736        items.extend_from_slice(&rlp::encode_u64(1)); // chain_id
1737        items.extend_from_slice(&rlp::encode_u64(0)); // nonce
1738        items.extend_from_slice(&rlp::encode_u64(1)); // max_priority_fee_per_gas
1739        items.extend_from_slice(&rlp::encode_u64(1)); // max_fee_per_gas
1740        items.extend_from_slice(&rlp::encode_u64(21_000)); // gas_limit
1741        items.extend_from_slice(&rlp::encode_bytes(&[])); // to (invalid for type3)
1742        items.extend_from_slice(&rlp::encode_u64(0)); // value
1743        items.extend_from_slice(&rlp::encode_bytes(&[])); // data
1744        items.extend_from_slice(&rlp::encode_empty_list()); // access_list
1745        items.extend_from_slice(&rlp::encode_u64(1)); // max_fee_per_blob_gas
1746        items.extend_from_slice(&rlp::encode_list(&blob_items)); // blob hashes
1747        items.extend_from_slice(&rlp::encode_u64(0)); // y_parity
1748        items.extend_from_slice(&rlp::encode_u64(1)); // r
1749        items.extend_from_slice(&rlp::encode_u64(1)); // s
1750        let mut raw = vec![0x03];
1751        raw.extend_from_slice(&rlp::encode_list(&items));
1752
1753        let err = decode_signed_tx(&raw).unwrap_err().to_string();
1754        assert!(err.contains("contract creation is not allowed"));
1755    }
1756
1757    #[test]
1758    fn test_decode_type3_rejects_bad_blob_version_hash() {
1759        let mut blob_items = Vec::new();
1760        let mut bad_hash = [0u8; 32];
1761        bad_hash[0] = 0x02;
1762        blob_items.extend_from_slice(&rlp::encode_bytes(&bad_hash));
1763
1764        let mut items = Vec::new();
1765        items.extend_from_slice(&rlp::encode_u64(1)); // chain_id
1766        items.extend_from_slice(&rlp::encode_u64(0)); // nonce
1767        items.extend_from_slice(&rlp::encode_u64(1)); // max_priority_fee_per_gas
1768        items.extend_from_slice(&rlp::encode_u64(1)); // max_fee_per_gas
1769        items.extend_from_slice(&rlp::encode_u64(21_000)); // gas_limit
1770        items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); // to
1771        items.extend_from_slice(&rlp::encode_u64(0)); // value
1772        items.extend_from_slice(&rlp::encode_bytes(&[])); // data
1773        items.extend_from_slice(&rlp::encode_empty_list()); // access_list
1774        items.extend_from_slice(&rlp::encode_u64(1)); // max_fee_per_blob_gas
1775        items.extend_from_slice(&rlp::encode_list(&blob_items)); // blob hashes
1776        items.extend_from_slice(&rlp::encode_u64(0)); // y_parity
1777        items.extend_from_slice(&rlp::encode_u64(1)); // r
1778        items.extend_from_slice(&rlp::encode_u64(1)); // s
1779        let mut raw = vec![0x03];
1780        raw.extend_from_slice(&rlp::encode_list(&items));
1781
1782        let err = decode_signed_tx(&raw).unwrap_err().to_string();
1783        assert!(err.contains("must start with 0x01"));
1784    }
1785}