diff --git a/src/proto_alpha/bin_tx_rollup_node/RPC.ml b/src/proto_alpha/bin_tx_rollup_node/RPC.ml index 3d0d85c702cfa26bc3207b929ded0205463df374..3a1df6bd89f79c944af12a259d04b9699e78ba42 100644 --- a/src/proto_alpha/bin_tx_rollup_node/RPC.ml +++ b/src/proto_alpha/bin_tx_rollup_node/RPC.ml @@ -34,6 +34,26 @@ type block_id = type context_id = [block_id | `Context of Tx_rollup_l2_context_hash.t] +let context_of_l2_block state b = + Stores.L2_block_store.context state.State.stores.blocks b + +let context_of_block_id state block_id = + let open Lwt_syntax in + match block_id with + | `L2_block b -> context_of_l2_block state b + | `Tezos_block b -> ( + let* b = State.get_tezos_l2_block_hash state b in + match b with None -> return_none | Some b -> context_of_l2_block state b) + | `Head -> return_some (State.get_head state).header.context + | `Level l -> ( + let* b = State.get_level state l in + match b with None -> return_none | Some b -> context_of_l2_block state b) + +let context_of_id state context_id = + match context_id with + | #block_id as block_id -> context_of_block_id state block_id + | `Context c -> Lwt.return_some c + module Arg = struct let indexable ~kind ~construct ~destruct = let construct i = @@ -182,6 +202,14 @@ module Block = struct | `Head -> return_some (State.get_head state) | `Level l -> State.get_level_l2_block state l + let proof = + RPC_service.get_service + ~description: + "Get the merkle proof for a given message for a given block inbox" + ~query:RPC_query.empty + ~output:Data_encoding.(option Protocol.Tx_rollup_l2_proof.encoding) + RPC_path.(path / "proof" / "message" /: RPC_arg.int) + let () = register block @@ fun (state, block_id) () () -> let*! block = block_of_id state block_id in @@ -206,6 +234,76 @@ module Block = struct return None | _ -> return (Some block.inbox)) + let () = + register proof @@ fun ((state, block_id), message_pos) () () -> + let*! block = block_of_id state block_id in + match block with + | None -> return_none + | Some block -> ( + match block_id with + | `Tezos_block b when Block_hash.(block.header.tezos_block <> b) -> + (* Tezos block has no l2 inbox *) + failwith "The tezos block (%a) has not l2 inbox" Block_hash.pp b + | _ -> + let open Inbox in + let inbox = block.inbox in + let index = state.context_index in + let* (prev_ctxt, message) = + if message_pos = 0 then + (* We must take the block predecessor context *) + let*? message = + match List.nth_opt inbox.contents message_pos with + | Some x -> ok x + | None -> + error + (Error.Tx_rollup_invalid_message_position_in_inbox + message_pos) + in + let pred_block_hash = block.header.predecessor in + let*! pred_block = State.get_block state pred_block_hash in + match pred_block with + | None -> + failwith + "The block (%a) does not have a predecessor" + L2block.Hash.pp + block.hash + | Some block -> ( + let hash = block.hash in + let*! ctxt_hash = context_of_l2_block state hash in + match ctxt_hash with + | Some ctxt_hash -> + let*! ctxt = Context.checkout_exn index ctxt_hash in + return (ctxt, message) + | None -> + failwith + "The block can not be retrieved from the hash %a" + L2block.Hash.pp + hash) + else + let*? (pred_message, message) = + match List.drop_n (message_pos - 1) inbox.contents with + | pred_message :: message :: _ -> ok (pred_message, message) + | _ -> + error + (Error.Tx_rollup_invalid_message_position_in_inbox + message_pos) + in + let ctxt_hash = pred_message.l2_context_hash.irmin_hash in + let*! ctxt = Context.checkout_exn index ctxt_hash in + return (ctxt, message) + in + let l2_parameters = + Protocol.Tx_rollup_l2_apply. + { + tx_rollup_max_withdrawals_per_batch = + state.l1_constants.tx_rollup_max_withdrawals_per_batch; + } + in + let* (proof, _) = + Prover_apply.apply_message prev_ctxt l2_parameters message.message + in + return_some proof) + let build_directory state = !directory |> RPC_directory.map (fun ((), block_id) -> Lwt.return (state, block_id)) @@ -404,30 +502,6 @@ module Context_RPC = struct | None -> return None | Some {public_key; _} -> return (Some public_key)) - let context_of_l2_block state b = - Stores.L2_block_store.context state.State.stores.blocks b - - let context_of_block_id state block_id = - let open Lwt_syntax in - match block_id with - | `L2_block b -> context_of_l2_block state b - | `Tezos_block b -> ( - let* b = State.get_tezos_l2_block_hash state b in - match b with - | None -> return_none - | Some b -> context_of_l2_block state b) - | `Head -> return_some (State.get_head state).header.context - | `Level l -> ( - let* b = State.get_level state l in - match b with - | None -> return_none - | Some b -> context_of_l2_block state b) - - let context_of_id state context_id = - match context_id with - | #block_id as block_id -> context_of_block_id state block_id - | `Context c -> Lwt.return_some c - let build_directory state = !directory |> RPC_directory.map (fun ((), context_id) -> diff --git a/src/proto_alpha/bin_tx_rollup_node/batcher.ml b/src/proto_alpha/bin_tx_rollup_node/batcher.ml index 16dfaba14a0a308f4523fe839cef5f1ccb455971..7e1b867c24be8229e647c86239eb10e22eb1727c 100644 --- a/src/proto_alpha/bin_tx_rollup_node/batcher.ml +++ b/src/proto_alpha/bin_tx_rollup_node/batcher.ml @@ -31,16 +31,13 @@ module Tx_queue = Hash_queue.Make (L2_transaction.Hash) (L2_transaction) type state = { cctxt : Protocol_client_context.full; rollup : Tx_rollup.t; - parameters : Protocol.Tx_rollup_l2_apply.parameters; signer : signer; transactions : Tx_queue.t; mutable incr_context : Context.t; lock : Lwt_mutex.t; + l1_constants : Protocol.Alpha_context.Constants.parametric; } -(* TODO/TORU Change me to correct value and have a configuration option *) -let max_batch_transactions = 10 - (* TODO/TORU Change me with bound on size and have a configuration option *) let max_number_of_batches = 10 @@ -130,55 +127,131 @@ let inject_batches state batches = in inject_operations state operations -let get_batches state = - let open Result_syntax in - let transactions = - Tx_queue.peek_at_most - state.transactions - (max_batch_transactions * max_number_of_batches) +(** [is_batch_valid] returns whether the batch is valid or not based on + two criterias: + + The proof produced by the batch interpretation must be smaller than + [l1_constants.tx_rollup_rejection_max_proof_size]. Otherwise, the associated + commitment can be rejected because of the size. + + The batch exceeds the [l1_constants.tx_rollup_hard_size_limit_per_message], + the submit batch operation will fail. +*) +let is_batch_valid ctxt + (l1_constants : Protocol.Alpha_context.Constants.parametric) batch = + let open Lwt_result_syntax in + (* The batch is ok if: + 1. The proof is small enough + 2. The batch is small enough *) + let batch_size_ok = + let size = Data_encoding.Binary.length Tx_rollup_l2_batch.encoding batch in + size <= l1_constants.tx_rollup_hard_size_limit_per_message in - let rec loop acc = function - | [] -> ok (List.rev acc) - | trs -> - let (trs, rest) = List.split_n max_batch_transactions trs in - let* batch = L2_transaction.batch trs in - loop (batch :: acc) rest + if batch_size_ok then + let l2_parameters = + Tx_rollup_l2_apply. + { + tx_rollup_max_withdrawals_per_batch = + l1_constants.tx_rollup_max_withdrawals_per_batch; + } + in + let*! res_interp = + Interpreter.interpret_batch + ctxt + l2_parameters + ~rejection_max_proof_size: + l1_constants.tx_rollup_rejection_max_proof_size + batch + in + let b_proof_size = Result.is_ok res_interp in + return b_proof_size + else return_false + +let get_batches ctxt l1_constants queue = + let open Lwt_result_syntax in + let exception + Batches_finished of { + rev_batches : + (Indexable.unknown, Indexable.unknown) Tx_rollup_l2_batch.t list; + to_remove : L2_transaction.hash list; + } in - let+ batches = loop [] transactions in - (batches, transactions) + try + let* (rev_batches, rev_current_trs, to_remove) = + Tx_queue.fold_es + (fun tr_hash tr (batches, rev_current_trs, to_remove) -> + let new_trs = tr :: rev_current_trs in + let*? batch = L2_transaction.batch (List.rev new_trs) in + let* b = is_batch_valid ctxt l1_constants batch in + if b then return (batches, new_trs, tr_hash :: to_remove) + else + match rev_current_trs with + | [_] -> + (* If only one transaction makes the batch invalid, we remove it + from the current transactions and it'll be removed later. *) + let*! () = Event.(emit Batcher.invalid_transaction) tr in + return (batches, [], tr_hash :: to_remove) + | _ -> + let*? batch = L2_transaction.batch (List.rev rev_current_trs) in + let new_batches = batch :: batches in + if + List.compare_length_with new_batches max_number_of_batches + >= 0 + then + (* We created enough batches, we exit the loop *) + raise + (Batches_finished {rev_batches = new_batches; to_remove}) + else + (* We add the batch to the accumulator and we go on. *) + let*? batch = L2_transaction.batch [tr] in + let* b = is_batch_valid ctxt l1_constants batch in + if b then return (new_batches, [tr], tr_hash :: to_remove) + else + let*! () = Event.(emit Batcher.invalid_transaction) tr in + return (new_batches, [], tr_hash :: to_remove)) + queue + ([], [], []) + in + let*? batches = + let open Result_syntax in + if rev_current_trs <> [] then + let+ last_batch = L2_transaction.batch (List.rev rev_current_trs) in + List.rev (last_batch :: rev_batches) + else return (List.rev rev_batches) + in + return (batches, to_remove) + with Batches_finished {rev_batches; to_remove} -> + return (List.rev rev_batches, to_remove) -let batch_and_inject ?(at_least_one_full_batch = false) state = +let batch_and_inject state = let open Lwt_result_syntax in - let*? (batches, to_remove) = get_batches state in - let*! () = - Event.(emit Batcher.batch) (List.length batches, List.length to_remove) + let* (batches, to_remove) = + get_batches state.incr_context state.l1_constants state.transactions in match batches with | [] -> return_none - | Tx_rollup_l2_batch.(V1 {V1.contents; _}) :: _ - when at_least_one_full_batch - && List.compare_length_with contents max_batch_transactions < 0 -> - (* The first batch is not full, and we requested it to be *) - let*! () = Event.(emit Batcher.no_full_batch) () in - return_none | _ -> + let*! () = + Event.(emit Batcher.batch) (List.length batches, List.length to_remove) + in let*! () = Event.(emit Batcher.inject) () in let* oph = inject_batches state batches in let*! () = Event.(emit Batcher.injection_success) oph in List.iter - (fun tr -> Tx_queue.remove state.transactions (L2_transaction.hash tr)) + (fun tr_hash -> Tx_queue.remove state.transactions tr_hash) to_remove ; return_some oph -let async_batch_and_inject ?at_least_one_full_batch state = +let async_batch_and_inject state = Lwt.async @@ fun () -> let open Lwt_syntax in (* let* _ = Lwt_unix.sleep 2. in *) - let* _ = batch_and_inject ?at_least_one_full_batch state in + let* _ = batch_and_inject state in return_unit -let init cctxt ~rollup ~signer index parameters = +let init cctxt ~rollup ~signer index l1_constants = let open Lwt_result_syntax in + let*! incr_context = Context.init_context index in let+ signer = get_signer cctxt signer in Option.map (fun signer -> @@ -186,10 +259,10 @@ let init cctxt ~rollup ~signer index parameters = cctxt = batcher_context cctxt; rollup; signer; - parameters; transactions = Tx_queue.create 500_000; - incr_context = Context.empty index; + incr_context; lock = Lwt_mutex.create (); + l1_constants; }) signer @@ -205,8 +278,15 @@ let register_transaction ?(eager_batch = false) ?(apply = true) state let prev_context = context in let* context = if apply then + let l2_parameters = + Tx_rollup_l2_apply. + { + tx_rollup_max_withdrawals_per_batch = + state.l1_constants.tx_rollup_max_withdrawals_per_batch; + } + in let* (new_context, result, _) = - L2_apply.Batch_V1.apply_batch context state.parameters batch + L2_apply.Batch_V1.apply_batch context l2_parameters batch in let open Tx_rollup_l2_apply.Message_result in let+ context = @@ -231,5 +311,5 @@ let register_transaction ?(eager_batch = false) ?(apply = true) state if eager_batch then (* TODO/TORU: find better solution as this reduces throughput when we have a single key to sign/inject. *) - async_batch_and_inject ~at_least_one_full_batch:true state ; + async_batch_and_inject state ; return tr_hash diff --git a/src/proto_alpha/bin_tx_rollup_node/batcher.mli b/src/proto_alpha/bin_tx_rollup_node/batcher.mli index 162958addbb8f22a019194ff4565165ca1334184..9d05ed0e3e641cbaa13d1e3965082b4d108e4c2e 100644 --- a/src/proto_alpha/bin_tx_rollup_node/batcher.mli +++ b/src/proto_alpha/bin_tx_rollup_node/batcher.mli @@ -36,7 +36,7 @@ val init : rollup:Tx_rollup.t -> signer:string option -> Context.index -> - Protocol.Tx_rollup_l2_apply.parameters -> + Protocol.Alpha_context.Constants.parametric -> state option tzresult Lwt.t (** Updates the incremental context in the batcher's state. @@ -68,13 +68,9 @@ val register_transaction : (** Create L2 batches of operations from the queue and pack them in an L1 batch operation. The batch operation is injected on the Tezos node by the signer. If the injection to L1 fails, the transactions are not removed from - the queue. Nothing is injected if [at_least_one_full_batch] is [true] (by - default [false]) and there isn't at least a full batch to inject. *) -val batch_and_inject : - ?at_least_one_full_batch:bool -> - state -> - Operation_hash.t option tzresult Lwt.t + the queue. *) +val batch_and_inject : state -> Operation_hash.t option tzresult Lwt.t (** Same as [batch_and_inject] but asynchronous. In particular, the potential failures are not reported here. *) -val async_batch_and_inject : ?at_least_one_full_batch:bool -> state -> unit +val async_batch_and_inject : state -> unit diff --git a/src/proto_alpha/bin_tx_rollup_node/context.ml b/src/proto_alpha/bin_tx_rollup_node/context.ml index 8b5cf5216178b87561c0cd3aa69492b110a6ce19..1beb8b9ee48a2c3bc146876c62233b69db498d4c 100644 --- a/src/proto_alpha/bin_tx_rollup_node/context.ml +++ b/src/proto_alpha/bin_tx_rollup_node/context.ml @@ -114,3 +114,99 @@ let checkout_exn index hash = let open Lwt_syntax in let+ context = checkout index hash in match context with None -> raise Not_found | Some context -> context + +(** {2 Prover context} *) + +exception Error of Environment.Error_monad.error + +module Prover_storage : + Protocol.Tx_rollup_l2_storage_sig.STORAGE + with type t = tree + and type 'a m = 'a Lwt.t = struct + type t = tree + + type 'a m = 'a Lwt.t + + module Syntax = struct + include Lwt.Syntax + + let return = Lwt.return + + let fail e = Lwt.fail (Error e) + + let catch (m : 'a m) k h = + Lwt.catch + (fun () -> m >>= k) + (function Error e -> h e | e -> Lwt.fail e) + + let list_fold_left_m = Lwt_list.fold_left_s + end + + let path k = [Bytes.to_string k] + + let get store key = Tree.find store (path key) + + let set store key value = Tree.add store (path key) value + + let remove store key = Tree.remove store (path key) +end + +module Prover_context = Protocol.Tx_rollup_l2_context.Make (Prover_storage) + +type 'a produce_proof_result = {tree : tree; result : 'a} + +let get_tree ctxt = + let open Lwt_result_syntax in + let*! tree_opt = find_tree ctxt [] in + match tree_opt with + | Some tree -> return tree + | None -> fail [Error.Tx_rollup_tree_not_found] + +let produce_proof ctxt f = + let open Lwt_result_syntax in + let index = index ctxt in + let* tree = get_tree ctxt in + let* kinded_key = + match Tree.kinded_key tree with + | Some kinded_key -> return kinded_key + | None -> fail [Error.Tx_rollup_tree_kinded_key_not_found] + in + let*! (proof, result) = + produce_stream_proof index kinded_key (fun tree -> + let*! res = f tree in + Lwt.return (res.tree, res)) + in + return (proof, result) + +let hash_tree = Tree.hash + +let tree_hash_of_context ctxt = + let open Lwt_result_syntax in + let+ tree = get_tree ctxt in + hash_tree tree + +let add_tree ctxt tree = + let open Lwt_syntax in + let* ctxt = add_tree ctxt [] tree in + (* Irmin requires that we commit the context before generating the proof. *) + let* ctxt_hash = commit ctxt in + return (ctxt, ctxt_hash) + +(** The initial context must be constructed using the internal empty tree. + This tree however, *needs* to be non-empty. Otherwise, its hash will + be inconsistent. + See {!Protocol.Tx_rollup_commitment_repr.empty_l2_context_hash} for more + context. +*) +let init_context index = + let open Prover_context.Syntax in + let ctxt = empty index in + let tree = Tree.empty ctxt in + let* tree = Prover_context.Address_index.init_counter tree in + let* tree = Prover_context.Ticket_index.init_counter tree in + let tree_hash = hash_tree tree in + assert ( + Context_hash.( + tree_hash = Protocol.Tx_rollup_message_result_repr.empty_l2_context_hash)) ; + let* (ctxt, _) = add_tree ctxt tree in + return ctxt diff --git a/src/proto_alpha/bin_tx_rollup_node/context.mli b/src/proto_alpha/bin_tx_rollup_node/context.mli index c9c8f386340491a7b38439860d461ef674ee53f6..0da34422ab50bbc931c8ead8af2d13c44d1132ee 100644 --- a/src/proto_alpha/bin_tx_rollup_node/context.mli +++ b/src/proto_alpha/bin_tx_rollup_node/context.mli @@ -44,8 +44,9 @@ val init : string -> index Lwt.t -(** Build an empty context from an index. Don't commit an empty context. *) -val empty : index -> t +(** Initialize an "empty" context from an index. It is not really empty in the + sense that the underlying tree is not empty, it is then committed. *) +val init_context : index -> t Lwt.t (** Close the index. Does not fail when the context is already closed. *) val close : index -> unit Lwt.t @@ -77,3 +78,53 @@ val hash : ?message:string -> t -> Protocol.Tx_rollup_l2_context_hash.t additional [message]. *) val commit : ?message:string -> context -> Protocol.Tx_rollup_l2_context_hash.t Lwt.t + +(** {2 Prover Context} *) + +(** The prover context is a subset of the context. It uses the internal + context tree to produce proofs. *) + +type tree + +module Prover_context : + Protocol.Tx_rollup_l2_context_sig.CONTEXT + with type t = tree + and type 'a m = 'a Lwt.t + +(** ['a produce_proof_result] is the result type needed for the {!produce_proof} + callback function. *) +type 'a produce_proof_result = { + tree : tree; (** the tree modified by the callback function *) + result : 'a; (** the callback result. *) +} + +(** [produce_proof ctxt f] applies [f] in the {!tree} inside [ctxt]. + + It returns a proof that is produced by applying [f], the proof is + constructed using low-levels accesses to the three, that is, it needs the + modified tree to be included in the [f]'s result to calculate the proof. + + Beside the proof production, this function can be used to perform sementical + changes in the {!Prover_context}. Thus, we give the possibility to return a + result in {!'a produce_proof_result} to observe [f]'s results. +*) +val produce_proof : + context -> + (tree -> 'a produce_proof_result Lwt.t) -> + (Protocol.Tx_rollup_l2_proof.t * 'a produce_proof_result) tzresult Lwt.t + +val hash_tree : tree -> Context_hash.t + +(** [add_tree ctxt tree] adds [tree] in the [ctxt]. In order to perform + actions on the tree (e.g. proof production), it needs to be persistent. Thus, + the context is committed on disk after we added the tree, that is, after + every modification on the tree such as a message interpretation. + + FIXME: https://gitlab.com/tezos/tezos/-/issues/2780 + We would like to avoid the commit in this function for performance + matters. +*) +val add_tree : + context -> tree -> (context * Protocol.Tx_rollup_l2_context_hash.t) Lwt.t + +val tree_hash_of_context : context -> Context_hash.t tzresult Lwt.t diff --git a/src/proto_alpha/bin_tx_rollup_node/daemon.ml b/src/proto_alpha/bin_tx_rollup_node/daemon.ml index 4abd78058fcb9cbbcccb02b20718e648bcc605a1..1edc9c600332b50d6fc3dc4f311c1ff840cdb8ca 100644 --- a/src/proto_alpha/bin_tx_rollup_node/daemon.ml +++ b/src/proto_alpha/bin_tx_rollup_node/daemon.ml @@ -38,37 +38,6 @@ let checkout_context (state : State.t) ctxt_hash = let+ context = Context.checkout state.context_index ctxt_hash in Option.to_result ~none:[Tx_rollup_cannot_checkout_context ctxt_hash] context -let interp_messages ctxt parameters messages cumulated_size = - let open Lwt_syntax in - let+ (ctxt, _ctxt_hash, rev_contents) = - List.fold_left_s - (fun (ctxt, ctxt_hash, acc) message -> - let+ apply_res = L2_apply.apply_message ctxt parameters message in - let (ctxt, ctxt_hash, result) = - match apply_res with - | Ok (ctxt, result) -> - (* The message was successfully interpreted but the status in - [result] may indicate that the application failed. The context - may have been modified with e.g. updated counters. *) - (ctxt, Context.hash ctxt, Inbox.Interpreted result) - | Error err -> - (* The message was discarded before attempting to interpret it. The - context is not modified. For instance if a batch is unparsable, - or the BLS signature is incorrect, or a counter is wrong, etc. *) - (ctxt, ctxt_hash, Inbox.Discarded err) - in - let inbox_message = {Inbox.message; result; context_hash = ctxt_hash} in - (ctxt, ctxt_hash, inbox_message :: acc)) - (ctxt, Context.hash ctxt, []) - messages - in - match rev_contents with - | [] -> (ctxt, None) - | _ -> - let contents = List.rev rev_contents in - let inbox = Inbox.{contents; cumulated_size} in - (ctxt, Some inbox) - let parse_tx_rollup_l2_address : Script.node -> Protocol.Tx_rollup_l2_address.Indexable.value tzresult = let open Protocol in @@ -239,12 +208,9 @@ let extract_messages_from_block block_info rollup_id = let create_genesis_block state tezos_block = let open Lwt_syntax in - let ctxt = Context.empty state.State.context_index in - let genesis_block = - L2block.genesis_block - state.context_index - state.rollup_info.rollup_id - tezos_block + let* ctxt = Context.init_context state.State.context_index in + let* genesis_block = + L2block.genesis_block ctxt state.rollup_info.rollup_id tezos_block in let+ _block_hash = State.save_block state genesis_block in (genesis_block, ctxt) @@ -262,14 +228,27 @@ let process_messages_and_inboxes (state : State.t) ~(predecessor : L2block.t) | None -> checkout_context state predecessor.header.context | Some context -> return context in - let*! (context, inbox) = - interp_messages predecessor_context state.parameters messages cumulated_size + let l2_parameters = + Protocol.Tx_rollup_l2_apply. + { + tx_rollup_max_withdrawals_per_batch = + state.l1_constants.tx_rollup_max_withdrawals_per_batch; + } + in + let* (context, contents) = + Interpreter.interpret_messages + predecessor_context + l2_parameters + ~rejection_max_proof_size: + state.l1_constants.tx_rollup_rejection_max_proof_size + messages in - match inbox with + match contents with | None -> (* No inbox at this block *) return (predecessor, predecessor_context) - | Some inbox -> + | Some contents -> + let inbox = Inbox.{contents; cumulated_size} in let*! context_hash = Context.commit context in let level = match predecessor.header.level with diff --git a/src/proto_alpha/bin_tx_rollup_node/error.ml b/src/proto_alpha/bin_tx_rollup_node/error.ml index 6cb52b82c3445b44cd7a68c38080a27abce6417b..805925cb8f8ce5eae966cae996ac0b2f2b38d0a9 100644 --- a/src/proto_alpha/bin_tx_rollup_node/error.ml +++ b/src/proto_alpha/bin_tx_rollup_node/error.ml @@ -275,3 +275,54 @@ let () = Data_encoding.(obj1 (req "block" Block_hash.encoding)) (function Tx_rollup_cannot_fetch_tezos_block b -> Some b | _ -> None) (fun b -> Tx_rollup_cannot_fetch_tezos_block b) + +type error += Tx_rollup_tree_not_found + +let () = + register_error_kind + ~id:"tx_rollup.node.tree_not_found" + ~title:"Tree not found in context" + ~description:"The tree is not found in the context." + ~pp:(fun ppf () -> + Format.fprintf + ppf + "The tree was not found in the context. The merkle proof associated to \ + a message can not be produced, the rollup can not interpret the \ + message.") + `Permanent + Data_encoding.empty + (function Tx_rollup_tree_not_found -> Some () | _ -> None) + (fun () -> Tx_rollup_tree_not_found) + +type error += Tx_rollup_tree_kinded_key_not_found + +let () = + register_error_kind + ~id:"tx_rollup.node.tree_kinded_key_not_found" + ~title:"Kinded key not found in tree" + ~description:"The kinded key is not found in the tree." + ~pp:(fun ppf () -> + Format.fprintf + ppf + "The kinded key was not found in the tree. The merkle proof associated \ + to a message can not be produced, the rollup can not interpret the \ + message.") + `Permanent + Data_encoding.empty + (function Tx_rollup_tree_kinded_key_not_found -> Some () | _ -> None) + (fun () -> Tx_rollup_tree_kinded_key_not_found) + +type error += Tx_rollup_invalid_message_position_in_inbox of int + +let () = + register_error_kind + ~id:"tx_rollup.node.invalid_message_position" + ~title:"Message position invalid in the inbox" + ~description:"The message position is invalid the inbox." + ~pp:(fun ppf i -> + Format.fprintf ppf "The message position %d is invalid in the inbox" i) + `Permanent + Data_encoding.(obj1 (req "message_position" int31)) + (function + | Tx_rollup_invalid_message_position_in_inbox i -> Some i | _ -> None) + (fun i -> Tx_rollup_invalid_message_position_in_inbox i) diff --git a/src/proto_alpha/bin_tx_rollup_node/error.mli b/src/proto_alpha/bin_tx_rollup_node/error.mli index d5cde5a59a7d6b958eec21dc8e645f1a1d7806e2..d40239e58458c9fddfe5d562b261a39d669b7707 100644 --- a/src/proto_alpha/bin_tx_rollup_node/error.mli +++ b/src/proto_alpha/bin_tx_rollup_node/error.mli @@ -75,3 +75,12 @@ type error += Tx_rollup_mismatch (** Error when Tezos block cannot be fetched. *) type error += Tx_rollup_cannot_fetch_tezos_block of Block_hash.t + +(** Error when the tree is not found in the context. *) +type error += Tx_rollup_tree_not_found + +(** Error when the kinded key is not found in the tree. *) +type error += Tx_rollup_tree_kinded_key_not_found + +(** Error when a message position does not exist in the inbox for the proof RPC *) +type error += Tx_rollup_invalid_message_position_in_inbox of int diff --git a/src/proto_alpha/bin_tx_rollup_node/event.ml b/src/proto_alpha/bin_tx_rollup_node/event.ml index 63d8b0a9cf0a777d274e43bd6339dd4cfd6e1c8d..2c1f0323f8e610b48d504bbb730269c873453b46 100644 --- a/src/proto_alpha/bin_tx_rollup_node/event.ml +++ b/src/proto_alpha/bin_tx_rollup_node/event.ml @@ -220,7 +220,7 @@ module Batcher = struct ~section ~name:"inject" ~msg:"Injecting batches on Tezos node" - ~level:Info + ~level:Notice () let injection_success = @@ -230,4 +230,11 @@ module Batcher = struct ~msg:"batches were successfully injected in operation {oph}" ~level:Notice ("oph", Operation_hash.encoding) + + let invalid_transaction = + declare_1 + ~section + ~name:"invalid_transaction" + ~msg:"a batch with this only transaction is invalid: {tr}" + ("tr", L2_transaction.encoding) end diff --git a/src/proto_alpha/bin_tx_rollup_node/inbox.ml b/src/proto_alpha/bin_tx_rollup_node/inbox.ml index 24ac43b07b7670a7e4061ecee92f696750f762e4..661e86e4cb7277d0e4ac4e65ca9fd474102cba84 100644 --- a/src/proto_alpha/bin_tx_rollup_node/inbox.ml +++ b/src/proto_alpha/bin_tx_rollup_node/inbox.ml @@ -32,10 +32,15 @@ type message_result = | Interpreted of Tx_rollup_l2_apply.Message_result.t | Discarded of tztrace +type l2_context_hash = { + irmin_hash : Tx_rollup_l2_context_hash.t; + tree_hash : Context_hash.t; +} + type message = { message : Tx_rollup_message.t; result : message_result; - context_hash : Tx_rollup_l2_context_hash.t; + l2_context_hash : l2_context_hash; } type t = {contents : message list; cumulated_size : int} @@ -59,15 +64,26 @@ let message_result_encoding = (fun e -> Discarded e); ] +let l2_context_hash_encoding = + let open Data_encoding in + conv + (fun {irmin_hash; tree_hash} -> (irmin_hash, tree_hash)) + (fun (irmin_hash, tree_hash) -> {irmin_hash; tree_hash}) + (obj2 + (req "irmin_hash" Tx_rollup_l2_context_hash.encoding) + (req "tree_hash" Context_hash.encoding)) + let message_encoding = let open Data_encoding in conv - (fun {message; result; context_hash} -> (message, result, context_hash)) - (fun (message, result, context_hash) -> {message; result; context_hash}) + (fun {message; result; l2_context_hash} -> + (message, result, l2_context_hash)) + (fun (message, result, l2_context_hash) -> + {message; result; l2_context_hash}) (obj3 (req "message" Tx_rollup_message.encoding) (req "result" message_result_encoding) - (req "context_hash" Tx_rollup_l2_context_hash.encoding)) + (req "l2_context_hash" l2_context_hash_encoding)) let encoding = let open Data_encoding in diff --git a/src/proto_alpha/bin_tx_rollup_node/inbox.mli b/src/proto_alpha/bin_tx_rollup_node/inbox.mli index 338aab426ae2767322920c7cceb8c695ee9688b8..10d70bcd4e77ac2937eee35bb384cccf06c384b9 100644 --- a/src/proto_alpha/bin_tx_rollup_node/inbox.mli +++ b/src/proto_alpha/bin_tx_rollup_node/inbox.mli @@ -38,12 +38,20 @@ type message_result = | Discarded of tztrace (** The message was discarded because it could not be interpreted *) +type l2_context_hash = { + irmin_hash : Tx_rollup_l2_context_hash.t; + (** The context hash of the commited context, used for checkout *) + tree_hash : Context_hash.t; + (** The tree hash is the hash of the underlying tree in the {!Context}, + used to produce proofs *) +} + (** Type of inbox message with the context hash resulting from the application of the message *) type message = { message : Tx_rollup_message.t; result : message_result; - context_hash : Tx_rollup_l2_context_hash.t; + l2_context_hash : l2_context_hash; } (** The type representing an inbox whose contents are the messages and not the diff --git a/src/proto_alpha/bin_tx_rollup_node/interpreter.ml b/src/proto_alpha/bin_tx_rollup_node/interpreter.ml new file mode 100644 index 0000000000000000000000000000000000000000..797da4adff83448dbbc7816709df83c379193a12 --- /dev/null +++ b/src/proto_alpha/bin_tx_rollup_node/interpreter.ml @@ -0,0 +1,124 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type error += Tx_rollup_message_proof_too_large of {limit : int; actual : int} + +let () = + register_error_kind + ~id:"tx_rollup.node.message_proof_too_large" + ~title:"Message's application proof is too large" + ~description: + "The proof associated to the application of the message is too large" + ~pp:(fun ppf (limit, actual) -> + Format.fprintf + ppf + "The message produces a proof of size %d where the protocol limit is \ + %d. It will be rejected by the protocol." + limit + actual) + `Permanent + Data_encoding.(obj2 (req "limit" int31) (req "actual" int31)) + (function + | Tx_rollup_message_proof_too_large {limit; actual} -> Some (limit, actual) + | _ -> None) + (fun (limit, actual) -> Tx_rollup_message_proof_too_large {limit; actual}) + +(** Interpret a message in the context. The function needs to be synchronised + with the [Tx_rollup_l2_verifier] module of the protocol, in particular + the proof size boundaries. *) +let interpret_message ~rejection_max_proof_size ctxt l2_parameters message = + let open Lwt_result_syntax in + let* (proof, res) = Prover_apply.apply_message ctxt l2_parameters message in + let proof_size = Prover_apply.proof_size proof in + let result = + if proof_size > rejection_max_proof_size then + (* The proof is too large, we can not commit this state. The + result is discarded. *) + Inbox.Discarded + [ + Tx_rollup_message_proof_too_large + {limit = rejection_max_proof_size; actual = proof_size}; + ] + else res.Context.result + in + return (res.Context.tree, result) + +let interpret_messages ~rejection_max_proof_size ctxt l2_parameters messages = + let open Lwt_result_syntax in + let ctxt_hash = Context.hash ctxt in + let* tree_hash = Context.tree_hash_of_context ctxt in + let+ (ctxt, _ctxt_hash, _tree_hash, rev_contents) = + List.fold_left_es + (fun (ctxt, ctxt_hash, tree_hash, acc) message -> + let* (tree, result) = + interpret_message ~rejection_max_proof_size ctxt l2_parameters message + in + let* (ctxt, ctxt_hash, tree_hash) = + match result with + | Inbox.Interpreted _ -> + (* The message was successfully interpreted but the status in + [result] may indicate that the application failed. The context + may have been modified with e.g. updated counters. *) + let tree_hash = Context.hash_tree tree in + let*! (ctxt, ctxt_hash) = Context.add_tree ctxt tree in + return (ctxt, ctxt_hash, tree_hash) + | Inbox.Discarded _ -> + (* The message was discarded before attempting to interpret it. The + context is not modified. For instance if a batch is unparsable, + or the BLS signature is incorrect, or a counter is wrong, etc. *) + return (ctxt, ctxt_hash, tree_hash) + in + let inbox_message = + Inbox. + { + message; + result; + l2_context_hash = {irmin_hash = ctxt_hash; tree_hash}; + } + in + return (ctxt, ctxt_hash, tree_hash, inbox_message :: acc)) + (ctxt, ctxt_hash, tree_hash, []) + messages + in + match rev_contents with + | [] -> (ctxt, None) + | _ -> + let contents = List.rev rev_contents in + (ctxt, Some contents) + +let interpret_batch ~rejection_max_proof_size ctxt l2_parameters batch = + let open Lwt_result_syntax in + let batch_bytes = + Data_encoding.Binary.to_string_exn + Protocol.Tx_rollup_l2_batch.encoding + batch + in + let (message, _) = + Protocol.Alpha_context.Tx_rollup_message.make_batch batch_bytes + in + let* (_tree, result) = + interpret_message ~rejection_max_proof_size ctxt l2_parameters message + in + match result with Inbox.Discarded trace -> fail trace | _ -> return () diff --git a/src/proto_alpha/bin_tx_rollup_node/interpreter.mli b/src/proto_alpha/bin_tx_rollup_node/interpreter.mli new file mode 100644 index 0000000000000000000000000000000000000000..77f38c8287a5f46ebfa1fc8b120de51853087894 --- /dev/null +++ b/src/proto_alpha/bin_tx_rollup_node/interpreter.mli @@ -0,0 +1,61 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Error result when the message's application produces a too large proof. + It overrides the layer2 apply message result. *) +type error += Tx_rollup_message_proof_too_large of {limit : int; actual : int} + +(** Interpreting the [messages] in the context. + + It uses internally the {!Prover_apply} to produce a proof associated + to the interpretation of each message. In the case where the proof is + larger than the configuration limit, the message's interpretation is + discarded alongside the modified context. +*) +val interpret_messages : + rejection_max_proof_size:int -> + Context.context -> + Protocol.Tx_rollup_l2_apply.parameters -> + Protocol.Alpha_context.Tx_rollup_message.t trace -> + (Context.context * Inbox.message list option) tzresult Lwt.t + +(** Interpreting the [batch] in the context. + + Similarly to {!interp_messages}, it uses internally the {!Prover_apply}. + However, the function fails if the interpretation produces a proof + that is larger than the configuration limit. + We here want to check only if the batch is interpretable, the modified + tree is discarded. + + TODO/TORU: maybe we could check the results for each transaction in the batch +*) +val interpret_batch : + rejection_max_proof_size:int -> + Context.context -> + Protocol.Tx_rollup_l2_apply.parameters -> + ( Protocol.Indexable.unknown, + Protocol.Indexable.unknown ) + Protocol.Tx_rollup_l2_batch.t -> + unit tzresult Lwt.t diff --git a/src/proto_alpha/bin_tx_rollup_node/l2block.ml b/src/proto_alpha/bin_tx_rollup_node/l2block.ml index c8c6bd50fff73127d758a0925d14e8c14a64ecf0..0eab0b4b4fdb8444077f6d33dba16394fd381594 100644 --- a/src/proto_alpha/bin_tx_rollup_node/l2block.ml +++ b/src/proto_alpha/bin_tx_rollup_node/l2block.ml @@ -126,9 +126,9 @@ let hash_header h = | Rollup_level _ -> Hash.hash_bytes [Data_encoding.Binary.to_bytes_exn header_encoding h] -let genesis_block index rollup tezos_block = - let ctxt = Context.empty index in - let context_hash = Context.hash ctxt in +let genesis_block ctxt rollup tezos_block = + let open Lwt_syntax in + let* context_hash = Context.commit ctxt in let hash = genesis_hash rollup in let header = { @@ -140,4 +140,4 @@ let genesis_block index rollup tezos_block = } in let inbox : Inbox.t = {contents = []; cumulated_size = 0} in - {hash; header; inbox} + return {hash; header; inbox} diff --git a/src/proto_alpha/bin_tx_rollup_node/l2block.mli b/src/proto_alpha/bin_tx_rollup_node/l2block.mli index 95a011bcb4b151a88d3aa1cf30fb59b16187588b..a5facf988755d8521286ae0fd6220852c9d4fe39 100644 --- a/src/proto_alpha/bin_tx_rollup_node/l2block.mli +++ b/src/proto_alpha/bin_tx_rollup_node/l2block.mli @@ -58,7 +58,7 @@ type header = { type t = {hash : hash; header : header; inbox : Inbox.t} (** Build the genesis block *) -val genesis_block : Context.index -> Tx_rollup.t -> Block_hash.t -> t +val genesis_block : Context.t -> Tx_rollup.t -> Block_hash.t -> t Lwt.t (** {2 Encoding} *) diff --git a/src/proto_alpha/bin_tx_rollup_node/prover_apply.ml b/src/proto_alpha/bin_tx_rollup_node/prover_apply.ml new file mode 100644 index 0000000000000000000000000000000000000000..51e49fdb133ba5dbebaf00e2f3db33c773aae4d9 --- /dev/null +++ b/src/proto_alpha/bin_tx_rollup_node/prover_apply.ml @@ -0,0 +1,46 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +module Prover_apply = Protocol.Tx_rollup_l2_apply.Make (Context.Prover_context) + +type proof = Protocol.Tx_rollup_l2_proof.t + +let proof_size = + Data_encoding.Binary.length Protocol.Tx_rollup_l2_proof.encoding + +let apply_message ctxt parameters message = + let open Lwt_result_syntax in + let f tree = + Context.Prover_context.Syntax.catch + (Prover_apply.apply_message tree parameters message) + (fun (tree, result) -> + Lwt.return Context.{tree; result = Inbox.Interpreted result}) + (fun err -> + Lwt.return + Context. + {tree; result = Inbox.Discarded [Environment.wrap_tzerror err]}) + in + let* (proof, result) = Context.produce_proof ctxt f in + return (proof, result) diff --git a/src/proto_alpha/bin_tx_rollup_node/prover_apply.mli b/src/proto_alpha/bin_tx_rollup_node/prover_apply.mli new file mode 100644 index 0000000000000000000000000000000000000000..f951e5cd33a2e7c0057a20b4222978b6597bfebd --- /dev/null +++ b/src/proto_alpha/bin_tx_rollup_node/prover_apply.mli @@ -0,0 +1,45 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs, *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type proof = Protocol.Tx_rollup_l2_proof.t + +val proof_size : proof -> int + +(** [apply_message ctxt parameters message] applies [message] on [ctxt] + using [parameters]. + + It uses internally the {!Context.Prover_context} to generate the associated + proof of the application. + + The modified prover context state is returned and can be either dropped or + committed. Note that if the underlying tree is meant to be kept, it + must be committed on disk, no others proofs can be generated on a + non-persistent tree. +*) +val apply_message : + Context.context -> + Protocol.Tx_rollup_l2_apply.parameters -> + Protocol.Alpha_context.Tx_rollup_message.t -> + (proof * Inbox.message_result Context.produce_proof_result) tzresult Lwt.t diff --git a/src/proto_alpha/bin_tx_rollup_node/state.ml b/src/proto_alpha/bin_tx_rollup_node/state.ml index e02e9e884e3177202f2b270c2b27059e74baff35..ed404efdf28bbd503644112a81549440bffbb15b 100644 --- a/src/proto_alpha/bin_tx_rollup_node/state.ml +++ b/src/proto_alpha/bin_tx_rollup_node/state.ml @@ -48,9 +48,9 @@ type t = { mutable head : L2block.t; rollup_info : rollup_info; tezos_blocks_cache : Alpha_block_services.block_info Tezos_blocks_cache.t; - parameters : Protocol.Tx_rollup_l2_apply.parameters; operator : signer option; batcher_state : Batcher.state option; + l1_constants : Protocol.Alpha_context.Constants.parametric; } type 'block reorg = { @@ -393,26 +393,23 @@ let init_context ~data_dir = let init_head (stores : Stores.t) context_index rollup rollup_info = let open Lwt_syntax in let* hash = Stores.Head_store.read stores.head in - let+ head = + let* head = match hash with | None -> return_none | Some hash -> get_block_store stores hash in match head with - | Some head -> head + | Some head -> return head | None -> - L2block.genesis_block context_index rollup rollup_info.origination_block + let* ctxt = Context.init_context context_index in + L2block.genesis_block ctxt rollup rollup_info.origination_block -let init_parameters cctxt = +let init_l1_constants cctxt = let open Lwt_result_syntax in - let* {parametric; _} = + let+ {parametric; _} = Protocol.Constants_services.all cctxt (cctxt#chain, cctxt#block) in - return - { - Protocol.Tx_rollup_l2_apply.tx_rollup_max_withdrawals_per_batch = - parametric.tx_rollup_max_withdrawals_per_batch; - } + parametric let init cctxt ~data_dir ?(readonly = false) ?rollup_genesis ~l2_blocks_cache_size ~operator rollup = @@ -427,9 +424,9 @@ let init cctxt ~data_dir ?(readonly = false) ?rollup_genesis |> lwt_map_error (function [] -> [] | trace :: _ -> trace) in let*! head = init_head stores context_index rollup rollup_info in - let* parameters = init_parameters cctxt in + let* l1_constants = init_l1_constants cctxt in let* batcher_state = - Batcher.init cctxt ~rollup ~signer:operator context_index parameters + Batcher.init cctxt ~rollup ~signer:operator context_index l1_constants in let* operator = get_signer cctxt operator in (* L1 blocks are cached to handle reorganizations efficiently *) @@ -441,7 +438,7 @@ let init cctxt ~data_dir ?(readonly = false) ?rollup_genesis head; rollup_info; tezos_blocks_cache; - parameters; operator; batcher_state; + l1_constants; } diff --git a/src/proto_alpha/bin_tx_rollup_node/state.mli b/src/proto_alpha/bin_tx_rollup_node/state.mli index 21fb0a6b762c99b603a3b02dffd8e0b3d1a3e0cc..29f9b5d5bbe55d6d2864f134ddd00aec39851d2c 100644 --- a/src/proto_alpha/bin_tx_rollup_node/state.mli +++ b/src/proto_alpha/bin_tx_rollup_node/state.mli @@ -51,9 +51,9 @@ type t = private { mutable head : L2block.t; rollup_info : rollup_info; tezos_blocks_cache : Alpha_block_services.block_info Tezos_blocks_cache.t; - parameters : Protocol.Tx_rollup_l2_apply.parameters; operator : signer option; batcher_state : Batcher.state option; + l1_constants : Protocol.Alpha_context.Constants.parametric; } (** Type of chain reorganizations. *) diff --git a/src/proto_alpha/lib_client_commands/client_proto_context_commands.ml b/src/proto_alpha/lib_client_commands/client_proto_context_commands.ml index 868aadc3e8cf6dc358fb8c9b0e1af2ad98e6a082..4d60f08451c3a9ccc4bdc9dbbbdc1312d1ce5438 100644 --- a/src/proto_alpha/lib_client_commands/client_proto_context_commands.ml +++ b/src/proto_alpha/lib_client_commands/client_proto_context_commands.ml @@ -2631,8 +2631,8 @@ let commands_rw () = Client_proto_args.string_parameter @@ prefixes ["and"; "withdraw"; "list"] @@ Clic.param - ~name:"withdrawals_merkle_root" - ~desc:"the hash of the merkelised withdraw list" + ~name:"withdraw_list_hash" + ~desc:"the hash of the withdraw list" Client_proto_args.string_parameter @@ prefixes ["with"; "path"] @@ Clic.param diff --git a/src/proto_alpha/lib_plugin/plugin.ml b/src/proto_alpha/lib_plugin/plugin.ml index f8608e4427683349cdee2ab82a4477995cd2c83b..83cf62fc670a832636ad8a60d830a4d0b4c05b08 100644 --- a/src/proto_alpha/lib_plugin/plugin.ml +++ b/src/proto_alpha/lib_plugin/plugin.ml @@ -3083,6 +3083,32 @@ module RPC = struct ~output: (obj1 (req "path" Tx_rollup_commitment.Merkle.path_encoding)) RPC_path.(path / "merkle_tree_path") + + let message_result_hash = + RPC_service.post_service + ~description:"Compute the message result hash" + ~query:RPC_query.empty + ~input: + (obj2 + (req "context_hash" Context_hash.encoding) + (req + "withdraw_list_hash" + Tx_rollup_withdraw_list_hash.encoding)) + ~output:(obj1 (req "hash" Tx_rollup_message_result_hash.encoding)) + RPC_path.(path / "message_result_hash") + end + + module Withdraw = struct + let path = RPC_path.(path / "withdraw") + + let withdraw_list_hash = + RPC_service.post_service + ~description:"Compute the hash of a withdraw list" + ~query:RPC_query.empty + ~input: + (obj1 (req "withdraw_list" (list Tx_rollup_withdraw.encoding))) + ~output:(obj1 (req "hash" Tx_rollup_withdraw_list_hash.encoding)) + RPC_path.(path / "withdraw_list_hash") end end end @@ -3146,7 +3172,19 @@ module RPC = struct (fun () (message_result_hashes, position) -> let open Tx_rollup_commitment.Merkle in let tree = List.fold_left snoc nil message_result_hashes in - Lwt.return (compute_path tree position)) + Lwt.return (compute_path tree position)) ; + Registration.register0_noctxt + ~chunked:true + S.Tx_rollup.Commitment.message_result_hash + (fun () (context_hash, withdraw_list_hash) -> + return + (Tx_rollup_message_result_hash.hash_uncarbonated + {context_hash; withdraw_list_hash})) ; + Registration.register0_noctxt + ~chunked:true + S.Tx_rollup.Withdraw.withdraw_list_hash + (fun () withdrawals -> + return (Tx_rollup_withdraw_list_hash.hash_uncarbonated withdrawals)) module Manager = struct let[@coq_axiom_with_reason "cast on e"] operations ctxt block ~branch diff --git a/src/proto_alpha/lib_protocol/tx_rollup_message_result_hash_repr.mli b/src/proto_alpha/lib_protocol/tx_rollup_message_result_hash_repr.mli index 8409c3ed7ca67a4923d5001ce1c913a8a4aa7619..7016002dde4c5e819fcee1949287022786d2d249 100644 --- a/src/proto_alpha/lib_protocol/tx_rollup_message_result_hash_repr.mli +++ b/src/proto_alpha/lib_protocol/tx_rollup_message_result_hash_repr.mli @@ -28,7 +28,7 @@ (** The hash of the result of a layer-2 operation: that is, the hash of [(l2_ctxt_hash ^ withdraw_hash)] where [l2_ctxt_hash] is the Merkle tree root of the L2 context after any message (ie. deposit or batch), - and [withdraw_hash] is a [Tx_rollup_withdraw_repr.withdrawals_merkle_root] *) + and [withdraw_hash] is a [Tx_rollup_withdraw_repr.withdraw_list_hash] *) include S.HASH diff --git a/tezt/lib_tezos/RPC.ml b/tezt/lib_tezos/RPC.ml index 471331d81a21d32d0feae3bab5f0cb272bbbe292..1a364cd9c6cb4efb5cd62cba1a50a0323cf37869 100644 --- a/tezt/lib_tezos/RPC.ml +++ b/tezt/lib_tezos/RPC.ml @@ -684,6 +684,42 @@ module Tx_rollup = struct ] in Client.Spawn.rpc ?endpoint ?hooks ~data POST path client + + let message_result_hash ?endpoint ?hooks ?(chain = "main") + ?(block = "head") ~data client = + let path = + [ + "chains"; + chain; + "blocks"; + block; + "helpers"; + "forge"; + "tx_rollup"; + "commitment"; + "message_result_hash"; + ] + in + Client.Spawn.rpc ?endpoint ?hooks ~data POST path client + end + + module Withdraw = struct + let withdraw_list_hash ?endpoint ?hooks ?(chain = "main") + ?(block = "head") ~data client = + let path = + [ + "chains"; + chain; + "blocks"; + block; + "helpers"; + "forge"; + "tx_rollup"; + "withdraw"; + "withdraw_list_hash"; + ] + in + Client.Spawn.rpc ?endpoint ?hooks ~data POST path client end end end diff --git a/tezt/lib_tezos/RPC.mli b/tezt/lib_tezos/RPC.mli index e7bf6e1f34dea5c326789c07387da9f18b005ced..fba25a15791b610139ebe6dd7fa3bbd428718edd 100644 --- a/tezt/lib_tezos/RPC.mli +++ b/tezt/lib_tezos/RPC.mli @@ -969,6 +969,26 @@ module Tx_rollup : sig data:JSON.u -> Client.t -> JSON.t Process.runnable + + val message_result_hash : + ?endpoint:Client.endpoint -> + ?hooks:Process.hooks -> + ?chain:string -> + ?block:string -> + data:JSON.u -> + Client.t -> + JSON.t Process.runnable + end + + module Withdraw : sig + val withdraw_list_hash : + ?endpoint:Client.endpoint -> + ?hooks:Process.hooks -> + ?chain:string -> + ?block:string -> + data:JSON.u -> + Client.t -> + JSON.t Process.runnable end end end diff --git a/tezt/lib_tezos/rollup.ml b/tezt/lib_tezos/rollup.ml index 4a7d585113476e87284f825c05878b41a187465e..dc6e1109918935837367c32aa39b4fe581d3592c 100644 --- a/tezt/lib_tezos/rollup.ml +++ b/tezt/lib_tezos/rollup.ml @@ -232,6 +232,26 @@ module Tx_rollup = struct in map_runnable parse runnable + let withdraw_list_hash ?hooks ~withdrawals client = + let parse json = JSON.(json |-> "hash" |> as_string) in + let data = + `O [("withdraw_list", `A (List.map (fun x -> `String x) withdrawals))] + in + RPC.Tx_rollup.Forge.Withdraw.withdraw_list_hash ?hooks ~data client + |> map_runnable parse + + let message_result_hash ?hooks ~context_hash ~withdraw_list_hash client = + let parse json = JSON.(json |-> "hash" |> as_string) in + let data = + `O + [ + ("context_hash", `String context_hash); + ("withdraw_list_hash", `String withdraw_list_hash); + ] + in + RPC.Tx_rollup.Forge.Commitment.message_result_hash ?hooks ~data client + |> map_runnable parse + let compute_inbox_from_messages ?hooks messages client = let* message_hashes = Lwt_list.map_p diff --git a/tezt/lib_tezos/rollup.mli b/tezt/lib_tezos/rollup.mli index b7bba1130e469a1a2a556fe7cbc47ecd914e1039..0965ccdf02f24cd9668e0dc05317deb5eaae0e16 100644 --- a/tezt/lib_tezos/rollup.mli +++ b/tezt/lib_tezos/rollup.mli @@ -124,6 +124,19 @@ module Tx_rollup : sig Client.t -> JSON.t Process.runnable + val withdraw_list_hash : + ?hooks:Process.hooks -> + withdrawals:string list -> + Client.t -> + string Process.runnable + + val message_result_hash : + ?hooks:Process.hooks -> + context_hash:string -> + withdraw_list_hash:string -> + Client.t -> + string Process.runnable + val compute_inbox_from_messages : ?hooks:Process.hooks -> message list -> Client.t -> inbox Lwt.t diff --git a/tezt/lib_tezos/tx_rollup_node.ml b/tezt/lib_tezos/tx_rollup_node.ml index 31ff285bac4c6a707fcc345551084c51ec238f8a..1978c6f39096bd6740f87ab3365db0f4a4ca86c9 100644 --- a/tezt/lib_tezos/tx_rollup_node.ml +++ b/tezt/lib_tezos/tx_rollup_node.ml @@ -162,6 +162,34 @@ let wait_for_tezos_level node level = ~where:("level >= " ^ string_of_int level) promise +let wait_for_full ?where node name filter = + let (promise, resolver) = Lwt.task () in + let current_events = + String_map.find_opt name node.one_shot_event_handlers + |> Option.value ~default:[] + in + node.one_shot_event_handlers <- + String_map.add + name + (Event_handler {filter; resolver} :: current_events) + node.one_shot_event_handlers ; + let* result = promise in + match result with + | None -> + raise (Terminated_before_event {daemon = node.name; event = name; where}) + | Some x -> return x + +let event_from_full_event_filter filter json = + let raw = get_event_from_full_event json in + (* If [json] does not match the correct JSON structure, it + will be filtered out, which will result in ignoring + the current event. + @see raw_event_from_event *) + Option.bind raw (fun {value; _} -> filter value) + +let wait_for ?where node name filter = + wait_for_full ?where node name (event_from_full_event_filter filter) + let create ?(path = Constant.tx_rollup_node) ?runner ?data_dir ?(addr = "127.0.0.1") ?(dormant_mode = false) ?color ?event_pipe ?name ~rollup_id ~rollup_genesis ?operator client tezos_node = @@ -227,3 +255,77 @@ let run node = "--data-dir"; node.persistent_state.data_dir; ] + +module Inbox = struct + type l2_context_hash = {irmin_hash : string; tree_hash : string} + + type message = { + message : JSON.t; + result : JSON.t; + l2_context_hash : l2_context_hash; + } + + type t = {contents : message list; cumulated_size : int} +end + +module Client = struct + let raw_tx_node_rpc node ~url = + let* rpc = RPC.Curl.get () in + match rpc with + | None -> assert false + | Some curl -> + let url = Printf.sprintf "%s/%s" (rpc_addr node) url in + curl ~url + + let get_inbox ~tx_node ~block = + let parse_l2_context_hash json = + let irmin_hash = JSON.(json |-> "irmin_hash" |> as_string) in + let tree_hash = JSON.(json |-> "tree_hash" |> as_string) in + Inbox.{irmin_hash; tree_hash} + in + let parse_message json = + let message = JSON.(json |-> "message") in + let result = JSON.(json |-> "result") in + let l2_context_hash = + parse_l2_context_hash JSON.(json |-> "l2_context_hash") + in + Inbox.{message; result; l2_context_hash} + in + let parse_json json = + let cumulated_size = JSON.(json |-> "cumulated_size" |> as_int) in + let contents = + JSON.(json |-> "contents" |> as_list) |> List.map parse_message + in + Inbox.{cumulated_size; contents} + in + let* json = raw_tx_node_rpc tx_node ~url:("block/" ^ block ^ "/inbox") in + return (parse_json json) + + let get_balance ~tx_node ~block ~ticket_id ~tz4_address = + let parse_json json = + match JSON.(json |> as_int_opt) with + | Some level -> level + | None -> + Test.fail "Cannot retrieve balance of tz4 address %s" tz4_address + in + let* json = + raw_tx_node_rpc + tx_node + ~url: + ("context/" ^ block ^ "/tickets/" ^ ticket_id ^ "/balance/" + ^ tz4_address) + in + return (parse_json json) + + let get_queue ~tx_node = raw_tx_node_rpc tx_node ~url:"queue" + + let get_transaction_in_queue ~tx_node txh = + raw_tx_node_rpc tx_node ~url:("queue/transaction/" ^ txh) + + let get_block ~tx_node ~block = raw_tx_node_rpc tx_node ~url:("block/" ^ block) + + let get_merkle_proof ~tx_node ~block ~message_pos = + raw_tx_node_rpc + tx_node + ~url:("block/" ^ block ^ "/proof/message/" ^ message_pos) +end diff --git a/tezt/lib_tezos/tx_rollup_node.mli b/tezt/lib_tezos/tx_rollup_node.mli index 01b3f9a51b9786bf16c32ef903641e49529076fa..e30e75b0122feea0516be6a5a2b63e4af907eab0 100644 --- a/tezt/lib_tezos/tx_rollup_node.mli +++ b/tezt/lib_tezos/tx_rollup_node.mli @@ -60,6 +60,46 @@ val wait_for_ready : t -> unit Lwt.t If such an event already occurred, return immediately. *) val wait_for_tezos_level : t -> int -> int Lwt.t +(** Wait for a custom event to occur. + + Usage: [wait_for_full daemon name filter] + + If an event named [name] occurs, apply [filter] to its + whole json, which is of the form: + {[{ + "fd-sink-item.v0": { + "hostname": "...", + "time_stamp": ..., + "section": [ ... ], + "event": { : ... } + } + }]} + If [filter] returns [None], continue waiting. + If [filter] returns [Some x], return [x]. + + [where] is used as the [where] field of the [Terminated_before_event] exception + if the daemon terminates. It should describe the constraint that [filter] applies, + such as ["field level exists"]. + + It is advised to register such event handlers before starting the daemon, + as if they occur before being registered, they will not trigger your handler. + For instance, you can define a promise with + [let x_event = wait_for daemon "x" (fun x -> Some x)] + and bind it later with [let* x = x_event]. *) +val wait_for_full : + ?where:string -> t -> string -> (JSON.t -> 'a option) -> 'a Lwt.t + +(** Same as [wait_for_full] but ignore metadata from the file descriptor sink. + + More precisely, [filter] is applied to the value of field + ["fd-sink-item.v0"."event".]. + + If the daemon receives a JSON value that does not match the right + JSON structure, it is not given to [filter] and the event is + ignored. See [wait_for_full] to know what the JSON value must + look like. *) +val wait_for : ?where:string -> t -> string -> (JSON.t -> 'a option) -> 'a Lwt.t + (** Connected to a tezos node. Returns the name of the configuration file. *) val config_init : t -> string -> string -> string Lwt.t @@ -72,3 +112,38 @@ val terminate : ?kill:bool -> t -> unit Lwt.t (** Get the RPC address given as [--rpc-addr] to a node. *) val rpc_addr : t -> string + +module Inbox : sig + type l2_context_hash = {irmin_hash : string; tree_hash : string} + + type message = { + message : JSON.t; + result : JSON.t; + l2_context_hash : l2_context_hash; + } + + type t = {contents : message list; cumulated_size : int} +end + +(* FIXME/TORU: This is a temporary way of querying the node without + tx_rollup_client. This aims to be replaced as soon as possible by + the dedicated client's RPC. *) +module Client : sig + val get_inbox : tx_node:t -> block:string -> Inbox.t Lwt.t + + val get_balance : + tx_node:t -> + block:string -> + ticket_id:string -> + tz4_address:string -> + int Lwt.t + + val get_queue : tx_node:t -> JSON.t Lwt.t + + val get_transaction_in_queue : tx_node:t -> string -> JSON.t Lwt.t + + val get_block : tx_node:t -> block:string -> JSON.t Lwt.t + + val get_merkle_proof : + tx_node:t -> block:string -> message_pos:string -> JSON.t Lwt.t +end diff --git a/tezt/tests/tx_rollup_node.ml b/tezt/tests/tx_rollup_node.ml index e79518662d58dfccb1faa2f0109f5f59b8815e40..0219bbf2b6d8fa735e89124f72b32b187e9633a3 100644 --- a/tezt/tests/tx_rollup_node.ml +++ b/tezt/tests/tx_rollup_node.ml @@ -70,7 +70,52 @@ let get_rollup_parameter_file ~protocol = let base = Either.right (protocol, None) in Protocol.write_parameter_file ~base enable_tx_rollup -(* Checks that the configuration is stored and that the required +(* Wait for the [injection_success] event from the rollup node batcher. *) +let wait_for_injection_success_event node = + Rollup_node.wait_for node "injection_success.v0" (fun _ -> Some ()) + +(* Check that all messages in the inbox have been successfully applied. *) +let check_inbox_success (inbox : Rollup_node.Inbox.t) = + let ( |->? ) json field = + let res = JSON.(json |-> field) in + match JSON.unannotate res with `Null -> None | _ -> Some res + in + List.iteri + (fun i msg -> + let result = + (* Pair of result and withdraws *) + JSON.(msg.Rollup_node.Inbox.result |=> 0) + in + match result |->? "deposit_result" with + | None -> + (* Not a deposit, must be a batch *) + let results = + JSON.( + result |->? "batch_v1_result" |> Option.get |-> "results" + |> as_list) + in + List.iteri + (fun j tr_json -> + match JSON.(tr_json |=> 1 |> as_string_opt) with + | Some "transaction_success" -> (* OK *) () + | _ -> + Test.fail + "Transaction at position %d of batch %d failed: %s" + j + i + (JSON.encode tr_json)) + results + | Some result -> ( + match result |->? "deposit_success" with + | Some _ -> (* OK *) () + | None -> + Test.fail + "Deposit at position %d failed: %s" + i + (JSON.encode result))) + inbox.contents + +(* Checks that the configuration is stored and that the required fields are present. *) let test_node_configuration = Protocol.register_test @@ -222,17 +267,6 @@ let test_tx_node_store_inbox = Computed %L" ; unit) -(* FIXME/TORU: This is a temporary way of querying the node without - tx_rollup_client. This aims to be replaced as soon as possible by - the dedicated client's RPC. *) -let raw_tx_node_rpc node ~url = - let* rpc = RPC.Curl.get () in - match rpc with - | None -> assert false - | Some curl -> - let url = Printf.sprintf "%s/%s" (Rollup_node.rpc_addr node) url in - curl ~url - let raw_tx_node_rpc_post node ~url data = let* rpc = RPC.Curl.post () in match rpc with @@ -241,19 +275,6 @@ let raw_tx_node_rpc_post node ~url data = let url = Printf.sprintf "%s/%s" (Rollup_node.rpc_addr node) url in curl ~url data -(* FIXME/TORU: Must be replaced by the Tx_client.get_balance command *) -let tx_client_get_balance ~tx_node ~block ~ticket_id ~tz4_address = - let* json = - raw_tx_node_rpc - tx_node - ~url: - ("context/" ^ block ^ "/tickets/" ^ ticket_id ^ "/balance/" - ^ tz4_address) - in - match JSON.(json |> as_int_opt) with - | Some level -> Lwt.return level - | None -> Test.fail "Cannot retrieve balance of tz4 address %s" tz4_address - let tx_client_inject_transaction ~tx_node ?failswith transaction signature = let open Tezos_protocol_alpha.Protocol in let signed_tx_json = @@ -288,18 +309,6 @@ let tx_client_inject_transaction ~tx_node ?failswith transaction signature = (* Dummy value for operation hash *) return "" -let tx_client_get_queue ~tx_node = raw_tx_node_rpc tx_node ~url:"queue" - -let tx_client_get_transaction_in_queue ~tx_node txh = - raw_tx_node_rpc tx_node ~url:("queue/transaction/" ^ txh) - -let tx_client_get_block ~tx_node ~block = - raw_tx_node_rpc tx_node ~url:("block/" ^ block) - -(* FIXME/TORU: Must be replaced by the Tx_client.get_inbox command *) -let tx_client_get_inbox ~tx_node ~block = - raw_tx_node_rpc tx_node ~url:("block/" ^ block ^ "/inbox") - (* Returns the ticket hash, if any, of a given operation. *) let get_ticket_hash_from_op op = let metadata = JSON.(op |-> "contents" |=> 0 |-> "metadata") in @@ -323,8 +332,8 @@ let get_ticket_hash_from_op op = JSON.(result |-> "ticket_hash" |> as_string) | None | Some _ -> Test.fail "The contract origination failed" -let get_ticket_hash_from_deposit d = - JSON.(d |-> "message" |-> "deposit" |-> "ticket_hash" |> as_string) +let get_ticket_hash_from_deposit (d : Rollup_node.Inbox.message) : string = + JSON.(d.message |-> "deposit" |-> "ticket_hash" |> as_string) (* The contract is expecting a parameter of the form: (Pair tx_rollup_txr1_address tx_rollup_tz4_address) *) @@ -366,7 +375,7 @@ let generate_bls_addr ?alias:_ _client = let check_tz4_balance ~tx_node ~block ~ticket_id ~tz4_address ~expected_balance = let* tz4_balance = - tx_client_get_balance ~tx_node ~block ~ticket_id ~tz4_address + Rollup_node.Client.get_balance ~tx_node ~block ~ticket_id ~tz4_address in Check.( ( = ) @@ -432,10 +441,8 @@ let test_ticket_deposit_from_l1_to_l2 = let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in (* Get the operation containing the ticket transfer. We assume that only one operation is issued in this block. *) - let* inbox = tx_client_get_inbox ~tx_node ~block:"head" in - let ticket_id = - get_ticket_hash_from_deposit JSON.(inbox |-> "contents" |=> 0) - in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let ticket_id = get_ticket_hash_from_deposit (List.hd inbox.contents) in Log.info "Ticket %s was successfully emitted" ticket_id ; let* () = check_tz4_balance @@ -533,12 +540,14 @@ let test_l2_to_l2_transaction = Log.info "The tx_rollup_deposit %s contract was successfully originated" contract_id ; - (* Genarating some identities *) + (* Generating some identities *) let (bls_1_pkh, bls_pk_1, bls_sk_1) = generate_bls_addr client in let bls_pkh_1_str = Bls_public_key_hash.to_b58check bls_1_pkh in (* FIXME/TORU: Use the client *) - let (bls_2_pkh, _, _) = generate_bls_addr client in + let (bls_2_pkh, bls_pk_2, bls_sk_2) = generate_bls_addr client in let bls_pkh_2_str = Bls_public_key_hash.to_b58check bls_2_pkh in + let (bls_3_pkh, _, _) = generate_bls_addr client in + let bls_pkh_3_str = Bls_public_key_hash.to_b58check bls_3_pkh in let arg_1 = make_tx_rollup_deposit_argument tx_rollup_hash bls_pkh_1_str in @@ -557,10 +566,8 @@ let test_l2_to_l2_transaction = let* () = Client.bake_for client in let* _ = Node.wait_for_level node 4 in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in - let* inbox = tx_client_get_inbox ~tx_node ~block:"head" in - let ticket_id = - get_ticket_hash_from_deposit JSON.(inbox |-> "contents" |=> 0) - in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let ticket_id = get_ticket_hash_from_deposit (List.hd inbox.contents) in Log.info "Ticket %s was successfully emitted" ticket_id ; let* () = check_tz4_balance @@ -598,23 +605,23 @@ let test_l2_to_l2_transaction = in Log.info "Crafting a l2 transaction" ; (* FIXME/TORU: Use the client *) - let tx = + let tx1 = craft_tx ~counter:1L ~signer:(Bls_pk bls_pk_1) ~dest:bls_pkh_2_str ~ticket:ticket_id - 1L + 5L in - Log.info "Crafting a batch" ; - let batch = craft_batch [[tx]] [[bls_sk_1]] in + Log.info "Crafting a first batch" ; + let batch = craft_batch [[tx1]] [[bls_sk_1]] in let content = Hex.of_string (Data_encoding.Binary.to_string_exn Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.encoding (Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.V1 batch)) in - Log.info "Submiting a batch" ; + Log.info "Submitting the first batch" ; let*! () = Client.Tx_rollup.submit_batch ~content @@ -622,6 +629,30 @@ let test_l2_to_l2_transaction = ~src:operator client in + Log.info "Crafting a second batch" ; + let tx2 = + craft_tx + ~counter:1L + ~signer:(Bls_pk bls_pk_2) + ~dest:bls_pkh_3_str + ~ticket:ticket_id + 15L + in + let batch = craft_batch [[tx2]] [[bls_sk_2]] in + let content = + Hex.of_string + (Data_encoding.Binary.to_string_exn + Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.encoding + (Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.V1 batch)) + in + Log.info "Submitting the second batch" ; + let*! () = + Client.Tx_rollup.submit_batch + ~content + ~rollup:tx_rollup_hash + ~src:Constant.bootstrap2.public_key_hash + client + in Log.info "Baking the batch" ; let* () = Client.bake_for client in let* _ = Node.wait_for_level node 6 in @@ -630,20 +661,31 @@ let test_l2_to_l2_transaction = line can be uncommented once it is fixed. let* _node_inbox = get_node_inbox tx_node client in *) + (* Having two batches in the same inbox we can test that: + 1. The batches are applied in the correct order + 2. The apply supports multiple batches + *) let* () = check_tz4_balance ~tx_node ~block:"head" ~ticket_id ~tz4_address:bls_pkh_1_str - ~expected_balance:99_999 + ~expected_balance:99_995 and* () = check_tz4_balance ~tx_node ~block:"head" ~ticket_id ~tz4_address:bls_pkh_2_str - ~expected_balance:100_001 + ~expected_balance:99_990 + and* () = + check_tz4_balance + ~tx_node + ~block:"head" + ~ticket_id + ~tz4_address:bls_pkh_3_str + ~expected_balance:15 in unit) @@ -702,10 +744,8 @@ let test_batcher = let* () = Client.bake_for client in let* _ = Node.wait_for_level node 4 in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in - let* inbox = tx_client_get_inbox ~tx_node ~block:"head" in - let ticket_id = - get_ticket_hash_from_deposit JSON.(inbox |-> "contents" |=> 0) - in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let ticket_id = get_ticket_hash_from_deposit (List.hd inbox.contents) in Log.info "Ticket %s was successfully emitted" ticket_id ; let* () = check_tz4_balance @@ -832,18 +872,17 @@ let test_batcher = signature in Log.info "Checking rollup node queue" ; - let* q = tx_client_get_queue ~tx_node in + let* q = Rollup_node.Client.get_queue ~tx_node in let len_q = JSON.(q |> as_list |> List.length) in Check.((len_q = 2) int) ~error_msg:"Queue length is %L but should be %R" ; Log.info "Checking rollup node queue transactions" ; - let* _t1 = tx_client_get_transaction_in_queue ~tx_node txh1 - and* _t2 = tx_client_get_transaction_in_queue ~tx_node txh2 in + let* _t1 = Rollup_node.Client.get_transaction_in_queue ~tx_node txh1 + and* _t2 = Rollup_node.Client.get_transaction_in_queue ~tx_node txh2 in let* () = Client.bake_for client in - let* _ = Node.wait_for_level node 6 in - let* _ = Rollup_node.wait_for_tezos_level tx_node 6 in let* () = Client.bake_for client in - let* _ = Node.wait_for_level node 7 in let* _ = Rollup_node.wait_for_tezos_level tx_node 7 in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + check_inbox_success inbox ; let* () = check_tz4_balance ~tx_node @@ -867,7 +906,10 @@ let test_batcher = let signature = sign_one_transaction sk [tx] in tx_client_inject_transaction ~tx_node [tx] signature in - let nbtxs1 = 13 in + let nbtxs1 = 70 in + let injection_success_promise = + wait_for_injection_success_event tx_node + in Log.info "Injecting %d transactions to queue" nbtxs1 ; let* () = Lwt_list.iter_s @@ -882,7 +924,7 @@ let test_batcher = unit) (List.init nbtxs1 (fun i -> Int64.of_int (i + 2))) in - let nbtxs2 = 6 in + let nbtxs2 = 30 in Log.info "Injecting %d transactions to queue" nbtxs2 ; let* () = Lwt_list.iter_s @@ -897,28 +939,32 @@ let test_batcher = unit) (List.init nbtxs2 (fun i -> Int64.of_int (i + 2))) in - let* q = tx_client_get_queue ~tx_node in + let* q = Rollup_node.Client.get_queue ~tx_node in let len_q = JSON.(q |> as_list |> List.length) in Check.((len_q = nbtxs1 + nbtxs2) int) ~error_msg:"Queue length is %L but should be %R" ; let* () = Client.bake_for client in - let* _ = Rollup_node.wait_for_tezos_level tx_node 8 in + Log.info "Waiting for injection on L1 to succeed" ; + let* () = injection_success_promise in + Log.info "Injection succeeded" ; let* () = Client.bake_for client in let* _ = Rollup_node.wait_for_tezos_level tx_node 9 in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + check_inbox_success inbox ; let* () = check_tz4_balance ~tx_node ~block:"head" ~ticket_id ~tz4_address:bls_pkh_1_str - ~expected_balance:99_997 + ~expected_balance:(100_004 - nbtxs1 + nbtxs2) and* () = check_tz4_balance ~tx_node ~block:"head" ~ticket_id ~tz4_address:bls_pkh_2_str - ~expected_balance:100_003 + ~expected_balance:(99_996 + nbtxs1 - nbtxs2) in unit) @@ -988,10 +1034,8 @@ let test_reorganization = let* () = Client.bake_for client1 in let* _ = Node.wait_for_level node1 4 in let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in - let* inbox = tx_client_get_inbox ~tx_node ~block:"head" in - let ticket_id = - get_ticket_hash_from_deposit JSON.(inbox |-> "contents" |=> 0) - in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let ticket_id = get_ticket_hash_from_deposit (List.hd inbox.contents) in (* Run the node that will be used to forge an alternative branch *) let* node2 = Node.init nodes_args in let* client2 = Client.init ~endpoint:Client.(Node node2) () in @@ -1073,6 +1117,313 @@ let test_reorganization = in unit) +let test_l2_proofs = + Protocol.register_test + ~__FILE__ + ~title:"TX_rollup: proofs l2 transactions" + ~tags:["tx_rollup"; "node"; "proofs"] + (fun protocol -> + let* parameter_file = get_rollup_parameter_file ~protocol in + let* (node, client) = + Client.init_with_protocol ~parameter_file `Client ~protocol () + in + let operator = Constant.bootstrap1.public_key_hash in + let* (tx_rollup_hash, tx_node) = + init_and_run_rollup_node ~operator node client + in + let* contract_id = + Client.originate_contract + ~alias:"rollup_deposit" + ~amount:Tez.zero + ~src:"bootstrap1" + ~prg:"file:./tezt/tests/contracts/proto_alpha/tx_rollup_deposit.tz" + ~init:"Unit" + ~burn_cap:Tez.(of_int 1) + client + in + let* () = Client.bake_for client in + let* _ = Node.wait_for_level node 3 in + Log.info + "The tx_rollup_deposit %s contract was successfully originated" + contract_id ; + (* Generating some identities *) + let (pkh1, pk1, sk1) = generate_bls_addr client in + let pkh1_str = Bls_public_key_hash.to_b58check pkh1 in + let (pkh2, pk2, sk2) = generate_bls_addr client in + let pkh2_str = Bls_public_key_hash.to_b58check pkh2 in + let arg = make_tx_rollup_deposit_argument tx_rollup_hash pkh1_str in + let* () = + Client.transfer + ~gas_limit:100_000 + ~fee:Tez.one + ~amount:Tez.zero + ~burn_cap:Tez.one + ~storage_limit:10_000 + ~giver:"bootstrap1" + ~receiver:contract_id + ~arg + client + in + let* () = Client.bake_for client in + let* _ = Node.wait_for_level node 4 in + let* _ = Rollup_node.wait_for_tezos_level tx_node 4 in + let* inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let ticket_id = get_ticket_hash_from_deposit (List.hd inbox.contents) in + Log.info "Ticket %s was successfully emitted" ticket_id ; + (* TODO: commitment here *) + Log.info "Crafting a commitment for the deposit" ; + let*! inbox_opt = + Rollup.get_inbox ~rollup:tx_rollup_hash ~level:0 client + in + let inbox = Option.get inbox_opt in + let inbox_merkle_root = inbox.merkle_root in + let* rollup_inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let deposit_context_hash = + match rollup_inbox.contents with + | [x] -> x.Rollup_node.Inbox.l2_context_hash.tree_hash + | _ -> assert false + in + let*! deposit_result_hash = + Rollup.message_result_hash + ~context_hash:deposit_context_hash + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + client + in + Log.info "Submit a commitment for the deposit" ; + let*! () = + Client.Tx_rollup.submit_commitment + ~level:0 + ~roots:[deposit_result_hash] + ~inbox_merkle_root + ~predecessor:None + ~rollup:tx_rollup_hash + ~src:Constant.bootstrap3.public_key_hash + client + in + (* FIXME/TORU: Use the client *) + let tx1 = + craft_tx + ~counter:1L + ~signer:(Bls_pk pk1) + ~dest:pkh2_str + ~ticket:ticket_id + 5L + in + let batch1 = craft_batch [[tx1]] [[sk1]] in + let content1 = + Hex.of_string + (Data_encoding.Binary.to_string_exn + Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.encoding + (Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.V1 batch1)) + in + let tx2 = + craft_tx + ~counter:1L + ~signer:(Bls_pk pk2) + ~dest:pkh1_str + ~ticket:ticket_id + 10L + in + let batch2 = craft_batch [[tx2]] [[sk2]] in + let content2 = + Hex.of_string + (Data_encoding.Binary.to_string_exn + Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.encoding + (Tezos_protocol_alpha.Protocol.Tx_rollup_l2_batch.V1 batch2)) + in + Log.info "Submiting two batches" ; + let*! () = + Client.Tx_rollup.submit_batch + ~content:content1 + ~rollup:tx_rollup_hash + ~src:operator + client + in + let*! () = + Client.Tx_rollup.submit_batch + ~content:content2 + ~rollup:tx_rollup_hash + ~src:Constant.bootstrap2.public_key_hash + client + in + Log.info "Baking the batches" ; + let* () = Client.bake_for client in + let* _ = Node.wait_for_level node 5 in + let* _ = Rollup_node.wait_for_tezos_level tx_node 5 in + Log.info "Crafting the commitment" ; + let*! inbox_opt = + Rollup.get_inbox ~rollup:tx_rollup_hash ~level:1 client + in + let inbox = Option.get inbox_opt in + let inbox_merkle_root = inbox.merkle_root in + let* rollup_inbox = Rollup_node.Client.get_inbox ~tx_node ~block:"head" in + let context_hashes = + List.map + (fun x -> x.Rollup_node.Inbox.l2_context_hash.tree_hash) + rollup_inbox.contents + in + let* roots = + Lwt_list.map_p + (fun context_hash -> + let*! root = + Rollup.message_result_hash + ~context_hash + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + client + in + return root) + context_hashes + in + let*! prev_commitment_opt = + Rollup.get_commitment ~rollup:tx_rollup_hash ~level:0 client + in + let prev_commitment = Option.get prev_commitment_opt in + let predecessor = Option.some prev_commitment.commitment_hash in + let*! () = + Client.Tx_rollup.submit_commitment + ~level:1 + ~roots + ~inbox_merkle_root + ~predecessor + ~rollup:tx_rollup_hash + ~src:operator + client + in + let* () = Client.bake_for client in + let* _ = Node.wait_for_level node 6 in + let* _ = Rollup_node.wait_for_tezos_level tx_node 6 in + Log.info "Get the proof for message at position 0" ; + let* proof1 = + Rollup_node.Client.get_merkle_proof + ~tx_node + ~block:"head" + ~message_pos:"0" + in + let proof1_str = JSON.encode proof1 in + let (`Hex content) = content1 in + let message = + Ezjsonm.value_to_string @@ `O [("batch", `String content)] + in + let*! message1_hash = + Rollup.message_hash ~message:(`Batch content1) client + in + let*! message2_hash = + Rollup.message_hash ~message:(`Batch content2) client + in + let message_hashes = [message1_hash; message2_hash] in + Log.info "Trying to reject a valid commitment" ; + let*! message1_path = + Rollup.inbox_merkle_tree_path ~message_hashes ~position:0 client + in + let*! message2_path = + Rollup.inbox_merkle_tree_path ~message_hashes ~position:1 client + in + let message1_path = JSON.encode message1_path in + let message2_path = JSON.encode message2_path in + let message1_context_hash = Stdlib.List.nth context_hashes 0 in + let message2_context_hash = Stdlib.List.nth context_hashes 1 in + let message1_result_hash = Stdlib.List.nth roots 0 in + let message2_result_hash = Stdlib.List.nth roots 1 in + let*! rejected_message1_result_hash = + Rollup.message_result_hash + ~context_hash:message1_context_hash + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + client + in + let*! rejected_message2_result_hash = + Rollup.message_result_hash + ~context_hash:message2_context_hash + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + client + in + let*! rejected_message1_result_path = + Rollup.commitment_merkle_tree_path + ~message_result_hashes: + [`Hash message1_result_hash; `Hash message2_result_hash] + ~position:0 + client + in + let*! rejected_message2_result_path = + Rollup.commitment_merkle_tree_path + ~message_result_hashes: + [`Hash message1_result_hash; `Hash message2_result_hash] + ~position:1 + client + in + let agreed_message1_result_hash = deposit_result_hash in + let*! agreed_message1_result_path = + Rollup.commitment_merkle_tree_path + ~message_result_hashes:[`Hash agreed_message1_result_hash] + ~position:0 + client + in + let*! agreed_message2_result_path = + Rollup.commitment_merkle_tree_path + ~message_result_hashes: + [`Hash message1_result_hash; `Hash message2_result_hash] + ~position:0 + client + in + let*? process = + Client.Tx_rollup.submit_rejection + ~src:operator + ~proof:proof1_str + ~rollup:tx_rollup_hash + ~level:1 + ~message + ~position:0 + ~path:message1_path + ~message_result_hash:rejected_message1_result_hash + ~rejected_message_result_path: + (JSON.encode rejected_message1_result_path) + ~context_hash:deposit_context_hash + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + ~agreed_message_result_path:(JSON.encode agreed_message1_result_path) + client + in + let* () = + Process.check_error + ~msg:(rex "proto.alpha.tx_rollup_proof_produced_rejected_state") + process + in + Log.info "Get the proof for the second message at level 1" ; + let* proof2 = + Rollup_node.Client.get_merkle_proof + ~tx_node + ~block:"head" + ~message_pos:"1" + in + let proof2_str = JSON.encode proof2 in + let (`Hex content) = content2 in + let message = + Ezjsonm.value_to_string @@ `O [("batch", `String content)] + in + Log.info "Trying to reject a valid commitment" ; + + let*? process = + Client.Tx_rollup.submit_rejection + ~src:operator + ~proof:proof2_str + ~rollup:tx_rollup_hash + ~level:1 + ~message + ~position:1 (* OK *) + ~path:message2_path (* OK *) + ~message_result_hash:rejected_message2_result_hash (* OK *) + ~rejected_message_result_path: + (JSON.encode rejected_message2_result_path) (* OK *) + ~context_hash:message1_context_hash (* OK *) + ~withdraw_list_hash:Constant.tx_rollup_empty_withdraw_list + ~agreed_message_result_path:(JSON.encode agreed_message2_result_path) + client + in + let* () = + Process.check_error + ~msg:(rex "proto.alpha.tx_rollup_proof_produced_rejected_state") + process + in + unit) + let register ~protocols = test_node_configuration protocols ; test_tx_node_origination protocols ; @@ -1080,4 +1431,5 @@ let register ~protocols = test_ticket_deposit_from_l1_to_l2 protocols ; test_l2_to_l2_transaction protocols ; test_batcher protocols ; - test_reorganization protocols + test_reorganization protocols ; + test_l2_proofs protocols