From 672e2027751b8c35e79a71c472d99eaf004721c9 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:40:22 +0100 Subject: [PATCH 01/10] Add cosmos/ics23 proto into local proto with conversion --- ibc/types/proofs.go | 107 +++++++++++++++++ ibc/types/proto/proofs.proto | 222 +++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 ibc/types/proofs.go create mode 100644 ibc/types/proto/proofs.proto diff --git a/ibc/types/proofs.go b/ibc/types/proofs.go new file mode 100644 index 000000000..9b0a5efb2 --- /dev/null +++ b/ibc/types/proofs.go @@ -0,0 +1,107 @@ +package types + +import ics23 "github.com/cosmos/ics23/go" + +var SmtSpec = &ProofSpec{ + LeafSpec: &LeafOp{ + Hash: HashOp_SHA256, + PrehashKey: HashOp_SHA256, + PrehashValue: HashOp_SHA256, + Length: LengthOp_NO_PREFIX, + Prefix: []byte{0}, + }, + InnerSpec: &InnerSpec{ + ChildOrder: []int32{0, 1}, + ChildSize: 32, + MinPrefixLength: 1, + MaxPrefixLength: 1, + EmptyChild: make([]byte, 32), + Hash: HashOp_SHA256, + }, + MaxDepth: 256, + PrehashKeyBeforeComparison: true, +} + +func (p *ProofSpec) ConvertToIcs23ProofSpec() *ics23.ProofSpec { + ics := new(ics23.ProofSpec) + ics.LeafSpec = p.LeafSpec.ConvertToIcs23LeafOp() + ics.InnerSpec = p.InnerSpec.ConvertToIcs23InnerSpec() + ics.MaxDepth = p.MaxDepth + ics.MinDepth = p.MinDepth + ics.PrehashKeyBeforeComparison = p.PrehashKeyBeforeComparison + return ics +} + +func (l *LeafOp) ConvertToIcs23LeafOp() *ics23.LeafOp { + ics := new(ics23.LeafOp) + ics.Hash = l.Hash.ConvertToIcs23HashOp() + ics.PrehashKey = l.PrehashKey.ConvertToIcs23HashOp() + ics.PrehashValue = l.PrehashValue.ConvertToIcs23HashOp() + ics.Length = l.Length.ConvertToIcs23LenthOp() + ics.Prefix = l.Prefix + return ics +} + +func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { + ics := new(ics23.InnerSpec) + ics.ChildOrder = i.ChildOrder + ics.MinPrefixLength = i.MinPrefixLength + ics.MaxPrefixLength = i.MaxPrefixLength + ics.EmptyChild = i.EmptyChild + ics.Hash = i.Hash.ConvertToIcs23HashOp() + return ics +} + +func (h HashOp) ConvertToIcs23HashOp() ics23.HashOp { + switch h { + case HashOp_NO_HASH: + return ics23.HashOp_NO_HASH + case HashOp_SHA256: + return ics23.HashOp_SHA256 + case HashOp_SHA512: + return ics23.HashOp_SHA512 + case HashOp_KECCAK: + return ics23.HashOp_KECCAK + case HashOp_RIPEMD160: + return ics23.HashOp_RIPEMD160 + case HashOp_BITCOIN: + return ics23.HashOp_BITCOIN + case HashOp_SHA512_256: + return ics23.HashOp_SHA512_256 + default: + panic("unknown hash op") + } +} + +func (l LengthOp) ConvertToIcs23LenthOp() ics23.LengthOp { + switch l { + case LengthOp_NO_PREFIX: + return ics23.LengthOp_NO_PREFIX + case LengthOp_VAR_PROTO: + return ics23.LengthOp_VAR_PROTO + case LengthOp_VAR_RLP: + return ics23.LengthOp_VAR_RLP + case LengthOp_FIXED32_BIG: + return ics23.LengthOp_FIXED32_BIG + case LengthOp_FIXED32_LITTLE: + return ics23.LengthOp_FIXED32_LITTLE + case LengthOp_FIXED64_BIG: + return ics23.LengthOp_FIXED64_BIG + case LengthOp_FIXED64_LITTLE: + return ics23.LengthOp_FIXED64_LITTLE + case LengthOp_REQUIRE_32_BYTES: + return ics23.LengthOp_REQUIRE_32_BYTES + case LengthOp_REQUIRE_64_BYTES: + return ics23.LengthOp_REQUIRE_64_BYTES + default: + panic("unknown length op") + } +} + +func (i *InnerOp) ConvertToIcs23InnerOp() *ics23.InnerOp { + ics := new(ics23.InnerOp) + ics.Hash = i.Hash.ConvertToIcs23HashOp() + ics.Prefix = i.Prefix + ics.Suffix = i.Suffix + return ics +} diff --git a/ibc/types/proto/proofs.proto b/ibc/types/proto/proofs.proto new file mode 100644 index 000000000..e79881ed4 --- /dev/null +++ b/ibc/types/proto/proofs.proto @@ -0,0 +1,222 @@ +syntax = "proto3"; + +// This file is a clone from the github.com/cosmos/ics23 repo, it has been +// cloned to be compiled with protoc generic. As such it will produce valid +// protobuf messages that can be serialised and cloned + +option go_package = "github.com/pokt-network/pocket/ibc/types"; + +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; + // prehash_key_before_comparison is a flag that indicates whether to use the + // prehash_key specified by LeafOp to compare lexical ordering of keys for + // non-existence proofs. + bool prehash_key_before_comparison = 5; +} + +// 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; +} From 513005c0ea23f103416b32edbcf87e2a3ab64eab Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Fri, 21 Jul 2023 10:02:33 +0100 Subject: [PATCH 02/10] Add makefile auto download and sed on proto --- Makefile | 20 ++++ ibc/types/proto/proofs.proto | 176 +++++++++++++++++++---------------- 2 files changed, 116 insertions(+), 80 deletions(-) diff --git a/Makefile b/Makefile index 003c72b11..192cd67a6 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,15 @@ CWD ?= CURRENT_WORKING_DIRECTIONRY_NOT_SUPPLIED # `VERBOSE_TEST="" make test_persistence` is an easy way to run the same tests without verbose output VERBOSE_TEST ?= -v +# Detect OS using the $(shell uname -s) command +ifeq ($(shell uname -s),Darwin) + # Add macOS-specific commands here + SEDI = sed -i '' +else ifeq ($(shell uname -s),Linux) + # Add Linux-specific commands here + SEDI = sed -i +endif + .SILENT: .PHONY: list ## List all make targets @@ -309,6 +318,7 @@ protogen_local: go_protoc-go-inject-tag ## Generate go structures for all of the $(PROTOC_SHARED) -I=./p2p/types/proto --go_out=./p2p/types ./p2p/types/proto/*.proto # IBC + make copy_ics23_proto $(PROTOC_SHARED) -I=./ibc/types/proto --go_out=./ibc/types ./ibc/types/proto/*.proto # echo "View generated proto files by running: make protogen_show" @@ -316,6 +326,16 @@ protogen_local: go_protoc-go-inject-tag ## Generate go structures for all of the # CONSIDERATION: Some proto files contain unused gRPC services so we may need to add the following # if/when we decide to include it: `grpc--go-grpc_opt=paths=source_relative --go-grpc_out=./output/path` +.PHONY: copy_ics23_proto +copy_ics23_proto: + echo "Downloading cosmos/ics23 proto definitions..." + curl -s -o ./ibc/types/proto/proofs.proto https://raw.githubusercontent.com/cosmos/ics23/master/proto/cosmos/ics23/v1/proofs.proto && \ + $(SEDI) \ + -e '/^package/{N;d;}' \ + -e 's@github.com/.*"@github.com/pokt-network/pocket/ibc/types"@g' \ + ./ibc/types/proto/proofs.proto && \ + awk 'BEGIN { print "// ===== !! THIS IS CLONED FROM cosmos/ics23 !! =====\n" } { print }' ./ibc/types/proto/proofs.proto > tmpfile && mv tmpfile ./ibc/types/proto/proofs.proto + .PHONY: protogen_docker_m1 ## TECHDEBT: Test, validate & update. protogen_docker_m1: docker_check diff --git a/ibc/types/proto/proofs.proto b/ibc/types/proto/proofs.proto index e79881ed4..e3659215e 100644 --- a/ibc/types/proto/proofs.proto +++ b/ibc/types/proto/proofs.proto @@ -1,8 +1,6 @@ -syntax = "proto3"; +// ===== !! THIS IS CLONED FROM cosmos/ics23 !! ===== -// This file is a clone from the github.com/cosmos/ics23 repo, it has been -// cloned to be compiled with protoc generic. As such it will produce valid -// protobuf messages that can be serialised and cloned +syntax = "proto3"; option go_package = "github.com/pokt-network/pocket/ibc/types"; @@ -17,10 +15,12 @@ enum HashOp { 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) +/** +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; @@ -42,25 +42,27 @@ enum LengthOp { 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. +/** +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; @@ -68,16 +70,20 @@ message ExistenceProof { 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. +/* +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 +/* +CommitmentProof is either an ExistenceProof or a NonExistenceProof, or a Batch of such messages +*/ message CommitmentProof { oneof proof { ExistenceProof exist = 1; @@ -87,20 +93,22 @@ message CommitmentProof { } } -// 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) +/** +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; @@ -111,37 +119,41 @@ message LeafOp { 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 +/** +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. +/** +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) @@ -157,14 +169,16 @@ message ProofSpec { bool prehash_key_before_comparison = 5; } -// 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) +/* +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) @@ -179,7 +193,9 @@ message InnerSpec { HashOp hash = 6; } -// BatchProof is a group of multiple proof types than can be compressed +/* +BatchProof is a group of multiple proof types than can be compressed +*/ message BatchProof { repeated BatchEntry entries = 1; } @@ -192,7 +208,7 @@ message BatchEntry { } } -// ====== all items here are compressed forms ======= +/****** all items here are compressed forms *******/ message CompressedBatchProof { repeated CompressedBatchEntry entries = 1; From 0ce694ab5db2c889c707baa5faa1f7163c35089f Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:46:59 +0100 Subject: [PATCH 03/10] add missing conversion field --- ibc/types/proofs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ibc/types/proofs.go b/ibc/types/proofs.go index 9b0a5efb2..2340a48b2 100644 --- a/ibc/types/proofs.go +++ b/ibc/types/proofs.go @@ -45,6 +45,7 @@ func (l *LeafOp) ConvertToIcs23LeafOp() *ics23.LeafOp { func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { ics := new(ics23.InnerSpec) ics.ChildOrder = i.ChildOrder + ics.ChildSize = i.ChildSize ics.MinPrefixLength = i.MinPrefixLength ics.MaxPrefixLength = i.MaxPrefixLength ics.EmptyChild = i.EmptyChild From ac50e065fe8d23916ae8cca1626cd9ef9bda5314 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Sun, 23 Jul 2023 14:31:34 +0100 Subject: [PATCH 04/10] pick: conversion funcs --- ibc/types/proofs.go | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/ibc/types/proofs.go b/ibc/types/proofs.go index 2340a48b2..c9f1b54ef 100644 --- a/ibc/types/proofs.go +++ b/ibc/types/proofs.go @@ -23,6 +23,9 @@ var SmtSpec = &ProofSpec{ } func (p *ProofSpec) ConvertToIcs23ProofSpec() *ics23.ProofSpec { + if p == nil { + return nil + } ics := new(ics23.ProofSpec) ics.LeafSpec = p.LeafSpec.ConvertToIcs23LeafOp() ics.InnerSpec = p.InnerSpec.ConvertToIcs23InnerSpec() @@ -32,7 +35,23 @@ func (p *ProofSpec) ConvertToIcs23ProofSpec() *ics23.ProofSpec { return ics } +func ConvertFromIcs23ProofSpec(p *ics23.ProofSpec) *ProofSpec { + if p == nil { + return nil + } + spc := new(ProofSpec) + spc.LeafSpec = ConvertFromIcs23LeafOp(p.LeafSpec) + spc.InnerSpec = ConvertFromIcs23InnerSpec(p.InnerSpec) + spc.MaxDepth = p.MaxDepth + spc.MinDepth = p.MinDepth + spc.PrehashKeyBeforeComparison = p.PrehashKeyBeforeComparison + return spc +} + func (l *LeafOp) ConvertToIcs23LeafOp() *ics23.LeafOp { + if l == nil { + return nil + } ics := new(ics23.LeafOp) ics.Hash = l.Hash.ConvertToIcs23HashOp() ics.PrehashKey = l.PrehashKey.ConvertToIcs23HashOp() @@ -42,7 +61,23 @@ func (l *LeafOp) ConvertToIcs23LeafOp() *ics23.LeafOp { return ics } +func ConvertFromIcs23LeafOp(l *ics23.LeafOp) *LeafOp { + if l == nil { + return nil + } + op := new(LeafOp) + op.Hash = ConvertFromIcs23HashOp(l.Hash) + op.PrehashKey = ConvertFromIcs23HashOp(l.PrehashKey) + op.PrehashValue = ConvertFromIcs23HashOp(l.PrehashValue) + op.Length = ConvertFromIcs23LengthOp(l.Length) + op.Prefix = l.Prefix + return op +} + func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { + if i == nil { + return nil + } ics := new(ics23.InnerSpec) ics.ChildOrder = i.ChildOrder ics.ChildSize = i.ChildSize @@ -53,6 +88,20 @@ func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { return ics } +func ConvertFromIcs23InnerSpec(i *ics23.InnerSpec) *InnerSpec { + if i == nil { + return nil + } + spec := new(InnerSpec) + spec.ChildOrder = i.ChildOrder + spec.ChildSize = i.ChildSize + spec.MinPrefixLength = i.MinPrefixLength + spec.MaxPrefixLength = i.MaxPrefixLength + spec.EmptyChild = i.EmptyChild + spec.Hash = ConvertFromIcs23HashOp(i.Hash) + return spec +} + func (h HashOp) ConvertToIcs23HashOp() ics23.HashOp { switch h { case HashOp_NO_HASH: @@ -74,6 +123,27 @@ func (h HashOp) ConvertToIcs23HashOp() ics23.HashOp { } } +func ConvertFromIcs23HashOp(h ics23.HashOp) HashOp { + switch h { + case ics23.HashOp_NO_HASH: + return HashOp_NO_HASH + case ics23.HashOp_SHA256: + return HashOp_SHA256 + case ics23.HashOp_SHA512: + return HashOp_SHA512 + case ics23.HashOp_KECCAK: + return HashOp_KECCAK + case ics23.HashOp_RIPEMD160: + return HashOp_RIPEMD160 + case ics23.HashOp_BITCOIN: + return HashOp_BITCOIN + case ics23.HashOp_SHA512_256: + return HashOp_SHA512_256 + default: + panic("unknown hash op") + } +} + func (l LengthOp) ConvertToIcs23LenthOp() ics23.LengthOp { switch l { case LengthOp_NO_PREFIX: @@ -99,10 +169,49 @@ func (l LengthOp) ConvertToIcs23LenthOp() ics23.LengthOp { } } +func ConvertFromIcs23LengthOp(l ics23.LengthOp) LengthOp { + switch l { + case ics23.LengthOp_NO_PREFIX: + return LengthOp_NO_PREFIX + case ics23.LengthOp_VAR_PROTO: + return LengthOp_VAR_PROTO + case ics23.LengthOp_VAR_RLP: + return LengthOp_VAR_RLP + case ics23.LengthOp_FIXED32_BIG: + return LengthOp_FIXED32_BIG + case ics23.LengthOp_FIXED32_LITTLE: + return LengthOp_FIXED32_LITTLE + case ics23.LengthOp_FIXED64_BIG: + return LengthOp_FIXED64_BIG + case ics23.LengthOp_FIXED64_LITTLE: + return LengthOp_FIXED64_LITTLE + case ics23.LengthOp_REQUIRE_32_BYTES: + return LengthOp_REQUIRE_32_BYTES + case ics23.LengthOp_REQUIRE_64_BYTES: + return LengthOp_REQUIRE_64_BYTES + default: + panic("unknown length op") + } +} + func (i *InnerOp) ConvertToIcs23InnerOp() *ics23.InnerOp { + if i == nil { + return nil + } ics := new(ics23.InnerOp) ics.Hash = i.Hash.ConvertToIcs23HashOp() ics.Prefix = i.Prefix ics.Suffix = i.Suffix return ics } + +func ConvertFromIcs23InnerOp(i *ics23.InnerOp) *InnerOp { + if i == nil { + return nil + } + op := new(InnerOp) + op.Hash = ConvertFromIcs23HashOp(i.Hash) + op.Prefix = i.Prefix + op.Suffix = i.Suffix + return op +} From 06cb90e6e3b6e78ed68b060a1bfc0076ef29ed4b Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Sun, 23 Jul 2023 14:57:56 +0100 Subject: [PATCH 05/10] unexport methods --- ibc/types/proofs.go | 52 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/ibc/types/proofs.go b/ibc/types/proofs.go index c9f1b54ef..cc9c90ed7 100644 --- a/ibc/types/proofs.go +++ b/ibc/types/proofs.go @@ -27,8 +27,8 @@ func (p *ProofSpec) ConvertToIcs23ProofSpec() *ics23.ProofSpec { return nil } ics := new(ics23.ProofSpec) - ics.LeafSpec = p.LeafSpec.ConvertToIcs23LeafOp() - ics.InnerSpec = p.InnerSpec.ConvertToIcs23InnerSpec() + ics.LeafSpec = p.LeafSpec.convertToIcs23LeafOp() + ics.InnerSpec = p.InnerSpec.convertToIcs23InnerSpec() ics.MaxDepth = p.MaxDepth ics.MinDepth = p.MinDepth ics.PrehashKeyBeforeComparison = p.PrehashKeyBeforeComparison @@ -40,41 +40,41 @@ func ConvertFromIcs23ProofSpec(p *ics23.ProofSpec) *ProofSpec { return nil } spc := new(ProofSpec) - spc.LeafSpec = ConvertFromIcs23LeafOp(p.LeafSpec) - spc.InnerSpec = ConvertFromIcs23InnerSpec(p.InnerSpec) + spc.LeafSpec = convertFromIcs23LeafOp(p.LeafSpec) + spc.InnerSpec = convertFromIcs23InnerSpec(p.InnerSpec) spc.MaxDepth = p.MaxDepth spc.MinDepth = p.MinDepth spc.PrehashKeyBeforeComparison = p.PrehashKeyBeforeComparison return spc } -func (l *LeafOp) ConvertToIcs23LeafOp() *ics23.LeafOp { +func (l *LeafOp) convertToIcs23LeafOp() *ics23.LeafOp { if l == nil { return nil } ics := new(ics23.LeafOp) - ics.Hash = l.Hash.ConvertToIcs23HashOp() - ics.PrehashKey = l.PrehashKey.ConvertToIcs23HashOp() - ics.PrehashValue = l.PrehashValue.ConvertToIcs23HashOp() - ics.Length = l.Length.ConvertToIcs23LenthOp() + ics.Hash = l.Hash.convertToIcs23HashOp() + ics.PrehashKey = l.PrehashKey.convertToIcs23HashOp() + ics.PrehashValue = l.PrehashValue.convertToIcs23HashOp() + ics.Length = l.Length.convertToIcs23LenthOp() ics.Prefix = l.Prefix return ics } -func ConvertFromIcs23LeafOp(l *ics23.LeafOp) *LeafOp { +func convertFromIcs23LeafOp(l *ics23.LeafOp) *LeafOp { if l == nil { return nil } op := new(LeafOp) - op.Hash = ConvertFromIcs23HashOp(l.Hash) - op.PrehashKey = ConvertFromIcs23HashOp(l.PrehashKey) - op.PrehashValue = ConvertFromIcs23HashOp(l.PrehashValue) - op.Length = ConvertFromIcs23LengthOp(l.Length) + op.Hash = convertFromIcs23HashOp(l.Hash) + op.PrehashKey = convertFromIcs23HashOp(l.PrehashKey) + op.PrehashValue = convertFromIcs23HashOp(l.PrehashValue) + op.Length = convertFromIcs23LengthOp(l.Length) op.Prefix = l.Prefix return op } -func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { +func (i *InnerSpec) convertToIcs23InnerSpec() *ics23.InnerSpec { if i == nil { return nil } @@ -84,11 +84,11 @@ func (i *InnerSpec) ConvertToIcs23InnerSpec() *ics23.InnerSpec { ics.MinPrefixLength = i.MinPrefixLength ics.MaxPrefixLength = i.MaxPrefixLength ics.EmptyChild = i.EmptyChild - ics.Hash = i.Hash.ConvertToIcs23HashOp() + ics.Hash = i.Hash.convertToIcs23HashOp() return ics } -func ConvertFromIcs23InnerSpec(i *ics23.InnerSpec) *InnerSpec { +func convertFromIcs23InnerSpec(i *ics23.InnerSpec) *InnerSpec { if i == nil { return nil } @@ -98,11 +98,11 @@ func ConvertFromIcs23InnerSpec(i *ics23.InnerSpec) *InnerSpec { spec.MinPrefixLength = i.MinPrefixLength spec.MaxPrefixLength = i.MaxPrefixLength spec.EmptyChild = i.EmptyChild - spec.Hash = ConvertFromIcs23HashOp(i.Hash) + spec.Hash = convertFromIcs23HashOp(i.Hash) return spec } -func (h HashOp) ConvertToIcs23HashOp() ics23.HashOp { +func (h HashOp) convertToIcs23HashOp() ics23.HashOp { switch h { case HashOp_NO_HASH: return ics23.HashOp_NO_HASH @@ -123,7 +123,7 @@ func (h HashOp) ConvertToIcs23HashOp() ics23.HashOp { } } -func ConvertFromIcs23HashOp(h ics23.HashOp) HashOp { +func convertFromIcs23HashOp(h ics23.HashOp) HashOp { switch h { case ics23.HashOp_NO_HASH: return HashOp_NO_HASH @@ -144,7 +144,7 @@ func ConvertFromIcs23HashOp(h ics23.HashOp) HashOp { } } -func (l LengthOp) ConvertToIcs23LenthOp() ics23.LengthOp { +func (l LengthOp) convertToIcs23LenthOp() ics23.LengthOp { switch l { case LengthOp_NO_PREFIX: return ics23.LengthOp_NO_PREFIX @@ -169,7 +169,7 @@ func (l LengthOp) ConvertToIcs23LenthOp() ics23.LengthOp { } } -func ConvertFromIcs23LengthOp(l ics23.LengthOp) LengthOp { +func convertFromIcs23LengthOp(l ics23.LengthOp) LengthOp { switch l { case ics23.LengthOp_NO_PREFIX: return LengthOp_NO_PREFIX @@ -194,23 +194,23 @@ func ConvertFromIcs23LengthOp(l ics23.LengthOp) LengthOp { } } -func (i *InnerOp) ConvertToIcs23InnerOp() *ics23.InnerOp { +func (i *InnerOp) convertToIcs23InnerOp() *ics23.InnerOp { if i == nil { return nil } ics := new(ics23.InnerOp) - ics.Hash = i.Hash.ConvertToIcs23HashOp() + ics.Hash = i.Hash.convertToIcs23HashOp() ics.Prefix = i.Prefix ics.Suffix = i.Suffix return ics } -func ConvertFromIcs23InnerOp(i *ics23.InnerOp) *InnerOp { +func convertFromIcs23InnerOp(i *ics23.InnerOp) *InnerOp { if i == nil { return nil } op := new(InnerOp) - op.Hash = ConvertFromIcs23HashOp(i.Hash) + op.Hash = convertFromIcs23HashOp(i.Hash) op.Prefix = i.Prefix op.Suffix = i.Suffix return op From 064f69cd1a59fb7392b861b5dfac2ee4bc8e3332 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:20:44 +0100 Subject: [PATCH 06/10] remove unused functions --- ibc/types/proofs.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/ibc/types/proofs.go b/ibc/types/proofs.go index cc9c90ed7..5147ea496 100644 --- a/ibc/types/proofs.go +++ b/ibc/types/proofs.go @@ -193,25 +193,3 @@ func convertFromIcs23LengthOp(l ics23.LengthOp) LengthOp { panic("unknown length op") } } - -func (i *InnerOp) convertToIcs23InnerOp() *ics23.InnerOp { - if i == nil { - return nil - } - ics := new(ics23.InnerOp) - ics.Hash = i.Hash.convertToIcs23HashOp() - ics.Prefix = i.Prefix - ics.Suffix = i.Suffix - return ics -} - -func convertFromIcs23InnerOp(i *ics23.InnerOp) *InnerOp { - if i == nil { - return nil - } - op := new(InnerOp) - op.Hash = convertFromIcs23HashOp(i.Hash) - op.Prefix = i.Prefix - op.Suffix = i.Suffix - return op -} From 61fcc3846b7edd6ca116e9fed6b5b8a0e3684b63 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:33:26 +0100 Subject: [PATCH 07/10] Remove Height field from Events, use uint64 height in queries --- ibc/events/event_manager.go | 9 +++++---- ibc/store/provable_store.go | 4 ++-- ibc/store/provable_store_test.go | 2 +- persistence/gov.go | 5 +++++ persistence/ibc.go | 6 +++--- persistence/test/ibc_test.go | 20 ++++++++------------ persistence/types/ibc.go | 6 +++--- shared/core/types/ibc_events.go | 5 +++++ shared/core/types/proto/ibc_events.proto | 16 +++++----------- shared/modules/ibc_event_module.go | 6 +++--- shared/modules/persistence_module.go | 3 ++- 11 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 shared/core/types/ibc_events.go diff --git a/ibc/events/event_manager.go b/ibc/events/event_manager.go index 19e48cb95..0b233e139 100644 --- a/ibc/events/event_manager.go +++ b/ibc/events/event_manager.go @@ -1,7 +1,7 @@ package events import ( - coreTypes "github.com/pokt-network/pocket/shared/core/types" + core_types "github.com/pokt-network/pocket/shared/core/types" "github.com/pokt-network/pocket/shared/modules" "github.com/pokt-network/pocket/shared/modules/base_modules" ) @@ -42,14 +42,15 @@ func (*EventManager) Create(bus modules.Bus, options ...modules.EventLoggerOptio func (e *EventManager) GetModuleName() string { return modules.EventLoggerModuleName } -func (e *EventManager) EmitEvent(event *coreTypes.IBCEvent) error { +func (e *EventManager) EmitEvent(event *core_types.IBCEvent) error { wCtx := e.GetBus().GetPersistenceModule().NewWriteContext() defer wCtx.Release() return wCtx.SetIBCEvent(event) } -func (e *EventManager) QueryEvents(topic string, height uint64) ([]*coreTypes.IBCEvent, error) { - rCtx, err := e.GetBus().GetPersistenceModule().NewReadContext(int64(height)) +func (e *EventManager) QueryEvents(topic string, height uint64) ([]*core_types.IBCEvent, error) { + currHeight := e.GetBus().GetConsensusModule().CurrentHeight() + rCtx, err := e.GetBus().GetPersistenceModule().NewReadContext(int64(currHeight)) if err != nil { return nil, err } diff --git a/ibc/store/provable_store.go b/ibc/store/provable_store.go index c3ff8a171..cc600843c 100644 --- a/ibc/store/provable_store.go +++ b/ibc/store/provable_store.go @@ -67,8 +67,8 @@ func newProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privat // keys are automatically prefixed with the CommitmentPrefix if not present func (p *provableStore) Get(key []byte) ([]byte, error) { prefixed := applyPrefix(p.prefix, key) - currHeight := int64(p.bus.GetConsensusModule().CurrentHeight()) - rCtx, err := p.bus.GetPersistenceModule().NewReadContext(currHeight) + currHeight := p.bus.GetConsensusModule().CurrentHeight() + rCtx, err := p.bus.GetPersistenceModule().NewReadContext(int64(currHeight)) if err != nil { return nil, err } diff --git a/ibc/store/provable_store_test.go b/ibc/store/provable_store_test.go index 10fdaf3c0..383441ae5 100644 --- a/ibc/store/provable_store_test.go +++ b/ibc/store/provable_store_test.go @@ -432,7 +432,7 @@ func newPersistenceMock(t *testing.T, EXPECT(). GetIBCStoreEntry(gomock.Any(), gomock.Any()). DoAndReturn( - func(key []byte, _ int64) ([]byte, error) { + func(key []byte, _ uint64) ([]byte, error) { value, ok := dbMap[hex.EncodeToString(key)] if !ok { return nil, coreTypes.ErrIBCKeyDoesNotExist(string(key)) diff --git a/persistence/gov.go b/persistence/gov.go index 5ddec2884..73694ac6e 100644 --- a/persistence/gov.go +++ b/persistence/gov.go @@ -17,6 +17,11 @@ func (p *PostgresContext) GetVersionAtHeight(height int64) (string, error) { return "", nil } +// TODO(#882): Implement this function +func (p *PostgresContext) GetRevisionNumber(height int64) uint64 { + return 1 +} + // TODO: Implement this function func (p *PostgresContext) GetSupportedChains(height int64) ([]string, error) { // This is a placeholder function for the RPC endpoint "v1/query/supportedchains" diff --git a/persistence/ibc.go b/persistence/ibc.go index fc9affea4..0544d7197 100644 --- a/persistence/ibc.go +++ b/persistence/ibc.go @@ -14,14 +14,14 @@ import ( // SetIBCStoreEntry sets the key value pair in the IBC store postgres table at the current height func (p *PostgresContext) SetIBCStoreEntry(key, value []byte) error { ctx, tx := p.getCtxAndTx() - if _, err := tx.Exec(ctx, pTypes.InsertIBCStoreEntryQuery(p.Height, key, value)); err != nil { + if _, err := tx.Exec(ctx, pTypes.InsertIBCStoreEntryQuery(uint64(p.Height), key, value)); err != nil { return err } return nil } // GetIBCStoreEntry returns the stored value for the key at the height provided from the IBC store table -func (p *PostgresContext) GetIBCStoreEntry(key []byte, height int64) ([]byte, error) { +func (p *PostgresContext) GetIBCStoreEntry(key []byte, height uint64) ([]byte, error) { ctx, tx := p.getCtxAndTx() row := tx.QueryRow(ctx, pTypes.GetIBCStoreEntryQuery(height, key)) var valueHex string @@ -50,7 +50,7 @@ func (p *PostgresContext) SetIBCEvent(event *coreTypes.IBCEvent) error { return err } eventHex := hex.EncodeToString(eventBz) - if _, err := tx.Exec(ctx, pTypes.InsertIBCEventQuery(p.Height, typeStr, eventHex)); err != nil { + if _, err := tx.Exec(ctx, pTypes.InsertIBCEventQuery(uint64(p.Height), typeStr, eventHex)); err != nil { return err } return nil diff --git a/persistence/test/ibc_test.go b/persistence/test/ibc_test.go index 2fcf86f4e..c12533ec4 100644 --- a/persistence/test/ibc_test.go +++ b/persistence/test/ibc_test.go @@ -75,7 +75,7 @@ func TestIBC_GetIBCStoreEntry(t *testing.T) { testCases := []struct { name string - height int64 + height uint64 key []byte expectedValue []byte expectedErr error @@ -133,13 +133,12 @@ var ( baseAttributeValue = []byte("testValue") ) -func TestIBCSetEvent(t *testing.T) { +func TestIBC_SetIBCEvent(t *testing.T) { // Setup database db := NewTestPostgresContext(t, 1) // Add a single event at height 1 event := new(coreTypes.IBCEvent) event.Topic = "test" - event.Height = 1 event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: baseAttributeKey, Value: baseAttributeValue, @@ -216,7 +215,6 @@ func TestIBCSetEvent(t *testing.T) { db.Height = int64(tc.height) event := new(coreTypes.IBCEvent) event.Topic = tc.topic - event.Height = tc.height for _, attr := range tc.attributes { event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: attr.key, @@ -233,7 +231,7 @@ func TestIBCSetEvent(t *testing.T) { } } -func TestGetIBCEvent(t *testing.T) { +func TestIBC_GetIBCEvent(t *testing.T) { // Setup database db := NewTestPostgresContext(t, 1) // Add events "testKey0", "testKey1", "testKey2", "testKey3" @@ -242,10 +240,6 @@ func TestGetIBCEvent(t *testing.T) { for i := 0; i < 4; i++ { event := new(coreTypes.IBCEvent) event.Topic = "test" - event.Height = uint64(i + 1) - if i == 3 { - event.Height = uint64(i) // add a second event at height 3 - } s := strconv.Itoa(i) event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: []byte("testKey" + s), @@ -253,8 +247,11 @@ func TestGetIBCEvent(t *testing.T) { }) events = append(events, event) } - for _, event := range events { - db.Height = int64(event.Height) + for i, event := range events { + db.Height = int64(i + 1) + if i == 3 { + db.Height = int64(i) + } require.NoError(t, db.SetIBCEvent(event)) } @@ -301,7 +298,6 @@ func TestGetIBCEvent(t *testing.T) { require.NoError(t, err) require.Len(t, got, tc.expectedLength) for i, index := range tc.eventsIndexes { - require.Equal(t, events[index].Height, got[i].Height) require.Equal(t, events[index].Topic, got[i].Topic) require.Equal(t, events[index].Attributes[0].Key, got[i].Attributes[0].Key) require.Equal(t, events[index].Attributes[0].Value, got[i].Attributes[0].Value) diff --git a/persistence/types/ibc.go b/persistence/types/ibc.go index a783bcf82..7b0d01ee5 100644 --- a/persistence/types/ibc.go +++ b/persistence/types/ibc.go @@ -23,7 +23,7 @@ const ( ) // InsertIBCStoreEntryQuery returns the query to insert a key/value pair into the ibc_entries table -func InsertIBCStoreEntryQuery(height int64, key, value []byte) string { +func InsertIBCStoreEntryQuery(height uint64, key, value []byte) string { return fmt.Sprintf( `INSERT INTO %s(height, key, value) VALUES(%d, '%s', '%s')`, IBCStoreTableName, @@ -34,7 +34,7 @@ func InsertIBCStoreEntryQuery(height int64, key, value []byte) string { } // InsertIBCEventQuery returns the query to insert an event into the ibc_events table -func InsertIBCEventQuery(height int64, topic, eventHex string) string { +func InsertIBCEventQuery(height uint64, topic, eventHex string) string { return fmt.Sprintf( `INSERT INTO %s(height, topic, event) VALUES(%d, '%s', '%s')`, IBCEventLogTableName, @@ -45,7 +45,7 @@ func InsertIBCEventQuery(height int64, topic, eventHex string) string { } // GetIBCStoreEntryQuery returns the latest value for the key at the height provided or at the last updated height -func GetIBCStoreEntryQuery(height int64, key []byte) string { +func GetIBCStoreEntryQuery(height uint64, key []byte) string { return fmt.Sprintf( `SELECT value FROM %s WHERE height <= %d AND key = '%s' ORDER BY height DESC LIMIT 1`, IBCStoreTableName, diff --git a/shared/core/types/ibc_events.go b/shared/core/types/ibc_events.go new file mode 100644 index 000000000..3c3c7580a --- /dev/null +++ b/shared/core/types/ibc_events.go @@ -0,0 +1,5 @@ +package types + +func NewAttribute(key, value []byte) *Attribute { + return &Attribute{Key: key, Value: value} +} diff --git a/shared/core/types/proto/ibc_events.proto b/shared/core/types/proto/ibc_events.proto index 15041214a..0ad47774b 100644 --- a/shared/core/types/proto/ibc_events.proto +++ b/shared/core/types/proto/ibc_events.proto @@ -4,18 +4,12 @@ package core; option go_package = "github.com/pokt-network/pocket/shared/core/types"; -// Attribute represents a key-value pair in an IBC event +message IBCEvent { + string topic = 1; + repeated Attribute attributes = 2; +} + message Attribute { bytes key = 1; bytes value = 2; } - -// IBCEvent are used after a series of insertions/updates/deletions to the IBC store -// they capture the type of changes made, such as creating a new light client, or -// opening a connection. They also capture the height at which the change was made -// and the different key-value pairs that were modified in the attributes field. -message IBCEvent { - string topic = 1; - uint64 height = 2; - repeated Attribute attributes = 3; -} diff --git a/shared/modules/ibc_event_module.go b/shared/modules/ibc_event_module.go index 56a80f07f..d3f628770 100644 --- a/shared/modules/ibc_event_module.go +++ b/shared/modules/ibc_event_module.go @@ -3,7 +3,7 @@ package modules //go:generate mockgen -destination=./mocks/ibc_event_module_mock.go github.com/pokt-network/pocket/shared/modules EventLogger import ( - coreTypes "github.com/pokt-network/pocket/shared/core/types" + core_types "github.com/pokt-network/pocket/shared/core/types" ) const EventLoggerModuleName = "event_logger" @@ -16,6 +16,6 @@ type EventLogger interface { Submodule eventLoggerFactory - EmitEvent(event *coreTypes.IBCEvent) error - QueryEvents(topic string, height uint64) ([]*coreTypes.IBCEvent, error) + EmitEvent(event *core_types.IBCEvent) error + QueryEvents(topic string, height uint64) ([]*core_types.IBCEvent, error) } diff --git a/shared/modules/persistence_module.go b/shared/modules/persistence_module.go index b510d835b..6ee724d63 100644 --- a/shared/modules/persistence_module.go +++ b/shared/modules/persistence_module.go @@ -161,6 +161,7 @@ type PersistenceReadContext interface { // Version queries GetVersionAtHeight(height int64) (string, error) // TODO: Implement this + GetRevisionNumber(height int64) uint64 // TODO(#882): Implement this // Supported Chains Queries GetSupportedChains(height int64) ([]string, error) // TODO: Implement this @@ -245,7 +246,7 @@ type PersistenceReadContext interface { // IBC Queries // GetIBCStoreEntry returns the value of the key at the given height from the ibc_entries table - GetIBCStoreEntry(key []byte, height int64) ([]byte, error) + GetIBCStoreEntry(key []byte, height uint64) ([]byte, error) // GetIBCEvent returns the matching IBC events for any topic at the height provied GetIBCEvents(height uint64, topic string) ([]*coreTypes.IBCEvent, error) } From 1519ad0009da1c55d5d9947024fe1fdd54621c22 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:39:41 +0100 Subject: [PATCH 08/10] Add ICS-02 Client Interfaces --- Makefile | 2 + .../light_clients/types/proto/pocket.proto | 58 +++++ ibc/client/types/proto/wasm.proto | 35 +++ shared/modules/bus_module.go | 1 + shared/modules/ibc_client_module.go | 212 ++++++++++++++++++ shared/modules/ibc_host_module.go | 177 +-------------- shared/modules/ibc_module.go | 153 ++++++++++++- 7 files changed, 461 insertions(+), 177 deletions(-) create mode 100644 ibc/client/light_clients/types/proto/pocket.proto create mode 100644 ibc/client/types/proto/wasm.proto create mode 100644 shared/modules/ibc_client_module.go diff --git a/Makefile b/Makefile index 192cd67a6..d9ab7c815 100644 --- a/Makefile +++ b/Makefile @@ -320,6 +320,8 @@ protogen_local: go_protoc-go-inject-tag ## Generate go structures for all of the # IBC make copy_ics23_proto $(PROTOC_SHARED) -I=./ibc/types/proto --go_out=./ibc/types ./ibc/types/proto/*.proto + $(PROTOC_SHARED) -I=./ibc/client/types/proto --go_out=./ibc/client/types ./ibc/client/types/proto/*.proto + $(PROTOC_SHARED) -I=./ibc/client/types/proto -I=./ibc/client/light_clients/types/proto -I=./shared/core/types/proto -I=./ibc/types/proto --go_out=./ibc/client/light_clients/types ./ibc/client/light_clients/types/proto/*.proto # echo "View generated proto files by running: make protogen_show" diff --git a/ibc/client/light_clients/types/proto/pocket.proto b/ibc/client/light_clients/types/proto/pocket.proto new file mode 100644 index 000000000..f6acd3d1b --- /dev/null +++ b/ibc/client/light_clients/types/proto/pocket.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package core; + +option go_package = "github.com/pokt-network/pocket/ibc/client/light_client/types"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "proofs.proto"; +import "wasm.proto"; +import "block.proto"; + +// PocketConsensusState defines the ibc client consensus state for Pocket +message PocketConsensusState { + google.protobuf.Timestamp timestamp = 1; // unixnano timestamp of the block + string state_hash = 2; // hex encoded root state tree hash + map state_tree_hashes = 3; // map of state tree hashes; map[TreeName]hex(TreeRootHash) + string next_val_set_hash = 4; // hex encoded sha3_256 hash of the next validator set +} + +// PocketClientState defines the ibc client state for Pocket +message PocketClientState { + string network_id = 1; // network identifier string + Fraction trust_level = 2; // fraction of the validator set that is required to sign off on new blocks + google.protobuf.Duration trusting_period = 3; // the duration of the period since the LastestTimestamp where the state can be upgraded + google.protobuf.Duration unbonding_period = 4; // the duration of the staking unbonding period + google.protobuf.Duration max_clock_drift = 5; // the max duration a new header's time can be in the future + Height latest_height = 6; // the latest height the client was updated to + uint64 frozen_height = 7; // the height at which the client was frozen due to a misbehaviour + ProofSpec proof_spec = 8; // ics23 proof spec used in verifying proofs + // RESEARCH: Figure out exactly what this is for in tendermint, why it is needed and if we need it also + // repeated string upgrade_path = 9; // the upgrade path for the new client state +} + +// Fraction defines a positive rational number +message Fraction { + uint64 numerator = 1; + uint64 denominator = 2; +} + +// PocketHeader defines the ibc client header for the Pocket network +message PocketHeader { + BlockHeader block_header = 1; // pocket consensus block header + ValidatorSet validator_set = 2; // new validator set for the updating client + // the consensus state at trusted_height must be within the unbonding_period to correctly verify the new header + Height trusted_height = 3; // height of the ConsensusState stored used to verify the new header + // trusted_validators must hash to the ConsensusState.NextValSetHash as this is the last trusted validator set + ValidatorSet trusted_validators = 4; // already stored validator set used to verify the update +} + +// PocketMisbehaviour defines the ibc client misbehaviour for the Pocket network +// +// The two conflicting headers are submitted as evidence to verify the Pocket +// network has misbehaved. +message PocketMisbehaviour { + PocketHeader header_1 = 1; // the first header + PocketHeader header_2 = 2; // the second header +} diff --git a/ibc/client/types/proto/wasm.proto b/ibc/client/types/proto/wasm.proto new file mode 100644 index 000000000..ebe6ce5e1 --- /dev/null +++ b/ibc/client/types/proto/wasm.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package core; + +option go_package = "github.com/pokt-network/pocket/ibc/client/types"; + +// ClientState for a Wasm light client +message ClientState { + bytes data = 1; // opaque data passed to the wasm client + bytes wasm_checksum = 2; // checksum of the wasm client code + Height recent_height = 3; // latest height of the client +} + +// ConsensusState for a Wasm light client +message ConsensusState { + bytes data = 1; // opaque data passed to the wasm client + uint64 timestamp = 2; // unix nano timestamp of the block +} + +// Header for a Wasm light client +message Header { + bytes data = 1; // opaque data passed to the wasm client + Height height = 2; // height of the header +} + +// Misbehaviour for a Wasm light client +message Misbehaviour { + bytes data = 1; // opaque data passed to the wasm client +} + +// Height represents the height of a client +message Height { + uint64 revision_number = 1; + uint64 revision_height = 2; +} diff --git a/shared/modules/bus_module.go b/shared/modules/bus_module.go index 3981f9f8c..14b329643 100644 --- a/shared/modules/bus_module.go +++ b/shared/modules/bus_module.go @@ -45,4 +45,5 @@ type Bus interface { GetIBCHost() IBCHostSubmodule GetBulkStoreCacher() BulkStoreCacher GetEventLogger() EventLogger + GetClientManager() ClientManager } diff --git a/shared/modules/ibc_client_module.go b/shared/modules/ibc_client_module.go new file mode 100644 index 000000000..d4dc1d716 --- /dev/null +++ b/shared/modules/ibc_client_module.go @@ -0,0 +1,212 @@ +package modules + +//go:generate mockgen -destination=./mocks/ibc_client_module_mock.go github.com/pokt-network/pocket/shared/modules ClientManager + +import ( + "google.golang.org/protobuf/proto" +) + +type ClientStatus string + +const ( + ClientManagerModuleName = "client_manager" + + // Client Status types + ActiveStatus ClientStatus = "active" + ExpiredStatus ClientStatus = "expired" + FrozenStatus ClientStatus = "frozen" + UnauthorizedStatus ClientStatus = "unauthorized" + UnknownStatus ClientStatus = "unknown" +) + +type ClientManagerOption func(ClientManager) + +type clientManagerFactory = FactoryWithOptions[ClientManager, ClientManagerOption] + +// ClientManager is the interface that defines the methods needed to interact with an +// IBC light client it manages the different lifecycle methods for the different clients +// https://github.com/cosmos/ibc/tree/main/spec/core/ics-002-client-semantics +type ClientManager interface { + Submodule + clientManagerFactory + + // === Client Lifecycle Management === + + // CreateClient creates a new client with the given client state and initial consensus state + // and initialises its unique identifier in the IBC store + CreateClient(ClientState, ConsensusState) (string, error) + + // UpdateClient updates an existing client with the given ClientMessage, given that + // the ClientMessage can be verified using the existing ClientState and ConsensusState + UpdateClient(identifier string, clientMessage ClientMessage) error + + // UpgradeClient upgrades an existing client with the given identifier using the + // ClientState and ConsenusState provided. It can only do so if the new client + // was committed to by the old client at the specified upgrade height + UpgradeClient( + identifier string, + clientState ClientState, consensusState ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, + ) error + + // === Client Queries === + + // GetConsensusState returns the ConsensusState at the given height for the given client + GetConsensusState(identifier string, height Height) (ConsensusState, error) + + // GetClientState returns the ClientState for the given client + GetClientState(identifier string) (ClientState, error) + + // GetHostConsensusState returns the ConsensusState at the given height for the host chain + GetHostConsensusState(height Height) (ConsensusState, error) + + // GetHostClientState returns the ClientState at the provided height for the host chain + GetHostClientState(height Height) (ClientState, error) + + // GetCurrentHeight returns the current IBC client height of the network + GetCurrentHeight() (Height, error) + + // VerifyHostClientState verifies the client state for a client running on a + // counterparty chain is valid, checking against the current host client state + VerifyHostClientState(ClientState) error +} + +// ClientState is an interface that defines the methods required by a clients +// implementation of their own client state object +// +// ClientState is an opaque data structure defined by a client type. It may keep +// arbitrary internal state to track verified roots and past misbehaviours. +type ClientState interface { + proto.Message + + GetData() []byte + GetWasmChecksum() []byte + ClientType() string + GetLatestHeight() Height + Validate() error + + // Status returns the status of the client. Only Active clients are allowed + // to process packets. + Status(clientStore ProvableStore) ClientStatus + + // GetTimestampAtHeight must return the timestamp for the consensus state + // associated with the provided height. + GetTimestampAtHeight(clientStore ProvableStore, height Height) (uint64, error) + + // Initialise is called upon client creation, it allows the client to perform + // validation on the initial consensus state and set the client state, + // consensus state and any client-specific metadata necessary for correct + // light client operation in the provided client store. + Initialise(clientStore ProvableStore, consensusState ConsensusState) error + + // VerifyMembership is a generic proof verification method which verifies a + // proof of the existence of a value at a given CommitmentPath at the + // specified height. The path is expected to be the full CommitmentPath + VerifyMembership( + clientStore ProvableStore, + height Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, path, value []byte, + ) error + + // VerifyNonMembership is a generic proof verification method which verifies + // the absence of a given CommitmentPath at a specified height. The path is + // expected to be the full CommitmentPath + VerifyNonMembership( + clientStore ProvableStore, + height Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, path []byte, + ) error + + // VerifyClientMessage verifies a ClientMessage. A ClientMessage could be a + // Header, Misbehaviour, or batch update. It must handle each type of + // ClientMessage appropriately. Calls to CheckForMisbehaviour, UpdateState, + // and UpdateStateOnMisbehaviour will assume that the content of the + // ClientMessage has been verified and can be trusted. An error should be + // returned if the ClientMessage fails to verify. + VerifyClientMessage(clientStore ProvableStore, clientMsg ClientMessage) error + + // Checks for evidence of a misbehaviour in Header or Misbehaviour type. + // It assumes the ClientMessage has already been verified. + CheckForMisbehaviour(clientStore ProvableStore, clientMsg ClientMessage) bool + + // UpdateStateOnMisbehaviour should perform appropriate state changes on a + // client state given that misbehaviour has been detected and verified + UpdateStateOnMisbehaviour(clientStore ProvableStore, clientMsg ClientMessage) error + + // UpdateState updates and stores as necessary any associated information + // for an IBC client, such as the ClientState and corresponding ConsensusState. + // Upon successful update, a consensus height is returned. + // It assumes the ClientMessage has already been verified. + UpdateState(clientStore ProvableStore, clientMsg ClientMessage) (Height, error) + + // Upgrade functions + // NOTE: proof heights are not included as upgrade to a new revision is expected to pass only on the last + // height committed by the current revision. Clients are responsible for ensuring that the planned last + // height of the current revision is somehow encoded in the proof verification process. + // This is to ensure that no premature upgrades occur, since upgrade plans committed to by the counterparty + // may be cancelled or modified before the last planned height. + // If the upgrade is verified, the upgraded client and consensus states must be set in the client store. + VerifyUpgradeAndUpdateState( + clientStore ProvableStore, + newClient ClientState, + newConsState ConsensusState, + proofUpgradeClient, + proofUpgradeConsState []byte, + ) error +} + +// ConsensusState is an interface that defines the methods required by a clients +// implementation of their own consensus state object +// +// ConsensusState is an opaque data structure defined by a client type, used by the +// validity predicate to verify new commits & state roots. Likely the structure will +// contain the last commit produced by the consensus process, including signatures +// and validator set metadata. +type ConsensusState interface { + proto.Message + + GetData() []byte + ClientType() string + GetTimestamp() uint64 + ValidateBasic() error +} + +// ClientMessage is an interface that defines the methods required by a clients +// implementation of their own client message object +// +// A ClientMessage is an opaque data structure defined by a client type which +// provides information to update the client. ClientMessages can be submitted +// to an associated client to add new ConsensusState(s) and/or update the +// ClientState. They likely contain a height, a proof, a commitment root, and +// possibly updates to the validity predicate. +type ClientMessage interface { + proto.Message + + GetData() []byte + ClientType() string + ValidateBasic() error +} + +// Height is an interface that defines the methods required by a clients +// implementation of their own height object +// +// Heights usually have two components: revision number and revision height. +type Height interface { + IsZero() bool + LT(Height) bool + LTE(Height) bool + EQ(Height) bool + GT(Height) bool + GTE(Height) bool + Increment() Height + Decrement() Height + GetRevisionNumber() uint64 + GetRevisionHeight() uint64 + ToString() string // must define a determinstic `String()` method not the generated protobuf method +} + +func (s ClientStatus) String() string { + return string(s) +} diff --git a/shared/modules/ibc_host_module.go b/shared/modules/ibc_host_module.go index 9a94d5b0f..c009337cf 100644 --- a/shared/modules/ibc_host_module.go +++ b/shared/modules/ibc_host_module.go @@ -4,7 +4,7 @@ import ( "github.com/pokt-network/pocket/runtime/configs" ) -//go:generate mockgen -destination=./mocks/ibc_host_module_mock.go github.com/pokt-network/pocket/shared/modules IBCHostSubmodule,IBCHandler +//go:generate mockgen -destination=./mocks/ibc_host_module_mock.go github.com/pokt-network/pocket/shared/modules IBCHostSubmodule const IBCHostSubmoduleName = "ibc_host" @@ -22,184 +22,9 @@ type IBCHostSubmodule interface { Submodule ibcHostFactory - // IBC related operations - IBCHandler - // GetTimestamp returns the current unix timestamp for the host machine GetTimestamp() uint64 // GetProvableStore returns an instance of a ProvableStore managed by the StoreManager GetProvableStore(name string) (ProvableStore, error) } - -// INCOMPLETE: Split into multiple interfaces per ICS component and embed in the handler -// IBCHandler is the interface through which the different IBC sub-modules can be interacted with -// https://github.com/cosmos/ibc/tree/main/spec/core/ics-025-handler-interface -type IBCHandler interface { - // === Client Lifecycle Management === - // https://github.com/cosmos/ibc/tree/main/spec/core/ics-002-client-semantics - - // CreateClient creates a new client with the given client state and initial consensus state - // and initialises its unique identifier in the IBC store - // CreateClient(clientState clientState, consensusState consensusState) error - - // UpdateClient updates an existing client with the given ClientMessage, given that - // the ClientMessage can be verified using the existing ClientState and ConsensusState - // UpdateClient(identifier Identifier, clientMessage ClientMessage) error - - // QueryConsensusState returns the ConsensusState at the given height for the given client - // QueryConsensusState(identifier Identifier, height Height) ConsensusState - - // QueryClientState returns the ClientState for the given client - // QueryClientState(identifier Identifier) ClientState - - // SubmitMisbehaviour submits evidence for a misbehaviour to the client, possibly invalidating - // previously valid state roots and thus preventing future updates - // SubmitMisbehaviour(identifier Identifier, clientMessage ClientMessage) error - - // === Connection Lifecycle Management === - // https://github.com/cosmos/ibc/tree/main/spec/core/ics-003-connection-semantics - - // ConnOpenInit attempts to initialise a connection to a given counterparty chain (executed on source chain) - /** - ConnOpenInit( - counterpartyPrefix CommitmentPrefix, - clientIdentifier, counterpartyClientIdentifier Identifier, - version: string, // Optional: If version is included, the handshake must fail if the version is not the same - delayPeriodTime, delayPeriodBlocks uint64, - ) error - **/ - - // ConnOpenTry relays a notice of a connection attempt to a counterpaty chain (executed on destination chain) - /** - ConnOpenTry( - counterpartyPrefix CommitmentPrefix, - counterpartyConnectionIdentifier, counterpartyClientIdentifier, clientIdentifier Identifier, - clientState ClientState, - counterpartyVersions []string, - delayPeriodTime, delayPeriodBlocks uint64, - proofInit, proofClient, proofConsensus ics23.CommitmentProof, - proofHeight, consensusHeight Height, - hostConsensusStateProof bytes, - ) error - **/ - - // ConnOpenAck relays the acceptance of a connection open attempt from counterparty chain (executed on source chain) - /** - ConnOpenAck( - identifier, counterpartyIdentifier Identifier, - clientState ClientState, - version string, - proofTry, proofClient, proofConsensus ics23.CommitmentProof, - proofHeight, consensusHeight Height, - hostConsensusStateProof bytes, - ) error - **/ - - // ConnOpenConfirm confirms opening of a connection to the counterparty chain after which the - // connection is open to both chains (executed on destination chain) - // ConnOpenConfirm(identifier Identifier, proofAck ics23.CommitmentProof, proofHeight Height) error - - // QueryConnection returns the ConnectionEnd for the given connection identifier - // QueryConnection(identifier Identifier) (ConnectionEnd, error) - - // QueryClientConnections returns the list of connection identifiers associated with a given client - // QueryClientConnections(clientIdentifier Identifier) ([]Identifier, error) - - // === Channel Lifecycle Management === - // https://github.com/cosmos/ibc/tree/main/spec/core/ics-004-channel-and-packet-semantics - - // ChanOpenInit initialises a channel opening handshake with a counterparty chain (executed on source chain) - /** - ChanOpenInit( - order ChannelOrder, - connectionHops []Identifier, - portIdentifier, counterpartyPortIdentifier Identifier, - version string, - ) (channelIdentifier Identifier, channelCapability CapabilityKey, err Error) - **/ - - // ChanOpenTry attempts to accept the channel opening handshake from a counterparty chain (executed on destination chain) - /** - ChanOpenTry( - order ChannelOrder, - connectionHops []Identifier, - portIdentifier, counterpartyPortIdentifier, counterpartyChannelIdentifier Identifier, - version, counterpartyVersion string, - proofInit ics23.CommitmentProof, - ) (channelIdentifier Identifier, channelCapability CapabilityKey, err Error) - **/ - - // ChanOpenAck relays acceptance of a channel opening handshake from a counterparty chain (executed on source chain) - /** - ChanOpenAck( - portIdentifier, channelIdentifier, counterpartyChannelIdentifier Identifier, - counterpartyVersion string, - proofTry ics23.CommitmentProof, - proofHeight Height, - ) error - **/ - - // ChanOpenConfirm acknowledges the acknowledgment of the channel opening hanshake on the counterparty - // chain after which the channel opening handshake is complete (executed on destination chain) - // ChanOpenConfirm(portIdentifier, channelIdentifier Identifier, proofAck ics23.CommitmentProof, proofHeight Height) error - - // ChanCloseInit is called to close the ChannelEnd with the given identifier on the host machine - // ChanCloseInit(portIdentifier, channelIdentifier Identifier) error - - // ChanCloseConfirm is called to close the ChannelEnd on the counterparty chain as the other end is closed - // ChanCloseConfirm(portIdentifier, channelIdentifier Identifier, proofInit ics23.CommitmentProof, proofHeight Height) error - - // === Packet Relaying === - - // SendPacket is called to send an IBC packet on the channel with the given identifier - /** - SendPacket( - capability CapabilityKey, - sourcePort Identifier, - sourceChannel Identifier, - timeoutHeight Height, - timeoutTimestamp uint64, - data []byte, - ) (sequence uint64, err error) - **/ - - // RecvPacket is called in order to receive an IBC packet on the corresponding channel end - // on the counterpaty chain - // RecvPacket(packet OpaquePacket, proof ics23.CommitmentProof, proofHeight Height, relayer string) (Packet, error) - - // AcknowledgePacket is called to acknowledge the receipt of an IBC packet to the corresponding chain - /** - AcknowledgePacket( - packet OpaquePacket, - acknowledgement []byte, - proof ics23.CommitmentProof, - proofHeight Height, - relayer string, - ) (Packet, error) - **/ - - // TimeoutPacket is called to timeout an IBC packet on the corresponding channel end after the - // timeout height or timeout timestamp has passed and the packet has not been committed - /** - TimeoutPacket( - packet OpaquePacket, - proof ics23.CommitmentProof, - proofHeight Height, - nextSequenceRecv *uint64, - relayer string, - ) (Packet, error) - **/ - - // TimeoutOnClose is called to prove to the counterparty chain that the channel end has been - // closed and that the packet sent over this channel will not be received - /** - TimeoutOnClose( - packet OpaquePacket, - proof, proofClosed ics23.CommitmentProof, - proofHeight Height, - nextSequenceRecv *uint64, - relayer string, - ) (Packet, error) - **/ -} diff --git a/shared/modules/ibc_module.go b/shared/modules/ibc_module.go index 20ed2fa65..ab66fb1b1 100644 --- a/shared/modules/ibc_module.go +++ b/shared/modules/ibc_module.go @@ -2,7 +2,7 @@ package modules import "google.golang.org/protobuf/types/known/anypb" -//go:generate mockgen -destination=./mocks/ibc_module_mock.go github.com/pokt-network/pocket/shared/modules IBCModule +//go:generate mockgen -destination=./mocks/ibc_module_mock.go github.com/pokt-network/pocket/shared/modules IBCModule,IBCHandler const IBCModuleName = "ibc" @@ -11,3 +11,154 @@ type IBCModule interface { HandleEvent(*anypb.Any) error } + +// INCOMPLETE: Split into multiple interfaces per ICS component and embed in the handler +// IBCHandler is the interface through which the different IBC sub-modules can be interacted with +// https://github.com/cosmos/ibc/tree/main/spec/core/ics-025-handler-interface +type IBCHandler interface { + // === Connection Lifecycle Management === + // https://github.com/cosmos/ibc/tree/main/spec/core/ics-003-connection-semantics + + // ConnOpenInit attempts to initialise a connection to a given counterparty chain (executed on source chain) + /** + ConnOpenInit( + counterpartyPrefix CommitmentPrefix, + clientIdentifier, counterpartyClientIdentifier Identifier, + version: string, // Optional: If version is included, the handshake must fail if the version is not the same + delayPeriodTime, delayPeriodBlocks uint64, + ) error + **/ + + // ConnOpenTry relays a notice of a connection attempt to a counterpaty chain (executed on destination chain) + /** + ConnOpenTry( + counterpartyPrefix CommitmentPrefix, + counterpartyConnectionIdentifier, counterpartyClientIdentifier, clientIdentifier Identifier, + clientState ClientState, + counterpartyVersions []string, + delayPeriodTime, delayPeriodBlocks uint64, + proofInit, proofClient, proofConsensus ics23.CommitmentProof, + proofHeight, consensusHeight Height, + hostConsensusStateProof bytes, + ) error + **/ + + // ConnOpenAck relays the acceptance of a connection open attempt from counterparty chain (executed on source chain) + /** + ConnOpenAck( + identifier, counterpartyIdentifier Identifier, + clientState ClientState, + version string, + proofTry, proofClient, proofConsensus ics23.CommitmentProof, + proofHeight, consensusHeight Height, + hostConsensusStateProof bytes, + ) error + **/ + + // ConnOpenConfirm confirms opening of a connection to the counterparty chain after which the + // connection is open to both chains (executed on destination chain) + // ConnOpenConfirm(identifier Identifier, proofAck ics23.CommitmentProof, proofHeight Height) error + + // QueryConnection returns the ConnectionEnd for the given connection identifier + // QueryConnection(identifier Identifier) (ConnectionEnd, error) + + // QueryClientConnections returns the list of connection identifiers associated with a given client + // QueryClientConnections(clientIdentifier Identifier) ([]Identifier, error) + + // === Channel Lifecycle Management === + // https://github.com/cosmos/ibc/tree/main/spec/core/ics-004-channel-and-packet-semantics + + // ChanOpenInit initialises a channel opening handshake with a counterparty chain (executed on source chain) + /** + ChanOpenInit( + order ChannelOrder, + connectionHops []Identifier, + portIdentifier, counterpartyPortIdentifier Identifier, + version string, + ) (channelIdentifier Identifier, channelCapability CapabilityKey, err Error) + **/ + + // ChanOpenTry attempts to accept the channel opening handshake from a counterparty chain (executed on destination chain) + /** + ChanOpenTry( + order ChannelOrder, + connectionHops []Identifier, + portIdentifier, counterpartyPortIdentifier, counterpartyChannelIdentifier Identifier, + version, counterpartyVersion string, + proofInit ics23.CommitmentProof, + ) (channelIdentifier Identifier, channelCapability CapabilityKey, err Error) + **/ + + // ChanOpenAck relays acceptance of a channel opening handshake from a counterparty chain (executed on source chain) + /** + ChanOpenAck( + portIdentifier, channelIdentifier, counterpartyChannelIdentifier Identifier, + counterpartyVersion string, + proofTry ics23.CommitmentProof, + proofHeight Height, + ) error + **/ + + // ChanOpenConfirm acknowledges the acknowledgment of the channel opening hanshake on the counterparty + // chain after which the channel opening handshake is complete (executed on destination chain) + // ChanOpenConfirm(portIdentifier, channelIdentifier Identifier, proofAck ics23.CommitmentProof, proofHeight Height) error + + // ChanCloseInit is called to close the ChannelEnd with the given identifier on the host machine + // ChanCloseInit(portIdentifier, channelIdentifier Identifier) error + + // ChanCloseConfirm is called to close the ChannelEnd on the counterparty chain as the other end is closed + // ChanCloseConfirm(portIdentifier, channelIdentifier Identifier, proofInit ics23.CommitmentProof, proofHeight Height) error + + // === Packet Relaying === + + // SendPacket is called to send an IBC packet on the channel with the given identifier + /** + SendPacket( + capability CapabilityKey, + sourcePort Identifier, + sourceChannel Identifier, + timeoutHeight Height, + timeoutTimestamp uint64, + data []byte, + ) (sequence uint64, err error) + **/ + + // RecvPacket is called in order to receive an IBC packet on the corresponding channel end + // on the counterpaty chain + // RecvPacket(packet OpaquePacket, proof ics23.CommitmentProof, proofHeight Height, relayer string) (Packet, error) + + // AcknowledgePacket is called to acknowledge the receipt of an IBC packet to the corresponding chain + /** + AcknowledgePacket( + packet OpaquePacket, + acknowledgement []byte, + proof ics23.CommitmentProof, + proofHeight Height, + relayer string, + ) (Packet, error) + **/ + + // TimeoutPacket is called to timeout an IBC packet on the corresponding channel end after the + // timeout height or timeout timestamp has passed and the packet has not been committed + /** + TimeoutPacket( + packet OpaquePacket, + proof ics23.CommitmentProof, + proofHeight Height, + nextSequenceRecv *uint64, + relayer string, + ) (Packet, error) + **/ + + // TimeoutOnClose is called to prove to the counterparty chain that the channel end has been + // closed and that the packet sent over this channel will not be received + /** + TimeoutOnClose( + packet OpaquePacket, + proof, proofClosed ics23.CommitmentProof, + proofHeight Height, + nextSequenceRecv *uint64, + relayer string, + ) (Packet, error) + **/ +} From d87cf1c0745d5766e35888618065c30f4b68f0f6 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:57:29 +0100 Subject: [PATCH 09/10] Add ICS-02 Client Implementations and Methods --- ibc/client/events.go | 74 ++++ ibc/client/introspect.go | 152 ++++++++ ibc/client/light_clients/types/fraction.go | 42 ++ ibc/client/queries.go | 34 ++ ibc/client/submodule.go | 225 +++++++++++ ibc/client/types/client.go | 242 ++++++++++++ ibc/client/types/consensus.go | 33 ++ ibc/client/types/events.go | 17 + ibc/client/types/header.go | 23 ++ ibc/client/types/height.go | 76 ++++ ibc/client/types/height_test.go | 430 +++++++++++++++++++++ ibc/client/types/misbehaviour.go | 22 ++ ibc/client/types/queries.go | 63 +++ ibc/client/types/queries_test.go | 411 ++++++++++++++++++++ ibc/client/types/update.go | 123 ++++++ ibc/client/types/upgrade.go | 73 ++++ ibc/client/types/validate_test.go | 172 +++++++++ ibc/docs/README.md | 9 + ibc/docs/ics02.md | 156 ++++++++ ibc/host/submodule.go | 16 +- ibc/host_introspection_test.go | 271 +++++++++++++ ibc/main_test.go | 5 +- ibc/path/keys_ics02.go | 30 +- ibc/store/bulk_store_cache.go | 2 +- ibc/store/provable_store.go | 8 +- ibc/store/provable_store_test.go | 8 +- runtime/bus.go | 4 + shared/core/types/error.go | 8 +- 28 files changed, 2704 insertions(+), 25 deletions(-) create mode 100644 ibc/client/events.go create mode 100644 ibc/client/introspect.go create mode 100644 ibc/client/light_clients/types/fraction.go create mode 100644 ibc/client/queries.go create mode 100644 ibc/client/submodule.go create mode 100644 ibc/client/types/client.go create mode 100644 ibc/client/types/consensus.go create mode 100644 ibc/client/types/events.go create mode 100644 ibc/client/types/header.go create mode 100644 ibc/client/types/height.go create mode 100644 ibc/client/types/height_test.go create mode 100644 ibc/client/types/misbehaviour.go create mode 100644 ibc/client/types/queries.go create mode 100644 ibc/client/types/queries_test.go create mode 100644 ibc/client/types/update.go create mode 100644 ibc/client/types/upgrade.go create mode 100644 ibc/client/types/validate_test.go create mode 100644 ibc/docs/ics02.md create mode 100644 ibc/host_introspection_test.go diff --git a/ibc/client/events.go b/ibc/client/events.go new file mode 100644 index 000000000..7ea984c5b --- /dev/null +++ b/ibc/client/events.go @@ -0,0 +1,74 @@ +package client + +import ( + client_types "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/shared/codec" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +// emitCreateClientEvent emits a create client event +func (c *clientManager) emitCreateClientEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicCreateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(clientState.GetLatestHeight().ToString())), + }, + }, + ) +} + +// emitUpdateClientEvent emits an update client event +func (c *clientManager) emitUpdateClientEvent( + clientId, clientType string, + consensusHeight modules.Height, + clientMessage modules.ClientMessage, +) error { + // Marshall the client message + clientMsgBz, err := codec.GetCodec().Marshal(clientMessage) + if err != nil { + return err + } + + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicUpdateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientType)), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(consensusHeight.ToString())), + core_types.NewAttribute(client_types.AttributeKeyHeader, clientMsgBz), + }, + }, + ) +} + +// emitUpgradeClientEvent emits an upgrade client event +func (c *clientManager) emitUpgradeClientEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicUpdateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(clientState.GetLatestHeight().ToString())), + }, + }, + ) +} + +// emitSubmitMisbehaviourEvent emits a submit misbehaviour event +func (c *clientManager) emitSubmitMisbehaviourEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicSubmitMisbehaviour, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + }, + }, + ) +} diff --git a/ibc/client/introspect.go b/ibc/client/introspect.go new file mode 100644 index 000000000..80b0bf2b8 --- /dev/null +++ b/ibc/client/introspect.go @@ -0,0 +1,152 @@ +package client + +import ( + "errors" + "time" + + light_client_types "github.com/pokt-network/pocket/ibc/client/light_clients/types" + "github.com/pokt-network/pocket/ibc/client/types" + ibc_types "github.com/pokt-network/pocket/ibc/types" + "github.com/pokt-network/pocket/shared/codec" + "github.com/pokt-network/pocket/shared/modules" + util_types "github.com/pokt-network/pocket/utility/types" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +// GetHostConsensusState returns the ConsensusState at the given height for the +// host chain, the Pocket network. It then serialises this and packs it into a +// ConsensusState object for use in a WASM client +func (c *clientManager) GetHostConsensusState(height modules.Height) (modules.ConsensusState, error) { + blockStore := c.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockStore.GetBlock(height.GetRevisionHeight()) + if err != nil { + return nil, err + } + pocketConsState := &light_client_types.PocketConsensusState{ + Timestamp: block.BlockHeader.Timestamp, + StateHash: block.BlockHeader.StateHash, + StateTreeHashes: block.BlockHeader.StateTreeHashes, + NextValSetHash: block.BlockHeader.NextValSetHash, + } + consBz, err := codec.GetCodec().Marshal(pocketConsState) + if err != nil { + return nil, err + } + return types.NewConsensusState(consBz, uint64(pocketConsState.Timestamp.AsTime().UnixNano())), nil +} + +// GetHostClientState returns the ClientState at the given height for the host +// chain, the Pocket network. +// +// This function is used to validate the state of a client running on a +// counterparty chain. +func (c *clientManager) GetHostClientState(height modules.Height) (modules.ClientState, error) { + blockStore := c.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockStore.GetBlock(height.GetRevisionHeight()) + if err != nil { + return nil, err + } + rCtx, err := c.GetBus().GetPersistenceModule().NewReadContext(int64(height.GetRevisionHeight())) + if err != nil { + return nil, err + } + defer rCtx.Release() + unbondingBlocks, err := rCtx.GetIntParam(util_types.ValidatorUnstakingBlocksParamName, int64(height.GetRevisionHeight())) + if err != nil { + return nil, err + } + // TODO_AFTER(#705): use the actual MinimumBlockTime once set + blockTime := time.Minute * 15 + unbondingPeriod := blockTime * time.Duration(unbondingBlocks) // approx minutes per block * blocks + pocketClient := &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(blockTime), // DISCUSS: What is a reasonable MaxClockDrift? + LatestHeight: &types.Height{ + RevisionNumber: height.GetRevisionNumber(), + RevisionHeight: height.GetRevisionHeight(), + }, + ProofSpec: ibc_types.SmtSpec, + } + clientBz, err := codec.GetCodec().Marshal(pocketClient) + if err != nil { + return nil, err + } + return &types.ClientState{ + Data: clientBz, + RecentHeight: pocketClient.LatestHeight, + }, nil +} + +// VerifyHostClientState verifies that a ClientState for a light client running +// on a counterparty chain is valid, by checking it against the result of +// GetHostClientState(counterpartyClientState.GetLatestHeight()) +func (c *clientManager) VerifyHostClientState(counterparty modules.ClientState) error { + height, err := c.GetCurrentHeight() + if err != nil { + return err + } + hostState, err := c.GetHostClientState(height) + if err != nil { + return err + } + poktHost := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(hostState.GetData(), poktHost) + if err != nil { + return err + } + poktCounter := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(counterparty.GetData(), poktCounter) + if err != nil { + return errors.New("counterparty client state is not a PocketClientState") + } + + if poktCounter.FrozenHeight > 0 { + return errors.New("counterparty client state is frozen") + } + if poktCounter.NetworkId != poktHost.NetworkId { + return errors.New("counterparty client state has different network id") + } + if poktCounter.LatestHeight.RevisionNumber != poktHost.LatestHeight.RevisionNumber { + return errors.New("counterparty client state has different revision number") + } + if poktCounter.GetLatestHeight().GTE(poktHost.GetLatestHeight()) { + return errors.New("counterparty client state has a height greater than or equal to the host client state") + } + if poktCounter.TrustLevel.LT(&light_client_types.Fraction{Numerator: 2, Denominator: 3}) || + poktCounter.TrustLevel.GT(&light_client_types.Fraction{Numerator: 1, Denominator: 1}) { + return errors.New("counterparty client state trust level is not in the accepted range") + } + if !proto.Equal(poktCounter.ProofSpec, poktHost.ProofSpec) { + return errors.New("counterparty client state has different proof spec") + } + if poktCounter.UnbondingPeriod != poktHost.UnbondingPeriod { + return errors.New("counterparty client state has different unbonding period") + } + if poktCounter.UnbondingPeriod.AsDuration().Nanoseconds() < poktHost.TrustingPeriod.AsDuration().Nanoseconds() { + return errors.New("counterparty client state unbonding period is less than trusting period") + } + + // RESEARCH: Look into upgrade paths, their use and if they should just be equal + + return nil +} + +// GetCurrentHeight returns the current IBC client height of the network +// TODO_AFTER(#882): Use actual revision number +func (h *clientManager) GetCurrentHeight() (modules.Height, error) { + currHeight := h.GetBus().GetConsensusModule().CurrentHeight() + rCtx, err := h.GetBus().GetPersistenceModule().NewReadContext(int64(currHeight)) + if err != nil { + return nil, err + } + defer rCtx.Release() + revNum := rCtx.GetRevisionNumber(int64(currHeight)) + return &types.Height{ + RevisionNumber: revNum, + RevisionHeight: currHeight, + }, nil +} diff --git a/ibc/client/light_clients/types/fraction.go b/ibc/client/light_clients/types/fraction.go new file mode 100644 index 000000000..75a2471ab --- /dev/null +++ b/ibc/client/light_clients/types/fraction.go @@ -0,0 +1,42 @@ +package types + +type ord int + +const ( + lt ord = iota + eq + gt +) + +func (f *Fraction) LT(other *Fraction) bool { + return f.compare(other) == lt +} + +func (f *Fraction) GT(other *Fraction) bool { + return f.compare(other) == gt +} + +func (f *Fraction) EQ(other *Fraction) bool { + return f.compare(other) == eq +} + +func (f *Fraction) LTE(other *Fraction) bool { + return f.compare(other) != gt +} + +func (f *Fraction) GTE(other *Fraction) bool { + return f.compare(other) != lt +} + +func (f *Fraction) compare(other *Fraction) ord { + comDenom := f.Denominator * other.Denominator + aNum := f.Numerator * (comDenom / f.Denominator) + bNum := other.Numerator * (comDenom / other.Denominator) + if aNum < bNum { + return lt + } + if aNum > bNum { + return gt + } + return eq +} diff --git a/ibc/client/queries.go b/ibc/client/queries.go new file mode 100644 index 000000000..2ea5b36dc --- /dev/null +++ b/ibc/client/queries.go @@ -0,0 +1,34 @@ +package client + +import ( + "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/path" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +// GetConsensusState returns the ConsensusState at the given height for the +// stored client with the given identifier +func (c *clientManager) GetConsensusState( + identifier string, height modules.Height, +) (modules.ConsensusState, error) { + // Retrieve the clientId prefixed client store + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return nil, err + } + + return types.GetConsensusState(clientStore, height) +} + +// GetClientState returns the ClientState for the stored client with the given identifier +func (c *clientManager) GetClientState(identifier string) (modules.ClientState, error) { + // Retrieve the client store + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(path.KeyClientStorePrefix) + if err != nil { + return nil, err + } + + return types.GetClientState(clientStore, identifier) +} diff --git a/ibc/client/submodule.go b/ibc/client/submodule.go new file mode 100644 index 000000000..8428f8ecd --- /dev/null +++ b/ibc/client/submodule.go @@ -0,0 +1,225 @@ +package client + +import ( + "fmt" + + "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/path" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/base_modules" +) + +var ( + _ modules.ClientManager = &clientManager{} + allowedClientTypes = make(map[string]struct{}, 0) +) + +func init() { + allowedClientTypes[types.WasmClientType] = struct{}{} +} + +type clientManager struct { + base_modules.IntegrableModule + + logger *modules.Logger +} + +func Create(bus modules.Bus, options ...modules.ClientManagerOption) (modules.ClientManager, error) { + return new(clientManager).Create(bus, options...) +} + +// WithLogger sets the logger for the clientManager +func WithLogger(logger *modules.Logger) modules.ClientManagerOption { + return func(m modules.ClientManager) { + if mod, ok := m.(*clientManager); ok { + mod.logger = logger + } + } +} + +func (*clientManager) Create(bus modules.Bus, options ...modules.ClientManagerOption) (modules.ClientManager, error) { + c := &clientManager{} + + for _, option := range options { + option(c) + } + + c.logger.Info().Msg("👨 Creating Client Manager 👨") + + bus.RegisterModule(c) + + return c, nil +} + +func (c *clientManager) GetModuleName() string { return modules.ClientManagerModuleName } + +// CreateClient creates a new client with the given client state and initial +// consensus state and initialises it with a unique identifier in the IBC client +// store and emits an event to the Event Logger +func (c *clientManager) CreateClient( + clientState modules.ClientState, consensusState modules.ConsensusState, +) (string, error) { + // Check if the client type is allowed + if !isAllowedClientType(clientState.ClientType()) { + return "", fmt.Errorf("client type %s is not supported", clientState.ClientType()) + } + + // Generate a unique identifier for the client + identifier := path.GenerateClientIdentifier() + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return "", err + } + + // Initialise the client with the clientState provided + if err := clientState.Initialise(clientStore, consensusState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to initialize client") + return "", err + } + + c.logger.Info().Str("identifier", identifier). + Str("height", clientState.GetLatestHeight().ToString()). + Msg("client created at height") + + // Emit the create client event to the event logger + if err := c.emitCreateClientEvent(identifier, clientState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client created event") + return "", err + } + + return identifier, nil +} + +// UpdateClient updates an existing client with the given identifier using the +// ClientMessage provided +func (c *clientManager) UpdateClient( + identifier string, clientMessage modules.ClientMessage, +) error { + // Get the client state + clientState, err := c.GetClientState(identifier) + if err != nil { + return err + } + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return err + } + + // Check the state is active + if clientState.Status(clientStore) != modules.ActiveStatus { + return core_types.ErrIBCClientNotActive() + } + + // Verify the client message + if err := clientState.VerifyClientMessage(clientStore, clientMessage); err != nil { + return err + } + + // Check for misbehaviour on the source chain + misbehaved := clientState.CheckForMisbehaviour(clientStore, clientMessage) + if misbehaved { + if err := clientState.UpdateStateOnMisbehaviour(clientStore, clientMessage); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to freeze client for misbehaviour") + return err + } + c.logger.Info().Str("identifier", identifier). + Msg("client frozen for misbehaviour") + + // emit the submit misbehaviour event to the event logger + if err := c.emitSubmitMisbehaviourEvent(identifier, clientState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client submit misbehaviour event") + return err + } + return nil + } + + // Update the client + consensusHeight, err := clientState.UpdateState(clientStore, clientMessage) + if err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Str("height", consensusHeight.ToString()). + Msg("failed to update client state") + return err + } + c.logger.Info().Str("identifier", identifier). + Str("height", consensusHeight.ToString()). + Msg("client state updated") + + // emit the update client event to the event logger + if err := c.emitUpdateClientEvent(identifier, clientState.ClientType(), consensusHeight, clientMessage); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client update event") + return err + } + + return nil +} + +// UpgradeClient upgrades an existing client with the given identifier using the +// ClientState and ConsentusState provided. It can only do so if the new client +// was committed to by the old client at the specified upgrade height +func (c *clientManager) UpgradeClient( + identifier string, + upgradedClient modules.ClientState, upgradedConsState modules.ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error { + // Get the client state + clientState, err := c.GetClientState(identifier) + if err != nil { + return err + } + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return err + } + + // Check the state is active + if clientState.Status(clientStore) != modules.ActiveStatus { + return core_types.ErrIBCClientNotActive() + } + + // Verify the upgrade + if err := clientState.VerifyUpgradeAndUpdateState( + clientStore, + upgradedClient, upgradedConsState, + proofUpgradeClient, proofUpgradeConsState, + ); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to verify upgrade") + return err + } + + c.logger.Info().Str("identifier", identifier). + Str("height", upgradedClient.GetLatestHeight().ToString()). + Msg("client upgraded") + + // emit the upgrade client event to the event logger + if err := c.emitUpgradeClientEvent(identifier, upgradedClient); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client upgrade event") + return err + } + + return nil +} + +func isAllowedClientType(clientType string) bool { + if _, ok := allowedClientTypes[clientType]; ok { + return true + } + return false +} diff --git a/ibc/client/types/client.go b/ibc/client/types/client.go new file mode 100644 index 000000000..e438eedfb --- /dev/null +++ b/ibc/client/types/client.go @@ -0,0 +1,242 @@ +package types + +import ( + "errors" + "fmt" + + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +const ( + // https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md + WasmClientType = "08-wasm" +) + +var _ modules.ClientState = &ClientState{} + +// ClientType returns the client type. +func (cs *ClientState) ClientType() string { return WasmClientType } + +// GetLatestHeight returns the latest height stored. +func (cs *ClientState) GetLatestHeight() modules.Height { return cs.RecentHeight } + +// Validate performs a basic validation of the client state fields. +func (cs *ClientState) Validate() error { + if len(cs.Data) == 0 { + return errors.New("data cannot be empty") + } + + lenWasmChecksum := len(cs.WasmChecksum) + if lenWasmChecksum == 0 { + return errors.New("wasm checksum cannot be empty") + } + if lenWasmChecksum != 32 { // sha256 output is 256 bits long + return fmt.Errorf("expected 32, got %d", lenWasmChecksum) + } + + return nil +} + +//nolint:unused // types defined for future use +type ( + statusInnerPayload struct{} + statusPayload struct { + Status statusInnerPayload `json:"status"` + } +) + +// Status returns the status of the wasm client. +// The client may be: +// - Active: frozen height is zero and client is not expired +// - Frozen: frozen height is not zero +// - Expired: the latest consensus state timestamp + trusting period <= current time +// - Unauthorized: the client type is not registered as an allowed client type +// +// A frozen client will become expired, so the Frozen status +// has higher precedence. +func (cs *ClientState) Status(clientStore modules.ProvableStore) modules.ClientStatus { + /* + payload := &statusPayload{Status: statusInnerPayload{}} + encodedData, err := json.Marshal(payload) + if err != nil { + return modules.UnknownStatus + } + + // TODO(#912): implement WASM contract querying + */ + return modules.ActiveStatus +} + +// GetTimestampAtHeight returns the timestamp of the consensus state at the given height. +func (cs *ClientState) GetTimestampAtHeight(clientStore modules.ProvableStore, height modules.Height) (uint64, error) { + consState, err := GetConsensusState(clientStore, height) + if err != nil { + return 0, err + } + return consState.GetTimestamp(), nil +} + +// Initialise checks that the initial consensus state is an 08-wasm consensus +// state and sets the client state, consensus state in the provided client store. +// It also initializes the wasm contract for the client. +func (cs *ClientState) Initialise(clientStore modules.ProvableStore, consensusState modules.ConsensusState) error { + consState, ok := consensusState.(*ConsensusState) + if !ok { + return errors.New("invalid consensus state type") + } + if err := setClientState(clientStore, cs); err != nil { + return fmt.Errorf("failed to set client state: %w", err) + } + if err := setConsensusState(clientStore, consState, cs.GetLatestHeight()); err != nil { + return fmt.Errorf("failed to set consensus state: %w", err) + } + // TODO(#912): implement WASM contract initialisation + return nil +} + +//nolint:unused // types defined for future use +type ( + verifyMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + Value []byte `json:"value"` + } + verifyMembershipPayload struct { + VerifyMembership verifyMembershipInnerPayload `json:"verify_membership"` + } +) + +// VerifyMembership is a generic proof verification method which verifies a proof +// of the existence of a value at a given CommitmentPath at the specified height. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix +// and a standardized path (as defined in ICS 24). +// +// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. +func (cs *ClientState) VerifyMembership( + clientStore modules.ProvableStore, + height modules.Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, key, value []byte, +) error { + if cs.GetLatestHeight().LT(height) { + return fmt.Errorf("client state height < proof height (%d < %d)", cs.GetLatestHeight(), height) + } + + if _, err := GetConsensusState(clientStore, height); err != nil { + return errors.New("consensus state not found for proof height") + } + + /* + payload := verifyMembershipPayload{ + VerifyMembership: verifyMembershipInnerPayload{ + Height: height, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: key, + Value: value, + }, + } + + // TODO(#912): implement WASM contract method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + verifyNonMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + } + verifyNonMembershipPayload struct { + VerifyNonMembership verifyNonMembershipInnerPayload `json:"verify_non_membership"` + } +) + +// VerifyNonMembership is a generic proof verification method which verifies +// the absence of a given CommitmentPath at a specified height. +// The caller is expected to construct the full CommitmentPath from a +// CommitmentPrefix and a standardized path (as defined in ICS 24). +// +// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. +func (cs *ClientState) VerifyNonMembership( + clientStore modules.ProvableStore, + height modules.Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, key []byte, +) error { + if cs.GetLatestHeight().LT(height) { + return fmt.Errorf("client state height < proof height (%d < %d)", cs.GetLatestHeight(), height) + } + + if _, err := GetConsensusState(clientStore, height); err != nil { + return errors.New("consensus state not found for proof height") + } + + /* + payload := verifyNonMembershipPayload{ + VerifyNonMembership: verifyNonMembershipInnerPayload{ + Height: height, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: key, + }, + } + + // TODO(#912): implement WASM contract method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + checkForMisbehaviourInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + checkForMisbehaviourPayload struct { + CheckForMisbehaviour checkForMisbehaviourInnerPayload `json:"check_for_misbehaviour"` + } +) + +// CheckForMisbehaviour detects misbehaviour in a submitted Header message and +// verifies the correctness of a submitted Misbehaviour ClientMessage +func (cs *ClientState) CheckForMisbehaviour(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) bool { + clientMsgConcrete := clientMessage{ + Header: nil, + Misbehaviour: nil, + } + switch msg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = msg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = msg + } + + if clientMsgConcrete.Header == nil && clientMsgConcrete.Misbehaviour == nil { + return false + } + + /* + inner := checkForMisbehaviourInnerPayload{ + ClientMessage: clientMsgConcrete, + } + payload := checkForMisbehaviourPayload{ + CheckForMisbehaviour: inner, + } + + // TODO(#912): implement WASM contract method calls + */ + + return true +} diff --git a/ibc/client/types/consensus.go b/ibc/client/types/consensus.go new file mode 100644 index 000000000..5e089cbd5 --- /dev/null +++ b/ibc/client/types/consensus.go @@ -0,0 +1,33 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ConsensusState = &ConsensusState{} + +// NewConsensusState creates a new ConsensusState instance. +func NewConsensusState(data []byte, timestamp uint64) *ConsensusState { + return &ConsensusState{ + Data: data, + Timestamp: timestamp, + } +} + +// ClientType returns the Wasm client type. +func (cs *ConsensusState) ClientType() string { + return WasmClientType +} + +// ValidateBasic defines a basic validation for the wasm client consensus state. +func (cs *ConsensusState) ValidateBasic() error { + if cs.Timestamp == 0 { + return errors.New("timestamp must be a positive Unix time") + } + if len(cs.Data) == 0 { + return errors.New("data cannot be empty") + } + return nil +} diff --git a/ibc/client/types/events.go b/ibc/client/types/events.go new file mode 100644 index 000000000..0900003c3 --- /dev/null +++ b/ibc/client/types/events.go @@ -0,0 +1,17 @@ +package types + +const ( + // Event topics for the events emitted by the Client submodule + EventTopicCreateClient = "create_client" + EventTopicUpdateClient = "update_client" + EventTopicUpgradeClient = "upgrade_client" + EventTopicSubmitMisbehaviour = "client_misbehaviour" +) + +var ( + // Attribute keys for the events emitted by the Client submodule + AttributeKeyClientID = []byte("client_id") + AttributeKeyClientType = []byte("client_type") + AttributeKeyConsensusHeight = []byte("consensus_height") + AttributeKeyHeader = []byte("header") +) diff --git a/ibc/client/types/header.go b/ibc/client/types/header.go new file mode 100644 index 000000000..1522c49db --- /dev/null +++ b/ibc/client/types/header.go @@ -0,0 +1,23 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ClientMessage = &Header{} + +// ClientType defines that the Header is a Wasm client consensus algorithm +func (h *Header) ClientType() string { + return WasmClientType +} + +// ValidateBasic defines a basic validation for the wasm client header. +func (h *Header) ValidateBasic() error { + if len(h.Data) == 0 { + return errors.New("data cannot be empty") + } + + return nil +} diff --git a/ibc/client/types/height.go b/ibc/client/types/height.go new file mode 100644 index 000000000..2c57bc359 --- /dev/null +++ b/ibc/client/types/height.go @@ -0,0 +1,76 @@ +package types + +import ( + "fmt" + + "github.com/pokt-network/pocket/shared/modules" +) + +type ord int + +const ( + lt ord = iota - 1 + eq + gt +) + +func (h *Height) ToString() string { + return fmt.Sprintf("%d-%d", h.RevisionNumber, h.RevisionHeight) +} + +func (h *Height) IsZero() bool { + return h.RevisionNumber == 0 && h.RevisionHeight == 0 +} + +func (h *Height) LT(other modules.Height) bool { + return h.compare(other) == lt +} + +func (h *Height) LTE(other modules.Height) bool { + return h.compare(other) != gt +} + +func (h *Height) GT(other modules.Height) bool { + return h.compare(other) == gt +} + +func (h *Height) GTE(other modules.Height) bool { + return h.compare(other) != lt +} + +func (h *Height) EQ(other modules.Height) bool { + return h.compare(other) == eq +} + +func (h *Height) Increment() modules.Height { + return &Height{ + RevisionNumber: h.RevisionNumber, + RevisionHeight: h.RevisionHeight + 1, + } +} + +func (h *Height) Decrement() modules.Height { + if h.RevisionHeight == 0 { + return h + } + return &Height{ + RevisionNumber: h.RevisionNumber, + RevisionHeight: h.RevisionHeight - 1, + } +} + +func (h *Height) compare(other modules.Height) ord { + if h.RevisionNumber > other.GetRevisionNumber() { + return gt + } + if h.RevisionNumber < other.GetRevisionNumber() { + return lt + } + if h.RevisionHeight > other.GetRevisionHeight() { + return gt + } + if h.RevisionHeight < other.GetRevisionHeight() { + return lt + } + return eq +} diff --git a/ibc/client/types/height_test.go b/ibc/client/types/height_test.go new file mode 100644 index 000000000..4b8c34eb8 --- /dev/null +++ b/ibc/client/types/height_test.go @@ -0,0 +1,430 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func FuzzHeight_ToStringDeterministic(f *testing.F) { + for i := 0; i < 100; i++ { + f.Add(uint64(i)) + } + f.Fuzz(func(t *testing.T, i uint64) { + height := &Height{ + RevisionNumber: i, + RevisionHeight: i, + } + str := height.ToString() + require.Equal(t, str, fmt.Sprintf("%d-%d", i, i)) + }) +} + +func TestHeight_IsZero(t *testing.T) { + testCases := []struct { + name string + height *Height + expected bool + }{ + { + name: "zero height", + height: &Height{ + RevisionNumber: 0, + RevisionHeight: 0, + }, + expected: true, + }, + { + name: "non-zero height: zero revision number", + height: &Height{ + RevisionNumber: 0, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "non-zero height: zero revision height", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + expected: false, + }, + { + name: "non-zero height", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.height.IsZero()) + }) + } +} + +func TestHeight_Increment(t *testing.T) { + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + } + newHeight := height.Increment() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(2), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Increment() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(3), newHeight.GetRevisionHeight()) +} + +func TestHeight_Decrement(t *testing.T) { + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + } + newHeight := height.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(1), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), newHeight.GetRevisionHeight()) +} + +func TestHeight_Comparisons(t *testing.T) { + testCases := []struct { + name string + op string + height *Height + other *Height + expected bool + }{ + { + name: "LT: height < other", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LT: height == other", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height > other", + op: "LT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height < other (same revision number)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LT: height > other (same revision number)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height > other (same revision height)", + op: "LT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height < other (same revision height)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "LTE: height < other", + op: "LTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LTE: height == other", + op: "LTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "LTE: height > other", + op: "LTE", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GT: height < other", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GT: height == other", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GT: height > other", + op: "GT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height < other (same revision number)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GT: height > other (same revision number)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height > other (same revision height)", + op: "GT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height < other (same revision height)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GTE: height < other", + op: "GTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GTE: height == other", + op: "GTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GTE: height > other", + op: "GTE", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "EQ: height < other", + op: "EQ", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "EQ: height == other", + op: "EQ", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "EQ: height > other", + op: "EQ", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + switch tc.op { + case "LT": + require.Equal(t, tc.expected, tc.height.LT(tc.other)) + case "LTE": + require.Equal(t, tc.expected, tc.height.LTE(tc.other)) + case "GT": + require.Equal(t, tc.expected, tc.height.GT(tc.other)) + case "GTE": + require.Equal(t, tc.expected, tc.height.GTE(tc.other)) + case "EQ": + require.Equal(t, tc.expected, tc.height.EQ(tc.other)) + default: + panic(fmt.Sprintf("invalid comparison op: %s", tc.op)) + } + }) + } +} diff --git a/ibc/client/types/misbehaviour.go b/ibc/client/types/misbehaviour.go new file mode 100644 index 000000000..823ab6f28 --- /dev/null +++ b/ibc/client/types/misbehaviour.go @@ -0,0 +1,22 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ClientMessage = (*Misbehaviour)(nil) + +// ClientType is Wasm light client +func (m *Misbehaviour) ClientType() string { + return WasmClientType +} + +// ValidateBasic implements Misbehaviour interface +func (m *Misbehaviour) ValidateBasic() error { + if len(m.Data) == 0 { + return errors.New("data cannot be empty") + } + return nil +} diff --git a/ibc/client/types/queries.go b/ibc/client/types/queries.go new file mode 100644 index 000000000..fd7a8d8be --- /dev/null +++ b/ibc/client/types/queries.go @@ -0,0 +1,63 @@ +package types + +import ( + "github.com/pokt-network/pocket/ibc/path" + "github.com/pokt-network/pocket/shared/codec" + "github.com/pokt-network/pocket/shared/modules" +) + +// GetConsensusState returns the consensus state at the given height from a +// prefixed client store, in the format: "clients/{clientID}" +func GetConsensusState(clientStore modules.ProvableStore, height modules.Height) (modules.ConsensusState, error) { + // Retrieve the consensus state bytes from the client store + consStateBz, err := clientStore.Get(path.ConsensusStateKey(height.ToString())) + if err != nil { + return nil, err + } + + // Unmarshal into a ConsensusState interface + consState := new(ConsensusState) + if err := codec.GetCodec().Unmarshal(consStateBz, consState); err != nil { + return nil, err + } + + return consState, nil +} + +// GetClientState returns the client state from a prefixed client store, +// in the format: "clients" using the clientID provided +func GetClientState(clientStore modules.ProvableStore, identifier string) (modules.ClientState, error) { + // Retrieve the client state bytes from the client store + clientStateBz, err := clientStore.Get(path.ClientStateKey(identifier)) + if err != nil { + return nil, err + } + + // Unmarshal into a ClientState interface + clientState := new(ClientState) + if err := codec.GetCodec().Unmarshal(clientStateBz, clientState); err != nil { + return nil, err + } + + return clientState, nil +} + +// setClientState stores the client state +// clientStore must be a prefixed client store: "clients/{clientID}" +func setClientState(clientStore modules.ProvableStore, clientState *ClientState) error { + val, err := codec.GetCodec().Marshal(clientState) + if err != nil { + return err + } + return clientStore.Set([]byte(path.KeyClientState), val) // key == nil ==> key == "clients/{clientID}" +} + +// setConsensusState stores the consensus state at the given height. +// clientStore must be a prefixed client store: "clients/{clientID}" +func setConsensusState(clientStore modules.ProvableStore, consensusState *ConsensusState, height modules.Height) error { + val, err := codec.GetCodec().Marshal(consensusState) + if err != nil { + return err + } + return clientStore.Set(path.ConsensusStateKey(height.ToString()), val) +} diff --git a/ibc/client/types/queries_test.go b/ibc/client/types/queries_test.go new file mode 100644 index 000000000..0e58c741d --- /dev/null +++ b/ibc/client/types/queries_test.go @@ -0,0 +1,411 @@ +package types + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pokt-network/pocket/ibc/store" + "github.com/pokt-network/pocket/persistence/kvstore" + "github.com/pokt-network/pocket/runtime" + "github.com/pokt-network/pocket/runtime/configs" + "github.com/pokt-network/pocket/runtime/test_artifacts" + "github.com/pokt-network/pocket/shared/codec" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/pokt-network/smt" + "github.com/stretchr/testify/require" +) + +func TestClientState_Set(t *testing.T) { + // get provable store prefixed with clients/123 + provableStore := newTestProvableStore(t, "123") + + // create a client state + clientState := &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + } + bz, err := codec.GetCodec().Marshal(clientState) + require.NoError(t, err) + + // set the client state + require.NoError(t, setClientState(provableStore, clientState)) + + // check cache + cache := kvstore.NewMemKVStore() + + // flush cache + require.NoError(t, provableStore.FlushEntries(cache)) + + // get all from cache + keys, vals, err := cache.GetAll([]byte{}, false) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Len(t, vals, 1) + + // check key and value set correctly + require.Equal(t, []byte("clients/123/1/clients/123/clientState"), keys[0]) + require.Equal(t, vals[0], bz) +} + +func TestConsensusState_Set(t *testing.T) { + // get provable store prefixed with clients/123 + provableStore := newTestProvableStore(t, "123") + + // create a consensus state + consensusState := &ConsensusState{ + Data: []byte("data"), + Timestamp: 1, + } + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + } + bz, err := codec.GetCodec().Marshal(consensusState) + require.NoError(t, err) + + // set the client state + require.NoError(t, setConsensusState(provableStore, consensusState, height)) + + // check cache + cache := kvstore.NewMemKVStore() + + // flush cache + require.NoError(t, provableStore.FlushEntries(cache)) + + // get all from cache + keys, vals, err := cache.GetAll([]byte{}, false) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Len(t, vals, 1) + + // check key and value set correctly + require.Equal(t, []byte("clients/123/1/clients/123/consensusStates/1-1"), keys[0]) + require.Equal(t, vals[0], bz) +} + +func TestClientState_Get(t *testing.T) { + clientStore := newTestProvableStore(t, "") + + testCases := []struct { + name string + clientId string + data []byte + checksum []byte + expectedErr error + }{ + { + name: "client state not found", + clientId: "124", + data: nil, + checksum: nil, + expectedErr: core_types.ErrIBCKeyDoesNotExist("clients/124/clientState"), + }, + { + name: "client state found", + clientId: "123", + data: []byte("data"), + checksum: make([]byte, 32), + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + clientState, err := GetClientState(clientStore, tc.clientId) + require.ErrorIs(t, err, tc.expectedErr) + if tc.expectedErr == nil { + require.Equal(t, clientState.GetData(), tc.data) + require.Equal(t, clientState.GetWasmChecksum(), tc.checksum) + } + }) + } +} + +func TestConsensusState_Get(t *testing.T) { + clientStore := newTestProvableStore(t, "123") + + testCases := []struct { + name string + height *Height + data []byte + timestamp uint64 + expectedErr error + }{ + { + name: "consensus state not found - wrong height", + height: &Height{RevisionNumber: 1, RevisionHeight: 2}, + data: nil, + timestamp: 0, + expectedErr: core_types.ErrIBCKeyDoesNotExist("clients/123/consensusStates/1-2"), + }, + { + name: "consensus state found", + height: &Height{RevisionNumber: 1, RevisionHeight: 1}, + data: []byte("data"), + timestamp: 1, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + consensusState, err := GetConsensusState(clientStore, tc.height) + require.ErrorIs(t, err, tc.expectedErr) + if tc.expectedErr == nil { + require.Equal(t, consensusState.GetData(), tc.data) + require.Equal(t, consensusState.GetTimestamp(), tc.timestamp) + } + }) + } +} + +func newTestProvableStore(t *testing.T, clientId string) modules.ProvableStore { + t.Helper() + + tree, nodeStore, dbMap := setupDB(t) + + runtimeCfg := newTestRuntimeConfig(t) + bus, err := runtime.CreateBus(runtimeCfg) + require.NoError(t, err) + + persistenceMock := newPersistenceMock(t, bus, dbMap) + bus.RegisterModule(persistenceMock) + consensusMock := newConsensusMock(t, bus) + bus.RegisterModule(consensusMock) + treeStoreMock := newTreeStoreMock(t, bus, tree, nodeStore) + bus.RegisterModule(treeStoreMock) + p2pMock := newTestP2PModule(t, bus) + bus.RegisterModule(p2pMock) + utilityMock := newUtilityMock(t, bus) + bus.RegisterModule(utilityMock) + + privKey := runtimeCfg.GetConfig().IBC.Host.PrivateKey + + t.Cleanup(func() { + err := persistenceMock.Stop() + require.NoError(t, err) + err = consensusMock.Stop() + require.NoError(t, err) + err = p2pMock.Stop() + require.NoError(t, err) + }) + + if clientId != "" { + clientId = "/" + clientId + } + + return store.NewProvableStore(bus, []byte("clients"+clientId), privKey) +} + +func setupDB(t *testing.T) (*smt.SMT, kvstore.KVStore, map[string]string) { + dbMap := make(map[string]string, 0) + nodeStore := kvstore.NewMemKVStore() + tree := smt.NewSparseMerkleTree(nodeStore, sha256.New()) + + clientState := &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + } + cliBz, err := codec.GetCodec().Marshal(clientState) + require.NoError(t, err) + consensusState := &ConsensusState{ + Data: []byte("data"), + Timestamp: 1, + } + conBz, err := codec.GetCodec().Marshal(consensusState) + require.NoError(t, err) + + keys := [][]byte{ + []byte("clients/123/consensusStates/1-1"), + []byte("clients/123/clientState"), + } + values := [][]byte{ + conBz, + cliBz, + } + + for i, key := range keys { + dbMap[hex.EncodeToString(key)] = hex.EncodeToString(values[i]) + err := tree.Update(key, values[i]) + require.NoError(t, err) + } + + require.NoError(t, tree.Commit()) + + t.Cleanup(func() { + err := nodeStore.Stop() + require.NoError(t, err) + }) + + return tree, nodeStore, dbMap +} + +func newConsensusMock(t *testing.T, bus modules.Bus) *mock_modules.MockConsensusModule { + t.Helper() + + ctrl := gomock.NewController(t) + consensusMock := mock_modules.NewMockConsensusModule(ctrl) + consensusMock.EXPECT().GetModuleName().Return(modules.ConsensusModuleName).AnyTimes() + consensusMock.EXPECT().Start().Return(nil).AnyTimes() + consensusMock.EXPECT().Stop().Return(nil).AnyTimes() + consensusMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + consensusMock.EXPECT().GetBus().Return(bus).AnyTimes() + consensusMock.EXPECT().CurrentHeight().Return(uint64(1)).AnyTimes() + + return consensusMock +} + +func newUtilityMock(t *testing.T, bus modules.Bus) *mock_modules.MockUtilityModule { + t.Helper() + + ctrl := gomock.NewController(t) + utilityMock := mock_modules.NewMockUtilityModule(ctrl) + utilityMock.EXPECT().GetModuleName().Return(modules.UtilityModuleName).AnyTimes() + utilityMock.EXPECT().Start().Return(nil).AnyTimes() + utilityMock.EXPECT().Stop().Return(nil).AnyTimes() + utilityMock.EXPECT().SetBus(bus).Return().AnyTimes() + utilityMock.EXPECT().GetBus().Return(bus).AnyTimes() + utilityMock.EXPECT().HandleTransaction(gomock.Any()).Return(nil).AnyTimes() + + return utilityMock +} + +func newPersistenceMock(t *testing.T, + bus modules.Bus, + dbMap map[string]string, +) *mock_modules.MockPersistenceModule { + t.Helper() + + ctrl := gomock.NewController(t) + persistenceMock := mock_modules.NewMockPersistenceModule(ctrl) + persistenceReadContextMock := mock_modules.NewMockPersistenceReadContext(ctrl) + + persistenceMock.EXPECT().GetModuleName().Return(modules.PersistenceModuleName).AnyTimes() + persistenceMock.EXPECT().Start().Return(nil).AnyTimes() + persistenceMock.EXPECT().Stop().Return(nil).AnyTimes() + persistenceMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + persistenceMock.EXPECT().GetBus().Return(bus).AnyTimes() + persistenceMock.EXPECT().NewReadContext(gomock.Any()).Return(persistenceReadContextMock, nil).AnyTimes() + + persistenceMock.EXPECT().ReleaseWriteContext().Return(nil).AnyTimes() + persistenceReadContextMock. + EXPECT(). + GetIBCStoreEntry(gomock.Any(), gomock.Any()). + DoAndReturn( + func(key []byte, _ uint64) ([]byte, error) { + value, ok := dbMap[hex.EncodeToString(key)] + if !ok { + return nil, core_types.ErrIBCKeyDoesNotExist(string(key)) + } + bz, err := hex.DecodeString(value) + if err != nil { + return nil, err + } + if bytes.Equal(bz, nil) { + return nil, core_types.ErrIBCKeyDoesNotExist(string(key)) + } + return bz, nil + }). + AnyTimes() + + persistenceReadContextMock. + EXPECT(). + Release(). + AnyTimes() + + return persistenceMock +} + +func newTreeStoreMock(t *testing.T, + bus modules.Bus, + tree *smt.SMT, + nodeStore kvstore.KVStore, +) *mock_modules.MockTreeStoreModule { + t.Helper() + + ctrl := gomock.NewController(t) + treeStoreMock := mock_modules.NewMockTreeStoreModule(ctrl) + treeStoreMock.EXPECT().GetModuleName().Return(modules.TreeStoreSubmoduleName).AnyTimes() + treeStoreMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + treeStoreMock.EXPECT().GetBus().Return(bus).AnyTimes() + + treeStoreMock. + EXPECT(). + GetTree(gomock.Any()). + DoAndReturn( + func(_ string) ([]byte, kvstore.KVStore) { + return tree.Root(), nodeStore + }). + AnyTimes() + + return treeStoreMock +} + +func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { + t.Helper() + + ctrl := gomock.NewController(t) + p2pMock := mock_modules.NewMockP2PModule(ctrl) + + p2pMock.EXPECT().Start().Return(nil).AnyTimes() + p2pMock.EXPECT().Stop().Return(nil).AnyTimes() + p2pMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + p2pMock.EXPECT().GetBus().Return(bus).AnyTimes() + p2pMock.EXPECT(). + Broadcast(gomock.Any()). + Return(nil). + AnyTimes() + p2pMock.EXPECT(). + Send(gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + p2pMock.EXPECT().GetModuleName().Return(modules.P2PModuleName).AnyTimes() + p2pMock.EXPECT().HandleEvent(gomock.Any()).Return(nil).AnyTimes() + + return p2pMock +} + +// TECHDEBT: centralise these helper functions in internal/testutils +func newTestRuntimeConfig(t *testing.T) *runtime.Manager { + t.Helper() + cfg, err := configs.CreateTempConfig(&configs.Config{ + Consensus: &configs.ConsensusConfig{ + PrivateKey: "0ca1a40ddecdab4f5b04fa0bfed1d235beaa2b8082e7554425607516f0862075dfe357de55649e6d2ce889acf15eb77e94ab3c5756fe46d3c7538d37f27f115e", + }, + Utility: &configs.UtilityConfig{ + MaxMempoolTransactionBytes: 1000000, + MaxMempoolTransactions: 1000, + }, + Persistence: &configs.PersistenceConfig{ + PostgresUrl: "", + NodeSchema: "test_schema", + BlockStorePath: ":memory:", + TxIndexerPath: ":memory:", + TreesStoreDir: ":memory:", + MaxConnsCount: 50, + MinConnsCount: 1, + MaxConnLifetime: "5m", + MaxConnIdleTime: "1m", + HealthCheckPeriod: "30s", + }, + Validator: &configs.ValidatorConfig{Enabled: true}, + IBC: &configs.IBCConfig{ + Enabled: true, + StoresDir: ":memory:", + Host: &configs.IBCHostConfig{ + PrivateKey: "0ca1a40ddecdab4f5b04fa0bfed1d235beaa2b8082e7554425607516f0862075dfe357de55649e6d2ce889acf15eb77e94ab3c5756fe46d3c7538d37f27f115e", + }, + }, + }) + if err != nil { + t.Fatalf("Error creating config: %s", err) + } + genesisState, _ := test_artifacts.NewGenesisState(0, 0, 0, 0) + runtimeCfg := runtime.NewManager(cfg, genesisState) + return runtimeCfg +} diff --git a/ibc/client/types/update.go b/ibc/client/types/update.go new file mode 100644 index 000000000..1fefaea0a --- /dev/null +++ b/ibc/client/types/update.go @@ -0,0 +1,123 @@ +package types + +import ( + "github.com/pokt-network/pocket/shared/modules" +) + +//nolint:unused // types defined for future use +type ( + clientMessage struct { + Header *Header `json:"header,omitempty"` + Misbehaviour *Misbehaviour `json:"misbehaviour,omitempty"` + } + verifyClientMessageInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + verifyClientMessagePayload struct { + VerifyClientMessage verifyClientMessageInnerPayload `json:"verify_client_message"` + } +) + +// VerifyClientMessage must verify a ClientMessage. A ClientMessage could be a Header, +// Misbehaviour, or batch update. It must handle each type of ClientMessage appropriately. +// +// Calls to CheckForMisbehaviour, UpdateState, and UpdateStateOnMisbehaviour will +// assume that the content of the ClientMessage has been verified and can be trusted +func (cs *ClientState) VerifyClientMessage(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) error { + clientMsgConcrete := clientMessage{ + Header: nil, + Misbehaviour: nil, + } + switch clientMsg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = clientMsg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = clientMsg + } + + /* + inner := verifyClientMessageInnerPayload{ + ClientMessage: clientMsgConcrete, + } + payload := verifyClientMessagePayload{ + VerifyClientMessage: inner, + } + + // TODO(#912): implement WASM method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + updateStateInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + updateStatePayload struct { + UpdateState updateStateInnerPayload `json:"update_state"` + } +) + +// UpdateState updates and stores as necessary any associated information for an +// IBC client. Upon successful update, a consensus height is returned. +// +// Client state and new consensus states are updated in the store by the contract +// Assumes the ClientMessage has already been verified +func (cs *ClientState) UpdateState(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) (modules.Height, error) { + /* + header, ok := clientMsg.(*Header) + if !ok { + return nil, errors.New("client message must be a header") + } + + payload := updateStatePayload{ + UpdateState: updateStateInnerPayload{ + ClientMessage: clientMessage{ + Header: header, + }, + }, + } + + // TODO(#912): implement WASM method calls + */ + + return clientMsg.(*Header).Height, nil +} + +//nolint:unused // types defined for future use +type ( + updateStateOnMisbehaviourInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + updateStateOnMisbehaviourPayload struct { + UpdateStateOnMisbehaviour updateStateOnMisbehaviourInnerPayload `json:"update_state_on_misbehaviour"` + } +) + +// UpdateStateOnMisbehaviour should perform appropriate state changes on a +// client state given that misbehaviour has been detected and verified +// Client state is updated in the store by contract. +func (cs *ClientState) UpdateStateOnMisbehaviour(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) error { + var clientMsgConcrete clientMessage + switch clientMsg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = clientMsg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = clientMsg + } + + /* + inner := updateStateOnMisbehaviourInnerPayload{ + ClientMessage: clientMsgConcrete, + } + + payload := updateStateOnMisbehaviourPayload{ + UpdateStateOnMisbehaviour: inner, + } + + // TODO(#912): implement WASM method calls + */ + + return nil +} diff --git a/ibc/client/types/upgrade.go b/ibc/client/types/upgrade.go new file mode 100644 index 000000000..9f89e06da --- /dev/null +++ b/ibc/client/types/upgrade.go @@ -0,0 +1,73 @@ +package types + +import ( + "fmt" + + "github.com/pokt-network/pocket/shared/modules" +) + +//nolint:unused // types defined for future use +type ( + verifyUpgradeAndUpdateStateInnerPayload struct { + UpgradeClientState modules.ClientState `json:"upgrade_client_state"` + UpgradeConsensusState modules.ConsensusState `json:"upgrade_consensus_state"` + ProofUpgradeClient []byte `json:"proof_upgrade_client"` + ProofUpgradeConsensusState []byte `json:"proof_upgrade_consensus_state"` + } + verifyUpgradeAndUpdateStatePayload struct { + VerifyUpgradeAndUpdateState verifyUpgradeAndUpdateStateInnerPayload `json:"verify_upgrade_and_update_state"` + } +) + +// VerifyUpgradeAndUpdateState, on a successful verification expects the contract +// to update the new client state, consensus state, and any other client metadata. +func (cs *ClientState) VerifyUpgradeAndUpdateState( + clientStore modules.ProvableStore, + upgradedClient modules.ClientState, + upgradedConsState modules.ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error { + //nolint:gocritic // Commented out code is for future us + /* + wasmUpgradeClientState, ok := upgradedClient.(*ClientState) + if !ok { + return errors.New("upgraded client state must be Wasm ClientState") + } + + wasmUpgradeConsState, ok := upgradedConsState.(*ConsensusState) + if !ok { + return errors.New("upgraded consensus state must be Wasm ConsensusState") + } + */ + + // last height of current counterparty chain must be client's latest height + lastHeight := cs.GetLatestHeight() + + if !upgradedClient.GetLatestHeight().GT(lastHeight) { + return fmt.Errorf("upgraded client height %s must be greater than current client height %s", + upgradedClient.GetLatestHeight(), lastHeight, + ) + } + + // Must prove against latest consensus state to ensure we are verifying + // against latest upgrade plan. + _, err := GetConsensusState(clientStore, lastHeight) + if err != nil { + return fmt.Errorf("could not retrieve consensus state for height %s", lastHeight) + } + + /* + payload := verifyUpgradeAndUpdateStatePayload{ + VerifyUpgradeAndUpdateState: verifyUpgradeAndUpdateStateInnerPayload{ + UpgradeClientState: upgradedClient, + UpgradeConsensusState: upgradedConsState, + ProofUpgradeClient: proofUpgradeClient, + ProofUpgradeConsensusState: proofUpgradeConsState, + }, + } + + // TODO(#912): implement WASM contract initialisation + */ + + return nil +} diff --git a/ibc/client/types/validate_test.go b/ibc/client/types/validate_test.go new file mode 100644 index 000000000..80a1fa4bf --- /dev/null +++ b/ibc/client/types/validate_test.go @@ -0,0 +1,172 @@ +package types + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestClientState_Validate(t *testing.T) { + testCases := []struct { + name string + clientState *ClientState + expectedErr error + }{ + { + name: "valid client state", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + }, + expectedErr: nil, + }, + { + name: "invalid client state: empty data", + clientState: &ClientState{ + Data: nil, + WasmChecksum: make([]byte, 32), + }, + expectedErr: errors.New("data cannot be empty"), + }, + { + name: "invalid client state: empty wasm checksum", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: nil, + }, + expectedErr: errors.New("wasm checksum cannot be empty"), + }, + { + name: "invalid client state: invalid wasm checksum", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: []byte("invalid"), + }, + expectedErr: errors.New("expected 32, got 7"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.clientState.Validate() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestConsensusState_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + consensusState *ConsensusState + expectedErr error + }{ + { + name: "valid consensus state", + consensusState: &ConsensusState{ + Timestamp: 1, + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid consensus state: zero timestamp", + consensusState: &ConsensusState{ + Timestamp: 0, + Data: []byte("data"), + }, + expectedErr: errors.New("timestamp must be a positive Unix time"), + }, + { + name: "invalid consensus state: empty data", + consensusState: &ConsensusState{ + Timestamp: 1, + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.consensusState.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestHeader_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + header *Header + expectedErr error + }{ + { + name: "valid header", + header: &Header{ + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid header: empty data", + header: &Header{ + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.header.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestMisbehaviour_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + misbehaviour *Misbehaviour + expectedErr error + }{ + { + name: "valid misbehaviour", + misbehaviour: &Misbehaviour{ + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid misbehaviour: empty data", + misbehaviour: &Misbehaviour{ + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.misbehaviour.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} diff --git a/ibc/docs/README.md b/ibc/docs/README.md index 28160a08b..be84e362e 100644 --- a/ibc/docs/README.md +++ b/ibc/docs/README.md @@ -10,6 +10,7 @@ - [Components](#components) - [ICS-24 Host Requirements](#ics-24-host-requirements) - [ICS-23 Vector Commitments](#ics-23-vector-commitments) + - [ICS-02 Client Semantics](#ics-02-client-semantics) ## Definitions @@ -115,7 +116,15 @@ See: [ICS-24](./ics24.md) for more details on the specifics of the ICS-24 implem See: [ICS-23](./ics23.md) for more details on the specifics of the ICS-23 implementation for Pocket. +### ICS-02 Client Semantics + +[ICS-02][ics02] defines the methods, and interfaces through which the IBC host will interact with and manage the different clients it uses. This includes the creation of clients, their updates and upgrades as well as verifying any proofs with the counterparty client's state. The following interfaces must be defined: `ClientState`, `ConsensusState`, `ClientMessage`, `Height` each of these will potentially have a different implementation for each client type. In order to improve client upgradeability Pocket uses [ICS-08][ics08] WASM clients, which use a generic implementation of each interface, passing in opaque serialised data to the WASM client to be deserialised and used internally. + +See [ICS-02](./ics02.md) for more details on the specifics of the ICS-02 implementation for Pocket. + [ibc-spec]: https://github.com/cosmos/ibc [ics24]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-024-host-requirements/README.md [ics23]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-023-vector-commitments/README.md [smt]: https://github.com/pokt-network/smt +[ics02]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-002-client-semantics/README.md +[ics08]: https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md diff --git a/ibc/docs/ics02.md b/ibc/docs/ics02.md new file mode 100644 index 000000000..dfa64d2a7 --- /dev/null +++ b/ibc/docs/ics02.md @@ -0,0 +1,156 @@ +# ICS-02 Client Semantics + +- [Definitions](#definitions) + - ["light client"](#light-client) +- [Overview](#overview) +- [Implementation](#implementation) + - [Client Manager](#client-manager) + - [Lifecycle Management](#lifecycle-management) + - [Client Queries](#client-queries) +- [Types](#types) +- [Provable Stores](#provable-stores) + +## Definitions + +### "light client" + +In the context of IBC a light client differs from a traditional "light client." An IBC light client is simply a state verification algorithm. It does not sync with the network, it does not download headers. Instead the updates/new headers for a client are provided by an IBC relayer. + +## Overview + +IBC utilises light clients to verify the correctness of the state of a counterparty chain. This allows for an IBC packet to be committed to in the state of the network on a source chain and then validated through the light client on the counterparty chain. + +[ICS-02][ics02] defines the interfaces and types through which the host machine can interact with the light clients it manages. This includes: client creation, client updates and upgrades as well as submitting misbehaviour from the chain the client is tracking. In addition to this, ICS-02 also defines numerous interfaces that are used by the different client implementations in order to carry out the previous actions as well as verify the state of the chain they represent via a proof. + +## Implementation + +[ICS-02][ics02] is implemented according to the specification. However as the Pocket protocol will utilise [ICS-08][ics08] WASM clients for the improvements to client upgradeability; the implementations of the `ClientState`, `ConsensusState` and other interfaces are specific to a WASM client. + +The implementation details are explored below, the code for ICS-02 can be found in [ibc/client](../client/) + +### Client Manager + +The `ClientManager` is the submodule that governs the light client implementations and implements the [ICS-02][ics02] interface. It is defined in [shared/modules/ibc_client_module.go](../../shared/modules/ibc_client_module.go). The `ClientManager` exposed the following methods: + +```go +// === Client Lifecycle Management === + +// CreateClient creates a new client with the given client state and initial consensus state +// and initialises its unique identifier in the IBC store +CreateClient(ClientState, ConsensusState) (string, error) + +// UpdateClient updates an existing client with the given ClientMessage, given that +// the ClientMessage can be verified using the existing ClientState and ConsensusState +UpdateClient(identifier string, clientMessage ClientMessage) error + +// UpgradeClient upgrades an existing client with the given identifier using the +// ClientState and ConsenusState provided. It can only do so if the new client +// was committed to by the old client at the specified upgrade height +UpgradeClient( + identifier string, + clientState ClientState, consensusState ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error + +// === Client Queries === + +// GetConsensusState returns the ConsensusState at the given height for the given client +GetConsensusState(identifier string, height Height) (ConsensusState, error) + +// GetClientState returns the ClientState for the given client +GetClientState(identifier string) (ClientState, error) + +// GetHostConsensusState returns the ConsensusState at the given height for the host chain +GetHostConsensusState(height Height) (ConsensusState, error) + +// GetHostClientState returns the ClientState at the provided height for the host chain +GetHostClientState(height Height) (ClientState, error) + +// GetCurrentHeight returns the current IBC client height of the network +GetCurrentHeight() Height + +// VerifyHostClientState verifies the client state for a client running on a +// counterparty chain is valid, checking against the current host client state +VerifyHostClientState(ClientState) error +``` + +#### Lifecycle Management + +The `ClientManager` handles the creation, updates and upgrades for a light client. It does so by utilising the following interfaces: + +```go +type ClientState interface +type ConsensusState interface +type ClientMessage interface +``` + +These interfaces are generic but have unique implementations for each client type. As Pocket utilises WASM light clients each implementation contains a `data []byte` field which contains a serialised, opaque data structure for use within the WASM client. + +The `data` field is a JSON serialised payload that contains the data required for the client to carry out the desired operation, as well as the operation name to carry out. For example, a verify membership payload is constructed using the following `struct`s: + +```go +type ( + verifyMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + Value []byte `json:"value"` + } + verifyMembershipPayload struct { + VerifyMembership verifyMembershipInnerPayload `json:"verify_membership"` + } +) +``` + +By utilising this pattern of JSON payloads the WASM client itself is able to unmarshal the opaque payload into their own internal protobuf definitions for the implementation of the `ClientState` for example. This allows them to have a much simpler implementation and focus solely on the logic around verification and utilising simple storage. + +See: [Types](#types) for more information on the interfaces and types used in the ICS-02 implementation + +#### Client Queries + +[ICS-24](./ics24.md) instructs that a host must allow for the introspection of both its own `ConsensusState` and `ClientState`. This is done through the `ClientManager`'s `GetHostConsensusState` and `GetHostClientState` methods. These are then used by relayers to: + +1. Provide light clients running on counterparty chains the `ConsensusState` and `ClientState` objects they need. +2. Verify the state of a light client running on a counterparty chain, against the host chain's current `ClientState` + +The other queries used by the `ClientManager` involve querying the [ICS-24](./ics24.md) stores to retrieve the `ClientState` and `ConsensusState` stored objects on a per-client basis. + +See [Provable Stores](#provable-stores) for more information on how the `ProvableStore`s are used in ICS-02. + +## Types + +The [ICS-02 specification][ics02] defines the need for numerous interfaces: + +1. `ClientState` + - `ClientState` is an opaque data structure defined by a client type. It may keep arbitrary internal state to track verified roots and past misbehaviours. +2. `ConsensusState` + - `ConsensusState` is an opaque data structure defined by a client type, used by the + validity predicate to verify new commits & state roots. Likely the structure will contain the last commit produced by the consensus process, including signatures and validator set metadata. +3. `ClientMessage` + - `ClientMessage` is an opaque data structure defined by a client type which provides information to update the client. `ClientMessage`s can be submitted to an associated client to add new `ConsensusState`(s) and/or update the `ClientState`. They likely contain a height, a proof, a commitment root, and possibly updates to the validity predicate. +4. `Height` + - `Height` is an interface that defines the methods required by a clients implementation of their own height object `Height`s usually have two components: revision number and revision height. + +As previously mentioned these interfaces have different implementations for each light client type. This is due to the different light clients representing different networks, consensus types and chains altogether. The implementation of these interfaces can be found in [ibc/client/types/proto/wasm.proto](../client/types/proto/wasm.proto). + +The `data` field in these messages represents the opaque data structure that is internal to the WASM client. This is a part of the JSON serialised payload that is passed into the WASM client, and is used to carry out any relevant operations. This enables the WASM client to define its own internal data structures that can unmarshal the JSON payload into its own internal protobuf definitions. + +See: [shared/modules/ibc_client_module.go](../../shared/modules/ibc_client_module.go) for the details on the interfaces and their methods. + +## Provable Stores + +ICS-02 requires a lot of data to be stored in the IBC stores (defined in [ICS-24](./ics24.md)). In order to do this the provable stores must be initialised on a per client ID basis. This means that any operation using the provable store does not require the use of the `clientID`. This is done as follows: + +```go +prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) +clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) +``` + +This allows the `clientStore` to be used by the WASM clients without them needing to keep track of their unique identifiers. + +See: [ibc/client/submodule.go](../client/submodule.go) for more details on how this is used. + +[ics02]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-002-client-semantics/README.md +[ics08]: https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md diff --git a/ibc/host/submodule.go b/ibc/host/submodule.go index 655985b73..ad95e72a9 100644 --- a/ibc/host/submodule.go +++ b/ibc/host/submodule.go @@ -4,6 +4,7 @@ import ( "errors" "time" + "github.com/pokt-network/pocket/ibc/client" "github.com/pokt-network/pocket/ibc/events" "github.com/pokt-network/pocket/ibc/store" "github.com/pokt-network/pocket/runtime/configs" @@ -20,10 +21,6 @@ type ibcHost struct { cfg *configs.IBCHostConfig logger *modules.Logger storesDir string - - // only a single bulk store cacher and event logger are allowed - bsc modules.BulkStoreCacher - em modules.EventLogger } func Create(bus modules.Bus, config *configs.IBCHostConfig, options ...modules.IBCHostOption) (modules.IBCHostSubmodule, error) { @@ -59,7 +56,7 @@ func (*ibcHost) Create(bus modules.Bus, config *configs.IBCHostConfig, options . bus.RegisterModule(h) - bsc, err := store.Create(h.GetBus(), + _, err := store.Create(h.GetBus(), h.cfg.BulkStoreCacher, store.WithLogger(h.logger), store.WithStoresDir(h.storesDir), @@ -68,13 +65,16 @@ func (*ibcHost) Create(bus modules.Bus, config *configs.IBCHostConfig, options . if err != nil { return nil, err } - h.bsc = bsc - em, err := events.Create(h.GetBus(), events.WithLogger(h.logger)) + _, err = events.Create(h.GetBus(), events.WithLogger(h.logger)) + if err != nil { + return nil, err + } + + _, err = client.Create(h.GetBus(), client.WithLogger(h.logger)) if err != nil { return nil, err } - h.em = em return h, nil } diff --git a/ibc/host_introspection_test.go b/ibc/host_introspection_test.go new file mode 100644 index 000000000..35f0af6b5 --- /dev/null +++ b/ibc/host_introspection_test.go @@ -0,0 +1,271 @@ +package ibc + +import ( + "errors" + "testing" + "time" + + ics23 "github.com/cosmos/ics23/go" + light_client_types "github.com/pokt-network/pocket/ibc/client/light_clients/types" + client_types "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/types" + "github.com/pokt-network/pocket/shared/codec" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestHost_GetCurrentHeight(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + // get the current height + height, err := cm.GetCurrentHeight() + require.NoError(t, err) + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), height.GetRevisionHeight()) + + // increment the height + publishNewHeightEvent(t, ibcMod.GetBus(), 1) + + height, err = cm.GetCurrentHeight() + require.NoError(t, err) + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(1), height.GetRevisionHeight()) +} + +func TestHost_GetHostConsensusState(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + consState, err := cm.GetHostConsensusState(&client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.NoError(t, err) + + require.Equal(t, "08-wasm", consState.ClientType()) + require.NoError(t, consState.ValidateBasic()) + require.Less(t, consState.GetTimestamp(), uint64(time.Now().UnixNano())) + + pocketConState := new(light_client_types.PocketConsensusState) + err = codec.GetCodec().Unmarshal(consState.GetData(), pocketConState) + require.NoError(t, err) + + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + require.Equal(t, block.BlockHeader.Timestamp, pocketConState.Timestamp) + require.Equal(t, block.BlockHeader.StateHash, pocketConState.StateHash) + require.Equal(t, block.BlockHeader.StateTreeHashes, pocketConState.StateTreeHashes) + require.Equal(t, block.BlockHeader.NextValSetHash, pocketConState.NextValSetHash) +} + +func TestHost_GetHostClientState(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + clientState, err := cm.GetHostClientState(&client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.NoError(t, err) + require.Equal(t, "08-wasm", clientState.ClientType()) + + pocketClientState := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(clientState.GetData(), pocketClientState) + require.NoError(t, err) + + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + require.Equal(t, pocketClientState.NetworkId, block.BlockHeader.NetworkId) + require.Equal(t, pocketClientState.TrustLevel, &light_client_types.Fraction{Numerator: 2, Denominator: 3}) + require.Equal(t, pocketClientState.TrustingPeriod.AsDuration().Nanoseconds(), int64(1814400000000000)) + require.Equal(t, pocketClientState.UnbondingPeriod.AsDuration().Nanoseconds(), int64(1814400000000000)) + require.Equal(t, pocketClientState.MaxClockDrift.AsDuration().Nanoseconds(), int64(900000000000)) + require.Equal(t, pocketClientState.LatestHeight, &client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.True(t, pocketClientState.ProofSpec.ConvertToIcs23ProofSpec().SpecEquals(ics23.SmtSpec)) +} + +func TestHost_VerifyHostClientState(t *testing.T) { + _, _, _, persistenceMod, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + approxTime := time.Minute * 15 + unbondingPeriod := time.Duration(1814400000000000) * approxTime + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + publishNewHeightEvent(t, ibcMod.GetBus(), 1) + + rwCtx, err := persistenceMod.NewRWContext(1) + require.NoError(t, err) + defer rwCtx.Release() + err = rwCtx.Commit(nil, nil) + require.NoError(t, err) + + testCases := []struct { + name string + pcs *light_client_types.PocketClientState + expectedErr error + }{ + { + name: "invalid: frozen client", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + FrozenHeight: 1, + }, + expectedErr: errors.New("counterparty client state is frozen"), + }, + { + name: "invalid: different network id", + pcs: &light_client_types.PocketClientState{ + NetworkId: "not correct", + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a different network id"), + }, + { + name: "invalid: different revision number", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 0, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a different revision number"), + }, + { + name: "invalid: equal height", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a height greater than or equal to the host client state"), + }, + { + name: "invalid: wrong trust level", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 1, Denominator: 4}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state trust level is not in the accepted range"), + }, + { + name: "invalid: different proof spec", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.ConvertFromIcs23ProofSpec(ics23.IavlSpec), + }, + expectedErr: errors.New("counterparty client state has different proof spec"), + }, + { + name: "invalid: different unbonding period", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod + 1), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has different unbonding period"), + }, + { + name: "invalid: unbonding period less than trusting period", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod - 1), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state unbonding period is less than trusting period"), + }, + { + name: "valid client state", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bz, err := codec.GetCodec().Marshal(tc.pcs) + require.NoError(t, err) + clientState := &client_types.ClientState{ + Data: bz, + RecentHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + } + err = cm.VerifyHostClientState(clientState) + require.ErrorAs(t, err, &tc.expectedErr) + }) + } +} diff --git a/ibc/main_test.go b/ibc/main_test.go index 51fa0c63f..bf93e685f 100644 --- a/ibc/main_test.go +++ b/ibc/main_test.go @@ -39,7 +39,7 @@ func newTestConsensusModule(t *testing.T, bus modules.Bus) modules.ConsensusModu return consensusMod.(modules.ConsensusModule) } -func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { +func newTestP2PModule(t *testing.T) modules.P2PModule { t.Helper() ctrl := gomock.NewController(t) @@ -57,7 +57,6 @@ func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { AnyTimes() p2pMock.EXPECT().GetModuleName().Return(modules.P2PModuleName).AnyTimes() p2pMock.EXPECT().HandleEvent(gomock.Any()).Return(nil).AnyTimes() - bus.RegisterModule(p2pMock) return p2pMock } @@ -117,7 +116,7 @@ func prepareEnvironment( require.NoError(t, err) bus.RegisterModule(testConsensusMod) - testP2PMock := newTestP2PModule(t, bus) + testP2PMock := newTestP2PModule(t) err = testP2PMock.Start() require.NoError(t, err) bus.RegisterModule(testP2PMock) diff --git a/ibc/path/keys_ics02.go b/ibc/path/keys_ics02.go index 5cce681be..7f06fd089 100644 --- a/ibc/path/keys_ics02.go +++ b/ibc/path/keys_ics02.go @@ -1,6 +1,8 @@ package path -import "fmt" +import ( + "fmt" +) //////////////////////////////////////////////////////////////////////////////// // ICS02 @@ -14,31 +16,43 @@ func FullClientStateKey(clientID string) []byte { return fullClientKey(clientID, KeyClientState) } -// ClientStatePath takes a client identifier and returns a Path string where it can be accessed +// clientStatePath takes a client identifier and returns a Path string where it can be accessed // within the client store -func ClientStatePath(clientID string) string { +func clientStatePath(clientID string) string { return clientPath(clientID, KeyClientState) } +// ClientStateKey takes a client identifier and returns a key where it can be accessed +// within the client store +func ClientStateKey(clientID string) []byte { + return []byte(clientStatePath(clientID)) +} + // consensusStatePath returns the suffix store key for the consensus state at a // particular height stored in a client prefixed store. -func consensusStatePath(height uint64) string { - return fmt.Sprintf("%s/%d", KeyConsensusStatePrefix, height) +func consensusStatePath(height string) string { + return fmt.Sprintf("%s/%s", KeyConsensusStatePrefix, height) +} + +// ConsensusStateKey returns the store key for the consensus state of a particular client +// in a prefixed client store +func ConsensusStateKey(height string) []byte { + return []byte(consensusStatePath(height)) } // fullConsensusStatePath takes a client identifier and returns a Path under which to // store the consensus state of a client. -func fullConsensusStatePath(clientID string, height uint64) string { +func fullConsensusStatePath(clientID, height string) string { return fullClientPath(clientID, consensusStatePath(height)) } // FullConsensusStateKey returns the store key for the consensus state of a particular client. -func FullConsensusStateKey(clientID string, height uint64) []byte { +func FullConsensusStateKey(clientID, height string) []byte { return []byte(fullConsensusStatePath(clientID, height)) } // ConsensusStatePath takes a client identifier and height and returns the Path where the consensus // state can be accessed in the client store -func ConsensusStatePath(clientID string, height uint64) string { +func ConsensusStatePath(clientID, height string) string { return clientPath(clientID, consensusStatePath(height)) } diff --git a/ibc/store/bulk_store_cache.go b/ibc/store/bulk_store_cache.go index 953e9ca3b..4e5e1bd13 100644 --- a/ibc/store/bulk_store_cache.go +++ b/ibc/store/bulk_store_cache.go @@ -92,7 +92,7 @@ func (s *bulkStoreCache) AddStore(name string) error { if _, ok := s.ls.stores[name]; ok { return coreTypes.ErrIBCStoreAlreadyExists(name) } - store := newProvableStore(s.GetBus(), coreTypes.CommitmentPrefix(name), s.privateKey) + store := NewProvableStore(s.GetBus(), coreTypes.CommitmentPrefix(name), s.privateKey) s.ls.stores[store.name] = store return nil } diff --git a/ibc/store/provable_store.go b/ibc/store/provable_store.go index cc600843c..fb13e74c6 100644 --- a/ibc/store/provable_store.go +++ b/ibc/store/provable_store.go @@ -51,8 +51,8 @@ type provableStore struct { privateKey string } -// newProvableStore returns a new instance of provableStore with the bus and prefix provided -func newProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privateKey string) *provableStore { +// NewProvableStore returns a new instance of provableStore with the bus and prefix provided +func NewProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privateKey string) *provableStore { return &provableStore{ m: sync.Mutex{}, bus: bus, @@ -233,5 +233,7 @@ func applyPrefix(prefix coreTypes.CommitmentPrefix, key []byte) coreTypes.Commit if len(prefix) > len(slashed) && bytes.Equal(prefix[:len(slashed)], slashed) { return key } - return path.ApplyPrefix(prefix, string(key)) + prefixed := path.ApplyPrefix(prefix, string(key)) + trimmed := strings.TrimSuffix(string(prefixed), "/") + return coreTypes.CommitmentPath(trimmed) } diff --git a/ibc/store/provable_store_test.go b/ibc/store/provable_store_test.go index 383441ae5..3cb5f808f 100644 --- a/ibc/store/provable_store_test.go +++ b/ibc/store/provable_store_test.go @@ -45,6 +45,12 @@ func TestProvableStore_Get(t *testing.T) { expectedValue: nil, expectedError: coreTypes.ErrIBCKeyDoesNotExist("test/key2"), }, + { + name: "key is nil", + key: nil, + expectedValue: nil, + expectedError: coreTypes.ErrIBCKeyDoesNotExist("test"), + }, } provableStore := newTestProvableStore(t) @@ -344,7 +350,7 @@ func newTestProvableStore(t *testing.T) modules.ProvableStore { require.NoError(t, err) }) - return newProvableStore(bus, []byte("test"), privKey) + return NewProvableStore(bus, []byte("test"), privKey) } func setupDB(t *testing.T) (*smt.SMT, kvstore.KVStore, map[string]string) { diff --git a/runtime/bus.go b/runtime/bus.go index 9d7a6e05f..7052e287f 100644 --- a/runtime/bus.go +++ b/runtime/bus.go @@ -143,6 +143,10 @@ func (m *bus) GetEventLogger() modules.EventLogger { return getModuleFromRegistry[modules.EventLogger](m, modules.EventLoggerModuleName) } +func (m *bus) GetClientManager() modules.ClientManager { + return getModuleFromRegistry[modules.ClientManager](m, modules.ClientManagerModuleName) +} + func (m *bus) GetCurrentHeightProvider() modules.CurrentHeightProvider { return getModuleFromRegistry[modules.CurrentHeightProvider](m, modules.CurrentHeightProviderSubmoduleName) } diff --git a/shared/core/types/error.go b/shared/core/types/error.go index 69369cced..af315a2cc 100644 --- a/shared/core/types/error.go +++ b/shared/core/types/error.go @@ -48,7 +48,7 @@ func NewError(code Code, msg string) Error { } } -// NextCode: 149 +// NextCode: 150 type Code float64 // CONSIDERATION: Should these be a proto enum or a golang iota? //nolint:gosec // G101 - Not hard-coded credentials @@ -198,6 +198,7 @@ const ( CodeIBCStoreAlreadyExistsError Code = 146 CodeIBCStoreDoesNotExistError Code = 147 CodeIBCKeyDoesNotExistError Code = 148 + CodeIBCClientNotActiveError Code = 149 ) const ( @@ -344,6 +345,7 @@ const ( IBCStoreAlreadyExistsError = "ibc store already exists in the store manager" IBCStoreDoesNotExistError = "ibc store does not exist in the store manager" IBCKeyDoesNotExistError = "key does not exist in the ibc store" + IBCClientNotActiveError = "ibc client is not active" ) func ErrUnknownParam(paramName string) Error { @@ -922,3 +924,7 @@ func ErrIBCStoreDoesNotExist(name string) Error { func ErrIBCKeyDoesNotExist(key string) Error { return NewError(CodeIBCKeyDoesNotExistError, fmt.Sprintf("%s: %s", IBCKeyDoesNotExistError, key)) } + +func ErrIBCClientNotActive() Error { + return NewError(CodeIBCClientNotActiveError, IBCClientNotActiveError) +} From b218b27c8e57ae953def76dc4114db2e031e4413 Mon Sep 17 00:00:00 2001 From: harry <53987565+h5law@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:03:45 +0100 Subject: [PATCH 10/10] fixup: flush --- ibc/client/types/queries_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ibc/client/types/queries_test.go b/ibc/client/types/queries_test.go index 0e58c741d..cbca75d54 100644 --- a/ibc/client/types/queries_test.go +++ b/ibc/client/types/queries_test.go @@ -39,7 +39,7 @@ func TestClientState_Set(t *testing.T) { cache := kvstore.NewMemKVStore() // flush cache - require.NoError(t, provableStore.FlushEntries(cache)) + require.NoError(t, provableStore.FlushCache(cache)) // get all from cache keys, vals, err := cache.GetAll([]byte{}, false) @@ -75,7 +75,7 @@ func TestConsensusState_Set(t *testing.T) { cache := kvstore.NewMemKVStore() // flush cache - require.NoError(t, provableStore.FlushEntries(cache)) + require.NoError(t, provableStore.FlushCache(cache)) // get all from cache keys, vals, err := cache.GetAll([]byte{}, false)