From de8149102a72915908acc0ed0cbd7b03e55001cb Mon Sep 17 00:00:00 2001 From: Alain Mebsout Date: Mon, 2 Jun 2025 15:22:01 +0200 Subject: [PATCH 1/2] Include commitment in proofs --- server/lib/directory.ml | 70 +++++++++++++---- server/lib/rollup_node_client.ml | 124 +++++++++++++++++++++++++++++- server/lib/rollup_node_client.mli | 39 +++++++++- server/lib/syntax.ml | 2 + server/lib/types.ml | 16 ++++ 5 files changed, 232 insertions(+), 19 deletions(-) diff --git a/server/lib/directory.ml b/server/lib/directory.ml index 8c88c6e..1800dbc 100644 --- a/server/lib/directory.ml +++ b/server/lib/directory.ml @@ -19,6 +19,7 @@ let handle_errors = function EzAPIServer.return_error 502 ~content | Error (`Invalid_input _ as content) -> EzAPIServer.return_error 400 ~content | Error (`Invalid_proof as content) -> EzAPIServer.return_error 422 ~content + | Error (`Unnotarized as content) -> EzAPIServer.return_error 404 ~content | Ok a -> EzAPIServer.return_ok a let register service handler = @@ -87,6 +88,15 @@ let err_422 = | _ -> None) ~deselect:(fun () -> Some `Invalid_proof) +let err_404 = + let open Json_encoding in + Err.make ~code:422 ~name:"unnotarized" + ~encoding:(obj1 (req "error" (constant "unnotarized"))) + ~select:(function + | Some `Unnotarized -> Some () + | _ -> None) + ~deselect:(fun () -> Some `Unnotarized) + let no_error_service = service ~errors:([] : no_error option Err.case list) let service ?(errors = [err_502]) ?section ?name ?descr ?meth ~output ?params @@ -156,6 +166,33 @@ module Encoding = struct with _ -> Lwt_result.fail (`Invalid_input "Invalid hash, must be a valid hexadecimal value") + + let commitment = + union + [ + case + (merge_objs + (obj1 (req "status" (constant "cemented"))) + commitment_with_hash) + (function + | `Cemented c -> Some ((), c) + | _ -> None) + (fun ((), c) -> `Cemented c); + case + (merge_objs + (obj1 (req "status" (constant "committed"))) + commitment_with_hash) + (function + | `Committed c -> Some ((), c) + | _ -> None) + (fun ((), c) -> `Committed c); + case + (obj1 (req "status" (constant "uncommitted"))) + (function + | `Uncommitted -> Some () + | _ -> None) + (fun () -> `Uncommitted); + ] end module Arg = struct @@ -287,42 +324,47 @@ module Notarization_status = struct end module Notarization_proof = struct - let service : - (string, string * Ezjsonm.value, _, Security.scheme) post_service0 = - post_service ~section ~name:"Notarization proof" ~errors:[err_502; err_400] + let service : (string, _, _, Security.scheme) post_service0 = + post_service ~section ~name:"Notarization proof" + ~errors:[err_502; err_400; err_404] ~descr:"Compute a notarization proof for a given hash" ~input:Encoding.hash ~input_example:Encoding.hash_example ~output: Json_encoding.( - obj2 (req "hash" Encoding.hash) (req "proof" any_ezjson_value)) - ~params:[Param.block] + obj3 (req "hash" Encoding.hash) + (req "commitment" Encoding.commitment) + (req "proof" any_ezjson_value)) Path.(root // "notarize" // "proof") - let handler state params _ hex_hash = + let handler state _param _ hex_hash = let*? hash = Encoding.parse_hex hex_hash in - let block = - Option.bind (Req.find_param Param.block params) Arg.block_id_of_string - in - let+? proof = Rollup_node_client.get_proof state.State.config ?block hash in - (hex_hash, proof) + let+? proof, commitment = + Rollup_node_client.get_proof_and_commitment state.State.config hash in + (hex_hash, commitment, proof) let () = register service handler end module Verify_notarization = struct let service : - (string * Ezjsonm.value, Ptime.t, _, Security.scheme) post_service0 = + ( (string * Ezjsonm.value) * unit, + Ptime.t, + _, + Security.scheme ) + post_service0 = post_service ~section ~name:"Verify notarization" ~errors:[err_502; err_400; err_422] ~descr:"Verify a notarization proof for a given hash" ~input: Json_encoding.( - obj2 (req "hash" Encoding.hash) (req "proof" any_ezjson_value)) + merge_objs + (obj2 (req "hash" Encoding.hash) (req "proof" any_ezjson_value)) + unit) ~output:Json_encoding.(obj1 (req "notarized" Encoding.timestamp)) ~params:[Param.block] Path.(root // "notarize" // "verify") - let handler state params _ (hash, proof) = + let handler state params _ ((hash, proof), ()) = let*? hash = Encoding.parse_hex hash in let block = Option.bind (Req.find_param Param.block params) Arg.block_id_of_string diff --git a/server/lib/rollup_node_client.ml b/server/lib/rollup_node_client.ml index eac49a6..0038fb8 100644 --- a/server/lib/rollup_node_client.ml +++ b/server/lib/rollup_node_client.ml @@ -13,7 +13,8 @@ let section = Doc.section "rollup-node" type block_id = [ `Head | `Hash of string - | `Level of int ] + | `Level of int + | `Cemented ] type msg_id = Msg_id of string @@ -26,6 +27,18 @@ type hash_ts = { type hashes_ts = hash_ts list +type commitment = { + compressed_state : State_hash.t; + inbox_level : int32; + predecessor : Commitment_hash.t; + number_of_ticks : int64; +} + +type commitment_with_hash = { + commitment : commitment; + hash : Commitment_hash.t; +} + module Encoding = struct open Json_encoding @@ -61,6 +74,8 @@ module Encoding = struct let msg_id = conv (fun (Msg_id s) -> s) (fun s -> Msg_id s) string + let int64 = conv Int64.to_string Int64.of_string string + let hash_ts = conv (fun { hash; timestamp } -> (hash, timestamp)) @@ -70,6 +85,26 @@ module Encoding = struct let hashes_ts = conv Fun.id Fun.id (list hash_ts) let unit = conv Fun.id Fun.id unit + + let commitment = + conv + (fun { compressed_state; inbox_level; predecessor; number_of_ticks } -> + (compressed_state, inbox_level, predecessor, number_of_ticks)) + (fun (compressed_state, inbox_level, predecessor, number_of_ticks) -> + { compressed_state; inbox_level; predecessor; number_of_ticks }) + @@ obj4 + (req "compressed_state" State_hash.base58_encoding) + (req "inbox_level" int32) + (req "predecessor" Commitment_hash.base58_encoding) + (req "number_of_ticks" int64) + + let commitment_with_hash = + conv + (fun { commitment; hash } -> (commitment, hash)) + (fun (commitment, hash) -> { commitment; hash }) + @@ obj2 + (req "commitment" commitment) + (req "hash" Commitment_hash.base58_encoding) end module Arg = struct @@ -81,6 +116,7 @@ module Arg = struct let block_id_of_string = function | "head" -> Some `Head + | "cemented" -> Some `Cemented | s -> ( match int_of_string_opt s with | Some l -> Some (`Level l) @@ -93,6 +129,7 @@ module Arg = struct | `Head -> "head" | `Hash h -> h | `Level l -> string_of_int l + | `Cemented -> "cemented" let block_id = Arg.make ~name:"block_id" ~example:`Head @@ -138,6 +175,32 @@ module Services = struct root // "global" // "block" /: Arg.block_id // "durable" // "wasm_2_0_0" // "value") + let _block_hash : + (Req.t * block_id, unit, string, unit, Security.scheme) service = + service ~section ~name:"block_hash" ~output:Json_encoding.string + ~register:false + Path.(root // "global" // "block" /: Arg.block_id // "hash") + + let block_commitment : + ( Req.t * block_id, + unit, + commitment_with_hash, + unit, + Security.scheme ) + service = + let output = + Json_encoding.conv + (fun { commitment; hash } -> ((commitment, hash), ())) + (fun ((commitment, hash), ()) -> { commitment; hash }) + Json_encoding.( + merge_objs + (obj2 + (req "commitment" Encoding.commitment) + (req "commitment_hash" Commitment_hash.base58_encoding)) + unit) in + service ~section ~name:"block_hash" ~output ~register:false + Path.(root // "global" // "block" /: Arg.block_id) + let _produce_proof : (Req.t * block_id, unit, Ezjsonm.value, unit, Security.scheme) service = service ~section ~name:"produce_proof_value" ~params:[Param.key] @@ -168,6 +231,27 @@ module Services = struct Path.( root // "global" // "block" /: Arg.block_id // "durable" // "wasm_2_0_0" // "verify_proof") + + let _lcc : (Req.t, unit, commitment_with_hash, unit, Security.scheme) service + = + service ~section ~name:"retrieve LCC" ~output:Encoding.commitment_with_hash + ~register:false + Path.(root // "global" // "last_cemented_commitment") + + let lpc : + ( Req.t, + unit, + commitment_with_hash * (int32 option * unit), + unit, + Security.scheme ) + service = + service ~section ~name:"retrieve LPC" + ~output: + Json_encoding.( + merge_objs Encoding.commitment_with_hash + (merge_objs (obj1 (opt "first_published_at_level" int32)) unit)) + ~register:false + Path.(root // "local" // "last_published_commitment") end let map_req_error service res = @@ -237,6 +321,13 @@ let get_durable_storage_value config ?(block = `Head) key = call_get_service1 config Services.durable_storage_value block ~params:[(Param.key, S key)] +(* let get_lcc config = call_get_service0 config Services.lcc *) + +let get_block_commitment config block = + call_get_service1 config Services.block_commitment block + +let get_lpc config = call_get_service0 config Services.lpc + let get_proof config ?(block = `Head) key = call_get_service1 config Services.produce_proof block ~params:[(Param.key, S key)] @@ -311,9 +402,40 @@ let notarization_status config ?block hash = | None -> Lwt.return_ok None | Some timestamp -> Lwt.return_ok (Some (Notarized { timestamp }))) +let get_proof_and_commitment config hash = + let (Base58 hash_b58) = Document_hash.(to_base58 (V hash)) in + let key = String.concat "/" ["/pandora/state/hashes"; hash_b58; "timestamp"] in + let* proof_cement = get_proof config ~block:`Cemented key + and* ts_cem = get_durable_storage_value config ~block:`Cemented key in + match (proof_cement, ts_cem) with + | Ok proof, Ok (Some _) -> + let*? commitment = get_block_commitment config `Cemented in + Lwt.return_ok (proof, `Cemented commitment) + | _ -> ( + let*? lpc, (_published_at, ()) = get_lpc config in + let block = `Level (Int32.to_int lpc.commitment.inbox_level) in + let* proof_commit = get_proof config ~block key + and* ts_com = get_durable_storage_value config ~block key in + match (proof_commit, ts_com) with + | Ok proof, Ok (Some _) -> Lwt.return_ok (proof, `Committed lpc) + | _ -> + let*? proof = get_proof config ~block:`Head key in + let*? timestamp_bytes = + get_durable_storage_value config ~block:`Head key in + let*? () = + match timestamp_bytes with + | None -> Lwt.return_error `Unnotarized + | Some _b -> Lwt.return_ok () in + Lwt.return_ok (proof, `Uncommitted)) + let get_proof config ?block hash = let (Base58 hash_b58) = Document_hash.(to_base58 (V hash)) in let key = String.concat "/" ["/pandora/state/hashes"; hash_b58; "timestamp"] in + let*? timestamp_bytes = get_durable_storage_value config ?block key in + let*? () = + match timestamp_bytes with + | None -> Lwt.return_error `Unnotarized + | Some _b -> Lwt.return_ok () in get_proof config ?block key let verify_proof config ?block hash proof = diff --git a/server/lib/rollup_node_client.mli b/server/lib/rollup_node_client.mli index 3028d25..830400f 100644 --- a/server/lib/rollup_node_client.mli +++ b/server/lib/rollup_node_client.mli @@ -10,7 +10,8 @@ open Types type block_id = [ `Hash of string | `Head - | `Level of int ] + | `Level of int + | `Cemented ] (** A message identifier for the batcher. *) type msg_id = Msg_id of string @@ -27,6 +28,18 @@ type hash_ts = { (** List of hash and associated timestamp *) type hashes_ts = hash_ts list +type commitment = { + compressed_state : State_hash.t; + inbox_level : int32; + predecessor : Commitment_hash.t; + number_of_ticks : int64; +} + +type commitment_with_hash = { + commitment : commitment; + hash : Commitment_hash.t; +} + module Encoding : sig val timestamp : Ptime.t Json_encoding.encoding @@ -38,11 +51,17 @@ module Encoding : sig val msg_id : msg_id Json_encoding.encoding + val int64 : int64 Json_encoding.encoding + val hash_ts : hash_ts Json_encoding.encoding - val hashes_ts : hashes_ts Json_encoding.encoding + val hashes_ts : hash_ts list Json_encoding.encoding val unit : unit Json_encoding.encoding + + val commitment : commitment Json_encoding.encoding + + val commitment_with_hash : commitment_with_hash Json_encoding.encoding end module Arg : sig @@ -132,8 +151,20 @@ val get_proof : ?block:block_id -> string -> ( Ezjsonm.value, - [> `Bad_gateway_to_rollup_node of string * Ezjsonm.value option * string] - ) + [> `Bad_gateway_to_rollup_node of string * Ezjsonm.value option * string + | `Unnotarized ] ) + result + Lwt.t + +val get_proof_and_commitment : + Configuration.t -> + string -> + ( Ezjsonm.value + * [> `Cemented of commitment_with_hash + | `Committed of commitment_with_hash + | `Uncommitted ], + [> `Bad_gateway_to_rollup_node of string * Ezjsonm.value option * string + | `Unnotarized ] ) result Lwt.t diff --git a/server/lib/syntax.ml b/server/lib/syntax.ml index 9037ec8..0f0c31d 100644 --- a/server/lib/syntax.ml +++ b/server/lib/syntax.ml @@ -11,3 +11,5 @@ let ( let+ ) f x = Lwt.map x f let ( let*? ) x f = Lwt_result.bind x f let ( let+? ) f x = Lwt_result.map x f + +let ( and* ) = Lwt.both diff --git a/server/lib/types.ml b/server/lib/types.ml index 50a7a4f..4fdb0fa 100644 --- a/server/lib/types.ml +++ b/server/lib/types.ml @@ -62,6 +62,22 @@ module Smart_rollup = Make (struct let check = true end) +module Commitment_hash = Make (struct + let name = "commitment_hash" + + let prefix = Prefixes.smart_rollup_commitment + + let check = true +end) + +module State_hash = Make (struct + let name = "state_hash" + + let prefix = Prefixes.smart_rollup_state + + let check = true +end) + module Document_hash = Make (struct let name = "document_hash" -- GitLab From c6807d943fd1b62fedecf68650d2610bd1e8281d Mon Sep 17 00:00:00 2001 From: Alain Mebsout Date: Mon, 2 Jun 2025 16:16:16 +0200 Subject: [PATCH 2/2] Check commitments in proofs --- server/lib/base58.ml | 8 ++++ server/lib/directory.ml | 65 +++++++++++++++++++++++++------ server/lib/rollup_node_client.ml | 6 +-- server/lib/rollup_node_client.mli | 2 +- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/server/lib/base58.ml b/server/lib/base58.ml index 09d8bb2..3160b78 100644 --- a/server/lib/base58.ml +++ b/server/lib/base58.ml @@ -55,6 +55,10 @@ module Prefixes = struct let generic_signature = ("\004\130\043", 64 (* sig(96) *)) end +module Blake = Digestif.Make_BLAKE2B (struct + let digest_size = 32 +end) + module type S = sig type t = V of string @@ -69,6 +73,8 @@ module type S = sig val base58_encoding : t Json_encoding.encoding val hex_encoding : t Json_encoding.encoding + + val hash_bytes : bytes -> t end let base58_generic_encoding = @@ -142,4 +148,6 @@ end) : S = struct (fun (V v) -> Hex.of_string v |> Hex.show) (fun h -> V (Hex.to_string (`Hex h))) string + + let hash_bytes b = V (Blake.digest_bytes b |> Blake.to_raw_string) end diff --git a/server/lib/directory.ml b/server/lib/directory.ml index 1800dbc..730bea1 100644 --- a/server/lib/directory.ml +++ b/server/lib/directory.ml @@ -18,7 +18,7 @@ let handle_errors = function | Error (`Bad_gateway_to_rollup_node _ as content) -> EzAPIServer.return_error 502 ~content | Error (`Invalid_input _ as content) -> EzAPIServer.return_error 400 ~content - | Error (`Invalid_proof as content) -> EzAPIServer.return_error 422 ~content + | Error (`Invalid_proof _ as content) -> EzAPIServer.return_error 422 ~content | Error (`Unnotarized as content) -> EzAPIServer.return_error 404 ~content | Ok a -> EzAPIServer.return_ok a @@ -82,11 +82,12 @@ let err_400 = let err_422 = let open Json_encoding in Err.make ~code:422 ~name:"invalid_proof" - ~encoding:(obj1 (req "error" (constant "invalid_proof"))) + ~encoding: + (obj2 (req "error" (constant "invalid_proof")) (req "message" string)) ~select:(function - | Some `Invalid_proof -> Some () + | Some (`Invalid_proof s) -> Some ((), s) | _ -> None) - ~deselect:(fun () -> Some `Invalid_proof) + ~deselect:(fun ((), s) -> Some (`Invalid_proof s)) let err_404 = let open Json_encoding in @@ -346,29 +347,69 @@ module Notarization_proof = struct end module Verify_notarization = struct - let service : - ( (string * Ezjsonm.value) * unit, - Ptime.t, - _, - Security.scheme ) - post_service0 = + let service : (_, Ptime.t, _, Security.scheme) post_service0 = post_service ~section ~name:"Verify notarization" ~errors:[err_502; err_400; err_422] ~descr:"Verify a notarization proof for a given hash" ~input: Json_encoding.( merge_objs - (obj2 (req "hash" Encoding.hash) (req "proof" any_ezjson_value)) + (obj3 (req "hash" Encoding.hash) + (req "proof" any_ezjson_value) + (opt "commitment" + (merge_objs Encoding.commitment_with_hash unit))) unit) ~output:Json_encoding.(obj1 (req "notarized" Encoding.timestamp)) ~params:[Param.block] Path.(root // "notarize" // "verify") - let handler state params _ ((hash, proof), ()) = + let state_hash_of_proof proof = + try + let root_n = + match Ezjsonm.find_opt proof ["after"; "node"] with + | Some v -> v + | None -> Ezjsonm.find proof ["before"; "node"] in + let root_hex = Ezjsonm.get_string root_n in + State_hash.V (Hex.to_string (`Hex root_hex)) + with _ -> failwith "Cannot extract state hash from proof" + + let hash_commitment + ({ compressed_state; inbox_level; predecessor; number_of_ticks } : + Rollup_node_client.commitment) = + let size = 32 + 4 + 32 + 8 in + let buf = Bytes.make size '\000' in + let (State_hash.V s) = compressed_state in + let (Commitment_hash.V p) = predecessor in + Bytes.blit_string s 0 buf 0 32 ; + EndianBytes.BigEndian.set_int32 buf 32 inbox_level ; + Bytes.blit_string p 0 buf 36 32 ; + EndianBytes.BigEndian.set_int64 buf 68 number_of_ticks ; + Commitment_hash.hash_bytes buf + + let handler state params _ ((hash, proof, commitment), ()) = let*? hash = Encoding.parse_hex hash in let block = Option.bind (Req.find_param Param.block params) Arg.block_id_of_string in + let*? () = + match commitment with + | None -> Lwt.return_ok () + | Some (Rollup_node_client.{ commitment; hash }, ()) -> + let (State_hash.V proof_state_hash) = state_hash_of_proof proof in + let (State_hash.V state_hash) = commitment.compressed_state in + let*? () = + if not @@ String.equal proof_state_hash state_hash then + Lwt.return_error (`Invalid_proof "Invalid state root hash") + else + Lwt.return_ok () in + let (Commitment_hash.V h2) = hash_commitment commitment in + let (Commitment_hash.V h1) = hash in + let*? () = + if not @@ String.equal h1 h2 then + Lwt.return_error (`Invalid_proof "Invalid commitment hash") + else + Lwt.return_ok () in + Lwt.return_ok () in Rollup_node_client.verify_proof state.State.config ?block hash proof let () = register service handler diff --git a/server/lib/rollup_node_client.ml b/server/lib/rollup_node_client.ml index 0038fb8..46035cc 100644 --- a/server/lib/rollup_node_client.ml +++ b/server/lib/rollup_node_client.ml @@ -444,14 +444,14 @@ let verify_proof config ?block hash proof = let* content = verify_proof config ?block key proof in match content with | Error (`Bad_gateway_to_rollup_node (_, _, "Internal Server Error")) -> - Lwt.return_error `Invalid_proof + Lwt.return_error (`Invalid_proof "incorrect proof") | Error e -> Lwt.return_error e - | Ok None -> Lwt.return_error `Invalid_proof + | Ok None -> Lwt.return_error (`Invalid_proof "incorrect hash") | Ok (Some b) -> ( let ts_int64 = EndianBytes.LittleEndian.get_int64 b 0 in let timestamp = Ptime.of_float_s (Int64.to_float ts_int64) in match timestamp with - | None -> failwith "Invalid timestamp in proof" + | None -> Lwt.return_error (`Invalid_proof "Invalid timestamp in proof") | Some timestamp -> Lwt.return_ok timestamp) (* This should probably be moved into a seperate file at some point. *) diff --git a/server/lib/rollup_node_client.mli b/server/lib/rollup_node_client.mli index 830400f..4ac0424 100644 --- a/server/lib/rollup_node_client.mli +++ b/server/lib/rollup_node_client.mli @@ -175,7 +175,7 @@ val verify_proof : Ezjsonm.value -> ( Ptime.t, [> `Bad_gateway_to_rollup_node of string * Ezjsonm.value option * string - | `Invalid_proof ] ) + | `Invalid_proof of string ] ) result Lwt.t -- GitLab