diff --git a/tezt/tests/dal.ml b/tezt/tests/dal.ml index 5fd93cbb1985fd9327f1ba10d2f8087a54a2da05..3f0a368a7d2c3981bb38e08a44e69c395468feee 100644 --- a/tezt/tests/dal.ml +++ b/tezt/tests/dal.ml @@ -500,8 +500,8 @@ let with_layer1 ?custom_constants ?additional_bootstrap_accounts ?commitment_period ?challenge_window ?dal_enable ?incentives_enable ?dal_rewards_weight ?traps_fraction ?event_sections_levels ?node_arguments ?activation_timestamp ?dal_bootstrap_peers ?(parameters = []) - ?(prover = true) ?smart_rollup_timeout_period_in_blocks ?l1_history_mode f - ~protocol = + ?(prover = true) ?smart_rollup_timeout_period_in_blocks ?l1_history_mode + ?blocks_per_cycle f ~protocol = let parameter_overrides = make_int_parameter ["dal_parametric"; "attestation_lag"] attestation_lag @ make_int_parameter ["dal_parametric"; "number_of_shards"] number_of_shards @@ -546,6 +546,7 @@ let with_layer1 ?custom_constants ?additional_bootstrap_accounts @ (if Protocol.(number protocol <= number Quebec) then make_bool_parameter ["adaptive_issuance_force_activation"] (Some true) else []) + @ make_int_parameter ["blocks_per_cycle"] blocks_per_cycle @ parameters in @@ -637,13 +638,13 @@ let with_dal_node ?peers ?attester_profiles ?producer_profiles (* Wrapper scenario functions that should be re-used as much as possible when writing tests. *) -let scenario_with_layer1_node ?regression ?(tags = []) +let scenario_with_layer1_node ?attestation_threshold ?regression ?(tags = []) ?additional_bootstrap_accounts ?attestation_lag ?number_of_shards ?number_of_slots ?custom_constants ?commitment_period ?challenge_window ?(dal_enable = true) ?incentives_enable ?traps_fraction ?dal_rewards_weight ?event_sections_levels ?node_arguments ?activation_timestamp ?consensus_committee_size ?minimal_block_delay ?delay_increment_per_round - variant scenario = + ?blocks_per_cycle variant scenario = let description = "Testing DAL L1 integration" in let tags = if List.mem team tags then tags else team :: tags in let tags = @@ -657,6 +658,8 @@ let scenario_with_layer1_node ?regression ?(tags = []) (Printf.sprintf "%s (%s)" description variant) (fun protocol -> with_layer1 + ?blocks_per_cycle + ?attestation_threshold ~custom_constants ?additional_bootstrap_accounts ?consensus_committee_size @@ -852,17 +855,21 @@ let different_delegates pkh = (* We support two formats for specifying the attested slots: either a list of slot ids or a bitset. *) -type attestation_availability = Slots of int list | Bitset of bool array +type attestation_availability = + | Slots of int list + | Bitset of bool array + | No_dal_attestation let craft_dal_attestation ?level ?(round = 0) ?payload_level ~signer ~nb_slots availability client = let dal_attestation = match availability with - | Bitset bitset -> bitset + | Bitset bitset -> Some bitset | Slots availability -> let dal_attestation = Array.make nb_slots false in List.iter (fun i -> dal_attestation.(i) <- true) availability ; - dal_attestation + Some dal_attestation + | No_dal_attestation -> None in let* level = match level with Some level -> return level | None -> Client.level client @@ -890,7 +897,7 @@ let craft_dal_attestation ?level ?(round = 0) ?payload_level ~signer ~nb_slots (Operation.Consensus.attestation ~level ~round - ~dal_attestation + ?dal_attestation ~slot ~block_payload_hash ()) @@ -9362,6 +9369,482 @@ let test_e2e_trap_faulty_dal_node _protocol dal_parameters _cryptobox node got %L" ; unit +type dal_participation = { + expected_assigned_shards_per_slot : int; + delegate_attested_dal_slots : int; + delegate_attestable_dal_slots : int; + expected_dal_rewards : Tez.t; + sufficient_dal_participation : bool; + denounced : bool; +} + +let get_dal_participation l1_node public_key_hash = + let open JSON in + let* json = + Node.RPC.( + call l1_node + @@ get_chain_block_context_delegate_dal_participation public_key_hash) + in + let expected_assigned_shards_per_slot = + json |-> "expected_assigned_shards_per_slot" |> as_int + in + let delegate_attested_dal_slots = + json |-> "delegate_attested_dal_slots" |> as_int + in + let delegate_attestable_dal_slots = + json |-> "delegate_attestable_dal_slots" |> as_int + in + let expected_dal_rewards = + json |-> "expected_dal_rewards" |> as_int64 |> Tez.of_mutez_int64 + in + let sufficient_dal_participation = + json |-> "sufficient_dal_participation" |> as_bool + in + let denounced = json |-> "denounced" |> as_bool in + return + { + expected_assigned_shards_per_slot; + delegate_attested_dal_slots; + delegate_attestable_dal_slots; + expected_dal_rewards; + sufficient_dal_participation; + denounced; + } + +let get_tb_expected_attesting_rewards l1_node public_key_hash = + let open JSON in + let* json = + Node.RPC.( + call l1_node + @@ get_chain_block_context_delegate_participation public_key_hash) + in + return (json |-> "expected_attesting_rewards" |> as_int) + +(** [test_dal_rewards_distribution _protocol dal_parameters cryptobox node + client _dal_node] verifies the correct distribution of DAL rewards among + delegates based on their participation in DAL attestations activity. + + The main steps of the test are: + + 1. Initialize delegates: we assign five bootstrap accounts to specific roles: + - **Baker:** Always attests both TenderBake (TB) and all DAL slots. + - **Attesting DAL Slot 10:** Always attests TB and specifically DAL slot 10. + - **Not Attesting at All:** Does not attest either TB or DAL slots. + - **Not Attesting DAL:** Does not attest DAL slots, either by not sending + any DAL attestation or by sending an empty bitset. + - **Not Sufficiently Attesting DAL Slot 10:** Attests DAL slot 10 only 25% + of the time. + + 2. Initial balances snapshot: we capture the initial balances of all + delegates to compare against post-test balances. + + 3. Publish DAL slots and inject DAL attestations: we define a helper + function to inject DAL attestations based on each delegate's role: + - we publish a dummy DAL slot at index 10. + - we inject attestations from each delegate according to their assigned + behavior. + + 4. Blocks production until ~end of cycle: we bake blocks up to the last + block of the current cycle, ensuring DAL slot publications and attestations + are appropriately injected. Each time a new block is produced, we check the + value of the "dal_attestation" bitset in the block's metadata and count the + number of times the DAL slot at index 10 is attested. + + 5. Second balances snapshot: this is done before TB and DAL rewards are + distributed at the next block, which marks the end of the current cycle. + + 6. Bake the final block of the cycle to trigger the distribution of DAL and + TB rewards. + + 7. Final snapshot of delegates' balances to assess the impact of rewards + distribution. + + 8. Balance Checks: + - Ensure that delegates who did not attest at all (for both TB & DAL) have + unchanged balances. + - Verify that delegates with sufficient DAL participation received the + expected DAL and TB rewards. + - Confirm that delegates with insufficient or no DAL participation received + only the expected TB rewards. + + 9. Some extra checks related to /dal_participation RPC: + - DAL participation status of each delegate matches the expected outcome + based on their attestation behavior. + - No delegates are denounced (there is no accuser running actually). + - The attestable slots field of /dal_participation's result is equal to the + number of attested slots counted in blocks metadata. *) +let test_dal_rewards_distribution _protocol dal_parameters cryptobox node client + _dal_node = + let* proto_params = + Node.RPC.call node @@ RPC.get_chain_block_context_constants () + in + let blocks_per_cycle = JSON.(proto_params |-> "blocks_per_cycle" |> as_int) in + assert (blocks_per_cycle >= dal_parameters.Dal.Parameters.attestation_lag) ; + let* () = bake_for client in + let* level = Node.get_level node in + assert (level < blocks_per_cycle) ; + let nb_slots = dal_parameters.Dal.Parameters.number_of_slots in + let all_slots = List.init nb_slots Fun.id in + + (* We get our available delegates (up to 5) and assign them different roles + they'll play during the test. *) + let ( accounts_list, + ( baker, + attesting_dal_slot_10, + not_attesting_at_all, + not_attesting_dal, + not_sufficiently_attesting_dal_slot_10 ) ) = + match Account.Bootstrap.keys with + | [|d1; d2; d3; d4; d5|] -> + ( [ + (d1, "baker"); + (d2, "attesting_dal_slot_10"); + (d3, "not_attesting_at_all"); + (d4, "not_attesting_dal"); + (d5, "not_sufficiently_attesting_dal_slot_10"); + ], + (d1, d2, d3, d4, d5) ) + | _ -> Test.fail "Expected exactly 5 bootstrap accounts." + in + + (* A helper function to get balances of the delegates above. *) + let snapshot_full_balances_helper () = + let bal account = + let* b = + Client.get_full_balance_for client ~account:account.Account.alias + in + return (Tez.mutez_int64 b) + in + let* l = + Lwt.all + [ + bal baker; + bal attesting_dal_slot_10; + bal not_attesting_at_all; + bal not_attesting_dal; + bal not_sufficiently_attesting_dal_slot_10; + ] + in + match l with + | [b1; b2; b3; b4; b5] -> return (b1, b2, b3, b4, b5) + | _ -> Test.fail "Not reachable." + in + + (* Snapshot the balances of the accounts at startup. *) + let* ( _baker_bal0, + attesting_dal_slot_10_bal0, + not_attesting_at_all_bal0, + not_attesting_dal_bal0, + not_sufficiently_attesting_dal_slot_10_bal0 ) = + snapshot_full_balances_helper () + in + + (* This is the main helper function which injects (DAL) attestations for + delegates depending on their profiles. *) + let inject_attestations () = + let* level = Node.get_level node in + let* rights = + Node.RPC.call node + @@ RPC.get_chain_block_helper_baking_rights + ~level + ~delegate:baker.public_key_hash + () + in + let round = JSON.(List.hd (as_list rights) |-> "round" |> as_int) in + + (* 1. Baker always attests TB and all DAL slots *) + let* (_ : (Operation_core.t * [`OpHash of peer_id]) list) = + (* The baker delegate will miss 1/10 of its DAL attestations and will send + [No_dal_attestation] in this case. *) + let baker_attestation = + if level mod 10 = 0 then No_dal_attestation else Slots all_slots + in + inject_dal_attestations + ~round + ~signers:[baker] + ~nb_slots + baker_attestation + client + in + (* 2. attesting_dal_slot_10 always attests TB and DAL slot 10 *) + let* (_ : (Operation_core.t * [`OpHash of peer_id]) list) = + (* The attesting_dal_slot_10 delegate misses 1/11th of its DAL + attestations and will send Slots [], but it should be fine for its + rewards. *) + let attestation = if level mod 11 = 0 then Slots [] else Slots [10] in + inject_dal_attestations + ~round + ~signers:[attesting_dal_slot_10] + ~nb_slots + attestation + client + in + (* 3. not_attesting_at_all is not attesting neither TB nor DAL slots *) + (* 4. not_attesting_dal either sends no DAL content or sends bitset 0 *) + let* (_ : (Operation_core.t * [`OpHash of peer_id]) list) = + let dal_attestation = + if level mod 2 = 0 then No_dal_attestation else Slots [] + in + inject_dal_attestations + ~round + ~signers:[not_attesting_dal] + ~nb_slots + dal_attestation + client + in + (* 5. not_sufficiently_attesting_dal_slot_10: is attesting DAL slot 10, but only 25% + of the time. *) + let* (_ : (Operation_core.t * [`OpHash of peer_id]) list) = + let* level = Node.get_level node in + let slots_to_attest = + if level mod 4 = 0 then Slots [10] else No_dal_attestation + in + inject_dal_attestations + ~round + ~signers:[not_sufficiently_attesting_dal_slot_10] + ~nb_slots + slots_to_attest + client + in + unit + in + + (* We'll count the number of times we see that slot 10 is attested in blocks + metadata. We'll then check the final value against the + delegate_attestable_dal_slots field of /dal_participation for each + delegate. *) + let count_set_dal_attestation_bitset = ref 0 in + + let count_slot_10_if_attested () = + let* block_json = Node.RPC.(call node @@ get_chain_block_metadata_raw ()) in + let block_dal_attestation_bitset = + JSON.(block_json |-> "dal_attestation" |> as_int) + in + (* count when slot 10 is attested *) + if block_dal_attestation_bitset = 1024 then + incr count_set_dal_attestation_bitset ; + unit + in + + (* This is the main entry of the test: we start by baking a number of blocks + in which we inject DAL slots publications at slot index 10 and TB/DAL + attestations. We stop before baking the last block of the current cycle, + where DAL rewards are distributed. *) + let* () = + repeat + (blocks_per_cycle - level - 1) + (fun () -> + let* () = count_slot_10_if_attested () in + let* (`OpHash _oph1) = + publish_dummy_slot + ~source:baker + ~index:10 + ~message:"hi!" + cryptobox + client + in + let* () = inject_attestations () in + bake_for ~delegates:(`For [baker.Account.public_key_hash]) client) + in + + (* After this first round of blocks, we snapshot the balances of our delegates + again. *) + let* ( _baker_bal1, + attesting_dal_slot_10_bal1, + not_attesting_at_all_bal1, + not_attesting_dal_bal1, + not_sufficiently_attesting_dal_slot_10_bal1 ) = + snapshot_full_balances_helper () + in + + let* () = count_slot_10_if_attested () in + + let* bootstrap_accounts_participation = + Lwt_list.map_s + (fun (account, account_role) -> + Log.info "Get DAL participation info of %s" account_role ; + let* dal_participation = + get_dal_participation node account.Account.public_key_hash + in + return (account, dal_participation)) + accounts_list + in + + (* We now bake the last block of the cycle, which should trigger TB and DAL + rewards distribution. TB rewards are actually set to 0. *) + let* () = bake_for ~delegates:(`For [baker.Account.public_key_hash]) client in + + (* We snapshot the balances of the delegates at the end of the cycle. *) + let* ( _baker_bal2, + attesting_dal_slot_10_bal2, + not_attesting_at_all_bal2, + not_attesting_dal_bal2, + not_sufficiently_attesting_dal_slot_10_bal2 ) = + snapshot_full_balances_helper () + in + + (* We're now ready to do some checks, both on balances and on + dal_participation RPC's result. *) + (* First, we define a function to check expected balances of delegates. *) + let check_bal_incr ~__LOC__ account bal_before bal_after ~delta = + let error_msg = + if delta = 0 then + "account " ^ account.Account.public_key_hash + ^ ", expected balance to be unchanged. Got %R, expecting %L" + else + "account " ^ account.Account.public_key_hash + ^ ", expected balance b1 to increase. Got %R, expecting %L" + in + Check.(Int64.(add bal_before (of_int delta)) = bal_after) + ~__LOC__ + Check.int64 + ~error_msg + in + + (* Except for the baker, we don't expect the balances of the delegates to + change before baking the last block of the current cycle. *) + List.iter + (fun (account, bal0, bal1) -> + check_bal_incr ~__LOC__ account bal0 bal1 ~delta:0) + [ + ( attesting_dal_slot_10, + attesting_dal_slot_10_bal0, + attesting_dal_slot_10_bal1 ); + ( not_attesting_at_all, + not_attesting_at_all_bal0, + not_attesting_at_all_bal1 ); + (not_attesting_dal, not_attesting_dal_bal0, not_attesting_dal_bal1); + ( not_sufficiently_attesting_dal_slot_10, + not_sufficiently_attesting_dal_slot_10_bal0, + not_sufficiently_attesting_dal_slot_10_bal1 ); + ] ; + + (* As the delegates have the same stake distribution, they're expected to have + the same number of the assigned shards, to have shards at the same level, + to have the same rewards allocated, ... *) + let expected_full_dal_rewards = + match bootstrap_accounts_participation with + | [] -> Test.fail "Not reachable." + | (_account, dal_part) :: rest -> + List.iter + (fun (_account, dal_participation) -> + Check.( + dal_part.expected_assigned_shards_per_slot + = dal_participation.expected_assigned_shards_per_slot) + ~__LOC__ + Check.int + ~error_msg:"expected_assigned_shards_per_slot mismatch" ; + Check.( + dal_part.delegate_attestable_dal_slots + = dal_participation.delegate_attestable_dal_slots) + ~__LOC__ + Check.int + ~error_msg:"delegate_attestable_dal_slots mismatch" ; + Check.( + Tez.to_mutez dal_part.expected_dal_rewards + = Tez.to_mutez dal_participation.expected_dal_rewards) + ~__LOC__ + Check.int + ~error_msg:"expected_dal_rewards mismatch") + rest ; + Tez.to_mutez dal_part.expected_dal_rewards + in + + (* We use /participation RPC to get the expect Tenderbake rewards of delegate + who TB-attested sufficiently. *) + let* expected_full_tb_rewards = + get_tb_expected_attesting_rewards node baker.public_key_hash + in + + (* After baking the last block of the cycle, we check that: + + - the balances of the delegates who didn't attest at all didn't change. + + - the participation RPC's result is aligned with the first check. *) + List.iter + (fun (account, bal1, bal2) -> + check_bal_incr ~__LOC__ account bal1 bal2 ~delta:0 ; + let dal_participation = + List.assoc account bootstrap_accounts_participation + in + Check.is_false + dal_participation.sufficient_dal_participation + ~__LOC__ + ~error_msg: + ("account " ^ account.Account.public_key_hash + ^ ", expected to have insufficient DAL participation.")) + [ + ( not_attesting_at_all, + not_attesting_at_all_bal1, + not_attesting_at_all_bal2 ); + ] ; + + (* After baking the last block of the cycle, we check that: + + - the balances of the delegates who didn't attest DAL sufficiently or + didn't attest DAL at all only increased by a delta equal to the expected + Tenderbake attestation rewards. + + - the participation RPC's result is aligned with the first check. *) + List.iter + (fun (account, bal1, bal2) -> + check_bal_incr ~__LOC__ account bal1 bal2 ~delta:expected_full_tb_rewards ; + let dal_participation = + List.assoc account bootstrap_accounts_participation + in + Check.is_false + dal_participation.sufficient_dal_participation + ~__LOC__ + ~error_msg: + ("account " ^ account.Account.public_key_hash + ^ ", expected to have insufficnent DAL participation.")) + [ + (not_attesting_dal, not_attesting_dal_bal1, not_attesting_dal_bal2); + ( not_sufficiently_attesting_dal_slot_10, + not_sufficiently_attesting_dal_slot_10_bal1, + not_sufficiently_attesting_dal_slot_10_bal2 ); + ] ; + + (* Below, we check that the only delegate who attested slot 10 correctly got + its (Tenderbake and) DAL rewards. *) + check_bal_incr + ~__LOC__ + attesting_dal_slot_10 + attesting_dal_slot_10_bal1 + attesting_dal_slot_10_bal2 + ~delta:(expected_full_tb_rewards + expected_full_dal_rewards) ; + let dal_participation = + List.assoc attesting_dal_slot_10 bootstrap_accounts_participation + in + Check.is_true + dal_participation.sufficient_dal_participation + ~__LOC__ + ~error_msg: + ("account " ^ attesting_dal_slot_10.Account.public_key_hash + ^ ", expected balance to have sufficient DAL participation.") ; + + (* As a final check, we verify that no delegate is denounced and that we + report the correct number of attested slots. *) + List.iter + (fun (account, dal_participation) -> + Check.is_false + dal_participation.denounced + ~__LOC__ + ~error_msg: + ("account " ^ account.Account.public_key_hash + ^ ", not expected to be denounced.") ; + + Check.( + dal_participation.delegate_attestable_dal_slots + = !count_set_dal_attestation_bitset) + ~__LOC__ + Check.int + ~error_msg: + "Expecting %L attestable DAL slots, but %R were reported in blocks \ + metadata") + bootstrap_accounts_participation ; + unit + let register ~protocols = (* Tests with Layer1 node only *) scenario_with_layer1_node @@ -9369,6 +9852,16 @@ let register ~protocols = "dal basic logic" test_slot_management_logic protocols ; + scenario_with_layer1_node + ~tags:[Tag.flaky] + "attesters receive expected DAL rewards depending on participation" + test_dal_rewards_distribution + (List.filter (fun p -> Protocol.number p >= 022) protocols) + (* We set attestation threshold to 30% because we'll have 2 regular bakers + who attest sufficiently. *) + ~attestation_threshold:30 + ~attestation_lag:2 + ~blocks_per_cycle:16 ; scenario_with_layer1_node ~attestation_lag:5 "slots attestation operation behavior"