diff --git a/proto/penumbra/penumbra/cnidarium/v1/cnidarium.proto b/proto/penumbra/penumbra/cnidarium/v1/cnidarium.proto new file mode 100644 index 0000000..87ecd84 --- /dev/null +++ b/proto/penumbra/penumbra/cnidarium/v1/cnidarium.proto @@ -0,0 +1,116 @@ +syntax = "proto3"; + +package penumbra.cnidarium.v1; + +import "ibc/core/commitment/v1/commitment.proto"; + +service QueryService { + // General-purpose key-value state query API, that can be used to query + // arbitrary keys in the JMT storage. + rpc KeyValue(KeyValueRequest) returns (KeyValueResponse); + + // General-purpose key-value state query API, that can be used to query + // arbitrary keys in the non-verifiable storage. + rpc NonVerifiableKeyValue(NonVerifiableKeyValueRequest) returns (NonVerifiableKeyValueResponse); + + // General-purpose prefixed key-value state query API, that can be used to query + // arbitrary prefixes in the JMT storage. + rpc PrefixValue(PrefixValueRequest) returns (stream PrefixValueResponse); + + // Subscribes to a stream of key-value updates, with regex filtering on keys. + rpc Watch(WatchRequest) returns (stream WatchResponse); +} + +// Performs a key-value query against the nonverifiable storage, +// using a byte-encoded key. +message NonVerifiableKeyValueRequest { + message Key { + bytes inner = 1; + } + + Key key = 1; +} + +message NonVerifiableKeyValueResponse { + message Value { + bytes value = 1; + } + // The value corresponding to the specified key, if it was found. + Value value = 1; +} + +// Performs a key-value query against the JMT, either by key or by key hash. +// +// Proofs are only supported by key. +message KeyValueRequest { + // If set, the key to fetch from storage. + string key = 2; + // whether to return a proof + bool proof = 3; +} + +message KeyValueResponse { + message Value { + bytes value = 1; + } + // The value corresponding to the specified key, if it was found. + Value value = 1; + // A proof of existence or non-existence. + .ibc.core.commitment.v1.MerkleProof proof = 2; +} + +// Performs a prefixed key-value query, by string prefix. +message PrefixValueRequest { + // The prefix to fetch subkeys from storage. + string prefix = 2; +} + +message PrefixValueResponse { + string key = 1; + bytes value = 2; +} + +// Requests a stream of new key-value pairs that have been committed to the state. +message WatchRequest { + // A regex for keys in the verifiable storage. + // + // Only key-value updates whose keys match this regex will be returned. + // Note that the empty string matches all keys. + // To exclude all keys, use the regex "$^", which matches no strings. + string key_regex = 1; + // A regex for keys in the nonverifiable storage. + // + // Only key-value updates whose keys match this regex will be returned. + // Note that the empty string matches all keys. + // To exclude all keys, use the regex "$^", which matches no strings. + string nv_key_regex = 2; +} + +// A key-value pair that has been committed to the state. +message WatchResponse { + // Elements of the verifiable storage have string keys. + message KeyValue { + string key = 1; + bytes value = 2; + // If set to true, the key-value pair was deleted. + // This allows distinguishing a deleted key-value pair from a key-value pair whose value is empty. + bool deleted = 3; + } + // Elements of the nonverifiable storage have byte keys. + message NvKeyValue { + bytes key = 1; + bytes value = 2; + // If set to true, the key-value pair was deleted. + // This allows distinguishing a deleted key-value pair from a key-value pair whose value is empty. + bool deleted = 3; + } + + // The state version the key-value pair was committed at. + uint64 version = 1; + + // The entry that was committed. + oneof entry { + KeyValue kv = 5; + NvKeyValue nv_kv = 6; + } +} diff --git a/proto/rust-vendored/cosmos/ics23/v1/proofs.proto b/proto/rust-vendored/cosmos/ics23/v1/proofs.proto new file mode 100644 index 0000000..0e75dfb --- /dev/null +++ b/proto/rust-vendored/cosmos/ics23/v1/proofs.proto @@ -0,0 +1,234 @@ +syntax = "proto3"; + +package cosmos.ics23.v1; + +option go_package = "github.com/cosmos/ics23/go;ics23"; + +enum HashOp { + // NO_HASH is the default if no data passed. Note this is an illegal argument some places. + NO_HASH = 0; + SHA256 = 1; + SHA512 = 2; + KECCAK = 3; + RIPEMD160 = 4; + BITCOIN = 5; // ripemd160(sha256(x)) + SHA512_256 = 6; +} + +/** +LengthOp defines how to process the key and value of the LeafOp +to include length information. After encoding the length with the given +algorithm, the length will be prepended to the key and value bytes. +(Each one with it's own encoded length) +*/ +enum LengthOp { + // NO_PREFIX don't include any length info + NO_PREFIX = 0; + // VAR_PROTO uses protobuf (and go-amino) varint encoding of the length + VAR_PROTO = 1; + // VAR_RLP uses rlp int encoding of the length + VAR_RLP = 2; + // FIXED32_BIG uses big-endian encoding of the length as a 32 bit integer + FIXED32_BIG = 3; + // FIXED32_LITTLE uses little-endian encoding of the length as a 32 bit integer + FIXED32_LITTLE = 4; + // FIXED64_BIG uses big-endian encoding of the length as a 64 bit integer + FIXED64_BIG = 5; + // FIXED64_LITTLE uses little-endian encoding of the length as a 64 bit integer + FIXED64_LITTLE = 6; + // REQUIRE_32_BYTES is like NONE, but will fail if the input is not exactly 32 bytes (sha256 output) + REQUIRE_32_BYTES = 7; + // REQUIRE_64_BYTES is like NONE, but will fail if the input is not exactly 64 bytes (sha512 output) + REQUIRE_64_BYTES = 8; +} + +/** +ExistenceProof takes a key and a value and a set of steps to perform on it. +The result of peforming all these steps will provide a "root hash", which can +be compared to the value in a header. + +Since it is computationally infeasible to produce a hash collission for any of the used +cryptographic hash functions, if someone can provide a series of operations to transform +a given key and value into a root hash that matches some trusted root, these key and values +must be in the referenced merkle tree. + +The only possible issue is maliablity in LeafOp, such as providing extra prefix data, +which should be controlled by a spec. Eg. with lengthOp as NONE, + prefix = FOO, key = BAR, value = CHOICE +and + prefix = F, key = OOBAR, value = CHOICE +would produce the same value. + +With LengthOp this is tricker but not impossible. Which is why the "leafPrefixEqual" field +in the ProofSpec is valuable to prevent this mutability. And why all trees should +length-prefix the data before hashing it. +*/ +message ExistenceProof { + bytes key = 1; + bytes value = 2; + LeafOp leaf = 3; + repeated InnerOp path = 4; +} + +/* +NonExistenceProof takes a proof of two neighbors, one left of the desired key, +one right of the desired key. If both proofs are valid AND they are neighbors, +then there is no valid proof for the given key. +*/ +message NonExistenceProof { + bytes key = 1; // TODO: remove this as unnecessary??? we prove a range + ExistenceProof left = 2; + ExistenceProof right = 3; +} + +/* +CommitmentProof is either an ExistenceProof or a NonExistenceProof, or a Batch of such messages +*/ +message CommitmentProof { + oneof proof { + ExistenceProof exist = 1; + NonExistenceProof nonexist = 2; + BatchProof batch = 3; + CompressedBatchProof compressed = 4; + } +} + +/** +LeafOp represents the raw key-value data we wish to prove, and +must be flexible to represent the internal transformation from +the original key-value pairs into the basis hash, for many existing +merkle trees. + +key and value are passed in. So that the signature of this operation is: + leafOp(key, value) -> output + +To process this, first prehash the keys and values if needed (ANY means no hash in this case): + hkey = prehashKey(key) + hvalue = prehashValue(value) + +Then combine the bytes, and hash it + output = hash(prefix || length(hkey) || hkey || length(hvalue) || hvalue) +*/ +message LeafOp { + HashOp hash = 1; + HashOp prehash_key = 2; + HashOp prehash_value = 3; + LengthOp length = 4; + // prefix is a fixed bytes that may optionally be included at the beginning to differentiate + // a leaf node from an inner node. + bytes prefix = 5; +} + +/** +InnerOp represents a merkle-proof step that is not a leaf. +It represents concatenating two children and hashing them to provide the next result. + +The result of the previous step is passed in, so the signature of this op is: + innerOp(child) -> output + +The result of applying InnerOp should be: + output = op.hash(op.prefix || child || op.suffix) + + where the || operator is concatenation of binary data, +and child is the result of hashing all the tree below this step. + +Any special data, like prepending child with the length, or prepending the entire operation with +some value to differentiate from leaf nodes, should be included in prefix and suffix. +If either of prefix or suffix is empty, we just treat it as an empty string +*/ +message InnerOp { + HashOp hash = 1; + bytes prefix = 2; + bytes suffix = 3; +} + +/** +ProofSpec defines what the expected parameters are for a given proof type. +This can be stored in the client and used to validate any incoming proofs. + + verify(ProofSpec, Proof) -> Proof | Error + +As demonstrated in tests, if we don't fix the algorithm used to calculate the +LeafHash for a given tree, there are many possible key-value pairs that can +generate a given hash (by interpretting the preimage differently). +We need this for proper security, requires client knows a priori what +tree format server uses. But not in code, rather a configuration object. +*/ +message ProofSpec { + // any field in the ExistenceProof must be the same as in this spec. + // except Prefix, which is just the first bytes of prefix (spec can be longer) + LeafOp leaf_spec = 1; + InnerSpec inner_spec = 2; + // max_depth (if > 0) is the maximum number of InnerOps allowed (mainly for fixed-depth tries) + int32 max_depth = 3; + // min_depth (if > 0) is the minimum number of InnerOps allowed (mainly for fixed-depth tries) + int32 min_depth = 4; +} + +/* +InnerSpec contains all store-specific structure info to determine if two proofs from a +given store are neighbors. + +This enables: + + isLeftMost(spec: InnerSpec, op: InnerOp) + isRightMost(spec: InnerSpec, op: InnerOp) + isLeftNeighbor(spec: InnerSpec, left: InnerOp, right: InnerOp) +*/ +message InnerSpec { + // Child order is the ordering of the children node, must count from 0 + // iavl tree is [0, 1] (left then right) + // merk is [0, 2, 1] (left, right, here) + repeated int32 child_order = 1; + int32 child_size = 2; + int32 min_prefix_length = 3; + int32 max_prefix_length = 4; + // empty child is the prehash image that is used when one child is nil (eg. 20 bytes of 0) + bytes empty_child = 5; + // hash is the algorithm that must be used for each InnerOp + HashOp hash = 6; +} + +/* +BatchProof is a group of multiple proof types than can be compressed +*/ +message BatchProof { + repeated BatchEntry entries = 1; +} + +// Use BatchEntry not CommitmentProof, to avoid recursion +message BatchEntry { + oneof proof { + ExistenceProof exist = 1; + NonExistenceProof nonexist = 2; + } +} + +/****** all items here are compressed forms *******/ + +message CompressedBatchProof { + repeated CompressedBatchEntry entries = 1; + repeated InnerOp lookup_inners = 2; +} + +// Use BatchEntry not CommitmentProof, to avoid recursion +message CompressedBatchEntry { + oneof proof { + CompressedExistenceProof exist = 1; + CompressedNonExistenceProof nonexist = 2; + } +} + +message CompressedExistenceProof { + bytes key = 1; + bytes value = 2; + LeafOp leaf = 3; + // these are indexes into the lookup_inners table in CompressedBatchProof + repeated int32 path = 4; +} + +message CompressedNonExistenceProof { + bytes key = 1; // TODO: remove this as unnecessary??? we prove a range + CompressedExistenceProof left = 2; + CompressedExistenceProof right = 3; +} diff --git a/proto/rust-vendored/gogoproto/gogo.proto b/proto/rust-vendored/gogoproto/gogo.proto new file mode 100644 index 0000000..974b36a --- /dev/null +++ b/proto/rust-vendored/gogoproto/gogo.proto @@ -0,0 +1,145 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/cosmos/gogoproto +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "GoGoProtos"; +option go_package = "github.com/cosmos/gogoproto/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; + + optional string castrepeated = 65013; +} diff --git a/proto/rust-vendored/ibc/core/commitment/v1/commitment.proto b/proto/rust-vendored/ibc/core/commitment/v1/commitment.proto new file mode 100644 index 0000000..bbad400 --- /dev/null +++ b/proto/rust-vendored/ibc/core/commitment/v1/commitment.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package ibc.core.commitment.v1; + +option go_package = "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types"; + +import "gogoproto/gogo.proto"; +import "cosmos/ics23/v1/proofs.proto"; + +// MerkleRoot defines a merkle root hash. +// In the Cosmos SDK, the AppHash of a block header becomes the root. +message MerkleRoot { + option (gogoproto.goproto_getters) = false; + + bytes hash = 1; +} + +// MerklePrefix is merkle path prefixed to the key. +// The constructed key from the Path and the key will be append(Path.KeyPath, +// append(Path.KeyPrefix, key...)) +message MerklePrefix { + bytes key_prefix = 1; +} + +// MerklePath is the path used to verify commitment proofs, which can be an +// arbitrary structured object (defined by a commitment type). +// MerklePath is represented from root-to-leaf +message MerklePath { + option (gogoproto.goproto_stringer) = false; + + repeated string key_path = 1; +} + +// MerkleProof is a wrapper type over a chain of CommitmentProofs. +// It demonstrates membership or non-membership for an element or set of +// elements, verifiable in conjunction with a known commitment root. Proofs +// should be succinct. +// MerkleProofs are ordered from leaf-to-root +message MerkleProof { + repeated cosmos.ics23.v1.CommitmentProof proofs = 1; +} diff --git a/src/gen/proto_descriptor.bin.no_lfs b/src/gen/proto_descriptor.bin.no_lfs index 6e23fc5..f41da74 100644 Binary files a/src/gen/proto_descriptor.bin.no_lfs and b/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/tools/proto-compiler/Cargo.toml b/tools/proto-compiler/Cargo.toml new file mode 100644 index 0000000..43dc1ca --- /dev/null +++ b/tools/proto-compiler/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "penumbra-proto-compiler" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow = "1" +ibc-proto = { version = "0.40.0" } +ics23 = "0.11.3" +pbjson = "0.6" +pbjson-build = "0.6" +pbjson-types = "0.6" +prost = "0.12.3" +prost-build = "0.12.3" +prost-types = "0.12" +tempfile = "3" +tonic-build = { version = "0.10.0", features = ["cleanup-markdown"] } diff --git a/tools/proto-compiler/README.md b/tools/proto-compiler/README.md new file mode 100644 index 0000000..5d20d17 --- /dev/null +++ b/tools/proto-compiler/README.md @@ -0,0 +1,5 @@ +## How to compile fresh proto structs + +* `cargo run` in the compiler folder. + +The resultant structs will be created in the `src/gen` folder. diff --git a/tools/proto-compiler/src/main.rs b/tools/proto-compiler/src/main.rs new file mode 100644 index 0000000..53155f5 --- /dev/null +++ b/tools/proto-compiler/src/main.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +fn main() -> anyhow::Result<()> { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + println!("root: {}", root.display()); + + let target_dir = root.join("..").join("..").join("src").join("gen"); + println!("target_dir: {}", target_dir.display()); + + // https://github.com/penumbra-zone/penumbra/issues/3038#issuecomment-1722534133 + // Using the "no_lfs" suffix prevents matching a catch-all LFS rule. + let descriptor_file_name = "proto_descriptor.bin.no_lfs"; + + // prost_build::Config isn't Clone, so we need to make two. + let mut config = prost_build::Config::new(); + + config.compile_well_known_types(); + // As recommended in pbjson_types docs. + config.extern_path(".google.protobuf", "::pbjson_types"); + // NOTE: we need this because the rust module that defines the IBC types is external, and not + // part of this crate. + // See https://docs.rs/prost-build/0.5.0/prost_build/struct.Config.html#method.extern_path + config.extern_path(".ibc", "::ibc_proto::ibc"); + // TODO: which of these is the right path? + config.extern_path(".ics23", "::ics23"); + config.extern_path(".cosmos.ics23", "::ics23"); + + config + .out_dir(&target_dir) + .file_descriptor_set_path(&target_dir.join(descriptor_file_name)) + .enable_type_names(); + + let rpc_doc_attr = r#"#[cfg(feature = "rpc")]"#; + + tonic_build::configure() + .out_dir(&target_dir) + .emit_rerun_if_changed(false) + .server_mod_attribute(".", rpc_doc_attr) + .client_mod_attribute(".", rpc_doc_attr) + .compile_with_config( + config, + &["../../proto/penumbra/penumbra/cnidarium/v1/cnidarium.proto"], + &["../../proto/penumbra/", "../../proto/rust-vendored/"], + )?; + // Finally, build pbjson Serialize, Deserialize impls: + let descriptor_set = std::fs::read(target_dir.join(descriptor_file_name))?; + + pbjson_build::Builder::new() + .register_descriptors(&descriptor_set)? + .ignore_unknown_fields() + .out_dir(&target_dir) + .build(&[".penumbra"])?; + + Ok(()) +}