From 943a39537c05e3e4912f9ab62338507c6ccbffc2 Mon Sep 17 00:00:00 2001 From: ElFantasma Date: Wed, 23 Oct 2024 17:21:37 -0300 Subject: [PATCH 01/49] feat(l1): initiate rlpx connections from discv4 (#936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivation** DiscV4 protocol discovers new peers, and after deciding it is a valid peer, a TCP connection is established and RLPx protocol starts **Description** Now, when receiving a DiscV4 Pong message and after evaluating the peer, a RLPx connection is created and established. If handshake fails it removes the peer from the Kademlia table. Closes #837. Also removed some hard-coded testing code. Once the listen loop is built (#840) there may be other conditions where a peer has to be discarded. (eg. when the other peer sends a Disconnect message). After #840 is completed we may create some more issues on this regard. --------- Co-authored-by: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Co-authored-by: Javier Rodríguez Chatruc <49622509+jrchatruc@users.noreply.github.com> Co-authored-by: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Co-authored-by: Marcos Nicolau <76252340+MarcosNicolau@users.noreply.github.com> Co-authored-by: Rodrigo Oliveri Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao Co-authored-by: Martin Paulucci Co-authored-by: François <67512048+gsbujo@users.noreply.github.com> --- crates/networking/p2p/bootnode.rs | 2 + crates/networking/p2p/net.rs | 134 ++++++++++------------- crates/networking/p2p/rlpx/connection.rs | 16 +++ crates/networking/p2p/rlpx/handshake.rs | 2 - crates/networking/p2p/types.rs | 10 +- crates/networking/rpc/rpc.rs | 2 +- 6 files changed, 86 insertions(+), 80 deletions(-) diff --git a/crates/networking/p2p/bootnode.rs b/crates/networking/p2p/bootnode.rs index f7ccca96c..0a0bbf2f8 100644 --- a/crates/networking/p2p/bootnode.rs +++ b/crates/networking/p2p/bootnode.rs @@ -11,6 +11,8 @@ impl FromStr for BootNode { type Err = ParseIntError; /// Takes a str with the format "enode://nodeID@IPaddress:port" and /// parses it to a BootNode + // TODO: fix it to support different UDP and TCP ports, according to + // https://github.com/lambdaclass/lambda_ethereum_rust/issues/905 fn from_str(input: &str) -> Result { // TODO: error handling let node_id = H512::from_str(&input[8..136]).expect("Failed to parse node id"); diff --git a/crates/networking/p2p/net.rs b/crates/networking/p2p/net.rs index b99b6eb58..ff1a42300 100644 --- a/crates/networking/p2p/net.rs +++ b/crates/networking/p2p/net.rs @@ -44,20 +44,32 @@ pub async fn start_network( ) { info!("Starting discovery service at {udp_addr}"); info!("Listening for requests at {tcp_addr}"); + let local_node_id = node_id_from_signing_key(&signer); + let table = Arc::new(Mutex::new(KademliaTable::new(local_node_id))); - let discovery_handle = tokio::spawn(discover_peers(udp_addr, signer.clone(), bootnodes)); - let server_handle = tokio::spawn(serve_requests(tcp_addr, signer.clone(), storage.clone())); - // TODO Remove this spawn, it's just for testing - // https://github.com/lambdaclass/lambda_ethereum_rust/issues/837 - tokio::spawn(start_hardcoded_connection(tcp_addr, signer, storage)); + let discovery_handle = tokio::spawn(discover_peers( + udp_addr, + signer.clone(), + table.clone(), + bootnodes, + )); + let server_handle = tokio::spawn(serve_requests( + tcp_addr, + signer.clone(), + storage.clone(), + table.clone(), + )); try_join!(discovery_handle, server_handle).unwrap(); } -async fn discover_peers(udp_addr: SocketAddr, signer: SigningKey, bootnodes: Vec) { +async fn discover_peers( + udp_addr: SocketAddr, + signer: SigningKey, + table: Arc>, + bootnodes: Vec, +) { let udp_socket = Arc::new(UdpSocket::bind(udp_addr).await.unwrap()); - let local_node_id = node_id_from_signing_key(&signer); - let table = Arc::new(Mutex::new(KademliaTable::new(local_node_id))); let server_handler = tokio::spawn(discover_peers_server( udp_addr, @@ -89,7 +101,7 @@ async fn discover_peers(udp_addr: SocketAddr, signer: SigningKey, bootnodes: Vec udp_socket.clone(), table.clone(), signer.clone(), - local_node_id, + node_id_from_signing_key(&signer), PEERS_RANDOM_LOOKUP_TIME_IN_MIN as u64 * 60, )); @@ -165,6 +177,7 @@ async fn discover_peers_server( } } Message::Pong(msg) => { + let table = table.clone(); if is_expired(msg.expiration) { debug!("Ignoring pong as it is expired."); continue; @@ -180,17 +193,14 @@ async fn discover_peers_server( } if peer.last_ping_hash.unwrap() == msg.ping_hash { table.lock().await.pong_answered(peer.node.node_id); - // TODO: make this work to initiate p2p thread - // https://github.com/lambdaclass/lambda_ethereum_rust/issues/837 - let _node = peer.node; - let mut msg_buf = vec![0; read - (32 + 65)]; - buf[32 + 65..read].clone_into(&mut msg_buf); - let mut sig_bytes = vec![0; 65]; - buf[32..32 + 65].clone_into(&mut sig_bytes); - let _signer_clone = signer.clone(); - // tokio::spawn(async move { - // handle_peer_as_initiator(signer_clone, &msg_buf, &node).await; - // }); + + let mut msg_buf = vec![0; read - 32]; + buf[32..read].clone_into(&mut msg_buf); + let signer_clone = signer.clone(); + tokio::spawn(async move { + handle_peer_as_initiator(signer_clone, &msg_buf, &peer.node, table) + .await; + }); } else { debug!( "Discarding pong as the hash did not match the last corresponding ping" @@ -306,7 +316,9 @@ async fn discovery_startup( table.lock().await.insert_node(Node { ip: bootnode.socket_address.ip(), udp_port: bootnode.socket_address.port(), - tcp_port: 0, + // TODO: udp port can differ from tcp port. + // see https://github.com/lambdaclass/lambda_ethereum_rust/issues/905 + tcp_port: bootnode.socket_address.port(), node_id: bootnode.node_id, }); let ping_hash = ping(&udp_socket, udp_addr, bootnode.socket_address, &signer).await; @@ -712,71 +724,44 @@ async fn pong(socket: &UdpSocket, to_addr: SocketAddr, ping_hash: H256, signer: let _ = socket.send_to(&buf, to_addr).await; } -// TODO: remove this function. It's used for a hardcoded test -// https://github.com/lambdaclass/lambda_ethereum_rust/issues/837 -async fn start_hardcoded_connection(tcp_addr: SocketAddr, signer: SigningKey, _storage: Store) { - let mut udp_addr = tcp_addr; - udp_addr.set_port(tcp_addr.port() + 1); - let udp_socket = UdpSocket::bind(udp_addr).await.unwrap(); - - // Try contacting a known peer - // TODO: this is just an example, and we should do this dynamically - let str_udp_addr = "127.0.0.1:30307"; - - let udp_addr: SocketAddr = str_udp_addr.parse().unwrap(); - - let mut buf = vec![0; MAX_DISC_PACKET_SIZE]; - - let (msg, endpoint, node_id) = loop { - ping(&udp_socket, tcp_addr, udp_addr, &signer).await; - - let (read, from) = udp_socket.recv_from(&mut buf).await.unwrap(); - debug!("RLPx: Received {read} bytes from {from}"); - let packet = Packet::decode(&buf[..read]).unwrap(); - debug!("RLPx: Message: {:?}", packet); - - match packet.get_message() { - Message::Pong(pong) => { - break (&buf[32..read], pong.to, packet.get_node_id()); - } - Message::Ping(ping) => { - break (&buf[32..read], ping.from, packet.get_node_id()); - } - _ => { - tracing::warn!("Unexpected message type"); - } - }; - }; - - let node = Node { - ip: endpoint.ip, - udp_port: 30307u16, //endpoint.udp_port, - tcp_port: 30307u16, //endpoint.tcp_port, - node_id, - }; - handle_peer_as_initiator(signer, msg, &node).await; -} - // TODO build a proper listen loop that receives requests from both // peers and business layer and propagate storage to use when required // https://github.com/lambdaclass/lambda_ethereum_rust/issues/840 -async fn serve_requests(tcp_addr: SocketAddr, signer: SigningKey, _storage: Store) { +async fn serve_requests( + tcp_addr: SocketAddr, + signer: SigningKey, + _storage: Store, + table: Arc>, +) { let tcp_socket = TcpSocket::new_v4().unwrap(); tcp_socket.bind(tcp_addr).unwrap(); let listener = tcp_socket.listen(50).unwrap(); loop { let (stream, _peer_addr) = listener.accept().await.unwrap(); - tokio::spawn(handle_peer_as_receiver(signer.clone(), stream)); + tokio::spawn(handle_peer_as_receiver( + signer.clone(), + stream, + table.clone(), + )); } } -async fn handle_peer_as_receiver(signer: SigningKey, stream: TcpStream) { +async fn handle_peer_as_receiver( + signer: SigningKey, + stream: TcpStream, + table: Arc>, +) { let conn = RLPxConnection::receiver(signer, stream); - handle_peer(conn).await; + handle_peer(conn, table).await; } -async fn handle_peer_as_initiator(signer: SigningKey, msg: &[u8], node: &Node) { +async fn handle_peer_as_initiator( + signer: SigningKey, + msg: &[u8], + node: &Node, + table: Arc>, +) { info!("Trying RLPx connection with {node:?}"); let stream = TcpSocket::new_v4() .unwrap() @@ -784,10 +769,10 @@ async fn handle_peer_as_initiator(signer: SigningKey, msg: &[u8], node: &Node) { .await .unwrap(); let conn = RLPxConnection::initiator(signer, msg, stream).await; - handle_peer(conn).await; + handle_peer(conn, table).await; } -async fn handle_peer(mut conn: RLPxConnection) { +async fn handle_peer(mut conn: RLPxConnection, table: Arc>) { match conn.handshake().await { Ok(_) => { // TODO Properly build listen loop @@ -797,8 +782,9 @@ async fn handle_peer(mut conn: RLPxConnection) { // } } Err(e) => { - // TODO propagate error to eventually discard peer from kademlia table + // Discard peer from kademlia table info!("Handshake failed, discarding peer: ({e})"); + table.lock().await.replace_peer(conn.get_remote_node_id()); } } } diff --git a/crates/networking/p2p/rlpx/connection.rs b/crates/networking/p2p/rlpx/connection.rs index 847a1c628..54c948bc1 100644 --- a/crates/networking/p2p/rlpx/connection.rs +++ b/crates/networking/p2p/rlpx/connection.rs @@ -289,6 +289,14 @@ impl RLPxConnection { _ => panic!("Received an unexpected message"), } } + + pub fn get_remote_node_id(&self) -> H512 { + match &self.state { + RLPxConnectionState::Established(state) => state.remote_node_id, + // TODO proper error + _ => panic!("Invalid state"), + } + } } enum RLPxConnectionState { @@ -358,6 +366,7 @@ impl ReceivedAuth { } struct InitiatedAuth { + pub(crate) remote_node_id: H512, pub(crate) local_nonce: H256, pub(crate) local_ephemeral_key: SecretKey, pub(crate) local_init_message: Vec, @@ -366,6 +375,7 @@ struct InitiatedAuth { impl InitiatedAuth { pub fn new(previous_state: &Initiator, local_init_message: Vec) -> Self { Self { + remote_node_id: previous_state.remote_node_id, local_nonce: previous_state.nonce, local_ephemeral_key: previous_state.ephemeral_key.clone(), local_init_message, @@ -374,6 +384,7 @@ impl InitiatedAuth { } pub struct Established { + pub remote_node_id: H512, pub(crate) mac_key: H256, pub ingress_mac: Keccak256, pub egress_mac: Keccak256, @@ -391,6 +402,7 @@ impl Established { .into(); Self::new( + previous_state.remote_node_id, init_message, previous_state.local_nonce, previous_state.local_ephemeral_key.clone(), @@ -413,6 +425,7 @@ impl Established { Keccak256::digest([remote_nonce.0, previous_state.local_nonce.0].concat()).into(); Self::new( + previous_state.remote_node_id, previous_state.local_init_message.clone(), previous_state.local_nonce, previous_state.local_ephemeral_key.clone(), @@ -423,7 +436,9 @@ impl Established { ) } + #[allow(clippy::too_many_arguments)] fn new( + remote_node_id: H512, local_init_message: Vec, local_nonce: H256, local_ephemeral_key: SecretKey, @@ -456,6 +471,7 @@ impl Established { let ingress_aes = ::new(&aes_key.0.into(), &[0; 16].into()); let egress_aes = ingress_aes.clone(); Self { + remote_node_id, mac_key, ingress_mac, egress_mac, diff --git a/crates/networking/p2p/rlpx/handshake.rs b/crates/networking/p2p/rlpx/handshake.rs index 987814d2b..679a0f8d8 100644 --- a/crates/networking/p2p/rlpx/handshake.rs +++ b/crates/networking/p2p/rlpx/handshake.rs @@ -113,8 +113,6 @@ pub(crate) fn decode_ack_message( } fn decrypt_message(static_key: &SecretKey, msg: &[u8], auth_data: [u8; 2]) -> Vec { - info!("msg {msg:?}"); - // Split the message into its components. General layout is: // public-key (65) || iv (16) || ciphertext || mac (32) let (pk, rest) = msg.split_at(65); diff --git a/crates/networking/p2p/types.rs b/crates/networking/p2p/types.rs index 06c47a7eb..c44397a63 100644 --- a/crates/networking/p2p/types.rs +++ b/crates/networking/p2p/types.rs @@ -80,9 +80,13 @@ impl Node { pub fn enode_url(&self) -> String { let node_id = hex::encode(self.node_id); let node_ip = self.ip; - let discovery_port = self.tcp_port; - let listener_port = self.udp_port; - format!("enode://{node_id}@{node_ip}:{listener_port}?discport={discovery_port}") + let discovery_port = self.udp_port; + let listener_port = self.tcp_port; + if discovery_port != listener_port { + format!("enode://{node_id}@{node_ip}:{listener_port}?discport={discovery_port}") + } else { + format!("enode://{node_id}@{node_ip}:{listener_port}") + } } } diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index e4676746a..ec6b18254 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -342,7 +342,7 @@ mod tests { let result = map_http_requests(&request, storage, local_p2p_node, Default::default()); let rpc_response = rpc_response(request.id, result); let expected_response = to_rpc_response_success_value( - r#"{"jsonrpc":"2.0","id":1,"result":{"enode":"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@127.0.0.1:30303?discport=30303","id":"d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666","ip":"127.0.0.1","name":"ethereum_rust/0.1.0/rust1.80","ports":{"discovery":30303,"listener":30303},"protocols":{"eth":{"chainId":3151908,"homesteadBlock":0,"daoForkBlock":null,"daoForkSupport":false,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"muirGlacierBlock":null,"berlinBlock":0,"londonBlock":0,"arrowGlacierBlock":null,"grayGlacierBlock":null,"mergeNetsplitBlock":0,"shanghaiTime":0,"cancunTime":0,"pragueTime":1718232101,"verkleTime":null,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true}}}}"#, + r#"{"jsonrpc":"2.0","id":1,"result":{"enode":"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@127.0.0.1:30303","id":"d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666","ip":"127.0.0.1","name":"ethereum_rust/0.1.0/rust1.80","ports":{"discovery":30303,"listener":30303},"protocols":{"eth":{"chainId":3151908,"homesteadBlock":0,"daoForkBlock":null,"daoForkSupport":false,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"muirGlacierBlock":null,"berlinBlock":0,"londonBlock":0,"arrowGlacierBlock":null,"grayGlacierBlock":null,"mergeNetsplitBlock":0,"shanghaiTime":0,"cancunTime":0,"pragueTime":1718232101,"verkleTime":null,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true}}}}"#, ); assert_eq!(rpc_response.to_string(), expected_response.to_string()) } From dac53039dcd15a1bb9f0cd20e0ab80910ecedef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerem=C3=ADas=20Salom=C3=B3n?= <48994069+JereSalo@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:14:25 -0300 Subject: [PATCH 02/49] feat(levm): implement Transaction Report (#930) **Motivation** Implement interface transaction report and make call work with it. **Description** - Implement interface transaction report and fixed problems with users of that interface. - Refactor of `returndata`. Created `sub_return_data` for subcontext and left return_data for current context. - Modify opcodes `RETURNDATASIZE` and `RETURNDATACOPY` to use `sub_return_data` - Changes in generic_call()'s logic Out of scope but implemented: - Fix revm comparison tests display (levm results weren't being displayed before) Closes #926 Closes #923 - partially addressed, there are still changes to make to `generic_call()` Closes #922 Closes #938 - Even though this issue is out of scope, the fix was only a one line change. --- crates/vm/levm/README.md | 3 + .../vm/levm/bench/revm_comparison/src/lib.rs | 20 +- crates/vm/levm/docs/callframe.md | 6 + crates/vm/levm/src/call_frame.rs | 13 +- crates/vm/levm/src/errors.rs | 27 ++ crates/vm/levm/src/memory.rs | 6 + .../levm/src/opcode_handlers/environment.rs | 4 +- crates/vm/levm/src/utils.rs | 2 +- crates/vm/levm/src/vm.rs | 110 +++-- crates/vm/levm/tests/tests.rs | 426 +++++++++--------- 10 files changed, 353 insertions(+), 264 deletions(-) create mode 100644 crates/vm/levm/docs/callframe.md diff --git a/crates/vm/levm/README.md b/crates/vm/levm/README.md index 55b1faa21..525ef97b0 100644 --- a/crates/vm/levm/README.md +++ b/crates/vm/levm/README.md @@ -29,6 +29,9 @@ Features: ### Running [🇼🇮🇵] +### Documentation +[CallFrame](./docs/callframe.md) + ### Testing To run the project's tests, do `make test`. diff --git a/crates/vm/levm/bench/revm_comparison/src/lib.rs b/crates/vm/levm/bench/revm_comparison/src/lib.rs index 254db0b7c..8b1ffcb0a 100644 --- a/crates/vm/levm/bench/revm_comparison/src/lib.rs +++ b/crates/vm/levm/bench/revm_comparison/src/lib.rs @@ -1,4 +1,6 @@ -use ethereum_rust_levm::{call_frame::CallFrame, primitives::Bytes, utils::new_vm_with_bytecode}; +use ethereum_rust_levm::{ + call_frame::CallFrame, errors::TxResult, primitives::Bytes, utils::new_vm_with_bytecode, +}; use revm::{ db::BenchmarkDB, primitives::{address, Bytecode, TransactTo}, @@ -22,20 +24,20 @@ pub fn run_with_levm(program: &str, runs: usize, number_of_iterations: u32) { let mut vm = new_vm_with_bytecode(Bytes::new()); *vm.current_call_frame_mut() = call_frame.clone(); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = black_box(vm.execute(&mut current_call_frame)); - assert!(result.is_ok()); + let tx_report = black_box(vm.execute(&mut current_call_frame)); + assert!(tx_report.result == TxResult::Success); } let mut vm = new_vm_with_bytecode(Bytes::new()); *vm.current_call_frame_mut() = call_frame.clone(); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = black_box(vm.execute(&mut current_call_frame)); - assert!(result.is_ok()); + let tx_report = black_box(vm.execute(&mut current_call_frame)); + assert!(tx_report.result == TxResult::Success); - match result { - Ok(_) => { - println!("\t\t0x{}", hex::encode(current_call_frame.calldata)); + match tx_report.result { + TxResult::Success => { + println!("\t\t0x{}", hex::encode(current_call_frame.returndata)); } - Err(error) => panic!("Execution failed: {:?}", error), + TxResult::Revert(error) => panic!("Execution failed: {:?}", error), } } diff --git a/crates/vm/levm/docs/callframe.md b/crates/vm/levm/docs/callframe.md new file mode 100644 index 000000000..0f0e148e3 --- /dev/null +++ b/crates/vm/levm/docs/callframe.md @@ -0,0 +1,6 @@ +### CallFrame + +The CallFrame has attributes `returndata` and `sub_return_data` to store both the return data of the current context and of the sub-context. + +Opcodes like `RETURNDATACOPY` and `RETURNDATASIZE` access the return data of the subcontext (`sub_return_data`). +Meanwhile, opcodes like `RETURN` or `REVERT` modify the return data of the current context (`returndata`). diff --git a/crates/vm/levm/src/call_frame.rs b/crates/vm/levm/src/call_frame.rs index c1fb05806..80e308229 100644 --- a/crates/vm/levm/src/call_frame.rs +++ b/crates/vm/levm/src/call_frame.rs @@ -62,7 +62,7 @@ pub struct CallFrame { pub gas_limit: U256, pub gas_used: U256, pub pc: usize, - pub msg_sender: Address, + pub msg_sender: Address, // Origin address? pub to: Address, pub code_address: Address, pub delegate: Option
, @@ -71,10 +71,13 @@ pub struct CallFrame { pub stack: Stack, // max 1024 in the future pub memory: Memory, pub calldata: Bytes, + /// Return data of the CURRENT CONTEXT (see docs for more details) pub returndata: Bytes, - // where to store return data of subcall - pub return_data_offset: Option, - pub return_data_size: Option, + /// Return data of the SUB-CONTEXT (see docs for more details) + pub sub_return_data: Bytes, + /// where to store return data of sub-context in memory + pub sub_return_data_offset: usize, + pub sub_return_data_size: usize, pub is_static: bool, pub transient_storage: TransientStorage, pub logs: Vec, @@ -101,6 +104,7 @@ impl CallFrame { calldata: Bytes, is_static: bool, gas_limit: U256, + gas_used: U256, depth: usize, ) -> Self { Self { @@ -114,6 +118,7 @@ impl CallFrame { calldata, is_static, depth, + gas_used, ..Default::default() } } diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index 5f0829aa6..d1f8a59da 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -1,3 +1,10 @@ +use std::collections::HashMap; + +use bytes::Bytes; +use ethereum_types::Address; + +use crate::{call_frame::Log, vm::Account}; + /// Errors that halt the program #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum VMError { @@ -12,6 +19,7 @@ pub enum VMError { OverflowInArithmeticOp, FatalError, InvalidTransaction, + RevertOpcode, SenderAccountDoesNotExist, SenderAccountShouldNotHaveBytecode, SenderBalanceShouldContainTransferValue, @@ -29,3 +37,22 @@ pub enum ResultReason { Revert, Return, } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TxResult { + Success, + Revert(VMError), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TransactionReport { + pub result: TxResult, + pub new_state: HashMap, + pub gas_used: u64, + pub gas_refunded: u64, + pub output: Bytes, + pub logs: Vec, + // This only applies to create transactions. It's fundamentally ambiguous since + // a transaction could create multiple new contracts, but whatever. + pub created_address: Option
, +} diff --git a/crates/vm/levm/src/memory.rs b/crates/vm/levm/src/memory.rs index 36c805c2b..eff1d1590 100644 --- a/crates/vm/levm/src/memory.rs +++ b/crates/vm/levm/src/memory.rs @@ -51,6 +51,12 @@ impl Memory { .splice(offset..offset + len, value.iter().copied()); } + pub fn store_n_bytes(&mut self, offset: usize, value: &[u8], size: usize) { + self.resize(offset + size); + self.data + .splice(offset..offset + size, value.iter().copied()); + } + pub fn size(&self) -> U256 { U256::from(self.data.len()) } diff --git a/crates/vm/levm/src/opcode_handlers/environment.rs b/crates/vm/levm/src/opcode_handlers/environment.rs index dd2880d00..6278a1702 100644 --- a/crates/vm/levm/src/opcode_handlers/environment.rs +++ b/crates/vm/levm/src/opcode_handlers/environment.rs @@ -310,7 +310,7 @@ impl VM { current_call_frame .stack - .push(U256::from(current_call_frame.returndata.len()))?; + .push(U256::from(current_call_frame.sub_return_data.len()))?; Ok(OpcodeSuccess::Continue) } @@ -351,7 +351,7 @@ impl VM { } let data = current_call_frame - .returndata + .sub_return_data .slice(returndata_offset..returndata_offset + size); current_call_frame.memory.store_bytes(dest_offset, &data); diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 6a51216b9..34d915abc 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -60,7 +60,7 @@ pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256 address, Default::default(), Default::default(), - U256::MAX, + U256::MAX, // arbitrary gas limit for now... Default::default(), Default::default(), Default::default(), diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index e44726e6f..bbca1e0dc 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -1,7 +1,7 @@ use crate::{ call_frame::CallFrame, constants::*, - errors::{OpcodeSuccess, ResultReason, VMError}, + errors::{OpcodeSuccess, ResultReason, TransactionReport, TxResult, VMError}, opcodes::Opcode, primitives::{Address, Bytes, H256, U256}, }; @@ -163,7 +163,7 @@ pub struct Environment { /// this execution. pub origin: Address, pub consumed_gas: U256, - refunded_gas: U256, + pub refunded_gas: U256, pub gas_limit: U256, pub block_number: U256, pub coinbase: Address, @@ -241,6 +241,7 @@ impl VM { calldata.clone(), false, gas_limit, + TX_BASE_COST, 0, ); @@ -268,10 +269,7 @@ impl VM { } } - pub fn execute( - &mut self, - current_call_frame: &mut CallFrame, - ) -> Result { + pub fn execute(&mut self, current_call_frame: &mut CallFrame) -> TransactionReport { // let mut current_call_frame = self // .call_frames // .pop() @@ -374,16 +372,47 @@ impl VM { _ => Err(VMError::OpcodeNotFound), }; + // Gas refunds are applied at the end of a transaction. Should it be implemented here? + match op_result { Ok(OpcodeSuccess::Continue) => {} - Ok(OpcodeSuccess::Result(_)) | Err(_) => { + Ok(OpcodeSuccess::Result(_)) => { self.call_frames.push(current_call_frame.clone()); - return op_result; + return TransactionReport { + result: TxResult::Success, + new_state: self.db.accounts.clone(), + gas_used: current_call_frame.gas_used.low_u64(), + gas_refunded: self.env.refunded_gas.low_u64(), + output: current_call_frame.returndata.clone(), + logs: current_call_frame.logs.clone(), + created_address: None, + }; + } + Err(error) => { + self.call_frames.push(current_call_frame.clone()); + + // CONSUME ALL GAS UNLESS THE ERROR IS FROM REVERT OPCODE + if error != VMError::RevertOpcode { + let left_gas = current_call_frame.gas_limit - current_call_frame.gas_used; + current_call_frame.gas_used += left_gas; + self.env.consumed_gas += left_gas; + } + + return TransactionReport { + result: TxResult::Revert(error), + new_state: self.db.accounts.clone(), + gas_used: current_call_frame.gas_used.low_u64(), + gas_refunded: self.env.refunded_gas.low_u64(), + output: current_call_frame.returndata.clone(), + logs: current_call_frame.logs.clone(), + created_address: None, + }; } } } } + // let account = self.db.accounts.get(&self.env.origin).unwrap(); /// Based on Ethereum yellow paper's initial tests of intrinsic validity (Section 6). The last version is /// Shanghai, so there are probably missing Cancun validations. The intrinsic validations are: /// @@ -422,7 +451,7 @@ impl VM { Ok(()) } - pub fn transact(&mut self) -> Result { + pub fn transact(&mut self) -> Result { self.validate_transaction()?; let initial_gas = Default::default(); @@ -430,7 +459,7 @@ impl VM { self.env.consumed_gas = initial_gas; let mut current_call_frame = self.call_frames.pop().unwrap(); - self.execute(&mut current_call_frame) + Ok(self.execute(&mut current_call_frame)) } pub fn current_call_frame_mut(&mut self) -> &mut CallFrame { @@ -494,52 +523,39 @@ impl VM { calldata, is_static, gas_limit, + U256::zero(), current_call_frame.depth + 1, ); - current_call_frame.return_data_offset = Some(ret_offset); - current_call_frame.return_data_size = Some(ret_size); + current_call_frame.sub_return_data_offset = ret_offset; + current_call_frame.sub_return_data_size = ret_size; // self.call_frames.push(new_call_frame.clone()); - let result = self.execute(&mut new_call_frame); - - // After this we should do current_call_frame.gas_used += new_call_frame.gas_used. Instead of consuming all gas and then returning unused gas, we just consume all gas the sub-context used. If an error happened then we consume all gas. - - match result { - Ok(OpcodeSuccess::Result(reason)) => match reason { - ResultReason::Stop | ResultReason::Return => { - let logs = new_call_frame.logs.clone(); - let return_data = new_call_frame.returndata.clone(); - - current_call_frame.logs.extend(logs); - current_call_frame - .memory - .store_bytes(ret_offset, &return_data); - current_call_frame.returndata = return_data; - current_call_frame - .stack - .push(U256::from(SUCCESS_FOR_CALL))?; - Ok(OpcodeSuccess::Continue) - } - ResultReason::Revert => { - let output = new_call_frame.returndata.clone(); - - current_call_frame.memory.store_bytes(ret_offset, &output); - current_call_frame.returndata = output; - current_call_frame.stack.push(U256::from(REVERT_FOR_CALL))?; - // current_call_frame.gas -= self.env.consumed_gas; - self.env.refunded_gas += self.env.consumed_gas; - Ok(OpcodeSuccess::Continue) - } - }, - Ok(OpcodeSuccess::Continue) => Ok(OpcodeSuccess::Continue), - Err(error) => { + let tx_report = self.execute(&mut new_call_frame); + + current_call_frame.gas_used += tx_report.gas_used.into(); // We add the gas used by the sub-context to the current one after it's execution. + current_call_frame.logs.extend(tx_report.logs); + current_call_frame + .memory + .store_n_bytes(ret_offset, &tx_report.output, ret_size); + current_call_frame.sub_return_data = tx_report.output; + + // What to do, depending on TxResult + match tx_report.result { + TxResult::Success => { current_call_frame .stack - .push(U256::from(error.clone() as u8))?; - Err(error) + .push(U256::from(SUCCESS_FOR_CALL))?; + } + TxResult::Revert(_error) => { + // Behavior for revert between contexts goes here if necessary + // It is also possible to differentiate between RevertOpcode error and other kinds of revert. + + current_call_frame.stack.push(U256::from(REVERT_FOR_CALL))?; } } + + Ok(OpcodeSuccess::Continue) } /// Calculates the address of a new conctract using the CREATE opcode as follow diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index 071592798..e90307734 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -1,6 +1,6 @@ use ethereum_rust_levm::{ constants::*, - errors::VMError, + errors::{TxResult, VMError}, operations::Operation, primitives::{Address, Bytes, H256, U256}, utils::{new_vm_with_ops, new_vm_with_ops_addr_bal}, @@ -61,7 +61,7 @@ fn add_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); assert!(vm.current_call_frame_mut().pc() == 68); @@ -77,7 +77,7 @@ fn mul_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(8)); } @@ -92,7 +92,7 @@ fn sub_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(2)); } @@ -108,7 +108,7 @@ fn div_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(5)); @@ -121,7 +121,7 @@ fn div_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::zero()); } @@ -137,7 +137,7 @@ fn sdiv_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(2)); } @@ -153,7 +153,7 @@ fn mod_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(1)); } @@ -170,7 +170,7 @@ fn smod_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(1)); @@ -196,7 +196,7 @@ fn smod_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let c = U256::from_str_radix( "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", @@ -219,7 +219,7 @@ fn addmod_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(4)); } @@ -236,7 +236,7 @@ fn mulmod_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(4)); } @@ -252,7 +252,7 @@ fn exp_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(100)); } @@ -268,7 +268,7 @@ fn sign_extend_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::from(0x7F)); // Case 2: Input: 0, 0xFF. Output: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF @@ -280,7 +280,7 @@ fn sign_extend_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::MAX); } @@ -295,7 +295,7 @@ fn lt_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); } @@ -311,7 +311,7 @@ fn gt_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); } @@ -327,7 +327,7 @@ fn slt_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); } @@ -343,7 +343,7 @@ fn sgt_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); } @@ -359,7 +359,7 @@ fn eq_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); // Case 2: Input: 10, 20. Output: 0 (false) @@ -371,7 +371,7 @@ fn eq_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::zero()); } @@ -385,7 +385,7 @@ fn is_zero_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::one()); // Case 2: Input is non-zero (e.g., 10), Output should be 0 (since 10 != 0 is false) @@ -396,7 +396,7 @@ fn is_zero_op() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!(vm.current_call_frame_mut().stack.pop().unwrap() == U256::zero()); } @@ -410,7 +410,7 @@ fn and_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1000)); @@ -429,7 +429,7 @@ fn and_binary_with_zero() { TX_BASE_COST + gas_cost::AND + gas_cost::PUSHN.checked_mul(U256::from(2)).unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -446,7 +446,7 @@ fn and_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF0F0)); @@ -460,7 +460,7 @@ fn and_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF000)); @@ -474,7 +474,7 @@ fn and_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1000000000000)); @@ -491,7 +491,7 @@ fn or_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1110)); @@ -505,7 +505,7 @@ fn or_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1010)); @@ -519,7 +519,7 @@ fn or_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFFFFFFFFFFFFFFFF_u64)); @@ -536,7 +536,7 @@ fn or_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFFFF)); @@ -550,7 +550,7 @@ fn or_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF0F0)); @@ -564,7 +564,7 @@ fn or_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1011111100101111)); @@ -581,7 +581,7 @@ fn xor_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b110)); @@ -595,7 +595,7 @@ fn xor_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b1010)); @@ -609,7 +609,7 @@ fn xor_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(u64::MAX)); @@ -623,7 +623,7 @@ fn xor_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -640,7 +640,7 @@ fn xor_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFF)); @@ -654,7 +654,7 @@ fn xor_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -668,7 +668,7 @@ fn xor_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF0F)); @@ -682,7 +682,7 @@ fn xor_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF0)); @@ -696,7 +696,7 @@ fn xor_with_hex_numbers() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0b111011001000100)); @@ -712,7 +712,7 @@ fn not() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); let expected = !U256::from(0b1010); @@ -726,7 +726,7 @@ fn not() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -739,7 +739,7 @@ fn not() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::MAX); @@ -752,7 +752,7 @@ fn not() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::MAX - 1); @@ -769,7 +769,7 @@ fn byte_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF1)); @@ -783,7 +783,7 @@ fn byte_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x33)); @@ -800,7 +800,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFF)); @@ -814,7 +814,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFF)); @@ -828,7 +828,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x0D)); @@ -842,7 +842,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -856,7 +856,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -870,7 +870,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -890,7 +890,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x90)); @@ -904,7 +904,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x57)); @@ -918,7 +918,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xDD)); @@ -932,7 +932,7 @@ fn byte_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x40)); @@ -949,7 +949,7 @@ fn shl_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xDDDD)); @@ -963,7 +963,7 @@ fn shl_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x2468acf0)); @@ -977,7 +977,7 @@ fn shl_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(4886718336_u64)); @@ -991,7 +991,7 @@ fn shl_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xFF << 4)); @@ -1008,7 +1008,7 @@ fn shl_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -1022,7 +1022,7 @@ fn shl_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -1036,7 +1036,7 @@ fn shl_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::MAX - 1); @@ -1053,7 +1053,7 @@ fn shr_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xDDDD)); @@ -1067,7 +1067,7 @@ fn shr_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x91a2b3c)); @@ -1081,7 +1081,7 @@ fn shr_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x1234567)); @@ -1095,7 +1095,7 @@ fn shr_basic() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0xF)); @@ -1112,7 +1112,7 @@ fn shr_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -1126,7 +1126,7 @@ fn shr_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::zero()); @@ -1140,7 +1140,7 @@ fn shr_edge_cases() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::MAX >> 1); @@ -1157,7 +1157,7 @@ fn sar_shift_by_0() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x12345678)); @@ -1180,7 +1180,7 @@ fn sar_shifting_large_value_with_all_bits_set() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); let expected = U256::from_big_endian(&[ @@ -1208,7 +1208,7 @@ fn sar_shifting_negative_value_and_small_shift() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); let expected = U256::from_big_endian(&[ @@ -1230,7 +1230,7 @@ fn sar_shift_positive_value() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(result, U256::from(0x07FFFF)); @@ -1253,7 +1253,7 @@ fn sar_shift_negative_value() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let result = vm.current_call_frame_mut().stack.pop().unwrap(); let expected = U256::from_big_endian(&[ @@ -1286,7 +1286,7 @@ fn keccak256_zero_offset_size_four() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1316,7 +1316,7 @@ fn keccak256_zero_offset_size_bigger_than_actual_memory() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert!( vm.current_call_frame_mut().stack.pop().unwrap() @@ -1338,7 +1338,7 @@ fn keccak256_zero_offset_zero_size() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1368,7 +1368,7 @@ fn keccak256_offset_four_size_four() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1389,7 +1389,7 @@ fn mstore() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1410,7 +1410,7 @@ fn mstore_saves_correct_value() { ]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let stored_value = vm.current_call_frame_mut().memory.load(0); @@ -1433,7 +1433,7 @@ fn mstore8() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let stored_value = vm.current_call_frame_mut().memory.load(0); @@ -1461,7 +1461,7 @@ fn mcopy() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let copied_value = vm.current_call_frame_mut().memory.load(64); assert_eq!(copied_value, U256::from(0x33333)); @@ -1485,7 +1485,7 @@ fn mload() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let loaded_value = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(loaded_value, U256::from(0x33333)); @@ -1499,7 +1499,7 @@ fn msize() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let initial_size = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(initial_size, U256::from(0)); @@ -1516,7 +1516,7 @@ fn msize() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let after_store_size = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(after_store_size, U256::from(32)); @@ -1533,7 +1533,7 @@ fn msize() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let final_size = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(final_size, U256::from(96)); @@ -1555,7 +1555,7 @@ fn mstore_mload_offset_not_multiple_of_32() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let memory_size = vm.current_call_frame_mut().stack.pop().unwrap(); let loaded_value = vm.current_call_frame_mut().stack.pop().unwrap(); @@ -1579,7 +1579,7 @@ fn mstore_mload_offset_not_multiple_of_32() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let memory_size = vm.current_call_frame_mut().stack.pop().unwrap(); let loaded_value = vm.current_call_frame_mut().stack.pop().unwrap(); @@ -1601,7 +1601,7 @@ fn mload_uninitialized_memory() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let memory_size = vm.current_call_frame_mut().stack.pop().unwrap(); let loaded_value = vm.current_call_frame_mut().stack.pop().unwrap(); @@ -1642,7 +1642,7 @@ fn call_returns_if_bytecode_empty() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let success = vm.current_call_frame_mut().stack.pop().unwrap(); assert_eq!(success, U256::one()); @@ -1679,18 +1679,19 @@ fn call_changes_callframe_and_stores() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); let success = current_call_frame.stack.pop().unwrap() == U256::one(); assert!(success); - let ret_offset = 0; - let ret_size = 32; - let return_data = current_call_frame - .returndata - .slice(ret_offset..ret_offset + ret_size); + // These are ret_offset and ret_size used in CALL operation before. + let ret_offset = current_call_frame.sub_return_data_offset; + let ret_size = current_call_frame.sub_return_data_size; + + // Return data of the sub-context will be in the memory position of the current context reserved for that purpose (ret_offset and ret_size) + let return_data = current_call_frame.memory.load_range(ret_offset, ret_size); assert_eq!(U256::from_big_endian(&return_data), U256::from(0xAAAAAAA)); } @@ -1767,7 +1768,7 @@ fn nested_calls() { vm.db.add_account(callee3_address, callee3_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -1777,7 +1778,7 @@ fn nested_calls() { let ret_offset = 0; let ret_size = 64; let return_data = current_call_frame - .returndata + .sub_return_data .slice(ret_offset..ret_offset + ret_size); let mut expected_bytes = vec![0u8; 64]; @@ -1835,7 +1836,7 @@ fn staticcall_changes_callframe_is_static() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let mut current_call_frame = vm.call_frames[0].clone(); @@ -1854,11 +1855,14 @@ fn pop_on_empty_stack() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); + let tx_report = vm.execute(&mut current_call_frame); // result should be a Halt with error VMError::StackUnderflow - assert!(matches!(result, Err(VMError::StackUnderflow))); + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::StackUnderflow) + )); // TODO: assert consumed gas } @@ -1868,7 +1872,7 @@ fn pc_op() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1888,7 +1892,7 @@ fn pc_op_with_push_offset() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -1942,7 +1946,7 @@ fn delegatecall_changes_own_storage_and_regular_call_doesnt() { current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let storage_slot = vm.db.read_account_storage( &Address::from_low_u64_be(U256::from(1).low_u64()), @@ -2001,7 +2005,7 @@ fn delegatecall_changes_own_storage_and_regular_call_doesnt() { current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let storage_slot = vm.db.read_account_storage(&callee_address, &U256::zero()); let slot = StorageSlot { @@ -2058,7 +2062,7 @@ fn delegatecall_and_callcode_differ_on_value_and_msg_sender() { current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -2109,7 +2113,7 @@ fn delegatecall_and_callcode_differ_on_value_and_msg_sender() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.call_frames[0].clone(); @@ -2143,8 +2147,11 @@ fn jump_position_bigger_than_program_bytecode_size() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); - assert!(matches!(result, Err(VMError::InvalidJump))); + let tx_report = vm.execute(&mut current_call_frame); + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::InvalidJump) + )); // TODO: assert consumed gas } @@ -2162,7 +2169,7 @@ fn jumpi_not_zero() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2187,7 +2194,7 @@ fn jumpi_for_zero() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2213,7 +2220,7 @@ fn calldataload() { vm.current_call_frame_mut().calldata = calldata; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -2282,7 +2289,7 @@ fn calldataload_being_set_by_parent() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -2306,7 +2313,7 @@ fn calldatasize() { vm.current_call_frame_mut().calldata = calldata; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); let top_of_stack = current_call_frame.stack.pop().unwrap(); @@ -2329,7 +2336,7 @@ fn calldatacopy() { vm.current_call_frame_mut().calldata = calldata; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); let memory = current_call_frame.memory.load_range(0, 2); @@ -2343,10 +2350,10 @@ fn returndatasize() { let ops = vec![Operation::ReturnDataSize, Operation::Stop]; let mut vm = new_vm_with_ops(&ops); - vm.current_call_frame_mut().returndata = returndata; + vm.current_call_frame_mut().sub_return_data = returndata; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); let top_of_stack = current_call_frame.stack.pop().unwrap(); @@ -2366,10 +2373,10 @@ fn returndatacopy() { ]; let mut vm = new_vm_with_ops(&ops); - vm.current_call_frame_mut().returndata = returndata; + vm.current_call_frame_mut().sub_return_data = returndata; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); let memory = current_call_frame.memory.load_range(0, 2); @@ -2411,7 +2418,7 @@ fn returndatacopy_being_set_by_parent() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -2441,7 +2448,7 @@ fn blockhash_op() { vm.env.block_number = current_block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2473,7 +2480,7 @@ fn blockhash_same_block_number() { vm.env.block_number = current_block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2503,7 +2510,7 @@ fn blockhash_block_number_not_from_recent_256() { vm.env.block_number = current_block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2522,7 +2529,7 @@ fn coinbase_op() { vm.env.coinbase = Address::from_low_u64_be(coinbase_address); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2541,7 +2548,7 @@ fn timestamp_op() { vm.env.timestamp = timestamp; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), timestamp); assert_eq!(vm.env.consumed_gas, TX_BASE_COST + 2); @@ -2557,7 +2564,7 @@ fn number_op() { vm.env.block_number = block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2576,7 +2583,7 @@ fn prevrandao_op() { vm.env.prev_randao = Some(prevrandao); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2595,7 +2602,7 @@ fn gaslimit_op() { vm.env.gas_limit = gas_limit; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), gas_limit); assert_eq!(vm.env.consumed_gas, TX_BASE_COST + 2); @@ -2611,7 +2618,7 @@ fn chain_id_op() { vm.env.chain_id = chain_id; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), chain_id); assert_eq!(vm.env.consumed_gas, TX_BASE_COST + 2); @@ -2627,7 +2634,7 @@ fn basefee_op() { vm.env.base_fee_per_gas = base_fee_per_gas; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2646,7 +2653,7 @@ fn blobbasefee_op() { vm.env.block_blob_gas_used = Some(U256::zero()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2665,7 +2672,7 @@ fn blobbasefee_minimum_cost() { vm.env.block_blob_gas_used = Some(U256::zero()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2686,7 +2693,7 @@ fn pop_op() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2709,7 +2716,7 @@ fn jump_op() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -2732,8 +2739,11 @@ fn jump_not_jumpdest_position() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); - assert!(matches!(result, Err(VMError::InvalidJump))); + let tx_report = vm.execute(&mut current_call_frame); + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::InvalidJump) + )); // TODO: assert consumed gas } @@ -2754,7 +2764,7 @@ fn sstore_op() { vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let account = vm.db.accounts.get(&sender_address).unwrap(); let stored_value = account.storage.get(&key).unwrap(); @@ -2762,7 +2772,6 @@ fn sstore_op() { } #[test] -#[should_panic] fn sstore_reverts_when_called_in_static() { let key = U256::from(80); let value = U256::from(100); @@ -2776,7 +2785,12 @@ fn sstore_reverts_when_called_in_static() { let mut vm = new_vm_with_ops(&operations); vm.current_call_frame_mut().is_static = true; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + let tx_report = vm.execute(&mut current_call_frame); + + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::OpcodeNotAllowedInStaticContext) + )); } #[test] @@ -2798,7 +2812,7 @@ fn sload_op() { vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(value, vm.current_call_frame_mut().stack.pop().unwrap()); } @@ -2814,7 +2828,7 @@ fn sload_untouched_key_of_storage() { vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( U256::zero(), @@ -2832,7 +2846,7 @@ fn sload_on_not_existing_account() { vm.current_call_frame_mut().msg_sender = sender_address; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( U256::zero(), @@ -2856,7 +2870,7 @@ fn log0() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -2886,7 +2900,7 @@ fn log1() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -2919,7 +2933,7 @@ fn log2() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -2958,7 +2972,7 @@ fn log3() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -3004,7 +3018,7 @@ fn log4() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -3038,7 +3052,7 @@ fn log_with_0_data_size() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; assert_eq!(logs.len(), 1); @@ -3064,11 +3078,11 @@ fn cant_create_log_in_static_context() { let mut vm: VM = new_vm_with_ops(&operations); vm.current_call_frame_mut().is_static = true; let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); + let tx_report = vm.execute(&mut current_call_frame); assert!(matches!( - result, - Err(VMError::OpcodeNotAllowedInStaticContext) + tx_report.result, + TxResult::Revert(VMError::OpcodeNotAllowedInStaticContext) )); } @@ -3088,7 +3102,7 @@ fn log_with_data_in_memory_smaller_than_size() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let mut data = vec![0_u8; 16]; @@ -3123,7 +3137,7 @@ fn multiple_logs_of_different_types() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let logs = &vm.current_call_frame_mut().logs; let data = [0xff_u8; 32].as_slice(); @@ -3185,7 +3199,7 @@ fn logs_from_multiple_callers() { vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(current_call_frame.logs.len(), 2) } @@ -3230,7 +3244,7 @@ fn logs_from_multiple_callers() { // vm.db.add_account(callee_address, callee_account); // let mut current_call_frame = vm.call_frames.pop().unwrap(); -// vm.execute(&mut current_call_frame).unwrap(); +// vm.execute(&mut current_call_frame); // assert_eq!( // vm.current_call_frame_mut().stack.pop().unwrap(), @@ -3243,7 +3257,7 @@ fn push0_ok() { let mut vm = new_vm_with_ops(&[Operation::Push0, Operation::Stop]); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.stack[0], U256::zero()); assert_eq!(vm.current_call_frame_mut().pc(), 2); @@ -3256,7 +3270,7 @@ fn push1_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.stack[0], to_push); assert_eq!(vm.current_call_frame_mut().pc(), 3); @@ -3269,7 +3283,7 @@ fn push5_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.stack[0], to_push); assert_eq!(vm.current_call_frame_mut().pc(), 7); @@ -3282,7 +3296,7 @@ fn push31_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.stack[0], to_push); assert_eq!(vm.current_call_frame_mut().pc(), 33); @@ -3295,7 +3309,7 @@ fn push32_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.stack[0], to_push); assert_eq!(vm.current_call_frame_mut().pc(), 34); @@ -3312,7 +3326,7 @@ fn dup1_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let stack_len = vm.current_call_frame_mut().stack.len(); @@ -3338,7 +3352,7 @@ fn dup16_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let stack_len = vm.current_call_frame_mut().stack.len(); @@ -3360,9 +3374,12 @@ fn dup_halts_if_stack_underflow() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); + let tx_report = vm.execute(&mut current_call_frame); - assert!(matches!(result, Err(VMError::StackUnderflow))); + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::StackUnderflow) + )); } #[test] @@ -3377,7 +3394,7 @@ fn swap1_ok() { ]; let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.len(), 2); assert_eq!(vm.current_call_frame_mut().pc(), 6); @@ -3397,7 +3414,7 @@ fn swap16_ok() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let stack_len = vm.current_call_frame_mut().stack.len(); assert_eq!(stack_len, 17); @@ -3418,9 +3435,12 @@ fn swap_halts_if_stack_underflow() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - let result = vm.execute(&mut current_call_frame); + let tx_report = vm.execute(&mut current_call_frame); - assert!(matches!(result, Err(VMError::StackUnderflow))); + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::StackUnderflow) + )); } #[test] @@ -3442,7 +3462,7 @@ fn transient_store() { assert!(current_call_frame.transient_storage.is_empty()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let current_call_frame = vm.current_call_frame_mut(); @@ -3456,15 +3476,19 @@ fn transient_store() { } #[test] -#[should_panic] -fn transient_store_no_values_panics() { +fn transient_store_stack_underflow() { let operations = [Operation::Tstore, Operation::Stop]; let mut vm = new_vm_with_ops(&operations); assert!(vm.current_call_frame_mut().transient_storage.is_empty()); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + let tx_report = vm.execute(&mut current_call_frame); + + assert!(matches!( + tx_report.result, + TxResult::Revert(VMError::StackUnderflow) + )); } #[test] @@ -3487,7 +3511,7 @@ fn transient_load() { .insert((caller, key), value); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( *vm.current_call_frame_mut().stack.stack.last().unwrap(), @@ -3521,7 +3545,7 @@ fn create_happy_path() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let return_of_created_callframe = call_frame.stack.pop().unwrap(); @@ -3553,7 +3577,7 @@ fn cant_create_with_size_longer_than_max_code_size() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let create_return_value = call_frame.stack.pop().unwrap(); @@ -3581,7 +3605,7 @@ fn cant_create_on_static_contexts() { vm.current_call_frame_mut().is_static = true; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let create_return_value = call_frame.stack.pop().unwrap(); @@ -3608,7 +3632,7 @@ fn cant_create_if_transfer_value_bigger_than_balance() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let create_return_value = call_frame.stack.pop().unwrap(); @@ -3645,7 +3669,7 @@ fn cant_create_if_sender_nonce_would_overflow() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let create_return_value = call_frame.stack.pop().unwrap(); @@ -3689,7 +3713,7 @@ fn cant_create_accounts_with_same_address() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); @@ -3717,7 +3741,7 @@ fn cant_create_accounts_with_same_address() { new_vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = new_vm.call_frames.pop().unwrap(); - new_vm.execute(&mut current_call_frame).unwrap(); + new_vm.execute(&mut current_call_frame); let call_frame = new_vm.current_call_frame_mut(); let return_of_created_callframe = call_frame.stack.pop().unwrap(); assert_eq!(return_of_created_callframe, U256::from(REVERT_FOR_CREATE)); @@ -3759,7 +3783,7 @@ fn create2_happy_path() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); let call_frame = vm.current_call_frame_mut(); let return_of_created_callframe = call_frame.stack.pop().unwrap(); @@ -3803,7 +3827,7 @@ fn create_on_create() { vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.db.accounts.len(), 4); } @@ -3839,7 +3863,7 @@ fn caller_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -3880,7 +3904,7 @@ fn origin_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -3906,7 +3930,7 @@ fn balance_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); dbg!(&vm); @@ -3947,7 +3971,7 @@ fn address_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -3990,7 +4014,7 @@ fn selfbalance_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), balance); assert_eq!(vm.env.consumed_gas, TX_BASE_COST + gas_cost::SELFBALANCE); @@ -4029,7 +4053,7 @@ fn callvalue_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), value); assert_eq!(vm.env.consumed_gas, TX_BASE_COST + gas_cost::CALLVALUE); @@ -4067,7 +4091,7 @@ fn codesize_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -4107,7 +4131,7 @@ fn gasprice_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), @@ -4164,7 +4188,7 @@ fn codecopy_op() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().memory.load(0), expected_memory); assert_eq!( @@ -4189,7 +4213,7 @@ fn extcodesize_existing_account() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), 23.into()); assert_eq!(vm.env.consumed_gas, 23603.into()); } @@ -4206,7 +4230,7 @@ fn extcodesize_non_existing_account() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!(vm.current_call_frame_mut().stack.pop().unwrap(), 0.into()); assert_eq!(vm.env.consumed_gas, 23603.into()); } @@ -4232,7 +4256,7 @@ fn extcodecopy_existing_account() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().memory.load_range(0, size), vec![0x60] @@ -4257,7 +4281,7 @@ fn extcodecopy_non_existing_account() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().memory.load_range(0, size), vec![0; size] @@ -4281,7 +4305,7 @@ fn extcodehash_account_with_empty_code() { ); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470".into() @@ -4301,7 +4325,7 @@ fn extcodehash_non_existing_account() { let mut vm = new_vm_with_ops(&operations); let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame).unwrap(); + vm.execute(&mut current_call_frame); assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470".into() From 31dc6c0b40e73d2fd6d50aa502549ea4f2251d80 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:49:24 -0300 Subject: [PATCH 03/49] feat(levm): implement blobhash logic (#940) **Motivation** The motivation is to implement the blobhash opcode, since was not implemented yet. **Description** Main changes: - Adds an atribute to store the blob hashes in Environment type - implements blobhash logic, basicly taking an element of the vec of blob hashes and pushing it to the stack - To do that, add some posible errors to VMError enum Closes #933 --- crates/vm/levm/src/errors.rs | 2 ++ crates/vm/levm/src/opcode_handlers/block.rs | 24 ++++++++++++++++++--- crates/vm/levm/src/utils.rs | 1 + crates/vm/levm/src/vm.rs | 3 +++ crates/vm/levm/tests/tests.rs | 8 +++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index d1f8a59da..12c107da1 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -19,6 +19,8 @@ pub enum VMError { OverflowInArithmeticOp, FatalError, InvalidTransaction, + MissingBlobHashes, + BlobHashIndexOutOfBounds, RevertOpcode, SenderAccountDoesNotExist, SenderAccountShouldNotHaveBytecode, diff --git a/crates/vm/levm/src/opcode_handlers/block.rs b/crates/vm/levm/src/opcode_handlers/block.rs index 5698087ee..739769108 100644 --- a/crates/vm/levm/src/opcode_handlers/block.rs +++ b/crates/vm/levm/src/opcode_handlers/block.rs @@ -2,6 +2,7 @@ use crate::{ block::LAST_AVAILABLE_BLOCK_LIMIT, constants::{BLOB_BASE_FEE_UPDATE_FRACTION, MIN_BASE_FEE_PER_BLOB_GAS}, }; +use keccak_hash::H256; // Block Information (11) // Opcodes: BLOCKHASH, COINBASE, TIMESTAMP, NUMBER, PREVRANDAO, GASLIMIT, CHAINID, SELFBALANCE, BASEFEE, BLOBHASH, BLOBBASEFEE @@ -143,16 +144,33 @@ impl VM { } // BLOBHASH operation + /// Currently not tested pub fn op_blobhash( &mut self, current_call_frame: &mut CallFrame, ) -> Result { self.increase_consumed_gas(current_call_frame, gas_cost::BLOBHASH)?; - // Should push in stack the blob hash - unimplemented!("when we have tx implemented"); + let index = current_call_frame.stack.pop()?.as_usize(); - // Ok(OpcodeSuccess::Continue) + let blob_hash: H256 = match &self.env.tx_blob_hashes { + Some(vec) => match vec.get(index) { + Some(el) => *el, + None => { + return Err(VMError::BlobHashIndexOutOfBounds); + } + }, + None => { + return Err(VMError::MissingBlobHashes); + } + }; + + // Could not find a better way to translate from H256 to U256 + let u256_blob = U256::from(blob_hash.as_bytes()); + + current_call_frame.stack.push(u256_blob)?; + + Ok(OpcodeSuccess::Continue) } fn get_blob_gasprice(&mut self) -> U256 { diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 34d915abc..599b3efea 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -71,5 +71,6 @@ pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256 state, Default::default(), Default::default(), + Default::default(), ) } diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index bbca1e0dc..ae305f6d9 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -174,6 +174,7 @@ pub struct Environment { pub gas_price: U256, pub block_excess_blob_gas: Option, pub block_blob_gas_used: Option, + pub tx_blob_hashes: Option>, } #[derive(Debug, Clone, Default)] @@ -218,6 +219,7 @@ impl VM { db: Db, block_blob_gas_used: Option, block_excess_blob_gas: Option, + tx_blob_hashes: Option>, ) -> Self { // TODO: This handles only CALL transactions. let bytecode = db.get_account_bytecode(&to); @@ -259,6 +261,7 @@ impl VM { gas_price, block_blob_gas_used, block_excess_blob_gas, + tx_blob_hashes, }; Self { diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index e90307734..4ddd2e8ee 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -3860,6 +3860,7 @@ fn caller_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3901,6 +3902,7 @@ fn origin_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3968,6 +3970,7 @@ fn address_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -4011,6 +4014,7 @@ fn selfbalance_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -4050,6 +4054,7 @@ fn callvalue_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -4088,6 +4093,7 @@ fn codesize_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -4128,6 +4134,7 @@ fn gasprice_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -4185,6 +4192,7 @@ fn codecopy_op() { db, Default::default(), Default::default(), + Default::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); From f648387414f1b2bd8f028b17b600722e2493bf76 Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:57:45 -0300 Subject: [PATCH 04/49] refactor(l2): new prover structure (#878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivation** Introduces a new structure for the L2 crates. Serves as a base for the rest of the L2 development. **Description** - mostly renames and refactored modules --------- Co-authored-by: Estéfano Bargas --- .github/workflows/ci.yaml | 10 +- Cargo.toml | 4 +- cmd/ethereum_rust/ethereum_rust.rs | 2 - cmd/ethereum_rust_l2/Cargo.toml | 6 +- cmd/ethereum_rust_l2/src/commands/stack.rs | 100 ++++++++++++---- crates/l2/.example.env | 7 +- crates/l2/Cargo.toml | 5 +- crates/l2/Makefile | 3 + crates/l2/docs/README.md | 2 +- crates/l2/docs/contracts.md | 8 +- crates/l2/docs/proposer.md | 4 +- crates/l2/docs/prover.md | 39 ++++-- crates/l2/l2.rs | 2 - crates/l2/proposer/errors.rs | 4 +- crates/l2/proposer/mod.rs | 2 +- crates/l2/proposer/prover_server.rs | 79 +++++++++---- crates/l2/prover/Cargo.toml | 45 +++++++ crates/l2/prover/mod.rs | 11 -- crates/l2/prover/prover.rs | 58 --------- crates/l2/prover/sp1/lib/Cargo.toml | 7 -- crates/l2/prover/sp1/lib/src/lib.rs | 22 ---- crates/l2/prover/sp1/program/.gitignore | 1 - crates/l2/prover/sp1/program/Cargo.toml | 11 -- crates/l2/prover/sp1/program/src/main.rs | 30 ----- crates/l2/prover/src/lib.rs | 10 ++ crates/l2/prover/src/main.rs | 20 ++++ crates/l2/prover/src/prover.rs | 77 ++++++++++++ crates/l2/prover/{ => src}/prover_client.rs | 62 +++++----- crates/l2/prover/src/utils/placeholder.rs | 0 crates/l2/prover/zkvm/.gitignore | 4 + crates/l2/prover/zkvm/Cargo.toml | 11 ++ crates/l2/prover/zkvm/NOTICE | 14 +++ crates/l2/prover/zkvm/interface/Cargo.toml | 27 +++++ crates/l2/prover/zkvm/interface/build.rs | 5 + .../l2/prover/zkvm/interface/guest/Cargo.toml | 21 ++++ .../prover/zkvm/interface/guest/src/main.rs | 111 ++++++++++++++++++ crates/l2/prover/zkvm/interface/src/lib.rs | 9 ++ crates/l2/utils/config/eth.rs | 2 +- crates/l2/utils/config/prover_client.rs | 5 +- 39 files changed, 584 insertions(+), 256 deletions(-) create mode 100644 crates/l2/prover/Cargo.toml delete mode 100644 crates/l2/prover/mod.rs delete mode 100644 crates/l2/prover/prover.rs delete mode 100644 crates/l2/prover/sp1/lib/Cargo.toml delete mode 100644 crates/l2/prover/sp1/lib/src/lib.rs delete mode 100644 crates/l2/prover/sp1/program/.gitignore delete mode 100644 crates/l2/prover/sp1/program/Cargo.toml delete mode 100644 crates/l2/prover/sp1/program/src/main.rs create mode 100644 crates/l2/prover/src/lib.rs create mode 100644 crates/l2/prover/src/main.rs create mode 100644 crates/l2/prover/src/prover.rs rename crates/l2/prover/{ => src}/prover_client.rs (56%) create mode 100644 crates/l2/prover/src/utils/placeholder.rs create mode 100644 crates/l2/prover/zkvm/.gitignore create mode 100644 crates/l2/prover/zkvm/Cargo.toml create mode 100644 crates/l2/prover/zkvm/NOTICE create mode 100644 crates/l2/prover/zkvm/interface/Cargo.toml create mode 100644 crates/l2/prover/zkvm/interface/build.rs create mode 100644 crates/l2/prover/zkvm/interface/guest/Cargo.toml create mode 100644 crates/l2/prover/zkvm/interface/guest/src/main.rs create mode 100644 crates/l2/prover/zkvm/interface/src/lib.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dfaa31600..9bcb87eb0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,10 +90,10 @@ jobs: - name: RISC-V zkVM toolchain install run: | - curl -L https://sp1.succinct.xyz | bash - ~/.sp1/bin/sp1up + curl -L https://risczero.com/install | bash + ~/.risc0/bin/rzup install - - name: Build program + - name: Build prover and zkVM run: | - cd crates/l2/prover/sp1/program - ~/.sp1/bin/cargo-prove prove build + cd crates/l2/prover + cargo build --release --features build_zkvm diff --git a/Cargo.toml b/Cargo.toml index b7c006566..53b224765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,10 @@ members = [ "cmd/ethereum_rust_l2", "crates/vm/levm", "crates/vm/levm/bench/revm_comparison", + "crates/l2/prover", ] resolver = "2" -default-members = ["cmd/ethereum_rust", "cmd/ethereum_rust_l2"] +default-members = ["cmd/ethereum_rust", "cmd/ethereum_rust_l2", "crates/l2/prover"] [workspace.package] version = "0.1.0" @@ -32,6 +33,7 @@ ethereum_rust-vm = { path = "./crates/vm" } ethereum_rust-trie = { path = "./crates/storage/trie" } ethereum_rust-rlp = { path = "./crates/common/rlp" } ethereum_rust-l2 = { path = "./crates/l2" } +ethereum_rust-prover = { path = "./crates/l2/prover" } tracing = { version = "0.1", features = ["log"] } tracing-subscriber = "0.3.0" diff --git a/cmd/ethereum_rust/ethereum_rust.rs b/cmd/ethereum_rust/ethereum_rust.rs index be6853b5e..af94e955f 100644 --- a/cmd/ethereum_rust/ethereum_rust.rs +++ b/cmd/ethereum_rust/ethereum_rust.rs @@ -177,8 +177,6 @@ async fn main() { if #[cfg(feature = "l2")] { let l2_proposer = ethereum_rust_l2::start_proposer(store.clone()).into_future(); tracker.spawn(l2_proposer); - // let l2_prover = ethereum_rust_l2::start_prover().into_future(); - // tracker.spawn(l2_prover); } else if #[cfg(feature = "dev")] { use ethereum_rust_dev; diff --git a/cmd/ethereum_rust_l2/Cargo.toml b/cmd/ethereum_rust_l2/Cargo.toml index 595144e72..4f8ac6d6c 100644 --- a/cmd/ethereum_rust_l2/Cargo.toml +++ b/cmd/ethereum_rust_l2/Cargo.toml @@ -23,12 +23,14 @@ colored = "2.1.0" spinoff = "0.8.0" itertools = "0.13.0" strum = "0.26.3" +libsecp256k1 = "0.7.1" +keccak-hash = "0.10.0" + ethereum_rust-l2.workspace = true ethereum_rust-core.workspace = true ethereum_rust-blockchain.workspace = true +ethereum_rust-prover.workspace = true ethereum_rust-rlp.workspace = true -libsecp256k1 = "0.7.1" -keccak-hash = "0.10.0" [[bin]] name = "ethereum_rust_l2" diff --git a/cmd/ethereum_rust_l2/src/commands/stack.rs b/cmd/ethereum_rust_l2/src/commands/stack.rs index 26f0d0ecf..3ea889e08 100644 --- a/cmd/ethereum_rust_l2/src/commands/stack.rs +++ b/cmd/ethereum_rust_l2/src/commands/stack.rs @@ -19,6 +19,13 @@ pub(crate) enum Command { help = "Skips L1 deployment. Beware that this will only work if the L1 is already set up. L1 contracts must be present in the config." )] skip_l1_deployment: bool, + #[arg( + long = "start-prover", + help = "Start ZK Prover for the L2 if set.", + short = 'p', + default_value_t = false + )] + start_prover: bool, }, #[clap(about = "Shutdown the stack.")] Shutdown { @@ -37,6 +44,13 @@ pub(crate) enum Command { l2: bool, #[clap(short = 'y', long, help = "Forces the start without confirmation.")] force: bool, + #[arg( + long = "start-prover", + help = "Start ZK Prover for the L2 if set.", + short = 'p', + default_value_t = false + )] + start_prover: bool, }, #[clap(about = "Cleans up the stack. Prompts for confirmation.")] Purge { @@ -68,7 +82,10 @@ impl Command { let l2_rpc_url = cfg.network.l2_rpc_url.clone(); match self { - Command::Init { skip_l1_deployment } => { + Command::Init { + skip_l1_deployment, + start_prover, + } => { // Delegate the command whether to init in a local environment // or in a testnet. If the L1 RPC URL is localhost, then it is // a local environment and the local node needs to be started. @@ -80,7 +97,7 @@ impl Command { contract_deps(&contracts_path)?; deploy_l1(&l1_rpc_url, &cfg.wallet.private_key, &contracts_path)?; } - start_l2(root.to_path_buf(), &l2_rpc_url).await?; + start_l2(root.to_path_buf(), &l2_rpc_url, start_prover).await?; } Command::Shutdown { l1, l2, force } => { if force || (l1 && confirm("Are you sure you want to shutdown the local L1 node?")?) @@ -91,12 +108,17 @@ impl Command { shutdown_l2()?; } } - Command::Start { l1, l2, force } => { + Command::Start { + l1, + l2, + force, + start_prover, + } => { if force || l1 { start_l1(&l2_crate_path, ðereum_rust_dev_path).await?; } if force || l2 { - start_l2(root.to_path_buf(), &l2_rpc_url).await?; + start_l2(root.to_path_buf(), &l2_rpc_url, start_prover).await?; } } Command::Purge { force } => { @@ -133,6 +155,7 @@ impl Command { Box::pin(async { Self::Init { skip_l1_deployment: false, + start_prover: false, } .run(cfg.clone()) .await @@ -250,25 +273,58 @@ fn docker_compose_l2_up(ethereum_rust_dev_path: &Path) -> eyre::Result<()> { Ok(()) } -async fn start_l2(root: PathBuf, l2_rpc_url: &str) -> eyre::Result<()> { +// The cli is not displaying tracing logs. +async fn start_l2(root: PathBuf, l2_rpc_url: &str, start_prover: bool) -> eyre::Result<()> { let l2_genesis_file_path = root.join("test_data/genesis-l2.json"); - let cmd = std::process::Command::new("cargo") - .arg("run") - .arg("--release") - .arg("--bin") - .arg("ethereum_rust") - .arg("--features") - .arg("l2") - .arg("--") - .arg("--network") - .arg(l2_genesis_file_path) - .arg("--http.port") - .arg(l2_rpc_url.split(':').last().unwrap()) - .current_dir(root) - .spawn()? - .wait()?; - if !cmd.success() { - eyre::bail!("Failed to run L2 node"); + let l2_rpc_url_owned = l2_rpc_url.to_owned(); + let root_clone = root.clone(); + let l2_start_cmd = std::thread::spawn(move || { + let status = std::process::Command::new("cargo") + .arg("run") + .arg("--release") + .arg("--bin") + .arg("ethereum_rust") + .arg("--features") + .arg("l2") + .arg("--") + .arg("--network") + .arg(l2_genesis_file_path) + .arg("--http.port") + .arg(l2_rpc_url_owned.split(':').last().unwrap()) + .current_dir(root) + .status(); + + match status { + Ok(s) if s.success() => Ok(()), + Ok(_) => Err(eyre::eyre!("Failed to run L2 node")), + Err(e) => Err(eyre::eyre!(e)), + } + }); + + let l2_result = l2_start_cmd.join().expect("L2 thread panicked"); + l2_result?; + + if start_prover { + let prover_start_cmd = std::thread::spawn(|| { + let status = std::process::Command::new("cargo") + .arg("run") + .arg("--release") + .arg("--features") + .arg("build_zkvm") + .arg("--bin") + .arg("ethereum_rust_prover") + .current_dir(root_clone) + .status(); + + match status { + Ok(s) if s.success() => Ok(()), + Ok(_) => Err(eyre::eyre!("Failed to Initialize Prover")), + Err(e) => Err(eyre::eyre!(e)), + } + }); + let prover_result = prover_start_cmd.join().expect("Prover thread panicked"); + prover_result?; } + Ok(()) } diff --git a/crates/l2/.example.env b/crates/l2/.example.env index f194dbadf..d2732cc00 100644 --- a/crates/l2/.example.env +++ b/crates/l2/.example.env @@ -8,9 +8,12 @@ ENGINE_API_RPC_URL=http://localhost:8551 ENGINE_API_JWT_PATH=./jwt.hex PROVER_SERVER_LISTEN_IP=127.0.0.1 PROVER_SERVER_LISTEN_PORT=3000 -PROVER_PROVER_SERVER_ENDPOINT=localhost:3000 -PROVER_ELF_PATH=./prover/sp1/program/elf/riscv32im-succinct-zkvm-elf +PROVER_CLIENT_PROVER_SERVER_ENDPOINT=localhost:3000 PROPOSER_ON_CHAIN_PROPOSER_ADDRESS=0xF04a082b0f773cA74B61278d9dBaaB5Ed5273DB5 PROPOSER_L1_ADDRESS=0x3d1e15a1a55578f7c920884a9943b3b35d0d885b PROPOSER_L1_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 PROPOSER_INTERVAL_MS=5000 +# https://dev.risczero.com/api/generating-proofs/dev-mode +# 1/true means fake proofs +RISC0_DEV_MODE=1 +RUST_LOG="[executor]=info" diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index 70e05ad8f..6b96d8a03 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -22,11 +22,14 @@ ethereum_rust-dev = { path = "../../crates/blockchain/dev" } hex.workspace = true bytes.workspace = true jsonwebtoken.workspace = true -sp1-sdk = "2.0.0" libsecp256k1 = "0.7.1" keccak-hash = "0.10.0" envy = "0.4.2" thiserror.workspace = true +# risc0 +risc0-zkvm = { version = "1.1.2" } + + [lib] path = "./l2.rs" diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 72e88081f..a42f2d484 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -68,3 +68,6 @@ down-l2: ## 🛑 Shuts down the L2 Lambda Ethereum Rust Client pkill -f ethereum_rust || exit 0 restart-l2: down-l2 init-l2 ## 🔄 Restarts the L2 Lambda Ethereum Rust Client + +init-l2-prover: ## 🚀 Initializes the Prover + cargo run --release --features build_zkvm --manifest-path ../../Cargo.toml --bin ethereum_rust_prover diff --git a/crates/l2/docs/README.md b/crates/l2/docs/README.md index 1da812539..adfc1483d 100644 --- a/crates/l2/docs/README.md +++ b/crates/l2/docs/README.md @@ -3,7 +3,7 @@ Ethereum Rust L2 is composed of three main parts: - [General Overview](./overview.md) -- [Operator](./operator.md) +- [Proposer](./proposer.md) - [Prover](./prover.md) - [Contracts](./contracts.md) diff --git a/crates/l2/docs/contracts.md b/crates/l2/docs/contracts.md index 2a42e55fc..1301e427b 100644 --- a/crates/l2/docs/contracts.md +++ b/crates/l2/docs/contracts.md @@ -2,11 +2,13 @@ ## ToC +- [ToC](#toc) - [L1 side](#l1-side) - - [`CommonBridge`](#commonbridge) - - [`OnChainOperator`](#blockexecutor) + - [`CommonBridge`](#commonbridge) + - [`OnChainOperator`](#onchainoperator) + - [`Verifier`](#verifier) - [L2 side](#l2-side) - - [`L1MessageSender`](#l1messagesender) + - [`L1MessageSender`](#l1messagesender) ## L1 side diff --git a/crates/l2/docs/proposer.md b/crates/l2/docs/proposer.md index 58ca4a01a..6e6635a5e 100644 --- a/crates/l2/docs/proposer.md +++ b/crates/l2/docs/proposer.md @@ -6,7 +6,7 @@ - [Components](#components) - [L1 Watcher](#l1-watcher) - [L1 Transaction Sender](#l1-transaction-sender) - - [Prover Client](#prover-client) + - [Prover Server](#prover-server) - [Configuration](#configuration) ## Components @@ -27,7 +27,7 @@ Commit transactions are sent when the Proposer wants to commit to a new block. T Verify transactions are sent by the Proposer after the prover has successfully generated a proof of block execution to verify it. These transactions contain the proof to be verified in the L1. -### Prover Client +### Prover Server TODO diff --git a/crates/l2/docs/prover.md b/crates/l2/docs/prover.md index 9d65cb9d2..88dcf2ceb 100644 --- a/crates/l2/docs/prover.md +++ b/crates/l2/docs/prover.md @@ -2,35 +2,48 @@ ## ToC +- [ToC](#toc) - [Prover](#prover) + - [How to Run](#how-to-run) - [Workflow](#workflow) - [Configuration](#configuration) +>[!NOTE] +> The shipping/deploying process and the `Prover` itself is under development. + ## Prover -The RISC-V zkVM Prover currently runs a demo fibonacci program. It uses Groth16, which requires at least 128GB of RAM. To mock proof generation and avoid RAM requirements, you can use the `SP1_PROVER=mock` env var. +The RISC-V zkVM Prover currently runs an empty program. To mock proof generation and avoid RAM requirements, you can use the following envar: `RISC0_DEV_MODE=1`. [risczero - dev-mode](https://dev.risczero.com/api/generating-proofs/dev-mode). + +### How to Run + +Dependencies: `cargo-risczero` [dev - risczero - installation](https://dev.risczero.com/api/zkvm/install) + +If you are at `crates/l2`, you will have to set the `.env` file (the `.example.env` can be used) and then run `make init-l2-prover`. + +The `build_zkvm` flag is used, if you don't have the risc0's "sdk", you can build the prover without the feature to check all the surrounding components of the `zkvm`. ## Workflow -The ProofDataClient is in charge of request for new jobs to the ProofDataProvider when the prover is free. When a new job arrives, the Prover will generate the proof and then the ProofDataClient will submit it to the ProofDataProvider +The `Prover Server` is monitoring requests for new jobs from the `Prover Client`, sent when the prover is free. When a new job arrives, the Prover will generate the proof and then the `Prover Client` will send it to the `Prover Server`. ```mermaid sequenceDiagram participant Prover - participant ProofDataClient - participant ProofDataProvider - ProofDataClient->>+ProofDataProvider: ProofData::Request - ProofDataProvider-->>-ProofDataClient: ProofData::Response(id) - ProofDataClient->>+Prover: Prove(id) - Prover-->>-ProofDataClient: Proof - ProofDataClient->>+ProofDataProvider: ProofData::Submit(id) - ProofDataProvider-->>-ProofDataClient: ProofData::SubmitAck(id) + participant ProverClient + participant ProverServer + ProverClient->>+ProverServer: ProofData::Request + ProverServer-->>-ProverClient: ProofData::Response(block_number, ProverInputs) + ProverClient->>+Prover: Prove(block_number, ProverInputs) + Prover-->>-ProverClient: Creates zkProof + ProverClient->>+ProverServer: ProofData::Submit(block_number, zkProof) + ProverServer-->>-ProverClient: ProofData::SubmitAck(block_number) ``` ## Configuration The following environment variables are available to configure the prover: -- `SP1_PROVER`: Prover type. Can be `local` or `mock`. -- `PROVER_PROVER_SERVER_ENDPOINT`: Endpoint to connect to the ProofDataProvider. -- `PROVER_ELF_PATH`: Path to the ELF file to prove. +- `PROVER_SERVER_LISTEN_IP`: IP used to start the Server. +- `PROVER_SERVER_LISTEN_PORT`: Port used to start the Server. +- `PROVER_CLIENT_PROVER_SERVER_ENDPOINT`: Prover Server's Endpoint used to connect the Client to the Server. diff --git a/crates/l2/l2.rs b/crates/l2/l2.rs index a19b5751d..ab605ed34 100644 --- a/crates/l2/l2.rs +++ b/crates/l2/l2.rs @@ -1,6 +1,4 @@ pub mod proposer; -pub mod prover; pub mod utils; pub use proposer::start_proposer; -pub use prover::start_prover; diff --git a/crates/l2/proposer/errors.rs b/crates/l2/proposer/errors.rs index edd58841d..6c7e5780b 100644 --- a/crates/l2/proposer/errors.rs +++ b/crates/l2/proposer/errors.rs @@ -18,8 +18,8 @@ pub enum L1WatcherError { } #[derive(Debug, thiserror::Error)] -pub enum ProofDataProviderError { - #[error("ProofDataProvider connection failed: {0}")] +pub enum ProverServerError { + #[error("ProverServer connection failed: {0}")] ConnectionError(#[from] std::io::Error), } diff --git a/crates/l2/proposer/mod.rs b/crates/l2/proposer/mod.rs index 98e4634d4..cf4c844ac 100644 --- a/crates/l2/proposer/mod.rs +++ b/crates/l2/proposer/mod.rs @@ -41,7 +41,7 @@ pub async fn start_proposer(store: Store) { } let l1_watcher = tokio::spawn(l1_watcher::start_l1_watcher(store.clone())); - let prover_server = tokio::spawn(prover_server::start_prover_server()); + let prover_server = tokio::spawn(prover_server::start_prover_server(store.clone())); let proposer = tokio::spawn(async move { let eth_config = EthConfig::from_env().expect("EthConfig::from_env"); let proposer_config = ProposerConfig::from_env().expect("ProposerConfig::from_env"); diff --git a/crates/l2/proposer/prover_server.rs b/crates/l2/proposer/prover_server.rs index dab52eb6c..08b79f37b 100644 --- a/crates/l2/proposer/prover_server.rs +++ b/crates/l2/proposer/prover_server.rs @@ -1,7 +1,7 @@ use crate::utils::eth_client::RpcResponse; +use ethereum_rust_storage::Store; use reqwest::Client; use serde::{Deserialize, Serialize}; -use sp1_sdk::SP1ProofWithPublicValues; use std::{ io::{BufReader, BufWriter}, net::{IpAddr, Shutdown, TcpListener, TcpStream}, @@ -10,13 +10,26 @@ use std::{ use tokio::signal::unix::{signal, SignalKind}; use tracing::{debug, info, warn}; +use ethereum_rust_core::types::{Block, BlockHeader}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ProverInputData { + pub db: MemoryDB, + pub parent_block_header: BlockHeader, + pub block: Block, +} + +// Placeholder structure until we have ExecutionDB on L1 +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MemoryDB; + use crate::utils::config::prover_server::ProverServerConfig; -use super::errors::ProofDataProviderError; +use super::errors::ProverServerError; -pub async fn start_prover_server() { - let config = ProverServerConfig::from_env().expect("ProofDataProviderConfig::from_env()"); - let prover_server = ProofDataProvider::new_from_config(config.clone()); +pub async fn start_prover_server(store: Store) { + let config = ProverServerConfig::from_env().expect("ProverServerConfig::from_env()"); + let prover_server = ProverServer::new_from_config(config.clone(), store); let (tx, rx) = mpsc::channel(); @@ -27,7 +40,7 @@ pub async fn start_prover_server() { .expect("prover_server.start()") }); - ProofDataProvider::handle_sigint(tx, config).await; + ProverServer::handle_sigint(tx, config).await; tokio::try_join!(server).expect("tokio::try_join!()"); } @@ -36,27 +49,31 @@ pub async fn start_prover_server() { pub enum ProofData { Request {}, Response { - id: Option, + block_number: Option, + input: ProverInputData, }, Submit { - id: u64, - proof: Box, + block_number: u64, + // zk Proof + receipt: Box, }, SubmitAck { - id: u64, + block_number: u64, }, } -struct ProofDataProvider { +struct ProverServer { ip: IpAddr, port: u16, + store: Store, } -impl ProofDataProvider { - pub fn new_from_config(config: ProverServerConfig) -> Self { +impl ProverServer { + pub fn new_from_config(config: ProverServerConfig, store: Store) -> Self { Self { ip: config.listen_ip, port: config.listen_port, + store, } } @@ -70,7 +87,7 @@ impl ProofDataProvider { .expect("TcpStream::shutdown()"); } - pub async fn start(&self, rx: Receiver<()>) -> Result<(), ProofDataProviderError> { + pub async fn start(&self, rx: Receiver<()>) -> Result<(), ProverServerError> { let listener = TcpListener::bind(format!("{}:{}", self.ip, self.port))?; let mut last_proved_block = 0; @@ -78,7 +95,7 @@ impl ProofDataProvider { info!("Starting TCP server at {}:{}", self.ip, self.port); for stream in listener.incoming() { if let Ok(()) = rx.try_recv() { - info!("Shutting down ProofDataProvider server"); + info!("Shutting down Prover Server"); break; } @@ -99,8 +116,11 @@ impl ProofDataProvider { warn!("Failed to handle request: {e}"); } } - Ok(ProofData::Submit { id, proof }) => { - if let Err(e) = self.handle_submit(&mut stream, id, proof) { + Ok(ProofData::Submit { + block_number, + receipt, + }) => { + if let Err(e) = self.handle_submit(&mut stream, block_number, receipt) { warn!("Failed to handle submit: {e}"); } *last_proved_block += 1; @@ -116,7 +136,7 @@ impl ProofDataProvider { debug!("Connection closed"); } - async fn get_last_block_number() -> Result { + async fn _get_last_block_number() -> Result { let response = Client::new() .post("http://localhost:8551") .header("content-type", "application/json") @@ -157,14 +177,23 @@ impl ProofDataProvider { ) -> Result<(), String> { debug!("Request received"); - let last_block_number = Self::get_last_block_number().await?; + //let last_block_number = Self::get_last_block_number().await?; + let last_block_number = self + .store + .get_latest_block_number() + .map_err(|e| e.to_string())? + .ok_or("missing latest block number".to_string())?; let response = if last_block_number > last_proved_block { ProofData::Response { - id: Some(last_proved_block + 1), + block_number: Some(last_block_number), + input: ProverInputData::default(), } } else { - ProofData::Response { id: None } + ProofData::Response { + block_number: None, + input: ProverInputData::default(), + } }; let writer = BufWriter::new(stream); serde_json::to_writer(writer, &response).map_err(|e| e.to_string()) @@ -173,12 +202,12 @@ impl ProofDataProvider { fn handle_submit( &self, stream: &mut TcpStream, - id: u64, - proof: Box, + block_number: u64, + receipt: Box, ) -> Result<(), String> { - debug!("Submit received. ID: {id}, proof: {:?}", proof.proof); + debug!("Submit received. ID: {block_number}, proof: {:?}", receipt); - let response = ProofData::SubmitAck { id }; + let response = ProofData::SubmitAck { block_number }; let writer = BufWriter::new(stream); serde_json::to_writer(writer, &response).map_err(|e| e.to_string()) } diff --git a/crates/l2/prover/Cargo.toml b/crates/l2/prover/Cargo.toml new file mode 100644 index 000000000..df2239f1c --- /dev/null +++ b/crates/l2/prover/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "ethereum_rust-prover" +version.workspace = true +edition.workspace = true + +[dependencies] +serde_json.workspace = true +serde.workspace = true +bytes.workspace = true +ethereum-types.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing.workspace = true +hex.workspace = true + +# ethereum_rust +ethereum_rust-core.workspace = true +ethereum_rust-rlp.workspace = true + +# l2 +ethereum_rust-l2.workspace = true + +# risc0 +zkvm_interface = { path = "./zkvm/interface", default-features = false } +risc0-zkvm = { version = "1.1.2" } + +# revm (this dep is temporary, should be replaced with ethereum_rust-vm after ExecutionDB is completely integrated into the L1) +revm = { version = "14.0.3", features = [ + "std", + "serde", + "kzg-rs", +], default-features = false } + +[lib] +name = "ethereum_rust_prover_lib" +path = "src/lib.rs" + +[[bin]] +name = "ethereum_rust_prover" +path = "src/main.rs" + +[features] +default = [] +build_zkvm = ["zkvm_interface/build_zkvm"] diff --git a/crates/l2/prover/mod.rs b/crates/l2/prover/mod.rs deleted file mode 100644 index 0ad5912fa..000000000 --- a/crates/l2/prover/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -use tracing::info; - -pub mod prover; -pub mod prover_client; - -pub async fn start_prover() { - let prover_client = tokio::spawn(prover_client::start_prover_client()); - - tokio::try_join!(prover_client).unwrap(); - info!("Prover finished!"); -} diff --git a/crates/l2/prover/prover.rs b/crates/l2/prover/prover.rs deleted file mode 100644 index 2a66d6310..000000000 --- a/crates/l2/prover/prover.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![allow(clippy::module_inception)] -use tracing::info; - -use sp1_sdk::{ProverClient, SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey}; - -use crate::utils::config::prover_client::ProverClientConfig; - -pub struct Prover { - client: ProverClient, - pk: SP1ProvingKey, - vk: SP1VerifyingKey, -} - -impl Default for Prover { - fn default() -> Self { - let config = ProverClientConfig::from_env().unwrap(); - Self::new_from_config(config) - } -} - -impl Prover { - pub fn new_from_config(config: ProverClientConfig) -> Self { - let elf = std::fs::read(config.elf_path).unwrap(); - - info!("Setting up prover..."); - let client = ProverClient::new(); - let (pk, vk) = client.setup(elf.as_slice()); - info!("Prover setup complete!"); - - Self { client, pk, vk } - } - - pub fn prove(&self, id: u64) -> Result { - // Setup the inputs. - let mut stdin = SP1Stdin::new(); - stdin.write(&id); - - info!("Starting Fibonacci proof for n = {id}"); - - // Generate the proof - let proof = self - .client - .prove(&self.pk, stdin) - .groth16() - .run() - .map_err(|_| "Failed to generate proof".to_string())?; - - info!("Successfully generated proof!"); - - // Verify the proof. - self.client - .verify(&proof, &self.vk) - .map_err(|_| "Failed to verify proof".to_string())?; - info!("Successfully verified proof!"); - - Ok(proof) - } -} diff --git a/crates/l2/prover/sp1/lib/Cargo.toml b/crates/l2/prover/sp1/lib/Cargo.toml deleted file mode 100644 index c15da238f..000000000 --- a/crates/l2/prover/sp1/lib/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "fibonacci-lib" -version = "0.1.0" -edition = "2021" - -[dependencies] -alloy-sol-types = "0.7.7" diff --git a/crates/l2/prover/sp1/lib/src/lib.rs b/crates/l2/prover/sp1/lib/src/lib.rs deleted file mode 100644 index f4b1779d6..000000000 --- a/crates/l2/prover/sp1/lib/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -use alloy_sol_types::sol; - -sol! { - /// The public values encoded as a struct that can be easily deserialized inside Solidity. - struct PublicValuesStruct { - uint32 n; - uint32 a; - uint32 b; - } -} - -/// Compute the n'th fibonacci number (wrapping around on overflows), using normal Rust code. -pub fn fibonacci(n: u32) -> (u32, u32) { - let mut a = 0u32; - let mut b = 1u32; - for _ in 0..n { - let c = a.wrapping_add(b); - a = b; - b = c; - } - (a, b) -} diff --git a/crates/l2/prover/sp1/program/.gitignore b/crates/l2/prover/sp1/program/.gitignore deleted file mode 100644 index 39f0c13fb..000000000 --- a/crates/l2/prover/sp1/program/.gitignore +++ /dev/null @@ -1 +0,0 @@ -elf/ diff --git a/crates/l2/prover/sp1/program/Cargo.toml b/crates/l2/prover/sp1/program/Cargo.toml deleted file mode 100644 index 6d2bbd15a..000000000 --- a/crates/l2/prover/sp1/program/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -version = "0.1.0" -name = "fibonacci-program" -edition = "2021" - -[workspace] - -[dependencies] -alloy-sol-types = "0.7.7" -sp1-zkvm = "2.0.0" -fibonacci-lib = { path = "../lib" } diff --git a/crates/l2/prover/sp1/program/src/main.rs b/crates/l2/prover/sp1/program/src/main.rs deleted file mode 100644 index fc35d4afe..000000000 --- a/crates/l2/prover/sp1/program/src/main.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! A simple program that takes a number `n` as input, and writes the `n-1`th and `n`th fibonacci -//! number as an output. - -// These two lines are necessary for the program to properly compile. -// -// Under the hood, we wrap your main function with some extra code so that it behaves properly -// inside the zkVM. -#![no_main] -sp1_zkvm::entrypoint!(main); - -use alloy_sol_types::SolType; -use fibonacci_lib::{fibonacci, PublicValuesStruct}; - -pub fn main() { - // Read an input to the program. - // - // Behind the scenes, this compiles down to a custom system call which handles reading inputs - // from the prover. - let n = sp1_zkvm::io::read::(); - - // Compute the n'th fibonacci number using a function from the workspace lib crate. - let (a, b) = fibonacci(n); - - // Encode the public values of the program. - let bytes = PublicValuesStruct::abi_encode(&PublicValuesStruct { n, a, b }); - - // Commit to the public values of the program. The final proof will have a commitment to all the - // bytes that were committed to. - sp1_zkvm::io::commit_slice(&bytes); -} diff --git a/crates/l2/prover/src/lib.rs b/crates/l2/prover/src/lib.rs new file mode 100644 index 000000000..8e81668f6 --- /dev/null +++ b/crates/l2/prover/src/lib.rs @@ -0,0 +1,10 @@ +pub mod prover; +pub mod prover_client; + +use ethereum_rust_l2::utils::config::prover_client::ProverClientConfig; +use tracing::warn; + +pub async fn init_client(config: ProverClientConfig) { + prover_client::start_proof_data_client(config).await; + warn!("Prover finished!"); +} diff --git a/crates/l2/prover/src/main.rs b/crates/l2/prover/src/main.rs new file mode 100644 index 000000000..9333fb753 --- /dev/null +++ b/crates/l2/prover/src/main.rs @@ -0,0 +1,20 @@ +use ethereum_rust_l2::utils::config::{prover_client::ProverClientConfig, read_env_file}; +use ethereum_rust_prover_lib::init_client; + +use tracing::{self, debug, warn, Level}; + +#[tokio::main] +async fn main() { + let subscriber = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(Level::DEBUG) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + + if let Err(e) = read_env_file() { + warn!("Failed to read .env file: {e}"); + } + + let config = ProverClientConfig::from_env().unwrap(); + debug!("Prover Client has started"); + init_client(config).await; +} diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs new file mode 100644 index 000000000..e4f8a2f67 --- /dev/null +++ b/crates/l2/prover/src/prover.rs @@ -0,0 +1,77 @@ +use tracing::info; + +// risc0 +use zkvm_interface::methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID}; + +use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder}; + +use ethereum_rust_rlp::encode::RLPEncode; + +use ethereum_rust_l2::proposer::prover_server::ProverInputData; +use ethereum_rust_l2::utils::config::prover_client::ProverClientConfig; + +pub struct Prover<'a> { + env_builder: ExecutorEnvBuilder<'a>, + elf: &'a [u8], + id: [u32; 8], +} + +impl<'a> Default for Prover<'a> { + fn default() -> Self { + let _config = ProverClientConfig::from_env().unwrap(); + Self::new() + } +} + +impl<'a> Prover<'a> { + pub fn new() -> Self { + Self { + env_builder: ExecutorEnv::builder(), + elf: ZKVM_PROGRAM_ELF, + id: ZKVM_PROGRAM_ID, + } + } + + pub fn set_input(&mut self, input: ProverInputData) -> &mut Self { + let head_block_rlp = input.block.encode_to_vec(); + let parent_block_header_rlp = input.parent_block_header.encode_to_vec(); + + // We should pass the inputs as a whole struct + self.env_builder.write(&head_block_rlp).unwrap(); + self.env_builder.write(&parent_block_header_rlp).unwrap(); + self.env_builder.write(&input.db).unwrap(); + + self + } + + /// Example: + /// let prover = Prover::new(); + /// let proof = prover.set_input(inputs).prove().unwrap(); + pub fn prove(&mut self) -> Result { + let env = self + .env_builder + .build() + .map_err(|_| "Failed to Build env".to_string())?; + + // Generate the Receipt + let prover = default_prover(); + + // Proof information by proving the specified ELF binary. + // This struct contains the receipt along with statistics about execution of the guest + let prove_info = prover + .prove(env, self.elf) + .map_err(|_| "Failed to prove".to_string())?; + + // extract the receipt. + let receipt = prove_info.receipt; + + info!("Successfully generated Receipt!"); + Ok(receipt) + } + + pub fn verify(&self, receipt: &risc0_zkvm::Receipt) -> Result<(), String> { + // Verify the proof. + receipt.verify(self.id).unwrap(); + Ok(()) + } +} diff --git a/crates/l2/prover/prover_client.rs b/crates/l2/prover/src/prover_client.rs similarity index 56% rename from crates/l2/prover/prover_client.rs rename to crates/l2/prover/src/prover_client.rs index 7a4ffbeee..93c5a1e2d 100644 --- a/crates/l2/prover/prover_client.rs +++ b/crates/l2/prover/src/prover_client.rs @@ -4,40 +4,41 @@ use std::{ time::Duration, }; -use sp1_sdk::SP1ProofWithPublicValues; use tokio::time::sleep; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; -use crate::{proposer::prover_server::ProofData, utils::config::prover_client::ProverClientConfig}; +use ethereum_rust_l2::{ + proposer::prover_server::{ProofData, ProverInputData}, + utils::config::prover_client::ProverClientConfig, +}; use super::prover::Prover; -pub async fn start_prover_client() { - let config = ProverClientConfig::from_env().unwrap(); - let prover_client = ProofDataClient::new(config.prover_server_endpoint.clone()); - prover_client.start(config).await; +pub async fn start_proof_data_client(config: ProverClientConfig) { + let proof_data_client = ProverClient::new(config.prover_server_endpoint.clone()); + proof_data_client.start().await; } -struct ProofDataClient { +struct ProverClient { prover_server_endpoint: String, } -impl ProofDataClient { +impl ProverClient { pub fn new(prover_server_endpoint: String) -> Self { Self { prover_server_endpoint, } } - pub async fn start(&self, config: ProverClientConfig) { - let prover = Prover::new_from_config(config); + pub async fn start(&self) { + let mut prover = Prover::new(); loop { match self.request_new_data() { - Ok(Some(id)) => { - match prover.prove(id) { + Ok((Some(block_number), input)) => { + match prover.set_input(input).prove() { Ok(proof) => { - if let Err(e) = self.submit_proof(id, proof) { + if let Err(e) = self.submit_proof(block_number, proof) { // TODO: Retry warn!("Failed to submit proof: {e}"); } @@ -45,15 +46,18 @@ impl ProofDataClient { Err(e) => error!(e), }; } - Ok(None) => sleep(Duration::from_secs(10)).await, - Err(e) => warn!("Failed to request new data: {e}"), + Ok((None, _)) => sleep(Duration::from_secs(10)).await, + Err(e) => { + sleep(Duration::from_secs(10)).await; + warn!("Failed to request new data: {e}"); + } } } } - fn request_new_data(&self) -> Result, String> { + fn request_new_data(&self) -> Result<(Option, ProverInputData), String> { let stream = TcpStream::connect(&self.prover_server_endpoint) - .map_err(|e| format!("Failed to connect to ProofDataProvider: {e}"))?; + .map_err(|e| format!("Failed to connect to Prover Server: {e}"))?; let buf_writer = BufWriter::new(&stream); debug!("Connection established!"); @@ -69,22 +73,22 @@ impl ProofDataClient { .map_err(|e| format!("Invalid response format: {e}"))?; match response { - ProofData::Response { id } => { - debug!("Received response: {id:?}"); - Ok(id) - } + ProofData::Response { + block_number, + input, + } => Ok((block_number, input)), _ => Err(format!("Unexpected response {response:?}")), } } - fn submit_proof(&self, id: u64, proof: SP1ProofWithPublicValues) -> Result<(), String> { + fn submit_proof(&self, block_number: u64, receipt: risc0_zkvm::Receipt) -> Result<(), String> { let stream = TcpStream::connect(&self.prover_server_endpoint) - .map_err(|e| format!("Failed to connect to ProofDataProvider: {e}"))?; + .map_err(|e| format!("Failed to connect to Prover Server: {e}"))?; let buf_writer = BufWriter::new(&stream); let submit = ProofData::Submit { - id, - proof: Box::new(proof), + block_number, + receipt: Box::new(receipt), }; serde_json::ser::to_writer(buf_writer, &submit).map_err(|e| e.to_string())?; stream @@ -95,8 +99,10 @@ impl ProofDataClient { let response: ProofData = serde_json::de::from_reader(buf_reader) .map_err(|e| format!("Invalid response format: {e}"))?; match response { - ProofData::SubmitAck { id: res_id } => { - debug!("Received submit ack: {res_id}"); + ProofData::SubmitAck { + block_number: res_id, + } => { + info!("Received submit ack: {res_id}"); Ok(()) } _ => Err(format!("Unexpected response {response:?}")), diff --git a/crates/l2/prover/src/utils/placeholder.rs b/crates/l2/prover/src/utils/placeholder.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/l2/prover/zkvm/.gitignore b/crates/l2/prover/zkvm/.gitignore new file mode 100644 index 000000000..f4247e182 --- /dev/null +++ b/crates/l2/prover/zkvm/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +Cargo.lock +methods/guest/Cargo.lock +target/ diff --git a/crates/l2/prover/zkvm/Cargo.toml b/crates/l2/prover/zkvm/Cargo.toml new file mode 100644 index 000000000..d0f7c8c61 --- /dev/null +++ b/crates/l2/prover/zkvm/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" +members = ["interface"] + +# Always optimize; building and running the guest takes much longer without optimization. +[profile.dev] +opt-level = 3 + +[profile.release] +debug = 1 +lto = true diff --git a/crates/l2/prover/zkvm/NOTICE b/crates/l2/prover/zkvm/NOTICE new file mode 100644 index 000000000..d30b286bb --- /dev/null +++ b/crates/l2/prover/zkvm/NOTICE @@ -0,0 +1,14 @@ +Original work Copyright [RISC Zero, Inc.] +Copyright [2024] [LambdaClass] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/l2/prover/zkvm/interface/Cargo.toml b/crates/l2/prover/zkvm/interface/Cargo.toml new file mode 100644 index 000000000..16d3f0842 --- /dev/null +++ b/crates/l2/prover/zkvm/interface/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "zkvm_interface" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", default-features = false, features = ["derive"] } +thiserror = "1.0.64" + +ethereum_rust-storage = { path = "../../../../storage/store" } + +# revm +revm = { version = "14.0.3", features = [ + "std", + "serde", + "kzg-rs", +], default-features = false } + +[build-dependencies] +risc0-build = { version = "1.1.2" } + +[package.metadata.risc0] +methods = ["guest"] + +[features] +default = [] +build_zkvm = [] diff --git a/crates/l2/prover/zkvm/interface/build.rs b/crates/l2/prover/zkvm/interface/build.rs new file mode 100644 index 000000000..5dc8be1ee --- /dev/null +++ b/crates/l2/prover/zkvm/interface/build.rs @@ -0,0 +1,5 @@ +fn main() { + #[cfg(not(clippy))] + #[cfg(feature = "build_zkvm")] + risc0_build::embed_methods(); +} diff --git a/crates/l2/prover/zkvm/interface/guest/Cargo.toml b/crates/l2/prover/zkvm/interface/guest/Cargo.toml new file mode 100644 index 000000000..a79a9a118 --- /dev/null +++ b/crates/l2/prover/zkvm/interface/guest/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "zkvm_program" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "1.1.2", default-features = false, features = ['std'] } + +ethereum_rust-core = { path = "../../../../../common", default-features = false } +ethereum_rust-rlp = { path = "../../../../../common/rlp" } +ethereum_rust-vm = { path = "../../../../../vm", default-features = false } +ethereum_rust-blockchain = { path = "../../../../../blockchain", default-features = false } + +# revm +revm = { version = "14.0.3", features = [ + "std", + "serde", + "kzg-rs", +], default-features = false } diff --git a/crates/l2/prover/zkvm/interface/guest/src/main.rs b/crates/l2/prover/zkvm/interface/guest/src/main.rs new file mode 100644 index 000000000..c3a2a57bd --- /dev/null +++ b/crates/l2/prover/zkvm/interface/guest/src/main.rs @@ -0,0 +1,111 @@ +use risc0_zkvm::guest::env; + +//use ethereum_rust_blockchain::validate_gas_used; +use ethereum_rust_core::types::{Receipt, Transaction}; +// We have to import the ExecutionDB. +use ethereum_rust_vm::{block_env, tx_env}; + +use revm::{ + db::CacheDB, inspectors::TracerEip3155, primitives::ResultAndState as RevmResultAndState, + Evm as Revm, +}; + +fn main() { + // Read the input + let head_block_bytes = env::read::>(); + let parent_header_bytes = env::read::>(); + //let execution_db = env::read::(); + + // SetUp data from inputs + let block = ::decode( + &head_block_bytes, + ) + .unwrap(); + + let parent_header = + ::decode( + &parent_header_bytes, + ) + .unwrap(); + + // Make DataInputs public. + env::commit(&block); + env::commit(&parent_header); + //env::commit(&execution_db); + + // SetUp CacheDB in order to use execute_block() + //let mut cache_db = CacheDB::new(execution_db); + println!("executing block"); + + //let block_receipts = execute_block(&block, &mut cache_db).unwrap(); + // TODO + // Handle the case in which the gas used differs and throws an error. + // Should the zkVM panic? Should it generate a dummy proof? + // Private function + //let _ = validate_gas_used(&block_receipts, &block.header); + + //env::commit(&block_receipts); +} + +// Modified from ethereum_rust-vm +/* +fn execute_block( + block: ðereum_rust_core::types::Block, + db: &mut CacheDB, +) -> Result, ethereum_rust_vm::EvmError> { + let spec_id = revm::primitives::SpecId::CANCUN; + let mut receipts = Vec::new(); + let mut cumulative_gas_used = 0; + + for transaction in block.body.transactions.iter() { + let result = execute_tx(transaction, &block.header, db, spec_id)?; + cumulative_gas_used += result.gas_used(); + let receipt = Receipt::new( + transaction.tx_type(), + result.is_success(), + cumulative_gas_used, + result.logs(), + ); + receipts.push(receipt); + } + + Ok(receipts) +} + +// Modified from ethereum_rust-vm +fn execute_tx( + transaction: &Transaction, + block_header: ðereum_rust_core::types::BlockHeader, + db: &mut CacheDB, + spec_id: revm::primitives::SpecId, +) -> Result { + let block_env = block_env(block_header); + let tx_env = tx_env(transaction); + run_evm(tx_env, block_env, db, spec_id) + .map(Into::into) + .map_err(ethereum_rust_vm::EvmError::from) +} + +// Modified from ethereum_rust-vm +fn run_evm( + tx_env: revm::primitives::TxEnv, + block_env: revm::primitives::BlockEnv, + db: &mut CacheDB, + spec_id: revm::primitives::SpecId, +) -> Result { + // let chain_spec = db.get_chain_config()?; + let mut evm = Revm::builder() + .with_db(db) + .with_block_env(block_env) + .with_tx_env(tx_env) + // If the chain_id is not correct, it throws: + // Transaction(InvalidChainId) + // TODO: do not hardcode the chain_id + .modify_cfg_env(|cfg| cfg.chain_id = 1729) + .with_spec_id(spec_id) + .with_external_context(TracerEip3155::new(Box::new(std::io::stderr())).without_summary()) + .build(); + let RevmResultAndState { result, state: _ } = evm.transact().unwrap(); + Ok(result.into()) +} +*/ diff --git a/crates/l2/prover/zkvm/interface/src/lib.rs b/crates/l2/prover/zkvm/interface/src/lib.rs new file mode 100644 index 000000000..ddec54513 --- /dev/null +++ b/crates/l2/prover/zkvm/interface/src/lib.rs @@ -0,0 +1,9 @@ +pub mod methods { + #[cfg(any(clippy, not(feature = "build_zkvm")))] + pub const ZKVM_PROGRAM_ELF: &[u8] = &[0]; + #[cfg(any(clippy, not(feature = "build_zkvm")))] + pub const ZKVM_PROGRAM_ID: [u32; 8] = [0_u32; 8]; + + #[cfg(all(not(clippy), feature = "build_zkvm"))] + include!(concat!(env!("OUT_DIR"), "/methods.rs")); +} diff --git a/crates/l2/utils/config/eth.rs b/crates/l2/utils/config/eth.rs index 3ccd7d945..77d0962ef 100644 --- a/crates/l2/utils/config/eth.rs +++ b/crates/l2/utils/config/eth.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use super::errors::ConfigError; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct EthConfig { pub rpc_url: String, } diff --git a/crates/l2/utils/config/prover_client.rs b/crates/l2/utils/config/prover_client.rs index 9d2e5f421..36b460efe 100644 --- a/crates/l2/utils/config/prover_client.rs +++ b/crates/l2/utils/config/prover_client.rs @@ -2,15 +2,14 @@ use serde::Deserialize; use super::errors::ConfigError; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct ProverClientConfig { - pub elf_path: String, pub prover_server_endpoint: String, } impl ProverClientConfig { pub fn from_env() -> Result { - envy::prefixed("PROVER_") + envy::prefixed("PROVER_CLIENT_") .from_env::() .map_err(ConfigError::from) } From 22e3cff1a2f65ecc4026dad31d1c3551f08874f6 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 24 Oct 2024 09:37:54 -0300 Subject: [PATCH 05/49] feat(l1): assertoor CI integration (#943) **Motivation** Assertoor integration as part of the CI **Description** This PR adds the [Assertoor GHA](https://github.com/ethpandaops/kurtosis-assertoor-github-action) to have a local devnet running with some checks as part of the CI. Most of the time for the stability checks was related to consensus, after removing those checks in a custom playbook created from [`stability-check.yaml`](https://github.com/ethpandaops/assertoor-test/blob/master/assertoor-tests/stability-check.yaml) we are able to run it as part of the CI. For assertoor to run with our [fork of ethereum-package](https://github.com/lambdaclass/ethereum-package) a small [commit](https://github.com/lambdaclass/ethereum-package/commit/811581ee8ab42639e4e9ef7c1197041689bfdfd1) was needed to the `ethereum-rust-integration` branch. I've also added some entries to the `paths-ignore` on the push rules. _Note about time: Most of the time spent in this new job is not the assertoor run, which takes ~3-5 mins, but the actual building of the docker image, which take ~9mins before starting the tests. This is done in parallel to the docker build in the CI, but now it's done 2 times. Fortunately, this is not the longest run, since `Hive / Cancun Engine tests (pull_request)` takes ~19m_ Resolves #952 --- .github/workflows/assertoor.yaml | 41 ++++++++++++++++++++++++++++++++ .github/workflows/ci.yaml | 5 ++++ .github/workflows/hive.yaml | 8 +++++-- test_data/el-stability-check.yml | 28 ++++++++++++++++++++++ test_data/network_params.yaml | 5 +++- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/assertoor.yaml create mode 100644 test_data/el-stability-check.yml diff --git a/.github/workflows/assertoor.yaml b/.github/workflows/assertoor.yaml new file mode 100644 index 000000000..bb5b8aa3e --- /dev/null +++ b/.github/workflows/assertoor.yaml @@ -0,0 +1,41 @@ +name: Assertoor +on: + merge_group: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + paths-ignore: + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + RUST_VERSION: 1.80.1 + +jobs: + test-run: + name: Stability Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + load: true # Important for building without pushing + tags: ethereum_rust + - name: Setup kurtosis testnet and run assertoor tests + uses: ethpandaops/kurtosis-assertoor-github-action@v1 + with: + ethereum_package_url: 'github.com/lambdaclass/ethereum-package' + ethereum_package_branch: 'ethereum-rust-integration' + ethereum_package_args: './test_data/network_params.yaml' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9bcb87eb0..c643500e5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,11 @@ on: branches: [main] pull_request: branches: ["*"] + paths-ignore: + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/hive.yaml b/.github/workflows/hive.yaml index 210227485..7d3f90850 100644 --- a/.github/workflows/hive.yaml +++ b/.github/workflows/hive.yaml @@ -4,10 +4,14 @@ on: merge_group: push: branches: [main] - paths-ignore: ["crates/l2/**"] pull_request: branches: ["*"] - paths-ignore: ["crates/l2/**"] + paths-ignore: + - "crates/l2/**" + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/test_data/el-stability-check.yml b/test_data/el-stability-check.yml new file mode 100644 index 000000000..5760091b3 --- /dev/null +++ b/test_data/el-stability-check.yml @@ -0,0 +1,28 @@ +# This file is based upon `assertoor-tests` stability check file: +# https://github.com/ethpandaops/assertoor-test/blob/master/assertoor-tests/stability-check.yaml +# +# We removed the consensus checks to keep it minimal. The checks removed were: +# - check_consensus_finality +# - check_consensus_attestation_stats +# - check_consensus_reorgs +# - check_consensus_forks + +id: el-stability-check +name: "Check Execution Stability" +timeout: 2h +tasks: +- name: check_clients_are_healthy + title: "Check if at least one client is ready" + timeout: 5m + config: + minClientCount: 1 + +- name: run_tasks_concurrent + title: "Check if all EL & CL clients are synced" + timeout: 30m + config: + tasks: + - name: check_consensus_sync_status + title: "Check if CL clients are synced" + - name: check_execution_sync_status + title: "Check if EL clients are synced" diff --git a/test_data/network_params.yaml b/test_data/network_params.yaml index 0799964ff..443e7082f 100644 --- a/test_data/network_params.yaml +++ b/test_data/network_params.yaml @@ -14,5 +14,8 @@ additional_services: - blob_spammer assertoor_params: - run_stability_check: true + run_stability_check: false run_block_proposal_check: false + tests: + - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_rust/refs/heads/assertoor-run-on-cd/test_data/el-stability-check.yml' + From 8ae4dfb23f99fefc3aa4a0974b95c920d359dc89 Mon Sep 17 00:00:00 2001 From: Francisco Krause Arnim <56402156+fkrause98@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:08:13 -0300 Subject: [PATCH 06/49] feat(l1): rpc endpoint eth_getFilterChanges (#889) **Motivation** - This endpoint is useful to poll events from contracts. **Description** - Moved filtering logic to a specific function under logs.rs Closes #427 . --- crates/networking/rpc/eth/filter.rs | 220 +++++++++++++++++++++++----- crates/networking/rpc/eth/logs.rs | 211 +++++++++++++------------- crates/networking/rpc/rpc.rs | 3 +- crates/networking/rpc/utils.rs | 4 + 4 files changed, 301 insertions(+), 137 deletions(-) diff --git a/crates/networking/rpc/eth/filter.rs b/crates/networking/rpc/eth/filter.rs index 1233e1e0f..bbeb2f9b7 100644 --- a/crates/networking/rpc/eth/filter.rs +++ b/crates/networking/rpc/eth/filter.rs @@ -1,18 +1,25 @@ +// The behaviour of the filtering endpoints is based on: +// - Manually testing the behaviour deploying contracts on the Sepolia test network. +// - Go-Ethereum, specifically: https://github.com/ethereum/go-ethereum/blob/368e16f39d6c7e5cce72a92ec289adbfbaed4854/eth/filters/filter.go +// - Ethereum's reference: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter +use ethereum_rust_core::types::BlockNumber; +use ethereum_rust_storage::Store; use std::{ collections::HashMap, sync::{Arc, Mutex}, time::{Duration, Instant}, }; - -use ethereum_rust_storage::Store; use tracing::error; -use crate::utils::{parse_json_hex, RpcErr, RpcRequest}; use crate::RpcHandler; +use crate::{ + types::block_identifier::{BlockIdentifier, BlockTag}, + utils::{parse_json_hex, RpcErr, RpcRequest}, +}; use rand::prelude::*; use serde_json::{json, Value}; -use super::logs::LogsFilter; +use super::logs::{fetch_logs_with_filter, LogsFilter}; #[derive(Debug, Clone)] pub struct NewFilterRequest { @@ -36,8 +43,19 @@ pub fn clean_outdated_filters(filters: ActiveFilters, filter_duration: Duration) active_filters_guard .retain(|_, (filter_timestamp, _)| filter_timestamp.elapsed() <= filter_duration); } -/// Maps IDs to active log filters and their timestamps. -pub type ActiveFilters = Arc>>; +/// Maps IDs to active pollable filters and their timestamps. +pub type ActiveFilters = Arc>>; + +#[derive(Debug, Clone)] +pub struct PollableFilter { + /// Last block number from when this + /// filter was requested or created. + /// i.e. if this filter is requested, + /// the log will be applied from this + /// block number up to the latest one. + pub last_block_number: BlockNumber, + pub filter_data: LogsFilter, +} impl NewFilterRequest { pub fn parse(params: &Option>) -> Result { @@ -67,6 +85,10 @@ impl NewFilterRequest { return Err(RpcErr::BadParams("Invalid block range".to_string())); } + let Some(last_block_number) = storage.get_latest_block_number()? else { + error!("Latest block number was requested but it does not exist"); + return Err(RpcErr::Internal("Failed to create filter".to_string())); + }; let id: u64 = random(); let timestamp = Instant::now(); let mut active_filters_guard = filters.lock().unwrap_or_else(|mut poisoned_guard| { @@ -75,8 +97,16 @@ impl NewFilterRequest { filters.clear_poison(); poisoned_guard.into_inner() }); - - active_filters_guard.insert(id, (timestamp, self.request_data.clone())); + active_filters_guard.insert( + id, + ( + timestamp, + PollableFilter { + last_block_number, + filter_data: self.request_data.clone(), + }, + ), + ); let as_hex = json!(format!("0x{:x}", id)); Ok(as_hex) } @@ -136,6 +166,90 @@ impl DeleteFilterRequest { } } +pub struct FilterChangesRequest { + pub id: u64, +} + +impl FilterChangesRequest { + pub fn parse(params: &Option>) -> Result { + match params.as_deref() { + Some([param]) => { + let id = parse_json_hex(param).map_err(|_err| RpcErr::BadHexFormat(0))?; + Ok(FilterChangesRequest { id }) + } + Some(_) => Err(RpcErr::BadParams( + "Expected an array with a single hex encoded id".to_string(), + )), + None => Err(RpcErr::MissingParam("0".to_string())), + } + } + pub fn handle( + &self, + storage: ethereum_rust_storage::Store, + filters: ActiveFilters, + ) -> Result { + let Some(latest_block_num) = storage.get_latest_block_number()? else { + error!("Latest block number was requested but it does not exist"); + return Err(RpcErr::Internal("Failed to create filter".to_string())); + }; + let mut active_filters_guard = filters.lock().unwrap_or_else(|mut poisoned_guard| { + error!("THREAD CRASHED WITH MUTEX TAKEN; SYSTEM MIGHT BE UNSTABLE"); + **poisoned_guard.get_mut() = HashMap::new(); + filters.clear_poison(); + poisoned_guard.into_inner() + }); + if let Some((timestamp, filter)) = active_filters_guard.get_mut(&self.id) { + // We'll only get changes for a filter that either has a block + // range for upcoming blocks, or for the 'latest' tag. + let valid_block_range = match filter.filter_data.to_block { + BlockIdentifier::Tag(BlockTag::Latest) => true, + BlockIdentifier::Number(block_num) if block_num >= latest_block_num => true, + _ => false, + }; + // This filter has a valid block range, so here's what we'll do: + // - Update the filter's timestamp and block number from the last poll. + // - Do the query to fetch logs in range last_block_number..=to_block for + // this filter. + if valid_block_range { + // Since the filter was polled, updated its timestamp, so + // it does not expire. + *timestamp = Instant::now(); + // Update this filter so the current query + // starts from the last polled block. + filter.filter_data.from_block = BlockIdentifier::Number(filter.last_block_number); + filter.last_block_number = latest_block_num; + let mut filter = filter.clone(); + filter.filter_data.to_block = BlockIdentifier::Number(latest_block_num); + // Drop the lock early to process this filter's query + // and not keep the lock more than we should. + drop(active_filters_guard); + let logs = fetch_logs_with_filter(&filter.filter_data, storage)?; + serde_json::to_value(logs).map_err(|error| { + tracing::error!("Log filtering request failed with: {error}"); + RpcErr::Internal("Failed to filter logs".to_string()) + }) + } else { + serde_json::to_value(Vec::::new()).map_err(|error| { + tracing::error!("Log filtering request failed with: {error}"); + RpcErr::Internal("Failed to filter logs".to_string()) + }) + } + } else { + Err(RpcErr::BadParams( + "No matching filter for given id".to_string(), + )) + } + } + pub fn stateful_call( + req: &RpcRequest, + storage: ethereum_rust_storage::Store, + filters: ActiveFilters, + ) -> Result { + let request = Self::parse(&req.params)?; + request.handle(storage, filters) + } +} + #[cfg(test)] mod tests { use std::{ @@ -144,20 +258,25 @@ mod tests { time::{Duration, Instant}, }; + use super::ActiveFilters; use crate::{ - eth::logs::{AddressFilter, LogsFilter, TopicFilter}, + eth::{ + filter::PollableFilter, + logs::{AddressFilter, LogsFilter, TopicFilter}, + }, map_http_requests, - utils::test_utils::start_test_api, + utils::test_utils::{self, start_test_api}, FILTER_DURATION, }; use crate::{ types::block_identifier::BlockIdentifier, utils::{test_utils::example_p2p_node, RpcRequest}, }; + use ethereum_rust_core::types::Genesis; use ethereum_rust_storage::{EngineType, Store}; - use serde_json::{json, Value}; - use super::ActiveFilters; + use serde_json::{json, Value}; + use test_utils::TEST_GENESIS; #[test] fn filter_request_smoke_test_valid_params() { @@ -184,10 +303,19 @@ mod tests { let filters = filters.lock().unwrap(); assert!(filters.len() == 1); let (_, filter) = filters.clone().get(&id).unwrap().clone(); - assert!(matches!(filter.from_block, BlockIdentifier::Number(1))); - assert!(matches!(filter.to_block, BlockIdentifier::Number(2))); - assert!(filter.address_filters.is_none()); - assert!(matches!(&filter.topics[..], [TopicFilter::Topic(_)])); + assert!(matches!( + filter.filter_data.from_block, + BlockIdentifier::Number(1) + )); + assert!(matches!( + filter.filter_data.to_block, + BlockIdentifier::Number(2) + )); + assert!(filter.filter_data.address_filters.is_none()); + assert!(matches!( + &filter.filter_data.topics[..], + [TopicFilter::Topic(_)] + )); } #[test] @@ -212,10 +340,16 @@ mod tests { let filters = filters.lock().unwrap(); assert!(filters.len() == 1); let (_, filter) = filters.clone().get(&id).unwrap().clone(); - assert!(matches!(filter.from_block, BlockIdentifier::Number(1))); - assert!(matches!(filter.to_block, BlockIdentifier::Number(255))); - assert!(filter.address_filters.is_none()); - assert!(matches!(&filter.topics[..], [])); + assert!(matches!( + filter.filter_data.from_block, + BlockIdentifier::Number(1) + )); + assert!(matches!( + filter.filter_data.to_block, + BlockIdentifier::Number(255) + )); + assert!(filter.filter_data.address_filters.is_none()); + assert!(matches!(&filter.filter_data.topics[..], [])); } #[test] @@ -240,13 +374,19 @@ mod tests { let filters = filters.lock().unwrap(); assert!(filters.len() == 1); let (_, filter) = filters.clone().get(&id).unwrap().clone(); - assert!(matches!(filter.from_block, BlockIdentifier::Number(1))); - assert!(matches!(filter.to_block, BlockIdentifier::Number(255))); assert!(matches!( - filter.address_filters.unwrap(), + filter.filter_data.from_block, + BlockIdentifier::Number(1) + )); + assert!(matches!( + filter.filter_data.to_block, + BlockIdentifier::Number(255) + )); + assert!(matches!( + filter.filter_data.address_filters.unwrap(), AddressFilter::Many(_) )); - assert!(matches!(&filter.topics[..], [])); + assert!(matches!(&filter.filter_data.topics[..], [])); } #[test] @@ -298,14 +438,16 @@ mod tests { ) -> u64 { let node = example_p2p_node(); let request: RpcRequest = serde_json::from_value(json_req).expect("Test json is incorrect"); - let response = map_http_requests( - &request, - Store::new("in-mem", EngineType::InMemory).unwrap(), - node, - filters_pointer.clone(), - ) - .unwrap() - .to_string(); + let genesis_config: Genesis = + serde_json::from_str(TEST_GENESIS).expect("Fatal: non-valid genesis test config"); + let store = Store::new("in-mem", EngineType::InMemory) + .expect("Fatal: could not create in memory test db"); + store + .add_initial_state(genesis_config) + .expect("Fatal: could not add test genesis in test"); + let response = map_http_requests(&request, store, node, filters_pointer.clone()) + .unwrap() + .to_string(); let trimmed_id = response.trim().trim_matches('"'); assert!(trimmed_id.starts_with("0x")); let hex = trimmed_id.trim_start_matches("0x"); @@ -331,11 +473,14 @@ mod tests { 0xFF, ( Instant::now(), - LogsFilter { - from_block: BlockIdentifier::Number(1), - to_block: BlockIdentifier::Number(2), - address_filters: None, - topics: vec![], + PollableFilter { + last_block_number: 0, + filter_data: LogsFilter { + from_block: BlockIdentifier::Number(1), + to_block: BlockIdentifier::Number(2), + address_filters: None, + topics: vec![], + }, }, ), ); @@ -413,6 +558,7 @@ mod tests { .await .unwrap(); + dbg!(&response); assert!( response.get("result").is_some(), "Response should have a 'result' field" diff --git a/crates/networking/rpc/eth/logs.rs b/crates/networking/rpc/eth/logs.rs index c63fc68d4..cefeb83d6 100644 --- a/crates/networking/rpc/eth/logs.rs +++ b/crates/networking/rpc/eth/logs.rs @@ -1,3 +1,7 @@ +// The behaviour of the filtering endpoints is based on: +// - Manually testing the behaviour deploying contracts on the Sepolia test network. +// - Go-Ethereum, specifically: https://github.com/ethereum/go-ethereum/blob/368e16f39d6c7e5cce72a92ec289adbfbaed4854/eth/filters/filter.go +// - Ethereum's reference: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter use crate::{ types::{block_identifier::BlockIdentifier, receipt::RpcLog}, RpcErr, RpcHandler, @@ -80,121 +84,130 @@ impl RpcHandler for LogsFilter { )), } } - // TODO: This is longer than it has the right to be, maybe we should refactor it. - // The main problem here is the layers of indirection needed - // to fetch tx and block data for a log rpc response, some ideas here are: - // - The ideal one is to have a key-value store BlockNumber -> Log, where the log also stores - // the block hash, transaction hash, transaction number and its own index. - // - Another on is the receipt stores the block hash, transaction hash and block number, - // then we simply could retrieve each log from the receipt and add the info - // needed for the RPCLog struct. fn handle(&self, storage: Store) -> Result { - let from = self - .from_block - .resolve_block_number(&storage)? - .ok_or(RpcErr::WrongParam("fromBlock".to_string()))?; - let to = self - .to_block - .resolve_block_number(&storage)? - .ok_or(RpcErr::WrongParam("toBlock".to_string()))?; + let filtered_logs = fetch_logs_with_filter(self, storage)?; + serde_json::to_value(filtered_logs).map_err(|error| { + tracing::error!("Log filtering request failed with: {error}"); + RpcErr::Internal("Failed to filter logs".to_string()) + }) + } +} - if (from..=to).is_empty() { - return Err(RpcErr::BadParams("Empty range".to_string())); - } +// TODO: This is longer than it has the right to be, maybe we should refactor it. +// The main problem here is the layers of indirection needed +// to fetch tx and block data for a log rpc response, some ideas here are: +// - The ideal one is to have a key-value store BlockNumber -> Log, where the log also stores +// the block hash, transaction hash, transaction number and its own index. +// - Another on is the receipt stores the block hash, transaction hash and block number, +// then we simply could retrieve each log from the receipt and add the info +// needed for the RPCLog struct. - let address_filter: HashSet<_> = match &self.address_filters { - Some(AddressFilter::Single(address)) => std::iter::once(address).collect(), - Some(AddressFilter::Many(addresses)) => addresses.iter().collect(), - None => HashSet::new(), - }; +pub(crate) fn fetch_logs_with_filter( + filter: &LogsFilter, + storage: Store, +) -> Result, RpcErr> { + let from = filter + .from_block + .resolve_block_number(&storage)? + .ok_or(RpcErr::WrongParam("fromBlock".to_string()))?; + let to = filter + .to_block + .resolve_block_number(&storage)? + .ok_or(RpcErr::WrongParam("toBlock".to_string()))?; + if (from..=to).is_empty() { + return Err(RpcErr::BadParams("Empty range".to_string())); + } + let address_filter: HashSet<_> = match &filter.address_filters { + Some(AddressFilter::Single(address)) => std::iter::once(address).collect(), + Some(AddressFilter::Many(addresses)) => addresses.iter().collect(), + None => HashSet::new(), + }; - let mut logs: Vec = Vec::new(); - // The idea here is to fetch every log and filter by address, if given. - // For that, we'll need each block in range, and its transactions, - // and for each transaction, we'll need its receipts, which - // contain the actual logs we want. - for block_num in from..=to { - // Take the header of the block, we - // will use it to access the transactions. - let block_body = - storage - .get_block_body(block_num)? - .ok_or(RpcErr::Internal(format!( - "Could not get body for block {block_num}" - )))?; - let block_header = storage - .get_block_header(block_num)? - .ok_or(RpcErr::Internal(format!( - "Could not get header for block {block_num}" - )))?; - let block_hash = block_header.compute_block_hash(); + let mut logs: Vec = Vec::new(); + // The idea here is to fetch every log and filter by address, if given. + // For that, we'll need each block in range, and its transactions, + // and for each transaction, we'll need its receipts, which + // contain the actual logs we want. + for block_num in from..=to { + // Take the header of the block, we + // will use it to access the transactions. + let block_body = storage + .get_block_body(block_num)? + .ok_or(RpcErr::Internal(format!( + "Could not get body for block {block_num}" + )))?; + let block_header = storage + .get_block_header(block_num)? + .ok_or(RpcErr::Internal(format!( + "Could not get header for block {block_num}" + )))?; + let block_hash = block_header.compute_block_hash(); - let mut block_log_index = 0_u64; + let mut block_log_index = 0_u64; - // Since transactions share indices with their receipts, - // we'll use them to fetch their receipts, which have the actual logs. - for (tx_index, tx) in block_body.transactions.iter().enumerate() { - let tx_hash = tx.compute_hash(); - let receipt = storage - .get_receipt(block_num, tx_index as u64)? - .ok_or(RpcErr::Internal("Could not get receipt".to_owned()))?; + // Since transactions share indices with their receipts, + // we'll use them to fetch their receipts, which have the actual logs. + for (tx_index, tx) in block_body.transactions.iter().enumerate() { + let tx_hash = tx.compute_hash(); + let receipt = storage + .get_receipt(block_num, tx_index as u64)? + .ok_or(RpcErr::Internal("Could not get receipt".to_owned()))?; - if receipt.succeeded { - for log in &receipt.logs { - if address_filter.is_empty() || address_filter.contains(&log.address) { - // Some extra data is needed when - // forming the RPC response. - logs.push(RpcLog { - log: log.clone().into(), - log_index: block_log_index, - transaction_hash: tx_hash, - transaction_index: tx_index as u64, - block_number: block_num, - block_hash, - removed: false, - }); - } - block_log_index += 1; + if receipt.succeeded { + for log in &receipt.logs { + if address_filter.is_empty() || address_filter.contains(&log.address) { + // Some extra data is needed when + // forming the RPC response. + logs.push(RpcLog { + log: log.clone().into(), + log_index: block_log_index, + transaction_hash: tx_hash, + transaction_index: tx_index as u64, + block_number: block_num, + block_hash, + removed: false, + }); } + block_log_index += 1; } } } - // Now that we have the logs filtered by address, - // we still need to filter by topics if it was a given parameter. + } + // Now that we have the logs filtered by address, + // we still need to filter by topics if it was a given parameter. - let filtered_logs = if self.topics.is_empty() { - logs - } else { - logs.into_iter() - .filter(|rpc_log| { - if self.topics.len() > rpc_log.log.topics.len() { - return false; - } - for (i, topic_filter) in self.topics.iter().enumerate() { - match topic_filter { - TopicFilter::Topic(t) => { - if let Some(topic) = t { - if rpc_log.log.topics[i] != *topic { - return false; - } - } - } - TopicFilter::Topics(sub_topics) => { - if !sub_topics.is_empty() - && !sub_topics - .iter() - .any(|st| st.map_or(true, |t| rpc_log.log.topics[i] == t)) - { + let filtered_logs = if filter.topics.is_empty() { + logs + } else { + logs.into_iter() + .filter(|rpc_log| { + if filter.topics.len() > rpc_log.log.topics.len() { + return false; + } + for (i, topic_filter) in filter.topics.iter().enumerate() { + match topic_filter { + TopicFilter::Topic(t) => { + if let Some(topic) = t { + if rpc_log.log.topics[i] != *topic { return false; } } } + TopicFilter::Topics(sub_topics) => { + if !sub_topics.is_empty() + && !sub_topics + .iter() + .any(|st| st.map_or(true, |t| rpc_log.log.topics[i] == t)) + { + return false; + } + } } - true - }) - .collect::>() - }; + } + true + }) + .collect::>() + }; - serde_json::to_value(filtered_logs).map_err(|error| RpcErr::Internal(error.to_string())) - } + Ok(filtered_logs) } diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index ec6b18254..71d14787d 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -23,7 +23,7 @@ use eth::{ }, client::{ChainId, Syncing}, fee_market::FeeHistoryRequest, - filter::{self, ActiveFilters, DeleteFilterRequest, NewFilterRequest}, + filter::{self, ActiveFilters, DeleteFilterRequest, FilterChangesRequest, NewFilterRequest}, gas_price::GasPrice, logs::LogsFilter, transaction::{ @@ -244,6 +244,7 @@ pub fn map_eth_requests( "eth_getLogs" => LogsFilter::call(req, storage), "eth_newFilter" => NewFilterRequest::stateful_call(req, storage, filters), "eth_uninstallFilter" => DeleteFilterRequest::stateful_call(req, storage, filters), + "eth_getFilterChanges" => FilterChangesRequest::stateful_call(req, storage, filters), "eth_sendRawTransaction" => SendRawTransactionRequest::call(req, storage), "eth_getProof" => GetProofRequest::call(req, storage), "eth_gasPrice" => GasPrice::call(req, storage), diff --git a/crates/networking/rpc/utils.rs b/crates/networking/rpc/utils.rs index 461b769f9..3fd23c2da 100644 --- a/crates/networking/rpc/utils.rs +++ b/crates/networking/rpc/utils.rs @@ -252,6 +252,7 @@ pub mod test_utils { use crate::start_api; + pub const TEST_GENESIS: &str = include_str!("../../../test_data/genesis-l1.json"); pub fn example_p2p_node() -> Node { let node_id_1 = H512::from_str("d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666").unwrap(); Node { @@ -278,6 +279,9 @@ pub mod test_utils { let authrpc_addr: SocketAddr = "127.0.0.1:8501".parse().unwrap(); let storage = Store::new("", EngineType::InMemory).expect("Failed to create in-memory storage"); + storage + .add_initial_state(serde_json::from_str(TEST_GENESIS).unwrap()) + .expect("Failed to build test genesis"); let jwt_secret = Default::default(); let local_p2p_node = example_p2p_node(); From b1b2810a3b2c1979cf6b47793652989524ce0621 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:23:18 -0300 Subject: [PATCH 07/49] feat(l2): refactor L1 deployment script (#954) **Motivation** In the project's early stages, we relied on Foundry for contract deployment for simplicity. At this stage of the project, Foundry is not needed anymore. **Description** - It adds the binary `ethereum_rust_l2_l1_deployer`, which when executed, compiles and deploys the L1 contracts using `CREATE2`. - Abstracts and integrates the `call`, `send`, and `deploy` logic to the `EthClient` in the `eth_sender` module. --------- Co-authored-by: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Co-authored-by: fborello-lambda --- Cargo.toml | 2 + cmd/ethereum_rust_l2/src/commands/stack.rs | 19 -- cmd/ethereum_rust_l2/src/commands/test.rs | 7 +- cmd/ethereum_rust_l2/src/commands/utils.rs | 4 +- cmd/ethereum_rust_l2/src/commands/wallet.rs | 154 +++++--------- crates/l2/.example.env | 2 + crates/l2/.gitignore | 1 + crates/l2/Makefile | 23 +- crates/l2/README.md | 128 +++++------ crates/l2/contracts/.gitignore | 10 - crates/l2/contracts/Cargo.toml | 21 ++ crates/l2/contracts/deployer.rs | 199 ++++++++++++++++++ crates/l2/contracts/foundry.toml | 6 - crates/l2/contracts/script/DeployL1.s.sol | 39 ---- crates/l2/contracts/script/Utils.sol | 44 ---- .../l2/contracts/test/l1/CommonBridge.t.sol | 17 -- crates/l2/contracts/test/l1/Deposit.t.sol | 36 ---- .../contracts/test/l1/OnChainProposer.t.sol | 13 -- crates/l2/proposer/mod.rs | 2 +- crates/l2/utils/eth_client/eth_sender.rs | 144 +++++++++++++ crates/l2/utils/eth_client/mod.rs | 40 +--- 21 files changed, 495 insertions(+), 416 deletions(-) create mode 100644 crates/l2/contracts/Cargo.toml create mode 100644 crates/l2/contracts/deployer.rs delete mode 100644 crates/l2/contracts/foundry.toml delete mode 100644 crates/l2/contracts/script/DeployL1.s.sol delete mode 100644 crates/l2/contracts/script/Utils.sol delete mode 100644 crates/l2/contracts/test/l1/CommonBridge.t.sol delete mode 100644 crates/l2/contracts/test/l1/Deposit.t.sol delete mode 100644 crates/l2/contracts/test/l1/OnChainProposer.t.sol create mode 100644 crates/l2/utils/eth_client/eth_sender.rs diff --git a/Cargo.toml b/Cargo.toml index 53b224765..4d98d829f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ members = [ "crates/l2/prover", ] resolver = "2" + +exclude = ["crates/l2/contracts"] default-members = ["cmd/ethereum_rust", "cmd/ethereum_rust_l2", "crates/l2/prover"] [workspace.package] diff --git a/cmd/ethereum_rust_l2/src/commands/stack.rs b/cmd/ethereum_rust_l2/src/commands/stack.rs index 3ea889e08..4c90649bc 100644 --- a/cmd/ethereum_rust_l2/src/commands/stack.rs +++ b/cmd/ethereum_rust_l2/src/commands/stack.rs @@ -94,7 +94,6 @@ impl Command { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } if !skip_l1_deployment { - contract_deps(&contracts_path)?; deploy_l1(&l1_rpc_url, &cfg.wallet.private_key, &contracts_path)?; } start_l2(root.to_path_buf(), &l2_rpc_url, start_prover).await?; @@ -170,24 +169,6 @@ impl Command { } } -fn contract_deps(contracts_path: &PathBuf) -> eyre::Result<()> { - if !contracts_path.join("lib/forge-std").exists() { - let cmd = std::process::Command::new("forge") - .arg("install") - .arg("foundry-rs/forge-std") - .arg("--no-git") - .arg("--root") - .arg(contracts_path) - .current_dir(contracts_path) - .spawn()? - .wait()?; - if !cmd.success() { - eyre::bail!("Failed to install forge-std"); - } - } - Ok(()) -} - fn deploy_l1( l1_rpc_url: &str, deployer_private_key: &SecretKey, diff --git a/cmd/ethereum_rust_l2/src/commands/test.rs b/cmd/ethereum_rust_l2/src/commands/test.rs index 083a2bc46..92a79a185 100644 --- a/cmd/ethereum_rust_l2/src/commands/test.rs +++ b/cmd/ethereum_rust_l2/src/commands/test.rs @@ -86,7 +86,7 @@ async fn transfer_from( println!("transfer {i} from {pk}"); } - let tx = EIP1559Transaction { + let mut tx = EIP1559Transaction { to: TxKind::Call(to_address), chain_id: cfg.network.l2_chain_id, nonce: i, @@ -97,10 +97,7 @@ async fn transfer_from( ..Default::default() }; - while let Err(e) = client - .send_eip1559_transaction(tx.clone(), private_key) - .await - { + while let Err(e) = client.send_eip1559_transaction(&mut tx, private_key).await { println!("Transaction failed (PK: {pk} - Nonce: {}): {e}", tx.nonce); retries += 1; sleep(std::time::Duration::from_secs(2)); diff --git a/cmd/ethereum_rust_l2/src/commands/utils.rs b/cmd/ethereum_rust_l2/src/commands/utils.rs index 9e46118a4..40b02e4c8 100644 --- a/cmd/ethereum_rust_l2/src/commands/utils.rs +++ b/cmd/ethereum_rust_l2/src/commands/utils.rs @@ -48,8 +48,8 @@ fn parse_arg(arg_type: &str, arg: &str) -> Vec { } "uint256" => { let number = U256::from_dec_str(arg).expect("Cannot parse number"); - let mut buf: &mut [u8] = &mut [0u8; 32]; - number.to_big_endian(&mut buf); + let buf = &mut [0u8; 32]; + number.to_big_endian(buf); buf.to_vec() } "bytes32" => { diff --git a/cmd/ethereum_rust_l2/src/commands/wallet.rs b/cmd/ethereum_rust_l2/src/commands/wallet.rs index 834b25d08..6210e4017 100644 --- a/cmd/ethereum_rust_l2/src/commands/wallet.rs +++ b/cmd/ethereum_rust_l2/src/commands/wallet.rs @@ -1,12 +1,10 @@ use crate::config::EthereumRustL2Config; use bytes::Bytes; use clap::Subcommand; -use ethereum_rust_core::types::{EIP1559Transaction, GenericTransaction, TxKind}; -use ethereum_rust_l2::utils::eth_client::{errors::EthClientError, EthClient}; -use ethereum_rust_rlp::encode::RLPEncode; +use ethereum_rust_core::types::{EIP1559Transaction, TxKind}; +use ethereum_rust_l2::utils::eth_client::{eth_sender::Overrides, EthClient}; use ethereum_types::{Address, H256, U256}; use hex::FromHexError; -use keccak_hash::keccak; #[derive(Subcommand)] pub(crate) enum Command { @@ -167,45 +165,10 @@ pub(crate) enum Command { } fn decode_hex(s: &str) -> Result { - if s.starts_with("0x") { - return hex::decode(&s[2..]).map(Into::into); + match s.strip_prefix("0x") { + Some(s) => hex::decode(s).map(Into::into), + None => hex::decode(s).map(Into::into), } - return hex::decode(s).map(Into::into); -} - -async fn make_eip1559_transaction( - client: &EthClient, - to: TxKind, - from: Address, - data: Bytes, - value: U256, - chain_id: u64, - nonce: Option, - gas_limit: Option, - gas_price: Option, - priority_gas_price: Option, -) -> Result { - let mut tx = EIP1559Transaction { - to, - data, - value, - chain_id, - nonce: match nonce { - Some(nonce) => nonce, - None => client.get_nonce(from).await?, - }, - max_fee_per_gas: match gas_price { - Some(price) => price, - None => client.get_gas_price().await?.as_u64(), - }, - max_priority_fee_per_gas: priority_gas_price.unwrap_or(Default::default()), - ..Default::default() - }; - tx.gas_limit = match gas_limit { - Some(gas) => gas, - None => client.estimate_gas(tx.clone()).await?, - }; - Ok(tx) } impl Command { @@ -293,11 +256,11 @@ impl Command { let tx_hash = if l1 { eth_client - .send_eip1559_transaction(transfer_transaction, cfg.wallet.private_key) + .send_eip1559_transaction(&mut transfer_transaction, cfg.wallet.private_key) .await? } else { rollup_client - .send_eip1559_transaction(transfer_transaction, cfg.wallet.private_key) + .send_eip1559_transaction(&mut transfer_transaction, cfg.wallet.private_key) .await? }; @@ -335,25 +298,22 @@ impl Command { false => rollup_client, }; - let tx = make_eip1559_transaction( - &client, - TxKind::Call(to), - from, - calldata, - value, - chain_id.unwrap_or_else(|| match l1 { - true => cfg.network.l1_chain_id, - false => cfg.network.l2_chain_id, - }), - nonce, - gas_limit, - gas_price, - priority_gas_price, - ) - .await?; - let tx_hash = client - .send_eip1559_transaction(tx, cfg.wallet.private_key) + .send( + calldata, + from, + TxKind::Call(to), + cfg.wallet.private_key, + Overrides { + value: value.into(), + nonce, + chain_id, + gas_limit, + gas_price, + priority_gas_price, + ..Default::default() + }, + ) .await?; println!( @@ -375,17 +335,19 @@ impl Command { false => rollup_client, }; - let call_tx = GenericTransaction { - to: TxKind::Call(to), - input: calldata, - value, - from: from.unwrap_or(Default::default()), - gas: gas_limit, - gas_price: gas_price.unwrap_or(Default::default()), - ..Default::default() - }; - - let result = client.call(call_tx).await?; + let result = client + .call( + to, + calldata, + Overrides { + from, + value: value.into(), + gas_limit, + gas_price, + ..Default::default() + }, + ) + .await?; println!("{result}"); } @@ -404,37 +366,25 @@ impl Command { false => rollup_client, }; - let nonce = nonce.unwrap_or(client.get_nonce(from).await?); - let tx = make_eip1559_transaction( - &client, - TxKind::Create, - from, - bytecode, - value, - chain_id.unwrap_or_else(|| match l1 { - true => cfg.network.l1_chain_id, - false => cfg.network.l2_chain_id, - }), - Some(nonce), - gas_limit, - gas_price, - priority_gas_price, - ) - .await?; - - let hash = client - .send_eip1559_transaction(tx, cfg.wallet.private_key) + let (deployment_tx_hash, deployed_contract_address) = client + .deploy( + from, + cfg.wallet.private_key, + bytecode, + Overrides { + value: value.into(), + nonce, + chain_id, + gas_limit, + gas_price, + priority_gas_price, + ..Default::default() + }, + ) .await?; - let encoded_from = from.encode_to_vec(); - let encoded_nonce = nonce.encode_to_vec(); - let mut encoded = vec![(0xc0 + encoded_from.len() + encoded_nonce.len()) as u8]; - encoded.extend(encoded_from.clone()); - encoded.extend(encoded_nonce.clone()); - let deployed_address = Address::from(keccak(encoded)); - - println!("Contract deployed in tx: {hash:#x}"); - println!("Contract address: {deployed_address:#x}"); + println!("Contract deployed in tx: {deployment_tx_hash:#x}"); + println!("Contract address: {deployed_contract_address:#x}"); } }; Ok(()) diff --git a/crates/l2/.example.env b/crates/l2/.example.env index d2732cc00..9979379cb 100644 --- a/crates/l2/.example.env +++ b/crates/l2/.example.env @@ -1,4 +1,6 @@ ETH_RPC_URL=http://localhost:8545 +DEPLOYER_ADDRESS=0x3d1e15a1a55578f7c920884a9943b3b35d0d885b +DEPLOYER_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 L1_WATCHER_BRIDGE_ADDRESS=0x90cD151b6e9500F13240dF68673f3B81245D57d4 L1_WATCHER_TOPICS=0x6f65d68a35457dd88c1f8641be5da191aa122bc76de22ab0789dcc71929d7d37 L1_WATCHER_CHECK_INTERVAL_MS=5000 diff --git a/crates/l2/.gitignore b/crates/l2/.gitignore index 4c49bd78f..475a009a6 100644 --- a/crates/l2/.gitignore +++ b/crates/l2/.gitignore @@ -1 +1,2 @@ .env +solc_out diff --git a/crates/l2/Makefile b/crates/l2/Makefile index a42f2d484..d0af141db 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -7,13 +7,13 @@ L2_GENESIS_FILE_PATH=../../test_data/genesis-l2.json help: ## 📚 Show help for each of the Makefile recipes @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -init: init-local-l1 contract-deps deploy-l1 init-l2 ## 🚀 Initializes a localnet with Lambda Ethereum Rust client as both L1 and L2 +init: init-local-l1 deploy-l1 init-l2 ## 🚀 Initializes a localnet with Lambda Ethereum Rust client as both L1 and L2 down: down-local-l1 down-l2 ## 🛑 Shuts down the localnet clean: clean-contract-deps ## 🧹 Cleans the localnet -restart: restart-local-l1 restart-contract-deps restart-l2 ## 🔄 Restarts the localnet +restart: restart-local-l1 restart-contract-deps deploy-l1 restart-l2 ## 🔄 Restarts the localnet cli: ## 🛠️ Builds the L2 Lambda Ethereum Rust CLI cargo build --release --manifest-path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust/Cargo.toml @@ -24,7 +24,7 @@ ETHEREUM_RUST_PATH=$(shell pwd)/../../ ETHEREUM_RUST_BIN_PATH=$(ETHEREUM_RUST_PATH)/target/release/ethereum_rust ETHEREUM_RUST_DEV_DOCKER_COMPOSE_PATH=$(ETHEREUM_RUST_PATH)/crates/blockchain/dev/docker-compose-dev.yaml -FOUNDRY_PROJECT_HOME=$(shell pwd)/contracts +ETHEREUM_RUST_L2_CONTRACTS_PATH=./contracts L1_RPC_URL=http://localhost:8545 L1_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 @@ -40,24 +40,15 @@ restart-local-l1: down-local-l1 init-local-l1 ## 🔄 Restarts the L1 Lambda Eth # Contracts -contract-deps: ## 📦 Installs the dependencies for the L1 contracts - mkdir -p ${FOUNDRY_PROJECT_HOME} - forge install foundry-rs/forge-std --no-git --root ${FOUNDRY_PROJECT_HOME} || exit 0 +# contract-deps: ## 📦 Installs the dependencies for the L1 contracts clean-contract-deps: ## 🧹 Cleans the dependencies for the L1 contracts. - rm -rf contracts/lib + rm -rf contracts/solc_out -restart-contract-deps: clean-contract-deps contract-deps ## 🔄 Restarts the dependencies for the L1 contracts. +restart-contract-deps: clean-contract-deps ## 🔄 Restarts the dependencies for the L1 contracts. deploy-l1: ## 📜 Deploys the L1 contracts - cd ${FOUNDRY_PROJECT_HOME} && \ - forge script script/DeployL1.s.sol:DeployL1Script --rpc-url ${L1_RPC_URL} --private-key ${L1_PRIVATE_KEY} --broadcast --use $$(which solc) - -deploy-on-chain-operator: ## 📜 Deploys the OnChainOperator contract in L1 - forge create ${FOUNDRY_PROJECT_HOME}/src/l1/OnChainOperator.sol:OnChainOperator --rpc-url ${L1_RPC_URL} --private-key ${L1_PRIVATE_KEY} - -deploy-bridge: ## 📜 Deploys the CommonBridge contract in L1 - forge create ${FOUNDRY_PROJECT_HOME}/src/l1/CommonBridge.sol:CommonBridge --rpc-url ${L1_RPC_URL} --private-key ${L1_PRIVATE_KEY} + cargo run --release --bin ethereum_rust_l2_l1_deployer --manifest-path ${ETHEREUM_RUST_L2_CONTRACTS_PATH}/Cargo.toml # L2 diff --git a/crates/l2/README.md b/crates/l2/README.md index 7df7cb9ba..834c7b3e6 100644 --- a/crates/l2/README.md +++ b/crates/l2/README.md @@ -23,8 +23,6 @@ - [Status](#status-7) - [Milestone 8: Validium](#milestone-8-validium) - [Status](#status-8) - - [Prerequisites](#prerequisites) - - [Foundry](#foundry) - [How to run](#how-to-run) - [Initialize the network](#initialize-the-network) - [Restarting the network](#restarting-the-network) @@ -36,15 +34,15 @@ | Milestone | Description | Status | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | -| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | -| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | -| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | -| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | -| 5 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | -| 6 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | -| 7 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | -| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | +| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | +| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | +| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | +| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | +| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | +| 5 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | +| 6 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | +| 7 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | +| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | ### Milestone 0 @@ -54,18 +52,17 @@ Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds o | | Name | Description | Status | | --------- | ----------------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | `CommonBridge` | Deposit method implementation | ✅ | -| | `OnChainOperator` | Commit and verify methods (placeholders for this stage) | ✅ | -| VM | | Adapt EVM to handle deposits | ✅ | -| Proposer | `Proposer` | Proposes new blocks to be executed | ✅ | -| | `L1Watcher` | Listens for and handles L1 deposits | ✅ | -| | `L1TxSender` | commits new block proposals and sends block execution proofs to be verified | ✅ | -| | Deposit transactions handling | new transaction type for minting funds corresponding to deposits | ✅ | -| CLI | `stack` | Support commands for initializing the network | ✅ | -| CLI | `config` | Support commands for network config management | ✅ | -| CLI | `wallet deposit` | Support command por depositing funds on L2 | ✅ | -| CLI | `wallet transfer` | Support command for transferring funds on L2 | ✅ | - +| Contracts | `CommonBridge` | Deposit method implementation | ✅ | +| | `OnChainOperator` | Commit and verify methods (placeholders for this stage) | ✅ | +| VM | | Adapt EVM to handle deposits | ✅ | +| Proposer | `Proposer` | Proposes new blocks to be executed | ✅ | +| | `L1Watcher` | Listens for and handles L1 deposits | ✅ | +| | `L1TxSender` | commits new block proposals and sends block execution proofs to be verified | ✅ | +| | Deposit transactions handling | new transaction type for minting funds corresponding to deposits | ✅ | +| CLI | `stack` | Support commands for initializing the network | ✅ | +| CLI | `config` | Support commands for network config management | ✅ | +| CLI | `wallet deposit` | Support command por depositing funds on L2 | ✅ | +| CLI | `wallet transfer` | Support command for transferring funds on L2 | ✅ | ### Milestone 1: MVP @@ -73,13 +70,13 @@ The network supports basic L2 functionality, allowing users to deposit and withd #### Status -| | Name | Description | Status | -| --------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------ | -| Contracts | `CommonBridge` | Withdraw method implementation | ❌ | -| | `OnChainOperator` | Commit and verify implementation | 🏗️ | -| | `Verifier` | verifier | 🏗️ | -| | Withdraw transactions handling | New transaction type for burning funds on L2 and unlock funds on L1 | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | +| | Name | Description | Status | +| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------ | +| Contracts | `CommonBridge` | Withdraw method implementation | ❌ | +| | `OnChainOperator` | Commit and verify implementation | 🏗️ | +| | `Verifier` | verifier | 🏗️ | +| | Withdraw transactions handling | New transaction type for burning funds on L2 and unlock funds on L1 | 🏗️ | +| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | ### Milestone 2: Block Execution Proofs @@ -87,12 +84,12 @@ The L2's block execution is proven with a RISC-V zkVM and the proof is verified #### Status -| | Name | Description | Status | -| --------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------ | -| VM | | `Return` the storage touched on block execution to pass the prover as a witness | 🏗️ | -| Contracts | `OnChainOperator` | Call the actual SNARK proof verification on the `verify` function implementation | 🏗️ | -| Proposer | `Prover Server` | Feeds the `Prover Client` with block data to be proven and delivers proofs to the `L1TxSender` for L1 verification | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | +| | Name | Description | Status | +| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------ | +| VM | | `Return` the storage touched on block execution to pass the prover as a witness | 🏗️ | +| Contracts | `OnChainOperator` | Call the actual SNARK proof verification on the `verify` function implementation | 🏗️ | +| Proposer | `Prover Server` | Feeds the `Prover Client` with block data to be proven and delivers proofs to the `L1TxSender` for L1 verification | 🏗️ | +| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | ### Milestone 3: State diffs + Data compression + EIP 4844 (Blobs) @@ -104,15 +101,15 @@ It also supports EIP 4844 for L1 commit transactions, which means state diffs ar | | Name | Description | Status | | --------- | ------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | OnChainOperator | Differentiate whether to execute in calldata or blobs mode | ❌ | -| Prover | RISC-V zkVM | Prove state diffs compression | ❌ | -| | RISC-V zkVM | Adapt state proofs | ❌ | -| VM | | The VM should return which storage slots were modified | ❌ | -| Proposer | Prover Server | Sends state diffs to the prover | ❌ | -| | L1TxSender | Differentiate whether to send the commit transaction with calldata or blobs | ❌ | -| | | Add program for proving blobs | ❌ | -| CLI | `reconstruct-state` | Add a command for reconstructing the state | ❌ | -| | `init` | Adapt network initialization to either send blobs or calldata | ❌ | +| Contracts | OnChainOperator | Differentiate whether to execute in calldata or blobs mode | ❌ | +| Prover | RISC-V zkVM | Prove state diffs compression | ❌ | +| | RISC-V zkVM | Adapt state proofs | ❌ | +| VM | | The VM should return which storage slots were modified | ❌ | +| Proposer | Prover Server | Sends state diffs to the prover | ❌ | +| | L1TxSender | Differentiate whether to send the commit transaction with calldata or blobs | ❌ | +| | | Add program for proving blobs | ❌ | +| CLI | `reconstruct-state` | Add a command for reconstructing the state | ❌ | +| | `init` | Adapt network initialization to either send blobs or calldata | ❌ | ### Milestone 4: Custom Native token @@ -122,11 +119,11 @@ The L2 can also be deployed using a custom native token, meaning that a certain | | Name | Description | Status | | --- | -------------- | ----------------------------------------------------------------------------------------- | ------ | -| | `CommonBridge` | For native token withdrawals, infer the native token and reimburse the user in that token | ❌ | -| | `CommonBridge` | For native token deposits, msg.value = 0 and valueToMintOnL2 > 0 | ❌ | -| | `CommonBridge` | Keep track of chain's native token | ❌ | -| | `deposit` | Handle native token deposits | ❌ | -| | `withdraw` | Handle native token withdrawals | ❌ | +| | `CommonBridge` | For native token withdrawals, infer the native token and reimburse the user in that token | ❌ | +| | `CommonBridge` | For native token deposits, msg.value = 0 and valueToMintOnL2 > 0 | ❌ | +| | `CommonBridge` | Keep track of chain's native token | ❌ | +| | `deposit` | Handle native token deposits | ❌ | +| | `withdraw` | Handle native token withdrawals | ❌ | ### Milestone 5: Based Contestable Rollup @@ -136,7 +133,7 @@ The network can be run as a Based Rollup, meaning sequencing is done by the Ethe | | Name | Description | Status | | --- | ----------------- | ------------------------------------------------------------------------------ | ------ | -| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | +| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | TODO: Expand on this. @@ -148,11 +145,11 @@ The L2 has added security mechanisms in place, running on Trusted Execution Envi | | Name | Description | Status | | --------- | ---- | ---------------------------------------------------- | ------ | -| VM/Prover | | Support proving with multiple different zkVMs | ❌ | -| Contracts | | Support verifying multiple different zkVM executions | ❌ | -| VM | | Support running the operator on a TEE environment | ❌ | +| VM/Prover | | Support proving with multiple different zkVMs | ❌ | +| Contracts | | Support verifying multiple different zkVM executions | ❌ | +| VM | | Support running the operator on a TEE environment | ❌ | -### Milestone 7: Account Abstraction +### Milestone 7: Account Abstraction The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. @@ -171,27 +168,15 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer | | Name | Description | Status | | --------- | ------------- | ---------------------------------------------------- | ------ | -| Contracts | BlockExecutor | Do not check data availability in Validium mode | ❌ | -| Proposer | L1TxSender | Do not send data in commit transactions | ❌ | -| CLI | `init` | Adapt network initialization to support Validium L2s | ❌ | -| Misc | | Add a DA integration example for Validium mode | ❌ | +| Contracts | BlockExecutor | Do not check data availability in Validium mode | ❌ | +| Proposer | L1TxSender | Do not send data in commit transactions | ❌ | +| CLI | `init` | Adapt network initialization to support Validium L2s | ❌ | +| Misc | | Add a DA integration example for Validium mode | ❌ | ## Prerequisites - [Rust (explained in the repo's main README)](../../README.md) - [Docker](https://docs.docker.com/engine/install/) (with [Docker Compose](https://docs.docker.com/compose/install/)) -- [Foundry](#foundry) - -### Foundry - -1. First, install `foundryup`: - ```shell - curl -L https://foundry.paradigm.xyz | bash - ``` -2. Then run `foundryup`: - ```shell - foundryup - ``` ## How to run @@ -199,6 +184,7 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer > [!IMPORTANT] > Before this step: +> > 1. make sure the Docker daemon is running. > 2. make sure you have created a `.env` file following the `.env.example` file. diff --git a/crates/l2/contracts/.gitignore b/crates/l2/contracts/.gitignore index b5ef8d8ff..61374d418 100644 --- a/crates/l2/contracts/.gitignore +++ b/crates/l2/contracts/.gitignore @@ -1,13 +1,3 @@ -# Compiler files -cache/ -out/ - -# Ignores development broadcast logs -broadcast/ - -# Docs -docs/ - # Dotenv file .env diff --git a/crates/l2/contracts/Cargo.toml b/crates/l2/contracts/Cargo.toml new file mode 100644 index 000000000..671d56804 --- /dev/null +++ b/crates/l2/contracts/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ethereum_rust_l2-deployer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.38.0", features = ["full"] } +ethereum-types = { version = "0.14.1", features = ["serialize"] } +bytes = { version = "1.6.0", features = ["serde"] } +libsecp256k1 = "0.7.1" +keccak-hash = "0.10.0" +hex = "0.4.3" + +ethereum_rust-l2 = { path = "../../l2" } +ethereum_rust-core = { path = "../../common" } + +[[bin]] +name = "ethereum_rust_l2_l1_deployer" +path = "./deployer.rs" diff --git a/crates/l2/contracts/deployer.rs b/crates/l2/contracts/deployer.rs new file mode 100644 index 000000000..2687c3ab0 --- /dev/null +++ b/crates/l2/contracts/deployer.rs @@ -0,0 +1,199 @@ +use bytes::Bytes; +use ethereum_rust_core::types::{TxKind, GAS_LIMIT_ADJUSTMENT_FACTOR, GAS_LIMIT_MINIMUM}; +use ethereum_rust_l2::utils::{ + config::read_env_file, + eth_client::{eth_sender::Overrides, EthClient}, +}; +use ethereum_types::{Address, H160, H256}; +use keccak_hash::keccak; +use libsecp256k1::SecretKey; +use std::{process::Command, str::FromStr}; + +// 0x4e59b44847b379578588920cA78FbF26c0B4956C +const DETERMINISTIC_CREATE2_ADDRESS: Address = H160([ + 0x4e, 0x59, 0xb4, 0x48, 0x47, 0xb3, 0x79, 0x57, 0x85, 0x88, 0x92, 0x0c, 0xa7, 0x8f, 0xbf, 0x26, + 0xc0, 0xb4, 0x95, 0x6c, +]); +const SALT: H256 = H256::zero(); + +#[tokio::main] +async fn main() { + read_env_file().unwrap(); + + let eth_client = EthClient::new(&std::env::var("ETH_RPC_URL").expect("ETH_RPC_URL not set")); + + let deployer = std::env::var("DEPLOYER_ADDRESS") + .expect("DEPLOYER_ADDRESS not set") + .parse() + .expect("Malformed DEPLOYER_ADDRESS"); + let deployer_private_key = SecretKey::parse( + H256::from_str( + std::env::var("DEPLOYER_PRIVATE_KEY") + .expect("DEPLOYER_PRIVATE_KEY not set") + .strip_prefix("0x") + .expect("Malformed DEPLOYER_ADDRESS (strip_prefix(\"0x\"))"), + ) + .expect("Malformed DEPLOYER_ADDRESS (H256::from_str)") + .as_fixed_bytes(), + ) + .expect("Malformed DEPLOYER_PRIVATE_KEY (SecretKey::parse)"); + + if std::fs::exists("solc_out").expect("Could not determine if solc_out exists") { + std::fs::remove_dir_all("solc_out").expect("Failed to remove solc_out"); + } + + let overrides = Overrides { + gas_limit: Some(GAS_LIMIT_MINIMUM * GAS_LIMIT_ADJUSTMENT_FACTOR), + gas_price: Some(1_000_000_000), + ..Default::default() + }; + + let (on_chain_proposer_deployment_tx_hash, on_chain_proposer_address) = + deploy_on_chain_proposer( + deployer, + deployer_private_key, + overrides.clone(), + ð_client, + ) + .await; + println!( + "OnChainProposer deployed at address {:#x} with tx hash {:#x}", + on_chain_proposer_address, on_chain_proposer_deployment_tx_hash + ); + + let (bridge_deployment_tx_hash, bridge_address) = + deploy_bridge(deployer, deployer_private_key, overrides, ð_client).await; + println!( + "Bridge deployed at address {:#x} with tx hash {:#x}", + bridge_address, bridge_deployment_tx_hash + ); +} + +async fn deploy_on_chain_proposer( + deployer: Address, + deployer_private_key: SecretKey, + overrides: Overrides, + eth_client: &EthClient, +) -> (H256, Address) { + // Both the contract path and the output path are relative to where the Makefile is. + assert!( + Command::new("solc") + .arg("--bin") + .arg("./contracts/src/l1/OnChainProposer.sol") + .arg("-o") + .arg("contracts/solc_out") + .spawn() + .expect("Failed to spawn solc") + .wait() + .expect("Failed to wait for solc") + .success(), + "Failed to compile OnChainProposer.sol" + ); + + let on_chain_proposer_init_code = hex::decode( + std::fs::read_to_string("./contracts/solc_out/OnChainProposer.bin") + .expect("Failed to read on_chain_proposer_init_code"), + ) + .expect("Failed to decode on_chain_proposer_init_code") + .into(); + + let (deploy_tx_hash, on_chain_proposer_address) = create2_deploy( + deployer, + deployer_private_key, + &on_chain_proposer_init_code, + overrides, + eth_client, + ) + .await; + + (deploy_tx_hash, on_chain_proposer_address) +} + +async fn deploy_bridge( + deployer: Address, + deployer_private_key: SecretKey, + overrides: Overrides, + eth_client: &EthClient, +) -> (H256, Address) { + assert!( + Command::new("solc") + .arg("--bin") + .arg("./contracts/src/l1/CommonBridge.sol") + .arg("-o") + .arg("contracts/solc_out") + .spawn() + .expect("Failed to spawn solc") + .wait() + .expect("Failed to wait for solc") + .success(), + "Failed to compile CommonBridge.sol" + ); + + let bridge_init_code = hex::decode( + std::fs::read_to_string("./contracts/solc_out/CommonBridge.bin") + .expect("Failed to read bridge_init_code"), + ) + .expect("Failed to decode bridge_init_code") + .into(); + + let (deploy_tx_hash, bridge_address) = create2_deploy( + deployer, + deployer_private_key, + &bridge_init_code, + overrides, + eth_client, + ) + .await; + + (deploy_tx_hash, bridge_address) +} + +async fn create2_deploy( + deployer: Address, + deployer_private_key: SecretKey, + init_code: &Bytes, + overrides: Overrides, + eth_client: &EthClient, +) -> (H256, Address) { + let calldata = [SALT.as_bytes(), init_code].concat(); + let deploy_tx_hash = eth_client + .send( + calldata.into(), + deployer, + TxKind::Call(DETERMINISTIC_CREATE2_ADDRESS), + deployer_private_key, + overrides, + ) + .await + .unwrap(); + + while eth_client + .get_transaction_receipt(deploy_tx_hash) + .await + .expect("Failed to get transaction receipt") + .is_none() + { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + + let deployed_address = create2_address(keccak(init_code)); + + (deploy_tx_hash, deployed_address) +} + +fn create2_address(init_code_hash: H256) -> Address { + Address::from_slice( + keccak( + [ + &[0xff], + DETERMINISTIC_CREATE2_ADDRESS.as_bytes(), + SALT.as_bytes(), + init_code_hash.as_bytes(), + ] + .concat(), + ) + .as_bytes() + .get(12..) + .expect("Failed to get create2 address"), + ) +} diff --git a/crates/l2/contracts/foundry.toml b/crates/l2/contracts/foundry.toml deleted file mode 100644 index 25b918f9c..000000000 --- a/crates/l2/contracts/foundry.toml +++ /dev/null @@ -1,6 +0,0 @@ -[profile.default] -src = "src" -out = "out" -libs = ["lib"] - -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/crates/l2/contracts/script/DeployL1.s.sol b/crates/l2/contracts/script/DeployL1.s.sol deleted file mode 100644 index abb63015e..000000000 --- a/crates/l2/contracts/script/DeployL1.s.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {OnChainProposer} from "../src/l1/OnChainProposer.sol"; -import {CommonBridge} from "../src/l1/CommonBridge.sol"; -import {Utils} from "./Utils.sol"; - -contract DeployL1Script is Script { - /// @notice Address of the deterministic create2 factory. - /// @dev This address corresponds to a contracts that is set in the storage - /// in the genesis file. The same contract with the same address is deployed - /// in every testnet, so if this script is run in a testnet instead of in a - /// local environment, it should work. - address constant DETERMINISTIC_CREATE2_ADDRESS = 0x4e59b44847b379578588920cA78FbF26c0B4956C; - - function setUp() public {} - - function run() public { - console.log("Deploying L1 contracts"); - - deployOnChainProposer(); - deployCommonBridge(); - } - - function deployOnChainProposer() internal { - bytes memory bytecode = type(OnChainProposer).creationCode; - bytes32 salt = bytes32(0); - address contractAddress = Utils.deployWithCreate2(bytecode, salt, DETERMINISTIC_CREATE2_ADDRESS); - console.log("OnChainProposer deployed at:", contractAddress); - } - - function deployCommonBridge() internal { - bytes memory bytecode = type(CommonBridge).creationCode; - bytes32 salt = bytes32(0); - address contractAddress = Utils.deployWithCreate2(bytecode, salt, DETERMINISTIC_CREATE2_ADDRESS); - console.log("CommonBridge deployed at:", contractAddress); - } -} diff --git a/crates/l2/contracts/script/Utils.sol b/crates/l2/contracts/script/Utils.sol deleted file mode 100644 index 1b3d59fc4..000000000 --- a/crates/l2/contracts/script/Utils.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import {Vm} from "forge-std/Vm.sol"; - -library Utils { - // Cheatcodes address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm internal constant vm = Vm(VM_ADDRESS); - - function deployWithCreate2(bytes memory bytecode, bytes32 salt, address create2Factory) internal returns (address) { - if (bytecode.length == 0) { - revert("Bytecode is not set"); - } - address contractAddress = vm.computeCreate2Address(salt, keccak256(bytecode), create2Factory); - if (contractAddress.code.length != 0) { - return contractAddress; - } - - vm.broadcast(); - (bool success, bytes memory data) = create2Factory.call(abi.encodePacked(salt, bytecode)); - contractAddress = bytesToAddress(data); - - if (!success) { - revert("Failed to deploy contract via create2: create2Factory call failed"); - } - - if (contractAddress == address(0)) { - revert("Failed to deploy contract via create2: contract address is zero"); - } - - if (contractAddress.code.length == 0) { - revert("Failed to deploy contract via create2: contract code is empty"); - } - - return contractAddress; - } - - function bytesToAddress(bytes memory bys) internal pure returns (address addr) { - assembly { - addr := mload(add(bys, 20)) - } - } -} diff --git a/crates/l2/contracts/test/l1/CommonBridge.t.sol b/crates/l2/contracts/test/l1/CommonBridge.t.sol deleted file mode 100644 index b520da526..000000000 --- a/crates/l2/contracts/test/l1/CommonBridge.t.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {CommonBridge} from "../../src/l1/CommonBridge.sol"; - -contract CommonBridgeTest is Test { - CommonBridge internal commonBridge; - address internal alice; - bytes32 internal dummyl2MintTxHash; - - constructor() { - commonBridge = new CommonBridge(); - alice = makeAddr("alice"); - dummyl2MintTxHash = keccak256(abi.encodePacked("dummyl2MintTxHash")); - } -} diff --git a/crates/l2/contracts/test/l1/Deposit.t.sol b/crates/l2/contracts/test/l1/Deposit.t.sol deleted file mode 100644 index 21ab5502f..000000000 --- a/crates/l2/contracts/test/l1/Deposit.t.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.27; - -import {CommonBridgeTest} from "./CommonBridge.t.sol"; - -contract DepositTest is CommonBridgeTest { - event DepositInitiated(uint256 indexed amount, address indexed to, bytes32 indexed l2MintTxHash); - - error AmountToDepositIsZero(); - - function test_cannotDepositWithAmountZero() public { - vm.expectRevert(abi.encodePacked(AmountToDepositIsZero.selector)); - commonBridge.deposit{value: 0 ether}(alice); - } - - function test_cannotDepositThroughEOATransferWithAmountZero() public { - vm.expectRevert(abi.encodePacked(AmountToDepositIsZero.selector)); - commonBridge.deposit{value: 0 ether}(alice); - } - - function test_canDeposit() public { - vm.expectEmit(true, true, true, true, address(commonBridge)); - emit DepositInitiated(0.1 ether, alice, dummyl2MintTxHash); - vm.deal(alice, 1 ether); - commonBridge.deposit{value: 0.1 ether}(alice); - } - - function test_canDepositThroughEOATransfer() public { - vm.expectEmit(true, true, true, true, address(commonBridge)); - emit DepositInitiated(0.1 ether, alice, dummyl2MintTxHash); - vm.deal(alice, 1 ether); - (bool success,) = address(commonBridge).call{value: 0.1 ether}(""); - require(success, "Transfer failed"); - } -} diff --git a/crates/l2/contracts/test/l1/OnChainProposer.t.sol b/crates/l2/contracts/test/l1/OnChainProposer.t.sol deleted file mode 100644 index 6d386c7a3..000000000 --- a/crates/l2/contracts/test/l1/OnChainProposer.t.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {OnChainProposer} from "../../src/l1/OnChainProposer.sol"; - -contract OnChainProposerTest is Test { - OnChainProposer public proposer; - - function setUp() public { - proposer = new OnChainProposer(); - } -} diff --git a/crates/l2/proposer/mod.rs b/crates/l2/proposer/mod.rs index cf4c844ac..eeb006983 100644 --- a/crates/l2/proposer/mod.rs +++ b/crates/l2/proposer/mod.rs @@ -271,7 +271,7 @@ impl Proposer { .saturating_add(TX_GAS_COST); self.eth_client - .send_eip1559_transaction(tx, self.l1_private_key) + .send_eip1559_transaction(&mut tx, self.l1_private_key) .await .map_err(ProposerError::from) } diff --git a/crates/l2/utils/eth_client/eth_sender.rs b/crates/l2/utils/eth_client/eth_sender.rs new file mode 100644 index 000000000..be1269947 --- /dev/null +++ b/crates/l2/utils/eth_client/eth_sender.rs @@ -0,0 +1,144 @@ +use crate::utils::eth_client::{ + errors::{CallError, EthClientError}, + EthClient, RpcResponse, +}; +use bytes::Bytes; +use ethereum_rust_core::types::{EIP1559Transaction, GenericTransaction, TxKind}; +use ethereum_rust_rlp::encode::RLPEncode; +use ethereum_rust_rpc::utils::{RpcRequest, RpcRequestId}; +use ethereum_types::{Address, U256}; +use keccak_hash::{keccak, H256}; +use libsecp256k1::SecretKey; +use serde_json::json; + +#[derive(Default, Clone)] +pub struct Overrides { + pub from: Option
, + pub value: Option, + pub nonce: Option, + pub chain_id: Option, + pub gas_limit: Option, + pub gas_price: Option, + pub priority_gas_price: Option, +} + +impl EthClient { + pub async fn call( + &self, + to: Address, + calldata: Bytes, + overrides: Overrides, + ) -> Result { + let tx = GenericTransaction { + to: TxKind::Call(to), + input: calldata, + value: overrides.value.unwrap_or_default(), + from: overrides.from.unwrap_or_default(), + gas: overrides.gas_limit, + gas_price: overrides + .gas_price + .unwrap_or(self.get_gas_price().await?.as_u64()), + ..Default::default() + }; + + let request = RpcRequest { + id: RpcRequestId::Number(1), + jsonrpc: "2.0".to_string(), + method: "eth_call".to_string(), + params: Some(vec![ + json!({ + "to": match tx.to { + TxKind::Call(addr) => format!("{addr:#x}"), + TxKind::Create => format!("{:#x}", Address::zero()), + }, + "input": format!("{:#x}", tx.input), + "value": format!("{}", tx.value), + "from": format!("{:#x}", tx.from), + }), + json!("latest"), + ]), + }; + + match self.send_request(request).await { + Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) + .map_err(CallError::SerdeJSONError) + .map_err(EthClientError::from), + Ok(RpcResponse::Error(error_response)) => { + Err(CallError::RPCError(error_response.error.message).into()) + } + Err(error) => Err(error), + } + } + + pub async fn send( + &self, + calldata: Bytes, + from: Address, + to: TxKind, + sender_private_key: SecretKey, + overrides: Overrides, + ) -> Result { + let mut tx = self + .make_eip1559_transaction(to, from, calldata, overrides) + .await?; + + self.send_eip1559_transaction(&mut tx, sender_private_key) + .await + } + + pub async fn deploy( + &self, + deployer: Address, + deployer_private_key: SecretKey, + init_code: Bytes, + overrides: Overrides, + ) -> Result<(H256, Address), EthClientError> { + let deploy_tx_hash = self + .send( + init_code, + deployer, + TxKind::Create, + deployer_private_key, + overrides, + ) + .await?; + + let encoded_from = deployer.encode_to_vec(); + // FIXME: We'll probably need to use nonce - 1 since it was updated above. + let encoded_nonce = self.get_nonce(deployer).await.unwrap().encode_to_vec(); + let mut encoded = vec![(0xc0 + encoded_from.len() + encoded_nonce.len()) as u8]; + encoded.extend(encoded_from.clone()); + encoded.extend(encoded_nonce.clone()); + let deployed_address = Address::from(keccak(encoded)); + + Ok((deploy_tx_hash, deployed_address)) + } + + async fn make_eip1559_transaction( + &self, + to: TxKind, + from: Address, + data: Bytes, + overrides: Overrides, + ) -> Result { + let mut tx = EIP1559Transaction { + to, + data, + value: overrides.value.unwrap_or_default(), + chain_id: overrides + .chain_id + .unwrap_or(self.get_chain_id().await?.as_u64()), + nonce: overrides.nonce.unwrap_or(self.get_nonce(from).await?), + max_fee_per_gas: overrides + .gas_price + .unwrap_or(self.get_gas_price().await?.as_u64()), + max_priority_fee_per_gas: overrides.priority_gas_price.unwrap_or_default(), + ..Default::default() + }; + tx.gas_limit = overrides + .gas_limit + .unwrap_or(self.estimate_gas(tx.clone()).await?); + + Ok(tx) + } +} diff --git a/crates/l2/utils/eth_client/mod.rs b/crates/l2/utils/eth_client/mod.rs index c65eca36e..6c1a9dba6 100644 --- a/crates/l2/utils/eth_client/mod.rs +++ b/crates/l2/utils/eth_client/mod.rs @@ -1,10 +1,9 @@ use crate::utils::config::eth::EthConfig; use errors::{ - CallError, EstimateGasPriceError, EthClientError, GetBalanceError, GetBlockNumberError, - GetGasPriceError, GetLogsError, GetNonceError, GetTransactionReceiptError, - SendRawTransactionError, + EstimateGasPriceError, EthClientError, GetBalanceError, GetBlockNumberError, GetGasPriceError, + GetLogsError, GetNonceError, GetTransactionReceiptError, SendRawTransactionError, }; -use ethereum_rust_core::types::{EIP1559Transaction, GenericTransaction, TxKind}; +use ethereum_rust_core::types::{EIP1559Transaction, TxKind}; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_rust_rpc::{ types::receipt::{RpcLog, RpcReceipt}, @@ -19,6 +18,7 @@ use serde_json::json; use transaction::PayloadRLPEncode; pub mod errors; +pub mod eth_sender; pub mod transaction; #[derive(Deserialize, Debug)] @@ -85,7 +85,7 @@ impl EthClient { pub async fn send_eip1559_transaction( &self, - mut tx: EIP1559Transaction, + tx: &mut EIP1559Transaction, private_key: SecretKey, ) -> Result { let mut payload = vec![EIP1559_TX_TYPE]; @@ -107,36 +107,6 @@ impl EthClient { self.send_raw_transaction(data.as_slice()).await } - pub async fn call(&self, transaction: GenericTransaction) -> Result { - let request = RpcRequest { - id: RpcRequestId::Number(1), - jsonrpc: "2.0".to_string(), - method: "eth_call".to_string(), - params: Some(vec![ - json!({ - "to": match transaction.to { - TxKind::Call(addr) => format!("{addr:#x}"), - TxKind::Create => format!("{:#x}", Address::zero()), - }, - "input": format!("{:#x}", transaction.input), - "value": format!("{}", transaction.value), - "from": format!("{:#x}", transaction.from), - }), - json!("latest"), - ]), - }; - - match self.send_request(request).await { - Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) - .map_err(CallError::SerdeJSONError) - .map_err(EthClientError::from), - Ok(RpcResponse::Error(error_response)) => { - Err(CallError::RPCError(error_response.error.message).into()) - } - Err(error) => Err(error), - } - } - pub async fn estimate_gas( &self, transaction: EIP1559Transaction, From e28af11ebe023156363e5ccbb872663d007d29ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:19:22 -0300 Subject: [PATCH 08/49] chore(core): add CI flow to run when changes are made to docs/readme (#970) **Motivation** **Description** --- .github/workflows/ci.yaml | 2 +- .github/workflows/ci_skipped.yaml | 33 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci_skipped.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c643500e5..2b93158c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: ["*"] + branches: ["**"] paths-ignore: - 'README.md' - 'LICENSE' diff --git a/.github/workflows/ci_skipped.yaml b/.github/workflows/ci_skipped.yaml new file mode 100644 index 000000000..5a53ab99c --- /dev/null +++ b/.github/workflows/ci_skipped.yaml @@ -0,0 +1,33 @@ +name: CI Skipped +on: + pull_request: + branches: ["**"] + paths: + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + if: false + steps: [run: true] + + test: + name: Test + runs-on: ubuntu-latest + if: false + steps: [run: true] + + docker-build: + name: Build Docker image + runs-on: ubuntu-latest + if: false + steps: [run: true] + + prover: + name: Build RISC-V zkVM program + runs-on: ubuntu-latest + if: false + steps: [run: true] From 709200013f0916a6253241f2e663b4d3292814a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:25:41 -0300 Subject: [PATCH 09/49] docs(core): readme reorg (#966) **Motivation** **Description** --------- Co-authored-by: Martin Paulucci --- README.md | 64 ++++++++++++++++++++--------------------- crates/l2/README.md | 70 +++++++++++++++++++++++++++------------------ 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index ab050d927..5ccde1b76 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Lambda Ethereum Rust Execution Client +# Lambda Ethereum Rust [![Telegram Chat][tg-badge]][tg-url] [![license](https://img.shields.io/github/license/lambdaclass/ethereum_rust)](/LICENSE) @@ -6,26 +6,14 @@ [tg-badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Frust_ethereum%2F&logo=telegram&label=chat&color=neon [tg-url]: https://t.me/rust_ethereum -## 📚 References and acknowledgements +# L1 and L2 support -The following links, repos, companies and projects have been important in the development of this repo, we have learned a lot from them and want to thank and acknowledge them. +This client supports running in two different modes: -- [Ethereum](https://ethereum.org/en/) -- [ZKsync](https://zksync.io/) -- [Starkware](https://starkware.co/) -- [Polygon](https://polygon.technology/) -- [Optimism](https://www.optimism.io/) -- [Arbitrum](https://arbitrum.io/) -- [Geth](https://github.com/ethereum/go-ethereum) -- [Taiko](https://taiko.xyz/) -- [RISC Zero](https://risczero.com/) -- [SP1](https://github.com/succinctlabs/sp1) -- [Aleo](https://aleo.org/) -- [Neptune](https://neptune.cash/) -- [Mina](https://minaprotocol.com/) -- [Nethermind](https://www.nethermind.io/) +- As a regular Ethereum execution client, like [Geth](https://github.com/ethereum/go-ethereum). +- As a ZK-Rollup, where block execution is proven and the proof sent to an L1 network for verification, thus inheriting the L1's security. -If we forgot to include anyone, please file an issue so we can add you. We always strive to reference the inspirations and code we use, but as an organization with multiple people, mistakes can happen, and someone might forget to include a reference. +We call the first one Lambda Ethereum Rust L1 and the second one Lambda Ethereum Rust L2. ## Philosophy @@ -45,15 +33,6 @@ Read more about our engineering philosophy [here](https://blog.lambdaclass.com/l - Prioritize code readability and maintainability over premature optimizations. - Avoid concurrency split all over the codebase. Concurrency adds complexity. Only use where strictly necessary. -# L1 and L2 support - -This client supports running in two different modes: - -- As a regular Ethereum execution client, like `geth`. -- As a ZK-Rollup, where block execution is proven and the proof sent to an L1 network for verification, thus inheriting the L1's security. - -We call the first one Lambda Ethereum Rust L1 and the second one Lambda Ethereum Rust L2. - # Lambda Ethereum Rust L2 [Full Roadmap](./crates/l2/README.md) @@ -67,17 +46,14 @@ The main differences between this mode and regular Ethereum Rust are: [](https://github.com/user-attachments/assets/99c96831-a641-4978-b70c-4acb519d0098) - - # Lambda Ethereum Rust L1 ### Table of Contents -- [Lambda Ethereum Rust Execution Client](#lambda-ethereum-rust-execution-client) - - [📚 References and acknowledgements](#-references-and-acknowledgements) +- [Lambda Ethereum Rust](#lambda-ethereum-rust) +- [L1 and L2 support](#l1-and-l2-support) - [Philosophy](#philosophy) - [Design Principles](#design-principles) -- [L1 and L2 support](#l1-and-l2-support) - [Lambda Ethereum Rust L2](#lambda-ethereum-rust-l2) - [Lambda Ethereum Rust L1](#lambda-ethereum-rust-l1) - [Table of Contents](#table-of-contents) @@ -97,9 +73,12 @@ The main differences between this mode and regular Ethereum Rust are: - [Ethereum Foundation Tests](#ethereum-foundation-tests) - [Crate Specific Tests](#crate-specific-tests) - [Hive Tests](#hive-tests) + - [Prereqs](#prereqs) + - [Running Simulations](#running-simulations) - [Run](#run) - [CLI Commands](#cli-commands) - [Crates documentation](#crates-documentation) + - [📚 References and acknowledgements](#-references-and-acknowledgements) ## Roadmap @@ -335,3 +314,24 @@ Documentation for each crate can be found on the following links (still a work i - [Networking](./crates/net/README.md) - [L2](./crates/l2/README.md) + +## 📚 References and acknowledgements + +The following links, repos, companies and projects have been important in the development of this repo, we have learned a lot from them and want to thank and acknowledge them. + +- [Ethereum](https://ethereum.org/en/) +- [ZKsync](https://zksync.io/) +- [Starkware](https://starkware.co/) +- [Polygon](https://polygon.technology/) +- [Optimism](https://www.optimism.io/) +- [Arbitrum](https://arbitrum.io/) +- [Geth](https://github.com/ethereum/go-ethereum) +- [Taiko](https://taiko.xyz/) +- [RISC Zero](https://risczero.com/) +- [SP1](https://github.com/succinctlabs/sp1) +- [Aleo](https://aleo.org/) +- [Neptune](https://neptune.cash/) +- [Mina](https://minaprotocol.com/) +- [Nethermind](https://www.nethermind.io/) + +If we forgot to include anyone, please file an issue so we can add you. We always strive to reference the inspirations and code we use, but as an organization with multiple people, mistakes can happen, and someone might forget to include a reference. diff --git a/crates/l2/README.md b/crates/l2/README.md index 834c7b3e6..b04c76089 100644 --- a/crates/l2/README.md +++ b/crates/l2/README.md @@ -15,14 +15,15 @@ - [Status](#status-3) - [Milestone 4: Custom Native token](#milestone-4-custom-native-token) - [Status](#status-4) - - [Milestone 5: Based Contestable Rollup](#milestone-5-based-contestable-rollup) + - [Milestone 5: Security (TEEs and Multi Prover support)](#milestone-5-security-tees-and-multi-prover-support) - [Status](#status-5) - - [Milestone 6: Security (TEEs and Multi Prover support)](#milestone-6-security-tees-and-multi-prover-support) + - [Milestone 6: Account Abstraction](#milestone-6-account-abstraction) - [Status](#status-6) - - [Milestone 7: Account Abstraction](#milestone-7-account-abstraction) + - [Milestone 7: Based Contestable Rollup](#milestone-7-based-contestable-rollup) - [Status](#status-7) - [Milestone 8: Validium](#milestone-8-validium) - [Status](#status-8) + - [Prerequisites](#prerequisites) - [How to run](#how-to-run) - [Initialize the network](#initialize-the-network) - [Restarting the network](#restarting-the-network) @@ -34,15 +35,15 @@ | Milestone | Description | Status | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | -| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | -| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | -| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | -| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | -| 5 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | -| 6 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | -| 7 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | -| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | +| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | +| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | +| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | +| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | +| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | +| 5 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | +| 6 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | +| 7 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | +| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | ### Milestone 0 @@ -125,19 +126,7 @@ The L2 can also be deployed using a custom native token, meaning that a certain | | `deposit` | Handle native token deposits | ❌ | | | `withdraw` | Handle native token withdrawals | ❌ | -### Milestone 5: Based Contestable Rollup - -The network can be run as a Based Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. - -#### Status - -| | Name | Description | Status | -| --- | ----------------- | ------------------------------------------------------------------------------ | ------ | -| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | - -TODO: Expand on this. - -### Milestone 6: Security (TEEs and Multi Prover support) +### Milestone 5: Security (TEEs and Multi Prover support) The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. @@ -149,7 +138,7 @@ The L2 has added security mechanisms in place, running on Trusted Execution Envi | Contracts | | Support verifying multiple different zkVM executions | ❌ | | VM | | Support running the operator on a TEE environment | ❌ | -### Milestone 7: Account Abstraction +### Milestone 6: Account Abstraction The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. @@ -160,6 +149,18 @@ The L2 supports native account abstraction following EIP 7702, allowing for cust TODO: Expand on account abstraction tasks. +### Milestone 7: Based Contestable Rollup + +The network can be run as a Based Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. + +#### Status + +| | Name | Description | Status | +| --- | ----------------- | ------------------------------------------------------------------------------ | ------ | +| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | + +TODO: Expand on this. + ### Milestone 8: Validium The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. @@ -221,8 +222,21 @@ Most of them are [here](https://github.com/ethpandaops/ethereum-package/blob/mai ## 📚 References and acknowledgements -The following links, repos, companies and projects have been important in the development of this library and we want to thank and acknowledge them. +The following links, repos, companies and projects have been important in the development of this repo, we have learned a lot from them and want to thank and acknowledge them. -- [Matter Labs](https://matter-labs.io/) +- [Ethereum](https://ethereum.org/en/) +- [ZKsync](https://zksync.io/) +- [Starkware](https://starkware.co/) +- [Polygon](https://polygon.technology/) - [Optimism](https://www.optimism.io/) +- [Arbitrum](https://arbitrum.io/) +- [Geth](https://github.com/ethereum/go-ethereum) +- [Taiko](https://taiko.xyz/) +- [RISC Zero](https://risczero.com/) - [SP1](https://github.com/succinctlabs/sp1) +- [Aleo](https://aleo.org/) +- [Neptune](https://neptune.cash/) +- [Mina](https://minaprotocol.com/) +- [Nethermind](https://www.nethermind.io/) + +If we forgot to include anyone, please file an issue so we can add you. We always strive to reference the inspirations and code we use, but as an organization with multiple people, mistakes can happen, and someone might forget to include a reference. From e11289b215b340cc4729a831e628c1c266549466 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:00:08 -0300 Subject: [PATCH 10/49] fix(l2): deployer script fixes (#976) **Motivation** **Description** Closes #issue_number --- crates/l2/{.example.env => .env.example} | 4 ++-- crates/l2/contracts/deployer.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename crates/l2/{.example.env => .env.example} (86%) diff --git a/crates/l2/.example.env b/crates/l2/.env.example similarity index 86% rename from crates/l2/.example.env rename to crates/l2/.env.example index 9979379cb..1400c08f1 100644 --- a/crates/l2/.example.env +++ b/crates/l2/.env.example @@ -1,7 +1,7 @@ ETH_RPC_URL=http://localhost:8545 DEPLOYER_ADDRESS=0x3d1e15a1a55578f7c920884a9943b3b35d0d885b DEPLOYER_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 -L1_WATCHER_BRIDGE_ADDRESS=0x90cD151b6e9500F13240dF68673f3B81245D57d4 +L1_WATCHER_BRIDGE_ADDRESS=0xca829334b66d86ea2c35ca4c4fa94e99851ef587 L1_WATCHER_TOPICS=0x6f65d68a35457dd88c1f8641be5da191aa122bc76de22ab0789dcc71929d7d37 L1_WATCHER_CHECK_INTERVAL_MS=5000 L1_WATCHER_MAX_BLOCK_STEP=5000 @@ -11,7 +11,7 @@ ENGINE_API_JWT_PATH=./jwt.hex PROVER_SERVER_LISTEN_IP=127.0.0.1 PROVER_SERVER_LISTEN_PORT=3000 PROVER_CLIENT_PROVER_SERVER_ENDPOINT=localhost:3000 -PROPOSER_ON_CHAIN_PROPOSER_ADDRESS=0xF04a082b0f773cA74B61278d9dBaaB5Ed5273DB5 +PROPOSER_ON_CHAIN_PROPOSER_ADDRESS=0xe9927d77c931f8648da4cc6751ef4e5e2ce74608 PROPOSER_L1_ADDRESS=0x3d1e15a1a55578f7c920884a9943b3b35d0d885b PROPOSER_L1_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 PROPOSER_INTERVAL_MS=5000 diff --git a/crates/l2/contracts/deployer.rs b/crates/l2/contracts/deployer.rs index 2687c3ab0..875c39db1 100644 --- a/crates/l2/contracts/deployer.rs +++ b/crates/l2/contracts/deployer.rs @@ -38,8 +38,8 @@ async fn main() { ) .expect("Malformed DEPLOYER_PRIVATE_KEY (SecretKey::parse)"); - if std::fs::exists("solc_out").expect("Could not determine if solc_out exists") { - std::fs::remove_dir_all("solc_out").expect("Failed to remove solc_out"); + if std::fs::exists("contracts/solc_out").expect("Could not determine if solc_out exists") { + std::fs::remove_dir_all("contracts/solc_out").expect("Failed to remove solc_out"); } let overrides = Overrides { From 3bca636a7c9298bb0736c472b7c9ddd2a681fdba Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Fri, 25 Oct 2024 11:38:25 -0300 Subject: [PATCH 11/49] feat(l1): enhance el assertoor ci test by cherrypicking more checks (#963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivation** This PR tries to add as much test as possible to our assertoor CI run. **Description** The idea is to add every check that is useful for validating the el execution without incurring in long running times to avoid making the CI slower. Right now every useful check from the [supported](https://github.com/ethpandaops/assertoor/wiki#supported-tasks-in-assertoor) is added except for: - ✅ `check_consensus_finality` | time: ~10m - ✅ `check_consensus_attestation_stats` | time: ~10m Apart from checks, we are not using most of the `generate_*` tasks due to timing or errors: - ✅ `run_transaction_test` | time: ~10m - ✅ `run_blob_transaction_test` | time: ~6m - ❎ `run_opcodes_transaction_test` | time: ~5m (We have issues processing some of the transactions) For the already available assertor playbooks, [see here](https://github.com/ethpandaops/assertoor-test/tree/master/assertoor-tests). The `run_blob_transaction_test` is the only one added until now from transaction checks and the total test take nearly 15m (which is less than the largest hive test in the CI). Closes #953 --- test_data/el-stability-check.yml | 43 +++++++++++++++++++++++--------- test_data/network_params.yaml | 12 +++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/test_data/el-stability-check.yml b/test_data/el-stability-check.yml index 5760091b3..6b4a38a64 100644 --- a/test_data/el-stability-check.yml +++ b/test_data/el-stability-check.yml @@ -1,28 +1,47 @@ -# This file is based upon `assertoor-tests` stability check file: -# https://github.com/ethpandaops/assertoor-test/blob/master/assertoor-tests/stability-check.yaml +# This file is mix and match based upon the basic `assertoor-tests` playbooks: +# - https://github.com/ethpandaops/assertoor-test/blob/master/assertoor-tests/stability-check.yaml +# - https://github.com/ethpandaops/assertoor-test/blob/master/assertoor-tests/block-proposal-check.yaml # -# We removed the consensus checks to keep it minimal. The checks removed were: -# - check_consensus_finality -# - check_consensus_attestation_stats -# - check_consensus_reorgs -# - check_consensus_forks +# For reference on each individual check see: https://github.com/ethpandaops/assertoor/wiki#supported-tasks-in-assertoor id: el-stability-check name: "Check Execution Stability" timeout: 2h tasks: - name: check_clients_are_healthy - title: "Check if at least one client is ready" - timeout: 5m - config: - minClientCount: 1 + title: "Check if all clients are ready" + timeout: 1m - name: run_tasks_concurrent title: "Check if all EL & CL clients are synced" - timeout: 30m + timeout: 5m config: tasks: - name: check_consensus_sync_status title: "Check if CL clients are synced" - name: check_execution_sync_status title: "Check if EL clients are synced" + +- name: run_task_matrix + title: "Check block proposals from all client pairs" + timeout: 2m + configVars: + matrixValues: "validatorPairNames" + config: + runConcurrent: true + matrixVar: "validatorPairName" + task: + name: check_consensus_block_proposals + title: "Wait for block proposal from ${validatorPairName}" + configVars: + validatorNamePattern: "validatorPairName" + +- name: run_tasks_concurrent + title: "Check chain stability (reorgs and forks)" + timeout: 7m + config: + tasks: + - name: check_consensus_reorgs + title: "Check consensus reorgs" + - name: check_consensus_forks + title: "Check consensus forks" diff --git a/test_data/network_params.yaml b/test_data/network_params.yaml index 443e7082f..b20776e53 100644 --- a/test_data/network_params.yaml +++ b/test_data/network_params.yaml @@ -1,21 +1,23 @@ participants: - el_type: geth cl_type: lighthouse - count: 2 + validator_count: 32 + - el_type: geth + cl_type: prysm + validator_count: 32 - el_type: ethereumrust cl_type: lighthouse - count: 1 + validator_count: 32 additional_services: - assertoor - dora - el_forkmon - tx_spammer - - blob_spammer assertoor_params: run_stability_check: false run_block_proposal_check: false + run_blob_transaction_test: true tests: - - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_rust/refs/heads/assertoor-run-on-cd/test_data/el-stability-check.yml' - + - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_rust/refs/heads/main/test_data/el-stability-check.yml' \ No newline at end of file From 8bd7afc94702ac38a482a131ed01ddaf5a52e1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:34:14 -0300 Subject: [PATCH 12/49] docs(l2): add state diffs and withdrawal docs (#945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivation** **Description** --------- Co-authored-by: Manuel Iñaki Bilbao Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> --- crates/l2/docs/overview.md | 14 +++++--- crates/l2/docs/state_diffs.md | 62 +++++++++++++++++++++++++++++++++++ crates/l2/docs/withdrawals.md | 24 ++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 crates/l2/docs/state_diffs.md create mode 100644 crates/l2/docs/withdrawals.md diff --git a/crates/l2/docs/overview.md b/crates/l2/docs/overview.md index a68bae858..49d045075 100644 --- a/crates/l2/docs/overview.md +++ b/crates/l2/docs/overview.md @@ -61,7 +61,7 @@ Going a bit further, instead of posting the entire transaction, we could just po This is called `state diffs`. Instead of publishing entire transactions for data availability, we only publish whatever state they modified. This is enough for anyone to reconstruct the entire state of the network. -TODO: Decide on a state diff architecture and document it in detail here. +Detailed documentation on the state diffs spec [here](./state_diffs.md). ### How do we prevent the sequencer from publishing the wrong state diffs? @@ -75,8 +75,6 @@ With that, we can be sure that state diffs are published and that they are corre Because state diffs are compressed to save space on L1, this compression needs to be proven as well. Otherwise, once again, the sequencer could send the wrong (compressed) state diffs. This is easy though, we just make the prover run the compression and we're done. -TODO: Decide on a compression algorithm and document it. - ## EIP 4844 (a.k.a. Blobs) While we could send state diffs through calldata, there is a (hopefully) cheaper way to do it: blobs. The Ethereum Cancun upgrade introduced a new type of transaction where users can submit a list of opaque blobs of data, each one of size at most 128 KB. The main purpose of this new type of transaction is precisely to be used by rollups for data availability; they are priced separately through a `blob_gas` market instead of the regular `gas` one and for all intents and purposes should be much cheaper than calldata. @@ -96,7 +94,15 @@ TODO: Explain this in more detail, show with an example what the polynomials P a ## How do deposits and withdrawals work? -TODO: Explain Withdrawal Logs merkle tree among other things. +### Deposits + +TODO + +### Withdrawals + +Detailed specs [here](./withdrawals.md). + +TODO: Explain it a high level maybe? ## Recap diff --git a/crates/l2/docs/state_diffs.md b/crates/l2/docs/state_diffs.md new file mode 100644 index 000000000..44aa73b86 --- /dev/null +++ b/crates/l2/docs/state_diffs.md @@ -0,0 +1,62 @@ +# State diffs + +This architecture was inspired by MatterLabs' ZKsync pubdata architecture (see [here](https://github.com/matter-labs/zksync-era/blob/main/docs/specs/data_availability/pubdata.md)). + +To provide data availability for our network, we need to publish enough information on every commit transaction to be able to reconstruct the entire state of the L2 from the beginning by querying the L1. + +The data needed is: + +- The nonce and balance of every `EOA`. +- The nonce, balance, and storage of every contract account. Note that storage here is a mapping `(U256 → U256)`, so there are a lot of values inside it. +- The bytecode of every contract deployed on the network. +- All withdrawal Logs. + +After executing each L2 block, the EVM will return the following data: + +- A list of every storage slot modified in the block, with their previous and next values. A storage slot is a mapping `(address, slot) -> value`. Note that, on a block, there could be repeated writes to the same slot. In that case, we keep only the latest write; all the others are discarded since they are not needed for state reconstruction. +- The bytecode of every newly deployed contract. Every contract deployed is then a pair `(address, bytecode)`. +- A list of withdrawal logs (as explained in milestone 1 we already collect these and publish a merkle root of their values as calldata, but we still need to send them as the state diff). +- A list of triples `(address, nonce_increase, balance)` for every modified account. The `nonce_increase` is a value that says by how much the nonce of the account was increased on the block (this could be more than one as there can be multiple transactions for the account on the block). The balance is just the new balance value for the account. + +The full state diff sent on every block will then be a sequence of bytes encoded as follows. We use the notation `un` for a sequence of `n` bits, so `u16` is a 16-bit sequence and `u96` a 96-bit one, we don’t really care about signedness here; if we don’t specify it, the value is of variable length and a field before it specifies it. + +- The first byte is a `u8`: the version header. For now it should always be zero, but we reserve it for future changes to the encoding/compression format. +- Next come the `ModifiedAccounts` list. The first two bytes (`u16`) are the amount of element it has, followed by its entries. Each entry correspond to an altered address and has the form: + - The first byte is the `type` of the modification. The value is a `u8`, constrained to the range `[1; 23]`, computed by adding the following values: + - `1` if the balance of the EOA/contract was modified. + - `2` if the nonce of the EOA/contract was modified. + - `4` if the storage of the contract was modified. + - `8` if the contract was created and the bytecode is previously unknown. + - `16` if the contract was created and the bytecode is previously known. + - The next 20 bytes, a `u160`, is the address of the modified account. + - If the balance was modified (i.e. `type & 0x01 == 1`), the next 32 bytes, a `u256`, is the new balance of the account. + - If the nonce was modified (i.e. `type & 0x02 == 2`), the next 2 bytes, a `u16`, is the increase in the nonce. + - If the storage was modified (i.e. `type & 0x04 == 4`), the next 2 bytes, a `u16`, is the number of storage slots modified. Then come the sequence of `(key_u256, new_value_u256)` key value pairs with the modified slots. + - If the contract was created and the bytecode is previously unknown (i.e. `type & 0x08 == 8`), the next 2 bytes, a `u16`, is the length of the bytecode in bytes. Then come the bytecode itself. + - If the contract was created and the bytecode is previously known (i.e. `type & 0x10 == 16`), the next 20 bytes, a `u160`, is the hash of the bytecode of the contract. + - Note that values `8` and `16` are mutually exclusive, and if `type` is greater or equal to `4`, then the address is a contract. Each address can only appear once in the list. +- Next the `WithdrawalLogs` field: + - First two bytes are the number of entries, then come the tuples `(to_u160, amount_u256, tx_hash_u256)`. +- In case of the only changes on an account are produced by withdrawals, the `ModifiedAccounts` for that address field must be omitted. In this case, the state diff can be computed by incrementing the nonce in one unit and subtracting the amount from the balance. + +To recap, using `||` for byte concatenation and `[]` for optional parameters, the full encoding for state diffs is: + +```jsx +version_header_u8 || +// Modified Accounts +number_of_modified_accounts_u16 || +( + type_u8 || address_u20 || [balance_u256] || [nonce_increase_u16] || + [number_of_modified_storage_slots_u16 || (key_u256 || value_u256)... ] || + [bytecode_len_u16 || bytecode ...] || + [code_hash_u256] +)... +// Withdraw Logs +number_of_withdraw_logs_u16 || +(to_u160 || amount_u256 || tx_hash_u256) ... +``` + +The sequencer will then make a commitment to this encoded state diff (explained in the EIP 4844 section how this is done) and send on the `commit` transaction: + +- Through calldata, the state diff commitment (which is part of the public input to the proof). +- Through the blob, the encoded state diff. diff --git a/crates/l2/docs/withdrawals.md b/crates/l2/docs/withdrawals.md new file mode 100644 index 000000000..e2f563265 --- /dev/null +++ b/crates/l2/docs/withdrawals.md @@ -0,0 +1,24 @@ +# Withdrawal specs + +This document contains a detailed explanation of the changes needed to handled withdrawals and the withdrawal flow. + +A new `Withdraw` type of transaction on L2 is introduced, where users send a certain amount of `eth` (or the native token in a custom token setup) with it. This money is then burned on the L2 and the operator sends a `WithdrawLog` to L1, so the user can then send a transaction to claim the withdrawal associated to that log and receive their funds from the Common bridge. + +In more detail, the full changes/additions are: + +- A `Withdraw` transaction type is introduced, comprised of the regular fields in an EIP-1559 transaction. +- On every block, each `Withdraw` transaction will burn (i.e. deduct from the sender) the value attached to it. +- After executing the block, the sequencer will collect all `Withdraw` transactions, will generate a `WithdrawLog` for each, will build a merkle tree from them and calculate the corresponding root, which we call `WithdrawLogsRoot`. The `WithdrawLog` contains the following fields: + - `to`: the address in L1 that is allowed to claim the funds (this is decided by the user as part of a withdraw transaction. This comes from the regular `to` field on the Withdraw transaction (i.e. we are reusing that field with a slightly different meaning; what it means here is “the address that can claim the funds on L1”). + - `amount`: the amount of money withdrawn (i.e. the `msg.value` of the transaction). + - `tx_hash`: the transaction hash in the L2 block it was included in. This will be important for claiming the withdrawal as it will require a merkle proof to be provided along with the index on the tree. +- As part of the L1 `commit` transaction, the sequencer will send the list of all `WithdrawLog`s on the EIP 4844 blob (i.e. as a section of the state diffs) and the `WithdrawLogsRoot` as calldata as part of the public input to the proof. The contract will then: + - Verify that the withdraw logs passed on the blob are the correct ones (this is done as part of the proof of equivalence protocol explained below). + - Store the `WithdrawLogsRoot` on a mapping `(blockNumber -> LogsRoot)` +- For users to complete their withdraw process and receive funds on the L1, they need to call a `claimWithdraw(withdrawLog, merkleProof, blockNumber)` function on the common bridge, where `merkleProof` is an inclusion proof of the withdraw log to the root of the merkle tree the contract has stored. The contract will then do the following: + - Check that the `blockNumber` corresponds to a committed and verified block. + - Check that this withdrawal has not been already claimed. + - Retrieve the `withdrawLogsRoot` from the given `blockNumber`. + - Verify the merkle proof given by the user, passing the proof, the root, and the `tx_hash`. + - If any check above failed, revert. If all checks passed, send the appropriate funds to the user, then set the `withdrawLog` as claimed. + - After the withdrawal is sent, we mark it as claimed so it cannot be claimed twice. From 7d694520a4185c96b42c8682636b16586cdadb9f Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:29:31 -0300 Subject: [PATCH 13/49] feat(l2): add `l2` crate to workspace members (#985) **Motivation** To run the tests and the linter workflows on the `l2` crate. --- Cargo.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4d98d829f..6325f8e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,17 @@ members = [ "cmd/ethereum_rust_l2", "crates/vm/levm", "crates/vm/levm/bench/revm_comparison", + "crates/l2/", "crates/l2/prover", ] resolver = "2" exclude = ["crates/l2/contracts"] -default-members = ["cmd/ethereum_rust", "cmd/ethereum_rust_l2", "crates/l2/prover"] +default-members = [ + "cmd/ethereum_rust", + "cmd/ethereum_rust_l2", + "crates/l2/prover", +] [workspace.package] version = "0.1.0" From 75f7c856eed85d394df11c8f2c62c4abbee8adc2 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:19:26 -0300 Subject: [PATCH 14/49] fix(l2): `help` target default when doing `make` (#983) **Motivation** We want the default target to be `help` instead of `init`. **Description** Set `help` as default target in Makefile --- crates/l2/Makefile | 2 +- crates/l2/README.md | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/l2/Makefile b/crates/l2/Makefile index d0af141db..6aad8e17d 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -1,4 +1,4 @@ -.DEFAULT_GOAL := init +.DEFAULT_GOAL := help .PHONY: help init down clean init-local-l1 down-local-l1 clean-local-l1 init-l2 down-l2 deploy-l1 deploy-block-executor deploy-inbox setup-prover diff --git a/crates/l2/README.md b/crates/l2/README.md index b04c76089..7a51a76de 100644 --- a/crates/l2/README.md +++ b/crates/l2/README.md @@ -35,15 +35,15 @@ | Milestone | Description | Status | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | -| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | -| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | -| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | -| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | -| 5 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | -| 6 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | -| 7 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | -| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | +| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | +| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | +| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | +| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | +| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | +| 5 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | +| 6 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | +| 7 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | +| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | ### Milestone 0 @@ -157,7 +157,7 @@ The network can be run as a Based Rollup, meaning sequencing is done by the Ethe | | Name | Description | Status | | --- | ----------------- | ------------------------------------------------------------------------------ | ------ | -| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | +| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | TODO: Expand on this. @@ -190,7 +190,7 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer > 2. make sure you have created a `.env` file following the `.env.example` file. ``` -make +make init ``` This will setup a local Ethereum network as the L1, deploy all the needed contracts on it, then start an Ethereum Rust L2 node pointing to it. From ad9ff711005a8bdf1e8991e8c3532927d4423d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Fri, 25 Oct 2024 17:39:01 -0300 Subject: [PATCH 15/49] chore(l2): skip assertoor CI on l2-only changes (#988) **Motivation** Changes to crate `l2` shoudn't affect this checks, so we could avoid it **Description** Skip the Assertoor CI when changes are only on `crates/l2/*` --- .github/workflows/assertoor.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/assertoor.yaml b/.github/workflows/assertoor.yaml index bb5b8aa3e..0de3c36ed 100644 --- a/.github/workflows/assertoor.yaml +++ b/.github/workflows/assertoor.yaml @@ -6,6 +6,7 @@ on: pull_request: branches: [ '*' ] paths-ignore: + - "crates/l2/**" - 'README.md' - 'LICENSE' - "**/README.md" From 0e5d8a27a39bfa7c934fda980d571e7d7ebb9e6a Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:02:35 -0300 Subject: [PATCH 16/49] refactor(l2): `cli` target (#984) **Motivation** To use `make cli` to install the CLI instead of building it. --- crates/l2/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 6aad8e17d..2473884cb 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -15,8 +15,8 @@ clean: clean-contract-deps ## 🧹 Cleans the localnet restart: restart-local-l1 restart-contract-deps deploy-l1 restart-l2 ## 🔄 Restarts the localnet -cli: ## 🛠️ Builds the L2 Lambda Ethereum Rust CLI - cargo build --release --manifest-path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust/Cargo.toml +cli: ## 🛠️ Installs the L2 Lambda Ethereum Rust CLI + cargo install --path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust/Cargo.toml --force # Variables From 693da10c569d3713230c92daa9352c1d244658b1 Mon Sep 17 00:00:00 2001 From: Federico Carrone Date: Sun, 27 Oct 2024 14:50:31 -0300 Subject: [PATCH 17/49] Update README.md --- README.md | 247 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 226 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 5ccde1b76..a025a3542 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This client supports running in two different modes: -- As a regular Ethereum execution client, like [Geth](https://github.com/ethereum/go-ethereum). +- As a regular Ethereum execution client - As a ZK-Rollup, where block execution is proven and the proof sent to an L1 network for verification, thus inheriting the L1's security. We call the first one Lambda Ethereum Rust L1 and the second one Lambda Ethereum Rust L2. @@ -33,19 +33,6 @@ Read more about our engineering philosophy [here](https://blog.lambdaclass.com/l - Prioritize code readability and maintainability over premature optimizations. - Avoid concurrency split all over the codebase. Concurrency adds complexity. Only use where strictly necessary. -# Lambda Ethereum Rust L2 - -[Full Roadmap](./crates/l2/README.md) - -The main differences between this mode and regular Ethereum Rust are: - -- There is no consensus, only one sequencer proposes blocks for the network. -- Block execution is proven using a RISC-V zkVM and its proofs are sent to L1 for verification. -- A set of Solidity contracts to be deployed to the L1 are included as part of network initialization. -- Two new types of transactions are included: deposits (native token mints) and withdrawals. - -[](https://github.com/user-attachments/assets/99c96831-a641-4978-b70c-4acb519d0098) - # Lambda Ethereum Rust L1 ### Table of Contents @@ -77,8 +64,6 @@ The main differences between this mode and regular Ethereum Rust are: - [Running Simulations](#running-simulations) - [Run](#run) - [CLI Commands](#cli-commands) -- [Crates documentation](#crates-documentation) - - [📚 References and acknowledgements](#-references-and-acknowledgements) ## Roadmap @@ -242,7 +227,6 @@ For example: make test CRATE="ethereum_rust-blockchain" ``` - ##### Hive Tests Finally, we have End-to-End tests with hive. @@ -308,12 +292,233 @@ Ethereum Rust supports the following command line arguments: - `--bootnodes `: Comma separated enode URLs for P2P discovery bootstrap. - `--log.level `: The verbosity level used for logs. Default value: info. possible values: info, debug, trace, warn, error -# Crates documentation +# Lambda Ethereum Rust L2 + +The main differences between this mode and regular Ethereum Rust are: + +- There is no consensus, only one sequencer proposes blocks for the network. +- Block execution is proven using a RISC-V zkVM and its proofs are sent to L1 for verification. +- A set of Solidity contracts to be deployed to the L1 are included as part of network initialization. +- Two new types of transactions are included: deposits (native token mints) and withdrawals. + +### Table of Contents +- [Ethereum Rust L2](#lambda-ethereum-rust-l2) + - [Table of Contents](#table-of-contents) + - [Roadmap](#roadmap) + - [Milestone 0](#milestone-0) + - [Status](#status) + - [Milestone 1: MVP](#milestone-1-mvp) + - [Status](#status-1) + - [Milestone 2: Block Execution Proofs](#milestone-2-block-execution-proofs) + - [Status](#status-2) + - [Milestone 3: State diffs + Data compression + EIP 4844 (Blobs)](#milestone-3-state-diffs--data-compression--eip-4844-blobs) + - [Status](#status-3) + - [Milestone 4: Custom Native token](#milestone-4-custom-native-token) + - [Status](#status-4) + - [Milestone 5: Security (TEEs and Multi Prover support)](#milestone-5-security-tees-and-multi-prover-support) + - [Status](#status-5) + - [Milestone 6: Account Abstraction](#milestone-6-account-abstraction) + - [Status](#status-6) + - [Milestone 7: Based Contestable Rollup](#milestone-7-based-contestable-rollup) + - [Status](#status-7) + - [Milestone 8: Validium](#milestone-8-validium) + - [Status](#status-8) + - [Prerequisites](#prerequisites) + - [How to run](#how-to-run) + - [Initialize the network](#initialize-the-network) + - [Restarting the network](#restarting-the-network) + - [Local L1 Rich Wallets](#local-l1-rich-wallets) + - [Docs](#docs) + +## Roadmap + +| Milestone | Description | Status | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | +| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | +| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | +| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | +| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | +| 5 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | +| 6 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | +| 7 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | +| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | + +### Milestone 0 + +Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. + +#### Status + +| | Name | Description | Status | +| --------- | ----------------------------- | --------------------------------------------------------------------------- | ------ | +| Contracts | `CommonBridge` | Deposit method implementation | ✅ | +| | `OnChainOperator` | Commit and verify methods (placeholders for this stage) | ✅ | +| VM | | Adapt EVM to handle deposits | ✅ | +| Proposer | `Proposer` | Proposes new blocks to be executed | ✅ | +| | `L1Watcher` | Listens for and handles L1 deposits | ✅ | +| | `L1TxSender` | commits new block proposals and sends block execution proofs to be verified | ✅ | +| | Deposit transactions handling | new transaction type for minting funds corresponding to deposits | ✅ | +| CLI | `stack` | Support commands for initializing the network | ✅ | +| CLI | `config` | Support commands for network config management | ✅ | +| CLI | `wallet deposit` | Support command por depositing funds on L2 | ✅ | +| CLI | `wallet transfer` | Support command for transferring funds on L2 | ✅ | + +### Milestone 1: MVP + +The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). + +#### Status + +| | Name | Description | Status | +| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------ | +| Contracts | `CommonBridge` | Withdraw method implementation | ❌ | +| | `OnChainOperator` | Commit and verify implementation | 🏗️ | +| | `Verifier` | verifier | 🏗️ | +| | Withdraw transactions handling | New transaction type for burning funds on L2 and unlock funds on L1 | 🏗️ | +| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | + +### Milestone 2: Block Execution Proofs + +The L2's block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. This work is being done in parallel with other milestones as it doesn't block anything else. + +#### Status + +| | Name | Description | Status | +| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------ | +| VM | | `Return` the storage touched on block execution to pass the prover as a witness | 🏗️ | +| Contracts | `OnChainOperator` | Call the actual SNARK proof verification on the `verify` function implementation | 🏗️ | +| Proposer | `Prover Server` | Feeds the `Prover Client` with block data to be proven and delivers proofs to the `L1TxSender` for L1 verification | 🏗️ | +| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | + +### Milestone 3: State diffs + Data compression + EIP 4844 (Blobs) + +The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. + +It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. + +#### Status + +| | Name | Description | Status | +| --------- | ------------------- | --------------------------------------------------------------------------- | ------ | +| Contracts | OnChainOperator | Differentiate whether to execute in calldata or blobs mode | ❌ | +| Prover | RISC-V zkVM | Prove state diffs compression | ❌ | +| | RISC-V zkVM | Adapt state proofs | ❌ | +| VM | | The VM should return which storage slots were modified | ❌ | +| Proposer | Prover Server | Sends state diffs to the prover | ❌ | +| | L1TxSender | Differentiate whether to send the commit transaction with calldata or blobs | ❌ | +| | | Add program for proving blobs | ❌ | +| CLI | `reconstruct-state` | Add a command for reconstructing the state | ❌ | +| | `init` | Adapt network initialization to either send blobs or calldata | ❌ | + +### Milestone 4: Custom Native token + +The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. + +#### Status + +| | Name | Description | Status | +| --- | -------------- | ----------------------------------------------------------------------------------------- | ------ | +| | `CommonBridge` | For native token withdrawals, infer the native token and reimburse the user in that token | ❌ | +| | `CommonBridge` | For native token deposits, msg.value = 0 and valueToMintOnL2 > 0 | ❌ | +| | `CommonBridge` | Keep track of chain's native token | ❌ | +| | `deposit` | Handle native token deposits | ❌ | +| | `withdraw` | Handle native token withdrawals | ❌ | + +### Milestone 5: Security (TEEs and Multi Prover support) + +The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. + +#### Status + +| | Name | Description | Status | +| --------- | ---- | ---------------------------------------------------- | ------ | +| VM/Prover | | Support proving with multiple different zkVMs | ❌ | +| Contracts | | Support verifying multiple different zkVM executions | ❌ | +| VM | | Support running the operator on a TEE environment | ❌ | + +### Milestone 6: Account Abstraction + +The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. + +#### Status + +| | Name | Description | Status | +| --- | ---- | ----------- | ------ | + +TODO: Expand on account abstraction tasks. + +### Milestone 7: Based Contestable Rollup + +The network can be run as a Based Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. + +#### Status + +| | Name | Description | Status | +| --- | ----------------- | ------------------------------------------------------------------------------ | ------ | +| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | + +TODO: Expand on this. + +### Milestone 8: Validium + +The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. + +#### Status + +| | Name | Description | Status | +| --------- | ------------- | ---------------------------------------------------- | ------ | +| Contracts | BlockExecutor | Do not check data availability in Validium mode | ❌ | +| Proposer | L1TxSender | Do not send data in commit transactions | ❌ | +| CLI | `init` | Adapt network initialization to support Validium L2s | ❌ | +| Misc | | Add a DA integration example for Validium mode | ❌ | + +## Prerequisites + +- [Rust (explained in the repo's main README)](../../README.md) +- [Docker](https://docs.docker.com/engine/install/) (with [Docker Compose](https://docs.docker.com/compose/install/)) + +## How to run + +### Initialize the network + +> [!IMPORTANT] +> Before this step: +> +> 1. make sure the Docker daemon is running. +> 2. make sure you have created a `.env` file following the `.env.example` file. + +``` +make init +``` + +This will setup a local Ethereum network as the L1, deploy all the needed contracts on it, then start an Ethereum Rust L2 node pointing to it. + +### Restarting the network + +> [!WARNING] +> This command will cleanup your running L1 and L2 nodes. + +``` +make restart +``` + +## Local L1 Rich Wallets + +Most of them are [here](https://github.com/ethpandaops/ethereum-package/blob/main/src/prelaunch_data_generator/genesis_constants/genesis_constants.star), but there's an extra one: + +``` +{ + "address": "0x3d1e15a1a55578f7c920884a9943b3b35d0d885b", + "private_key": "0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924" +} +``` + +## Docs -Documentation for each crate can be found on the following links (still a work in progress, we will be adding more documentation as we go). +- [Ethereum Rust L2 Docs](./docs/README.md) +- [Ethereum Rust L2 CLI Docs](../../cmd/ethereum_rust_l2/README.md) -- [Networking](./crates/net/README.md) -- [L2](./crates/l2/README.md) ## 📚 References and acknowledgements From 31f22f7e87c356335beee73f8acaa75835f43ce7 Mon Sep 17 00:00:00 2001 From: Federico Carrone Date: Sun, 27 Oct 2024 14:51:59 -0300 Subject: [PATCH 18/49] Update README.md --- crates/l2/README.md | 241 -------------------------------------------- 1 file changed, 241 deletions(-) diff --git a/crates/l2/README.md b/crates/l2/README.md index 7a51a76de..41bb9b67d 100644 --- a/crates/l2/README.md +++ b/crates/l2/README.md @@ -1,242 +1 @@ # Ethereum Rust L2 - -## Table of Contents - -- [Ethereum Rust L2](#ethereum-rust-l2) - - [Table of Contents](#table-of-contents) - - [Roadmap](#roadmap) - - [Milestone 0](#milestone-0) - - [Status](#status) - - [Milestone 1: MVP](#milestone-1-mvp) - - [Status](#status-1) - - [Milestone 2: Block Execution Proofs](#milestone-2-block-execution-proofs) - - [Status](#status-2) - - [Milestone 3: State diffs + Data compression + EIP 4844 (Blobs)](#milestone-3-state-diffs--data-compression--eip-4844-blobs) - - [Status](#status-3) - - [Milestone 4: Custom Native token](#milestone-4-custom-native-token) - - [Status](#status-4) - - [Milestone 5: Security (TEEs and Multi Prover support)](#milestone-5-security-tees-and-multi-prover-support) - - [Status](#status-5) - - [Milestone 6: Account Abstraction](#milestone-6-account-abstraction) - - [Status](#status-6) - - [Milestone 7: Based Contestable Rollup](#milestone-7-based-contestable-rollup) - - [Status](#status-7) - - [Milestone 8: Validium](#milestone-8-validium) - - [Status](#status-8) - - [Prerequisites](#prerequisites) - - [How to run](#how-to-run) - - [Initialize the network](#initialize-the-network) - - [Restarting the network](#restarting-the-network) - - [Local L1 Rich Wallets](#local-l1-rich-wallets) - - [Docs](#docs) - - [📚 References and acknowledgements](#-references-and-acknowledgements) - -## Roadmap - -| Milestone | Description | Status | -| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | -| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | -| 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | -| 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | -| 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | -| 5 | The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. | ❌ | -| 6 | The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. | ❌ | -| 7 | The network can be run as a Based Contestable Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. | ❌ | -| 8 | The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. | ❌ | - -### Milestone 0 - -Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. - -#### Status - -| | Name | Description | Status | -| --------- | ----------------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | `CommonBridge` | Deposit method implementation | ✅ | -| | `OnChainOperator` | Commit and verify methods (placeholders for this stage) | ✅ | -| VM | | Adapt EVM to handle deposits | ✅ | -| Proposer | `Proposer` | Proposes new blocks to be executed | ✅ | -| | `L1Watcher` | Listens for and handles L1 deposits | ✅ | -| | `L1TxSender` | commits new block proposals and sends block execution proofs to be verified | ✅ | -| | Deposit transactions handling | new transaction type for minting funds corresponding to deposits | ✅ | -| CLI | `stack` | Support commands for initializing the network | ✅ | -| CLI | `config` | Support commands for network config management | ✅ | -| CLI | `wallet deposit` | Support command por depositing funds on L2 | ✅ | -| CLI | `wallet transfer` | Support command for transferring funds on L2 | ✅ | - -### Milestone 1: MVP - -The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). - -#### Status - -| | Name | Description | Status | -| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------ | -| Contracts | `CommonBridge` | Withdraw method implementation | ❌ | -| | `OnChainOperator` | Commit and verify implementation | 🏗️ | -| | `Verifier` | verifier | 🏗️ | -| | Withdraw transactions handling | New transaction type for burning funds on L2 and unlock funds on L1 | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | - -### Milestone 2: Block Execution Proofs - -The L2's block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. This work is being done in parallel with other milestones as it doesn't block anything else. - -#### Status - -| | Name | Description | Status | -| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------ | -| VM | | `Return` the storage touched on block execution to pass the prover as a witness | 🏗️ | -| Contracts | `OnChainOperator` | Call the actual SNARK proof verification on the `verify` function implementation | 🏗️ | -| Proposer | `Prover Server` | Feeds the `Prover Client` with block data to be proven and delivers proofs to the `L1TxSender` for L1 verification | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | - -### Milestone 3: State diffs + Data compression + EIP 4844 (Blobs) - -The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. - -It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. - -#### Status - -| | Name | Description | Status | -| --------- | ------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | OnChainOperator | Differentiate whether to execute in calldata or blobs mode | ❌ | -| Prover | RISC-V zkVM | Prove state diffs compression | ❌ | -| | RISC-V zkVM | Adapt state proofs | ❌ | -| VM | | The VM should return which storage slots were modified | ❌ | -| Proposer | Prover Server | Sends state diffs to the prover | ❌ | -| | L1TxSender | Differentiate whether to send the commit transaction with calldata or blobs | ❌ | -| | | Add program for proving blobs | ❌ | -| CLI | `reconstruct-state` | Add a command for reconstructing the state | ❌ | -| | `init` | Adapt network initialization to either send blobs or calldata | ❌ | - -### Milestone 4: Custom Native token - -The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. - -#### Status - -| | Name | Description | Status | -| --- | -------------- | ----------------------------------------------------------------------------------------- | ------ | -| | `CommonBridge` | For native token withdrawals, infer the native token and reimburse the user in that token | ❌ | -| | `CommonBridge` | For native token deposits, msg.value = 0 and valueToMintOnL2 > 0 | ❌ | -| | `CommonBridge` | Keep track of chain's native token | ❌ | -| | `deposit` | Handle native token deposits | ❌ | -| | `withdraw` | Handle native token withdrawals | ❌ | - -### Milestone 5: Security (TEEs and Multi Prover support) - -The L2 has added security mechanisms in place, running on Trusted Execution Environments and Multi Prover setup where multiple guarantees (Execution on TEEs, zkVMs/proving systems) are required for settlement on the L1. This better protects against possible security bugs on implementations. - -#### Status - -| | Name | Description | Status | -| --------- | ---- | ---------------------------------------------------- | ------ | -| VM/Prover | | Support proving with multiple different zkVMs | ❌ | -| Contracts | | Support verifying multiple different zkVM executions | ❌ | -| VM | | Support running the operator on a TEE environment | ❌ | - -### Milestone 6: Account Abstraction - -The L2 supports native account abstraction following EIP 7702, allowing for custom transaction validation logic and paymaster flows. - -#### Status - -| | Name | Description | Status | -| --- | ---- | ----------- | ------ | - -TODO: Expand on account abstraction tasks. - -### Milestone 7: Based Contestable Rollup - -The network can be run as a Based Rollup, meaning sequencing is done by the Ethereum Validator set; transactions are sent to a private mempool and L1 Validators that opt into the L2 sequencing propose blocks for the L2 on every L1 block. - -#### Status - -| | Name | Description | Status | -| --- | ----------------- | ------------------------------------------------------------------------------ | ------ | -| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | - -TODO: Expand on this. - -### Milestone 8: Validium - -The L2 can be initialized in Validium Mode, meaning the Data Availability layer is no longer the L1, but rather a DA layer of the user's choice. - -#### Status - -| | Name | Description | Status | -| --------- | ------------- | ---------------------------------------------------- | ------ | -| Contracts | BlockExecutor | Do not check data availability in Validium mode | ❌ | -| Proposer | L1TxSender | Do not send data in commit transactions | ❌ | -| CLI | `init` | Adapt network initialization to support Validium L2s | ❌ | -| Misc | | Add a DA integration example for Validium mode | ❌ | - -## Prerequisites - -- [Rust (explained in the repo's main README)](../../README.md) -- [Docker](https://docs.docker.com/engine/install/) (with [Docker Compose](https://docs.docker.com/compose/install/)) - -## How to run - -### Initialize the network - -> [!IMPORTANT] -> Before this step: -> -> 1. make sure the Docker daemon is running. -> 2. make sure you have created a `.env` file following the `.env.example` file. - -``` -make init -``` - -This will setup a local Ethereum network as the L1, deploy all the needed contracts on it, then start an Ethereum Rust L2 node pointing to it. - -### Restarting the network - -> [!WARNING] -> This command will cleanup your running L1 and L2 nodes. - -``` -make restart -``` - -## Local L1 Rich Wallets - -Most of them are [here](https://github.com/ethpandaops/ethereum-package/blob/main/src/prelaunch_data_generator/genesis_constants/genesis_constants.star), but there's an extra one: - -``` -{ - "address": "0x3d1e15a1a55578f7c920884a9943b3b35d0d885b", - "private_key": "0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924" -} -``` - -## Docs - -- [Ethereum Rust L2 Docs](./docs/README.md) -- [Ethereum Rust L2 CLI Docs](../../cmd/ethereum_rust_l2/README.md) - -## 📚 References and acknowledgements - -The following links, repos, companies and projects have been important in the development of this repo, we have learned a lot from them and want to thank and acknowledge them. - -- [Ethereum](https://ethereum.org/en/) -- [ZKsync](https://zksync.io/) -- [Starkware](https://starkware.co/) -- [Polygon](https://polygon.technology/) -- [Optimism](https://www.optimism.io/) -- [Arbitrum](https://arbitrum.io/) -- [Geth](https://github.com/ethereum/go-ethereum) -- [Taiko](https://taiko.xyz/) -- [RISC Zero](https://risczero.com/) -- [SP1](https://github.com/succinctlabs/sp1) -- [Aleo](https://aleo.org/) -- [Neptune](https://neptune.cash/) -- [Mina](https://minaprotocol.com/) -- [Nethermind](https://www.nethermind.io/) - -If we forgot to include anyone, please file an issue so we can add you. We always strive to reference the inspirations and code we use, but as an organization with multiple people, mistakes can happen, and someone might forget to include a reference. From ab30aa41d1c2f34872ae84b1a3a1adb0df5d435e Mon Sep 17 00:00:00 2001 From: Federico Carrone Date: Sun, 27 Oct 2024 14:55:42 -0300 Subject: [PATCH 19/49] Update README.md (#992) **Motivation** **Description** Closes #issue_number --- README.md | 63 +------------------------------------------------------ 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/README.md b/README.md index a025a3542..3c4c3a933 100644 --- a/README.md +++ b/README.md @@ -35,36 +35,6 @@ Read more about our engineering philosophy [here](https://blog.lambdaclass.com/l # Lambda Ethereum Rust L1 -### Table of Contents - -- [Lambda Ethereum Rust](#lambda-ethereum-rust) -- [L1 and L2 support](#l1-and-l2-support) - - [Philosophy](#philosophy) - - [Design Principles](#design-principles) -- [Lambda Ethereum Rust L2](#lambda-ethereum-rust-l2) -- [Lambda Ethereum Rust L1](#lambda-ethereum-rust-l1) - - [Table of Contents](#table-of-contents) - - [Roadmap](#roadmap) - - [Milestone 1: Read-only RPC Node Support](#milestone-1-read-only-rpc-node-support) - - [Milestone 2: History \& Reorgs](#milestone-2-history--reorgs) - - [Milestone 3: Block building](#milestone-3-block-building) - - [Milestone 4: P2P Network](#milestone-4-p2p-network) - - [Milestone 5: State Sync](#milestone-5-state-sync) - - [Quick Start (L1 localnet)](#quick-start-l1-localnet) - - [Prerequisites](#prerequisites) - - [Dev Setup](#dev-setup) - - [Build](#build) - - [Rust](#rust) - - [Database](#database) - - [Test](#test) - - [Ethereum Foundation Tests](#ethereum-foundation-tests) - - [Crate Specific Tests](#crate-specific-tests) - - [Hive Tests](#hive-tests) - - [Prereqs](#prereqs) - - [Running Simulations](#running-simulations) - - [Run](#run) - - [CLI Commands](#cli-commands) - ## Roadmap An Ethereum execution client consists roughly of the following parts: @@ -148,8 +118,6 @@ Detailed issues and progress [here](https://github.com/lambdaclass/ethereum_rust ## Quick Start (L1 localnet) -![Demo](https://raw.githubusercontent.com/lambdaclass/ethereum_rust/8e3b69d727225686eec30b2c2b79cecdf7eac2d9/Demo.png) - ### Prerequisites - [Kurtosis](https://docs.kurtosis.com/install/#ii-install-the-cli) - [Rust](#rust) @@ -301,35 +269,6 @@ The main differences between this mode and regular Ethereum Rust are: - A set of Solidity contracts to be deployed to the L1 are included as part of network initialization. - Two new types of transactions are included: deposits (native token mints) and withdrawals. -### Table of Contents -- [Ethereum Rust L2](#lambda-ethereum-rust-l2) - - [Table of Contents](#table-of-contents) - - [Roadmap](#roadmap) - - [Milestone 0](#milestone-0) - - [Status](#status) - - [Milestone 1: MVP](#milestone-1-mvp) - - [Status](#status-1) - - [Milestone 2: Block Execution Proofs](#milestone-2-block-execution-proofs) - - [Status](#status-2) - - [Milestone 3: State diffs + Data compression + EIP 4844 (Blobs)](#milestone-3-state-diffs--data-compression--eip-4844-blobs) - - [Status](#status-3) - - [Milestone 4: Custom Native token](#milestone-4-custom-native-token) - - [Status](#status-4) - - [Milestone 5: Security (TEEs and Multi Prover support)](#milestone-5-security-tees-and-multi-prover-support) - - [Status](#status-5) - - [Milestone 6: Account Abstraction](#milestone-6-account-abstraction) - - [Status](#status-6) - - [Milestone 7: Based Contestable Rollup](#milestone-7-based-contestable-rollup) - - [Status](#status-7) - - [Milestone 8: Validium](#milestone-8-validium) - - [Status](#status-8) - - [Prerequisites](#prerequisites) - - [How to run](#how-to-run) - - [Initialize the network](#initialize-the-network) - - [Restarting the network](#restarting-the-network) - - [Local L1 Rich Wallets](#local-l1-rich-wallets) - - [Docs](#docs) - ## Roadmap | Milestone | Description | Status | @@ -514,7 +453,7 @@ Most of them are [here](https://github.com/ethpandaops/ethereum-package/blob/mai } ``` -## Docs +## Lambda Ethereum Rust L2 Docs - [Ethereum Rust L2 Docs](./docs/README.md) - [Ethereum Rust L2 CLI Docs](../../cmd/ethereum_rust_l2/README.md) From 53290fb76147d5e301d96d48cc12edb3a629a18c Mon Sep 17 00:00:00 2001 From: Federico Carrone Date: Sun, 27 Oct 2024 14:56:12 -0300 Subject: [PATCH 20/49] Delete crates/l2/README.md --- crates/l2/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 crates/l2/README.md diff --git a/crates/l2/README.md b/crates/l2/README.md deleted file mode 100644 index 41bb9b67d..000000000 --- a/crates/l2/README.md +++ /dev/null @@ -1 +0,0 @@ -# Ethereum Rust L2 From 3c874fde8a4d17e69185c4627464d0be35131df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 28 Oct 2024 11:00:35 -0300 Subject: [PATCH 21/49] feat(l2): withdrawal handling (#877) **Motivation** Withdrawals are an essential requirement for L2 networks. **Description** 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. Closes #816 Closes #839 --------- Co-authored-by: ilitteri Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> --- cmd/ethereum_rust_l2/Cargo.toml | 2 +- cmd/ethereum_rust_l2/src/commands/utils.rs | 124 +++++---- cmd/ethereum_rust_l2/src/commands/wallet.rs | 147 +++++++++- crates/common/types/receipt.rs | 1 + crates/common/types/transaction.rs | 205 ++++++++++++++ crates/l2/Makefile | 3 +- crates/l2/contracts/deployer.rs | 250 ++++++++++++++---- crates/l2/contracts/src/l1/CommonBridge.sol | 136 +++++++++- .../l2/contracts/src/l1/OnChainProposer.sol | 82 +++++- .../src/l1/interfaces/ICommonBridge.sol | 78 +++++- .../src/l1/interfaces/IOnChainProposer.sol | 33 ++- crates/l2/proposer/l1_watcher.rs | 17 +- crates/l2/proposer/mod.rs | 93 +++++-- crates/l2/utils/eth_client/errors.rs | 28 ++ crates/l2/utils/eth_client/eth_sender.rs | 13 +- crates/l2/utils/eth_client/mod.rs | 150 ++++++++++- crates/l2/utils/eth_client/transaction.rs | 19 +- crates/l2/utils/merkle_tree.rs | 54 ++++ crates/l2/utils/mod.rs | 1 + crates/networking/rpc/eth/fee_market.rs | 3 + crates/networking/rpc/types/transaction.rs | 8 +- crates/storage/store/storage.rs | 2 +- crates/vm/Cargo.toml | 1 + crates/vm/mods.rs | 51 +++- crates/vm/vm.rs | 47 +++- 25 files changed, 1363 insertions(+), 185 deletions(-) create mode 100644 crates/l2/utils/merkle_tree.rs diff --git a/cmd/ethereum_rust_l2/Cargo.toml b/cmd/ethereum_rust_l2/Cargo.toml index 4f8ac6d6c..ea1b205c4 100644 --- a/cmd/ethereum_rust_l2/Cargo.toml +++ b/cmd/ethereum_rust_l2/Cargo.toml @@ -33,5 +33,5 @@ ethereum_rust-prover.workspace = true ethereum_rust-rlp.workspace = true [[bin]] -name = "ethereum_rust_l2" +name = "l2" path = "./src/main.rs" diff --git a/cmd/ethereum_rust_l2/src/commands/utils.rs b/cmd/ethereum_rust_l2/src/commands/utils.rs index 40b02e4c8..8d99f0de1 100644 --- a/cmd/ethereum_rust_l2/src/commands/utils.rs +++ b/cmd/ethereum_rust_l2/src/commands/utils.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use bytes::Bytes; use clap::Subcommand; use ethereum_types::{Address, H32, U256}; +use eyre::eyre; use keccak_hash::{keccak, H256}; #[derive(Subcommand)] @@ -69,30 +70,41 @@ fn parse_arg(arg_type: &str, arg: &str) -> Vec { fn parse_vec_arg(arg_type: &str, arg: &str) -> Vec { let args = arg.split(','); + let length = &mut [0u8; 32]; + U256::from(args.clone().count()).to_big_endian(length); + let length = length.to_vec(); + match arg_type { "address[]" => { - return args - .map(|arg| { + return [ + length, + args.map(|arg| { H256::from(Address::from_str(arg).expect("Cannot parse address[]")) .0 .to_vec() }) .collect::>>() - .concat(); + .concat(), + ] + .concat(); } "uint8[]" => { - return args - .map(|arg| { + return [ + length, + args.map(|arg| { let buf: &mut [u8] = &mut [0u8; 32]; U256::from(u8::from_str(arg).expect("Cannot parse u8[]")).to_big_endian(buf); buf.to_vec() }) .collect::>>() - .concat(); + .concat(), + ] + .concat(); } "uint256[]" => { - return args - .map(|arg| { + return [ + length, + args.map(|arg| { let buf: &mut [u8] = &mut [0u8; 32]; U256::from_dec_str(arg) .expect("Cannot parse u256[]") @@ -100,18 +112,23 @@ fn parse_vec_arg(arg_type: &str, arg: &str) -> Vec { buf.to_vec() }) .collect::>>() - .concat(); + .concat(), + ] + .concat(); } "bytes32[]" => { - return args - .map(|arg| { + return [ + length, + args.map(|arg| { H256::from_str(arg) .expect("Cannot parse bytes32[]") .0 .to_vec() }) .collect::>>() - .concat(); + .concat(), + ] + .concat(); } _ => { println!("Unsupported type: {arg_type}"); @@ -120,6 +137,48 @@ fn parse_vec_arg(arg_type: &str, arg: &str) -> Vec { vec![] } +pub fn encode_calldata( + signature: &str, + args: &str, + only_args: bool, +) -> Result, eyre::Error> { + let (name, params) = parse_signature(signature); + let function_selector = compute_function_selector(&name, params.clone()); + + let args: Vec<&str> = args.split(' ').collect(); + + if params.len() != args.len() { + return Err(eyre!( + "Number of arguments does not match ({} != {})", + params.len(), + args.len() + )); + } + + let mut calldata: Vec = vec![]; + let mut dynamic_calldata: Vec = vec![]; + if !only_args { + calldata.extend(function_selector.as_bytes().to_vec()); + }; + for (param, arg) in params.iter().zip(args.clone()) { + if param.as_str().ends_with("[]") { + let offset: &mut [u8] = &mut [0u8; 32]; + (U256::from(args.len()) + .checked_mul(U256::from(32)) + .expect("Calldata too long") + .checked_add(U256::from(dynamic_calldata.len())) + .expect("Calldata too long")) + .to_big_endian(offset); + calldata.extend(offset.to_vec()); + dynamic_calldata.extend(parse_vec_arg(param, arg)); + } else { + calldata.extend(parse_arg(param, arg)); + } + } + + Ok([calldata, dynamic_calldata].concat()) +} + impl Command { pub async fn run(self) -> eyre::Result<()> { match self { @@ -128,45 +187,8 @@ impl Command { args, only_args, } => { - let (name, params) = parse_signature(&signature); - let function_selector = compute_function_selector(&name, params.clone()); - - let args: Vec<&str> = args.split(' ').collect(); - - if params.len() != args.len() { - println!( - "Number of arguments does not match ({} != {})", - params.len(), - args.len() - ); - return Ok(()); - } - - let mut calldata: Vec = vec![]; - let mut dynamic_calldata: Vec = vec![]; - if !only_args { - calldata.extend(function_selector.as_bytes().to_vec()); - }; - for (param, arg) in params.iter().zip(args.clone()) { - if param.as_str().ends_with("[]") { - let offset: &mut [u8] = &mut [0u8; 32]; - (U256::from(args.len()) - .checked_mul(U256::from(32)) - .expect("Calldata too long") - .checked_add(U256::from(dynamic_calldata.len())) - .expect("Calldata too long")) - .to_big_endian(offset); - calldata.extend(offset.to_vec()); - dynamic_calldata.extend(parse_vec_arg(param, arg)); - } else { - calldata.extend(parse_arg(param, arg)); - } - } - println!( - "0x{}{}", - hex::encode(calldata), - hex::encode(dynamic_calldata) - ); + let calldata = encode_calldata(&signature, &args, only_args)?; + println!("0x{}", hex::encode(calldata)); } }; Ok(()) diff --git a/cmd/ethereum_rust_l2/src/commands/wallet.rs b/cmd/ethereum_rust_l2/src/commands/wallet.rs index 6210e4017..00fec9347 100644 --- a/cmd/ethereum_rust_l2/src/commands/wallet.rs +++ b/cmd/ethereum_rust_l2/src/commands/wallet.rs @@ -1,10 +1,20 @@ -use crate::config::EthereumRustL2Config; +use crate::{commands::utils::encode_calldata, config::EthereumRustL2Config}; use bytes::Bytes; use clap::Subcommand; -use ethereum_rust_core::types::{EIP1559Transaction, TxKind}; -use ethereum_rust_l2::utils::eth_client::{eth_sender::Overrides, EthClient}; +use ethereum_rust_core::types::{ + EIP1559Transaction, PrivilegedL2Transaction, PrivilegedTxType, Transaction, TxKind, +}; +use ethereum_rust_l2::utils::{ + eth_client::{eth_sender::Overrides, EthClient}, + merkle_tree::merkle_proof, +}; use ethereum_types::{Address, H256, U256}; +use eyre::OptionExt; use hex::FromHexError; +use itertools::Itertools; + +const CLAIM_WITHDRAWAL_SIGNATURE: &str = + "claimWithdrawal(bytes32,uint256,uint256,uint256,bytes32[])"; #[derive(Subcommand)] pub(crate) enum Command { @@ -36,10 +46,7 @@ pub(crate) enum Command { explorer_url: bool, }, #[clap(about = "Finalize a pending withdrawal.")] - FinalizeWithdraw { - #[clap(long = "hash")] - l2_withdrawal_tx_hash: H256, - }, + ClaimWithdraw { l2_withdrawal_tx_hash: H256 }, #[clap(about = "Transfer funds to another wallet.")] Transfer { // TODO: Parse ether instead. @@ -63,6 +70,10 @@ pub(crate) enum Command { // TODO: Parse ether instead. #[clap(long = "amount", value_parser = U256::from_dec_str)] amount: U256, + #[clap(long = "to")] + to: Option
, + #[clap(long = "nonce")] + nonce: Option, #[clap( long = "token", help = "Specify the token address, the base token is used as default." @@ -71,6 +82,11 @@ pub(crate) enum Command { #[clap(long, short = 'e', required = false)] explorer_url: bool, }, + #[clap(about = "Get the withdrawal merkle proof of a transaction.")] + WithdrawalProof { + #[clap(long = "hash")] + tx_hash: H256, + }, #[clap(about = "Get the wallet address.")] Address, #[clap(about = "Get the wallet private key.")] @@ -171,6 +187,50 @@ fn decode_hex(s: &str) -> Result { } } +async fn get_withdraw_merkle_proof( + client: &EthClient, + tx_hash: H256, +) -> Result<(u64, Vec), eyre::Error> { + let tx_receipt = client + .get_transaction_receipt(tx_hash) + .await? + .ok_or_eyre("Transaction receipt not found")?; + + let transactions = client + .get_block_by_hash(tx_receipt.block_info.block_hash) + .await? + .transactions; + + let (index, tx_withdrawal_hash) = transactions + .iter() + .filter(|tx| match tx { + Transaction::PrivilegedL2Transaction(tx) => tx.tx_type == PrivilegedTxType::Withdrawal, + _ => false, + }) + .find_position(|tx| tx.compute_hash() == tx_hash) + .map(|(i, tx)| match tx { + Transaction::PrivilegedL2Transaction(tx) => { + (i as u64, tx.get_withdrawal_hash().unwrap()) + } + _ => unreachable!(), + }) + .ok_or_eyre("Transaction is not a Withdrawal")?; + + let path = merkle_proof( + transactions + .iter() + .filter_map(|tx| match tx { + Transaction::PrivilegedL2Transaction(tx) => tx.get_withdrawal_hash(), + _ => None, + }) + .collect(), + tx_withdrawal_hash, + ) + .ok_or_eyre("Transaction's WithdrawalData is not in block's WithdrawalDataMerkleRoot")?; + + Ok((index, path)) +} + impl Command { pub async fn run(self, cfg: EthereumRustL2Config) -> eyre::Result<()> { let eth_client = EthClient::new(&cfg.network.l1_rpc_url); @@ -223,10 +283,52 @@ impl Command { }) .await?; } - Command::FinalizeWithdraw { - l2_withdrawal_tx_hash: _, + Command::ClaimWithdraw { + l2_withdrawal_tx_hash, } => { - todo!() + let (withdrawal_l2_block_number, claimed_amount) = match rollup_client + .get_transaction_by_hash(l2_withdrawal_tx_hash) + .await? + { + Some(l2_withdrawal_tx) => { + (l2_withdrawal_tx.block_number, l2_withdrawal_tx.value) + } + None => { + println!("Withdrawal transaction not found in L2"); + return Ok(()); + } + }; + + let (index, proof) = + get_withdraw_merkle_proof(&rollup_client, l2_withdrawal_tx_hash).await?; + + let claim_withdrawal_data = encode_calldata( + CLAIM_WITHDRAWAL_SIGNATURE, + &format!( + "{l2_withdrawal_tx_hash:#x} {claimed_amount} {withdrawal_l2_block_number} {index} {}", + proof.iter().map(hex::encode).join(",") + ), + false + )?; + println!( + "ClaimWithdrawalData: {}", + hex::encode(claim_withdrawal_data.clone()) + ); + + let tx_hash = eth_client + .send( + claim_withdrawal_data.into(), + from, + TxKind::Call(cfg.contracts.common_bridge), + cfg.wallet.private_key, + Overrides { + chain_id: Some(cfg.network.l1_chain_id), + ..Default::default() + }, + ) + .await?; + + println!("Withdrawal claim sent: {tx_hash:#x}"); } Command::Transfer { amount, @@ -270,11 +372,32 @@ impl Command { ); } Command::Withdraw { - amount: _, + amount, + to, + nonce, token_address: _, explorer_url: _, } => { - todo!() + let withdraw_transaction = PrivilegedL2Transaction { + to: TxKind::Call(to.unwrap_or(cfg.wallet.address)), + value: amount, + chain_id: cfg.network.l2_chain_id, + nonce: nonce.unwrap_or(rollup_client.get_nonce(from).await?), + max_fee_per_gas: 800000000, + tx_type: PrivilegedTxType::Withdrawal, + gas_limit: 21000 * 2, + ..Default::default() + }; + + let tx_hash = rollup_client + .send_privileged_l2_transaction(withdraw_transaction, cfg.wallet.private_key) + .await?; + + println!("Withdrawal sent: {tx_hash:#x}"); + } + Command::WithdrawalProof { tx_hash } => { + let (_index, path) = get_withdraw_merkle_proof(&rollup_client, tx_hash).await?; + println!("{path:?}"); } Command::Address => { todo!() diff --git a/crates/common/types/receipt.rs b/crates/common/types/receipt.rs index 8d7af15a3..d70d5bad9 100644 --- a/crates/common/types/receipt.rs +++ b/crates/common/types/receipt.rs @@ -70,6 +70,7 @@ impl RLPDecode for Receipt { 0x1 => (TxType::EIP2930, &rlp[1..]), 0x2 => (TxType::EIP1559, &rlp[1..]), 0x3 => (TxType::EIP4844, &rlp[1..]), + 0x7e => (TxType::Privileged, &rlp[1..]), ty => { return Err(RLPDecodeError::Custom(format!( "Invalid transaction type: {ty}" diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 2c87b3296..882407f43 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -23,6 +23,7 @@ pub enum Transaction { EIP2930Transaction(EIP2930Transaction), EIP1559Transaction(EIP1559Transaction), EIP4844Transaction(EIP4844Transaction), + PrivilegedL2Transaction(PrivilegedL2Transaction), } #[derive(Clone, Debug, PartialEq, Eq, Default)] @@ -89,6 +90,30 @@ pub struct EIP4844Transaction { pub signature_s: U256, } +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct PrivilegedL2Transaction { + pub chain_id: u64, + pub nonce: u64, + pub max_priority_fee_per_gas: u64, + pub max_fee_per_gas: u64, + pub gas_limit: u64, + pub to: TxKind, + pub value: U256, + pub data: Bytes, + pub access_list: Vec<(Address, Vec)>, + pub tx_type: PrivilegedTxType, + pub signature_y_parity: bool, + pub signature_r: U256, + pub signature_s: U256, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum PrivilegedTxType { + #[default] + Deposit = 0x01, + Withdrawal = 0x02, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum TxType { #[default] @@ -96,6 +121,9 @@ pub enum TxType { EIP2930 = 0x01, EIP1559 = 0x02, EIP4844 = 0x03, + // We take the same approach as Optimism to define the privileged tx prefix + // https://github.com/ethereum-optimism/specs/blob/c6903a3b2cad575653e1f5ef472debb573d83805/specs/protocol/deposits.md#the-deposited-transaction-type + Privileged = 0x7e, } impl Transaction { @@ -105,6 +133,7 @@ impl Transaction { Transaction::EIP2930Transaction(_) => TxType::EIP2930, Transaction::EIP1559Transaction(_) => TxType::EIP1559, Transaction::EIP4844Transaction(_) => TxType::EIP4844, + Transaction::PrivilegedL2Transaction(_) => TxType::Privileged, } } } @@ -147,6 +176,9 @@ impl RLPDecode for Transaction { // EIP4844 0x3 => EIP4844Transaction::decode_unfinished(tx_encoding) .map(|(tx, rem)| (Transaction::EIP4844Transaction(tx), rem)), + // PriviligedL2 + 0x7e => PrivilegedL2Transaction::decode_unfinished(tx_encoding) + .map(|(tx, rem)| (Transaction::PrivilegedL2Transaction(tx), rem)), ty => Err(RLPDecodeError::Custom(format!( "Invalid transaction type: {ty}" ))), @@ -186,6 +218,24 @@ impl RLPDecode for TxKind { } } +impl RLPEncode for PrivilegedTxType { + fn encode(&self, buf: &mut dyn bytes::BufMut) { + match self { + Self::Deposit => buf.put_u8(0x01), + Self::Withdrawal => buf.put_u8(0x02), + } + } +} + +impl RLPDecode for PrivilegedTxType { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoded = u8::decode_unfinished(rlp)?; + let tx_type = PrivilegedTxType::from_u8(decoded.0) + .ok_or(RLPDecodeError::Custom("Invalid".to_string()))?; + Ok((tx_type, decoded.1)) + } +} + impl RLPEncode for LegacyTransaction { fn encode(&self, buf: &mut dyn bytes::BufMut) { Encoder::new(buf) @@ -260,6 +310,26 @@ impl RLPEncode for EIP4844Transaction { } } +impl RLPEncode for PrivilegedL2Transaction { + fn encode(&self, buf: &mut dyn bytes::BufMut) { + Encoder::new(buf) + .encode_field(&self.chain_id) + .encode_field(&self.nonce) + .encode_field(&self.max_priority_fee_per_gas) + .encode_field(&self.max_fee_per_gas) + .encode_field(&self.gas_limit) + .encode_field(&self.to) + .encode_field(&self.value) + .encode_field(&self.data) + .encode_field(&self.access_list) + .encode_field(&self.tx_type) + .encode_field(&self.signature_y_parity) + .encode_field(&self.signature_r) + .encode_field(&self.signature_s) + .finish() + } +} + impl RLPDecode for LegacyTransaction { fn decode_unfinished(rlp: &[u8]) -> Result<(LegacyTransaction, &[u8]), RLPDecodeError> { let decoder = Decoder::new(rlp)?; @@ -394,6 +464,43 @@ impl RLPDecode for EIP4844Transaction { } } +impl RLPDecode for PrivilegedL2Transaction { + fn decode_unfinished(rlp: &[u8]) -> Result<(PrivilegedL2Transaction, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (chain_id, decoder) = decoder.decode_field("chain_id")?; + let (nonce, decoder) = decoder.decode_field("nonce")?; + let (max_priority_fee_per_gas, decoder) = + decoder.decode_field("max_priority_fee_per_gas")?; + let (max_fee_per_gas, decoder) = decoder.decode_field("max_fee_per_gas")?; + let (gas_limit, decoder) = decoder.decode_field("gas_limit")?; + let (to, decoder) = decoder.decode_field("to")?; + let (value, decoder) = decoder.decode_field("value")?; + let (data, decoder) = decoder.decode_field("data")?; + let (access_list, decoder) = decoder.decode_field("access_list")?; + let (tx_type, decoder) = decoder.decode_field("tx_type")?; + let (signature_y_parity, decoder) = decoder.decode_field("signature_y_parity")?; + let (signature_r, decoder) = decoder.decode_field("signature_r")?; + let (signature_s, decoder) = decoder.decode_field("signature_s")?; + + let tx = PrivilegedL2Transaction { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + data, + access_list, + tx_type, + signature_y_parity, + signature_r, + signature_s, + }; + Ok((tx, decoder.finish()?)) + } +} + impl Transaction { pub fn sender(&self) -> Address { match self { @@ -487,6 +594,27 @@ impl Transaction { &Bytes::from(buf), ) } + Transaction::PrivilegedL2Transaction(tx) => { + let mut buf = vec![self.tx_type() as u8]; + Encoder::new(&mut buf) + .encode_field(&tx.chain_id) + .encode_field(&tx.nonce) + .encode_field(&tx.max_priority_fee_per_gas) + .encode_field(&tx.max_fee_per_gas) + .encode_field(&tx.gas_limit) + .encode_field(&tx.to) + .encode_field(&tx.value) + .encode_field(&tx.data) + .encode_field(&tx.access_list) + .encode_field(&tx.tx_type) + .finish(); + recover_address( + &tx.signature_r, + &tx.signature_s, + tx.signature_y_parity, + &Bytes::from(buf), + ) + } } } @@ -496,6 +624,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.gas_limit, Transaction::EIP1559Transaction(tx) => tx.gas_limit, Transaction::EIP4844Transaction(tx) => tx.gas, + Transaction::PrivilegedL2Transaction(tx) => tx.gas_limit, } } @@ -505,6 +634,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.gas_price, Transaction::EIP1559Transaction(tx) => tx.max_fee_per_gas, Transaction::EIP4844Transaction(tx) => tx.max_fee_per_gas, + Transaction::PrivilegedL2Transaction(tx) => tx.max_fee_per_gas, } } @@ -514,6 +644,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.to.clone(), Transaction::EIP1559Transaction(tx) => tx.to.clone(), Transaction::EIP4844Transaction(tx) => TxKind::Call(tx.to), + Transaction::PrivilegedL2Transaction(tx) => tx.to.clone(), } } @@ -523,6 +654,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.value, Transaction::EIP1559Transaction(tx) => tx.value, Transaction::EIP4844Transaction(tx) => tx.value, + Transaction::PrivilegedL2Transaction(tx) => tx.value, } } @@ -532,6 +664,7 @@ impl Transaction { Transaction::EIP2930Transaction(_tx) => None, Transaction::EIP1559Transaction(tx) => Some(tx.max_priority_fee_per_gas), Transaction::EIP4844Transaction(tx) => Some(tx.max_priority_fee_per_gas), + Transaction::PrivilegedL2Transaction(tx) => Some(tx.max_priority_fee_per_gas), } } @@ -541,6 +674,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => Some(tx.chain_id), Transaction::EIP1559Transaction(tx) => Some(tx.chain_id), Transaction::EIP4844Transaction(tx) => Some(tx.chain_id), + Transaction::PrivilegedL2Transaction(tx) => Some(tx.chain_id), } } @@ -550,6 +684,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.access_list.clone(), Transaction::EIP1559Transaction(tx) => tx.access_list.clone(), Transaction::EIP4844Transaction(tx) => tx.access_list.clone(), + Transaction::PrivilegedL2Transaction(tx) => tx.access_list.clone(), } } @@ -559,6 +694,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => tx.nonce, Transaction::EIP1559Transaction(tx) => tx.nonce, Transaction::EIP4844Transaction(tx) => tx.nonce, + Transaction::PrivilegedL2Transaction(tx) => tx.nonce, } } @@ -568,6 +704,7 @@ impl Transaction { Transaction::EIP2930Transaction(tx) => &tx.data, Transaction::EIP1559Transaction(tx) => &tx.data, Transaction::EIP4844Transaction(tx) => &tx.data, + Transaction::PrivilegedL2Transaction(tx) => &tx.data, } } @@ -577,6 +714,7 @@ impl Transaction { Transaction::EIP2930Transaction(_tx) => Vec::new(), Transaction::EIP1559Transaction(_tx) => Vec::new(), Transaction::EIP4844Transaction(tx) => tx.blob_versioned_hashes.clone(), + Transaction::PrivilegedL2Transaction(_tx) => Vec::new(), } } @@ -586,6 +724,7 @@ impl Transaction { Transaction::EIP2930Transaction(_tx) => None, Transaction::EIP1559Transaction(_tx) => None, Transaction::EIP4844Transaction(tx) => Some(tx.max_fee_per_blob_gas), + Transaction::PrivilegedL2Transaction(_tx) => None, } } @@ -595,6 +734,7 @@ impl Transaction { Transaction::EIP2930Transaction(t) => matches!(t.to, TxKind::Create), Transaction::EIP1559Transaction(t) => matches!(t.to, TxKind::Create), Transaction::EIP4844Transaction(_) => false, + Transaction::PrivilegedL2Transaction(t) => matches!(t.to, TxKind::Create), } } @@ -604,6 +744,7 @@ impl Transaction { Transaction::EIP2930Transaction(_tx) => None, Transaction::EIP1559Transaction(tx) => Some(tx.max_fee_per_gas), Transaction::EIP4844Transaction(tx) => Some(tx.max_fee_per_gas), + Transaction::PrivilegedL2Transaction(tx) => Some(tx.max_fee_per_gas), } } @@ -685,6 +826,41 @@ impl TxType { 0x01 => Some(Self::EIP2930), 0x02 => Some(Self::EIP1559), 0x03 => Some(Self::EIP4844), + 0x7e => Some(Self::Privileged), + _ => None, + } + } +} + +impl PrivilegedTxType { + pub fn from_u8(value: u8) -> Option { + match value { + 0x01 => Some(Self::Deposit), + 0x02 => Some(Self::Withdrawal), + _ => None, + } + } +} + +impl PrivilegedL2Transaction { + pub fn get_withdrawal_hash(&self) -> Option { + match self.tx_type { + PrivilegedTxType::Withdrawal => { + let to = match self.to { + TxKind::Call(to) => to, + _ => return None, + }; + + let value = &mut [0u8; 32]; + self.value.to_big_endian(value); + + let mut encoded = self.encode_to_vec(); + encoded.insert(0, TxType::Privileged as u8); + let tx_hash = keccak_hash::keccak(encoded); + Some(keccak_hash::keccak( + [to.as_bytes(), value, tx_hash.as_bytes()].concat(), + )) + } _ => None, } } @@ -725,6 +901,8 @@ mod canonic_encoding { // EIP4844 0x3 => EIP4844Transaction::decode(tx_bytes) .map(Transaction::EIP4844Transaction), + 0x7e => PrivilegedL2Transaction::decode(tx_bytes) + .map(Transaction::PrivilegedL2Transaction), ty => Err(RLPDecodeError::Custom(format!( "Invalid transaction type: {ty}" ))), @@ -751,6 +929,7 @@ mod canonic_encoding { Transaction::EIP2930Transaction(t) => t.encode(buf), Transaction::EIP1559Transaction(t) => t.encode(buf), Transaction::EIP4844Transaction(t) => t.encode(buf), + Transaction::PrivilegedL2Transaction(t) => t.encode(buf), }; } @@ -1269,6 +1448,32 @@ mod serde_impl { #[serde(default, with = "crate::serde_utils::u64::hex_str_opt")] pub chain_id: Option, } + + impl From for GenericTransaction { + fn from(value: EIP1559Transaction) -> Self { + Self { + r#type: TxType::EIP1559, + nonce: value.nonce, + to: value.to, + gas: Some(value.gas_limit), + value: value.value, + input: value.data, + gas_price: value.max_fee_per_gas, + max_priority_fee_per_gas: Some(value.max_priority_fee_per_gas), + max_fee_per_gas: Some(value.max_fee_per_gas), + max_fee_per_blob_gas: None, + access_list: value + .access_list + .iter() + .map(AccessListEntry::from) + .collect(), + blob_versioned_hashes: vec![], + blobs: vec![], + chain_id: Some(value.chain_id), + ..Default::default() + } + } + } } mod mempool { diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 2473884cb..b04aef95d 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -40,10 +40,9 @@ restart-local-l1: down-local-l1 init-local-l1 ## 🔄 Restarts the L1 Lambda Eth # Contracts -# contract-deps: ## 📦 Installs the dependencies for the L1 contracts - clean-contract-deps: ## 🧹 Cleans the dependencies for the L1 contracts. rm -rf contracts/solc_out + rm -rf contracts/lib restart-contract-deps: clean-contract-deps ## 🔄 Restarts the dependencies for the L1 contracts. diff --git a/crates/l2/contracts/deployer.rs b/crates/l2/contracts/deployer.rs index 875c39db1..981e5bc2c 100644 --- a/crates/l2/contracts/deployer.rs +++ b/crates/l2/contracts/deployer.rs @@ -18,10 +18,24 @@ const SALT: H256 = H256::zero(); #[tokio::main] async fn main() { - read_env_file().unwrap(); + let (deployer, deployer_private_key, eth_client) = setup(); + download_contract_deps(); + compile_contracts(); + let (on_chain_proposer, bridge_address) = + deploy_contracts(deployer, deployer_private_key, ð_client).await; + initialize_contracts( + deployer, + deployer_private_key, + on_chain_proposer, + bridge_address, + ð_client, + ) + .await; +} +fn setup() -> (Address, SecretKey, EthClient) { + read_env_file().expect("Failed to read .env file"); let eth_client = EthClient::new(&std::env::var("ETH_RPC_URL").expect("ETH_RPC_URL not set")); - let deployer = std::env::var("DEPLOYER_ADDRESS") .expect("DEPLOYER_ADDRESS not set") .parse() @@ -38,6 +52,59 @@ async fn main() { ) .expect("Malformed DEPLOYER_PRIVATE_KEY (SecretKey::parse)"); + (deployer, deployer_private_key, eth_client) +} + +fn download_contract_deps() { + std::fs::create_dir_all("contracts/lib").expect("Failed to create contracts/lib"); + Command::new("git") + .arg("clone") + .arg("https://github.com/OpenZeppelin/openzeppelin-contracts.git") + .arg("contracts/lib/openzeppelin-contracts") + .spawn() + .expect("Failed to spawn git") + .wait() + .expect("Failed to wait for git"); +} + +fn compile_contracts() { + // Both the contract path and the output path are relative to where the Makefile is. + assert!( + Command::new("solc") + .arg("--bin") + .arg("./contracts/src/l1/OnChainProposer.sol") + .arg("-o") + .arg("contracts/solc_out") + .arg("--overwrite") + .spawn() + .expect("Failed to spawn solc") + .wait() + .expect("Failed to wait for solc") + .success(), + "Failed to compile OnChainProposer.sol" + ); + + assert!( + Command::new("solc") + .arg("--bin") + .arg("./contracts/src/l1/CommonBridge.sol") + .arg("-o") + .arg("contracts/solc_out") + .arg("--overwrite") + .spawn() + .expect("Failed to spawn solc") + .wait() + .expect("Failed to wait for solc") + .success(), + "Failed to compile CommonBridge.sol" + ); +} + +async fn deploy_contracts( + deployer: Address, + deployer_private_key: SecretKey, + eth_client: &EthClient, +) -> (Address, Address) { if std::fs::exists("contracts/solc_out").expect("Could not determine if solc_out exists") { std::fs::remove_dir_all("contracts/solc_out").expect("Failed to remove solc_out"); } @@ -53,7 +120,7 @@ async fn main() { deployer, deployer_private_key, overrides.clone(), - ð_client, + eth_client, ) .await; println!( @@ -62,11 +129,13 @@ async fn main() { ); let (bridge_deployment_tx_hash, bridge_address) = - deploy_bridge(deployer, deployer_private_key, overrides, ð_client).await; + deploy_bridge(deployer, deployer_private_key, overrides, eth_client).await; println!( "Bridge deployed at address {:#x} with tx hash {:#x}", bridge_address, bridge_deployment_tx_hash ); + + (on_chain_proposer_address, bridge_address) } async fn deploy_on_chain_proposer( @@ -75,21 +144,6 @@ async fn deploy_on_chain_proposer( overrides: Overrides, eth_client: &EthClient, ) -> (H256, Address) { - // Both the contract path and the output path are relative to where the Makefile is. - assert!( - Command::new("solc") - .arg("--bin") - .arg("./contracts/src/l1/OnChainProposer.sol") - .arg("-o") - .arg("contracts/solc_out") - .spawn() - .expect("Failed to spawn solc") - .wait() - .expect("Failed to wait for solc") - .success(), - "Failed to compile OnChainProposer.sol" - ); - let on_chain_proposer_init_code = hex::decode( std::fs::read_to_string("./contracts/solc_out/OnChainProposer.bin") .expect("Failed to read on_chain_proposer_init_code"), @@ -97,7 +151,7 @@ async fn deploy_on_chain_proposer( .expect("Failed to decode on_chain_proposer_init_code") .into(); - let (deploy_tx_hash, on_chain_proposer_address) = create2_deploy( + let (deploy_tx_hash, on_chain_proposer) = create2_deploy( deployer, deployer_private_key, &on_chain_proposer_init_code, @@ -106,7 +160,7 @@ async fn deploy_on_chain_proposer( ) .await; - (deploy_tx_hash, on_chain_proposer_address) + (deploy_tx_hash, on_chain_proposer) } async fn deploy_bridge( @@ -115,31 +169,25 @@ async fn deploy_bridge( overrides: Overrides, eth_client: &EthClient, ) -> (H256, Address) { - assert!( - Command::new("solc") - .arg("--bin") - .arg("./contracts/src/l1/CommonBridge.sol") - .arg("-o") - .arg("contracts/solc_out") - .spawn() - .expect("Failed to spawn solc") - .wait() - .expect("Failed to wait for solc") - .success(), - "Failed to compile CommonBridge.sol" - ); - - let bridge_init_code = hex::decode( + let mut bridge_init_code = hex::decode( std::fs::read_to_string("./contracts/solc_out/CommonBridge.bin") .expect("Failed to read bridge_init_code"), ) - .expect("Failed to decode bridge_init_code") - .into(); + .expect("Failed to decode bridge_init_code"); + + let encoded_owner = { + let offset = 32 - deployer.as_bytes().len() % 32; + let mut encoded_owner = vec![0; offset]; + encoded_owner.extend_from_slice(deployer.as_bytes()); + encoded_owner + }; + + bridge_init_code.extend_from_slice(&encoded_owner); let (deploy_tx_hash, bridge_address) = create2_deploy( deployer, deployer_private_key, - &bridge_init_code, + &bridge_init_code.into(), overrides, eth_client, ) @@ -167,14 +215,7 @@ async fn create2_deploy( .await .unwrap(); - while eth_client - .get_transaction_receipt(deploy_tx_hash) - .await - .expect("Failed to get transaction receipt") - .is_none() - { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } + wait_for_transaction_receipt(deploy_tx_hash, eth_client).await; let deployed_address = create2_address(keccak(init_code)); @@ -197,3 +238,118 @@ fn create2_address(init_code_hash: H256) -> Address { .expect("Failed to get create2 address"), ) } + +async fn initialize_contracts( + deployer: Address, + deployer_private_key: SecretKey, + on_chain_proposer: Address, + bridge: Address, + eth_client: &EthClient, +) { + initialize_on_chain_proposer( + on_chain_proposer, + bridge, + deployer, + deployer_private_key, + eth_client, + ) + .await; + initialize_bridge( + on_chain_proposer, + bridge, + deployer, + deployer_private_key, + eth_client, + ) + .await; +} + +async fn initialize_on_chain_proposer( + on_chain_proposer: Address, + bridge: Address, + deployer: Address, + deployer_private_key: SecretKey, + eth_client: &EthClient, +) { + let on_chain_proposer_initialize_selector = keccak(b"initialize(address)") + .as_bytes() + .get(..4) + .expect("Failed to get initialize selector") + .to_vec(); + let encoded_bridge = { + let offset = 32 - bridge.as_bytes().len() % 32; + let mut encoded_bridge = vec![0; offset]; + encoded_bridge.extend_from_slice(bridge.as_bytes()); + encoded_bridge + }; + + let mut on_chain_proposer_initialization_calldata = Vec::new(); + on_chain_proposer_initialization_calldata + .extend_from_slice(&on_chain_proposer_initialize_selector); + on_chain_proposer_initialization_calldata.extend_from_slice(&encoded_bridge); + + let initialize_tx_hash = eth_client + .send( + on_chain_proposer_initialization_calldata.into(), + deployer, + TxKind::Call(on_chain_proposer), + deployer_private_key, + Overrides::default(), + ) + .await + .expect("Failed to send initialize transaction"); + + wait_for_transaction_receipt(initialize_tx_hash, eth_client).await; + + println!("OnChainProposer initialized with tx hash {initialize_tx_hash:#x}\n"); +} + +async fn initialize_bridge( + on_chain_proposer: Address, + bridge: Address, + deployer: Address, + deployer_private_key: SecretKey, + eth_client: &EthClient, +) { + let bridge_initialize_selector = keccak(b"initialize(address)") + .as_bytes() + .get(..4) + .expect("Failed to get initialize selector") + .to_vec(); + let encoded_on_chain_proposer = { + let offset = 32 - on_chain_proposer.as_bytes().len() % 32; + let mut encoded_owner = vec![0; offset]; + encoded_owner.extend_from_slice(on_chain_proposer.as_bytes()); + encoded_owner + }; + + let mut bridge_initialization_calldata = Vec::new(); + bridge_initialization_calldata.extend_from_slice(&bridge_initialize_selector); + bridge_initialization_calldata.extend_from_slice(&encoded_on_chain_proposer); + + let initialize_tx_hash = eth_client + .send( + bridge_initialization_calldata.into(), + deployer, + TxKind::Call(bridge), + deployer_private_key, + Overrides::default(), + ) + .await + .expect("Failed to send initialize transaction"); + + wait_for_transaction_receipt(initialize_tx_hash, eth_client).await; + + println!("Bridge initialized with tx hash {initialize_tx_hash:#x}\n"); +} + +async fn wait_for_transaction_receipt(tx_hash: H256, eth_client: &EthClient) { + while eth_client + .get_transaction_receipt(tx_hash) + .await + .expect("Failed to get transaction receipt") + .is_none() + { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } +} diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index d2b7d4326..304970abb 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -1,11 +1,58 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity ^0.8.27; +import "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import "../../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/ICommonBridge.sol"; +import "./interfaces/IOnChainProposer.sol"; /// @title CommonBridge contract. /// @author LambdaClass -contract CommonBridge is ICommonBridge { +contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { + /// @notice Mapping of unclaimed withdrawals. A withdrawal is claimed if + /// there is a non-zero value in the mapping (a merkle root) for the hash + /// of the L2 transaction that requested the withdrawal. + /// @dev The key is the hash of the L2 transaction that requested the + /// withdrawal. + /// @dev The value is a boolean indicating if the withdrawal was claimed or not. + mapping(bytes32 => bool) public claimedWithdrawals; + + /// @notice Mapping of merkle roots to the L2 withdrawal transaction logs. + /// @dev The key is the L2 block number where the logs were emitted. + /// @dev The value is the merkle root of the logs. + /// @dev If there exist a merkle root for a given block number it means + /// that the logs were published on L1, and that that block was committed. + mapping(uint256 => bytes32) public blockWithdrawalsLogs; + + address public ON_CHAIN_PROPOSER; + + modifier onlyOnChainProposer() { + require( + msg.sender == ON_CHAIN_PROPOSER, + "CommonBridge: caller is not the OnChainProposer" + ); + _; + } + + constructor(address owner) Ownable(owner) {} + + /// @inheritdoc ICommonBridge + function initialize(address onChainProposer) public nonReentrant { + require( + ON_CHAIN_PROPOSER == address(0), + "CommonBridge: contract already initialized" + ); + require( + onChainProposer != address(0), + "CommonBridge: onChainProposer is the zero address" + ); + require( + onChainProposer != address(this), + "CommonBridge: onChainProposer is the contract address" + ); + ON_CHAIN_PROPOSER = onChainProposer; + } + /// @inheritdoc ICommonBridge function deposit(address to) public payable { if (msg.value == 0) { @@ -19,4 +66,89 @@ contract CommonBridge is ICommonBridge { receive() external payable { deposit(msg.sender); } + + /// @inheritdoc ICommonBridge + function publishWithdrawals( + uint256 withdrawalLogsBlockNumber, + bytes32 withdrawalsLogsMerkleRoot + ) public onlyOnChainProposer { + require( + blockWithdrawalsLogs[withdrawalLogsBlockNumber] == bytes32(0), + "CommonBridge: withdrawal logs already published" + ); + blockWithdrawalsLogs[ + withdrawalLogsBlockNumber + ] = withdrawalsLogsMerkleRoot; + emit WithdrawalsPublished( + withdrawalLogsBlockNumber, + withdrawalsLogsMerkleRoot + ); + } + + /// @inheritdoc ICommonBridge + function claimWithdrawal( + bytes32 l2WithdrawalTxHash, + uint256 claimedAmount, + uint256 withdrawalBlockNumber, + uint256 withdrawalLogIndex, + bytes32[] calldata withdrawalProof + ) public nonReentrant { + require( + blockWithdrawalsLogs[withdrawalBlockNumber] != bytes32(0), + "CommonBridge: the block that emitted the withdrawal logs was not committed" + ); + require( + IOnChainProposer(ON_CHAIN_PROPOSER).verifiedBlocks( + withdrawalBlockNumber + ), + "CommonBridge: the block that emitted the withdrawal logs was not verified" + ); + require( + claimedWithdrawals[l2WithdrawalTxHash] == false, + "CommonBridge: the withdrawal was already claimed" + ); + require( + _verifyWithdrawProof( + l2WithdrawalTxHash, + claimedAmount, + withdrawalBlockNumber, + withdrawalLogIndex, + withdrawalProof + ), + "CommonBridge: invalid withdrawal proof" + ); + + (bool success, ) = payable(msg.sender).call{value: claimedAmount}(""); + + require(success, "CommonBridge: failed to send the claimed amount"); + + claimedWithdrawals[l2WithdrawalTxHash] = true; + + emit WithdrawalClaimed(l2WithdrawalTxHash, msg.sender, claimedAmount); + } + + function _verifyWithdrawProof( + bytes32 l2WithdrawalTxHash, + uint256 claimedAmount, + uint256 withdrawalBlockNumber, + uint256 withdrawalLogIndex, + bytes32[] calldata withdrawalProof + ) internal view returns (bool) { + bytes32 withdrawalLeaf = keccak256( + abi.encodePacked(msg.sender, claimedAmount, l2WithdrawalTxHash) + ); + for (uint256 i = 0; i < withdrawalProof.length; i++) { + if (withdrawalLogIndex % 2 == 0) { + withdrawalLeaf = keccak256( + abi.encodePacked(withdrawalLeaf, withdrawalProof[i]) + ); + } else { + withdrawalLeaf = keccak256( + abi.encodePacked(withdrawalProof[i], withdrawalLeaf) + ); + } + withdrawalLogIndex /= 2; + } + return withdrawalLeaf == blockWithdrawalsLogs[withdrawalBlockNumber]; + } } diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 8c41f3ced..17fb12aec 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -1,19 +1,87 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity ^0.8.27; +import "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import "../../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IOnChainProposer.sol"; +import {CommonBridge} from "./CommonBridge.sol"; +import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; /// @title OnChainProposer contract. /// @author LambdaClass -contract OnChainProposer is IOnChainProposer { +contract OnChainProposer is IOnChainProposer, ReentrancyGuard { + /// @notice The commitments of the committed blocks. + /// @dev If a block is committed, the commitment is stored here. + /// @dev If a block was not committed yet, it won't be here. + /// @dev It is used by other contracts to verify if a block was committed. + mapping(uint256 => bytes32) public blockCommitments; + + /// @notice The verified blocks. + /// @dev If a block is verified, the block hash is stored here. + /// @dev If a block was not verified yet, it won't be here. + /// @dev It is used by other contracts to verify if a block was verified. + mapping(uint256 => bool) public verifiedBlocks; + + address public BRIDGE; + + /// @inheritdoc IOnChainProposer + function initialize(address bridge) public nonReentrant { + require( + BRIDGE == address(0), + "OnChainProposer: contract already initialized" + ); + require( + bridge != address(0), + "OnChainProposer: bridge is the zero address" + ); + require( + bridge != address(this), + "OnChainProposer: bridge is the contract address" + ); + BRIDGE = bridge; + } + /// @inheritdoc IOnChainProposer - function commit(bytes32 currentBlockCommitment) external override { - emit BlockCommitted(currentBlockCommitment); + function commit( + uint256 blockNumber, + bytes32 newL2StateRoot, + bytes32 withdrawalsLogsMerkleRoot + ) external override { + require( + !verifiedBlocks[blockNumber], + "OnChainProposer: block already verified" + ); + require( + blockCommitments[blockNumber] == bytes32(0), + "OnChainProposer: block already committed" + ); + bytes32 blockCommitment = keccak256( + abi.encode(blockNumber, newL2StateRoot, withdrawalsLogsMerkleRoot) + ); + blockCommitments[blockNumber] = blockCommitment; + if (withdrawalsLogsMerkleRoot != bytes32(0)) { + ICommonBridge(BRIDGE).publishWithdrawals( + blockNumber, + withdrawalsLogsMerkleRoot + ); + } + emit BlockCommitted(blockCommitment); } /// @inheritdoc IOnChainProposer - function verify(bytes calldata blockProof) external override { - bytes32 blockHash = keccak256(abi.encode(blockProof)); - emit BlockVerified(blockHash); + function verify( + uint256 blockNumber, + bytes calldata // blockProof + ) external override { + require( + blockCommitments[blockNumber] != bytes32(0), + "OnChainProposer: block not committed" + ); + require( + !verifiedBlocks[blockNumber], + "OnChainProposer: block already verified" + ); + verifiedBlocks[blockNumber] = true; + emit BlockVerified(blockNumber); } } diff --git a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol index a6d41d455..693b8a434 100644 --- a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol +++ b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity ^0.8.27; /// @title Interface for the CommonBridge contract. /// @author LambdaClass @@ -14,15 +14,87 @@ interface ICommonBridge { /// deposit in L2. Could be used to track the status of the deposit finalization /// on L2. You can use this hash to retrive the tx data. /// It is the result of keccak(abi.encode(transaction)). - event DepositInitiated(uint256 indexed amount, address indexed to, bytes32 indexed l2MintTxHash); + event DepositInitiated( + uint256 indexed amount, + address indexed to, + bytes32 indexed l2MintTxHash + ); + + /// @notice L2 withdrawals have been published on L1. + /// @dev Event emitted when the L2 withdrawals are published on L1. + /// @param withdrawalLogsBlockNumber the block number in L2 where the + /// withdrawal logs were emitted. + /// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs. + event WithdrawalsPublished( + uint256 indexed withdrawalLogsBlockNumber, + bytes32 indexed withdrawalsLogsMerkleRoot + ); + + /// @notice A withdrawal has been claimed. + /// @dev Event emitted when a withdrawal is claimed. + /// @param l2WithdrawalTxHash the hash of the L2 withdrawal transaction. + /// @param claimee the address that claimed the withdrawal. + /// @param claimedAmount the amount that was claimed. + event WithdrawalClaimed( + bytes32 indexed l2WithdrawalTxHash, + address indexed claimee, + uint256 indexed claimedAmount + ); /// @notice Error for when the deposit amount is 0. error AmountToDepositIsZero(); - + + /// @notice Initializes the contract. + /// @dev This method is called only once after the contract is deployed. + /// @dev It sets the OnChainProposer address. + /// @param onChainProposer the address of the OnChainProposer contract. + function initialize(address onChainProposer) external; + /// @notice Method that starts an L2 ETH deposit process. /// @dev The deposit process starts here by emitting a DepositInitiated /// event. This event will later be intercepted by the L2 operator to /// finalize the deposit. /// @param to, the address in L2 to which the tokens will be minted to. function deposit(address to) external payable; + + /// @notice Publishes the L2 withdrawals on L1. + /// @dev This method is used by the L2 OnChainOperator to publish the L2 + /// withdrawals when an L2 block is committed. + /// @param withdrawalLogsBlockNumber the block number in L2 where the + /// withdrawal logs were emitted. + /// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs. + function publishWithdrawals( + uint256 withdrawalLogsBlockNumber, + bytes32 withdrawalsLogsMerkleRoot + ) external; + + /// @notice Method that claims an L2 withdrawal. + /// @dev For a user to claim a withdrawal, this method verifies: + /// - The l2WithdrawalBlockNumber was committed. If the given block was not + /// committed, this means that the withdrawal was not published on L1. + /// - The l2WithdrawalBlockNumber was verified. If the given block was not + /// verified, this means that the withdrawal claim was not enabled. + /// - The withdrawal was not claimed yet. This is to avoid double claims. + /// - The withdrawal proof is valid. This is, there exists a merkle path + /// from the withdrawal log to the withdrawal root, hence the claimed + /// withdrawal exists. + /// @dev We do not need to check that the claimee is the same as the + /// beneficiary of the withdrawal, because the withdrawal proof already + /// contains the beneficiary. + /// @param l2WithdrawalTxHash the hash of the L2 withdrawal transaction. + /// @param claimedAmount the amount that will be claimed. + /// @param withdrawalProof the merkle path to the withdrawal log. + /// @param withdrawalLogIndex the index of the withdrawal log in the block. + /// This is the index of the withdraw transaction relative to the block's + /// withdrawal transctions. + /// A pseudocode would be [tx if tx is withdrawx for tx in block.txs()].index(leaf_tx). + /// @param l2WithdrawalBlockNumber the block number where the withdrawal log + /// was emitted. + function claimWithdrawal( + bytes32 l2WithdrawalTxHash, + uint256 claimedAmount, + uint256 l2WithdrawalBlockNumber, + uint256 withdrawalLogIndex, + bytes32[] calldata withdrawalProof + ) external; } diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 7088f69af..7b786e97d 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -1,28 +1,45 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.27; +pragma solidity ^0.8.27; /// @title Interface for the OnChainProposer contract. /// @author LambdaClass /// @notice A OnChainProposer contract ensures the advancement of the L2. It is used /// by the proposer to commit blocks and verify block proofs. interface IOnChainProposer { + /// @notice The commitments of the committed blocks. + function verifiedBlocks(uint256) external view returns (bool); + /// @notice A block has been committed. /// @dev Event emitted when a block is committed. event BlockCommitted(bytes32 indexed currentBlockCommitment); /// @notice A block has been verified. /// @dev Event emitted when a block is verified. - event BlockVerified(bytes32 indexed blockHash); + event BlockVerified(uint256 indexed blockNumber); - /// @notice Method used to commit an L2 block to be proved. - /// @dev This method is used by the operator when a block is ready to be - /// proved. - /// @param currentBlockCommitment is the committment to the block to be proved. - function commit(bytes32 currentBlockCommitment) external; + /// @notice Initializes the contract. + /// @dev This method is called only once after the contract is deployed. + /// @dev It sets the bridge address. + /// @param bridge the address of the bridge contract. + function initialize(address bridge) external; + + /// @notice Commits to an L2 block. + /// @dev Committing to an L2 block means to store the block's commitment + /// and to publish withdrawals if any. + /// @param blockNumber the number of the block to be committed. + /// @param newL2StateRoot the new L2 state root of the block to be committed. + /// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs + /// of the block to be committed. + function commit( + uint256 blockNumber, + bytes32 newL2StateRoot, + bytes32 withdrawalsLogsMerkleRoot + ) external; /// @notice Method used to verify an L2 block proof. /// @dev This method is used by the operator when a block is ready to be /// verified (this is after proved). + /// @param blockNumber is the number of the block to be verified. /// @param blockProof is the proof of the block to be verified. - function verify(bytes calldata blockProof) external; + function verify(uint256 blockNumber, bytes calldata blockProof) external; } diff --git a/crates/l2/proposer/l1_watcher.rs b/crates/l2/proposer/l1_watcher.rs index 396fab410..3ef75a3fe 100644 --- a/crates/l2/proposer/l1_watcher.rs +++ b/crates/l2/proposer/l1_watcher.rs @@ -5,9 +5,10 @@ use crate::{ eth_client::{transaction::PayloadRLPEncode, EthClient}, }, }; -use bytes::Bytes; use ethereum_rust_blockchain::{constants::TX_GAS_COST, mempool}; -use ethereum_rust_core::types::{EIP1559Transaction, Transaction, TxKind, TxType}; +use ethereum_rust_core::types::{ + PrivilegedL2Transaction, PrivilegedTxType, Transaction, TxKind, TxType, +}; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_rust_rpc::types::receipt::RpcLog; use ethereum_rust_storage::Store; @@ -72,7 +73,7 @@ impl L1Watcher { let logs = self .eth_client .get_logs( - self.last_block_fetched, + self.last_block_fetched + 1, new_last_block, self.address, self.topics[0], @@ -112,9 +113,9 @@ impl L1Watcher { info!("Initiating mint transaction for {beneficiary:#x} with value {mint_value:#x}",); - let mut mint_transaction = EIP1559Transaction { + let mut mint_transaction = PrivilegedL2Transaction { + tx_type: PrivilegedTxType::Deposit, to: TxKind::Call(beneficiary), - data: Bytes::from(b"mint".as_slice()), chain_id: store .get_chain_config() .map_err(|e| L1WatcherError::FailedToRetrieveChainConfig(e.to_string()))? @@ -138,7 +139,7 @@ impl L1Watcher { mint_transaction.gas_limit = TX_GAS_COST.mul(2); mint_transaction.value = mint_value; - let mut payload = vec![TxType::EIP1559 as u8]; + let mut payload = vec![TxType::Privileged as u8]; payload.append(mint_transaction.encode_payload_to_vec().as_mut()); let data = Message::parse(&keccak(payload).0); @@ -151,11 +152,11 @@ impl L1Watcher { let mut encoded_tx = Vec::new(); mint_transaction.encode(&mut encoded_tx); - let mut data = vec![TxType::EIP1559 as u8]; + let mut data = vec![TxType::Privileged as u8]; data.append(&mut encoded_tx); match mempool::add_transaction( - Transaction::EIP1559Transaction(mint_transaction), + Transaction::PrivilegedL2Transaction(mint_transaction), store.clone(), ) { Ok(hash) => { diff --git a/crates/l2/proposer/mod.rs b/crates/l2/proposer/mod.rs index eeb006983..123b8c976 100644 --- a/crates/l2/proposer/mod.rs +++ b/crates/l2/proposer/mod.rs @@ -1,16 +1,19 @@ use crate::utils::{ config::{eth::EthConfig, proposer::ProposerConfig, read_env_file}, eth_client::EthClient, + merkle_tree::merkelize, }; use bytes::Bytes; use errors::ProposerError; use ethereum_rust_blockchain::constants::TX_GAS_COST; -use ethereum_rust_core::types::{Block, EIP1559Transaction, TxKind}; +use ethereum_rust_core::types::{ + Block, EIP1559Transaction, GenericTransaction, Transaction, TxKind, +}; use ethereum_rust_dev::utils::engine_client::{config::EngineApiConfig, EngineClient}; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_rust_rpc::types::fork_choice::{ForkChoiceState, PayloadAttributesV3}; use ethereum_rust_storage::Store; -use ethereum_types::{Address, H256}; +use ethereum_types::{Address, H256, U256}; use keccak_hash::keccak; use libsecp256k1::SecretKey; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -22,8 +25,9 @@ pub mod prover_server; pub mod errors; -const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [241, 79, 203, 200]; -const VERIFY_FUNCTION_SELECTOR: [u8; 4] = [142, 118, 10, 254]; +const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [28, 217, 139, 206]; +const VERIFY_FUNCTION_SELECTOR: [u8; 4] = [133, 133, 44, 228]; + pub struct Proposer { eth_client: EthClient, engine_client: EngineClient, @@ -102,9 +106,37 @@ impl Proposer { "Failed to get block by hash from storage".to_string(), ))?; - let commitment = keccak(block.encode_to_vec()); + let withdrawal_data_hashes: Vec = block + .body + .transactions + .iter() + .filter_map(|tx| match tx { + Transaction::PrivilegedL2Transaction(tx) => tx.get_withdrawal_hash(), + _ => None, + }) + .collect(); + + let withdrawals_logs_merkle_root = if !withdrawal_data_hashes.is_empty() { + merkelize(withdrawal_data_hashes.clone()) + } else { + H256::zero() + }; + + let new_state_root_hash = store + .state_trie(block.header.compute_block_hash()) + .unwrap() + .unwrap() + .hash() + .unwrap(); - match self.send_commitment(commitment).await { + match self + .send_commitment( + block.header.number, + new_state_root_hash, + withdrawals_logs_merkle_root, + ) + .await + { Ok(commit_tx_hash) => { info!( "Sent commitment to block {head_block_hash:#x}, with transaction hash {commit_tx_hash:#x}" @@ -118,15 +150,15 @@ impl Proposer { let proof = Vec::new(); - match self.send_proof(&proof).await { + match self.send_proof(block.header.number, &proof).await { Ok(verify_tx_hash) => { info!( "Sent proof for block {head_block_hash}, with transaction hash {verify_tx_hash:#x}" ); } Err(error) => { - error!("Failed to send commitment to block {head_block_hash:#x}. Manual intervention required: {error}"); - panic!("Failed to send commitment to block {head_block_hash:#x}. Manual intervention required: {error}"); + error!("Failed to send proof to block {head_block_hash:#x}. Manual intervention required: {error}"); + panic!("Failed to send proof to block {head_block_hash:#x}. Manual intervention required: {error}"); } } @@ -206,13 +238,24 @@ impl Proposer { keccak(block.encode_to_vec()) } - pub async fn send_commitment(&self, commitment: H256) -> Result { + pub async fn send_commitment( + &self, + block_number: u64, + new_l2_state_root: H256, + withdrawal_logs_merkle_root: H256, + ) -> Result { info!("Sending commitment"); let mut calldata = Vec::with_capacity(68); calldata.extend(COMMIT_FUNCTION_SELECTOR); - calldata.extend(commitment.0); + let mut block_number_bytes = [0_u8; 32]; + U256::from(block_number).to_big_endian(&mut block_number_bytes); + calldata.extend(block_number_bytes); + calldata.extend(new_l2_state_root.0); + calldata.extend(withdrawal_logs_merkle_root.0); - let commit_tx_hash = self.send_transaction_with_calldata(calldata.into()).await?; + let commit_tx_hash = self + .send_transaction_with_calldata(self.on_chain_proposer_address, calldata.into()) + .await?; info!("Commitment sent: {commit_tx_hash:#x}"); @@ -228,17 +271,26 @@ impl Proposer { Ok(commit_tx_hash) } - pub async fn send_proof(&self, block_proof: &[u8]) -> Result { + pub async fn send_proof( + &self, + block_number: u64, + block_proof: &[u8], + ) -> Result { info!("Sending proof"); let mut calldata = Vec::new(); calldata.extend(VERIFY_FUNCTION_SELECTOR); + let mut block_number_bytes = [0_u8; 32]; + U256::from(block_number).to_big_endian(&mut block_number_bytes); + calldata.extend(block_number_bytes); calldata.extend(H256::from_low_u64_be(32).as_bytes()); calldata.extend(H256::from_low_u64_be(block_proof.len() as u64).as_bytes()); calldata.extend(block_proof); let leading_zeros = 32 - (calldata.len() % 32); calldata.extend(vec![0; leading_zeros]); - let verify_tx_hash = self.send_transaction_with_calldata(calldata.into()).await?; + let verify_tx_hash = self + .send_transaction_with_calldata(self.on_chain_proposer_address, calldata.into()) + .await?; info!("Proof sent: {verify_tx_hash:#x}"); @@ -254,9 +306,13 @@ impl Proposer { Ok(verify_tx_hash) } - async fn send_transaction_with_calldata(&self, calldata: Bytes) -> Result { + async fn send_transaction_with_calldata( + &self, + to: Address, + calldata: Bytes, + ) -> Result { let mut tx = EIP1559Transaction { - to: TxKind::Call(self.on_chain_proposer_address), + to: TxKind::Call(to), data: calldata, max_fee_per_gas: self.eth_client.get_gas_price().await?.as_u64(), nonce: self.eth_client.get_nonce(self.l1_address).await?, @@ -264,9 +320,12 @@ impl Proposer { ..Default::default() }; + let mut generic_tx = GenericTransaction::from(tx.clone()); + generic_tx.from = self.l1_address; + tx.gas_limit = self .eth_client - .estimate_gas(tx.clone()) + .estimate_gas(generic_tx) .await? .saturating_add(TX_GAS_COST); diff --git a/crates/l2/utils/eth_client/errors.rs b/crates/l2/utils/eth_client/errors.rs index bdfd10b70..26915ee7a 100644 --- a/crates/l2/utils/eth_client/errors.rs +++ b/crates/l2/utils/eth_client/errors.rs @@ -18,6 +18,8 @@ pub enum EthClientError { GetNonceError(#[from] GetNonceError), #[error("eth_blockNumber request error: {0}")] GetBlockNumberError(#[from] GetBlockNumberError), + #[error("eth_getBlockByHash request error: {0}")] + GetBlockByHashError(#[from] GetBlockByHashError), #[error("eth_getLogs request error: {0}")] GetLogsError(#[from] GetLogsError), #[error("eth_getTransactionReceipt request error: {0}")] @@ -26,6 +28,8 @@ pub enum EthClientError { FailedToSerializeRequestBody(String), #[error("Failed to deserialize response body: {0}")] GetBalanceError(#[from] GetBalanceError), + #[error("eth_getTransactionByHash request error: {0}")] + GetTransactionByHashError(#[from] GetTransactionByHashError), } #[derive(Debug, thiserror::Error)] @@ -100,6 +104,18 @@ pub enum GetBlockNumberError { ParseIntError(#[from] std::num::ParseIntError), } +#[derive(Debug, thiserror::Error)] +pub enum GetBlockByHashError { + #[error("{0}")] + ReqwestError(#[from] reqwest::Error), + #[error("{0}")] + SerdeJSONError(#[from] serde_json::Error), + #[error("{0}")] + RPCError(String), + #[error("{0}")] + ParseIntError(#[from] std::num::ParseIntError), +} + #[derive(Debug, thiserror::Error)] pub enum GetLogsError { #[error("{0}")] @@ -135,3 +151,15 @@ pub enum GetBalanceError { #[error("{0}")] ParseIntError(#[from] std::num::ParseIntError), } + +#[derive(Debug, thiserror::Error)] +pub enum GetTransactionByHashError { + #[error("{0}")] + ReqwestError(#[from] reqwest::Error), + #[error("{0}")] + SerdeJSONError(#[from] serde_json::Error), + #[error("{0}")] + RPCError(String), + #[error("{0}")] + ParseIntError(#[from] std::num::ParseIntError), +} diff --git a/crates/l2/utils/eth_client/eth_sender.rs b/crates/l2/utils/eth_client/eth_sender.rs index be1269947..69e90bc36 100644 --- a/crates/l2/utils/eth_client/eth_sender.rs +++ b/crates/l2/utils/eth_client/eth_sender.rs @@ -3,7 +3,7 @@ use crate::utils::eth_client::{ EthClient, RpcResponse, }; use bytes::Bytes; -use ethereum_rust_core::types::{EIP1559Transaction, GenericTransaction, TxKind}; +use ethereum_rust_core::types::{EIP1559Transaction, GenericTransaction, TxKind, TxType}; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_rust_rpc::utils::{RpcRequest, RpcRequestId}; use ethereum_types::{Address, U256}; @@ -121,6 +121,15 @@ impl EthClient { data: Bytes, overrides: Overrides, ) -> Result { + let generic_transaction = GenericTransaction { + r#type: TxType::EIP1559, + from, + to: to.clone(), + input: data.clone(), + nonce: overrides.nonce.unwrap_or(self.get_nonce(from).await?), + ..Default::default() + }; + let mut tx = EIP1559Transaction { to, data, @@ -137,7 +146,7 @@ impl EthClient { }; tx.gas_limit = overrides .gas_limit - .unwrap_or(self.estimate_gas(tx.clone()).await?); + .unwrap_or(self.estimate_gas(generic_transaction).await?); Ok(tx) } diff --git a/crates/l2/utils/eth_client/mod.rs b/crates/l2/utils/eth_client/mod.rs index 6c1a9dba6..59f4297a2 100644 --- a/crates/l2/utils/eth_client/mod.rs +++ b/crates/l2/utils/eth_client/mod.rs @@ -1,9 +1,12 @@ use crate::utils::config::eth::EthConfig; use errors::{ - EstimateGasPriceError, EthClientError, GetBalanceError, GetBlockNumberError, GetGasPriceError, - GetLogsError, GetNonceError, GetTransactionReceiptError, SendRawTransactionError, + EstimateGasPriceError, EthClientError, GetBalanceError, GetBlockByHashError, + GetBlockNumberError, GetGasPriceError, GetLogsError, GetNonceError, GetTransactionByHashError, + GetTransactionReceiptError, SendRawTransactionError, +}; +use ethereum_rust_core::types::{ + BlockBody, EIP1559Transaction, GenericTransaction, PrivilegedL2Transaction, TxKind, TxType, }; -use ethereum_rust_core::types::{EIP1559Transaction, TxKind}; use ethereum_rust_rlp::encode::RLPEncode; use ethereum_rust_rpc::{ types::receipt::{RpcLog, RpcReceipt}, @@ -13,7 +16,7 @@ use ethereum_types::{Address, H256, U256}; use keccak_hash::keccak; use libsecp256k1::{sign, Message, SecretKey}; use reqwest::Client; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::json; use transaction::PayloadRLPEncode; @@ -33,7 +36,8 @@ pub struct EthClient { url: String, } -const EIP1559_TX_TYPE: u8 = 2; +// 0x08c379a0 == Error(String) +pub const ERROR_FUNCTION_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0]; impl EthClient { pub fn new(url: &str) -> Self { @@ -88,7 +92,7 @@ impl EthClient { tx: &mut EIP1559Transaction, private_key: SecretKey, ) -> Result { - let mut payload = vec![EIP1559_TX_TYPE]; + let mut payload = vec![TxType::EIP1559 as u8]; payload.append(tx.encode_payload_to_vec().as_mut()); let data = Message::parse(&keccak(payload).0); @@ -101,7 +105,31 @@ impl EthClient { let mut encoded_tx = Vec::new(); tx.encode(&mut encoded_tx); - let mut data = vec![EIP1559_TX_TYPE]; + let mut data = vec![TxType::EIP1559 as u8]; + data.append(&mut encoded_tx); + + self.send_raw_transaction(data.as_slice()).await + } + + pub async fn send_privileged_l2_transaction( + &self, + mut tx: PrivilegedL2Transaction, + private_key: SecretKey, + ) -> Result { + let mut payload = vec![TxType::Privileged as u8]; + payload.append(tx.encode_payload_to_vec().as_mut()); + + let data = Message::parse(&keccak(payload).0); + let signature = sign(&data, &private_key); + + tx.signature_r = U256::from(signature.0.r.b32()); + tx.signature_s = U256::from(signature.0.s.b32()); + tx.signature_y_parity = signature.1.serialize() != 0; + + let mut encoded_tx = Vec::new(); + tx.encode(&mut encoded_tx); + + let mut data = vec![TxType::Privileged as u8]; data.append(&mut encoded_tx); self.send_raw_transaction(data.as_slice()).await @@ -109,7 +137,7 @@ impl EthClient { pub async fn estimate_gas( &self, - transaction: EIP1559Transaction, + transaction: GenericTransaction, ) -> Result { let to = match transaction.to { TxKind::Call(addr) => addr, @@ -117,7 +145,9 @@ impl EthClient { }; let data = json!({ "to": format!("{to:#x}"), - "input": format!("{:#x}", transaction.data), + "input": format!("{:#x}", transaction.input), + "from": format!("{:#x}", transaction.from), + "nonce": format!("{:#x}", transaction.nonce), }); let request = RpcRequest { @@ -136,7 +166,25 @@ impl EthClient { .map_err(EstimateGasPriceError::ParseIntError) .map_err(EthClientError::from), Ok(RpcResponse::Error(error_response)) => { - Err(EstimateGasPriceError::RPCError(error_response.error.message).into()) + let error_data = if let Some(error_data) = error_response.error.data { + if &error_data == "0x" { + "unknown error".to_owned() + } else { + let abi_decoded_error_data = + hex::decode(error_data.strip_prefix("0x").unwrap()).unwrap(); + let string_length = U256::from_big_endian(&abi_decoded_error_data[36..68]); + let string_data = + &abi_decoded_error_data[68..68 + string_length.as_usize()]; + String::from_utf8(string_data.to_vec()).unwrap() + } + } else { + "unknown error".to_owned() + }; + Err(EstimateGasPriceError::RPCError(format!( + "{}: {}", + error_response.error.message, error_data + )) + .into()) } Err(error) => Err(error), } @@ -203,6 +251,25 @@ impl EthClient { } } + pub async fn get_block_by_hash(&self, block_hash: H256) -> Result { + let request = RpcRequest { + id: RpcRequestId::Number(1), + jsonrpc: "2.0".to_string(), + method: "eth_getBlockByHash".to_string(), + params: Some(vec![json!(format!("{block_hash:#x}")), json!(true)]), + }; + + match self.send_request(request).await { + Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) + .map_err(GetBlockByHashError::SerdeJSONError) + .map_err(EthClientError::from), + Ok(RpcResponse::Error(error_response)) => { + Err(GetBlockByHashError::RPCError(error_response.error.message).into()) + } + Err(error) => Err(error), + } + } + pub async fn get_logs( &self, from_block: U256, @@ -294,4 +361,67 @@ impl EthClient { Err(error) => Err(error), } } + + pub async fn get_transaction_by_hash( + &self, + tx_hash: H256, + ) -> Result, EthClientError> { + let request = RpcRequest { + id: RpcRequestId::Number(1), + jsonrpc: "2.0".to_string(), + method: "eth_getTransactionByHash".to_string(), + params: Some(vec![json!(format!("{tx_hash:#x}"))]), + }; + + match self.send_request(request).await { + Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) + .map_err(GetTransactionByHashError::SerdeJSONError) + .map_err(EthClientError::from), + Ok(RpcResponse::Error(error_response)) => { + Err(GetTransactionByHashError::RPCError(error_response.error.message).into()) + } + Err(error) => Err(error), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetTransactionByHashTransaction { + #[serde(default)] + pub chain_id: u64, + #[serde(default)] + pub nonce: u64, + #[serde(default)] + pub max_priority_fee_per_gas: u64, + #[serde(default)] + pub max_fee_per_gas: u64, + #[serde(default)] + pub gas_limit: u64, + #[serde(default)] + pub to: Address, + #[serde(default)] + pub value: U256, + #[serde(default)] + pub data: Vec, + #[serde(default)] + pub access_list: Vec<(Address, Vec)>, + #[serde(default)] + pub r#type: TxType, + #[serde(default)] + pub signature_y_parity: bool, + #[serde(default, with = "ethereum_rust_core::serde_utils::u64::hex_str")] + pub signature_r: u64, + #[serde(default, with = "ethereum_rust_core::serde_utils::u64::hex_str")] + pub signature_s: u64, + #[serde(default)] + pub block_number: U256, + #[serde(default)] + pub block_hash: H256, + #[serde(default)] + pub from: Address, + #[serde(default)] + pub hash: H256, + #[serde(default, with = "ethereum_rust_core::serde_utils::u64::hex_str")] + pub transaction_index: u64, } diff --git a/crates/l2/utils/eth_client/transaction.rs b/crates/l2/utils/eth_client/transaction.rs index beda16455..9e08b8bfe 100644 --- a/crates/l2/utils/eth_client/transaction.rs +++ b/crates/l2/utils/eth_client/transaction.rs @@ -1,4 +1,4 @@ -use ethereum_rust_core::types::EIP1559Transaction; +use ethereum_rust_core::types::{EIP1559Transaction, PrivilegedL2Transaction}; use ethereum_rust_rlp::structs::Encoder; pub trait PayloadRLPEncode { @@ -25,3 +25,20 @@ impl PayloadRLPEncode for EIP1559Transaction { .finish(); } } + +impl PayloadRLPEncode for PrivilegedL2Transaction { + fn encode_payload(&self, buf: &mut dyn bytes::BufMut) { + Encoder::new(buf) + .encode_field(&self.chain_id) + .encode_field(&self.nonce) + .encode_field(&self.max_priority_fee_per_gas) + .encode_field(&self.max_fee_per_gas) + .encode_field(&self.gas_limit) + .encode_field(&self.to) + .encode_field(&self.value) + .encode_field(&self.data) + .encode_field(&self.access_list) + .encode_field(&self.tx_type) + .finish(); + } +} diff --git a/crates/l2/utils/merkle_tree.rs b/crates/l2/utils/merkle_tree.rs new file mode 100644 index 000000000..d52ffb364 --- /dev/null +++ b/crates/l2/utils/merkle_tree.rs @@ -0,0 +1,54 @@ +use keccak_hash::{keccak, H256}; +use tracing::info; + +pub fn merkelize(data: Vec) -> H256 { + info!("Merkelizing {:?}", data); + let mut data = data; + let mut first = true; + while data.len() > 1 || first { + first = false; + data = data + .chunks(2) + .map(|chunk| { + let left = chunk[0]; + let right = *chunk.get(1).unwrap_or(&left); + keccak([left.as_bytes(), right.as_bytes()].concat()) + }) + .collect(); + } + data[0] +} + +pub fn merkle_proof(data: Vec, base_element: H256) -> Option> { + if !data.contains(&base_element) { + return None; + } + + let mut proof = vec![]; + let mut data = data; + + let mut target_hash = base_element; + let mut first = true; + while data.len() > 1 || first { + first = false; + let current_target = target_hash; + data = data + .chunks(2) + .map(|chunk| { + let left = chunk[0]; + let right = *chunk.get(1).unwrap_or(&left); + let result = keccak([left.as_bytes(), right.as_bytes()].concat()); + if left == current_target { + proof.push(right); + target_hash = result; + } else if right == current_target { + proof.push(left); + target_hash = result; + } + result + }) + .collect(); + } + + Some(proof) +} diff --git a/crates/l2/utils/mod.rs b/crates/l2/utils/mod.rs index 134401ade..56e2b0a25 100644 --- a/crates/l2/utils/mod.rs +++ b/crates/l2/utils/mod.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod config; pub mod eth_client; +pub mod merkle_tree; pub fn secret_key_deserializer<'de, D>(deserializer: D) -> Result where diff --git a/crates/networking/rpc/eth/fee_market.rs b/crates/networking/rpc/eth/fee_market.rs index 759b41d6e..bea60a435 100644 --- a/crates/networking/rpc/eth/fee_market.rs +++ b/crates/networking/rpc/eth/fee_market.rs @@ -200,6 +200,9 @@ impl FeeHistoryRequest { Transaction::EIP4844Transaction(t) => t .max_priority_fee_per_gas .min(t.max_fee_per_gas.saturating_sub(base_fee_per_gas)), + Transaction::PrivilegedL2Transaction(t) => t + .max_priority_fee_per_gas + .min(t.max_fee_per_gas.saturating_sub(base_fee_per_gas)), }) .collect(); diff --git a/crates/networking/rpc/types/transaction.rs b/crates/networking/rpc/types/transaction.rs index 7b75a6dc8..6e140d4ae 100644 --- a/crates/networking/rpc/types/transaction.rs +++ b/crates/networking/rpc/types/transaction.rs @@ -2,7 +2,7 @@ use ethereum_rust_core::{ serde_utils, types::{ BlobsBundle, BlockHash, BlockNumber, EIP1559Transaction, EIP2930Transaction, - EIP4844Transaction, LegacyTransaction, Transaction, + EIP4844Transaction, LegacyTransaction, PrivilegedL2Transaction, Transaction, }, Address, H256, }; @@ -50,6 +50,7 @@ pub enum SendRawTransactionRequest { EIP2930(EIP2930Transaction), EIP1559(EIP1559Transaction), EIP4844(WrappedEIP4844Transaction), + PriviligedL2(PrivilegedL2Transaction), } // NOTE: We might move this transaction definitions to `core/types/transactions.rs` later on. @@ -87,6 +88,9 @@ impl SendRawTransactionRequest { SendRawTransactionRequest::EIP1559(t) => Transaction::EIP1559Transaction(t.clone()), SendRawTransactionRequest::EIP2930(t) => Transaction::EIP2930Transaction(t.clone()), SendRawTransactionRequest::EIP4844(t) => Transaction::EIP4844Transaction(t.tx.clone()), + SendRawTransactionRequest::PriviligedL2(t) => { + Transaction::PrivilegedL2Transaction(t.clone()) + } } } @@ -113,6 +117,8 @@ impl SendRawTransactionRequest { // EIP4844 0x3 => WrappedEIP4844Transaction::decode(tx_bytes) .map(SendRawTransactionRequest::EIP4844), + 0x7e => PrivilegedL2Transaction::decode(tx_bytes) + .map(SendRawTransactionRequest::PriviligedL2), ty => Err(RLPDecodeError::Custom(format!( "Invalid transaction type: {ty}" ))), diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index e8d27b29c..3cdd94c04 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -607,7 +607,7 @@ impl Store { } // Obtain the storage trie for the given block - fn state_trie(&self, block_hash: BlockHash) -> Result, StoreError> { + pub fn state_trie(&self, block_hash: BlockHash) -> Result, StoreError> { let Some(header) = self.get_block_header_by_hash(block_hash)? else { return Ok(None); }; diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 61ddaf423..8646baf0b 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -24,6 +24,7 @@ thiserror.workspace = true hex.workspace = true lazy_static.workspace = true cfg-if.workspace = true +tracing.workspace = true serde.workspace = true bincode = "1" diff --git a/crates/vm/mods.rs b/crates/vm/mods.rs index 6f7be1a14..57297739c 100644 --- a/crates/vm/mods.rs +++ b/crates/vm/mods.rs @@ -1,7 +1,11 @@ use revm::{ primitives::{EVMError, Spec}, - Context, Database, + Context, Database, FrameResult, }; +use revm_primitives::{Address, TxKind, U256}; +use tracing::info; + +use crate::{DEPOSIT_MAGIC_DATA, WITHDRAWAL_MAGIC_DATA}; pub fn deduct_caller( context: &mut revm::Context, @@ -15,8 +19,15 @@ pub fn deduct_caller( // If the transaction is a deposit with a `mint` value, add the mint value // in wei to the caller's balance. This should be persisted to the database // prior to the rest of execution. - if context.evm.inner.env.tx.data == *b"mint".as_slice() { - caller_account.info.balance += context.evm.inner.env.tx.value; + if context.evm.inner.env.tx.caller == Address::ZERO + && context.evm.inner.env.tx.data == *DEPOSIT_MAGIC_DATA + { + info!("TX from privileged account with `mint` data"); + caller_account.info.balance = caller_account + .info + .balance + // .saturating_add(context.evm.inner.env.tx.value) + .saturating_add(U256::from(U256::MAX)); } // deduct gas cost from caller's account. revm::handler::mainnet::deduct_caller_inner::( @@ -29,8 +40,40 @@ pub fn deduct_caller( pub fn validate_tx_against_state( context: &mut Context, ) -> Result<(), EVMError> { - if context.evm.inner.env.tx.data == *b"mint".as_slice() { + if context.evm.inner.env.tx.caller == Address::ZERO { return Ok(()); } revm::handler::mainnet::validate_tx_against_state::(context) } + +pub fn last_frame_return( + context: &mut Context, + frame_result: &mut FrameResult, +) -> Result<(), EVMError> { + match context.evm.inner.env.tx.transact_to { + TxKind::Call(address) if address == Address::ZERO => { + if context + .evm + .inner + .env + .tx + .data + .starts_with(WITHDRAWAL_MAGIC_DATA) + && frame_result.interpreter_result().is_ok() + { + info!("TX to privileged account with `burn` data"); + let mut destination_account = context + .evm + .inner + .journaled_state + .load_account(address, &mut context.evm.inner.db)?; + destination_account.info.balance = destination_account + .info + .balance + .saturating_sub(context.evm.inner.env.tx.value); + } + } + _ => {} + } + revm::handler::mainnet::last_frame_return::(context, frame_result) +} diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 2c751174d..617a48882 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -9,8 +9,8 @@ use std::cmp::min; use ethereum_rust_core::{ types::{ - AccountInfo, Block, BlockHash, BlockHeader, Fork, GenericTransaction, Receipt, Transaction, - TxKind, Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, + AccountInfo, Block, BlockHash, BlockHeader, Fork, GenericTransaction, PrivilegedTxType, + Receipt, Transaction, TxKind, Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, }, Address, BigEndianHash, H256, U256, }; @@ -27,7 +27,8 @@ use revm::{ use revm_inspectors::access_list::AccessListInspector; // Rename imported types for clarity use revm_primitives::{ - ruint::Uint, AccessList as RevmAccessList, AccessListItem, FixedBytes, TxKind as RevmTxKind, + ruint::Uint, AccessList as RevmAccessList, AccessListItem, Bytes, FixedBytes, + TxKind as RevmTxKind, }; // Export needed types pub use errors::EvmError; @@ -36,6 +37,9 @@ pub use revm::primitives::{Address as RevmAddress, SpecId}; type AccessList = Vec<(Address, Vec)>; +pub const WITHDRAWAL_MAGIC_DATA: &[u8] = b"burn"; +pub const DEPOSIT_MAGIC_DATA: &[u8] = b"mint"; + /// State used when running the EVM // Encapsulates state behaviour to be agnostic to the evm implementation for crate users pub struct EvmState(revm::db::State); @@ -145,6 +149,7 @@ fn run_evm( let mut evm_handler = Handler::new(HandlerCfg::new(SpecId::LATEST)); evm_handler.pre_execution.deduct_caller = Arc::new(mods::deduct_caller::); evm_handler.validation.tx_against_state = Arc::new(mods::validate_tx_against_state::); + evm_handler.execution.last_frame_return = Arc::new(mods::last_frame_return::); // TODO: Override `end` function. We should deposit even if we revert. // evm_handler.pre_execution.end evm_handler @@ -427,15 +432,41 @@ pub fn tx_env(tx: &Transaction) -> TxEnv { None => None, }; TxEnv { - caller: RevmAddress(tx.sender().0.into()), + caller: match tx { + Transaction::PrivilegedL2Transaction(tx) if tx.tx_type == PrivilegedTxType::Deposit => { + RevmAddress::ZERO + } + _ => RevmAddress(tx.sender().0.into()), + }, gas_limit: tx.gas_limit(), gas_price: RevmU256::from(tx.gas_price()), - transact_to: match tx.to() { - TxKind::Call(address) => RevmTxKind::Call(address.0.into()), - TxKind::Create => RevmTxKind::Create, + transact_to: match tx { + Transaction::PrivilegedL2Transaction(tx) + if tx.tx_type == PrivilegedTxType::Withdrawal => + { + RevmTxKind::Call(RevmAddress::ZERO) + } + _ => match tx.to() { + TxKind::Call(address) => RevmTxKind::Call(address.0.into()), + TxKind::Create => RevmTxKind::Create, + }, }, value: RevmU256::from_limbs(tx.value().0), - data: tx.data().clone().into(), + data: match tx { + Transaction::PrivilegedL2Transaction(tx) => match tx.tx_type { + PrivilegedTxType::Deposit => DEPOSIT_MAGIC_DATA.into(), + PrivilegedTxType::Withdrawal => { + let to = match tx.to { + TxKind::Call(to) => to, + _ => Address::zero(), + }; + [Bytes::from(WITHDRAWAL_MAGIC_DATA), Bytes::from(to.0)] + .concat() + .into() + } + }, + _ => tx.data().clone().into(), + }, nonce: Some(tx.nonce()), chain_id: tx.chain_id(), access_list: tx From 2c5b33f4c01c5809c36d85de7f84b5f394aa81a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:40:38 -0300 Subject: [PATCH 22/49] chore(l2): update README (#994) **Motivation** **Description** --- README.md | 24 ++++++++++++++++++------ crates/l2/docs/README.md | 5 ++++- crates/vm/levm/README.md | 4 +--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3c4c3a933..7801c0b77 100644 --- a/README.md +++ b/README.md @@ -262,13 +262,24 @@ Ethereum Rust supports the following command line arguments: # Lambda Ethereum Rust L2 +In this mode, the Ethereum Rust code is repurposed to run a rollup that settles on Ethereum as the L1. + The main differences between this mode and regular Ethereum Rust are: -- There is no consensus, only one sequencer proposes blocks for the network. +- There is no consensus, the node is turned into a sequencer that proposes blocks for the network. - Block execution is proven using a RISC-V zkVM and its proofs are sent to L1 for verification. - A set of Solidity contracts to be deployed to the L1 are included as part of network initialization. - Two new types of transactions are included: deposits (native token mints) and withdrawals. +At a high level, the following new parts are added to the node: + +- A `proposer` component, in charge of continually creating new blocks from the mempool transactions. This replaces the regular flow that an Ethereum L1 node has, where new blocks come from the consensus layer through the `forkChoiceUpdate` -> `getPayload` -> `NewPayload` Engine API flow in communication with the consensus layer. +- A `prover` subsystem, which itself consists of two parts: + - A `proverClient` that takes new blocks from the node, proves them, then sends the proof back to the node to send to the L1. This is a separate binary running outside the node, as proving has very different (and higher) hardware requirements than the sequencer. + - A `proverServer` component inside the node that communicates with the prover, sending witness data for proving and receiving proofs for settlement on L1. +- L1 contracts with functions to commit to new state and then verify the state transition function, only advancing the state of the L2 if the proof verifies. It also has functionality to process deposits and withdrawals to/from the L2. +- The EVM is lightly modified with new features to process deposits and withdrawals accordingly. + ## Roadmap | Milestone | Description | Status | @@ -414,7 +425,7 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer ## Prerequisites -- [Rust (explained in the repo's main README)](../../README.md) +- [Rust (explained in L1 requirements section above)](#build) - [Docker](https://docs.docker.com/engine/install/) (with [Docker Compose](https://docs.docker.com/compose/install/)) ## How to run @@ -424,8 +435,9 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer > [!IMPORTANT] > Before this step: > -> 1. make sure the Docker daemon is running. -> 2. make sure you have created a `.env` file following the `.env.example` file. +> 1. Make sure you are inside the `crates/l2` directory. +> 2. Make sure the Docker daemon is running. +> 3. Make sure you have created a `.env` file following the `.env.example` file. ``` make init @@ -455,8 +467,8 @@ Most of them are [here](https://github.com/ethpandaops/ethereum-package/blob/mai ## Lambda Ethereum Rust L2 Docs -- [Ethereum Rust L2 Docs](./docs/README.md) -- [Ethereum Rust L2 CLI Docs](../../cmd/ethereum_rust_l2/README.md) +- [Ethereum Rust L2 Docs](./crates/l2/docs/README.md) +- [Ethereum Rust L2 CLI Docs](./cmd/ethereum_rust_l2/README.md) ## 📚 References and acknowledgements diff --git a/crates/l2/docs/README.md b/crates/l2/docs/README.md index adfc1483d..e1ae9f833 100644 --- a/crates/l2/docs/README.md +++ b/crates/l2/docs/README.md @@ -1,8 +1,11 @@ # Ethereum Rust L2 Docs -Ethereum Rust L2 is composed of three main parts: +For a high level overview of the L2: - [General Overview](./overview.md) + +For more detailed documentation on each part of the system: + - [Proposer](./proposer.md) - [Prover](./prover.md) - [Contracts](./contracts.md) diff --git a/crates/vm/levm/README.md b/crates/vm/levm/README.md index 525ef97b0..e589c125c 100644 --- a/crates/vm/levm/README.md +++ b/crates/vm/levm/README.md @@ -9,7 +9,7 @@ Meaning: - ❌: Work not Started yet Features: -- Opcodes 🏗️ +- Opcodes ✅ - Precompiles 🏗️ - Transaction validation 🏗️ - Pass all EF tests 🏗️ @@ -27,8 +27,6 @@ Features: - Rust - Git -### Running [🇼🇮🇵] - ### Documentation [CallFrame](./docs/callframe.md) From f57520e4de573ed83ced0c333f0471689e8deac9 Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:21:02 -0300 Subject: [PATCH 23/49] fix(l2): cli cmd (#997) **Motivation** fix cli cmd --- crates/l2/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/l2/Makefile b/crates/l2/Makefile index b04aef95d..3fe872317 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -16,11 +16,11 @@ clean: clean-contract-deps ## 🧹 Cleans the localnet restart: restart-local-l1 restart-contract-deps deploy-l1 restart-l2 ## 🔄 Restarts the localnet cli: ## 🛠️ Installs the L2 Lambda Ethereum Rust CLI - cargo install --path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust/Cargo.toml --force + cargo install --path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust_l2/ --force # Variables -ETHEREUM_RUST_PATH=$(shell pwd)/../../ +ETHEREUM_RUST_PATH=$(shell pwd)/../.. ETHEREUM_RUST_BIN_PATH=$(ETHEREUM_RUST_PATH)/target/release/ethereum_rust ETHEREUM_RUST_DEV_DOCKER_COMPOSE_PATH=$(ETHEREUM_RUST_PATH)/crates/blockchain/dev/docker-compose-dev.yaml From 3ecbdb2edd66ace5feec0d884cea9f62a97f8aa4 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:15:40 -0300 Subject: [PATCH 24/49] fix(l2): fix withdraw feature merge (#996) --- Cargo.toml | 2 +- crates/l2/contracts/deployer.rs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6325f8e07..aeb8b3b97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ members = [ "crates/vm/levm/bench/revm_comparison", "crates/l2/", "crates/l2/prover", + "crates/l2/contracts", ] resolver = "2" -exclude = ["crates/l2/contracts"] default-members = [ "cmd/ethereum_rust", "cmd/ethereum_rust_l2", diff --git a/crates/l2/contracts/deployer.rs b/crates/l2/contracts/deployer.rs index 981e5bc2c..e52180701 100644 --- a/crates/l2/contracts/deployer.rs +++ b/crates/l2/contracts/deployer.rs @@ -105,10 +105,6 @@ async fn deploy_contracts( deployer_private_key: SecretKey, eth_client: &EthClient, ) -> (Address, Address) { - if std::fs::exists("contracts/solc_out").expect("Could not determine if solc_out exists") { - std::fs::remove_dir_all("contracts/solc_out").expect("Failed to remove solc_out"); - } - let overrides = Overrides { gas_limit: Some(GAS_LIMIT_MINIMUM * GAS_LIMIT_ADJUSTMENT_FACTOR), gas_price: Some(1_000_000_000), From f88e7fff13c3eed909413e283c4226c74675747c Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:29:39 -0300 Subject: [PATCH 25/49] feat(core): add caching to CI (#993) **Motivation** To speed up the CI we can implement caching. **Description** Implement `rust-cache` job. --- .github/workflows/ci.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2b93158c5..5e2648116 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,8 +6,8 @@ on: pull_request: branches: ["**"] paths-ignore: - - 'README.md' - - 'LICENSE' + - "README.md" + - "LICENSE" - "**/README.md" - "**/docs/**" @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Rustup toolchain install uses: dtolnay/rust-toolchain@stable @@ -32,6 +32,9 @@ jobs: toolchain: ${{ env.RUST_VERSION }} components: rustfmt, clippy + - name: Add Rust Cache + uses: Swatinem/rust-cache@v2 + - name: Run cargo check run: cargo check @@ -52,13 +55,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Rustup toolchain install uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} + - name: Caching + uses: Swatinem/rust-cache@v2 + - name: Download test vectors run: | make download-test-vectors From 08da47f9fb8e57443422298847f7989808ed36c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Rodr=C3=ADguez=20Chatruc?= <49622509+jrchatruc@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:51:23 -0300 Subject: [PATCH 26/49] docs(l2): improve L2 README tables (#995) **Motivation** **Description** --- README.md | 108 ++++++++++++++++++++++++------------------------------ 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 7801c0b77..b07f34874 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ At a high level, the following new parts are added to the node: | Milestone | Description | Status | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | 0 | Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds on the L2. | ✅ | -| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | 🏗️ | +| 1 | The network supports basic L2 functionality, allowing users to deposit and withdraw funds to join and exit the network, while also interacting with the network as they do normally on the Ethereum network (deploying contracts, sending transactions, etc). | ✅ | | 2 | The block execution is proven with a RISC-V zkVM and the proof is verified by the Verifier L1 contract. | 🏗️ | | 3 | The network now commits to state diffs instead of the full state, lowering the commit transactions costs. These diffs are also submitted in compressed form, further reducing costs. It also supports EIP 4844 for L1 commit transactions, which means state diffs are sent as blob sidecars instead of calldata. | ❌ | | 4 | The L2 can also be deployed using a custom native token, meaning that a certain ERC20 can be the common currency that's used for paying network fees. | ❌ | @@ -300,19 +300,14 @@ Users can deposit Eth in the L1 (Ethereum) and receive the corresponding funds o #### Status -| | Name | Description | Status | -| --------- | ----------------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | `CommonBridge` | Deposit method implementation | ✅ | -| | `OnChainOperator` | Commit and verify methods (placeholders for this stage) | ✅ | -| VM | | Adapt EVM to handle deposits | ✅ | -| Proposer | `Proposer` | Proposes new blocks to be executed | ✅ | -| | `L1Watcher` | Listens for and handles L1 deposits | ✅ | -| | `L1TxSender` | commits new block proposals and sends block execution proofs to be verified | ✅ | -| | Deposit transactions handling | new transaction type for minting funds corresponding to deposits | ✅ | -| CLI | `stack` | Support commands for initializing the network | ✅ | -| CLI | `config` | Support commands for network config management | ✅ | -| CLI | `wallet deposit` | Support command por depositing funds on L2 | ✅ | -| CLI | `wallet transfer` | Support command for transferring funds on L2 | ✅ | +| Description | Status | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Add a new `privilegedL2Transaction` type for deposits on the sequencer, which mints funds on the L2 and sends the processed deposits to the L1 on each `commit` transaction. | ✅ | +| Adapt the EVM to handle deposit transactions (minting money to the corresponding account). | ✅ | +| Make the proposer continuously build and execute new blocks by internally calling the appropriate Engine API methods | ✅ | +| Add an `L1Watcher` component that listens for and handles L1 deposits, executing the appropriate mint transaction on the L2. | ✅ | +| Add a `proposer` component that commits to new blocks and sends block execution proofs to the L1. | ✅ | +| Add a CLI with commands for initializing the network, managing network config, operating in the L2 and allowing for deposits. | ✅ | ### Milestone 1: MVP @@ -320,13 +315,11 @@ The network supports basic L2 functionality, allowing users to deposit and withd #### Status -| | Name | Description | Status | -| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------ | -| Contracts | `CommonBridge` | Withdraw method implementation | ❌ | -| | `OnChainOperator` | Commit and verify implementation | 🏗️ | -| | `Verifier` | verifier | 🏗️ | -| | Withdraw transactions handling | New transaction type for burning funds on L2 and unlock funds on L1 | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | +| Description | Status | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Add a new `privilegedL2Transaction` type for withdrawals on the sequencer, which burns funds on L2 and unlocks funds on L1 by sending the merkle root of each block's withdrawals. | ✅ | +| Add a `claimWithdrawal` function on the `commonBridge` so users can claim their funds on L1 after the L2 withdrawal transaction is finalized on L1. | ✅ | +| Add a CLI feature for making withdrawals | ✅ | ### Milestone 2: Block Execution Proofs @@ -334,12 +327,12 @@ The L2's block execution is proven with a RISC-V zkVM and the proof is verified #### Status -| | Name | Description | Status | -| --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------ | -| VM | | `Return` the storage touched on block execution to pass the prover as a witness | 🏗️ | -| Contracts | `OnChainOperator` | Call the actual SNARK proof verification on the `verify` function implementation | 🏗️ | -| Proposer | `Prover Server` | Feeds the `Prover Client` with block data to be proven and delivers proofs to the `L1TxSender` for L1 verification | 🏗️ | -| Prover | `Prover Client` | Asks for block execution data to prove, generates proofs of execution and submits proofs to the `Prover Server` | 🏗️ | +| Task Description | Status | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| On the EVM, return all storage touched during block execution to pass to the prover as a witness | ✅ | +| Make the `onChainproposer` L1 contract verify the SNARK proof on the `verify` function. | 🏗️ | +| Add a `proverClient` binary that asks the sequencer for witness data to prove, generates proofs of execution and submits proofs to the `proverServer` component (see below) | 🏗️ | +| Add a `proverServer` component that feeds the `proverClient` with block witness data to be proven and delivers proofs to the `proposer` to send the L1 transaction for block verification | ✅ | ### Milestone 3: State diffs + Data compression + EIP 4844 (Blobs) @@ -349,17 +342,13 @@ It also supports EIP 4844 for L1 commit transactions, which means state diffs ar #### Status -| | Name | Description | Status | -| --------- | ------------------- | --------------------------------------------------------------------------- | ------ | -| Contracts | OnChainOperator | Differentiate whether to execute in calldata or blobs mode | ❌ | -| Prover | RISC-V zkVM | Prove state diffs compression | ❌ | -| | RISC-V zkVM | Adapt state proofs | ❌ | -| VM | | The VM should return which storage slots were modified | ❌ | -| Proposer | Prover Server | Sends state diffs to the prover | ❌ | -| | L1TxSender | Differentiate whether to send the commit transaction with calldata or blobs | ❌ | -| | | Add program for proving blobs | ❌ | -| CLI | `reconstruct-state` | Add a command for reconstructing the state | ❌ | -| | `init` | Adapt network initialization to either send blobs or calldata | ❌ | +| Task Description | Status | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| The sequencer sends state diffs to the prover instead of full transaction data. | ❌ | +| On the prover, prove the state diffs compression | ❌ | +| On the `proposer`, send the state diffs through a blob in a EIP 4844 transaction. | ❌ | +| Adapt the prover to prove a KZG commitment to the state diff and use the point evaluation precompile to show that the blob sent to the L1 is indeed the correct one through a proof of equivalence protocol | ❌ | +| Add a command to the CLI to reconstructing the full L2 state from all the blob data on the L1. | ❌ | ### Milestone 4: Custom Native token @@ -367,13 +356,11 @@ The L2 can also be deployed using a custom native token, meaning that a certain #### Status -| | Name | Description | Status | -| --- | -------------- | ----------------------------------------------------------------------------------------- | ------ | -| | `CommonBridge` | For native token withdrawals, infer the native token and reimburse the user in that token | ❌ | -| | `CommonBridge` | For native token deposits, msg.value = 0 and valueToMintOnL2 > 0 | ❌ | -| | `CommonBridge` | Keep track of chain's native token | ❌ | -| | `deposit` | Handle native token deposits | ❌ | -| | `withdraw` | Handle native token withdrawals | ❌ | +| Task Description | Status | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| On the `commonBridge`, keep track of the chain's native token. For custom native token withdrawals, infer the native token and reimburse the user in that token | ❌ | +| On the `commonBridge`, for custom native token deposits, `msg.value` should always be zero, and the amount of the native token to mint should be a new `valueToMintOnL2` argument. The amount should be deducted from the caller thorugh a `transferFrom`. | ❌ | +| On the CLI, add support for custom native token deposits and withdrawals | ❌ | ### Milestone 5: Security (TEEs and Multi Prover support) @@ -381,11 +368,11 @@ The L2 has added security mechanisms in place, running on Trusted Execution Envi #### Status -| | Name | Description | Status | -| --------- | ---- | ---------------------------------------------------- | ------ | -| VM/Prover | | Support proving with multiple different zkVMs | ❌ | -| Contracts | | Support verifying multiple different zkVM executions | ❌ | -| VM | | Support running the operator on a TEE environment | ❌ | +| Task Description | Status | +| ------------------------------------------------------------------------------------------ | ------ | +| Support proving with multiple different zkVMs | ❌ | +| Support verifying multiple different zkVM executions on the `onChainProposer` L1 contract. | ❌ | +| Support running the operator on a TEE environment | ❌ | ### Milestone 6: Account Abstraction @@ -393,8 +380,8 @@ The L2 supports native account abstraction following EIP 7702, allowing for cust #### Status -| | Name | Description | Status | -| --- | ---- | ----------- | ------ | +| Task Description | Status | +| ---------------- | ------ | TODO: Expand on account abstraction tasks. @@ -404,9 +391,9 @@ The network can be run as a Based Rollup, meaning sequencing is done by the Ethe #### Status -| | Name | Description | Status | -| --- | ----------------- | ------------------------------------------------------------------------------ | ------ | -| | `OnChainOperator` | Add methods for proposing new blocks so the sequencing can be done from the L1 | ❌ | +| Task Description | Status | +| ------------------------------------------------------------------------------------------------------------------- | ------ | +| Add methods on the `onChainProposer` L1 contract for proposing new blocks so the sequencing can be done from the L1 | ❌ | TODO: Expand on this. @@ -416,12 +403,11 @@ The L2 can be initialized in Validium Mode, meaning the Data Availability layer #### Status -| | Name | Description | Status | -| --------- | ------------- | ---------------------------------------------------- | ------ | -| Contracts | BlockExecutor | Do not check data availability in Validium mode | ❌ | -| Proposer | L1TxSender | Do not send data in commit transactions | ❌ | -| CLI | `init` | Adapt network initialization to support Validium L2s | ❌ | -| Misc | | Add a DA integration example for Validium mode | ❌ | +| Task Description | Status | +| --------------------------------------------------------------------------------------------------------------------------------- | ------ | +| Make the `onChainProposer` L1 contract conditional on the data availability mode. On validium, don't check for data availability. | ❌ | +| The sequencer can initialize on Validium mode, not sending state diff data on `commit` transactions | ❌ | +| Add a DA integration example for Validium mode | ❌ | ## Prerequisites From ae772010c27416a35185f254f9aba692f2f7c89c Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Mon, 28 Oct 2024 18:57:03 -0300 Subject: [PATCH 27/49] fix(l1): map the actual address when sharing bootstrap node info (#987) **Motivation** Running in any position of the `network_params.yaml` except the last generates an error for the following node when checking our enode url **Description** When setting up the `Node` struct, we were using `0.0.0.0` instead of the actual ip, this PR maps `0.0.0.0` to the local ip there. This force us to specify the IP of the node in case of using a public one. The local IP is used due to it being the one needed in Kurtosis. If this could be an issue for other common forms of running the Node outside of the CI checks we'll need to go with a more complex solution using nat for example. Also if we prefer to not use a crate and obtain it by ourselves I could go that path! _Note regarding `tx_spammer`: Unfortunately `tx_spammer` is not working as we expected, being the first node in the configuration file completely removed transactions from the kurtosis run (no nodes have them now), we need to create another issue and investigate that further._ Resolves #847 --- cmd/ethereum_rust/Cargo.toml | 1 + cmd/ethereum_rust/ethereum_rust.rs | 13 +++++++++++-- test_data/network_params.yaml | 6 +++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cmd/ethereum_rust/Cargo.toml b/cmd/ethereum_rust/Cargo.toml index 0d0f2ec47..cd14bad00 100644 --- a/cmd/ethereum_rust/Cargo.toml +++ b/cmd/ethereum_rust/Cargo.toml @@ -26,6 +26,7 @@ tokio = { version = "1.38.0", features = ["full"] } anyhow = "1.0.86" rand = "0.8.5" k256 = { version = "0.13.3", features = ["ecdh"] } +local-ip-address = "0.6" tokio-util.workspace = true cfg-if = "1.0.0" diff --git a/cmd/ethereum_rust/ethereum_rust.rs b/cmd/ethereum_rust/ethereum_rust.rs index af94e955f..4f10fd1bc 100644 --- a/cmd/ethereum_rust/ethereum_rust.rs +++ b/cmd/ethereum_rust/ethereum_rust.rs @@ -8,6 +8,7 @@ use ethereum_rust_net::node_id_from_signing_key; use ethereum_rust_net::types::Node; use ethereum_rust_storage::{EngineType, Store}; use k256::ecdsa::SigningKey; +use local_ip_address::local_ip; use std::future::IntoFuture; use std::path::Path; use std::str::FromStr as _; @@ -15,7 +16,7 @@ use std::time::Duration; use std::{ fs::File, io, - net::{SocketAddr, ToSocketAddrs}, + net::{Ipv4Addr, SocketAddr, ToSocketAddrs}, }; use tokio_util::task::TaskTracker; use tracing::{info, warn}; @@ -147,8 +148,16 @@ async fn main() { let signer = SigningKey::from_slice(key_bytes.as_bytes()).unwrap(); let local_node_id = node_id_from_signing_key(&signer); + // TODO: If hhtp.addr is 0.0.0.0 we get the local ip as the one of the node, otherwise we use the provided one. + // This is fine for now, but we might need to support more options in the future. + let p2p_node_ip = if udp_socket_addr.ip() == Ipv4Addr::new(0, 0, 0, 0) { + local_ip().expect("Failed to get local ip") + } else { + udp_socket_addr.ip() + }; + let local_p2p_node = Node { - ip: udp_socket_addr.ip(), + ip: p2p_node_ip, udp_port: udp_socket_addr.port(), tcp_port: tcp_socket_addr.port(), node_id: local_node_id, diff --git a/test_data/network_params.yaml b/test_data/network_params.yaml index b20776e53..cbb4ac117 100644 --- a/test_data/network_params.yaml +++ b/test_data/network_params.yaml @@ -1,13 +1,13 @@ participants: + - el_type: ethereumrust + cl_type: lighthouse + validator_count: 32 - el_type: geth cl_type: lighthouse validator_count: 32 - el_type: geth cl_type: prysm validator_count: 32 - - el_type: ethereumrust - cl_type: lighthouse - validator_count: 32 additional_services: - assertoor From 2f422982b2d936ddc0a77f55e2c7a920ba4d751d Mon Sep 17 00:00:00 2001 From: fmoletta <99273364+fmoletta@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:30:19 -0300 Subject: [PATCH 28/49] fix(l1): fix logic in `Transaction::effective_gas_tip` (#1001) **Motivation** There is a logic bug in the `Transaction` method `effective_gas_tip` **Description** Fix logic bug in `Transaction` method `effective_gas_tip` Closes #issue_number --- crates/common/types/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index 882407f43..d14c39599 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -766,7 +766,7 @@ impl Transaction { }; self.gas_fee_cap() .checked_sub(base_fee) - .map(|tip| min(tip, self.gas_fee_cap())) + .map(|tip| min(tip, self.gas_tip_cap())) } /// Returns whether the transaction is replay-protected. From f92b3780cf69f07e845b8ab8862d9026292a2184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 29 Oct 2024 10:54:03 -0300 Subject: [PATCH 29/49] chore(l2): replace solidity custom errors with `require()` (#1003) **Motivation** It's easier to parse a string error rather than have the function selector for all posible custom errors. **Description** Replace `AmountToDepositIsZero` custom error with a `require()` Closes #999 --- crates/l2/contracts/src/l1/CommonBridge.sol | 5 ++--- crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index 304970abb..c4fc4cc29 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -55,9 +55,8 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { /// @inheritdoc ICommonBridge function deposit(address to) public payable { - if (msg.value == 0) { - revert AmountToDepositIsZero(); - } + require(msg.value > 0, "CommonBridge: amount to deposit is zero"); + // TODO: Build the tx. bytes32 l2MintTxHash = keccak256(abi.encodePacked("dummyl2MintTxHash")); emit DepositInitiated(msg.value, to, l2MintTxHash); diff --git a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol index 693b8a434..ae8ed1259 100644 --- a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol +++ b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol @@ -41,9 +41,6 @@ interface ICommonBridge { uint256 indexed claimedAmount ); - /// @notice Error for when the deposit amount is 0. - error AmountToDepositIsZero(); - /// @notice Initializes the contract. /// @dev This method is called only once after the contract is deployed. /// @dev It sets the OnChainProposer address. From 08f5a7ccf3e8124998abb6837a2bd4019346257d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 29 Oct 2024 11:03:02 -0300 Subject: [PATCH 30/49] feat(l2): deposit verification (#1000) **Motivation** Deposits information must be saved on L1 until it gets verified so we can trust the proposer don't put any invalid mint transaction. **Description** When send the commit, the proposer will also send the deposit logs, a hash with format `N || keccak(d1 || d2 || ... || dN)[2..32]`, with `N` being the number of deposits executed encoded in 2 bytes, and `dX = keccack(to, value)` of the transaction `X`. That info is stored on an array until a `verify` call removes it. Closes #977 --- cmd/ethereum_rust_l2/src/commands/wallet.rs | 2 +- crates/l2/contracts/src/l1/CommonBridge.sol | 19 ++++++++ .../l2/contracts/src/l1/OnChainProposer.sol | 35 ++++++++++++--- .../src/l1/interfaces/ICommonBridge.sol | 8 ++++ .../src/l1/interfaces/IOnChainProposer.sol | 3 +- crates/l2/docs/state_diffs.md | 6 ++- crates/l2/proposer/l1_watcher.rs | 26 +++++++---- crates/l2/proposer/mod.rs | 43 +++++++++++++++++-- crates/l2/utils/config/l1_watcher.rs | 1 + 9 files changed, 122 insertions(+), 21 deletions(-) diff --git a/cmd/ethereum_rust_l2/src/commands/wallet.rs b/cmd/ethereum_rust_l2/src/commands/wallet.rs index 00fec9347..e6b10dfcf 100644 --- a/cmd/ethereum_rust_l2/src/commands/wallet.rs +++ b/cmd/ethereum_rust_l2/src/commands/wallet.rs @@ -354,7 +354,7 @@ impl Command { // .estimate_gas(transfer_transaction.clone()) // .await?; - transfer_transaction.gas_limit = 21000 * 2; + transfer_transaction.gas_limit = 21000 * 5; let tx_hash = if l1 { eth_client diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index c4fc4cc29..60f670f30 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -24,6 +24,8 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { /// that the logs were published on L1, and that that block was committed. mapping(uint256 => bytes32) public blockWithdrawalsLogs; + bytes32[] public depositLogs; + address public ON_CHAIN_PROPOSER; modifier onlyOnChainProposer() { @@ -59,6 +61,7 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { // TODO: Build the tx. bytes32 l2MintTxHash = keccak256(abi.encodePacked("dummyl2MintTxHash")); + depositLogs.push(keccak256(abi.encodePacked(to, msg.value))); emit DepositInitiated(msg.value, to, l2MintTxHash); } @@ -66,6 +69,22 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { deposit(msg.sender); } + /// @inheritdoc ICommonBridge + function removeDepositLogs(uint number) public onlyOnChainProposer { + require( + number <= depositLogs.length, + "CommonBridge: number is greater than the length of depositLogs" + ); + + for (uint i = 0; i < depositLogs.length - number; i++) { + depositLogs[i] = depositLogs[i + number]; + } + + for (uint _i = 0; _i < number; _i++) { + depositLogs.pop(); + } + } + /// @inheritdoc ICommonBridge function publishWithdrawals( uint256 withdrawalLogsBlockNumber, diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 17fb12aec..e78e3d0b7 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -10,11 +10,16 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; /// @title OnChainProposer contract. /// @author LambdaClass contract OnChainProposer is IOnChainProposer, ReentrancyGuard { + struct BlockCommitmentInfo { + bytes32 commitmentHash; + bytes32 depositLogs; + } + /// @notice The commitments of the committed blocks. /// @dev If a block is committed, the commitment is stored here. /// @dev If a block was not committed yet, it won't be here. /// @dev It is used by other contracts to verify if a block was committed. - mapping(uint256 => bytes32) public blockCommitments; + mapping(uint256 => BlockCommitmentInfo) public blockCommitments; /// @notice The verified blocks. /// @dev If a block is verified, the block hash is stored here. @@ -45,20 +50,29 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard { function commit( uint256 blockNumber, bytes32 newL2StateRoot, - bytes32 withdrawalsLogsMerkleRoot + bytes32 withdrawalsLogsMerkleRoot, + bytes32 depositLogs ) external override { require( !verifiedBlocks[blockNumber], "OnChainProposer: block already verified" ); require( - blockCommitments[blockNumber] == bytes32(0), + blockCommitments[blockNumber].commitmentHash == bytes32(0), "OnChainProposer: block already committed" ); bytes32 blockCommitment = keccak256( - abi.encode(blockNumber, newL2StateRoot, withdrawalsLogsMerkleRoot) + abi.encode( + blockNumber, + newL2StateRoot, + withdrawalsLogsMerkleRoot, + depositLogs + ) + ); + blockCommitments[blockNumber] = BlockCommitmentInfo( + blockCommitment, + depositLogs ); - blockCommitments[blockNumber] = blockCommitment; if (withdrawalsLogsMerkleRoot != bytes32(0)) { ICommonBridge(BRIDGE).publishWithdrawals( blockNumber, @@ -74,14 +88,23 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard { bytes calldata // blockProof ) external override { require( - blockCommitments[blockNumber] != bytes32(0), + blockCommitments[blockNumber].commitmentHash != bytes32(0), "OnChainProposer: block not committed" ); require( !verifiedBlocks[blockNumber], "OnChainProposer: block already verified" ); + verifiedBlocks[blockNumber] = true; + ICommonBridge(BRIDGE).removeDepositLogs( + // The first 2 bytes are the number of deposits. + uint16(uint256(blockCommitments[blockNumber].depositLogs >> 240)) + ); + + // Remove previous block commitment as it is no longer needed. + delete blockCommitments[blockNumber - 1]; + emit BlockVerified(blockNumber); } } diff --git a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol index ae8ed1259..ca545b7db 100644 --- a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol +++ b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol @@ -54,6 +54,14 @@ interface ICommonBridge { /// @param to, the address in L2 to which the tokens will be minted to. function deposit(address to) external payable; + /// @notice Remove deposit from depositLogs queue. + /// @dev This method is used by the L2 OnChainOperator to remove the deposit + /// logs from the queue after the deposit is verified. + /// @param number of deposit logs to remove. + /// As deposits are processed in order, we don't need to specify + /// the deposit logs to remove, only the number of them. + function removeDepositLogs(uint number) external; + /// @notice Publishes the L2 withdrawals on L1. /// @dev This method is used by the L2 OnChainOperator to publish the L2 /// withdrawals when an L2 block is committed. diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index 7b786e97d..a7feecd83 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -33,7 +33,8 @@ interface IOnChainProposer { function commit( uint256 blockNumber, bytes32 newL2StateRoot, - bytes32 withdrawalsLogsMerkleRoot + bytes32 withdrawalsLogsMerkleRoot, + bytes32 depositLogs ) external; /// @notice Method used to verify an L2 block proof. diff --git a/crates/l2/docs/state_diffs.md b/crates/l2/docs/state_diffs.md index 44aa73b86..289e454ce 100644 --- a/crates/l2/docs/state_diffs.md +++ b/crates/l2/docs/state_diffs.md @@ -37,6 +37,8 @@ The full state diff sent on every block will then be a sequence of bytes encoded - Note that values `8` and `16` are mutually exclusive, and if `type` is greater or equal to `4`, then the address is a contract. Each address can only appear once in the list. - Next the `WithdrawalLogs` field: - First two bytes are the number of entries, then come the tuples `(to_u160, amount_u256, tx_hash_u256)`. +- Next the `DepositLogs` field: + - First two bytes are the number of entries, then come the last 30 bytes of the `keccak` encoding of the concatenation of deposits with form `keccack256(to_u160 || value_u256)`. - In case of the only changes on an account are produced by withdrawals, the `ModifiedAccounts` for that address field must be omitted. In this case, the state diff can be computed by incrementing the nonce in one unit and subtracting the amount from the balance. To recap, using `||` for byte concatenation and `[]` for optional parameters, the full encoding for state diffs is: @@ -46,7 +48,7 @@ version_header_u8 || // Modified Accounts number_of_modified_accounts_u16 || ( - type_u8 || address_u20 || [balance_u256] || [nonce_increase_u16] || + type_u8 || address_u160 || [balance_u256] || [nonce_increase_u16] || [number_of_modified_storage_slots_u16 || (key_u256 || value_u256)... ] || [bytecode_len_u16 || bytecode ...] || [code_hash_u256] @@ -54,6 +56,8 @@ number_of_modified_accounts_u16 || // Withdraw Logs number_of_withdraw_logs_u16 || (to_u160 || amount_u256 || tx_hash_u256) ... +// Deposit Logs +number_of_deposit_logs_u16 || keccak256(keccack256(to_u160 || value_u256) || ...)[2:32] ``` The sequencer will then make a commitment to this encoded state diff (explained in the EIP 4844 section how this is done) and send on the `commit` transaction: diff --git a/crates/l2/proposer/l1_watcher.rs b/crates/l2/proposer/l1_watcher.rs index 3ef75a3fe..a61cdd362 100644 --- a/crates/l2/proposer/l1_watcher.rs +++ b/crates/l2/proposer/l1_watcher.rs @@ -40,6 +40,7 @@ pub struct L1Watcher { max_block_step: U256, last_block_fetched: U256, l2_proposer_pk: SecretKey, + l2_proposer_address: Address, } impl L1Watcher { @@ -52,6 +53,7 @@ impl L1Watcher { max_block_step: watcher_config.max_block_step, last_block_fetched: U256::zero(), l2_proposer_pk: watcher_config.l2_proposer_private_key, + l2_proposer_address: watcher_config.l2_proposer_address, } } @@ -94,7 +96,20 @@ impl L1Watcher { logs: Vec, store: &Store, ) -> Result, L1WatcherError> { + if logs.is_empty() { + return Ok(Vec::new()); + } + let mut deposit_txs = Vec::new(); + let mut operator_nonce = store + .get_account_info( + self.eth_client.get_block_number().await?.as_u64(), + self.l2_proposer_address, + ) + .map_err(|e| L1WatcherError::FailedToRetrieveDepositorAccountInfo(e.to_string()))? + .map(|info| info.nonce) + .unwrap_or_default(); + for log in logs { let mint_value = format!("{:#x}", log.log.topics[1]) .parse::() @@ -123,14 +138,9 @@ impl L1Watcher { ..Default::default() }; - mint_transaction.nonce = store - .get_account_info( - self.eth_client.get_block_number().await?.as_u64(), - beneficiary, - ) - .map_err(|e| L1WatcherError::FailedToRetrieveDepositorAccountInfo(e.to_string()))? - .map(|info| info.nonce) - .unwrap_or_default(); + mint_transaction.nonce = operator_nonce; + operator_nonce += 1; + mint_transaction.max_fee_per_gas = self.eth_client.get_gas_price().await?.as_u64(); // TODO(IMPORTANT): gas_limit should come in the log and must // not be calculated in here. The reason for this is that the diff --git a/crates/l2/proposer/mod.rs b/crates/l2/proposer/mod.rs index 123b8c976..eb982215f 100644 --- a/crates/l2/proposer/mod.rs +++ b/crates/l2/proposer/mod.rs @@ -7,7 +7,7 @@ use bytes::Bytes; use errors::ProposerError; use ethereum_rust_blockchain::constants::TX_GAS_COST; use ethereum_rust_core::types::{ - Block, EIP1559Transaction, GenericTransaction, Transaction, TxKind, + Block, EIP1559Transaction, GenericTransaction, PrivilegedTxType, Transaction, TxKind, }; use ethereum_rust_dev::utils::engine_client::{config::EngineApiConfig, EngineClient}; use ethereum_rust_rlp::encode::RLPEncode; @@ -25,7 +25,7 @@ pub mod prover_server; pub mod errors; -const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [28, 217, 139, 206]; +const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [132, 97, 12, 179]; const VERIFY_FUNCTION_SELECTOR: [u8; 4] = [133, 133, 44, 228]; pub struct Proposer { @@ -122,6 +122,38 @@ impl Proposer { H256::zero() }; + let deposit_hashes: Vec<[u8; 32]> = block + .body + .transactions + .iter() + .filter_map(|tx| match tx { + Transaction::PrivilegedL2Transaction(tx) + if tx.tx_type == PrivilegedTxType::Deposit => + { + let to = match tx.to { + TxKind::Call(to) => to, + TxKind::Create => Address::zero(), + }; + let value_bytes = &mut [0u8; 32]; + tx.value.to_big_endian(value_bytes); + Some(keccak([H256::from(to).0, H256::from(value_bytes).0].concat()).0) + } + _ => None, + }) + .collect(); + let deposit_logs_hash = if deposit_hashes.is_empty() { + H256::zero() + } else { + H256::from_slice( + [ + &(deposit_hashes.len() as u16).to_be_bytes(), + &keccak(deposit_hashes.concat()).0[2..32], + ] + .concat() + .as_slice(), + ) + }; + let new_state_root_hash = store .state_trie(block.header.compute_block_hash()) .unwrap() @@ -134,6 +166,7 @@ impl Proposer { block.header.number, new_state_root_hash, withdrawals_logs_merkle_root, + deposit_logs_hash, ) .await { @@ -243,15 +276,17 @@ impl Proposer { block_number: u64, new_l2_state_root: H256, withdrawal_logs_merkle_root: H256, + deposit_logs_hash: H256, ) -> Result { info!("Sending commitment"); - let mut calldata = Vec::with_capacity(68); + let mut calldata = Vec::with_capacity(132); calldata.extend(COMMIT_FUNCTION_SELECTOR); let mut block_number_bytes = [0_u8; 32]; U256::from(block_number).to_big_endian(&mut block_number_bytes); calldata.extend(block_number_bytes); calldata.extend(new_l2_state_root.0); calldata.extend(withdrawal_logs_merkle_root.0); + calldata.extend(deposit_logs_hash.0); let commit_tx_hash = self .send_transaction_with_calldata(self.on_chain_proposer_address, calldata.into()) @@ -285,7 +320,7 @@ impl Proposer { calldata.extend(H256::from_low_u64_be(32).as_bytes()); calldata.extend(H256::from_low_u64_be(block_proof.len() as u64).as_bytes()); calldata.extend(block_proof); - let leading_zeros = 32 - (calldata.len() % 32); + let leading_zeros = 32 - ((calldata.len() - 4) % 32); calldata.extend(vec![0; leading_zeros]); let verify_tx_hash = self diff --git a/crates/l2/utils/config/l1_watcher.rs b/crates/l2/utils/config/l1_watcher.rs index 6d846e797..9e82ae276 100644 --- a/crates/l2/utils/config/l1_watcher.rs +++ b/crates/l2/utils/config/l1_watcher.rs @@ -13,6 +13,7 @@ pub struct L1WatcherConfig { pub max_block_step: U256, #[serde(deserialize_with = "secret_key_deserializer")] pub l2_proposer_private_key: SecretKey, + pub l2_proposer_address: Address, } impl L1WatcherConfig { From 4d43fd5ac000903aa26a0860407e250264c66581 Mon Sep 17 00:00:00 2001 From: Martin Paulucci Date: Tue, 29 Oct 2024 18:26:40 +0100 Subject: [PATCH 31/49] test(l1): fix updating hive revision to latest (#1007) **Description** If `HIVE_REVISION` was equal to the latest commit, the `if` in `setup-hive` would not be true and the hive command would not be executed. Let's fix it and update the hive revision to the latest commit. --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e50324028..fbc3272a1 100644 --- a/Makefile +++ b/Makefile @@ -71,19 +71,21 @@ stop-localnet-silent: @kurtosis enclave stop lambdanet >/dev/null 2>&1 || true @kurtosis enclave rm lambdanet --force >/dev/null 2>&1 || true -HIVE_REVISION := 3be4465a45c421651d765f4a28702962567b40e6 +HIVE_REVISION := ccf28e5c3e940b2bc4b4f387317ee6a46f5d15c8 # Shallow clones can't specify a single revision, but at least we avoid working # the whole history by making it shallow since a given date (one day before our # target revision). HIVE_SHALLOW_SINCE := 2024-09-02 hive: git clone --single-branch --branch master --shallow-since=$(HIVE_SHALLOW_SINCE) https://github.com/lambdaclass/hive + cd hive && git checkout --detach $(HIVE_REVISION) && go build . setup-hive: hive ## 🐝 Set up Hive testing framework if [ "$$(cd hive && git rev-parse HEAD)" != "$(HIVE_REVISION)" ]; then \ cd hive && \ + git checkout master && \ git fetch --shallow-since=$(HIVE_SHALLOW_SINCE) && \ - git checkout $(HIVE_REVISION) && go build . ;\ + git checkout --detach $(HIVE_REVISION) && go build . ;\ fi TEST_PATTERN ?= / From a39bab3bc063ce93e45ab3b19ae1f9bef04a8903 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:25:40 -0300 Subject: [PATCH 32/49] feat(levm): add contract creation (#990) **Motivation** Creation transaction is not supported, and it's essential to execute the EVM. **Description** Some functionality has been taken from `create()` method of VM. Maybe a refactor can be done in that sense. In case of error I used the `VMerror` enum, but a future implementation may use other structure --- crates/vm/levm/.gitignore | 2 + crates/vm/levm/src/constants.rs | 2 + crates/vm/levm/src/errors.rs | 6 +- crates/vm/levm/src/utils.rs | 9 +- crates/vm/levm/src/vm.rs | 217 +++++++++++++++++++++++++++++--- crates/vm/levm/tests/tests.rs | 72 +++++++---- 6 files changed, 263 insertions(+), 45 deletions(-) diff --git a/crates/vm/levm/.gitignore b/crates/vm/levm/.gitignore index 335ec9573..adb63666b 100644 --- a/crates/vm/levm/.gitignore +++ b/crates/vm/levm/.gitignore @@ -1 +1,3 @@ *.tar.gz + +tests/ef_testcases diff --git a/crates/vm/levm/src/constants.rs b/crates/vm/levm/src/constants.rs index c2f8ae1b7..c38f3f429 100644 --- a/crates/vm/levm/src/constants.rs +++ b/crates/vm/levm/src/constants.rs @@ -115,6 +115,8 @@ pub const TX_BASE_COST: U256 = U256([21000, 0, 0, 0]); pub const MAX_CODE_SIZE: usize = 0x6000; pub const MAX_CREATE_CODE_SIZE: usize = 2 * MAX_CODE_SIZE; +pub const INVALID_CONTRACT_PREFIX: u8 = 0xef; + // Costs in gas for init word and init code (in wei) pub const INIT_WORD_COST: i64 = 2; diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index 12c107da1..c3f372276 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -22,10 +22,14 @@ pub enum VMError { MissingBlobHashes, BlobHashIndexOutOfBounds, RevertOpcode, - SenderAccountDoesNotExist, + AddressDoesNotMatchAnAccount, SenderAccountShouldNotHaveBytecode, SenderBalanceShouldContainTransferValue, GasPriceIsLowerThanBaseFee, + AddressAlreadyOccupied, + ContractOutputTooBig, + InvalidInitialByte, + NonceOverflow, } pub enum OpcodeSuccess { diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 599b3efea..c80aa682a 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -46,7 +46,7 @@ pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256 ), ]; - let state = Db { + let mut state = Db { accounts: accounts.into(), block_hashes: Default::default(), }; @@ -56,7 +56,7 @@ pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256 // add the account passed by parameter VM::new( - Address::from_low_u64_be(42), + Some(Address::from_low_u64_be(42)), address, Default::default(), Default::default(), @@ -68,9 +68,12 @@ pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256 U256::one(), Default::default(), Default::default(), - state, + &mut state, Default::default(), Default::default(), Default::default(), + Default::default(), + None, ) + .unwrap() } diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index ae305f6d9..952e1ba5a 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -201,9 +201,8 @@ pub fn word_to_address(word: U256) -> Address { } impl VM { - // TODO: Refactor this. #[allow(clippy::too_many_arguments)] - pub fn new( + fn call_type_transaction( to: Address, msg_sender: Address, value: U256, @@ -220,23 +219,13 @@ impl VM { block_blob_gas_used: Option, block_excess_blob_gas: Option, tx_blob_hashes: Option>, - ) -> Self { - // TODO: This handles only CALL transactions. + ) -> Result { let bytecode = db.get_account_bytecode(&to); - // TODO: This handles only CALL transactions. - // TODO: Remove this allow when CREATE is implemented. - #[allow(clippy::redundant_locals)] - let to = to; - - // TODO: In CALL this is the `to`, in CREATE it is not. - let code_addr = to; - - // TODO: this is mostly placeholder let initial_call_frame = CallFrame::new( msg_sender, to, - code_addr, + to, None, bytecode, value, @@ -264,11 +253,204 @@ impl VM { tx_blob_hashes, }; - Self { + Ok(VM { call_frames: vec![initial_call_frame], db, env, accrued_substate: Substate::default(), + }) + } + + // Functionality should be: + // (1) Check whether caller has enough balance to make a transfer + // (2) Derive the new contract’s address from the caller’s address (passing in the creator account’s nonce) + // (3) Create the new contract account using the derived contract address (changing the “world state” StateDB) + // (4) Transfer the initial Ether endowment from caller to the new contract + // (5) Set input data as contract’s deploy code, then execute it with EVM. The ret variable is the returned contract code + // (6) Check for error. Or if the contract code is too big, fail. Charge the user gas then set the contract code + // Source: https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-5-the-smart-contract-creation-process-cb7b6133b855 + #[allow(clippy::too_many_arguments)] + fn create_type_transaction( + sender: Address, + secret_key: H256, + db: &mut Db, + value: U256, + calldata: Bytes, + block_number: U256, + coinbase: Address, + timestamp: U256, + prev_randao: Option, + chain_id: U256, + base_fee_per_gas: U256, + gas_price: U256, + block_blob_gas_used: Option, + block_excess_blob_gas: Option, + tx_blob_hashes: Option>, + salt: Option, + ) -> Result { + let mut db_copy = db.clone(); + let mut sender_account = match db_copy.accounts.get(&sender) { + Some(acc) => acc, + None => { + return Err(VMError::OutOfGas); + } + } + .clone(); + + // (1) + if sender_account.balance < value { + return Err(VMError::OutOfGas); // Maybe a more personalized error + } + + sender_account.nonce = sender_account + .nonce + .checked_add(1) + .ok_or(VMError::NonceOverflow)?; + + // (2) + let new_contract_address = match salt { + Some(salt) => VM::calculate_create2_address(sender, &calldata, salt), + None => VM::calculate_create_address(sender, sender_account.nonce), + }; + + // If address is already in db, there's an error + if db_copy.accounts.contains_key(&new_contract_address) { + return Err(VMError::AddressAlreadyOccupied); + } + + // (3) + let mut created_contract = Account::new( + new_contract_address, + value, + calldata.clone(), + 1, + Default::default(), + ); + db_copy.add_account(new_contract_address, created_contract.clone()); + + // (4) + sender_account.balance -= value; + created_contract.balance += value; + + // (5) + let code: Bytes = calldata.clone(); + + // Call the contract + let mut vm = VM::new( + Some(created_contract.address), + sender, + value, + code, + sender_account.balance, + block_number, + coinbase, + timestamp, + prev_randao, + chain_id, + base_fee_per_gas, + gas_price, + &mut db_copy, + block_blob_gas_used, + block_excess_blob_gas, + tx_blob_hashes, + secret_key, + None, + )?; + + let res = vm.transact()?; + // Don't use a revert bc work with clones, so don't have to save previous state + + let contract_code = res.output; + + // (6) + if contract_code.len() > MAX_CODE_SIZE { + return Err(VMError::ContractOutputTooBig); + } + // Supposing contract code has contents + if contract_code[0] == INVALID_CONTRACT_PREFIX { + return Err(VMError::InvalidInitialByte); + } + + // If the initialization code completes successfully, a final contract-creation cost is paid, + // the code-deposit cost, c, proportional to the size of the created contract’s code + let creation_cost = 200 * contract_code.len(); + + sender_account.balance = sender_account + .balance + .checked_sub(U256::from(creation_cost)) + .ok_or(VMError::OutOfGas)?; + + created_contract.bytecode = contract_code; + + let mut acc = db_copy.accounts.get_mut(&sender).unwrap(); + *acc = sender_account; + acc = db_copy.accounts.get_mut(&new_contract_address).unwrap(); + *acc = created_contract; + + *db = db_copy; + Ok(vm) + } + + // TODO: Refactor this. + #[allow(clippy::too_many_arguments)] + pub fn new( + to: Option
, + msg_sender: Address, + value: U256, + calldata: Bytes, + gas_limit: U256, + block_number: U256, + coinbase: Address, + timestamp: U256, + prev_randao: Option, + chain_id: U256, + base_fee_per_gas: U256, + gas_price: U256, + db: &mut Db, + block_blob_gas_used: Option, + block_excess_blob_gas: Option, + tx_blob_hashes: Option>, + secret_key: H256, + salt: Option, + ) -> Result { + // Maybe this desicion should be made in an upper layer + match to { + Some(address) => VM::call_type_transaction( + address, + msg_sender, + value, + calldata, + gas_limit, + block_number, + coinbase, + timestamp, + prev_randao, + chain_id, + base_fee_per_gas, + gas_price, + db.clone(), + block_blob_gas_used, + block_excess_blob_gas, + tx_blob_hashes, + ), + None => VM::create_type_transaction( + msg_sender, + secret_key, + db, + value, + calldata, + block_number, + coinbase, + timestamp, + prev_randao, + chain_id, + base_fee_per_gas, + gas_price, + block_blob_gas_used, + block_excess_blob_gas, + tx_blob_hashes, + salt, + ), } } @@ -415,7 +597,6 @@ impl VM { } } - // let account = self.db.accounts.get(&self.env.origin).unwrap(); /// Based on Ethereum yellow paper's initial tests of intrinsic validity (Section 6). The last version is /// Shanghai, so there are probably missing Cancun validations. The intrinsic validations are: /// @@ -437,7 +618,9 @@ impl VM { // Validations (1), (2), (3), (5), and (8) are assumed done in upper layers. let sender_account = match self.db.accounts.get(&self.env.origin) { Some(acc) => acc, - None => return Err(VMError::SenderAccountDoesNotExist), + None => return Err(VMError::AddressDoesNotMatchAnAccount), + // This is a check for completeness. However if it were a none and + // it was not caught it would be caught in clause 6. }; // (4) if sender_account.has_code() { diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index 4ddd2e8ee..982f2bcc8 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -3845,7 +3845,7 @@ fn caller_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), caller, Default::default(), Default::default(), @@ -3857,11 +3857,14 @@ fn caller_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3887,7 +3890,7 @@ fn origin_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), msg_sender, Default::default(), Default::default(), @@ -3899,11 +3902,14 @@ fn origin_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3955,7 +3961,7 @@ fn address_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), Default::default(), Default::default(), @@ -3967,11 +3973,14 @@ fn address_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3999,7 +4008,7 @@ fn selfbalance_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), Default::default(), Default::default(), @@ -4011,11 +4020,14 @@ fn selfbalance_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4039,7 +4051,7 @@ fn callvalue_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), value, Default::default(), @@ -4051,11 +4063,14 @@ fn callvalue_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4078,7 +4093,7 @@ fn codesize_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), Default::default(), Default::default(), @@ -4090,11 +4105,14 @@ fn codesize_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4119,7 +4137,7 @@ fn gasprice_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), Default::default(), Default::default(), @@ -4131,11 +4149,14 @@ fn gasprice_op() { Default::default(), Default::default(), U256::from(0x9876), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4177,7 +4198,7 @@ fn codecopy_op() { ); let mut vm = VM::new( - address_that_has_the_code, + Some(address_that_has_the_code), Default::default(), Default::default(), Default::default(), @@ -4189,11 +4210,14 @@ fn codecopy_op() { Default::default(), Default::default(), Default::default(), - db, + &mut db, Default::default(), Default::default(), Default::default(), - ); + Default::default(), + None, + ) + .unwrap(); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); From 67764fc07d87d816a1db7de0a44a1fccb36f48ea Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:40:20 -0300 Subject: [PATCH 33/49] feat(l2): `make restart` does not clean contract deps (#1010) --- crates/l2/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 3fe872317..23914ec55 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -13,7 +13,7 @@ down: down-local-l1 down-l2 ## 🛑 Shuts down the localnet clean: clean-contract-deps ## 🧹 Cleans the localnet -restart: restart-local-l1 restart-contract-deps deploy-l1 restart-l2 ## 🔄 Restarts the localnet +restart: restart-local-l1 deploy-l1 restart-l2 ## 🔄 Restarts the localnet cli: ## 🛠️ Installs the L2 Lambda Ethereum Rust CLI cargo install --path ${ETHEREUM_RUST_PATH}/cmd/ethereum_rust_l2/ --force From ac7559aa8292feeccfe1838a292c20c16167cde8 Mon Sep 17 00:00:00 2001 From: ElFantasma Date: Tue, 29 Oct 2024 15:41:18 -0300 Subject: [PATCH 34/49] feat(l1): rlpx peer listen loop (#980) **Motivation** A p2p RLPx connection is maintained per connected peer. A listen loop is required to listen for requests and send messages **Description** - Added basic listen loop to start listening to expected messages - Added capabilities initialization to send initial messages per capability Closes #840 --- cmd/ethereum_rust/ethereum_rust.rs | 14 +- crates/blockchain/error.rs | 10 +- crates/networking/p2p/net.rs | 36 ++-- crates/networking/p2p/rlpx/connection.rs | 165 ++++++++++-------- crates/networking/p2p/rlpx/error.rs | 7 +- crates/networking/p2p/rlpx/eth.rs | 116 +----------- crates/networking/p2p/rlpx/eth/backend.rs | 29 +++ crates/networking/p2p/rlpx/eth/receipts.rs | 4 +- crates/networking/p2p/rlpx/eth/status.rs | 96 ++++++++++ .../networking/p2p/rlpx/eth/transactions.rs | 4 +- crates/networking/p2p/rlpx/handshake.rs | 22 +-- crates/networking/p2p/rlpx/message.rs | 15 +- crates/networking/p2p/rlpx/p2p.rs | 38 +++- 13 files changed, 318 insertions(+), 238 deletions(-) create mode 100644 crates/networking/p2p/rlpx/eth/backend.rs create mode 100644 crates/networking/p2p/rlpx/eth/status.rs diff --git a/cmd/ethereum_rust/ethereum_rust.rs b/cmd/ethereum_rust/ethereum_rust.rs index 4f10fd1bc..ff1e49803 100644 --- a/cmd/ethereum_rust/ethereum_rust.rs +++ b/cmd/ethereum_rust/ethereum_rust.rs @@ -1,6 +1,7 @@ use bytes::Bytes; use directories::ProjectDirs; use ethereum_rust_blockchain::add_block; +use ethereum_rust_blockchain::fork_choice::apply_fork_choice; use ethereum_rust_core::types::{Block, Genesis}; use ethereum_rust_core::H256; use ethereum_rust_net::bootnode::BootNode; @@ -123,19 +124,24 @@ async fn main() { if let Some(chain_rlp_path) = matches.get_one::("import") { let blocks = read_chain_file(chain_rlp_path); let size = blocks.len(); - for block in blocks { + for block in &blocks { let hash = block.header.compute_block_hash(); info!( "Adding block {} with hash {:#x}.", block.header.number, hash ); - if add_block(&block, &store).is_err() { + let result = add_block(block, &store); + if let Some(error) = result.err() { warn!( - "Failed to add block {} with hash {:#x}.", - block.header.number, hash + "Failed to add block {} with hash {:#x}: {}.", + block.header.number, hash, error ); } } + if let Some(last_block) = blocks.last() { + let hash = last_block.header.compute_block_hash(); + apply_fork_choice(&store, hash, hash, hash).unwrap(); + } info!("Added {} blocks to blockchain", size); } let jwt_secret = read_jwtsecret_file(authrpc_jwtsecret); diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs index e2e7c57ec..9446795b4 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -1,10 +1,8 @@ -use thiserror::Error; - use ethereum_rust_core::types::InvalidBlockHeaderError; use ethereum_rust_storage::error::StoreError; use ethereum_rust_vm::EvmError; -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum ChainError { #[error("Invalid Block: {0}")] InvalidBlock(#[from] InvalidBlockError), @@ -20,7 +18,7 @@ pub enum ChainError { EvmError(#[from] EvmError), } -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum InvalidBlockError { #[error("World State Root does not match the one in the header after executing")] StateRootMismatch, @@ -36,7 +34,7 @@ pub enum InvalidBlockError { BlobGasUsedMismatch, } -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum MempoolError { #[error("No block header")] NoBlockHeaderError, @@ -67,7 +65,7 @@ pub enum ForkChoiceElement { Finalized, } -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum InvalidForkChoice { #[error("DB error: {0}")] StoreError(#[from] StoreError), diff --git a/crates/networking/p2p/net.rs b/crates/networking/p2p/net.rs index ff1a42300..fd512c138 100644 --- a/crates/networking/p2p/net.rs +++ b/crates/networking/p2p/net.rs @@ -50,6 +50,7 @@ pub async fn start_network( let discovery_handle = tokio::spawn(discover_peers( udp_addr, signer.clone(), + storage.clone(), table.clone(), bootnodes, )); @@ -66,6 +67,7 @@ pub async fn start_network( async fn discover_peers( udp_addr: SocketAddr, signer: SigningKey, + storage: Store, table: Arc>, bootnodes: Vec, ) { @@ -74,6 +76,7 @@ async fn discover_peers( let server_handler = tokio::spawn(discover_peers_server( udp_addr, udp_socket.clone(), + storage, table.clone(), signer.clone(), )); @@ -111,6 +114,7 @@ async fn discover_peers( async fn discover_peers_server( udp_addr: SocketAddr, udp_socket: Arc, + storage: Store, table: Arc>, signer: SigningKey, ) { @@ -196,9 +200,10 @@ async fn discover_peers_server( let mut msg_buf = vec![0; read - 32]; buf[32..read].clone_into(&mut msg_buf); - let signer_clone = signer.clone(); + let signer = signer.clone(); + let storage = storage.clone(); tokio::spawn(async move { - handle_peer_as_initiator(signer_clone, &msg_buf, &peer.node, table) + handle_peer_as_initiator(signer, &msg_buf, &peer.node, storage, table) .await; }); } else { @@ -724,13 +729,10 @@ async fn pong(socket: &UdpSocket, to_addr: SocketAddr, ping_hash: H256, signer: let _ = socket.send_to(&buf, to_addr).await; } -// TODO build a proper listen loop that receives requests from both -// peers and business layer and propagate storage to use when required -// https://github.com/lambdaclass/lambda_ethereum_rust/issues/840 async fn serve_requests( tcp_addr: SocketAddr, signer: SigningKey, - _storage: Store, + storage: Store, table: Arc>, ) { let tcp_socket = TcpSocket::new_v4().unwrap(); @@ -742,6 +744,7 @@ async fn serve_requests( tokio::spawn(handle_peer_as_receiver( signer.clone(), stream, + storage.clone(), table.clone(), )); } @@ -750,9 +753,10 @@ async fn serve_requests( async fn handle_peer_as_receiver( signer: SigningKey, stream: TcpStream, + storage: Store, table: Arc>, ) { - let conn = RLPxConnection::receiver(signer, stream); + let conn = RLPxConnection::receiver(signer, stream, storage); handle_peer(conn, table).await; } @@ -760,6 +764,7 @@ async fn handle_peer_as_initiator( signer: SigningKey, msg: &[u8], node: &Node, + storage: Store, table: Arc>, ) { info!("Trying RLPx connection with {node:?}"); @@ -768,19 +773,16 @@ async fn handle_peer_as_initiator( .connect(SocketAddr::new(node.ip, node.tcp_port)) .await .unwrap(); - let conn = RLPxConnection::initiator(signer, msg, stream).await; + let conn = RLPxConnection::initiator(signer, msg, stream, storage).await; handle_peer(conn, table).await; } async fn handle_peer(mut conn: RLPxConnection, table: Arc>) { match conn.handshake().await { - Ok(_) => { - // TODO Properly build listen loop - // https://github.com/lambdaclass/lambda_ethereum_rust/issues/840 - // loop { - // conn.await_messages(); - // } - } + Ok(_) => match conn.handle_peer().await { + Ok(_) => unreachable!(), + Err(e) => info!("Error during RLPx connection: ({e})"), + }, Err(e) => { // Discard peer from kademlia table info!("Handshake failed, discarding peer: ({e})"); @@ -798,6 +800,7 @@ pub fn node_id_from_signing_key(signer: &SigningKey) -> H512 { #[cfg(test)] mod tests { use super::*; + use ethereum_rust_storage::EngineType; use kademlia::bucket_number; use rand::rngs::OsRng; use std::{ @@ -844,12 +847,15 @@ mod tests { let signer = SigningKey::random(&mut OsRng); let udp_socket = Arc::new(UdpSocket::bind(addr).await.unwrap()); let node_id = node_id_from_signing_key(&signer); + let storage = + Store::new("temp.db", EngineType::InMemory).expect("Failed to create test DB"); let table = Arc::new(Mutex::new(KademliaTable::new(node_id))); if should_start_server { tokio::spawn(discover_peers_server( addr, udp_socket.clone(), + storage.clone(), table.clone(), signer.clone(), )); diff --git a/crates/networking/p2p/rlpx/connection.rs b/crates/networking/p2p/rlpx/connection.rs index 54c948bc1..642535904 100644 --- a/crates/networking/p2p/rlpx/connection.rs +++ b/crates/networking/p2p/rlpx/connection.rs @@ -1,5 +1,5 @@ use crate::{ - rlpx::{handshake::encode_ack_message, message::Message, p2p, utils::id2pubkey}, + rlpx::{eth::backend, handshake::encode_ack_message, message::Message, p2p, utils::id2pubkey}, MAX_DISC_PACKET_SIZE, }; @@ -8,12 +8,14 @@ use super::{ frame, handshake::{decode_ack_message, decode_auth_message, encode_auth_message}, message as rlpx, + p2p::Capability, utils::{ecdh_xchng, pubkey2id}, }; use aes::cipher::KeyIvInit; use bytes::BufMut as _; use ethereum_rust_core::{H256, H512}; use ethereum_rust_rlp::decode::RLPDecode; +use ethereum_rust_storage::Store; use k256::{ ecdsa::{RecoveryId, Signature, SigningKey, VerifyingKey}, PublicKey, SecretKey, @@ -21,8 +23,11 @@ use k256::{ use sha3::{Digest, Keccak256}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tracing::{error, info}; -pub const SUPPORTED_CAPABILITIES: [(&str, u8); 2] = [("p2p", 5), ("eth", 68)]; -// pub const SUPPORTED_CAPABILITIES: [(&str, u8); 3] = [("p2p", 5), ("eth", 68), ("snap", 1)]; +const CAP_P2P: (Capability, u8) = (Capability::P2p, 5); +const CAP_ETH: (Capability, u8) = (Capability::Eth, 68); +//const CAP_SNAP: (Capability, u8) = (Capability::Snap, 1); +const SUPPORTED_CAPABILITIES: [(Capability, u8); 2] = [CAP_P2P, CAP_ETH]; +// pub const SUPPORTED_CAPABILITIES: [(&str, u8); 3] = [CAP_P2P, CAP_ETH, CAP_SNAP)]; pub(crate) type Aes256Ctr64BE = ctr::Ctr64BE; @@ -31,20 +36,22 @@ pub(crate) struct RLPxConnection { signer: SigningKey, state: RLPxConnectionState, stream: S, - capabilities: Vec<(String, u8)>, + storage: Store, + capabilities: Vec<(Capability, u8)>, } impl RLPxConnection { - fn new(signer: SigningKey, stream: S, state: RLPxConnectionState) -> Self { + fn new(signer: SigningKey, stream: S, state: RLPxConnectionState, storage: Store) -> Self { Self { signer, state, stream, + storage, capabilities: vec![], } } - pub fn receiver(signer: SigningKey, stream: S) -> Self { + pub fn receiver(signer: SigningKey, stream: S, storage: Store) -> Self { let mut rng = rand::thread_rng(); Self::new( signer, @@ -53,10 +60,11 @@ impl RLPxConnection { H256::random_using(&mut rng), SecretKey::random(&mut rng), )), + storage, ) } - pub async fn initiator(signer: SigningKey, msg: &[u8], stream: S) -> Self { + pub async fn initiator(signer: SigningKey, msg: &[u8], stream: S, storage: Store) -> Self { let mut rng = rand::thread_rng(); let digest = Keccak256::digest(&msg[65..]); let signature = &Signature::from_bytes(msg[..64].into()).unwrap(); @@ -67,7 +75,7 @@ impl RLPxConnection { SecretKey::random(&mut rng), pubkey2id(&peer_pk.into()), )); - RLPxConnection::new(signer, stream, state) + RLPxConnection::new(signer, stream, state, storage) } pub async fn handshake(&mut self) -> Result<(), RLPxError> { @@ -89,11 +97,84 @@ impl RLPxConnection { info!("Completed handshake!"); self.exchange_hello_messages().await?; - info!("Completed Hello roundtrip!"); Ok(()) } - pub async fn send_auth(&mut self) { + pub async fn exchange_hello_messages(&mut self) -> Result<(), RLPxError> { + let hello_msg = Message::Hello(p2p::HelloMessage::new( + SUPPORTED_CAPABILITIES.to_vec(), + PublicKey::from(self.signer.verifying_key()), + )); + + self.send(hello_msg).await; + + // Receive Hello message + match self.receive().await { + Message::Hello(hello_message) => { + self.capabilities = hello_message.capabilities; + + // Check if we have any capability in common + for cap in self.capabilities.clone() { + if SUPPORTED_CAPABILITIES.contains(&cap) { + return Ok(()); + } + } + // Return error if not + Err(RLPxError::HandshakeError( + "No matching capabilities".to_string(), + )) + } + _ => { + // Fail if it is not a hello message + Err(RLPxError::HandshakeError( + "Expected Hello message".to_string(), + )) + } + } + } + + pub async fn handle_peer(&mut self) -> Result<(), RLPxError> { + self.start_capabilities().await?; + match &self.state { + RLPxConnectionState::Established(_) => { + info!("Started peer main loop"); + loop { + match self.receive().await { + // TODO: implement handlers for each message type + Message::Disconnect(_) => info!("Received Disconnect"), + Message::Ping(_) => info!("Received Ping"), + Message::Pong(_) => info!("Received Pong"), + Message::Status(_) => info!("Received Status"), + // TODO: Add new message types and handlers as they are implemented + message => return Err(RLPxError::UnexpectedMessage(message)), + }; + } + } + _ => Err(RLPxError::InvalidState( + "Invalid connection state".to_string(), + )), + } + } + + pub fn get_remote_node_id(&self) -> H512 { + match &self.state { + RLPxConnectionState::Established(state) => state.remote_node_id, + // TODO proper error + _ => panic!("Invalid state"), + } + } + + async fn start_capabilities(&mut self) -> Result<(), RLPxError> { + // Sending eth Status if peer supports it + if self.capabilities.contains(&CAP_ETH) { + let status = backend::get_status(&self.storage).unwrap(); + self.send(Message::Status(status)).await; + } + // TODO: add new capabilities startup when required (eg. snap) + Ok(()) + } + + async fn send_auth(&mut self) { match &self.state { RLPxConnectionState::Initiator(initiator_state) => { let secret_key: SecretKey = self.signer.clone().into(); @@ -109,7 +190,6 @@ impl RLPxConnection { auth_message.put_slice(&msg); self.stream.write_all(&auth_message).await.unwrap(); - info!("Sent auth message correctly!"); self.state = RLPxConnectionState::InitiatedAuth(InitiatedAuth::new( initiator_state, @@ -121,24 +201,20 @@ impl RLPxConnection { }; } - pub async fn send_ack(&mut self) { + async fn send_ack(&mut self) { match &self.state { RLPxConnectionState::ReceivedAuth(received_auth_state) => { - let secret_key: SecretKey = self.signer.clone().into(); let peer_pk = id2pubkey(received_auth_state.remote_node_id).unwrap(); let mut ack_message = vec![]; let msg = encode_ack_message( - &secret_key, &received_auth_state.local_ephemeral_key, received_auth_state.local_nonce, &peer_pk, - &received_auth_state.remote_ephemeral_key, ); ack_message.put_slice(&msg); self.stream.write_all(&ack_message).await.unwrap(); - info!("Sent ack message correctly!"); self.state = RLPxConnectionState::Established(Box::new(Established::for_receiver( received_auth_state, @@ -150,7 +226,7 @@ impl RLPxConnection { }; } - pub async fn receive_auth(&mut self) { + async fn receive_auth(&mut self) { match &self.state { RLPxConnectionState::Receiver(receiver_state) => { let secret_key: SecretKey = self.signer.clone().into(); @@ -169,7 +245,6 @@ impl RLPxConnection { let auth_bytes = &buf[..msg_size + 2]; let msg = &buf[2..msg_size + 2]; let (auth, remote_ephemeral_key) = decode_auth_message(&secret_key, msg, auth_data); - info!("Received auth message correctly!"); // Build next state self.state = RLPxConnectionState::ReceivedAuth(ReceivedAuth::new( @@ -185,7 +260,7 @@ impl RLPxConnection { }; } - pub async fn receive_ack(&mut self) { + async fn receive_ack(&mut self) { match &self.state { RLPxConnectionState::InitiatedAuth(initiated_auth_state) => { let secret_key: SecretKey = self.signer.clone().into(); @@ -205,7 +280,6 @@ impl RLPxConnection { let msg = &buf[2..msg_size + 2]; let ack = decode_ack_message(&secret_key, msg, ack_data); let remote_ephemeral_key = ack.get_ephemeral_pubkey().unwrap(); - info!("Received ack message correctly!"); // Build next state self.state = RLPxConnectionState::Established(Box::new(Established::for_initiator( @@ -220,46 +294,7 @@ impl RLPxConnection { }; } - pub async fn exchange_hello_messages(&mut self) -> Result<(), RLPxError> { - let supported_capabilities: Vec<(String, u8)> = SUPPORTED_CAPABILITIES - .into_iter() - .map(|(name, version)| (name.to_string(), version)) - .collect(); - let hello_msg = Message::Hello(p2p::HelloMessage::new( - supported_capabilities.clone(), - PublicKey::from(self.signer.verifying_key()), - )); - - self.send(hello_msg).await; - info!("Hello message sent!"); - - // Receive Hello message - match self.receive().await { - Message::Hello(hello_message) => { - info!("Hello message received {hello_message:?}"); - self.capabilities = hello_message.capabilities; - - // Check if we have any capability in common - for cap in self.capabilities.clone() { - if supported_capabilities.contains(&cap) { - return Ok(()); - } - } - // Return error if not - Err(RLPxError::HandshakeError( - "No matching capabilities".to_string(), - )) - } - _ => { - // Fail if it is not a hello message - Err(RLPxError::HandshakeError( - "Expected Hello message".to_string(), - )) - } - } - } - - pub async fn send(&mut self, message: rlpx::Message) { + async fn send(&mut self, message: rlpx::Message) { match &mut self.state { RLPxConnectionState::Established(state) => { let mut frame_buffer = vec![]; @@ -277,7 +312,7 @@ impl RLPxConnection { } } - pub async fn receive(&mut self) -> rlpx::Message { + async fn receive(&mut self) -> rlpx::Message { match &mut self.state { RLPxConnectionState::Established(state) => { let frame_data = frame::read(state, &mut self.stream).await; @@ -289,14 +324,6 @@ impl RLPxConnection { _ => panic!("Received an unexpected message"), } } - - pub fn get_remote_node_id(&self) -> H512 { - match &self.state { - RLPxConnectionState::Established(state) => state.remote_node_id, - // TODO proper error - _ => panic!("Invalid state"), - } - } } enum RLPxConnectionState { diff --git a/crates/networking/p2p/rlpx/error.rs b/crates/networking/p2p/rlpx/error.rs index e74d4339a..4177ea10f 100644 --- a/crates/networking/p2p/rlpx/error.rs +++ b/crates/networking/p2p/rlpx/error.rs @@ -1,8 +1,13 @@ +use crate::rlpx::message::Message; use thiserror::Error; // TODO improve errors #[derive(Debug, Error)] -pub enum RLPxError { +pub(crate) enum RLPxError { #[error("{0}")] HandshakeError(String), + #[error("{0}")] + InvalidState(String), + #[error("Unexpected message: {0}")] + UnexpectedMessage(Message), } diff --git a/crates/networking/p2p/rlpx/eth.rs b/crates/networking/p2p/rlpx/eth.rs index 52387e6aa..a03ff256c 100644 --- a/crates/networking/p2p/rlpx/eth.rs +++ b/crates/networking/p2p/rlpx/eth.rs @@ -1,111 +1,5 @@ -use super::{message::RLPxMessage, utils::snappy_encode}; -use bytes::BufMut; -use ethereum_rust_core::{ - types::{BlockHash, ForkId}, - U256, -}; -use ethereum_rust_rlp::{ - encode::RLPEncode, - error::{RLPDecodeError, RLPEncodeError}, - structs::{Decoder, Encoder}, -}; -use ethereum_rust_storage::{error::StoreError, Store}; -use snap::raw::Decoder as SnappyDecoder; - -pub const ETH_VERSION: u32 = 68; -pub const HASH_FIRST_BYTE_DECODER: u8 = 160; - -mod blocks; -mod receipts; -mod transactions; - -#[derive(Debug)] -pub(crate) struct StatusMessage { - eth_version: u32, - network_id: u64, - total_difficulty: U256, - block_hash: BlockHash, - genesis: BlockHash, - fork_id: ForkId, -} - -// TODO remove this allow once we construct StatusMessages -#[allow(unused)] -impl StatusMessage { - pub fn new(storage: &Store) -> Result { - let chain_config = storage.get_chain_config()?; - let total_difficulty = - U256::from(chain_config.terminal_total_difficulty.unwrap_or_default()); - let network_id = chain_config.chain_id; - - // These blocks must always be available - let genesis_header = storage.get_block_header(0)?.unwrap(); - let block_number = storage.get_latest_block_number()?.unwrap(); - let block_header = storage.get_block_header(block_number)?.unwrap(); - - let genesis = genesis_header.compute_block_hash(); - let block_hash = block_header.compute_block_hash(); - let fork_id = ForkId::new(chain_config, genesis, block_header.timestamp, block_number); - Ok(Self { - eth_version: ETH_VERSION, - network_id, - total_difficulty, - block_hash, - genesis, - fork_id, - }) - } -} - -impl RLPxMessage for StatusMessage { - fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { - 16_u8.encode(buf); // msg_id - - let mut encoded_data = vec![]; - Encoder::new(&mut encoded_data) - .encode_field(&self.eth_version) - .encode_field(&self.network_id) - .encode_field(&self.total_difficulty) - .encode_field(&self.block_hash) - .encode_field(&self.genesis) - .encode_field(&self.fork_id) - .finish(); - - let msg_data = snappy_encode(encoded_data)?; - buf.put_slice(&msg_data); - Ok(()) - } - - fn decode(msg_data: &[u8]) -> Result { - let mut snappy_decoder = SnappyDecoder::new(); - let decompressed_data = snappy_decoder - .decompress_vec(msg_data) - .map_err(|e| RLPDecodeError::Custom(e.to_string()))?; - let decoder = Decoder::new(&decompressed_data)?; - let (eth_version, decoder): (u32, _) = decoder.decode_field("protocolVersion")?; - - assert_eq!(eth_version, 68, "only eth version 68 is supported"); - - let (network_id, decoder): (u64, _) = decoder.decode_field("networkId")?; - - let (total_difficulty, decoder): (U256, _) = decoder.decode_field("totalDifficulty")?; - - let (block_hash, decoder): (BlockHash, _) = decoder.decode_field("blockHash")?; - - let (genesis, decoder): (BlockHash, _) = decoder.decode_field("genesis")?; - - let (fork_id, decoder): (ForkId, _) = decoder.decode_field("forkId")?; - - // Implementations must ignore any additional list elements - let _padding = decoder.finish_unchecked(); - - Ok(Self { - eth_version, - network_id, - total_difficulty, - block_hash, - genesis, - fork_id, - }) - } -} +pub(crate) mod backend; +pub(crate) mod blocks; +pub(crate) mod receipts; +pub(crate) mod status; +pub(crate) mod transactions; diff --git a/crates/networking/p2p/rlpx/eth/backend.rs b/crates/networking/p2p/rlpx/eth/backend.rs new file mode 100644 index 000000000..1af62214a --- /dev/null +++ b/crates/networking/p2p/rlpx/eth/backend.rs @@ -0,0 +1,29 @@ +use ethereum_rust_core::{types::ForkId, U256}; +use ethereum_rust_storage::{error::StoreError, Store}; + +use super::status::StatusMessage; + +pub const ETH_VERSION: u32 = 68; + +pub fn get_status(storage: &Store) -> Result { + let chain_config = storage.get_chain_config()?; + let total_difficulty = U256::from(chain_config.terminal_total_difficulty.unwrap_or_default()); + let network_id = chain_config.chain_id; + + // These blocks must always be available + let genesis_header = storage.get_block_header(0)?.unwrap(); + let block_number = storage.get_latest_block_number()?.unwrap(); + let block_header = storage.get_block_header(block_number)?.unwrap(); + + let genesis = genesis_header.compute_block_hash(); + let block_hash = block_header.compute_block_hash(); + let fork_id = ForkId::new(chain_config, genesis, block_header.timestamp, block_number); + Ok(StatusMessage::new( + ETH_VERSION, + network_id, + total_difficulty, + block_hash, + genesis, + fork_id, + )) +} diff --git a/crates/networking/p2p/rlpx/eth/receipts.rs b/crates/networking/p2p/rlpx/eth/receipts.rs index 496273341..5d76a2f27 100644 --- a/crates/networking/p2p/rlpx/eth/receipts.rs +++ b/crates/networking/p2p/rlpx/eth/receipts.rs @@ -6,9 +6,7 @@ use ethereum_rust_rlp::{ }; use snap::raw::Decoder as SnappyDecoder; -use crate::rlpx::message::RLPxMessage; - -use super::snappy_encode; +use crate::rlpx::{message::RLPxMessage, utils::snappy_encode}; // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#getreceipts-0x0f #[derive(Debug)] diff --git a/crates/networking/p2p/rlpx/eth/status.rs b/crates/networking/p2p/rlpx/eth/status.rs new file mode 100644 index 000000000..9050ac433 --- /dev/null +++ b/crates/networking/p2p/rlpx/eth/status.rs @@ -0,0 +1,96 @@ +use bytes::BufMut; +use ethereum_rust_core::{ + types::{BlockHash, ForkId}, + U256, +}; +use ethereum_rust_rlp::{ + encode::RLPEncode, + error::{RLPDecodeError, RLPEncodeError}, + structs::{Decoder, Encoder}, +}; +use snap::raw::Decoder as SnappyDecoder; + +use crate::rlpx::{message::RLPxMessage, utils::snappy_encode}; + +#[derive(Debug)] +pub(crate) struct StatusMessage { + eth_version: u32, + network_id: u64, + total_difficulty: U256, + block_hash: BlockHash, + genesis: BlockHash, + fork_id: ForkId, +} + +impl StatusMessage { + pub fn new( + eth_version: u32, + network_id: u64, + total_difficulty: U256, + block_hash: BlockHash, + genesis: BlockHash, + fork_id: ForkId, + ) -> Self { + Self { + eth_version, + network_id, + total_difficulty, + block_hash, + genesis, + fork_id, + } + } +} + +impl RLPxMessage for StatusMessage { + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { + 16_u8.encode(buf); // msg_id + + let mut encoded_data = vec![]; + Encoder::new(&mut encoded_data) + .encode_field(&self.eth_version) + .encode_field(&self.network_id) + .encode_field(&self.total_difficulty) + .encode_field(&self.block_hash) + .encode_field(&self.genesis) + .encode_field(&self.fork_id) + .finish(); + + let msg_data = snappy_encode(encoded_data)?; + buf.put_slice(&msg_data); + Ok(()) + } + + fn decode(msg_data: &[u8]) -> Result { + let mut snappy_decoder = SnappyDecoder::new(); + let decompressed_data = snappy_decoder + .decompress_vec(msg_data) + .map_err(|e| RLPDecodeError::Custom(e.to_string()))?; + let decoder = Decoder::new(&decompressed_data)?; + let (eth_version, decoder): (u32, _) = decoder.decode_field("protocolVersion")?; + + assert_eq!(eth_version, 68, "only eth version 68 is supported"); + + let (network_id, decoder): (u64, _) = decoder.decode_field("networkId")?; + + let (total_difficulty, decoder): (U256, _) = decoder.decode_field("totalDifficulty")?; + + let (block_hash, decoder): (BlockHash, _) = decoder.decode_field("blockHash")?; + + let (genesis, decoder): (BlockHash, _) = decoder.decode_field("genesis")?; + + let (fork_id, decoder): (ForkId, _) = decoder.decode_field("forkId")?; + + // Implementations must ignore any additional list elements + let _padding = decoder.finish_unchecked(); + + Ok(Self::new( + eth_version, + network_id, + total_difficulty, + block_hash, + genesis, + fork_id, + )) + } +} diff --git a/crates/networking/p2p/rlpx/eth/transactions.rs b/crates/networking/p2p/rlpx/eth/transactions.rs index 4923e3b19..d84e9c228 100644 --- a/crates/networking/p2p/rlpx/eth/transactions.rs +++ b/crates/networking/p2p/rlpx/eth/transactions.rs @@ -6,9 +6,7 @@ use ethereum_rust_rlp::{ }; use snap::raw::Decoder as SnappyDecoder; -use crate::rlpx::message::RLPxMessage; - -use super::snappy_encode; +use crate::rlpx::{message::RLPxMessage, utils::snappy_encode}; // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#transactions-0x02 // Broadcast message diff --git a/crates/networking/p2p/rlpx/handshake.rs b/crates/networking/p2p/rlpx/handshake.rs index 679a0f8d8..c88941ca5 100644 --- a/crates/networking/p2p/rlpx/handshake.rs +++ b/crates/networking/p2p/rlpx/handshake.rs @@ -13,7 +13,6 @@ use k256::{ PublicKey, SecretKey, }; use rand::Rng; -use tracing::info; type Aes128Ctr64BE = ctr::Ctr64BE; @@ -56,39 +55,20 @@ pub(crate) fn decode_auth_message( // RLP-decode the message. let (auth, _padding) = AuthMessage::decode_unfinished(&payload).unwrap(); - info!( - "signature: {:?} node_id: {:?} nonce: {:?}", - &auth.signature, &auth.node_id, &auth.nonce - ); - - let peer_pk = id2pubkey(auth.node_id).unwrap(); - // Derive a shared secret from the static keys. + let peer_pk = id2pubkey(auth.node_id).unwrap(); let static_shared_secret = ecdh_xchng(static_key, &peer_pk); - info!("token {static_shared_secret:?}"); - let remote_ephemeral_key = retrieve_remote_ephemeral_key(static_shared_secret.into(), auth.nonce, auth.signature); - - info!("remote pub key {remote_ephemeral_key:?}"); - (auth, remote_ephemeral_key) } /// Encodes an Ack message, to complete a handshake pub fn encode_ack_message( - static_key: &SecretKey, local_ephemeral_key: &SecretKey, local_nonce: H256, remote_static_pubkey: &PublicKey, - remote_ephemeral_key: &PublicKey, ) -> Vec { - // Derive a shared secret from the static keys. - let static_shared_secret = ecdh_xchng(static_key, remote_static_pubkey); - info!("token {static_shared_secret:?}"); - - info!("remote pub key {remote_ephemeral_key:?}"); - // Compose the ack message. let ack_msg = AckMessage::new(pubkey2id(&local_ephemeral_key.public_key()), local_nonce); diff --git a/crates/networking/p2p/rlpx/message.rs b/crates/networking/p2p/rlpx/message.rs index e94b2ccb8..8f06159be 100644 --- a/crates/networking/p2p/rlpx/message.rs +++ b/crates/networking/p2p/rlpx/message.rs @@ -1,7 +1,8 @@ use bytes::BufMut; use ethereum_rust_rlp::error::{RLPDecodeError, RLPEncodeError}; +use std::fmt::Display; -use super::eth::StatusMessage; +use super::eth::status::StatusMessage; use super::p2p::{DisconnectMessage, HelloMessage, PingMessage, PongMessage}; pub trait RLPxMessage: Sized { @@ -40,3 +41,15 @@ impl Message { } } } + +impl Display for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Hello(_) => "p2p:Hello".fmt(f), + Message::Disconnect(_) => "p2p:Disconnect".fmt(f), + Message::Ping(_) => "p2p:Ping".fmt(f), + Message::Pong(_) => "p2p:Pong".fmt(f), + Message::Status(_) => "eth:Status".fmt(f), + } + } +} diff --git a/crates/networking/p2p/rlpx/p2p.rs b/crates/networking/p2p/rlpx/p2p.rs index 852e66545..4521c5bef 100644 --- a/crates/networking/p2p/rlpx/p2p.rs +++ b/crates/networking/p2p/rlpx/p2p.rs @@ -1,7 +1,8 @@ use bytes::BufMut; use ethereum_rust_core::H512; use ethereum_rust_rlp::{ - encode::RLPEncode as _, + decode::RLPDecode, + encode::RLPEncode, error::{RLPDecodeError, RLPEncodeError}, structs::{Decoder, Encoder}, }; @@ -15,14 +16,43 @@ use super::{ utils::{pubkey2id, snappy_encode}, }; +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Capability { + P2p, + Eth, + Snap, +} + +impl RLPEncode for Capability { + fn encode(&self, buf: &mut dyn BufMut) { + match self { + Self::P2p => "p2p".encode(buf), + Self::Eth => "eth".encode(buf), + Self::Snap => "snap".encode(buf), + } + } +} + +impl RLPDecode for Capability { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let (cap_string, rest) = String::decode_unfinished(rlp)?; + match cap_string.as_str() { + "p2p" => Ok((Capability::P2p, rest)), + "eth" => Ok((Capability::Eth, rest)), + "snap" => Ok((Capability::Snap, rest)), + _ => Err(RLPDecodeError::UnexpectedString), + } + } +} + #[derive(Debug)] pub(crate) struct HelloMessage { - pub(crate) capabilities: Vec<(String, u8)>, + pub(crate) capabilities: Vec<(Capability, u8)>, pub(crate) node_id: PublicKey, } impl HelloMessage { - pub fn new(capabilities: Vec<(String, u8)>, node_id: PublicKey) -> Self { + pub fn new(capabilities: Vec<(Capability, u8)>, node_id: PublicKey) -> Self { Self { capabilities, node_id, @@ -55,7 +85,7 @@ impl RLPxMessage for HelloMessage { // TODO: store client id for debugging purposes // [[cap1, capVersion1], [cap2, capVersion2], ...] - let (capabilities, decoder): (Vec<(String, u8)>, _) = + let (capabilities, decoder): (Vec<(Capability, u8)>, _) = decoder.decode_field("capabilities").unwrap(); // This field should be ignored From 2210f1ebf9983f1ef7621a06a15304efb0b8d398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Wed, 30 Oct 2024 16:43:21 +0100 Subject: [PATCH 35/49] feat(l1): rudimentary syncing (#962) **Motivation** This PR adds the capability to store blocks even if their parents are missing, before returning "syncing" on NewPayload calls. This allows us to actually sync if needed, and also to know if we need to declare a certain payload invalid, even if it's temporarily. It also allows us to have the missing ancestor reorg tests, even if it doesn't fix syncing with P2P. **Changelist** - Fixes the ParentStateNotFound response to be invalid. - Saves the payload if syncing. - Triggers a check in fork choice updated and return invalid. Closes #894 Also part of #438 --- crates/blockchain/blockchain.rs | 13 +++++---- crates/blockchain/error.rs | 6 +++-- crates/blockchain/fork_choice.rs | 14 ++++++++++ crates/blockchain/smoke_test.rs | 32 +++++++++++++++++++++-- crates/networking/rpc/engine/payload.rs | 27 ++++++++----------- crates/storage/store/engines/api.rs | 3 +++ crates/storage/store/engines/in_memory.rs | 12 +++++++++ crates/storage/store/engines/libmdbx.rs | 19 ++++++++++++++ crates/storage/store/storage.rs | 13 +++++++++ 9 files changed, 114 insertions(+), 25 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 8cc3b8c35..527a0c767 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -28,10 +28,14 @@ use ethereum_rust_vm::{ /// /// Performs pre and post execution validation, and updates the database with the post state. pub fn add_block(block: &Block, storage: &Store) -> Result<(), ChainError> { - // TODO(#438): handle cases where blocks are missing between the canonical chain and the block. + let block_hash = block.header.compute_block_hash(); // Validate if it can be the new head and find the parent - let parent_header = find_parent_header(&block.header, storage)?; + let Ok(parent_header) = find_parent_header(&block.header, storage) else { + // If the parent is not present, we store it as pending. + storage.add_pending_block(block.clone())?; + return Err(ChainError::ParentNotFound); + }; let mut state = evm_state(storage.clone(), block.header.parent_hash); // Validate the block pre-execution @@ -47,12 +51,11 @@ pub fn add_block(block: &Block, storage: &Store) -> Result<(), ChainError> { let new_state_root = state .database() .apply_account_updates(block.header.parent_hash, &account_updates)? - .unwrap_or_default(); + .ok_or(ChainError::ParentStateNotFound)?; // Check state root matches the one in block header after execution validate_state_root(&block.header, new_state_root)?; - let block_hash = block.header.compute_block_hash(); store_block(storage, block.clone())?; store_receipts(storage, receipts, block_hash)?; @@ -105,7 +108,7 @@ pub fn latest_canonical_block_hash(storage: &Store) -> Result } /// Validates if the provided block could be the new head of the chain, and returns the -/// parent_header in that case +/// parent_header in that case. If not found, the new block is saved as pending. pub fn find_parent_header( block_header: &BlockHeader, storage: &Store, diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs index 9446795b4..230b0fff3 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -10,8 +10,8 @@ pub enum ChainError { ParentNotFound, //TODO: If a block with block_number greater than latest plus one is received //maybe we are missing data and should wait for syncing - #[error("Block number is not child of a canonical block.")] - NonCanonicalParent, + #[error("The post-state of the parent-block.")] + ParentStateNotFound, #[error("DB error: {0}")] StoreError(#[from] StoreError), #[error("EVM error: {0}")] @@ -83,4 +83,6 @@ pub enum InvalidForkChoice { Unordered, #[error("The following blocks are not connected between each other: {:?}, {:?}", ._0, ._1)] Disconnected(ForkChoiceElement, ForkChoiceElement), + #[error("Requested head is an invalid block.")] + InvalidHead, } diff --git a/crates/blockchain/fork_choice.rs b/crates/blockchain/fork_choice.rs index 876a113ff..3514d35cb 100644 --- a/crates/blockchain/fork_choice.rs +++ b/crates/blockchain/fork_choice.rs @@ -8,6 +8,7 @@ use crate::{ error::{self, InvalidForkChoice}, is_canonical, }; +use tracing::error; /// Applies new fork choice data to the current blockchain. It performs validity checks: /// - The finalized, safe and head hashes must correspond to already saved blocks. @@ -54,6 +55,9 @@ pub fn apply_fork_choice( } let Some(head_block) = head_res else { + if let Some(block) = store.get_pending_block(head_hash)? { + trigger_sync(block); + }; return Err(InvalidForkChoice::Syncing); }; @@ -137,6 +141,16 @@ pub fn apply_fork_choice( Ok(head) } +// Trigger a backfill sync from the block until we find a valid block that we're familiar with or +// something goes wrong. +fn trigger_sync(head_block: Block) { + // TODO(#438): add immediate reorg if all needed blocks are pending. + error!( + "A sync for block {} should be triggered but it's not yet supported.", + head_block.header.compute_block_hash() + ); +} + // Checks that block 1 is prior to block 2 and that if the second is present, the first one is too. fn check_order(block_1: &Option, block_2: &Option) -> Result<(), InvalidForkChoice> { // We don't need to perform the check if the hashes are null diff --git a/crates/blockchain/smoke_test.rs b/crates/blockchain/smoke_test.rs index 98bef2456..f9df6cbc5 100644 --- a/crates/blockchain/smoke_test.rs +++ b/crates/blockchain/smoke_test.rs @@ -1,10 +1,10 @@ #[cfg(test)] -mod test { +mod blockchain_integration_test { use std::{fs::File, io::BufReader}; use crate::{ add_block, - error::InvalidForkChoice, + error::{ChainError, InvalidForkChoice}, fork_choice::apply_fork_choice, is_canonical, latest_canonical_block_hash, payload::{build_payload, create_payload, BuildPayloadArgs}, @@ -67,6 +67,34 @@ mod test { assert!(!is_canonical(&store, 1, hash_1a).unwrap()); } + #[test] + fn test_sync_not_supported_yet() { + let store = test_store(); + let genesis_header = store.get_block_header(0).unwrap().unwrap(); + + // Build a single valid block. + let block_1 = new_block(&store, &genesis_header); + let hash_1 = block_1.header.compute_block_hash(); + add_block(&block_1, &store).unwrap(); + apply_fork_choice(&store, hash_1, H256::zero(), H256::zero()).unwrap(); + + // Build a child, then change its parent, making it effectively a pending block. + let mut block_2 = new_block(&store, &block_1.header); + block_2.header.parent_hash = H256::random(); + let hash_2 = block_2.header.compute_block_hash(); + let result = add_block(&block_2, &store); + assert!(matches!(result, Err(ChainError::ParentNotFound))); + + // block 2 should now be pending. + assert!(store.get_pending_block(hash_2).unwrap().is_some()); + + let fc_result = apply_fork_choice(&store, hash_2, H256::zero(), H256::zero()); + assert!(matches!(fc_result, Err(InvalidForkChoice::Syncing))); + + // block 2 should still be pending. + assert!(store.get_pending_block(hash_2).unwrap().is_some()); + } + #[test] fn test_reorg_from_long_to_short_chain() { // Store and genesis diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 2ff5d3381..02b27b812 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -5,7 +5,7 @@ use ethereum_rust_core::types::Fork; use ethereum_rust_core::{H256, U256}; use ethereum_rust_storage::Store; use serde_json::Value; -use tracing::{info, warn}; +use tracing::{error, info, warn}; use crate::types::payload::ExecutionPayloadResponse; use crate::utils::RpcRequest; @@ -112,27 +112,22 @@ impl RpcHandler for NewPayloadV3Request { .map_err(|error| RpcErr::Internal(error.to_string())); } - // Check that the incoming block extends the current chain - let last_block_number = storage.get_latest_block_number()?.ok_or(RpcErr::Internal( - "Could not get latest block number".to_owned(), - ))?; - - // NOTE: We should check if it's connected instead of future. - if block.header.number > last_block_number + 1 { - let result = PayloadStatus::syncing(); - return serde_json::to_value(result) - .map_err(|error| RpcErr::Internal(error.to_string())); - } - // Execute and store the block info!("Executing payload with block hash: {block_hash:#x}"); let payload_status = match add_block(&block, &storage) { - Err(ChainError::NonCanonicalParent) => Ok(PayloadStatus::syncing()), Err(ChainError::ParentNotFound) => Ok(PayloadStatus::syncing()), + // Under the current implementation this is not possible: we always calculate the state + // transition of any new payload as long as the parent is present. If we received the + // parent payload but it was stashed, then new payload would stash this one too, with a + // ParentNotFoundError. + Err(ChainError::ParentStateNotFound) => { + let e = "Failed to obtain parent state"; + error!("{e} for block {block_hash}"); + Err(RpcErr::Internal(e.to_string())) + } Err(ChainError::InvalidBlock(error)) => { warn!("Error adding block: {error}"); - // If we got to this point it means that the parent is present and valid, as we - // only save valid blocks. That means that the parent is the latest valid hash. + // TODO(#982): this is only valid for the cases where the parent was found, but fully invalid ones may also happen. Ok(PayloadStatus::invalid_with( block.header.parent_hash, error.to_string(), diff --git a/crates/storage/store/engines/api.rs b/crates/storage/store/engines/api.rs index d67385de4..cd2303e80 100644 --- a/crates/storage/store/engines/api.rs +++ b/crates/storage/store/engines/api.rs @@ -44,6 +44,9 @@ pub trait StoreEngine: Debug + Send + Sync + RefUnwindSafe { block_hash: BlockHash, ) -> Result, StoreError>; + fn add_pending_block(&self, block: Block) -> Result<(), StoreError>; + fn get_pending_block(&self, block_hash: BlockHash) -> Result, StoreError>; + /// Add block number for a given hash fn add_block_number( &self, diff --git a/crates/storage/store/engines/in_memory.rs b/crates/storage/store/engines/in_memory.rs index 46039ea3c..5249ebcf2 100644 --- a/crates/storage/store/engines/in_memory.rs +++ b/crates/storage/store/engines/in_memory.rs @@ -41,6 +41,7 @@ struct StoreInner { block_total_difficulties: HashMap, // Stores local blocks by payload id payloads: HashMap, + pending_blocks: HashMap, } #[derive(Default, Debug)] @@ -83,6 +84,17 @@ impl StoreEngine for Store { } } + fn add_pending_block(&self, block: Block) -> Result<(), StoreError> { + self.inner() + .pending_blocks + .insert(block.header.compute_block_hash(), block); + Ok(()) + } + + fn get_pending_block(&self, block_hash: BlockHash) -> Result, StoreError> { + Ok(self.inner().pending_blocks.get(&block_hash).cloned()) + } + fn add_block_header( &self, block_hash: BlockHash, diff --git a/crates/storage/store/engines/libmdbx.rs b/crates/storage/store/engines/libmdbx.rs index 0d2f75515..fd925d345 100644 --- a/crates/storage/store/engines/libmdbx.rs +++ b/crates/storage/store/engines/libmdbx.rs @@ -477,6 +477,19 @@ impl StoreEngine for Store { .map(|_| ()) .map_err(StoreError::LibmdbxError) } + + fn add_pending_block(&self, block: Block) -> std::result::Result<(), StoreError> { + self.write::(block.header.compute_block_hash().into(), block.into()) + } + + fn get_pending_block( + &self, + block_hash: BlockHash, + ) -> std::result::Result, StoreError> { + Ok(self + .read::(block_hash.into())? + .map(|b| b.to())) + } } impl Debug for Store { @@ -562,6 +575,11 @@ table!( ( Payloads ) u64 => BlockRLP ); +table!( + /// Stores blocks that are pending validation. + ( PendingBlocks ) BlockHashRLP => BlockRLP +); + // Storage values are stored as bytes instead of using their rlp encoding // As they are stored in a dupsort table, they need to have a fixed size, and encoding them doesn't preserve their size pub struct AccountStorageKeyBytes(pub [u8; 32]); @@ -661,6 +679,7 @@ pub fn init_db(path: Option>) -> Database { table_info!(StorageTriesNodes), table_info!(CanonicalBlockHashes), table_info!(Payloads), + table_info!(PendingBlocks), ] .into_iter() .collect(); diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index 3cdd94c04..36bda16a6 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -158,6 +158,19 @@ impl Store { self.engine.get_block_body(block_number) } + pub fn add_pending_block(&self, block: Block) -> Result<(), StoreError> { + info!( + "Adding block to pending: {}", + block.header.compute_block_hash() + ); + self.engine.add_pending_block(block) + } + + pub fn get_pending_block(&self, block_hash: BlockHash) -> Result, StoreError> { + info!("get pending: {}", block_hash); + self.engine.get_pending_block(block_hash) + } + pub fn add_block_number( &self, block_hash: BlockHash, From 1feceb6ad8341342ed9338bc1e198c29f1523810 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Wed, 30 Oct 2024 12:59:40 -0300 Subject: [PATCH 36/49] fix(l1): fix kurtosis version to the latest working: 1.3.1 (#1019) **Motivation** Kurtosis moved from 1.3.1 to 1.4.0 and our checks started failing when loading the kurtosis engine **Description** This PR fixes kurtosis version to the latest working one: 1.3.1 --- .github/workflows/assertoor.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/assertoor.yaml b/.github/workflows/assertoor.yaml index 0de3c36ed..77431ed9c 100644 --- a/.github/workflows/assertoor.yaml +++ b/.github/workflows/assertoor.yaml @@ -37,6 +37,7 @@ jobs: - name: Setup kurtosis testnet and run assertoor tests uses: ethpandaops/kurtosis-assertoor-github-action@v1 with: + kurtosis_version: '1.3.1' ethereum_package_url: 'github.com/lambdaclass/ethereum-package' ethereum_package_branch: 'ethereum-rust-integration' ethereum_package_args: './test_data/network_params.yaml' From b93011cc82b1ac6d3210cd92a0b382469c3059b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9fano=20Bargas?= Date: Wed, 30 Oct 2024 16:12:09 +0000 Subject: [PATCH 37/49] feat(l1): in-memory cache database and new `EvmState` (#888) **Motivation** implement a memory cache database and expand the `EvmState` struct into an enum. This is to take advantage of the block execution and validation implementations of the L1 to use it for proving in L2 mode. **Description** - adds new `EvmState` enum containing two variants (store and cache) - adds `ExecutionDB` database for storing only the relevant data of the execution of a block (this is going to be passed into a zkVM for proving) - adds and modifies some `EvmState` functions and adapts the codebase to those changes --------- Co-authored-by: fborello-lambda Co-authored-by: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> --- crates/blockchain/blockchain.rs | 6 +- crates/blockchain/payload.rs | 47 +++-- crates/networking/rpc/eth/transaction.rs | 3 +- crates/storage/store/error.rs | 2 + crates/vm/errors.rs | 40 +++- crates/vm/execution_db.rs | 129 +++++++++++++ crates/vm/vm.rs | 224 ++++++++++++++++------- 7 files changed, 366 insertions(+), 85 deletions(-) create mode 100644 crates/vm/execution_db.rs diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 527a0c767..63732b45d 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -50,6 +50,7 @@ pub fn add_block(block: &Block, storage: &Store) -> Result<(), ChainError> { // Apply the account updates over the last block's state and compute the new state root let new_state_root = state .database() + .ok_or(ChainError::StoreError(StoreError::MissingStore))? .apply_account_updates(block.header.parent_hash, &account_updates)? .ok_or(ChainError::ParentStateNotFound)?; @@ -127,7 +128,10 @@ pub fn validate_block( parent_header: &BlockHeader, state: &EvmState, ) -> Result<(), ChainError> { - let spec = spec_id(state.database(), block.header.timestamp).unwrap(); + let spec = spec_id( + &state.chain_config().map_err(ChainError::from)?, + block.header.timestamp, + ); // Verify initial header validity against parent validate_block_header(&block.header, parent_header).map_err(InvalidBlockError::from)?; diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index 3ad9398c0..09c3bbc52 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -7,8 +7,8 @@ use ethereum_rust_core::{ types::{ calculate_base_fee_per_blob_gas, calculate_base_fee_per_gas, compute_receipts_root, compute_transactions_root, compute_withdrawals_root, BlobsBundle, Block, BlockBody, - BlockHash, BlockHeader, BlockNumber, MempoolTransaction, Receipt, Transaction, Withdrawal, - DEFAULT_OMMERS_HASH, + BlockHash, BlockHeader, BlockNumber, ChainConfig, MempoolTransaction, Receipt, Transaction, + Withdrawal, DEFAULT_OMMERS_HASH, }, Address, Bloom, Bytes, H256, U256, }; @@ -180,10 +180,14 @@ impl<'a> PayloadBuildContext<'a> { self.payload.header.number } - fn store(&self) -> &Store { + fn store(&self) -> Option<&Store> { self.evm_state.database() } + fn chain_config(&self) -> Result { + self.evm_state.chain_config() + } + fn base_fee_per_gas(&self) -> Option { self.payload.header.base_fee_per_gas } @@ -205,7 +209,7 @@ pub fn build_payload( pub fn apply_withdrawals(context: &mut PayloadBuildContext) -> Result<(), EvmError> { // Apply withdrawals & call beacon root contract, and obtain the new state root - let spec_id = spec_id(context.store(), context.payload.header.timestamp)?; + let spec_id = spec_id(&context.chain_config()?, context.payload.header.timestamp); if context.payload.header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { beacon_root_contract_call(context.evm_state, &context.payload.header, spec_id)?; } @@ -233,15 +237,18 @@ fn fetch_mempool_transactions( only_blob_txs: true, ..tx_filter }; + let store = context.store().ok_or(StoreError::Custom( + "no store in the context (is an ExecutionDB being used?)".to_string(), + ))?; Ok(( // Plain txs TransactionQueue::new( - mempool::filter_transactions(&plain_tx_filter, context.store())?, + mempool::filter_transactions(&plain_tx_filter, store)?, context.base_fee_per_gas(), ), // Blob txs TransactionQueue::new( - mempool::filter_transactions(&blob_tx_filter, context.store())?, + mempool::filter_transactions(&blob_tx_filter, store)?, context.base_fee_per_gas(), ), )) @@ -250,7 +257,7 @@ fn fetch_mempool_transactions( /// Fills the payload with transactions taken from the mempool /// Returns the block value pub fn fill_transactions(context: &mut PayloadBuildContext) -> Result<(), ChainError> { - let chain_config = context.store().get_chain_config()?; + let chain_config = context.chain_config()?; debug!("Fetching transactions from mempool"); // Fetch mempool transactions let (mut plain_txs, mut blob_txs) = fetch_mempool_transactions(context)?; @@ -302,7 +309,12 @@ pub fn fill_transactions(context: &mut PayloadBuildContext) -> Result<(), ChainE // Pull transaction from the mempool debug!("Ignoring replay-protected transaction: {}", tx_hash); txs.pop(); - mempool::remove_transaction(tx_hash, context.store())?; + mempool::remove_transaction( + tx_hash, + context + .store() + .ok_or(ChainError::StoreError(StoreError::MissingStore))?, + )?; continue; } // Execute tx @@ -310,7 +322,12 @@ pub fn fill_transactions(context: &mut PayloadBuildContext) -> Result<(), ChainE Ok(receipt) => { txs.shift(); // Pull transaction from the mempool - mempool::remove_transaction(tx_hash, context.store())?; + mempool::remove_transaction( + tx_hash, + context + .store() + .ok_or(ChainError::StoreError(StoreError::MissingStore))?, + )?; receipt } // Ignore following txs from sender @@ -348,7 +365,11 @@ fn apply_blob_transaction( ) -> Result { // Fetch blobs bundle let tx_hash = head.tx.compute_hash(); - let Some(blobs_bundle) = context.store().get_blobs_bundle_from_pool(tx_hash)? else { + let Some(blobs_bundle) = context + .store() + .ok_or(ChainError::StoreError(StoreError::MissingStore))? + .get_blobs_bundle_from_pool(tx_hash)? + else { // No blob tx should enter the mempool without its blobs bundle so this is an internal error return Err( StoreError::Custom(format!("No blobs bundle found for blob tx {tx_hash}")).into(), @@ -379,7 +400,10 @@ fn apply_plain_transaction( &head.tx, &context.payload.header, context.evm_state, - spec_id(context.store(), context.payload.header.timestamp)?, + spec_id( + &context.chain_config().map_err(ChainError::from)?, + context.payload.header.timestamp, + ), )?; context.remaining_gas = context.remaining_gas.saturating_sub(result.gas_used()); context.block_value += U256::from(result.gas_used()) * head.tip; @@ -396,6 +420,7 @@ fn finalize_payload(context: &mut PayloadBuildContext) -> Result<(), StoreError> let account_updates = get_state_transitions(context.evm_state); context.payload.header.state_root = context .store() + .ok_or(StoreError::MissingStore)? .apply_account_updates(context.parent_hash(), &account_updates)? .unwrap_or_default(); context.payload.header.transactions_root = diff --git a/crates/networking/rpc/eth/transaction.rs b/crates/networking/rpc/eth/transaction.rs index a8d8481a3..d0cbcca82 100644 --- a/crates/networking/rpc/eth/transaction.rs +++ b/crates/networking/rpc/eth/transaction.rs @@ -431,7 +431,8 @@ impl RpcHandler for EstimateGasRequest { // Block not found _ => return Ok(Value::Null), }; - let spec_id = ethereum_rust_vm::spec_id(&storage, block_header.timestamp)?; + let spec_id = + ethereum_rust_vm::spec_id(&storage.get_chain_config()?, block_header.timestamp); // If the transaction is a plain value transfer, short circuit estimation. if let TxKind::Call(address) = self.transaction.to { diff --git a/crates/storage/store/error.rs b/crates/storage/store/error.rs index 5ed5dedaa..ce8b8f335 100644 --- a/crates/storage/store/error.rs +++ b/crates/storage/store/error.rs @@ -16,4 +16,6 @@ pub enum StoreError { RLPDecode(#[from] RLPDecodeError), #[error(transparent)] Trie(#[from] TrieError), + #[error("missing store: is an execution DB being used instead?")] + MissingStore, } diff --git a/crates/vm/errors.rs b/crates/vm/errors.rs index 2ba5b93b6..1454f9168 100644 --- a/crates/vm/errors.rs +++ b/crates/vm/errors.rs @@ -1,5 +1,7 @@ use ethereum_rust_storage::error::StoreError; -use revm::primitives::result::EVMError as RevmError; +use revm::primitives::{ + result::EVMError as RevmError, Address as RevmAddress, B256 as RevmB256, U256 as RevmU256, +}; use thiserror::Error; #[derive(Debug, Error)] @@ -10,12 +12,36 @@ pub enum EvmError { Header(String), #[error("DB error: {0}")] DB(#[from] StoreError), + #[error("Execution DB error: {0}")] + ExecutionDB(#[from] ExecutionDBError), #[error("{0}")] Custom(String), #[error("{0}")] Precompile(String), } +#[derive(Debug, Error)] +pub enum ExecutionDBError { + #[error("Store error: {0}")] + Store(#[from] StoreError), + #[error("Evm error: {0}")] + Evm(#[from] Box), // boxed to avoid cyclic definition + #[error("Account {0} not found")] + AccountNotFound(RevmAddress), + #[error("Code by hash {0} not found")] + CodeNotFound(RevmB256), + #[error("Storage value for address {0} and slot {1} not found")] + StorageNotFound(RevmAddress, RevmU256), + #[error("Hash of block with number {0} not found")] + BlockHashNotFound(u64), + #[error("Missing account {0} info while trying to create ExecutionDB")] + NewMissingAccountInfo(RevmAddress), + #[error("Missing earliest or latest block number while trying to create ExecutionDB")] + NewMissingBlockNumber(), + #[error("{0}")] + Custom(String), +} + impl From> for EvmError { fn from(value: RevmError) -> Self { match value { @@ -27,3 +53,15 @@ impl From> for EvmError { } } } + +impl From> for EvmError { + fn from(value: RevmError) -> Self { + match value { + RevmError::Transaction(err) => EvmError::Transaction(err.to_string()), + RevmError::Header(err) => EvmError::Header(err.to_string()), + RevmError::Database(err) => EvmError::ExecutionDB(err), + RevmError::Custom(err) => EvmError::Custom(err), + RevmError::Precompile(err) => EvmError::Precompile(err), + } + } +} diff --git a/crates/vm/execution_db.rs b/crates/vm/execution_db.rs new file mode 100644 index 000000000..2366f6342 --- /dev/null +++ b/crates/vm/execution_db.rs @@ -0,0 +1,129 @@ +use std::collections::HashMap; + +use ethereum_rust_core::types::{Block, ChainConfig}; +use ethereum_rust_storage::Store; +use revm::{ + primitives::{ + AccountInfo as RevmAccountInfo, Address as RevmAddress, Bytecode as RevmBytecode, + B256 as RevmB256, U256 as RevmU256, + }, + Database, DatabaseRef, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + db::StoreWrapper, errors::ExecutionDBError, evm_state, execute_block, get_state_transitions, +}; + +/// In-memory EVM database for caching execution data. +/// +/// This is mainly used to store the relevant state data for executing a particular block and then +/// feeding the DB into a zkVM program to prove the execution. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ExecutionDB { + /// indexed by account address + accounts: HashMap, + /// indexed by code hash + code: HashMap, + /// indexed by account address and storage slot + storage: HashMap>, + /// indexed by block number + block_hashes: HashMap, + /// stored chain config + chain_config: ChainConfig, +} + +impl ExecutionDB { + /// Creates a database by executing a block, without performing any validation. + pub fn from_exec(block: &Block, store: &Store) -> Result { + // TODO: perform validation to exit early + + let mut state = evm_state(store.clone(), block.header.parent_hash); + let mut store_wrapper = StoreWrapper { + store: store.clone(), + block_hash: block.header.parent_hash, + }; + + let chain_config = store.get_chain_config()?; + + execute_block(block, &mut state).map_err(Box::new)?; + + let account_updates = get_state_transitions(&mut state); + + let mut accounts = HashMap::new(); + let code = HashMap::new(); // TODO: `code` remains empty for now + let mut storage = HashMap::new(); + let block_hashes = HashMap::new(); // TODO: `block_hashes` remains empty for now + + for account_update in account_updates { + let address = RevmAddress::from_slice(account_update.address.as_bytes()); + let account_info = store_wrapper + .basic(address)? + .ok_or(ExecutionDBError::NewMissingAccountInfo(address))?; + accounts.insert(address, account_info); + + let account_storage = account_update + .added_storage + .iter() + .map(|(slot, value)| { + let mut value_bytes = [0u8; 32]; + value.to_big_endian(&mut value_bytes); + ( + RevmU256::from_be_bytes(slot.to_fixed_bytes()), + RevmU256::from_be_slice(&value_bytes), + ) + }) + .collect(); + + storage.insert(address, account_storage); + } + + Ok(Self { + accounts, + code, + storage, + block_hashes, + chain_config, + }) + } + + pub fn get_chain_config(&self) -> ChainConfig { + self.chain_config + } +} + +impl DatabaseRef for ExecutionDB { + /// The database error type. + type Error = ExecutionDBError; + + /// Get basic account information. + fn basic_ref(&self, address: RevmAddress) -> Result, Self::Error> { + Ok(self.accounts.get(&address).cloned()) + } + + /// Get account code by its hash. + fn code_by_hash_ref(&self, code_hash: RevmB256) -> Result { + self.code + .get(&code_hash) + .cloned() + .ok_or(ExecutionDBError::CodeNotFound(code_hash)) + } + + /// Get storage value of address at index. + fn storage_ref(&self, address: RevmAddress, index: RevmU256) -> Result { + self.storage + .get(&address) + .ok_or(ExecutionDBError::AccountNotFound(address))? + .get(&index) + .cloned() + .ok_or(ExecutionDBError::StorageNotFound(address, index)) + } + + /// Get block hash by block number. + fn block_hash_ref(&self, number: u64) -> Result { + self.block_hashes + .get(&number) + .cloned() + .ok_or(ExecutionDBError::BlockHashNotFound(number)) + } +} diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 617a48882..99374a5a1 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -1,23 +1,25 @@ mod db; mod errors; +pub mod execution_db; mod execution_result; #[cfg(feature = "l2")] mod mods; use db::StoreWrapper; +use execution_db::ExecutionDB; use std::cmp::min; use ethereum_rust_core::{ types::{ - AccountInfo, Block, BlockHash, BlockHeader, Fork, GenericTransaction, PrivilegedTxType, - Receipt, Transaction, TxKind, Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, + AccountInfo, Block, BlockHash, BlockHeader, ChainConfig, Fork, GenericTransaction, + PrivilegedTxType, Receipt, Transaction, TxKind, Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, }, Address, BigEndianHash, H256, U256, }; use ethereum_rust_storage::{error::StoreError, AccountUpdate, Store}; use lazy_static::lazy_static; use revm::{ - db::{states::bundle_state::BundleRetention, AccountStatus}, + db::{states::bundle_state::BundleRetention, AccountStatus, State as RevmState}, inspector_handle_register, inspectors::TracerEip3155, precompile::{PrecompileSpecId, Precompiles}, @@ -40,21 +42,39 @@ type AccessList = Vec<(Address, Vec)>; pub const WITHDRAWAL_MAGIC_DATA: &[u8] = b"burn"; pub const DEPOSIT_MAGIC_DATA: &[u8] = b"mint"; -/// State used when running the EVM -// Encapsulates state behaviour to be agnostic to the evm implementation for crate users -pub struct EvmState(revm::db::State); +/// State used when running the EVM. The state can be represented with a [StoreWrapper] database, or +/// with a [ExecutionDB] in case we only want to store the necessary data for some particular +/// execution, for example when proving in L2 mode. +/// +/// Encapsulates state behaviour to be agnostic to the evm implementation for crate users. +pub enum EvmState { + Store(revm::db::State), + Execution(revm::db::CacheDB), +} impl EvmState { /// Get a reference to inner `Store` database - pub fn database(&self) -> &Store { - &self.0.database.store + pub fn database(&self) -> Option<&Store> { + if let EvmState::Store(db) = self { + Some(&db.database.store) + } else { + None + } + } + + /// Gets the stored chain config + pub fn chain_config(&self) -> Result { + match self { + EvmState::Store(db) => db.database.store.get_chain_config().map_err(EvmError::from), + EvmState::Execution(db) => Ok(db.db.get_chain_config()), + } } } /// Executes all transactions in a block and returns their receipts. pub fn execute_block(block: &Block, state: &mut EvmState) -> Result, EvmError> { let block_header = &block.header; - let spec_id = spec_id(state.database(), block_header.timestamp)?; + let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); //eip 4788: execute beacon_root_contract_call before block transactions if block_header.parent_beacon_block_root.is_some() && spec_id == SpecId::CANCUN { beacon_root_contract_call(state, block_header, spec_id)?; @@ -129,10 +149,9 @@ fn run_evm( spec_id: SpecId, ) -> Result { let tx_result = { - let chain_spec = state.database().get_chain_config()?; + let chain_spec = state.chain_config()?; #[allow(unused_mut)] let mut evm_builder = Evm::builder() - .with_db(&mut state.0) .with_block_env(block_env) .with_tx_env(tx_env) .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) @@ -156,8 +175,17 @@ fn run_evm( }); } } - let mut evm = evm_builder.build(); - evm.transact_commit().map_err(EvmError::from)? + + match state { + EvmState::Store(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact_commit().map_err(EvmError::from)? + } + EvmState::Execution(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact_commit().map_err(EvmError::from)? + } + } }; Ok(tx_result.into()) } @@ -207,20 +235,34 @@ fn create_access_list_inner( spec_id: SpecId, ) -> Result<(ExecutionResult, RevmAccessList), EvmError> { let mut access_list_inspector = access_list_inspector(&tx_env, state, spec_id)?; + #[allow(unused_mut)] + let mut evm_builder = Evm::builder() + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .modify_cfg_env(|env| { + env.disable_base_fee = true; + env.disable_block_gas_limit = true + }) + .with_external_context(&mut access_list_inspector); + let tx_result = { - let mut evm = Evm::builder() - .with_db(&mut state.0) - .with_block_env(block_env) - .with_tx_env(tx_env) - .with_spec_id(spec_id) - .modify_cfg_env(|env| { - env.disable_base_fee = true; - env.disable_block_gas_limit = true - }) - .with_external_context(&mut access_list_inspector) - .append_handler_register(inspector_handle_register) - .build(); - evm.transact().map_err(EvmError::from)? + match state { + EvmState::Store(db) => { + let mut evm = evm_builder + .with_db(db) + .append_handler_register(inspector_handle_register) + .build(); + evm.transact().map_err(EvmError::from)? + } + EvmState::Execution(db) => { + let mut evm = evm_builder + .with_db(db) + .append_handler_register(inspector_handle_register) + .build(); + evm.transact().map_err(EvmError::from)? + } + } }; let access_list = access_list_inspector.into_access_list(); @@ -239,9 +281,9 @@ fn run_without_commit( tx_env.gas_price, tx_env.max_fee_per_blob_gas, ); - let chain_config = state.database().get_chain_config()?; - let mut evm = Evm::builder() - .with_db(&mut state.0) + let chain_config = state.chain_config()?; + #[allow(unused_mut)] + let mut evm_builder = Evm::builder() .with_block_env(block_env) .with_tx_env(tx_env) .with_spec_id(spec_id) @@ -249,17 +291,34 @@ fn run_without_commit( env.disable_base_fee = true; env.disable_block_gas_limit = true; env.chain_id = chain_config.chain_id; - }) - .build(); - let tx_result = evm.transact().map_err(EvmError::from)?; + }); + let tx_result = match state { + EvmState::Store(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact().map_err(EvmError::from)? + } + EvmState::Execution(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact().map_err(EvmError::from)? + } + }; Ok(tx_result.result.into()) } /// Merges transitions stored when executing transactions and returns the resulting account updates /// Doesn't update the DB pub fn get_state_transitions(state: &mut EvmState) -> Vec { - state.0.merge_transitions(BundleRetention::PlainState); - let bundle = state.0.take_bundle(); + let bundle = match state { + EvmState::Store(db) => { + db.merge_transitions(BundleRetention::PlainState); + db.take_bundle() + } + EvmState::Execution(db) => { + let mut db = RevmState::builder().with_database_ref(db).build(); + db.merge_transitions(BundleRetention::PlainState); + db.take_bundle() + } + }; // Update accounts let mut account_updates = Vec::new(); for (address, account) in bundle.state() { @@ -328,25 +387,34 @@ pub fn process_withdrawals( state: &mut EvmState, withdrawals: &[Withdrawal], ) -> Result<(), StoreError> { - //balance_increments is a vector of tuples (Address, increment as u128) - let balance_increments = withdrawals - .iter() - .filter(|withdrawal| withdrawal.amount > 0) - .map(|withdrawal| { - ( - RevmAddress::from_slice(withdrawal.address.as_bytes()), - (withdrawal.amount as u128 * GWEI_TO_WEI as u128), - ) - }) - .collect::>(); - - state.0.increment_balances(balance_increments)?; + match state { + EvmState::Store(db) => { + //balance_increments is a vector of tuples (Address, increment as u128) + let balance_increments = withdrawals + .iter() + .filter(|withdrawal| withdrawal.amount > 0) + .map(|withdrawal| { + ( + RevmAddress::from_slice(withdrawal.address.as_bytes()), + (withdrawal.amount as u128 * GWEI_TO_WEI as u128), + ) + }) + .collect::>(); + + db.increment_balances(balance_increments)?; + } + EvmState::Execution(_) => { + // TODO: We should check withdrawals are valid + // (by checking that accounts exist if this is the only error) but there's no state to + // change. + } + } Ok(()) } /// Builds EvmState from a Store pub fn evm_state(store: Store, block_hash: BlockHash) -> EvmState { - EvmState( + EvmState::Store( revm::db::State::builder() .with_database(StoreWrapper { store, block_hash }) .with_bundle_update() @@ -390,21 +458,37 @@ pub fn beacon_root_contract_call( block_env.basefee = RevmU256::ZERO; block_env.gas_limit = RevmU256::from(30_000_000); - let mut evm = Evm::builder() - .with_db(&mut state.0) - .with_block_env(block_env) - .with_tx_env(tx_env) - .with_spec_id(spec_id) - .build(); + match state { + EvmState::Store(db) => { + let mut evm = Evm::builder() + .with_db(db) + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .build(); - let transaction_result = evm.transact()?; - let mut result_state = transaction_result.state; - result_state.remove(&*SYSTEM_ADDRESS); - result_state.remove(&evm.block().coinbase); + let transaction_result = evm.transact()?; + let mut result_state = transaction_result.state; + result_state.remove(&*SYSTEM_ADDRESS); + result_state.remove(&evm.block().coinbase); - evm.context.evm.db.commit(result_state); + evm.context.evm.db.commit(result_state); - Ok(transaction_result.result.into()) + Ok(transaction_result.result.into()) + } + EvmState::Execution(db) => { + let mut evm = Evm::builder() + .with_db(db) + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .build(); + + // Not necessary to commit to DB + let transaction_result = evm.transact()?; + Ok(transaction_result.result.into()) + } + } } pub fn block_env(header: &BlockHeader) -> BlockEnv { @@ -559,11 +643,12 @@ fn access_list_inspector( let to = match tx_env.transact_to { RevmTxKind::Call(address) => address, RevmTxKind::Create => { - let nonce = state - .0 - .basic(tx_env.caller)? - .map(|info| info.nonce) - .unwrap_or_default(); + let nonce = match state { + EvmState::Store(db) => db.basic(tx_env.caller)?, + EvmState::Execution(db) => db.basic(tx_env.caller)?, + } + .map(|info| info.nonce) + .unwrap_or_default(); tx_env.caller.create(nonce) } }; @@ -577,15 +662,12 @@ fn access_list_inspector( /// Returns the spec id according to the block timestamp and the stored chain config /// WARNING: Assumes at least Merge fork is active -pub fn spec_id(store: &Store, block_timestamp: u64) -> Result { - let chain_config = store.get_chain_config()?; - let spec = match chain_config.get_fork(block_timestamp) { +pub fn spec_id(chain_config: &ChainConfig, block_timestamp: u64) -> SpecId { + match chain_config.get_fork(block_timestamp) { Fork::Cancun => SpecId::CANCUN, Fork::Shanghai => SpecId::SHANGHAI, Fork::Paris => SpecId::MERGE, - }; - - Ok(spec) + } } /// Calculating gas_price according to EIP-1559 rules From 3e0a4cf851305e11eed2cb75dcb9a646e892ca79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Wed, 30 Oct 2024 17:24:48 +0100 Subject: [PATCH 38/49] feat(l1): compute block hash once (#1021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Description** Use OnceCell to cache hash computations for a single block in a thread safe way. It also doesn't require the block to be mutable in order to update the cache, which is ideal. On a mac M1: - The first hash computation (for a default block) takes about 150-450 μs. Storing it in the OnceCell is about 10μs. - The subsequent hash() calls that get from the cache take ~40ns (10k times better). Follow-up from #962 closes #1014 --- cmd/ef_tests/test_runner.rs | 2 +- cmd/ef_tests/types.rs | 8 +-- cmd/ethereum_rust/decode.rs | 6 +- cmd/ethereum_rust/ethereum_rust.rs | 4 +- crates/blockchain/payload.rs | 82 ++++++++++++------------- crates/blockchain/smoke_test.rs | 24 ++++---- crates/common/Cargo.toml | 1 + crates/common/types/block.rs | 37 ++++++++++- crates/common/types/genesis.rs | 8 +-- crates/l2/proposer/mod.rs | 2 +- crates/networking/rpc/engine/payload.rs | 2 +- crates/networking/rpc/eth/block.rs | 2 +- crates/networking/rpc/eth/fee_market.rs | 2 +- crates/networking/rpc/eth/gas_price.rs | 25 ++------ crates/networking/rpc/types/block.rs | 9 +-- crates/networking/rpc/types/payload.rs | 56 ++++++++--------- crates/storage/store/engines/api.rs | 2 +- crates/storage/store/engines/libmdbx.rs | 2 +- crates/storage/store/storage.rs | 2 +- 19 files changed, 144 insertions(+), 132 deletions(-) diff --git a/cmd/ef_tests/test_runner.rs b/cmd/ef_tests/test_runner.rs index 77d320cf1..2bfb66507 100644 --- a/cmd/ef_tests/test_runner.rs +++ b/cmd/ef_tests/test_runner.rs @@ -30,7 +30,7 @@ pub fn run_ef_test(test_key: &str, test: &TestUnit) { // Won't panic because test has been validated let block: &CoreBlock = &block_fixture.block().unwrap().clone().into(); - let hash = block.header.compute_block_hash(); + let hash = block.hash(); // Attempt to add the block as the head of the chain let chain_result = add_block(block, &store); diff --git a/cmd/ef_tests/types.rs b/cmd/ef_tests/types.rs index 0e2deb7c1..05a525475 100644 --- a/cmd/ef_tests/types.rs +++ b/cmd/ef_tests/types.rs @@ -175,14 +175,14 @@ impl BlockWithRLP { } impl From for CoreBlock { fn from(val: Block) -> Self { - Self { - header: val.block_header.into(), - body: BlockBody { + CoreBlock::new( + val.block_header.into(), + BlockBody { transactions: val.transactions.iter().map(|t| t.clone().into()).collect(), ommers: val.uncle_headers.iter().map(|h| h.clone().into()).collect(), withdrawals: val.withdrawals, }, - } + ) } } diff --git a/cmd/ethereum_rust/decode.rs b/cmd/ethereum_rust/decode.rs index 0f6b611a2..a3a01a48d 100644 --- a/cmd/ethereum_rust/decode.rs +++ b/cmd/ethereum_rust/decode.rs @@ -56,19 +56,19 @@ mod tests { assert_eq!( H256::from_str("0xac5c61edb087a51279674fe01d5c1f65eac3fd8597f9bea215058e745df8088e") .unwrap(), - blocks.first().unwrap().header.compute_block_hash(), + blocks.first().unwrap().hash(), "First block hash does not match" ); assert_eq!( H256::from_str("0xa111ce2477e1dd45173ba93cac819e62947e62a63a7d561b6f4825fb31c22645") .unwrap(), - blocks.get(1).unwrap().header.compute_block_hash(), + blocks.get(1).unwrap().hash(), "Second block hash does not match" ); assert_eq!( H256::from_str("0x8f64c4436f7213cfdf02cfb9f45d012f1774dfb329b8803de5e7479b11586902") .unwrap(), - blocks.get(19).unwrap().header.compute_block_hash(), + blocks.get(19).unwrap().hash(), "Last block hash does not match" ); } diff --git a/cmd/ethereum_rust/ethereum_rust.rs b/cmd/ethereum_rust/ethereum_rust.rs index ff1e49803..f9b99bf60 100644 --- a/cmd/ethereum_rust/ethereum_rust.rs +++ b/cmd/ethereum_rust/ethereum_rust.rs @@ -125,7 +125,7 @@ async fn main() { let blocks = read_chain_file(chain_rlp_path); let size = blocks.len(); for block in &blocks { - let hash = block.header.compute_block_hash(); + let hash = block.hash(); info!( "Adding block {} with hash {:#x}.", block.header.number, hash @@ -139,7 +139,7 @@ async fn main() { } } if let Some(last_block) = blocks.last() { - let hash = last_block.header.compute_block_hash(); + let hash = last_block.hash(); apply_fork_choice(&store, hash, hash, hash).unwrap(); } info!("Added {} blocks to blockchain", size); diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index 09c3bbc52..885cac812 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -69,51 +69,51 @@ pub fn create_payload(args: &BuildPayloadArgs, storage: &Store) -> Result u64 { diff --git a/crates/blockchain/smoke_test.rs b/crates/blockchain/smoke_test.rs index f9df6cbc5..f2897b7ff 100644 --- a/crates/blockchain/smoke_test.rs +++ b/crates/blockchain/smoke_test.rs @@ -25,7 +25,7 @@ mod blockchain_integration_test { // Add first block. We'll make it canonical. let block_1a = new_block(&store, &genesis_header); - let hash_1a = block_1a.header.compute_block_hash(); + let hash_1a = block_1a.hash(); add_block(&block_1a, &store).unwrap(); store.set_canonical_block(1, hash_1a).unwrap(); let retrieved_1a = store.get_block_header(1).unwrap().unwrap(); @@ -35,7 +35,7 @@ mod blockchain_integration_test { // Add second block at height 1. Will not be canonical. let block_1b = new_block(&store, &genesis_header); - let hash_1b = block_1b.header.compute_block_hash(); + let hash_1b = block_1b.hash(); add_block(&block_1b, &store).expect("Could not add block 1b."); let retrieved_1b = store.get_block_header_by_hash(hash_1b).unwrap().unwrap(); @@ -44,7 +44,7 @@ mod blockchain_integration_test { // Add a third block at height 2, child to the non canonical block. let block_2 = new_block(&store, &block_1b.header); - let hash_2 = block_2.header.compute_block_hash(); + let hash_2 = block_2.hash(); add_block(&block_2, &store).expect("Could not add block 2."); let retrieved_2 = store.get_block_header_by_hash(hash_2).unwrap(); @@ -54,7 +54,7 @@ mod blockchain_integration_test { // Receive block 2 as new head. apply_fork_choice( &store, - block_2.header.compute_block_hash(), + block_2.hash(), genesis_header.compute_block_hash(), genesis_header.compute_block_hash(), ) @@ -104,7 +104,7 @@ mod blockchain_integration_test { // Add first block. Not canonical. let block_1a = new_block(&store, &genesis_header); - let hash_1a = block_1a.header.compute_block_hash(); + let hash_1a = block_1a.hash(); add_block(&block_1a, &store).unwrap(); let retrieved_1a = store.get_block_header_by_hash(hash_1a).unwrap().unwrap(); @@ -112,7 +112,7 @@ mod blockchain_integration_test { // Add second block at height 1. Canonical. let block_1b = new_block(&store, &genesis_header); - let hash_1b = block_1b.header.compute_block_hash(); + let hash_1b = block_1b.hash(); add_block(&block_1b, &store).expect("Could not add block 1b."); apply_fork_choice(&store, hash_1b, genesis_hash, genesis_hash).unwrap(); let retrieved_1b = store.get_block_header(1).unwrap().unwrap(); @@ -124,7 +124,7 @@ mod blockchain_integration_test { // Add a third block at height 2, child to the canonical one. let block_2 = new_block(&store, &block_1b.header); - let hash_2 = block_2.header.compute_block_hash(); + let hash_2 = block_2.hash(); add_block(&block_2, &store).expect("Could not add block 2."); apply_fork_choice(&store, hash_2, genesis_hash, genesis_hash).unwrap(); let retrieved_2 = store.get_block_header_by_hash(hash_2).unwrap(); @@ -137,7 +137,7 @@ mod blockchain_integration_test { // Receive block 1a as new head. apply_fork_choice( &store, - block_1a.header.compute_block_hash(), + block_1a.hash(), genesis_header.compute_block_hash(), genesis_header.compute_block_hash(), ) @@ -159,12 +159,12 @@ mod blockchain_integration_test { // Add block at height 1. let block_1 = new_block(&store, &genesis_header); - let hash_1 = block_1.header.compute_block_hash(); + let hash_1 = block_1.hash(); add_block(&block_1, &store).expect("Could not add block 1b."); // Add child at height 2. let block_2 = new_block(&store, &block_1.header); - let hash_2 = block_2.header.compute_block_hash(); + let hash_2 = block_2.hash(); add_block(&block_2, &store).expect("Could not add block 2."); assert!(!is_canonical(&store, 1, hash_1).unwrap()); @@ -205,7 +205,7 @@ mod blockchain_integration_test { // Add child at height 2. let block_2 = new_block(&store, &block_1.header); - let hash_2 = block_2.header.compute_block_hash(); + let hash_2 = block_2.hash(); add_block(&block_2, &store).expect("Could not add block 2."); assert_eq!(latest_canonical_block_hash(&store).unwrap(), genesis_hash); @@ -217,7 +217,7 @@ mod blockchain_integration_test { // Add a new, non canonical block, starting from genesis. let block_1b = new_block(&store, &genesis_header); - let hash_b = block_1b.header.compute_block_hash(); + let hash_b = block_1b.hash(); add_block(&block_1b, &store).expect("Could not add block b."); // The latest block should be the same. diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 21fdf5b70..2e0fee151 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -20,6 +20,7 @@ secp256k1 = { version = "0.29", default-features = false, features = [ "global-context", "recovery", ] } +once_cell = "1.20.2" crc32fast.workspace = true bytes.workspace = true hex.workspace = true diff --git a/crates/common/types/block.rs b/crates/common/types/block.rs index f6fc90d45..ba6261314 100644 --- a/crates/common/types/block.rs +++ b/crates/common/types/block.rs @@ -24,6 +24,7 @@ pub type BlockNumber = u64; pub type BlockHash = H256; use lazy_static::lazy_static; +use once_cell::sync::OnceCell; lazy_static! { pub static ref DEFAULT_OMMERS_HASH: H256 = H256::from_slice(&hex::decode("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347").unwrap()); // = Keccak256(RLP([])) as of EIP-3675 @@ -32,6 +33,22 @@ lazy_static! { pub struct Block { pub header: BlockHeader, pub body: BlockBody, + #[serde(skip)] + hash: OnceCell, +} + +impl Block { + pub fn new(header: BlockHeader, body: BlockBody) -> Block { + Block { + header, + body, + hash: OnceCell::new(), + } + } + + pub fn hash(&self) -> BlockHash { + *self.hash.get_or_init(|| self.header.compute_block_hash()) + } } impl RLPEncode for Block { @@ -58,7 +75,7 @@ impl RLPDecode for Block { ommers, withdrawals, }; - let block = Block { header, body }; + let block = Block::new(header, body); Ok((block, remaining)) } } @@ -527,13 +544,27 @@ fn calc_excess_blob_gas(parent_header: &BlockHeader) -> u64 { #[cfg(test)] mod test { - - use std::str::FromStr; + use std::{str::FromStr, time::Instant}; use super::*; use ethereum_types::H160; use hex_literal::hex; + #[test] + fn compute_hash() { + let block = Block::default(); + + let start = Instant::now(); + block.hash(); + let duration = start.elapsed(); + + let start_2 = Instant::now(); + block.hash(); + let duration_2 = start_2.elapsed(); + + assert!(duration > 1000 * duration_2); + } + #[test] fn test_compute_withdrawals_root() { // Source: https://github.com/ethereum/tests/blob/9760400e667eba241265016b02644ef62ab55de2/BlockchainTests/EIPTests/bc4895-withdrawals/amountIs0.json diff --git a/crates/common/types/genesis.rs b/crates/common/types/genesis.rs index 8155d8b81..fddf86f27 100644 --- a/crates/common/types/genesis.rs +++ b/crates/common/types/genesis.rs @@ -171,9 +171,7 @@ pub struct GenesisAccount { impl Genesis { pub fn get_block(&self) -> Block { - let header = self.get_block_header(); - let body = self.get_block_body(); - Block { header, body } + Block::new(self.get_block_header(), self.get_block_body()) } fn get_block_header(&self) -> BlockHeader { @@ -379,7 +377,7 @@ mod tests { let reader = BufReader::new(file); let genesis: Genesis = serde_json::from_reader(reader).expect("Failed to deserialize genesis file"); - let genesis_block_hash = genesis.get_block().header.compute_block_hash(); + let genesis_block_hash = genesis.get_block().hash(); assert_eq!( genesis_block_hash, H256::from_str("0xcb5306dd861d0f2c1f9952fbfbc75a46d0b6ce4f37bea370c3471fe8410bf40b") @@ -403,7 +401,7 @@ mod tests { let reader = BufReader::new(file); let genesis: Genesis = serde_json::from_reader(reader).expect("Failed to deserialize genesis file"); - let computed_block_hash = genesis.get_block().header.compute_block_hash(); + let computed_block_hash = genesis.get_block().hash(); let genesis_block_hash = H256::from_str("0x30f516e34fc173bb5fc4daddcc7532c4aca10b702c7228f3c806b4df2646fb7e") .unwrap(); diff --git a/crates/l2/proposer/mod.rs b/crates/l2/proposer/mod.rs index eb982215f..c4af65dc1 100644 --- a/crates/l2/proposer/mod.rs +++ b/crates/l2/proposer/mod.rs @@ -155,7 +155,7 @@ impl Proposer { }; let new_state_root_hash = store - .state_trie(block.header.compute_block_hash()) + .state_trie(block.hash()) .unwrap() .unwrap() .hash() diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 02b27b812..59e6e2ed5 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -83,7 +83,7 @@ impl RpcHandler for NewPayloadV3Request { } // Check that block_hash is valid - let actual_block_hash = block.header.compute_block_hash(); + let actual_block_hash = block.hash(); if block_hash != actual_block_hash { let result = PayloadStatus::invalid_with_err("Invalid block hash"); return serde_json::to_value(result) diff --git a/crates/networking/rpc/eth/block.rs b/crates/networking/rpc/eth/block.rs index 6474ec7d0..fea369977 100644 --- a/crates/networking/rpc/eth/block.rs +++ b/crates/networking/rpc/eth/block.rs @@ -258,7 +258,7 @@ impl RpcHandler for GetRawBlockRequest { (Some(header), Some(body)) => (header, body), _ => return Ok(Value::Null), }; - let block = Block { header, body }.encode_to_vec(); + let block = Block::new(header, body).encode_to_vec(); serde_json::to_value(format!("0x{}", &hex::encode(block))) .map_err(|error| RpcErr::Internal(error.to_string())) diff --git a/crates/networking/rpc/eth/fee_market.rs b/crates/networking/rpc/eth/fee_market.rs index bea60a435..cc83a1f73 100644 --- a/crates/networking/rpc/eth/fee_market.rs +++ b/crates/networking/rpc/eth/fee_market.rs @@ -110,7 +110,7 @@ impl RpcHandler for FeeHistoryRequest { ); if let Some(percentiles) = &self.reward_percentiles { - let block = Block { header, body }; + let block = Block::new(header, body); reward.push(Self::calculate_percentiles_for_block(block, percentiles)); } } diff --git a/crates/networking/rpc/eth/gas_price.rs b/crates/networking/rpc/eth/gas_price.rs index 2549af92f..8fc995146 100644 --- a/crates/networking/rpc/eth/gas_price.rs +++ b/crates/networking/rpc/eth/gas_price.rs @@ -211,10 +211,7 @@ mod tests { withdrawals: Default::default(), }; let block_header = test_header(block_num); - let block = Block { - body: block_body, - header: block_header.clone(), - }; + let block = Block::new(block_header.clone(), block_body); store.add_block(block).unwrap(); store .set_canonical_block(block_num, block_header.compute_block_hash()) @@ -241,10 +238,7 @@ mod tests { withdrawals: Default::default(), }; let block_header = test_header(block_num); - let block = Block { - body: block_body, - header: block_header.clone(), - }; + let block = Block::new(block_header.clone(), block_body); store.add_block(block).unwrap(); store .set_canonical_block(block_num, block_header.compute_block_hash()) @@ -272,10 +266,7 @@ mod tests { withdrawals: Default::default(), }; let block_header = test_header(block_num); - let block = Block { - body: block_body, - header: block_header.clone(), - }; + let block = Block::new(block_header.clone(), block_body); store.add_block(block).unwrap(); store .set_canonical_block(block_num, block_header.compute_block_hash()) @@ -298,10 +289,7 @@ mod tests { withdrawals: Default::default(), }; let block_header = test_header(block_num); - let block = Block { - body: block_body, - header: block_header.clone(), - }; + let block = Block::new(block_header.clone(), block_body); store.add_block(block).unwrap(); store .set_canonical_block(block_num, block_header.compute_block_hash()) @@ -342,10 +330,7 @@ mod tests { withdrawals: Default::default(), }; let block_header = test_header(block_num); - let block = Block { - body: block_body, - header: block_header.clone(), - }; + let block = Block::new(block_header.clone(), block_body); storage.add_block(block).unwrap(); storage .set_canonical_block(block_num, block_header.compute_block_hash()) diff --git a/crates/networking/rpc/types/block.rs b/crates/networking/rpc/types/block.rs index 1bc2ca2f3..0276f6717 100644 --- a/crates/networking/rpc/types/block.rs +++ b/crates/networking/rpc/types/block.rs @@ -52,12 +52,9 @@ impl RpcBlock { full_transactions: bool, total_difficulty: U256, ) -> RpcBlock { - let size = Block { - header: header.clone(), - body: body.clone(), - } - .encode_to_vec() - .len(); + let size = Block::new(header.clone(), body.clone()) + .encode_to_vec() + .len(); let body_wrapper = if full_transactions { BlockBodyWrapper::Full(FullBlockBody::from_body(body, header.number, hash)) } else { diff --git a/crates/networking/rpc/types/payload.rs b/crates/networking/rpc/types/payload.rs index 2e2111b07..b1abb57cb 100644 --- a/crates/networking/rpc/types/payload.rs +++ b/crates/networking/rpc/types/payload.rs @@ -91,33 +91,33 @@ impl ExecutionPayloadV3 { ommers: vec![], withdrawals: Some(self.withdrawals), }; - Ok(Block { - header: BlockHeader { - parent_hash: self.parent_hash, - ommers_hash: *DEFAULT_OMMERS_HASH, - coinbase: self.fee_recipient, - state_root: self.state_root, - transactions_root: compute_transactions_root(&body.transactions), - receipts_root: self.receipts_root, - logs_bloom: self.logs_bloom, - difficulty: 0.into(), - number: self.block_number, - gas_limit: self.gas_limit, - gas_used: self.gas_used, - timestamp: self.timestamp, - extra_data: self.extra_data, - prev_randao: self.prev_randao, - nonce: 0, - base_fee_per_gas: Some(self.base_fee_per_gas), - withdrawals_root: Some(compute_withdrawals_root( - &body.withdrawals.clone().unwrap_or_default(), - )), - blob_gas_used: Some(self.blob_gas_used), - excess_blob_gas: Some(self.excess_blob_gas), - parent_beacon_block_root: Some(parent_beacon_block_root), - }, - body, - }) + + let header = BlockHeader { + parent_hash: self.parent_hash, + ommers_hash: *DEFAULT_OMMERS_HASH, + coinbase: self.fee_recipient, + state_root: self.state_root, + transactions_root: compute_transactions_root(&body.transactions), + receipts_root: self.receipts_root, + logs_bloom: self.logs_bloom, + difficulty: 0.into(), + number: self.block_number, + gas_limit: self.gas_limit, + gas_used: self.gas_used, + timestamp: self.timestamp, + extra_data: self.extra_data, + prev_randao: self.prev_randao, + nonce: 0, + base_fee_per_gas: Some(self.base_fee_per_gas), + withdrawals_root: Some(compute_withdrawals_root( + &body.withdrawals.clone().unwrap_or_default(), + )), + blob_gas_used: Some(self.blob_gas_used), + excess_blob_gas: Some(self.excess_blob_gas), + parent_beacon_block_root: Some(parent_beacon_block_root), + }; + + Ok(Block::new(header, body)) } pub fn from_block(block: Block) -> Self { @@ -134,7 +134,7 @@ impl ExecutionPayloadV3 { timestamp: block.header.timestamp, extra_data: block.header.extra_data.clone(), base_fee_per_gas: block.header.base_fee_per_gas.unwrap_or_default(), - block_hash: block.header.compute_block_hash(), + block_hash: block.hash(), transactions: block .body .transactions diff --git a/crates/storage/store/engines/api.rs b/crates/storage/store/engines/api.rs index cd2303e80..6f6fc2ac6 100644 --- a/crates/storage/store/engines/api.rs +++ b/crates/storage/store/engines/api.rs @@ -175,7 +175,7 @@ pub trait StoreEngine: Debug + Send + Sync + RefUnwindSafe { Some(body) => body, None => return Ok(None), }; - Ok(Some(Block { header, body })) + Ok(Some(Block::new(header, body))) } // Get the canonical block hash for a given block number. diff --git a/crates/storage/store/engines/libmdbx.rs b/crates/storage/store/engines/libmdbx.rs index fd925d345..97281ce17 100644 --- a/crates/storage/store/engines/libmdbx.rs +++ b/crates/storage/store/engines/libmdbx.rs @@ -466,7 +466,7 @@ impl StoreEngine for Store { Some(body) => body, None => return Ok(None), }; - Ok(Some(Block { header, body })) + Ok(Some(Block::new(header, body))) } fn unset_canonical_block(&self, number: BlockNumber) -> Result<(), StoreError> { diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index 36bda16a6..733112697 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -456,7 +456,7 @@ impl Store { let genesis_block = genesis.get_block(); let genesis_block_number = genesis_block.header.number; - let genesis_hash = genesis_block.header.compute_block_hash(); + let genesis_hash = genesis_block.hash(); if let Some(header) = self.get_block_header(genesis_block_number)? { if header.compute_block_hash() == genesis_hash { From 66f1d1b15900070fd70a69bf2eca88ab13beeaa4 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:49:23 -0300 Subject: [PATCH 39/49] fix(l2): `wallet transfer` (#1022) **Motivation** Transfer transactions were being prepared wrongly. The `max_fee_per_gas`, `nonce`, and `chain_id` were picked from the `eth_client` without taking into account the `l1` flag. **Description** Define the client to use at the top of the `wallet transfer` cmd handler to avoid using the wrong one later. --- cmd/ethereum_rust_l2/src/commands/wallet.rs | 31 ++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/cmd/ethereum_rust_l2/src/commands/wallet.rs b/cmd/ethereum_rust_l2/src/commands/wallet.rs index e6b10dfcf..b8ccbfbb4 100644 --- a/cmd/ethereum_rust_l2/src/commands/wallet.rs +++ b/cmd/ethereum_rust_l2/src/commands/wallet.rs @@ -341,30 +341,29 @@ impl Command { todo!("Handle ERC20 transfers") } + let client = if l1 { eth_client } else { rollup_client }; + let mut transfer_transaction = EIP1559Transaction { to: TxKind::Call(to), value: amount, - chain_id: cfg.network.l1_chain_id, - nonce: eth_client.get_nonce(from).await?, - max_fee_per_gas: eth_client.get_gas_price().await?.as_u64(), + chain_id: if l1 { + cfg.network.l1_chain_id + } else { + cfg.network.l2_chain_id + }, + nonce: client.get_nonce(from).await?, + max_fee_per_gas: client.get_gas_price().await?.as_u64() * 100, + gas_limit: 21000 * 100, ..Default::default() }; - // let estimated_gas = eth_client - // .estimate_gas(transfer_transaction.clone()) + // transfer_transaction.gas_limit = client + // .estimate_gas(transfer_transaction.clone().into()) // .await?; - transfer_transaction.gas_limit = 21000 * 5; - - let tx_hash = if l1 { - eth_client - .send_eip1559_transaction(&mut transfer_transaction, cfg.wallet.private_key) - .await? - } else { - rollup_client - .send_eip1559_transaction(&mut transfer_transaction, cfg.wallet.private_key) - .await? - }; + let tx_hash = client + .send_eip1559_transaction(&mut transfer_transaction, cfg.wallet.private_key) + .await?; println!( "[{}] Transfer sent: {tx_hash:#x}", From 7800e5fb42cd29559ebcd9e32e313267762434c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Est=C3=A9fano=20Bargas?= Date: Thu, 31 Oct 2024 14:18:07 +0000 Subject: [PATCH 40/49] feat(l2): integrate ExecutionDB, prove execution (#956) **Motivation** Integrates the `ExecutionDB` defined in the `ethereum_rust-vm` crate with the L2 prover, using it to represent the blockchain state in the zkVM and executing a block. **Description** - prover server creates the DB from a pre-execution at request - the zkVM program takes the DB and block as inputs, and executes the block - generate groth16 proof instead of the default, which is not EVM-friendly and has no recursion. Recursion adds proving time and computing requirements, so we must take it into account. --------- Co-authored-by: fborello-lambda Co-authored-by: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> --- crates/blockchain/blockchain.rs | 5 +- crates/l2/Cargo.toml | 1 + crates/l2/proposer/prover_server.rs | 46 +++++-- crates/l2/prover/src/prover.rs | 16 ++- .../l2/prover/zkvm/interface/guest/Cargo.toml | 11 +- .../prover/zkvm/interface/guest/src/main.rs | 120 ++++-------------- crates/vm/vm.rs | 4 + 7 files changed, 86 insertions(+), 117 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 63732b45d..3b8acef93 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -161,7 +161,10 @@ pub fn is_canonical( } } -fn validate_gas_used(receipts: &[Receipt], block_header: &BlockHeader) -> Result<(), ChainError> { +pub fn validate_gas_used( + receipts: &[Receipt], + block_header: &BlockHeader, +) -> Result<(), ChainError> { if let Some(last) = receipts.last() { if last.cumulative_gas_used != block_header.gas_used { return Err(ChainError::InvalidBlock(InvalidBlockError::GasUsedMismatch)); diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index 6b96d8a03..2957b4ab4 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -18,6 +18,7 @@ ethereum_rust-rlp.workspace = true ethereum_rust-rpc.workspace = true ethereum_rust-blockchain.workspace = true ethereum_rust-storage.workspace = true +ethereum_rust-vm.workspace = true ethereum_rust-dev = { path = "../../crates/blockchain/dev" } hex.workspace = true bytes.workspace = true diff --git a/crates/l2/proposer/prover_server.rs b/crates/l2/proposer/prover_server.rs index 08b79f37b..1c752337b 100644 --- a/crates/l2/proposer/prover_server.rs +++ b/crates/l2/proposer/prover_server.rs @@ -1,5 +1,6 @@ use crate::utils::eth_client::RpcResponse; use ethereum_rust_storage::Store; +use ethereum_rust_vm::execution_db::ExecutionDB; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::{ @@ -14,15 +15,11 @@ use ethereum_rust_core::types::{Block, BlockHeader}; #[derive(Debug, Serialize, Deserialize, Default)] pub struct ProverInputData { - pub db: MemoryDB, - pub parent_block_header: BlockHeader, + pub db: ExecutionDB, pub block: Block, + pub parent_header: BlockHeader, } -// Placeholder structure until we have ExecutionDB on L1 -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct MemoryDB; - use crate::utils::config::prover_server::ProverServerConfig; use super::errors::ProverServerError; @@ -177,22 +174,22 @@ impl ProverServer { ) -> Result<(), String> { debug!("Request received"); - //let last_block_number = Self::get_last_block_number().await?; let last_block_number = self .store .get_latest_block_number() .map_err(|e| e.to_string())? .ok_or("missing latest block number".to_string())?; + let input = self.create_prover_input(last_block_number)?; let response = if last_block_number > last_proved_block { ProofData::Response { block_number: Some(last_block_number), - input: ProverInputData::default(), + input, } } else { ProofData::Response { block_number: None, - input: ProverInputData::default(), + input, } }; let writer = BufWriter::new(stream); @@ -211,4 +208,35 @@ impl ProverServer { let writer = BufWriter::new(stream); serde_json::to_writer(writer, &response).map_err(|e| e.to_string()) } + + fn create_prover_input(&self, block_number: u64) -> Result { + let header = self + .store + .get_block_header(block_number) + .map_err(|err| err.to_string())? + .ok_or("block header not found")?; + let body = self + .store + .get_block_body(block_number) + .map_err(|err| err.to_string())? + .ok_or("block body not found")?; + + let block = Block::new(header, body); + + let db = ExecutionDB::from_exec(&block, &self.store).map_err(|err| err.to_string())?; + + let parent_header = self + .store + .get_block_header_by_hash(block.header.parent_hash) + .map_err(|err| err.to_string())? + .ok_or("missing parent header".to_string())?; + + debug!("Created prover input for block {block_number}"); + + Ok(ProverInputData { + db, + block, + parent_header, + }) + } } diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs index e4f8a2f67..4826755a6 100644 --- a/crates/l2/prover/src/prover.rs +++ b/crates/l2/prover/src/prover.rs @@ -1,9 +1,10 @@ +use ethereum_rust_core::types::Block; use tracing::info; // risc0 use zkvm_interface::methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID}; -use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder}; +use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder, ProverOpts}; use ethereum_rust_rlp::encode::RLPEncode; @@ -34,12 +35,12 @@ impl<'a> Prover<'a> { pub fn set_input(&mut self, input: ProverInputData) -> &mut Self { let head_block_rlp = input.block.encode_to_vec(); - let parent_block_header_rlp = input.parent_block_header.encode_to_vec(); + let parent_header_rlp = input.parent_header.encode_to_vec(); // We should pass the inputs as a whole struct self.env_builder.write(&head_block_rlp).unwrap(); - self.env_builder.write(&parent_block_header_rlp).unwrap(); self.env_builder.write(&input.db).unwrap(); + self.env_builder.write(&parent_header_rlp).unwrap(); self } @@ -59,13 +60,18 @@ impl<'a> Prover<'a> { // Proof information by proving the specified ELF binary. // This struct contains the receipt along with statistics about execution of the guest let prove_info = prover - .prove(env, self.elf) + .prove_with_opts(env, self.elf, &ProverOpts::groth16()) .map_err(|_| "Failed to prove".to_string())?; // extract the receipt. let receipt = prove_info.receipt; - info!("Successfully generated Receipt!"); + let executed_block: Block = receipt.journal.decode().map_err(|err| err.to_string())?; + + info!( + "Successfully generated execution proof receipt for block {}", + executed_block.header.compute_block_hash() + ); Ok(receipt) } diff --git a/crates/l2/prover/zkvm/interface/guest/Cargo.toml b/crates/l2/prover/zkvm/interface/guest/Cargo.toml index a79a9a118..ff0d14608 100644 --- a/crates/l2/prover/zkvm/interface/guest/Cargo.toml +++ b/crates/l2/prover/zkvm/interface/guest/Cargo.toml @@ -13,9 +13,8 @@ ethereum_rust-rlp = { path = "../../../../../common/rlp" } ethereum_rust-vm = { path = "../../../../../vm", default-features = false } ethereum_rust-blockchain = { path = "../../../../../blockchain", default-features = false } -# revm -revm = { version = "14.0.3", features = [ - "std", - "serde", - "kzg-rs", -], default-features = false } +[patch.crates-io] +crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } +k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.6-risczero.0" } +secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", branch = "patch-secp256k1-v0.29.1" } diff --git a/crates/l2/prover/zkvm/interface/guest/src/main.rs b/crates/l2/prover/zkvm/interface/guest/src/main.rs index c3a2a57bd..95eefaf92 100644 --- a/crates/l2/prover/zkvm/interface/guest/src/main.rs +++ b/crates/l2/prover/zkvm/interface/guest/src/main.rs @@ -1,111 +1,39 @@ +use ethereum_rust_rlp::{decode::RLPDecode, error::RLPDecodeError}; use risc0_zkvm::guest::env; -//use ethereum_rust_blockchain::validate_gas_used; -use ethereum_rust_core::types::{Receipt, Transaction}; -// We have to import the ExecutionDB. -use ethereum_rust_vm::{block_env, tx_env}; - -use revm::{ - db::CacheDB, inspectors::TracerEip3155, primitives::ResultAndState as RevmResultAndState, - Evm as Revm, -}; +use ethereum_rust_blockchain::{validate_block, validate_gas_used}; +use ethereum_rust_core::types::{Block, BlockHeader}; +use ethereum_rust_vm::{execute_block, execution_db::ExecutionDB, get_state_transitions, EvmState}; fn main() { - // Read the input - let head_block_bytes = env::read::>(); - let parent_header_bytes = env::read::>(); - //let execution_db = env::read::(); + let (block, execution_db, parent_header) = read_inputs().expect("failed to read inputs"); + let mut state = EvmState::from_exec_db(execution_db.clone()); - // SetUp data from inputs - let block = ::decode( - &head_block_bytes, - ) - .unwrap(); + // Validate the block pre-execution + validate_block(&block, &parent_header, &state).expect("invalid block"); - let parent_header = - ::decode( - &parent_header_bytes, - ) - .unwrap(); + let receipts = execute_block(&block, &mut state).unwrap(); - // Make DataInputs public. - env::commit(&block); - env::commit(&parent_header); - //env::commit(&execution_db); - - // SetUp CacheDB in order to use execute_block() - //let mut cache_db = CacheDB::new(execution_db); - println!("executing block"); + validate_gas_used(&receipts, &block.header).expect("invalid gas used"); - //let block_receipts = execute_block(&block, &mut cache_db).unwrap(); - // TODO - // Handle the case in which the gas used differs and throws an error. - // Should the zkVM panic? Should it generate a dummy proof? - // Private function - //let _ = validate_gas_used(&block_receipts, &block.header); + let _account_updates = get_state_transitions(&mut state); - //env::commit(&block_receipts); + // TODO: compute new state root from account updates and check it matches with the block's + // header one. } -// Modified from ethereum_rust-vm -/* -fn execute_block( - block: ðereum_rust_core::types::Block, - db: &mut CacheDB, -) -> Result, ethereum_rust_vm::EvmError> { - let spec_id = revm::primitives::SpecId::CANCUN; - let mut receipts = Vec::new(); - let mut cumulative_gas_used = 0; - - for transaction in block.body.transactions.iter() { - let result = execute_tx(transaction, &block.header, db, spec_id)?; - cumulative_gas_used += result.gas_used(); - let receipt = Receipt::new( - transaction.tx_type(), - result.is_success(), - cumulative_gas_used, - result.logs(), - ); - receipts.push(receipt); - } +fn read_inputs() -> Result<(Block, ExecutionDB, BlockHeader), RLPDecodeError> { + let head_block_bytes = env::read::>(); + let execution_db = env::read::(); + let parent_header_bytes = env::read::>(); - Ok(receipts) -} + let block = Block::decode(&head_block_bytes)?; + let parent_header = BlockHeader::decode(&parent_header_bytes)?; -// Modified from ethereum_rust-vm -fn execute_tx( - transaction: &Transaction, - block_header: ðereum_rust_core::types::BlockHeader, - db: &mut CacheDB, - spec_id: revm::primitives::SpecId, -) -> Result { - let block_env = block_env(block_header); - let tx_env = tx_env(transaction); - run_evm(tx_env, block_env, db, spec_id) - .map(Into::into) - .map_err(ethereum_rust_vm::EvmError::from) -} + // make inputs public + env::commit(&block); + env::commit(&execution_db); + env::commit(&parent_header); -// Modified from ethereum_rust-vm -fn run_evm( - tx_env: revm::primitives::TxEnv, - block_env: revm::primitives::BlockEnv, - db: &mut CacheDB, - spec_id: revm::primitives::SpecId, -) -> Result { - // let chain_spec = db.get_chain_config()?; - let mut evm = Revm::builder() - .with_db(db) - .with_block_env(block_env) - .with_tx_env(tx_env) - // If the chain_id is not correct, it throws: - // Transaction(InvalidChainId) - // TODO: do not hardcode the chain_id - .modify_cfg_env(|cfg| cfg.chain_id = 1729) - .with_spec_id(spec_id) - .with_external_context(TracerEip3155::new(Box::new(std::io::stderr())).without_summary()) - .build(); - let RevmResultAndState { result, state: _ } = evm.transact().unwrap(); - Ok(result.into()) + Ok((block, execution_db, parent_header)) } -*/ diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 99374a5a1..d691d6920 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -53,6 +53,10 @@ pub enum EvmState { } impl EvmState { + pub fn from_exec_db(db: ExecutionDB) -> Self { + EvmState::Execution(revm::db::CacheDB::new(db)) + } + /// Get a reference to inner `Store` database pub fn database(&self) -> Option<&Store> { if let EvmState::Store(db) = self { From 63e3efa2f221ab659feace3db6f82e32dba43e18 Mon Sep 17 00:00:00 2001 From: Akash S M Date: Thu, 31 Oct 2024 20:07:44 +0530 Subject: [PATCH 41/49] feat(l1): add Deserialize trait for EIP4844Transaction (#981) **Motivation** Add the custom Deserialize trait for EIP4844Transaction. Closes #882 --------- Co-authored-by: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> --- crates/common/types/transaction.rs | 166 ++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 4 deletions(-) diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index d14c39599..ce05e54db 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -1403,13 +1403,112 @@ mod serde_impl { } impl<'de> Deserialize<'de> for EIP4844Transaction { - fn deserialize(_deserializer: D) -> Result + fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - Err(serde::de::Error::custom( - "EIP4844Transaction deserialization unimplemented", - )) + let mut map = >::deserialize(deserializer)?; + let chain_id = serde_json::from_value::( + map.remove("chainId") + .ok_or_else(|| serde::de::Error::missing_field("chainId"))?, + ) + .map_err(serde::de::Error::custom)? + .as_u64(); + let nonce = serde_json::from_value::( + map.remove("nonce") + .ok_or_else(|| serde::de::Error::missing_field("nonce"))?, + ) + .map_err(serde::de::Error::custom)? + .as_u64(); + let max_priority_fee_per_gas = serde_json::from_value::( + map.remove("maxPriorityFeePerGas") + .ok_or_else(|| serde::de::Error::missing_field("maxPriorityFeePerGas"))?, + ) + .map_err(serde::de::Error::custom)? + .as_u64(); + let max_fee_per_gas = serde_json::from_value::( + map.remove("maxFeePerGas") + .ok_or_else(|| serde::de::Error::missing_field("maxFeePerGas"))?, + ) + .map_err(serde::de::Error::custom)? + .as_u64(); + let gas = serde_json::from_value::( + map.remove("gas") + .ok_or_else(|| serde::de::Error::missing_field("gas"))?, + ) + .map_err(serde::de::Error::custom)? + .as_u64(); + let to = serde_json::from_value( + map.remove("to") + .ok_or_else(|| serde::de::Error::missing_field("to"))?, + ) + .map_err(serde::de::Error::custom)?; + let value = serde_json::from_value( + map.remove("value") + .ok_or_else(|| serde::de::Error::missing_field("value"))?, + ) + .map_err(serde::de::Error::custom)?; + let data = serde_json::from_value( + map.remove("input") + .ok_or_else(|| serde::de::Error::missing_field("input"))?, + ) + .map_err(serde::de::Error::custom)?; + let access_list = serde_json::from_value::>( + map.remove("accessList") + .ok_or_else(|| serde::de::Error::missing_field("accessList"))?, + ) + .map_err(serde::de::Error::custom)? + .into_iter() + .map(|v| (v.address, v.storage_keys)) + .collect::>(); + let max_fee_per_blob_gas = serde_json::from_value::( + map.remove("maxFeePerBlobGas") + .ok_or_else(|| serde::de::Error::missing_field("maxFeePerBlobGas"))?, + ) + .map_err(serde::de::Error::custom)?; + let blob_versioned_hashes = serde_json::from_value( + map.remove("blobVersionedHashes") + .ok_or_else(|| serde::de::Error::missing_field("blobVersionedHashes"))?, + ) + .map_err(serde::de::Error::custom)?; + let signature_y_parity = u8::from_str_radix( + serde_json::from_value::( + map.remove("yParity") + .ok_or_else(|| serde::de::Error::missing_field("yParity"))?, + ) + .map_err(serde::de::Error::custom)? + .trim_start_matches("0x"), + 16, + ) + .map_err(serde::de::Error::custom)? + != 0; + let signature_r = serde_json::from_value( + map.remove("r") + .ok_or_else(|| serde::de::Error::missing_field("r"))?, + ) + .map_err(serde::de::Error::custom)?; + let signature_s = serde_json::from_value( + map.remove("s") + .ok_or_else(|| serde::de::Error::missing_field("s"))?, + ) + .map_err(serde::de::Error::custom)?; + + Ok(EIP4844Transaction { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas, + to, + value, + data, + access_list, + max_fee_per_blob_gas, + blob_versioned_hashes, + signature_y_parity, + signature_r, + signature_s, + }) } } @@ -1785,4 +1884,63 @@ mod tests { serde_json::from_str(generic_transaction).unwrap() ) } + + #[test] + fn deserialize_eip4844_transaction() { + let eip4844_transaction = r#"{ + "chainId":"0x01", + "nonce":"0x02", + "maxPriorityFeePerGas":"0x01", + "maxFeePerGas":"0x01", + "gas":"0x5208", + "to":"0x6177843db3138ae69679A54b95cf345ED759450d", + "value":"0x01", + "input":"0x", + "accessList": [ + { + "address": "0x000f3df6d732807ef1319fb7b8bb8522d0beac02", + "storageKeys": [ + "0x000000000000000000000000000000000000000000000000000000000000000c", + "0x000000000000000000000000000000000000000000000000000000000000200b" + ] + } + ], + "maxFeePerBlobGas":"0x03", + "blobVersionedHashes": [ + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002" + ], + "yParity":"0x0", + "r": "0x01", + "s": "0x02" + }"#; + let deserialized_eip4844_transaction = EIP4844Transaction { + chain_id: 0x01, + nonce: 0x02, + to: Address::from_slice( + &hex::decode("6177843db3138ae69679A54b95cf345ED759450d").unwrap(), + ), + max_priority_fee_per_gas: 1, + max_fee_per_gas: 1, + max_fee_per_blob_gas: U256::from(0x03), + gas: 0x5208, + value: U256::from(0x01), + data: Bytes::from_static(b"0x"), + access_list: vec![( + Address::from_slice( + &hex::decode("000f3df6d732807ef1319fb7b8bb8522d0beac02").unwrap(), + ), + vec![H256::from_low_u64_be(12), H256::from_low_u64_be(8203)], + )], + blob_versioned_hashes: vec![H256::from_low_u64_be(1), H256::from_low_u64_be(2)], + signature_y_parity: false, + signature_r: U256::from(0x01), + signature_s: U256::from(0x02), + }; + + assert_eq!( + deserialized_eip4844_transaction, + serde_json::from_str(eip4844_transaction).unwrap() + ) + } } From 27159b9ece306752c1e58c37e412d9c81180b57c Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:54:25 -0300 Subject: [PATCH 42/49] fix(l1): add temporary fix to transaction filtering & handle tx filtering unwrap edge case (#1018) **Motivation** To avoid panicking when the transaction filter fails to filter a transaction because we do not yet filter transactions with invalid tip. **Description** - Filter transactions whose effective gas tip calculation underflowed. - Handle the unwrap of the `effective_gas_tip` calculation. Even though this is an edge case that shouldn't happen, it is better to have a descriptive error if it happens someday than to panic. --------- Co-authored-by: fmoletta <99273364+fmoletta@users.noreply.github.com> --- crates/blockchain/error.rs | 2 ++ crates/blockchain/mempool.rs | 7 +++++++ crates/blockchain/payload.rs | 34 +++++++++++++++++++++++----------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs index 230b0fff3..e4632ca6f 100644 --- a/crates/blockchain/error.rs +++ b/crates/blockchain/error.rs @@ -32,6 +32,8 @@ pub enum InvalidBlockError { GasUsedMismatch, #[error("Blob gas used doesn't match value in header")] BlobGasUsedMismatch, + #[error("Invalid transaction: {0}")] + InvalidTransaction(String), } #[derive(Debug, thiserror::Error)] diff --git a/crates/blockchain/mempool.rs b/crates/blockchain/mempool.rs index f32ed10bc..3b7fe0a0e 100644 --- a/crates/blockchain/mempool.rs +++ b/crates/blockchain/mempool.rs @@ -79,6 +79,7 @@ pub fn filter_transactions( if filter.only_plain_txs && is_blob_tx || filter.only_blob_txs && !is_blob_tx { return false; } + // Filter by tip & base_fee if let Some(min_tip) = filter.min_tip { if !tx @@ -87,7 +88,13 @@ pub fn filter_transactions( { return false; } + // This is a temporary fix to avoid invalid transactions to be included. + // This should be removed once https://github.com/lambdaclass/ethereum_rust/issues/680 + // is addressed. + } else if tx.effective_gas_tip(filter.base_fee).is_none() { + return false; } + // Filter by blob gas fee if let (true, Some(blob_fee)) = (is_blob_tx, filter.blob_fee) { if !tx.max_fee_per_blob_gas().is_some_and(|fee| fee >= blob_fee) { diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index 885cac812..e2fd6a624 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -25,7 +25,7 @@ use crate::{ GAS_LIMIT_BOUND_DIVISOR, GAS_PER_BLOB, MAX_BLOB_GAS_PER_BLOCK, MIN_GAS_LIMIT, TARGET_BLOB_GAS_PER_BLOCK, TX_GAS_COST, }, - error::ChainError, + error::{ChainError, InvalidBlockError}, mempool::{self, PendingTxFilter}, }; @@ -222,7 +222,7 @@ pub fn apply_withdrawals(context: &mut PayloadBuildContext) -> Result<(), EvmErr /// Returns two transaction queues, one for plain and one for blob txs fn fetch_mempool_transactions( context: &mut PayloadBuildContext, -) -> Result<(TransactionQueue, TransactionQueue), StoreError> { +) -> Result<(TransactionQueue, TransactionQueue), ChainError> { let tx_filter = PendingTxFilter { /*TODO(https://github.com/lambdaclass/ethereum_rust/issues/680): add tip filter */ base_fee: context.base_fee_per_gas(), @@ -245,12 +245,12 @@ fn fetch_mempool_transactions( TransactionQueue::new( mempool::filter_transactions(&plain_tx_filter, store)?, context.base_fee_per_gas(), - ), + )?, // Blob txs TransactionQueue::new( mempool::filter_transactions(&blob_tx_filter, store)?, context.base_fee_per_gas(), - ), + )?, )) } @@ -320,7 +320,7 @@ pub fn fill_transactions(context: &mut PayloadBuildContext) -> Result<(), ChainE // Execute tx let receipt = match apply_transaction(&head_tx, context) { Ok(receipt) => { - txs.shift(); + txs.shift()?; // Pull transaction from the mempool mempool::remove_transaction( tx_hash, @@ -464,7 +464,10 @@ impl From for Transaction { impl TransactionQueue { /// Creates a new TransactionQueue from a set of transactions grouped by sender and sorted by nonce - fn new(mut txs: HashMap>, base_fee: Option) -> Self { + fn new( + mut txs: HashMap>, + base_fee: Option, + ) -> Result { let mut heads = Vec::new(); for (address, txs) in txs.iter_mut() { // Pull the first tx from each list and add it to the heads list @@ -472,18 +475,22 @@ impl TransactionQueue { let head_tx = txs.remove(0); heads.push(HeadTransaction { // We already ran this method when filtering the transactions from the mempool so it shouldn't fail - tip: head_tx.effective_gas_tip(base_fee).unwrap(), + tip: head_tx + .effective_gas_tip(base_fee) + .ok_or(ChainError::InvalidBlock( + InvalidBlockError::InvalidTransaction("Attempted to add an invalid transaction to the block. The transaction filter must have failed.".to_owned()), + ))?, tx: head_tx, sender: *address, }); } // Sort heads by higest tip (and lowest timestamp if tip is equal) heads.sort(); - TransactionQueue { + Ok(TransactionQueue { heads, txs, base_fee, - } + }) } /// Remove all transactions from the queue @@ -513,7 +520,7 @@ impl TransactionQueue { /// Remove the top transaction /// Add a tx from the same sender to the head transactions - fn shift(&mut self) { + fn shift(&mut self) -> Result<(), ChainError> { let tx = self.heads.remove(0); if let Some(txs) = self.txs.get_mut(&tx.sender) { // Fetch next head @@ -521,7 +528,11 @@ impl TransactionQueue { let head_tx = txs.remove(0); let head = HeadTransaction { // We already ran this method when filtering the transactions from the mempool so it shouldn't fail - tip: head_tx.effective_gas_tip(self.base_fee).unwrap(), + tip: head_tx.effective_gas_tip(self.base_fee).ok_or( + ChainError::InvalidBlock( + InvalidBlockError::InvalidTransaction("Attempted to add an invalid transaction to the block. The transaction filter must have failed.".to_owned()), + ), + )?, tx: head_tx, sender: tx.sender, }; @@ -533,6 +544,7 @@ impl TransactionQueue { self.heads.insert(index, head); } } + Ok(()) } } From acad2ee0dfcb5c5b387e62831d46f2e846759279 Mon Sep 17 00:00:00 2001 From: Ivan Litteri <67517699+ilitteri@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:16:59 -0300 Subject: [PATCH 43/49] refactor(l2): add `-w` flag to cmds that send txs (#1036) **Motivation** Nowadays, all the commands that send transactions do not wait for transaction receipts. If you run the same command multiple times the same transaction with the same nonce is going to be sent to the node; another problem is that the users have to perform additional steps to make sure that their transaction was finalized or not. As this is not an implementation problem but a misuse of the CLI, it'd be good for users to also have the option to wait for their transactions to be finalized. **Description** Adds a `-w` flag to the cmds `deploy`, `transfer`, `send`, `deposit`, `withdraw`, and `claim-withdraw` which when set, waits for the transaction sent to be finalized (a.k.a. wait for its receipt). --- cmd/ethereum_rust_l2/src/commands/wallet.rs | 58 ++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/cmd/ethereum_rust_l2/src/commands/wallet.rs b/cmd/ethereum_rust_l2/src/commands/wallet.rs index b8ccbfbb4..8f4c57020 100644 --- a/cmd/ethereum_rust_l2/src/commands/wallet.rs +++ b/cmd/ethereum_rust_l2/src/commands/wallet.rs @@ -42,11 +42,17 @@ pub(crate) enum Command { help = "Specify the wallet in which you want to deposit your funds." )] to: Option
, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, #[clap(long, short = 'e', required = false)] explorer_url: bool, }, #[clap(about = "Finalize a pending withdrawal.")] - ClaimWithdraw { l2_withdrawal_tx_hash: H256 }, + ClaimWithdraw { + l2_withdrawal_tx_hash: H256, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, + }, #[clap(about = "Transfer funds to another wallet.")] Transfer { // TODO: Parse ether instead. @@ -56,6 +62,10 @@ pub(crate) enum Command { token_address: Option
, #[clap(long = "to")] to: Address, + #[clap(long = "nonce")] + nonce: Option, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, #[clap( long = "l1", required = false, @@ -79,6 +89,8 @@ pub(crate) enum Command { help = "Specify the token address, the base token is used as default." )] token_address: Option
, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, #[clap(long, short = 'e', required = false)] explorer_url: bool, }, @@ -121,6 +133,8 @@ pub(crate) enum Command { gas_price: Option, #[clap(long = "priority-gas-price", required = false)] priority_gas_price: Option, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, }, #[clap(about = "Make a call to a contract")] Call { @@ -177,6 +191,8 @@ pub(crate) enum Command { gas_price: Option, #[clap(long = "priority-gas-price", required = false)] priority_gas_price: Option, + #[clap(short = 'w', required = false)] + wait_for_receipt: bool, }, } @@ -258,6 +274,7 @@ impl Command { amount, token_address, to, + wait_for_receipt, explorer_url: _, } => { if to.is_some() { @@ -275,7 +292,9 @@ impl Command { amount, token_address: None, to: cfg.contracts.common_bridge, + wait_for_receipt, l1: true, + nonce: None, explorer_url: false, } .run(cfg) @@ -285,6 +304,7 @@ impl Command { } Command::ClaimWithdraw { l2_withdrawal_tx_hash, + wait_for_receipt, } => { let (withdrawal_l2_block_number, claimed_amount) = match rollup_client .get_transaction_by_hash(l2_withdrawal_tx_hash) @@ -329,11 +349,17 @@ impl Command { .await?; println!("Withdrawal claim sent: {tx_hash:#x}"); + + if wait_for_receipt { + wait_for_transaction_receipt(ð_client, tx_hash).await?; + } } Command::Transfer { amount, token_address, to, + nonce, + wait_for_receipt, l1, explorer_url: _, } => { @@ -351,7 +377,7 @@ impl Command { } else { cfg.network.l2_chain_id }, - nonce: client.get_nonce(from).await?, + nonce: nonce.unwrap_or(client.get_nonce(from).await?), max_fee_per_gas: client.get_gas_price().await?.as_u64() * 100, gas_limit: 21000 * 100, ..Default::default() @@ -369,12 +395,17 @@ impl Command { "[{}] Transfer sent: {tx_hash:#x}", if l1 { "L1" } else { "L2" } ); + + if wait_for_receipt { + wait_for_transaction_receipt(&client, tx_hash).await?; + } } Command::Withdraw { amount, to, nonce, token_address: _, + wait_for_receipt, explorer_url: _, } => { let withdraw_transaction = PrivilegedL2Transaction { @@ -393,6 +424,10 @@ impl Command { .await?; println!("Withdrawal sent: {tx_hash:#x}"); + + if wait_for_receipt { + wait_for_transaction_receipt(&rollup_client, tx_hash).await?; + } } Command::WithdrawalProof { tx_hash } => { let (_index, path) = get_withdraw_merkle_proof(&rollup_client, tx_hash).await?; @@ -414,6 +449,7 @@ impl Command { gas_limit, gas_price, priority_gas_price, + wait_for_receipt, } => { let client = match l1 { true => eth_client, @@ -442,6 +478,10 @@ impl Command { "[{}] Transaction sent: {tx_hash:#x}", if l1 { "L1" } else { "L2" } ); + + if wait_for_receipt { + wait_for_transaction_receipt(&client, tx_hash).await?; + } } Command::Call { to, @@ -482,6 +522,7 @@ impl Command { gas_limit, gas_price, priority_gas_price, + wait_for_receipt, } => { let client = match l1 { true => eth_client, @@ -507,8 +548,21 @@ impl Command { println!("Contract deployed in tx: {deployment_tx_hash:#x}"); println!("Contract address: {deployed_contract_address:#x}"); + + if wait_for_receipt { + wait_for_transaction_receipt(&client, deployment_tx_hash).await?; + } } }; Ok(()) } } + +pub async fn wait_for_transaction_receipt(client: &EthClient, tx_hash: H256) -> eyre::Result<()> { + println!("Waiting for transaction receipt..."); + while client.get_transaction_receipt(tx_hash).await?.is_none() { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + println!("Transaction confirmed"); + Ok(()) +} From acd036579d90de89632e3df2f5ce0870615fee77 Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 31 Oct 2024 14:41:22 -0300 Subject: [PATCH 44/49] fix(l1): made the tx-spammer work with our node (#1027) **Motivation** Have the tx spammer running instead of immediatly shutting down when we are the first node in the devnet setup. **Description** This PR tackled a couple of issues until we were able to process an initial transaction, (they are small changes): - We were returning an error on `eth_getTransactionCount` when we either didn't have a `latest` block before genesis or at any point when we don't have `pending` blocks. The solution in this case was returning `0x0` in those cases. - When requesting `eth_getTransactionCount` on `pending` after some transaction went through we were defaulting to `0x0`, it appears that the idea is to default to the `latest` in those case which make sense. - There were a missing filter that made the node panic when building payloads for transactions with fees lower than the base_fee_per_gas, this generated that those kind of transactions stopped the node's ability to build blocks when they were in the mempool, we made a quick workaround in this PR, but will remove it in favor of #1018 Closes #1026 --- crates/networking/rpc/eth/account.rs | 5 ++--- crates/networking/rpc/types/block_identifier.rs | 10 +++++++++- test_data/el-stability-check.yml | 2 +- test_data/network_params.yaml | 4 +++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/networking/rpc/eth/account.rs b/crates/networking/rpc/eth/account.rs index 283d83b3f..953999daa 100644 --- a/crates/networking/rpc/eth/account.rs +++ b/crates/networking/rpc/eth/account.rs @@ -156,9 +156,8 @@ impl RpcHandler for GetTransactionCountRequest { ); let Some(block_number) = self.block.resolve_block_number(&storage)? else { - return Err(RpcErr::Internal( - "Could not resolve block number".to_owned(), - )); // Should we return Null here? + return serde_json::to_value("0x0") + .map_err(|error| RpcErr::Internal(error.to_string())); }; let nonce = storage diff --git a/crates/networking/rpc/types/block_identifier.rs b/crates/networking/rpc/types/block_identifier.rs index a995b35d2..5b596ccad 100644 --- a/crates/networking/rpc/types/block_identifier.rs +++ b/crates/networking/rpc/types/block_identifier.rs @@ -39,7 +39,15 @@ impl BlockIdentifier { BlockTag::Finalized => storage.get_finalized_block_number(), BlockTag::Safe => storage.get_safe_block_number(), BlockTag::Latest => storage.get_latest_block_number(), - BlockTag::Pending => storage.get_pending_block_number(), + BlockTag::Pending => { + storage + .get_pending_block_number() + // If there are no pending blocks, we return the latest block number + .and_then(|pending_block_number| match pending_block_number { + Some(block_number) => Ok(Some(block_number)), + None => storage.get_latest_block_number(), + }) + } }, } } diff --git a/test_data/el-stability-check.yml b/test_data/el-stability-check.yml index 6b4a38a64..d28765a06 100644 --- a/test_data/el-stability-check.yml +++ b/test_data/el-stability-check.yml @@ -24,7 +24,7 @@ tasks: - name: run_task_matrix title: "Check block proposals from all client pairs" - timeout: 2m + timeout: 3m configVars: matrixValues: "validatorPairNames" config: diff --git a/test_data/network_params.yaml b/test_data/network_params.yaml index cbb4ac117..d2c4fc7ec 100644 --- a/test_data/network_params.yaml +++ b/test_data/network_params.yaml @@ -20,4 +20,6 @@ assertoor_params: run_block_proposal_check: false run_blob_transaction_test: true tests: - - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_rust/refs/heads/main/test_data/el-stability-check.yml' \ No newline at end of file + - 'https://raw.githubusercontent.com/lambdaclass/lambda_ethereum_rust/refs/heads/main/test_data/el-stability-check.yml' +tx_spammer_params: + tx_spammer_extra_args: ["--accounts=10", --txcount=10] From 9c85075682042fae3aa7beae4c59a1c8f1270c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerem=C3=ADas=20Salom=C3=B3n?= <48994069+JereSalo@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:50:53 -0300 Subject: [PATCH 45/49] feat(levm): implement `Cache` and refactor `Db` (#991) **Motivation** We currently have an in-memory database that should soon be replaced by the node's in-disk database. For that purpose we want to allow us to switch between both kinds of databases. The need to implement this PR's features and refactor the `Db` arose while working on #904. **Description** This PR includes: - Adding a `Cache` to store warm accounts. This removes the need of having `accessed_accounts` and `accessed_storage_slots` sets in `Substate` because we know that if they are cached then they are warm. - Making our `Db` implement the `Database` trait and interact with it only using methods and not it's attributes, so in the future we can implement that trait for the actual node's database. - Fix call opcodes and remove delegate attribute from `CallFrame`. Part of #814. --------- Co-authored-by: Juani Medone Co-authored-by: maximopalopoli Co-authored-by: Javier Chatruc --- crates/vm/levm/Cargo.toml | 4 - crates/vm/levm/docs/substate.md | 3 + crates/vm/levm/src/call_frame.rs | 9 +- crates/vm/levm/src/constants.rs | 1 - crates/vm/levm/src/db.rs | 126 ++ crates/vm/levm/src/lib.rs | 1 + crates/vm/levm/src/opcode_handlers/block.rs | 16 +- .../levm/src/opcode_handlers/environment.rs | 93 +- .../stack_memory_storage_flow.rs | 54 +- crates/vm/levm/src/opcode_handlers/system.rs | 43 +- crates/vm/levm/src/utils.rs | 89 +- crates/vm/levm/src/vm.rs | 667 +++++----- crates/vm/levm/tests/tests.rs | 1131 +++++++++-------- 13 files changed, 1239 insertions(+), 998 deletions(-) create mode 100644 crates/vm/levm/docs/substate.md create mode 100644 crates/vm/levm/src/db.rs diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index d41d028e4..cd09d7d50 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -19,7 +19,3 @@ hex = "0.4.3" [features] ethereum_foundation_tests = [] - -[profile.test] -opt-level = 3 -debug-assertions = true diff --git a/crates/vm/levm/docs/substate.md b/crates/vm/levm/docs/substate.md new file mode 100644 index 000000000..806511ed2 --- /dev/null +++ b/crates/vm/levm/docs/substate.md @@ -0,0 +1,3 @@ +## Substate + +`accessed_addresses` and `accessed_storage_keys` follow the structure defined in [EIP 2929](https://eips.ethereum.org/EIPS/eip-2929#specification) diff --git a/crates/vm/levm/src/call_frame.rs b/crates/vm/levm/src/call_frame.rs index 80e308229..2d94b63b1 100644 --- a/crates/vm/levm/src/call_frame.rs +++ b/crates/vm/levm/src/call_frame.rs @@ -62,10 +62,13 @@ pub struct CallFrame { pub gas_limit: U256, pub gas_used: U256, pub pc: usize, - pub msg_sender: Address, // Origin address? + /// Address of the account that sent the message + pub msg_sender: Address, + /// Address of the recipient of the message pub to: Address, + /// Address of the code to execute. Usually the same as `to`, but can be different pub code_address: Address, - pub delegate: Option
, + /// Bytecode to execute pub bytecode: Bytes, pub msg_value: U256, pub stack: Stack, // max 1024 in the future @@ -98,7 +101,6 @@ impl CallFrame { msg_sender: Address, to: Address, code_address: Address, - delegate: Option
, bytecode: Bytes, msg_value: U256, calldata: Bytes, @@ -112,7 +114,6 @@ impl CallFrame { msg_sender, to, code_address, - delegate, bytecode, msg_value, calldata, diff --git a/crates/vm/levm/src/constants.rs b/crates/vm/levm/src/constants.rs index c38f3f429..c9733334a 100644 --- a/crates/vm/levm/src/constants.rs +++ b/crates/vm/levm/src/constants.rs @@ -47,7 +47,6 @@ pub mod gas_cost { pub const RETURNDATACOPY_STATIC: U256 = U256([3, 0, 0, 0]); pub const RETURNDATACOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]); pub const ADDRESS: U256 = U256([2, 0, 0, 0]); - pub const BALANCE: U256 = U256([100, 0, 0, 0]); pub const ORIGIN: U256 = U256([2, 0, 0, 0]); pub const CALLER: U256 = U256([2, 0, 0, 0]); pub const BLOCKHASH: U256 = U256([20, 0, 0, 0]); diff --git a/crates/vm/levm/src/db.rs b/crates/vm/levm/src/db.rs new file mode 100644 index 000000000..ec0f14e5f --- /dev/null +++ b/crates/vm/levm/src/db.rs @@ -0,0 +1,126 @@ +use crate::vm::{Account, AccountInfo, StorageSlot}; +use ethereum_types::{Address, U256}; +use keccak_hash::H256; +use std::collections::HashMap; + +pub trait Database { + fn get_account_info(&self, address: Address) -> AccountInfo; + fn get_storage_slot(&self, address: Address, key: H256) -> U256; + fn get_block_hash(&self, block_number: u64) -> Option; +} + +#[derive(Debug, Default)] +pub struct Db { + pub accounts: HashMap, + pub block_hashes: HashMap, +} + +// Methods here are for testing purposes only, for initializing the Db with some values +impl Db { + pub fn new() -> Self { + Self { + accounts: HashMap::new(), + block_hashes: HashMap::new(), + } + } + + /// Add accounts to database + pub fn add_accounts(&mut self, accounts: Vec<(Address, Account)>) { + self.accounts.extend(accounts); + } + + /// Add block hashes to database + pub fn add_block_hashes(&mut self, block_hashes: Vec<(u64, H256)>) { + self.block_hashes.extend(block_hashes); + } + + /// Builder method with accounts [for testing only] + pub fn with_accounts(mut self, accounts: HashMap) -> Self { + self.accounts = accounts; + self + } + + /// Builder method with block hashes [for testing only] + pub fn with_block_hashes(mut self, block_hashes: HashMap) -> Self { + self.block_hashes = block_hashes; + self + } +} + +impl Database for Db { + fn get_account_info(&self, address: Address) -> AccountInfo { + self.accounts + .get(&address) + .unwrap_or(&Account::default()) + .info + .clone() + } + + fn get_storage_slot(&self, address: Address, key: H256) -> U256 { + // both `original_value` and `current_value` should work here because they have the same values on Db + self.accounts + .get(&address) + .unwrap_or(&Account::default()) + .storage + .get(&key) + .unwrap_or(&StorageSlot::default()) + .original_value + } + + fn get_block_hash(&self, block_number: u64) -> Option { + self.block_hashes.get(&block_number).cloned() + } +} + +#[derive(Debug, Default, Clone)] +pub struct Cache { + pub accounts: HashMap, +} + +impl Cache { + pub fn get_account(&self, address: Address) -> Option<&Account> { + self.accounts.get(&address) + } + + pub fn get_mut_account(&mut self, address: Address) -> Option<&mut Account> { + self.accounts.get_mut(&address) + } + + pub fn get_storage_slot(&self, address: Address, key: H256) -> Option { + self.get_account(address) + .expect("Account should have been cached") + .storage + .get(&key) + .cloned() + } + + pub fn add_account(&mut self, address: &Address, account: &Account) { + self.accounts.insert(*address, account.clone()); + } + + pub fn write_account_storage(&mut self, address: &Address, key: H256, slot: StorageSlot) { + self.accounts + .get_mut(address) + .expect("Account should have been cached") + .storage + .insert(key, slot); + } + + pub fn increment_account_nonce(&mut self, address: &Address) { + if let Some(account) = self.accounts.get_mut(address) { + account.info.nonce += 1; + } + } + + pub fn is_account_cached(&self, address: &Address) -> bool { + self.accounts.contains_key(address) + } + + pub fn is_slot_cached(&self, address: &Address, key: H256) -> bool { + self.is_account_cached(address) + && self + .get_account(*address) + .map(|account| account.storage.contains_key(&key)) + .unwrap_or(false) + } +} diff --git a/crates/vm/levm/src/lib.rs b/crates/vm/levm/src/lib.rs index c9d38842b..d07a5dc33 100644 --- a/crates/vm/levm/src/lib.rs +++ b/crates/vm/levm/src/lib.rs @@ -1,6 +1,7 @@ pub mod block; pub mod call_frame; pub mod constants; +pub mod db; pub mod errors; pub mod memory; pub mod opcode_handlers; diff --git a/crates/vm/levm/src/opcode_handlers/block.rs b/crates/vm/levm/src/opcode_handlers/block.rs index 739769108..019cc19e5 100644 --- a/crates/vm/levm/src/opcode_handlers/block.rs +++ b/crates/vm/levm/src/opcode_handlers/block.rs @@ -30,10 +30,12 @@ impl VM { return Ok(OpcodeSuccess::Continue); } - if let Some(block_hash) = self.db.block_hashes.get(&block_number) { + let block_number = block_number.as_u64(); + + if let Some(block_hash) = self.db.get_block_hash(block_number) { current_call_frame .stack - .push(U256::from_big_endian(&block_hash.0))?; + .push(U256::from_big_endian(block_hash.as_bytes()))?; } else { current_call_frame.stack.push(U256::zero())?; } @@ -125,9 +127,15 @@ impl VM { ) -> Result { self.increase_consumed_gas(current_call_frame, gas_cost::SELFBALANCE)?; - let balance = self.db.balance(¤t_call_frame.code_address); - current_call_frame.stack.push(balance)?; + // the current account should have been cached when the contract was called + let balance = self + .cache + .get_account(current_call_frame.code_address) + .expect("The current account should always be cached") + .info + .balance; + current_call_frame.stack.push(balance)?; Ok(OpcodeSuccess::Continue) } diff --git a/crates/vm/levm/src/opcode_handlers/environment.rs b/crates/vm/levm/src/opcode_handlers/environment.rs index 6278a1702..d017ae10f 100644 --- a/crates/vm/levm/src/opcode_handlers/environment.rs +++ b/crates/vm/levm/src/opcode_handlers/environment.rs @@ -1,6 +1,9 @@ use super::*; use crate::{ - constants::{call_opcode, WORD_SIZE}, + constants::{ + call_opcode::{COLD_ADDRESS_ACCESS_COST, WARM_ADDRESS_ACCESS_COST}, + WORD_SIZE, + }, vm::word_to_address, }; use sha3::{Digest, Keccak256}; @@ -16,11 +19,7 @@ impl VM { ) -> Result { self.increase_consumed_gas(current_call_frame, gas_cost::ADDRESS)?; - let addr = if current_call_frame.delegate.is_some() { - current_call_frame.msg_sender - } else { - current_call_frame.code_address - }; + let addr = current_call_frame.to; // The recipient of the current call. current_call_frame.stack.push(U256::from(addr.as_bytes()))?; @@ -32,13 +31,18 @@ impl VM { &mut self, current_call_frame: &mut CallFrame, ) -> Result { - self.increase_consumed_gas(current_call_frame, gas_cost::BALANCE)?; + let address = &word_to_address(current_call_frame.stack.pop()?); - let addr = current_call_frame.stack.pop()?; + if self.cache.is_account_cached(address) { + self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?; + } else { + self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?; + self.cache_from_db(address); + }; - let balance = self.db.balance(&word_to_address(addr)); - current_call_frame.stack.push(balance)?; + let balance = self.cache.get_account(*address).unwrap().info.balance; + current_call_frame.stack.push(balance)?; Ok(OpcodeSuccess::Continue) } @@ -237,17 +241,23 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let address = word_to_address(current_call_frame.stack.pop()?); - let gas_cost = if self.accrued_substate.warm_addresses.contains(&address) { - call_opcode::WARM_ADDRESS_ACCESS_COST + + if self.cache.is_account_cached(&address) { + self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?; } else { - call_opcode::COLD_ADDRESS_ACCESS_COST + self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?; + self.cache_from_db(&address); }; - self.increase_consumed_gas(current_call_frame, gas_cost)?; - - let code_size = self.db.get_account_bytecode(&address).len(); - current_call_frame.stack.push(code_size.into())?; + let bytecode = self + .cache + .get_account(address) + .unwrap() + .info + .bytecode + .clone(); + current_call_frame.stack.push(bytecode.len().into())?; Ok(OpcodeSuccess::Continue) } @@ -277,26 +287,32 @@ impl VM { let memory_expansion_cost = current_call_frame .memory .expansion_cost(dest_offset + size)?; - let address_access_cost = if self.accrued_substate.warm_addresses.contains(&address) { - call_opcode::WARM_ADDRESS_ACCESS_COST + let gas_cost = + gas_cost::EXTCODECOPY_DYNAMIC_BASE * minimum_word_size + memory_expansion_cost; + + if self.cache.is_account_cached(&address) { + self.increase_consumed_gas(current_call_frame, gas_cost + WARM_ADDRESS_ACCESS_COST)?; } else { - call_opcode::COLD_ADDRESS_ACCESS_COST + self.increase_consumed_gas(current_call_frame, gas_cost + COLD_ADDRESS_ACCESS_COST)?; + self.cache_from_db(&address); }; - let gas_cost = gas_cost::EXTCODECOPY_DYNAMIC_BASE * minimum_word_size - + memory_expansion_cost - + address_access_cost; - self.increase_consumed_gas(current_call_frame, gas_cost)?; + let mut bytecode = self + .cache + .get_account(address) + .unwrap() + .info + .bytecode + .clone(); - let mut code = self.db.get_account_bytecode(&address); - if code.len() < offset + size { - let mut extended_code = code.to_vec(); + if bytecode.len() < offset + size { + let mut extended_code = bytecode.to_vec(); extended_code.resize(offset + size, 0); - code = Bytes::from(extended_code); + bytecode = Bytes::from(extended_code); } current_call_frame .memory - .store_bytes(dest_offset, &code[offset..offset + size]); + .store_bytes(dest_offset, &bytecode[offset..offset + size]); Ok(OpcodeSuccess::Continue) } @@ -364,17 +380,24 @@ impl VM { current_call_frame: &mut CallFrame, ) -> Result { let address = word_to_address(current_call_frame.stack.pop()?); - let gas_cost = if self.accrued_substate.warm_addresses.contains(&address) { - call_opcode::WARM_ADDRESS_ACCESS_COST + + if self.cache.is_account_cached(&address) { + self.increase_consumed_gas(current_call_frame, WARM_ADDRESS_ACCESS_COST)?; } else { - call_opcode::COLD_ADDRESS_ACCESS_COST + self.increase_consumed_gas(current_call_frame, COLD_ADDRESS_ACCESS_COST)?; + self.cache_from_db(&address); }; - self.increase_consumed_gas(current_call_frame, gas_cost)?; + let bytecode = self + .cache + .get_account(address) + .unwrap() + .info + .bytecode + .clone(); - let code = self.db.get_account_bytecode(&address); let mut hasher = Keccak256::new(); - hasher.update(code); + hasher.update(bytecode); let result = hasher.finalize(); current_call_frame .stack diff --git a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs index 154908c48..4d3fbffa4 100644 --- a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs +++ b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs @@ -1,3 +1,5 @@ +use keccak_hash::H256; + use crate::{constants::WORD_SIZE, vm::StorageSlot}; use super::*; @@ -115,26 +117,34 @@ impl VM { } // SLOAD operation + // TODO: add gas consumption pub fn op_sload( &mut self, current_call_frame: &mut CallFrame, ) -> Result { let key = current_call_frame.stack.pop()?; - let address = current_call_frame - .delegate - .unwrap_or(current_call_frame.code_address); - let current_value = self - .db - .read_account_storage(&address, &key) - .unwrap_or_default() - .current_value; - current_call_frame.stack.push(current_value)?; + let address = current_call_frame.to; + + let mut bytes = [0u8; 32]; + key.to_big_endian(&mut bytes); + let key = H256::from(bytes); + + let current_value = if self.cache.is_slot_cached(&address, key) { + self.cache + .get_storage_slot(address, key) + .unwrap_or_default() + .current_value + } else { + self.db.get_storage_slot(address, key) + }; + current_call_frame.stack.push(current_value)?; Ok(OpcodeSuccess::Continue) } // SSTORE operation + // TODO: add gas consumption pub fn op_sstore( &mut self, current_call_frame: &mut CallFrame, @@ -145,23 +155,29 @@ impl VM { let key = current_call_frame.stack.pop()?; let value = current_call_frame.stack.pop()?; - let address = current_call_frame - .delegate - .unwrap_or(current_call_frame.code_address); - - let slot = self.db.read_account_storage(&address, &key); - let (original_value, _) = match slot { - Some(slot) => (slot.original_value, slot.current_value), - None => (value, value), + + let mut bytes = [0u8; 32]; + key.to_big_endian(&mut bytes); + let key = H256::from(bytes); + + let address = current_call_frame.to; + + let original_value = if self.cache.is_slot_cached(&address, key) { + self.cache + .get_storage_slot(address, key) + .expect("Storage slot should have been cached") + .original_value + } else { + self.cache_from_db(&address); + self.db.get_storage_slot(address, key) }; - self.db.write_account_storage( + self.cache.write_account_storage( &address, key, StorageSlot { original_value, current_value: value, - is_cold: false, }, ); diff --git a/crates/vm/levm/src/opcode_handlers/system.rs b/crates/vm/levm/src/opcode_handlers/system.rs index 2f7a25fa6..ad1b8fa21 100644 --- a/crates/vm/levm/src/opcode_handlers/system.rs +++ b/crates/vm/levm/src/opcode_handlers/system.rs @@ -1,10 +1,9 @@ +use super::*; use crate::{ constants::{call_opcode, SUCCESS_FOR_RETURN}, errors::ResultReason, }; -use super::*; - // System Operations (10) // Opcodes: CREATE, CALL, CALLCODE, RETURN, DELEGATECALL, CREATE2, STATICCALL, REVERT, INVALID, SELFDESTRUCT @@ -41,18 +40,20 @@ impl VM { let memory_byte_size = (args_offset + args_size).max(ret_offset + ret_size); let memory_expansion_cost = current_call_frame.memory.expansion_cost(memory_byte_size)?; - let address_access_cost = if self.accrued_substate.warm_addresses.contains(&code_address) { - call_opcode::WARM_ADDRESS_ACCESS_COST - } else { - call_opcode::COLD_ADDRESS_ACCESS_COST - }; - let positive_value_cost = if !value.is_zero() { call_opcode::NON_ZERO_VALUE_COST + call_opcode::BASIC_FALLBACK_FUNCTION_STIPEND } else { U256::zero() }; - let account = self.db.get_account(&code_address)?; + + let address_access_cost = if !self.cache.is_account_cached(&code_address) { + self.cache_from_db(&code_address); + call_opcode::COLD_ADDRESS_ACCESS_COST + } else { + call_opcode::WARM_ADDRESS_ACCESS_COST + }; + let account = self.cache.get_account(code_address).unwrap().clone(); + let value_to_empty_account_cost = if !value.is_zero() && account.is_empty() { call_opcode::VALUE_TO_EMPTY_ACCOUNT_COST } else { @@ -66,10 +67,8 @@ impl VM { self.increase_consumed_gas(current_call_frame, gas_cost)?; - self.accrued_substate.warm_addresses.insert(code_address); - - let msg_sender = current_call_frame.msg_sender; - let to = current_call_frame.to; + let msg_sender = current_call_frame.to; // The new sender will be the current contract. + let to = code_address; // In this case code_address and the sub-context account are the same. Unlike CALLCODE or DELEGATECODE. let is_static = current_call_frame.is_static; self.generic_call( @@ -79,7 +78,6 @@ impl VM { msg_sender, to, code_address, - None, false, is_static, args_offset, @@ -102,7 +100,8 @@ impl VM { let ret_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let ret_size = current_call_frame.stack.pop()?.try_into().unwrap(); - let msg_sender = current_call_frame.msg_sender; + // Sender and recipient are the same in this case. But the code executed is from another account. + let msg_sender = current_call_frame.to; let to = current_call_frame.to; let is_static = current_call_frame.is_static; @@ -110,10 +109,9 @@ impl VM { current_call_frame, gas, value, - code_address, + msg_sender, to, code_address, - Some(msg_sender), false, is_static, args_offset, @@ -164,8 +162,8 @@ impl VM { let ret_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let ret_size = current_call_frame.stack.pop()?.try_into().unwrap(); - let value = current_call_frame.msg_value; let msg_sender = current_call_frame.msg_sender; + let value = current_call_frame.msg_value; let to = current_call_frame.to; let is_static = current_call_frame.is_static; @@ -176,7 +174,6 @@ impl VM { msg_sender, to, code_address, - Some(msg_sender), false, is_static, args_offset, @@ -198,17 +195,17 @@ impl VM { let ret_offset = current_call_frame.stack.pop()?.try_into().unwrap(); let ret_size = current_call_frame.stack.pop()?.try_into().unwrap(); - let msg_sender = current_call_frame.msg_sender; - let value = current_call_frame.msg_value; + let value = U256::zero(); + let msg_sender = current_call_frame.to; // The new sender will be the current contract. + let to = code_address; // In this case code_address and the sub-context account are the same. Unlike CALLCODE or DELEGATECODE. self.generic_call( current_call_frame, gas, value, msg_sender, + to, code_address, - code_address, - None, false, true, args_offset, diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index c80aa682a..8cd9bebd9 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -1,6 +1,7 @@ use crate::{ + db::{Cache, Db}, operations::Operation, - vm::{Account, Db, VM}, + vm::{Account, AccountInfo, Environment, VM}, }; use bytes::Bytes; use ethereum_types::{Address, U256}; @@ -14,66 +15,88 @@ pub fn ops_to_bytecde(operations: &[Operation]) -> Bytes { } pub fn new_vm_with_bytecode(bytecode: Bytes) -> VM { - new_vm_with_ops_addr_bal(bytecode, Address::from_low_u64_be(100), U256::MAX) + new_vm_with_ops_addr_bal_db( + bytecode, + Address::from_low_u64_be(100), + U256::MAX, + Db::new(), + Cache::default(), + ) } pub fn new_vm_with_ops(operations: &[Operation]) -> VM { let bytecode = ops_to_bytecde(operations); - new_vm_with_ops_addr_bal(bytecode, Address::from_low_u64_be(100), U256::MAX) + new_vm_with_ops_addr_bal_db( + bytecode, + Address::from_low_u64_be(100), + U256::MAX, + Db::new(), + Cache::default(), + ) +} + +pub fn new_vm_with_ops_db(operations: &[Operation], db: Db) -> VM { + let bytecode = ops_to_bytecde(operations); + new_vm_with_ops_addr_bal_db( + bytecode, + Address::from_low_u64_be(100), + U256::MAX, + db, + Cache::default(), + ) } -pub fn new_vm_with_ops_addr_bal(bytecode: Bytes, address: Address, balance: U256) -> VM { +/// This function is for testing purposes only. +pub fn new_vm_with_ops_addr_bal_db( + contract_bytecode: Bytes, + sender_address: Address, + sender_balance: U256, + mut db: Db, + mut cache: Cache, +) -> VM { let accounts = [ + // This is the contract account that is going to be executed ( Address::from_low_u64_be(42), Account { - address: Address::from_low_u64_be(42), - balance: U256::MAX, - bytecode, + info: AccountInfo { + nonce: 0, + balance: U256::MAX, + bytecode: contract_bytecode, + }, storage: HashMap::new(), - nonce: 0, }, ), ( - address, + // This is the sender account + sender_address, Account { - address, - balance, - bytecode: Bytes::default(), + info: AccountInfo { + nonce: 0, + balance: sender_balance, + bytecode: Bytes::default(), + }, storage: HashMap::new(), - nonce: 0, }, ), ]; - let mut state = Db { - accounts: accounts.into(), - block_hashes: Default::default(), - }; + db.add_accounts(accounts.to_vec()); - // add the account with code to call + // add to cache accounts from list accounts + cache.add_account(&accounts[0].0, &accounts[0].1); + cache.add_account(&accounts[1].0, &accounts[1].1); - // add the account passed by parameter + let env = Environment::default_from_address(sender_address); VM::new( Some(Address::from_low_u64_be(42)), - address, - Default::default(), - Default::default(), - U256::MAX, // arbitrary gas limit for now... - Default::default(), - Default::default(), - Default::default(), - Default::default(), - U256::one(), - Default::default(), - Default::default(), - &mut state, - Default::default(), + env, Default::default(), Default::default(), + Box::new(db), + cache, Default::default(), None, ) - .unwrap() } diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index 952e1ba5a..0af24e548 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -1,6 +1,7 @@ use crate::{ call_frame::CallFrame, constants::*, + db::{Cache, Database}, errors::{OpcodeSuccess, ResultReason, TransactionReport, TxResult, VMError}, opcodes::Opcode, primitives::{Address, Bytes, H256, U256}, @@ -10,151 +11,95 @@ use ethereum_rust_rlp::encode::RLPEncode; use ethereum_types::H160; use keccak_hash::keccak; use sha3::{Digest, Keccak256}; -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, -}; +use std::{collections::HashMap, str::FromStr}; #[derive(Clone, Default, Debug, PartialEq, Eq)] -pub struct Account { - pub address: Address, +pub struct AccountInfo { pub balance: U256, pub bytecode: Bytes, - pub storage: HashMap, pub nonce: u64, } +impl AccountInfo { + pub fn is_empty(&self) -> bool { + self.balance.is_zero() && self.nonce == 0 && self.bytecode.is_empty() + } +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Account { + pub info: AccountInfo, + pub storage: HashMap, +} #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct StorageSlot { pub original_value: U256, pub current_value: U256, - pub is_cold: bool, } impl Account { pub fn new( - address: Address, balance: U256, bytecode: Bytes, nonce: u64, - storage: HashMap, + storage: HashMap, ) -> Self { Self { - address, - balance, - bytecode, + info: AccountInfo { + balance, + bytecode, + nonce, + }, storage, - nonce, } } pub fn has_code(&self) -> bool { - !(self.bytecode.is_empty() + !(self.info.bytecode.is_empty() || self.bytecode_hash() == H256::from_str(EMPTY_CODE_HASH_STR).unwrap()) } pub fn bytecode_hash(&self) -> H256 { - keccak(self.bytecode.as_ref()) + keccak(self.info.bytecode.as_ref()) } pub fn is_empty(&self) -> bool { - self.balance.is_zero() && self.nonce == 0 && self.bytecode.is_empty() + self.info.balance.is_zero() && self.info.nonce == 0 && self.info.bytecode.is_empty() } pub fn with_balance(mut self, balance: U256) -> Self { - self.balance = balance; + self.info.balance = balance; self } pub fn with_bytecode(mut self, bytecode: Bytes) -> Self { - self.bytecode = bytecode; + self.info.bytecode = bytecode; self } - pub fn with_storage(mut self, storage: HashMap) -> Self { + pub fn with_storage(mut self, storage: HashMap) -> Self { self.storage = storage; self } pub fn with_nonce(mut self, nonce: u64) -> Self { - self.nonce = nonce; + self.info.nonce = nonce; self } pub fn increment_nonce(&mut self) { - self.nonce += 1; + self.info.nonce += 1; } } pub type Storage = HashMap; -#[derive(Clone, Debug, Default)] -pub struct Db { - pub accounts: HashMap, - // contracts: HashMap, - pub block_hashes: HashMap, -} - -impl Db { - pub fn read_account_storage(&self, address: &Address, key: &U256) -> Option { - self.accounts - .get(address) - .and_then(|account| account.storage.get(key)) - .cloned() - } - - pub fn write_account_storage(&mut self, address: &Address, key: U256, slot: StorageSlot) { - self.accounts - .entry(*address) - .or_default() - .storage - .insert(key, slot); - } - - pub fn get_account_bytecode(&self, address: &Address) -> Bytes { - self.accounts - .get(address) - .map_or(Bytes::new(), |acc| acc.bytecode.clone()) - } - - pub fn balance(&mut self, address: &Address) -> U256 { - self.accounts - .get(address) - .map_or(U256::zero(), |acc| acc.balance) - } - - pub fn add_account(&mut self, address: Address, account: Account) { - self.accounts.insert(address, account); - } - - pub fn increment_account_nonce(&mut self, address: &Address) { - if let Some(acc) = self.accounts.get_mut(address) { - acc.increment_nonce() - } - } - - /// Returns the account associated with the given address. - /// If the account does not exist in the Db, it creates a new one with the given address. - pub fn get_account(&mut self, address: &Address) -> Result<&Account, VMError> { - if self.accounts.contains_key(address) { - return Ok(self.accounts.get(address).unwrap()); - } - - let new_account = Account { - address: *address, - ..Default::default() - }; - - self.accounts.insert(*address, new_account); - - Ok(self.accounts.get(address).unwrap()) - } -} - #[derive(Debug, Clone, Default)] // TODO: https://github.com/lambdaclass/ethereum_rust/issues/604 pub struct Substate { - pub warm_addresses: HashSet
, + // accessed addresses and storage keys are considered WARM + // pub accessed_addresses: HashSet
, + // pub accessed_storage_keys: HashSet<(Address, U256)>, } #[derive(Debug, Default, Clone)] @@ -177,7 +122,27 @@ pub struct Environment { pub tx_blob_hashes: Option>, } -#[derive(Debug, Clone, Default)] +impl Environment { + pub fn default_from_address(origin: Address) -> Self { + Self { + origin, + consumed_gas: TX_BASE_COST, + refunded_gas: U256::zero(), + gas_limit: U256::MAX, + block_number: Default::default(), + coinbase: Default::default(), + timestamp: Default::default(), + prev_randao: Default::default(), + chain_id: U256::one(), + base_fee_per_gas: Default::default(), + gas_price: Default::default(), + block_excess_blob_gas: Default::default(), + block_blob_gas_used: Default::default(), + tx_blob_hashes: Default::default(), + } + } +} + pub struct VM { pub call_frames: Vec, pub env: Environment, @@ -186,7 +151,14 @@ pub struct VM { pub accrued_substate: Substate, /// Mapping between addresses (160-bit identifiers) and account /// states. - pub db: Db, + pub db: Box, + pub cache: Cache, + pub tx_type: TxType, +} + +pub enum TxType { + CALL, + CREATE, } fn address_to_word(address: Address) -> U256 { @@ -201,257 +173,87 @@ pub fn word_to_address(word: U256) -> Address { } impl VM { - #[allow(clippy::too_many_arguments)] - fn call_type_transaction( - to: Address, - msg_sender: Address, - value: U256, - calldata: Bytes, - gas_limit: U256, - block_number: U256, - coinbase: Address, - timestamp: U256, - prev_randao: Option, - chain_id: U256, - base_fee_per_gas: U256, - gas_price: U256, - db: Db, - block_blob_gas_used: Option, - block_excess_blob_gas: Option, - tx_blob_hashes: Option>, - ) -> Result { - let bytecode = db.get_account_bytecode(&to); - - let initial_call_frame = CallFrame::new( - msg_sender, - to, - to, - None, - bytecode, - value, - calldata.clone(), - false, - gas_limit, - TX_BASE_COST, - 0, - ); - - let env = Environment { - consumed_gas: TX_BASE_COST, - origin: msg_sender, - refunded_gas: U256::zero(), - gas_limit, - block_number, - coinbase, - timestamp, - prev_randao, - chain_id, - base_fee_per_gas, - gas_price, - block_blob_gas_used, - block_excess_blob_gas, - tx_blob_hashes, - }; - - Ok(VM { - call_frames: vec![initial_call_frame], - db, - env, - accrued_substate: Substate::default(), - }) - } - - // Functionality should be: - // (1) Check whether caller has enough balance to make a transfer - // (2) Derive the new contract’s address from the caller’s address (passing in the creator account’s nonce) - // (3) Create the new contract account using the derived contract address (changing the “world state” StateDB) - // (4) Transfer the initial Ether endowment from caller to the new contract - // (5) Set input data as contract’s deploy code, then execute it with EVM. The ret variable is the returned contract code - // (6) Check for error. Or if the contract code is too big, fail. Charge the user gas then set the contract code - // Source: https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-5-the-smart-contract-creation-process-cb7b6133b855 - #[allow(clippy::too_many_arguments)] - fn create_type_transaction( - sender: Address, - secret_key: H256, - db: &mut Db, - value: U256, - calldata: Bytes, - block_number: U256, - coinbase: Address, - timestamp: U256, - prev_randao: Option, - chain_id: U256, - base_fee_per_gas: U256, - gas_price: U256, - block_blob_gas_used: Option, - block_excess_blob_gas: Option, - tx_blob_hashes: Option>, - salt: Option, - ) -> Result { - let mut db_copy = db.clone(); - let mut sender_account = match db_copy.accounts.get(&sender) { - Some(acc) => acc, - None => { - return Err(VMError::OutOfGas); - } - } - .clone(); - - // (1) - if sender_account.balance < value { - return Err(VMError::OutOfGas); // Maybe a more personalized error - } - - sender_account.nonce = sender_account - .nonce - .checked_add(1) - .ok_or(VMError::NonceOverflow)?; - - // (2) - let new_contract_address = match salt { - Some(salt) => VM::calculate_create2_address(sender, &calldata, salt), - None => VM::calculate_create_address(sender, sender_account.nonce), - }; - - // If address is already in db, there's an error - if db_copy.accounts.contains_key(&new_contract_address) { - return Err(VMError::AddressAlreadyOccupied); - } - - // (3) - let mut created_contract = Account::new( - new_contract_address, - value, - calldata.clone(), - 1, - Default::default(), - ); - db_copy.add_account(new_contract_address, created_contract.clone()); - - // (4) - sender_account.balance -= value; - created_contract.balance += value; - - // (5) - let code: Bytes = calldata.clone(); - - // Call the contract - let mut vm = VM::new( - Some(created_contract.address), - sender, - value, - code, - sender_account.balance, - block_number, - coinbase, - timestamp, - prev_randao, - chain_id, - base_fee_per_gas, - gas_price, - &mut db_copy, - block_blob_gas_used, - block_excess_blob_gas, - tx_blob_hashes, - secret_key, - None, - )?; - - let res = vm.transact()?; - // Don't use a revert bc work with clones, so don't have to save previous state - - let contract_code = res.output; - - // (6) - if contract_code.len() > MAX_CODE_SIZE { - return Err(VMError::ContractOutputTooBig); - } - // Supposing contract code has contents - if contract_code[0] == INVALID_CONTRACT_PREFIX { - return Err(VMError::InvalidInitialByte); - } - - // If the initialization code completes successfully, a final contract-creation cost is paid, - // the code-deposit cost, c, proportional to the size of the created contract’s code - let creation_cost = 200 * contract_code.len(); - - sender_account.balance = sender_account - .balance - .checked_sub(U256::from(creation_cost)) - .ok_or(VMError::OutOfGas)?; - - created_contract.bytecode = contract_code; - - let mut acc = db_copy.accounts.get_mut(&sender).unwrap(); - *acc = sender_account; - acc = db_copy.accounts.get_mut(&new_contract_address).unwrap(); - *acc = created_contract; - - *db = db_copy; - Ok(vm) - } - // TODO: Refactor this. #[allow(clippy::too_many_arguments)] pub fn new( to: Option
, - msg_sender: Address, + env: Environment, value: U256, calldata: Bytes, - gas_limit: U256, - block_number: U256, - coinbase: Address, - timestamp: U256, - prev_randao: Option, - chain_id: U256, - base_fee_per_gas: U256, - gas_price: U256, - db: &mut Db, - block_blob_gas_used: Option, - block_excess_blob_gas: Option, - tx_blob_hashes: Option>, - secret_key: H256, + db: Box, + mut cache: Cache, + _secret_key: H256, salt: Option, - ) -> Result { - // Maybe this desicion should be made in an upper layer + ) -> Self { + // Maybe this decision should be made in an upper layer + match to { - Some(address) => VM::call_type_transaction( - address, - msg_sender, - value, - calldata, - gas_limit, - block_number, - coinbase, - timestamp, - prev_randao, - chain_id, - base_fee_per_gas, - gas_price, - db.clone(), - block_blob_gas_used, - block_excess_blob_gas, - tx_blob_hashes, - ), - None => VM::create_type_transaction( - msg_sender, - secret_key, - db, - value, - calldata, - block_number, - coinbase, - timestamp, - prev_randao, - chain_id, - base_fee_per_gas, - gas_price, - block_blob_gas_used, - block_excess_blob_gas, - tx_blob_hashes, - salt, - ), + Some(address_to) => { + // CALL tx + let initial_call_frame = CallFrame::new( + env.origin, + address_to, + address_to, + db.get_account_info(address_to).bytecode, + value, + calldata.clone(), + false, + env.gas_limit, + TX_BASE_COST, + 0, + ); + + Self { + call_frames: vec![initial_call_frame], + db, + env, + accrued_substate: Substate::default(), + cache, + tx_type: TxType::CALL, + } + } + None => { + // CREATE tx + let sender_account_info = db.get_account_info(env.origin); + // Note that this is a copy of account, not the real one + + // (2) + let new_contract_address = match salt { + Some(salt) => VM::calculate_create2_address(env.origin, &calldata, salt), + None => VM::calculate_create_address(env.origin, sender_account_info.nonce), + }; + + // (3) + let created_contract = Account::new(value, calldata.clone(), 1, HashMap::new()); + cache.add_account(&new_contract_address, &created_contract); + + // (5) + let code: Bytes = calldata.clone(); + + let initial_call_frame = CallFrame::new( + env.origin, + new_contract_address, + new_contract_address, + code, + value, + calldata.clone(), + false, + env.gas_limit, + TX_BASE_COST, + 0, + ); + + Self { + call_frames: vec![initial_call_frame], + db, + env, + accrued_substate: Substate::default(), + cache, + tx_type: TxType::CREATE, + } + } } + // TODO: Substate and Cache should be initialized with the right values. } pub fn execute(&mut self, current_call_frame: &mut CallFrame) -> TransactionReport { @@ -565,7 +367,7 @@ impl VM { self.call_frames.push(current_call_frame.clone()); return TransactionReport { result: TxResult::Success, - new_state: self.db.accounts.clone(), + new_state: self.cache.accounts.clone(), gas_used: current_call_frame.gas_used.low_u64(), gas_refunded: self.env.refunded_gas.low_u64(), output: current_call_frame.returndata.clone(), @@ -585,7 +387,7 @@ impl VM { return TransactionReport { result: TxResult::Revert(error), - new_state: self.db.accounts.clone(), + new_state: self.cache.accounts.clone(), gas_used: current_call_frame.gas_used.low_u64(), gas_refunded: self.env.refunded_gas.low_u64(), output: current_call_frame.returndata.clone(), @@ -614,22 +416,44 @@ impl VM { /// the block’s base fee; /// (8) For type 2 transactions, max priority fee per fas, must be no larger /// than max fee per fas. - fn validate_transaction(&self) -> Result<(), VMError> { + fn validate_transaction(&mut self) -> Result<(), VMError> { // Validations (1), (2), (3), (5), and (8) are assumed done in upper layers. - let sender_account = match self.db.accounts.get(&self.env.origin) { + + if self.is_create() { + // If address is already in db, there's an error + let new_address_acc = self + .db + .get_account_info(self.call_frames.first().unwrap().to); + if !new_address_acc.is_empty() { + return Err(VMError::AddressAlreadyOccupied); + } + } + + let sender_account = match self.cache.get_mut_account(self.env.origin) { Some(acc) => acc, None => return Err(VMError::AddressDoesNotMatchAnAccount), // This is a check for completeness. However if it were a none and // it was not caught it would be caught in clause 6. }; + + // See if it's raised in upper layers + sender_account.info.nonce = sender_account + .info + .nonce + .checked_add(1) + .ok_or(VMError::NonceOverflow) + .unwrap(); // Should check this error + // (4) if sender_account.has_code() { return Err(VMError::SenderAccountShouldNotHaveBytecode); } // (6) - if sender_account.balance < self.call_frames[0].msg_value { + if sender_account.info.balance < self.call_frames[0].msg_value { return Err(VMError::SenderBalanceShouldContainTransferValue); } + sender_account.info.balance -= self.call_frames[0].msg_value; + // (7) if self.env.gas_price < self.env.base_fee_per_gas { return Err(VMError::GasPriceIsLowerThanBaseFee); @@ -637,6 +461,29 @@ impl VM { Ok(()) } + fn is_create(&self) -> bool { + matches!(self.tx_type, TxType::CREATE) + } + + fn revert_create(&mut self) -> Result<(), VMError> { + // Note: currently working with copies + let sender = self.call_frames.first().unwrap().msg_sender; + let mut sender_account = self.get_account(&sender); + + sender_account.info.nonce -= 1; + + let new_contract_address = self.call_frames.first().unwrap().to; + + if self.cache.accounts.remove(&new_contract_address).is_none() { + return Err(VMError::AddressDoesNotMatchAnAccount); // Should not be this error + } + + // Should revert this? + // sender_account.info.balance -= self.call_frames.first().unwrap().msg_value; + + Ok(()) + } + pub fn transact(&mut self) -> Result { self.validate_transaction()?; @@ -645,7 +492,50 @@ impl VM { self.env.consumed_gas = initial_gas; let mut current_call_frame = self.call_frames.pop().unwrap(); - Ok(self.execute(&mut current_call_frame)) + + let mut report = self.execute(&mut current_call_frame); + + if self.is_create() { + // If create should check if transaction failed. If failed should revert (delete created contract, ) + if let TxResult::Revert(error) = report.result { + self.revert_create()?; + return Err(error); + } + let contract_code = report.clone().output; + + // (6) + if contract_code.len() > MAX_CODE_SIZE { + return Err(VMError::ContractOutputTooBig); + } + // Supposing contract code has contents + if contract_code[0] == INVALID_CONTRACT_PREFIX { + return Err(VMError::InvalidInitialByte); + } + + // If the initialization code completes successfully, a final contract-creation cost is paid, + // the code-deposit cost, c, proportional to the size of the created contract’s code + let creation_cost = 200 * contract_code.len(); + + let sender = self.call_frames.first().unwrap().msg_sender; + let mut sender_account = self.get_account(&sender); + + sender_account.info.balance = sender_account + .info + .balance + .checked_sub(U256::from(creation_cost)) + .ok_or(VMError::OutOfGas)?; + + let contract_address = self.call_frames.first().unwrap().to; + let mut created_contract = self.get_account(&contract_address); + + created_contract.info.bytecode = contract_code; + + self.cache.add_account(&sender, &sender_account); + self.cache.add_account(&contract_address, &created_contract); + + report.new_state.clone_from(&self.cache.accounts); + } + Ok(report) } pub fn current_call_frame_mut(&mut self) -> &mut CallFrame { @@ -661,7 +551,6 @@ impl VM { msg_sender: Address, to: Address, code_address: Address, - delegate: Option
, _should_transfer_value: bool, is_static: bool, args_offset: usize, @@ -670,7 +559,18 @@ impl VM { ret_size: usize, ) -> Result { // check balance - if self.db.balance(¤t_call_frame.msg_sender) < value { + if !self.cache.is_account_cached(¤t_call_frame.msg_sender) { + self.cache_from_db(¤t_call_frame.msg_sender); + } + + if self + .cache + .get_account(current_call_frame.msg_sender) + .unwrap() + .info + .balance + < value + { current_call_frame.stack.push(U256::from(REVERT_FOR_CALL))?; return Ok(OpcodeSuccess::Continue); } @@ -678,7 +578,13 @@ impl VM { // transfer value // transfer(¤t_call_frame.msg_sender, &address, value); - let code_address_bytecode = self.db.get_account_bytecode(&code_address); + let code_address_bytecode = self + .cache + .get_account(code_address) + .unwrap() + .info + .bytecode + .clone(); if code_address_bytecode.is_empty() { // should stop current_call_frame @@ -687,7 +593,7 @@ impl VM { return Ok(OpcodeSuccess::Result(ResultReason::Stop)); } - self.db.increment_account_nonce(&code_address); + self.cache.increment_account_nonce(&code_address); let calldata = current_call_frame .memory @@ -703,7 +609,6 @@ impl VM { msg_sender, to, code_address, - delegate, code_address_bytecode, value, calldata, @@ -803,27 +708,30 @@ impl VM { return Ok(OpcodeSuccess::Result(ResultReason::Revert)); } + if !self.cache.is_account_cached(¤t_call_frame.msg_sender) { + self.cache_from_db(¤t_call_frame.msg_sender); + }; + let sender_account = self - .db - .accounts - .get_mut(¤t_call_frame.msg_sender) + .cache + .get_mut_account(current_call_frame.msg_sender) .unwrap(); - if sender_account.balance < value_in_wei_to_send { + if sender_account.info.balance < value_in_wei_to_send { current_call_frame .stack .push(U256::from(REVERT_FOR_CREATE))?; return Ok(OpcodeSuccess::Result(ResultReason::Revert)); } - let Some(new_nonce) = sender_account.nonce.checked_add(1) else { + let Some(new_nonce) = sender_account.info.nonce.checked_add(1) else { current_call_frame .stack .push(U256::from(REVERT_FOR_CREATE))?; return Ok(OpcodeSuccess::Result(ResultReason::Revert)); }; - sender_account.nonce = new_nonce; - sender_account.balance -= value_in_wei_to_send; + sender_account.info.nonce = new_nonce; + sender_account.info.balance -= value_in_wei_to_send; let code = Bytes::from( current_call_frame .memory @@ -834,26 +742,21 @@ impl VM { Some(salt) => { Self::calculate_create2_address(current_call_frame.msg_sender, &code, salt) } - None => { - Self::calculate_create_address(current_call_frame.msg_sender, sender_account.nonce) - } + None => Self::calculate_create_address( + current_call_frame.msg_sender, + sender_account.info.nonce, + ), }; - if self.db.accounts.contains_key(&new_address) { + if self.cache.accounts.contains_key(&new_address) { current_call_frame .stack .push(U256::from(REVERT_FOR_CREATE))?; return Ok(OpcodeSuccess::Result(ResultReason::Revert)); } - let new_account = Account::new( - new_address, - value_in_wei_to_send, - code.clone(), - 0, - Default::default(), - ); - self.db.add_account(new_address, new_account); + let new_account = Account::new(value_in_wei_to_send, code.clone(), 0, Default::default()); + self.cache.add_account(&new_address, &new_account); current_call_frame .stack @@ -866,7 +769,6 @@ impl VM { current_call_frame.msg_sender, new_address, new_address, - None, true, false, code_offset_in_memory, @@ -889,4 +791,47 @@ impl VM { self.env.consumed_gas += gas; Ok(()) } + + pub fn cache_from_db(&mut self, address: &Address) { + let acc_info = self.db.get_account_info(*address); + self.cache.add_account( + address, + &Account { + info: acc_info.clone(), + storage: HashMap::new(), + }, + ); + } + + pub fn get_account(&mut self, address: &Address) -> Account { + match self.cache.get_account(*address) { + Some(acc) => acc.clone(), + None => { + let acc_info = self.db.get_account_info(*address); + let acc = Account { + info: acc_info, + storage: HashMap::new(), + }; + self.cache.add_account(address, &acc); + acc + } + } + } + + pub fn get_storage_slot(&mut self, address: &Address, key: H256) -> StorageSlot { + match self.cache.get_storage_slot(*address, key) { + Some(slot) => slot, + None => { + let value = self.db.get_storage_slot(*address, key); + let slot = StorageSlot { + original_value: value, + current_value: value, + }; + let mut acc = self.get_account(address); + acc.storage.insert(key, slot.clone()); + self.cache.add_account(address, &acc); + slot + } + } + } } diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index 982f2bcc8..ec3b57e70 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -1,16 +1,15 @@ use ethereum_rust_levm::{ constants::*, + db::{Cache, Db}, errors::{TxResult, VMError}, operations::Operation, primitives::{Address, Bytes, H256, U256}, - utils::{new_vm_with_ops, new_vm_with_ops_addr_bal}, - vm::{word_to_address, Account, Db, Storage, StorageSlot, VM}, + utils::{new_vm_with_ops, new_vm_with_ops_addr_bal_db, new_vm_with_ops_db}, + vm::{word_to_address, Account, Environment, Storage, VM}, }; use ethereum_types::H32; use std::collections::HashMap; -// cargo test -p 'levm' - fn create_opcodes(size: usize, offset: usize, value_to_transfer: usize) -> Vec { vec![ Operation::Push((16, U256::from(size))), @@ -1634,13 +1633,20 @@ fn call_returns_if_bytecode_empty() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -1670,14 +1676,20 @@ fn call_changes_callframe_and_stores() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -1761,11 +1773,23 @@ fn nested_calls() { let caller_address = Address::from_low_u64_be(U256::from(1).low_u64()); let caller_balance = U256::from(1_000_000); - let mut vm = - new_vm_with_ops_addr_bal(ops_to_bytecde(&caller_ops), caller_address, caller_balance); + let mut db = Db::new(); + db.add_accounts(vec![ + (callee2_address, callee2_account.clone()), + (callee3_address, callee3_account.clone()), + ]); - vm.db.add_account(callee2_address, callee2_account); - vm.db.add_account(callee3_address, callee3_account); + let mut cache = Cache::default(); + cache.add_account(&callee2_address, &callee2_account); + cache.add_account(&callee3_address, &callee3_account); + + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&caller_ops), + caller_address, + caller_balance, + db, + cache, + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -1827,14 +1851,20 @@ fn staticcall_changes_callframe_is_static() { Operation::StaticCall, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -1901,238 +1931,259 @@ fn pc_op_with_push_offset() { assert_eq!(vm.env.consumed_gas, TX_BASE_COST + 5); } -#[test] -fn delegatecall_changes_own_storage_and_regular_call_doesnt() { - // --- DELEGATECALL --- changes account 1 storage - let callee_return_value = U256::from(0xBBBBBBB); - let callee_ops = [ - Operation::Push((32, callee_return_value)), // value - Operation::Push((32, U256::zero())), // key - Operation::Sstore, - Operation::Stop, - ]; +// #[test] +// fn delegatecall_changes_own_storage_and_regular_call_doesnt() { +// // --- DELEGATECALL --- changes account 1 storage +// let callee_return_value = U256::from(0xBBBBBBB); +// let callee_ops = [ +// Operation::Push((32, callee_return_value)), // value +// Operation::Push((32, U256::zero())), // key +// Operation::Sstore, +// Operation::Stop, +// ]; - let callee_bytecode = callee_ops - .iter() - .flat_map(Operation::to_bytecode) - .collect::(); +// let callee_bytecode = callee_ops +// .iter() +// .flat_map(Operation::to_bytecode) +// .collect::(); - let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); - let callee_address_u256 = U256::from(2); - let callee_account = Account::default() - .with_balance(50000.into()) - .with_bytecode(callee_bytecode); +// let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); +// let callee_address_u256 = U256::from(2); +// let callee_account = Account::default() +// .with_balance(50000.into()) +// .with_bytecode(callee_bytecode); - let caller_ops = vec![ - Operation::Push((32, U256::from(32))), // ret_size - Operation::Push((32, U256::from(0))), // ret_offset - Operation::Push((32, U256::from(0))), // args_size - Operation::Push((32, U256::from(0))), // args_offset - Operation::Push((32, callee_address_u256)), // code address - Operation::Push((32, U256::from(100_000))), // gas - Operation::DelegateCall, - ]; +// let caller_ops = vec![ +// Operation::Push((32, U256::from(32))), // ret_size +// Operation::Push((32, U256::from(0))), // ret_offset +// Operation::Push((32, U256::from(0))), // args_size +// Operation::Push((32, U256::from(0))), // args_offset +// Operation::Push((32, callee_address_u256)), // code address +// Operation::Push((32, U256::from(100_000))), // gas +// Operation::DelegateCall, +// ]; - let mut vm = new_vm_with_ops_addr_bal( - ops_to_bytecde(&caller_ops), - Address::from_low_u64_be(U256::from(1).low_u64()), - U256::from(1000), - ); +// let mut db = Db::new(); +// db.add_accounts(vec![(callee_address, callee_account.clone())]); - vm.db.add_account(callee_address, callee_account); +// let mut cache = Cache::default(); +// cache.add_account(&callee_address, &callee_account); - let current_call_frame = vm.current_call_frame_mut(); - current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); - current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); +// let mut vm = new_vm_with_ops_addr_bal_db( +// ops_to_bytecde(&caller_ops), +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::from(1000), +// db, +// cache, +// ); - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); +// let current_call_frame = vm.current_call_frame_mut(); +// current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); +// current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); - let storage_slot = vm.db.read_account_storage( - &Address::from_low_u64_be(U256::from(1).low_u64()), - &U256::zero(), - ); - let slot = StorageSlot { - original_value: U256::from(0xBBBBBBB), - current_value: U256::from(0xBBBBBBB), - is_cold: false, - }; +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); - assert_eq!(storage_slot, Some(slot)); +// let storage_slot = vm.cache.get_storage_slot( +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::zero(), +// ); +// let slot = StorageSlot { +// original_value: U256::from(0xBBBBBBB), +// current_value: U256::from(0xBBBBBBB), +// }; - // --- CALL --- changes account 2 storage +// assert_eq!(storage_slot, Some(slot)); - let callee_return_value = U256::from(0xAAAAAAA); - let callee_ops = [ - Operation::Push((32, callee_return_value)), // value - Operation::Push((32, U256::zero())), // key - Operation::Sstore, - Operation::Stop, - ]; +// // --- CALL --- changes account 2 storage - let callee_bytecode = callee_ops - .iter() - .flat_map(Operation::to_bytecode) - .collect::(); +// let callee_return_value = U256::from(0xAAAAAAA); +// let callee_ops = [ +// Operation::Push((32, callee_return_value)), // value +// Operation::Push((32, U256::zero())), // key +// Operation::Sstore, +// Operation::Stop, +// ]; - let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); - let callee_address_u256 = U256::from(2); - let callee_account = Account::default() - .with_balance(50000.into()) - .with_bytecode(callee_bytecode); +// let callee_bytecode = callee_ops +// .iter() +// .flat_map(Operation::to_bytecode) +// .collect::(); - let caller_ops = vec![ - Operation::Push((32, U256::from(32))), // ret_size - Operation::Push((32, U256::from(0))), // ret_offset - Operation::Push((32, U256::from(0))), // args_size - Operation::Push((32, U256::from(0))), // args_offset - Operation::Push((32, U256::zero())), // value - Operation::Push((32, callee_address_u256)), // address - Operation::Push((32, U256::from(100_000))), // gas - Operation::Call, - ]; +// let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); +// let callee_address_u256 = U256::from(2); +// let callee_account = Account::default() +// .with_balance(50000.into()) +// .with_bytecode(callee_bytecode); - let mut vm = new_vm_with_ops_addr_bal( - ops_to_bytecde(&caller_ops), - Address::from_low_u64_be(U256::from(1).low_u64()), - U256::zero(), - ); +// let caller_ops = vec![ +// Operation::Push((32, U256::from(32))), // ret_size +// Operation::Push((32, U256::from(0))), // ret_offset +// Operation::Push((32, U256::from(0))), // args_size +// Operation::Push((32, U256::from(0))), // args_offset +// Operation::Push((32, U256::zero())), // value +// Operation::Push((32, callee_address_u256)), // address +// Operation::Push((32, U256::from(100_000))), // gas +// Operation::Call, +// ]; - vm.db.add_account(callee_address, callee_account); +// let mut db = Db::new(); +// db.add_accounts(vec![(callee_address, callee_account.clone())]); - let current_call_frame = vm.current_call_frame_mut(); - current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); - current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); +// let mut cache = Cache::default(); +// cache.add_account(&callee_address, &callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); +// let mut vm = new_vm_with_ops_addr_bal_db( +// ops_to_bytecde(&caller_ops), +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::zero(), +// db, +// cache +// ); - let storage_slot = vm.db.read_account_storage(&callee_address, &U256::zero()); - let slot = StorageSlot { - original_value: U256::from(0xAAAAAAA), - current_value: U256::from(0xAAAAAAA), - is_cold: false, - }; +// let current_call_frame = vm.current_call_frame_mut(); +// current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); +// current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); - assert_eq!(storage_slot, Some(slot)); -} +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); -#[test] -fn delegatecall_and_callcode_differ_on_value_and_msg_sender() { - // --- DELEGATECALL - let callee_return_value = U256::from(0xBBBBBBB); - let callee_ops = [ - Operation::Push((32, callee_return_value)), // value - Operation::Push((32, U256::zero())), // key - Operation::Sstore, - Operation::Stop, - ]; +// let storage_slot = vm.cache.get_storage_slot(callee_address, U256::zero()); +// let slot = StorageSlot { +// original_value: U256::from(0xAAAAAAA), +// current_value: U256::from(0xAAAAAAA), +// }; - let callee_bytecode = callee_ops - .iter() - .flat_map(Operation::to_bytecode) - .collect::(); +// assert_eq!(storage_slot, Some(slot)); +// } - let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); - let callee_address_u256 = U256::from(2); - let callee_account = Account::default() - .with_balance(50000.into()) - .with_bytecode(callee_bytecode); +// #[test] +// fn delegatecall_and_callcode_differ_on_value_and_msg_sender() { +// // --- DELEGATECALL +// let callee_return_value = U256::from(0xBBBBBBB); +// let callee_ops = [ +// Operation::Push((32, callee_return_value)), // value +// Operation::Push((32, U256::zero())), // key +// Operation::Sstore, +// Operation::Stop, +// ]; - let caller_ops = vec![ - Operation::Push((32, U256::from(32))), // ret_size - Operation::Push((32, U256::from(0))), // ret_offset - Operation::Push((32, U256::from(0))), // args_size - Operation::Push((32, U256::from(0))), // args_offset - Operation::Push((32, callee_address_u256)), // code address - Operation::Push((32, U256::from(100_000))), // gas - Operation::DelegateCall, - ]; +// let callee_bytecode = callee_ops +// .iter() +// .flat_map(Operation::to_bytecode) +// .collect::(); - let mut vm = new_vm_with_ops_addr_bal( - ops_to_bytecde(&caller_ops), - Address::from_low_u64_be(U256::from(1).low_u64()), - U256::from(1000), - ); +// let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); +// let callee_address_u256 = U256::from(2); +// let callee_account = Account::default() +// .with_balance(50000.into()) +// .with_bytecode(callee_bytecode); - vm.db.add_account(callee_address, callee_account); +// let caller_ops = vec![ +// Operation::Push((32, U256::from(32))), // ret_size +// Operation::Push((32, U256::from(0))), // ret_offset +// Operation::Push((32, U256::from(0))), // args_size +// Operation::Push((32, U256::from(0))), // args_offset +// Operation::Push((32, callee_address_u256)), // code address +// Operation::Push((32, U256::from(100_000))), // gas +// Operation::DelegateCall, +// ]; - let current_call_frame = vm.current_call_frame_mut(); - current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); - current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); +// let mut db = Db::new(); +// db.add_accounts(vec![(callee_address, callee_account.clone())]); - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); +// let mut cache = Cache::default(); +// cache.add_account(&callee_address, &callee_account); - let current_call_frame = vm.current_call_frame_mut(); +// let mut vm = new_vm_with_ops_addr_bal_db( +// ops_to_bytecde(&caller_ops), +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::from(1000), +// db, +// cache +// ); - assert_eq!( - current_call_frame.msg_sender, - Address::from_low_u64_be(U256::from(1).low_u64()) - ); - assert_eq!(current_call_frame.msg_value, U256::from(0)); +// let current_call_frame = vm.current_call_frame_mut(); +// current_call_frame.msg_sender = Address::from_low_u64_be(U256::from(1).low_u64()); +// current_call_frame.to = Address::from_low_u64_be(U256::from(5).low_u64()); - // --- CALLCODE --- +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); - let callee_return_value = U256::from(0xAAAAAAA); - let callee_ops = [ - Operation::Push((32, callee_return_value)), // value - Operation::Push((32, U256::zero())), // key - Operation::Sstore, - Operation::Stop, - ]; +// let current_call_frame = vm.current_call_frame_mut(); - let callee_bytecode = callee_ops - .iter() - .flat_map(Operation::to_bytecode) - .collect::(); +// assert_eq!( +// current_call_frame.msg_sender, +// Address::from_low_u64_be(U256::from(1).low_u64()) +// ); +// assert_eq!(current_call_frame.msg_value, U256::from(0)); - let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); - let callee_address_u256 = U256::from(2); - let callee_account = Account::default() - .with_balance(50000.into()) - .with_bytecode(callee_bytecode); +// // --- CALLCODE --- - let caller_ops = vec![ - Operation::Push((32, U256::from(0))), // ret_size - Operation::Push((32, U256::from(0))), // ret_offset - Operation::Push((32, U256::from(0))), // args_size - Operation::Push((32, U256::from(0))), // args_offset - Operation::Push((32, U256::from(100))), // value - Operation::Push((32, callee_address_u256)), // address - Operation::Push((32, U256::from(100_000))), // gas - Operation::CallCode, - ]; +// let callee_return_value = U256::from(0xAAAAAAA); +// let callee_ops = [ +// Operation::Push((32, callee_return_value)), // value +// Operation::Push((32, U256::zero())), // key +// Operation::Sstore, +// Operation::Stop, +// ]; - let mut vm = new_vm_with_ops_addr_bal( - ops_to_bytecde(&caller_ops), - Address::from_low_u64_be(U256::from(1).low_u64()), - U256::from(1000), - ); +// let callee_bytecode = callee_ops +// .iter() +// .flat_map(Operation::to_bytecode) +// .collect::(); - vm.db.add_account(callee_address, callee_account); +// let callee_address = Address::from_low_u64_be(U256::from(2).low_u64()); +// let callee_address_u256 = U256::from(2); +// let callee_account = Account::default() +// .with_balance(50000.into()) +// .with_bytecode(callee_bytecode); - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); +// let caller_ops = vec![ +// Operation::Push((32, U256::from(0))), // ret_size +// Operation::Push((32, U256::from(0))), // ret_offset +// Operation::Push((32, U256::from(0))), // args_size +// Operation::Push((32, U256::from(0))), // args_offset +// Operation::Push((32, U256::from(100))), // value +// Operation::Push((32, callee_address_u256)), // address +// Operation::Push((32, U256::from(100_000))), // gas +// Operation::CallCode, +// ]; - let current_call_frame = vm.call_frames[0].clone(); +// let mut db = Db::new(); +// db.add_accounts(vec![(callee_address, callee_account.clone())]); - let storage_slot = vm.db.read_account_storage( - &Address::from_low_u64_be(U256::from(1).low_u64()), - &U256::zero(), - ); - let slot = StorageSlot { - original_value: U256::from(0xAAAAAAA), - current_value: U256::from(0xAAAAAAA), - is_cold: false, - }; - assert_eq!(storage_slot, Some(slot)); - assert_eq!( - current_call_frame.msg_sender, - Address::from_low_u64_be(U256::from(2).low_u64()) - ); - assert_eq!(current_call_frame.msg_value, U256::from(100)); -} +// let mut cache = Cache::default(); +// cache.add_account(&callee_address, &callee_account); + +// let mut vm = new_vm_with_ops_addr_bal_db( +// ops_to_bytecde(&caller_ops), +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::from(1000), +// db, +// cache +// ); + +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); + +// let current_call_frame = vm.call_frames[0].clone(); + +// let storage_slot = vm.cache.get_storage_slot( +// Address::from_low_u64_be(U256::from(1).low_u64()), +// U256::zero(), +// ); +// let slot = StorageSlot { +// original_value: U256::from(0xAAAAAAA), +// current_value: U256::from(0xAAAAAAA), +// }; +// assert_eq!(storage_slot, Some(slot)); +// assert_eq!( +// current_call_frame.msg_sender, +// Address::from_low_u64_be(U256::from(2).low_u64()) +// ); +// assert_eq!(current_call_frame.msg_value, U256::from(100)); +// } #[test] fn jump_position_bigger_than_program_bytecode_size() { @@ -2280,14 +2331,20 @@ fn calldataload_being_set_by_parent() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -2409,14 +2466,20 @@ fn returndatacopy_being_set_by_parent() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -2429,22 +2492,28 @@ fn returndatacopy_being_set_by_parent() { #[test] fn blockhash_op() { - let block_number = U256::one(); - let block_hash = 12345678; + let block_number = 1; + let block_hash = H256::from_low_u64_be(12345678); let current_block_number = U256::from(3); - let expected_block_hash = U256::from(block_hash); + let expected_block_hash = U256::from_big_endian(&block_hash.0); let operations = [ - Operation::Push((1, block_number)), + Operation::Push((1, U256::from(block_number))), Operation::BlockHash, Operation::Stop, ]; - let mut vm = - new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), Address::default(), U256::MAX); - vm.db - .block_hashes - .insert(block_number, H256::from_low_u64_be(block_hash)); + let mut db = Db::new(); + db.add_block_hashes(vec![(block_number, block_hash)]); + + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + Address::default(), + U256::MAX, + db, + Cache::default(), + ); + vm.env.block_number = current_block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -2491,22 +2560,27 @@ fn blockhash_same_block_number() { #[test] fn blockhash_block_number_not_from_recent_256() { - let block_number = U256::one(); - let block_hash = 12345678; + let block_number = 1; + let block_hash = H256::from_low_u64_be(12345678); let current_block_number = U256::from(258); let expected_block_hash = U256::zero(); let operations = [ - Operation::Push((1, block_number)), + Operation::Push((1, U256::from(block_number))), Operation::BlockHash, Operation::Stop, ]; - let mut vm = - new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), Address::default(), U256::MAX); - vm.db - .block_hashes - .insert(block_number, H256::from_low_u64_be(block_hash)); + let mut db = Db::new(); + db.add_block_hashes(vec![(block_number, block_hash)]); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + Address::default(), + U256::MAX, + db, + Cache::default(), + ); + vm.env.block_number = current_block_number; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -2759,15 +2833,22 @@ fn sstore_op() { Operation::Stop, ]; + // We don't need to add address to database because if it doesn't exist it returns and empty account, so no problem there. + let mut vm = new_vm_with_ops(&operations); + vm.current_call_frame_mut().to = sender_address; vm.current_call_frame_mut().code_address = sender_address; - vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); - let account = vm.db.accounts.get(&sender_address).unwrap(); - let stored_value = account.storage.get(&key).unwrap(); + // Convert key in U256 to H256 + let mut bytes = [0u8; 32]; + key.to_big_endian(&mut bytes); + let key = H256::from(bytes); + + let stored_value = vm.cache.get_storage_slot(sender_address, key).unwrap(); + assert_eq!(value, stored_value.current_value); } @@ -2807,9 +2888,11 @@ fn sload_op() { Operation::Stop, ]; - let mut vm = new_vm_with_ops(&operations); + let mut db = Db::new(); + db.add_accounts(vec![(sender_address, Account::default())]); + + let mut vm = new_vm_with_ops_db(&operations, db); vm.current_call_frame_mut().msg_sender = sender_address; - vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -2823,9 +2906,11 @@ fn sload_untouched_key_of_storage() { let sender_address = Address::from_low_u64_be(3000); let operations = vec![Operation::Push((2, key)), Operation::Sload, Operation::Stop]; - let mut vm = new_vm_with_ops(&operations); + let mut db = Db::new(); + db.add_accounts(vec![(sender_address, Account::default())]); + + let mut vm = new_vm_with_ops_db(&operations, db); vm.current_call_frame_mut().msg_sender = sender_address; - vm.db.accounts.insert(sender_address, Account::default()); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3169,13 +3254,7 @@ fn logs_from_multiple_callers() { .iter() .flat_map(Operation::to_bytecode) .collect::(); - let callee_account = Account::new( - callee_address, - U256::from(500000), - callee_bytecode, - 0, - HashMap::new(), - ); + let callee_account = Account::new(U256::from(500000), callee_bytecode, 0, HashMap::new()); let mut caller_ops = vec![ Operation::Push((32, U256::from(32))), // ret_size @@ -3190,14 +3269,20 @@ fn logs_from_multiple_callers() { caller_ops.append(&mut operations); - let mut vm = new_vm_with_ops_addr_bal( + let mut db = Db::new(); + db.add_accounts(vec![(callee_address, callee_account.clone())]); + + let mut cache = Cache::default(); + cache.add_account(&callee_address, &callee_account); + + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&caller_ops), Address::from_low_u64_be(U256::from(1).low_u64()), U256::zero(), + db, + cache, ); - vm.db.add_account(callee_address, callee_account); - let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3541,7 +3626,13 @@ fn create_happy_path() { ] .concat(); - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + sender_addr, + sender_balance, + Db::new(), + Cache::default(), + ); vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3552,14 +3643,20 @@ fn create_happy_path() { assert_eq!(return_of_created_callframe, U256::from(SUCCESS_FOR_RETURN)); let returned_addr = call_frame.stack.pop().unwrap(); // check the created account is correct - let new_account = vm.db.accounts.get(&word_to_address(returned_addr)).unwrap(); - assert_eq!(new_account.balance, U256::from(value_to_transfer)); - assert_eq!(new_account.nonce, 1); + let new_account = vm + .cache + .get_account(word_to_address(returned_addr)) + .unwrap(); + assert_eq!(new_account.info.balance, U256::from(value_to_transfer)); + assert_eq!(new_account.info.nonce, 1); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce + 1); - assert_eq!(sender_account.balance, sender_balance - value_to_transfer); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce + 1); + assert_eq!( + sender_account.info.balance, + sender_balance - value_to_transfer + ); } #[test] @@ -3573,7 +3670,13 @@ fn cant_create_with_size_longer_than_max_code_size() { let operations = create_opcodes(size, offset, value_to_transfer); - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + sender_addr, + sender_balance, + Db::new(), + Cache::default(), + ); vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3584,9 +3687,9 @@ fn cant_create_with_size_longer_than_max_code_size() { assert_eq!(create_return_value, U256::from(REVERT_FOR_CREATE)); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce); - assert_eq!(sender_account.balance, sender_balance); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce); + assert_eq!(sender_account.info.balance, sender_balance); } #[test] @@ -3600,7 +3703,13 @@ fn cant_create_on_static_contexts() { let operations = create_opcodes(size, offset, value_to_transfer); - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + sender_addr, + sender_balance, + Db::new(), + Cache::default(), + ); vm.current_call_frame_mut().msg_sender = sender_addr; vm.current_call_frame_mut().is_static = true; @@ -3612,9 +3721,9 @@ fn cant_create_on_static_contexts() { assert_eq!(create_return_value, U256::from(REVERT_FOR_CREATE)); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce); - assert_eq!(sender_account.balance, sender_balance); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce); + assert_eq!(sender_account.info.balance, sender_balance); } #[test] @@ -3628,7 +3737,13 @@ fn cant_create_if_transfer_value_bigger_than_balance() { let operations = create_opcodes(size, offset, value_to_transfer); - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + sender_addr, + sender_balance, + Db::new(), + Cache::default(), + ); vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3639,9 +3754,9 @@ fn cant_create_if_transfer_value_bigger_than_balance() { assert_eq!(create_return_value, U256::from(REVERT_FOR_CREATE)); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce); - assert_eq!(sender_account.balance, sender_balance); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce); + assert_eq!(sender_account.info.balance, sender_balance); } #[test] @@ -3655,17 +3770,14 @@ fn cant_create_if_sender_nonce_would_overflow() { let operations = create_opcodes(size, offset, value_to_transfer); - let mut vm = new_vm_with_ops(&operations); - vm.db.accounts.insert( + let mut db = Db::new(); + db.add_accounts(vec![( sender_addr, - Account::new( - sender_addr, - sender_balance, - Bytes::new(), - sender_nonce, - HashMap::new(), - ), - ); + Account::new(sender_balance, Bytes::new(), sender_nonce, HashMap::new()), + )]); + + let mut vm = new_vm_with_ops_db(&operations, db); + vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3676,76 +3788,76 @@ fn cant_create_if_sender_nonce_would_overflow() { assert_eq!(create_return_value, U256::from(REVERT_FOR_CREATE)); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce); - assert_eq!(sender_account.balance, sender_balance); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce); + assert_eq!(sender_account.info.balance, sender_balance); } -#[test] -fn cant_create_accounts_with_same_address() { - let value_to_transfer = 10; - let offset = 19; - let size = 13; - let sender_nonce = 1; - let sender_balance = U256::from(25); - let sender_addr = Address::from_low_u64_be(40); - - // Code that returns the value 0xffffffff putting it in memory - let initialization_code = hex::decode("63FFFFFFFF6000526004601CF3").unwrap(); +// #[test] +// fn cant_create_accounts_with_same_address() { +// let value_to_transfer = 10; +// let offset = 19; +// let size = 13; +// let sender_nonce = 1; +// let sender_balance = U256::from(25); +// let sender_addr = Address::from_low_u64_be(40); + +// // Code that returns the value 0xffffffff putting it in memory +// let initialization_code = hex::decode("63FFFFFFFF6000526004601CF3").unwrap(); + +// let operations = [ +// vec![ +// Operation::Push((13, U256::from_big_endian(&initialization_code))), +// Operation::Push0, +// Operation::Mstore, +// ], +// create_opcodes(size, offset, value_to_transfer), +// ] +// .concat(); + +// let mut vm = new_vm_with_ops(&operations); +// vm.db.accounts.insert( +// sender_addr, +// Account::default() +// .with_balance(sender_balance) +// .with_nonce(sender_nonce), +// ); +// vm.current_call_frame_mut().msg_sender = sender_addr; - let operations = [ - vec![ - Operation::Push((13, U256::from_big_endian(&initialization_code))), - Operation::Push0, - Operation::Mstore, - ], - create_opcodes(size, offset, value_to_transfer), - ] - .concat(); +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); - let mut vm = new_vm_with_ops(&operations); - vm.db.accounts.insert( - sender_addr, - Account::default() - .with_balance(sender_balance) - .with_nonce(sender_nonce), - ); - vm.current_call_frame_mut().msg_sender = sender_addr; +// let call_frame = vm.current_call_frame_mut(); - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); +// let return_of_created_callframe = call_frame.stack.pop().unwrap(); - let call_frame = vm.current_call_frame_mut(); +// assert_eq!(return_of_created_callframe, U256::from(SUCCESS_FOR_RETURN)); - let return_of_created_callframe = call_frame.stack.pop().unwrap(); +// let returned_addr = call_frame.stack.pop().unwrap(); +// // check the created account is correct +// let new_account = vm.db.accounts.get(&word_to_address(returned_addr)).unwrap(); +// assert_eq!(new_account.balance, U256::from(value_to_transfer)); +// assert_eq!(new_account.nonce, 1); - assert_eq!(return_of_created_callframe, U256::from(SUCCESS_FOR_RETURN)); +// // Check that the sender account is updated +// let sender_account = vm.db.accounts.get_mut(&sender_addr).unwrap(); +// assert_eq!(sender_account.nonce, sender_nonce + 1); +// assert_eq!(sender_account.balance, sender_balance - value_to_transfer); - let returned_addr = call_frame.stack.pop().unwrap(); - // check the created account is correct - let new_account = vm.db.accounts.get(&word_to_address(returned_addr)).unwrap(); - assert_eq!(new_account.balance, U256::from(value_to_transfer)); - assert_eq!(new_account.nonce, 1); +// // after a happy create, we do again a create with same inputs, this should revert as we will create +// // an account with the same address +// sender_account.nonce = sender_nonce; +// let mut new_vm = new_vm_with_ops(&operations); +// new_vm.db = vm.db.clone(); +// new_vm.db.accounts = vm.db.accounts.clone(); +// new_vm.current_call_frame_mut().msg_sender = sender_addr; - // Check that the sender account is updated - let sender_account = vm.db.accounts.get_mut(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce + 1); - assert_eq!(sender_account.balance, sender_balance - value_to_transfer); - - // after a happy create, we do again a create with same inputs, this should revert as we will create - // an account with the same address - sender_account.nonce = sender_nonce; - let mut new_vm = new_vm_with_ops(&operations); - new_vm.db = vm.db.clone(); - new_vm.db.accounts = vm.db.accounts.clone(); - new_vm.current_call_frame_mut().msg_sender = sender_addr; - - let mut current_call_frame = new_vm.call_frames.pop().unwrap(); - new_vm.execute(&mut current_call_frame); - let call_frame = new_vm.current_call_frame_mut(); - let return_of_created_callframe = call_frame.stack.pop().unwrap(); - assert_eq!(return_of_created_callframe, U256::from(REVERT_FOR_CREATE)); -} +// let mut current_call_frame = new_vm.call_frames.pop().unwrap(); +// new_vm.execute(&mut current_call_frame); +// let call_frame = new_vm.current_call_frame_mut(); +// let return_of_created_callframe = call_frame.stack.pop().unwrap(); +// assert_eq!(return_of_created_callframe, U256::from(REVERT_FOR_CREATE)); +// } #[test] fn create2_happy_path() { @@ -3779,7 +3891,13 @@ fn create2_happy_path() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + let mut vm = new_vm_with_ops_addr_bal_db( + ops_to_bytecde(&operations), + sender_addr, + sender_balance, + Db::new(), + Cache::default(), + ); vm.current_call_frame_mut().msg_sender = sender_addr; let mut current_call_frame = vm.call_frames.pop().unwrap(); @@ -3791,45 +3909,48 @@ fn create2_happy_path() { let returned_addr = call_frame.stack.pop().unwrap(); assert_eq!(word_to_address(returned_addr), expected_address); // check the created account is correct - let new_account = vm.db.accounts.get(&word_to_address(returned_addr)).unwrap(); - assert_eq!(new_account.balance, U256::from(value)); - assert_eq!(new_account.nonce, 1); + let new_account = vm + .cache + .get_account(word_to_address(returned_addr)) + .unwrap(); + assert_eq!(new_account.info.balance, U256::from(value)); + assert_eq!(new_account.info.nonce, 1); // Check that the sender account is updated - let sender_account = vm.db.accounts.get(&sender_addr).unwrap(); - assert_eq!(sender_account.nonce, sender_nonce + 1); - assert_eq!(sender_account.balance, sender_balance - value); + let sender_account = vm.cache.get_account(sender_addr).unwrap(); + assert_eq!(sender_account.info.nonce, sender_nonce + 1); + assert_eq!(sender_account.info.balance, sender_balance - value); } -#[test] -fn create_on_create() { - let value_to_transfer = 10; - let offset = 19; - let size = 13; - let sender_balance = U256::from(25); - let sender_addr = Address::from_low_u64_be(40); - - // push0, push0, mstore, push1 0, push1 0, push1 0, create, push0, push0, return - let initialization_code = hex::decode("5f5f52600060006000f05f5ff3").unwrap(); - - let operations = [ - vec![ - Operation::Push((13, U256::from_big_endian(&initialization_code))), - Operation::Push0, - Operation::Mstore, - ], - create_opcodes(size, offset, value_to_transfer), - ] - .concat(); - - let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); - - vm.current_call_frame_mut().msg_sender = sender_addr; +// #[test] +// fn create_on_create() { +// let value_to_transfer = 10; +// let offset = 19; +// let size = 13; +// let sender_balance = U256::from(25); +// let sender_addr = Address::from_low_u64_be(40); + +// // push0, push0, mstore, push1 0, push1 0, push1 0, create, push0, push0, return +// let initialization_code = hex::decode("5f5f52600060006000f05f5ff3").unwrap(); + +// let operations = [ +// vec![ +// Operation::Push((13, U256::from_big_endian(&initialization_code))), +// Operation::Push0, +// Operation::Mstore, +// ], +// create_opcodes(size, offset, value_to_transfer), +// ] +// .concat(); + +// let mut vm = new_vm_with_ops_addr_bal(ops_to_bytecde(&operations), sender_addr, sender_balance); + +// vm.current_call_frame_mut().msg_sender = sender_addr; - let mut current_call_frame = vm.call_frames.pop().unwrap(); - vm.execute(&mut current_call_frame); - assert_eq!(vm.db.accounts.len(), 4); -} +// let mut current_call_frame = vm.call_frames.pop().unwrap(); +// vm.execute(&mut current_call_frame); +// assert_eq!(vm.db.accounts.len(), 4); +// } #[test] fn caller_op() { @@ -3839,32 +3960,29 @@ fn caller_op() { let operations = [Operation::Caller, Operation::Stop]; let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(caller); + let mut vm = VM::new( Some(address_that_has_the_code), - caller, - Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), + env, Default::default(), Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3884,32 +4002,29 @@ fn origin_op() { let operations = [Operation::Origin, Operation::Stop]; let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &msg_sender, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(msg_sender); + let mut vm = VM::new( Some(address_that_has_the_code), - msg_sender, - Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), + env, Default::default(), Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -3931,17 +4046,17 @@ fn balance_op() { Operation::Stop, ]; - let mut vm = new_vm_with_ops_addr_bal( + let mut vm = new_vm_with_ops_addr_bal_db( ops_to_bytecde(&operations), Address::from_low_u64_be(address), U256::from(1234), + Db::new(), + Cache::default(), ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); - dbg!(&vm); - assert_eq!( vm.current_call_frame_mut().stack.pop().unwrap(), U256::from(1234) @@ -3955,32 +4070,29 @@ fn address_op() { let operations = [Operation::Address, Operation::Stop]; let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(Address::from_low_u64_be(42)); + let mut vm = VM::new( Some(address_that_has_the_code), + env, Default::default(), Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4000,34 +4112,33 @@ fn selfbalance_op() { let operations = [Operation::SelfBalance, Operation::Stop]; let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default() .with_bytecode(ops_to_bytecde(&operations)) .with_balance(balance), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default() + .with_bytecode(ops_to_bytecde(&operations)) + .with_balance(balance), ); + let env = Environment::default_from_address(Address::from_low_u64_be(42)); + let mut vm = VM::new( Some(address_that_has_the_code), + env, Default::default(), Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4045,32 +4156,29 @@ fn callvalue_op() { let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(Address::from_low_u64_be(42)); + let mut vm = VM::new( Some(address_that_has_the_code), - Default::default(), + env, value, Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4087,32 +4195,29 @@ fn codesize_op() { let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(Address::from_low_u64_be(42)); + let mut vm = VM::new( Some(address_that_has_the_code), + env, Default::default(), Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4131,32 +4236,30 @@ fn gasprice_op() { let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let mut env = Environment::default_from_address(Address::from_low_u64_be(42)); + env.gas_price = U256::from_str_radix("9876", 16).unwrap(); + let mut vm = VM::new( Some(address_that_has_the_code), + env, Default::default(), Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - U256::from(0x9876), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4192,32 +4295,29 @@ fn codecopy_op() { let mut db = Db::default(); - db.add_account( + db.add_accounts(vec![( address_that_has_the_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), + )]); + + let mut cache = Cache::default(); + cache.add_account( + &address_that_has_the_code, + &Account::default().with_bytecode(ops_to_bytecde(&operations)), ); + let env = Environment::default_from_address(Address::from_low_u64_be(42)); + let mut vm = VM::new( Some(address_that_has_the_code), + env, Default::default(), Default::default(), - Default::default(), - U256::MAX, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - &mut db, - Default::default(), - Default::default(), - Default::default(), + Box::new(db), + cache, Default::default(), None, - ) - .unwrap(); + ); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4238,11 +4338,13 @@ fn extcodesize_existing_account() { Operation::Stop, ]; - let mut vm = new_vm_with_ops(&operations); - vm.db.add_account( + let mut db = Db::default(); + db.add_accounts(vec![( address_with_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), - ); + )]); + + let mut vm = new_vm_with_ops_db(&operations, db); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4281,11 +4383,13 @@ fn extcodecopy_existing_account() { Operation::Stop, ]; - let mut vm = new_vm_with_ops(&operations); - vm.db.add_account( + let mut db = Db::new(); + db.add_accounts(vec![( address_with_code, Account::default().with_bytecode(ops_to_bytecde(&operations)), - ); + )]); + + let mut vm = new_vm_with_ops_db(&operations, db); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); @@ -4330,11 +4434,10 @@ fn extcodehash_account_with_empty_code() { Operation::Stop, ]; - let mut vm = new_vm_with_ops(&operations); - vm.db.add_account( - address_with_code, - Account::default().with_bytecode(Bytes::new()), - ); + let mut db = Db::default(); + db.add_accounts(vec![(address_with_code, Account::default())]); + + let mut vm = new_vm_with_ops_db(&operations, db); let mut current_call_frame = vm.call_frames.pop().unwrap(); vm.execute(&mut current_call_frame); From 236c1a139a3feb781b4ba41466e23a45cf3e8f88 Mon Sep 17 00:00:00 2001 From: fmoletta <99273364+fmoletta@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:53:27 -0300 Subject: [PATCH 46/49] feat(l1): implement `TrieIterator` + `GetAccountRange` snap request handling logic (#960) **Motivation** Handling snap capability message `GetAccountRange` **Description** * Add functionality to iterate the Trie (TrieIterator) * Add functionality to iterate over all accounts in the state (Store::iter_accounts) * Add logic to handle `GetAccountRange` snap request * Fix slim encoding of `AccountState` * Remove unneeded trait `RLPEncodeSlim` **Notes** * ~We don't have the listen loop implemented so this PR only adds the standalone logic for handling the request and creating a response, we still need to plug it in to the main loop.~ * ~We are not able to run the hive test suite due to missing listen loop + old blocks being used by the test suite. I instead copied the state from a Geth execution (loading genesis + importing chain) and used that state to replicate hive tests as unit tests. These tests could be removed once we fix those two problems~ Partially addresses #184 --------- Co-authored-by: Esteban Dimitroff Hodi --- .github/workflows/hive.yaml | 3 + Makefile | 2 +- crates/common/rlp/encode.rs | 42 -- crates/common/rlp/structs.rs | 9 - crates/common/types/account.rs | 27 +- crates/networking/p2p/Cargo.toml | 1 + crates/networking/p2p/net.rs | 1 + crates/networking/p2p/rlpx/connection.rs | 11 +- crates/networking/p2p/rlpx/error.rs | 3 + crates/networking/p2p/rlpx/frame.rs | 1 - crates/networking/p2p/rlpx/message.rs | 18 + crates/networking/p2p/rlpx/snap.rs | 191 +++-- crates/networking/p2p/snap.rs | 860 +++++++++++++++++++++++ crates/networking/rpc/eth/filter.rs | 1 - crates/storage/store/storage.rs | 31 + crates/storage/trie/trie.rs | 21 +- crates/storage/trie/trie_iter.rs | 58 ++ 17 files changed, 1139 insertions(+), 141 deletions(-) create mode 100644 crates/networking/p2p/snap.rs create mode 100644 crates/storage/trie/trie_iter.rs diff --git a/.github/workflows/hive.yaml b/.github/workflows/hive.yaml index 7d3f90850..1f0fe044b 100644 --- a/.github/workflows/hive.yaml +++ b/.github/workflows/hive.yaml @@ -36,6 +36,9 @@ jobs: - simulation: discv4 name: "Devp2p discv4 tests" run_command: make run-hive SIMULATION=devp2p TEST_PATTERN="discv4" + - simulation: snap + name: "Devp2p snap tests" + run_command: make run-hive SIMULATION=devp2p TEST_PATTERN="/AccountRange" - simulation: engine name: "Engine tests" run_command: make run-hive SIMULATION=ethereum/engine TEST_PATTERN="/Blob Transactions On Block 1, Cancun Genesis|Blob Transactions On Block 1, Shanghai Genesis|Blob Transaction Ordering, Single Account, Single Blob|Blob Transaction Ordering, Single Account, Dual Blob|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|ForkchoiceUpdated Version on Payload Request" diff --git a/Makefile b/Makefile index fbc3272a1..39440f534 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ stop-localnet-silent: @kurtosis enclave stop lambdanet >/dev/null 2>&1 || true @kurtosis enclave rm lambdanet --force >/dev/null 2>&1 || true -HIVE_REVISION := ccf28e5c3e940b2bc4b4f387317ee6a46f5d15c8 +HIVE_REVISION := 421852ec25e4e608fe5460656f4bf0637649619e # Shallow clones can't specify a single revision, but at least we avoid working # the whole history by making it shallow since a given date (one day before our # target revision). diff --git a/crates/common/rlp/encode.rs b/crates/common/rlp/encode.rs index 6b93886d1..2f25755d7 100644 --- a/crates/common/rlp/encode.rs +++ b/crates/common/rlp/encode.rs @@ -29,16 +29,6 @@ pub trait RLPEncode { } } -pub trait RLPEncodeSlim { - fn encode(&self, buf: &mut dyn BufMut); - - fn length(&self) -> usize { - let mut buf = Vec::new(); - self.encode(&mut buf); - buf.len() - } -} - impl RLPEncode for bool { #[inline(always)] fn encode(&self, buf: &mut dyn BufMut) { @@ -378,38 +368,6 @@ impl RLPEncode for ethereum_types::H256 { } } -impl RLPEncodeSlim for ethereum_types::H256 { - fn encode(&self, buf: &mut dyn BufMut) { - self.as_bytes().encode(buf) - } -} - -impl RLPEncodeSlim for Vec { - fn encode(&self, buf: &mut dyn BufMut) { - if self.is_empty() { - buf.put_u8(0xc0); - } else { - let mut total_len = 0; - for item in self { - total_len += item.length(); - } - encode_length(total_len, buf); - for item in self { - item.encode(buf); - } - } - } -} - -impl RLPEncodeSlim for (S, T) { - fn encode(&self, buf: &mut dyn BufMut) { - let total_len = self.0.length() + self.1.length(); - encode_length(total_len, buf); - self.0.encode(buf); - self.1.encode(buf); - } -} - impl RLPEncode for ethereum_types::H264 { fn encode(&self, buf: &mut dyn BufMut) { self.as_bytes().encode(buf) diff --git a/crates/common/rlp/structs.rs b/crates/common/rlp/structs.rs index 207545e47..01e228e51 100644 --- a/crates/common/rlp/structs.rs +++ b/crates/common/rlp/structs.rs @@ -1,5 +1,3 @@ -use crate::encode::RLPEncodeSlim; - use super::{ decode::{decode_rlp_item, get_item_with_prefix, RLPDecode}, encode::{encode_length, RLPEncode}, @@ -185,13 +183,6 @@ impl<'a> Encoder<'a> { self } - /// Stores a field to be encoded, but in slim format - /// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#data-format - pub fn encode_slim_field(mut self, value: &T) -> Self { - ::encode(value, &mut self.temp_buf); - self - } - /// If `Some`, stores a field to be encoded, else does nothing. pub fn encode_optional_field(mut self, opt_value: &Option) -> Self { if let Some(value) = opt_value { diff --git a/crates/common/types/account.rs b/crates/common/types/account.rs index 2919999f4..cbc0a0cda 100644 --- a/crates/common/types/account.rs +++ b/crates/common/types/account.rs @@ -6,9 +6,9 @@ use ethereum_types::{H256, U256}; use sha3::{Digest as _, Keccak256}; use ethereum_rust_rlp::{ - constants::{RLP_EMPTY_LIST, RLP_NULL}, + constants::RLP_NULL, decode::RLPDecode, - encode::{RLPEncode, RLPEncodeSlim}, + encode::RLPEncode, error::RLPDecodeError, structs::{Decoder, Encoder}, }; @@ -100,17 +100,6 @@ impl RLPEncode for AccountInfo { } } -impl RLPEncodeSlim for AccountInfo { - fn encode(&self, buf: &mut dyn bytes::BufMut) { - // TODO: check if it's okay to use RLP_EMPTY_LIST - Encoder::new(buf) - .encode_field(&RLP_EMPTY_LIST) - .encode_field(&self.balance) - .encode_field(&self.nonce) - .finish(); - } -} - impl RLPDecode for AccountInfo { fn decode_unfinished(rlp: &[u8]) -> Result<(AccountInfo, &[u8]), RLPDecodeError> { let decoder = Decoder::new(rlp)?; @@ -137,18 +126,6 @@ impl RLPEncode for AccountState { } } -impl RLPEncodeSlim for AccountState { - fn encode(&self, buf: &mut dyn bytes::BufMut) { - // TODO: check if it's okay to use RLP_EMPTY_LIST - Encoder::new(buf) - .encode_field(&self.nonce) - .encode_field(&self.balance) - .encode_field(&RLP_EMPTY_LIST) - .encode_field(&self.code_hash) - .finish(); - } -} - impl RLPDecode for AccountState { fn decode_unfinished(rlp: &[u8]) -> Result<(AccountState, &[u8]), RLPDecodeError> { let decoder = Decoder::new(rlp)?; diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index 53e6f0dba..128cff7e7 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -15,6 +15,7 @@ tokio.workspace = true bytes.workspace = true hex.workspace = true thiserror.workspace = true +lazy_static.workspace = true k256 = { version = "0.13.3", features = ["ecdh"] } sha3 = "0.10.8" diff --git a/crates/networking/p2p/net.rs b/crates/networking/p2p/net.rs index fd512c138..dc4937dac 100644 --- a/crates/networking/p2p/net.rs +++ b/crates/networking/p2p/net.rs @@ -31,6 +31,7 @@ pub mod bootnode; pub(crate) mod discv4; pub(crate) mod kademlia; pub mod rlpx; +pub(crate) mod snap; pub mod types; const MAX_DISC_PACKET_SIZE: usize = 1280; diff --git a/crates/networking/p2p/rlpx/connection.rs b/crates/networking/p2p/rlpx/connection.rs index 642535904..22b7389d4 100644 --- a/crates/networking/p2p/rlpx/connection.rs +++ b/crates/networking/p2p/rlpx/connection.rs @@ -1,5 +1,6 @@ use crate::{ rlpx::{eth::backend, handshake::encode_ack_message, message::Message, p2p, utils::id2pubkey}, + snap::process_account_range_request, MAX_DISC_PACKET_SIZE, }; @@ -25,9 +26,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tracing::{error, info}; const CAP_P2P: (Capability, u8) = (Capability::P2p, 5); const CAP_ETH: (Capability, u8) = (Capability::Eth, 68); -//const CAP_SNAP: (Capability, u8) = (Capability::Snap, 1); -const SUPPORTED_CAPABILITIES: [(Capability, u8); 2] = [CAP_P2P, CAP_ETH]; -// pub const SUPPORTED_CAPABILITIES: [(&str, u8); 3] = [CAP_P2P, CAP_ETH, CAP_SNAP)]; +const CAP_SNAP: (Capability, u8) = (Capability::Snap, 1); +const SUPPORTED_CAPABILITIES: [(Capability, u8); 3] = [CAP_P2P, CAP_ETH, CAP_SNAP]; pub(crate) type Aes256Ctr64BE = ctr::Ctr64BE; @@ -145,6 +145,11 @@ impl RLPxConnection { Message::Ping(_) => info!("Received Ping"), Message::Pong(_) => info!("Received Pong"), Message::Status(_) => info!("Received Status"), + Message::GetAccountRange(req) => { + let response = + process_account_range_request(req, self.storage.clone())?; + self.send(Message::AccountRange(response)).await + } // TODO: Add new message types and handlers as they are implemented message => return Err(RLPxError::UnexpectedMessage(message)), }; diff --git a/crates/networking/p2p/rlpx/error.rs b/crates/networking/p2p/rlpx/error.rs index 4177ea10f..83b158762 100644 --- a/crates/networking/p2p/rlpx/error.rs +++ b/crates/networking/p2p/rlpx/error.rs @@ -1,4 +1,5 @@ use crate::rlpx::message::Message; +use ethereum_rust_storage::error::StoreError; use thiserror::Error; // TODO improve errors @@ -10,4 +11,6 @@ pub(crate) enum RLPxError { InvalidState(String), #[error("Unexpected message: {0}")] UnexpectedMessage(Message), + #[error(transparent)] + Store(#[from] StoreError), } diff --git a/crates/networking/p2p/rlpx/frame.rs b/crates/networking/p2p/rlpx/frame.rs index d2b007704..9c5c8d266 100644 --- a/crates/networking/p2p/rlpx/frame.rs +++ b/crates/networking/p2p/rlpx/frame.rs @@ -65,7 +65,6 @@ pub(crate) async fn write( }; state.egress_mac.update(frame_mac_seed); let frame_mac = state.egress_mac.clone().finalize(); - // Send frame-mac stream.write_all(&frame_mac[..16]).await.unwrap(); } diff --git a/crates/networking/p2p/rlpx/message.rs b/crates/networking/p2p/rlpx/message.rs index 8f06159be..2d5edffa3 100644 --- a/crates/networking/p2p/rlpx/message.rs +++ b/crates/networking/p2p/rlpx/message.rs @@ -4,6 +4,9 @@ use std::fmt::Display; use super::eth::status::StatusMessage; use super::p2p::{DisconnectMessage, HelloMessage, PingMessage, PongMessage}; +use super::snap::{AccountRange, GetAccountRange}; + +use ethereum_rust_rlp::encode::RLPEncode; pub trait RLPxMessage: Sized { fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError>; @@ -17,6 +20,9 @@ pub(crate) enum Message { Ping(PingMessage), Pong(PongMessage), Status(StatusMessage), + // snap capability + GetAccountRange(GetAccountRange), + AccountRange(AccountRange), } impl Message { @@ -27,6 +33,8 @@ impl Message { 0x02 => Ok(Message::Ping(PingMessage::decode(msg_data)?)), 0x03 => Ok(Message::Pong(PongMessage::decode(msg_data)?)), 0x10 => Ok(Message::Status(StatusMessage::decode(msg_data)?)), + 0x21 => Ok(Message::GetAccountRange(GetAccountRange::decode(msg_data)?)), + 0x22 => Ok(Message::AccountRange(AccountRange::decode(msg_data)?)), _ => Err(RLPDecodeError::MalformedData), } } @@ -38,6 +46,14 @@ impl Message { Message::Ping(msg) => msg.encode(buf), Message::Pong(msg) => msg.encode(buf), Message::Status(msg) => msg.encode(buf), + Message::GetAccountRange(msg) => { + 0x21_u8.encode(buf); + msg.encode(buf) + } + Message::AccountRange(msg) => { + 0x22_u8.encode(buf); + msg.encode(buf) + } } } } @@ -50,6 +66,8 @@ impl Display for Message { Message::Ping(_) => "p2p:Ping".fmt(f), Message::Pong(_) => "p2p:Pong".fmt(f), Message::Status(_) => "eth:Status".fmt(f), + Message::GetAccountRange(_) => "snap:GetAccountRange".fmt(f), + Message::AccountRange(_) => "snap:AccountRange".fmt(f), } } } diff --git a/crates/networking/p2p/rlpx/snap.rs b/crates/networking/p2p/rlpx/snap.rs index cdfafd1b6..12007ac82 100644 --- a/crates/networking/p2p/rlpx/snap.rs +++ b/crates/networking/p2p/rlpx/snap.rs @@ -1,6 +1,11 @@ -use bytes::BufMut; -use ethereum_rust_core::{types::AccountState, H256}; +use bytes::{BufMut, Bytes}; +use ethereum_rust_core::{ + types::{AccountState, EMPTY_KECCACK_HASH, EMPTY_TRIE_HASH}, + H256, U256, +}; use ethereum_rust_rlp::{ + decode::RLPDecode, + encode::RLPEncode, error::{RLPDecodeError, RLPEncodeError}, structs::{Decoder, Encoder}, }; @@ -8,34 +13,24 @@ use snap::raw::Decoder as SnappyDecoder; use super::{message::RLPxMessage, utils::snappy_encode}; -// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getaccountrange-0x00 +// Snap Capability Messages + #[derive(Debug)] pub(crate) struct GetAccountRange { // id is a u64 chosen by the requesting peer, the responding peer must mirror the value for the response - // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#protocol-messages - id: u64, - root_hash: H256, - starting_hash: H256, - limit_hash: H256, - response_bytes: u64, + pub id: u64, + pub root_hash: H256, + pub starting_hash: H256, + pub limit_hash: H256, + pub response_bytes: u64, } -impl GetAccountRange { - pub fn new( - id: u64, - root_hash: H256, - starting_hash: H256, - limit_hash: H256, - response_bytes: u64, - ) -> Self { - Self { - id, - root_hash, - starting_hash, - limit_hash, - response_bytes, - } - } +#[derive(Debug)] +pub(crate) struct AccountRange { + // id is a u64 chosen by the requesting peer, the responding peer must mirror the value for the response + pub id: u64, + pub accounts: Vec, + pub proof: Vec, } impl RLPxMessage for GetAccountRange { @@ -66,33 +61,13 @@ impl RLPxMessage for GetAccountRange { let (limit_hash, decoder): (H256, _) = decoder.decode_field("limitHash")?; let (response_bytes, _): (u64, _) = decoder.decode_field("responseBytes")?; - Ok(Self::new( + Ok(Self { id, root_hash, starting_hash, limit_hash, response_bytes, - )) - } -} - -// https://github.com/ethereum/devp2p/blob/master/caps/snap.md#accountrange-0x01 -#[derive(Debug)] -pub(crate) struct AccountRange { - // id is a u64 chosen by the requesting peer, the responding peer must mirror the value for the response - // https://github.com/ethereum/devp2p/blob/master/caps/eth.md#protocol-messages - id: u64, - accounts: Vec<(H256, AccountState)>, - proof: Vec, -} - -impl AccountRange { - pub fn new(id: u64, accounts: Vec<(H256, AccountState)>, proof: Vec) -> Self { - Self { - id, - accounts, - proof, - } + }) } } @@ -101,7 +76,7 @@ impl RLPxMessage for AccountRange { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) .encode_field(&self.id) - .encode_slim_field(&self.accounts) + .encode_field(&self.accounts) .encode_field(&self.proof) .finish(); @@ -116,11 +91,121 @@ impl RLPxMessage for AccountRange { .decompress_vec(msg_data) .map_err(|e| RLPDecodeError::Custom(e.to_string()))?; let decoder = Decoder::new(&decompressed_data)?; - let (id, decoder): (u64, _) = decoder.decode_field("request-id")?; - let (accounts, decoder): (Vec<(H256, AccountState)>, _) = - decoder.decode_field("accounts")?; - let (proof, _): (Vec, _) = decoder.decode_field("proof")?; + let (id, decoder) = decoder.decode_field("request-id")?; + let (accounts, decoder) = decoder.decode_field("accounts")?; + let (proof, decoder) = decoder.decode_field("proof")?; + decoder.finish()?; - Ok(Self::new(id, accounts, proof)) + Ok(Self { + id, + accounts, + proof, + }) + } +} + +// Intermediate structures + +#[derive(Debug)] +pub struct AccountRangeUnit { + pub hash: H256, + pub account: AccountStateSlim, +} + +#[derive(Debug)] +pub struct AccountStateSlim { + pub nonce: u64, + pub balance: U256, + pub storage_root: Bytes, + pub code_hash: Bytes, +} + +impl RLPEncode for AccountRangeUnit { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.hash) + .encode_field(&self.account) + .finish(); + } +} + +impl RLPDecode for AccountRangeUnit { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (hash, decoder) = decoder.decode_field("hash")?; + let (account, decoder) = decoder.decode_field("account")?; + Ok((Self { hash, account }, decoder.finish()?)) + } +} + +impl RLPEncode for AccountStateSlim { + fn encode(&self, buf: &mut dyn BufMut) { + Encoder::new(buf) + .encode_field(&self.nonce) + .encode_field(&self.balance) + .encode_field(&self.storage_root) + .encode_field(&self.code_hash) + .finish(); + } +} + +impl RLPDecode for AccountStateSlim { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + let (nonce, decoder) = decoder.decode_field("nonce")?; + let (balance, decoder) = decoder.decode_field("balance")?; + let (storage_root, decoder) = decoder.decode_field("storage_root")?; + let (code_hash, decoder) = decoder.decode_field("code_hash")?; + Ok(( + Self { + nonce, + balance, + storage_root, + code_hash, + }, + decoder.finish()?, + )) + } +} + +impl From for AccountStateSlim { + fn from(value: AccountState) -> Self { + let storage_root = if value.storage_root == *EMPTY_TRIE_HASH { + Bytes::new() + } else { + Bytes::copy_from_slice(value.storage_root.as_bytes()) + }; + let code_hash = if value.code_hash == *EMPTY_KECCACK_HASH { + Bytes::new() + } else { + Bytes::copy_from_slice(value.code_hash.as_bytes()) + }; + Self { + nonce: value.nonce, + balance: value.balance, + storage_root, + code_hash, + } + } +} + +impl From for AccountState { + fn from(value: AccountStateSlim) -> Self { + let storage_root = if value.storage_root.is_empty() { + *EMPTY_TRIE_HASH + } else { + H256::from_slice(value.storage_root.as_ref()) + }; + let code_hash = if value.code_hash.is_empty() { + *EMPTY_KECCACK_HASH + } else { + H256::from_slice(value.code_hash.as_ref()) + }; + Self { + nonce: value.nonce, + balance: value.balance, + storage_root, + code_hash, + } } } diff --git a/crates/networking/p2p/snap.rs b/crates/networking/p2p/snap.rs new file mode 100644 index 000000000..2c40620cd --- /dev/null +++ b/crates/networking/p2p/snap.rs @@ -0,0 +1,860 @@ +use bytes::Bytes; +use ethereum_rust_rlp::encode::RLPEncode; +use ethereum_rust_storage::{error::StoreError, Store}; + +use crate::rlpx::snap::{AccountRange, AccountRangeUnit, AccountStateSlim, GetAccountRange}; + +pub fn process_account_range_request( + request: GetAccountRange, + store: Store, +) -> Result { + let mut accounts = vec![]; + let mut bytes_used = 0; + for (hash, account) in store.iter_accounts(request.root_hash) { + if hash >= request.starting_hash { + let account = AccountStateSlim::from(account); + bytes_used += 32 + account.length() as u64; + accounts.push(AccountRangeUnit { hash, account }); + } + if hash >= request.limit_hash || bytes_used >= request.response_bytes { + break; + } + } + let proof = store + .get_account_range_proof( + request.root_hash, + request.starting_hash, + accounts.last().map(|acc| acc.hash), + )? + .iter() + .map(|bytes| Bytes::copy_from_slice(bytes)) + .collect(); + Ok(AccountRange { + id: request.id, + accounts, + proof, + }) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use ethereum_rust_core::{types::AccountState, BigEndianHash, H256}; + use ethereum_rust_rlp::{decode::RLPDecode, encode::RLPEncode}; + use ethereum_rust_storage::EngineType; + + use crate::rlpx::snap::AccountStateSlim; + + use super::*; + + // Hive `AccounRange` Tests + // Requests & invariantes taken from https://github.com/ethereum/go-ethereum/blob/3e567b8b2901611f004b5a6070a9b6d286be128d/cmd/devp2p/internal/ethtest/snap.go#L69 + + use lazy_static::lazy_static; + + lazy_static! { + // Constant values for hive `AccountRange` tests + static ref HASH_MIN: H256 = H256::zero(); + static ref HASH_MAX: H256 = + H256::from_str("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",) + .unwrap(); + static ref HASH_FIRST: H256 = + H256::from_str("0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6") + .unwrap(); + static ref HASH_SECOND: H256 = + H256::from_str("0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f") + .unwrap(); + static ref HASH_FIRST_MINUS_500: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 500)); + static ref HASH_FIRST_MINUS_450: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 450)); + static ref HASH_FIRST_MINUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 1)); + static ref HASH_FIRST_PLUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() + 1)); + } + + #[test] + fn hive_account_range_a() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") + .unwrap() + ); + } + + #[test] + fn hive_account_range_b() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 3000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 65); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6") + .unwrap() + ); + } + + #[test] + fn hive_account_range_c() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 2000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 44); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595") + .unwrap() + ); + } + + #[test] + fn hive_account_range_d() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 1, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + #[test] + fn hive_account_range_e() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 0, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + #[test] + fn hive_account_range_f() { + // In this test, we request a range where startingHash is before the first available + // account key, and limitHash is after. The server should return the first and second + // account of the state (because the second account is the 'next available'). + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_MINUS_500, + limit_hash: *HASH_FIRST_PLUS_ONE, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 2); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_SECOND); + } + + #[test] + fn hive_account_range_g() { + // Here we request range where both bounds are before the first available account key. + // This should return the first account (even though it's out of bounds). + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_MINUS_500, + limit_hash: *HASH_FIRST_MINUS_450, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + #[test] + fn hive_account_range_h() { + // In this test, both startingHash and limitHash are zero. + // The server should return the first available account. + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MIN, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + #[test] + fn hive_account_range_i() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") + .unwrap() + ); + } + + #[test] + fn hive_account_range_j() { + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_PLUS_ONE, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_SECOND); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa") + .unwrap() + ); + } + + // Tests for different roots skipped (we don't have other state's data loaded) + + // Non-sensical requests + + #[test] + fn hive_account_range_k() { + // In this test, the startingHash is the first available key, and limitHash is + // a key before startingHash (wrong order). The server should return the first available key. + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_FIRST_MINUS_ONE, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + #[test] + fn hive_account_range_m() { + // In this test, the startingHash is the first available key and limitHash is zero. + // (wrong order). The server should return the first available key. + let (store, root) = setup_initial_state(); + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_MIN, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + } + + // Initial state setup for hive snap tests + + fn setup_initial_state() -> (Store, H256) { + // We cannot process the old blocks that hive uses for the devp2p snap tests + // So I copied the state from a geth execution of the test suite + + // State was trimmed to only the first 100 accounts (as the furthest account used by the tests is account 87) + // If the full 408 account state is needed check out previous commits the PR that added this code + + let accounts: Vec<(&str, Vec)> = vec![ + ( + "0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6", + vec![ + 228_u8, 1, 128, 160, 223, 151, 249, 75, 196, 116, 113, 135, 6, 6, 246, 38, 251, + 122, 11, 66, 238, 210, 212, 95, 204, 132, 220, 18, 0, 206, 98, 247, 131, 29, + 169, 144, 128, + ], + ), + ( + "0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f", + vec![196, 128, 1, 128, 128], + ), + ( + "0x00aa781aff39a8284ef43790e3a511b2caa50803613c5096bc782e8de08fa4c5", + vec![ + 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, + 128, 128, + ], + ), + ( + "0x016d92531f4754834b0502de5b0342ceff21cde5bef386a83d2292f4445782c2", + vec![196, 128, 1, 128, 128], + ), + ( + "0x02547b56492bfe767f3d18be2aab96441c449cd945770ef7ef8555acc505b2e4", + vec![196, 128, 1, 128, 128], + ), + ( + "0x025f478d53bf78add6fa3708d9e061d59bfe14b21329b2a4cf1156d4f81b3d2d", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0267c643f67b47cac9efacf6fcf0e4f4e1b273a727ded155db60eb9907939eb6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0304d8eaccf0b942c468074250cbcb625ec5c4688b6b5d17d2a9bdd8dd565d5a", + vec![ + 228, 1, 128, 160, 224, 12, 73, 166, 88, 73, 208, 92, 191, 39, 164, 215, 120, + 138, 104, 188, 123, 96, 19, 174, 51, 65, 29, 64, 188, 137, 40, 47, 192, 100, + 243, 61, 128, + ], + ), + ( + "0x0463e52cda557221b0b66bd7285b043071df4c2ab146260f4e010970f3a0cccf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x04d9aa4f67f8b24d70a0ffd757e82456d9184113106b7d9e8eb6c3e8a8df27ee", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x053df2c3b574026812b154a99b13b626220af85cd01bb1693b1d42591054bce6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0579e46a5ed8a88504ac7d579b12eb346fbe4fd7e281bdd226b891f8abed4789", + vec![ + 228, 1, 128, 160, 61, 14, 43, 165, 55, 243, 89, 65, 6, 135, 9, 69, 15, 37, 254, + 228, 90, 175, 77, 198, 174, 46, 210, 42, 209, 46, 7, 67, 172, 124, 84, 167, + 128, + ], + ), + ( + "0x05f6de281d8c2b5d98e8e01cd529bd76416b248caf11e0552047c5f1d516aab6", + vec![196, 128, 1, 128, 128], + ), + ( + "0x07b49045c401bcc408f983d91a199c908cdf0d646049b5b83629a70b0117e295", + vec![ + 228, 1, 128, 160, 134, 154, 203, 146, 159, 89, 28, 84, 203, 133, 132, 42, 81, + 242, 150, 99, 94, 125, 137, 87, 152, 197, 71, 162, 147, 175, 228, 62, 123, 247, + 244, 23, 128, + ], + ), + ( + "0x0993fd5b750fe4414f93c7880b89744abb96f7af1171ed5f47026bdf01df1874", + vec![196, 128, 1, 128, 128], + ), + ( + "0x099d5081762b8b265e8ba4cd8e43f08be4715d903a0b1d96b3d9c4e811cbfb33", + vec![196, 128, 1, 128, 128], + ), + ( + "0x09d6e6745d272389182a510994e2b54d14b731fac96b9c9ef434bc1924315371", + vec![196, 128, 128, 128, 128], + ), + ( + "0x0a93a7231976ad485379a3b66c2d8983ba0b2ca87abaf0ca44836b2a06a2b102", + vec![196, 128, 1, 128, 128], + ), + ( + "0x0b564e4a0203cbcec8301709a7449e2e7371910778df64c89f48507390f2d129", + vec![196, 1, 128, 128, 128], + ), + ( + "0x0cd2a7c53c76f228ed3aa7a29644b1915fde9ec22e0433808bf5467d914e7c7a", + vec![ + 228, 1, 128, 160, 7, 84, 3, 90, 164, 7, 51, 129, 162, 17, 52, 43, 80, 125, 232, + 231, 117, 201, 124, 150, 16, 150, 230, 226, 39, 93, 240, 191, 203, 179, 160, + 28, 128, + ], + ), + ( + "0x0e0e4646090b881949ec9991e48dec768ccd1980896aefd0d51fd56fd5689790", + vec![ + 228, 1, 128, 160, 96, 252, 105, 16, 13, 142, 99, 38, 103, 200, 11, 148, 212, + 52, 0, 136, 35, 237, 117, 65, 107, 113, 203, 209, 18, 180, 208, 176, 47, 86, + 48, 39, 128, + ], + ), + ( + "0x0e27113c09de0a0cb0ff268c677aba17d39a3190fe15aec0ff7f54184955cba4", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0e57ffa6cc6cbd96c1400150417dd9b30d958c58f63c36230a90a02b076f78b5", + vec![196, 128, 1, 128, 128], + ), + ( + "0x0f30822f90f33f1d1ba6d1521a00935630d2c81ab12fa03d4a0f4915033134f3", + vec![ + 228, 1, 128, 160, 128, 120, 243, 37, 157, 129, 153, 183, 202, 57, 213, 30, 53, + 213, 181, 141, 113, 255, 20, 134, 6, 115, 16, 96, 56, 109, 50, 60, 93, 25, 24, + 44, 128, + ], + ), + ( + "0x1017b10a7cc3732d729fe1f71ced25e5b7bc73dc62ca61309a8c7e5ac0af2f72", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1098f06082dc467088ecedb143f9464ebb02f19dc10bd7491b03ba68d751ce45", + vec![196, 1, 128, 128, 128], + ), + ( + "0x11eb0304c1baa92e67239f6947cb93e485a7db05e2b477e1167a8960458fa8cc", + vec![196, 1, 128, 128, 128], + ), + ( + "0x12be3bf1f9b1dab5f908ca964115bee3bcff5371f84ede45bc60591b21117c51", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x12c1bb3dddf0f06f62d70ed5b7f7db7d89b591b3f23a838062631c4809c37196", + vec![196, 128, 1, 128, 128], + ), + ( + "0x12e394ad62e51261b4b95c431496e46a39055d7ada7dbf243f938b6d79054630", + vec![196, 1, 128, 128, 128], + ), + ( + "0x13cfc46f6bdb7a1c30448d41880d061c3b8d36c55a29f1c0c8d95a8e882b8c25", + vec![ + 228, 1, 128, 160, 148, 79, 9, 90, 251, 209, 56, 62, 93, 15, 145, 239, 2, 137, + 93, 57, 143, 79, 118, 253, 182, 216, 106, 223, 71, 101, 242, 91, 220, 48, 79, + 95, 128, + ], + ), + ( + "0x15293aec87177f6c88f58bc51274ba75f1331f5cb94f0c973b1deab8b3524dfe", + vec![196, 128, 1, 128, 128], + ), + ( + "0x170c927130fe8f1db3ae682c22b57f33f54eb987a7902ec251fe5dba358a2b25", + vec![196, 128, 1, 128, 128], + ), + ( + "0x17350c7adae7f08d7bbb8befcc97234462831638443cd6dfea186cbf5a08b7c7", + vec![ + 228, 1, 128, 160, 76, 231, 156, 217, 100, 86, 80, 240, 160, 14, 255, 168, 111, + 111, 234, 115, 60, 236, 234, 158, 162, 105, 100, 130, 143, 242, 92, 240, 87, + 123, 201, 116, 128, + ], + ), + ( + "0x174f1a19ff1d9ef72d0988653f31074cb59e2cf37cd9d2992c7b0dd3d77d84f9", + vec![196, 128, 1, 128, 128], + ), + ( + "0x17984cc4b4aac0492699d37662b53ec2acf8cbe540c968b817061e4ed27026d0", + vec![196, 128, 1, 128, 128], + ), + ( + "0x181abdd5e212171007e085fdc284a84d42d5bfc160960d881ccb6a10005ff089", + vec![196, 1, 128, 128, 128], + ), + ( + "0x188111c233bf6516bb9da8b5c4c31809a42e8604cd0158d933435cfd8e06e413", + vec![196, 1, 128, 128, 128], + ), + ( + "0x18f4256a59e1b2e01e96ac465e1d14a45d789ce49728f42082289fc25cf32b8d", + vec![196, 128, 1, 128, 128], + ), + ( + "0x1960414a11f8896c7fc4243aba7ed8179b0bc6979b7c25da7557b17f5dee7bf7", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1a28912018f78f7e754df6b9fcec33bea25e5a232224db622e0c3343cf079eff", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1bf7626cec5330a127e439e68e6ee1a1537e73b2de1aa6d6f7e06bc0f1e9d763", + vec![196, 128, 1, 128, 128], + ), + ( + "0x1c248f110218eaae2feb51bc82e9dcc2844bf93b88172c52afcb86383d262323", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595", + vec![ + 228, 1, 128, 160, 175, 134, 126, 108, 186, 232, 16, 202, 169, 36, 184, 182, + 172, 61, 140, 8, 145, 131, 20, 145, 166, 144, 109, 208, 190, 122, 211, 36, 220, + 209, 83, 61, 128, + ], + ), + ( + "0x1d38ada74301c31f3fd7d92dd5ce52dc37ae633e82ac29c4ef18dfc141298e26", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1d6ee979097e29141ad6b97ae19bb592420652b7000003c55eb52d5225c3307d", + vec![ + 228, 1, 128, 160, 247, 53, 145, 231, 145, 175, 76, 124, 95, 160, 57, 195, 61, + 217, 209, 105, 202, 177, 75, 29, 155, 12, 167, 139, 204, 78, 116, 13, 85, 59, + 26, 207, 128, + ], + ), + ( + "0x1dff76635b74ddba16bba3054cc568eed2571ea6becaabd0592b980463f157e2", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1ee7e0292fba90d9733f619f976a2655c484adb30135ef0c5153b5a2f32169df", + vec![196, 1, 128, 128, 128], + ), + ( + "0x209b102e507b8dfc6acfe2cf55f4133b9209357af679a6d507e6ee87112bfe10", + vec![196, 1, 128, 128, 128], + ), + ( + "0x210ce6d692a21d75de3764b6c0356c63a51550ebec2c01f56c154c24b1cf8888", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2116ab29b4cb8547af547fe472b7ce30713f234ed49cb1801ea6d3cf9c796d57", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x2290ea88cc63f09ab5e8c989a67e2e06613311801e39c84aae3badd8bb38409c", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x2369a492b6cddcc0218617a060b40df0e7dda26abe48ba4e4108c532d3f2b84f", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2374954008440ca3d17b1472d34cc52a6493a94fb490d5fb427184d7d5fd1cbf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x23ddaac09188c12e5d88009afa4a34041175c5531f45be53f1560a1cbfec4e8a", + vec![ + 228, 1, 128, 160, 71, 250, 72, 226, 93, 54, 105, 169, 187, 25, 12, 89, 147, + 143, 75, 228, 157, 226, 208, 131, 105, 110, 185, 57, 195, 180, 7, 46, 198, 126, + 67, 177, 128, + ], + ), + ( + "0x246cc8a2b79a30ec71390d829d0cb37cce1b953e89cb14deae4945526714a71c", + vec![196, 128, 1, 128, 128], + ), + ( + "0x255ec86eac03ba59f6dfcaa02128adbb22c561ae0c49e9e62e4fff363750626e", + vec![ + 228, 1, 128, 160, 102, 235, 22, 7, 27, 163, 121, 191, 12, 99, 47, 203, 82, 249, + 23, 90, 101, 107, 239, 98, 173, 240, 190, 245, 52, 154, 127, 90, 106, 173, 93, + 136, 128, + ], + ), + ( + "0x26ce7d83dfb0ab0e7f15c42aeb9e8c0c5dba538b07c8e64b35fb64a37267dd96", + vec![ + 228, 1, 128, 160, 36, 52, 191, 198, 67, 236, 54, 65, 22, 205, 113, 81, 154, 57, + 118, 98, 178, 12, 82, 209, 173, 207, 240, 184, 48, 232, 10, 115, 142, 25, 243, + 14, 128, + ], + ), + ( + "0x2705244734f69af78e16c74784e1dc921cb8b6a98fe76f577cc441c831e973bf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x28f25652ec67d8df6a2e33730e5d0983443e3f759792a0128c06756e8eb6c37f", + vec![ + 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, + 128, 128, + ], + ), + ( + "0x2a248c1755e977920284c8054fceeb20530dc07cd8bbe876f3ce02000818cc3a", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2a39afbe88f572c23c90da2d059af3de125f1da5c3753c530dc5619a4857119f", + vec![ + 228, 1, 128, 160, 130, 137, 181, 88, 134, 95, 44, 161, 245, 76, 152, 181, 255, + 93, 249, 95, 7, 194, 78, 198, 5, 226, 71, 181, 140, 119, 152, 96, 93, 205, 121, + 79, 128, + ], + ), + ( + "0x2b8d12301a8af18405b3c826b6edcc60e8e034810f00716ca48bebb84c4ce7ab", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2baa718b760c0cbd0ec40a3c6df7f2948b40ba096e6e4b116b636f0cca023bde", + vec![196, 128, 1, 128, 128], + ), + ( + "0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6", + vec![ + 228, 128, 128, 128, 160, 142, 3, 136, 236, 246, 76, 250, 118, 179, 166, 175, + 21, 159, 119, 69, 21, 25, 167, 249, 187, 134, 46, 76, 206, 36, 23, 92, 121, 31, + 220, 176, 223, + ], + ), + ( + "0x2fe5767f605b7b821675b223a22e4e5055154f75e7f3041fdffaa02e4787fab8", + vec![196, 128, 1, 128, 128], + ), + ( + "0x303f57a0355c50bf1a0e1cf0fa8f9bdbc8d443b70f2ad93ac1c6b9c1d1fe29a2", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x30ce5b7591126d5464dfb4fc576a970b1368475ce097e244132b06d8cc8ccffe", + vec![196, 128, 1, 128, 128], + ), + ( + "0x315ccc15883d06b4e743f8252c999bf1ee994583ff6114d89c0f3ddee828302b", + vec![196, 1, 128, 128, 128], + ), + ( + "0x3197690074092fe51694bdb96aaab9ae94dac87f129785e498ab171a363d3b40", + vec![196, 128, 1, 128, 128], + ), + ( + "0x34a715e08b77afd68cde30b62e222542f3db90758370400c94d0563959a1d1a0", + vec![ + 228, 1, 128, 160, 79, 68, 99, 41, 181, 238, 61, 19, 212, 246, 181, 229, 242, + 16, 221, 194, 217, 15, 237, 186, 56, 75, 149, 14, 54, 161, 209, 154, 249, 92, + 92, 177, 128, + ], + ), + ( + "0x37310559ceaade42e45b3e3f05925aadca9e60aeeb9dd60d824875d9e9e71e26", + vec![ + 228, 1, 128, 160, 114, 200, 146, 33, 218, 237, 204, 221, 63, 187, 166, 108, 27, + 8, 27, 54, 52, 206, 137, 213, 160, 105, 190, 151, 255, 120, 50, 119, 143, 123, + 2, 58, 128, + ], + ), + ( + "0x37d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42", + vec![ + 248, 68, 128, 42, 160, 172, 49, 98, 168, 185, 219, 180, 49, 139, 132, 33, 159, + 49, 64, 231, 169, 236, 53, 18, 98, 52, 18, 2, 151, 221, 225, 15, 81, 178, 95, + 106, 38, 160, 245, 122, 205, 64, 37, 152, 114, 96, 109, 118, 25, 126, 240, 82, + 243, 211, 85, 136, 218, 223, 145, 158, 225, 240, 227, 203, 155, 98, 211, 244, + 176, 44, + ], + ), + ( + "0x37ddfcbcb4b2498578f90e0fcfef9965dcde4d4dfabe2f2836d2257faa169947", + vec![ + 228, 1, 128, 160, 82, 214, 210, 145, 58, 228, 75, 202, 17, 181, 161, 22, 2, 29, + 185, 124, 145, 161, 62, 56, 94, 212, 139, 160, 102, 40, 231, 66, 1, 35, 29, + 186, 128, + ], + ), + ( + "0x37e51740ad994839549a56ef8606d71ace79adc5f55c988958d1c450eea5ac2d", + vec![196, 1, 128, 128, 128], + ), + ( + "0x38152bce526b7e1c2bedfc9d297250fcead02818be7806638564377af145103b", + vec![ + 228, 1, 128, 160, 108, 0, 224, 145, 218, 227, 212, 34, 111, 172, 214, 190, 128, + 44, 134, 93, 93, 176, 245, 36, 117, 77, 34, 102, 100, 6, 19, 139, 84, 250, 176, + 230, 128, + ], + ), + ( + "0x3848b7da914222540b71e398081d04e3849d2ee0d328168a3cc173a1cd4e783b", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x389093badcaa24c3a8cbb4461f262fba44c4f178a162664087924e85f3d55710", + vec![196, 1, 128, 128, 128], + ), + ( + "0x3897cb9b6f68765022f3c74f84a9f2833132858f661f4bc91ccd7a98f4e5b1ee", + vec![196, 1, 128, 128, 128], + ), + ( + "0x395b92f75f8e06b5378a84ba03379f025d785d8b626b2b6a1c84b718244b9a91", + vec![ + 228, 1, 128, 160, 84, 70, 184, 24, 244, 198, 105, 102, 156, 211, 49, 71, 38, + 255, 19, 76, 241, 140, 88, 169, 165, 54, 223, 19, 199, 0, 97, 7, 5, 168, 183, + 200, 128, + ], + ), + ( + "0x3be526914a7d688e00adca06a0c47c580cb7aa934115ca26006a1ed5455dd2ce", + vec![196, 128, 1, 128, 128], + ), + ( + "0x3e57e37bc3f588c244ffe4da1f48a360fa540b77c92f0c76919ec4ee22b63599", + vec![196, 128, 1, 128, 128], + ), + ( + "0x415ded122ff7b6fe5862f5c443ea0375e372862b9001c5fe527d276a3a420280", + vec![196, 1, 128, 128, 128], + ), + ( + "0x419809ad1512ed1ab3fb570f98ceb2f1d1b5dea39578583cd2b03e9378bbe418", + vec![196, 1, 128, 128, 128], + ), + ( + "0x4363d332a0d4df8582a84932729892387c623fe1ec42e2cfcbe85c183ed98e0e", + vec![ + 213, 130, 1, 146, 143, 192, 151, 206, 123, 201, 7, 21, 179, 73, 233, 122, 138, + 101, 46, 31, 128, 128, + ], + ), + ( + "0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099", + vec![ + 228, 1, 1, 160, 190, 61, 117, 161, 114, 155, 225, 87, 231, 156, 59, 119, 240, + 2, 6, 219, 77, 84, 227, 234, 20, 55, 90, 1, 84, 81, 200, 142, 192, 103, 199, + 144, 128, + ], + ), + ( + "0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa", + vec![196, 1, 128, 128, 128], + ), + ( + "0x465311df0bf146d43750ed7d11b0451b5f6d5bfc69b8a216ef2f1c79c93cd848", + vec![196, 128, 1, 128, 128], + ), + ( + "0x47450e5beefbd5e3a3f80cbbac474bb3db98d5e609aa8d15485c3f0d733dea3a", + vec![ + 228, 1, 128, 160, 84, 66, 224, 39, 157, 63, 17, 73, 222, 76, 232, 217, 226, + 211, 240, 29, 24, 84, 117, 80, 56, 172, 26, 15, 174, 92, 72, 116, 155, 247, 31, + 32, 128, + ], + ), + ( + "0x482814ea8f103c39dcf6ba7e75df37145bde813964d82e81e5d7e3747b95303d", + vec![196, 128, 1, 128, 128], + ), + ( + "0x4845aac9f26fcd628b39b83d1ccb5c554450b9666b66f83aa93a1523f4db0ab6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x48e291f8a256ab15da8401c8cae555d5417a992dff3848926fa5b71655740059", + vec![ + 228, 1, 128, 160, 162, 231, 8, 75, 169, 206, 193, 121, 81, 156, 126, 137, 80, + 198, 106, 211, 203, 168, 88, 106, 96, 207, 249, 244, 214, 12, 24, 141, 214, 33, + 82, 42, 128, + ], + ), + ( + "0x4973f6aa8cf5b1190fc95379aa01cff99570ee6b670725880217237fb49e4b24", + vec![ + 228, 1, 128, 160, 174, 46, 127, 28, 147, 60, 108, 168, 76, 232, 190, 129, 30, + 244, 17, 222, 231, 115, 251, 105, 80, 128, 86, 215, 36, 72, 4, 142, 161, 219, + 92, 71, 128, + ], + ), + ( + "0x4b238e08b80378d0815e109f350a08e5d41ec4094df2cfce7bc8b9e3115bda70", + vec![ + 228, 1, 128, 160, 17, 245, 211, 153, 202, 143, 183, 169, 175, 90, 212, 129, + 190, 96, 207, 97, 212, 84, 147, 205, 32, 32, 108, 157, 10, 35, 124, 231, 215, + 87, 30, 95, 128, + ], + ), + ( + "0x4b9f335ce0bdffdd77fdb9830961c5bc7090ae94703d0392d3f0ff10e6a4fbab", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x4bd8ef9873a5e85d4805dbcb0dbf6810e558ea175167549ef80545a9cafbb0e1", + vec![ + 228, 1, 128, 160, 161, 73, 19, 213, 72, 172, 29, 63, 153, 98, 162, 26, 86, 159, + 229, 47, 20, 54, 182, 210, 245, 234, 78, 54, 222, 19, 234, 133, 94, 222, 84, + 224, 128, + ], + ), + ( + "0x4c2765139cace1d217e238cc7ccfbb751ef200e0eae7ec244e77f37e92dfaee5", + vec![196, 1, 128, 128, 128], + ), + ( + "0x4c310e1f5d2f2e03562c4a5c473ae044b9ee19411f07097ced41e85bd99c3364", + vec![196, 128, 1, 128, 128], + ), + ( + "0x4ccd31891378d2025ef58980481608f11f5b35a988e877652e7cbb0a6127287c", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x4ceaf2371fcfb54a4d8bc1c804d90b06b3c32c9f17112b57c29b30a25cf8ca12", + vec![196, 128, 1, 128, 128], + ), + ]; + + // Create a store and load it up with the accounts + let store = Store::new("null", EngineType::InMemory).unwrap(); + let mut state_trie = store.new_state_trie_for_test(); + for (address, account) in accounts { + let hashed_address = H256::from_str(address).unwrap().as_bytes().to_vec(); + let account = AccountState::from(AccountStateSlim::decode(&account).unwrap()); + state_trie + .insert(hashed_address, account.encode_to_vec()) + .unwrap(); + } + (store, state_trie.hash().unwrap()) + } +} diff --git a/crates/networking/rpc/eth/filter.rs b/crates/networking/rpc/eth/filter.rs index bbeb2f9b7..a5e8c8f13 100644 --- a/crates/networking/rpc/eth/filter.rs +++ b/crates/networking/rpc/eth/filter.rs @@ -558,7 +558,6 @@ mod tests { .await .unwrap(); - dbg!(&response); assert!( response.get("result").is_some(), "Response should have a 'result' field" diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index 733112697..269286cf6 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -690,6 +690,32 @@ impl Store { Ok(trie.get_proof(&hash_key(storage_key))?) } + // Returns an iterator across all accounts in the state trie given by the state_root + // Does not check that the state_root is valid + pub fn iter_accounts(&self, state_root: H256) -> impl Iterator { + self.engine + .open_state_trie(state_root) + .into_iter() + .content() + .map_while(|(path, value)| { + Some((H256::from_slice(&path), AccountState::decode(&value).ok()?)) + }) + } + + pub fn get_account_range_proof( + &self, + state_root: H256, + starting_hash: H256, + last_hash: Option, + ) -> Result>, StoreError> { + let state_trie = self.engine.open_state_trie(state_root); + let mut proof = state_trie.get_proof(&starting_hash.as_bytes().to_vec())?; + if let Some(last_hash) = last_hash { + proof.extend_from_slice(&state_trie.get_proof(&last_hash.as_bytes().to_vec())?); + } + Ok(proof) + } + pub fn add_payload(&self, payload_id: u64, block: Block) -> Result<(), StoreError> { self.engine.add_payload(payload_id, block) } @@ -697,6 +723,11 @@ impl Store { pub fn get_payload(&self, payload_id: u64) -> Result, StoreError> { self.engine.get_payload(payload_id) } + + /// Creates a new state trie with an empty state root, for testing purposes only + pub fn new_state_trie_for_test(&self) -> Trie { + self.engine.open_state_trie(*EMPTY_TRIE_HASH) + } } fn hash_address(address: &Address) -> Vec { diff --git a/crates/storage/trie/trie.rs b/crates/storage/trie/trie.rs index b43559a16..45b098f03 100644 --- a/crates/storage/trie/trie.rs +++ b/crates/storage/trie/trie.rs @@ -5,6 +5,7 @@ mod node; mod node_hash; mod rlp; mod state; +mod trie_iter; #[cfg(test)] mod test_utils; @@ -21,7 +22,7 @@ pub use self::db::{libmdbx::LibmdbxTrieDB, libmdbx_dupsort::LibmdbxDupsortTrieDB pub use self::db::{in_memory::InMemoryTrieDB, TrieDB}; pub use self::error::TrieError; -use self::{nibble::NibbleSlice, node::LeafNode, state::TrieState}; +use self::{nibble::NibbleSlice, node::LeafNode, state::TrieState, trie_iter::TrieIterator}; use lazy_static::lazy_static; @@ -147,11 +148,9 @@ impl Trie { if let NodeHash::Inline(node) = root { node_path.push(node.to_vec()); } - let root_node = self - .state - .get_node(root.clone())? - .expect("inconsistent tree structure"); - root_node.get_path(&self.state, NibbleSlice::new(path), &mut node_path)?; + if let Some(root_node) = self.state.get_node(root.clone())? { + root_node.get_path(&self.state, NibbleSlice::new(path), &mut node_path)?; + } Ok(node_path) } @@ -206,6 +205,16 @@ impl Trie { } } +impl IntoIterator for Trie { + type Item = Node; + + type IntoIter = TrieIterator; + + fn into_iter(self) -> Self::IntoIter { + TrieIterator::new(self) + } +} + #[cfg(test)] mod test { use cita_trie::{MemoryDB as CitaMemoryDB, PatriciaTrie as CitaTrie, Trie as CitaTrieTrait}; diff --git a/crates/storage/trie/trie_iter.rs b/crates/storage/trie/trie_iter.rs new file mode 100644 index 000000000..e60d548dd --- /dev/null +++ b/crates/storage/trie/trie_iter.rs @@ -0,0 +1,58 @@ +use crate::{node::Node, node_hash::NodeHash, PathRLP, Trie, ValueRLP}; + +pub struct TrieIterator { + trie: Trie, + stack: Vec, +} + +impl TrieIterator { + pub(crate) fn new(trie: Trie) -> Self { + let stack = if let Some(root) = &trie.root { + vec![root.clone()] + } else { + vec![] + }; + Self { trie, stack } + } +} + +impl Iterator for TrieIterator { + type Item = Node; + + fn next(&mut self) -> Option { + if self.stack.is_empty() { + return None; + }; + // Fetch the last node in the stack + let next_node_hash = self.stack.pop()?; + let next_node = self.trie.state.get_node(next_node_hash).ok()??; + match &next_node { + Node::Branch(branch_node) => { + // Add all children to the stack (in reverse order so we process first child frist) + for child in branch_node.choices.iter().rev() { + if child.is_valid() { + self.stack.push(child.clone()) + } + } + } + Node::Extension(extension_node) => { + // Add child to the stack + self.stack.push(extension_node.child.clone()); + } + Node::Leaf(_) => {} + } + Some(next_node) + } +} + +impl TrieIterator { + pub fn content(self) -> impl Iterator { + self.filter_map(|n| match n { + Node::Branch(branch_node) => { + (!branch_node.path.is_empty()).then_some((branch_node.path, branch_node.value)) + } + Node::Extension(_) => None, + Node::Leaf(leaf_node) => Some((leaf_node.path, leaf_node.value)), + }) + } +} From ef9c51143dda041e0c8e4ca2a8833b0857e73bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Arjovsky?= Date: Thu, 31 Oct 2024 19:00:49 +0100 Subject: [PATCH 47/49] refactor(l1): pass context to all RpcHandlers (#1039) Closes #1038 --- crates/networking/rpc/authentication.rs | 6 +- .../rpc/engine/exchange_transition_config.rs | 11 +- crates/networking/rpc/engine/fork_choice.rs | 12 +- crates/networking/rpc/engine/mod.rs | 4 +- crates/networking/rpc/engine/payload.rs | 13 +- crates/networking/rpc/eth/account.rs | 36 ++-- crates/networking/rpc/eth/block.rs | 57 +++--- crates/networking/rpc/eth/client.rs | 13 +- crates/networking/rpc/eth/fee_market.rs | 7 +- crates/networking/rpc/eth/filter.rs | 53 +++--- crates/networking/rpc/eth/gas_price.rs | 104 +++++++---- crates/networking/rpc/eth/logs.rs | 6 +- crates/networking/rpc/eth/transaction.rs | 57 +++--- crates/networking/rpc/rpc.rs | 172 +++++++++--------- 14 files changed, 304 insertions(+), 247 deletions(-) diff --git a/crates/networking/rpc/authentication.rs b/crates/networking/rpc/authentication.rs index 1aee37178..40bcbac30 100644 --- a/crates/networking/rpc/authentication.rs +++ b/crates/networking/rpc/authentication.rs @@ -16,7 +16,7 @@ pub enum AuthenticationError { } pub fn authenticate( - secret: Bytes, + secret: &Bytes, auth_header: Option>>, ) -> Result<(), RpcErr> { match auth_header { @@ -39,8 +39,8 @@ struct Claims { } /// Authenticates bearer jwt to check that authrpc calls are sent by the consensus layer -pub fn validate_jwt_authentication(token: &str, secret: Bytes) -> Result<(), AuthenticationError> { - let decoding_key = DecodingKey::from_secret(&secret); +pub fn validate_jwt_authentication(token: &str, secret: &Bytes) -> Result<(), AuthenticationError> { + let decoding_key = DecodingKey::from_secret(secret); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = false; validation.set_required_spec_claims(&["iat"]); diff --git a/crates/networking/rpc/engine/exchange_transition_config.rs b/crates/networking/rpc/engine/exchange_transition_config.rs index 84ff9438a..7d0faa2f1 100644 --- a/crates/networking/rpc/engine/exchange_transition_config.rs +++ b/crates/networking/rpc/engine/exchange_transition_config.rs @@ -1,10 +1,9 @@ use ethereum_rust_core::{serde_utils, H256}; -use ethereum_rust_storage::Store; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::{info, warn}; -use crate::{utils::RpcErr, RpcHandler}; +use crate::{utils::RpcErr, RpcApiContext, RpcHandler}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -45,11 +44,11 @@ impl RpcHandler for ExchangeTransitionConfigV1Req { Ok(ExchangeTransitionConfigV1Req { payload }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Received new engine request: {self}"); let payload = &self.payload; - let chain_config = storage.get_chain_config()?; + let chain_config = context.storage.get_chain_config()?; let terminal_total_difficulty = chain_config.terminal_total_difficulty; if terminal_total_difficulty.unwrap_or_default() != payload.terminal_total_difficulty { @@ -59,7 +58,9 @@ impl RpcHandler for ExchangeTransitionConfigV1Req { ); }; - let block = storage.get_block_header(payload.terminal_block_number)?; + let block = context + .storage + .get_block_header(payload.terminal_block_number)?; let terminal_block_hash = block.map_or(H256::zero(), |block| block.compute_block_hash()); serde_json::to_value(ExchangeTransitionConfigPayload { diff --git a/crates/networking/rpc/engine/fork_choice.rs b/crates/networking/rpc/engine/fork_choice.rs index 7824a6486..78b7f7e89 100644 --- a/crates/networking/rpc/engine/fork_choice.rs +++ b/crates/networking/rpc/engine/fork_choice.rs @@ -4,7 +4,6 @@ use ethereum_rust_blockchain::{ latest_canonical_block_hash, payload::{create_payload, BuildPayloadArgs}, }; -use ethereum_rust_storage::Store; use serde_json::Value; use tracing::{info, warn}; @@ -14,7 +13,7 @@ use crate::{ payload::PayloadStatus, }, utils::RpcRequest, - RpcErr, RpcHandler, + RpcApiContext, RpcErr, RpcHandler, }; #[derive(Debug)] @@ -58,7 +57,8 @@ impl RpcHandler for ForkChoiceUpdatedV3 { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!( "New fork choice request with head: {}, safe: {}, finalized: {}.", self.fork_choice_state.head_block_hash, @@ -68,7 +68,7 @@ impl RpcHandler for ForkChoiceUpdatedV3 { let fork_choice_error_to_response = |error| { let response = match error { InvalidForkChoice::NewHeadAlreadyCanonical => ForkChoiceResponse::from( - PayloadStatus::valid_with_hash(latest_canonical_block_hash(&storage).unwrap()), + PayloadStatus::valid_with_hash(latest_canonical_block_hash(storage).unwrap()), ), InvalidForkChoice::Syncing => ForkChoiceResponse::from(PayloadStatus::syncing()), reason => { @@ -83,7 +83,7 @@ impl RpcHandler for ForkChoiceUpdatedV3 { }; let head_block = match apply_fork_choice( - &storage, + storage, self.fork_choice_state.head_block_hash, self.fork_choice_state.safe_block_hash, self.fork_choice_state.finalized_block_hash, @@ -125,7 +125,7 @@ impl RpcHandler for ForkChoiceUpdatedV3 { }; let payload_id = args.id(); response.set_id(payload_id); - let payload = match create_payload(&args, &storage) { + let payload = match create_payload(&args, storage) { Ok(payload) => payload, Err(ChainError::EvmError(error)) => return Err(error.into()), // Parent block is guaranteed to be present at this point, diff --git a/crates/networking/rpc/engine/mod.rs b/crates/networking/rpc/engine/mod.rs index 59e855a49..cd57a46ee 100644 --- a/crates/networking/rpc/engine/mod.rs +++ b/crates/networking/rpc/engine/mod.rs @@ -2,7 +2,7 @@ pub mod exchange_transition_config; pub mod fork_choice; pub mod payload; -use crate::{utils::RpcRequest, RpcErr, RpcHandler, Store}; +use crate::{utils::RpcRequest, RpcApiContext, RpcErr, RpcHandler}; use serde_json::{json, Value}; pub type ExchangeCapabilitiesRequest = Vec; @@ -30,7 +30,7 @@ impl RpcHandler for ExchangeCapabilitiesRequest { }) } - fn handle(&self, _storage: Store) -> Result { + fn handle(&self, _context: RpcApiContext) -> Result { Ok(json!(*self)) } } diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 59e6e2ed5..b85188b7d 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -3,12 +3,12 @@ use ethereum_rust_blockchain::error::ChainError; use ethereum_rust_blockchain::payload::build_payload; use ethereum_rust_core::types::Fork; use ethereum_rust_core::{H256, U256}; -use ethereum_rust_storage::Store; use serde_json::Value; use tracing::{error, info, warn}; use crate::types::payload::ExecutionPayloadResponse; use crate::utils::RpcRequest; +use crate::RpcApiContext; use crate::{ types::payload::{ExecutionPayloadV3, PayloadStatus}, RpcErr, RpcHandler, @@ -56,7 +56,8 @@ impl RpcHandler for NewPayloadV3Request { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; let block_hash = self.payload.block_hash; info!("Received new payload with block hash: {block_hash:#x}"); @@ -114,7 +115,7 @@ impl RpcHandler for NewPayloadV3Request { // Execute and store the block info!("Executing payload with block hash: {block_hash:#x}"); - let payload_status = match add_block(&block, &storage) { + let payload_status = match add_block(&block, storage) { Err(ChainError::ParentNotFound) => Ok(PayloadStatus::syncing()), // Under the current implementation this is not possible: we always calculate the state // transition of any new payload as long as the parent is present. If we received the @@ -185,15 +186,15 @@ impl RpcHandler for GetPayloadV3Request { Ok(GetPayloadV3Request { payload_id }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Requested payload with id: {:#018x}", self.payload_id); - let Some(mut payload) = storage.get_payload(self.payload_id)? else { + let Some(mut payload) = context.storage.get_payload(self.payload_id)? else { return Err(RpcErr::UnknownPayload(format!( "Payload with id {:#018x} not found", self.payload_id ))); }; - let (blobs_bundle, block_value) = build_payload(&mut payload, &storage) + let (blobs_bundle, block_value) = build_payload(&mut payload, &context.storage) .map_err(|err| RpcErr::Internal(err.to_string()))?; serde_json::to_value(ExecutionPayloadResponse { execution_payload: ExecutionPayloadV3::from_block(payload), diff --git a/crates/networking/rpc/eth/account.rs b/crates/networking/rpc/eth/account.rs index 953999daa..060ec8278 100644 --- a/crates/networking/rpc/eth/account.rs +++ b/crates/networking/rpc/eth/account.rs @@ -1,9 +1,9 @@ -use ethereum_rust_storage::Store; use serde_json::Value; use tracing::info; use crate::types::account_proof::{AccountProof, StorageProof}; use crate::types::block_identifier::BlockIdentifierOrHash; +use crate::RpcApiContext; use crate::{utils::RpcErr, RpcHandler}; use ethereum_rust_core::{Address, BigEndianHash, H256, U256}; @@ -47,19 +47,21 @@ impl RpcHandler for GetBalanceRequest { block: BlockIdentifierOrHash::parse(params[1].clone(), 1)?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested balance of account {} at block {}", self.address, self.block ); - let Some(block_number) = self.block.resolve_block_number(&storage)? else { + let Some(block_number) = self.block.resolve_block_number(&context.storage)? else { return Err(RpcErr::Internal( "Could not resolve block number".to_owned(), )); // Should we return Null here? }; - let account = storage.get_account_info(block_number, self.address)?; + let account = context + .storage + .get_account_info(block_number, self.address)?; let balance = account.map(|acc| acc.balance).unwrap_or_default(); serde_json::to_value(format!("{:#x}", balance)) @@ -80,19 +82,20 @@ impl RpcHandler for GetCodeRequest { block: BlockIdentifierOrHash::parse(params[1].clone(), 1)?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested code of account {} at block {}", self.address, self.block ); - let Some(block_number) = self.block.resolve_block_number(&storage)? else { + let Some(block_number) = self.block.resolve_block_number(&context.storage)? else { return Err(RpcErr::Internal( "Could not resolve block number".to_owned(), )); // Should we return Null here? }; - let code = storage + let code = context + .storage .get_code_by_account_address(block_number, self.address)? .unwrap_or_default(); @@ -115,19 +118,20 @@ impl RpcHandler for GetStorageAtRequest { block: BlockIdentifierOrHash::parse(params[2].clone(), 2)?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested storage sot {} of account {} at block {}", self.storage_slot, self.address, self.block ); - let Some(block_number) = self.block.resolve_block_number(&storage)? else { + let Some(block_number) = self.block.resolve_block_number(&context.storage)? else { return Err(RpcErr::Internal( "Could not resolve block number".to_owned(), )); // Should we return Null here? }; - let storage_value = storage + let storage_value = context + .storage .get_storage_at(block_number, self.address, self.storage_slot)? .unwrap_or_default(); let storage_value = H256::from_uint(&storage_value); @@ -149,18 +153,19 @@ impl RpcHandler for GetTransactionCountRequest { block: BlockIdentifierOrHash::parse(params[1].clone(), 1)?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested nonce of account {} at block {}", self.address, self.block ); - let Some(block_number) = self.block.resolve_block_number(&storage)? else { + let Some(block_number) = self.block.resolve_block_number(&context.storage)? else { return serde_json::to_value("0x0") .map_err(|error| RpcErr::Internal(error.to_string())); }; - let nonce = storage + let nonce = context + .storage .get_nonce_by_account_address(block_number, self.address)? .unwrap_or_default(); @@ -186,12 +191,13 @@ impl RpcHandler for GetProofRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!( "Requested proof for account {} at block {} with storage keys: {:?}", self.address, self.block, self.storage_keys ); - let Some(block_number) = self.block.resolve_block_number(&storage)? else { + let Some(block_number) = self.block.resolve_block_number(storage)? else { return Ok(Value::Null); }; // Create account proof diff --git a/crates/networking/rpc/eth/block.rs b/crates/networking/rpc/eth/block.rs index fea369977..74295d090 100644 --- a/crates/networking/rpc/eth/block.rs +++ b/crates/networking/rpc/eth/block.rs @@ -10,7 +10,7 @@ use crate::{ receipt::{RpcReceipt, RpcReceiptBlockInfo, RpcReceiptTxInfo}, }, utils::RpcErr, - RpcHandler, + RpcApiContext, RpcHandler, }; use ethereum_rust_core::{ types::{ @@ -68,9 +68,10 @@ impl RpcHandler for GetBlockByNumberRequest { hydrated: serde_json::from_value(params[1].clone())?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!("Requested block with number: {}", self.block); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; @@ -109,7 +110,8 @@ impl RpcHandler for GetBlockByHashRequest { hydrated: serde_json::from_value(params[1].clone())?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!("Requested block with hash: {:#x}", self.block); let block_number = match storage.get_block_number(self.block)? { Some(number) => number, @@ -149,16 +151,16 @@ impl RpcHandler for GetBlockTransactionCountRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested transaction count for block with number: {}", self.block ); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(&context.storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; - let block_body = match storage.get_block_body(block_number)? { + let block_body = match context.storage.get_block_body(block_number)? { Some(block_body) => block_body, _ => return Ok(Value::Null), }; @@ -182,9 +184,10 @@ impl RpcHandler for GetBlockReceiptsRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!("Requested receipts for block with number: {}", self.block); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; @@ -195,7 +198,7 @@ impl RpcHandler for GetBlockReceiptsRequest { // Block not found _ => return Ok(Value::Null), }; - let receipts = get_all_block_rpc_receipts(block_number, header, body, &storage)?; + let receipts = get_all_block_rpc_receipts(block_number, header, body, storage)?; serde_json::to_value(&receipts).map_err(|error| RpcErr::Internal(error.to_string())) } @@ -214,16 +217,17 @@ impl RpcHandler for GetRawHeaderRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested raw header for block with identifier: {}", self.block ); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(&context.storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; - let header = storage + let header = context + .storage .get_block_header(block_number)? .ok_or(RpcErr::BadParams("Header not found".to_owned()))?; @@ -246,14 +250,14 @@ impl RpcHandler for GetRawBlockRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Requested raw block: {}", self.block); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(&context.storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; - let header = storage.get_block_header(block_number)?; - let body = storage.get_block_body(block_number)?; + let header = context.storage.get_block_header(block_number)?; + let body = context.storage.get_block_body(block_number)?; let (header, body) = match (header, body) { (Some(header), Some(body)) => (header, body), _ => return Ok(Value::Null), @@ -279,8 +283,9 @@ impl RpcHandler for GetRawReceipts { }) } - fn handle(&self, storage: Store) -> Result { - let block_number = match self.block.resolve_block_number(&storage)? { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; + let block_number = match self.block.resolve_block_number(storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; @@ -290,7 +295,7 @@ impl RpcHandler for GetRawReceipts { (Some(header), Some(body)) => (header, body), _ => return Ok(Value::Null), }; - let receipts: Vec = get_all_block_receipts(block_number, header, body, &storage)? + let receipts: Vec = get_all_block_receipts(block_number, header, body, storage)? .iter() .map(|receipt| format!("0x{}", hex::encode(receipt.encode_to_vec()))) .collect(); @@ -303,9 +308,9 @@ impl RpcHandler for BlockNumberRequest { Ok(Self {}) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Requested latest block number"); - match storage.get_latest_block_number() { + match context.storage.get_latest_block_number() { Ok(Some(block_number)) => serde_json::to_value(format!("{:#x}", block_number)) .map_err(|error| RpcErr::Internal(error.to_string())), Ok(None) => Err(RpcErr::Internal("No blocks found".to_owned())), @@ -319,15 +324,15 @@ impl RpcHandler for GetBlobBaseFee { Ok(Self {}) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Requested blob gas price"); - match storage.get_latest_block_number() { + match context.storage.get_latest_block_number() { Ok(Some(block_number)) => { - let header = match storage.get_block_header(block_number)? { + let header = match context.storage.get_block_header(block_number)? { Some(header) => header, _ => return Err(RpcErr::Internal("Could not get block header".to_owned())), }; - let parent_header = match find_parent_header(&header, &storage) { + let parent_header = match find_parent_header(&header, &context.storage) { Ok(header) => header, Err(error) => return Err(RpcErr::Internal(error.to_string())), }; diff --git a/crates/networking/rpc/eth/client.rs b/crates/networking/rpc/eth/client.rs index a52eece66..f090b0c70 100644 --- a/crates/networking/rpc/eth/client.rs +++ b/crates/networking/rpc/eth/client.rs @@ -1,9 +1,7 @@ -use tracing::info; - -use ethereum_rust_storage::Store; use serde_json::Value; +use tracing::info; -use crate::{utils::RpcErr, RpcHandler}; +use crate::{utils::RpcErr, RpcApiContext, RpcHandler}; pub struct ChainId; impl RpcHandler for ChainId { @@ -11,9 +9,10 @@ impl RpcHandler for ChainId { Ok(Self {}) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!("Requested chain id"); - let chain_spec = storage + let chain_spec = context + .storage .get_chain_config() .map_err(|error| RpcErr::Internal(error.to_string()))?; serde_json::to_value(format!("{:#x}", chain_spec.chain_id)) @@ -27,7 +26,7 @@ impl RpcHandler for Syncing { Ok(Self {}) } - fn handle(&self, _storage: Store) -> Result { + fn handle(&self, _context: RpcApiContext) -> Result { Ok(Value::Bool(false)) } } diff --git a/crates/networking/rpc/eth/fee_market.rs b/crates/networking/rpc/eth/fee_market.rs index cc83a1f73..e1b8cf663 100644 --- a/crates/networking/rpc/eth/fee_market.rs +++ b/crates/networking/rpc/eth/fee_market.rs @@ -4,7 +4,7 @@ use serde::Serialize; use serde_json::Value; use tracing::info; -use crate::{types::block_identifier::BlockIdentifier, utils::RpcErr, RpcHandler}; +use crate::{types::block_identifier::BlockIdentifier, utils::RpcErr, RpcApiContext, RpcHandler}; use ethereum_rust_core::types::calculate_base_fee_per_blob_gas; use ethereum_rust_storage::Store; @@ -67,7 +67,8 @@ impl RpcHandler for FeeHistoryRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!( "Requested fee history for {} blocks starting from {}", self.block_count, self.newest_block @@ -79,7 +80,7 @@ impl RpcHandler for FeeHistoryRequest { } let (start_block, end_block) = - Self::get_range(&storage, self.block_count, &self.newest_block)?; + Self::get_range(storage, self.block_count, &self.newest_block)?; let oldest_block = start_block; let block_count = (end_block - start_block) as usize; let mut base_fee_per_gas = Vec::::with_capacity(block_count + 1); diff --git a/crates/networking/rpc/eth/filter.rs b/crates/networking/rpc/eth/filter.rs index a5e8c8f13..c0e6018fd 100644 --- a/crates/networking/rpc/eth/filter.rs +++ b/crates/networking/rpc/eth/filter.rs @@ -266,7 +266,7 @@ mod tests { }, map_http_requests, utils::test_utils::{self, start_test_api}, - FILTER_DURATION, + RpcApiContext, FILTER_DURATION, }; use crate::{ types::block_identifier::BlockIdentifier, @@ -436,18 +436,22 @@ mod tests { json_req: serde_json::Value, filters_pointer: ActiveFilters, ) -> u64 { - let node = example_p2p_node(); + let context = RpcApiContext { + storage: Store::new("in-mem", EngineType::InMemory) + .expect("Fatal: could not create in memory test db"), + jwt_secret: Default::default(), + local_p2p_node: example_p2p_node(), + active_filters: filters_pointer.clone(), + }; let request: RpcRequest = serde_json::from_value(json_req).expect("Test json is incorrect"); let genesis_config: Genesis = serde_json::from_str(TEST_GENESIS).expect("Fatal: non-valid genesis test config"); - let store = Store::new("in-mem", EngineType::InMemory) - .expect("Fatal: could not create in memory test db"); - store + + context + .storage .add_initial_state(genesis_config) .expect("Fatal: could not add test genesis in test"); - let response = map_http_requests(&request, store, node, filters_pointer.clone()) - .unwrap() - .to_string(); + let response = map_http_requests(&request, context).unwrap().to_string(); let trimmed_id = response.trim().trim_matches('"'); assert!(trimmed_id.starts_with("0x")); let hex = trimmed_id.trim_start_matches("0x"); @@ -485,13 +489,15 @@ mod tests { ), ); let active_filters = Arc::new(Mutex::new(HashMap::from([filter]))); - map_http_requests( - &uninstall_filter_req, - Store::new("in-mem", EngineType::InMemory).unwrap(), - example_p2p_node(), - active_filters.clone(), - ) - .unwrap(); + let context = RpcApiContext { + storage: Store::new("in-mem", EngineType::InMemory).unwrap(), + local_p2p_node: example_p2p_node(), + jwt_secret: Default::default(), + active_filters: active_filters.clone(), + }; + + map_http_requests(&uninstall_filter_req, context).unwrap(); + assert!( active_filters.clone().lock().unwrap().len() == 0, "Expected filter map to be empty after request" @@ -500,6 +506,14 @@ mod tests { #[test] fn removing_non_existing_filter_returns_false() { + let active_filters = Arc::new(Mutex::new(HashMap::new())); + + let context = RpcApiContext { + storage: Store::new("in-mem", EngineType::InMemory).unwrap(), + local_p2p_node: example_p2p_node(), + active_filters: active_filters.clone(), + jwt_secret: Default::default(), + }; let uninstall_filter_req: RpcRequest = serde_json::from_value(json!( { "jsonrpc":"2.0", @@ -511,14 +525,7 @@ mod tests { ,"id":1 })) .expect("Json for test is not a valid request"); - let active_filters = Arc::new(Mutex::new(HashMap::new())); - let res = map_http_requests( - &uninstall_filter_req, - Store::new("in-mem", EngineType::InMemory).unwrap(), - example_p2p_node(), - active_filters.clone(), - ) - .unwrap(); + let res = map_http_requests(&uninstall_filter_req, context).unwrap(); assert!(matches!(res, serde_json::Value::Bool(false))); } diff --git a/crates/networking/rpc/eth/gas_price.rs b/crates/networking/rpc/eth/gas_price.rs index 8fc995146..fe3f18417 100644 --- a/crates/networking/rpc/eth/gas_price.rs +++ b/crates/networking/rpc/eth/gas_price.rs @@ -1,9 +1,8 @@ use ethereum_rust_blockchain::constants::MIN_GAS_LIMIT; -use ethereum_rust_storage::Store; use tracing::error; use crate::utils::RpcErr; -use crate::RpcHandler; +use crate::{RpcApiContext, RpcHandler}; use serde_json::Value; // TODO: This does not need a struct, @@ -41,8 +40,8 @@ impl RpcHandler for GasPrice { // we can look into more sophisticated estimation methods, if needed. /// Estimate Gas Price based on already accepted transactions, /// as per the spec, this will be returned in wei. - fn handle(&self, storage: Store) -> Result { - let Some(latest_block_number) = storage.get_latest_block_number()? else { + fn handle(&self, context: RpcApiContext) -> Result { + let Some(latest_block_number) = context.storage.get_latest_block_number()? else { error!("FATAL: LATEST BLOCK NUMBER IS MISSING"); return Err(RpcErr::Internal("Error calculating gas price".to_string())); }; @@ -64,7 +63,7 @@ impl RpcHandler for GasPrice { // caching this result, also we can have a specific DB method // that returns a block range to not query them one-by-one. for block_num in block_range { - let Some(block_body) = storage.get_block_body(block_num)? else { + let Some(block_body) = context.storage.get_block_body(block_num)? else { error!("Block body for block number {block_num} is missing but is below the latest known block!"); return Err(RpcErr::Internal( "Error calculating gas price: missing data".to_string(), @@ -93,7 +92,7 @@ mod tests { use crate::{ map_http_requests, utils::{parse_json_hex, test_utils::example_p2p_node, RpcRequest}, - RpcHandler, + RpcApiContext, RpcHandler, }; use bytes::Bytes; use ethereum_rust_blockchain::constants::MIN_GAS_LIMIT; @@ -104,10 +103,11 @@ mod tests { }, Address, Bloom, H256, U256, }; + use ethereum_rust_net::types::Node; use ethereum_rust_storage::{EngineType, Store}; use hex_literal::hex; use serde_json::json; - use std::str::FromStr; + use std::{net::Ipv4Addr, str::FromStr}; // Base price for each test transaction. const BASE_PRICE_IN_WEI: u64 = 10_u64.pow(9); fn test_header(block_num: u64) -> BlockHeader { @@ -198,7 +198,7 @@ mod tests { } #[test] fn test_for_legacy_txs() { - let store = setup_store(); + let context = default_context(); for block_num in 1..100 { let mut txs = vec![]; for nonce in 1..=3 { @@ -212,21 +212,25 @@ mod tests { }; let block_header = test_header(block_num); let block = Block::new(block_header.clone(), block_body); - store.add_block(block).unwrap(); - store + context.storage.add_block(block).unwrap(); + context + .storage .set_canonical_block(block_num, block_header.compute_block_hash()) .unwrap(); - store.update_latest_block_number(block_num).unwrap(); + context + .storage + .update_latest_block_number(block_num) + .unwrap(); } let gas_price = GasPrice {}; - let response = gas_price.handle(store).unwrap(); + let response = gas_price.handle(context).unwrap(); let parsed_result = parse_json_hex(&response).unwrap(); assert_eq!(parsed_result, 2000000000); } #[test] fn test_for_eip_1559_txs() { - let store = setup_store(); + let context = default_context(); for block_num in 1..100 { let mut txs = vec![]; for nonce in 1..=3 { @@ -239,20 +243,24 @@ mod tests { }; let block_header = test_header(block_num); let block = Block::new(block_header.clone(), block_body); - store.add_block(block).unwrap(); - store + context.storage.add_block(block).unwrap(); + context + .storage .set_canonical_block(block_num, block_header.compute_block_hash()) .unwrap(); - store.update_latest_block_number(block_num).unwrap(); + context + .storage + .update_latest_block_number(block_num) + .unwrap(); } let gas_price = GasPrice {}; - let response = gas_price.handle(store).unwrap(); + let response = gas_price.handle(context).unwrap(); let parsed_result = parse_json_hex(&response).unwrap(); assert_eq!(parsed_result, 2000000000); } #[test] fn test_with_mixed_transactions() { - let store = setup_store(); + let context = default_context(); for block_num in 1..100 { let txs = vec![ legacy_tx_for_test(1), @@ -267,20 +275,24 @@ mod tests { }; let block_header = test_header(block_num); let block = Block::new(block_header.clone(), block_body); - store.add_block(block).unwrap(); - store + context.storage.add_block(block).unwrap(); + context + .storage .set_canonical_block(block_num, block_header.compute_block_hash()) .unwrap(); - store.update_latest_block_number(block_num).unwrap(); + context + .storage + .update_latest_block_number(block_num) + .unwrap(); } let gas_price = GasPrice {}; - let response = gas_price.handle(store).unwrap(); + let response = gas_price.handle(context).unwrap(); let parsed_result = parse_json_hex(&response).unwrap(); assert_eq!(parsed_result, 2000000000); } #[test] fn test_with_not_enough_blocks_or_transactions() { - let store = setup_store(); + let context = default_context(); for block_num in 1..10 { let txs = vec![legacy_tx_for_test(1)]; let block_body = BlockBody { @@ -290,23 +302,27 @@ mod tests { }; let block_header = test_header(block_num); let block = Block::new(block_header.clone(), block_body); - store.add_block(block).unwrap(); - store + context.storage.add_block(block).unwrap(); + context + .storage .set_canonical_block(block_num, block_header.compute_block_hash()) .unwrap(); - store.update_latest_block_number(block_num).unwrap(); + context + .storage + .update_latest_block_number(block_num) + .unwrap(); } let gas_price = GasPrice {}; - let response = gas_price.handle(store).unwrap(); + let response = gas_price.handle(context).unwrap(); let parsed_result = parse_json_hex(&response).unwrap(); assert_eq!(parsed_result, 1000000000); } #[test] fn test_with_no_blocks_but_genesis() { - let store = setup_store(); + let context = default_context(); let gas_price = GasPrice {}; let expected_gas_price = MIN_GAS_LIMIT; - let response = gas_price.handle(store).unwrap(); + let response = gas_price.handle(context).unwrap(); let parsed_result = parse_json_hex(&response).unwrap(); assert_eq!(parsed_result, expected_gas_price); } @@ -320,7 +336,8 @@ mod tests { }); let expected_response = json!("0x3b9aca00"); let request: RpcRequest = serde_json::from_value(raw_json).expect("Test json is not valid"); - let storage = setup_store(); + let mut context = default_context(); + context.local_p2p_node = example_p2p_node(); for block_num in 1..100 { let txs = vec![legacy_tx_for_test(1)]; @@ -331,14 +348,31 @@ mod tests { }; let block_header = test_header(block_num); let block = Block::new(block_header.clone(), block_body); - storage.add_block(block).unwrap(); - storage + context.storage.add_block(block).unwrap(); + context + .storage .set_canonical_block(block_num, block_header.compute_block_hash()) .unwrap(); - storage.update_latest_block_number(block_num).unwrap(); + context + .storage + .update_latest_block_number(block_num) + .unwrap(); } - let response = - map_http_requests(&request, storage, example_p2p_node(), Default::default()).unwrap(); + let response = map_http_requests(&request, context).unwrap(); assert_eq!(response, expected_response) } + + fn default_context() -> RpcApiContext { + RpcApiContext { + storage: setup_store(), + jwt_secret: Default::default(), + local_p2p_node: Node { + ip: std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + udp_port: Default::default(), + tcp_port: Default::default(), + node_id: Default::default(), + }, + active_filters: Default::default(), + } + } } diff --git a/crates/networking/rpc/eth/logs.rs b/crates/networking/rpc/eth/logs.rs index cefeb83d6..b45034e70 100644 --- a/crates/networking/rpc/eth/logs.rs +++ b/crates/networking/rpc/eth/logs.rs @@ -4,7 +4,7 @@ // - Ethereum's reference: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter use crate::{ types::{block_identifier::BlockIdentifier, receipt::RpcLog}, - RpcErr, RpcHandler, + RpcApiContext, RpcErr, RpcHandler, }; use ethereum_rust_core::{H160, H256}; use ethereum_rust_storage::Store; @@ -84,8 +84,8 @@ impl RpcHandler for LogsFilter { )), } } - fn handle(&self, storage: Store) -> Result { - let filtered_logs = fetch_logs_with_filter(self, storage)?; + fn handle(&self, context: RpcApiContext) -> Result { + let filtered_logs = fetch_logs_with_filter(self, context.storage)?; serde_json::to_value(filtered_logs).map_err(|error| { tracing::error!("Log filtering request failed with: {error}"); RpcErr::Internal("Failed to filter logs".to_string()) diff --git a/crates/networking/rpc/eth/transaction.rs b/crates/networking/rpc/eth/transaction.rs index d0cbcca82..a88dd9832 100644 --- a/crates/networking/rpc/eth/transaction.rs +++ b/crates/networking/rpc/eth/transaction.rs @@ -5,7 +5,7 @@ use crate::{ transaction::{RpcTransaction, SendRawTransactionRequest}, }, utils::RpcErr, - RpcHandler, + RpcApiContext, RpcHandler, }; use ethereum_rust_core::{ types::{AccessListEntry, BlockHash, BlockHeader, BlockNumber, GenericTransaction, TxKind}, @@ -96,16 +96,16 @@ impl RpcHandler for CallRequest { block, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { let block = self.block.clone().unwrap_or_default(); info!("Requested call on block: {}", block); - let header = match block.resolve_block_header(&storage)? { + let header = match block.resolve_block_header(&context.storage)? { Some(header) => header, // Block not found _ => return Ok(Value::Null), }; // Run transaction - let result = simulate_tx(&self.transaction, &header, storage, SpecId::CANCUN)?; + let result = simulate_tx(&self.transaction, &header, context.storage, SpecId::CANCUN)?; serde_json::to_value(format!("0x{:#x}", result.output())) .map_err(|error| RpcErr::Internal(error.to_string())) } @@ -132,20 +132,20 @@ impl RpcHandler for GetTransactionByBlockNumberAndIndexRequest { }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested transaction at index: {} of block with number: {}", self.transaction_index, self.block, ); - let block_number = match self.block.resolve_block_number(&storage)? { + let block_number = match self.block.resolve_block_number(&context.storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; - let block_body = match storage.get_block_body(block_number)? { + let block_body = match context.storage.get_block_body(block_number)? { Some(block_body) => block_body, _ => return Ok(Value::Null), }; - let block_header = match storage.get_block_header(block_number)? { + let block_header = match context.storage.get_block_header(block_number)? { Some(block_body) => block_body, _ => return Ok(Value::Null), }; @@ -183,16 +183,16 @@ impl RpcHandler for GetTransactionByBlockHashAndIndexRequest { .map_err(|error| RpcErr::BadParams(error.to_string()))?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { info!( "Requested transaction at index: {} of block with hash: {:#x}", self.transaction_index, self.block, ); - let block_number = match storage.get_block_number(self.block)? { + let block_number = match context.storage.get_block_number(self.block)? { Some(number) => number, _ => return Ok(Value::Null), }; - let block_body = match storage.get_block_body(block_number)? { + let block_body = match context.storage.get_block_body(block_number)? { Some(block_body) => block_body, _ => return Ok(Value::Null), }; @@ -221,7 +221,8 @@ impl RpcHandler for GetTransactionByHashRequest { transaction_hash: serde_json::from_value(params[0].clone())?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!( "Requested transaction with hash: {:#x}", self.transaction_hash, @@ -259,7 +260,8 @@ impl RpcHandler for GetTransactionReceiptRequest { transaction_hash: serde_json::from_value(params[0].clone())?, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; info!( "Requested receipt for transaction {:#x}", self.transaction_hash, @@ -274,7 +276,7 @@ impl RpcHandler for GetTransactionReceiptRequest { None => return Ok(Value::Null), }; let receipts = - block::get_all_block_rpc_receipts(block_number, block.header, block.body, &storage)?; + block::get_all_block_rpc_receipts(block_number, block.header, block.body, storage)?; serde_json::to_value(receipts.get(index as usize)) .map_err(|error| RpcErr::Internal(error.to_string())) } @@ -304,14 +306,14 @@ impl RpcHandler for CreateAccessListRequest { block, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { let block = self.block.clone().unwrap_or_default(); info!("Requested access list creation for tx on block: {}", block); - let block_number = match block.resolve_block_number(&storage)? { + let block_number = match block.resolve_block_number(&context.storage)? { Some(block_number) => block_number, _ => return Ok(Value::Null), }; - let header = match storage.get_block_header(block_number)? { + let header = match context.storage.get_block_header(block_number)? { Some(header) => header, // Block not found _ => return Ok(Value::Null), @@ -320,7 +322,7 @@ impl RpcHandler for CreateAccessListRequest { let (gas_used, access_list, error) = match ethereum_rust_vm::create_access_list( &self.transaction, &header, - &mut evm_state(storage, header.compute_block_hash()), + &mut evm_state(context.storage, header.compute_block_hash()), SpecId::CANCUN, )? { ( @@ -386,8 +388,10 @@ impl RpcHandler for GetRawTransaction { }) } - fn handle(&self, storage: Store) -> Result { - let tx = storage.get_transaction_by_hash(self.transaction_hash)?; + fn handle(&self, context: RpcApiContext) -> Result { + let tx = context + .storage + .get_transaction_by_hash(self.transaction_hash)?; let tx = match tx { Some(tx) => tx, @@ -423,10 +427,11 @@ impl RpcHandler for EstimateGasRequest { block, }) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { + let storage = &context.storage; let block = self.block.clone().unwrap_or_default(); info!("Requested estimate on block: {}", block); - let block_header = match block.resolve_block_header(&storage)? { + let block_header = match block.resolve_block_header(storage)? { Some(header) => header, // Block not found _ => return Ok(Value::Null), @@ -464,7 +469,7 @@ impl RpcHandler for EstimateGasRequest { highest_gas_limit = recap_with_account_balances( highest_gas_limit, &self.transaction, - &storage, + storage, block_header.number, )?; } @@ -570,15 +575,15 @@ impl RpcHandler for SendRawTransactionRequest { Ok(transaction) } - fn handle(&self, storage: Store) -> Result { + fn handle(&self, context: RpcApiContext) -> Result { let hash = if let SendRawTransactionRequest::EIP4844(wrapped_blob_tx) = self { mempool::add_blob_transaction( wrapped_blob_tx.tx.clone(), wrapped_blob_tx.blobs_bundle.clone(), - storage, + context.storage, ) } else { - mempool::add_transaction(self.to_transaction(), storage) + mempool::add_transaction(self.to_transaction(), context.storage) }?; serde_json::to_value(format!("{:#x}", hash)) .map_err(|error| RpcErr::Internal(error.to_string())) diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 71d14787d..645386c29 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -70,12 +70,12 @@ pub struct RpcApiContext { trait RpcHandler: Sized { fn parse(params: &Option>) -> Result; - fn call(req: &RpcRequest, storage: Store) -> Result { + fn call(req: &RpcRequest, context: RpcApiContext) -> Result { let request = Self::parse(&req.params)?; - request.handle(storage) + request.handle(context) } - fn handle(&self, storage: Store) -> Result; + fn handle(&self, context: RpcApiContext) -> Result; } const FILTER_DURATION: Duration = { @@ -149,15 +149,8 @@ pub async fn handle_http_request( State(service_context): State, body: String, ) -> Json { - let storage = service_context.storage; - let local_p2p_node = service_context.local_p2p_node; let req: RpcRequest = serde_json::from_str(&body).unwrap(); - let res = map_http_requests( - &req, - storage, - local_p2p_node, - service_context.active_filters, - ); + let res = map_http_requests(&req, service_context); rpc_response(req.id, res) } @@ -166,129 +159,116 @@ pub async fn handle_authrpc_request( auth_header: Option>>, body: String, ) -> Json { - let storage = service_context.storage; - let secret = service_context.jwt_secret; let req: RpcRequest = serde_json::from_str(&body).unwrap(); - match authenticate(secret, auth_header) { + match authenticate(&service_context.jwt_secret, auth_header) { Err(error) => rpc_response(req.id, Err(error)), Ok(()) => { // Proceed with the request - let res = map_authrpc_requests(&req, storage, service_context.active_filters); + let res = map_authrpc_requests(&req, service_context); rpc_response(req.id, res) } } } /// Handle requests that can come from either clients or other users -pub fn map_http_requests( - req: &RpcRequest, - storage: Store, - local_p2p_node: Node, - filters: ActiveFilters, -) -> Result { +pub fn map_http_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.namespace() { - Ok(RpcNamespace::Eth) => map_eth_requests(req, storage, filters), - Ok(RpcNamespace::Admin) => map_admin_requests(req, storage, local_p2p_node), - Ok(RpcNamespace::Debug) => map_debug_requests(req, storage), - Ok(RpcNamespace::Web3) => map_web3_requests(req, storage), + Ok(RpcNamespace::Eth) => map_eth_requests(req, context), + Ok(RpcNamespace::Admin) => map_admin_requests(req, context), + Ok(RpcNamespace::Debug) => map_debug_requests(req, context), + Ok(RpcNamespace::Web3) => map_web3_requests(req, context), _ => Err(RpcErr::MethodNotFound(req.method.clone())), } } /// Handle requests from consensus client -pub fn map_authrpc_requests( - req: &RpcRequest, - storage: Store, - active_filters: ActiveFilters, -) -> Result { +pub fn map_authrpc_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.namespace() { - Ok(RpcNamespace::Engine) => map_engine_requests(req, storage), - Ok(RpcNamespace::Eth) => map_eth_requests(req, storage, active_filters), + Ok(RpcNamespace::Engine) => map_engine_requests(req, context), + Ok(RpcNamespace::Eth) => map_eth_requests(req, context), _ => Err(RpcErr::MethodNotFound(req.method.clone())), } } -pub fn map_eth_requests( - req: &RpcRequest, - storage: Store, - filters: ActiveFilters, -) -> Result { +pub fn map_eth_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.method.as_str() { - "eth_chainId" => ChainId::call(req, storage), - "eth_syncing" => Syncing::call(req, storage), - "eth_getBlockByNumber" => GetBlockByNumberRequest::call(req, storage), - "eth_getBlockByHash" => GetBlockByHashRequest::call(req, storage), - "eth_getBalance" => GetBalanceRequest::call(req, storage), - "eth_getCode" => GetCodeRequest::call(req, storage), - "eth_getStorageAt" => GetStorageAtRequest::call(req, storage), + "eth_chainId" => ChainId::call(req, context), + "eth_syncing" => Syncing::call(req, context), + "eth_getBlockByNumber" => GetBlockByNumberRequest::call(req, context), + "eth_getBlockByHash" => GetBlockByHashRequest::call(req, context), + "eth_getBalance" => GetBalanceRequest::call(req, context), + "eth_getCode" => GetCodeRequest::call(req, context), + "eth_getStorageAt" => GetStorageAtRequest::call(req, context), "eth_getBlockTransactionCountByNumber" => { - GetBlockTransactionCountRequest::call(req, storage) + GetBlockTransactionCountRequest::call(req, context) } - "eth_getBlockTransactionCountByHash" => GetBlockTransactionCountRequest::call(req, storage), + "eth_getBlockTransactionCountByHash" => GetBlockTransactionCountRequest::call(req, context), "eth_getTransactionByBlockNumberAndIndex" => { - GetTransactionByBlockNumberAndIndexRequest::call(req, storage) + GetTransactionByBlockNumberAndIndexRequest::call(req, context) } "eth_getTransactionByBlockHashAndIndex" => { - GetTransactionByBlockHashAndIndexRequest::call(req, storage) + GetTransactionByBlockHashAndIndexRequest::call(req, context) } - "eth_getBlockReceipts" => GetBlockReceiptsRequest::call(req, storage), - "eth_getTransactionByHash" => GetTransactionByHashRequest::call(req, storage), - "eth_getTransactionReceipt" => GetTransactionReceiptRequest::call(req, storage), - "eth_createAccessList" => CreateAccessListRequest::call(req, storage), - "eth_blockNumber" => BlockNumberRequest::call(req, storage), - "eth_call" => CallRequest::call(req, storage), - "eth_blobBaseFee" => GetBlobBaseFee::call(req, storage), - "eth_getTransactionCount" => GetTransactionCountRequest::call(req, storage), - "eth_feeHistory" => FeeHistoryRequest::call(req, storage), - "eth_estimateGas" => EstimateGasRequest::call(req, storage), - "eth_getLogs" => LogsFilter::call(req, storage), - "eth_newFilter" => NewFilterRequest::stateful_call(req, storage, filters), - "eth_uninstallFilter" => DeleteFilterRequest::stateful_call(req, storage, filters), - "eth_getFilterChanges" => FilterChangesRequest::stateful_call(req, storage, filters), - "eth_sendRawTransaction" => SendRawTransactionRequest::call(req, storage), - "eth_getProof" => GetProofRequest::call(req, storage), - "eth_gasPrice" => GasPrice::call(req, storage), + "eth_getBlockReceipts" => GetBlockReceiptsRequest::call(req, context), + "eth_getTransactionByHash" => GetTransactionByHashRequest::call(req, context), + "eth_getTransactionReceipt" => GetTransactionReceiptRequest::call(req, context), + "eth_createAccessList" => CreateAccessListRequest::call(req, context), + "eth_blockNumber" => BlockNumberRequest::call(req, context), + "eth_call" => CallRequest::call(req, context), + "eth_blobBaseFee" => GetBlobBaseFee::call(req, context), + "eth_getTransactionCount" => GetTransactionCountRequest::call(req, context), + "eth_feeHistory" => FeeHistoryRequest::call(req, context), + "eth_estimateGas" => EstimateGasRequest::call(req, context), + "eth_getLogs" => LogsFilter::call(req, context), + "eth_newFilter" => { + NewFilterRequest::stateful_call(req, context.storage, context.active_filters) + } + "eth_uninstallFilter" => { + DeleteFilterRequest::stateful_call(req, context.storage, context.active_filters) + } + "eth_getFilterChanges" => { + FilterChangesRequest::stateful_call(req, context.storage, context.active_filters) + } + "eth_sendRawTransaction" => SendRawTransactionRequest::call(req, context), + "eth_getProof" => GetProofRequest::call(req, context), + "eth_gasPrice" => GasPrice::call(req, context), unknown_eth_method => Err(RpcErr::MethodNotFound(unknown_eth_method.to_owned())), } } -pub fn map_debug_requests(req: &RpcRequest, storage: Store) -> Result { +pub fn map_debug_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.method.as_str() { - "debug_getRawHeader" => GetRawHeaderRequest::call(req, storage), - "debug_getRawBlock" => GetRawBlockRequest::call(req, storage), - "debug_getRawTransaction" => GetRawTransaction::call(req, storage), - "debug_getRawReceipts" => GetRawReceipts::call(req, storage), + "debug_getRawHeader" => GetRawHeaderRequest::call(req, context), + "debug_getRawBlock" => GetRawBlockRequest::call(req, context), + "debug_getRawTransaction" => GetRawTransaction::call(req, context), + "debug_getRawReceipts" => GetRawReceipts::call(req, context), unknown_debug_method => Err(RpcErr::MethodNotFound(unknown_debug_method.to_owned())), } } -pub fn map_engine_requests(req: &RpcRequest, storage: Store) -> Result { +pub fn map_engine_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.method.as_str() { - "engine_exchangeCapabilities" => ExchangeCapabilitiesRequest::call(req, storage), - "engine_forkchoiceUpdatedV3" => ForkChoiceUpdatedV3::call(req, storage), - "engine_newPayloadV3" => NewPayloadV3Request::call(req, storage), + "engine_exchangeCapabilities" => ExchangeCapabilitiesRequest::call(req, context), + "engine_forkchoiceUpdatedV3" => ForkChoiceUpdatedV3::call(req, context), + "engine_newPayloadV3" => NewPayloadV3Request::call(req, context), "engine_exchangeTransitionConfigurationV1" => { - ExchangeTransitionConfigV1Req::call(req, storage) + ExchangeTransitionConfigV1Req::call(req, context) } - "engine_getPayloadV3" => GetPayloadV3Request::call(req, storage), + "engine_getPayloadV3" => GetPayloadV3Request::call(req, context), unknown_engine_method => Err(RpcErr::MethodNotFound(unknown_engine_method.to_owned())), } } -pub fn map_admin_requests( - req: &RpcRequest, - storage: Store, - local_p2p_node: Node, -) -> Result { +pub fn map_admin_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.method.as_str() { - "admin_nodeInfo" => admin::node_info(storage, local_p2p_node), + "admin_nodeInfo" => admin::node_info(context.storage, context.local_p2p_node), unknown_admin_method => Err(RpcErr::MethodNotFound(unknown_admin_method.to_owned())), } } -pub fn map_web3_requests(req: &RpcRequest, storage: Store) -> Result { +pub fn map_web3_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match req.method.as_str() { - "web3_clientVersion" => web3::client_version(req, storage), + "web3_clientVersion" => web3::client_version(req, context.storage), unknown_web3_method => Err(RpcErr::MethodNotFound(unknown_web3_method.to_owned())), } } @@ -340,7 +320,13 @@ mod tests { let storage = Store::new("temp.db", EngineType::InMemory).expect("Failed to create test DB"); storage.set_chain_config(&example_chain_config()).unwrap(); - let result = map_http_requests(&request, storage, local_p2p_node, Default::default()); + let context = RpcApiContext { + local_p2p_node, + storage, + jwt_secret: Default::default(), + active_filters: Default::default(), + }; + let result = map_http_requests(&request, context); let rpc_response = rpc_response(request.id, result); let expected_response = to_rpc_response_success_value( r#"{"jsonrpc":"2.0","id":1,"result":{"enode":"enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@127.0.0.1:30303","id":"d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666","ip":"127.0.0.1","name":"ethereum_rust/0.1.0/rust1.80","ports":{"discovery":30303,"listener":30303},"protocols":{"eth":{"chainId":3151908,"homesteadBlock":0,"daoForkBlock":null,"daoForkSupport":false,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"muirGlacierBlock":null,"berlinBlock":0,"londonBlock":0,"arrowGlacierBlock":null,"grayGlacierBlock":null,"mergeNetsplitBlock":0,"shanghaiTime":0,"cancunTime":0,"pragueTime":1718232101,"verkleTime":null,"terminalTotalDifficulty":0,"terminalTotalDifficultyPassed":true}}}}"#, @@ -371,7 +357,13 @@ mod tests { .expect("Failed to add genesis block to DB"); let local_p2p_node = example_p2p_node(); // Process request - let result = map_http_requests(&request, storage, local_p2p_node, Default::default()); + let context = RpcApiContext { + local_p2p_node, + storage, + jwt_secret: Default::default(), + active_filters: Default::default(), + }; + let result = map_http_requests(&request, context); let response = rpc_response(request.id, result); let expected_response = to_rpc_response_success_value( r#"{"jsonrpc":"2.0","id":1,"result":{"accessList":[],"gasUsed":"0x5208"}}"#, @@ -394,7 +386,13 @@ mod tests { .expect("Failed to add genesis block to DB"); let local_p2p_node = example_p2p_node(); // Process request - let result = map_http_requests(&request, storage, local_p2p_node, Default::default()); + let context = RpcApiContext { + local_p2p_node, + storage, + jwt_secret: Default::default(), + active_filters: Default::default(), + }; + let result = map_http_requests(&request, context); let response = serde_json::from_value::(rpc_response(request.id, result).0) .expect("Request failed"); From ce7eb98ddaa73fab7219c1f0f2de4b7ebd258f9e Mon Sep 17 00:00:00 2001 From: Rodrigo Oliveri Date: Thu, 31 Oct 2024 17:31:12 -0300 Subject: [PATCH 48/49] chore(l1): reduce unnecessary docker builds and remove duplicate workflow executions in main (#1032) **Motivation** We are re-building our docker image across the jobs/workflows multiple times, the idea is to remove as much of the duplicate executions as possible. At the same time, while merging to main we run duplicated jobs, the idea is to remove that duplication. **Description** This PR accomplish a couple of things: - It creates a new make tasks for running hive without depending on the image build - It generates a new job in the hive workflow to build the image once and then share it with every simulation - Merges the assertoor stability check with hive to leverage the docker image already built - Fix an issue were workflows were triggered two times in a row while merging to main (both `merge_group` and `push` rules were set) Resolves #1008 Resolves #1041 --- .github/workflows/assertoor.yaml | 43 -------- .github/workflows/ci.yaml | 2 - .github/workflows/hive.yaml | 67 ------------ .github/workflows/hive_and_assertoor.yaml | 124 ++++++++++++++++++++++ .github/workflows/levm_bench.yaml | 2 - Makefile | 3 + 6 files changed, 127 insertions(+), 114 deletions(-) delete mode 100644 .github/workflows/assertoor.yaml delete mode 100644 .github/workflows/hive.yaml create mode 100644 .github/workflows/hive_and_assertoor.yaml diff --git a/.github/workflows/assertoor.yaml b/.github/workflows/assertoor.yaml deleted file mode 100644 index 77431ed9c..000000000 --- a/.github/workflows/assertoor.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Assertoor -on: - merge_group: - push: - branches: [ main ] - pull_request: - branches: [ '*' ] - paths-ignore: - - "crates/l2/**" - - 'README.md' - - 'LICENSE' - - "**/README.md" - - "**/docs/**" - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - RUST_VERSION: 1.80.1 - -jobs: - test-run: - name: Stability Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - load: true # Important for building without pushing - tags: ethereum_rust - - name: Setup kurtosis testnet and run assertoor tests - uses: ethpandaops/kurtosis-assertoor-github-action@v1 - with: - kurtosis_version: '1.3.1' - ethereum_package_url: 'github.com/lambdaclass/ethereum-package' - ethereum_package_branch: 'ethereum-rust-integration' - ethereum_package_args: './test_data/network_params.yaml' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e2648116..e5a31c14b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,8 +1,6 @@ name: CI on: merge_group: - push: - branches: [main] pull_request: branches: ["**"] paths-ignore: diff --git a/.github/workflows/hive.yaml b/.github/workflows/hive.yaml deleted file mode 100644 index 1f0fe044b..000000000 --- a/.github/workflows/hive.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# Runs the specified hive testing suites -name: Hive -on: - merge_group: - push: - branches: [main] - pull_request: - branches: ["*"] - paths-ignore: - - "crates/l2/**" - - 'README.md' - - 'LICENSE' - - "**/README.md" - - "**/docs/**" - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - RUST_VERSION: 1.80.1 - -jobs: - run-hive: - name: ${{ matrix.name }} - runs-on: ubuntu-latest - strategy: - matrix: - include: - - simulation: rpc-compat - name: "Rpc Compat tests" - run_command: make run-hive SIMULATION=ethereum/rpc-compat TEST_PATTERN="/eth_chainId|eth_getTransactionByBlockHashAndIndex|eth_getTransactionByBlockNumberAndIndex|eth_getCode|eth_getStorageAt|eth_call|eth_getTransactionByHash|eth_getBlockByHash|eth_getBlockByNumber|eth_createAccessList|eth_getBlockTransactionCountByNumber|eth_getBlockTransactionCountByHash|eth_getBlockReceipts|eth_getTransactionReceipt|eth_blobGasPrice|eth_blockNumber|ethGetTransactionCount|debug_getRawHeader|debug_getRawBlock|debug_getRawTransaction|debug_getRawReceipts|eth_estimateGas|eth_getBalance|eth_sendRawTransaction|eth_getProof|eth_getLogs" - - simulation: rpc-auth - name: "Rpc Auth tests" - run_command: make run-hive SIMULATION=ethereum/rpc-compat TEST_PATTERN="/engine-auth" - - simulation: discv4 - name: "Devp2p discv4 tests" - run_command: make run-hive SIMULATION=devp2p TEST_PATTERN="discv4" - - simulation: snap - name: "Devp2p snap tests" - run_command: make run-hive SIMULATION=devp2p TEST_PATTERN="/AccountRange" - - simulation: engine - name: "Engine tests" - run_command: make run-hive SIMULATION=ethereum/engine TEST_PATTERN="/Blob Transactions On Block 1, Cancun Genesis|Blob Transactions On Block 1, Shanghai Genesis|Blob Transaction Ordering, Single Account, Single Blob|Blob Transaction Ordering, Single Account, Dual Blob|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|ForkchoiceUpdated Version on Payload Request" - - simulation: engine-cancun - name: "Cancun Engine tests" - run_command: make run-hive SIMULATION=ethereum/engine TEST_PATTERN="cancun/Unique Payload ID|ParentHash equals BlockHash on NewPayload|Re-Execute Payload|Payload Build after New Invalid Payload|RPC|Build Payload with Invalid ChainID|Invalid PayloadAttributes, Zero timestamp, Syncing=False|Invalid PayloadAttributes, Parent timestamp, Syncing=False|Invalid PayloadAttributes, Missing BeaconRoot, Syncing=False|Suggested Fee Recipient Test|PrevRandao Opcode Transactions Test|Invalid Missing Ancestor ReOrg, StateRoot" - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Rustup toolchain install - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: Setup Go - uses: actions/setup-go@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Download Hive - run: make setup-hive - - - name: Run Hive Simulation - run: ${{ matrix.run_command }} diff --git a/.github/workflows/hive_and_assertoor.yaml b/.github/workflows/hive_and_assertoor.yaml new file mode 100644 index 000000000..be66b53b0 --- /dev/null +++ b/.github/workflows/hive_and_assertoor.yaml @@ -0,0 +1,124 @@ +name: "Hive & Assertoor" +on: + merge_group: + paths-ignore: + - "crates/l2/**" + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" + pull_request: + branches: ["**"] + paths-ignore: + - "crates/l2/**" + - 'README.md' + - 'LICENSE' + - "**/README.md" + - "**/docs/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + RUST_VERSION: 1.80.1 + +jobs: + docker-build: + name: Docker Build image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + load: true + tags: ethereum_rust + outputs: type=docker,dest=/tmp/ethereum_rust_image.tar + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ethereum_rust_image + path: /tmp/ethereum_rust_image.tar + + run-hive: + name: Hive - ${{ matrix.name }} + needs: [docker-build] + runs-on: ubuntu-latest + strategy: + matrix: + include: + - simulation: rpc-compat + name: "Rpc Compat tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/rpc-compat TEST_PATTERN="/eth_chainId|eth_getTransactionByBlockHashAndIndex|eth_getTransactionByBlockNumberAndIndex|eth_getCode|eth_getStorageAt|eth_call|eth_getTransactionByHash|eth_getBlockByHash|eth_getBlockByNumber|eth_createAccessList|eth_getBlockTransactionCountByNumber|eth_getBlockTransactionCountByHash|eth_getBlockReceipts|eth_getTransactionReceipt|eth_blobGasPrice|eth_blockNumber|ethGetTransactionCount|debug_getRawHeader|debug_getRawBlock|debug_getRawTransaction|debug_getRawReceipts|eth_estimateGas|eth_getBalance|eth_sendRawTransaction|eth_getProof|eth_getLogs" + - simulation: rpc-auth + name: "Rpc Auth tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/rpc-compat TEST_PATTERN="/engine-auth" + - simulation: discv4 + name: "Devp2p discv4 tests" + run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="discv4" + - simulation: snap + name: "Devp2p snap tests" + run_command: make run-hive-on-latest SIMULATION=devp2p TEST_PATTERN="/AccountRange" + - simulation: engine + name: "Engine tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="/Blob Transactions On Block 1, Cancun Genesis|Blob Transactions On Block 1, Shanghai Genesis|Blob Transaction Ordering, Single Account, Single Blob|Blob Transaction Ordering, Single Account, Dual Blob|Blob Transaction Ordering, Multiple Accounts|Replace Blob Transactions|Parallel Blob Transactions|ForkchoiceUpdatedV3 Modifies Payload ID on Different Beacon Root|NewPayloadV3 After Cancun|NewPayloadV3 Versioned Hashes|ForkchoiceUpdated Version on Payload Request" + - simulation: engine-cancun + name: "Cancun Engine tests" + run_command: make run-hive-on-latest SIMULATION=ethereum/engine TEST_PATTERN="cancun/Unique Payload ID|ParentHash equals BlockHash on NewPayload|Re-Execute Payload|Payload Build after New Invalid Payload|RPC|Build Payload with Invalid ChainID|Invalid PayloadAttributes, Zero timestamp, Syncing=False|Invalid PayloadAttributes, Parent timestamp, Syncing=False|Invalid PayloadAttributes, Missing BeaconRoot, Syncing=False|Suggested Fee Recipient Test|PrevRandao Opcode Transactions Test|Invalid Missing Ancestor ReOrg, StateRoot" + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ethereum_rust_image + path: /tmp + + - name: Load image + run: | + docker load --input /tmp/ethereum_rust_image.tar + + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Rustup toolchain install + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_VERSION }} + + - name: Setup Go + uses: actions/setup-go@v3 + + - name: Run Hive Simulation + run: ${{ matrix.run_command }} + + run-assertoor: + name: Assertoor - Stability Check + runs-on: ubuntu-latest + needs: [docker-build] + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: ethereum_rust_image + path: /tmp + + - name: Load image + run: | + docker load --input /tmp/ethereum_rust_image.tar + + - name: Setup kurtosis testnet and run assertoor tests + uses: ethpandaops/kurtosis-assertoor-github-action@v1 + with: + kurtosis_version: '1.3.1' + ethereum_package_url: 'github.com/lambdaclass/ethereum-package' + ethereum_package_branch: 'ethereum-rust-integration' + ethereum_package_args: './test_data/network_params.yaml' diff --git a/.github/workflows/levm_bench.yaml b/.github/workflows/levm_bench.yaml index 6a4aed5fa..d297972c2 100644 --- a/.github/workflows/levm_bench.yaml +++ b/.github/workflows/levm_bench.yaml @@ -2,10 +2,8 @@ name: LEVM benchmarks on: merge_group: - push: paths: - 'crates/vm/levm/**' - branches: [ main ] pull_request: paths: - 'crates/vm/levm/**' diff --git a/Makefile b/Makefile index 39440f534..4a6b3ff07 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,9 @@ TEST_PATTERN ?= / run-hive: build-image setup-hive ## 🧪 Run Hive testing suite cd hive && ./hive --sim $(SIMULATION) --client ethereumrust --sim.limit "$(TEST_PATTERN)" +run-hive-on-latest: setup-hive ## 🧪 Run Hive testing suite with the latest docker image + cd hive && ./hive --sim $(SIMULATION) --client ethereumrust --sim.limit "$(TEST_PATTERN)" + run-hive-debug: build-image setup-hive ## 🐞 Run Hive testing suite in debug mode cd hive && ./hive --sim $(SIMULATION) --client ethereumrust --sim.limit "$(TEST_PATTERN)" --docker.output From 4c18bd43314c043a46b995b76da78474bc4ee785 Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:16:09 -0300 Subject: [PATCH 49/49] feat(l2): standalone test for prover (#969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivation** There is a need to run the zkVMs efficiently and quickly to perform time measurements for performance estimations. **Description** The PR adds: - Standalone test for the prover. - CI for the prover running the test in `dev` mode - No Proof Generation. Closes #968 --------- Co-authored-by: Estéfano Bargas --- .github/workflows/ci.yaml | 24 +----- .github/workflows/l2_prover_ci.yaml | 57 +++++++++++++ Makefile | 2 +- crates/l2/Makefile | 3 + crates/l2/docs/prover.md | 80 +++++++++++++++--- crates/l2/prover/Cargo.toml | 7 ++ crates/l2/prover/Makefile | 10 +++ crates/l2/prover/src/prover.rs | 51 ++++++----- crates/l2/prover/tests/perf_zkvm.rs | 80 ++++++++++++++++++ .../l2/prover/zkvm/interface/guest/Cargo.toml | 3 +- .../prover/zkvm/interface/guest/src/main.rs | 8 +- crates/l2/utils/mod.rs | 1 + crates/l2/utils/test_data_io.rs | 72 ++++++++++++++++ test_data/l2-loadtest.rlp | Bin 0 -> 274983 bytes 14 files changed, 338 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/l2_prover_ci.yaml create mode 100644 crates/l2/prover/Makefile create mode 100644 crates/l2/prover/tests/perf_zkvm.rs create mode 100644 crates/l2/utils/test_data_io.rs create mode 100644 test_data/l2-loadtest.rlp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e5a31c14b..c9e4cd144 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,7 @@ jobs: - name: Run cargo clippy run: | - cargo clippy --all-targets --all-features --workspace -- -D warnings + cargo clippy --all-targets --all-features --workspace --exclude ethereum_rust-prover -- -D warnings - name: Run cargo fmt run: | @@ -84,25 +84,3 @@ jobs: context: . file: ./Dockerfile load: true # Important for building without pushing - - prover: - name: Build RISC-V zkVM program - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Rust toolchain install - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ env.RUST_VERSION }} - - - name: RISC-V zkVM toolchain install - run: | - curl -L https://risczero.com/install | bash - ~/.risc0/bin/rzup install - - - name: Build prover and zkVM - run: | - cd crates/l2/prover - cargo build --release --features build_zkvm diff --git a/.github/workflows/l2_prover_ci.yaml b/.github/workflows/l2_prover_ci.yaml new file mode 100644 index 000000000..d437832d0 --- /dev/null +++ b/.github/workflows/l2_prover_ci.yaml @@ -0,0 +1,57 @@ +name: L2 Prover CI +on: + push: + branches: ["main"] + paths: + - "crates/l2/prover/**" + pull_request: + branches: ["**"] + paths: + - "crates/l2/prover/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + action: + - command: check + args: -p ethereum_rust-prover + - command: clippy + args: -p ethereum_rust-prover --all-targets --no-default-features + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Add Rust Cache + uses: Swatinem/rust-cache@v2 + - name: ${{ matrix.action.command }} Command + run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }} + + build_and_test: + name: Build and Test RISC-V zkVM program + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Rust toolchain install + uses: dtolnay/rust-toolchain@stable + - name: RISC-V zkVM toolchain install + run: | + curl -L https://risczero.com/install | bash + ~/.risc0/bin/rzup install + - name: Caching + uses: Swatinem/rust-cache@v2 + - name: Build prover and zkVM + run: | + cd crates/l2/prover + cargo build --release --features build_zkvm + - name: Test Prover Execution + run: | + cd crates/l2/prover + RUST_LOG=info make perf_test_proving diff --git a/Makefile b/Makefile index 4a6b3ff07..dd958c872 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ SPECTEST_VECTORS_DIR := cmd/ef_tests/vectors CRATE ?= * test: $(SPECTEST_VECTORS_DIR) ## 🧪 Run each crate's tests - cargo test -p '$(CRATE)' --workspace + cargo test -p '$(CRATE)' --workspace --exclude ethereum_rust-prover clean: clean-vectors ## 🧹 Remove build artifacts cargo clean diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 23914ec55..a43fcc7d6 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -61,3 +61,6 @@ restart-l2: down-l2 init-l2 ## 🔄 Restarts the L2 Lambda Ethereum Rust Client init-l2-prover: ## 🚀 Initializes the Prover cargo run --release --features build_zkvm --manifest-path ../../Cargo.toml --bin ethereum_rust_prover + +init-l2-prover-gpu: ## 🚀 Initializes the Prover with GPU support + cargo run --release --features "build_zkvm,cuda" --manifest-path ../../Cargo.toml --bin ethereum_rust_prover diff --git a/crates/l2/docs/prover.md b/crates/l2/docs/prover.md index 88dcf2ceb..df3c45060 100644 --- a/crates/l2/docs/prover.md +++ b/crates/l2/docs/prover.md @@ -3,29 +3,28 @@ ## ToC - [ToC](#toc) -- [Prover](#prover) - - [How to Run](#how-to-run) +- [What](#what) - [Workflow](#workflow) +- [How](#how) + - [Dev Mode](#dev-mode) + - [Quick Test](#quick-test) + - [GPU mode](#gpu-mode) + - [Proving Process Test](#proving-process-test) - [Configuration](#configuration) >[!NOTE] > The shipping/deploying process and the `Prover` itself is under development. -## Prover +## What -The RISC-V zkVM Prover currently runs an empty program. To mock proof generation and avoid RAM requirements, you can use the following envar: `RISC0_DEV_MODE=1`. [risczero - dev-mode](https://dev.risczero.com/api/generating-proofs/dev-mode). +The prover consists of two main components: handling incoming proving data from the `L2 proposer`, specifically the `prover_server` component, and the `zkVM`. The `prover_client` is responsible for this first part, while the `zkVM` serves as a RISC-V emulator executing code specified in `crates/l2/prover/zkvm/interface/guest/src`. +Before the `zkVM` code (or guest), there is a directory called `interface`, which indicates that we access the `zkVM` through the "interface" crate. -### How to Run - -Dependencies: `cargo-risczero` [dev - risczero - installation](https://dev.risczero.com/api/zkvm/install) - -If you are at `crates/l2`, you will have to set the `.env` file (the `.example.env` can be used) and then run `make init-l2-prover`. - -The `build_zkvm` flag is used, if you don't have the risc0's "sdk", you can build the prover without the feature to check all the surrounding components of the `zkvm`. +In summary, the `prover_client` manages the inputs from the `prover_server` and then "calls" the `zkVM` to perform the proving process and generate the `groth16` ZK proof. ## Workflow -The `Prover Server` is monitoring requests for new jobs from the `Prover Client`, sent when the prover is free. When a new job arrives, the Prover will generate the proof and then the `Prover Client` will send it to the `Prover Server`. +The `Prover Server` monitors requests for new jobs from the `Prover Client`, which are sent when the prover is available. Upon receiving a new job, the Prover generates the proof, after which the `Prover Client` sends the proof back to the `Prover Server`. ```mermaid sequenceDiagram @@ -40,6 +39,63 @@ sequenceDiagram ProverServer-->>-ProverClient: ProofData::SubmitAck(block_number) ``` +## How + +### Dev Mode + +**Dependencies:** +- [RISC0](https://dev.risczero.com/api/zkvm/install) + +To run the blockchain (`proposer`) and prover in conjunction in a development environment, set the following environment variable: `RISC0_DEV_MODE=1` [(docs)](https://dev.risczero.com/api/generating-proofs/dev-mode). If you are in the `crates/l2` directory, you will need to set the environment variable for `dev_mode`. The `.env.example` file should suffice. + +To start the `prover_client`, use the following command: + +```sh +make init-l2-prover +``` + +The `build_zkvm` flag is used, if you don't have the risc0's "sdk", you can build the prover without the feature to check if all the surrounding components of the `zkvm` can be compiled. + +#### Quick Test + +To test the `zkvm` execution quickly, the following test can be run: + +```sh +cd crates/l2/prover +make perf_test_proving +``` + +### GPU mode + +**Dependencies (based on the Docker CUDA image):** + +>[!NOTE] +> If you don't want to run it inside a Docker container based on the NVIDIA CUDA image, [the following steps from RISC0](https://dev.risczero.com/api/generating-proofs/local-proving) may be helpful. + +- [Rust](https://www.rust-lang.org/tools/install) +- [RISC0](https://dev.risczero.com/api/zkvm/install) + +Next, install the following packages: + +```sh +sudo apt-get install libssl-dev pkg-config libclang-dev clang +``` + +To start the `prover_client`, use the following command: + +```sh +make init-l2-prover-gpu +``` + +#### Proving Process Test + +To test the `zkvm` proving process using a `gpu` quickly, the following test can be run: + +```sh +cd crates/l2/prover +make perf_gpu +``` + ## Configuration The following environment variables are available to configure the prover: diff --git a/crates/l2/prover/Cargo.toml b/crates/l2/prover/Cargo.toml index df2239f1c..3c93e46b5 100644 --- a/crates/l2/prover/Cargo.toml +++ b/crates/l2/prover/Cargo.toml @@ -16,6 +16,7 @@ hex.workspace = true # ethereum_rust ethereum_rust-core.workspace = true +ethereum_rust-vm.workspace = true ethereum_rust-rlp.workspace = true # l2 @@ -32,6 +33,11 @@ revm = { version = "14.0.3", features = [ "kzg-rs", ], default-features = false } +[dev-dependencies] +ethereum_rust-vm.workspace = true +ethereum_rust-storage.workspace = true +ethereum_rust-blockchain.workspace = true + [lib] name = "ethereum_rust_prover_lib" path = "src/lib.rs" @@ -43,3 +49,4 @@ path = "src/main.rs" [features] default = [] build_zkvm = ["zkvm_interface/build_zkvm"] +gpu = ["risc0-zkvm/cuda"] diff --git a/crates/l2/prover/Makefile b/crates/l2/prover/Makefile new file mode 100644 index 000000000..468e14c7a --- /dev/null +++ b/crates/l2/prover/Makefile @@ -0,0 +1,10 @@ +RISC0_DEV_MODE?=1 +RUST_LOG?="debug" +perf_test_proving: + @echo "Using RISC0_DEV_MODE: ${RISC0_DEV_MODE}" + RISC0_DEV_MODE=${RISC0_DEV_MODE} RUST_LOG=${RUST_LOG} cargo test --release --test perf_zkvm --features build_zkvm -- --show-output +.PHONY: perf_test_proving + +perf_gpu: + RUSTFLAGS="-C target-cpu=native" RISC0_DEV_MODE=0 RUST_LOG="debug" cargo test --release --test perf_zkvm --features "build_zkvm,gpu" -- --show-output +.PHONY: perf_gpu diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs index 4826755a6..1840d2b25 100644 --- a/crates/l2/prover/src/prover.rs +++ b/crates/l2/prover/src/prover.rs @@ -1,4 +1,4 @@ -use ethereum_rust_core::types::Block; +use serde::Deserialize; use tracing::info; // risc0 @@ -6,10 +6,24 @@ use zkvm_interface::methods::{ZKVM_PROGRAM_ELF, ZKVM_PROGRAM_ID}; use risc0_zkvm::{default_prover, ExecutorEnv, ExecutorEnvBuilder, ProverOpts}; +use ethereum_rust_core::types::Receipt; +use ethereum_rust_l2::{ + proposer::prover_server::ProverInputData, utils::config::prover_client::ProverClientConfig, +}; use ethereum_rust_rlp::encode::RLPEncode; - -use ethereum_rust_l2::proposer::prover_server::ProverInputData; -use ethereum_rust_l2::utils::config::prover_client::ProverClientConfig; +use ethereum_rust_vm::execution_db::ExecutionDB; + +// The order of variables in this structure should match the order in which they were +// committed in the zkVM, with each variable represented by a field. +#[derive(Debug, Deserialize)] +pub struct ProverOutputData { + /// It is rlp encoded, it has to be decoded. + /// Block::decode(&prover_output_data.block).unwrap()); + pub _block: Vec, + pub _execution_db: ExecutionDB, + pub _parent_block_header: Vec, + pub block_receipts: Vec, +} pub struct Prover<'a> { env_builder: ExecutorEnvBuilder<'a>, @@ -48,36 +62,33 @@ impl<'a> Prover<'a> { /// Example: /// let prover = Prover::new(); /// let proof = prover.set_input(inputs).prove().unwrap(); - pub fn prove(&mut self) -> Result { - let env = self - .env_builder - .build() - .map_err(|_| "Failed to Build env".to_string())?; + pub fn prove(&mut self) -> Result> { + let env = self.env_builder.build()?; // Generate the Receipt let prover = default_prover(); // Proof information by proving the specified ELF binary. // This struct contains the receipt along with statistics about execution of the guest - let prove_info = prover - .prove_with_opts(env, self.elf, &ProverOpts::groth16()) - .map_err(|_| "Failed to prove".to_string())?; + let prove_info = prover.prove_with_opts(env, self.elf, &ProverOpts::groth16())?; // extract the receipt. let receipt = prove_info.receipt; - let executed_block: Block = receipt.journal.decode().map_err(|err| err.to_string())?; - - info!( - "Successfully generated execution proof receipt for block {}", - executed_block.header.compute_block_hash() - ); + info!("Successfully generated execution receipt."); Ok(receipt) } - pub fn verify(&self, receipt: &risc0_zkvm::Receipt) -> Result<(), String> { + pub fn verify(&self, receipt: &risc0_zkvm::Receipt) -> Result<(), Box> { // Verify the proof. - receipt.verify(self.id).unwrap(); + receipt.verify(self.id)?; Ok(()) } + + pub fn get_commitment( + receipt: &risc0_zkvm::Receipt, + ) -> Result> { + let commitment: ProverOutputData = receipt.journal.decode()?; + Ok(commitment) + } } diff --git a/crates/l2/prover/tests/perf_zkvm.rs b/crates/l2/prover/tests/perf_zkvm.rs new file mode 100644 index 000000000..7fdec57d3 --- /dev/null +++ b/crates/l2/prover/tests/perf_zkvm.rs @@ -0,0 +1,80 @@ +use std::path::PathBuf; +use tracing::info; + +use ethereum_rust_blockchain::add_block; +use ethereum_rust_l2::proposer::prover_server::ProverInputData; +use ethereum_rust_prover_lib::prover::Prover; +use ethereum_rust_storage::{EngineType, Store}; +use ethereum_rust_vm::execution_db::ExecutionDB; + +#[tokio::test] +async fn test_performance_zkvm() { + tracing_subscriber::fmt::init(); + + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + // Go back 3 levels (Go to the root of the project) + for _ in 0..3 { + path.pop(); + } + path.push("test_data"); + + // Another use is genesis-execution-api.json in conjunction with chain.rlp(20 blocks not too loaded). + let genesis_file_path = path.join("genesis-l2.json"); + // l2-loadtest.rlp has blocks with many txs. + let chain_file_path = path.join("l2-loadtest.rlp"); + + let store = Store::new("memory", EngineType::InMemory).expect("Failed to create Store"); + + let genesis = ethereum_rust_l2::utils::test_data_io::read_genesis_file( + genesis_file_path.to_str().unwrap(), + ); + store.add_initial_state(genesis.clone()).unwrap(); + + let blocks = + ethereum_rust_l2::utils::test_data_io::read_chain_file(chain_file_path.to_str().unwrap()); + info!("Number of blocks to insert: {}", blocks.len()); + + for block in &blocks { + add_block(block, &store).unwrap(); + } + let block_to_prove = blocks.last().unwrap(); + + let db = ExecutionDB::from_exec(block_to_prove, &store).unwrap(); + + let parent_header = store + .get_block_header_by_hash(block_to_prove.header.parent_hash) + .unwrap() + .unwrap(); + + let input = ProverInputData { + db, + block: block_to_prove.clone(), + parent_header, + }; + + let mut prover = Prover::new(); + prover.set_input(input); + + let start = std::time::Instant::now(); + + let receipt = prover.prove().unwrap(); + + let duration = start.elapsed(); + info!( + "Number of EIP1559 transactions in the proven block: {}", + block_to_prove.body.transactions.len() + ); + info!("[SECONDS] Proving Took: {:?}", duration); + info!("[MINUTES] Proving Took: {}[m]", duration.as_secs() / 60); + + prover.verify(&receipt).unwrap(); + + let output = Prover::get_commitment(&receipt).unwrap(); + + let execution_cumulative_gas_used = output.block_receipts.last().unwrap().cumulative_gas_used; + info!("Cumulative Gas Used {execution_cumulative_gas_used}"); + + let gas_per_second = execution_cumulative_gas_used as f64 / duration.as_secs_f64(); + + info!("Gas per Second: {}", gas_per_second); +} diff --git a/crates/l2/prover/zkvm/interface/guest/Cargo.toml b/crates/l2/prover/zkvm/interface/guest/Cargo.toml index ff0d14608..a0f8c93e8 100644 --- a/crates/l2/prover/zkvm/interface/guest/Cargo.toml +++ b/crates/l2/prover/zkvm/interface/guest/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [workspace] [dependencies] -risc0-zkvm = { version = "1.1.2", default-features = false, features = ['std'] } +risc0-zkvm = { version = "1.1.2", default-features = false, features = ["std"] } ethereum_rust-core = { path = "../../../../../common", default-features = false } ethereum_rust-rlp = { path = "../../../../../common/rlp" } @@ -18,3 +18,4 @@ crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.6-risczero.0" } secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", branch = "patch-secp256k1-v0.29.1" } +ecdsa-core = { git = "https://github.com/sp1-patches/signatures", package = "ecdsa", branch = "patch-ecdsa-v0.16.9" } diff --git a/crates/l2/prover/zkvm/interface/guest/src/main.rs b/crates/l2/prover/zkvm/interface/guest/src/main.rs index 95eefaf92..967f0eeb1 100644 --- a/crates/l2/prover/zkvm/interface/guest/src/main.rs +++ b/crates/l2/prover/zkvm/interface/guest/src/main.rs @@ -1,4 +1,4 @@ -use ethereum_rust_rlp::{decode::RLPDecode, error::RLPDecodeError}; +use ethereum_rust_rlp::{decode::RLPDecode, encode::RLPEncode, error::RLPDecodeError}; use risc0_zkvm::guest::env; use ethereum_rust_blockchain::{validate_block, validate_gas_used}; @@ -14,6 +14,8 @@ fn main() { let receipts = execute_block(&block, &mut state).unwrap(); + env::commit(&receipts); + validate_gas_used(&receipts, &block.header).expect("invalid gas used"); let _account_updates = get_state_transitions(&mut state); @@ -31,9 +33,9 @@ fn read_inputs() -> Result<(Block, ExecutionDB, BlockHeader), RLPDecodeError> { let parent_header = BlockHeader::decode(&parent_header_bytes)?; // make inputs public - env::commit(&block); + env::commit(&block.encode_to_vec()); env::commit(&execution_db); - env::commit(&parent_header); + env::commit(&parent_header.encode_to_vec()); Ok((block, execution_db, parent_header)) } diff --git a/crates/l2/utils/mod.rs b/crates/l2/utils/mod.rs index 56e2b0a25..6b6123054 100644 --- a/crates/l2/utils/mod.rs +++ b/crates/l2/utils/mod.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod config; pub mod eth_client; pub mod merkle_tree; +pub mod test_data_io; pub fn secret_key_deserializer<'de, D>(deserializer: D) -> Result where diff --git a/crates/l2/utils/test_data_io.rs b/crates/l2/utils/test_data_io.rs new file mode 100644 index 000000000..dcc7833ca --- /dev/null +++ b/crates/l2/utils/test_data_io.rs @@ -0,0 +1,72 @@ +use ethereum_rust_core::types::{Block, Genesis}; +use ethereum_rust_rlp::{decode::RLPDecode, encode::RLPEncode}; +use ethereum_rust_storage::Store; +use tracing::info; + +use std::{ + fs::File, + io::{BufReader, Read as _, Write}, + path::PathBuf, +}; + +// From cmd/ethereum_rust +pub fn read_chain_file(chain_rlp_path: &str) -> Vec { + let chain_file = File::open(chain_rlp_path).expect("Failed to open chain rlp file"); + _chain_file(chain_file).expect("Failed to decode chain rlp file") +} + +// From cmd/ethereum_rust +pub fn read_genesis_file(genesis_file_path: &str) -> Genesis { + let genesis_file = std::fs::File::open(genesis_file_path).expect("Failed to open genesis file"); + _genesis_file(genesis_file).expect("Failed to decode genesis file") +} + +/// Generates a `test.rlp` file for use by the prover during testing. +/// Place this in the `proposer/mod.rs` file, +/// specifically in the `start` function, +/// before calling `send_commitment()` to send the block commitment. +pub fn generate_rlp( + up_to_block_number: u64, + block: Block, + store: &Store, +) -> Result<(), Box> { + if block.header.number == up_to_block_number { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let file_name = "l2-test.rlp"; + + path.push(file_name); + + let mut file = std::fs::File::create(path.to_str().unwrap())?; + for i in 1..up_to_block_number { + let body = store.get_block_body(i)?.unwrap(); + let header = store.get_block_header(i)?.unwrap(); + + let block = Block::new(header, body); + let vec = block.encode_to_vec(); + file.write_all(&vec)?; + } + + info!("TEST RLP GENERATED AT: {path:?}"); + } + Ok(()) +} + +// From cmd/ethereum_rust/decode.rs +fn _chain_file(file: File) -> Result, Box> { + let mut chain_rlp_reader = BufReader::new(file); + let mut buf = vec![]; + chain_rlp_reader.read_to_end(&mut buf)?; + let mut blocks = Vec::new(); + while !buf.is_empty() { + let (item, rest) = Block::decode_unfinished(&buf)?; + blocks.push(item); + buf = rest.to_vec(); + } + Ok(blocks) +} + +// From cmd/ethereum_rust/decode.rs +fn _genesis_file(file: File) -> Result { + let genesis_reader = BufReader::new(file); + serde_json::from_reader(genesis_reader) +} diff --git a/test_data/l2-loadtest.rlp b/test_data/l2-loadtest.rlp new file mode 100644 index 0000000000000000000000000000000000000000..75b0a1d607960bbcb23abf67fc0ac2ff476a895f GIT binary patch literal 274983 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 literal 0 HcmV?d00001