diff --git a/src/proto_alpha/lib_delegate/client_baking_blocks.ml b/src/proto_alpha/lib_delegate/client_baking_blocks.ml index f4c7de12cdab2953c2b5545189434c1c99382b14..c78e919b9f027c738b1929df913668d4ae93f04d 100644 --- a/src/proto_alpha/lib_delegate/client_baking_blocks.ml +++ b/src/proto_alpha/lib_delegate/client_baking_blocks.ml @@ -176,15 +176,18 @@ let blocks_from_current_cycle cctxt ?(chain = `Main) block ?(offset = 0l) () = return_nil | Error _ as err -> Lwt.return err - | Ok (first, last) -> - let length = Int32.to_int (Int32.sub level (Raw_level.to_int32 first)) in - Shell_services.Blocks.list cctxt ~chain ~heads:[hash] ~length () - >>=? fun blocks -> - let blocks = - List.remove - (length - Int32.to_int (Raw_level.diff last first)) - (List.hd blocks) + | Ok (first, last) -> ( + let length = + 1 + Int32.to_int (Int32.sub level (Raw_level.to_int32 first)) in - if Int32.equal level (Raw_level.to_int32 last) then - return (hash :: blocks) - else return blocks + Shell_services.Blocks.list cctxt ~chain ~heads:[hash] ~length () + >>=? function + | [] -> + return_nil + | blocks -> + let blocks = + List.remove + (length - (1 + Int32.to_int (Raw_level.diff last first))) + (List.hd blocks) + in + return blocks ) diff --git a/src/proto_alpha/lib_delegate/client_baking_forge.ml b/src/proto_alpha/lib_delegate/client_baking_forge.ml index d36ed6f62072d6d0f3a89de4132239a4f4cab43d..b528919308fac7be2ad369f8cd9b4ba4b46aacca 100644 --- a/src/proto_alpha/lib_delegate/client_baking_forge.ml +++ b/src/proto_alpha/lib_delegate/client_baking_forge.ml @@ -105,13 +105,6 @@ let get_delegates cctxt state = | _ -> return state.delegates -let generate_seed_nonce () = - match Nonce.of_bytes (Rand.generate Constants.nonce_length) with - | Error _errs -> - assert false - | Ok nonce -> - nonce - let forge_block_header (cctxt : #Protocol_client_context.full) ~chain block delegate_sk shell priority seed_nonce_hash = Client_baking_pow.mine cctxt chain block shell (fun proof_of_work_nonce -> @@ -1078,16 +1071,11 @@ let fetch_operations (cctxt : #Protocol_client_context.full) ~chain with consistent operations that went through the client-side validation *) let build_block cctxt ~user_activated_upgrades state seed_nonce_hash - ((slot_timestamp, (bi, priority, delegate)) as slot) = - let chain = `Hash bi.Client_baking_blocks.chain_id in - let block = `Hash (bi.hash, 0) in - Alpha_services.Helpers.current_level cctxt ~offset:1l (chain, block) - >>=? fun next_level -> - let seed_nonce_hash = - if next_level.Level.expected_commitment then Some seed_nonce_hash else None - in + ((slot_timestamp, (bi, priority, delegate)) as slot) next_level = Client_keys.Public_key_hash.name cctxt delegate >>=? fun name -> + let chain = `Hash bi.Client_baking_blocks.chain_id in + let block = `Hash (bi.hash, 0) in lwt_debug Tag.DSL.( fun f -> @@ -1277,10 +1265,27 @@ let bake (cctxt : #Protocol_client_context.full) ~user_activated_upgrades assert false (* unreachable *) | Some slot -> return slot ) - >>=? fun slot -> - let seed_nonce = generate_seed_nonce () in - let seed_nonce_hash = Nonce.hash seed_nonce in - build_block cctxt ~user_activated_upgrades state seed_nonce_hash slot + >>=? fun ((_, (info, _, delegate)) as slot) -> + let chain_id = `Hash info.Client_baking_blocks.chain_id in + let block = `Hash (info.hash, 0) in + Alpha_services.Helpers.current_level cctxt ~offset:1l (chain_id, block) + >>=? fun next_level -> + ( if next_level.Level.expected_commitment then + Client_baking_nonces.generate_nonce cctxt delegate next_level.level + >>=? fun baker_nonce_hash -> + let seed_nonce_hash = + Client_baking_nonces.get_nonce_hash baker_nonce_hash + in + return (Some baker_nonce_hash, Some seed_nonce_hash) + else return (None, None) ) + >>=? fun (baker_nonce_hash, seed_nonce_hash) -> + build_block + cctxt + ~user_activated_upgrades + state + seed_nonce_hash + slot + next_level >>=? function | Some (head, priority, shell_header, operations, delegate, seed_nonce_hash) -> ( @@ -1340,13 +1345,21 @@ let bake (cctxt : #Protocol_client_context.full) ~user_activated_upgrades -% a operations_tag operations) >>= fun () -> ( if seed_nonce_hash <> None then - cctxt#with_lock (fun () -> - let open Client_baking_nonces in - load cctxt state.nonces_location - >>=? fun nonces -> - let nonces = add nonces block_hash seed_nonce in - save cctxt state.nonces_location nonces) - |> trace_exn (Failure "Error while recording nonce") + (* Register the nonce only if the user doesn't support + deterministic nonces *) + let open Client_baking_nonces in + match baker_nonce_hash with + | None -> + return_unit + | Some (Hash_of_deterministic_nonce _seed_nonce_hash) -> + return_unit + | Some (Hash_of_random_nonce (seed_nonce, _seed_nonce_hash)) -> + cctxt#with_lock (fun () -> + load cctxt state.nonces_location + >>=? fun nonces -> + let nonces = add nonces block_hash seed_nonce in + save cctxt state.nonces_location nonces) + |> trace_exn (Failure "Error while recording nonce") else return_unit ) >>=? fun () -> return_unit ) | None -> @@ -1441,7 +1454,7 @@ let compute_best_slot_on_current_level ?max_priority (** [reveal_potential_nonces] reveal registered nonces *) let reveal_potential_nonces (cctxt : #Client_context.full) constants ~chain - ~block = + ~block delegates = cctxt#with_lock (fun () -> Client_baking_files.resolve_location cctxt ~chain `Nonce >>=? fun nonces_location -> @@ -1458,6 +1471,7 @@ let reveal_potential_nonces (cctxt : #Client_context.full) constants ~chain Client_baking_nonces.get_unrevealed_nonces cctxt nonces_location + delegates nonces >>= function | Error err -> @@ -1471,6 +1485,12 @@ let reveal_potential_nonces (cctxt : #Client_context.full) constants ~chain | Ok [] -> return_unit | Ok nonces_to_reveal -> ( + lwt_log_notice + Tag.DSL.( + fun f -> + f "Revealing %d nonces" -% t event "reveal_nonces" + -% s Logging.num_nonces_tag (List.length nonces_to_reveal)) + >>= fun () -> Client_baking_revelation.inject_seed_nonce_revelation cctxt ~chain @@ -1535,6 +1555,7 @@ let create (cctxt : #Protocol_client_context.full) ~user_activated_upgrades state.constants ~chain ~block:(`Hash (new_head.Client_baking_blocks.hash, 0)) + delegates >>= fun _ignore_nonce_err -> compute_best_slot_on_current_level ?max_priority cctxt state new_head >>=? fun slot -> diff --git a/src/proto_alpha/lib_delegate/client_baking_forge.mli b/src/proto_alpha/lib_delegate/client_baking_forge.mli index c1182e356001a5ab1d5289ccbd2d0c06855bf561..9a5ec153ad1506b77173db9a27790dce99f8c242 100644 --- a/src/proto_alpha/lib_delegate/client_baking_forge.mli +++ b/src/proto_alpha/lib_delegate/client_baking_forge.mli @@ -26,12 +26,6 @@ open Protocol open Alpha_context -(** [generate_seed_nonce ()] is a random nonce that is typically used - in block headers. When baking, bakers generate random nonces whose - hash is committed in the block they bake. They will typically - reveal the aforementioned nonce during the next cycle. *) -val generate_seed_nonce : unit -> Nonce.t - (** [inject_block cctxt blk ?force ~priority ~timestamp ~fitness ~seed_nonce ~src_sk ops] tries to inject a block in the node. If [?force] is set, the fitness check will be bypassed. [priority] diff --git a/src/proto_alpha/lib_delegate/client_baking_lib.ml b/src/proto_alpha/lib_delegate/client_baking_lib.ml index 72f1b91a6d810885c37a10eb5ae057e81a9dcd28..df1c1feb1f44fc639ea8807aa5f8f1e2c1982cce 100644 --- a/src/proto_alpha/lib_delegate/client_baking_lib.ml +++ b/src/proto_alpha/lib_delegate/client_baking_lib.ml @@ -39,13 +39,18 @@ let bake_block (cctxt : #Protocol_client_context.full) ?minimal_fees >>=? fun src_sk -> Alpha_services.Helpers.current_level cctxt ~offset:1l (chain, head) >>=? fun level -> - let (seed_nonce, seed_nonce_hash) = - if level.expected_commitment then - let seed_nonce = Client_baking_forge.generate_seed_nonce () in - let seed_nonce_hash = Nonce.hash seed_nonce in - (Some seed_nonce, Some seed_nonce_hash) - else (None, None) - in + ( if level.expected_commitment then + Client_baking_nonces.generate_nonce + (cctxt :> #Client_context.wallet) + delegate + level.level + >>=? fun baker_nonce_hash -> + let seed_nonce_hash = + Client_baking_nonces.get_nonce_hash baker_nonce_hash + in + return (Some baker_nonce_hash, Some seed_nonce_hash) + else return (None, None) ) + >>=? fun (baker_nonce_hash, seed_nonce_hash) -> let timestamp = if minimal_timestamp then None else Some Time.System.(to_protocol (Systime_os.now ())) @@ -66,12 +71,16 @@ let bake_block (cctxt : #Protocol_client_context.full) ?minimal_fees ~delegate_sk:src_sk head >>=? fun block_hash -> - ( match seed_nonce with + let open Client_baking_nonces in + ( match baker_nonce_hash with | None -> return_unit - | Some seed_nonce -> + | Some (Hash_of_deterministic_nonce _seed_nonce_hash) -> + return_unit + | Some (Hash_of_random_nonce (seed_nonce, _seed_nonce_hash)) -> + (* Register the nonce only if the user doesn't support + deterministic nonces *) cctxt#with_lock (fun () -> - let open Client_baking_nonces in Client_baking_files.resolve_location cctxt ~chain `Nonce >>=? fun nonces_location -> load cctxt nonces_location @@ -155,13 +164,16 @@ let reveal_block_nonces (cctxt : #Protocol_client_context.full) ~chain ~block do_reveal cctxt ~chain ~block nonces let reveal_nonces (cctxt : #Protocol_client_context.full) ~chain ~block () = + Client_keys.get_keys cctxt + >>=? fun keys -> + let delegates = List.map (fun (_, delegate, _, _) -> delegate) keys in let open Client_baking_nonces in cctxt#with_lock (fun () -> Client_baking_files.resolve_location cctxt ~chain `Nonce >>=? fun nonces_location -> load cctxt nonces_location >>=? fun nonces -> - get_unrevealed_nonces cctxt nonces_location nonces + get_unrevealed_nonces cctxt nonces_location delegates nonces >>=? fun nonces_to_reveal -> do_reveal cctxt ~chain ~block nonces_to_reveal >>=? fun () -> diff --git a/src/proto_alpha/lib_delegate/client_baking_nonces.ml b/src/proto_alpha/lib_delegate/client_baking_nonces.ml index 825e8c504a4317f365742804a221c4ddab3db137..4535d1a948a782b9cfb29ea1d35f616e9d7ef10e 100644 --- a/src/proto_alpha/lib_delegate/client_baking_nonces.ml +++ b/src/proto_alpha/lib_delegate/client_baking_nonces.ml @@ -25,6 +25,7 @@ open Protocol open Alpha_context +module Alpha_block_services = Block_services.Make (Protocol) (Protocol) include Internal_event.Legacy_logging.Make_semantic (struct let name = Protocol.name ^ ".baking.nonces" @@ -32,6 +33,56 @@ end) type t = Nonce.t Block_hash.Map.t +type baker_nonce_hash = + | Hash_of_random_nonce of Nonce.t * Nonce_hash.t (* old method *) + | Hash_of_deterministic_nonce of Nonce_hash.t + +(* new method *) + +(* Generates a pair (nonce * nonce_hash) either deterministically via + the wallet if it supports it or via a random seed *) +let generate_nonce cctxt delegate level = + Client_keys.get_key cctxt delegate + >>=? fun (_, _, sk) -> + Client_keys.supports_deterministic_nonces sk + >>=? function + | true -> + let data = Data_encoding.Binary.to_bytes_exn Raw_level.encoding level in + Client_keys.deterministic_nonce_hash sk data + >>=? fun nonce_hash_bytes -> + let nonce_hash = Nonce_hash.of_bytes_exn nonce_hash_bytes in + lwt_log_notice + Tag.DSL.( + fun f -> + f + "Client_baking_forge.inject_block: generated deterministic \ + nonce hash %a for level %a" + -% t event "generate_deterministic_nonce" + -% a Logging.nonce_hash_tag nonce_hash + -% a Logging.level_tag level) + >>= fun () -> return (Hash_of_deterministic_nonce nonce_hash) + | false -> + let bytes = Rand.generate Constants.nonce_length in + let nonce = Nonce.of_bytes bytes in + Lwt.return (Environment.wrap_error nonce) + >>=? fun nonce -> + let nonce_hash = Nonce.hash nonce in + lwt_log_notice + Tag.DSL.( + fun f -> + f + "Client_baking_forge.inject_block: generated seed nonce %a for \ + level %a" + -% t event "generate_seed_nonce" + -% a Logging.nonce_hash_tag nonce_hash + -% a Logging.level_tag level) + >>= fun () -> return (Hash_of_random_nonce (nonce, nonce_hash)) + +let get_nonce_hash = function + | Hash_of_random_nonce (_, nonce_hash) + | Hash_of_deterministic_nonce nonce_hash -> + nonce_hash + let empty = Block_hash.Map.empty let encoding = @@ -147,7 +198,121 @@ let filter_outdated_nonces cctxt ?constants location nonces = else Lwt.return_unit ) >>= fun () -> return (remove_all nonces outdated_nonces) -let get_unrevealed_nonces cctxt location nonces = +let get_random_nonce_for_block cctxt chain block_hash level nonce = + Alpha_services.Nonce.get cctxt (chain, `Head 0) level + >>=? function + | Missing nonce_hash when Nonce.check_hash nonce nonce_hash -> + lwt_log_notice + Tag.DSL.( + fun f -> + f "Found nonce to reveal for %a (level: %a)" + -% t event "found_nonce" + -% a Block_hash.Logging.tag block_hash + -% a Logging.level_tag level) + >>= fun () -> return_some (level, nonce) + | Missing _nonce_hash -> + lwt_log_error + Tag.DSL.( + fun f -> + f "Incoherent nonce for level %a" + -% t event "bad_nonce" -% a Logging.level_tag level) + >>= fun () -> return_none + | Forgotten -> + return_none + | Revealed _ -> + return_none + +let get_deterministic_nonce_for_hash cctxt chain delegates_keys block_hash + level = + Alpha_services.Nonce.get cctxt (chain, `Head 0) level + >>=? function + | Forgotten -> + return_none + | Revealed _ -> + return_none + | Missing included_nonce_hash -> ( + Alpha_block_services.metadata + cctxt + ~chain + ~block:(`Hash (block_hash, 0)) + () + >>=? fun {Alpha_block_services.protocol_data = {baker; _}; _} -> + (* Check that the baker that included the nonce is one of ours *) + match + List.find_opt + (fun (delegate, (_, _, _)) -> + Signature.Public_key_hash.(delegate = baker)) + delegates_keys + with + | None -> + return_none + | Some (baker_pkh, (_, _, baker_sk)) -> ( + (* It should support deterministic nonces if it + was not written in the file *) + Client_keys.supports_deterministic_nonces baker_sk + >>=? function + | false -> + return_none + | true -> ( + let data = + Data_encoding.Binary.to_bytes_exn Raw_level.encoding level + in + Client_keys.deterministic_nonce baker_sk data + >>= function + | Ok nonce -> + let nonce = + Data_encoding.Binary.of_bytes_exn + Nonce.encoding + (Bigstring.to_bytes nonce) + in + let computed_nonce_hash = Nonce.hash nonce in + if Nonce_hash.(computed_nonce_hash = included_nonce_hash) + then return_some (level, nonce) + else + lwt_log_error + Tag.DSL.( + fun f -> + f + "Inconsistent hash found when computing a \ + deterministic nonce for level %a - expected : \ + %a, got %a" + -% t event "inconsistent_deterministic_nonce_hash" + -% a Logging.level_tag level + -% a Logging.nonce_hash_tag computed_nonce_hash + -% a Logging.nonce_hash_tag included_nonce_hash) + >>= fun () -> return_none + | Error _err -> + lwt_log_error + Tag.DSL.( + fun f -> + f "Cannot create a deterministic nonce for %a" + -% t event "unsupported_deterministic_nonce" + -% a Signature.Public_key_hash.Logging.tag baker_pkh) + >>= fun () -> return_none ) ) ) + +let get_nonce_for_block cctxt chain nonces delegates_keys block_hash = + get_block_level_opt cctxt ~chain ~block:(`Hash (block_hash, 0)) + >>= function + | None -> + return_none + | Some level -> ( + Lwt.return (Environment.wrap_error (Raw_level.of_int32 level)) + >>=? fun level -> + match find_opt nonces block_hash with + | Some nonce -> + get_random_nonce_for_block cctxt chain block_hash level nonce + | None -> + get_deterministic_nonce_for_hash + cctxt + chain + delegates_keys + block_hash + level ) + +(* May be optimised to check every `blocks_per_commitment` instead of + the whole list (under the hypothesis that blocks_per_commitment + divides blocks_per_cycle) *) +let get_unrevealed_nonces cctxt location delegates nonces = let chain = Client_baking_files.chain location in Client_baking_blocks.blocks_from_current_cycle cctxt @@ -156,39 +321,12 @@ let get_unrevealed_nonces cctxt location nonces = ~offset:(-1l) () >>=? fun blocks -> + map_s + (fun delegate -> + Client_keys.get_key cctxt delegate + >>=? fun keys -> return (delegate, keys)) + delegates + >>=? fun delegates_keys -> filter_map_s - (fun hash -> - match find_opt nonces hash with - | None -> - return_none - | Some nonce -> ( - get_block_level_opt cctxt ~chain ~block:(`Hash (hash, 0)) - >>= function - | Some level -> ( - Lwt.return (Environment.wrap_error (Raw_level.of_int32 level)) - >>=? fun level -> - Alpha_services.Nonce.get cctxt (chain, `Head 0) level - >>=? function - | Missing nonce_hash when Nonce.check_hash nonce nonce_hash -> - lwt_log_notice - Tag.DSL.( - fun f -> - f "Found nonce to reveal for %a (level: %a)" - -% t event "found_nonce" - -% a Block_hash.Logging.tag hash - -% a Logging.level_tag level) - >>= fun () -> return_some (level, nonce) - | Missing _nonce_hash -> - lwt_log_error - Tag.DSL.( - fun f -> - f "Incoherent nonce for level %a" - -% t event "bad_nonce" -% a Logging.level_tag level) - >>= fun () -> return_none - | Forgotten -> - return_none - | Revealed _ -> - return_none ) - | None -> - return_none )) + (fun hash -> get_nonce_for_block cctxt chain nonces delegates_keys hash) blocks diff --git a/src/proto_alpha/lib_delegate/client_baking_nonces.mli b/src/proto_alpha/lib_delegate/client_baking_nonces.mli index b5c18d3b6a681f5bde092c3c992eaea74d4dae98..cd996ba7592bd3567e09328a91bfe8f596bf12f6 100644 --- a/src/proto_alpha/lib_delegate/client_baking_nonces.mli +++ b/src/proto_alpha/lib_delegate/client_baking_nonces.mli @@ -28,6 +28,26 @@ open Alpha_context type t = Nonce.t Block_hash.Map.t +type baker_nonce_hash = + | Hash_of_random_nonce of Nonce.t * Nonce_hash.t (* old method *) + | Hash_of_deterministic_nonce of Nonce_hash.t + +(* new method *) + +(** [generate_seed_nonce wallet delegate level] is a nonce that is + typically used in block headers. When baking, bakers generate + random nonces whose hash is commited in the block they bake. They + will typically reveal the aforementionned nonce during the next + cycle. If the wallet supports deterministic nonces, it will instead + generate a deterministic nonce based on the block's level. *) +val generate_nonce : + #Client_context.wallet -> + Signature.Public_key_hash.t -> + Raw_level.t -> + baker_nonce_hash tzresult Lwt.t + +val get_nonce_hash : baker_nonce_hash -> Nonce_hash.t + val encoding : t Data_encoding.t val empty : t @@ -77,5 +97,6 @@ val filter_outdated_nonces : val get_unrevealed_nonces : #Protocol_client_context.full -> [`Nonce] Client_baking_files.location -> + Signature.Public_key_hash.t list -> t -> (Raw_level.t * Nonce.t) list tzresult Lwt.t diff --git a/src/proto_alpha/lib_delegate/logging.ml b/src/proto_alpha/lib_delegate/logging.ml index aaa7172e0375db2e1d89eed3606b50ad3b0afd32..28bb77a9368614b5a8429dc5de96440dc4948a39 100644 --- a/src/proto_alpha/lib_delegate/logging.ml +++ b/src/proto_alpha/lib_delegate/logging.ml @@ -117,6 +117,13 @@ let nonce_tag = Data_encoding.Json.( fun ppf nonce -> pp ppf (construct Nonce.encoding nonce)) +let nonce_hash_tag = + Tag.def + ~doc:"Nonce hash" + "nonce_hash" + Data_encoding.Json.( + fun ppf nonce -> pp ppf (construct Nonce_hash.encoding nonce)) + let chain_tag = Tag.def ~doc:"Chain selector" @@ -152,3 +159,9 @@ let conflicting_endorsements_tag = (Operation.hash a) Operation_hash.pp (Operation.hash b)) + +let num_nonces_tag = + Tag.def + ~doc:"Number of revealed nonces" + "number_revealed_nonces" + Format.pp_print_int diff --git a/src/proto_alpha/lib_delegate/logging.mli b/src/proto_alpha/lib_delegate/logging.mli index 544b2ef25007ecdf1ec39a490229604ad94f9de8..310f206b4457a9ac156870745d6bb2b4b575d8fb 100644 --- a/src/proto_alpha/lib_delegate/logging.mli +++ b/src/proto_alpha/lib_delegate/logging.mli @@ -68,6 +68,8 @@ val level_tag : Raw_level.t Tag.def val nonce_tag : Nonce.t Tag.def +val nonce_hash_tag : Nonce_hash.t Tag.def + val chain_tag : Block_services.chain Tag.def val block_tag : Block_services.block Tag.def @@ -78,3 +80,5 @@ val block_header_tag : Block_header.t Tag.def val conflicting_endorsements_tag : (Kind.endorsement operation * Kind.endorsement operation) Tag.def + +val num_nonces_tag : int Tag.def diff --git a/tests_python/client/client.py b/tests_python/client/client.py index 8195aa38ffc694ca608e69bb66d8963e473d362b..60c34cf3ad2e777aef676564bbd1c5baefa3b540 100644 --- a/tests_python/client/client.py +++ b/tests_python/client/client.py @@ -495,6 +495,45 @@ class Client: rpc_res = self.get_checkpoint() return rpc_res['caboose'] + def get_cycle(self, params: List[str] = None) -> str: + rpc_res = self.rpc('get', '/chains/main/blocks/head/metadata', + params=params) + return rpc_res['level']['cycle'] + + def get_nonces_from_file(self): + nonces_file = os.path.join(self.base_dir, "nonces") + if os.path.isfile(nonces_file): + with open(nonces_file) as json_file: + return json.load(json_file) + else: + return [] + + def get_nonces_status(self, cycle): + rpc_res = self.rpc( + 'get', '/chains/main/blocks/head/context/raw/json/cycle/' + f'{cycle}/nonces?depth=1') + revealed = [] + unrevealed = [] + for info in rpc_res: + level = info[0] + if isinstance(info[1], list): + assert len(info[1]) == 4 + # at position 1, we find the baker's address + unrevealed.append((level, info[1][1])) + else: + revealed.append(level) + return {"revealed": revealed, "unrevealed": unrevealed} + + def get_baker(self, level): + rpc_res = self.rpc('get', + f'/chains/main/blocks/genesis+{level}/metadata') + return rpc_res['baker'] + + def get_hash(self, level): + rpc_res = self.rpc('get', + f'/chains/main/blocks/genesis+{level}/header') + return rpc_res['hash'] + def wait_for_inclusion(self, operation_hash: str, branch: str = None, diff --git a/tests_python/launchers/sandbox.py b/tests_python/launchers/sandbox.py index 44ec06b48d8a09a8f0a0d6cea2c1360e75d633fc..1231648e22c4dcb75bfba219ec5b4f7ec042de91 100644 --- a/tests_python/launchers/sandbox.py +++ b/tests_python/launchers/sandbox.py @@ -539,24 +539,32 @@ class SandboxMultiBranch(Sandbox): error_msg = f'{binaries_path}/{branch} not a dir' assert os.path.isdir(f'{binaries_path}/{branch}'), error_msg - def add_baker(self, + def add_baker(self, # pylint: disable=arguments-differ node_id: int, account: str, proto: str, params: List[str] = None, - branch: str = "") -> None: - """branch is overridden by branch_map""" - branch = self._branch_map[node_id] + branch: str = "", + map_id: int = None) -> None: + """branch is overridden by branch_map + map_id is used to override the map association""" + if map_id is None: + map_id = node_id + branch = self._branch_map[map_id] super().add_baker(node_id, account, proto, params, branch) - def add_endorser(self, + def add_endorser(self, # pylint: disable=arguments-differ node_id: int, account: str, proto: str, endorsement_delay: float = 0., - branch: str = "") -> None: - """branchs is overridden by branch_map""" - branch = self._branch_map[node_id] + branch: str = "", + map_id: int = None) -> None: + """branch is overridden by branch_map + map_id is used to override the map association""" + if map_id is None: + map_id = node_id + branch = self._branch_map[map_id] super().add_endorser(node_id, account, proto, endorsement_delay, branch) diff --git a/tests_python/tests/ledger/test_deterministic_nonces.py b/tests_python/tests/ledger/test_deterministic_nonces.py new file mode 100644 index 0000000000000000000000000000000000000000..75f32e931f7d8c20b694d4647d9dff037fa6d678 --- /dev/null +++ b/tests_python/tests/ledger/test_deterministic_nonces.py @@ -0,0 +1,122 @@ +r"""The main idea of this test: +- in cycle n, there's only one baker (say alice) that bakes + (therefore, only this baker generates and commits to nonces) +- in cycle n+1, by default, alice will reveal all its nonces for cycle n + +By setting the constants "blocks_per_cycle" to `2 * n` and +"blocks_per_commitment" to `2`, alice will reveal `n` nonces. +Then we can look up how much time this takes. + +NB: setting "blocks_per_commitment" to `1` gives the following error +when baking the last block of cycle 1: +Error: + Storage error: + Missing key 'cycle/0/nonces/1'. +This is probably related to the fact that the activation block is +somehow special. + +For this, we do the following: + tezos-client list connected ledgers + tezos-client import secret key alice "ledger://.../ed25519/0h/0h" + tezos-client set ledger high water mark for alice to 0 + tezos-client setup ledger to bake for alice + tezos-client register key alice as delegate + +NB: The user needs to confirm these steps manually on the ledger! + +""" + +import time +import pytest +from tools import constants + +BAKE_ARGS = ['--minimal-timestamp', '--max-priority', '512'] + + +def get_ledger_key(string): + for line in string.split('\n'): + if line.find("ed25519") != -1: + pos = line.find("ledger:") + return line[pos:-1] + return "" + + +@pytest.mark.slow +@pytest.mark.incremental +class TestDeterministicNoncesWithLedger: + """Test generation of nonces with ledger""" + + params = dict(constants.PARAMETERS) + + params["initial_endorsers"] = 0 # so we can bake every second + params["preserved_cycles"] = 1 + params["blocks_per_cycle"] = 8 + params["blocks_per_commitment"] = 1 + + baker = 'alice-ledger' + + def test_setup(self, sandbox): + sandbox.add_node(0, params=constants.NODE_PARAMS) + + client = sandbox.client(0) + client.activate_protocol_json(constants.ALPHA, self.params) + + res = client.run(['list', 'connected', 'ledgers']) + + ledger_key = get_ledger_key(res) + + client.import_secret_key(self.baker, ledger_key) + + client.run(['set', 'ledger', 'high', 'water', 'mark', + 'for', self.baker, 'to', '0']) + client.run(['setup', 'ledger', 'to', 'bake', 'for', self.baker]) + + def test_add_new_baker(self, sandbox): + client = sandbox.client(0) + client.transfer(1000000, "bootstrap1", self.baker, + ['--burn-cap', '0.257']) + client.bake('bootstrap1', BAKE_ARGS) + client.register_delegate(self.baker) + + def test_bake(self, sandbox): + client = sandbox.client(0) + + # bake with some boostrap account till the new baker becomes active + for cycle in range(self.params["preserved_cycles"] + 2): + for level in range(self.params['blocks_per_cycle']): + client.bake('bootstrap1', BAKE_ARGS) + if (cycle == self.params["preserved_cycles"] + 1 and + level == self.params['blocks_per_cycle'] - 3): + break + + # bake with the new baker account for a cycle + # so only this baker will commit nonces + for _level in range(self.params['blocks_per_cycle']): + client.bake(self.baker, BAKE_ARGS) + + client.bake(self.baker, BAKE_ARGS) # first block in new cycle + + # NB: In the first level of the next cycle, the baker can + # reveal all the nonces! Revealing the nonces before baking + # this block would not help, as we are not yet in the new + # cycle; this is way revelations appear in the second block of + # a cycle... + + def test_reveal_nonces(self, sandbox): + client = sandbox.client(0) + + # method 1, use the client + # client.run(['reveal', 'nonces']) + + # method 2, use the baker daemon + cycle = client.get_cycle() + print(client.get_nonces_status(cycle - 1)) + + sandbox.add_baker(0, self.baker, proto=constants.ALPHA_DAEMON) + # wait a few seconds for the daemon to bake a block + # NB: Waiting for 5 seconds was not enough... + time.sleep(10) + + # the first baked block should include all nonces + nonces_status = client.get_nonces_status(cycle - 1) + assert nonces_status["unrevealed"] == [] diff --git a/tests_python/tests/multibranch/test_deterministic_nonces.py b/tests_python/tests/multibranch/test_deterministic_nonces.py new file mode 100644 index 0000000000000000000000000000000000000000..f3666189be1ccfc12e191b8278dba8d861ef7f12 --- /dev/null +++ b/tests_python/tests/multibranch/test_deterministic_nonces.py @@ -0,0 +1,174 @@ +r"""There are two methods used by the baker to generate nonces for seed +generation: +- the "old method" generates a random number and stores it in the + client 'nonces' file +- the "new method" asks the signer to generate it and the nonce is not + stored, the baker just asks again the signer again for the nonce + +This test checks that the update from the old method to the new method +works properly. + +We run 5 nodes and 5 bakers: +- 3 bakers use the old method +- one baker uses the new method +- one baker starts using the old method, and it is restarted using the + new method + +Note that the nodes and clients in the two methods are the same, only +the baker changes. + +Note: the test relies on `TEZOS_BINARIES`. The test will be be skipped +if the environment variable isn't defined. +""" + +import time +import pytest +from tools import utils, constants + +OLDM_BRANCH = "master" +NEWM_BRANCH = "eugenz@baker_with_deterministic_nonces" +# TODO: when the branch is merged, to be replaced with commit hashes + +# If the TEZOS_BINARIES is set to, say 'bin/', then the directories +# bin/master and bin/eugenz@baker_with_deterministic_nonces need to +# exist and to contain the relevant binaries + +PROTO_HASH = "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK" +PROTO = "alpha" + +TBB = 2 # time_between_blocks + +# the number of nodes equals the number of bakers +NUM_NODES = len(constants.PARAMETERS["bootstrap_accounts"]) +IDX_NEWM = NUM_NODES - 1 # the index of the baker that runs the new method +IDX_BOTH = NUM_NODES - 2 # the index of the baker that changes method + +MAP = {i: OLDM_BRANCH if i < IDX_NEWM else NEWM_BRANCH + for i in range(NUM_NODES)} + + +def get_baker_index(identity): + for i in range(NUM_NODES): + if identity == constants.IDENTITIES[f'bootstrap{i+1}']['identity']: + return i + return NUM_NODES # or fail? + + +def nonce_for_block(nonces, block_hash): + for entry in nonces: + if entry['block'] == block_hash: + return True + return False + + +@pytest.mark.parametrize('sandbox_multibranch', [MAP], indirect=True) +@pytest.mark.baker +@pytest.mark.multinode +@pytest.mark.slow +@pytest.mark.multibranch +@pytest.mark.incremental +class TestUpgradeToDeterministicNonce: + parameters = dict(constants.PARAMETERS) + + def test_setup_network(self, sandbox_multibranch): + params = ['--connections', f'{NUM_NODES}', '--history-mode', 'archive'] + for i in range(NUM_NODES): + sandbox_multibranch.add_node(i, params=params) + + self.parameters["time_between_blocks"] = [str(TBB), "0"] + self.parameters["blocks_per_cycle"] = 8 + self.parameters["preserved_cycles"] = 2 + self.parameters["blocks_per_roll_snapshot"] = 4 + self.parameters["blocks_per_commitment"] = 4 + + sandbox_multibranch.client(0).activate_protocol_json(PROTO_HASH, + self.parameters) + + for i in range(IDX_NEWM): + sandbox_multibranch.add_baker(i, f'bootstrap{i+1}', proto=PROTO) + sandbox_multibranch.add_baker(IDX_NEWM, f'bootstrap{IDX_NEWM+1}', + proto=PROTO) + + def test_wait_for_alpha(self, sandbox_multibranch): + clients = sandbox_multibranch.all_clients() + for client in clients: + assert utils.check_protocol(client, PROTO_HASH) + + def test_nonces_created(self, sandbox_multibranch): + # wait for some cycles: + time.sleep(2 * self.parameters["blocks_per_cycle"] * TBB) + + clients = sandbox_multibranch.all_clients() + + # we assume enough time has passed that each baker baked a + # block with a commitment + + # we check that there are nonces in the nonce file for bakers + # using the old method and that there are no nonces in the + # nonce file for the baker using the new method + all_nonces = [] + print() + for i in range(NUM_NODES): + nonces = clients[i].get_nonces_from_file() + print("The nonces of ", f'bootstrap{i+1}', ": ", nonces) + all_nonces.append(nonces) + if i != IDX_NEWM: + assert all_nonces[i] # not empty + else: + # the baker using the new method + assert not all_nonces[i] # empty + + # we also check that each block at a commitment level has an + # entry in the nonce file of the baker that created the block + crt_level = clients[0].get_level() + for level in range(4, crt_level, + int(self.parameters["blocks_per_commitment"])): + baker = clients[0].get_baker(level) + baker_index = get_baker_index(baker) + print("for level", level, "the baker is", baker, "that is,", + f'bootstrap{baker_index+1}') + if baker_index != IDX_NEWM: + # we should find the block hash in the nonces file + block_hash = clients[0].get_hash(level) + assert nonce_for_block(all_nonces[baker_index], block_hash) + + def test_restart_baker(self, sandbox_multibranch): + sandbox_multibranch.rm_baker(IDX_BOTH, proto=PROTO) + sandbox_multibranch.add_baker(IDX_BOTH, f'bootstrap{IDX_BOTH+1}', + proto=PROTO, map_id=IDX_NEWM) + + def test_nonces_deleted(self, sandbox_multibranch): + # wait for enough cycles + cycles_to_wait = int(self.parameters["preserved_cycles"]) + time.sleep((2 + cycles_to_wait) * + self.parameters["blocks_per_cycle"] * TBB) + + clients = sandbox_multibranch.all_clients() + + all_nonces = [] + print() + for i in range(NUM_NODES): + nonces = clients[i].get_nonces_from_file() + print("The nonces of ", f'bootstrap{i+1}', ": ", nonces) + all_nonces.append(nonces) + if i < IDX_BOTH: + assert all_nonces[i] # not empty + else: + assert not all_nonces[i] # empty + + crt_level = clients[0].get_level() + crt_cycle = crt_level // int(self.parameters["blocks_per_cycle"]) + min_level = (int(self.parameters["blocks_per_cycle"]) * + (crt_cycle - cycles_to_wait)) + print("Current level is", crt_level, "(cycle", crt_cycle, + "). Oldest level with stored nonces is", min_level) + for level in range(min_level, crt_level, + int(self.parameters["blocks_per_commitment"])): + baker = clients[0].get_baker(level) + baker_index = get_baker_index(baker) + print("for level", level, "the baker is", baker, + "that is, ", f'bootstrap{baker_index+1}') + if baker_index < IDX_BOTH: + # we should find the block hash in the nonces file + block_hash = clients[0].get_hash(level) + assert nonce_for_block(all_nonces[baker_index], block_hash)