diff --git a/src/lib_scoru_wasm/durable.ml b/src/lib_scoru_wasm/durable.ml index 66f39e44d15ad478102fe118272b05ef7cb7dff9..16cf02a5e51177ae0b438f69e7ce528303ce9385 100644 --- a/src/lib_scoru_wasm/durable.ml +++ b/src/lib_scoru_wasm/durable.ml @@ -49,10 +49,10 @@ type key = string list (* A key is bounded to 250 bytes, including the implicit '/durable' prefix. Additionally, values are implicitly appended with '_'. **) -let max_prefix_key_length = 250 - String.length "/durable" - String.length "/_" +let max_key_length = 250 - String.length "/durable" - String.length "/_" let key_of_string_exn s = - if String.length s > max_prefix_key_length then raise (Invalid_key s) ; + if String.length s > max_key_length then raise (Invalid_key s) ; let key = match String.split '/' s with | "" :: tl -> tl (* Must start with '/' *) @@ -96,3 +96,9 @@ let find_value_exn tree key = match opt with | None -> raise Not_found | Some subtree -> Runner.decode E.chunked_byte_vector subtree + +let count_subtrees tree key = + let open Lwt.Syntax in + let* opt = T.find_tree tree @@ to_value_key key in + let+ len = T.length tree key in + if Option.is_none opt then len else len - 1 diff --git a/src/lib_scoru_wasm/durable.mli b/src/lib_scoru_wasm/durable.mli index 8a8400b8bd8dd41efe9e2d3c77345073a9049fd5..4aad6ca565d6012dd8aa59e050d40b30f53c93fb 100644 --- a/src/lib_scoru_wasm/durable.mli +++ b/src/lib_scoru_wasm/durable.mli @@ -54,6 +54,9 @@ val to_storage : t -> Tezos_webassembly_interpreter.Durable_storage.t - a step only contains alphanumeric ascii, or dots ('.') *) type key +(** [max_key_length] is the maximum length of a key in bytes. *) +val max_key_length : int + (** raise @Invalid_key *) val key_of_string_exn : string -> key @@ -63,3 +66,6 @@ val find_value : t -> key -> Lazy_containers.Chunked_byte_vector.t option Lwt.t (** raise @Not_found *) val find_value_exn : t -> key -> Lazy_containers.Chunked_byte_vector.t Lwt.t + +(** [count_subtrees durable key] returns the number of subtrees under [key]. *) +val count_subtrees : t -> key -> int Lwt.t diff --git a/src/lib_scoru_wasm/host_funcs.ml b/src/lib_scoru_wasm/host_funcs.ml index 7416b6ff31425cf73232fe7ca0cb27c98ae60211..375da506b8747d3b4b5ca615a024539d9ed0296d 100644 --- a/src/lib_scoru_wasm/host_funcs.ml +++ b/src/lib_scoru_wasm/host_funcs.ml @@ -28,6 +28,8 @@ open Instance exception Bad_input +exception Key_too_large of int + let retrieve_memory memories = match Vector.num_elements memories with | 1l -> Vector.get 0l memories @@ -203,6 +205,48 @@ let _alternate_write_debug = Lwt.return (durable, []) | _ -> raise Bad_input) +let store_has_name = "tezos_store_has" + +let store_has_type = + let input_types = + Types.[NumType I32Type; NumType I32Type] |> Vector.of_list + in + let output_types = Types.[NumType I32Type] |> Vector.of_list in + Types.FuncType (input_types, output_types) + +let store_has_unknown_key = 0 + +let store_has_value_only = 1 + +let store_has_subtrees_only = 2 + +let store_has_value_and_subtrees = 3 + +let store_has = + 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* value_opt = Durable.find_value tree key in + let+ num_subtrees = Durable.count_subtrees tree key in + let r = + match (value_opt, num_subtrees) with + | None, 0 -> store_has_unknown_key + | Some _, 0 -> store_has_value_only + | None, _ -> store_has_subtrees_only + | _ -> store_has_value_and_subtrees + in + (durable, [Values.(Num (I32 (I32.of_int_s r)))]) + | _ -> raise Bad_input) + let lookup_opt name = match name with | "read_input" -> @@ -211,6 +255,7 @@ let lookup_opt name = Some (ExternFunc (HostFunc (write_output_type, write_output_name))) | "write_debug" -> Some (ExternFunc (HostFunc (write_debug_type, write_debug_name))) + | "store_has" -> Some (ExternFunc (HostFunc (store_has_type, store_has_name))) | _ -> None let lookup name = @@ -225,6 +270,7 @@ let register_host_funcs registry = (read_input_name, read_input); (write_output_name, write_output); (write_debug_name, write_debug); + (store_has_name, store_has); ] module Internal_for_tests = struct @@ -235,4 +281,6 @@ module Internal_for_tests = struct let aux_write_input_in_memory = aux_write_input_in_memory let read_input = Func.HostFunc (read_input_type, read_input_name) + + let store_has = Func.HostFunc (store_has_type, store_has_name) end diff --git a/src/lib_scoru_wasm/host_funcs.mli b/src/lib_scoru_wasm/host_funcs.mli index 123a27118de088bd0003762edf07744f3cde2a82..1b6fdee5f133169b6e05191b7b92d8678d0a4640 100644 --- a/src/lib_scoru_wasm/host_funcs.mli +++ b/src/lib_scoru_wasm/host_funcs.mli @@ -56,6 +56,9 @@ val register_host_funcs : exception Bad_input +(** A durable key was given by the kernel with a longer-than-allowed length. *) +exception Key_too_large of int + module Internal_for_tests : sig (** [aux_write_output ~input_buffer ~output_buffer ~module_inst ~src ~num_bytes] reads num_bytes from the memory of module_inst starting at @@ -92,4 +95,13 @@ module Internal_for_tests : sig int Lwt.t val read_input : Tezos_webassembly_interpreter.Instance.func_inst + + (** [store_has] returns whether a key corresponds to a value and/or subtrees. + Namely, it returns the following enum: + - [0]: There is no value at [key], nor subtrees under [key]. + - [1]: There is a value at [key], but no subtrees under [key]. + - [2]: There is no value at [key], but there are subtrees under [key]. + - [3]: There is a value at [key], and subtrees under [key]. + *) + val store_has : 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 24e85b22eb4e2d0298e695dd7abc85b8215869fe..f9f898802ef5477cf1e0af09b454980ae341534b 100644 --- a/src/lib_scoru_wasm/test/test_durable_storage.ml +++ b/src/lib_scoru_wasm/test/test_durable_storage.ml @@ -33,6 +33,7 @@ open Tztest open Lazy_containers +open Tezos_webassembly_interpreter open Tezos_scoru_wasm include Test_encodings_util module Wasm = Wasm_pvm.Make (Tree) @@ -54,6 +55,40 @@ let wrap_as_durable_storage tree = Tezos_webassembly_interpreter.Durable_storage.of_tree @@ Tree_encoding.Wrapped.wrap tree +(* Test checking that if [key] is missing, [store_has key] returns [false] *) +let test_store_has_missing_key () = + let open Lwt.Syntax in + let* tree = empty_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 = 10l in + let key = "/test/path" 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 values = + Values.[Num (I32 src); Num (I32 (Int32.of_int @@ String.length key))] + 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* _, result = + Eval.invoke + ~module_reg + ~caller:module_key + ~durable + host_funcs_registry + Host_funcs.Internal_for_tests.store_has + values + in + assert (result = Values.[Num (I32 (I32.of_int_s 0))]) ; + Lwt.return @@ Result.return_unit + let assert_invalid_key run = let open Lwt_syntax in Lwt.catch @@ -63,8 +98,140 @@ let assert_invalid_key run = (fun caught_exn -> match caught_exn with | Durable.Invalid_key _ -> Lwt.return_ok () + | Host_funcs.Key_too_large _ -> Lwt.return_ok () | x -> raise x) +(* Test checking that if [key] is too large, [store_has key] traps. *) +let test_store_has_key_too_long () = + let open Lwt_syntax in + let* tree = empty_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 = 10l in + (* Together with the '/durable' prefix, and '/_' this key is too long *) + let key = List.repeat 240 "a" |> String.concat "" |> ( ^ ) "/" 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* _ = + assert_invalid_key (fun () -> + Eval.invoke + ~module_reg + ~caller:module_key + ~durable + host_funcs_registry + Host_funcs.Internal_for_tests.store_has + values) + in + (* We can tell [store_has] that [key] is one byte less long, which makes it valid *) + let values = + Values.[Num (I32 src); Num (I32 (Int32.of_int @@ (String.length key - 1)))] + in + let* _, result = + Eval.invoke + ~module_reg + ~caller:module_key + ~durable + host_funcs_registry + Host_funcs.Internal_for_tests.store_has + values + in + assert (result = Values.[Num (I32 (I32.of_int_s 0))]) ; + 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 () = + let open Lwt_syntax in + let* tree = empty_tree () in + let value () = Chunked_byte_vector.of_string "a very long value" in + let key_steps = + [ + "thequickbrownfoxjumpedoverthelazydog"; + "THEQUICKBROWNFOXJUMPEDOVERTHELAZYDOG"; + "0123456789"; + "key.containing.all.valid.chars"; + ] + 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"; "three"; "_"]) + 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 key = "/" ^ String.concat "/" key_steps in + let _ = Memory.store_bytes memory src key in + let src_one = 1000l in + let key_one = key ^ "/one" in + let _ = Memory.store_bytes memory src_one key_one in + let src_two = 2000l in + let key_two = key ^ "/two" in + let _ = Memory.store_bytes memory src_two key_two 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 check src key expected = + 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_has + values + in + assert (result = Values.[Num (I32 (I32.of_int_s expected))]) + in + (* key has a value, and subtrees at 'one' & 'two' *) + let* _ = check src key 3 in + (* key/one has a value, and no subtrees. *) + let* _ = check src_one key_one 1 in + (* key/two has no value, and a subtree at key/two/three. *) + let* _ = check src_two key_two 2 in + Lwt.return_ok () + (* Test that find_value/find_value_exn correctly decode the chunked_byte_vector *) let test_durable_find_value () = @@ -102,6 +269,34 @@ let test_durable_find_value () = assert (Option.is_none r) ; Lwt.return_ok () +let test_durable_count_subtrees () = + let open Lwt_syntax in + let* tree = empty_tree () in + let assert_subtree_count t count under = + let* t = wrap_as_durable_storage t in + let dbl = Durable.of_storage_exn t in + let+ n = Durable.count_subtrees dbl @@ Durable.key_of_string_exn under in + assert (n = count) + in + let add_value t at = + let value = Chunked_byte_vector.of_string "a very long value" in + Tree_encoding_runner.encode + (Tree_encoding.scope at Tree_encoding.chunked_byte_vector) + value + t + in + let* () = assert_subtree_count tree 0 "/hello" in + let* tree = add_value tree ["durable"; "hello"; "world"; "_"] in + let* () = assert_subtree_count tree 1 "/hello" in + let* tree = add_value tree ["durable"; "hello"; "you"; "_"] in + let* () = assert_subtree_count tree 2 "/hello" in + let* tree = add_value tree ["durable"; "hello"; "you"; "too"; "_"] in + let* () = assert_subtree_count tree 2 "/hello" in + let* () = assert_subtree_count tree 1 "/hello/you" in + let* () = assert_subtree_count tree 0 "/hello/you/too" in + let* () = assert_subtree_count tree 0 "/bye" in + Lwt.return_ok () + (* Test invalid key encodings are rejected. *) let test_durable_invalid_keys () = let open Lwt.Syntax in @@ -129,6 +324,10 @@ let test_durable_invalid_keys () = 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 "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 d4dd49da9aa4cec9972923239e63439f742b921c..90b87dc85792e2d4028dbcf0d6201d4bc3263ea0 100644 --- a/src/lib_scoru_wasm/test/test_wasm_pvm.ml +++ b/src/lib_scoru_wasm/test/test_wasm_pvm.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs *) +(* Copyright (c) 2022 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -41,6 +42,16 @@ let unreachable_kernel = "unreachable" (* Kernel writing `"hello"` to debug output. *) let test_write_debug_kernel = "test-write-debug" +(* Kernel checking the return of the store_has host func. + + This kernel expects a collection of values to exist: + - `/durable/hi/bye` + - `/durable/hello` + - `/durable/hello/universe` + and asserts that `store_has` returns the correct type for each. +*) +let test_store_has_kernel = "test-store-has" + (** [check_error kind reason error] checks a Wasm PVM error [error] is of a given [kind] with a possible [reason]. @@ -170,6 +181,45 @@ let should_run_debug_kernel kernel = (* The kernel should not fail. *) assert (not @@ is_stuck state_after_first_message) +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 + (* 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 + let* state_before_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_before_first_message) ; + (* We now delete the path ["hello"; "universe"] - this will cause the kernel + assertion on this path to fail, and the PVM should become stuck. *) + let* tree = set_input_step "test" 0 tree in + let* tree = + Test_encodings_util.Tree.remove tree ["durable"; "hello"; "universe"; "_"] + in + 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 now expected to fail, the PVM should be in stuck state. *) + assert (is_stuck state_after_first_message) + let test_with_kernel kernel test () = let open Lwt_result_syntax in let open Tezt.Base in @@ -210,4 +260,8 @@ let tests = "Test write_debug kernel" `Quick (test_with_kernel test_write_debug_kernel should_run_debug_kernel); + tztest + "Test store-has kernel" + `Quick + (test_with_kernel test_store_has_kernel should_run_store_has_kernel); ] diff --git a/src/lib_scoru_wasm/test/wasm_kernels/README.md b/src/lib_scoru_wasm/test/wasm_kernels/README.md index 57950461c5cad47176f43afaaf8bfbee095408c8..403e7288af9fe8ecd7e9d742b3e3d4c15fea0a26 100644 --- a/src/lib_scoru_wasm/test/wasm_kernels/README.md +++ b/src/lib_scoru_wasm/test/wasm_kernels/README.md @@ -51,3 +51,15 @@ git checkout 0c98b17c4599d6f656312b16f17798406d491d77 ./scripts/build-unit-kernel.sh "test-write-debug" ``` + +## [test-store-has.wasm](./test-store-has.wasm) +This kernel is designed to test the `store_has` host function behaviour, on different keys in *durable storage*. + +It may be originated directly within a boot sector. + +To build the `test-store-has.wasm` kernel, run the following from the checked-out `trili/kernel` repo: +```shell +git checkout 4788b8a882efbc9c19621ab43d617b2bdd5b1baf + +./scripts/build-unit-kernel.sh "test-store-has" +``` diff --git a/src/lib_scoru_wasm/test/wasm_kernels/test-store-has.wasm b/src/lib_scoru_wasm/test/wasm_kernels/test-store-has.wasm new file mode 100755 index 0000000000000000000000000000000000000000..51e8068e6c65b21c6f9af7153f99ab8fab48714c Binary files /dev/null and b/src/lib_scoru_wasm/test/wasm_kernels/test-store-has.wasm differ diff --git a/src/lib_tree_encoding/tree.ml b/src/lib_tree_encoding/tree.ml index f6a8f09ed82085b71f862719a7a1d754667d085e..afd1da519dd6c56174f6cf0073b341007f62998b 100644 --- a/src/lib_tree_encoding/tree.ml +++ b/src/lib_tree_encoding/tree.ml @@ -46,6 +46,8 @@ module type S = sig val find : tree -> key -> value option Lwt.t val find_tree : tree -> key -> tree option Lwt.t + + val length : tree -> key -> int Lwt.t end type 'tree backend = (module S with type tree = 'tree) @@ -71,6 +73,9 @@ let find : type tree. tree backend -> tree -> key -> value option Lwt.t = let find_tree : type tree. tree backend -> tree -> key -> tree option Lwt.t = fun (module T) tree key -> T.find_tree tree key +let length : type tree. tree backend -> tree -> key -> int Lwt.t = + fun (module T) tree key -> T.length tree key + type wrapped_tree = Wrapped_tree : 'tree * 'tree backend -> wrapped_tree type Lazy_containers.Lazy_map.tree += Wrapped of wrapped_tree @@ -99,6 +104,8 @@ module Wrapped : S with type tree = wrapped_tree = struct let+ t' = find_tree b t key in match t' with Some t' -> Some (Wrapped_tree (t', b)) | None -> None + let length (Wrapped_tree (t, b)) key = length b t key + let find (Wrapped_tree (t, b)) key = find b t key let select = function diff --git a/src/lib_tree_encoding/tree.mli b/src/lib_tree_encoding/tree.mli index f5318033646125c3934cc7bf899914c4e226f814..b9e109731eb22a9157bf32f3ee14e13e57cad95e 100644 --- a/src/lib_tree_encoding/tree.mli +++ b/src/lib_tree_encoding/tree.mli @@ -50,6 +50,8 @@ module type S = sig val find : tree -> key -> value option Lwt.t val find_tree : tree -> key -> tree option Lwt.t + + val length : tree -> key -> int Lwt.t end type 'tree backend = (module S with type tree = 'tree) @@ -68,6 +70,8 @@ val find : 'tree backend -> 'tree -> key -> value option Lwt.t val find_tree : 'tree backend -> 'tree -> key -> 'tree option Lwt.t +val length : 'tree backend -> 'tree -> key -> int Lwt.t + (** A [wrapped_tree] allows modifications to the underlying tree, without affecting the tree that it was decoded from. *) type wrapped_tree = Wrapped_tree : 'tree * 'tree backend -> wrapped_tree diff --git a/src/lib_tree_encoding/tree_encoding.mli b/src/lib_tree_encoding/tree_encoding.mli index 1d0fc0932ac496895625a0864a6edf3134c5785e..97ffe0cfd0e6bd1851a75268302eb982e63eafdc 100644 --- a/src/lib_tree_encoding/tree_encoding.mli +++ b/src/lib_tree_encoding/tree_encoding.mli @@ -266,6 +266,8 @@ module type TREE = sig val find : tree -> key -> value option Lwt.t val find_tree : tree -> key -> tree option Lwt.t + + val length : tree -> key -> int Lwt.t end type wrapped_tree