From 48f0ea4b101b79a3917ba23d26898dba267bd0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 25 Jun 2025 00:37:59 +0200 Subject: [PATCH 1/4] Tezlink: operation batches --- etherlink/bin_node/lib_dev/rpc_server.ml | 2 +- etherlink/bin_node/lib_dev/sequencer.ml | 5 +- etherlink/bin_node/lib_dev/services.ml | 2 +- .../bin_node/lib_dev/tezlink/tezos_types.ml | 52 +++++++++++++------ .../bin_node/lib_dev/tezlink/tezos_types.mli | 3 +- etherlink/bin_node/lib_dev/tx_queue_types.ml | 2 +- 6 files changed, 45 insertions(+), 21 deletions(-) diff --git a/etherlink/bin_node/lib_dev/rpc_server.ml b/etherlink/bin_node/lib_dev/rpc_server.ml index b11ed8ea8938..6d79c9c662ca 100644 --- a/etherlink/bin_node/lib_dev/rpc_server.ml +++ b/etherlink/bin_node/lib_dev/rpc_server.ml @@ -188,7 +188,7 @@ let start_public_server (type f) ~is_sequencer ~add_operation:(fun op raw -> (* TODO: https://gitlab.com/tezos/tezos/-/issues/8007 Validate the operation and use the resulting "next_nonce" *) - let next_nonce = Ethereum_types.Qty op.counter in + let next_nonce = Ethereum_types.Qty op.first_counter in let* hash_res = Tx_container.add ~next_nonce diff --git a/etherlink/bin_node/lib_dev/sequencer.ml b/etherlink/bin_node/lib_dev/sequencer.ml index bb8f2de4bc59..f07c6a1fce67 100644 --- a/etherlink/bin_node/lib_dev/sequencer.ml +++ b/etherlink/bin_node/lib_dev/sequencer.ml @@ -69,7 +69,10 @@ let validate_and_add_tezlink_operation in let raw_tx = Ethereum_types.hex_of_utf8 raw_tx in let* _ = - Tx_container.add ~next_nonce:(Ethereum_types.Qty op.counter) op ~raw_tx + Tx_container.add + ~next_nonce:(Ethereum_types.Qty op.first_counter) + op + ~raw_tx in return_unit diff --git a/etherlink/bin_node/lib_dev/services.ml b/etherlink/bin_node/lib_dev/services.ml index bde288cb81d5..b5c1368b64b0 100644 --- a/etherlink/bin_node/lib_dev/services.ml +++ b/etherlink/bin_node/lib_dev/services.ml @@ -1227,7 +1227,7 @@ let dispatch_private_request (type f) ~websocket | Michelson_tx_container m -> return m in Tx_container.add - ~next_nonce:(Ethereum_types.Qty op.counter) + ~next_nonce:(Ethereum_types.Qty op.first_counter) op ~raw_tx:(Ethereum_types.hex_of_bytes raw_op) in diff --git a/etherlink/bin_node/lib_dev/tezlink/tezos_types.ml b/etherlink/bin_node/lib_dev/tezlink/tezos_types.ml index b63f68fd9cd4..209a145019fd 100644 --- a/etherlink/bin_node/lib_dev/tezlink/tezos_types.ml +++ b/etherlink/bin_node/lib_dev/tezlink/tezos_types.ml @@ -71,7 +71,8 @@ module Operation = struct type t = { source : Signature.public_key_hash; - counter : Z.t; + first_counter : Z.t; + length : int; op : Tezlink_imports.Alpha_context.packed_operation; raw : bytes; } @@ -87,8 +88,9 @@ module Operation = struct | None -> error_with "Can't parse the operation" | Some op -> return op in - let from_contents (type kind) - (contents : kind Tezlink_imports.Alpha_context.contents) : t tzresult = + let source_and_counter_from_contents (type kind) + (contents : kind Tezlink_imports.Alpha_context.contents) : + (Signature.public_key_hash * Z.t) tzresult = match contents with | Tezlink_imports.Alpha_context.Manager_operation {source; counter; _} -> let* counter = @@ -99,30 +101,48 @@ module Operation = struct Tezlink_imports.Alpha_context.Manager_counter.encoding_for_RPCs counter in - return ({source; counter; op; raw} : t) + return (source, counter) | _ -> error_with "Not a manager operation" in - let (Operation_data op) = op.protocol_data in - match op.contents with - | Single contents -> from_contents contents - | Cons _ -> - (* TODO: https://gitlab.com/tezos/tezos/-/issues/8008 - support operation batches - *) - error_with "Unsupported feature: operation batch" + let source_and_first_counter_from_contents_list (type kind) + (contents_list : kind Tezlink_imports.Alpha_context.contents_list) : + (Signature.public_key_hash * Z.t) tzresult = + match contents_list with + | Single contents -> source_and_counter_from_contents contents + | Cons (first_contents, _) -> + source_and_counter_from_contents first_contents + in + let rec contents_list_length : + type kind. + kind Tezlink_imports.Alpha_context.contents_list -> int -> int = + fun contents_list acc -> + match contents_list with + | Single _ -> 1 + acc + | Cons (_, remaining) -> contents_list_length remaining (1 + acc) + in + let (Operation_data op_data) = op.protocol_data in + let l = op_data.contents in + let* source, first_counter = + source_and_first_counter_from_contents_list l + in + let length = contents_list_length l 0 in + return {source; first_counter; length; op; raw} let encoding : t Data_encoding.t = let open Data_encoding in conv - (fun {source; counter; op; raw} -> (source, counter, op, raw)) - (fun (source, counter, op, raw) -> {source; counter; op; raw}) - (tup4 + (fun {source; first_counter; length; op; raw} -> + (source, first_counter, length, op, raw)) + (fun (source, first_counter, length, op, raw) -> + {source; first_counter; length; op; raw}) + (tup5 Signature.Public_key_hash.encoding z + int31 (dynamic_size Tezlink_imports.Alpha_context.Operation.encoding) bytes) - let hash_operation {source = _; counter = _; op; raw = _} = + let hash_operation {source = _; first_counter = _; length = _; op; raw = _} = let hash = ImportedOperation.hash_packed op in let (`Hex hex) = Operation_hash.to_hex hash in Ethereum_types.Hash (Ethereum_types.Hex hex) diff --git a/etherlink/bin_node/lib_dev/tezlink/tezos_types.mli b/etherlink/bin_node/lib_dev/tezlink/tezos_types.mli index 9be1cf7399c5..1f52e7041ae4 100644 --- a/etherlink/bin_node/lib_dev/tezlink/tezos_types.mli +++ b/etherlink/bin_node/lib_dev/tezlink/tezos_types.mli @@ -51,7 +51,8 @@ end module Operation : sig type t = { source : Signature.public_key_hash; - counter : Z.t; + first_counter : Z.t; + length : int; op : Tezlink_imports.Alpha_context.packed_operation; raw : bytes; } diff --git a/etherlink/bin_node/lib_dev/tx_queue_types.ml b/etherlink/bin_node/lib_dev/tx_queue_types.ml index 909a95b291d7..93d3e2fe2ca5 100644 --- a/etherlink/bin_node/lib_dev/tx_queue_types.ml +++ b/etherlink/bin_node/lib_dev/tx_queue_types.ml @@ -125,7 +125,7 @@ module Tezlink_operation : let nonce_to_z_opt _nonce = None let nonce_of_tx_object (op : Tezos_types.Operation.t) = - {first = op.counter; length = 1} + {first = op.first_counter; length = op.length} let transaction_object_from_legacy op = op -- GitLab From 91c42aef8cc2c5ab1b68ceea194e9178c2c604c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Cauderlier?= Date: Wed, 2 Jul 2025 13:18:49 +0200 Subject: [PATCH 2/4] Tezt/Client: log-requests option --- tezt/lib_tezos/client.ml | 25 ++++++++++++++----------- tezt/lib_tezos/client.mli | 3 +++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 51faf6a31670..99879944940c 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -1247,17 +1247,18 @@ let spawn_add_address ?(force = false) client ~alias ~src = let add_address ?force client ~alias ~src = spawn_add_address ?force client ~alias ~src |> Process.check -let spawn_transfer ?env ?hooks ?log_output ?endpoint ?(wait = "none") ?burn_cap - ?fee ?fee_cap ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint - ?arg ?(simulation = false) ?(force = false) ~amount ~giver ~receiver client - = +let spawn_transfer ?env ?hooks ?(log_requests = false) ?log_output ?endpoint + ?(wait = "none") ?burn_cap ?fee ?fee_cap ?gas_limit ?safety_guard + ?storage_limit ?counter ?entrypoint ?arg ?(simulation = false) + ?(force = false) ~amount ~giver ~receiver client = spawn_command ?env ?log_output ?endpoint ?hooks client - (["--wait"; wait] + ((if log_requests then ["--log-requests"] else []) + @ ["--wait"; wait] @ ["transfer"; Tez.to_string amount; "from"; giver; "to"; receiver] @ Option.fold ~none:[] @@ -1274,11 +1275,12 @@ let spawn_transfer ?env ?hooks ?log_output ?endpoint ?(wait = "none") ?burn_cap @ (if simulation then ["--simulation"] else []) @ if force then ["--force"] else []) -let transfer ?env ?hooks ?log_output ?endpoint ?wait ?burn_cap ?fee ?fee_cap - ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint ?arg - ?simulation ?force ?expect_failure ~amount ~giver ~receiver client = +let transfer ?env ?hooks ?log_requests ?log_output ?endpoint ?wait ?burn_cap + ?fee ?fee_cap ?gas_limit ?safety_guard ?storage_limit ?counter ?entrypoint + ?arg ?simulation ?force ?expect_failure ~amount ~giver ~receiver client = spawn_transfer ?env + ?log_requests ?log_output ?endpoint ?hooks @@ -1421,13 +1423,14 @@ let call_contract ?hooks ?endpoint ?burn_cap ~src ~destination ?entrypoint ?arg client |> Process.check -let reveal ?endpoint ?(wait = "none") ?fee ?fee_cap ?(force_low_fee = false) - ~src client = +let reveal ?endpoint ?(log_requests = false) ?(wait = "none") ?fee ?fee_cap + ?(force_low_fee = false) ~src client = let value = spawn_command ?endpoint client - (["--wait"; wait] + ((if log_requests then ["--log-requests"] else []) + @ ["--wait"; wait] @ ["reveal"; "key"; "for"; src] @ optional_arg "fee" Tez.to_string fee @ optional_arg "fee-cap" Tez.to_string fee_cap diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index 67341ec4324b..d02dafaf7fa8 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -881,6 +881,7 @@ val spawn_activate_account : val transfer : ?env:string String_map.t -> ?hooks:Process.hooks -> + ?log_requests:bool -> ?log_output:bool -> ?endpoint:endpoint -> ?wait:string -> @@ -906,6 +907,7 @@ val transfer : val spawn_transfer : ?env:string String_map.t -> ?hooks:Process.hooks -> + ?log_requests:bool -> ?log_output:bool -> ?endpoint:endpoint -> ?wait:string -> @@ -1037,6 +1039,7 @@ val spawn_call_contract : (** Run [octez-client reveal key for ]. *) val reveal : ?endpoint:endpoint -> + ?log_requests:bool -> ?wait:string -> ?fee:Tez.t -> ?fee_cap:Tez.t -> -- GitLab From 07184871d0bbc757c6f24cdada6398cfc691a60f Mon Sep 17 00:00:00 2001 From: Luciano Freitas Date: Tue, 5 Aug 2025 09:36:59 +0200 Subject: [PATCH 3/4] Tezlink/Tezt: batch with multiple transfers --- etherlink/tezt/tests/tezlink.ml | 81 ++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/etherlink/tezt/tests/tezlink.ml b/etherlink/tezt/tests/tezlink.ml index 69e3c67204b8..54e525bb6303 100644 --- a/etherlink/tezt/tests/tezlink.ml +++ b/etherlink/tezt/tests/tezlink.ml @@ -1031,6 +1031,84 @@ let test_tezlink_execution = ~error_msg:"Expected \"%R\" but got \"%L\"") ; unit +let test_tezlink_batch = + register_tezlink_test + ~title:"Test of tezlink batches" + ~tags:["batch"; "multiple_transfers"] + ~bootstrap_accounts:[Constant.bootstrap1] + @@ fun {sequencer; client; _} _protocol -> + let endpoint = + Client.( + Foreign_endpoint + Endpoint. + {(Evm_node.rpc_endpoint_record sequencer) with path = "/tezlink"}) + in + let fee_2 = Tez.of_mutez_int 100_000 in + let fee_3 = Tez.of_mutez_int 200_000 in + let amount2 = Tez.of_mutez_int 1_000_000 in + let amount3 = Tez.of_mutez_int 2_000_000 in + let json_batch = + `A + [ + `O + [ + ("destination", `String Constant.bootstrap2.alias); + ("amount", `String (Tez.to_string amount2)); + ("fee", `String (Tez.to_string fee_2)); + ]; + `O + [ + ("destination", `String Constant.bootstrap3.alias); + ("amount", `String (Tez.to_string amount3)); + ("fee", `String (Tez.to_string fee_3)); + ]; + ] + |> JSON.encode_u + in + let* init_balance1 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap1.alias client + in + let* init_balance2 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap2.alias client + in + let* init_balance3 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap3.alias client + in + let*! () = + Client.multiple_transfers + ~endpoint + ~giver:Constant.bootstrap1.alias + ~json_batch + client + in + let*@ _ = produce_block sequencer in + let* end_balance1 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap1.alias client + in + let* end_balance2 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap2.alias client + in + let* end_balance3 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap3.alias client + in + Check.( + (Tez.to_mutez end_balance1 + = Tez.to_mutez init_balance1 - Tez.to_mutez amount2 - Tez.to_mutez amount3 + - Tez.to_mutez fee_2 - Tez.to_mutez fee_3) + int) + ~error_msg:"Wrong balance for bootstrap1: expected %R, actual %L" ; + Check.( + (Tez.to_mutez end_balance2 + = Tez.to_mutez init_balance2 + Tez.to_mutez amount2) + int) + ~error_msg:"Wrong balance for bootstrap2: expected %R, actual %L" ; + Check.( + (Tez.to_mutez end_balance3 + = Tez.to_mutez init_balance3 + Tez.to_mutez amount3) + int) + ~error_msg:"Wrong balance for bootstrap3: expected %R, actual %L" ; + unit + let test_tezlink_sandbox () = Test.register ~__FILE__ @@ -1152,7 +1230,7 @@ let test_tezlink_internal_operation = Check.( (Tez.to_mutez balance = Tez.to_mutez bootstrap_balance + Tez.(to_mutez one)) int) - ~error_msg:"Wrong balance for bootstrap1: exptected %R, actual %L" ; + ~error_msg:"Wrong balance for bootstrap1: expected %R, actual %L" ; unit let () = @@ -1184,6 +1262,7 @@ let () = test_tezlink_block_info [Alpha] ; test_tezlink_storage [Alpha] ; test_tezlink_execution [Alpha] ; + test_tezlink_batch [Alpha] ; test_tezlink_bootstrap_block_info [Alpha] ; test_tezlink_sandbox () ; test_tezlink_internal_operation [Alpha] -- GitLab From 54e81f2a5074a0d4af9ca43531b0375a0a6575dc Mon Sep 17 00:00:00 2001 From: Luciano Freitas Date: Thu, 7 Aug 2025 11:10:18 +0200 Subject: [PATCH 4/4] Tezlink/Tezt: test reveal + transfer batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raphaël Cauderlier --- etherlink/tezt/tests/tezlink.ml | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/etherlink/tezt/tests/tezlink.ml b/etherlink/tezt/tests/tezlink.ml index 54e525bb6303..7b52f7fb3996 100644 --- a/etherlink/tezt/tests/tezlink.ml +++ b/etherlink/tezt/tests/tezlink.ml @@ -1031,6 +1031,97 @@ let test_tezlink_execution = ~error_msg:"Expected \"%R\" but got \"%L\"") ; unit +let test_tezlink_reveal_transfer_batch = + let bootstrap_balance = Tez.of_mutez_int 3_800_000_000_000 in + register_tezlink_test + ~title:"Test Tezlink reveal+transfer batch" + ~tags:["kernel"; "reveal"; "transfer"; "batch"] + ~bootstrap_accounts:[Constant.bootstrap1] + @@ fun {sequencer; client; _} _protocol -> + let endpoint = + Client.( + Foreign_endpoint + Endpoint. + {(Evm_node.rpc_endpoint_record sequencer) with path = "/tezlink"}) + in + + (* Unfortunately, the client does not allow us to stipulate fees when revealing + an account with a batch, so we need to capture this information from the log *) + let collected_fees : int list ref = ref [] in + + (* Regex: look for "fee" and then find the number after the ꜩ symbol *) + let fee_regex = Str.regexp_case_fold "fee[^ꜩ]*ꜩ\\([0-9]+\\.?[0-9]*\\)" in + + let on_log line = + match + try Some (Str.search_forward fee_regex line 0) with Not_found -> None + with + | Some _ -> ( + let fee_str = Str.matched_group 1 line in + (* Remove the dot and calculate the new integer string *) + let fee_mutez_str = + match String.split_on_char '.' fee_str with + | [int_part; frac_part] -> + let frac_part_padded = + frac_part + ^ String.make (max 0 (6 - String.length frac_part)) '0' + in + int_part ^ String.sub frac_part_padded 0 6 + | [int_part] -> int_part ^ "000000" + | _ -> "" (* fallback for malformed input *) + in + match int_of_string_opt fee_mutez_str with + | Some fee_mutez -> + collected_fees := fee_mutez :: !collected_fees ; + Log.info "Captured fee: %s (mutez: %d)" fee_str fee_mutez + | None -> Log.warn "Could not parse fee: %s" fee_str) + | None -> () + in + + let on_spawn _ _ = () in + + let* () = + Client.transfer + ~endpoint + ~amount:(Tez.of_int 2) + ~giver:Constant.bootstrap1.alias + ~receiver:Constant.bootstrap2.alias + ~burn_cap:Tez.one + ~hooks:{on_log; on_spawn} + client + in + let fee_bootstrap1 = List.fold_left ( + ) 0 !collected_fees in + collected_fees := [] ; + let*@ _ = produce_block sequencer in + let* () = + Client.transfer + ~endpoint + ~amount:Tez.one + ~giver:Constant.bootstrap2.alias + ~receiver:Constant.bootstrap1.alias + ~burn_cap:Tez.one + ~hooks:{on_log; on_spawn} + client + in + let fee_bootstrap2 = List.fold_left ( + ) 0 !collected_fees in + let*@ _ = produce_block sequencer in + let* balance1 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap1.alias client + in + let* balance2 = + Client.get_balance_for ~endpoint ~account:Constant.bootstrap2.alias client + in + Log.info "bootstrap_1 fees: %d" fee_bootstrap1 ; + Log.info "bootstrap_2 fees: %d" fee_bootstrap2 ; + Check.( + (Tez.to_mutez balance1 + = Tez.to_mutez bootstrap_balance - Tez.(to_mutez one) - fee_bootstrap1) + int) + ~error_msg:"Wrong balance for bootstrap1: expected %R, actual %L" ; + Check.((Tez.to_mutez balance2 = Tez.(to_mutez one) - fee_bootstrap2) int) + ~error_msg:"Wrong balance for bootstrap2: expected %R, actual %L" ; + unit + let test_tezlink_batch = register_tezlink_test ~title:"Test of tezlink batches" @@ -1262,6 +1353,7 @@ let () = test_tezlink_block_info [Alpha] ; test_tezlink_storage [Alpha] ; test_tezlink_execution [Alpha] ; + test_tezlink_reveal_transfer_batch [Alpha] ; test_tezlink_batch [Alpha] ; test_tezlink_bootstrap_block_info [Alpha] ; test_tezlink_sandbox () ; -- GitLab