1use super::rlp;
33use super::EthereumSigner;
34use crate::error::SignerError;
35use core::cmp::Ordering;
36
37#[derive(Debug, Clone)]
41pub struct SignedTransaction {
42 raw: Vec<u8>,
44}
45
46impl SignedTransaction {
47 #[must_use]
51 pub fn raw_tx(&self) -> &[u8] {
52 &self.raw
53 }
54
55 #[must_use]
57 pub fn tx_hash(&self) -> [u8; 32] {
58 keccak256(&self.raw)
59 }
60
61 #[must_use]
63 pub fn raw_tx_hex(&self) -> String {
64 format!("0x{}", hex::encode(&self.raw))
65 }
66}
67
68#[derive(Debug, Clone)]
74pub struct LegacyTransaction {
75 pub nonce: u64,
77 pub gas_price: u128,
79 pub gas_limit: u64,
81 pub to: Option<[u8; 20]>,
83 pub value: u128,
85 pub data: Vec<u8>,
87 pub chain_id: u64,
89}
90
91impl LegacyTransaction {
92 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 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 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 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#[derive(Debug, Clone)]
158pub struct EIP2930Transaction {
159 pub chain_id: u64,
161 pub nonce: u64,
163 pub gas_price: u128,
165 pub gas_limit: u64,
167 pub to: Option<[u8; 20]>,
169 pub value: u128,
171 pub data: Vec<u8>,
173 pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
175}
176
177impl EIP2930Transaction {
178 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]; payload.extend_from_slice(&rlp::encode_list(&items));
192 keccak256(&payload)
193 }
194
195 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; 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]; raw.extend_from_slice(&rlp::encode_list(&items));
221
222 Ok(SignedTransaction { raw })
223 }
224}
225
226#[derive(Debug, Clone)]
233pub struct EIP1559Transaction {
234 pub chain_id: u64,
236 pub nonce: u64,
238 pub max_priority_fee_per_gas: u128,
240 pub max_fee_per_gas: u128,
242 pub gas_limit: u64,
244 pub to: Option<[u8; 20]>,
246 pub value: u128,
248 pub data: Vec<u8>,
250 pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
252}
253
254impl EIP1559Transaction {
255 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]; payload.extend_from_slice(&rlp::encode_list(&items));
270 keccak256(&payload)
271 }
272
273 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; 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]; raw.extend_from_slice(&rlp::encode_list(&items));
305
306 Ok(SignedTransaction { raw })
307 }
308}
309
310#[derive(Debug, Clone)]
318pub struct EIP4844Transaction {
319 pub chain_id: u64,
321 pub nonce: u64,
323 pub max_priority_fee_per_gas: u128,
325 pub max_fee_per_gas: u128,
327 pub gas_limit: u64,
329 pub to: [u8; 20],
331 pub value: u128,
333 pub data: Vec<u8>,
335 pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
337 pub max_fee_per_blob_gas: u128,
339 pub blob_versioned_hashes: Vec<[u8; 32]>,
341}
342
343impl EIP4844Transaction {
344 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 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]; payload.extend_from_slice(&rlp::encode_list(&items));
366 keccak256(&payload)
367 }
368
369 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
424pub 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
440pub 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
456pub const EIP1271_MAGIC: [u8; 4] = [0x16, 0x26, 0xBA, 0x7E];
460
461pub fn encode_is_valid_signature(hash: &[u8; 32], signature: &[u8]) -> Vec<u8> {
465 let selector = &keccak256(b"isValidSignature(bytes32,bytes)")[..4];
467
468 let mut calldata = Vec::new();
469 calldata.extend_from_slice(selector);
470 calldata.extend_from_slice(hash);
472 let mut offset = [0u8; 32];
474 offset[31] = 64;
475 calldata.extend_from_slice(&offset);
476 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 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
488fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510pub enum TxType {
511 Legacy,
513 Type1AccessList,
515 Type2DynamicFee,
517 Type3Blob,
519}
520
521#[derive(Debug, Clone)]
523pub struct DecodedTransaction {
524 pub tx_type: TxType,
526 pub chain_id: u64,
528 pub nonce: u64,
530 pub to: Option<[u8; 20]>,
532 pub value: Vec<u8>,
534 pub data: Vec<u8>,
536 pub gas_limit: u64,
538 pub gas_price_or_max_fee: Vec<u8>,
540 pub max_priority_fee: Vec<u8>,
542 pub v: u64,
544 pub r: [u8; 32],
546 pub s: [u8; 32],
548 pub from: [u8; 20],
550 pub tx_hash: [u8; 32],
552}
553
554pub 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 0x01 => decode_type1_tx(raw, tx_hash),
579 0x02 => decode_type2_tx(raw, tx_hash),
580 0x03 => decode_type3_tx(raw, tx_hash),
581 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 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 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 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 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 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 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
885fn 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
899fn 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
1032fn 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..]; 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, gas_limit: 21_000,
1109 to: Some([0xBB; 20]),
1110 value: 1_000_000_000_000_000_000, 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 let decoded = rlp::decode(raw).unwrap();
1119 let items = decoded.as_list().unwrap();
1120 assert_eq!(items.len(), 9); }
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 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, value: 0,
1150 data: vec![0x60, 0x00], 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 let sender = [0u8; 20];
1237 let addr = create_address(&sender, 0);
1238 assert_eq!(addr.len(), 20);
1239 assert_eq!(addr, create_address(&sender, 0));
1241 assert_ne!(addr, create_address(&sender, 1));
1243 }
1244
1245 #[test]
1246 fn test_create2_address_eip1014_vector() {
1247 let sender = [0u8; 20];
1253 let salt = [0u8; 32];
1254 let addr = create2_address(&sender, &salt, &[0x00]);
1255 assert_eq!(addr, create2_address(&sender, &salt, &[0x00]));
1257 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 assert_eq!(
1268 &calldata[..4],
1269 &keccak256(b"isValidSignature(bytes32,bytes)")[..4]
1270 );
1271 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 #[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)); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(21_000)); items.extend_from_slice(&crate::ethereum::rlp::encode_bytes(&[0x11; 20])); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(0)); items.extend_from_slice(&crate::ethereum::rlp::encode_bytes(&[])); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(35)); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); items.extend_from_slice(&crate::ethereum::rlp::encode_u64(1)); 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 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 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 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 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; 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)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(2)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(21_000)); items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_bytes(&[])); items.extend_from_slice(&rlp::encode_empty_list()); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); 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)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(21_000)); items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_bytes(&[])); items.extend_from_slice(&rlp::encode_bytes(&[0x01])); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); 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)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(21_000)); items.extend_from_slice(&rlp::encode_bytes(&[])); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_bytes(&[])); items.extend_from_slice(&rlp::encode_empty_list()); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_list(&blob_items)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); 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)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(21_000)); items.extend_from_slice(&rlp::encode_bytes(&[0x11; 20])); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_bytes(&[])); items.extend_from_slice(&rlp::encode_empty_list()); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_list(&blob_items)); items.extend_from_slice(&rlp::encode_u64(0)); items.extend_from_slice(&rlp::encode_u64(1)); items.extend_from_slice(&rlp::encode_u64(1)); 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}