diff --git a/src/app/cli/src/init/client.ml b/src/app/cli/src/init/client.ml index e139910bc378..a2da71d99a59 100644 --- a/src/app/cli/src/init/client.ml +++ b/src/app/cli/src/init/client.ml @@ -2558,6 +2558,7 @@ let advanced ~itn_features = ; ("thread-graph", thread_graph) ; ("print-signature-kind", signature_kind) ; ("generate-hardfork-config", generate_hardfork_config) + ; ("fix-persistent-frontier", Fix_persistent_frontier.command) ; ( "test" , Command.group ~summary:"Testing-only commands" [ ("create-genesis", test_genesis_creation) diff --git a/src/app/cli/src/init/fix_persistent_frontier.ml b/src/app/cli/src/init/fix_persistent_frontier.ml new file mode 100644 index 000000000000..302f38f794ad --- /dev/null +++ b/src/app/cli/src/init/fix_persistent_frontier.ml @@ -0,0 +1,363 @@ +open Core +open Async +open Mina_base +open Frontier_base + +(* Build path from frontier root to persistent root by walking backwards *) +let rec build_path_to_root ~(frontier : Transition_frontier.t) ~current_hash + ~target_hash acc = + if State_hash.equal current_hash target_hash then + (* Reached the target (frontier root), return accumulated path *) + Ok acc + else + match Transition_frontier.find frontier current_hash with + | None -> + Error + (sprintf "Block %s not found in frontier" + (State_hash.to_base58_check current_hash) ) + | Some breadcrumb -> + let parent_hash = Breadcrumb.parent_hash breadcrumb in + build_path_to_root ~frontier ~current_hash:parent_hash ~target_hash + (breadcrumb :: acc) + +let check_directories_exist ~logger ~persistent_root_location + ~persistent_frontier_location = + let%bind root_exists = + Sys.file_exists persistent_root_location + >>| function `Yes -> true | `No | `Unknown -> false + in + let%bind frontier_exists = + Sys.file_exists persistent_frontier_location + >>| function `Yes -> true | `No | `Unknown -> false + in + if not root_exists then ( + [%log' error logger] "Persistent root directory not found at $location" + ~metadata:[ ("location", `String persistent_root_location) ] ; + Deferred.return (Error "Persistent root not found - nothing to fix against") + ) + else if not frontier_exists then ( + [%log' info logger] + "Persistent frontier directory not found - nothing to fix" ; + Deferred.return (Ok `No_frontier) ) + else Deferred.return (Ok `Both_exist) + +(* Apply a sequence of root transition diffs to the persistent database *) +let apply_root_transitions ~logger ~db diffs = + try + (* Get initial root hash *) + let initial_root_hash = + Transition_frontier.Persistent_frontier.Database.get_root_hash db + |> Result.map_error ~f:(fun err -> + Exn.create_s + (Sexp.of_string + ( "Failed to get root hash: " + ^ Transition_frontier.Persistent_frontier.Database.Error + .message err ) ) ) + |> Result.ok_exn + in + Transition_frontier.Persistent_frontier.Database.with_batch db + ~f:(fun batch -> + ( List.fold diffs ~init:initial_root_hash ~f:(fun old_root_hash diff -> + match diff with + | Diff.Lite.E.E + (Diff.Root_transitioned + { new_root; garbage = Lite garbage; _ } ) -> + let parent_hash = + Root_data.Limited.Stable.Latest.transition new_root + |> Mina_block.Validated.Stable.Latest.header + |> Mina_block.Header.protocol_state + |> Mina_state.Protocol_state.previous_state_hash + in + assert (State_hash.equal parent_hash old_root_hash) ; + Transition_frontier.Persistent_frontier.Database.move_root + ~old_root_hash ~new_root ~garbage batch ; + (* Return new root hash for next iteration *) + (Root_data.Limited.Stable.Latest.hashes new_root).state_hash + | _ -> + failwith "Expected Root_transitioned diff" ) + : State_hash.t ) + |> ignore ) ; + [%log' info logger] "Successfully applied $count diffs" + ~metadata:[ ("count", `Int (List.length diffs)) ] ; + Ok () + with exn -> + [%log' error logger] "Failed to apply root transitions: $error" + ~metadata:[ ("error", `String (Exn.to_string exn)) ] ; + Error ("Failed to apply root transitions: " ^ Exn.to_string exn) + +let fix_persistent_frontier_root_do ~logger ~config_directory + ~chain_state_locations ~max_frontier_depth runtime_config = + let signature_kind = Mina_signature_kind.t_DEPRECATED in + (* Get compile-time constants *) + let genesis_constants = Genesis_constants.Compiled.genesis_constants in + let constraint_constants = Genesis_constants.Compiled.constraint_constants in + let proof_level = Genesis_constants.Compiled.proof_level in + let%bind.Deferred.Result precomputed_values, _runtime_config_opt = + Genesis_ledger_helper.init_from_config_file ~genesis_constants + ~constraint_constants ~logger ~proof_level ~cli_proof_level:None + ~genesis_dir:chain_state_locations.Chain_state_locations.genesis + ~genesis_backing_type:Stable_db runtime_config + >>| Result.map_error ~f:Error.to_string_mach + in + (* Initialize Parallel as master before creating verifier *) + Parallel.init_master () ; + (* Create verifier - simplified without blockchain keys for now *) + let%bind ( `Blockchain blockchain_verification_key + , `Transaction transaction_verification_key ) = + Verifier.get_verification_keys_eagerly ~constraint_constants ~proof_level + ~signature_kind + in + let%bind verifier = + Verifier.create ~logger ~commit_id:"" ~blockchain_verification_key + ~transaction_verification_key ~signature_kind + ~proof_level:precomputed_values.proof_level + ~pids:(Child_processes.Termination.create_pid_table ()) + ~conf_dir:(Some config_directory) () + in + let tmp_root_location = chain_state_locations.root ^ "-tmp" in + let%bind.Deferred.Result () = + Mina_stdlib_unix.File_system.copy_dir chain_state_locations.root + tmp_root_location + >>| Result.map_error ~f:Exn.to_string + in + (* Set up persistent root and frontier *) + let persistent_root = + Persistent_root.create ~logger ~backing_type:Stable_db + ~directory:tmp_root_location + ~ledger_depth:precomputed_values.constraint_constants.ledger_depth + in + let persistent_frontier = + Persistent_frontier.create ~logger ~verifier + ~directory:chain_state_locations.frontier + ~time_controller:(Block_time.Controller.basic ~logger) + ~signature_kind + in + let proof_cache_db = Proof_cache_tag.create_identity_db () in + let%bind.Deferred.Result persistent_frontier_root_hash = + Persistent_frontier.with_instance_exn persistent_frontier + ~f:Persistent_frontier.Instance.get_root_hash + in + let%bind.Deferred.Result persistent_root_id = + Deferred.return + @@ + match Persistent_root.load_root_identifier persistent_root with + | Some id -> + Ok id + | None -> + Error "couldn't load persistent root hash" + in + let persistent_root_hash = persistent_root_id.state_hash in + Persistent_root.set_root_state_hash persistent_root + persistent_frontier_root_hash ; + (* Set up context module for frontier loading *) + let module Context = struct + let logger = logger + + let precomputed_values = precomputed_values + + let constraint_constants = precomputed_values.constraint_constants + + let consensus_constants = precomputed_values.consensus_constants + + let proof_cache_db = proof_cache_db + + let signature_kind = signature_kind + end in + let consensus_local_state = + Consensus.Data.Local_state.create + ~context:(module Context) + ~genesis_ledger:precomputed_values.genesis_ledger + ~genesis_epoch_data:precomputed_values.genesis_epoch_data + ~epoch_ledger_location:chain_state_locations.epoch_ledger + ~genesis_state_hash: + (State_hash.With_state_hashes.state_hash + precomputed_values.protocol_state_with_hashes ) + ~epoch_ledger_backing_type:Stable_db + Signature_lib.Public_key.Compressed.Set.empty + in + (* TODO loading of frontier is redundant unless fixing is needed *) + (* Load transition frontier using the standard API *) + let%bind frontier = + match%map + Transition_frontier.load + ~context:(module Context) + ~retry_with_fresh_db:false ~max_frontier_depth ~verifier + ~consensus_local_state ~persistent_root ~persistent_frontier + ~catchup_mode:`Super ~set_best_tip:false () + with + | Error err -> + let err_str = + match err with + | `Failure s -> + sprintf "Failure: %s" s + | `Bootstrap_required -> + "Bootstrap required" + | `Persistent_frontier_malformed -> + "Persistent frontier malformed" + | `Snarked_ledger_mismatch -> + "Snarked ledger mismatch" + in + [%log' error logger] "Failed to load transition frontier: $error" + ~metadata:[ ("error", `String err_str) ] ; + failwith (sprintf "Failed to load frontier: %s" err_str) + | Ok f -> + f + in + let frontier_root_hash = + Transition_frontier.root frontier |> Breadcrumb.state_hash + in + assert (State_hash.equal frontier_root_hash persistent_frontier_root_hash) ; + let clean_frontier () = + let%bind () = Transition_frontier.close ~loc:__LOC__ frontier in + Mina_stdlib_unix.File_system.remove_dir tmp_root_location + in + (* Check if persistent root is in the frontier *) + match + ( State_hash.equal frontier_root_hash persistent_root_hash + , Transition_frontier.find frontier persistent_root_hash ) + with + | true, _ -> + [%log info] + "Frontier root already matches persistent root. Nothing to do." ; + let%map () = clean_frontier () in + Ok () + | _, None -> + [%log error] + "Persistent root $persistent_root not found in frontier. Bootstrap \ + required." + ~metadata: + [ ("persistent_root", State_hash.to_yojson persistent_root_hash) + ; ("frontier_root", State_hash.to_yojson frontier_root_hash) + ] ; + let%map () = clean_frontier () in + Error "Persistent root not found in frontier. Bootstrap required." + | _, Some _persistent_root_breadcrumb -> + (* Build path from persistent root back to frontier root *) + let%bind.Deferred.Result path = + build_path_to_root ~frontier ~current_hash:persistent_root_hash + ~target_hash:frontier_root_hash [] + |> Deferred.return + in + [%log info] + "Built path from persistent root to frontier root: $length blocks" + ~metadata:[ ("length", `Int (List.length path)) ] ; + assert ( + State_hash.equal + (List.hd_exn path |> Breadcrumb.parent_hash) + frontier_root_hash ) ; + (* Generate root transition diffs for each step *) + let _, diffs = + let successors = Transition_frontier.successors frontier in + let init = + ( Transition_frontier.root frontier + , Transition_frontier.protocol_states_for_root_scan_state frontier ) + in + List.fold_map path ~init + ~f:(fun (parent, protocol_states_for_root_scan_state) breadcrumb -> + let root_transition = + Transition_frontier.Util.calculate_root_transition_diff + ~protocol_states_for_root_scan_state ~parent ~successors + breadcrumb + in + let res = + Diff.Full.E.to_lite (E (Root_transitioned root_transition)) + in + ( ( breadcrumb + , Transition_frontier.Util.to_protocol_states_map_exn + @@ Root_data.Limited.Stable.Latest.protocol_states + @@ root_transition.new_root ) + , res ) ) + in + [%log info] "Generated $count transition diffs" + ~metadata:[ ("count", `Int (List.length diffs)) ] ; + let%bind () = clean_frontier () in + (* Apply the diffs to persistent frontier database *) + let%map.Deferred.Result () = + Persistent_frontier.with_instance_exn persistent_frontier + ~f:(fun instance -> + apply_root_transitions ~logger ~db:instance.db diffs ) + in + [%log info] "Successfully moved frontier root to match persistent root" + +let fix_persistent_frontier_root ~config_directory ~config_file + ~max_frontier_depth = + Logger.Consumer_registry.register ~commit_id:"" ~id:Logger.Logger_id.mina + ~processor:Internal_tracing.For_logger.processor + ~transport: + (Internal_tracing.For_logger.json_lines_rotate_transport + ~directory:(config_directory ^ "/internal-tracing") + () ) + () ; + let logger = Logger.create ~id:Logger.Logger_id.mina () in + let log_processor = + Logger.Processor.pretty ~log_level:Logger.Level.Trace + ~config: + { Interpolator_lib.Interpolator.mode = After + ; max_interpolation_length = 50 + ; pretty_print = true + } + in + Logger.Consumer_registry.register ~commit_id:Mina_version.commit_id + ~id:Logger.Logger_id.mina ~processor:log_processor + ~transport:(Logger.Transport.stdout ()) + () ; + let%bind () = Internal_tracing.toggle ~commit_id:"" ~logger `Enabled in + (* Load the persistent root identifier *) + (* Load and initialize precomputed values from config *) + let%bind.Deferred.Result runtime_config_json = + Genesis_ledger_helper.load_config_json config_file + >>| Result.map_error ~f:Error.to_string_mach + in + let%bind.Deferred.Result runtime_config = + Deferred.return @@ Runtime_config.of_yojson runtime_config_json + in + let chain_state_locations = + Chain_state_locations.of_config ~conf_dir:config_directory runtime_config + in + (* Check if directories exist *) + match%bind.Deferred.Result + check_directories_exist ~logger + ~persistent_root_location:chain_state_locations.root + ~persistent_frontier_location:chain_state_locations.frontier + with + | `No_frontier -> + Deferred.Result.return () + | `Both_exist -> + fix_persistent_frontier_root_do ~logger ~config_directory + ~chain_state_locations ~max_frontier_depth runtime_config + +let command = + Command.async + ~summary: + "Fix persistent frontier root hash mismatch with persistent root by \ + applying proper root transitions" + (let open Command.Let_syntax in + let%map_open config_directory = Cli_lib.Flag.conf_dir + and config_file = + flag "--config-file" ~doc:"PATH path to a configuration file" + (required string) + and max_frontier_depth = + flag "--max-frontier-depth" + ~doc:"INT maximum frontier depth (default: 10)" (optional int) + in + Cli_lib.Exceptions.handle_nicely + @@ fun () -> + let open Deferred.Let_syntax in + let%bind conf_dir = + match config_directory with + | Some dir -> + Deferred.return dir + | None -> + let%map home = Sys.home_directory () in + home ^/ Cli_lib.Default.conf_dir_name + in + match%bind + fix_persistent_frontier_root ~config_directory:conf_dir ~config_file + ~max_frontier_depth:(Option.value max_frontier_depth ~default:10) + with + | Ok () -> + printf "Persistent frontier root fix completed successfully.\n" ; + Deferred.unit + | Error msg -> + eprintf "Failed to fix persistent frontier: %s\n" msg ; + exit 1) diff --git a/src/lib/mina_block/validated_block.ml b/src/lib/mina_block/validated_block.ml index bb7653208ef0..28bd56ff08a5 100644 --- a/src/lib/mina_block/validated_block.ml +++ b/src/lib/mina_block/validated_block.ml @@ -14,6 +14,8 @@ module Stable = struct let to_latest = ident let hashes (t, _) = With_hash.hash t + + let header (t, _) = With_hash.data t |> Block.Stable.V2.header end end] diff --git a/src/lib/mina_block/validated_block.mli b/src/lib/mina_block/validated_block.mli index 5110c0f33038..2e7665f606d2 100644 --- a/src/lib/mina_block/validated_block.mli +++ b/src/lib/mina_block/validated_block.mli @@ -7,7 +7,9 @@ module Stable : sig module V2 : sig type t [@@deriving equal] - val hashes : t -> State_hash.State_hashes.t + val hashes : t -> State_hash.State_hashes.Stable.V1.t + + val header : t -> Header.Stable.V2.t end end] diff --git a/src/lib/mina_stdlib_unix/file_system.ml b/src/lib/mina_stdlib_unix/file_system.ml index c28e320a4ac6..25c0212b7c90 100644 --- a/src/lib/mina_stdlib_unix/file_system.ml +++ b/src/lib/mina_stdlib_unix/file_system.ml @@ -15,6 +15,11 @@ let remove_dir dir = in Deferred.unit +let copy_dir src dst = + Monitor.try_with ~here:[%here] (fun () -> + Process.run_exn ~prog:"cp" ~args:[ "-r"; src; dst ] () ) + |> Deferred.Result.map ~f:ignore + let rec rmrf path = match Core.Sys.is_directory path with | `Yes -> diff --git a/src/lib/transition_frontier/frontier_base/dune b/src/lib/transition_frontier/frontier_base/dune index e8d6cb61c5d4..6616c79e702f 100644 --- a/src/lib/transition_frontier/frontier_base/dune +++ b/src/lib/transition_frontier/frontier_base/dune @@ -57,7 +57,8 @@ kimchi_pasta kimchi_pasta.basic mina_wire_types - internal_tracing) + internal_tracing + proof_cache_tag) (instrumentation (backend bisect_ppx)) (preprocess diff --git a/src/lib/transition_frontier/frontier_base/frontier_intf.ml b/src/lib/transition_frontier/frontier_base/frontier_intf.ml index 907a57b62c33..6af72a3f0fe9 100644 --- a/src/lib/transition_frontier/frontier_base/frontier_intf.ml +++ b/src/lib/transition_frontier/frontier_base/frontier_intf.ml @@ -1,4 +1,5 @@ open Mina_base +open Core_kernel (** This is the base signature for a full frontier, shared by any implementation * of a full frontier. Currently, this includes the internal [Full_frontier] @@ -43,4 +44,9 @@ module type S = sig val precomputed_values : t -> Precomputed_values.t val genesis_constants : t -> Genesis_constants.t + + module Protocol_states_for_root_scan_state : T + + val protocol_states_for_root_scan_state : + t -> Protocol_states_for_root_scan_state.t end diff --git a/src/lib/transition_frontier/frontier_base/root_data.ml b/src/lib/transition_frontier/frontier_base/root_data.ml index bf3e497a7cbc..7850bd3fc2aa 100644 --- a/src/lib/transition_frontier/frontier_base/root_data.ml +++ b/src/lib/transition_frontier/frontier_base/root_data.ml @@ -121,6 +121,13 @@ module Limited = struct let scan_state t = Common.scan_state t.common let pending_coinbase t = Common.pending_coinbase t.common + + let read_all_proofs_from_disk t = + { Stable.Latest.transition = + Mina_block.Validated.read_all_proofs_from_disk t.transition + ; protocol_states = t.protocol_states + ; common = Common.read_all_proofs_from_disk t.common + } end module Minimal = struct @@ -174,6 +181,17 @@ module Minimal = struct let pending_coinbase t = Common.pending_coinbase t.common + let write_all_proofs_to_disk ~proof_cache_db ~signature_kind + Stable.Latest.{ hash; common = { scan_state; pending_coinbase } } = + { hash + ; common = + { pending_coinbase + ; scan_state = + Staged_ledger.Scan_state.write_all_proofs_to_disk ~signature_kind + ~proof_cache_db scan_state + } + } + let read_all_proofs_from_disk { hash; common = { scan_state; pending_coinbase } } = { Stable.Latest.hash diff --git a/src/lib/transition_frontier/frontier_base/root_data.mli b/src/lib/transition_frontier/frontier_base/root_data.mli index aac914dd9c7d..a77c4e415983 100644 --- a/src/lib/transition_frontier/frontier_base/root_data.mli +++ b/src/lib/transition_frontier/frontier_base/root_data.mli @@ -62,6 +62,8 @@ module Limited : sig State_hash.With_state_hashes.Stable.V1.t list -> t + + val transition : t -> Mina_block.Validated.Stable.V2.t end end] @@ -87,6 +89,8 @@ module Limited : sig -> t val common : t -> Common.t + + val read_all_proofs_from_disk : t -> Stable.Latest.t end (* Minimal root data contains the smallest amount of information about a root. @@ -138,6 +142,12 @@ module Minimal : sig -> t val read_all_proofs_from_disk : t -> Stable.Latest.t + + val write_all_proofs_to_disk : + proof_cache_db:Proof_cache_tag.cache_db + -> signature_kind:Mina_signature_kind.t + -> Stable.Latest.t + -> t end type t = diff --git a/src/lib/transition_frontier/full_frontier/full_frontier.mli b/src/lib/transition_frontier/full_frontier/full_frontier.mli index 6dd2ec222da4..23d38e87e3f2 100644 --- a/src/lib/transition_frontier/full_frontier/full_frontier.mli +++ b/src/lib/transition_frontier/full_frontier/full_frontier.mli @@ -24,8 +24,6 @@ module type CONTEXT = sig val consensus_constants : Consensus.Constants.t end -include Frontier_intf.S - module Protocol_states_for_root_scan_state : sig type t = Protocol_state.value State_hash.With_state_hashes.t State_hash.Map.t @@ -36,6 +34,10 @@ module Protocol_states_for_root_scan_state : sig -> Protocol_state.value State_hash.With_state_hashes.t list end +include + Frontier_intf.S + with module Protocol_states_for_root_scan_state := Protocol_states_for_root_scan_state + val create : context:(module CONTEXT) -> root_data:Root_data.t @@ -54,9 +56,6 @@ val root_data : t -> Root_data.t val calculate_diffs : t -> Breadcrumb.t -> Diff.Full.E.t list -val protocol_states_for_root_scan_state : - t -> Protocol_states_for_root_scan_state.t - val apply_diffs : t -> Diff.Full.E.t list diff --git a/src/lib/transition_frontier/persistent_frontier/persistent_frontier.ml b/src/lib/transition_frontier/persistent_frontier/persistent_frontier.ml index cdc4501cfe2a..f71692001091 100644 --- a/src/lib/transition_frontier/persistent_frontier/persistent_frontier.ml +++ b/src/lib/transition_frontier/persistent_frontier/persistent_frontier.ml @@ -153,6 +153,9 @@ module Instance = struct let check_database t = Database.check t.db + let get_root_hash t = + Database.get_root_hash t.db |> Result.map_error ~f:Database.Error.message + let get_root_transition ~signature_kind ~proof_cache_db t = let open Result.Let_syntax in Database.get_root_hash t.db diff --git a/src/lib/transition_frontier/transition_frontier.ml b/src/lib/transition_frontier/transition_frontier.ml index 61e1866a8207..bf4c3b1262d0 100644 --- a/src/lib/transition_frontier/transition_frontier.ml +++ b/src/lib/transition_frontier/transition_frontier.ml @@ -9,13 +9,13 @@ open Mina_base module Ledger = Mina_ledger.Ledger module Root_ledger = Mina_ledger.Root include Frontier_base -module Full_frontier = Full_frontier module Extensions = Extensions module Persistent_root = Persistent_root module Persistent_frontier = Persistent_frontier module Catchup_state = Catchup_state module Full_catchup_tree = Full_catchup_tree module Catchup_hash_tree = Catchup_hash_tree +module Util = Full_frontier.Util module type CONTEXT = sig val logger : Logger.t @@ -564,6 +564,12 @@ include struct (* why can't this one be proxied? *) let path_map ?max_length { full_frontier; _ } breadcrumb ~f = path_map ?max_length full_frontier breadcrumb ~f + + module Protocol_states_for_root_scan_state = + Protocol_states_for_root_scan_state + + let protocol_states_for_root_scan_state = + proxy1 protocol_states_for_root_scan_state end module For_tests = struct diff --git a/src/lib/transition_frontier/transition_frontier.mli b/src/lib/transition_frontier/transition_frontier.mli index e5d3a165408c..7a818656797d 100644 --- a/src/lib/transition_frontier/transition_frontier.mli +++ b/src/lib/transition_frontier/transition_frontier.mli @@ -18,6 +18,7 @@ module Catchup_state = Catchup_state module Full_catchup_tree = Full_catchup_tree module Catchup_hash_tree = Catchup_hash_tree module Root_ledger = Mina_ledger.Root +module Util = Full_frontier.Util module type CONTEXT = sig val logger : Logger.t @@ -33,7 +34,10 @@ module type CONTEXT = sig val signature_kind : Mina_signature_kind.t end -include Frontier_intf.S +include + Frontier_intf.S + with module Protocol_states_for_root_scan_state := Full_frontier + .Protocol_states_for_root_scan_state type Structured_log_events.t += Added_breadcrumb_user_commands [@@deriving register_event]