From 564d8f6f2516960f7fdc5632913bed94c6cc9164 Mon Sep 17 00:00:00 2001 From: Diane Gallois-Wong Date: Mon, 23 Jun 2025 17:41:54 +0200 Subject: [PATCH 1/3] Proto/test: test double attestation with duplicate slot in committee --- .../lib_protocol/test/helpers/op.ml | 11 +- .../lib_protocol/test/helpers/op.mli | 5 + .../consensus/test_double_attestation.ml | 100 ++++++++++++++++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.ml b/src/proto_alpha/lib_protocol/test/helpers/op.ml index b8ad0f87f50d..7cda4d12ebf6 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/op.ml @@ -148,7 +148,7 @@ let raw_attestation ?delegate ?slot ?level ?round ?block_payload_hash branch contents -let aggregate attestations = +let raw_aggregate attestations = let aggregate_content = List.fold_left (fun acc ({shell; protocol_data = {contents; signature}} : _ Operation.t) -> @@ -182,10 +182,11 @@ let aggregate attestations = (* Reverse committee to preserve [attestations] order *) {consensus_content; committee = List.rev committee}) in - let protocol_data = - Operation_data {contents; signature = Some (Bls signature)} - in - {shell; protocol_data} + let protocol_data = {contents; signature = Some (Bls signature)} in + ({shell; protocol_data} : Kind.attestations_aggregate operation) + +let aggregate attestations = + Option.map Operation.pack (raw_aggregate attestations) let aggregate_preattestations preattestations = let aggregate_content = diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.mli b/src/proto_alpha/lib_protocol/test/helpers/op.mli index 13685a020607..ccf6ab5e3500 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/op.mli @@ -122,6 +122,11 @@ val attestations_aggregate : Attestations signed by non-bls delegates are ignored. Evaluates to {!None} if no bls-signed attestations are found or if signature_aggregation failed (due to unreadable signature representation). *) +val raw_aggregate : + Kind.attestation_consensus_kind Kind.consensus operation trace -> + Kind.attestations_aggregate operation option + +(** Same as {!raw_aggregate} but returns the packed operation. *) val aggregate : Kind.attestation_consensus_kind Kind.consensus operation trace -> Operation.packed option diff --git a/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_attestation.ml b/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_attestation.ml index 6b56a645a9b2..83c5e4da1437 100644 --- a/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_attestation.ml +++ b/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_attestation.ml @@ -596,6 +596,102 @@ let test_invalid_double_attestation_variant () = true | _ -> false) +let invalid_denunciation kind = function + | Validate_errors.Anonymous.Invalid_denunciation kind' -> + Misbehaviour.equal_kind kind kind' + | _ -> false + +(** Check that a double attestation operation fails if the same slot + is duplicated in the committee of one of the evidences. *) +let test_invalid_double_attestation_duplicate_in_committee () = + let open Lwt_result_wrap_syntax in + let* _genesis, block = + Test_aggregate.init_genesis_with_some_bls_accounts + ~aggregate_attestation:true + () + in + let* b = Block.bake_until_cycle_end block in + let* blk_1, blk_2 = block_fork b in + let* blk_a = Block.bake blk_1 in + let* blk_b = Block.bake blk_2 in + let* attesters = Context.get_attesters (B blk_a) in + let attester, slot = + WithExceptions.Option.get + ~loc:__LOC__ + (Test_aggregate.find_attester_with_bls_key attesters) + in + let* op1 = + Op.raw_attestation ~delegate:attester.RPC.Validators.delegate ~slot blk_a + in + let* op2_standalone = + Op.raw_attestation ~delegate:attester.RPC.Validators.delegate ~slot blk_b + in + let op2 = + WithExceptions.Option.get + ~loc:__LOC__ + (Op.raw_aggregate [op2_standalone; op2_standalone]) + in + let op = + let contents = + if Operation_hash.(Operation.hash op1 < Operation.hash op2) then + Single (Double_consensus_operation_evidence {slot; op1; op2}) + else + Single + (Double_consensus_operation_evidence {slot; op1 = op2; op2 = op1}) + in + let branch = Context.branch (B blk_a) in + { + shell = {branch}; + protocol_data = Operation_data {contents; signature = None}; + } + in + let* () = + Op.check_validation_and_application_all_modes + ~loc:__LOC__ + ~error:(invalid_denunciation Double_attesting) + ~predecessor:blk_a + op + in + (* Also check with duplicate slots with different dal contents *) + let* op2_standalone' = + let number_of_slots = + Default_parameters.constants_test.dal.number_of_slots + in + let*?@ slot_index = Dal.Slot_index.of_int ~number_of_slots 3 in + let dal_content = + {attestation = Dal.Attestation.(commit empty slot_index)} + in + Op.raw_attestation + ~delegate:attester.RPC.Validators.delegate + ~slot + ~dal_content + blk_b + in + let op2 = + WithExceptions.Option.get + ~loc:__LOC__ + (Op.raw_aggregate [op2_standalone; op2_standalone']) + in + let op = + let contents = + if Operation_hash.(Operation.hash op1 < Operation.hash op2) then + Single (Double_consensus_operation_evidence {slot; op1; op2}) + else + Single + (Double_consensus_operation_evidence {slot; op1 = op2; op2 = op1}) + in + let branch = Context.branch (B blk_a) in + { + shell = {branch}; + protocol_data = Operation_data {contents; signature = None}; + } + in + Op.check_validation_and_application_all_modes + ~loc:__LOC__ + ~error:(invalid_denunciation Double_attesting) + ~predecessor:blk_a + op + (** Check that a future-cycle double attestation fails. *) let test_too_early_double_attestation_evidence () = let open Lwt_result_syntax in @@ -998,6 +1094,10 @@ let tests = "another invalid double attestation evidence" `Quick test_invalid_double_attestation_variant; + Tztest.tztest + "invalid double attestation evidence: duplicate slot in committee" + `Quick + test_invalid_double_attestation_duplicate_in_committee; Tztest.tztest "too early double attestation evidence" `Quick -- GitLab From 48004121b44ff262ceb36426d136227df98314a7 Mon Sep 17 00:00:00 2001 From: Diane Gallois-Wong Date: Mon, 23 Jun 2025 17:57:28 +0200 Subject: [PATCH 2/3] Proto/test: test double preattestation with duplicate slot in committee --- .../lib_protocol/test/helpers/op.ml | 29 ++++++-- .../lib_protocol/test/helpers/op.mli | 17 +++++ .../consensus/test_double_preattestation.ml | 66 +++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.ml b/src/proto_alpha/lib_protocol/test/helpers/op.ml index 7cda4d12ebf6..6bbd1b795081 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/op.ml @@ -188,7 +188,7 @@ let raw_aggregate attestations = let aggregate attestations = Option.map Operation.pack (raw_aggregate attestations) -let aggregate_preattestations preattestations = +let raw_aggregate_preattestations preattestations = let aggregate_content = List.fold_left (fun acc ({shell; protocol_data = {contents; signature}} : _ Operation.t) -> @@ -217,10 +217,11 @@ let aggregate_preattestations preattestations = (* Reverse committee to preserve [preattestations] order *) {consensus_content; committee = List.rev committee}) in - let protocol_data = - Operation_data {contents; signature = Some (Bls signature)} - in - {shell; protocol_data} + let protocol_data = {contents; signature = Some (Bls signature)} in + ({shell; protocol_data} : Kind.preattestations_aggregate operation) + +let aggregate_preattestations preattestations = + Option.map Operation.pack (raw_aggregate_preattestations preattestations) let attestation ?delegate ?slot ?level ?round ?block_payload_hash ?dal_content ?branch attested_block = @@ -306,7 +307,7 @@ let preattestation ?delegate ?slot ?level ?round ?block_payload_hash ?branch in return (Operation.pack op) -let preattestations_aggregate ?committee ?level ?round ?block_payload_hash +let raw_preattestations_aggregate ?committee ?level ?round ?block_payload_hash ?branch attested_block = let open Lwt_result_syntax in let* committee = @@ -334,10 +335,24 @@ let preattestations_aggregate ?committee ?level ?round ?block_payload_hash attested_block) committee in - match aggregate_preattestations preattestations with + match raw_aggregate_preattestations preattestations with | Some preattestations_aggregate -> return preattestations_aggregate | None -> failwith "no Bls delegate found" +let preattestations_aggregate ?committee ?level ?round ?block_payload_hash + ?branch attested_block = + let open Lwt_result_syntax in + let* op = + raw_preattestations_aggregate + ?committee + ?level + ?round + ?block_payload_hash + ?branch + attested_block + in + return (Operation.pack op) + let sign ?watermark ctxt sk branch (Contents_list contents) = let open Lwt_result_syntax in let* op = sign ctxt ?watermark sk branch contents in diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.mli b/src/proto_alpha/lib_protocol/test/helpers/op.mli index ccf6ab5e3500..3be9e34a43e3 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/op.mli @@ -146,6 +146,17 @@ val preattestation : (** Create a packed preattestations_aggregate that is expected for a given [Block.t]. Block context is expected to include at least one delegate with a BLS key (or a registered consensus keys). *) +val raw_preattestations_aggregate : + ?committee:public_key_hash list -> + ?level:Raw_level.t -> + ?round:Round.t -> + ?block_payload_hash:Block_payload_hash.t -> + ?branch:Block_hash.t -> + Block.t -> + Kind.preattestations_aggregate operation tzresult Lwt.t + +(** Same as {!raw_preattestations_aggregate} but returns the packed + operation. *) val preattestations_aggregate : ?committee:public_key_hash list -> ?level:Raw_level.t -> @@ -158,6 +169,12 @@ val preattestations_aggregate : (** Aggregate a list of preattestations in a single Preattestations_aggregate. Preattestations signed by non-bls delegates are ignored. Evaluates to {!None} if no bls-signed attestations are found or if signature_aggregation failed. *) +val raw_aggregate_preattestations : + Kind.preattestation_consensus_kind Kind.consensus operation trace -> + Kind.preattestations_aggregate operation option + +(** Same as {!raw_aggregate_preattestations} but returns the packed + operation. *) val aggregate_preattestations : Kind.preattestation_consensus_kind Kind.consensus operation trace -> Operation.packed option diff --git a/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_preattestation.ml b/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_preattestation.ml index 718859008607..ceb59129d324 100644 --- a/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_preattestation.ml +++ b/src/proto_alpha/lib_protocol/test/integration/consensus/test_double_preattestation.ml @@ -432,6 +432,67 @@ end = struct in return_unit + let invalid_denunciation kind = function + | Validate_errors.Anonymous.Invalid_denunciation kind' -> + Misbehaviour.equal_kind kind kind' + | _ -> false + + (** Check that a double preattestation operation fails if the same slot + is duplicated in the committee of one of the evidences. *) + let test_invalid_double_preattestation_duplicate_in_committee () = + let open Lwt_result_syntax in + let* _genesis, block = + Test_aggregate.init_genesis_with_some_bls_accounts + ~aggregate_attestation:true + () + in + let* b = Block.bake_until_cycle_end block in + let* blk_1, blk_2 = block_fork b in + let* blk_a = Block.bake blk_1 in + let* blk_b = Block.bake blk_2 in + let* attesters = Context.get_attesters (B blk_a) in + let attester, slot = + WithExceptions.Option.get + ~loc:__LOC__ + (Test_aggregate.find_attester_with_bls_key attesters) + in + let* op1 = + Op.raw_preattestation + ~delegate:attester.RPC.Validators.delegate + ~slot + blk_a + in + let* op2_standalone = + Op.raw_preattestation + ~delegate:attester.RPC.Validators.delegate + ~slot + blk_b + in + let op2 = + WithExceptions.Option.get + ~loc:__LOC__ + (Op.raw_aggregate_preattestations [op2_standalone; op2_standalone]) + in + let op = + let contents = + if Operation_hash.(Operation.hash op1 < Operation.hash op2) then + Single (Double_consensus_operation_evidence {slot; op1; op2}) + else + Single + (Double_consensus_operation_evidence {slot; op1 = op2; op2 = op1}) + in + let branch = Context.branch (B blk_a) in + { + shell = {branch}; + protocol_data = Operation_data {contents; signature = None}; + } + in + Op.check_validation_and_application_all_modes + ~loc:__LOC__ + ~error:(invalid_denunciation Double_preattesting) + ~predecessor:blk_a + op + let my_tztest title test = Tztest.tztest (Format.sprintf "%s: %s" name title) test @@ -479,6 +540,11 @@ end = struct "different slots under feature flag" `Quick different_slots_under_feature_flag; + my_tztest + "ko: invalid double preattestation evidence: duplicate slot in \ + committee" + `Quick + test_invalid_double_preattestation_duplicate_in_committee; ] end -- GitLab From c2af12e2f61bfa590450f54c62e39c347dc26de4 Mon Sep 17 00:00:00 2001 From: Diane Gallois-Wong Date: Mon, 23 Jun 2025 17:01:10 +0200 Subject: [PATCH 3/3] Proto/validate/double consensus op: check slot unicity in each evidence op --- src/proto_alpha/lib_protocol/validate.ml | 55 +++++++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/src/proto_alpha/lib_protocol/validate.ml b/src/proto_alpha/lib_protocol/validate.ml index 2fe2ac45ca87..76f9ee5770c7 100644 --- a/src/proto_alpha/lib_protocol/validate.ml +++ b/src/proto_alpha/lib_protocol/validate.ml @@ -2042,6 +2042,36 @@ module Anonymous = struct | Attestations_aggregate {consensus_content; _} ) -> consensus_content + let check_no_duplicates_in_committee_preattestation committee + error_if_duplicate = + let open Result_syntax in + let* (_ : Slot.Set.t) = + List.fold_left_e + (fun seen_slots slot -> + let* () = + error_when (Slot.Set.mem slot seen_slots) error_if_duplicate + in + return (Slot.Set.add slot seen_slots)) + Slot.Set.empty + committee + in + return_unit + + let check_no_duplicates_in_committee_attestation committee error_if_duplicate + = + let open Result_syntax in + let* (_ : Slot.Set.t) = + List.fold_left_e + (fun seen_slots (slot, (_ : dal_content option)) -> + let* () = + error_when (Slot.Set.mem slot seen_slots) error_if_duplicate + in + return (Slot.Set.add slot seen_slots)) + Slot.Set.empty + committee + in + return_unit + let check_double_consensus_operation_evidence vi (operation : Kind.double_consensus_operation_evidence operation) = let open Lwt_result_syntax in @@ -2070,7 +2100,8 @@ module Anonymous = struct they must be either a standalone (pre)attestation for [slot], or an aggregate whose committee includes [slot]. *) let open Result_syntax in - let check_slot (type a) (op : a Kind.consensus Operation.t) = + let check_slot_and_committee (type a) (op : a Kind.consensus Operation.t) + = match op.protocol_data.contents with | Single (Preattestation consensus_content) | Single (Attestation {consensus_content; _}) -> @@ -2078,16 +2109,26 @@ module Anonymous = struct Slot.(consensus_content.slot = slot) (Invalid_denunciation kind) | Single (Preattestations_aggregate {committee; _}) -> - error_unless - (List.mem ~equal:Slot.equal slot committee) + let* () = + error_unless + (List.mem ~equal:Slot.equal slot committee) + (Invalid_denunciation kind) + in + check_no_duplicates_in_committee_preattestation + committee (Invalid_denunciation kind) | Single (Attestations_aggregate {committee; _}) -> - error_unless - (List.mem_assoc ~equal:Slot.equal slot committee) + let* () = + error_unless + (List.mem_assoc ~equal:Slot.equal slot committee) + (Invalid_denunciation kind) + in + check_no_duplicates_in_committee_attestation + committee (Invalid_denunciation kind) in - let* () = check_slot op1 in - check_slot op2 + let* () = check_slot_and_committee op1 in + check_slot_and_committee op2 in let*? () = (* For the double operations to be punishable, they must have at -- GitLab