diff --git a/CHANGES.rst b/CHANGES.rst index 83f06a6a21bd9de0b7ef61e8707a6accd200ba76..62bf32537b3d6a2beca94447d14c8ff1d342be6f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -249,6 +249,9 @@ DAL node Protocol ~~~~~~~~ +- A new RPC ``/chains/main/blocks/head/context/delegates//dal_participation`` + similar to Tenderbake's ``/participation`` RPC to track bakers' DAL activity + (MR :gl:`!16168`) - A new antonymous operation "DAL entrapment evidence" was added. This operation is not valid when the feature flag for DAL incentives is turned off. (MR :gl:`!15677`) diff --git a/src/proto_alpha/lib_plugin/delegate_services.ml b/src/proto_alpha/lib_plugin/delegate_services.ml index 7df778d3efb49e5a33cdbee1e9287883ba3739aa..1394aa53b54f6e3460496657b42b87f50ebbe843 100644 --- a/src/proto_alpha/lib_plugin/delegate_services.ml +++ b/src/proto_alpha/lib_plugin/delegate_services.ml @@ -143,6 +143,41 @@ let participation_info_encoding = (req "remaining_allowed_missed_slots" int31) (req "expected_attesting_rewards" Tez.encoding)) +let dal_participation_info_encoding = + let open Data_encoding in + conv + (fun Delegate.For_RPC. + { + expected_assigned_shards; + delegate_attested_dal_slots; + total_dal_attested_slots; + expected_dal_rewards; + sufficient_dal_participation; + } -> + ( expected_assigned_shards, + delegate_attested_dal_slots, + total_dal_attested_slots, + expected_dal_rewards, + sufficient_dal_participation )) + (fun ( expected_assigned_shards, + delegate_attested_dal_slots, + total_dal_attested_slots, + expected_dal_rewards, + sufficient_dal_participation ) -> + { + expected_assigned_shards; + delegate_attested_dal_slots; + total_dal_attested_slots; + expected_dal_rewards; + sufficient_dal_participation; + }) + (obj5 + (req "expected_assigned_shards" int31) + (req "delegate_attested_dal_slots" int31) + (req "total_dal_attested_slots" int31) + (req "expected_dal_rewards" Tez.encoding) + (req "sufficient_dal_participation" bool)) + type deposit_per_cycle = {cycle : Cycle.t; deposit : Tez.t} let deposit_per_cycle_encoding : deposit_per_cycle Data_encoding.t = @@ -845,6 +880,31 @@ module S = struct ~output:participation_info_encoding RPC_path.(path / "participation") + let dal_participation = + RPC_service.get_service + ~description: + "Returns information about the delegate's participation in the \ + attestation of slots published into the Data Availability Layer (DAL) \ + during the current cycle. The field 'expected_assigned_shards' \ + indicates the expected number of shards assigned to the delegate in \ + the cycle. The field 'delegate_attested_dal_slots' represents the \ + number of attested DAL slots which are also attested by the delegate, \ + while 'total_dal_attested_slots' provides the total number of DAL \ + slots attested during the cycle, regardless of the delegate's \ + participation. The 'expected_dal_rewards' field specifies the \ + expected amount of rewards for the delegate based on DAL \ + participation, provided the delegate meets the required participation \ + Whether this threshold is currently met is determined by the \ + 'sufficient_dal_participation' flag, which is true if currently the \ + delegate has sufficiently participated in attesting DAL slots \ + declared to be attested by the protocol. Note that this flag may \ + evolve during the cycle. Also note, in particular, that if no DAL no \ + DAL slots have been globally attested during the cycle (i.e., when \ + 'total_dal_attested_slots' is zero), the flag is true." + ~query:RPC_query.empty + ~output:dal_participation_info_encoding + RPC_path.(path / "dal_participation") + let active_staking_parameters = RPC_service.get_service ~description: @@ -1334,6 +1394,9 @@ let register () = register1 ~chunked:false S.participation (fun ctxt pkh () () -> let* () = check_delegate_registered ctxt pkh in Delegate.For_RPC.participation_info ctxt pkh) ; + register1 ~chunked:false S.dal_participation (fun ctxt pkh () () -> + let* () = check_delegate_registered ctxt pkh in + Delegate.For_RPC.dal_participation_info ctxt pkh) ; register1 ~chunked:false S.active_staking_parameters (fun ctxt pkh () () -> Delegate.Staking_parameters.of_delegate ctxt pkh) ; register1 ~chunked:false S.pending_staking_parameters (fun ctxt pkh () () -> @@ -1425,6 +1488,9 @@ let consensus_key ctxt block pkh = let participation ctxt block pkh = RPC_context.make_call1 S.participation ctxt block pkh () () +let dal_participation ctxt block pkh = + RPC_context.make_call1 S.dal_participation ctxt block pkh () () + let active_staking_parameters ctxt block pkh = RPC_context.make_call1 S.active_staking_parameters ctxt block pkh () () diff --git a/src/proto_alpha/lib_plugin/delegate_services.mli b/src/proto_alpha/lib_plugin/delegate_services.mli index dde556a498debb1cf97719d2bbda135c814706d5..2967d93fbfc6ee55e56a603d295dc1254e34ea63 100644 --- a/src/proto_alpha/lib_plugin/delegate_services.mli +++ b/src/proto_alpha/lib_plugin/delegate_services.mli @@ -172,6 +172,12 @@ val participation : public_key_hash -> Delegate.For_RPC.participation_info shell_tzresult Lwt.t +val dal_participation : + 'a #RPC_context.simple -> + 'a -> + public_key_hash -> + Delegate.For_RPC.dal_participation_info shell_tzresult Lwt.t + val active_staking_parameters : 'a #RPC_context.simple -> 'a -> diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index f12c981df6bd8b9e8515069c5fd7fcce18e08e81..eb860a93b097d348b3ef49ac5ec2fb55e0bce4d8 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -2505,9 +2505,20 @@ module Delegate : sig expected_attesting_rewards : Tez.t; } + type dal_participation_info = { + expected_assigned_shards : int; + delegate_attested_dal_slots : int; + total_dal_attested_slots : int; + expected_dal_rewards : Tez.t; + sufficient_dal_participation : bool; + } + val participation_info : context -> public_key_hash -> participation_info tzresult Lwt.t + val dal_participation_info : + context -> public_key_hash -> dal_participation_info tzresult Lwt.t + (** Returns the full 'balance' of the implicit contract associated to a given key, i.e. the sum of the spendable balance (given by [balance] or [Contract_storage.get_balance]) and of the frozen balance of the diff --git a/src/proto_alpha/lib_protocol/delegate_cycles.ml b/src/proto_alpha/lib_protocol/delegate_cycles.ml index 22f997346a71b68b5d6ef8454a2a6240ef713e97..1598d0570ed330e6f04abb4ec5b719a2f04f0dd1 100644 --- a/src/proto_alpha/lib_protocol/delegate_cycles.ml +++ b/src/proto_alpha/lib_protocol/delegate_cycles.ml @@ -62,18 +62,11 @@ let distribute_dal_attesting_rewards ctxt delegate ctxt delegate in - let minimal_dal_participation_ratio = - (Raw_context.constants ctxt).dal.minimal_participation_ratio - in let sufficient_dal_participation = - let open Z in - geq - (of_int32 dal_attested_slots_by_delegate) - (div - (mul - (of_int32 total_dal_attested_slots) - minimal_dal_participation_ratio.num) - minimal_dal_participation_ratio.den) + Delegate_missed_attestations_storage.is_dal_participation_sufficient + ctxt + ~dal_attested_slots_by_delegate + ~total_dal_attested_slots in let expected_dal_shards = Delegate_missed_attestations_storage diff --git a/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.ml b/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.ml index 91f3604a7ea6abf3f3d411484492fa95d8803da3..a969999368f8c849478d63710e333ebb7a6746d5 100644 --- a/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.ml +++ b/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.ml @@ -140,6 +140,20 @@ let record_dal_participation ctxt ~delegate ~number_of_attested_slots = contract (Int32.of_int number_of_attested_slots) +let is_dal_participation_sufficient ctxt ~dal_attested_slots_by_delegate + ~total_dal_attested_slots = + let open Z in + let minimal_dal_participation_ratio = + (Raw_context.constants ctxt).dal.minimal_participation_ratio + in + geq + (of_int32 dal_attested_slots_by_delegate) + (div + (mul + (of_int32 total_dal_attested_slots) + minimal_dal_participation_ratio.num) + minimal_dal_participation_ratio.den) + let record_baking_activity_and_pay_rewards_and_fees ctxt ~payload_producer ~block_producer ~baking_reward ~reward_bonus = let open Lwt_result_syntax in @@ -188,16 +202,29 @@ let check_and_reset_delegate_participation ctxt delegate = let*! ctxt = Storage.Contract.Missed_attestations.remove ctxt contract in return (ctxt, Compare.Int.(missed_attestations.remaining_slots >= 0)) -let get_and_reset_delegate_dal_participation ctxt delegate = +let get_and_maybe_reset_delegate_dal_participation ~reset ctxt delegate = let open Lwt_result_syntax in let contract = Contract_repr.Implicit delegate in let* result = Storage.Contract.Attested_dal_slots.find ctxt contract in match result with | None -> return (ctxt, 0l) | Some num_slots -> - let*! ctxt = Storage.Contract.Attested_dal_slots.remove ctxt contract in + let*! ctxt = + if reset then Storage.Contract.Attested_dal_slots.remove ctxt contract + else Lwt.return ctxt + in return (ctxt, num_slots) +let get_and_reset_delegate_dal_participation = + get_and_maybe_reset_delegate_dal_participation ~reset:true + +let get_delegate_dal_participation ctxt delegate = + let open Lwt_result_syntax in + let* _ctxt, n = + get_and_maybe_reset_delegate_dal_participation ~reset:false ctxt delegate + in + return n + module For_RPC = struct type participation_info = { expected_cycle_activity : int; @@ -287,4 +314,87 @@ module For_RPC = struct remaining_allowed_missed_slots; expected_attesting_rewards; } + + type dal_participation_info = { + expected_assigned_shards : int; + delegate_attested_dal_slots : int; + total_dal_attested_slots : int; + expected_dal_rewards : Tez_repr.t; + sufficient_dal_participation : bool; + } + + (* Inefficient, only for RPC *) + let dal_participation_info_enabled ctxt delegate = + let open Lwt_result_syntax in + let level = Level_storage.current ctxt in + let* stake_distribution = + Stake_storage.get_selected_distribution ctxt level.cycle + in + match + List.assoc_opt + ~equal:Signature.Public_key_hash.equal + delegate + stake_distribution + with + | None -> + (* delegate does not have an active stake at the current cycle *) + return + { + expected_assigned_shards = 0; + delegate_attested_dal_slots = 0; + total_dal_attested_slots = 0; + expected_dal_rewards = Tez_repr.zero; + sufficient_dal_participation = false; + } + | Some active_stake -> + let* total_active_stake = + Stake_storage.get_total_active_stake ctxt level.cycle + in + let expected_assigned_shards = + let active_stake_weight = Stake_repr.staking_weight active_stake in + let total_active_stake_weight = + Stake_repr.staking_weight total_active_stake + in + expected_dal_shards_for_given_active_stake + ctxt + ~total_active_stake_weight + ~active_stake_weight + in + let* dal_attested_slots_by_delegate = + get_delegate_dal_participation ctxt delegate + in + let* total_dal_attested_slots = + let+ total_opt = Storage.Dal.Total_attested_slots.find ctxt in + Option.value ~default:0l total_opt + in + let sufficient_dal_participation = + is_dal_participation_sufficient + ctxt + ~dal_attested_slots_by_delegate + ~total_dal_attested_slots + in + let*? dal_attesting_reward_per_shard = + Delegate_rewards.dal_attesting_reward_per_shard ctxt + in + let expected_dal_rewards = + Tez_repr.mul_exn + dal_attesting_reward_per_shard + expected_assigned_shards + in + return + { + expected_assigned_shards; + delegate_attested_dal_slots = + Int32.to_int dal_attested_slots_by_delegate; + total_dal_attested_slots = Int32.to_int total_dal_attested_slots; + expected_dal_rewards; + sufficient_dal_participation; + } + + (* Inefficient, only for RPC *) + let dal_participation_info ctxt delegate = + Raw_context.Dal.only_if_incentives_enabled + ctxt + ~default:(fun _ctxt -> tzfail Dal_errors_repr.Dal_incentives_disabled) + (fun ctxt -> dal_participation_info_enabled ctxt delegate) end diff --git a/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.mli b/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.mli index 71cf2fa21ccc6adeda3dacaa7c0e404c461343c2..a908cc595ce8d2a5f760a2ed5dc6399e5da2312e 100644 --- a/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.mli +++ b/src/proto_alpha/lib_protocol/delegate_missed_attestations_storage.mli @@ -71,6 +71,19 @@ val record_dal_participation : number_of_attested_slots:int -> Raw_context.t tzresult Lwt.t +(** Returns [true] iff the protocol considers that the delegate attested + sufficiently many slots during a cycle, given the total number of attested + slots for that cycle and the number of those slots also attested by the + delegate. + + The decision depends on the [minimal_participation_ratio] protocol + parameter. *) +val is_dal_participation_sufficient : + Raw_context.t -> + dal_attested_slots_by_delegate:int32 -> + total_dal_attested_slots:int32 -> + bool + (** Sets the payload and block producer as active. Pays the baking reward and the fees to the payload producer and the reward bonus to the payload producer (if the reward_bonus is not None).*) @@ -130,4 +143,39 @@ module For_RPC : sig Raw_context.t -> Signature.Public_key_hash.t -> participation_info tzresult Lwt.t + + (** Participation information. We denote by: + - "static" information that does not change during the cycle + - "dynamic" information that may change during the cycle *) + type dal_participation_info = { + expected_assigned_shards : int; + (** The total expected number of assigned shard indexes for the delegate + during the current cycle. (static) *) + delegate_attested_dal_slots : int; + (** The number of attested slots during the current cycle that are + attested by the delegate. (dynamic) *) + total_dal_attested_slots : int; + (** The total number of attested slots during the current cycle + (regardless whether this delegate attested them or + not). (dynamic) *) + expected_dal_rewards : Tez_repr.t; + (** The expected amount of DAL rewards for the delegate, assuming a + sufficient DAL participation (see [sufficient_dal_participation] + below). (static) *) + sufficient_dal_participation : bool; + (** A boolean flag telling whether the delegate sufficiently + participated in (attested) DAL slots attestation or not. In + particular, this flag is true if no DAL slot is attested globally by + the protocol at a given cycle (i.e. [total_dal_attested_slots] = + 0). (dynamic) *) + } + + (** Only use this function for RPC: this is expensive. + + [dal_participation_info] forms the implementation of RPC call + "/context/delegates//dal_participation". *) + val dal_participation_info : + Raw_context.t -> + Signature.Public_key_hash.t -> + dal_participation_info tzresult Lwt.t end diff --git a/src/proto_alpha/lib_protocol/test/helpers/context.ml b/src/proto_alpha/lib_protocol/test/helpers/context.ml index df0ba87c62fa7d624dea77db05797506404bb4c5..2ae2f7a87592c53a9e1592f6f63059c8dfdc2cfe 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/context.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/context.ml @@ -585,6 +585,9 @@ module Delegate = struct let participation ctxt pkh = Delegate_services.participation rpc_ctxt ctxt pkh + let dal_participation ctxt pkh = + Delegate_services.dal_participation rpc_ctxt ctxt pkh + let is_forbidden ctxt pkh = Delegate_services.is_forbidden rpc_ctxt ctxt pkh let stake_for_cycle ctxt cycle pkh = diff --git a/src/proto_alpha/lib_protocol/test/helpers/context.mli b/src/proto_alpha/lib_protocol/test/helpers/context.mli index 591f4f4dffde085c09ef876c9c37c866c54a8f1b..2c595d0f37114775ecbd83dc1c3d83c77786bf6c 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/context.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/context.mli @@ -322,6 +322,11 @@ module Delegate : sig val participation : t -> public_key_hash -> Delegate.For_RPC.participation_info tzresult Lwt.t + val dal_participation : + t -> + public_key_hash -> + Delegate.For_RPC.dal_participation_info tzresult Lwt.t + val is_forbidden : t -> public_key_hash -> bool tzresult Lwt.t val stake_for_cycle : t -> Cycle.t -> public_key_hash -> stake tzresult Lwt.t diff --git a/src/proto_alpha/lib_protocol/test/integration/consensus/test_participation.ml b/src/proto_alpha/lib_protocol/test/integration/consensus/test_participation.ml index 941f165903e60122206e425a5a2e7f5c12193857..e74a6e553d542c4d1ed5c1b6b467422e1dd91212 100644 --- a/src/proto_alpha/lib_protocol/test/integration/consensus/test_participation.ml +++ b/src/proto_alpha/lib_protocol/test/integration/consensus/test_participation.ml @@ -148,6 +148,34 @@ let test_participation ~sufficient_participation () = in Assert.equal_int64 ~loc:__LOC__ bal2_at_b expected_bal2_at_b +let check_no_dal_participation + (dal_info : Delegate.For_RPC.dal_participation_info) = + let open Lwt_result_wrap_syntax in + let* () = + (* Some shards are assigned to the delegate. *) + Assert.not_equal_int ~loc:__LOC__ dal_info.expected_assigned_shards 0 + in + let* () = + (* No slot is attested globally. *) + Assert.equal_int ~loc:__LOC__ dal_info.total_dal_attested_slots 0 + in + let* () = + (* No attested slot is also attested by this delegate. *) + Assert.equal_int ~loc:__LOC__ dal_info.delegate_attested_dal_slots 0 + in + let* () = + (* The delegate sufficiently participated in DAL (are there is no slot + attested globally). *) + Assert.equal_bool ~loc:__LOC__ dal_info.sufficient_dal_participation true + in + let* () = + (* No Tez are actually provisioned for DAL rewards as incentives are not + enabled in this test. Turn the test to [Assert.not_equal_tez] when + incentives are enabled by default. *) + Assert.equal_tez ~loc:__LOC__ dal_info.expected_dal_rewards Tez.zero + in + return_unit + (* We bake and attest with 1 out of 2 accounts; we monitor the result returned by the '../delegates//participation' RPC for the non-participating account. *) @@ -182,6 +210,12 @@ let test_participation_rpc () = let* _, _, _ = List.fold_left_es (fun (b_pred, b_crt, total_attesting_power) level_int -> + let* () = + if csts.Constants.parametric.dal.incentives_enable then + let* dal_info = Context.Delegate.dal_participation (B b_crt) del2 in + check_no_dal_participation dal_info + else return_unit + in let* info = Context.Delegate.participation (B b_crt) del2 in let* () = Assert.equal_int