diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index dc3aaf1657beb7324904d0f999844676dca6e692..f9380be7f37ceb0f6695b35656868e295928d0a2 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -1278,6 +1278,8 @@ module Level : sig val dawn_of_a_new_cycle : context -> Cycle.t option val may_compute_randao : context -> bool + + module Map : Map.S with type key = t end (** This module re-exports definitions from {!Fitness_repr}. *) @@ -5415,6 +5417,7 @@ module Consensus : sig with type t := t and type slot := Slot.t and type 'a slot_map := 'a Slot.Map.t + and type 'a level_map := 'a Level.Map.t and type slot_set := Slot.Set.t and type round := Round.t and type consensus_pk := Consensus_key.pk diff --git a/src/proto_alpha/lib_protocol/level_repr.ml b/src/proto_alpha/lib_protocol/level_repr.ml index e05e060345823192a8e71724500e607c8ba67eda..5c9aeac422277dcec68d4e7083df49a74e90589b 100644 --- a/src/proto_alpha/lib_protocol/level_repr.ml +++ b/src/proto_alpha/lib_protocol/level_repr.ml @@ -31,11 +31,14 @@ type t = { expected_commitment : bool; } -include Compare.Make (struct +module Ordered = struct type nonrec t = t let compare {level = l1; _} {level = l2; _} = Raw_level_repr.compare l1 l2 -end) +end + +include Compare.Make (Ordered) +module Map = Map.Make (Ordered) type level = t diff --git a/src/proto_alpha/lib_protocol/level_repr.mli b/src/proto_alpha/lib_protocol/level_repr.mli index bc5ffd4988a9d98d9e16b1da877b2fc123754aff..f5dab726edcedc3994738fdc02c817edc6094ae4 100644 --- a/src/proto_alpha/lib_protocol/level_repr.mli +++ b/src/proto_alpha/lib_protocol/level_repr.mli @@ -48,6 +48,8 @@ type level = t include Compare.S with type t := level +module Map : Map.S with type key = t + val encoding : level Data_encoding.t val pp : Format.formatter -> level -> unit diff --git a/src/proto_alpha/lib_protocol/main.ml b/src/proto_alpha/lib_protocol/main.ml index 573bb82d59e46941802ddf316d33e833e1c36a92..c4ad1e69f82fa17a062d9af0ccc0e55f96426b3b 100644 --- a/src/proto_alpha/lib_protocol/main.ml +++ b/src/proto_alpha/lib_protocol/main.ml @@ -162,30 +162,59 @@ let init_consensus_rights_for_block ctxt mode ~predecessor_level = let init_consensus_rights_for_mempool ctxt ~predecessor_level = let open Lwt_result_syntax in let open Alpha_context in - (* We don't want to compute the tables by first slot for all three - possible levels because it is time-consuming. So we don't compute - any [allowed_attestations] or [allowed_preattestations] tables. *) - let ctxt = - Consensus.initialize_consensus_operation - ctxt - ~allowed_attestations:None - ~allowed_preattestations:None - in - (* However, we want to ensure that the cycle rights are loaded in - the context, so that {!Stake_distribution.slot_owner} doesn't - have to initialize them each time it is called (we do this now - because the context is discarded at the end of the validation of - each operation, so we can't rely on the caching done by - [slot_owner] itself). *) - let cycle = (Level.current ctxt).cycle in - let* ctxt = Stake_distribution.load_sampler_for_cycle ctxt cycle in - (* If the cycle has changed between the grandparent level and the - current level, we also initialize the sampler for that - cycle. That way, all three allowed levels are covered. *) - match Level.pred ctxt predecessor_level with - | Some gp_level when Cycle.(gp_level.cycle <> cycle) -> - Stake_distribution.load_sampler_for_cycle ctxt gp_level.cycle - | Some _ | None -> return ctxt + if Constants.aggregate_attestation ctxt then + (* Under feature flag, we do compute minimal_slots for each level. *) + let allowed_levels = + let levels = [predecessor_level; Level.(succ ctxt predecessor_level)] in + match Level.pred ctxt predecessor_level with + | Some grandparent_level -> grandparent_level :: levels + | None -> levels + in + let* ctxt, minimal_slots = + List.fold_left_es + (fun (ctxt, minimal_slots) level -> + let* ctxt, level_minimal_slot = + Baking.attesting_rights_by_first_slot ctxt level + in + return (ctxt, Level.Map.add level level_minimal_slot minimal_slots)) + (ctxt, Level.Map.empty) + allowed_levels + in + let ctxt = + Consensus.initialize_consensus_operation + ctxt + ~allowed_attestations:None + ~allowed_preattestations:None + in + let ctxt = + Consensus.initialize_allowed_consensus ctxt (Some minimal_slots) + in + return ctxt + else + (* We don't want to compute the tables by first slot for all three + possible levels because it is time-consuming. So we don't compute + any [allowed_attestations] or [allowed_preattestations] tables. *) + let ctxt = + Consensus.initialize_consensus_operation + ctxt + ~allowed_attestations:None + ~allowed_preattestations:None + in + (* However, we want to ensure that the cycle rights are loaded in + the context, so that {!Stake_distribution.slot_owner} doesn't + have to initialize them each time it is called (we do this now + because the context is discarded at the end of the validation of + each operation, so we can't rely on the caching done by + [slot_owner] itself). *) + let cycle = (Level.current ctxt).cycle in + let* ctxt = Stake_distribution.load_sampler_for_cycle ctxt cycle in + (* If the cycle has changed between the grandparent level and the + current level, we also initialize the sampler for that + cycle. That way, all three allowed levels are covered. *) + match Level.pred ctxt predecessor_level with + | Some gp_level when Cycle.(gp_level.cycle <> cycle) -> + Stake_distribution.load_sampler_for_cycle ctxt gp_level.cycle + | Some _ | None -> return ctxt let prepare_ctxt ctxt mode ~(predecessor : Block_header.shell_header) = let open Lwt_result_syntax in diff --git a/src/proto_alpha/lib_protocol/raw_context.ml b/src/proto_alpha/lib_protocol/raw_context.ml index afeb7f0999d738d947199e963198569eaec21198..e8ebe72b7a3ba34966148ee3e7a7692a3f4e78aa 100644 --- a/src/proto_alpha/lib_protocol/raw_context.ml +++ b/src/proto_alpha/lib_protocol/raw_context.ml @@ -100,6 +100,10 @@ module Raw_consensus = struct consensus attestation power and DAL attestation power. This is [None] only in mempool mode, or in application mode when there is no locked round (so the block cannot contain any preattestations). *) + allowed_consensus : + (consensus_pk * int * int) Slot_repr.Map.t Level_repr.Map.t option; + (** In mempool mode, hold delegates minimal slots for all allowed + levels. [None] in all other modes. *) forbidden_delegates : Signature.Public_key_hash.Set.t; (** Delegates that are not allowed to bake or attest blocks; i.e., delegates which have zero frozen deposit due to a previous @@ -132,6 +136,7 @@ module Raw_consensus = struct current_attestation_power = 0; allowed_attestations = Some Slot_repr.Map.empty; allowed_preattestations = Some Slot_repr.Map.empty; + allowed_consensus = None; forbidden_delegates = Signature.Public_key_hash.Set.empty; attestations_seen = Slot_repr.Set.empty; preattestations_seen = Slot_repr.Set.empty; @@ -215,6 +220,9 @@ module Raw_consensus = struct ~allowed_preattestations t = {t with allowed_attestations; allowed_preattestations} + let initialize_with_allowed_consensus ~allowed_consensus t = + {t with allowed_consensus} + let locked_round_evidence t = t.locked_round_evidence let attestation_branch t = t.attestation_branch @@ -1928,6 +1936,8 @@ module type CONSENSUS = sig type 'value slot_map + type 'value level_map + type slot_set type slot @@ -1940,6 +1950,9 @@ module type CONSENSUS = sig val allowed_preattestations : t -> (consensus_pk * int * int) slot_map option + val allowed_consensus : + t -> (consensus_pk * int * int) slot_map level_map option + val forbidden_delegates : t -> Signature.Public_key_hash.Set.t type error += Slot_map_not_found of {loc : string} @@ -1952,6 +1965,9 @@ module type CONSENSUS = sig allowed_preattestations:(consensus_pk * int * int) slot_map option -> t + val initialize_allowed_consensus : + t -> (consensus_pk * int * int) slot_map level_map option -> t + val record_attestation : t -> initial_slot:slot -> power:int -> t tzresult val record_preattestation : @@ -1979,6 +1995,7 @@ module Consensus : with type t := t and type slot := Slot_repr.t and type 'a slot_map := 'a Slot_repr.Map.t + and type 'a level_map := 'a Level_repr.Map.t and type slot_set := Slot_repr.Set.t and type round := Round_repr.t and type consensus_pk := consensus_pk = struct @@ -1996,6 +2013,8 @@ module Consensus : let[@inline] allowed_preattestations ctxt = ctxt.back.consensus.allowed_preattestations + let[@inline] allowed_consensus ctxt = ctxt.back.consensus.allowed_consensus + let[@inline] forbidden_delegates ctxt = ctxt.back.consensus.forbidden_delegates @@ -2019,6 +2038,11 @@ module Consensus : ~allowed_attestations ~allowed_preattestations) + let[@inline] initialize_allowed_consensus ctxt allowed_consensus = + update_consensus_with + ctxt + (Raw_consensus.initialize_with_allowed_consensus ~allowed_consensus) + let[@inline] record_preattestation ctxt ~initial_slot ~power round = update_consensus_with_tzresult ctxt diff --git a/src/proto_alpha/lib_protocol/raw_context.mli b/src/proto_alpha/lib_protocol/raw_context.mli index a5289b02b6bbbbefee60b7b80d44c8da9dcca40f..c4b0a70e30c268040181716acceb474527c55a5e 100644 --- a/src/proto_alpha/lib_protocol/raw_context.mli +++ b/src/proto_alpha/lib_protocol/raw_context.mli @@ -334,6 +334,8 @@ module type CONSENSUS = sig type 'value slot_map + type 'value level_map + type slot_set type slot @@ -350,6 +352,12 @@ module type CONSENSUS = sig (** See {!allowed_attestations}. *) val allowed_preattestations : t -> (consensus_pk * int * int) slot_map option + (** Returns a map that associates a level with a slot map. + The slot map links a delegate's public key to a tuple containing + (minimal_slot, voting_power, dal_power). See {!allowed_attestations} *) + val allowed_consensus : + t -> (consensus_pk * int * int) slot_map level_map option + (** Returns the set of delegates that are not allowed to bake or attest blocks; i.e., delegates which have zero frozen deposit due to a previous slashing. *) @@ -371,6 +379,12 @@ module type CONSENSUS = sig allowed_preattestations:(consensus_pk * int * int) slot_map option -> t + (** Initializes the map of allowed consensus operations, this + function must be called only once and before applying any consensus + operation. *) + val initialize_allowed_consensus : + t -> (consensus_pk * int * int) slot_map level_map option -> t + (** [record_attestation ctx ~initial_slot ~power] records an attestation for the current block. @@ -427,6 +441,7 @@ module Consensus : with type t := t and type slot := Slot_repr.t and type 'a slot_map := 'a Slot_repr.Map.t + and type 'a level_map := 'a Level_repr.Map.t and type slot_set := Slot_repr.Set.t and type round := Round_repr.t and type consensus_pk := consensus_pk diff --git a/src/proto_alpha/lib_protocol/validate.ml b/src/proto_alpha/lib_protocol/validate.ml index 4a9dc28ba37f5a1a6a2d1f4f012ad3c2e6b9e60e..c6a7dee2aad898211cbb13eb66d85886f205690f 100644 --- a/src/proto_alpha/lib_protocol/validate.ml +++ b/src/proto_alpha/lib_protocol/validate.ml @@ -32,6 +32,8 @@ type consensus_info = { predecessor_round : Round.t; preattestation_slot_map : (Consensus_key.pk * int * int) Slot.Map.t option; attestation_slot_map : (Consensus_key.pk * int * int) Slot.Map.t option; + consensus_slot_map : + (Consensus_key.pk * int * int) Slot.Map.t Level.Map.t option; } let init_consensus_info ctxt (predecessor_level, predecessor_round) = @@ -40,6 +42,7 @@ let init_consensus_info ctxt (predecessor_level, predecessor_round) = predecessor_round; preattestation_slot_map = Consensus.allowed_preattestations ctxt; attestation_slot_map = Consensus.allowed_attestations ctxt; + consensus_slot_map = Consensus.allowed_consensus ctxt; } (** Map used to detect consensus operation conflicts. Each delegate may @@ -603,10 +606,27 @@ module Consensus = struct (Consensus_operation_for_future_level {kind; expected; provided}) else ok_unit in - let* (_ctxt : t), consensus_key = - Stake_distribution.slot_owner vi.ctxt (Level.from_raw vi.ctxt level) slot - in - return (consensus_key, 0 (* Fake voting power *)) + if Constants.aggregate_attestation vi.ctxt then + (* Under feature flag, minimal slots are pre-computed for all accepted + levels and stored in consensus_info.consensus_slot_map *) + let level = Level.from_raw vi.ctxt level in + match consensus_info.consensus_slot_map with + | None -> tzfail (Consensus.Slot_map_not_found {loc = __LOC__}) + | Some level_map -> + let slot_map = Level.Map.find level level_map in + let*? consensus_key, voting_power, _dal_power = + get_delegate_details slot_map kind slot + in + return (consensus_key, voting_power) + else + let* (_ctxt : t), consensus_key = + Stake_distribution.slot_owner + vi.ctxt + (Level.from_raw vi.ctxt level) + slot + in + return (consensus_key, 0 (* Fake voting power *)) + (* We do not check that the frozen deposits are positive because this only needs to be true in the context of a block that actually contains the operation, which may not be the same as the current diff --git a/tezt/tests/double_consensus.ml b/tezt/tests/double_consensus.ml index e1a7e889f983718ea4bec2332e7e9523629d357c..c47d6a13c6f6c61d1cbc3a1e47ce338968578e5b 100644 --- a/tezt/tests/double_consensus.ml +++ b/tezt/tests/double_consensus.ml @@ -151,42 +151,6 @@ let double_consensus_wrong_slot let* () = waiter_already_denounced in unit -(** Adaptation of [double_consensus_wrong_slot] under [aggregate_attestations] - feature flag *) -let double_consensus_wrong_slot_feature_flag - (consensus_for, mk_consensus, _, consensus_name) protocol = - let* parameter_file = - Protocol.write_parameter_file - ~base:(Right (protocol, None)) - [(["aggregate_attestation"], `Bool true)] - in - let* (client, accuser), (branch, level, round, slots, block_payload_hash) = - double_attestation_init - ~parameter_file - consensus_for - consensus_name - protocol - () - in - Log.info "Inject an invalid %s and wait for denounciation" consensus_name ; - let op = - mk_consensus ~slot:(List.nth slots 1) ~level ~round ~block_payload_hash - in - let waiter = - Accuser.wait_for accuser "attestation_conflict_ignored.v0" (fun _ -> - Some ()) - in - let* _ = - Operation.Consensus.inject - ~protocol - ~branch - ~signer:Constant.bootstrap1 - op - client - in - let* () = waiter in - unit - let attest_utils = ( Client.attest_for, (fun ~slot ~level ~round ~block_payload_hash -> @@ -216,16 +180,6 @@ let double_preattestation_wrong_slot = ~uses:(fun protocol -> [Protocol.accuser protocol]) @@ fun protocol -> double_consensus_wrong_slot preattest_utils protocol -let double_attestation_wrong_slot_feature_flag = - Protocol.register_test - ~__FILE__ - ~title:"double attestation using wrong slot under feature flag" - ~tags:[Tag.layer1; "double"; "attestation"; "accuser"; "slot"; "node"] - ~supports:(Protocol.From_protocol 023) - ~uses:(fun protocol -> [Protocol.accuser protocol]) - @@ fun protocol -> - double_consensus_wrong_slot_feature_flag attest_utils protocol - let double_consensus_wrong_block_payload_hash (consensus_for, mk_consensus, consensus_waiter, consensus_name) protocol = let* (client, accuser), (branch, level, round, slots, _block_payload_hash) = @@ -513,7 +467,6 @@ let operation_too_far_in_future = let register ~protocols = double_attestation_wrong_slot protocols ; double_preattestation_wrong_slot protocols ; - double_attestation_wrong_slot_feature_flag protocols ; double_attestation_wrong_block_payload_hash protocols ; double_preattestation_wrong_block_payload_hash protocols ; double_attestation_wrong_branch protocols ; diff --git a/tezt/tests/prevalidator.ml b/tezt/tests/prevalidator.ml index c7f755474a4339c375b41967a2a9c731e3edd37c..fb917d6a6334e15cc4ef79ec6259206dde6fb69f 100644 --- a/tezt/tests/prevalidator.ml +++ b/tezt/tests/prevalidator.ml @@ -3760,6 +3760,104 @@ module Revamped = struct client) in unit + + let consensus_minimal_slots_feature_flag = + Protocol.register_test + ~__FILE__ + ~title:"Mempool filters attestations with non-minimal slots" + ~tags:[team; "mempool"; "attestations"; "minimal"; "slots"] + ~supports:(Protocol.From_protocol 023) + @@ fun protocol -> + log_step 1 "Initialize node and activate protocol" ; + let* node, client = + Client.init_with_node + ~nodes_args:[Synchronisation_threshold 0; Private_mode; Connections 0] + `Client + () + in + let* parameter_file = + Protocol.write_parameter_file + ~base:(Either.Right (protocol, None)) + [(["aggregate_attestation"], `Bool true)] + in + let* () = + Client.activate_protocol_and_wait ~protocol ~parameter_file client + in + log_step 2 "Bake 3 blocks to have multiple valid levels to attest for" ; + let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in + let* level = + bake_for ~empty:true ~protocol ~wait_for_flush:true node client + in + let* block_payload_hash = + Operation.Consensus.get_block_payload_hash client + in + log_step 3 "Inject attestations for all accepted levels " ; + (* Mempool is expected to accept attestations for the following three levels *) + let accepted_levels = [level - 1; level; level + 1] in + let* validated, refused = + Lwt_list.fold_left_s + (fun (validated, refused) level -> + let* attesting_rights = Operation.Consensus.get_slots ~level client in + (* Look for a delegate that has more than one slot *) + let delegate, slots = + let delegate_opt = + Array.find_map + (fun account -> + match + List.assoc_opt + account.Account.public_key_hash + attesting_rights + with + | Some (_ :: _ :: _ as slots) -> Some (account, slots) + | _ -> None) + Account.Bootstrap.keys + in + match delegate_opt with + | Some (delegate, slots) -> (delegate, slots) + | None -> + Test.fail + "found no delegate with more than one slot at level %d" + level + in + (* Inject an attestation with a non-minimal slot *) + let* (`OpHash op_refused) = + Operation.Consensus.( + inject + (attestation + ~slot:(List.nth slots 1) + ~level + ~round:0 + ~block_payload_hash + ()) + ~force:true + ~protocol + ~signer:delegate + client) + in + (* Inject an attestation with a minimal slot *) + let* (`OpHash op_valid) = + Operation.Consensus.( + inject + (attestation + ~slot:(List.hd slots) + ~level + ~round:0 + ~block_payload_hash + ()) + ~force:true + ~protocol + ~signer:delegate + client) + in + return (op_valid :: validated, op_refused :: refused)) + ([], []) + accepted_levels + in + log_step 4 "Check that operations where correctly filtered by the mempool" ; + (* Check that all attestations with non-minimal slots where refused, and all + attestations with a minimal slot where validated *) + let* () = check_mempool ~validated ~refused client in + unit end let check_operation_is_in_validated_mempool ops oph = @@ -4271,4 +4369,5 @@ let register ~protocols = Revamped.refused_operations_are_not_reclassified protocols ; Revamped.request_operations_from_peer protocols ; force_operation_injection protocols ; - Revamped.injecting_old_operation_fails protocols + Revamped.injecting_old_operation_fails protocols ; + Revamped.consensus_minimal_slots_feature_flag protocols