diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 6efdbb7e6..0788879f4 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -221,6 +221,7 @@ impl TransactionKind { value, data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::Deploy(data) => { @@ -238,6 +239,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::DeployErc20(_) => { @@ -256,6 +258,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } Self::FtOnTransfer(args) => { @@ -277,6 +280,7 @@ impl TransactionKind { value, data: Vec::new(), access_list: Vec::new(), + authorization_list: Vec::new(), } } else { let from = Self::get_implicit_address(engine_account); @@ -313,6 +317,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } } } @@ -345,6 +350,7 @@ impl TransactionKind { value, data: Vec::new(), access_list: Vec::new(), + authorization_list: Vec::new(), } }, |erc20_address| { @@ -372,6 +378,7 @@ impl TransactionKind { value: Wei::zero(), data, access_list: Vec::new(), + authorization_list: Vec::new(), } }, ) @@ -474,6 +481,7 @@ impl TransactionKind { value: Wei::zero(), data: method_name.as_bytes().to_vec(), access_list: Vec::new(), + authorization_list: Vec::new(), } } diff --git a/engine-tests/src/tests/transaction.rs b/engine-tests/src/tests/transaction.rs index 13282da3f..14f0ebd54 100644 --- a/engine-tests/src/tests/transaction.rs +++ b/engine-tests/src/tests/transaction.rs @@ -5,9 +5,12 @@ use crate::prelude::Wei; use crate::prelude::{H256, U256}; use crate::utils; use aurora_engine::parameters::SubmitResult; -use aurora_engine_transactions::eip_2930; use aurora_engine_transactions::eip_2930::Transaction2930; +use aurora_engine_transactions::eip_7702::{AuthorizationTuple, Transaction7702}; +use aurora_engine_transactions::{eip_2930, eip_7702}; use aurora_engine_types::borsh::BorshDeserialize; +use aurora_engine_types::types::Address; +use aurora_engine_types::H160; use std::convert::TryFrom; use std::iter; @@ -22,6 +25,15 @@ const CONTRACT_BALANCE: Wei = Wei::new_u64(0x0de0b6b3a7640000); const EXAMPLE_TX_HEX: &str = "02f8c101010a8207d0833d090094cccccccccccccccccccccccccccccccccccccccc8000f85bf85994ccccccccccccccccccccccccccccccccccccccccf842a00000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000180a0d671815898b8dd34321adbba4cb6a57baa7017323c26946f3719b00e70c755c2a03528b9efe3be57ea65a933d1e6bbf3b7d0c78830138883c1201e0c641fee6464"; +/* + PUSH20 0xd16292912e440956828f1a847ca6efc8412f45b6 [0x73d16292912e440956828f1a847ca6efc8412f45b6] + EXTCODESIZE [0x3B] + PUSH1 0x00 [0x6000] + SSTORE [0x55] + STOP [0x00] +*/ +const CONTRACT_CODE_EIP7702: &str = "73d16292912e440956828f1a847ca6efc8412f45b63B60005500"; + // Test taken from https://github.com/ethereum/tests/blob/develop/GeneralStateTests/stExample/eip1559.json // TODO(#170): generally support Ethereum tests #[test] @@ -161,6 +173,129 @@ fn test_access_list_tx_encoding_decoding() { ); } +#[test] +fn test_eip_7702_tx_encoding_decoding() { + let secret_key = example_signer().secret_key; + let transaction = eip7702_transaction(0); + + let signed_tx = utils::sign_eip_7702_transaction(transaction, &secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let decoded_tx = match EthTransactionKind::try_from(tx_bytes.as_slice()) { + Ok(EthTransactionKind::Eip7702(tx)) => tx, + Ok(_) => panic!("Unexpected transaction type"), + Err(e) => panic!("Transaction parsing failed: {e:?}"), + }; + + assert_eq!(signed_tx, decoded_tx); + assert_eq!( + signed_tx.sender().unwrap(), + utils::address_from_secret_key(&secret_key) + ); +} + +#[test] +fn test_eip_7702_success() { + // 0xa52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3, + let mut runner = utils::deploy_runner(); + let signer = example_signer(); + let signer_address = utils::address_from_secret_key(&signer.secret_key); + let contract_address = utils::address_from_hex(CONTRACT_ADDRESS); + let contract_code = hex::decode(CONTRACT_CODE_EIP7702).unwrap(); + + runner.create_address(signer_address, INITIAL_BALANCE, signer.nonce.into()); + runner.create_address_with_code( + contract_address, + CONTRACT_BALANCE, + CONTRACT_NONCE.into(), + contract_code.clone(), + ); + + let mut transaction = eip7702_transaction(0); + transaction.chain_id = runner.chain_id; + let signed_tx = utils::sign_eip_7702_transaction(transaction, &signer.secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let sender = "relay.aurora"; + let outcome = runner.call(utils::SUBMIT, sender, tx_bytes).unwrap(); + // Unwrapping execution results validates outcome + let result = SubmitResult::try_from_slice(&outcome.return_data.as_value().unwrap()).unwrap(); + + let delegated_designator = Address::decode("d16292912e440956828f1a847ca6efc8412f45b6").unwrap(); + + assert_eq!(result.gas_used, 68206); + assert_eq!(runner.get_nonce(signer_address), (signer.nonce + 1).into()); + assert_eq!(runner.get_balance(contract_address), CONTRACT_BALANCE); + assert_eq!(runner.get_nonce(contract_address), CONTRACT_NONCE.into()); + assert_eq!(runner.get_code(contract_address), contract_code); + // `EXTCODESIZE` should return size of `EF01` = 2 for delegated designator + assert_eq!( + runner.get_storage(contract_address, H256::zero()), + H256::from(H160::from_low_u64_be(2)) + ); + + // Authority address should increase Nonce + assert_eq!(runner.get_nonce(delegated_designator), 1.into()); + // Get delegated designator address + assert_eq!( + hex::encode(runner.get_code(delegated_designator)), + "ef0100cccccccccccccccccccccccccccccccccccccccc" + ); +} + +#[test] +fn test_eip_7702_wrong_auth_chain_id() { + let mut runner = utils::deploy_runner(); + let signer = example_signer(); + let signer_address = utils::address_from_secret_key(&signer.secret_key); + let contract_address = utils::address_from_hex(CONTRACT_ADDRESS); + // Contract for auth address: 0xa52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3 + let contract_code = + hex::decode("73a52a8a2229e3c512d6ed27b6e6e7d39958ca9fb33B60005500").unwrap(); + + runner.create_address(signer_address, INITIAL_BALANCE, signer.nonce.into()); + runner.create_address_with_code( + contract_address, + CONTRACT_BALANCE, + CONTRACT_NONCE.into(), + contract_code.clone(), + ); + + let mut transaction = eip7702_transaction(10); + transaction.chain_id = runner.chain_id; + let signed_tx = utils::sign_eip_7702_transaction(transaction, &signer.secret_key); + let tx_bytes: Vec = iter::once(eip_7702::TYPE_BYTE) + .chain(rlp::encode(&signed_tx)) + .collect(); + + let sender = "relay.aurora"; + let outcome = runner.call(utils::SUBMIT, sender, tx_bytes).unwrap(); + // Unwrapping execution results validates outcome + let result = SubmitResult::try_from_slice(&outcome.return_data.as_value().unwrap()).unwrap(); + + let delegated_designator = Address::decode("a52a8a2229e3c512d6ed27b6e6e7d39958ca9fb3").unwrap(); + + assert_eq!(result.gas_used, 50806); + assert_eq!(runner.get_nonce(signer_address), (signer.nonce + 1).into()); + assert_eq!(runner.get_balance(contract_address), CONTRACT_BALANCE); + assert_eq!(runner.get_nonce(contract_address), CONTRACT_NONCE.into()); + assert_eq!(runner.get_code(contract_address), contract_code); + // `EXTCODESIZE` should return zero, as `authorization_list` failed validation + assert_eq!( + runner.get_storage(contract_address, H256::zero()), + H256::zero() + ); + + // Authority address should not increase Nonce because authorization failed + assert_eq!(runner.get_nonce(delegated_designator), 0.into()); + // Get delegated designator address: in that particular case it should be empty + assert!(runner.get_code(delegated_designator).is_empty()); +} + fn encode_tx(signed_tx: &SignedTransaction1559) -> Vec { iter::once(eip_1559::TYPE_BYTE) .chain(rlp::encode(signed_tx)) @@ -177,6 +312,28 @@ fn example_signer() -> utils::Signer { } } +fn eip7702_transaction(auth_chain_id: u64) -> Transaction7702 { + Transaction7702 { + chain_id: 1, + nonce: INITIAL_NONCE.into(), + gas_limit: U256::from(0x3d0900), + max_fee_per_gas: U256::from(0x07d0), + max_priority_fee_per_gas: U256::from(0x0a), + to: utils::address_from_hex(CONTRACT_ADDRESS), + value: Wei::zero(), + data: vec![], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: auth_chain_id.into(), + address: utils::address_from_hex(CONTRACT_ADDRESS).raw(), + nonce: 0, + parity: 1.into(), + r: 2.into(), + s: 3.into(), + }], + } +} + fn example_transaction() -> Transaction1559 { Transaction1559 { chain_id: 1, diff --git a/engine-tests/src/utils/mod.rs b/engine-tests/src/utils/mod.rs index a1c1ec6b5..3cc4280c6 100644 --- a/engine-tests/src/utils/mod.rs +++ b/engine-tests/src/utils/mod.rs @@ -1,5 +1,17 @@ +#[cfg(not(feature = "ext-connector"))] +use crate::prelude::parameters::InitCallArgs; +use crate::prelude::parameters::{StartHashchainArgs, SubmitResult, TransactionStatus}; +use crate::prelude::transactions::{ + eip_1559::{self, SignedTransaction1559, Transaction1559}, + eip_2930::{self, SignedTransaction2930, Transaction2930}, + legacy::{LegacyEthSignedTransaction, TransactionLegacy}, +}; +use crate::prelude::{sdk, Address, Wei, H256, U256}; +use crate::utils::solidity::{ContractConstructor, DeployedContract}; use aurora_engine::engine::{EngineError, EngineErrorKind, GasPaymentError}; use aurora_engine::parameters::{SubmitArgs, ViewCallArgs}; +use aurora_engine_transactions::eip_7702; +use aurora_engine_transactions::eip_7702::{SignedTransaction7702, Transaction7702}; use aurora_engine_types::account_id::AccountId; use aurora_engine_types::borsh::BorshDeserialize; #[cfg(not(feature = "ext-connector"))] @@ -25,17 +37,6 @@ use rlp::RlpStream; use std::borrow::Cow; use std::sync::Arc; -#[cfg(not(feature = "ext-connector"))] -use crate::prelude::parameters::InitCallArgs; -use crate::prelude::parameters::{StartHashchainArgs, SubmitResult, TransactionStatus}; -use crate::prelude::transactions::{ - eip_1559::{self, SignedTransaction1559, Transaction1559}, - eip_2930::{self, SignedTransaction2930, Transaction2930}, - legacy::{LegacyEthSignedTransaction, TransactionLegacy}, -}; -use crate::prelude::{sdk, Address, Wei, H256, U256}; -use crate::utils::solidity::{ContractConstructor, DeployedContract}; - pub const DEFAULT_AURORA_ACCOUNT_ID: &str = "aurora"; pub const SUBMIT: &str = "submit"; pub const SUBMIT_WITH_ARGS: &str = "submit_with_args"; @@ -945,6 +946,28 @@ pub fn sign_eip_1559_transaction( } } +pub fn sign_eip_7702_transaction( + tx: Transaction7702, + secret_key: &SecretKey, +) -> SignedTransaction7702 { + let mut rlp_stream = RlpStream::new(); + rlp_stream.append(&eip_7702::TYPE_BYTE); + tx.rlp_append_unsigned(&mut rlp_stream); + let message_hash = sdk::keccak(rlp_stream.as_raw()); + let message = Message::parse_slice(message_hash.as_bytes()).unwrap(); + + let (signature, recovery_id) = libsecp256k1::sign(&message, secret_key); + let r = U256::from_big_endian(&signature.r.b32()); + let s = U256::from_big_endian(&signature.s.b32()); + + SignedTransaction7702 { + transaction: tx, + parity: recovery_id.serialize(), + r, + s, + } +} + pub fn address_from_secret_key(sk: &SecretKey) -> Address { let pk = PublicKey::from_secret_key(sk); let hash = sdk::keccak(&pk.serialize()[1..]); diff --git a/engine-tests/src/utils/solidity/erc20.rs b/engine-tests/src/utils/solidity/erc20.rs index 10a38491b..32c3f97bd 100644 --- a/engine-tests/src/utils/solidity/erc20.rs +++ b/engine-tests/src/utils/solidity/erc20.rs @@ -204,5 +204,6 @@ pub fn legacy_into_normalized_tx(tx: TransactionLegacy) -> NormalizedEthTransact value: tx.value, data: tx.data, access_list: Vec::new(), + authorization_list: Vec::new(), } } diff --git a/engine-transactions/src/backwards_compatibility.rs b/engine-transactions/src/backwards_compatibility.rs index 002562cfc..eeb5135d7 100644 --- a/engine-transactions/src/backwards_compatibility.rs +++ b/engine-transactions/src/backwards_compatibility.rs @@ -49,6 +49,9 @@ impl EthTransactionKindAdapter { tx.transaction.to = None; } } + EthTransactionKind::Eip7702(_) => { + // For Prague hard fork `tx.transaction.to` can't be `None` + } } } diff --git a/engine-transactions/src/eip_7702.rs b/engine-transactions/src/eip_7702.rs new file mode 100644 index 000000000..ba0754316 --- /dev/null +++ b/engine-transactions/src/eip_7702.rs @@ -0,0 +1,573 @@ +use crate::eip_2930::AccessTuple; +use crate::Error; +use aurora_engine_precompiles::secp256k1::ecrecover; +use aurora_engine_types::types::{Address, Wei}; +use aurora_engine_types::{Vec, H160, U256}; +use evm::executor::stack::Authorization; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Type indicator (per EIP-7702) +pub const TYPE_BYTE: u8 = 0x04; + +// EIP-7702 `MAGIC` number +pub const MAGIC: u8 = 0x5; + +/// The order of the secp256k1 curve, divided by two. Signatures that should be checked according +/// to EIP-2 should have an S value less than or equal to this. +/// +/// `57896044618658097711785492504343953926418782139537452191302581570759080747168` +pub const SECP256K1N_HALF: U256 = U256([ + 0xDFE9_2F46_681B_20A0, + 0x5D57_6E73_57A4_501D, + 0xFFFF_FFFF_FFFF_FFFF, + 0x7FFF_FFFF_FFFF_FFFF, +]); + +#[derive(Debug, Eq, PartialEq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct AuthorizationTuple { + pub chain_id: U256, + pub address: H160, + pub nonce: u64, + pub parity: U256, + pub r: U256, + pub s: U256, +} + +impl Decodable for AuthorizationTuple { + fn decode(rlp: &Rlp<'_>) -> Result { + let chain_id = rlp.val_at(0)?; + let address = rlp.val_at(1)?; + let nonce = rlp.val_at(2)?; + let parity = rlp.val_at(3)?; + let r = rlp.val_at(4)?; + let s = rlp.val_at(5)?; + Ok(Self { + chain_id, + address, + nonce, + parity, + r, + s, + }) + } +} + +/// EIP-7702 transaction kind from the Prague hard fork. +/// +/// See [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md) +/// for more details. +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Transaction7702 { + /// ID of chain which the transaction belongs. + pub chain_id: u64, + /// A monotonically increasing transaction counter for this sender + pub nonce: U256, + /// Determined by the sender and is optional. Priority Fee is also known as Miner Tip as it is + /// paid directly to block producers. + pub max_priority_fee_per_gas: U256, + /// Maximum amount the sender is willing to pay to get their transaction included in a block. + pub max_fee_per_gas: U256, + /// The maximum amount of gas the sender is willing to consume on a transaction. + pub gas_limit: U256, + /// The receiving address. + pub to: Address, + /// The amount of ETH to transfer. + pub value: Wei, + /// Arbitrary binary data for a contract call invocation. + pub data: Vec, + /// A list of addresses and storage keys that the transaction plans to access. + /// Accesses outside the list are possible, but become more expensive. + pub access_list: Vec, + /// A list of authorizations for EIP-7702 + pub authorization_list: Vec, +} + +impl Transaction7702 { + const TRANSACTION_FIELDS: usize = 10; + /// RLP encoding of the data for an unsigned message (used to make signature) + pub fn rlp_append_unsigned(&self, s: &mut RlpStream) { + self.rlp_append(s, Self::TRANSACTION_FIELDS); + } + + /// RLP encoding for a signed message (used to encode the transaction for sending to tx pool) + pub fn rlp_append_signed(&self, s: &mut RlpStream) { + self.rlp_append(s, SignedTransaction7702::TRANSACTION_FIELDS); + } + + fn rlp_append(&self, s: &mut RlpStream, list_len: usize) { + s.begin_list(list_len); + s.append(&self.chain_id); + s.append(&self.nonce); + s.append(&self.max_priority_fee_per_gas); + s.append(&self.max_fee_per_gas); + s.append(&self.gas_limit); + s.append(&self.to.raw()); + s.append(&self.value.raw()); + s.append(&self.data); + s.begin_list(self.access_list.len()); + for tuple in &self.access_list { + s.begin_list(2); + s.append(&tuple.address); + s.begin_list(tuple.storage_keys.len()); + for key in &tuple.storage_keys { + s.append(key); + } + } + s.begin_list(self.authorization_list.len()); + for tuple in &self.authorization_list { + s.begin_list(6); + s.append(&tuple.chain_id); + s.append(&tuple.address); + s.append(&tuple.nonce); + s.append(&tuple.parity); + s.append(&tuple.r); + s.append(&tuple.s); + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct SignedTransaction7702 { + pub transaction: Transaction7702, + /// The parity (0 for even, 1 for odd) of the y-value of a secp256k1 signature. + pub parity: u8, + pub r: U256, + pub s: U256, +} + +impl SignedTransaction7702 { + const TRANSACTION_FIELDS: usize = 13; + + pub fn sender(&self) -> Result { + let mut rlp_stream = RlpStream::new(); + rlp_stream.append(&TYPE_BYTE); + self.transaction.rlp_append_unsigned(&mut rlp_stream); + let message_hash = aurora_engine_sdk::keccak(rlp_stream.as_raw()); + ecrecover( + message_hash, + &super::vrs_to_arr(self.parity, self.r, self.s), + ) + .map_err(|_e| Error::EcRecover) + } + + pub fn authorization_list(&self) -> Result, Error> { + if self.transaction.authorization_list.is_empty() { + return Err(Error::EmptyAuthorizationList); + } + let current_tx_chain_id = U256::from(self.transaction.chain_id); + let mut authorization_list = Vec::with_capacity(self.transaction.authorization_list.len()); + // According to EIP-7702 we should validate each authorization. We shouldn't skip any of them. + // And just put `is_valid` flag to `false` if any of them is invalid. It's related to + // gas calculation, as each `authorization_list` must be charged, even if it's invalid. + for auth in &self.transaction.authorization_list { + // According to EIP-7702 step 1. validation, we should verify is + // `chain_id = 0 || current_chain_id`. + // AS `current_chain_id` we used `transaction.chain_id` as we will validate `chain_id` in + // Engine `submit_transaction` method. + + // Step 2 - validation logic inside EVM itself. + // Step 3. Checking: authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]) + // Validate the signature, as in tests it is possible to have invalid signatures values. + // Value `v` shouldn't be greater then 1 + let mut is_valid = (auth.chain_id.is_zero() || auth.chain_id == current_tx_chain_id) + && auth.parity <= U256::from(1); + + let v = u8::try_from(auth.parity.as_u64()).map_err(|_| Error::InvalidV)?; + // EIP-2 validation + if auth.s > SECP256K1N_HALF { + is_valid = false; + } + + let mut rlp_stream = RlpStream::new(); + rlp_stream.begin_list(3); + rlp_stream.append(&auth.chain_id); + rlp_stream.append(&auth.address); + rlp_stream.append(&auth.nonce); + + let message_bytes = [&[MAGIC], rlp_stream.as_raw()].concat(); + let signature_hash = aurora_engine_sdk::keccak(&message_bytes); + + let auth_address = ecrecover(signature_hash, &super::vrs_to_arr(v, auth.r, auth.s)); + let auth_address = auth_address.unwrap_or_else(|_| { + is_valid = false; + Address::default() + }); + + // Validations steps 2,4-9 0f EIP-7702 provided by EVM itself. + authorization_list.push(Authorization { + authority: auth_address.raw(), + address: auth.address, + nonce: auth.nonce, + is_valid, + }); + } + Ok(authorization_list) + } +} + +impl Encodable for SignedTransaction7702 { + fn rlp_append(&self, s: &mut RlpStream) { + self.transaction.rlp_append_signed(s); + s.append(&self.parity); + s.append(&self.r); + s.append(&self.s); + } +} + +impl Decodable for SignedTransaction7702 { + fn decode(rlp: &Rlp<'_>) -> Result { + if rlp.item_count() != Ok(Self::TRANSACTION_FIELDS) { + return Err(DecoderError::RlpIncorrectListLen); + } + let chain_id = rlp.val_at(0)?; + let nonce = rlp.val_at(1)?; + let max_priority_fee_per_gas = rlp.val_at(2)?; + let max_fee_per_gas = rlp.val_at(3)?; + let gas_limit = rlp.val_at(4)?; + let to = Address::new(rlp.val_at(5)?); + let value = Wei::new(rlp.val_at(6)?); + let data = rlp.val_at(7)?; + let access_list = rlp.list_at(8)?; + let authorization_list = rlp.list_at(9)?; + let parity = rlp.val_at(10)?; + let r = rlp.val_at(11)?; + let s = rlp.val_at(12)?; + Ok(Self { + transaction: Transaction7702 { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + data, + access_list, + authorization_list, + }, + parity, + r, + s, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rlp::RlpStream; + + #[test] + fn test_authorization_tuple_decode() { + let chain_id = 1.into(); + let address = H160::from_low_u64_be(0x1234); + let nonce = 1u64; + let parity = U256::zero(); + let r = 2.into(); + let s = 3.into(); + + let mut stream = RlpStream::new_list(6); + stream.append(&chain_id); + stream.append(&address); + stream.append(&nonce); + stream.append(&parity); + stream.append(&r); + stream.append(&s); + + let rlp = Rlp::new(stream.as_raw()); + let decoded: AuthorizationTuple = rlp.as_val().unwrap(); + + assert_eq!(decoded.chain_id, chain_id); + assert_eq!(decoded.address, address); + assert_eq!(decoded.nonce, nonce); + assert_eq!(decoded.parity, parity); + assert_eq!(decoded.r, r); + assert_eq!(decoded.s, s); + } + + #[test] + fn test_transaction7702_rlp_append_unsigned() { + let tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: U256::from(3), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: U256::zero(), + r: 2.into(), + s: 3.into(), + }], + }; + + let mut stream = RlpStream::new(); + tx.rlp_append_unsigned(&mut stream); + + let rlp = Rlp::new(stream.as_raw()); + assert_eq!( + rlp.item_count().unwrap(), + Transaction7702::TRANSACTION_FIELDS + ); + } + + #[test] + fn test_signed_transaction7702_rlp_encode_decode() { + let tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: U256::zero(), + r: 2.into(), + s: 3.into(), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 7.into(), + s: 8.into(), + }; + + let mut stream = RlpStream::new(); + signed_tx.rlp_append(&mut stream); + + let rlp = Rlp::new(stream.as_raw()); + let decoded: SignedTransaction7702 = rlp.as_val().unwrap(); + + assert_eq!(decoded, signed_tx); + } + + #[test] + fn test_signed_transaction7702_invalid_chain_id() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 2.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + // Fail + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(!auth_list[0].is_valid); + + // Success + tx.chain_id = 2; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + + // Success + tx.authorization_list = vec![AuthorizationTuple { + chain_id: U256::zero(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + } + + #[test] + fn test_signed_transaction7702_empty_auth_list() { + let tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + if let Err(err) = signed_tx.authorization_list() { + assert_eq!(err, Error::EmptyAuthorizationList); + } + } + + #[test] + fn test_signed_transaction7702_invalid_signature_v() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 2.into(), + r: 2.into(), + s: 3.into(), + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(!auth_list[0].is_valid); + + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: u8::MAX.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(!auth_list[0].is_valid); + + // Success + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 0.into(), + r: 2.into(), + s: 3.into(), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(auth_list[0].is_valid); + } + + #[test] + fn test_signed_transaction7702_invalid_signature_s() { + let mut tx = Transaction7702 { + chain_id: 1, + nonce: 1.into(), + max_priority_fee_per_gas: 2.into(), + max_fee_per_gas: 3.into(), + gas_limit: 4.into(), + to: Address::new(H160::from_low_u64_be(0x1234)), + value: Wei::new(5.into()), + data: vec![0x6], + access_list: vec![], + authorization_list: vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: 1.into(), + r: 2.into(), + s: SECP256K1N_HALF, + }], + }; + + let signed_tx = SignedTransaction7702 { + transaction: tx.clone(), + parity: 0, + r: 2.into(), + s: 3.into(), + }; + + // Success + let auth_list = signed_tx.authorization_list().unwrap(); + assert_eq!(auth_list.len(), 1); + assert!(auth_list[0].is_valid); + + // Fails + tx.authorization_list = vec![AuthorizationTuple { + chain_id: 1.into(), + address: H160::from_low_u64_be(0x1234), + nonce: 1u64, + parity: u8::MAX.into(), + r: 2.into(), + s: SECP256K1N_HALF + U256::from(1), + }]; + let signed_tx = SignedTransaction7702 { + transaction: tx, + parity: 0, + r: 2.into(), + s: 3.into(), + }; + let auth_list = signed_tx.authorization_list().unwrap(); + assert!(!auth_list[0].is_valid); + } +} diff --git a/engine-transactions/src/lib.rs b/engine-transactions/src/lib.rs index 05dbdf526..4fc6e9a29 100644 --- a/engine-transactions/src/lib.rs +++ b/engine-transactions/src/lib.rs @@ -4,12 +4,14 @@ use aurora_engine_types::types::{Address, Wei}; use aurora_engine_types::{vec, Vec, H160, U256}; use eip_2930::AccessTuple; +use evm::executor::stack::Authorization; use rlp::{Decodable, DecoderError, Rlp}; pub mod backwards_compatibility; pub mod eip_1559; pub mod eip_2930; pub mod eip_4844; +pub mod eip_7702; pub mod legacy; /// Typed Transaction Envelope (see `https://eips.ethereum.org/EIPS/eip-2718`) @@ -18,6 +20,7 @@ pub enum EthTransactionKind { Legacy(legacy::LegacyEthSignedTransaction), Eip2930(eip_2930::SignedTransaction2930), Eip1559(eip_1559::SignedTransaction1559), + Eip7702(eip_7702::SignedTransaction7702), } impl TryFrom<&[u8]> for EthTransactionKind { @@ -36,6 +39,10 @@ impl TryFrom<&[u8]> for EthTransactionKind { )?)) } else if bytes[0] == eip_4844::TYPE_BYTE { Err(Error::UnsupportedTransactionEip4844) + } else if bytes[0] == eip_7702::TYPE_BYTE { + Ok(Self::Eip7702(eip_7702::SignedTransaction7702::decode( + &Rlp::new(&bytes[1..]), + )?)) } else if bytes[0] <= 0x7f { Err(Error::UnknownTransactionType) } else if bytes[0] == 0xff { @@ -54,12 +61,16 @@ impl From<&EthTransactionKind> for Vec { EthTransactionKind::Legacy(tx) => { stream.append(tx); } + EthTransactionKind::Eip2930(tx) => { + stream.append(&eip_2930::TYPE_BYTE); + stream.append(tx); + } EthTransactionKind::Eip1559(tx) => { stream.append(&eip_1559::TYPE_BYTE); stream.append(tx); } - EthTransactionKind::Eip2930(tx) => { - stream.append(&eip_2930::TYPE_BYTE); + EthTransactionKind::Eip7702(tx) => { + stream.append(&eip_7702::TYPE_BYTE); stream.append(tx); } } @@ -80,13 +91,15 @@ pub struct NormalizedEthTransaction { pub value: Wei, pub data: Vec, pub access_list: Vec, + // Contains additional information - `chain_id` for each authorization item + pub authorization_list: Vec, } impl TryFrom for NormalizedEthTransaction { type Error = Error; fn try_from(kind: EthTransactionKind) -> Result { - use EthTransactionKind::{Eip1559, Eip2930, Legacy}; + use EthTransactionKind::{Eip1559, Eip2930, Eip7702, Legacy}; Ok(match kind { Legacy(tx) => Self { address: tx.sender()?, @@ -99,6 +112,7 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: vec![], + authorization_list: vec![], }, Eip2930(tx) => Self { address: tx.sender()?, @@ -111,6 +125,7 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: tx.transaction.access_list, + authorization_list: vec![], }, Eip1559(tx) => Self { address: tx.sender()?, @@ -123,6 +138,20 @@ impl TryFrom for NormalizedEthTransaction { value: tx.transaction.value, data: tx.transaction.data, access_list: tx.transaction.access_list, + authorization_list: vec![], + }, + Eip7702(tx) => Self { + address: tx.sender()?, + chain_id: Some(tx.transaction.chain_id), + nonce: tx.transaction.nonce, + gas_limit: tx.transaction.gas_limit, + max_priority_fee_per_gas: tx.transaction.max_priority_fee_per_gas, + max_fee_per_gas: tx.transaction.max_fee_per_gas, + to: Some(tx.transaction.to), + value: tx.transaction.value, + data: tx.transaction.data.clone(), + access_list: tx.transaction.access_list.clone(), + authorization_list: tx.authorization_list()?, }, }) } @@ -209,6 +238,7 @@ pub enum Error { #[cfg_attr(feature = "serde", serde(serialize_with = "decoder_err_to_str"))] RlpDecodeError(DecoderError), UnsupportedTransactionEip4844, + EmptyAuthorizationList, } #[cfg(feature = "serde")] @@ -229,6 +259,7 @@ impl Error { Self::IntegerConversion => "ERR_INTEGER_CONVERSION", Self::RlpDecodeError(_) => "ERR_TX_RLP_DECODE", Self::UnsupportedTransactionEip4844 => "ERR_UNSUPPORTED_TX_EIP4844", + Self::EmptyAuthorizationList => "ERR_EMPTY_AUTHORIZATION_LIST", } } } @@ -271,7 +302,7 @@ fn vrs_to_arr(v: u8, r: U256, s: U256) -> [u8; 65] { #[cfg(test)] mod tests { use super::{Error, EthTransactionKind}; - use crate::{eip_1559, eip_2930}; + use crate::{eip_1559, eip_2930, eip_7702}; #[test] fn test_try_parse_empty_input() { @@ -294,5 +325,9 @@ mod tests { EthTransactionKind::try_from([0x80].as_ref()), Err(Error::RlpDecodeError(_)) )); + assert!(matches!( + EthTransactionKind::try_from([eip_7702::TYPE_BYTE].as_ref()), + Err(Error::RlpDecodeError(_)) + )); } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs index ce217af35..18286f6ba 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -43,6 +43,7 @@ use aurora_engine_types::parameters::engine::FunctionCallArgsV2; use aurora_engine_types::types::EthGas; use core::cell::RefCell; use core::iter::once; +use evm::executor::stack::Authorization; /// Used as the first byte in the concatenation of data used to compute the blockhash. /// Could be useful in the future as a version byte, or to distinguish different types of blocks. @@ -583,6 +584,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } @@ -597,6 +599,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } @@ -611,7 +614,8 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { value: Wei, input: Vec, gas_limit: u64, - access_list: Vec<(H160, Vec)>, // See EIP-2930 + access_list: Vec<(H160, Vec)>, // See EIP-2930 + authorization_list: Vec, // See EIP-7702 handler: &mut P, ) -> EngineResult { let pause_flags = EnginePrecompilesPauser::from_io(self.io).paused(); @@ -626,7 +630,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { input, gas_limit, access_list, - Vec::new(), + authorization_list, ); let used_gas = executor.used_gas(); @@ -737,6 +741,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { Vec::new(), gas_limit, Vec::new(), + Vec::new(), handler, ) } @@ -810,6 +815,7 @@ impl<'env, I: IO + Copy, E: Env, M: ModExpAlgorithm> Engine<'env, I, E, M> { setup_receive_erc20_tokens_input(args, &recipient), u64::MAX, Vec::new(), // TODO: are there values we should put here? + Vec::new(), handler, ) .and_then(submit_result_or_err) @@ -1030,6 +1036,7 @@ pub fn submit_with_alt_modexp< tx.try_into() .map_err(|_e| EngineErrorKind::InvalidSignature)? }; + // Retrieve the signer of the transaction: let sender = transaction.address; @@ -1092,6 +1099,7 @@ pub fn submit_with_alt_modexp< .into_iter() .map(|a| (a.address, a.storage_keys)) .collect(); + let result = if let Some(receiver) = transaction.to { engine.call( &sender, @@ -1100,6 +1108,7 @@ pub fn submit_with_alt_modexp< transaction.data, gas_limit, access_list, + transaction.authorization_list, handler, ) // TODO: charge for storage @@ -1176,6 +1185,7 @@ pub fn refund_on_error( input, u64::MAX, Vec::new(), + Vec::new(), handler, ) } else { @@ -1195,6 +1205,7 @@ pub fn refund_on_error( (exit_address.raw(), Vec::new()), (refund_address.raw(), Vec::new()), ], + Vec::new(), handler, ) } @@ -2504,6 +2515,7 @@ mod tests { value: Wei::default(), data: vec![], access_list: vec![], + authorization_list: vec![], }; let actual_result = engine .charge_gas(&origin, &transaction, None, None) @@ -2540,6 +2552,7 @@ mod tests { value: Wei::default(), data: vec![], access_list: vec![], + authorization_list: vec![], }; let actual_result = engine .charge_gas(&origin, &transaction, None, None)