diff --git a/CHANGES.rst b/CHANGES.rst index 0cca95ff9fa6cbd18ba108490a155e7b5700de8e..ae86556b2e0d7050134a9a9c9527e028117aab6e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,9 @@ Node - Added RPC ``POST /bls/check_proof`` to check a BLS proof. (MR :gl:`!17461`) +- Added RPC ``POST /bls/threshold_signatures`` to recover a BLS + threshold signature. (MR :gl:`!17467`) + Client ------ @@ -59,6 +62,18 @@ Client - Add a new command ``check bls proof`` to check a BLS proof. (MR :gl:`!17461`) +- Add a new command ``share bls secret key between shares + with threshold `` to share a BLS secret key between ``n`` + participants so that any ``m`` participants can collaboratively sign + messages, while fewer than ``m`` participants cannot produce a valid + signature. Note that this command requires a secret key: make sure + that you are in a secure environment before using it. Alternatively, + one can implement their own version of secret sharing. (MR + :gl:`!17467`) + +- Add a new command ``threshold bls signatures`` to recover a BLS + threshold signature. (MR :gl:`!17467`) + Signer ------ diff --git a/src/lib_shell/bls_directory.ml b/src/lib_shell/bls_directory.ml index 54e47ef331d53d7623c29a61ce6631891f7bff26..d6ed859142de964e1552eff6ec9432ba06cd4cfc 100644 --- a/src/lib_shell/bls_directory.ml +++ b/src/lib_shell/bls_directory.ml @@ -49,4 +49,11 @@ let build_rpc_directory () = else None in return res) ; + register0 Bls_services.S.threshold_signatures (fun () sigs -> + let sigs = + List.map + (fun (s : Bls_services.S.threshold_signature) -> (s.id, s.signature)) + sigs + in + return @@ Bls.threshold_signature_opt sigs) ; !dir diff --git a/src/lib_shell_services/bls_services.ml b/src/lib_shell_services/bls_services.ml index be3d9170d7463330855869459506fbb1acdecea6..e1c6f914bbf3eb13420a1c0cc6a02102bf2b2c90 100644 --- a/src/lib_shell_services/bls_services.ml +++ b/src/lib_shell_services/bls_services.ml @@ -38,6 +38,15 @@ module S = struct (req "public_key" Bls.Public_key.encoding) (req "public_key_hash" Bls.Public_key_hash.encoding)) + type threshold_signature = {id : int; signature : Bls.t} + + let threshold_signature_encoding = + let open Data_encoding in + conv + (fun {id; signature} -> (id, signature)) + (fun (id, signature) -> {id; signature}) + (obj2 (req "id" Data_encoding.int8) (req "signature" Bls.encoding)) + let aggregate_signatures = Tezos_rpc.Service.post_service ~description:"Aggregate BLS signatures" @@ -61,6 +70,14 @@ module S = struct ~input:(list public_key_with_proof_encoding) ~output:(option public_key_and_public_key_hash_encoding) Tezos_rpc.Path.(path / "aggregate_public_keys") + + let threshold_signatures = + Tezos_rpc.Service.post_service + ~description:"Threshold BLS signatures" + ~query:Tezos_rpc.Query.empty + ~input:(list threshold_signature_encoding) + ~output:(option Bls.encoding) + Tezos_rpc.Path.(path / "threshold_signatures") end let aggregate_signatures ctxt sigs = @@ -71,3 +88,6 @@ let check_proof ctxt pk_with_proof = let aggregate_public_keys ctxt pks_with_proofs = Tezos_rpc.Context.make_call S.aggregate_public_keys ctxt () () pks_with_proofs + +let threshold_signatures ctxt sigs = + Tezos_rpc.Context.make_call S.threshold_signatures ctxt () () sigs diff --git a/src/lib_shell_services/bls_services.mli b/src/lib_shell_services/bls_services.mli index 1a2c7b6c6098d7ab06769c6804db4ca11f2704fd..898c7ae0edf924254488964a873bc4273ca35b1d 100644 --- a/src/lib_shell_services/bls_services.mli +++ b/src/lib_shell_services/bls_services.mli @@ -16,6 +16,8 @@ module S : sig pkh : Bls.Public_key_hash.t; } + type threshold_signature = {id : int; signature : Bls.t} + val aggregate_signatures : ([`POST], unit, unit, unit, Bls.t list, Bls.t option) Tezos_rpc.Service.t @@ -30,6 +32,15 @@ module S : sig public_key_with_proof list, public_key_and_public_key_hash option ) Tezos_rpc.Service.t + + val threshold_signatures : + ( [`POST], + unit, + unit, + unit, + threshold_signature list, + Bls.t option ) + Tezos_rpc.Service.t end val aggregate_signatures : @@ -42,3 +53,8 @@ val aggregate_public_keys : #Tezos_rpc.Context.simple -> S.public_key_with_proof list -> S.public_key_and_public_key_hash option tzresult Lwt.t + +val threshold_signatures : + #Tezos_rpc.Context.simple -> + S.threshold_signature list -> + Bls.t option tzresult Lwt.t diff --git a/src/proto_alpha/lib_client_commands/client_bls_commands.ml b/src/proto_alpha/lib_client_commands/client_bls_commands.ml index 6e3f841d05e72ac387cdf9fffe09c7dc654d7b80..95e54c751b4628ca5937233735e091ab0a4df9d5 100644 --- a/src/proto_alpha/lib_client_commands/client_bls_commands.ml +++ b/src/proto_alpha/lib_client_commands/client_bls_commands.ml @@ -29,6 +29,16 @@ let public_key_parameter ~name ~desc = | Some pk -> return pk | None -> cctxt#error "Failed to read a BLS public key")) +let secret_key_parameter ~name ~desc = + param + ~name + ~desc + (parameter (fun (cctxt : #Protocol_client_context.full) s -> + let open Lwt_result_syntax in + match Signature.Bls.Secret_key.of_b58check_opt s with + | Some sk -> return sk + | None -> cctxt#error "Failed to read a BLS secret key")) + type public_key_with_proof = { pk : Signature.Bls.Public_key.t; proof : Signature.Bls.t; @@ -57,6 +67,40 @@ let public_key_and_public_key_hash_encoding = (req "public_key" Signature.Bls.Public_key.encoding) (req "public_key_hash" Signature.Bls.Public_key_hash.encoding)) +type threshold_secret_key = {id : int; sk : Signature.Bls.Secret_key.t} + +let threshold_secret_key_encoding = + let open Data_encoding in + conv + (fun {id; sk} -> (id, sk)) + (fun (id, sk) -> {id; sk}) + (obj2 (req "id" int8) (req "secret_key" Signature.Bls.Secret_key.encoding)) + +type threshold_keys = { + pk : Signature.Bls.Public_key.t; + pkh : Signature.Bls.Public_key_hash.t; + secret_shares : threshold_secret_key list; +} + +let threshold_keys_encoding = + let open Data_encoding in + conv + (fun {pk; pkh; secret_shares} -> (pk, pkh, secret_shares)) + (fun (pk, pkh, secret_shares) -> {pk; pkh; secret_shares}) + (obj3 + (req "public_key" Signature.Bls.Public_key.encoding) + (req "public_key_hash" Signature.Bls.Public_key_hash.encoding) + (req "secret_shares" (list threshold_secret_key_encoding))) + +type threshold_signature = {id : int; signature : Signature.Bls.t} + +let threshold_signature_encoding = + let open Data_encoding in + conv + (fun {id; signature} -> (id, signature)) + (fun (id, signature) -> {id; signature}) + (obj2 (req "id" int8) (req "signature" Signature.Bls.encoding)) + let check_public_key_with_proof pk proof = let msg = Data_encoding.Binary.to_bytes_exn @@ -162,4 +206,67 @@ let commands () = return_unit | None -> cctxt#error "Failed to aggregate the public keys" else cctxt#error "Failed to check proofs"); + command + ~group + ~desc: + "Shamir's Secret Sharing: share a secret key between N participants so \ + that any M participants can collaboratively sign messages, while \ + fewer than M participants cannot produce a valid signature" + no_options + (prefixes ["share"; "bls"; "secret"; "key"] + @@ secret_key_parameter + ~name:"BLS secret key" + ~desc:"B58 encoded BLS secret key" + @@ prefixes ["between"] + @@ Tezos_clic.param + ~name:"shares" + ~desc:"Number of shares (N)" + Client_proto_args.int_parameter + @@ prefixes ["shares"; "with"; "threshold"] + @@ Tezos_clic.param + ~name:"threshold" + ~desc:"Number of required signatures (M)" + Client_proto_args.int_parameter + @@ stop) + (fun () sk n m (cctxt : #Protocol_client_context.full) -> + let open Lwt_result_syntax in + let pk, pkh, secret_shares = + Signature.Bls.generate_threshold_key sk ~n ~m + in + let secret_shares = List.map (fun (id, sk) -> {id; sk}) secret_shares in + let json = + Data_encoding.Json.construct + threshold_keys_encoding + {pk; pkh; secret_shares} + in + let*! () = cctxt#message "%a@." Data_encoding.Json.pp json in + return_unit); + command + ~group + ~desc:"Threshold BLS signatures" + no_options + (prefixes ["threshold"; "bls"; "signatures"] + @@ Client_proto_args.json_encoded_param + ~name:"list of BLS identifiers with signatures" + ~desc:"list of BLS identifier (int) and B58 encoded BLS signature" + (Data_encoding.list threshold_signature_encoding) + @@ stop) + (fun () sigs (cctxt : #Protocol_client_context.full) -> + let open Lwt_result_syntax in + let* sigs = + List.map_es + (fun (s : threshold_signature) -> return (s.id, s.signature)) + sigs + in + let threshold_signature = Signature.Bls.threshold_signature_opt sigs in + match threshold_signature with + | Some threshold_signature -> + let*! () = + cctxt#message + "%a" + Signature.pp + (Signature.Bls threshold_signature) + in + return_unit + | None -> cctxt#error "Failed to produce the threshold signature"); ] diff --git a/tezt/lib_tezos/RPC.ml b/tezt/lib_tezos/RPC.ml index 6e59248c69982d3aaec936a3d2aa30ff5db047cf..e41353c59a950254c4907519bd46663462188b88 100644 --- a/tezt/lib_tezos/RPC.ml +++ b/tezt/lib_tezos/RPC.ml @@ -910,6 +910,19 @@ let post_bls_aggregate_public_keys pks_with_proofs = let group_pkh = JSON.(json |-> "public_key_hash" |> as_string) in (group_pk, group_pkh) +let post_bls_threshold_signatures id_signatures = + let data = + `A + (List.map + (fun (id, signature) -> + `O + [ + ("id", `Float (float_of_int id)); ("signature", `String signature); + ]) + id_signatures) + in + make ~data:(Data data) POST ["bls"; "threshold_signatures"] JSON.as_string + let get_chain_block_context_big_map ?(chain = "main") ?(block = "head") ~id ~key_hash () = make diff --git a/tezt/lib_tezos/RPC.mli b/tezt/lib_tezos/RPC.mli index 5dbfa062d288877347b8d03e03c4eac5127d2e21..8687761285609034b0e57ea4f4d84b711e3ff87d 100644 --- a/tezt/lib_tezos/RPC.mli +++ b/tezt/lib_tezos/RPC.mli @@ -814,6 +814,9 @@ val post_bls_check_proof : pk:string -> proof:string -> unit -> bool t val post_bls_aggregate_public_keys : (string * string) list -> (string * string) t +(** RPC: [POST /bls/threshold_signatures] *) +val post_bls_threshold_signatures : (int * string) list -> string t + (** {2 Big maps RPC module} *) (** RPC: [GET /chains//blocks//context/big_maps//] diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 7fecc632fe11d76ecc3a9d5733c55b2d1a211d5f..f1f084bc3b3fb1b8d83bfebc7c411fef1815a276 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -4559,3 +4559,54 @@ let aggregate_bls_public_keys client pks_with_proofs = let group_pk = JSON.(output |-> "public_key" |> as_string) in let group_pkh = JSON.(output |-> "public_key_hash" |> as_string) in return (group_pk, group_pkh) + +let spawn_share_bls_secret_key ~sk ~n ~m client = + spawn_command client + @@ [ + "share"; + "bls"; + "secret"; + "key"; + sk; + "between"; + string_of_int n; + "shares"; + "with"; + "threshold"; + string_of_int m; + ] + +let share_bls_secret_key ~sk ~n ~m client = + let* client_output = + spawn_share_bls_secret_key ~sk ~n ~m client |> Process.check_and_read_stdout + in + let output = JSON.parse ~origin:"share_bls_secret_key" client_output in + let group_pk = JSON.(output |-> "public_key" |> as_string) in + let group_pkh = JSON.(output |-> "public_key_hash" |> as_string) in + let secret_shares_list = JSON.(output |-> "secret_shares" |> as_list) in + let secret_shares = + List.map + (fun s -> + let id = JSON.(s |-> "id" |> as_int) in + let sk = JSON.(s |-> "secret_key" |> as_string) in + (id, sk)) + secret_shares_list + in + return (group_pk, group_pkh, secret_shares) + +let spawn_threshold_bls_signatures client id_signatures = + let id_signatures = + List.map + (fun (id, signature) -> + `O [("id", `Float (float_of_int id)); ("signature", `String signature)]) + id_signatures + in + let json_batch = `A id_signatures |> JSON.encode_u in + spawn_command client @@ ["threshold"; "bls"; "signatures"; json_batch] + +let threshold_bls_signatures client id_signatures = + let* s = + spawn_threshold_bls_signatures client id_signatures + |> Process.check_and_read_stdout + in + return (String.trim s) diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index 81674271d94dde18bfc745094425ad817fe3f34b..aa58306b6dda2e1164814c299ede8ee6670a64d5 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -3293,3 +3293,15 @@ val check_bls_proof : pk:string -> proof:string -> t -> unit Lwt.t Returns [(aggregated_public_key, aggregated_public_key_hash)]. *) val aggregate_bls_public_keys : t -> (string * string) list -> (string * string) Lwt.t + +(** Run [octez-client share bls secret key between shares + with threshold ]. *) +val share_bls_secret_key : + sk:string -> + n:int -> + m:int -> + t -> + (string * string * (int * string) list) Lwt.t + +(** Run [octez-client threshold bls signatures ]. *) +val threshold_bls_signatures : t -> (int * string) list -> string Lwt.t diff --git a/tezt/tests/bls_signature.ml b/tezt/tests/bls_signature.ml index 3cc6cb4810aad59ce57ebbc96fdb1eab0369b9bf..8a2c1e6b3ec1660791361d3e9eba1a92595cba89 100644 --- a/tezt/tests/bls_signature.ml +++ b/tezt/tests/bls_signature.ml @@ -286,6 +286,26 @@ module Local_helpers = struct secret_key = Encrypted ""; } + let mk_account_from_bls_sk ~bls_sk ~alias = + let sk = Tezos_crypto.Signature.Bls.Secret_key.of_b58check_exn bls_sk in + let pk = Tezos_crypto.Signature.Bls.Secret_key.to_public_key sk in + let pkh = + Tezos_crypto.Signature.Bls.Public_key.hash pk + |> Tezos_crypto.Signature.Bls.Public_key_hash.to_b58check + in + let public_key = Tezos_crypto.Signature.Bls.Public_key.to_b58check pk in + let secret_key = Account.Unencrypted bls_sk in + Log.info + ~color:Log.Color.FG.green + "Create an account for %s with pkh = %s." + alias + pkh ; + Account.{alias; public_key_hash = pkh; public_key; secret_key} + + let bls_sk_to_b58_string (sk : Bls12_381_signature.sk) = + Tezos_crypto.Signature.Bls sk + |> Tezos_crypto.Signature.Secret_key.to_b58check + let sign_and_aggregate_signatures ~kind ~watermark ~(signers : Account.key list) (msg : bytes) client = let signatures = @@ -300,6 +320,23 @@ module Local_helpers = struct | RPC -> Client.RPC.call client @@ RPC.post_bls_aggregate_signatures signatures + let sign_and_recover_threshold_signature ~kind ~watermark + ~(signers : (int * Account.key) list) (msg : bytes) client = + let signatures = + List.map + (fun (id, signer) -> + let signature = + Account.sign_bytes ~watermark ~signer msg + |> Tezos_crypto.Signature.to_b58check + in + (id, signature)) + signers + in + match kind with + | Client -> Client.threshold_bls_signatures client signatures + | RPC -> + Client.RPC.call client @@ RPC.post_bls_threshold_signatures signatures + let inject_bls_group_sign_op ~baker ~group_signature (op : Operation.t) client = let group_signature = @@ -328,6 +365,36 @@ module Local_helpers = struct in inject_bls_group_sign_op ~baker ~group_signature op client + let inject_threshold_bls_sign_op ~kind ~baker + ~(signers : (int * Account.key) list) (op : Operation.t) client = + let* op_hex = Operation.hex op client in + let manager_op = Hex.to_bytes op_hex in + let* group_signature = + sign_and_recover_threshold_signature + ~kind + ~watermark:Generic_operation + ~signers + manager_op + client + in + inject_bls_group_sign_op ~baker ~group_signature op client + + let create_accounts_from_master_sk ~sk ~m ~n client = + if not (1 < m && m <= n) then + Test.fail "Invalid parameters for N = %d and M = %d" n m ; + let* group_pk, group_pkh, secret_shares = + Client.share_bls_secret_key ~sk ~n:5 ~m:3 client + in + let stakers = + List.map + (fun (id, bls_sk) -> + ( id, + mk_account_from_bls_sk ~bls_sk ~alias:("staker_" ^ string_of_int id) + )) + secret_shares + in + return (group_pk, group_pkh, stakers) + let print_parameters parameters = let blocks_per_cycle = JSON.(get "blocks_per_cycle" parameters |> as_int) in let consensus_rights_delay = @@ -904,6 +971,214 @@ let test_all_stakers_sign_staking_operation_consensus_key ~kind = in unit +(** N = 5 and M = 3 *) +let test_threshold_number_stakers_sign_staking_operation_external_delegate ~kind + = + Protocol.register_test + ~__FILE__ + ~title: + (sf + "threshold number of stakers sign a staking operation with an \ + external delegate (%s)" + (kind_to_string kind)) + ~tags:(threshold_bls_tags @ ["threshold"; "external_delegate"]) + ~supports:(Protocol.From_protocol 023) + @@ fun protocol -> + let* _parameters, client, default_baker, funder, delegate = + Local_helpers.init_node_and_client_with_external_delegate ~protocol + in + let master_sk = + Bls12_381_signature.generate_sk @@ Tezos_hacl.Hacl.Rand.gen 32 + |> Local_helpers.bls_sk_to_b58_string + in + (* Shamir's Secret Sharing *) + let* group_pk_bls, group_pkh_bls, stakers = + Local_helpers.create_accounts_from_master_sk ~sk:master_sk ~m:3 ~n:5 client + in + let group_staker = + Local_helpers.mk_fake_account_from_bls_pk + ~bls_pk:group_pk_bls + ~bls_pkh:group_pkh_bls + ~alias:"group_staker" + in + (* transfer 150000 from bootstrap2 to group_staker *) + let* () = + Local_helpers.transfer + ~baker:default_baker + ~amount:(Tez.of_int 150_000) + ~giver:funder + ~receiver:group_staker.public_key_hash + client + in + + let signers = List.filteri (fun i _s -> i < 3) stakers in + (* reveal key for group_staker *) + let* op_reveal = Local_helpers.mk_op_reveal group_staker client in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_reveal + client + in + (* set delegate for group_staker to delegate *) + let* op_set_delegate = + Local_helpers.mk_op_set_delegate ~src:group_staker ~delegate client + in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_set_delegate + client + in + (* stake 140000 for group_staker *) + let* op_stake = + Local_helpers.mk_op_stake + ~staker:group_staker + ~amount:(Tez.of_int 140_000) + client + in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_stake + client + in + let* () = + Local_helpers.check_staked_balance_increase_when_baking + ~baker:delegate + ~staker:group_staker + client + in + unit + +(** N = 5 and M = 3 *) +let test_threshold_number_stakers_sign_staking_operation_consensus_key ~kind = + Protocol.register_test + ~__FILE__ + ~title: + (sf + "threshold number of stakers sign a staking operation with a \ + consensus key (%s)" + (kind_to_string kind)) + ~tags:(threshold_bls_tags @ ["threshold"; "consensus_key"]) + ~supports:(Protocol.From_protocol 023) + @@ fun protocol -> + let* parameters, client, default_baker, funder = + Local_helpers.init_node_and_client ~protocol + in + let master_sk = + Bls12_381_signature.generate_sk @@ Tezos_hacl.Hacl.Rand.gen 32 + |> Local_helpers.bls_sk_to_b58_string + in + (* Shamir's Secret Sharing *) + let* group_pk_bls, group_pkh_bls, stakers = + Local_helpers.create_accounts_from_master_sk ~sk:master_sk ~m:3 ~n:5 client + in + let group_staker = + Local_helpers.mk_fake_account_from_bls_pk + ~bls_pk:group_pk_bls + ~bls_pkh:group_pkh_bls + ~alias:"group_staker" + in + (* transfer 150000 from bootstrap2 to group_staker *) + let* () = + Local_helpers.transfer + ~baker:default_baker + ~amount:(Tez.of_int 150_000) + ~giver:funder + ~receiver:group_staker.public_key_hash + client + in + + let signers = List.filteri (fun i _s -> i < 3) stakers in + (* reveal key for group_staker *) + let* op_reveal = Local_helpers.mk_op_reveal group_staker client in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_reveal + client + in + (* set delegate for group_staker to group_staker *) + let* op_set_delegate = + Local_helpers.mk_op_set_delegate + ~src:group_staker + ~delegate:group_staker + client + in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_set_delegate + client + in + (* stake 140000 for group_staker *) + let* op_stake = + Local_helpers.mk_op_stake + ~staker:group_staker + ~amount:(Tez.of_int 140_000) + client + in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_stake + client + in + (* gen keys delegate_consensus_key -s bls *) + let* delegate_consensus_key = + Local_helpers.gen_and_show_keys + ~alias:"delegate_consensus_key" + ~sig_alg:"bls" + client + in + (* create a proof for a consensus key *) + let* proof = + Client.create_bls_proof ~signer:delegate_consensus_key.alias client + in + (* set consensus key for group_staker to delegate_consensus_key *) + let* op_update_consensus_key = + Local_helpers.mk_op_update_consensus_key + ~delegate:group_staker + ~delegate_consensus_key + ~proof + client + in + let* _op_hash = + Local_helpers.inject_threshold_bls_sign_op + ~kind + ~baker:default_baker + ~signers + op_update_consensus_key + client + in + let* () = + Local_helpers.bake_for_consensus_rights_delay_and_wait + ~baker:default_baker + ~parameters + ~delegate:delegate_consensus_key + client + in + let* () = + Local_helpers.check_staked_balance_increase_when_baking + ~baker:delegate_consensus_key + ~staker:group_staker + client + in + unit + let register ~protocols = test_single_staker_sign_staking_operation_self_delegate protocols ; test_single_staker_sign_staking_operation_external_delegate protocols ; @@ -914,4 +1189,16 @@ let register ~protocols = protocols ; test_all_stakers_sign_staking_operation_external_delegate ~kind:RPC protocols ; test_all_stakers_sign_staking_operation_consensus_key ~kind:Client protocols ; - test_all_stakers_sign_staking_operation_consensus_key ~kind:RPC protocols + test_all_stakers_sign_staking_operation_consensus_key ~kind:RPC protocols ; + test_threshold_number_stakers_sign_staking_operation_external_delegate + ~kind:Client + protocols ; + test_threshold_number_stakers_sign_staking_operation_external_delegate + ~kind:RPC + protocols ; + test_threshold_number_stakers_sign_staking_operation_consensus_key + ~kind:Client + protocols ; + test_threshold_number_stakers_sign_staking_operation_consensus_key + ~kind:RPC + protocols