From 2f076e2f8058e3ae973877966953373cdd9749dc Mon Sep 17 00:00:00 2001 From: Emma Turner Date: Tue, 6 Sep 2022 15:43:50 +0100 Subject: [PATCH] SCORU: Wasm PVM: implement store_list_size --- src/lib_scoru_wasm/host_funcs.ml | 32 ++++++++ src/lib_scoru_wasm/host_funcs.mli | 2 + .../test/test_durable_storage.ml | 72 ++++++++++++++++++ src/lib_scoru_wasm/test/test_wasm_pvm.ml | 70 ++++++++++++++--- .../test/wasm_kernels/README.md | 12 +++ .../wasm_kernels/test-store-list-size.wasm | Bin 0 -> 21145 bytes 6 files changed, 177 insertions(+), 11 deletions(-) create mode 100755 src/lib_scoru_wasm/test/wasm_kernels/test-store-list-size.wasm diff --git a/src/lib_scoru_wasm/host_funcs.ml b/src/lib_scoru_wasm/host_funcs.ml index 375da506b874..7a6555f519ed 100644 --- a/src/lib_scoru_wasm/host_funcs.ml +++ b/src/lib_scoru_wasm/host_funcs.ml @@ -247,6 +247,32 @@ let store_has = (durable, [Values.(Num (I32 (I32.of_int_s r)))]) | _ -> raise Bad_input) +let store_list_size_name = "tezos_store_list_size" + +let store_list_size_type = + let input_types = + Types.[NumType I32Type; NumType I32Type] |> Vector.of_list + in + let output_types = Types.[NumType I64Type] |> Vector.of_list in + Types.FuncType (input_types, output_types) + +let store_list_size = + Host_funcs.Host_func + (fun _input_buffer _output_buffer durable memories inputs -> + let open Lwt.Syntax in + match inputs with + | [Values.(Num (I32 key_offset)); Values.(Num (I32 key_length))] -> + let key_length = Int32.to_int key_length in + if key_length > Durable.max_key_length then + raise (Key_too_large key_length) ; + let* memory = retrieve_memory memories in + let* key = Memory.load_bytes memory key_offset key_length in + let tree = Durable.of_storage_exn durable in + let key = Durable.key_of_string_exn key in + let+ num_subtrees = Durable.count_subtrees tree key in + (durable, [Values.(Num (I64 (I64.of_int_s num_subtrees)))]) + | _ -> raise Bad_input) + let lookup_opt name = match name with | "read_input" -> @@ -256,6 +282,8 @@ let lookup_opt name = | "write_debug" -> Some (ExternFunc (HostFunc (write_debug_type, write_debug_name))) | "store_has" -> Some (ExternFunc (HostFunc (store_has_type, store_has_name))) + | "store_list_size" -> + Some (ExternFunc (HostFunc (store_list_size_type, store_list_size_name))) | _ -> None let lookup name = @@ -271,6 +299,7 @@ let register_host_funcs registry = (write_output_name, write_output); (write_debug_name, write_debug); (store_has_name, store_has); + (store_list_size_name, store_list_size); ] module Internal_for_tests = struct @@ -283,4 +312,7 @@ module Internal_for_tests = struct let read_input = Func.HostFunc (read_input_type, read_input_name) let store_has = Func.HostFunc (store_has_type, store_has_name) + + let store_list_size = + Func.HostFunc (store_list_size_type, store_list_size_name) end diff --git a/src/lib_scoru_wasm/host_funcs.mli b/src/lib_scoru_wasm/host_funcs.mli index 1b6fdee5f133..57e5d0251766 100644 --- a/src/lib_scoru_wasm/host_funcs.mli +++ b/src/lib_scoru_wasm/host_funcs.mli @@ -104,4 +104,6 @@ module Internal_for_tests : sig - [3]: There is a value at [key], and subtrees under [key]. *) val store_has : Tezos_webassembly_interpreter.Instance.func_inst + + val store_list_size : Tezos_webassembly_interpreter.Instance.func_inst end diff --git a/src/lib_scoru_wasm/test/test_durable_storage.ml b/src/lib_scoru_wasm/test/test_durable_storage.ml index f9f898802ef5..2a538a1e409e 100644 --- a/src/lib_scoru_wasm/test/test_durable_storage.ml +++ b/src/lib_scoru_wasm/test/test_durable_storage.ml @@ -150,6 +150,77 @@ let test_store_has_key_too_long () = assert (result = Values.[Num (I32 (I32.of_int_s 0))]) ; Lwt.return_ok () +(* Test checking that [store_list_size key] returns the number of immediate + subtrees. *) +let test_store_list_size () = + let open Lwt_syntax in + let* tree = empty_tree () in + let value () = Chunked_byte_vector.of_string "a very long value" in + (* + Store the following tree: + + /durable/a/short/path/_ = "..." + /durable/a/short/path/one/_ = "..." + /durable/a/short/path/two/_ = "..." + + We expect that the list size of "/a/short/path" is 2. + + Note that the value of "/durable/a/short/path/_" is not included in the listing. + *) + let key = "/a/short/path" in + let key_steps = ["a"; "short"; "path"] in + let* tree = + Tree_encoding_runner.encode + (Tree_encoding.scope + ("durable" :: List.append key_steps ["_"]) + Tree_encoding.chunked_byte_vector) + (value ()) + tree + in + let* tree = + Tree_encoding_runner.encode + (Tree_encoding.scope + ("durable" :: List.append key_steps ["one"; "_"]) + Tree_encoding.chunked_byte_vector) + (value ()) + tree + in + let* tree = + Tree_encoding_runner.encode + (Tree_encoding.scope + ("durable" :: List.append key_steps ["two"; "_"]) + Tree_encoding.chunked_byte_vector) + (value ()) + tree + in + let* durable = wrap_as_durable_storage tree in + let module_inst = Tezos_webassembly_interpreter.Instance.empty_module_inst in + let memory = Memory.alloc (MemoryType Types.{min = 20l; max = Some 3600l}) in + let src = 20l in + let _ = Memory.store_bytes memory src key in + let memories = Lazy_vector.Int32Vector.cons memory module_inst.memories in + let module_inst = {module_inst with memories} in + let host_funcs_registry = Tezos_webassembly_interpreter.Host_funcs.empty () in + Host_funcs.register_host_funcs host_funcs_registry ; + + let module_reg = Instance.ModuleMap.create () in + let module_key = Instance.Module_key "test" in + Instance.update_module_ref module_reg module_key module_inst ; + let values = + Values.[Num (I32 src); Num (I32 (Int32.of_int @@ String.length key))] + in + let* _, result = + Eval.invoke + ~module_reg + ~caller:module_key + ~durable + host_funcs_registry + Host_funcs.Internal_for_tests.store_list_size + values + in + assert (result = Values.[Num (I64 (I64.of_int_s 2))]) ; + Lwt.return_ok () + (* Test checking that if [key] has value/subtree, [store_has key] returns the correct enum value. *) let test_store_has_existing_key () = @@ -327,6 +398,7 @@ let tests = tztest "store_has missing key" `Quick test_store_has_missing_key; tztest "store_has existing key" `Quick test_store_has_existing_key; tztest "store_has key too long key" `Quick test_store_has_key_too_long; + tztest "store_list_size counts subtrees" `Quick test_store_list_size; tztest "Durable: find value" `Quick test_durable_find_value; tztest "Durable: count subtrees" `Quick test_durable_count_subtrees; tztest "Durable: invalid keys" `Quick test_durable_invalid_keys; diff --git a/src/lib_scoru_wasm/test/test_wasm_pvm.ml b/src/lib_scoru_wasm/test/test_wasm_pvm.ml index 90b87dc85792..55b39ce6dfa8 100644 --- a/src/lib_scoru_wasm/test/test_wasm_pvm.ml +++ b/src/lib_scoru_wasm/test/test_wasm_pvm.ml @@ -52,6 +52,16 @@ let test_write_debug_kernel = "test-write-debug" *) let test_store_has_kernel = "test-store-has" +(* Kernel checking the return value of store_list_size host func. + + This kernel expects a collection of values to exist: + - `/durable/one/two` + - `/durable/one/three` + - `/durable/one/four` + and asserts that `store_list_size(/one) = 3`. +*) +let test_store_list_size_kernel = "test-store-list-size" + (** [check_error kind reason error] checks a Wasm PVM error [error] is of a given [kind] with a possible [reason]. @@ -181,20 +191,20 @@ let should_run_debug_kernel kernel = (* The kernel should not fail. *) assert (not @@ is_stuck state_after_first_message) +let add_value tree key_steps = + let open Lazy_containers in + let open Test_encodings_util in + let value = Chunked_byte_vector.of_string "a very long value" in + Tree_encoding_runner.encode + (Tree_encoding.scope + ("durable" :: List.append key_steps ["_"]) + Tree_encoding.chunked_byte_vector) + value + tree + let should_run_store_has_kernel kernel = let open Lwt_syntax in let* tree = initial_boot_sector_from_kernel kernel in - let add_value tree key_steps = - let open Lazy_containers in - let open Test_encodings_util in - let value = Chunked_byte_vector.of_string "a very long value" in - Tree_encoding_runner.encode - (Tree_encoding.scope - ("durable" :: List.append key_steps ["_"]) - Tree_encoding.chunked_byte_vector) - value - tree - in let* tree = add_value tree ["hi"; "bye"] in let* tree = add_value tree ["hello"] in let* tree = add_value tree ["hello"; "universe"] in @@ -220,6 +230,38 @@ let should_run_store_has_kernel kernel = (* The kernel is now expected to fail, the PVM should be in stuck state. *) assert (is_stuck state_after_first_message) +let should_run_store_list_size_kernel kernel = + let open Lwt_syntax in + let* tree = initial_boot_sector_from_kernel kernel in + let* tree = add_value tree ["one"; "two"] in + let* tree = add_value tree ["one"; "three"] in + let* tree = add_value tree ["one"; "four"] in + (* Make the first ticks of the WASM PVM (parsing of origination + message, parsing and init of the kernel), to switch it to + “Input_requested” mode. *) + let* tree = eval_until_input_requested tree in + (* Feeding it with one input *) + let* tree = set_input_step "test" 0 tree in + (* Adding a value at ["one"] should not affect the count. *) + let* tree = add_value tree ["one"] in + (* running until waiting for input *) + let* tree = eval_until_input_requested tree in + let* state_after_first_message = + Wasm.Internal_for_tests.get_tick_state tree + in + (* The kernel is not expected to fail, the PVM should not be in stuck state. *) + assert (not @@ is_stuck state_after_first_message) ; + (* We now add another value - this will cause the kernel + assertion on this path to fail, as there are now four subtrees. *) + let* tree = set_input_step "test" 1 tree in + let* tree = add_value tree ["one"; "five"] in + let* tree = eval_until_input_requested tree in + let+ state_after_second_message = + Wasm.Internal_for_tests.get_tick_state tree + in + (* The kernel is now expected to fail, the PVM should be in stuck state. *) + assert (is_stuck state_after_second_message) + let test_with_kernel kernel test () = let open Lwt_result_syntax in let open Tezt.Base in @@ -264,4 +306,10 @@ let tests = "Test store-has kernel" `Quick (test_with_kernel test_store_has_kernel should_run_store_has_kernel); + tztest + "Test store-list-size kernel" + `Quick + (test_with_kernel + test_store_list_size_kernel + should_run_store_list_size_kernel); ] diff --git a/src/lib_scoru_wasm/test/wasm_kernels/README.md b/src/lib_scoru_wasm/test/wasm_kernels/README.md index 403e7288af9f..1a964275424e 100644 --- a/src/lib_scoru_wasm/test/wasm_kernels/README.md +++ b/src/lib_scoru_wasm/test/wasm_kernels/README.md @@ -63,3 +63,15 @@ git checkout 4788b8a882efbc9c19621ab43d617b2bdd5b1baf ./scripts/build-unit-kernel.sh "test-store-has" ``` + +## [test-store-list-size.wasm](./test-store-list-size.wasm) +This kernel is designed to test the `store_list_size` host function behaviour, on different keys in *durable storage*. + +It may be originated directly within a boot sector. + +To build the `test-store-list-size.wasm` kernel, run the following from the checked-out `trili/kernel` repo: +```shell +git checkout 0c98b17c4599d6f656312b16f17798406d491d77 + +./scripts/build-unit-kernel.sh "test-store-list-size" +``` diff --git a/src/lib_scoru_wasm/test/wasm_kernels/test-store-list-size.wasm b/src/lib_scoru_wasm/test/wasm_kernels/test-store-list-size.wasm new file mode 100755 index 0000000000000000000000000000000000000000..d3ee4c732230bcaa64b552f1030baa34cb624400 GIT binary patch literal 21145 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 literal 0 HcmV?d00001 -- GitLab