Skip to content

Commit 3c874fd

Browse files
feat(l2): withdrawal handling (#877)
**Motivation** <!-- Why does this pull request exist? What are its goals? --> Withdrawals are an essential requirement for L2 networks. **Description** <!-- A clear and concise general description of the changes this PR introduces --> We introduce a new EIP-2718 transaction type called `PrivilegedL2Transaction` used to manage deposits and withdraws. Handlers were set to the VM so when this transactions get executed, it runs custom logic like minting or burning ETH. Different CLI commands were also introduced to easily create and send this transactions. <!-- Link to issues: Resolves #111, Resolves #222 --> Closes #816 Closes #839 --------- Co-authored-by: ilitteri <ilitteri@fi.uba.ar> Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com>
1 parent 53290fb commit 3c874fd

File tree

25 files changed

+1363
-185
lines changed

25 files changed

+1363
-185
lines changed

cmd/ethereum_rust_l2/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ ethereum_rust-prover.workspace = true
3333
ethereum_rust-rlp.workspace = true
3434

3535
[[bin]]
36-
name = "ethereum_rust_l2"
36+
name = "l2"
3737
path = "./src/main.rs"

cmd/ethereum_rust_l2/src/commands/utils.rs

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::str::FromStr;
33
use bytes::Bytes;
44
use clap::Subcommand;
55
use ethereum_types::{Address, H32, U256};
6+
use eyre::eyre;
67
use keccak_hash::{keccak, H256};
78

89
#[derive(Subcommand)]
@@ -69,49 +70,65 @@ fn parse_arg(arg_type: &str, arg: &str) -> Vec<u8> {
6970

7071
fn parse_vec_arg(arg_type: &str, arg: &str) -> Vec<u8> {
7172
let args = arg.split(',');
73+
let length = &mut [0u8; 32];
74+
U256::from(args.clone().count()).to_big_endian(length);
75+
let length = length.to_vec();
76+
7277
match arg_type {
7378
"address[]" => {
74-
return args
75-
.map(|arg| {
79+
return [
80+
length,
81+
args.map(|arg| {
7682
H256::from(Address::from_str(arg).expect("Cannot parse address[]"))
7783
.0
7884
.to_vec()
7985
})
8086
.collect::<Vec<Vec<u8>>>()
81-
.concat();
87+
.concat(),
88+
]
89+
.concat();
8290
}
8391
"uint8[]" => {
84-
return args
85-
.map(|arg| {
92+
return [
93+
length,
94+
args.map(|arg| {
8695
let buf: &mut [u8] = &mut [0u8; 32];
8796
U256::from(u8::from_str(arg).expect("Cannot parse u8[]")).to_big_endian(buf);
8897
buf.to_vec()
8998
})
9099
.collect::<Vec<Vec<u8>>>()
91-
.concat();
100+
.concat(),
101+
]
102+
.concat();
92103
}
93104
"uint256[]" => {
94-
return args
95-
.map(|arg| {
105+
return [
106+
length,
107+
args.map(|arg| {
96108
let buf: &mut [u8] = &mut [0u8; 32];
97109
U256::from_dec_str(arg)
98110
.expect("Cannot parse u256[]")
99111
.to_big_endian(buf);
100112
buf.to_vec()
101113
})
102114
.collect::<Vec<Vec<u8>>>()
103-
.concat();
115+
.concat(),
116+
]
117+
.concat();
104118
}
105119
"bytes32[]" => {
106-
return args
107-
.map(|arg| {
120+
return [
121+
length,
122+
args.map(|arg| {
108123
H256::from_str(arg)
109124
.expect("Cannot parse bytes32[]")
110125
.0
111126
.to_vec()
112127
})
113128
.collect::<Vec<Vec<u8>>>()
114-
.concat();
129+
.concat(),
130+
]
131+
.concat();
115132
}
116133
_ => {
117134
println!("Unsupported type: {arg_type}");
@@ -120,6 +137,48 @@ fn parse_vec_arg(arg_type: &str, arg: &str) -> Vec<u8> {
120137
vec![]
121138
}
122139

140+
pub fn encode_calldata(
141+
signature: &str,
142+
args: &str,
143+
only_args: bool,
144+
) -> Result<Vec<u8>, eyre::Error> {
145+
let (name, params) = parse_signature(signature);
146+
let function_selector = compute_function_selector(&name, params.clone());
147+
148+
let args: Vec<&str> = args.split(' ').collect();
149+
150+
if params.len() != args.len() {
151+
return Err(eyre!(
152+
"Number of arguments does not match ({} != {})",
153+
params.len(),
154+
args.len()
155+
));
156+
}
157+
158+
let mut calldata: Vec<u8> = vec![];
159+
let mut dynamic_calldata: Vec<u8> = vec![];
160+
if !only_args {
161+
calldata.extend(function_selector.as_bytes().to_vec());
162+
};
163+
for (param, arg) in params.iter().zip(args.clone()) {
164+
if param.as_str().ends_with("[]") {
165+
let offset: &mut [u8] = &mut [0u8; 32];
166+
(U256::from(args.len())
167+
.checked_mul(U256::from(32))
168+
.expect("Calldata too long")
169+
.checked_add(U256::from(dynamic_calldata.len()))
170+
.expect("Calldata too long"))
171+
.to_big_endian(offset);
172+
calldata.extend(offset.to_vec());
173+
dynamic_calldata.extend(parse_vec_arg(param, arg));
174+
} else {
175+
calldata.extend(parse_arg(param, arg));
176+
}
177+
}
178+
179+
Ok([calldata, dynamic_calldata].concat())
180+
}
181+
123182
impl Command {
124183
pub async fn run(self) -> eyre::Result<()> {
125184
match self {
@@ -128,45 +187,8 @@ impl Command {
128187
args,
129188
only_args,
130189
} => {
131-
let (name, params) = parse_signature(&signature);
132-
let function_selector = compute_function_selector(&name, params.clone());
133-
134-
let args: Vec<&str> = args.split(' ').collect();
135-
136-
if params.len() != args.len() {
137-
println!(
138-
"Number of arguments does not match ({} != {})",
139-
params.len(),
140-
args.len()
141-
);
142-
return Ok(());
143-
}
144-
145-
let mut calldata: Vec<u8> = vec![];
146-
let mut dynamic_calldata: Vec<u8> = vec![];
147-
if !only_args {
148-
calldata.extend(function_selector.as_bytes().to_vec());
149-
};
150-
for (param, arg) in params.iter().zip(args.clone()) {
151-
if param.as_str().ends_with("[]") {
152-
let offset: &mut [u8] = &mut [0u8; 32];
153-
(U256::from(args.len())
154-
.checked_mul(U256::from(32))
155-
.expect("Calldata too long")
156-
.checked_add(U256::from(dynamic_calldata.len()))
157-
.expect("Calldata too long"))
158-
.to_big_endian(offset);
159-
calldata.extend(offset.to_vec());
160-
dynamic_calldata.extend(parse_vec_arg(param, arg));
161-
} else {
162-
calldata.extend(parse_arg(param, arg));
163-
}
164-
}
165-
println!(
166-
"0x{}{}",
167-
hex::encode(calldata),
168-
hex::encode(dynamic_calldata)
169-
);
190+
let calldata = encode_calldata(&signature, &args, only_args)?;
191+
println!("0x{}", hex::encode(calldata));
170192
}
171193
};
172194
Ok(())

cmd/ethereum_rust_l2/src/commands/wallet.rs

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
use crate::config::EthereumRustL2Config;
1+
use crate::{commands::utils::encode_calldata, config::EthereumRustL2Config};
22
use bytes::Bytes;
33
use clap::Subcommand;
4-
use ethereum_rust_core::types::{EIP1559Transaction, TxKind};
5-
use ethereum_rust_l2::utils::eth_client::{eth_sender::Overrides, EthClient};
4+
use ethereum_rust_core::types::{
5+
EIP1559Transaction, PrivilegedL2Transaction, PrivilegedTxType, Transaction, TxKind,
6+
};
7+
use ethereum_rust_l2::utils::{
8+
eth_client::{eth_sender::Overrides, EthClient},
9+
merkle_tree::merkle_proof,
10+
};
611
use ethereum_types::{Address, H256, U256};
12+
use eyre::OptionExt;
713
use hex::FromHexError;
14+
use itertools::Itertools;
15+
16+
const CLAIM_WITHDRAWAL_SIGNATURE: &str =
17+
"claimWithdrawal(bytes32,uint256,uint256,uint256,bytes32[])";
818

919
#[derive(Subcommand)]
1020
pub(crate) enum Command {
@@ -36,10 +46,7 @@ pub(crate) enum Command {
3646
explorer_url: bool,
3747
},
3848
#[clap(about = "Finalize a pending withdrawal.")]
39-
FinalizeWithdraw {
40-
#[clap(long = "hash")]
41-
l2_withdrawal_tx_hash: H256,
42-
},
49+
ClaimWithdraw { l2_withdrawal_tx_hash: H256 },
4350
#[clap(about = "Transfer funds to another wallet.")]
4451
Transfer {
4552
// TODO: Parse ether instead.
@@ -63,6 +70,10 @@ pub(crate) enum Command {
6370
// TODO: Parse ether instead.
6471
#[clap(long = "amount", value_parser = U256::from_dec_str)]
6572
amount: U256,
73+
#[clap(long = "to")]
74+
to: Option<Address>,
75+
#[clap(long = "nonce")]
76+
nonce: Option<u64>,
6677
#[clap(
6778
long = "token",
6879
help = "Specify the token address, the base token is used as default."
@@ -71,6 +82,11 @@ pub(crate) enum Command {
7182
#[clap(long, short = 'e', required = false)]
7283
explorer_url: bool,
7384
},
85+
#[clap(about = "Get the withdrawal merkle proof of a transaction.")]
86+
WithdrawalProof {
87+
#[clap(long = "hash")]
88+
tx_hash: H256,
89+
},
7490
#[clap(about = "Get the wallet address.")]
7591
Address,
7692
#[clap(about = "Get the wallet private key.")]
@@ -171,6 +187,50 @@ fn decode_hex(s: &str) -> Result<Bytes, FromHexError> {
171187
}
172188
}
173189

190+
async fn get_withdraw_merkle_proof(
191+
client: &EthClient,
192+
tx_hash: H256,
193+
) -> Result<(u64, Vec<H256>), eyre::Error> {
194+
let tx_receipt = client
195+
.get_transaction_receipt(tx_hash)
196+
.await?
197+
.ok_or_eyre("Transaction receipt not found")?;
198+
199+
let transactions = client
200+
.get_block_by_hash(tx_receipt.block_info.block_hash)
201+
.await?
202+
.transactions;
203+
204+
let (index, tx_withdrawal_hash) = transactions
205+
.iter()
206+
.filter(|tx| match tx {
207+
Transaction::PrivilegedL2Transaction(tx) => tx.tx_type == PrivilegedTxType::Withdrawal,
208+
_ => false,
209+
})
210+
.find_position(|tx| tx.compute_hash() == tx_hash)
211+
.map(|(i, tx)| match tx {
212+
Transaction::PrivilegedL2Transaction(tx) => {
213+
(i as u64, tx.get_withdrawal_hash().unwrap())
214+
}
215+
_ => unreachable!(),
216+
})
217+
.ok_or_eyre("Transaction is not a Withdrawal")?;
218+
219+
let path = merkle_proof(
220+
transactions
221+
.iter()
222+
.filter_map(|tx| match tx {
223+
Transaction::PrivilegedL2Transaction(tx) => tx.get_withdrawal_hash(),
224+
_ => None,
225+
})
226+
.collect(),
227+
tx_withdrawal_hash,
228+
)
229+
.ok_or_eyre("Transaction's WithdrawalData is not in block's WithdrawalDataMerkleRoot")?;
230+
231+
Ok((index, path))
232+
}
233+
174234
impl Command {
175235
pub async fn run(self, cfg: EthereumRustL2Config) -> eyre::Result<()> {
176236
let eth_client = EthClient::new(&cfg.network.l1_rpc_url);
@@ -223,10 +283,52 @@ impl Command {
223283
})
224284
.await?;
225285
}
226-
Command::FinalizeWithdraw {
227-
l2_withdrawal_tx_hash: _,
286+
Command::ClaimWithdraw {
287+
l2_withdrawal_tx_hash,
228288
} => {
229-
todo!()
289+
let (withdrawal_l2_block_number, claimed_amount) = match rollup_client
290+
.get_transaction_by_hash(l2_withdrawal_tx_hash)
291+
.await?
292+
{
293+
Some(l2_withdrawal_tx) => {
294+
(l2_withdrawal_tx.block_number, l2_withdrawal_tx.value)
295+
}
296+
None => {
297+
println!("Withdrawal transaction not found in L2");
298+
return Ok(());
299+
}
300+
};
301+
302+
let (index, proof) =
303+
get_withdraw_merkle_proof(&rollup_client, l2_withdrawal_tx_hash).await?;
304+
305+
let claim_withdrawal_data = encode_calldata(
306+
CLAIM_WITHDRAWAL_SIGNATURE,
307+
&format!(
308+
"{l2_withdrawal_tx_hash:#x} {claimed_amount} {withdrawal_l2_block_number} {index} {}",
309+
proof.iter().map(hex::encode).join(",")
310+
),
311+
false
312+
)?;
313+
println!(
314+
"ClaimWithdrawalData: {}",
315+
hex::encode(claim_withdrawal_data.clone())
316+
);
317+
318+
let tx_hash = eth_client
319+
.send(
320+
claim_withdrawal_data.into(),
321+
from,
322+
TxKind::Call(cfg.contracts.common_bridge),
323+
cfg.wallet.private_key,
324+
Overrides {
325+
chain_id: Some(cfg.network.l1_chain_id),
326+
..Default::default()
327+
},
328+
)
329+
.await?;
330+
331+
println!("Withdrawal claim sent: {tx_hash:#x}");
230332
}
231333
Command::Transfer {
232334
amount,
@@ -270,11 +372,32 @@ impl Command {
270372
);
271373
}
272374
Command::Withdraw {
273-
amount: _,
375+
amount,
376+
to,
377+
nonce,
274378
token_address: _,
275379
explorer_url: _,
276380
} => {
277-
todo!()
381+
let withdraw_transaction = PrivilegedL2Transaction {
382+
to: TxKind::Call(to.unwrap_or(cfg.wallet.address)),
383+
value: amount,
384+
chain_id: cfg.network.l2_chain_id,
385+
nonce: nonce.unwrap_or(rollup_client.get_nonce(from).await?),
386+
max_fee_per_gas: 800000000,
387+
tx_type: PrivilegedTxType::Withdrawal,
388+
gas_limit: 21000 * 2,
389+
..Default::default()
390+
};
391+
392+
let tx_hash = rollup_client
393+
.send_privileged_l2_transaction(withdraw_transaction, cfg.wallet.private_key)
394+
.await?;
395+
396+
println!("Withdrawal sent: {tx_hash:#x}");
397+
}
398+
Command::WithdrawalProof { tx_hash } => {
399+
let (_index, path) = get_withdraw_merkle_proof(&rollup_client, tx_hash).await?;
400+
println!("{path:?}");
278401
}
279402
Command::Address => {
280403
todo!()

crates/common/types/receipt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ impl RLPDecode for Receipt {
7070
0x1 => (TxType::EIP2930, &rlp[1..]),
7171
0x2 => (TxType::EIP1559, &rlp[1..]),
7272
0x3 => (TxType::EIP4844, &rlp[1..]),
73+
0x7e => (TxType::Privileged, &rlp[1..]),
7374
ty => {
7475
return Err(RLPDecodeError::Custom(format!(
7576
"Invalid transaction type: {ty}"

0 commit comments

Comments
 (0)