Skip to content

Commit c998542

Browse files
authored
fix(anvil): fix incorrect op-stack deposit tx hashes (#8567)
* fix encoding and hash for deposit tx * rename * add it test * remove comments * fix suggested changes
1 parent 53bf620 commit c998542

File tree

3 files changed

+197
-14
lines changed

3 files changed

+197
-14
lines changed

crates/anvil/core/src/eth/transaction/mod.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ impl TypedTransaction {
925925
/// This appends the `address` before hashing it
926926
#[cfg(feature = "impersonated-tx")]
927927
pub fn impersonated_hash(&self, sender: Address) -> B256 {
928-
let mut buffer = Vec::<u8>::new();
928+
let mut buffer = Vec::new();
929929
Encodable::encode(self, &mut buffer);
930930
buffer.extend_from_slice(sender.as_ref());
931931
B256::from_slice(alloy_primitives::utils::keccak256(&buffer).as_slice())
@@ -1101,7 +1101,7 @@ impl Decodable for TypedTransaction {
11011101
if ty != 0x7E {
11021102
Ok(TxEnvelope::decode(buf)?.into())
11031103
} else {
1104-
Ok(Self::Deposit(DepositTransaction::decode(&mut h_decode_copy)?))
1104+
Ok(Self::Deposit(DepositTransaction::decode_2718(buf)?))
11051105
}
11061106
}
11071107
}
@@ -1133,8 +1133,7 @@ impl Encodable2718 for TypedTransaction {
11331133
Self::EIP4844(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
11341134
Self::EIP7702(tx) => tx.tx().encode_with_signature(tx.signature(), out, false),
11351135
Self::Deposit(tx) => {
1136-
out.put_u8(0x7E);
1137-
tx.encode(out);
1136+
tx.encode_2718(out);
11381137
}
11391138
}
11401139
}
@@ -1697,6 +1696,28 @@ mod tests {
16971696
assert_eq!(from, address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
16981697
}
16991698

1699+
#[test]
1700+
fn test_decode_encode_deposit_tx() {
1701+
// https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
1702+
let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
1703+
.parse::<TxHash>()
1704+
.unwrap();
1705+
1706+
// https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
1707+
let raw_tx = alloy_primitives::hex::decode(
1708+
"7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
1709+
)
1710+
.unwrap();
1711+
let dep_tx = TypedTransaction::decode(&mut raw_tx.as_slice()).unwrap();
1712+
1713+
let mut encoded = Vec::new();
1714+
dep_tx.encode_2718(&mut encoded);
1715+
1716+
assert_eq!(raw_tx, encoded);
1717+
1718+
assert_eq!(tx_hash, dep_tx.hash());
1719+
}
1720+
17001721
#[test]
17011722
fn can_recover_sender_not_normalized() {
17021723
let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();

crates/anvil/core/src/eth/transaction/optimism.rs

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
use alloy_consensus::{SignableTransaction, Signed, Transaction, TxType};
1+
use alloy_consensus::{SignableTransaction, Signed, Transaction};
22
use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, TxKind, B256, U256};
33
use alloy_rlp::{
44
length_of_length, Decodable, Encodable, Error as DecodeError, Header as RlpHeader,
55
};
6+
use bytes::BufMut;
67
use serde::{Deserialize, Serialize};
78
use std::mem;
89

10+
pub const DEPOSIT_TX_TYPE_ID: u8 = 0x7E;
11+
912
#[derive(Clone, Debug, PartialEq, Eq)]
1013
pub struct DepositTransactionRequest {
1114
pub source_hash: B256,
@@ -20,13 +23,17 @@ pub struct DepositTransactionRequest {
2023

2124
impl DepositTransactionRequest {
2225
pub fn hash(&self) -> B256 {
23-
B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice())
26+
let mut encoded = Vec::new();
27+
encoded.put_u8(DEPOSIT_TX_TYPE_ID);
28+
self.encode(&mut encoded);
29+
30+
B256::from_slice(alloy_primitives::keccak256(encoded).as_slice())
2431
}
2532

2633
/// Encodes only the transaction's fields into the desired buffer, without a RLP header.
2734
pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
28-
self.from.encode(out);
2935
self.source_hash.encode(out);
36+
self.from.encode(out);
3037
self.kind.encode(out);
3138
self.mint.encode(out);
3239
self.value.encode(out);
@@ -103,8 +110,8 @@ impl DepositTransactionRequest {
103110
}
104111

105112
/// Get transaction type
106-
pub(crate) const fn tx_type(&self) -> TxType {
107-
TxType::Eip1559
113+
pub(crate) const fn tx_type(&self) -> u8 {
114+
DEPOSIT_TX_TYPE_ID
108115
}
109116

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

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

249256
pub fn hash(&self) -> B256 {
250-
B256::from_slice(alloy_primitives::keccak256(alloy_rlp::encode(self)).as_slice())
257+
let mut encoded = Vec::new();
258+
self.encode_2718(&mut encoded);
259+
B256::from_slice(alloy_primitives::keccak256(encoded).as_slice())
251260
}
252261

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

271+
pub(crate) fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
272+
out.put_u8(DEPOSIT_TX_TYPE_ID);
273+
self.encode(out);
274+
}
275+
262276
/// Encodes only the transaction's fields into the desired buffer, without a RLP header.
263277
pub(crate) fn encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
264-
self.nonce.encode(out);
265278
self.source_hash.encode(out);
266279
self.from.encode(out);
267280
self.kind.encode(out);
@@ -286,6 +299,20 @@ impl DepositTransaction {
286299
len
287300
}
288301

302+
pub fn decode_2718(buf: &mut &[u8]) -> Result<Self, DecodeError> {
303+
use bytes::Buf;
304+
305+
let tx_type = *buf.first().ok_or(alloy_rlp::Error::Custom("empty slice"))?;
306+
307+
if tx_type != DEPOSIT_TX_TYPE_ID {
308+
return Err(alloy_rlp::Error::Custom("invalid tx type: expected deposit tx type"));
309+
}
310+
311+
// Skip the tx type byte
312+
buf.advance(1);
313+
Self::decode(buf)
314+
}
315+
289316
/// Decodes the inner fields from RLP bytes
290317
///
291318
/// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following
@@ -325,11 +352,76 @@ impl Decodable for DepositTransaction {
325352
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
326353
let header = RlpHeader::decode(buf)?;
327354
let remaining_len = buf.len();
328-
329355
if header.payload_length > remaining_len {
330356
return Err(alloy_rlp::Error::InputTooShort);
331357
}
332358

333359
Self::decode_inner(buf)
334360
}
335361
}
362+
363+
#[cfg(test)]
364+
mod tests {
365+
use super::*;
366+
367+
#[test]
368+
fn test_encode_decode() {
369+
let tx = DepositTransaction {
370+
nonce: 0,
371+
source_hash: B256::default(),
372+
from: Address::default(),
373+
kind: TxKind::Call(Address::default()),
374+
mint: U256::from(100),
375+
value: U256::from(100),
376+
gas_limit: 50000,
377+
is_system_tx: false,
378+
input: Bytes::default(),
379+
};
380+
381+
let encoded_tx: Vec<u8> = alloy_rlp::encode(&tx);
382+
383+
let decoded_tx = DepositTransaction::decode(&mut encoded_tx.as_slice()).unwrap();
384+
385+
assert_eq!(tx, decoded_tx);
386+
}
387+
#[test]
388+
fn test_encode_decode_2718() {
389+
let tx = DepositTransaction {
390+
nonce: 0,
391+
source_hash: B256::default(),
392+
from: Address::default(),
393+
kind: TxKind::Call(Address::default()),
394+
mint: U256::from(100),
395+
value: U256::from(100),
396+
gas_limit: 50000,
397+
is_system_tx: false,
398+
input: Bytes::default(),
399+
};
400+
401+
let mut encoded_tx: Vec<u8> = Vec::new();
402+
tx.encode_2718(&mut encoded_tx);
403+
404+
let decoded_tx = DepositTransaction::decode_2718(&mut encoded_tx.as_slice()).unwrap();
405+
406+
assert_eq!(tx, decoded_tx);
407+
}
408+
409+
#[test]
410+
fn test_tx_request_hash_equals_tx_hash() {
411+
let tx = DepositTransaction {
412+
nonce: 0,
413+
source_hash: B256::default(),
414+
from: Address::default(),
415+
kind: TxKind::Call(Address::default()),
416+
mint: U256::from(100),
417+
value: U256::from(100),
418+
gas_limit: 50000,
419+
is_system_tx: false,
420+
input: Bytes::default(),
421+
};
422+
423+
let tx_request = DepositTransactionRequest::from(tx.clone());
424+
425+
assert_eq!(tx.hash(), tx_request.hash());
426+
}
427+
}

crates/anvil/tests/it/optimism.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::utils::http_provider_with_signer;
44
use alloy_eips::eip2718::Encodable2718;
55
use alloy_network::{EthereumWallet, TransactionBuilder};
6-
use alloy_primitives::{b256, U256};
6+
use alloy_primitives::{b256, Address, TxHash, U256};
77
use alloy_provider::Provider;
88
use alloy_rpc_types::{optimism::OptimismTransactionFields, TransactionRequest};
99
use alloy_serde::WithOtherFields;
@@ -144,3 +144,73 @@ async fn test_send_value_raw_deposit_transaction() {
144144
let after_balance_to = provider.get_balance(to).await.unwrap();
145145
assert_eq!(after_balance_to, before_balance_to + send_value);
146146
}
147+
148+
#[tokio::test(flavor = "multi_thread")]
149+
async fn test_deposit_transaction_hash_matches_sepolia() {
150+
// enable the Optimism flag
151+
let (api, handle) =
152+
spawn(NodeConfig::test().with_optimism(true).with_hardfork(Some(Hardfork::Paris))).await;
153+
154+
let accounts: Vec<_> = handle.dev_wallets().collect();
155+
let signer: EthereumWallet = accounts[0].clone().into();
156+
let sender_addr = accounts[0].address();
157+
158+
// https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
159+
let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
160+
.parse::<TxHash>()
161+
.unwrap();
162+
163+
// https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
164+
let raw_deposit_tx = alloy_primitives::hex::decode(
165+
"7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
166+
)
167+
.unwrap();
168+
let deposit_tx_from = "0x778F2146F48179643473B82931c4CD7B8F153eFd".parse::<Address>().unwrap();
169+
170+
let provider = http_provider_with_signer(&handle.http_endpoint(), signer.clone());
171+
172+
// TODO: necessary right now because transaction validation fails for deposit tx
173+
// with `from` account that doesn't have sufficient ETH balance.
174+
// Should update the tx validation logic for deposit tx to
175+
// 1. check if `tx.value > account.balance + tx.mint`
176+
// 2. don't check `account.balance > gas * price + value` (the gas costs have been prepaid on
177+
// L1)
178+
// source: https://specs.optimism.io/protocol/deposits.html#execution
179+
let fund_account_tx = TransactionRequest::default()
180+
.with_chain_id(31337)
181+
.with_nonce(0)
182+
.with_from(sender_addr)
183+
.with_to(deposit_tx_from)
184+
.with_value(U256::from(1e18))
185+
.with_gas_limit(21_000)
186+
.with_max_fee_per_gas(20_000_000_000)
187+
.with_max_priority_fee_per_gas(1_000_000_000);
188+
189+
provider
190+
.send_transaction(WithOtherFields::new(fund_account_tx))
191+
.await
192+
.unwrap()
193+
.register()
194+
.await
195+
.unwrap();
196+
197+
// mine block
198+
api.evm_mine(None).await.unwrap();
199+
200+
let pending = provider
201+
.send_raw_transaction(raw_deposit_tx.as_slice())
202+
.await
203+
.unwrap()
204+
.register()
205+
.await
206+
.unwrap();
207+
208+
// mine block
209+
api.evm_mine(None).await.unwrap();
210+
211+
let receipt =
212+
provider.get_transaction_receipt(pending.tx_hash().to_owned()).await.unwrap().unwrap();
213+
214+
assert_eq!(pending.tx_hash(), &tx_hash);
215+
assert_eq!(receipt.transaction_hash, tx_hash);
216+
}

0 commit comments

Comments
 (0)