diff --git a/src/proto_alpha/lib_protocol/test/helpers/block.ml b/src/proto_alpha/lib_protocol/test/helpers/block.ml index 916c28091a85d1f01543696847632e3444c7b62e..7848833a01727fef97da74e3169862acb1f8a42e 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/block.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/block.ml @@ -1312,6 +1312,11 @@ let current_cycle b = let current_level = b.header.shell.level in current_cycle_of_level ~blocks_per_cycle ~current_level +let cycle_position b = + let blocks_per_cycle = b.constants.blocks_per_cycle in + let level = b.header.shell.level in + Int32.rem level blocks_per_cycle + let first_level_of_cycle (constants : Constants.Parametric.t) ~level = let blocks_per_cycle = constants.blocks_per_cycle in Int32.(equal (rem level blocks_per_cycle) zero) diff --git a/src/proto_alpha/lib_protocol/test/helpers/block.mli b/src/proto_alpha/lib_protocol/test/helpers/block.mli index ceac153a49021cae77ede214e0d8db8abbd20a3c..8b385294ab6b0443e889763700cd5f8a830bd72a 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/block.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/block.mli @@ -366,6 +366,8 @@ val current_level : block -> int32 val current_cycle : block -> Cycle.t +val cycle_position : block -> int32 + val first_level_of_cycle : Constants.Parametric.t -> level:int32 -> bool val first_block_of_cycle : block -> bool diff --git a/src/proto_alpha/lib_protocol/test/integration/consensus/test_dal_entrapment.ml b/src/proto_alpha/lib_protocol/test/integration/consensus/test_dal_entrapment.ml index 7c00e6c2acffbb41d6e78530562fb33d5b7f8aa0..d778a74ca385b3f03d3bdbefafd2ab2d6dead71d 100644 --- a/src/proto_alpha/lib_protocol/test/integration/consensus/test_dal_entrapment.ml +++ b/src/proto_alpha/lib_protocol/test/integration/consensus/test_dal_entrapment.ml @@ -20,12 +20,77 @@ open Alpha_context let commitment_and_proofs_cache = ref None -(** Check various accusation operation injection scenarios. *) -let test_accusation_injection ?(initial_blocks_to_bake = 2) ?expect_failure - ?(publish_slot = true) ?(with_dal_content = true) ?(attest_slot = true) () = +type injection_time = Now | Last_valid | First_invalid + +module IntMap = Map.Make (Int) +module PkhMap = Map.Make (Signature.Public_key_hash) + +let get_traps_and_non_traps ~traps_fraction shard_assignment shards_with_proofs + = + List.of_seq shards_with_proofs + |> List.fold_left + (fun map (shard, proof) -> + let pkh_opt = + IntMap.find shard.Tezos_crypto_dal.Cryptobox.index shard_assignment + in + match pkh_opt with + | None -> + Test.fail + ~__LOC__ + "Did not find a delegate for shard index %d" + shard.index + | Some pkh -> ( + let res = + Environment.Dal.share_is_trap pkh shard.share ~traps_fraction + in + match res with + | Ok b -> + let shard_with_proof = Dal.Shard_with_proof.{shard; proof} in + let traps, not_traps = + match PkhMap.find_opt pkh map with + | None -> ([], []) + | Some v -> v + in + let traps, not_traps = + if b then (shard_with_proof :: traps, not_traps) + else (traps, shard_with_proof :: not_traps) + in + PkhMap.add pkh (traps, not_traps) map + | Error `Decoding_error -> + Test.fail ~__LOC__ "Decoding error in [share_is_trap]")) + PkhMap.empty + +let indexes_to_delegates = + List.fold_left + (fun map Plugin.RPC.Dal.S.{delegate; indexes} -> + List.fold_left + (fun map index -> IntMap.add index delegate map) + map + indexes) + IntMap.empty + +(** This function checks various accusation operation injection scenarios. + + The simplest scenario, when all optional arguments are not given, is as + follows: + 1. Bake two blocks (because of #7686, see below). + 2. Bake a block that publishes a slot. + 3. Bake [lag - 1] blocks. + 4. Build an attestation. + 5. Bake one block before accusing. + 6. Retrieve traps and build an accusation. + 7. Bake a block with the accusation. + + This scenario varies slightly depending on the optional arguments. For their + use, look first at the relevant tests. +*) +let test_accusation_injection ?initial_blocks_to_bake ?expect_failure + ?(publish_slot = true) ?(with_dal_content = true) ?(attest_slot = true) + ?(inclusion_time = Now) ?(not_trap = false) ?(wrong_owner = false) + ?(traps_fraction = Q.(1 // 2)) () = let open Lwt_result_syntax in let c = Default_parameters.constants_test in - let dal = {c.dal with incentives_enable = true; traps_fraction = Q.one} in + let dal = {c.dal with incentives_enable = true; traps_fraction} in let cryptobox_parameters = dal.cryptobox_parameters in let number_of_slots = dal.number_of_slots in let lag = dal.attestation_lag in @@ -60,20 +125,61 @@ let test_accusation_injection ?(initial_blocks_to_bake = 2) ?expect_failure commitment_and_proofs_cache := Some result ; result in - let* genesis, contract = Context.init_with_constants1 constants in - (* TODO: https://gitlab.com/tezos/tezos/-/issues/7686 - We bake two blocks because we need the accusation to be introduced at level - at least 10 (2 = 10 - attestation_lag). In protocol S we will not need this - restriction. *) - let* blk = Block.bake_n initial_blocks_to_bake genesis in + let* genesis, (contract, _contract2) = + Context.init_with_constants2 constants + in + let* blk = + let blocks_to_bake = + match inclusion_time with + | Now -> ( + match initial_blocks_to_bake with + | None -> + (* TODO: https://gitlab.com/tezos/tezos/-/issues/7686 + We bake two blocks because we need the accusation to be introduced + at level at least 10 (2 = 10 - attestation_lag). In protocol S we + will not need this restriction. *) + 2 + | Some v -> v) + | _ -> + (* In this case we want the attestation to be as far as possible from + the accusation, so we want the attestation level to be the first + level of a cycle. Checking this "extreme" case ensure that slot + headers are not deleted too soon. *) + assert (Option.is_none initial_blocks_to_bake) ; + let blocks_per_cycle = genesis.constants.blocks_per_cycle in + Int32.( + rem (sub blocks_per_cycle (of_int lag)) blocks_per_cycle |> to_int) + in + Log.info "1. Bake %d blocks" blocks_to_bake ; + Block.bake_n blocks_to_bake genesis + in + let slot_header = Dal.Operations.Publish_commitment.{slot_index; commitment; commitment_proof} in let* op = Op.dal_publish_commitment (B genesis) contract slot_header in let* blk = - if publish_slot then Block.bake blk ~operation:op else Block.bake blk + if publish_slot then ( + Log.info "2. Bake a block with a publish operation" ; + Block.bake blk ~operation:op) + else ( + Log.info "2. Bake a block without a publish operation" ; + Block.bake blk) in + + Log.info "3. Bake 'attestation_lag - 1' blocks" ; let* blk = Block.bake_n (lag - 1) blk in + + Log.info "4. Build an attestation" ; + (match inclusion_time with + | Now -> () + | _ -> + let position = Block.cycle_position blk |> Int32.to_int in + Check.( + (position = 0) + int + ~__LOC__ + ~error_msg:"Expected cycle position to be 0, got %L")) ; let dal_content = if with_dal_content then let attestation = @@ -83,33 +189,111 @@ let test_accusation_injection ?(initial_blocks_to_bake = 2) ?expect_failure Some {attestation} else None in - let* attestation = Op.raw_attestation blk ?dal_content in - let (shard, proof), _ = Seq.uncons shards_with_proofs |> Stdlib.Option.get in - let shard_with_proof = Dal.Shard_with_proof.{shard; proof} in - let operation = + let* shard_assignment = Context.Dal.shards (B blk) () in + let indexes_to_delegates = indexes_to_delegates shard_assignment in + let delegate_to_shards_map = + get_traps_and_non_traps + ~traps_fraction:blk.constants.dal.traps_fraction + indexes_to_delegates + shards_with_proofs + in + let delegate = + match PkhMap.min_binding_opt delegate_to_shards_map with + | None -> Test.fail ~__LOC__ "Unexpected case: there are no delegates" + | Some (pkh, _) -> pkh + in + let* attestation = Op.raw_attestation blk ~delegate ?dal_content in + let attestation_level = blk.header.shell.level in + + let* blk = + let blocks_to_bake = + let position = Block.cycle_position blk |> Int32.to_int in + let blocks_per_cycle = blk.constants.blocks_per_cycle |> Int32.to_int in + match inclusion_time with + | Now -> + (* bake one block such that accusation level is different from the + attestation level; though this does not really matter *) + 1 + | Last_valid -> (2 * blocks_per_cycle) - 2 - position + | First_invalid -> (2 * blocks_per_cycle) - 1 - position + in + Log.info "5. Bake %d blocks before including an accusation" blocks_to_bake ; + Block.bake_n blocks_to_bake blk + in + + Log.info "6. Retrieve traps and build accusation" ; + let shard_with_proof = + let owner = + if wrong_owner then ( + match PkhMap.max_binding delegate_to_shards_map with + | None -> Test.fail ~__LOC__ "Unexpected case: there are no delegates" + | Some (pkh, _) -> + if Signature.Public_key_hash.equal delegate pkh then + Test.fail + ~__LOC__ + "Unexpected case: there should be at least two delegates" ; + pkh) + else delegate + in + match PkhMap.find owner delegate_to_shards_map with + | None -> + Test.fail + ~__LOC__ + "Unexpected case: delegate %a not found in map" + Signature.Public_key_hash.pp + owner + | Some (traps, not_traps) -> + if not_trap then Stdlib.List.hd not_traps else Stdlib.List.hd traps + in + let accusation = Op.dal_entrapment (B blk) attestation slot_index shard_with_proof in - match expect_failure with - | None -> - let* _blk_final = Block.bake ~operation blk in - return_unit - | Some f -> - let expect_failure = f blk in + + Log.info "7. Bake a block with the accusation" ; + let* blk = + match expect_failure with + | None -> Block.bake ~operation:accusation blk + | Some f -> + let expect_failure = f attestation_level in + let* ctxt = Incremental.begin_construction blk in + let* _ = Incremental.add_operation ctxt accusation ~expect_failure in + Incremental.finalize_block ctxt + in + (* Re-include the accusation in the following cycle and check that it is + rejected as a duplicate. *) + match (inclusion_time, expect_failure) with + | Now, None -> + let expect_failure = function + | [ + Environment.Ecoproto_error + (Validate_errors.Anonymous.Dal_already_denounced {level; _}); + ] + when Raw_level.to_int32 level = attestation_level -> + Lwt_result_syntax.return_unit + | errs -> + Test.fail + ~__LOC__ + "Error trace:@, %a does not match the expected one" + Error_monad.pp_print_trace + errs + in let* ctxt = Incremental.begin_construction blk in - let* _ = Incremental.add_operation ctxt operation ~expect_failure in + let* _ = Incremental.add_operation ctxt accusation ~expect_failure in return_unit + | _ -> return_unit let test_invalid_accusation_too_close_to_migration = - let expect_failure blk = function + let expect_failure attestation_level = function | [ Environment.Ecoproto_error (Validate_errors.Anonymous .Denunciations_not_allowed_just_after_migration {level; _}); ] - when Raw_level.to_int32 level = blk.Block.header.shell.level -> + when Raw_level.to_int32 level = attestation_level -> Lwt_result_syntax.return_unit | errs -> - failwith + Test.fail + ~__LOC__ "Error trace:@, %a does not match the expected one" Error_monad.pp_print_trace errs @@ -117,16 +301,17 @@ let test_invalid_accusation_too_close_to_migration = test_accusation_injection ~initial_blocks_to_bake:1 ~expect_failure let test_invalid_accusation_no_dal_content = - let expect_failure blk = function + let expect_failure attestation_level = function | [ Environment.Ecoproto_error (Validate_errors.Anonymous.Invalid_accusation_no_dal_content {level; _}); ] - when Raw_level.to_int32 level = blk.Block.header.shell.level -> + when Raw_level.to_int32 level = attestation_level -> Lwt_result_syntax.return_unit | errs -> - failwith + Test.fail + ~__LOC__ "Error trace:@, %a does not match the expected one" Error_monad.pp_print_trace errs @@ -134,16 +319,17 @@ let test_invalid_accusation_no_dal_content = test_accusation_injection ~with_dal_content:false ~expect_failure let test_invalid_accusation_slot_not_attested = - let expect_failure blk = function + let expect_failure attestation_level = function | [ Environment.Ecoproto_error (Validate_errors.Anonymous.Invalid_accusation_slot_not_attested {level; _}); ] - when Raw_level.to_int32 level = blk.Block.header.shell.level -> + when Raw_level.to_int32 level = attestation_level -> Lwt_result_syntax.return_unit | errs -> - failwith + Test.fail + ~__LOC__ "Error trace:@, %a does not match the expected one" Error_monad.pp_print_trace errs @@ -151,22 +337,82 @@ let test_invalid_accusation_slot_not_attested = test_accusation_injection ~attest_slot:false ~expect_failure let test_invalid_accusation_slot_not_published = - let expect_failure blk = function + let expect_failure attestation_level = function | [ Environment.Ecoproto_error (Validate_errors.Anonymous.Invalid_accusation_slot_not_published {level; _}); ] - when Raw_level.to_int32 level = blk.Block.header.shell.level -> + when Raw_level.to_int32 level = attestation_level -> Lwt_result_syntax.return_unit | errs -> - failwith + Test.fail + ~__LOC__ "Error trace:@, %a does not match the expected one" Error_monad.pp_print_trace errs in test_accusation_injection ~publish_slot:false ~expect_failure +let test_invalid_accusation_include_late = + let expect_failure attestation_level = function + | [ + Environment.Ecoproto_error + (Validate_errors.Anonymous.Outdated_dal_denunciation {level; _}); + ] + when Raw_level.to_int32 level = attestation_level -> + Lwt_result_syntax.return_unit + | errs -> + Test.fail + ~__LOC__ + "Error trace:@, %a does not match the expected one" + Error_monad.pp_print_trace + errs + in + test_accusation_injection ~inclusion_time:First_invalid ~expect_failure + +let test_invalid_accusation_shard_is_not_trap = + let expect_failure attestation_level = function + | [ + Environment.Ecoproto_error + (Validate_errors.Anonymous.Invalid_accusation_shard_is_not_trap + {level; _}); + ] + when Raw_level.to_int32 level = attestation_level -> + Lwt_result_syntax.return_unit + | errs -> + Test.fail + ~__LOC__ + "Error trace:@, %a does not match the expected one" + Error_monad.pp_print_trace + errs + in + test_accusation_injection ~not_trap:true ~expect_failure + +let test_invalid_accusation_wrong_shard_owner = + let expect_failure attestation_level = function + | [ + Environment.Ecoproto_error + (Validate_errors.Anonymous.Invalid_accusation_wrong_shard_owner + {level; _}); + ] + when Raw_level.to_int32 level = attestation_level -> + Lwt_result_syntax.return_unit + | errs -> + Test.fail + ~__LOC__ + "Error trace:@, %a does not match the expected one" + Error_monad.pp_print_trace + errs + in + (* By using [traps_fraction = 1] we are sure that the shard is a trap for the + attester. Otherwise the validation may fail with "shard is not trap" error + (which is emitted first). *) + test_accusation_injection + ~wrong_owner:true + ~traps_fraction:Q.one + ~expect_failure + let tests = [ Tztest.tztest "test valid accusation" `Quick test_accusation_injection; @@ -186,6 +432,22 @@ let tests = "test invalid accusation (slot_not_published)" `Quick test_invalid_accusation_slot_not_published; + Tztest.tztest + "test invalid accusation (include last)" + `Quick + (test_accusation_injection ~inclusion_time:Last_valid); + Tztest.tztest + "test invalid accusation (include late)" + `Quick + test_invalid_accusation_include_late; + Tztest.tztest + "test invalid accusation (shard is not trap)" + `Quick + test_invalid_accusation_shard_is_not_trap; + Tztest.tztest + "test invalid accusation (wrong shard owner)" + `Quick + test_invalid_accusation_wrong_shard_owner; ] let () =