Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions crates/anvil/core/src/eth/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ impl TypedTransaction {
/// This appends the `address` before hashing it
#[cfg(feature = "impersonated-tx")]
pub fn impersonated_hash(&self, sender: Address) -> B256 {
let mut buffer = Vec::<u8>::new();
let mut buffer = Vec::new();
Encodable::encode(self, &mut buffer);
buffer.extend_from_slice(sender.as_ref());
B256::from_slice(alloy_primitives::utils::keccak256(&buffer).as_slice())
Expand Down Expand Up @@ -1101,7 +1101,7 @@ impl Decodable for TypedTransaction {
if ty != 0x7E {
Ok(TxEnvelope::decode(buf)?.into())
} else {
Ok(Self::Deposit(DepositTransaction::decode(&mut h_decode_copy)?))
Ok(Self::Deposit(DepositTransaction::decode_2718(buf)?))
}
}
}
Expand Down Expand Up @@ -1133,8 +1133,7 @@ impl Encodable2718 for TypedTransaction {
Self::EIP4844(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
Self::EIP7702(tx) => tx.tx().encode_with_signature(tx.signature(), out, false),
Self::Deposit(tx) => {
out.put_u8(0x7E);
tx.encode(out);
tx.encode_2718(out);
}
}
}
Expand Down Expand Up @@ -1697,6 +1696,28 @@ mod tests {
assert_eq!(from, address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
}

#[test]
fn test_decode_encode_deposit_tx() {
// https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
.parse::<TxHash>()
.unwrap();

// https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
let raw_tx = alloy_primitives::hex::decode(
"7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
)
.unwrap();
let dep_tx = TypedTransaction::decode(&mut raw_tx.as_slice()).unwrap();

let mut encoded = Vec::new();
dep_tx.encode_2718(&mut encoded);

assert_eq!(raw_tx, encoded);

assert_eq!(tx_hash, dep_tx.hash());
}

#[test]
fn can_recover_sender_not_normalized() {
let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
Expand Down
110 changes: 101 additions & 9 deletions crates/anvil/core/src/eth/transaction/optimism.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use alloy_consensus::{SignableTransaction, Signed, Transaction, TxType};
use alloy_consensus::{SignableTransaction, Signed, Transaction};
use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, TxKind, B256, U256};
use alloy_rlp::{
length_of_length, Decodable, Encodable, Error as DecodeError, Header as RlpHeader,
};
use bytes::BufMut;
use serde::{Deserialize, Serialize};
use std::mem;

pub const DEPOSIT_TX_TYPE_ID: u8 = 0x7E;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DepositTransactionRequest {
pub source_hash: B256,
Expand All @@ -20,13 +23,17 @@ pub struct DepositTransactionRequest {

impl DepositTransactionRequest {
pub fn hash(&self) -> B256 {
B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice())
let mut encoded = Vec::new();
encoded.put_u8(DEPOSIT_TX_TYPE_ID);
self.encode(&mut encoded);

B256::from_slice(alloy_primitives::keccak256(encoded).as_slice())
}

/// Encodes only the transaction's fields into the desired buffer, without a RLP header.
pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
self.from.encode(out);
self.source_hash.encode(out);
self.from.encode(out);
self.kind.encode(out);
self.mint.encode(out);
self.value.encode(out);
Expand Down Expand Up @@ -103,8 +110,8 @@ impl DepositTransactionRequest {
}

/// Get transaction type
pub(crate) const fn tx_type(&self) -> TxType {
TxType::Eip1559
pub(crate) const fn tx_type(&self) -> u8 {
DEPOSIT_TX_TYPE_ID
}

/// Calculates a heuristic for the in-memory size of the [DepositTransaction] transaction.
Expand All @@ -121,7 +128,7 @@ impl DepositTransactionRequest {

/// Encodes the legacy transaction in RLP for signing.
pub(crate) fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
out.put_u8(self.tx_type() as u8);
out.put_u8(self.tx_type());
alloy_rlp::Header { list: true, payload_length: self.fields_len() }.encode(out);
self.encode_fields(out);
}
Expand Down Expand Up @@ -247,7 +254,9 @@ impl DepositTransaction {
}

pub fn hash(&self) -> B256 {
B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice())
let mut encoded = Vec::new();
self.encode_2718(&mut encoded);
B256::from_slice(alloy_primitives::keccak256(encoded).as_slice())
}

// /// Recovers the Ethereum address which was used to sign the transaction.
Expand All @@ -259,9 +268,13 @@ impl DepositTransaction {
None
}

pub(crate) fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
out.put_u8(DEPOSIT_TX_TYPE_ID);
self.encode(out);
}

/// Encodes only the transaction's fields into the desired buffer, without a RLP header.
pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
self.nonce.encode(out);
self.source_hash.encode(out);
self.from.encode(out);
self.kind.encode(out);
Expand All @@ -286,6 +299,20 @@ impl DepositTransaction {
len
}

pub fn decode_2718(buf: &mut &[u8]) -> Result<Self, DecodeError> {
use bytes::Buf;

let tx_type = *buf.first().ok_or(alloy_rlp::Error::Custom("empty slice"))?;

if tx_type != DEPOSIT_TX_TYPE_ID {
return Err(alloy_rlp::Error::Custom("invalid tx type: expected deposit tx type"));
}

// Skip the tx type byte
buf.advance(1);
Self::decode(buf)
}

/// Decodes the inner fields from RLP bytes
///
/// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following
Expand Down Expand Up @@ -325,11 +352,76 @@ impl Decodable for DepositTransaction {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
let header = RlpHeader::decode(buf)?;
let remaining_len = buf.len();

if header.payload_length > remaining_len {
return Err(alloy_rlp::Error::InputTooShort);
}

Self::decode_inner(buf)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_encode_decode() {
let tx = DepositTransaction {
nonce: 0,
source_hash: B256::default(),
from: Address::default(),
kind: TxKind::Call(Address::default()),
mint: U256::from(100),
value: U256::from(100),
gas_limit: 50000,
is_system_tx: false,
input: Bytes::default(),
};

let encoded_tx: Vec<u8> = alloy_rlp::encode(&tx);

let decoded_tx = DepositTransaction::decode(&mut encoded_tx.as_slice()).unwrap();

assert_eq!(tx, decoded_tx);
}
#[test]
fn test_encode_decode_2718() {
let tx = DepositTransaction {
nonce: 0,
source_hash: B256::default(),
from: Address::default(),
kind: TxKind::Call(Address::default()),
mint: U256::from(100),
value: U256::from(100),
gas_limit: 50000,
is_system_tx: false,
input: Bytes::default(),
};

let mut encoded_tx: Vec<u8> = Vec::new();
tx.encode_2718(&mut encoded_tx);

let decoded_tx = DepositTransaction::decode_2718(&mut encoded_tx.as_slice()).unwrap();

assert_eq!(tx, decoded_tx);
}

#[test]
fn test_tx_request_hash_equals_tx_hash() {
let tx = DepositTransaction {
nonce: 0,
source_hash: B256::default(),
from: Address::default(),
kind: TxKind::Call(Address::default()),
mint: U256::from(100),
value: U256::from(100),
gas_limit: 50000,
is_system_tx: false,
input: Bytes::default(),
};

let tx_request = DepositTransactionRequest::from(tx.clone());

assert_eq!(tx.hash(), tx_request.hash());
}
}
72 changes: 71 additions & 1 deletion crates/anvil/tests/it/optimism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::utils::http_provider_with_signer;
use alloy_eips::eip2718::Encodable2718;
use alloy_network::{EthereumWallet, TransactionBuilder};
use alloy_primitives::{b256, U256};
use alloy_primitives::{b256, Address, TxHash, U256};
use alloy_provider::Provider;
use alloy_rpc_types::{optimism::OptimismTransactionFields, TransactionRequest};
use alloy_serde::WithOtherFields;
Expand Down Expand Up @@ -144,3 +144,73 @@ async fn test_send_value_raw_deposit_transaction() {
let after_balance_to = provider.get_balance(to).await.unwrap();
assert_eq!(after_balance_to, before_balance_to + send_value);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_deposit_transaction_hash_matches_sepolia() {
// enable the Optimism flag
let (api, handle) =
spawn(NodeConfig::test().with_optimism(true).with_hardfork(Some(Hardfork::Paris))).await;

let accounts: Vec<_> = handle.dev_wallets().collect();
let signer: EthereumWallet = accounts[0].clone().into();
let sender_addr = accounts[0].address();

// https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
.parse::<TxHash>()
.unwrap();

// https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
let raw_deposit_tx = alloy_primitives::hex::decode(
"7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
)
.unwrap();
let deposit_tx_from = "0x778F2146F48179643473B82931c4CD7B8F153eFd".parse::<Address>().unwrap();

let provider = http_provider_with_signer(&handle.http_endpoint(), signer.clone());

// TODO: necessary right now because transaction validation fails for deposit tx
// with `from` account that doesn't have sufficient ETH balance.
// Should update the tx validation logic for deposit tx to
// 1. check if `tx.value > account.balance + tx.mint`
// 2. don't check `account.balance > gas * price + value` (the gas costs have been prepaid on
// L1)
// source: https://specs.optimism.io/protocol/deposits.html#execution
let fund_account_tx = TransactionRequest::default()
.with_chain_id(31337)
.with_nonce(0)
.with_from(sender_addr)
.with_to(deposit_tx_from)
.with_value(U256::from(1e18))
.with_gas_limit(21_000)
.with_max_fee_per_gas(20_000_000_000)
.with_max_priority_fee_per_gas(1_000_000_000);

provider
.send_transaction(WithOtherFields::new(fund_account_tx))
.await
.unwrap()
.register()
.await
.unwrap();

// mine block
api.evm_mine(None).await.unwrap();

let pending = provider
.send_raw_transaction(raw_deposit_tx.as_slice())
.await
.unwrap()
.register()
.await
.unwrap();

// mine block
api.evm_mine(None).await.unwrap();

let receipt =
provider.get_transaction_receipt(pending.tx_hash().to_owned()).await.unwrap().unwrap();

assert_eq!(pending.tx_hash(), &tx_hash);
assert_eq!(receipt.transaction_hash, tx_hash);
}