diff --git a/etherlink/bin_node/config/configuration.ml b/etherlink/bin_node/config/configuration.ml index 3e4c075646ffafa735019981143f59f36df290b8..0899b1e6568b560dd62f2390ca9d19b29008bd82 100644 --- a/etherlink/bin_node/config/configuration.ml +++ b/etherlink/bin_node/config/configuration.ml @@ -22,6 +22,7 @@ type sequencer = { preimages : string; preimages_endpoint : Uri.t option; time_between_blocks : time_between_blocks; + max_number_of_chunks : int; private_rpc_port : int option; sequencer : Signature.public_key_hash; } @@ -74,6 +75,16 @@ let default_evm_node_endpoint = Uri.empty let default_time_between_blocks = Time_between_blocks 5. +let hard_maximum_number_of_chunks = + (* The kernel doesn't accept blueprints whose cumulated chunk size is higher + than 512kb. *) + let max_cumulated_chunks_size = 512 * 1024 in + (* External message size *) + let chunk_size = 4095 in + max_cumulated_chunks_size / chunk_size + +let default_max_number_of_chunks = hard_maximum_number_of_chunks + let log_filter_config_encoding : log_filter_config Data_encoding.t = let open Data_encoding in conv @@ -114,6 +125,7 @@ let encoding_sequencer = preimages_endpoint; rollup_node_endpoint; time_between_blocks; + max_number_of_chunks; private_rpc_port; sequencer; } -> @@ -121,12 +133,14 @@ let encoding_sequencer = preimages_endpoint, Uri.to_string rollup_node_endpoint, time_between_blocks, + max_number_of_chunks, private_rpc_port, sequencer )) (fun ( preimages, preimages_endpoint, rollup_node_endpoint, time_between_blocks, + max_number_of_chunks, private_rpc_port, sequencer ) -> { @@ -134,10 +148,11 @@ let encoding_sequencer = preimages_endpoint; rollup_node_endpoint = Uri.of_string rollup_node_endpoint; time_between_blocks; + max_number_of_chunks; private_rpc_port; sequencer; }) - (obj6 + (obj7 (dft "preimages" string default_preimages) (opt "preimages_endpoint" Tezos_rpc.Encoding.uri_encoding) (dft @@ -148,6 +163,7 @@ let encoding_sequencer = "time_between_blocks" encoding_time_between_blocks default_time_between_blocks) + (dft "max_number_of_chunks" int31 default_max_number_of_chunks) (opt "private-rpc-port" ~description:"RPC port for private server" @@ -296,7 +312,8 @@ module Cli = struct let create_sequencer ?private_rpc_port ~devmode ?rpc_addr ?rpc_port ?cors_origins ?cors_headers ?log_filter ?rollup_node_endpoint ?preimages - ?preimages_endpoint ?time_between_blocks ~sequencer = + ?preimages_endpoint ?time_between_blocks ?max_number_of_chunks ~sequencer + = let mode = { rollup_node_endpoint = @@ -307,6 +324,10 @@ module Cli = struct preimages_endpoint; time_between_blocks = Option.value ~default:default_time_between_blocks time_between_blocks; + max_number_of_chunks = + Option.value + ~default:default_max_number_of_chunks + max_number_of_chunks; private_rpc_port; sequencer; } @@ -375,7 +396,7 @@ module Cli = struct let patch_sequencer_configuration_from_args ?private_rpc_port ~devmode ?rpc_addr ?rpc_port ?cors_origins ?cors_headers ?log_filter ?rollup_node_endpoint ?preimages ?preimages_endpoint ?time_between_blocks - configuration ~sequencer = + ?max_number_of_chunks configuration ~sequencer = let mode = { rollup_node_endpoint = @@ -389,6 +410,10 @@ module Cli = struct Option.value ~default:configuration.mode.time_between_blocks time_between_blocks; + max_number_of_chunks = + Option.value + ~default:default_max_number_of_chunks + max_number_of_chunks; private_rpc_port; sequencer; } @@ -495,7 +520,7 @@ module Cli = struct let create_or_read_sequencer_config ~data_dir ~devmode ?rpc_addr ?rpc_port ?private_rpc_port ?cors_origins ?cors_headers ?log_filter ?rollup_node_endpoint ?preimages ?preimages_endpoint ?time_between_blocks - ~sequencer () = + ?max_number_of_chunks ~sequencer () = create_or_read_config ~data_dir ~devmode @@ -512,6 +537,7 @@ module Cli = struct ?preimages ?preimages_endpoint ?time_between_blocks + ?max_number_of_chunks ~sequencer) ~create: (create_sequencer @@ -519,6 +545,7 @@ module Cli = struct ?preimages ?preimages_endpoint ?time_between_blocks + ?max_number_of_chunks ~sequencer) () diff --git a/etherlink/bin_node/config/configuration.mli b/etherlink/bin_node/config/configuration.mli index 7ac03c3a16ba9cf800feb78e390a942d9bc97ba2..09ad23786790f8edc63ccd86db78055b9009562f 100644 --- a/etherlink/bin_node/config/configuration.mli +++ b/etherlink/bin_node/config/configuration.mli @@ -32,6 +32,8 @@ type sequencer = { preimages_endpoint : Uri.t option; (** Endpoint where pre-images can be fetched individually when missing. *) time_between_blocks : time_between_blocks; (** See {!time_between_blocks}. *) + max_number_of_chunks : int; + (** The maximum number of chunks per blueprints. *) private_rpc_port : int option; (** Port for internal RPC services *) sequencer : Signature.public_key_hash; (** The key used to sign the blueprints. *) @@ -108,6 +110,7 @@ module Cli : sig ?preimages:string -> ?preimages_endpoint:Uri.t -> ?time_between_blocks:time_between_blocks -> + ?max_number_of_chunks:int -> sequencer:Signature.public_key_hash -> unit -> sequencer t @@ -150,6 +153,7 @@ module Cli : sig ?preimages:string -> ?preimages_endpoint:Uri.t -> ?time_between_blocks:time_between_blocks -> + ?max_number_of_chunks:int -> sequencer:Signature.public_key_hash -> unit -> sequencer t tzresult Lwt.t diff --git a/etherlink/bin_node/lib_dev/block_producer.ml b/etherlink/bin_node/lib_dev/block_producer.ml index 743ed14aab6a2e5c8623a9a2f16a8b80f63774a0..ee0da822950d13fca25ad9e84839e1a5201f8210 100644 --- a/etherlink/bin_node/lib_dev/block_producer.ml +++ b/etherlink/bin_node/lib_dev/block_producer.ml @@ -9,8 +9,54 @@ type parameters = { cctxt : Client_context.wallet; smart_rollup_address : string; sequencer_key : Client_keys.sk_uri; + maximum_number_of_chunks : int; } +(* The size of a delayed transaction is overapproximated to the maximum size + of an inbox message, as chunks are not supported in the delayed bridge. *) +let maximum_delayed_transaction_size = 4096 + +(* + The legacy transactions are as follows: + ----------------------------- + | Nonce | Up to 32 bytes | + ----------------------------- + | GasPrice | Up to 32 bytes | + ----------------------------- + | GasLimit | Up to 32 bytes | + ----------------------------- + | To | 20 bytes addr | + ----------------------------- + | Value | Up to 32 bytes | + ----------------------------- + | Data | 0 - unlimited | + ----------------------------- + | V | 1 (usually) | + ----------------------------- + | R | 32 bytes | + ----------------------------- + | S | 32 bytes | + ----------------------------- + + where `up to` start at 0, and encoded as the empty byte for the 0 value + according to RLP specification. +*) +let minimum_ethereum_transaction_size = + Rlp.( + List + [ + Value Bytes.empty; + Value Bytes.empty; + Value Bytes.empty; + Value (Bytes.make 20 '\000'); + Value Bytes.empty; + Value Bytes.empty; + Value Bytes.empty; + Value (Bytes.make 32 '\000'); + Value (Bytes.make 32 '\000'); + ] + |> encode |> Bytes.length) + module Types = struct type nonrec parameters = parameters @@ -74,51 +120,77 @@ let get_hashes ~transactions ~delayed_transactions = in return (delayed_transactions @ hashes) +let take_delayed_transactions maximum_number_of_chunks = + let open Lwt_result_syntax in + let maximum_cumulative_size = + Sequencer_blueprint.maximum_usable_space_in_blueprint + maximum_number_of_chunks + in + let maximum_delayed_transactions = + maximum_cumulative_size / maximum_delayed_transaction_size + in + let* delayed_transactions = Evm_context.delayed_inbox_hashes () in + let delayed_transactions = + List.take_n maximum_delayed_transactions delayed_transactions + in + let remaining_cumulative_size = + maximum_cumulative_size - (List.length delayed_transactions * 4096) + in + return (delayed_transactions, remaining_cumulative_size) + let produce_block ~cctxt ~smart_rollup_address ~sequencer_key ~force ~timestamp - = + ~maximum_number_of_chunks = let open Lwt_result_syntax in - let* tx_pool_response = Tx_pool.pop_transactions () in - match tx_pool_response with - | Transactions transactions -> - let* delayed_transactions = Evm_context.delayed_inbox_hashes () in - let n = List.length transactions + List.length delayed_transactions in - if force || n > 0 then - let*! head_info = Evm_context.head_info () in + let* is_locked = Tx_pool.is_locked () in + if is_locked then + let*! () = Block_producer_events.production_locked () in + return 0 + else + let* delayed_transactions, remaining_cumulative_size = + take_delayed_transactions maximum_number_of_chunks + in + let* transactions = + (* Low key optimization to avoid even checking the txpool if there is not + enough space for the smallest transaction. *) + if remaining_cumulative_size <= minimum_ethereum_transaction_size then + return [] + else + Tx_pool.pop_transactions + ~maximum_cumulative_size:remaining_cumulative_size + in + let n = List.length transactions + List.length delayed_transactions in + if force || n > 0 then + let*! head_info = Evm_context.head_info () in + Helpers.with_timing + (Blueprint_events.blueprint_production head_info.next_blueprint_number) + @@ fun () -> + let*? hashes = get_hashes ~transactions ~delayed_transactions in + let* blueprint = Helpers.with_timing - (Blueprint_events.blueprint_production - head_info.next_blueprint_number) + (Blueprint_events.blueprint_proposal head_info.next_blueprint_number) @@ fun () -> - let*? hashes = get_hashes ~transactions ~delayed_transactions in - let* blueprint = - Helpers.with_timing - (Blueprint_events.blueprint_proposal - head_info.next_blueprint_number) - @@ fun () -> - Sequencer_blueprint.create - ~sequencer_key - ~cctxt - ~timestamp - ~smart_rollup_address - ~transactions - ~delayed_transactions - ~parent_hash:head_info.current_block_hash - ~number:head_info.next_blueprint_number - in - let* () = - Evm_context.apply_blueprint timestamp blueprint delayed_transactions - in - let (Qty number) = head_info.next_blueprint_number in - let* () = Blueprints_publisher.publish number blueprint in - let*! () = - List.iter_p - (fun hash -> Block_producer_events.transaction_selected ~hash) - hashes - in - return n - else return 0 - | Locked -> - let*! () = Block_producer_events.production_locked () in - return 0 + Sequencer_blueprint.create + ~sequencer_key + ~cctxt + ~timestamp + ~smart_rollup_address + ~transactions + ~delayed_transactions + ~parent_hash:head_info.current_block_hash + ~number:head_info.next_blueprint_number + in + let* () = + Evm_context.apply_blueprint timestamp blueprint delayed_transactions + in + let (Qty number) = head_info.next_blueprint_number in + let* () = Blueprints_publisher.publish number blueprint in + let*! () = + List.iter_p + (fun hash -> Block_producer_events.transaction_selected ~hash) + hashes + in + return n + else return 0 module Handlers = struct type self = worker @@ -132,13 +204,21 @@ module Handlers = struct match request with | Request.Produce_block (timestamp, force) -> protect @@ fun () -> - let {cctxt; smart_rollup_address; sequencer_key} = state in + let { + cctxt; + smart_rollup_address; + sequencer_key; + maximum_number_of_chunks; + } = + state + in produce_block ~cctxt ~smart_rollup_address ~sequencer_key ~force ~timestamp + ~maximum_number_of_chunks type launch_error = error trace diff --git a/etherlink/bin_node/lib_dev/block_producer.mli b/etherlink/bin_node/lib_dev/block_producer.mli index 06e4e4243e668cb28de782da186f7ea38f14081b..457eaf7add09c664acfe25a80fe6c699594fb99b 100644 --- a/etherlink/bin_node/lib_dev/block_producer.mli +++ b/etherlink/bin_node/lib_dev/block_producer.mli @@ -9,6 +9,7 @@ type parameters = { cctxt : Client_context.wallet; smart_rollup_address : string; sequencer_key : Client_keys.sk_uri; + maximum_number_of_chunks : int; } (** [start parameters] starts the events follower. *) diff --git a/etherlink/bin_node/lib_dev/sequencer.ml b/etherlink/bin_node/lib_dev/sequencer.ml index 5a98de78063dfe13f0d15377945ea9753d712937..f819c2dc590e725e890711d9993c7a35293824d2 100644 --- a/etherlink/bin_node/lib_dev/sequencer.ml +++ b/etherlink/bin_node/lib_dev/sequencer.ml @@ -242,7 +242,12 @@ let main ~data_dir ~rollup_node_endpoint ~max_blueprints_lag in let* () = Block_producer.start - {cctxt; smart_rollup_address; sequencer_key = sequencer} + { + cctxt; + smart_rollup_address; + sequencer_key = sequencer; + maximum_number_of_chunks = configuration.mode.max_number_of_chunks; + } in let* () = Evm_events_follower.start diff --git a/etherlink/bin_node/lib_dev/sequencer_blueprint.ml b/etherlink/bin_node/lib_dev/sequencer_blueprint.ml index b136a4fcce9b18cf2bcaa0c5f9bfa0c0dbc666f7..be819f0657488d8ffa8403637c2cb60bea85235f 100644 --- a/etherlink/bin_node/lib_dev/sequencer_blueprint.ml +++ b/etherlink/bin_node/lib_dev/sequencer_blueprint.ml @@ -41,6 +41,11 @@ let max_chunk_size = - blueprint_tag_size - blueprint_number_size - nb_chunks_size - chunk_index_size - rlp_tags_size - signature_size +let maximum_usable_space_in_blueprint chunks_count = + chunks_count * max_chunk_size + +let maximum_chunks_per_l1_level = 512 * 1024 / 4096 + let encode_transaction raw = let open Rlp in Value (Bytes.of_string raw) diff --git a/etherlink/bin_node/lib_dev/sequencer_blueprint.mli b/etherlink/bin_node/lib_dev/sequencer_blueprint.mli index bafec7020a9022601212813613271616be8beb3b..78fc1777a3c21fb5d7bfd33b36cc61e6cd3f454f 100644 --- a/etherlink/bin_node/lib_dev/sequencer_blueprint.mli +++ b/etherlink/bin_node/lib_dev/sequencer_blueprint.mli @@ -23,3 +23,11 @@ val create : delayed_transactions:Ethereum_types.hash list -> transactions:string list -> t tzresult Lwt.t + +(** [maximum_usable_size_in_blueprint chunks_count] returns the available space + for transactions in a blueprint composed of [chunks_count] chunks. *) +val maximum_usable_space_in_blueprint : int -> int + +(* [maximum_chunks_per_l1_level] is the maximum number of chunks a L1 block can + hold at once. *) +val maximum_chunks_per_l1_level : int diff --git a/etherlink/bin_node/lib_dev/tx_pool.ml b/etherlink/bin_node/lib_dev/tx_pool.ml index 00ca1c3614232ba0de449504ce40d74421aa05d1..8405d0ce7c984b9593a373fc5a805accccacbf08 100644 --- a/etherlink/bin_node/lib_dev/tx_pool.ml +++ b/etherlink/bin_node/lib_dev/tx_pool.ml @@ -115,8 +115,6 @@ type parameters = { mode : mode; } -type popped_transactions = Locked | Transactions of string list - module Types = struct type state = { rollup_node : (module Services_backend_sig.S); @@ -147,10 +145,11 @@ module Request = struct | Add_transaction : string -> ((Ethereum_types.hash, string) result, tztrace) t - | Pop_transactions : (popped_transactions, tztrace) t + | Pop_transactions : int -> (string list, tztrace) t | Pop_and_inject_transactions : (unit, tztrace) t | Lock_transactions : (unit, tztrace) t | Unlock_transactions : (unit, tztrace) t + | Is_locked : (bool, tztrace) t type view = View : _ t -> view @@ -173,9 +172,15 @@ module Request = struct case (Tag 1) ~title:"Pop_transactions" - (obj1 (req "request" (constant "pop_transactions"))) - (function View Pop_transactions -> Some () | _ -> None) - (fun () -> View Pop_transactions); + (obj2 + (req "request" (constant "pop_transactions")) + (req "maximum_cumulatize_size" int31)) + (function + | View (Pop_transactions maximum_cumulative_size) -> + Some ((), maximum_cumulative_size) + | _ -> None) + (fun ((), maximum_cumulative_size) -> + View (Pop_transactions maximum_cumulative_size)); case (Tag 2) ~title:"Pop_and_inject_transactions" @@ -194,6 +199,12 @@ module Request = struct (obj1 (req "request" (constant "unlock_transactions"))) (function View Unlock_transactions -> Some () | _ -> None) (fun () -> View Unlock_transactions); + case + (Tag 5) + ~title:"Is_locked" + (obj1 (req "request" (constant "is_locked"))) + (function View Is_locked -> Some () | _ -> None) + (fun () -> View Is_locked); ] let pp ppf (View r) = @@ -203,11 +214,16 @@ module Request = struct ppf "Add tx [%s] to tx-pool" (Hex.of_string tx_raw |> Hex.show) - | Pop_transactions -> Format.fprintf ppf "Popping transactions" + | Pop_transactions maximum_cumulative_size -> + Format.fprintf + ppf + "Popping transactions of maximum cumulative size %d bytes" + maximum_cumulative_size | Pop_and_inject_transactions -> Format.fprintf ppf "Popping and injecting transactions" | Lock_transactions -> Format.fprintf ppf "Locking the transactions" | Unlock_transactions -> Format.fprintf ppf "Unlocking the transactions" + | Is_locked -> Format.fprintf ppf "Checking if the tx pool is locked" end module Worker = Worker.MakeSingle (Name) (Request) (Types) @@ -251,7 +267,7 @@ let can_prepay ~balance ~gas_price ~gas_limit = let can_pay_with_current_base_fee ~gas_price ~base_fee_per_gas = gas_price >= base_fee_per_gas -let pop_transactions state = +let pop_transactions state ~maximum_cumulative_size = let open Lwt_result_syntax in let Types. { @@ -262,7 +278,7 @@ let pop_transactions state = } = state in - if locked then return Locked + if locked then return [] else (* Get all the addresses in the tx-pool. *) let addresses = Pool.addresses pool in @@ -292,24 +308,40 @@ let pop_transactions state = pool) pool in - (* Select transaction with nonce equal to user's nonce and that - can be prepaid. + (* Select transaction with nonce equal to user's nonce, that can be prepaid + and selects a sum of transactions that wouldn't go above the size limit + of the blueprint. Also removes the transactions from the pool. *) - let txs, pool = + let txs, pool, _ = addr_with_nonces |> List.fold_left - (fun (txs, pool) (pkey, _, current_nonce) -> + (fun (txs, pool, cumulative_size) (pkey, _, current_nonce) -> + (* This mutable counter is purely local and used only for the + partition. *) + let accumulated_size = ref cumulative_size in let selected, pool = Pool.partition pkey - (fun nonce {gas_price; _} -> - nonce = current_nonce - && can_pay_with_current_base_fee ~gas_price ~base_fee_per_gas) + (fun nonce {gas_price; raw_tx; _} -> + let check_nonce = nonce = current_nonce in + let can_fit = + !accumulated_size + String.length raw_tx + <= maximum_cumulative_size + in + let can_pay = + can_pay_with_current_base_fee ~gas_price ~base_fee_per_gas + in + let selected = check_nonce && can_pay && can_fit in + (* If the transaction is selected, this means it will fit *) + if selected then + accumulated_size := + !accumulated_size + String.length raw_tx ; + selected) pool in let txs = List.append txs selected in - (txs, pool)) - ([], pool) + (txs, pool, !accumulated_size)) + ([], pool, 0) in (* Sorting transactions by index. First tx in the pool is the first tx to be sent to the batcher. *) @@ -321,7 +353,7 @@ let pop_transactions state = in (* update the pool *) state.pool <- pool ; - return (Transactions txs) + return txs let pop_and_inject_transactions state = let open Lwt_result_syntax in @@ -330,40 +362,45 @@ let pop_and_inject_transactions state = | Sequencer -> failwith "Internal error: the sequencer is not supposed to use this function" - | Observer | Proxy _ -> ( - let* res = pop_transactions state in - match res with - | Locked -> return_unit - | Transactions txs -> - if not (List.is_empty txs) then - let (module Rollup_node : Services_backend_sig.S) = - state.rollup_node - in - let*! hashes = - Rollup_node.inject_raw_transactions - (* The timestamp is ignored in observer and proxy mode, it's just for - compatibility with sequencer mode. *) - ~timestamp:(Helpers.now ()) - ~smart_rollup_address:state.smart_rollup_address - ~transactions:txs + | Observer | Proxy _ -> + (* We over approximate the number of transactions to pop in proxy and + observer mode to the maximum size an L1 block can hold. If the proxy + sends more, they won't be applied at the next level. For the observer, + it prevents spamming the sequencer. *) + let maximum_cumulative_size = + Sequencer_blueprint.maximum_usable_space_in_blueprint + Sequencer_blueprint.maximum_chunks_per_l1_level + in + let* txs = pop_transactions state ~maximum_cumulative_size in + if not (List.is_empty txs) then + let (module Rollup_node : Services_backend_sig.S) = state.rollup_node in + let*! hashes = + Rollup_node.inject_raw_transactions + (* The timestamp is ignored in observer and proxy mode, it's just for + compatibility with sequencer mode. *) + ~timestamp:(Helpers.now ()) + ~smart_rollup_address:state.smart_rollup_address + ~transactions:txs + in + match hashes with + | Error trace -> + let*! () = Tx_pool_events.transaction_injection_failed trace in + return_unit + | Ok hashes -> + let*! () = + List.iter_s + (fun hash -> Tx_pool_events.transaction_injected ~hash) + hashes in - match hashes with - | Error trace -> - let*! () = Tx_pool_events.transaction_injection_failed trace in - return_unit - | Ok hashes -> - let*! () = - List.iter_s - (fun hash -> Tx_pool_events.transaction_injected ~hash) - hashes - in - return_unit - else return_unit) + return_unit + else return_unit let lock_transactions state = state.Types.locked <- true let unlock_transactions state = state.Types.locked <- false +let is_locked state = state.Types.locked + module Handlers = struct type self = worker @@ -391,12 +428,14 @@ module Handlers = struct let* res = on_normal_transaction state transaction in let* () = observer_self_inject_request w in return res - | Request.Pop_transactions -> protect @@ fun () -> pop_transactions state + | Request.Pop_transactions maximum_cumulative_size -> + protect @@ fun () -> pop_transactions state ~maximum_cumulative_size | Request.Pop_and_inject_transactions -> protect @@ fun () -> pop_and_inject_transactions state | Request.Lock_transactions -> protect @@ fun () -> return (lock_transactions state) | Request.Unlock_transactions -> return (unlock_transactions state) + | Request.Is_locked -> protect @@ fun () -> return (is_locked state) type launch_error = error trace @@ -490,10 +529,12 @@ let nonce pkey = in return next_nonce -let pop_transactions () = +let pop_transactions ~maximum_cumulative_size = let open Lwt_result_syntax in let*? worker = Lazy.force worker in - Worker.Queue.push_request_and_wait worker Request.Pop_transactions + Worker.Queue.push_request_and_wait + worker + (Request.Pop_transactions maximum_cumulative_size) |> handle_request_error let pop_and_inject_transactions () = @@ -521,3 +562,9 @@ let unlock_transactions () = let*? worker = Lazy.force worker in Worker.Queue.push_request_and_wait worker Request.Unlock_transactions |> handle_request_error + +let is_locked () = + let open Lwt_result_syntax in + let*? worker = Lazy.force worker in + Worker.Queue.push_request_and_wait worker Request.Is_locked + |> handle_request_error diff --git a/etherlink/bin_node/lib_dev/tx_pool.mli b/etherlink/bin_node/lib_dev/tx_pool.mli index 374e6f01251e154ff8c36bb8fd021b798a0ef7e1..b9ea9394a968d48d357553894a0e62be4d5c030f 100644 --- a/etherlink/bin_node/lib_dev/tx_pool.mli +++ b/etherlink/bin_node/lib_dev/tx_pool.mli @@ -13,8 +13,6 @@ type parameters = { mode : mode; } -type popped_transactions = Locked | Transactions of string list - (** [start parameters] starts the tx-pool *) val start : parameters -> unit tzresult Lwt.t @@ -26,15 +24,17 @@ val shutdown : unit -> unit tzresult Lwt.t val add : string -> (Ethereum_types.hash, string) result tzresult Lwt.t (** [nonce address] returns the nonce of the user - Returns the first gap in the tx-pool, or the nonce stored on the rollup + Returns the first gap in the tx-pool, or the nonce stored on the rollup if no transactions are in the pool. *) val nonce : Ethereum_types.Address.t -> Ethereum_types.quantity tzresult Lwt.t -(** [pop_transactions ()] pops the valid transactions from the pool. *) -val pop_transactions : unit -> popped_transactions tzresult Lwt.t +(** [pop_transactions maximum_cumulative_size] pops as much valid transactions + as possible from the pool, until their cumulative size exceeds + `maximum_cumulative_size`. Returns no transactions if the pool is locked. *) +val pop_transactions : maximum_cumulative_size:int -> string list tzresult Lwt.t (** [pop_and_inject_transactions ()] pops the valid transactions from - the pool using {!pop_transactions }and injects them using + the pool using {!pop_transactions} and injects them using [inject_raw_transactions] provided by {!parameters.rollup_node}. *) val pop_and_inject_transactions : unit -> unit tzresult Lwt.t @@ -50,3 +50,6 @@ val lock_transactions : unit -> unit tzresult Lwt.t (** [unlock_transactions] unlocks the transactions if it was locked by {!lock_transactions}. *) val unlock_transactions : unit -> unit tzresult Lwt.t + +(** [is_locked] checks if the pools is locked. *) +val is_locked : unit -> bool tzresult Lwt.t diff --git a/etherlink/bin_node/main.ml b/etherlink/bin_node/main.ml index 95be4e2ea4bd12d64ef62388aa868dcb78f66e8a..d7a7a441fcbeec3672b373e790cb94fba620ea29 100644 --- a/etherlink/bin_node/main.ml +++ b/etherlink/bin_node/main.ml @@ -475,6 +475,13 @@ let time_between_blocks_arg = ~placeholder:"10." Params.time_between_blocks +let max_number_of_chunks_arg = + Tezos_clic.arg + ~long:"max-number-of-chunks" + ~doc:"Maximum number of chunks per blueprint." + ~placeholder:"10." + Params.int + let keep_alive_arg = Tezos_clic.switch ~doc: @@ -655,7 +662,7 @@ let sequencer_command = let open Lwt_result_syntax in command ~desc:"Start the EVM node in sequencer mode" - (args18 + (args19 data_dir_arg rpc_addr_arg rpc_port_arg @@ -672,6 +679,7 @@ let sequencer_command = maximum_blueprints_ahead_arg maximum_blueprints_catchup_arg catchup_cooldown_arg + max_number_of_chunks_arg devmode_arg wallet_dir_arg) (prefixes ["run"; "sequencer"; "with"; "endpoint"] @@ -699,6 +707,7 @@ let sequencer_command = max_blueprints_ahead, max_blueprints_catchup, catchup_cooldown, + max_number_of_chunks, devmode, wallet_dir ) rollup_node_endpoint @@ -747,6 +756,7 @@ let sequencer_command = ?preimages ?preimages_endpoint ?time_between_blocks + ?max_number_of_chunks ~sequencer:sequencer_pkh () in diff --git a/etherlink/tezt/lib/evm_node.ml b/etherlink/tezt/lib/evm_node.ml index fc5930092a4fcfb3b80eb4ed4d244cdf87d1cb99..253aa8ed84f2b7f7a0429ea0e659d541cbc52202 100644 --- a/etherlink/tezt/lib/evm_node.ml +++ b/etherlink/tezt/lib/evm_node.ml @@ -44,6 +44,7 @@ type mode = max_blueprints_ahead : int option; max_blueprints_catchup : int option; catchup_cooldown : int option; + max_number_of_chunks : int option; devmode : bool; wallet_dir : string option; } @@ -435,6 +436,7 @@ let run_args evm_node = max_blueprints_ahead; max_blueprints_catchup; catchup_cooldown; + max_number_of_chunks; devmode; wallet_dir; } -> @@ -480,6 +482,10 @@ let run_args evm_node = (fun timestamp -> Client.time_of_timestamp timestamp |> Client.Time.to_notation) genesis_timestamp + @ Cli_arg.optional_arg + "max-number-of-chunks" + string_of_int + max_number_of_chunks @ Cli_arg.optional_switch "devmode" devmode @ Cli_arg.optional_arg "wallet-dir" Fun.id wallet_dir | Observer {preimages_dir; initial_kernel; rollup_node_endpoint} -> diff --git a/etherlink/tezt/lib/evm_node.mli b/etherlink/tezt/lib/evm_node.mli index 65146448b606e47c0911d1fe2b519c8a82e4fcf7..ca61f0db5ef5aaaa761bbd8e666dd78cbe7a6530 100644 --- a/etherlink/tezt/lib/evm_node.mli +++ b/etherlink/tezt/lib/evm_node.mli @@ -56,6 +56,7 @@ type mode = max_blueprints_ahead : int option; max_blueprints_catchup : int option; catchup_cooldown : int option; + max_number_of_chunks : int option; devmode : bool; (** --devmode flag. *) wallet_dir : string option; (** --wallet-dir: client directory. *) } diff --git a/etherlink/tezt/lib/helpers.ml b/etherlink/tezt/lib/helpers.ml index 3175db8b1f598b9de5cf452adbf92b264d8677dc..d79fc5fe336c5928daedafe12e324ca526b1c265 100644 --- a/etherlink/tezt/lib/helpers.ml +++ b/etherlink/tezt/lib/helpers.ml @@ -182,3 +182,79 @@ let bake_until_sync ?(timeout = 30.) ~sc_rollup_node ~proxy ~sequencer ~client go () in Lwt.pick [go (); Lwt_unix.sleep timeout] + +(** [wait_for_transaction_receipt ~evm_node ~transaction_hash] takes an + transaction_hash and returns only when the receipt is non null, or [count] + blocks have passed and the receipt is still not available. *) +let wait_for_transaction_receipt ?(count = 3) ~evm_node ~transaction_hash () = + let rec loop count = + let* () = Lwt_unix.sleep 5. in + let* receipt = + Evm_node.( + call_evm_rpc + evm_node + { + method_ = "eth_getTransactionReceipt"; + parameters = `A [`String transaction_hash]; + }) + in + if receipt |> Evm_node.extract_result |> JSON.is_null then + if count > 0 then loop (count - 1) + else Test.fail "Transaction still hasn't been included" + else + receipt |> Evm_node.extract_result + |> Transaction.transaction_receipt_of_json |> return + in + loop count + +let wait_for_application ~evm_node ~sc_rollup_node ~client apply = + let max_iteration = 10 in + let application_result = apply () in + let rec loop current_iteration = + let* () = Lwt_unix.sleep 5. in + let* () = next_evm_level ~evm_node ~sc_rollup_node ~client in + if max_iteration < current_iteration then + Test.fail + "Baked more than %d blocks and the operation's application is still \ + pending" + max_iteration ; + if Lwt.state application_result = Lwt.Sleep then loop (current_iteration + 1) + else unit + in + (* Using [Lwt.both] ensures that any exception thrown in [tx_hash] will be + thrown by [Lwt.both] as well. *) + let* result, () = Lwt.both application_result (loop 0) in + return result + +let batch_n_transactions ~evm_node txs = + let requests = + List.map + (fun tx -> + Evm_node. + {method_ = "eth_sendRawTransaction"; parameters = `A [`String tx]}) + txs + in + let* hashes = Evm_node.batch_evm_rpc evm_node requests in + let hashes = + hashes |> JSON.as_list + |> List.map (fun json -> Evm_node.extract_result json |> JSON.as_string) + in + return (requests, hashes) + +(* sending more than ~300 tx could fail, because or curl *) +let send_n_transactions ~sc_rollup_node ~client ~evm_node ?wait_for_blocks txs = + let* requests, hashes = batch_n_transactions ~evm_node txs in + let first_hash = List.hd hashes in + (* Let's wait until one of the transactions is injected into a block, and + test this block contains the `n` transactions as expected. *) + let* receipt = + wait_for_application + ~evm_node + ~sc_rollup_node + ~client + (wait_for_transaction_receipt + ?count:wait_for_blocks + ~evm_node + ~transaction_hash:first_hash) + in + return (requests, receipt, hashes) diff --git a/etherlink/tezt/lib/helpers.mli b/etherlink/tezt/lib/helpers.mli index 0726ae366c598422f241347f668012f0b449f7a2..83b6d7cd60920b54b7cd243fbf9ca5d3fee27c1f 100644 --- a/etherlink/tezt/lib/helpers.mli +++ b/etherlink/tezt/lib/helpers.mli @@ -129,3 +129,41 @@ val bake_until_sync : client:Client.t -> unit -> unit Lwt.t + +(** [wait_for_transaction_receipt ?count ~evm_node ~transaction_hash ()] takes a + transaction_hash and returns only when the receipt is non null, or [count] + blocks have passed and the receipt is still not available. *) +val wait_for_transaction_receipt : + ?count:int -> + evm_node:Evm_node.t -> + transaction_hash:string -> + unit -> + Transaction.transaction_receipt Lwt.t + +(** [wait_for_application ~evm_node ~sc_rollup_node ~client apply] returns only + when the `apply` yields, or fails when 10 blocks have passed. *) +val wait_for_application : + evm_node:Evm_node.t -> + sc_rollup_node:Sc_rollup_node.t -> + client:Client.t -> + (unit -> 'a Lwt.t) -> + 'a Lwt.t + +(** [batch_n_transactions ~evm_node raw_transactions] batches [raw_transactions] + to the [evm_node] and returns the requests and transaction hashes. *) +val batch_n_transactions : + evm_node:Evm_node.t -> + string list -> + (Evm_node.request list * string list) Lwt.t + +(** [send_n_transactions ~sc_rollup_node ~evm_node ?wait_for_blocks + raw_transactions] batches [raw_transactions] to the [evm_node] and waits + until the first one is applied in a block and returns, or fails if it isn't + applied after [wait_for_blocks] blocks. *) +val send_n_transactions : + sc_rollup_node:Sc_rollup_node.t -> + client:Client.t -> + evm_node:Evm_node.t -> + ?wait_for_blocks:int -> + string list -> + (Evm_node.request list * Transaction.transaction_receipt * string list) Lwt.t diff --git a/etherlink/tezt/tests/evm_rollup.ml b/etherlink/tezt/tests/evm_rollup.ml index ee22c08ecc1e9ad09c66cfe0569f1928ece19bf7..0299efd311484b84410312a0990d10fc2193f0ae 100644 --- a/etherlink/tezt/tests/evm_rollup.ml +++ b/etherlink/tezt/tests/evm_rollup.ml @@ -186,78 +186,6 @@ let check_storage_size sc_rollup_node ~address size = ~error_msg:"Unexpected storage size, should be %R, but is %L" ; unit -(** [wait_for_transaction_receipt ~evm_node ~transaction_hash] takes an - transaction_hash and returns only when the receipt is non null, or [count] - blocks have passed and the receipt is still not available. *) -let wait_for_transaction_receipt ?(count = 3) ~evm_node ~transaction_hash () = - let rec loop count = - let* () = Lwt_unix.sleep 5. in - let* receipt = - Evm_node.( - call_evm_rpc - evm_node - { - method_ = "eth_getTransactionReceipt"; - parameters = `A [`String transaction_hash]; - }) - in - if receipt |> Evm_node.extract_result |> JSON.is_null then - if count > 0 then loop (count - 1) - else Test.fail "Transaction still hasn't been included" - else - receipt |> Evm_node.extract_result - |> Transaction.transaction_receipt_of_json |> return - in - loop count - -let wait_for_application ~evm_node ~sc_rollup_node ~client apply = - let max_iteration = 10 in - let application_result = apply () in - let rec loop current_iteration = - let* () = Lwt_unix.sleep 5. in - let* () = Helpers.next_evm_level ~evm_node ~sc_rollup_node ~client in - if max_iteration < current_iteration then - Test.fail - "Baked more than %d blocks and the operation's application is still \ - pending" - max_iteration ; - if Lwt.state application_result = Lwt.Sleep then loop (current_iteration + 1) - else unit - in - (* Using [Lwt.both] ensures that any exception thrown in [tx_hash] will be - thrown by [Lwt.both] as well. *) - let* result, () = Lwt.both application_result (loop 0) in - return result - -(* sending more than ~300 tx could fail, because or curl *) -let send_n_transactions ~sc_rollup_node ~client ~evm_node ?wait_for_blocks txs = - let requests = - List.map - (fun tx -> - Evm_node. - {method_ = "eth_sendRawTransaction"; parameters = `A [`String tx]}) - txs - in - let* hashes = Evm_node.batch_evm_rpc evm_node requests in - let hashes = - hashes |> JSON.as_list - |> List.map (fun json -> Evm_node.extract_result json |> JSON.as_string) - in - let first_hash = List.hd hashes in - (* Let's wait until one of the transactions is injected into a block, and - test this block contains the `n` transactions as expected. *) - let* receipt = - wait_for_application - ~evm_node - ~sc_rollup_node - ~client - (wait_for_transaction_receipt - ?count:wait_for_blocks - ~evm_node - ~transaction_hash:first_hash) - in - return (requests, receipt, hashes) - let setup_l1_contracts ~admin ?sequencer_admin client = (* Originates the exchanger. *) let* exchanger = @@ -478,6 +406,7 @@ let setup_evm_kernel ?(setup_kernel_root_hash = true) ?config max_blueprints_ahead = None; max_blueprints_catchup = None; catchup_cooldown = None; + max_number_of_chunks = None; devmode; wallet_dir = Some (Client.base_dir client); }) @@ -595,7 +524,7 @@ let deploy ~contract ~sender full_evm_setup = ~abi:contract.label ~bin:contract.bin in - wait_for_application ~evm_node ~sc_rollup_node ~client send_deploy + Helpers.wait_for_application ~evm_node ~sc_rollup_node ~client send_deploy type deploy_checks = { contract : contract; @@ -4587,6 +4516,7 @@ let test_migrate_proxy_to_sequencer_future = max_blueprints_ahead = None; max_blueprints_catchup = None; catchup_cooldown = None; + max_number_of_chunks = None; devmode = true; wallet_dir = Some (Client.base_dir client); } @@ -4748,6 +4678,7 @@ let test_migrate_proxy_to_sequencer_past = max_blueprints_ahead = None; max_blueprints_catchup = None; catchup_cooldown = None; + max_number_of_chunks = None; devmode = true; wallet_dir = Some (Client.base_dir client); } diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index f2e9e92dcce6840b37049c31dfdc4a6123ff1e67..c885fafd833609f8775aa76c7070d0170da1779e 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -132,7 +132,7 @@ let setup_l1_contracts ?(dictator = Constant.bootstrap2) client = let setup_sequencer ?(devmode = true) ?config ?genesis_timestamp ?time_between_blocks ?max_blueprints_lag ?max_blueprints_ahead ?max_blueprints_catchup ?catchup_cooldown ?delayed_inbox_timeout - ?delayed_inbox_min_levels + ?delayed_inbox_min_levels ?max_number_of_chunks ?(bootstrap_accounts = Eth_account.bootstrap_accounts) ?(sequencer = Constant.bootstrap1) ?(kernel = Constant.WASM.evm_kernel) ?da_fee ?minimum_base_fee_per_gas ?preimages_dir protocol = @@ -199,6 +199,7 @@ let setup_sequencer ?(devmode = true) ?config ?genesis_timestamp max_blueprints_ahead; max_blueprints_catchup; catchup_cooldown; + max_number_of_chunks; devmode; wallet_dir = Some (Client.base_dir client); } @@ -234,8 +235,9 @@ let setup_sequencer ?(devmode = true) ?config ?genesis_timestamp sc_rollup_node; } -let send_raw_transaction_to_delayed_inbox ?(amount = Tez.one) ?expect_failure - ~sc_rollup_node ~client ~l1_contracts ~sc_rollup_address raw_tx = +let send_raw_transaction_to_delayed_inbox ?(wait_for_next_level = true) + ?(amount = Tez.one) ?expect_failure ~sc_rollup_node ~client ~l1_contracts + ~sc_rollup_address ?(sender = Constant.bootstrap2) raw_tx = let expected_hash = `Hex raw_tx |> Hex.to_bytes |> Tezos_crypto.Hacl.Hash.Keccak_256.digest |> Hex.of_bytes |> Hex.show @@ -244,14 +246,18 @@ let send_raw_transaction_to_delayed_inbox ?(amount = Tez.one) ?expect_failure Client.transfer ~arg:(sf "Pair %S 0x%s" sc_rollup_address raw_tx) ~amount - ~giver:Constant.bootstrap2.public_key_hash + ~giver:sender.public_key_hash ~receiver:l1_contracts.delayed_transaction_bridge ~burn_cap:Tez.one ?expect_failure client in - let* () = Client.bake_for_and_wait ~keys:[] client in - let* _ = next_rollup_node_level ~sc_rollup_node ~client in + let* () = + if wait_for_next_level then + let* _ = next_rollup_node_level ~sc_rollup_node ~client in + unit + else unit + in Lwt.return expected_hash let send_deposit_to_delayed_inbox ~amount ~l1_contracts ~depositor ~receiver @@ -2315,6 +2321,179 @@ let test_stage_one_reboot = was expected." ; unit +let test_blueprint_is_limited_in_size = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "sequencer"; "blueprint"; "limit"] + ~title: + "Checks the sequencer doesn't produce blueprint bigger than the given \ + maximum number of chunks" + ~uses + @@ fun protocol -> + let* {sc_rollup_node; client; sequencer; _} = + setup_sequencer + ~config:(`Path (kernel_inputs_path ^ "/100-inputs-for-proxy-config.yaml")) + ~sequencer:Constant.bootstrap1 + ~time_between_blocks:Nothing + ~max_number_of_chunks:2 + ~minimum_base_fee_per_gas:base_fee_for_hardcoded_tx + protocol + in + let txs = read_tx_from_file () |> List.map (fun (tx, _hash) -> tx) in + let* requests, hashes = + Helpers.batch_n_transactions ~evm_node:sequencer txs + in + (* Each transaction is about 114 bytes, hence 100 * 114 = 11400 bytes, which + will fit in two blueprints of two chunks each. *) + let* () = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + let* () = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + let first_hash = List.hd hashes in + let* level_of_first_transaction = + let*@ receipt = Rpc.get_transaction_receipt ~tx_hash:first_hash sequencer in + match receipt with + | None -> Test.fail "Delayed transaction hasn't be included" + | Some receipt -> return receipt.blockNumber + in + let*@ block_with_first_transaction = + Rpc.get_block_by_number + ~block:(Int32.to_string level_of_first_transaction) + sequencer + in + (* The block containing the first transaction of the batch cannot contain the + 100 transactions of the batch, as it doesn't fit in two chunks. *) + let block_size_of_first_transaction = + match block_with_first_transaction.Block.transactions with + | Block.Empty -> Test.fail "Expected a non empty block" + | Block.Full _ -> + Test.fail "Block is supposed to contain only transaction hashes" + | Block.Hash hashes -> + Check.((List.length hashes < List.length requests) int) + ~error_msg:"Expected less than %R transactions in the block, got %L" ; + List.length hashes + in + + let* () = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + (* It's not clear the first transaction of the batch is applied in the first + blueprint or the second, as it depends how the tx_pool sorts the + transactions (by caller address). We need to check that either the previous + block or the next block contains transactions, which puts in evidence that + the batch has been splitted into two consecutive blueprints. + *) + let check_block_size block_number = + let*@ block = + Rpc.get_block_by_number ~block:(Int32.to_string block_number) sequencer + in + match block.Block.transactions with + | Block.Empty -> return 0 + | Block.Full _ -> + Test.fail "Block is supposed to contain only transaction hashes" + | Block.Hash hashes -> return (List.length hashes) + in + let* next_block_size = + check_block_size (Int32.succ level_of_first_transaction) + in + let* previous_block_size = + check_block_size (Int32.pred level_of_first_transaction) + in + if next_block_size = 0 && previous_block_size = 0 then + Test.fail + "The sequencer didn't apply the 100 transactions in two consecutive \ + blueprints" ; + Check.( + (block_size_of_first_transaction + previous_block_size + next_block_size + = List.length hashes) + int + ~error_msg: + "Not all the transactions have been injected, only %L, while %R was \ + expected.") ; + unit + +let test_blueprint_limit_with_delayed_inbox = + Protocol.register_test + ~__FILE__ + ~tags:["evm"; "sequencer"; "blueprint"; "limit"; "delayed"] + ~title: + "Checks the sequencer doesn't produce blueprint bigger than the given \ + maximum number of chunks and count delayed transactions size in the \ + blueprint" + ~uses + @@ fun protocol -> + let* {sc_rollup_node; client; sequencer; sc_rollup_address; l1_contracts; _} = + setup_sequencer + ~config:(`Path (kernel_inputs_path ^ "/100-inputs-for-proxy-config.yaml")) + ~sequencer:Constant.bootstrap1 + ~time_between_blocks:Nothing + ~max_number_of_chunks:2 + ~minimum_base_fee_per_gas:base_fee_for_hardcoded_tx + ~devmode:true + protocol + in + let txs = read_tx_from_file () |> List.map (fun (tx, _hash) -> tx) in + (* The first 3 transactions will be sent to the delayed inbox *) + let delayed_txs, direct_txs = Tezos_base.TzPervasives.TzList.split_n 3 txs in + let send_to_delayed_inbox (sender, raw_tx) = + send_raw_transaction_to_delayed_inbox + ~wait_for_next_level:false + ~sender + ~sc_rollup_node + ~sc_rollup_address + ~client + ~l1_contracts + raw_tx + in + let* delayed_hashes = + Lwt_list.map_s send_to_delayed_inbox + @@ List.combine + [Constant.bootstrap2; Constant.bootstrap3; Constant.bootstrap4] + delayed_txs + in + (* Ensures the transactions are added to the rollup delayed inbox and picked + by the sequencer *) + let* () = + repeat 4 (fun () -> + let* _l1_level = next_rollup_node_level ~sc_rollup_node ~client in + unit) + in + let* _requests, _hashes = + Helpers.batch_n_transactions ~evm_node:sequencer direct_txs + in + (* Due to the overapproximation of 4096 bytes per delayed transactions, there + should be only a single delayed transaction per blueprints with 2 chunks. *) + let* _ = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + let* _ = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + let* _ = next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client in + (* Checks the delayed transactions and at least the first transaction from the + batch have been applied *) + let* block_numbers = + Lwt_list.map_s + (fun tx_hash -> + let*@ receipt = Rpc.get_transaction_receipt ~tx_hash sequencer in + match receipt with + | None -> Test.fail "Delayed transaction hasn't be included" + | Some receipt -> return receipt.blockNumber) + delayed_hashes + in + let check_block_contains_delayed_transaction_and_transactions + (delayed_hash, block_number) = + let*@ block = + Rpc.get_block_by_number ~block:(Int32.to_string block_number) sequencer + in + match block.Block.transactions with + | Block.Empty -> Test.fail "Block shouldn't be empty" + | Block.Full _ -> + Test.fail "Block is supposed to contain only transaction hashes" + | Block.Hash hashes -> + if not (List.mem ("0x" ^ delayed_hash) hashes && 2 < List.length hashes) + then + Test.fail + "The delayed transaction %s hasn't been included in the expected \ + block along other transactions from the pool" + delayed_hash ; + unit + in + Lwt_list.iter_s check_block_contains_delayed_transaction_and_transactions + @@ List.combine delayed_hashes block_numbers + let protocols = Protocol.all let () = @@ -2348,4 +2527,6 @@ let () = test_sequencer_upgrade protocols ; test_sequencer_diverge protocols ; test_sequencer_can_catch_up_on_event protocols ; - test_stage_one_reboot protocols + test_stage_one_reboot protocols ; + test_blueprint_is_limited_in_size protocols ; + test_blueprint_limit_with_delayed_inbox protocols