diff --git a/src/bin_dal_node/RPC_server.ml b/src/bin_dal_node/RPC_server.ml index f5b75e17aea601a5030988be8a0a79479ab65e8d..6988c994b760bc872c8ba3fd99f470cf4864c201 100644 --- a/src/bin_dal_node/RPC_server.ml +++ b/src/bin_dal_node/RPC_server.ml @@ -87,6 +87,16 @@ module Slots_handlers = struct | Error (Ok `Not_found) -> return_none | Error (Error e) -> fail e) ctxt + + let get_commitment_headers ctxt commitment (slot_level, slot_index) () = + call_handler + (fun store _cryptobox -> + Slot_manager.get_commitment_headers + commitment + ?slot_level + ?slot_index + store) + ctxt end module Profile_handlers = struct @@ -136,6 +146,10 @@ let register_new : Tezos_rpc.Directory.register0 Services.get_profiles (Profile_handlers.get_profiles ctxt) + |> add_service + Tezos_rpc.Directory.register1 + Services.get_commitment_headers + (Slots_handlers.get_commitment_headers ctxt) let register_legacy ctxt = let open RPC_server_legacy in diff --git a/src/bin_dal_node/slot_manager.ml b/src/bin_dal_node/slot_manager.ml index 993c1e68c0b713083e8e9277df35054c040ba5ec..3c677c05b29d8118c4a654ad60d427047bec65a0 100644 --- a/src/bin_dal_node/slot_manager.ml +++ b/src/bin_dal_node/slot_manager.ml @@ -114,3 +114,10 @@ let get_commitment_by_published_level_and_index ~level ~slot_index node_store = ~level ~slot_index node_store + +let get_commitment_headers commitment ?slot_level ?slot_index node_store = + Store.Legacy.get_commitment_headers + commitment + ?slot_level + ?slot_index + node_store diff --git a/src/bin_dal_node/slot_manager.mli b/src/bin_dal_node/slot_manager.mli index a3df07961a7344952a9e7cf01439eea03de9973f..2e2015fb9b0e9ce04a462f6b020cfd9d799d3600 100644 --- a/src/bin_dal_node/slot_manager.mli +++ b/src/bin_dal_node/slot_manager.mli @@ -104,3 +104,13 @@ val get_commitment_by_published_level_and_index : slot_index:Services.Types.slot_index -> Store.node_store -> (Cryptobox.commitment, [`Not_found] tzresult) result Lwt.t + +(** [get_commitment_headers commitment ?slot_level ?slot_index store] returns + the list of accepted slot headers {!Services.Types.slot_header} that are + known by the DAL together with their respective statuses. *) +val get_commitment_headers : + Cryptobox.commitment -> + ?slot_level:Services.Types.level -> + ?slot_index:Services.Types.slot_index -> + Store.node_store -> + Services.Types.slot_header list tzresult Lwt.t diff --git a/src/bin_dal_node/store.ml b/src/bin_dal_node/store.ml index 24806324a56599cd7104c8fca5935f3566dc5dd3..634e35fb227a13b8ef6643c7a12eeb4cee8be30d 100644 --- a/src/bin_dal_node/store.ml +++ b/src/bin_dal_node/store.ml @@ -140,9 +140,7 @@ module Legacy = struct let header commitment index = let open Services.Types in let prefix = headers commitment in - prefix - / Int32.to_string index.slot_level - / Int.to_string index.slot_index + prefix / Data_encoding.Binary.to_string_exn slot_id_encoding index let shards commitment = let commitment_repr = Cryptobox.Commitment.to_b58check commitment in @@ -256,6 +254,9 @@ module Legacy = struct let decode_commitment = Cryptobox.Commitment.of_b58check_opt + let decode_slot_id = + Data_encoding.Binary.of_string_exn Services.Types.slot_id_encoding + let add_slot_headers ~block_level ~block_hash slot_headers node_store = let open Lwt_syntax in let* () = legacy_add_slot_headers ~block_hash slot_headers node_store in @@ -271,11 +272,17 @@ module Legacy = struct in (* This invariant should hold. *) assert (Int32.equal published_level block_level) ; + let index = Services.Types.{slot_level = published_level; slot_index} in + let header_path = Path.Commitment.header commitment index in + let* () = + set + ~msg:(Path.to_string ~prefix:"add_slot_headers:" header_path) + slots_store + header_path + "" + in match status with | Dal_plugin.Succeeded -> - let index = - Services.Types.{slot_level = published_level; slot_index} - in let commitment_path = Path.Level.accepted_header_commitment index in let status_path = Path.Level.accepted_header_status index in let data = encode_commitment commitment in @@ -294,9 +301,6 @@ module Legacy = struct (Services.Types.header_attestation_status_to_string `Waiting_for_attestations) | Dal_plugin.Failed -> - let index = - Services.Types.{slot_level = published_level; slot_index} - in let path = Path.Level.other_header_status index commitment in set ~msg:(Path.to_string ~prefix:"add_slot_headers:" path) @@ -375,4 +379,79 @@ module Legacy = struct node_store.store path "" + + (** Filter the given list of indices according to the values of the given slot + level and index. *) + let filter_indexes = + let keep_field v = function None -> true | Some f -> f = v in + fun ?slot_level ?slot_index indexes -> + List.map (fun (slot_id, _) -> decode_slot_id slot_id) indexes + |> List.filter (fun {Services.Types.slot_level = l; slot_index = i} -> + keep_field l slot_level && keep_field i slot_index) + + let get_accepted_headers_of_commitment commitment indices store accu = + let open Lwt_result_syntax in + let encoded_commitment = encode_commitment commitment in + List.fold_left_es + (fun acc slot_id -> + let commitment_path = Path.Level.accepted_header_commitment slot_id in + let*! commitment_opt = find store commitment_path in + match commitment_opt with + | None -> return acc + | Some read_commitment -> ( + if not @@ String.equal read_commitment encoded_commitment then + return acc + else + let status_path = Path.Level.accepted_header_status slot_id in + let*! status_opt = find store status_path in + match status_opt with + | None -> return acc + | Some status_str -> ( + (* TODO: https://gitlab.com/tezos/tezos/-/issues/4466 + Use more compact encodings to reduce allocated storage. *) + match + Services.Types.header_attestation_status_of_string + status_str + with + | None -> failwith "Attestation status decoding failed" + | Some status -> + return + @@ { + Services.Types.slot_id; + commitment; + status = (status :> Services.Types.header_status); + } + :: acc))) + accu + indices + + let get_other_headers_of_commitment commitment indices store accu = + let open Lwt_result_syntax in + List.fold_left_es + (fun acc slot_id -> + let*! status_opt = + find store @@ Path.Level.other_header_status slot_id commitment + in + match status_opt with + | None -> return acc + | Some status_str -> ( + match Services.Types.header_status_of_string status_str with + | None -> failwith "Attestation status decoding failed" + | Some status -> + return @@ ({Services.Types.slot_id; commitment; status} :: acc))) + accu + indices + + let get_commitment_headers commitment ?slot_level ?slot_index node_store = + let open Lwt_result_syntax in + let store = node_store.store in + (* Get the list of known slot identifiers for [commitment]. *) + let*! indexes = list store @@ Path.Commitment.headers commitment in + (* Filter the list of indices by the values of [slot_level] [slot_index]. *) + let indices = filter_indexes ?slot_level ?slot_index indexes in + (* Retrieve the headers that haven't been "accepted" on L1. *) + let* accu = get_other_headers_of_commitment commitment indices store [] in + (* Retrieve the headers that have "accepted" on L1 (i.e. with + [`Waiting_for_attestation], [`Attested] or [`Unattested] statuses). *) + get_accepted_headers_of_commitment commitment indices store accu end diff --git a/src/lib_dal_node_services/services.ml b/src/lib_dal_node_services/services.ml index 07b5e404e551ee9fde3efd8c2862fc3479122247..4b88c42520413d5c2e35f508651a9d6780667274 100644 --- a/src/lib_dal_node_services/services.ml +++ b/src/lib_dal_node_services/services.ml @@ -50,22 +50,44 @@ module Types = struct type header_status = [`Not_selected | `Unseen | header_attestation_status] + type slot_header = { + slot_id : slot_id; + commitment : Cryptobox.Commitment.t; + status : header_status; + } + (* TODO: https://gitlab.com/tezos/tezos/-/issues/4442 Add missing profiles. *) type profile = Attestor of Tezos_crypto.Signature.public_key_hash (* Auxiliary functions. *) + let header_attestation_statuses = + [`Waiting_for_attestations; `Attested; `Unattested] + let header_attestation_status_to_string = function | `Waiting_for_attestations -> "waiting_for_attestations" | `Attested -> "attested" | `Unattested -> "unattested" + let header_attestation_status_of_string = function + | "waiting_for_attestations" -> Some `Waiting_for_attestations + | "attested" -> Some `Attested + | "unattested" -> Some `Unattested + | _ -> None + + let header_statuses = `Not_selected :: `Unseen :: header_attestation_statuses + let header_status_to_string = function | `Not_selected -> "not_selected" | `Unseen -> "unseen" | #header_attestation_status as e -> header_attestation_status_to_string e + let header_status_of_string = function + | "not_selected" -> Some `Not_selected + | "unseen" -> Some `Unseen + | s -> header_attestation_status_of_string s + (* Encodings associated to the types. *) let slot_id_encoding = @@ -79,6 +101,22 @@ module Types = struct let slot_encoding = Data_encoding.bytes + let header_status_encoding = + let open Data_encoding in + string_enum + (List.map (fun t -> (header_status_to_string t, t)) header_statuses) + + let slot_header_encoding = + let open Data_encoding in + conv + (fun {slot_id; commitment; status} -> (slot_id, (commitment, status))) + (fun (slot_id, (commitment, status)) -> {slot_id; commitment; status}) + (merge_objs + slot_id_encoding + (obj2 + (req "commitment" Cryptobox.Commitment.encoding) + (req "status" header_status_encoding))) + let equal_profile (Attestor p1) (Attestor p2) = Tezos_crypto.Signature.Public_key_hash.( = ) p1 p2 @@ -97,6 +135,16 @@ module Types = struct (function Attestor attest -> Some ((), attest)) (function (), attest -> Attestor attest); ] + + (* String parameters queries. *) + + let slot_id_query = + let open Tezos_rpc in + let open Query in + query (fun slot_level slot_index -> (slot_level, slot_index)) + |+ opt_field "slot_level" Arg.int32 fst + |+ opt_field "slot_index" Arg.int snd + |> seal end let post_slots : @@ -179,6 +227,22 @@ let get_commitment_by_published_level_and_index : open_root / "levels" /: Tezos_rpc.Arg.int32 / "slot_indices" /: Tezos_rpc.Arg.int / "commitment") +let get_commitment_headers : + < meth : [`GET] + ; input : unit + ; output : Types.slot_header list + ; prefix : unit + ; params : unit * Cryptobox.commitment + ; query : Types.level option * Types.slot_index option > + service = + Tezos_rpc.Service.get_service + ~description: + "Return the known headers for the the slot whose commitment is given." + ~query:Types.slot_id_query + ~output:(Data_encoding.list Types.slot_header_encoding) + Tezos_rpc.Path.( + open_root / "commitments" /: Cryptobox.Commitment.rpc_arg / "headers") + let patch_profile : < meth : [`PATCH] ; input : Types.profile diff --git a/src/lib_dal_node_services/services.mli b/src/lib_dal_node_services/services.mli index 0a489dac732348d161d3bf2c2297b16517a2e17e..7525264f8fec856cb8e9a94332c5073865f30cc4 100644 --- a/src/lib_dal_node_services/services.mli +++ b/src/lib_dal_node_services/services.mli @@ -77,19 +77,36 @@ module Types : sig (** The slot header was successfully included in a block. see {!header_attestation_status} for more details. *) - (** DAL node can track one or many profiles that correspond to various modes + (** DAL node can track one or many profiles that correspond to various modes that the DAL node would operate in *) type profile = Attestor of Tezos_crypto.Signature.public_key_hash + (** Information associated to a slot header in the RPC services of the DAL + node. *) + type slot_header = { + slot_id : slot_id; + commitment : Cryptobox.Commitment.t; + status : header_status; + } + val slot_id_encoding : slot_id Data_encoding.t (** Return the string representation for values of type {!header_attestation_status}. *) val header_attestation_status_to_string : header_attestation_status -> string + (** Convert the string to the correponding value of type + {!header_attestation_status}. Return {!None} in case of mismatch. *) + val header_attestation_status_of_string : + string -> header_attestation_status option + (** Return the string representation for values of type {!header_status}. *) val header_status_to_string : header_status -> string + (** Convert the string to the correponding value of type + {!header_status}. Return {!None} in case of mismatch. *) + val header_status_of_string : string -> header_status option + val profile_encoding : profile Data_encoding.t val equal_profile : profile -> profile -> bool @@ -149,6 +166,16 @@ val get_commitment_by_published_level_and_index : ; query : unit > service +(** Return the known headers for the slot whose commitment is given. *) +val get_commitment_headers : + < meth : [`GET] + ; input : unit + ; output : Types.slot_header list + ; prefix : unit + ; params : unit * Cryptobox.commitment + ; query : Types.level option * Types.slot_index option > + service + (** Update the list of profiles tracked by the DAL node *) val patch_profile : < meth : [`PATCH] diff --git a/tezt/lib_tezos/rollup.ml b/tezt/lib_tezos/rollup.ml index 033a79bc4ef7c65dd926e58caa52975dbaab02b4..142a8c0a772c2c47d6ff4e35c43f8b317599a9c5 100644 --- a/tezt/lib_tezos/rollup.ml +++ b/tezt/lib_tezos/rollup.ml @@ -675,6 +675,25 @@ module Dal = struct type profile = Attestor of string + type slot_header = { + slot_level : int; + slot_index : int; + commitment : string; + status : string; + } + + let slot_header_of_json json = + let open JSON in + { + slot_level = json |-> "slot_level" |> as_int; + slot_index = json |-> "slot_index" |> as_int; + commitment = json |-> "commitment" |> as_string; + status = json |-> "status" |> as_string; + } + + let slot_headers_of_json json = + JSON.as_list json |> List.map slot_header_of_json + let as_empty_object_or_fail t = match JSON.as_object t with | [] -> () @@ -737,6 +756,20 @@ module Dal = struct make ~data PATCH ["profiles"] as_empty_object_or_fail let get_profiles () = make GET ["profiles"] profiles_of_json + + let mk_query_arg field v_opt = + Option.fold ~none:[] ~some:(fun v -> [(field, string_of_int v)]) v_opt + + let get_commitment_headers ?slot_level ?slot_index commitment = + let query_string = + mk_query_arg "slot_level" slot_level + @ mk_query_arg "slot_index" slot_index + in + make + ~query_string + GET + ["commitments"; commitment; "headers"] + slot_headers_of_json end let make diff --git a/tezt/lib_tezos/rollup.mli b/tezt/lib_tezos/rollup.mli index 5e5b0fc26b708307de750d276b3ab5f6444cb867..245d443499f99517622c9f8f3d33ad17b9628172 100644 --- a/tezt/lib_tezos/rollup.mli +++ b/tezt/lib_tezos/rollup.mli @@ -297,6 +297,22 @@ module Dal : sig type profile = Attestor of string + (** Information contained in a slot header fetched from the DAL node. *) + type slot_header = { + slot_level : int; + slot_index : int; + commitment : string; + status : string; + } + + (** [slot_header_of_json json] decodes [json] as a slot header. The function + fails if the given [json] cannot be decoded. *) + val slot_header_of_json : JSON.t -> slot_header + + (** [slot_header_of_json json_] similar to {!slot_header_of_json}, but + the input (and output) is expected to be a list. *) + val slot_headers_of_json : JSON.t -> slot_header list + (** Call RPC "POST /slots" to store a slot and retrun the commitment in case of success. *) val post_slot : slot -> (Dal_node.t, commitment) RPC_core.t @@ -332,6 +348,15 @@ module Dal : sig (** Call RPC "GET /profiles" to retrieve the list of profiles tracked by the DAL node. *) val get_profiles : unit -> (Dal_node.t, profile list) RPC_core.t + + (** Call RPC "GET /commitments//headers" to get the header and + status know about the given commitment. The resulting list can be filtered by a + given header publication level and slot index. *) + val get_commitment_headers : + ?slot_level:int -> + ?slot_index:int -> + commitment -> + (Dal_node.t, slot_header list) RPC_core.t end val make : diff --git a/tezt/tests/dal.ml b/tezt/tests/dal.ml index 3011f50051804d3bc3c4f0f32753064f4596e52d..50945fb427935d18d77e2b200a90cd9e44f829bc 100644 --- a/tezt/tests/dal.ml +++ b/tezt/tests/dal.ml @@ -444,6 +444,14 @@ let dal_attestation ?level ?(force = false) ~signer ~nb_slots availability Operation.Consensus.( inject ~force ~signer (dal_attestation ~level ~attestation) client) +let dal_attestations ?level ?force + ?(signers = Array.to_list Account.Bootstrap.keys) ~nb_slots availability + client = + Lwt_list.map_s + (fun signer -> + dal_attestation ?level ?force ~signer ~nb_slots availability client) + signers + type status = Applied | Failed of {error_id : string} let pp fmt = function @@ -635,14 +643,19 @@ let test_slot_management_logic _protocol parameters cryptobox node client check_manager_operation_status operations_result Applied oph2 ; let nb_slots = parameters.Rollup.Dal.Parameters.number_of_slots in let* _ = - dal_attestation ~nb_slots ~signer:Constant.bootstrap1 [1; 0] client + dal_attestations + ~nb_slots + ~signers:[Constant.bootstrap1; Constant.bootstrap2] + [1; 0] + client in let* _ = - dal_attestation ~nb_slots ~signer:Constant.bootstrap2 [1; 0] client + dal_attestations + ~nb_slots + ~signers:[Constant.bootstrap3; Constant.bootstrap4; Constant.bootstrap5] + [1] + client in - let* _ = dal_attestation ~nb_slots ~signer:Constant.bootstrap3 [1] client in - let* _ = dal_attestation ~nb_slots ~signer:Constant.bootstrap4 [1] client in - let* _ = dal_attestation ~nb_slots ~signer:Constant.bootstrap5 [1] client in let* () = Client.bake_for_and_wait client in let* metadata = RPC.call node (RPC.get_chain_block_metadata ()) in let attestation = @@ -762,15 +775,9 @@ let test_slots_attestation_operation_behavior _protocol parameters cryptobox let* () = Client.bake_for_and_wait client in let now = Node.get_level node in let* attestation_ops = - let open Constant in let level = now + lag in - Lwt_list.map_s - (fun signer -> - let* (`OpHash h) = - dal_attestation ~force:true ~nb_slots ~level ~signer [10] client - in - return h) - [bootstrap1; bootstrap2; bootstrap3; bootstrap4; bootstrap5] + let* hashes = dal_attestations ~force:true ~nb_slots ~level [10] client in + return @@ List.map (fun (`OpHash h) -> h) hashes in let applied = [] in let branch_delayed = attestation_ops in @@ -889,7 +896,7 @@ let publish_and_store_slot ?(fee = 1_200) node client dal_node source index let* _ = publish_slot ~source ~fee ~index ~commitment ~proof node client in return (index, slot_commitment) -let check_get_commitment ~check_result dal_node ~slot_level slots_info = +let check_get_commitment dal_node ~slot_level check_result slots_info = Lwt_list.iter_s (fun (slot_index, commitment') -> let* response = @@ -897,11 +904,53 @@ let check_get_commitment ~check_result dal_node ~slot_level slots_info = dal_node (Rollup.Dal.RPC.get_level_index_commitment ~slot_index ~slot_level) in - check_result commitment' response ; - unit) + return @@ check_result commitment' response) slots_info -let test_dal_node_slots_headers_tracking _protocol _parameters _cryptobox node +let get_commitment_succeeds expected_commitment response = + let commitment = + JSON.(parse ~origin:__LOC__ response.RPC_core.body |> as_string) + in + Check.(commitment = expected_commitment) + Check.string + ~error_msg: + "The value of a stored commitment should match the one computed locally \ + (current = %L, expected = %R)" + +let get_commitment_not_found _commit r = RPC.check_string_response ~code:404 r + +let check_get_commitment_headers dal_node ~slot_level check_result slots_info = + let test check_result ~query_string (slot_index, commit) = + let slot_level, slot_index = + if not query_string then (None, None) + else (Some slot_level, Some slot_index) + in + let* response = + RPC.call_raw + dal_node + (Rollup.Dal.RPC.get_commitment_headers ?slot_index ?slot_level commit) + in + return @@ check_result response + in + let* () = Lwt_list.iter_s (test check_result ~query_string:true) slots_info in + Lwt_list.iter_s (test check_result ~query_string:false) slots_info + +let get_headers_succeeds expected_status response = + let headers = + JSON.( + parse ~origin:"get_headers_succeeds" response.RPC_core.body + |> Rollup.Dal.RPC.slot_headers_of_json) + in + List.iter + (fun header -> + Check.(header.Rollup.Dal.RPC.status = expected_status) + Check.string + ~error_msg: + "The value of the fetched status should match the expected \ + one(current = %L, expected = %R)") + headers + +let test_dal_node_slots_headers_tracking _protocol parameters _cryptobox node client dal_node = let publish ?fee = publish_and_store_slot ?fee node client dal_node in let* slot0 = publish Constant.bootstrap1 0 "test0" in @@ -930,44 +979,62 @@ let test_dal_node_slots_headers_tracking _protocol _parameters _cryptobox node "Published header is different from stored header (current = %L, \ expected = %R)" ; let slot_level = Node.get_level node in + let check_get_commitment_headers = + check_get_commitment_headers dal_node ~slot_level + in + let check_get_commitment = check_get_commitment dal_node ~slot_level in let ok = [slot0; slot1; slot2_b] in - ignore slot2_a ; + let attested = [slot0; slot2_b] in + let unattested = [slot1] in let ko = slot3 :: List.map (fun (i, c) -> (i + 100, c)) ok in - (* Aux function to check succesful RPC calls. *) - let result_ok commitment' response = - let commitment = - JSON.( - parse - ~origin:"test_dal_node_slots_headers_tracking" - response.RPC_core.body - |> as_string) - in - Check.(commitment' = commitment) - Check.string - ~error_msg: - "The value of a stored commitment should match the one computed \ - locally (current = %L, expected = %R)" - in - (* Aux function to check RPC calls that are expected to fail with 404. *) - let result_ko _commitment' response = - RPC.check_string_response ~code:404 response - in - (* Slots waiting for confirmation. *) + + (* Slots waiting for attestation. *) + let* () = check_get_commitment get_commitment_succeeds ok in let* () = - check_get_commitment ~check_result:result_ok dal_node ~slot_level ok + check_get_commitment_headers + (get_headers_succeeds "waiting_for_attestations") + ok in - (* Slots not selected/accepted. *) + (* slot_2_a is not selected. *) let* () = - check_get_commitment ~check_result:result_ko dal_node ~slot_level ko + check_get_commitment_headers (get_headers_succeeds "not_selected") [slot2_a] in + (* Slots not published or not included in blocks. *) + let* () = check_get_commitment get_commitment_not_found ko in + + (* Attest slots slot0 = (2, 0) and slot2_b = (2,4), bake and wait + two seconds so that the DAL node updates its state. *) + let nb_slots = parameters.Rollup.Dal.Parameters.number_of_slots in + let* _op_hash = dal_attestations ~nb_slots (List.map fst attested) client in let* () = Client.bake_for_and_wait client in + let* () = Lwt_unix.sleep 2.0 in + (* Slot confirmed. *) + let* () = check_get_commitment get_commitment_succeeds ok in + (* Slot that were waiting for attestation and now attested. *) + let* () = + check_get_commitment_headers (get_headers_succeeds "attested") attested + in + (* Slots not published or not included in blocks. *) + let* () = check_get_commitment get_commitment_not_found ko in + (* Slot that were waiting for attestation and now unattested. *) let* () = - check_get_commitment ~check_result:result_ok dal_node ~slot_level ok + check_get_commitment_headers (get_headers_succeeds "unattested") unattested in - (* Slots still not selected/accepted. *) + (* slot_2_a is still not selected. *) let* () = - check_get_commitment ~check_result:result_ko dal_node ~slot_level ko + check_get_commitment_headers (get_headers_succeeds "not_selected") [slot2_a] + in + (* slot_3 never finished in an L1 block, so the DAL node never tracked it + (except when its content has been injected). *) + let* () = + check_get_commitment_headers + (fun response -> + Check.( + (String.trim response.RPC_core.body = "[]") + string + ~error_msg:"Slot3 is not expected to be indexed by DAL node")) + [slot3] in unit @@ -1341,21 +1408,7 @@ let rollup_node_stores_dal_slots ?expand_test _protocol dal_node sc_rollup_node (* 6. attest only slots 1 and 2. *) let* parameters = Rollup.Dal.Parameters.from_client client in let nb_slots = parameters.number_of_slots in - let* _op_hash = - dal_attestation ~nb_slots ~signer:Constant.bootstrap1 [2; 1] client - in - let* _op_hash = - dal_attestation ~nb_slots ~signer:Constant.bootstrap2 [2; 1] client - in - let* _op_hash = - dal_attestation ~nb_slots ~signer:Constant.bootstrap3 [2; 1] client - in - let* _op_hash = - dal_attestation ~nb_slots ~signer:Constant.bootstrap4 [2; 1] client - in - let* _op_hash = - dal_attestation ~nb_slots ~signer:Constant.bootstrap5 [2; 1] client - in + let* _ops_hashes = dal_attestations ~nb_slots [2; 1] client in let* () = Client.bake_for_and_wait client in let* slot_confirmed_level = Sc_rollup_node.wait_for_level sc_rollup_node (slots_published_level + 1)