diff --git a/manifest/product_octez.ml b/manifest/product_octez.ml index 2a0735b604d5cad768dda1105b9368125eb4b975..007ab3034095027c868477c10d5adc0430343f41 100644 --- a/manifest/product_octez.ml +++ b/manifest/product_octez.ml @@ -3370,6 +3370,13 @@ let octez_protocol_compiler_registerer = [octez_base |> open_ ~m:"TzPervasives"; tezos_protocol_environment_sigs] ~flags:(Flags.standard ~opaque:true ()) +let octez_protocol_compiler_alternative_hashes = + public_lib + "octez-protocol-compiler.alternative-hashes" + ~path:"src/lib_protocol_compiler/hashes" + ~internal_name:"octez_protocol_alternative_hashes" + ~deps:[octez_base |> open_ ~m:"TzPervasives"] + let octez_protocol_compiler_compat = public_lib "octez-protocol-compiler-compat" @@ -3434,6 +3441,7 @@ let octez_protocol_compiler_lib = tezos_protocol_environment_sigs; octez_stdlib_unix |> open_; octez_protocol_compiler_compat |> open_; + octez_protocol_compiler_alternative_hashes |> open_; lwt_unix; ocplib_ocamlres; unix; @@ -3547,6 +3555,7 @@ let octez_protocol_updater = octez_micheline |> open_; octez_shell_services |> open_; octez_protocol_environment; + octez_protocol_compiler_alternative_hashes; octez_protocol_compiler_registerer; octez_protocol_compiler_native; octez_context_ops |> open_; @@ -3795,6 +3804,7 @@ let octez_shell = octez_stdlib_unix |> open_; octez_shell_services |> open_; octez_p2p_services |> open_; + octez_protocol_compiler_alternative_hashes; octez_protocol_updater |> open_; octez_requester |> open_; octez_workers |> open_; diff --git a/src/lib_protocol_compiler/compiler.ml b/src/lib_protocol_compiler/compiler.ml index d1443433c91a051dbb6b0e647a74b4d9d5511a05..10f5dbb4c0957b4ebcf6f055bf15b7b14a37fe40 100644 --- a/src/lib_protocol_compiler/compiler.ml +++ b/src/lib_protocol_compiler/compiler.ml @@ -137,6 +137,18 @@ type driver = { let parse_options errflag s = Option.iter Location.(prerr_alert none) (Warnings.parse_options errflag s) +let pp_protocol_hash_result ppf (computed_hash, stored_hash_opt) = + match stored_hash_opt with + | Some stored_hash when not (Protocol_hash.equal stored_hash computed_hash) -> + Format.fprintf + ppf + "%a as %a" + Protocol_hash.pp + stored_hash + Protocol_hash.pp + computed_hash + | None | Some _ -> Protocol_hash.pp ppf computed_hash + let main {compile_ml; pack_objects; link_shared} version = Random.self_init () ; parse_options false default_warnings ; @@ -217,7 +229,10 @@ let main {compile_ml; pack_objects; link_shared} version = | None -> computed_hash | Some stored_hash when !check_protocol_hash - && not (Protocol_hash.equal computed_hash stored_hash) -> + && not + (Protocol_hash_representative.equivalent + computed_hash + stored_hash) -> Format.eprintf "Inconsistent hash for protocol in TEZOS_PROTOCOL.@\n\ Computed hash: %a@\n\ @@ -307,4 +322,7 @@ let main {compile_ml; pack_objects; link_shared} version = Format.printf "let src_digest = %S ;;\n" (Digest.to_hex dsrc) ; Format.printf "let impl_digest = %S ;;\n" (Digest.to_hex dimpl) ; Format.printf "let intf_digest = %S ;;\n" (Digest.to_hex dintf)) ; - Format.printf "Success: %a.@." Protocol_hash.pp hash + Format.printf + "Success: %a.@." + pp_protocol_hash_result + (computed_hash, stored_hash_opt) diff --git a/src/lib_protocol_compiler/dune b/src/lib_protocol_compiler/dune index 7b2a58224a312e29336df6334cfd2363f48cb70d..13189adf32355eed1f9e307a669b7a6ba87d4941 100644 --- a/src/lib_protocol_compiler/dune +++ b/src/lib_protocol_compiler/dune @@ -11,6 +11,7 @@ octez-proto-libs.protocol-environment.sigs octez-libs.stdlib-unix octez-protocol-compiler-compat + octez-protocol-compiler.alternative-hashes lwt.unix ocplib-ocamlres unix) @@ -19,7 +20,8 @@ -open Tezos_base.TzPervasives -open Tezos_base_unix -open Tezos_stdlib_unix - -open Octez_protocol_compiler_compat) + -open Octez_protocol_compiler_compat + -open Octez_protocol_alternative_hashes) (modules Embedded_cmis_env Embedded_cmis_register Packer Compiler Defaults)) (rule diff --git a/src/lib_protocol_compiler/hashes/dune b/src/lib_protocol_compiler/hashes/dune new file mode 100644 index 0000000000000000000000000000000000000000..29da01b7174512c34136f21278f9febbacff484b --- /dev/null +++ b/src/lib_protocol_compiler/hashes/dune @@ -0,0 +1,12 @@ +; This file was automatically generated, do not edit. +; Edit file manifest/main.ml instead. + +(library + (name octez_protocol_alternative_hashes) + (public_name octez-protocol-compiler.alternative-hashes) + (instrumentation (backend bisect_ppx)) + (libraries + octez-libs.base) + (flags + (:standard) + -open Tezos_base.TzPervasives)) diff --git a/src/lib_protocol_compiler/hashes/protocol_hash_representative.ml b/src/lib_protocol_compiler/hashes/protocol_hash_representative.ml new file mode 100644 index 0000000000000000000000000000000000000000..e09093fd1260371a29401e3d7be7cfa1db199c2e --- /dev/null +++ b/src/lib_protocol_compiler/hashes/protocol_hash_representative.ml @@ -0,0 +1,32 @@ +(*****************************************************************************) +(* *) +(* SPDX-License-Identifier: MIT *) +(* Copyright (c) 2024 Nomadic Labs *) +(* *) +(*****************************************************************************) + +(** Maps protocol hashes to their representative. *) +module Representatives = Map.Make (Protocol_hash) + +let register_protocol_hash computed_hash declared_hash map = + Representatives.add + (Protocol_hash.of_b58check_exn computed_hash) + (Protocol_hash.of_b58check_exn declared_hash) + map + +let representatives = + Representatives.empty + (* Protocol used in tests only, see `tezt/test/injection.ml`. *) + |> register_protocol_hash + "Pry4stD6qN1ZagUX6YCHvMxA1xvSjARkdt8bhs86j74JGLoLDKN" + "Ps8MVx2JuQaFrXpbuwSeBvXmi1xraGmXuJZHULEbPBY7mzFchxz" + +(* Resolve is not transitive on purpose: there is no reason a protocol hash is + represented by another protocol hash that has itself a representative. *) +let resolve_repr hash = + Representatives.find_opt hash representatives |> Option.value ~default:hash + +let equivalent protocol_hash protocol_hash' = + let protocol_hash = resolve_repr protocol_hash in + let protocol_hash' = resolve_repr protocol_hash' in + Protocol_hash.equal protocol_hash protocol_hash' diff --git a/src/lib_protocol_updater/dune b/src/lib_protocol_updater/dune index ed4be641b5f560f6f6078ea60a805665caea67e0..1d331d59571d2c18bdd4ae5f55f84e573dd5fad1 100644 --- a/src/lib_protocol_updater/dune +++ b/src/lib_protocol_updater/dune @@ -11,6 +11,7 @@ octez-libs.micheline octez-shell-libs.shell-services octez-proto-libs.protocol-environment + octez-protocol-compiler.alternative-hashes octez-protocol-compiler.registerer octez-protocol-compiler.native octez-shell-libs.context-ops diff --git a/src/lib_shell/dune b/src/lib_shell/dune index 116e523762116c40338871aeec51be6b4d70fb0e..649976b8743a5f690e4366127155f68c4b9ac9ce 100644 --- a/src/lib_shell/dune +++ b/src/lib_shell/dune @@ -20,6 +20,7 @@ octez-libs.stdlib-unix octez-shell-libs.shell-services octez-libs.tezos-p2p-services + octez-protocol-compiler.alternative-hashes octez-shell-libs.protocol-updater octez-shell-libs.requester octez-libs.tezos-workers diff --git a/src/lib_shell/node.ml b/src/lib_shell/node.ml index dd68e35915eb9e166f98d798035fcefcd127408c..bf9cfe333207ba8debe4e8dc1c5f448edaabb38c 100644 --- a/src/lib_shell/node.ml +++ b/src/lib_shell/node.ml @@ -165,7 +165,14 @@ let store_known_protocols store = | None -> Node_event.(emit store_protocol_missing_files) protocol_hash | Some protocol -> ( let hash = Protocol.hash protocol in - if not (Protocol_hash.equal hash protocol_hash) then + if + not + (Octez_protocol_alternative_hashes + .Protocol_hash_representative + .equivalent + hash + protocol_hash) + then if List.mem ~equal:Protocol_hash.equal diff --git a/tezt/lib_tezos/protocol_compiler.ml b/tezt/lib_tezos/protocol_compiler.ml index f0901efd4f59e103cd51522c745def223cdd4ef1..0fee105bc50f0b17c2b7f43097f729ffa02e9992 100644 --- a/tezt/lib_tezos/protocol_compiler.ml +++ b/tezt/lib_tezos/protocol_compiler.ml @@ -40,7 +40,10 @@ let compile ?path ?hooks ?(hash_only = false) proto_dir = in if hash_only then return (String.trim output) else - match output =~* rex "Success: ([a-zA-Z0-9]+)" with - | Some hash -> return hash - | None -> + match + ( output =~* rex "Success: ([a-zA-Z0-9]+)", + output =~** rex "Success: ([a-zA-Z0-9]+) as ([a-zA-Z0-9]+)" ) + with + | Some hash, _ | _, Some (hash, _) -> return hash + | None, None -> Test.fail "Couldn't parse output from compiling %s: %s" proto_dir output diff --git a/tezt/tests/injection.ml b/tezt/tests/injection.ml index afc35a9dd317c92b5a7223f6a5349780de2d38d7..adbe7344d63f716d9dd1cecee0613b24b79c5f58 100644 --- a/tezt/tests/injection.ml +++ b/tezt/tests/injection.ml @@ -207,6 +207,76 @@ let test_activation () : unit = unit +let test_alternative_protocol_hash = + Test.register + ~__FILE__ + ~title:"Protocol is compiled with another expected hash" + ~tags:[team; "protocol"; "compilation"; "hash"] + ~uses:[Constant.octez_protocol_compiler] + @@ fun () -> + (* First, prepare a new protocol. *) + let protocol_desc_file = "TEZOS_PROTOCOL" in + let protocol_desc = + `O [("modules", `A [`String "Main"]); ("expected_env_version", `Float 13.)] + |> JSON.annotate ~origin:"" + in + let protocol_main_file = "main.ml" in + let protocol_main = "(* This is a dummy protocol *)" in + + (* And write it in a temporary folder. *) + let protocol = Tezt.Temp.dir "protocol" in + JSON.encode_to_file + (Filename.concat protocol protocol_desc_file) + protocol_desc ; + Base.write_file + (Filename.concat protocol protocol_main_file) + ~contents:protocol_main ; + + (* Let's hash the protocol to fill the `TEZOS_PROTOCOL` file. *) + let* protocol_hash = Protocol_compiler.compile ~hash_only:true protocol in + let protocol_desc_patched = + JSON.( + put ("hash", annotate ~origin:"" (`String protocol_hash)) protocol_desc) + in + JSON.encode_to_file + (Filename.concat protocol protocol_desc_file) + protocol_desc_patched ; + + (* Now we can start patching the files so that we produce different hashes. *) + (* This one should be rejected by the protocol compiler, as the hash it will + produce is not registered. *) + let patched_protocol_main = + protocol_main ^ "(* This hash will be rejected. *)" + in + Base.write_file + (Filename.concat protocol protocol_main_file) + ~contents:patched_protocol_main ; + + (* Then compile it and see it fail as the hash is inconsistent. *) + let failing_compilation_process = Protocol_compiler.spawn_compile protocol in + let* () = Process.check ~expect_failure:true failing_compilation_process in + + (* This time, let's patch it so that it produces a hash we are aware of: + `Pry4stD6qN1ZagUX6YCHvMxA1xvSjARkdt8bhs86j74JGLoLDKN`. See + `src/lib_protocol_compiler/hashes/protocol_hash_representatives.ml`. *) + let patched_protocol_main = + protocol_main + ^ "(* This is a harmless comment that will change the protocol hash. *)" + in + Base.write_file + (Filename.concat protocol protocol_main_file) + ~contents:patched_protocol_main ; + + (* This time, the compilation will succeed and the resulting hash will be the + one declared in `TEZOS_PROTOCOL`. *) + let* hash = Protocol_compiler.compile protocol in + Check.( + (hash = protocol_hash) + string + ~error_msg:"Protocol hash expected was %R, but got %L") ; + unit + let register_protocol_independent () = test_injection_and_activation () ; - test_activation () + test_activation () ; + test_alternative_protocol_hash