diff --git a/src/bin_evm_proxy/ethereum_types.ml b/src/bin_evm_proxy/ethereum_types.ml index 0a080e415da682eb4ea560b0676388ebfced040f..c528da50eb03e5b13eede78e38c76d3d54b6f037 100644 --- a/src/bin_evm_proxy/ethereum_types.ml +++ b/src/bin_evm_proxy/ethereum_types.ml @@ -41,8 +41,10 @@ type address = Address of string [@@ocaml.unboxed] let address_of_string s = Address (String.lowercase_ascii (strip_0x s)) +let address_to_string (Address a) = append_0x a + let address_encoding = - Data_encoding.(conv (fun (Address a) -> append_0x a) address_of_string string) + Data_encoding.(conv address_to_string address_of_string string) (** Ethereum generic quantity, always encoded in hexadecimal. *) type quantity = Qty of Z.t [@@ocaml.unboxed] @@ -136,6 +138,8 @@ let hash_to_bytes (Hash h) = Hex.to_bytes_exn (`Hex h) |> Bytes.to_string let hash_encoding = Data_encoding.(conv hash_to_string hash_of_string string) +let empty_hash = Hash "0x" + (** Ethereum block hash representation from RPCs. *) type block = { number : block_height option; @@ -424,13 +428,13 @@ let transaction_receipt_encoding = (req "contractAddress" (option address_encoding)))) type transaction_object = { - blockHash : block_hash; - blockNumber : quantity; + blockHash : block_hash option; + blockNumber : quantity option; from : address; gas : quantity; gasPrice : quantity; hash : hash; - input : hash option; + input : hash; nonce : quantity; to_ : address option; transactionIndex : quantity; @@ -500,13 +504,13 @@ let transaction_object_encoding = }) (merge_objs (obj10 - (req "blockHash" block_hash_encoding) - (req "blockNumber" quantity_encoding) + (req "blockHash" (option block_hash_encoding)) + (req "blockNumber" (option quantity_encoding)) (req "from" address_encoding) (req "gas" quantity_encoding) (req "gasPrice" quantity_encoding) (req "hash" hash_encoding) - (req "input" (option hash_encoding)) + (req "input" hash_encoding) (req "nonce" quantity_encoding) (req "to" (option address_encoding)) (req "transactionIndex" quantity_encoding)) @@ -565,3 +569,102 @@ let call_encoding = (opt "gasPrice" quantity_encoding) (opt "value" quantity_encoding) (req "data" (option hash_encoding))) + +(** The txpool encoding can be found in + https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-txpool#txpool-content. + + Basically, `txpool_content` is a map associating addresses to counters and + transactions. In JSON, it is encoded as an object associating addresses as + fields to objects that contain counters as field and transaction objects as + values. I.e., the `txpool_` encodes it as: + + ``` + { "address1" : + { "counter1" : , + "counter2" : , + ... + }, + "address2" : + { "counter1" : , + "counter2" : , + ... + }, + ... + } + ``` + + As such, the encoding uses Ezjsonm representation directly to encode and + decode the txpool. +*) +module MapMake (Key : sig + include Stdlib.Map.OrderedType + + val to_string : t -> string + + val of_string : string -> t +end) : sig + include Map.S with type key = Key.t + + val associative_array_encoding : 'a Data_encoding.t -> 'a t Data_encoding.t +end = struct + module Instance = Map.Make (Key) + + let associative_array_encoding value_encoding = + let open Data_encoding in + conv + (fun map -> + let bindings = Instance.bindings map in + let fields = + List.map + (fun (name, value) -> + (Key.to_string name, Json.construct value_encoding value)) + bindings + in + `O fields) + (function + | `O fields -> + let bindings = + List.filter_map + (fun (name, value) -> + try + Some (Key.of_string name, Json.destruct value_encoding value) + with _ -> None) + fields + |> List.to_seq + in + Instance.of_seq bindings + | _ -> Instance.empty) + Json.encoding + + include Instance +end + +module NonceMap = MapMake (Z) + +module Address = struct + type t = address + + let compare (Address h) (Address h') = String.compare h h' + + let to_string = address_to_string + + let of_string = address_of_string +end + +module AddressMap = MapMake (Address) + +type txpool = { + pending : transaction_object NonceMap.t AddressMap.t; + queued : transaction_object NonceMap.t AddressMap.t; +} + +let txpool_encoding = + let open Data_encoding in + let field_encoding = + AddressMap.associative_array_encoding + (NonceMap.associative_array_encoding transaction_object_encoding) + in + conv + (fun {pending; queued} -> (pending, queued)) + (fun (pending, queued) -> {pending; queued}) + (obj2 (req "pending" field_encoding) (req "queued" field_encoding)) diff --git a/src/bin_evm_proxy/mockup.ml b/src/bin_evm_proxy/mockup.ml index 8dcb5c75ce894cdd90bf2f61f67e2d888a836cc5..1aac57b86a53fcb3952e60689b79fd8047035fac 100644 --- a/src/bin_evm_proxy/mockup.ml +++ b/src/bin_evm_proxy/mockup.ml @@ -95,14 +95,20 @@ let block () = uncles = []; } +let bootstrap_address = + address_of_string "0x6F4d14B90C48bEFb49CA3fe6663dEC70731A8bC7" + +let bootstrap_address2 = + address_of_string "0xA5A5bf58c7Dc91cBE5005A7E5c6314998Eda479E" + let transaction_receipt () = { transactionHash = transaction_hash; transactionIndex = qty_f Z.zero; blockHash = block_hash; blockNumber = qty_f @@ Z.of_int !block_height_counter; - from = address_of_string "0x6F4d14B90C48bEFb49CA3fe6663dEC70731A8bC7"; - to_ = Some (address_of_string "0xA5A5bf58c7Dc91cBE5005A7E5c6314998Eda479E"); + from = bootstrap_address; + to_ = Some bootstrap_address2; contractAddress = Some (address_of_string "0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c"); cumulativeGasUsed = gas_price; @@ -116,13 +122,13 @@ let transaction_receipt () = let transaction_object = { - blockHash = block_hash; - blockNumber = qty_f @@ Z.of_int 42; + blockHash = Some block_hash; + blockNumber = Some (qty_f @@ Z.of_int 42); from = address_of_string "0x6F4d14B90C48bEFb49CA3fe6663dEC70731A8bC7"; gas = qty_f Z.zero; gasPrice = qty_f Z.zero; hash = transaction_hash; - input = None; + input = empty_hash; nonce = qty_f Z.zero; to_ = Some (address_of_string "0xA5A5bf58c7Dc91cBE5005A7E5c6314998Eda479E"); transactionIndex = qty_f Z.zero; @@ -132,7 +138,7 @@ let transaction_object = s = hash_f @@ "00"; } -let call = hash_f "0x" +let call = empty_hash let return = Lwt_result_syntax.return @@ -156,3 +162,56 @@ let nth_block ~full_transaction_object:_ _n = return (block ()) let transaction_receipt _tx_hash = return (transaction_receipt ()) let transaction_object _tx_hash = return (Some transaction_object) + +let tx_pending = + { + blockHash = None; + blockNumber = None; + from = bootstrap_address; + gas = qty_f Z.zero; + gasPrice = qty_f Z.zero; + hash = + Hash "af953a2d01f55cfe080c0c94150a60105e8ac3d51153058a1f03dd239dd08586"; + input = empty_hash; + nonce = qty_f Z.zero; + to_ = Some bootstrap_address2; + transactionIndex = qty_f Z.zero; + value = qty_f Z.zero; + v = qty_f Z.zero; + r = hash_f @@ "00"; + s = hash_f @@ "00"; + } + +let tx_queued = + { + blockHash = None; + blockNumber = None; + from = bootstrap_address2; + gas = qty_f Z.zero; + gasPrice = qty_f Z.zero; + hash = + Hash "af953a2d01f55cfe080c0c94150a60105e8ac3d51153058a1f03dd239dd08588"; + input = empty_hash; + nonce = qty_f Z.zero; + to_ = Some bootstrap_address; + transactionIndex = qty_f Z.zero; + value = qty_f Z.zero; + v = qty_f Z.zero; + r = hash_f @@ "00"; + s = hash_f @@ "00"; + } + +let txpool _ = + return + { + pending = + AddressMap.add + bootstrap_address + (NonceMap.add Z.zero tx_pending NonceMap.empty) + AddressMap.empty; + queued = + AddressMap.add + bootstrap_address2 + (NonceMap.add Z.zero tx_queued NonceMap.empty) + AddressMap.empty; + } diff --git a/src/bin_evm_proxy/rollup_node.ml b/src/bin_evm_proxy/rollup_node.ml index a7bdb839464fb4a8700e38a9dae4c8b9a8ba5ccd..d3c24facc325fca9b5973c0f1e90c94867050768 100644 --- a/src/bin_evm_proxy/rollup_node.ml +++ b/src/bin_evm_proxy/rollup_node.ml @@ -583,13 +583,13 @@ module RPC = struct in return_some { - blockHash = block_hash; - blockNumber = block_number; + blockHash = Some block_hash; + blockNumber = Some block_number; from; gas = gas_used; gasPrice = gas_price; hash; - input; + input = Option.value ~default:(Hash "") input; nonce; to_; transactionIndex = index; @@ -598,6 +598,9 @@ module RPC = struct r; s; } + + let txpool _ () = + Lwt.return_ok {pending = AddressMap.empty; queued = AddressMap.empty} end module type S = sig @@ -626,6 +629,8 @@ module type S = sig val transaction_object : Ethereum_types.hash -> Ethereum_types.transaction_object option tzresult Lwt.t + + val txpool : unit -> Ethereum_types.txpool tzresult Lwt.t end module Make (Base : sig @@ -650,4 +655,6 @@ end) : S = struct let transaction_receipt = RPC.transaction_receipt Base.base let transaction_object = RPC.transaction_object Base.base + + let txpool = RPC.txpool Base.base end diff --git a/src/bin_evm_proxy/rollup_node.mli b/src/bin_evm_proxy/rollup_node.mli index 8129c5fb20d9fbcbf7744f02aec8f5a4ad060b16..d89089548d343c222bf507eee357ff71d6f33544 100644 --- a/src/bin_evm_proxy/rollup_node.mli +++ b/src/bin_evm_proxy/rollup_node.mli @@ -78,6 +78,9 @@ module type S = sig val transaction_object : Ethereum_types.hash -> Ethereum_types.transaction_object option tzresult Lwt.t + + (** [txpool ()] returns the pending and queued transactions. *) + val txpool : unit -> Ethereum_types.txpool tzresult Lwt.t end (** Instantiate a module of type {!S} that communicates with a rollup diff --git a/src/bin_evm_proxy/rpc_encodings.ml b/src/bin_evm_proxy/rpc_encodings.ml index 8cc2952775c20277a02c49383685c70d8d22d8d4..8edd4c86ee05ad077d3cf1b50d41f9bf480e0929 100644 --- a/src/bin_evm_proxy/rpc_encodings.ml +++ b/src/bin_evm_proxy/rpc_encodings.ml @@ -406,6 +406,20 @@ module Get_estimate_gas = MethodMaker (struct let method_ = "eth_estimateGas" end) +module Txpool_content = MethodMaker (struct + open Ethereum_types + + type input = unit + + type output = txpool + + let input_encoding = Data_encoding.unit + + let output_encoding = txpool_encoding + + let method_ = "txpool_content" +end) + let methods : (module METHOD) list = [ (module Network_id); @@ -424,6 +438,7 @@ let methods : (module METHOD) list = (module Send_raw_transaction); (module Eth_call); (module Get_estimate_gas); + (module Txpool_content); ] module Input = struct diff --git a/src/bin_evm_proxy/rpc_encodings.mli b/src/bin_evm_proxy/rpc_encodings.mli index 80501dfc1c953e5e70890ed0b1a0caac0f428eb6..cd4b6a6532832c4c6b9705feaeef4bf2cc9ecb73 100644 --- a/src/bin_evm_proxy/rpc_encodings.mli +++ b/src/bin_evm_proxy/rpc_encodings.mli @@ -249,3 +249,6 @@ module Get_estimate_gas : METHOD with type m_input = Ethereum_types.call and type m_output = Ethereum_types.quantity + +module Txpool_content : + METHOD with type m_input = unit and type m_output = Ethereum_types.txpool diff --git a/src/bin_evm_proxy/services.ml b/src/bin_evm_proxy/services.ml index 184de541a5e0c359642f387fc82aaff032b07a89..7be91ae0428d476ce22555ad923c3005a30c46b9 100644 --- a/src/bin_evm_proxy/services.ml +++ b/src/bin_evm_proxy/services.ml @@ -127,6 +127,9 @@ let dispatch_input | Eth_call.Input _ -> return (Eth_call.Output (Ok Mockup.call)) | Get_estimate_gas.Input _ -> return (Get_estimate_gas.Output (Ok Mockup.gas_price)) + | Txpool_content.Input _ -> + let* txpool = Rollup_node_rpc.txpool () in + return (Txpool_content.Output (Ok txpool)) | _ -> Error_monad.failwith "Unsupported method\n%!" in return (output, id) diff --git a/tezt/lib_tezos/evm_proxy_server.ml b/tezt/lib_tezos/evm_proxy_server.ml index 9da6cc5f8ca3a8abdabf1b662ef1753a60e554f9..b418a4e5f4eabd91a87418b2f552722c1f64b44a 100644 --- a/tezt/lib_tezos/evm_proxy_server.ml +++ b/tezt/lib_tezos/evm_proxy_server.ml @@ -29,7 +29,7 @@ module Parameters = struct mutable pending_ready : unit option Lwt.u list; rpc_addr : string; rpc_port : int; - rollup_node : Sc_rollup_node.t; + rollup_node : Sc_rollup_node.t option; runner : Runner.t option; } @@ -95,7 +95,11 @@ let wait_for_ready proxy_server = check_event proxy_server event_ready_name promise let create ?runner ?rpc_addr ?rpc_port rollup_node = - let rollup_node_endpoint = Sc_rollup_node.endpoint rollup_node in + let rollup_node_endpoint = + match rollup_node with + | None -> "mockup" + | Some rollup_node -> Sc_rollup_node.endpoint rollup_node + in let arguments, rpc_addr, rpc_port = connection_arguments ?rpc_addr ?rpc_port rollup_node_endpoint in @@ -107,6 +111,12 @@ let create ?runner ?rpc_addr ?rpc_port rollup_node = on_event proxy_server (handle_event proxy_server) ; proxy_server +let mockup ?runner ?rpc_addr ?rpc_port () = + create ?runner ?rpc_addr ?rpc_port None + +let create ?runner ?rpc_addr ?rpc_port rollup_node = + create ?runner ?rpc_addr ?rpc_port (Some rollup_node) + let run proxy_server = let* () = run @@ -172,3 +182,31 @@ let fetch_contract_code evm_proxy_server contract_address = } in return JSON.(code |-> "result" |> as_string) + +type txpool_slot = {address : string; transactions : (int64 * JSON.t) list} + +let txpool_content evm_proxy_server = + let* txpool = + call_evm_rpc + evm_proxy_server + {method_ = "txpool_content"; parameters = `A []} + in + Log.info "Result: %s" (JSON.encode txpool) ; + let txpool = JSON.(txpool |-> "result") in + let parse field = + let open JSON in + let pool = txpool |-> field in + (* `|->` returns `Null if the field does not exists, and `Null is + interpreted as the empty list by `as_object`. As such, we must ensure the + field exists. *) + if is_null pool then Test.fail "%s must exists" field + else + pool |> as_object + |> List.map (fun (address, transactions) -> + let transactions = + transactions |> as_object + |> List.map (fun (nonce, tx) -> (Int64.of_string nonce, tx)) + in + {address; transactions}) + in + return (parse "pending", parse "queued") diff --git a/tezt/lib_tezos/evm_proxy_server.mli b/tezt/lib_tezos/evm_proxy_server.mli index 524c8bc2e809df83e8d6cb737678b8d6c5435a7d..ac2c6188ee4063e82e13fa898ad7446fe9b33eb0 100644 --- a/tezt/lib_tezos/evm_proxy_server.mli +++ b/tezt/lib_tezos/evm_proxy_server.mli @@ -38,6 +38,10 @@ type t val create : ?runner:Runner.t -> ?rpc_addr:string -> ?rpc_port:int -> Sc_rollup_node.t -> t +(** [mockup ?runner ?rpc_addr ?rpc_port ()] is like [create] but doesn't + communicate with a [rollup_node] and serves mockup values. *) +val mockup : ?runner:Runner.t -> ?rpc_addr:string -> ?rpc_port:int -> unit -> t + (** [run proxy_server] launches the EVM proxy server with the arguments given during {!create}. *) val run : t -> unit Lwt.t @@ -72,3 +76,11 @@ val batch_evm_rpc : t -> request list -> JSON.t Lwt.t (** [fetch_contract_code proxy_server contract] returns the code associated to the given contract in the rollup. *) val fetch_contract_code : t -> string -> string Lwt.t + +(** A slot in the transaction pool associates an address to a mapping of nonces + to transactions. *) +type txpool_slot = {address : string; transactions : (int64 * JSON.t) list} + +(** [txpool_content proxy_server] returns the transaction hash and nonce + contained in the `pending` and `queued` pools. *) +val txpool_content : t -> (txpool_slot list * txpool_slot list) Lwt.t diff --git a/tezt/tests/evm_rollup.ml b/tezt/tests/evm_rollup.ml index 393b500d7af0a6172adc250a277080815cc4b3b3..618ddbf9dd49c29ee21538ed27ed29073d046bc6 100644 --- a/tezt/tests/evm_rollup.ml +++ b/tezt/tests/evm_rollup.ml @@ -160,6 +160,11 @@ let setup_past_genesis ?originator_key ?rollup_operator_key protocol = let* _level = next_evm_level ~sc_rollup_node ~node ~client in return full_setup +let setup_mockup () = + let evm_proxy_server = Evm_proxy_server.mockup () in + let* () = Evm_proxy_server.run evm_proxy_server in + return evm_proxy_server + let test_evm_proxy_server_connection = Protocol.register_test ~__FILE__ @@ -519,6 +524,20 @@ let test_chunked_transaction = ~title:"Check L2 chunked transfers are applied" @@ transfer ~data:("0x" ^ String.make 12_000 'a') +let test_rpc_txpool_content = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "txpool_content"] + ~title:"Check RPC txpool_content is available" + @@ fun _protocol -> + let* evm_proxy_server = setup_mockup () in + (* The content of the txpool is not relevant for now, this test only checks + the the RPC is correct, i.e. an object containing both the `pending` and + `queued` fields, containing the correct objects: addresses pointing to a + mapping of nonces to transactions. *) + let* _result = Evm_proxy_server.txpool_content evm_proxy_server in + unit + let register_evm_proxy_server ~protocols = test_originate_evm_kernel protocols ; test_evm_proxy_server_connection protocols ; @@ -530,6 +549,7 @@ let register_evm_proxy_server ~protocols = test_l2_blocks_progression protocols ; test_l2_transfer protocols ; test_chunked_transaction protocols ; - test_l2_deploy protocols + test_l2_deploy protocols ; + test_rpc_txpool_content protocols let register ~protocols = register_evm_proxy_server ~protocols