From 6e55d1e5abf7ee16d630fd186d4e0a2333cab361 Mon Sep 17 00:00:00 2001 From: Adam Allombert-Goget Date: Wed, 2 Apr 2025 18:05:41 +0200 Subject: [PATCH 1/5] tezt/node: add helper wait_for_branch_switch --- tezt/lib_tezos/node.ml | 14 ++++++++++++++ tezt/lib_tezos/node.mli | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/tezt/lib_tezos/node.ml b/tezt/lib_tezos/node.ml index a9cb0d9531eb..52c4071a0f3d 100644 --- a/tezt/lib_tezos/node.ml +++ b/tezt/lib_tezos/node.ml @@ -795,6 +795,20 @@ let wait_for_disconnections node disconnections = let* () = wait_for_ready node in waiter +let wait_for_branch_switch ?level ?hash node = + wait_for + node + "branch_switch.v0" + JSON.( + fun json -> + let level' = json |-> "level" |> as_int in + let hash' = json |-> "view" |-> "hash" |> as_string in + if + Option.fold ~none:true ~some:(Int.equal level') level + && Option.fold ~none:true ~some:(String.equal hash') hash + then Some (level', hash') + else None) + let enable_external_rpc_process = match Sys.getenv_opt "TZ_SCHEDULE_KIND" with | Some "EXTENDED_RPC_TESTS" -> true diff --git a/tezt/lib_tezos/node.mli b/tezt/lib_tezos/node.mli index 9aa434948c37..4baf449902fd 100644 --- a/tezt/lib_tezos/node.mli +++ b/tezt/lib_tezos/node.mli @@ -624,6 +624,11 @@ val wait_for_connections : t -> int -> unit Lwt.t [n] ["disconnection"] Chain validator events. *) val wait_for_disconnections : t -> int -> unit Lwt.t +(** Waits for the node to switch branches. + Resolves when the new branch matches [hash] and [level], if provided. *) +val wait_for_branch_switch : + ?level:int -> ?hash:string -> t -> (int * string) Lwt.t + (** Raw events. *) type event = {name : string; value : JSON.t; timestamp : float} -- GitLab From abcbd45ec8cf8c4b69db0fa9e2a7fa02821e72d2 Mon Sep 17 00:00:00 2001 From: Adam Allombert-Goget Date: Wed, 23 Apr 2025 06:08:00 +0200 Subject: [PATCH 2/5] tezt/operation_core: add consensus helpers --- tezt/lib_tezos/operation_core.ml | 39 +++++++++++++++++++++++++++++++ tezt/lib_tezos/operation_core.mli | 33 ++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/tezt/lib_tezos/operation_core.ml b/tezt/lib_tezos/operation_core.ml index e9a49c78aea6..501336dad1a7 100644 --- a/tezt/lib_tezos/operation_core.ml +++ b/tezt/lib_tezos/operation_core.ml @@ -407,6 +407,19 @@ module Consensus = struct (delegate, slots)) (as_list rpc_json) + let get_slots_by_consensus_key ~level client = + let* rpc_json = + Client.RPC.call client @@ RPC.get_chain_block_helper_validators ~level () + in + let open JSON in + return + @@ List.map + (fun json -> + let consensus_key = json |-> "consensus_key" |> as_string in + let slots = json |-> "slots" |> as_list |> List.map as_int in + (consensus_key, slots)) + (as_list rpc_json) + let first_slot ~slots (delegate : Account.key) = match List.assoc_opt delegate.public_key_hash slots with | Some slots -> List.hd slots @@ -418,6 +431,32 @@ module Consensus = struct @@ RPC.get_chain_block_header ?block () in return JSON.(block_header |-> "payload_hash" |> as_string) + + let get_branch ~attested_level client = + let block = string_of_int (attested_level - 2) in + Client.RPC.call_via_endpoint client @@ RPC.get_chain_block_hash ~block () + + let preattest_for ~protocol ~slot ~level ~round ~block_payload_hash ?branch + delegate client = + let preattestation = + preattestation ~slot ~level ~round ~block_payload_hash + in + let* branch = + match branch with + | Some branch -> return branch + | None -> get_branch ~attested_level:level client + in + inject ~protocol ~branch ~signer:delegate preattestation client + + let attest_for ~protocol ~slot ~level ~round ~block_payload_hash ?branch + delegate client = + let attestation = attestation ~slot ~level ~round ~block_payload_hash () in + let* branch = + match branch with + | Some branch -> return branch + | None -> get_branch ~attested_level:level client + in + inject ~protocol ~branch ~signer:delegate attestation client end module Anonymous = struct diff --git a/tezt/lib_tezos/operation_core.mli b/tezt/lib_tezos/operation_core.mli index a74b7292eec0..f1e725fc9efa 100644 --- a/tezt/lib_tezos/operation_core.mli +++ b/tezt/lib_tezos/operation_core.mli @@ -312,6 +312,10 @@ module Consensus : sig the owned slot list *) val get_slots : level:int -> Client.t -> (string * int list) list Lwt.t + (** Same as [get_slots] but maps a consensus key to the owned slot list. *) + val get_slots_by_consensus_key : + level:int -> Client.t -> (string * int list) list Lwt.t + (** Returns the first slot of the provided delegate in the [slots] association list that describes all attestation rights at some level. @@ -321,6 +325,35 @@ module Consensus : sig (** Calls the [GET /chains//blocks//header] RPC and extracts the head block's payload hash from the result. *) val get_block_payload_hash : ?block:string -> Client.t -> string Lwt.t + + (** Calls the [GET /chains/main/blocks//hash] RPC with + = [attested_level] - 2. The returned block hash can be used as the + branch field of a consensus operation for [attested_level]. *) + val get_branch : attested_level:int -> Client.t -> string Lwt.t + + (** Forge and inject a preattestation for the given account. *) + val preattest_for : + protocol:Protocol.t -> + slot:int -> + level:int -> + round:int -> + block_payload_hash:string -> + ?branch:string -> + Account.key -> + Client.t -> + [`OpHash of string] Lwt.t + + (** Forge and inject an attestation for the given account. *) + val attest_for : + protocol:Protocol.t -> + slot:int -> + level:int -> + round:int -> + block_payload_hash:string -> + ?branch:string -> + Account.key -> + Client.t -> + [`OpHash of string] Lwt.t end module Anonymous : sig -- GitLab From 4c94ef3e39468544749fb3163d8e22d399021112 Mon Sep 17 00:00:00 2001 From: Adam Allombert-Goget Date: Thu, 10 Apr 2025 17:43:30 +0200 Subject: [PATCH 3/5] tezt/client: add helper update_fresh_consensus_key --- tezt/lib_tezos/client.ml | 16 ++++++++++++++++ tezt/lib_tezos/client.mli | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 62bc0a4db6e2..db73920733fe 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -1482,6 +1482,22 @@ let update_consensus_key ?hooks ?endpoint ?(wait = "none") ?burn_cap @ optional_arg "burn-cap" Tez.to_string burn_cap) |> Process.check ?expect_failure +let update_fresh_consensus_key ?alias ?algo ?hooks ?endpoint ?wait ?burn_cap + ?expect_failure (delegate : Account.key) client = + let* key = gen_and_show_keys ?alias ?sig_alg:algo client in + let* () = + update_consensus_key + ?hooks + ?endpoint + ?wait + ?burn_cap + ?expect_failure + ~src:delegate.alias + ~pk:key.alias + client + in + return key + let update_companion_key ?hooks ?endpoint ?(wait = "none") ?burn_cap ?expect_failure ~src ~pk client = spawn_command diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index 1851ef99b423..f0de43a94b20 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -1166,6 +1166,23 @@ val update_consensus_key : t -> unit Lwt.t +(** [update_fresh_consensus_key delegate client] runs the following commands: + [octez-client gen keys] to generate a new key, + [octez-client show address] to retrieve the newly generated key , + and [octez-client update consensus key for to ]. + Returns the newly generated key . *) +val update_fresh_consensus_key : + ?alias:string -> + ?algo:string -> + ?hooks:Process_hooks.t -> + ?endpoint:endpoint -> + ?wait:string -> + ?burn_cap:Tez.t -> + ?expect_failure:bool -> + Account.key -> + t -> + Account.key Lwt.t + (** Run [octez-client set companion key for to ] *) val update_companion_key : ?hooks:Process.hooks -> -- GitLab From 4f3525b3ef15836ad132164a5451275aad416f1f Mon Sep 17 00:00:00 2001 From: Adam Allombert-Goget Date: Wed, 23 Apr 2025 06:13:36 +0200 Subject: [PATCH 4/5] tezt/baker_test: add helper functions --- tezt/tests/baker_test.ml | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tezt/tests/baker_test.ml b/tezt/tests/baker_test.ml index d891410b9f5f..66e1e9cdf27c 100644 --- a/tezt/tests/baker_test.ml +++ b/tezt/tests/baker_test.ml @@ -55,6 +55,9 @@ let fetch_baking_rights client level = (round, delegate_pkh)) JSON.(as_list baking_rights_json) +let fetch_round ?block client = + Client.RPC.call client @@ RPC.get_chain_block_helper_round ?block () + let check_node_version_check_bypass_test = Protocol.register_test ~__FILE__ @@ -455,6 +458,97 @@ let check_aggregate ~expected_committee aggregate_json = pp committee +let fetch_consensus_operations ?block client = + let* json = + Client.RPC.call client + @@ RPC.get_chain_block_operations_validation_pass + ?block + ~validation_pass:0 + () + in + return JSON.(as_list json) + +type kind = Attestation | Preattestation + +let pp_kind fmt = function + | Attestation -> Format.fprintf fmt "attestation" + | Preattestation -> Format.fprintf fmt "preattestation" + +(** [check_consensus_aux kind ~expected found] fails if the set of delegates in + the consensus operations list [found] differs from the set [expected]. + See [check_consensus_operations]. *) +let check_consensus_aux kind ~expected found = + match (expected, found) with + | None, _ -> () + | Some expected, _ -> + let sorted_expected = List.sort String.compare expected in + let sorted_found = + found + |> List.map + JSON.( + fun operation -> + operation |-> "contents" |> as_list |> List.hd |-> "metadata" + |-> "delegate" |> as_string) + |> List.sort String.compare + in + if not (List.equal String.equal sorted_expected sorted_found) then + let pp = Format.(pp_print_list ~pp_sep:pp_print_cut pp_print_string) in + Test.fail + "@[Wrong %a set@,@[expected:@,%a@]@,@[found:@,%a@]@]" + pp_kind + kind + pp + sorted_expected + pp + sorted_found + +(** Fetch consensus operations and check that they match the expected contents. + *) +let check_consensus_operations ?expected_aggregated_committee + ?expected_preattestations ?expected_attestations ?block client = + let* consensus_operations = fetch_consensus_operations ?block client in + (* Partition the consensus operations list by kind *) + let attestations_aggregates, attestations, preattestations = + List.fold_left + (fun (attestations_aggregates, attestations, preattestations) operation -> + let kind = + JSON.( + operation |-> "contents" |> as_list |> List.hd |-> "kind" + |> as_string) + in + match kind with + | "attestations_aggregate" -> + (operation :: attestations_aggregates, attestations, preattestations) + | "attestation" | "attestation_with_dal" -> + (attestations_aggregates, operation :: attestations, preattestations) + | "preattestation" -> + (attestations_aggregates, attestations, operation :: preattestations) + | _ -> Test.fail "check_consensus_operations: unexpected operation") + ([], [], []) + consensus_operations + in + (* Checking attestations_aggregate *) + let* () = + match (expected_aggregated_committee, attestations_aggregates) with + | _, _ :: _ :: _ -> Test.fail "Multiple attestations_aggregate found" + | None, _ -> unit + | Some _, [] -> Test.fail "No attestations_aggregate found" + | Some expected_committee, [attestations_aggregate] -> + return @@ check_aggregate ~expected_committee attestations_aggregate + in + (* Checking attestations *) + let () = + check_consensus_aux Attestation ~expected:expected_attestations attestations + in + (* Checking preattestations *) + let () = + check_consensus_aux + Preattestation + ~expected:expected_preattestations + preattestations + in + unit + (* [find_aggregate_receipt operations] returns the sole attestations aggregate found in [operations]. Fails the test if no such operation exists or if more than one if found. *) @@ -501,6 +595,9 @@ let check_for_non_aggregated_eligible_attestations consensus_operations = if has_non_aggregated_eligible_attestations then Test.fail "The block contains a non-aggregated eligible attestation" +let public_key_hashes = + List.map (fun (account : Account.key) -> account.public_key_hash) + (* Test that the baker aggregates eligible attestations.*) let simple_attestations_aggregation = Protocol.register_test -- GitLab From a98bcc412a451af6b2c94280e0a0ac1fead028d1 Mon Sep 17 00:00:00 2001 From: Adam Allombert-Goget Date: Tue, 22 Apr 2025 21:14:52 +0200 Subject: [PATCH 5/5] tezt/baker_test: add a test for baker aggregation when reproposing --- tezt/tests/baker_test.ml | 217 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 1 deletion(-) diff --git a/tezt/tests/baker_test.ml b/tezt/tests/baker_test.ml index 66e1e9cdf27c..fa14f109b785 100644 --- a/tezt/tests/baker_test.ml +++ b/tezt/tests/baker_test.ml @@ -847,6 +847,220 @@ let prequorum_check_levels = let* _ = Mempool.get_mempool client in unit +(* Test that the baker correctly aggregates eligible attestations on reproposals.*) +let attestations_aggregation_on_reproposal = + Protocol.register_test + ~__FILE__ + ~title:"Attestations aggregation on reproposal" + ~tags:[team; "baker"; "attestation"; "aggregation"; "reproposal"] + ~supports:Protocol.(From_protocol 023) + ~uses:(fun _protocol -> [Constant.octez_agnostic_baker]) + @@ fun protocol -> + let consensus_rights_delay = 1 in + let consensus_committee_size = 256 in + let* parameter_file = + Protocol.write_parameter_file + ~base:(Right (protocol, None)) + [ + (["allow_tz4_delegate_enable"], `Bool true); + (["aggregate_attestation"], `Bool true); + (* Using custom consensus constants to be able to trigger reproposals *) + (["consensus_committee_size"], `Int consensus_committee_size); + (["consensus_threshold_size"], `Int 70); + (* Diminish some constants to activate consensus keys faster, + and make round durations as small as possible *) + (["minimal_block_delay"], `String "4"); + (["delay_increment_per_round"], `String "0"); + (["blocks_per_cycle"], `Int 2); + (["nonce_revelation_threshold"], `Int 1); + (["consensus_rights_delay"], `Int consensus_rights_delay); + (["cache_sampler_state_cycles"], `Int (consensus_rights_delay + 3)); + (["cache_stake_distribution_cycles"], `Int (consensus_rights_delay + 3)); + ] + in + let* node, client = + Client.init_with_protocol + `Client + ~additional_revealed_bootstrap_account_count:1 + ~protocol + ~parameter_file + ~timestamp:Now + () + in + let* _ = Node.wait_for_level node 1 in + let bootstrap1, bootstrap2, bootstrap3, bootstrap4, bootstrap5 = + Constant.(bootstrap1, bootstrap2, bootstrap3, bootstrap4, bootstrap5) + in + (* Setup bootstrap6 as an additional delegate *) + let* bootstrap6 = Client.show_address ~alias:"bootstrap6" client in + Log.info + "Generate BLS keys and assign them as consensus keys for bootstrap 1 to 3" ; + let* ck1 = Client.update_fresh_consensus_key ~algo:"bls" bootstrap1 client in + let* ck2 = Client.update_fresh_consensus_key ~algo:"bls" bootstrap2 client in + let* ck3 = Client.update_fresh_consensus_key ~algo:"bls" bootstrap3 client in + Log.info "Launch a baker with bootstrap5" ; + (* Bootstrap5 does not have enough voting power to progress independently. We + manually inject consensus operations to control the progression of + consensus. *) + let* _baker = + Agnostic_baker.init + ~delegates:[Constant.bootstrap5.public_key_hash] + node + client + in + Log.info "Bake until BLS consensus keys are activated" ; + let keys = + public_key_hashes + [ck1; ck2; ck3; bootstrap1; bootstrap2; bootstrap3; bootstrap4] + in + let* () = Client.bake_for_and_wait ~keys ~count:4 client in + (* BLS consensus keys are now activated. We feed the node with just enough + consensus operations for the baker to bake a block at level 6. *) + let* slots = Operation.Consensus.get_slots_by_consensus_key ~level:5 client in + let* round = fetch_round client in + let* branch = Operation.Consensus.get_branch ~attested_level:5 client in + let* block_payload_hash = + Operation.Consensus.get_block_payload_hash ~block:"5" client + in + Log.info "Injecting consensus for bootstrap1 at level 5 round %d@." round ; + let* () = + Operation.Consensus.( + let slot = first_slot ~slots ck1 in + let* _ = + preattest_for + ~protocol + ~branch + ~slot + ~level:5 + ~round + ~block_payload_hash + ck1 + client + in + let* _ = + attest_for + ~protocol + ~branch + ~slot + ~level:5 + ~round + ~block_payload_hash + ck1 + client + in + unit) + in + let* _ = Node.wait_for_level node 6 in + let* () = + check_consensus_operations + ~expected_aggregated_committee:(public_key_hashes [bootstrap1]) + ~expected_attestations:(public_key_hashes [bootstrap5]) + client + in + (* The baker running bootstrap5 doesn't have enough voting power to progress + alone. Since we won't attest any block at level 6, it will keep baking + level 6 as round increases. *) + Log.info "Attesting level 5 round %d with bootstrap2 & 4" round ; + (* Inject additional attestations for level 5. These attestations are expected + to be included in the coming level 6 proposal. In particular, bootstrap2 + attestation is expected to be incorporated into the aggregation. + Since the baker didn't witnessed a prequorum, it is expected to bake a + fresh proposal. *) + let* () = + Lwt_list.iter_s + Operation.Consensus.( + fun (delegate : Account.key) -> + let slot = first_slot ~slots delegate in + let* _ = + attest_for + ~protocol + ~branch + ~slot + ~level:5 + ~round + ~block_payload_hash + delegate + client + in + unit) + [ck2; bootstrap4] + in + let* _ = Node.wait_for_branch_switch ~level:6 node in + let* () = + check_consensus_operations + ~expected_aggregated_committee: + (public_key_hashes [bootstrap1; bootstrap2]) + ~expected_attestations:(public_key_hashes [bootstrap4; bootstrap5]) + client + in + Log.info "Preattesting the latest block at level 6 with bootstrap1 & 2" ; + (* We preattest the latest block at level 6 with enough voting power to + trigger a prequorum. Consequently, the baker is expected to lock on the + preattested payload and only bake reproposals. *) + let* () = + let* slots = + Operation.Consensus.get_slots_by_consensus_key ~level:6 client + in + let* round = fetch_round client in + let* branch = Operation.Consensus.get_branch ~attested_level:6 client in + let* block_payload_hash = + Operation.Consensus.get_block_payload_hash client + in + Log.info "Preattesting level 6 round %d with branch %s" round branch ; + Lwt_list.iter_s + Operation.Consensus.( + fun (delegate : Account.key) -> + let slot = first_slot ~slots delegate in + let* _ = + preattest_for + ~protocol + ~branch + ~slot + ~level:6 + ~round + ~block_payload_hash + delegate + client + in + unit) + [ck1; ck2; bootstrap4] + in + (* Inject additional attestations for level 5. These attestations are expected + to be included in the coming level 6 reproposals. In particular, bootstrap3 + attestation is expected to be incorporated into the aggregation. *) + Log.info "Attesting level 5 round %d with bootstrap3 & 6" round ; + let* () = + Lwt_list.iter_s + Operation.Consensus.( + fun (delegate : Account.key) -> + let slot = first_slot ~slots delegate in + let* _ = + attest_for + ~protocol + ~branch + ~slot + ~level:5 + ~round + ~block_payload_hash + delegate + client + in + unit) + [ck3; bootstrap6] + in + let* _ = Node.wait_for_branch_switch ~level:6 node in + let* () = + check_consensus_operations + ~expected_aggregated_committee: + (public_key_hashes [bootstrap1; bootstrap2; bootstrap3]) + ~expected_attestations: + (public_key_hashes [bootstrap4; bootstrap5; bootstrap6]) + ~expected_preattestations: + (public_key_hashes [bootstrap1; bootstrap2; bootstrap4; bootstrap5]) + client + in + unit + let register ~protocols = check_node_version_check_bypass_test protocols ; check_node_version_allowed_test protocols ; @@ -861,4 +1075,5 @@ let register ~protocols = baker_check_consensus_branch protocols ; force_apply_from_round protocols ; simple_attestations_aggregation protocols ; - prequorum_check_levels protocols + prequorum_check_levels protocols ; + attestations_aggregation_on_reproposal protocols -- GitLab