diff --git a/client/context/context.go b/client/context/context.go index c778a4345c3c..2814d4dbc491 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -111,6 +111,53 @@ func NewCLIContextWithFrom(from string) CLIContext { return NewCLIContextWithInputAndFrom(os.Stdin, from) } +// NewCLIContextIBC takes additional arguements +func NewCLIContextIBC(from string, chainID string, nodeURI string) CLIContext { + var rpc rpcclient.Client + + genOnly := viper.GetBool(flags.FlagGenerateOnly) + fromAddress, fromName, err := GetFromFields(os.Stdin, from, genOnly) + if err != nil { + fmt.Printf("failed to get from fields: %v", err) + os.Exit(1) + } + + if !genOnly { + if nodeURI != "" { + rpc = rpcclient.NewHTTP(nodeURI, "/websocket") + } + } + + ctx := CLIContext{ + Client: rpc, + ChainID: chainID, + Output: os.Stdout, + NodeURI: nodeURI, + From: from, + OutputFormat: viper.GetString(cli.OutputFlag), + Height: viper.GetInt64(flags.FlagHeight), + HomeDir: viper.GetString(flags.FlagHome), + TrustNode: viper.GetBool(flags.FlagTrustNode), + UseLedger: viper.GetBool(flags.FlagUseLedger), + BroadcastMode: viper.GetString(flags.FlagBroadcastMode), + Simulate: viper.GetBool(flags.FlagDryRun), + GenerateOnly: genOnly, + FromAddress: fromAddress, + FromName: fromName, + Indent: viper.GetBool(flags.FlagIndentResponse), + SkipConfirm: viper.GetBool(flags.FlagSkipConfirmation), + } + + // create a verifier for the specific chain ID and RPC client + verifier, err := CreateVerifier(ctx, DefaultVerifierCacheSize) + if err != nil && viper.IsSet(flags.FlagTrustNode) { + fmt.Printf("failed to create verifier: %s\n", err) + os.Exit(1) + } + + return ctx.WithVerifier(verifier) +} + // NewCLIContext returns a new initialized CLIContext with parameters from the // command line using Viper. func NewCLIContext() CLIContext { return NewCLIContextWithFrom(viper.GetString(flags.FlagFrom)) } diff --git a/client/context/query.go b/client/context/query.go index 2a553d663367..2a01a7577a0c 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -3,6 +3,7 @@ package context import ( "fmt" "strings" + "time" "github.com/pkg/errors" @@ -12,6 +13,7 @@ import ( tmliteErr "github.com/tendermint/tendermint/lite/errors" tmliteProxy "github.com/tendermint/tendermint/lite/proxy" rpcclient "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" tmtypes "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/store/rootmulti" @@ -28,6 +30,55 @@ func (ctx CLIContext) GetNode() (rpcclient.Client, error) { return ctx.Client, nil } +// WaitForNBlocks blocks until the node defined on the context has advanced N blocks +func (ctx CLIContext) WaitForNBlocks(n int64) { + node, err := ctx.GetNode() + if err != nil { + panic(err) + } + + resBlock, err := node.Block(nil) + var height int64 + if err != nil || resBlock.Block == nil { + // wait for the first block to exist + ctx.waitForHeight(1) + height = 1 + n + } else { + height = resBlock.Block.Height + n + } + ctx.waitForHeight(height) +} + +func (ctx CLIContext) waitForHeight(height int64) { + node, err := ctx.GetNode() + if err != nil { + panic(err) + } + + for { + // get url, try a few times + var resBlock *ctypes.ResultBlock + var err error + INNER: + for i := 0; i < 5; i++ { + resBlock, err = node.Block(nil) + if err == nil { + break INNER + } + time.Sleep(time.Millisecond * 200) + } + if err != nil { + panic(err) + } + + if resBlock.Block != nil && resBlock.Block.Height >= height { + return + } + + time.Sleep(time.Millisecond * 100) + } +} + // Query performs a query to a Tendermint node with the provided path. // It returns the result and height of the query upon success or an error if // the query fails. @@ -49,6 +100,12 @@ func (ctx CLIContext) QueryStore(key cmn.HexBytes, storeName string) ([]byte, in return ctx.queryStore(key, storeName, "key") } +// QueryABCI performs a query to a Tendermint node with the provide RequestQuery. +// It returns the ResultQuery obtained from the query. +func (ctx CLIContext) QueryABCI(req abci.RequestQuery) (abci.ResponseQuery, error) { + return ctx.queryABCI(req) +} + // QuerySubspace performs a query to a Tendermint node with the provided // store name and subspace. It returns key value pair and height of the query // upon success or an error if the query fails. @@ -72,40 +129,64 @@ func (ctx CLIContext) GetFromName() string { return ctx.FromName } -// query performs a query to a Tendermint node with the provided store name -// and path. It returns the result and height of the query upon success -// or an error if the query fails. In addition, it will verify the returned -// proof if TrustNode is disabled. If proof verification fails or the query -// height is invalid, an error will be returned. -func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, height int64, err error) { +func (ctx CLIContext) queryABCI(req abci.RequestQuery) (resp abci.ResponseQuery, err error) { + node, err := ctx.GetNode() if err != nil { - return res, height, err + return resp, err + } + + // When a client did not provide a query height, manually query for it so it can + // be injected downstream into responses. + if ctx.Height == 0 { + status, err := node.Status() + if err != nil { + return resp, err + } + ctx = ctx.WithHeight(status.SyncInfo.LatestBlockHeight) } opts := rpcclient.ABCIQueryOptions{ Height: ctx.Height, - Prove: !ctx.TrustNode, + Prove: req.Prove || !ctx.TrustNode, } - result, err := node.ABCIQueryWithOptions(path, key, opts) + result, err := node.ABCIQueryWithOptions(req.Path, req.Data, opts) if err != nil { - return res, height, err + return } - resp := result.Response + resp = result.Response if !resp.IsOK() { - return res, resp.Height, errors.New(resp.Log) + err = errors.New(resp.Log) + return } // data from trusted node or subspace query doesn't need verification - if ctx.TrustNode || !isQueryStoreWithProof(path) { - return resp.Value, resp.Height, nil + if ctx.TrustNode || !isQueryStoreWithProof(req.Path) { + return resp, nil } - err = ctx.verifyProof(path, resp) + err = ctx.verifyProof(req.Path, resp) + if err != nil { + return + } + + return +} + +// query performs a query to a Tendermint node with the provided store name +// and path. It returns the result and height of the query upon success +// or an error if the query fails. In addition, it will verify the returned +// proof if TrustNode is disabled. If proof verification fails or the query +// height is invalid, an error will be returned. +func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, height int64, err error) { + resp, err := ctx.queryABCI(abci.RequestQuery{ + Path: path, + Data: key, + }) if err != nil { - return res, resp.Height, err + return } return resp.Value, resp.Height, nil diff --git a/x/ibc/23-commitment/codec.go b/x/ibc/23-commitment/codec.go new file mode 100644 index 000000000000..7e90e8cac69e --- /dev/null +++ b/x/ibc/23-commitment/codec.go @@ -0,0 +1,18 @@ +package commitment + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// RegisterCodec registers types declared in this package +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*RootI)(nil), nil) + cdc.RegisterInterface((*PrefixI)(nil), nil) + cdc.RegisterInterface((*PathI)(nil), nil) + cdc.RegisterInterface((*ProofI)(nil), nil) + + cdc.RegisterConcrete(Root{}, "ibc/commitment/merkle/Root", nil) + cdc.RegisterConcrete(Prefix{}, "ibc/commitment/merkle/Prefix", nil) + cdc.RegisterConcrete(Path{}, "ibc/commitment/merkle/Path", nil) + cdc.RegisterConcrete(Proof{}, "ibc/commitment/merkle/Proof", nil) +} diff --git a/x/ibc/23-commitment/commitment_test.go b/x/ibc/23-commitment/commitment_test.go new file mode 100644 index 000000000000..2535f5705e29 --- /dev/null +++ b/x/ibc/23-commitment/commitment_test.go @@ -0,0 +1,37 @@ +package commitment_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/store/iavl" + "github.com/cosmos/cosmos-sdk/store/rootmulti" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + + dbm "github.com/tendermint/tm-db" +) + +type MerkleTestSuite struct { + suite.Suite + + store *rootmulti.Store + storeKey *storetypes.KVStoreKey + iavlStore *iavl.Store +} + +func (suite *MerkleTestSuite) SetupTest() { + db := dbm.NewMemDB() + suite.store = rootmulti.NewStore(db) + + suite.storeKey = storetypes.NewKVStoreKey("iavlStoreKey") + + suite.store.MountStoreWithDB(suite.storeKey, storetypes.StoreTypeIAVL, nil) + suite.store.LoadVersion(0) + + suite.iavlStore = suite.store.GetCommitStore(suite.storeKey).(*iavl.Store) +} + +func TestMerkleTestSuite(t *testing.T) { + suite.Run(t, new(MerkleTestSuite)) +} diff --git a/x/ibc/23-commitment/merkle.go b/x/ibc/23-commitment/merkle.go new file mode 100644 index 000000000000..ab428eeaf737 --- /dev/null +++ b/x/ibc/23-commitment/merkle.go @@ -0,0 +1,143 @@ +package commitment + +import ( + "errors" + + "github.com/tendermint/tendermint/crypto/merkle" + + "github.com/cosmos/cosmos-sdk/store/rootmulti" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +// ICS 023 Merkle Types Implementation +// +// This file defines Merkle commitment types that implements ICS 023. + +// Merkle proof implementation of the Proof interface +// Applied on SDK-based IBC implementation +var _ RootI = Root{} + +// Root defines a merkle root hash. +// In the Cosmos SDK, the AppHash of a block header becomes the Root. +type Root struct { + Hash []byte `json:"hash" yaml:"hash"` +} + +// NewRoot constructs a new Root +func NewRoot(hash []byte) Root { + return Root{ + Hash: hash, + } +} + +// GetCommitmentType implements RootI interface +func (Root) GetCommitmentType() Type { + return Merkle +} + +// GetHash implements RootI interface +func (r Root) GetHash() []byte { + return r.Hash +} + +var _ PrefixI = Prefix{} + +// Prefix is merkle path prefixed to the key. +// The constructed key from the Path and the key will be append(Path.KeyPath, append(Path.KeyPrefix, key...)) +type Prefix struct { + KeyPrefix []byte `json:"key_prefix" yaml:"key_prefix"` // byte slice prefixed before the key +} + +// NewPrefix constructs new Prefix instance +func NewPrefix(keyPrefix []byte) Prefix { + return Prefix{ + KeyPrefix: keyPrefix, + } +} + +// GetCommitmentType implements PrefixI +func (Prefix) GetCommitmentType() Type { + return Merkle +} + +// Bytes returns the key prefix bytes +func (p Prefix) Bytes() []byte { + return p.KeyPrefix +} + +var _ PathI = Path{} + +// Path is the path used to verify commitment proofs, which can be an arbitrary +// structured object (defined by a commitment type). +type Path struct { + KeyPath merkle.KeyPath `json:"key_path" yaml:"key_path"` // byte slice prefixed before the key +} + +// NewPath creates a new CommitmentPath instance +func NewPath(keyPathStr []string) Path { + merkleKeyPath := merkle.KeyPath{} + for _, keyStr := range keyPathStr { + merkleKeyPath = merkleKeyPath.AppendKey([]byte(keyStr), merkle.KeyEncodingURL) + } + + return Path{ + KeyPath: merkleKeyPath, + } +} + +// GetCommitmentType implements PathI +func (Path) GetCommitmentType() Type { + return Merkle +} + +// String implements fmt.Stringer +func (p Path) String() string { + return p.KeyPath.String() +} + +// ApplyPrefix constructs a new commitment path from the arguments. It interprets +// the path argument in the context of the prefix argument. +// +// CONTRACT: provided path string MUST be a well formated path. See ICS24 for +// reference. +func ApplyPrefix(prefix PrefixI, path string) (Path, error) { + err := host.DefaultPathValidator(path) + if err != nil { + return Path{}, err + } + + if prefix == nil || len(prefix.Bytes()) == 0 { + return Path{}, errors.New("prefix can't be empty") + } + + return NewPath([]string{string(prefix.Bytes()), path}), nil +} + +var _ ProofI = Proof{} + +// Proof is a wrapper type that contains a merkle proof. +// It demonstrates membership or non-membership for an element or set of elements, +// verifiable in conjunction with a known commitment root. Proofs should be +// succinct. +type Proof struct { + Proof *merkle.Proof `json:"proof" yaml:"proof"` +} + +// GetCommitmentType implements ProofI +func (Proof) GetCommitmentType() Type { + return Merkle +} + +// VerifyMembership verifies the membership pf a merkle proof against the given root, path, and value. +func (proof Proof) VerifyMembership(root RootI, path PathI, value []byte) bool { + runtime := rootmulti.DefaultProofRuntime() + err := runtime.VerifyValue(proof.Proof, root.GetHash(), path.String(), value) + return err == nil +} + +// VerifyNonMembership verifies the absence of a merkle proof against the given root and path. +func (proof Proof) VerifyNonMembership(root RootI, path PathI) bool { + runtime := rootmulti.DefaultProofRuntime() + err := runtime.VerifyAbsence(proof.Proof, root.GetHash(), path.String()) + return err == nil +} diff --git a/x/ibc/23-commitment/merkle_test.go b/x/ibc/23-commitment/merkle_test.go new file mode 100644 index 000000000000..bfd2dc5a485c --- /dev/null +++ b/x/ibc/23-commitment/merkle_test.go @@ -0,0 +1,122 @@ +package commitment_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + commitment "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment" + + abci "github.com/tendermint/tendermint/abci/types" +) + +func (suite *MerkleTestSuite) TestVerifyMembership() { + suite.iavlStore.Set([]byte("MYKEY"), []byte("MYVALUE")) + cid := suite.store.Commit() + + res := suite.store.Query(abci.RequestQuery{ + Path: fmt.Sprintf("/%s/key", suite.storeKey.Name()), // required path to get key/value+proof + Data: []byte("MYKEY"), + Prove: true, + }) + require.NotNil(suite.T(), res.Proof) + + proof := commitment.Proof{ + Proof: res.Proof, + } + + cases := []struct { + name string + root []byte + pathArr []string + value []byte + shouldPass bool + }{ + {"valid proof", cid.Hash, []string{suite.storeKey.Name(), "MYKEY"}, []byte("MYVALUE"), true}, // valid proof + {"wrong value", cid.Hash, []string{suite.storeKey.Name(), "MYKEY"}, []byte("WRONGVALUE"), false}, // invalid proof with wrong value + {"nil value", cid.Hash, []string{suite.storeKey.Name(), "MYKEY"}, []byte(nil), false}, // invalid proof with nil value + {"wrong key", cid.Hash, []string{suite.storeKey.Name(), "NOTMYKEY"}, []byte("MYVALUE"), false}, // invalid proof with wrong key + {"wrong path 1", cid.Hash, []string{suite.storeKey.Name(), "MYKEY", "MYKEY"}, []byte("MYVALUE"), false}, // invalid proof with wrong path + {"wrong path 2", cid.Hash, []string{suite.storeKey.Name()}, []byte("MYVALUE"), false}, // invalid proof with wrong path + {"wrong path 3", cid.Hash, []string{"MYKEY"}, []byte("MYVALUE"), false}, // invalid proof with wrong path + {"wrong storekey", cid.Hash, []string{"otherStoreKey", "MYKEY"}, []byte("MYVALUE"), false}, // invalid proof with wrong store prefix + {"wrong root", []byte("WRONGROOT"), []string{suite.storeKey.Name(), "MYKEY"}, []byte("MYVALUE"), false}, // invalid proof with wrong root + {"nil root", []byte(nil), []string{suite.storeKey.Name(), "MYKEY"}, []byte("MYVALUE"), false}, // invalid proof with nil root + } + + for i, tc := range cases { + suite.Run(tc.name, func() { + root := commitment.NewRoot(tc.root) + path := commitment.NewPath(tc.pathArr) + + ok := proof.VerifyMembership(root, path, tc.value) + + require.True(suite.T(), ok == tc.shouldPass, "Test case %d failed", i) + }) + } + +} + +func (suite *MerkleTestSuite) TestVerifyNonMembership() { + suite.iavlStore.Set([]byte("MYKEY"), []byte("MYVALUE")) + cid := suite.store.Commit() + + // Get Proof + res := suite.store.Query(abci.RequestQuery{ + Path: fmt.Sprintf("/%s/key", suite.storeKey.Name()), // required path to get key/value+proof + Data: []byte("MYABSENTKEY"), + Prove: true, + }) + require.NotNil(suite.T(), res.Proof) + + proof := commitment.Proof{ + Proof: res.Proof, + } + + cases := []struct { + name string + root []byte + pathArr []string + shouldPass bool + }{ + {"valid proof", cid.Hash, []string{suite.storeKey.Name(), "MYABSENTKEY"}, true}, // valid proof + {"wrong key", cid.Hash, []string{suite.storeKey.Name(), "MYKEY"}, false}, // invalid proof with existent key + {"wrong path 1", cid.Hash, []string{suite.storeKey.Name(), "MYKEY", "MYABSENTKEY"}, false}, // invalid proof with wrong path + {"wrong path 2", cid.Hash, []string{suite.storeKey.Name(), "MYABSENTKEY", "MYKEY"}, false}, // invalid proof with wrong path + {"wrong path 3", cid.Hash, []string{suite.storeKey.Name()}, false}, // invalid proof with wrong path + {"wrong path 4", cid.Hash, []string{"MYABSENTKEY"}, false}, // invalid proof with wrong path + {"wrong storeKey", cid.Hash, []string{"otherStoreKey", "MYABSENTKEY"}, false}, // invalid proof with wrong store prefix + {"wrong root", []byte("WRONGROOT"), []string{suite.storeKey.Name(), "MYABSENTKEY"}, false}, // invalid proof with wrong root + {"nil root", []byte(nil), []string{suite.storeKey.Name(), "MYABSENTKEY"}, false}, // invalid proof with nil root + } + + for i, tc := range cases { + suite.Run(tc.name, func() { + root := commitment.NewRoot(tc.root) + path := commitment.NewPath(tc.pathArr) + + ok := proof.VerifyNonMembership(root, path) + + require.True(suite.T(), ok == tc.shouldPass, "Test case %d failed", i) + }) + } + +} + +func TestApplyPrefix(t *testing.T) { + prefix := commitment.NewPrefix([]byte("storePrefixKey")) + + pathStr := "path1/path2/path3/key" + + prefixedPath, err := commitment.ApplyPrefix(prefix, pathStr) + require.Nil(t, err, "valid prefix returns error") + + require.Equal(t, "/storePrefixKey/path1/path2/path3/key", prefixedPath.String(), "Prefixed path incorrect") + + // invalid prefix contains non-alphanumeric character + invalidPathStr := "invalid-path/doesitfail?/hopefully" + invalidPath, err := commitment.ApplyPrefix(prefix, invalidPathStr) + require.NotNil(t, err, "invalid prefix does not returns error") + require.Equal(t, commitment.Path{}, invalidPath, "invalid prefix returns valid Path on ApplyPrefix") +} diff --git a/x/ibc/23-commitment/types.go b/x/ibc/23-commitment/types.go new file mode 100644 index 000000000000..1b70f4f04d9c --- /dev/null +++ b/x/ibc/23-commitment/types.go @@ -0,0 +1,65 @@ +package commitment + +// ICS 023 Types Implementation +// +// This file includes types defined under +// https://github.com/cosmos/ics/tree/master/spec/ics-023-vector-commitments + +// spec:Path and spec:Value are defined as bytestring + +// RootI implements spec:CommitmentRoot. +// A root is constructed from a set of key-value pairs, +// and the inclusion or non-inclusion of an arbitrary key-value pair +// can be proven with the proof. +type RootI interface { + GetCommitmentType() Type + GetHash() []byte +} + +// PrefixI implements spec:CommitmentPrefix. +// Prefix represents the common "prefix" that a set of keys shares. +type PrefixI interface { + GetCommitmentType() Type + Bytes() []byte +} + +// PathI implements spec:CommitmentPath. +// A path is the additional information provided to the verification function. +type PathI interface { + GetCommitmentType() Type + String() string +} + +// ProofI implements spec:CommitmentProof. +// Proof can prove whether the key-value pair is a part of the Root or not. +// Each proof has designated key-value pair it is able to prove. +// Proofs includes key but value is provided dynamically at the verification time. +type ProofI interface { + GetCommitmentType() Type + VerifyMembership(RootI, PathI, []byte) bool + VerifyNonMembership(RootI, PathI) bool +} + +// Type defines the type of the commitment +type Type byte + +// Registered commitment types +const ( + Merkle Type = iota + 1 // 1 +) + +// string representation of the commitment types +const ( + TypeMerkle string = "merkle" +) + +// String implements the Stringer interface +func (ct Type) String() string { + switch ct { + case Merkle: + return TypeMerkle + + default: + return "" + } +} diff --git a/x/ibc/23-commitment/verify.go b/x/ibc/23-commitment/verify.go new file mode 100644 index 000000000000..7c620c2b5a90 --- /dev/null +++ b/x/ibc/23-commitment/verify.go @@ -0,0 +1,62 @@ +package commitment + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// CalculateRoot returns the application Hash at the curretn block height as a commitment +// root for proof verification. +func CalculateRoot(ctx sdk.Context) RootI { + return NewRoot(ctx.BlockHeader().AppHash) +} + +// BatchVerifyMembership verifies a proof that many paths have been set to +// specific values in a commitment. It calls the proof's VerifyMembership method +// with the calculated root and the provided paths. +// Returns false on the first failed membership verification. +func BatchVerifyMembership( + ctx sdk.Context, + proof ProofI, + prefix PrefixI, + items map[string][]byte, +) bool { + root := CalculateRoot(ctx) + + for pathStr, value := range items { + path, err := ApplyPrefix(prefix, pathStr) + if err != nil { + return false + } + + if ok := proof.VerifyMembership(root, path, value); !ok { + return false + } + } + + return true +} + +// BatchVerifyNonMembership verifies a proof that many paths have not been set +// to any value in a commitment. It calls the proof's VerifyNonMembership method +// with the calculated root and the provided paths. +// Returns false on the first failed non-membership verification. +func BatchVerifyNonMembership( + ctx sdk.Context, + proof ProofI, + prefix PrefixI, + paths []string, +) bool { + root := CalculateRoot(ctx) + for _, pathStr := range paths { + path, err := ApplyPrefix(prefix, pathStr) + if err != nil { + return false + } + + if ok := proof.VerifyNonMembership(root, path); !ok { + return false + } + } + + return true +} diff --git a/x/ibc/24-host/errors.go b/x/ibc/24-host/errors.go new file mode 100644 index 000000000000..afc1c26ea303 --- /dev/null +++ b/x/ibc/24-host/errors.go @@ -0,0 +1,19 @@ +package host + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// IBCCodeSpace is the codespace for all errors defined in the ibc module +const IBCCodeSpace = "ibc" + +var ( + // ErrInvalidID is returned if identifier string is invalid + ErrInvalidID = sdkerrors.Register(IBCCodeSpace, 1, "invalid identifier") + + // ErrInvalidPath is returned if path string is invalid + ErrInvalidPath = sdkerrors.Register(IBCCodeSpace, 2, "invalid path") + + // ErrInvalidPacket is returned if packets embedded in msg are invalid + ErrInvalidPacket = sdkerrors.Register(IBCCodeSpace, 3, "invalid packet extracted from msg") +) diff --git a/x/ibc/24-host/utils.go b/x/ibc/24-host/utils.go new file mode 100644 index 000000000000..c75f356561f6 --- /dev/null +++ b/x/ibc/24-host/utils.go @@ -0,0 +1,11 @@ +package host + +// RemovePath is an util function to remove a path from a set. +func RemovePath(paths []string, path string) ([]string, bool) { + for i, p := range paths { + if p == path { + return append(paths[:i], paths[i+1:]...), true + } + } + return paths, false +} diff --git a/x/ibc/24-host/validate.go b/x/ibc/24-host/validate.go new file mode 100644 index 000000000000..6632d27c25c1 --- /dev/null +++ b/x/ibc/24-host/validate.go @@ -0,0 +1,98 @@ +package host + +import ( + "regexp" + "strings" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// ICS 024 Identifier and Path Validation Implementation +// +// This file defines ValidateFn to validate identifier and path strings +// The spec for ICS 024 can be located here: +// https://github.com/cosmos/ics/tree/master/spec/ics-024-host-requirements + +// regular expression to check string is lowercase alphabetic characters only +var isAlphaLower = regexp.MustCompile(`^[a-z]+$`).MatchString + +// regular expression to check string is alphanumeric +var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString + +// ValidateFn function type to validate path and identifier bytestrings +type ValidateFn func(string) error + +func defaultIdentifierValidator(id string, min, max int) error { + // valid id MUST NOT contain "/" separator + if strings.Contains(id, "/") { + return sdkerrors.Wrap(ErrInvalidID, "identifier cannot contain separator: /") + } + // valid id must be between 10 and 20 characters + if len(id) < min || len(id) > max { + return sdkerrors.Wrapf(ErrInvalidID, "identifier has invalid length: %d, must be between %d-%d characters", len(id), min, max) + } + // valid id must contain only lower alphabetic characters + if !isAlphaLower(id) { + return sdkerrors.Wrap(ErrInvalidID, "identifier must contain only lowercase alphabetic characters") + } + return nil +} + +// DefaultClientIdentifierValidator is the default validator function for Client identifiers +// A valid Identifier must be between 10-20 characters and only contain lowercase +// alphabetic characters, +func DefaultClientIdentifierValidator(id string) error { + return defaultIdentifierValidator(id, 10, 20) +} + +// DefaultConnectionIdentifierValidator is the default validator function for Connection identifiers +// A valid Identifier must be between 10-20 characters and only contain lowercase +// alphabetic characters, +func DefaultConnectionIdentifierValidator(id string) error { + return defaultIdentifierValidator(id, 10, 20) +} + +// DefaultChannelIdentifierValidator is the default validator function for Channel identifiers +// A valid Identifier must be between 10-20 characters and only contain lowercase +// alphabetic characters, +func DefaultChannelIdentifierValidator(id string) error { + return defaultIdentifierValidator(id, 10, 20) +} + +// DefaultPortIdentifierValidator is the default validator function for Port identifiers +// A valid Identifier must be between 2-20 characters and only contain lowercase +// alphabetic characters, +func DefaultPortIdentifierValidator(id string) error { + return defaultIdentifierValidator(id, 2, 20) +} + +// NewPathValidator takes in a Identifier Validator function and returns +// a Path Validator function which requires path only has valid identifiers +// alphanumeric character strings, and "/" separators +func NewPathValidator(idValidator ValidateFn) ValidateFn { + return func(path string) error { + pathArr := strings.Split(path, "/") + for _, p := range pathArr { + // Each path element must either be valid identifier or alphanumeric + err := idValidator(p) + if err != nil && !isAlphaNumeric(p) { + return sdkerrors.Wrapf(ErrInvalidPath, "path contains invalid identifier or non-alphanumeric path element: %s", p) + } + } + return nil + } +} + +// DefaultPathValidator takes in path string and validates +// with default identifier rules. This is optimized by simply +// checking that all path elements are alphanumeric +func DefaultPathValidator(path string) error { + pathArr := strings.Split(path, "/") + for _, p := range pathArr { + // Each path element must either be alphanumeric + if !isAlphaNumeric(p) { + return sdkerrors.Wrapf(ErrInvalidPath, "invalid path element containing non-alphanumeric characters: %s", p) + } + } + return nil +}