diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9d8716542 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ibc-test-framework diff --git a/assets/badcontract.wasm b/assets/badcontract.wasm new file mode 100644 index 000000000..6a6ac22b3 Binary files /dev/null and b/assets/badcontract.wasm differ diff --git a/cmd/custom.go b/cmd/custom.go new file mode 100644 index 000000000..d0d6753ee --- /dev/null +++ b/cmd/custom.go @@ -0,0 +1,158 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/strangelove-ventures/ibc-test-framework/ibc" + "golang.org/x/sync/errgroup" +) + +// customCmd represents the custom command +var customCmd = &cobra.Command{ + Use: "custom", + Short: "Run with custom chain configurations", + Long: `This command allows you to provide all of the possible configuration parameters +for spinning up the source and destination chains +`, + Run: func(cmd *cobra.Command, args []string) { + flags := cmd.Flags() + relayerImplementationString, _ := flags.GetString("relayer") + testCasesString := args[0] + + relayerImplementation := parseRelayerImplementation(relayerImplementationString) + + srcType, _ := flags.GetString("src-type") + dstType, _ := flags.GetString("dst-type") + + // only cosmos chains supported for now + switch srcType { + case "cosmos": + break + default: + panic(fmt.Sprintf("chain type not supported: %s", srcType)) + } + + switch dstType { + case "cosmos": + break + default: + panic(fmt.Sprintf("chain type not supported: %s", dstType)) + } + + srcVals, _ := flags.GetInt("src-vals") + dstVals, _ := flags.GetInt("dst-vals") + + srcChainID, _ := flags.GetString("src-chain-id") + dstChainID, _ := flags.GetString("dst-chain-id") + + srcName, _ := flags.GetString("src-name") + dstName, _ := flags.GetString("dst-name") + + srcImage, _ := flags.GetString("src-image") + dstImage, _ := flags.GetString("dst-image") + + srcVersion, _ := flags.GetString("src-version") + dstVersion, _ := flags.GetString("dst-version") + + srcBinary, _ := flags.GetString("src-binary") + dstBinary, _ := flags.GetString("dst-binary") + + srcBech32Prefix, _ := flags.GetString("src-bech32") + dstBech32Prefix, _ := flags.GetString("dst-bech32") + + srcDenom, _ := flags.GetString("src-denom") + dstDenom, _ := flags.GetString("dst-denom") + + srcGasPrices, _ := flags.GetString("src-gas-prices") + dstGasPrices, _ := flags.GetString("dst-gas-prices") + + srcGasAdjustment, _ := flags.GetFloat64("src-gas-adjustment") + dstGasAdjustment, _ := flags.GetFloat64("dst-gas-adjustment") + + srcTrustingPeriod, _ := flags.GetString("src-trusting-period") + dstTrustingPeriod, _ := flags.GetString("dst-trusting-period") + + parallel, _ := flags.GetBool("parallel") + + srcChainCfg := ibc.NewCosmosChainConfig(srcName, srcImage, srcBinary, srcBech32Prefix, srcDenom, srcGasPrices, srcGasAdjustment, srcTrustingPeriod) + dstChainCfg := ibc.NewCosmosChainConfig(dstName, dstImage, dstBinary, dstBech32Prefix, dstDenom, dstGasPrices, dstGasAdjustment, dstTrustingPeriod) + + srcChainCfg.ChainID = srcChainID + dstChainCfg.ChainID = dstChainID + + srcChainCfg.Version = srcVersion + dstChainCfg.Version = dstVersion + + var testCases []func(testName string, srcChain ibc.Chain, dstChain ibc.Chain, relayerImplementation ibc.RelayerImplementation) error + + for _, testCaseString := range strings.Split(testCasesString, ",") { + testCase, err := ibc.GetTestCase(testCaseString) + if err != nil { + panic(err) + } + testCases = append(testCases, testCase) + } + + if parallel { + var eg errgroup.Group + for i, testCase := range testCases { + testCase := testCase + testName := fmt.Sprintf("Test%d", i) + srcChain := ibc.NewCosmosChain(testName, srcChainCfg, srcVals, 1) + dstChain := ibc.NewCosmosChain(testName, dstChainCfg, dstVals, 1) + eg.Go(func() error { + return testCase(testName, srcChain, dstChain, relayerImplementation) + }) + } + if err := eg.Wait(); err != nil { + panic(err) + } + } else { + for i, testCase := range testCases { + testName := fmt.Sprintf("Test%d", i) + srcChain := ibc.NewCosmosChain(testName, srcChainCfg, srcVals, 1) + dstChain := ibc.NewCosmosChain(testName, dstChainCfg, dstVals, 1) + if err := testCase(testName, srcChain, dstChain, relayerImplementation); err != nil { + panic(err) + } + } + } + fmt.Println("PASS") + }, +} + +func init() { + testCmd.AddCommand(customCmd) + + customCmd.Flags().StringP("src-name", "s", "gaia", "Source chain name") + customCmd.Flags().String("src-type", "cosmos", "Type of source chain") + customCmd.Flags().String("src-bech32", "cosmos", "Bech32 prefix for source chain") + customCmd.Flags().String("src-denom", "uatom", "Native denomination for source chain") + customCmd.Flags().String("src-gas-prices", "0.01uatom", "Gas prices for source chain") + customCmd.Flags().Float64("src-gas-adjustment", 1.3, "Gas adjustment for source chain") + customCmd.Flags().String("src-trust", "504h", "Trusting period for source chain ") + customCmd.Flags().String("src-image", "ghcr.io/strangelove-ventures/heighliner/gaia", "Docker image for source chain") + customCmd.Flags().String("src-version", "v7.0.1", "Docker image version for source chain") + customCmd.Flags().String("src-binary", "gaiad", "Binary for source chain") + customCmd.Flags().String("src-chain-id", "srcchain-1", "Chain ID to use for the source chain") + customCmd.Flags().Int("src-vals", 4, "Number of Validator nodes on source chain") + + customCmd.Flags().StringP("dst-name", "d", "gaia", "Destination chain name") + customCmd.Flags().String("dst-type", "cosmos", "Type of destination chain") + customCmd.Flags().String("dst-bech32", "cosmos", "Bech32 prefix for destination chain") + customCmd.Flags().String("dst-denom", "uatom", "Native denomination for destination chain") + customCmd.Flags().String("dst-gas-prices", "0.01uatom", "Gas prices for destination chain") + customCmd.Flags().Float64("dst-gas-adjustment", 1.3, "Gas adjustment for destination chain") + customCmd.Flags().String("dst-trust", "504h", "Trusting period for destination chain") + customCmd.Flags().String("dst-image", "ghcr.io/strangelove-ventures/heighliner/gaia", "Docker image for destination chain") + customCmd.Flags().String("dst-version", "v7.0.1", "Docker image version for destination chain") + customCmd.Flags().String("dst-binary", "gaiad", "Binary for destination chain") + customCmd.Flags().String("dst-chain-id", "dstchain-1", "Chain ID to use for the source chain") + customCmd.Flags().Int("dst-vals", 4, "Number of Validator nodes on destination chain") + + customCmd.Flags().StringP("relayer", "r", "rly", "Relayer implementation to use (rly or hermes)") + customCmd.Flags().BoolP("parallel", "p", false, "Run tests in parallel") + +} diff --git a/cmd/test.go b/cmd/test.go index a716d16ca..f2988844b 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -61,13 +61,12 @@ e.g. ibc-test-framework test # Specify specific chains/versions, relayer implementation, and test cases -ibc-test-framework test --source osmosis:v7.0.4 --destination juno:v2.3.0 --relayer rly RelayPacketTest,RelayPacketTestHeightTimeout +ibc-test-framework test --src osmosis:v7.0.4 --dst juno:v2.3.0 --relayer rly RelayPacketTest,RelayPacketTestHeightTimeout # Shorthand flags -ibc-test-framework test -src osmosis:v7.0.4 -dst juno:v2.3.0 -r rly RelayPacketTest +ibc-test-framework test -s osmosis:v7.0.4 -d juno:v2.3.0 -r rly RelayPacketTest `, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("IBC Test Framework") flags := cmd.Flags() srcChainNameVersion, _ := flags.GetString("src") dstChainNameVersion, _ := flags.GetString("dst") @@ -101,7 +100,7 @@ ibc-test-framework test -src osmosis:v7.0.4 -dst juno:v2.3.0 -r rly RelayPacketT var eg errgroup.Group for i, testCase := range testCases { testCase := testCase - testName := fmt.Sprintf("RelayTest%d", i) + testName := fmt.Sprintf("Test%d", i) eg.Go(func() error { return runTestCase(testName, testCase, relayerImplementation, srcChainName, srcChainVersion, srcChainID, srcVals, dstChainName, dstChainVersion, dstChainID, dstVals) }) @@ -111,7 +110,7 @@ ibc-test-framework test -src osmosis:v7.0.4 -dst juno:v2.3.0 -r rly RelayPacketT } } else { for i, testCase := range testCases { - testName := fmt.Sprintf("RelayTest%d", i) + testName := fmt.Sprintf("Test%d", i) if err := runTestCase(testName, testCase, relayerImplementation, srcChainName, srcChainVersion, srcChainID, srcVals, dstChainName, dstChainVersion, dstChainID, dstVals); err != nil { panic(err) } diff --git a/go.mod b/go.mod index cc24fdf8f..96826db0e 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/cosmos/cosmos-sdk v0.45.1 github.com/cosmos/ibc-go/v3 v3.0.0 github.com/ory/dockertest v3.3.5+incompatible + github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.7.0 github.com/tendermint/tendermint v0.34.14 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c @@ -54,6 +55,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect github.com/google/btree v1.0.0 // indirect + github.com/google/go-cmp v0.5.7 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect @@ -95,7 +97,6 @@ require ( github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.4.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.10.1 // indirect @@ -109,12 +110,12 @@ require ( github.com/tendermint/tm-db v0.6.4 // indirect github.com/zondax/hid v0.9.0 // indirect go.etcd.io/bbolt v1.3.5 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect - golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect + google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index f647ea3a7..cd33717a2 100644 --- a/go.sum +++ b/go.sum @@ -394,8 +394,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64= @@ -833,7 +834,6 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= @@ -969,8 +969,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU= +golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1067,8 +1068,10 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1192,11 +1195,14 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1391,8 +1397,9 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/ibc/Chain.go b/ibc/Chain.go index 09329e830..ff5d5a03d 100644 --- a/ibc/Chain.go +++ b/ibc/Chain.go @@ -42,28 +42,55 @@ type Chain interface { // sets up everything needed (validators, gentx, fullnodes, peering, additional accounts) for chain to start from genesis Start(testName string, ctx context.Context, additionalGenesisWallets []WalletAmount) error + // start a chain with a provided genesis file. Will override validators for first 2/3 of voting power + StartWithGenesisFile(testName string, ctx context.Context, home string, pool *dockertest.Pool, networkID string, genesisFilePath string) error + + // export state at specific height + ExportState(ctx context.Context, height int64) (string, error) + // retrieves rpc address that can be reached by other containers in the docker network GetRPCAddress() string // retrieves grpc address that can be reached by other containers in the docker network GetGRPCAddress() string + // get current height + Height() (int64, error) + // creates a test key in the "user" node, (either the first fullnode or the first validator if no fullnodes) CreateKey(ctx context.Context, keyName string) error // fetches the bech32 address for a test key on the "user" node (either the first fullnode or the first validator if no fullnodes) GetAddress(keyName string) ([]byte, error) + // send funds to wallet from user account + SendFunds(ctx context.Context, keyName string, amount WalletAmount) error + // sends an IBC transfer from a test key on the "user" node (either the first fullnode or the first validator if no fullnodes) // returns tx hash SendIBCTransfer(ctx context.Context, channelID, keyName string, amount WalletAmount, timeout *IBCTimeout) (string, error) - // waits for # of blocks to be produced - WaitForBlocks(number int64) error + // takes file path to smart contract and initialization message. returns contract address + InstantiateContract(ctx context.Context, keyName string, amount WalletAmount, fileName, initMessage string, needsNoAdminFlag bool) (string, error) + + // executes a contract transaction with a message using it's address + ExecuteContract(ctx context.Context, keyName string, contractAddress string, message string) error + + // dump state of contract at block height + DumpContractState(ctx context.Context, contractAddress string, height int64) (*DumpContractStateResponse, error) + + // create balancer pool + CreatePool(ctx context.Context, keyName string, contractAddress string, swapFee float64, exitFee float64, assets []WalletAmount) error + + // waits for # of blocks to be produced. Returns latest height + WaitForBlocks(number int64) (int64, error) // fetch balance for a specific account address and denom GetBalance(ctx context.Context, address string, denom string) (int64, error) + // get the fees in native denom for an amount of spent gas + GetGasFeesInNativeDenom(gasPaid int64) int64 + // fetch transaction GetTransaction(ctx context.Context, txHash string) (*types.TxResponse, error) } diff --git a/ibc/Relayer.go b/ibc/Relayer.go index 99453ff95..3d1683d63 100644 --- a/ibc/Relayer.go +++ b/ibc/Relayer.go @@ -40,6 +40,9 @@ type Relayer interface { // setup channels, connections, and clients LinkPath(ctx context.Context, pathName string) error + // update clients, such as after new genesis + UpdateClients(ctx context.Context, pathName string) error + // get channel IDs for chain GetChannels(ctx context.Context, chainID string) ([]ChannelOutput, error) diff --git a/ibc/cosmos_chain.go b/ibc/cosmos_chain.go index 2a6122c16..185d1006a 100644 --- a/ibc/cosmos_chain.go +++ b/ibc/cosmos_chain.go @@ -1,13 +1,22 @@ package ibc import ( + "bytes" "context" + "encoding/hex" + "encoding/json" "fmt" "io/ioutil" + "math" "os" "path" + "sort" + "strconv" + "strings" + "time" "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" authTx "github.com/cosmos/cosmos-sdk/x/auth/tx" bankTypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -23,10 +32,31 @@ type CosmosChain struct { cfg ChainConfig numValidators int numFullNodes int - chainNodes ChainNodes + ChainNodes ChainNodes } func NewCosmosChainConfig(name string, + dockerImage string, + binary string, + bech32Prefix string, + denom string, + gasPrices string, + gasAdjustment float64, + trustingPeriod string) ChainConfig { + return ChainConfig{ + Type: "cosmos", + Name: name, + Bech32Prefix: bech32Prefix, + Denom: denom, + GasPrices: gasPrices, + GasAdjustment: gasAdjustment, + TrustingPeriod: trustingPeriod, + Repository: dockerImage, + Bin: binary, + } +} + +func NewCosmosHeighlinerChainConfig(name string, binary string, bech32Prefix string, denom string, @@ -67,12 +97,12 @@ func (c *CosmosChain) Initialize(testName string, homeDirectory string, dockerPo } func (c *CosmosChain) getRelayerNode() *ChainNode { - if len(c.chainNodes) > c.numValidators { + if len(c.ChainNodes) > c.numValidators { // use first full node - return c.chainNodes[c.numValidators] + return c.ChainNodes[c.numValidators] } // use first validator - return c.chainNodes[0] + return c.ChainNodes[0] } // Implements Chain interface @@ -100,16 +130,50 @@ func (c *CosmosChain) GetAddress(keyName string) ([]byte, error) { return keyInfo.GetAddress().Bytes(), nil } +// Implements Chain interface +func (c *CosmosChain) SendFunds(ctx context.Context, keyName string, amount WalletAmount) error { + return c.getRelayerNode().SendFunds(ctx, keyName, amount) +} + // Implements Chain interface func (c *CosmosChain) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount WalletAmount, timeout *IBCTimeout) (string, error) { return c.getRelayerNode().SendIBCTransfer(ctx, channelID, keyName, amount, timeout) } // Implements Chain interface -func (c *CosmosChain) WaitForBlocks(number int64) error { +func (c *CosmosChain) InstantiateContract(ctx context.Context, keyName string, amount WalletAmount, fileName, initMessage string, needsNoAdminFlag bool) (string, error) { + return c.getRelayerNode().InstantiateContract(ctx, keyName, amount, fileName, initMessage, needsNoAdminFlag) +} + +// Implements Chain interface +func (c *CosmosChain) ExecuteContract(ctx context.Context, keyName string, contractAddress string, message string) error { + return c.getRelayerNode().ExecuteContract(ctx, keyName, contractAddress, message) +} + +// Implements Chain interface +func (c *CosmosChain) DumpContractState(ctx context.Context, contractAddress string, height int64) (*DumpContractStateResponse, error) { + return c.getRelayerNode().DumpContractState(ctx, contractAddress, height) +} + +// Implements Chain interface +func (c *CosmosChain) ExportState(ctx context.Context, height int64) (string, error) { + return c.getRelayerNode().ExportState(ctx, height) +} + +// Implements Chain interface +func (c *CosmosChain) CreatePool(ctx context.Context, keyName string, contractAddress string, swapFee float64, exitFee float64, assets []WalletAmount) error { + return c.getRelayerNode().CreatePool(ctx, keyName, contractAddress, swapFee, exitFee, assets) +} + +// Implements Chain interface +func (c *CosmosChain) WaitForBlocks(number int64) (int64, error) { return c.getRelayerNode().WaitForBlocks(number) } +func (c *CosmosChain) Height() (int64, error) { + return c.getRelayerNode().Height() +} + // Implements Chain interface func (c *CosmosChain) GetBalance(ctx context.Context, address string, denom string) (int64, error) { params := &bankTypes.QueryBalanceRequest{Address: address, Denom: denom} @@ -134,10 +198,16 @@ func (c *CosmosChain) GetTransaction(ctx context.Context, txHash string) (*types return authTx.QueryTx(c.getRelayerNode().CliContext(), txHash) } +func (c *CosmosChain) GetGasFeesInNativeDenom(gasPaid int64) int64 { + gasPrice, _ := strconv.ParseFloat(strings.Replace(c.cfg.GasPrices, c.cfg.Denom, "", 1), 64) + fees := float64(gasPaid) * gasPrice + return int64(fees) +} + // creates the test node objects required for bootstrapping tests func (c *CosmosChain) initializeChainNodes(testName, home string, pool *dockertest.Pool, networkID string) { - chainNodes := []*ChainNode{} + ChainNodes := []*ChainNode{} count := c.numValidators + c.numFullNodes chainCfg := c.Config() err := pool.Client.PullImage(docker.PullImageOptions{ @@ -151,9 +221,178 @@ func (c *CosmosChain) initializeChainNodes(testName, home string, tn := &ChainNode{Home: home, Index: i, Chain: c, Pool: pool, NetworkID: networkID, testName: testName} tn.MkDir() - chainNodes = append(chainNodes, tn) + ChainNodes = append(ChainNodes, tn) + } + c.ChainNodes = ChainNodes +} + +type GenesisValidatorPubKey struct { + Type string `json:"type"` + Value string `json:"value"` +} +type GenesisValidators struct { + Address string `json:"address"` + Name string `json:"name"` + Power string `json:"power"` + PubKey GenesisValidatorPubKey `json:"pub_key"` +} +type GenesisFile struct { + Validators []GenesisValidators `json:"validators"` +} + +type ValidatorWithIntPower struct { + Address string + Power int64 + PubKeyBase64 string +} + +// Bootstraps the chain and starts it from genesis +func (c *CosmosChain) StartWithGenesisFile(testName string, ctx context.Context, home string, pool *dockertest.Pool, networkID string, genesisFilePath string) error { + // copy genesis file to tmp path for modification + genesisTmpFilePath := path.Join(c.getRelayerNode().Dir(), "genesis_tmp.json") + if _, err := copy(genesisFilePath, genesisTmpFilePath); err != nil { + return err } - c.chainNodes = chainNodes + + chainCfg := c.Config() + + genesisJsonBytes, err := ioutil.ReadFile(genesisTmpFilePath) + if err != nil { + return err + } + + genesisFile := GenesisFile{} + if err := json.Unmarshal(genesisJsonBytes, &genesisFile); err != nil { + return err + } + + genesisValidators := genesisFile.Validators + totalPower := int64(0) + + validatorsWithPower := make([]ValidatorWithIntPower, 0) + + for _, genesisValidator := range genesisValidators { + power, err := strconv.ParseInt(genesisValidator.Power, 10, 64) + if err != nil { + return err + } + totalPower += power + validatorsWithPower = append(validatorsWithPower, ValidatorWithIntPower{ + Address: genesisValidator.Address, + Power: power, + PubKeyBase64: genesisValidator.PubKey.Value, + }) + } + + sort.Slice(validatorsWithPower, func(i, j int) bool { + return validatorsWithPower[i].Power > validatorsWithPower[j].Power + }) + + twoThirdsConsensus := int64(math.Ceil(float64(totalPower) * 2 / 3)) + totalConsensus := int64(0) + + c.ChainNodes = []*ChainNode{} + + for i, validator := range validatorsWithPower { + tn := &ChainNode{Home: home, Index: i, Chain: c, + Pool: pool, NetworkID: networkID, testName: testName} + tn.MkDir() + c.ChainNodes = append(c.ChainNodes, tn) + + // just need to get pubkey here + // don't care about what goes into this node's genesis file since it will be overwritten with the modified one + if err := tn.InitHomeFolder(ctx); err != nil { + return err + } + + testNodePubKeyJsonBytes, err := ioutil.ReadFile(tn.PrivValKeyFilePath()) + if err != nil { + return err + } + + testNodePrivValFile := PrivValidatorKeyFile{} + if err := json.Unmarshal(testNodePubKeyJsonBytes, &testNodePrivValFile); err != nil { + return err + } + + // modify genesis file overwriting validators address with the one generated for this test node + genesisJsonBytes = bytes.ReplaceAll(genesisJsonBytes, []byte(validator.Address), []byte(testNodePrivValFile.Address)) + + // modify genesis file overwriting validators base64 pub_key.value with the one generated for this test node + genesisJsonBytes = bytes.ReplaceAll(genesisJsonBytes, []byte(validator.PubKeyBase64), []byte(testNodePrivValFile.PubKey.Value)) + + existingValAddressBytes, err := hex.DecodeString(validator.Address) + if err != nil { + return err + } + + testNodeAddressBytes, err := hex.DecodeString(testNodePrivValFile.Address) + if err != nil { + return err + } + + valConsPrefix := fmt.Sprintf("%svalcons", chainCfg.Bech32Prefix) + + existingValBech32ValConsAddress, err := bech32.ConvertAndEncode(valConsPrefix, existingValAddressBytes) + if err != nil { + return err + } + + testNodeBech32ValConsAddress, err := bech32.ConvertAndEncode(valConsPrefix, testNodeAddressBytes) + if err != nil { + return err + } + + genesisJsonBytes = bytes.ReplaceAll(genesisJsonBytes, []byte(existingValBech32ValConsAddress), []byte(testNodeBech32ValConsAddress)) + + totalConsensus += validator.Power + + if totalConsensus > twoThirdsConsensus { + break + } + } + + for i := 0; i < len(c.ChainNodes); i++ { + if err := ioutil.WriteFile(c.ChainNodes[i].GenesisFilePath(), genesisJsonBytes, 0644); err != nil { //nolint + return err + } + } + + if err := ChainNodes(c.ChainNodes).LogGenesisHashes(); err != nil { + return err + } + + var eg errgroup.Group + + for _, n := range c.ChainNodes { + n := n + eg.Go(func() error { + return n.CreateNodeContainer() + }) + } + if err := eg.Wait(); err != nil { + return err + } + + peers := ChainNodes(c.ChainNodes).PeerString() + + for _, n := range c.ChainNodes { + n.SetValidatorConfigAndPeers(peers) + } + + for _, n := range c.ChainNodes { + n := n + fmt.Printf("{%s} => starting container...\n", n.Name()) + if err := n.StartContainer(ctx); err != nil { + return err + } + } + + time.Sleep(2 * time.Hour) + + // Wait for 5 blocks before considering the chains "started" + _, err = c.getRelayerNode().WaitForBlocks(5) + return err } // Bootstraps the chain and starts it from genesis @@ -179,8 +418,8 @@ func (c *CosmosChain) Start(testName string, ctx context.Context, additionalGene genesisAmounts := []types.Coin{genesisAmount, genesisStakeAmount} - validators := c.chainNodes[:c.numValidators] - fullnodes := c.chainNodes[c.numValidators:] + validators := c.ChainNodes[:c.numValidators] + fullnodes := c.ChainNodes[c.numValidators:] // sign gentx for each validator for _, v := range validators { @@ -281,5 +520,6 @@ func (c *CosmosChain) Start(testName string, ctx context.Context, additionalGene } // Wait for 5 blocks before considering the chains "started" - return c.getRelayerNode().WaitForBlocks(5) + _, err = c.getRelayerNode().WaitForBlocks(5) + return err } diff --git a/ibc/cosmos_relayer.go b/ibc/cosmos_relayer.go index aee577fd6..88e0d9808 100644 --- a/ibc/cosmos_relayer.go +++ b/ibc/cosmos_relayer.go @@ -48,7 +48,7 @@ type CosmosRelayerChainConfig struct { var ( containerImage = "ghcr.io/cosmos/relayer" - containerVersion = "latest" + containerVersion = "v2.0.0-beta4" ) func ChainConfigToCosmosRelayerChainConfig(chainConfig ChainConfig, keyName, rpcAddr, gprcAddr string) CosmosRelayerChainConfig { @@ -129,7 +129,14 @@ func (relayer *CosmosRelayer) StartRelayer(ctx context.Context, pathName string) // Implements Relayer interface func (relayer *CosmosRelayer) StopRelayer(ctx context.Context) error { - return relayer.StopContainer() + if err := relayer.StopContainer(); err != nil { + return err + } + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + _ = relayer.pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: relayer.container.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "50", Follow: false, Timestamps: false}) + fmt.Printf("{%s} - stdout:\n%s\n{%s} - stderr:\n%s\n", relayer.Name(), stdout.String(), relayer.Name(), stderr.String()) + return relayer.pool.Client.RemoveContainer(docker.RemoveContainerOptions{ID: relayer.container.ID}) } // Implements Relayer interface @@ -181,6 +188,13 @@ func (relayer *CosmosRelayer) GeneratePath(ctx context.Context, srcChainID, dstC return handleNodeJobError(relayer.NodeJob(ctx, command)) } +func (relayer *CosmosRelayer) UpdateClients(ctx context.Context, pathName string) error { + command := []string{"rly", "tx", "update-clients", pathName, + "--home", relayer.NodeHome(), + } + return handleNodeJobError(relayer.NodeJob(ctx, command)) +} + func (relayer *CosmosRelayer) CreateNodeContainer(pathName string) error { err := relayer.pool.Client.PullImage(docker.PullImageOptions{ Repository: containerImage, @@ -190,7 +204,7 @@ func (relayer *CosmosRelayer) CreateNodeContainer(pathName string) error { return err } containerName := fmt.Sprintf("%s-%s", relayer.Name(), pathName) - cmd := []string{"rly", "start", pathName, "--home", relayer.NodeHome()} + cmd := []string{"rly", "start", pathName, "--home", relayer.NodeHome(), "--debug"} fmt.Printf("{%s} -> '%s'\n", containerName, strings.Join(cmd, " ")) cont, err := relayer.pool.Client.CreateContainer(docker.CreateContainerOptions{ Name: containerName, @@ -266,7 +280,7 @@ func (relayer *CosmosRelayer) NodeJob(ctx context.Context, cmd []string) (int, s exitCode, err := relayer.pool.Client.WaitContainerWithContext(cont.ID, ctx) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - _ = relayer.pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: cont.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "100", Follow: false, Timestamps: false}) + _ = relayer.pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: cont.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "50", Follow: false, Timestamps: false}) _ = relayer.pool.Client.RemoveContainer(docker.RemoveContainerOptions{ID: cont.ID}) fmt.Printf("{%s} - stdout:\n%s\n{%s} - stderr:\n%s\n", container, stdout.String(), container, stderr.String()) return exitCode, stdout.String(), stderr.String(), err diff --git a/ibc/test_chains.go b/ibc/test_chains.go index 51f5b49d2..af9187982 100644 --- a/ibc/test_chains.go +++ b/ibc/test_chains.go @@ -3,9 +3,9 @@ package ibc import "fmt" var chainConfigs = []ChainConfig{ - NewCosmosChainConfig("gaia", "gaiad", "cosmos", "uatom", "0.01uatom", 1.3, "504h"), - NewCosmosChainConfig("osmosis", "osmosisd", "osmo", "uosmo", "0.0uosmo", 1.3, "336h"), - NewCosmosChainConfig("juno", "junod", "juno", "ujuno", "0.0ujuno", 1.3, "672h"), + NewCosmosHeighlinerChainConfig("gaia", "gaiad", "cosmos", "uatom", "0.01uatom", 1.3, "504h"), + NewCosmosHeighlinerChainConfig("osmosis", "osmosisd", "osmo", "uosmo", "0.0uosmo", 1.3, "336h"), + NewCosmosHeighlinerChainConfig("juno", "junod", "juno", "ujuno", "0.0025ujuno", 1.3, "672h"), } var chainConfigMap map[string]ChainConfig diff --git a/ibc/test_node.go b/ibc/test_node.go index 3a6e62ce2..79b20af93 100644 --- a/ibc/test_node.go +++ b/ibc/test_node.go @@ -7,9 +7,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "os" "path" + "path/filepath" "runtime" "strings" "time" @@ -57,7 +59,7 @@ type Hosts []ContainerPort var ( valKey = "validator" - blockTime = 3 // seconds + blockTime = 2 // seconds p2pPort = "26656/tcp" rpcPort = "26657/tcp" grpcPort = "9090/tcp" @@ -134,6 +136,21 @@ func (tn *ChainNode) GenesisFilePath() string { return path.Join(tn.Dir(), "config", "genesis.json") } +type PrivValidatorKey struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type PrivValidatorKeyFile struct { + Address string `json:"address"` + PubKey PrivValidatorKey `json:"pub_key"` + PrivKey PrivValidatorKey `json:"priv_key"` +} + +func (tn *ChainNode) PrivValKeyFilePath() string { + return path.Join(tn.Dir(), "config", "priv_validator_key.json") +} + func (tn *ChainNode) TMConfigPath() string { return path.Join(tn.Dir(), "config", "config.toml") } @@ -176,13 +193,14 @@ func (tn *ChainNode) SetPrivValdidatorListen(peers string) { } // Wait until we have signed n blocks in a row -func (tn *ChainNode) WaitForBlocks(blocks int64) error { +func (tn *ChainNode) WaitForBlocks(blocks int64) (int64, error) { stat, err := tn.Client.Status(context.Background()) if err != nil { - return err + return -1, err } startingBlock := stat.SyncInfo.LatestBlockHeight + mostRecentBlock := startingBlock fmt.Printf("{WaitForBlocks-%s} Initial Height: %d\n", tn.Chain.Config().ChainID, startingBlock) // timeout after ~1 minute plus block time timeoutSeconds := blocks*int64(blockTime) + int64(60) @@ -191,19 +209,27 @@ func (tn *ChainNode) WaitForBlocks(blocks int64) error { stat, err := tn.Client.Status(context.Background()) if err != nil { - return err + return mostRecentBlock, err } - mostRecentBlock := stat.SyncInfo.LatestBlockHeight + mostRecentBlock = stat.SyncInfo.LatestBlockHeight deltaBlocks := mostRecentBlock - startingBlock if deltaBlocks >= blocks { fmt.Printf("{WaitForBlocks-%s} Time (sec) waiting for %d blocks: %d\n", tn.Chain.Config().ChainID, blocks, i+1) - return nil // done waiting for consecutive signed blocks + return mostRecentBlock, nil // done waiting for consecutive signed blocks } } - return errors.New("timed out waiting for blocks") + return mostRecentBlock, errors.New("timed out waiting for blocks") +} + +func (tn *ChainNode) Height() (int64, error) { + stat, err := tn.Client.Status(context.Background()) + if err != nil { + return -1, err + } + return stat.SyncInfo.LatestBlockHeight, nil } func applyConfigChanges(cfg *tmconfig.Config, peers string) { @@ -286,6 +312,8 @@ func (tn *ChainNode) SendIBCTransfer(ctx context.Context, channelID string, keyN command := []string{tn.Chain.Config().Bin, "tx", "ibc-transfer", "transfer", "transfer", channelID, amount.Address, fmt.Sprintf("%d%s", amount.Amount, amount.Denom), "--keyring-backend", keyring.BackendTest, + "--gas-prices", tn.Chain.Config().GasPrices, + "--gas-adjustment", fmt.Sprint(tn.Chain.Config().GasAdjustment), "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), "--from", keyName, "--output", "json", @@ -312,10 +340,264 @@ func (tn *ChainNode) SendIBCTransfer(ctx context.Context, channelID string, keyN return output.TxHash, nil } +func (tn *ChainNode) SendFunds(ctx context.Context, keyName string, amount WalletAmount) error { + command := []string{tn.Chain.Config().Bin, "tx", "bank", "send", keyName, + amount.Address, fmt.Sprintf("%d%s", amount.Amount, amount.Denom), + "--keyring-backend", keyring.BackendTest, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "-y", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + + return handleNodeJobError(tn.NodeJob(ctx, command)) +} + +func copy(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + nBytes, err := io.Copy(destination, source) + return nBytes, err +} + +type InstantiateContractAttribute struct { + Value string `json:"value"` +} + +type InstantiateContractEvent struct { + Attributes []InstantiateContractAttribute `json:"attributes"` +} + +type InstantiateContractLog struct { + Events []InstantiateContractEvent `json:"event"` +} + +type InstantiateContractResponse struct { + Logs []InstantiateContractLog `json:"log"` +} + +type QueryContractResponse struct { + Contracts []string `json:"contracts"` +} + +type CodeInfo struct { + CodeID string `json:"code_id"` +} +type CodeInfosResponse struct { + CodeInfos []CodeInfo `json:"code_infos"` +} + +func (tn *ChainNode) InstantiateContract(ctx context.Context, keyName string, amount WalletAmount, fileName, initMessage string, needsNoAdminFlag bool) (string, error) { + _, file := filepath.Split(fileName) + newFilePath := path.Join(tn.Dir(), file) + newFilePathContainer := path.Join(tn.NodeHome(), file) + if _, err := copy(fileName, newFilePath); err != nil { + return "", err + } + + command := []string{tn.Chain.Config().Bin, "tx", "wasm", "store", newFilePathContainer, + "--from", keyName, + "--gas-prices", tn.Chain.Config().GasPrices, + "--gas-adjustment", fmt.Sprint(tn.Chain.Config().GasAdjustment), + "--keyring-backend", keyring.BackendTest, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "-y", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + + exitCode, stdout, stderr, err := tn.NodeJob(ctx, command) + if err != nil { + return "", handleNodeJobError(exitCode, stdout, stderr, err) + } + + if _, err := tn.Chain.WaitForBlocks(5); err != nil { + return "", err + } + + command = []string{tn.Chain.Config().Bin, + "query", "wasm", "list-code", "--reverse", + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + + exitCode, stdout, stderr, err = tn.NodeJob(ctx, command) + if err != nil { + return "", handleNodeJobError(exitCode, stdout, stderr, err) + } + + res := CodeInfosResponse{} + if err := json.Unmarshal([]byte(stdout), &res); err != nil { + return "", err + } + + codeID := res.CodeInfos[0].CodeID + + command = []string{tn.Chain.Config().Bin, + "tx", "wasm", "instantiate", codeID, initMessage, + "--gas-prices", tn.Chain.Config().GasPrices, + "--gas-adjustment", fmt.Sprint(tn.Chain.Config().GasAdjustment), + "--label", "satoshi-test", + "--from", keyName, + "--keyring-backend", keyring.BackendTest, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "-y", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + + if needsNoAdminFlag { + command = append(command, "--no-admin") + } + + exitCode, stdout, stderr, err = tn.NodeJob(ctx, command) + if err != nil { + return "", handleNodeJobError(exitCode, stdout, stderr, err) + } + + if _, err := tn.Chain.WaitForBlocks(5); err != nil { + return "", err + } + + command = []string{tn.Chain.Config().Bin, + "query", "wasm", "list-contract-by-code", codeID, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + + exitCode, stdout, stderr, err = tn.NodeJob(ctx, command) + if err != nil { + return "", handleNodeJobError(exitCode, stdout, stderr, err) + } + + contactsRes := QueryContractResponse{} + if err := json.Unmarshal([]byte(stdout), &contactsRes); err != nil { + return "", err + } + + contractAddress := contactsRes.Contracts[len(contactsRes.Contracts)-1] + return contractAddress, nil +} + +func (tn *ChainNode) ExecuteContract(ctx context.Context, keyName string, contractAddress string, message string) error { + command := []string{tn.Chain.Config().Bin, + "tx", "wasm", "execute", contractAddress, message, + "--from", keyName, + "--gas-prices", tn.Chain.Config().GasPrices, + "--gas-adjustment", fmt.Sprint(tn.Chain.Config().GasAdjustment), + "--keyring-backend", keyring.BackendTest, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "-y", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + return handleNodeJobError(tn.NodeJob(ctx, command)) +} + +type ContractStateModels struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type DumpContractStateResponse struct { + Models []ContractStateModels `json:"models"` +} + +func (tn *ChainNode) DumpContractState(ctx context.Context, contractAddress string, height int64) (*DumpContractStateResponse, error) { + command := []string{tn.Chain.Config().Bin, + "query", "wasm", "contract-state", "all", contractAddress, + "--height", fmt.Sprint(height), + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + exitCode, stdout, stderr, err := tn.NodeJob(ctx, command) + if err != nil { + return nil, handleNodeJobError(exitCode, stdout, stderr, err) + } + + res := &DumpContractStateResponse{} + if err := json.Unmarshal([]byte(stdout), res); err != nil { + return nil, err + } + return res, nil +} + +func (tn *ChainNode) ExportState(ctx context.Context, height int64) (string, error) { + command := []string{tn.Chain.Config().Bin, + "export", + "--height", fmt.Sprint(height), + "--home", tn.NodeHome(), + } + + exitCode, stdout, stderr, err := tn.NodeJob(ctx, command) + if err != nil { + return "", handleNodeJobError(exitCode, stdout, stderr, err) + } + // output comes to stderr for some reason + return stderr, nil +} + +func (tn *ChainNode) UnsafeResetAll(ctx context.Context) error { + command := []string{tn.Chain.Config().Bin, + "unsafe-reset-all", + "--home", tn.NodeHome(), + } + + return handleNodeJobError(tn.NodeJob(ctx, command)) +} + +func (tn *ChainNode) CreatePool(ctx context.Context, keyName string, contractAddress string, swapFee float64, exitFee float64, assets []WalletAmount) error { + // TODO generate --pool-file + poolFilePath := "TODO" + command := []string{tn.Chain.Config().Bin, + "tx", "gamm", "create-pool", + "--pool-file", poolFilePath, + "--gas-prices", tn.Chain.Config().GasPrices, + "--gas-adjustment", fmt.Sprint(tn.Chain.Config().GasAdjustment), + "--from", keyName, + "--keyring-backend", keyring.BackendTest, + "--node", fmt.Sprintf("tcp://%s:26657", tn.Name()), + "--output", "json", + "-y", + "--home", tn.NodeHome(), + "--chain-id", tn.Chain.Config().ChainID, + } + return handleNodeJobError(tn.NodeJob(ctx, command)) +} + func (tn *ChainNode) CreateNodeContainer() error { chainCfg := tn.Chain.Config() - cmd := []string{chainCfg.Bin, "start", "--home", tn.NodeHome()} + cmd := []string{chainCfg.Bin, "start", "--home", tn.NodeHome(), "--x-crisis-skip-assert-invariants"} fmt.Printf("{%s} -> '%s'\n", tn.Name(), strings.Join(cmd, " ")) + cont, err := tn.Pool.Client.CreateContainer(docker.CreateContainerOptions{ Name: tn.Name(), Config: &docker.Config{ @@ -548,7 +830,7 @@ func (tn *ChainNode) NodeJob(ctx context.Context, cmd []string) (int, string, st exitCode, err := tn.Pool.Client.WaitContainerWithContext(cont.ID, ctx) stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - _ = tn.Pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: cont.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "100", Follow: false, Timestamps: false}) + _ = tn.Pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: cont.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "50", Follow: false, Timestamps: false}) _ = tn.Pool.Client.RemoveContainer(docker.RemoveContainerOptions{ID: cont.ID}) fmt.Printf("{%s} - stdout:\n%s\n{%s} - stderr:\n%s\n", container, stdout.String(), container, stderr.String()) return exitCode, stdout.String(), stderr.String(), err diff --git a/ibc/test_relay.go b/ibc/test_relay.go index 9963dda5e..5e18547f5 100644 --- a/ibc/test_relay.go +++ b/ibc/test_relay.go @@ -1,39 +1,12 @@ package ibc import ( - "errors" "fmt" - "reflect" "time" transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" ) -// all methods on this struct have the same signature and are method names that will be called by the CLI -type IBCTestCase struct{} - -// uses reflection to get test case -func GetTestCase(testCase string) (func(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error, error) { - v := reflect.ValueOf(IBCTestCase{}) - m := v.MethodByName(testCase) - if m.Kind() != reflect.Func { - return nil, fmt.Errorf("invalid test case: %s", testCase) - } - - testCaseFunc := func(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error { - args := []reflect.Value{reflect.ValueOf(testName), reflect.ValueOf(srcChain), reflect.ValueOf(dstChain), reflect.ValueOf(relayerImplementation)} - result := m.Call(args) - if len(result) != 1 || !result[0].CanInterface() { - return errors.New("error reflecting error return var") - } - - err, _ := result[0].Interface().(error) - return err - } - - return testCaseFunc, nil -} - func (ibc IBCTestCase) RelayPacketTest(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error { ctx, home, pool, network, cleanup, err := SetupTestRun(testName) if err != nil { @@ -46,7 +19,7 @@ func (ibc IBCTestCase) RelayPacketTest(testName string, srcChain Chain, dstChain // funds relayer src and dst wallets on respective chain in genesis // creates a user account on the src chain (separate fullnode) // funds user account on src chain in genesis - channels, user, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, nil) + _, channels, srcUser, dstUser, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, nil) if err != nil { return err } @@ -54,34 +27,34 @@ func (ibc IBCTestCase) RelayPacketTest(testName string, srcChain Chain, dstChain // will test a user sending an ibc transfer from the src chain to the dst chain // denom will be src chain native denom - testDenom := srcChain.Config().Denom + testDenomSrc := srcChain.Config().Denom // query initial balance of user wallet for src chain native denom on the src chain - srcInitialBalance, err := srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + srcInitialBalance, err := srcChain.GetBalance(ctx, srcUser.SrcChainAddress, testDenomSrc) if err != nil { return err } // get ibc denom for test denom on dst chain - denomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom(channels[0].Counterparty.PortID, channels[0].Counterparty.ChannelID, testDenom)) + denomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom(channels[0].Counterparty.PortID, channels[0].Counterparty.ChannelID, testDenomSrc)) dstIbcDenom := denomTrace.IBCDenom() // query initial balance of user wallet for src chain native denom on the dst chain // don't care about error here, account does not exist on destination chain - dstInitialBalance, _ := dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + dstInitialBalance, _ := dstChain.GetBalance(ctx, srcUser.DstChainAddress, dstIbcDenom) fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) // test coin, address is recipient of ibc transfer on dst chain - testCoin := WalletAmount{ - Address: user.DstChainAddress, - Denom: testDenom, + testCoinSrc := WalletAmount{ + Address: srcUser.DstChainAddress, + Denom: testDenomSrc, Amount: 1000000, } // send ibc transfer from the user wallet using its fullnode // timeout is nil so that it will use the default timeout - txHash, err := srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, nil) + srcTxHash, err := srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, srcUser.KeyName, testCoinSrc, nil) if err != nil { return err } @@ -92,31 +65,106 @@ func (ibc IBCTestCase) RelayPacketTest(testName string, srcChain Chain, dstChain } // fetch ibc transfer tx - srcTx, err := srcChain.GetTransaction(ctx, txHash) + srcTx, err := srcChain.GetTransaction(ctx, srcTxHash) if err != nil { return err } fmt.Printf("Transaction:\n%v\n", srcTx) - // query final balance of user wallet for src chain native denom on the src chain - srcFinalBalance, err := srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + // query final balance of src user wallet for src chain native denom on the src chain + srcFinalBalance, err := srcChain.GetBalance(ctx, srcUser.SrcChainAddress, testDenomSrc) if err != nil { return err } - // query final balance of user wallet for src chain native denom on the dst chain - dstFinalBalance, err := dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + // query final balance of src user wallet for src chain native denom on the dst chain + dstFinalBalance, err := dstChain.GetBalance(ctx, srcUser.DstChainAddress, dstIbcDenom) if err != nil { return err } - if srcFinalBalance != srcInitialBalance-testCoin.Amount { - return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-testCoin.Amount, srcFinalBalance) + totalFees := srcChain.GetGasFeesInNativeDenom(srcTx.GasWanted) + expectedDifference := testCoinSrc.Amount + totalFees + + if srcFinalBalance != srcInitialBalance-expectedDifference { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-expectedDifference, srcFinalBalance) } - if dstFinalBalance != dstInitialBalance+testCoin.Amount { - return fmt.Errorf("destination balances do not match. expected: %d, actual: %d", dstInitialBalance+testCoin.Amount, dstFinalBalance) + if dstFinalBalance != dstInitialBalance+testCoinSrc.Amount { + return fmt.Errorf("destination balances do not match. expected: %d, actual: %d", dstInitialBalance+testCoinSrc.Amount, dstFinalBalance) + } + + // Now relay from dst chain to src chain using dst user wallet + + // will test a user sending an ibc transfer from the dst chain to the src chain + // denom will be dst chain native denom + testDenomDst := dstChain.Config().Denom + + // query initial balance of dst user wallet for dst chain native denom on the dst chain + dstInitialBalance, err = dstChain.GetBalance(ctx, dstUser.DstChainAddress, testDenomDst) + if err != nil { + return err + } + + // get ibc denom for test denom on src chain + srcDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom(channels[0].PortID, channels[0].ChannelID, testDenomDst)) + srcIbcDenom := srcDenomTrace.IBCDenom() + + // query initial balance of user wallet for src chain native denom on the dst chain + // don't care about error here, account does not exist on destination chain + srcInitialBalance, _ = srcChain.GetBalance(ctx, dstUser.SrcChainAddress, srcIbcDenom) + + fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) + + // test coin, address is recipient of ibc transfer on src chain + testCoinDst := WalletAmount{ + Address: dstUser.SrcChainAddress, + Denom: testDenomDst, + Amount: 1000000, + } + + // send ibc transfer from the dst user wallet using its fullnode + // timeout is nil so that it will use the default timeout + dstTxHash, err := dstChain.SendIBCTransfer(ctx, channels[0].Counterparty.ChannelID, dstUser.KeyName, testCoinDst, nil) + if err != nil { + return err + } + + // wait for both chains to produce 10 blocks + if err := WaitForBlocks(srcChain, dstChain, 10); err != nil { + return err + } + + // fetch ibc transfer tx + dstTx, err := dstChain.GetTransaction(ctx, dstTxHash) + if err != nil { + return err + } + + fmt.Printf("Transaction:\n%v\n", dstTx) + + // query final balance of dst user wallet for dst chain native denom on the dst chain + dstFinalBalance, err = dstChain.GetBalance(ctx, dstUser.DstChainAddress, testDenomDst) + if err != nil { + return err + } + + // query final balance of dst user wallet for dst chain native denom on the src chain + srcFinalBalance, err = srcChain.GetBalance(ctx, dstUser.SrcChainAddress, srcIbcDenom) + if err != nil { + return err + } + + totalFeesDst := dstChain.GetGasFeesInNativeDenom(dstTx.GasWanted) + expectedDifference = testCoinDst.Amount + totalFeesDst + + if dstFinalBalance != dstInitialBalance-expectedDifference { + return fmt.Errorf("destination balances do not match. expected: %d, actual: %d", dstInitialBalance-expectedDifference, dstFinalBalance) + } + + if srcFinalBalance != srcInitialBalance+testCoinDst.Amount { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance+testCoinDst.Amount, srcFinalBalance) } return nil @@ -137,9 +185,9 @@ func (ibc IBCTestCase) RelayPacketTestNoTimeout(testName string, srcChain Chain, var testCoin WalletAmount // Query user account balances on both chains and send IBC transfer before starting the relayer - preRelayerStart := func(channels []ChannelOutput, user User) error { + preRelayerStart := func(channels []ChannelOutput, srcUser User, dstUser User) error { var err error - srcInitialBalance, err = srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + srcInitialBalance, err = srcChain.GetBalance(ctx, srcUser.SrcChainAddress, testDenom) if err != nil { return err } @@ -149,23 +197,23 @@ func (ibc IBCTestCase) RelayPacketTestNoTimeout(testName string, srcChain Chain, dstIbcDenom = denomTrace.IBCDenom() // don't care about error here, account does not exist on destination chain - dstInitialBalance, _ = dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + dstInitialBalance, _ = dstChain.GetBalance(ctx, srcUser.DstChainAddress, dstIbcDenom) fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) testCoin = WalletAmount{ - Address: user.DstChainAddress, + Address: srcUser.DstChainAddress, Denom: testDenom, Amount: 1000000, } // send ibc transfer with both timeouts disabled - txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, &IBCTimeout{Height: 0, NanoSeconds: 0}) + txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, srcUser.KeyName, testCoin, &IBCTimeout{Height: 0, NanoSeconds: 0}) return err } // Startup both chains and relayer - _, user, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) + _, _, user, _, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) if err != nil { return err } @@ -194,8 +242,11 @@ func (ibc IBCTestCase) RelayPacketTestNoTimeout(testName string, srcChain Chain, return err } - if srcFinalBalance != srcInitialBalance-testCoin.Amount { - return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-testCoin.Amount, srcFinalBalance) + totalFees := srcChain.GetGasFeesInNativeDenom(srcTx.GasWanted) + expectedDifference := testCoin.Amount + totalFees + + if srcFinalBalance != srcInitialBalance-expectedDifference { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-expectedDifference, srcFinalBalance) } if dstFinalBalance != dstInitialBalance+testCoin.Amount { @@ -219,9 +270,9 @@ func (ibc IBCTestCase) RelayPacketTestHeightTimeout(testName string, srcChain Ch var dstIbcDenom string // Query user account balances on both chains and send IBC transfer before starting the relayer - preRelayerStart := func(channels []ChannelOutput, user User) error { + preRelayerStart := func(channels []ChannelOutput, srcUser User, dstUser User) error { var err error - srcInitialBalance, err = srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + srcInitialBalance, err = srcChain.GetBalance(ctx, srcUser.SrcChainAddress, testDenom) if err != nil { return err } @@ -231,28 +282,29 @@ func (ibc IBCTestCase) RelayPacketTestHeightTimeout(testName string, srcChain Ch dstIbcDenom = denomTrace.IBCDenom() // don't care about error here, account does not exist on destination chain - dstInitialBalance, _ = dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + dstInitialBalance, _ = dstChain.GetBalance(ctx, srcUser.DstChainAddress, dstIbcDenom) fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) testCoin := WalletAmount{ - Address: user.DstChainAddress, + Address: srcUser.DstChainAddress, Denom: testDenom, Amount: 1000000, } // send ibc transfer with a timeout of 10 blocks from now on counterparty chain - txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, &IBCTimeout{Height: 10}) + txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, srcUser.KeyName, testCoin, &IBCTimeout{Height: 10}) if err != nil { return err } // wait until counterparty chain has passed the timeout - return dstChain.WaitForBlocks(11) + _, err = dstChain.WaitForBlocks(11) + return err } // Startup both chains and relayer - _, user, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) + _, _, user, _, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) if err != nil { return err } @@ -281,8 +333,10 @@ func (ibc IBCTestCase) RelayPacketTestHeightTimeout(testName string, srcChain Ch return err } - if srcFinalBalance != srcInitialBalance { - return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance, srcFinalBalance) + totalFees := srcChain.GetGasFeesInNativeDenom(srcTx.GasWanted) + + if srcFinalBalance != srcInitialBalance-totalFees { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-totalFees, srcFinalBalance) } if dstFinalBalance != dstInitialBalance { @@ -307,9 +361,9 @@ func (ibc IBCTestCase) RelayPacketTestTimestampTimeout(testName string, srcChain var dstIbcDenom string // Query user account balances on both chains and send IBC transfer before starting the relayer - preRelayerStart := func(channels []ChannelOutput, user User) error { + preRelayerStart := func(channels []ChannelOutput, srcUser User, dstUser User) error { var err error - srcInitialBalance, err = srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + srcInitialBalance, err = srcChain.GetBalance(ctx, srcUser.SrcChainAddress, testDenom) if err != nil { return err } @@ -319,18 +373,18 @@ func (ibc IBCTestCase) RelayPacketTestTimestampTimeout(testName string, srcChain dstIbcDenom = denomTrace.IBCDenom() // don't care about error here, account does not exist on destination chain - dstInitialBalance, _ = dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + dstInitialBalance, _ = dstChain.GetBalance(ctx, srcUser.DstChainAddress, dstIbcDenom) fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) testCoin := WalletAmount{ - Address: user.DstChainAddress, + Address: srcUser.DstChainAddress, Denom: testDenom, Amount: 1000000, } // send ibc transfer with a timeout of 10 blocks from now on counterparty chain - txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, &IBCTimeout{NanoSeconds: uint64((10 * time.Second).Nanoseconds())}) + txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, srcUser.KeyName, testCoin, &IBCTimeout{NanoSeconds: uint64((10 * time.Second).Nanoseconds())}) if err != nil { return err } @@ -342,7 +396,7 @@ func (ibc IBCTestCase) RelayPacketTestTimestampTimeout(testName string, srcChain } // Startup both chains and relayer - _, user, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) + _, _, user, _, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, preRelayerStart) if err != nil { return err } @@ -371,8 +425,10 @@ func (ibc IBCTestCase) RelayPacketTestTimestampTimeout(testName string, srcChain return err } - if srcFinalBalance != srcInitialBalance { - return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance, srcFinalBalance) + totalFees := srcChain.GetGasFeesInNativeDenom(srcTx.GasWanted) + + if srcFinalBalance != srcInitialBalance-totalFees { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-totalFees, srcFinalBalance) } if dstFinalBalance != dstInitialBalance { diff --git a/ibc/test_setup.go b/ibc/test_setup.go index 7f54790d4..371c2138f 100644 --- a/ibc/test_setup.go +++ b/ibc/test_setup.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "crypto/rand" + "errors" "fmt" "io/ioutil" "math/big" "net" "os" + "reflect" "strings" "time" @@ -32,6 +34,33 @@ const ( testPathName = "test-path" ) +// all methods on this struct have the same signature and are method names that will be called by the CLI +// func (ibc IBCTestCase) TestCaseName(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error +type IBCTestCase struct{} + +// uses reflection to get test case +func GetTestCase(testCase string) (func(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error, error) { + v := reflect.ValueOf(IBCTestCase{}) + m := v.MethodByName(testCase) + + if m.Kind() != reflect.Func { + return nil, fmt.Errorf("invalid test case: %s", testCase) + } + + testCaseFunc := func(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error { + args := []reflect.Value{reflect.ValueOf(testName), reflect.ValueOf(srcChain), reflect.ValueOf(dstChain), reflect.ValueOf(relayerImplementation)} + result := m.Call(args) + if len(result) != 1 || !result[0].CanInterface() { + return errors.New("error reflecting error return var") + } + + err, _ := result[0].Interface().(error) + return err + } + + return testCaseFunc, nil +} + // RandLowerCaseLetterString returns a lowercase letter string of given length func RandLowerCaseLetterString(length int) string { chars := []rune("abcdefghijklmnopqrstuvwxyz") @@ -83,8 +112,8 @@ func StartChainsAndRelayer( srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation, - preRelayerStart func(channels []ChannelOutput, user User) error, -) ([]ChannelOutput, User, func(), error) { + preRelayerStart func([]ChannelOutput, User, User) error, +) (Relayer, []ChannelOutput, *User, *User, func(), error) { var relayerImpl Relayer switch relayerImplementation { case CosmosRly: @@ -100,8 +129,8 @@ func StartChainsAndRelayer( // not yet supported } - errResponse := func(err error) ([]ChannelOutput, User, func(), error) { - return []ChannelOutput{}, User{}, nil, err + errResponse := func(err error) (Relayer, []ChannelOutput, *User, *User, func(), error) { + return nil, []ChannelOutput{}, nil, nil, nil, err } if err := srcChain.Initialize(testName, home, pool, networkID); err != nil { @@ -141,14 +170,14 @@ func StartChainsAndRelayer( } // Fund relayer account on src chain - srcWallet := WalletAmount{ + srcRelayerWalletAmount := WalletAmount{ Address: srcAccount, Denom: srcChainCfg.Denom, Amount: 10000000, } // Fund relayer account on dst chain - dstWallet := WalletAmount{ + dstRelayerWalletAmount := WalletAmount{ Address: dstAccount, Denom: dstChainCfg.Denom, Amount: 10000000, @@ -158,41 +187,74 @@ func StartChainsAndRelayer( if err := srcChain.CreateKey(ctx, userAccountKeyName); err != nil { return errResponse(err) } - userAccountAddressBytes, err := srcChain.GetAddress(userAccountKeyName) + + srcUserAccountAddressBytes, err := srcChain.GetAddress(userAccountKeyName) + if err != nil { + return errResponse(err) + } + + srcUserAccountSrc, err := types.Bech32ifyAddressBytes(srcChainCfg.Bech32Prefix, srcUserAccountAddressBytes) + if err != nil { + return errResponse(err) + } + + srcUserAccountDst, err := types.Bech32ifyAddressBytes(dstChainCfg.Bech32Prefix, srcUserAccountAddressBytes) + if err != nil { + return errResponse(err) + } + + if err := dstChain.CreateKey(ctx, userAccountKeyName); err != nil { + return errResponse(err) + } + + dstUserAccountAddressBytes, err := dstChain.GetAddress(userAccountKeyName) if err != nil { return errResponse(err) } - userAccountSrc, err := types.Bech32ifyAddressBytes(srcChainCfg.Bech32Prefix, userAccountAddressBytes) + dstUserAccountSrc, err := types.Bech32ifyAddressBytes(srcChainCfg.Bech32Prefix, dstUserAccountAddressBytes) if err != nil { return errResponse(err) } - userAccountDst, err := types.Bech32ifyAddressBytes(dstChainCfg.Bech32Prefix, userAccountAddressBytes) + dstUserAccountDst, err := types.Bech32ifyAddressBytes(dstChainCfg.Bech32Prefix, dstUserAccountAddressBytes) if err != nil { return errResponse(err) } - user := User{ + srcUser := User{ + KeyName: userAccountKeyName, + SrcChainAddress: srcUserAccountSrc, + DstChainAddress: srcUserAccountDst, + } + + dstUser := User{ KeyName: userAccountKeyName, - SrcChainAddress: userAccountSrc, - DstChainAddress: userAccountDst, + SrcChainAddress: dstUserAccountSrc, + DstChainAddress: dstUserAccountDst, } // Fund user account on src chain in order to relay from src to dst - userWalletSrc := WalletAmount{ - Address: userAccountSrc, + srcUserWalletAmount := WalletAmount{ + Address: srcUserAccountSrc, Denom: srcChainCfg.Denom, - Amount: 100000000, + Amount: 10000000000, + } + + // Fund user account on dst chain in order to relay from dst to src + dstUserWalletAmount := WalletAmount{ + Address: dstUserAccountDst, + Denom: dstChainCfg.Denom, + Amount: 10000000000, } // start chains from genesis, wait until they are producing blocks chainsGenesisWaitGroup := errgroup.Group{} chainsGenesisWaitGroup.Go(func() error { - return srcChain.Start(testName, ctx, []WalletAmount{srcWallet, userWalletSrc}) + return srcChain.Start(testName, ctx, []WalletAmount{srcRelayerWalletAmount, srcUserWalletAmount}) }) chainsGenesisWaitGroup.Go(func() error { - return dstChain.Start(testName, ctx, []WalletAmount{dstWallet}) + return dstChain.Start(testName, ctx, []WalletAmount{dstRelayerWalletAmount, dstUserWalletAmount}) }) if err := chainsGenesisWaitGroup.Wait(); err != nil { @@ -212,7 +274,7 @@ func StartChainsAndRelayer( } if preRelayerStart != nil { - if err := preRelayerStart(channels, user); err != nil { + if err := preRelayerStart(channels, srcUser, dstUser); err != nil { return errResponse(err) } } @@ -231,16 +293,18 @@ func StartChainsAndRelayer( } } - return channels, user, relayerCleanup, nil + return relayerImpl, channels, &srcUser, &dstUser, relayerCleanup, nil } func WaitForBlocks(srcChain Chain, dstChain Chain, blocksToWait int64) error { chainsConsecutiveBlocksWaitGroup := errgroup.Group{} - chainsConsecutiveBlocksWaitGroup.Go(func() error { - return srcChain.WaitForBlocks(blocksToWait) + chainsConsecutiveBlocksWaitGroup.Go(func() (err error) { + _, err = srcChain.WaitForBlocks(blocksToWait) + return }) - chainsConsecutiveBlocksWaitGroup.Go(func() error { - return dstChain.WaitForBlocks(blocksToWait) + chainsConsecutiveBlocksWaitGroup.Go(func() (err error) { + _, err = dstChain.WaitForBlocks(blocksToWait) + return }) return chainsConsecutiveBlocksWaitGroup.Wait() } @@ -278,6 +342,7 @@ func CreateTestNetwork(pool *dockertest.Pool, name string, testName string) (*do // Cleanup will clean up Docker containers, networks, and the other various config files generated in testing func Cleanup(testName string, pool *dockertest.Pool, testDir string) func() { return func() { + showContainerLogs := os.Getenv("SHOW_CONTAINER_LOGS") cont, _ := pool.Client.ListContainers(docker.ListContainersOptions{All: true}) ctx := context.Background() for _, c := range cont { @@ -289,7 +354,9 @@ func Cleanup(testName string, pool *dockertest.Pool, testDir string) func() { stderr := new(bytes.Buffer) _ = pool.Client.Logs(docker.LogsOptions{Context: ctx, Container: c.ID, OutputStream: stdout, ErrorStream: stderr, Stdout: true, Stderr: true, Tail: "50", Follow: false, Timestamps: false}) names := strings.Join(c.Names, ",") - fmt.Printf("{%s} - stdout:\n%s\n{%s} - stderr:\n%s\n", names, stdout, names, stderr) + if showContainerLogs != "" { + fmt.Printf("{%s} - stdout:\n%s\n{%s} - stderr:\n%s\n", names, stdout, names, stderr) + } _ = pool.Client.RemoveContainer(docker.RemoveContainerOptions{ID: c.ID}) } } diff --git a/trophies/test_juno.go b/trophies/test_juno.go new file mode 100644 index 000000000..c26ca2865 --- /dev/null +++ b/trophies/test_juno.go @@ -0,0 +1,410 @@ +//go:build exclude + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" + "github.com/ory/dockertest/docker" +) + +func (ibc IBCTestCase) JunoHaltTest(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error { + ctx, home, pool, network, cleanup, err := SetupTestRun(testName) + if err != nil { + return err + } + defer cleanup() + + if err := srcChain.Initialize(testName, home, pool, network); err != nil { + return err + } + + srcChainCfg := srcChain.Config() + + // Generate key to be used for "user" that will execute transactions + if err := srcChain.CreateKey(ctx, userAccountKeyName); err != nil { + return err + } + + userAccountAddressBytes, err := srcChain.GetAddress(userAccountKeyName) + if err != nil { + return err + } + + userAccountSrc, err := types.Bech32ifyAddressBytes(srcChainCfg.Bech32Prefix, userAccountAddressBytes) + if err != nil { + return err + } + + // Fund user account on src chain that will be used to instantiate and execute contract + userWalletSrc := WalletAmount{ + Address: userAccountSrc, + Denom: srcChainCfg.Denom, + Amount: 100000000000, + } + + if err := srcChain.Start(testName, ctx, []WalletAmount{userWalletSrc}); err != nil { + return err + } + + executablePath, err := os.Executable() + if err != nil { + return err + } + rootPath := filepath.Dir(executablePath) + contractPath := path.Join(rootPath, "assets", "badcontract.wasm") + + contractAddress, err := srcChain.InstantiateContract(ctx, userAccountKeyName, WalletAmount{Amount: 100, Denom: srcChain.Config().Denom}, contractPath, "{\"count\":0}", srcChainCfg.Version == "v2.3.0") + if err != nil { + return err + } + + resets := []int{0, 15, 84, 0, 84, 42, 55, 42, 15, 84, 42} + + for _, resetCount := range resets { + // run reset + if err := srcChain.ExecuteContract(ctx, userAccountKeyName, contractAddress, fmt.Sprintf("{\"reset\":{\"count\": %d}}", resetCount)); err != nil { + return err + } + latestHeight, err := srcChain.WaitForBlocks(5) + if err != nil { + return err + } + + // dump current contract state + res, err := srcChain.DumpContractState(ctx, contractAddress, latestHeight) + if err != nil { + return err + } + contractData, err := base64.StdEncoding.DecodeString(res.Models[1].Value) + if err != nil { + return err + } + fmt.Printf("Contract data: %s\n", contractData) + + // run increment a bunch of times + for i := 0; i < 5; i++ { + if err := srcChain.ExecuteContract(ctx, userAccountKeyName, contractAddress, "{\"increment\":{}}"); err != nil { + return err + } + if _, err := srcChain.WaitForBlocks(1); err != nil { + return err + } + } + } + + return nil +} + +func (ibc IBCTestCase) JunoPostHaltGenesis(testName string, srcChain Chain, dstChain Chain, relayerImplementation RelayerImplementation) error { + ctx, home, pool, network, cleanup, err := SetupTestRun(testName) + if err != nil { + return err + } + defer cleanup() + + if err := srcChain.Initialize(testName, home, pool, network); err != nil { + return err + } + + executablePath, err := os.Executable() + if err != nil { + return err + } + rootPath := filepath.Dir(executablePath) + genesisFilePath := path.Join(rootPath, "assets", "juno-1-96.json") + + if err := srcChain.StartWithGenesisFile(testName, ctx, home, pool, network, genesisFilePath); err != nil { + return err + } + + _, err = srcChain.WaitForBlocks(20) + return err +} + +func (ibc IBCTestCase) JunoHaltNewGenesis(testName string, _ Chain, _ Chain, relayerImplementation RelayerImplementation) error { + ctx, home, pool, network, cleanup, err := SetupTestRun(testName) + if err != nil { + return err + } + defer cleanup() + + // overriding input vars + srcChain, err := GetChain(testName, "juno", "v2.1.0", "juno-1", 10, 1) + if err != nil { + return err + } + + dstChain, err := GetChain(testName, "osmosis", "v7.1.0", "osmosis-1", 4, 0) + if err != nil { + return err + } + + // startup both chains and relayer + // creates wallets in the relayer for src and dst chain + // funds relayer src and dst wallets on respective chain in genesis + // creates a user account on the src chain (separate fullnode) + // funds user account on src chain in genesis + relayer, channels, user, rlyCleanup, err := StartChainsAndRelayer(testName, ctx, pool, network, home, srcChain, dstChain, relayerImplementation, nil) + if err != nil { + return err + } + defer rlyCleanup() + + // will test a user sending an ibc transfer from the src chain to the dst chain + // denom will be src chain native denom + testDenom := srcChain.Config().Denom + + // query initial balance of user wallet for src chain native denom on the src chain + srcInitialBalance, err := srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + if err != nil { + return err + } + + // get ibc denom for test denom on dst chain + denomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom(channels[0].Counterparty.PortID, channels[0].Counterparty.ChannelID, testDenom)) + dstIbcDenom := denomTrace.IBCDenom() + + // query initial balance of user wallet for src chain native denom on the dst chain + // don't care about error here, account does not exist on destination chain + dstInitialBalance, _ := dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + + fmt.Printf("Initial balances: Src chain: %d\nDst chain: %d\n", srcInitialBalance, dstInitialBalance) + + // test coin, address is recipient of ibc transfer on dst chain + testCoin := WalletAmount{ + Address: user.DstChainAddress, + Denom: testDenom, + Amount: 1000000, + } + + // send ibc transfer from the user wallet using its fullnode + // timeout is nil so that it will use the default timeout + txHash, err := srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, nil) + if err != nil { + return err + } + + // wait for both chains to produce 10 blocks + if err := WaitForBlocks(srcChain, dstChain, 10); err != nil { + return err + } + + // fetch ibc transfer tx + srcTx, err := srcChain.GetTransaction(ctx, txHash) + if err != nil { + return err + } + + fmt.Printf("Transaction:\n%v\n", srcTx) + + // query final balance of user wallet for src chain native denom on the src chain + srcFinalBalance, err := srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + if err != nil { + return err + } + + // query final balance of user wallet for src chain native denom on the dst chain + dstFinalBalance, err := dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + if err != nil { + return err + } + + fmt.Printf("First balance check: Source: %d, Destination: %d\n", srcFinalBalance, dstFinalBalance) + + totalFees := srcChain.GetGasFeesInNativeDenom(srcTx.GasWanted) + expectedDifference := testCoin.Amount + totalFees + + if srcFinalBalance != srcInitialBalance-expectedDifference { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcInitialBalance-expectedDifference, srcFinalBalance) + } + + if dstFinalBalance != dstInitialBalance+testCoin.Amount { + return fmt.Errorf("destination balances do not match. expected: %d, actual: %d", dstInitialBalance+testCoin.Amount, dstFinalBalance) + } + + // IBC is confirmed working on 2.1.0, now use bad contract to halt chain + + executablePath, err := os.Executable() + if err != nil { + return err + } + rootPath := filepath.Dir(executablePath) + contractPath := path.Join(rootPath, "assets", "badcontract.wasm") + + contractAddress, err := srcChain.InstantiateContract(ctx, userAccountKeyName, WalletAmount{Amount: 100, Denom: srcChain.Config().Denom}, contractPath, "{\"count\":0}", false) + if err != nil { + return err + } + + resets := []int{0, 15, 84, 0, 84, 42, 55, 42, 15, 84, 42} + + for _, resetCount := range resets { + // run reset + if err := srcChain.ExecuteContract(ctx, userAccountKeyName, contractAddress, fmt.Sprintf("{\"reset\":{\"count\": %d}}", resetCount)); err != nil { + return err + } + // halt happens here on the first 42 reset + latestHeight, err := srcChain.WaitForBlocks(5) + if err != nil { + fmt.Println("Chain is halted") + break + } + + // dump current contract state + res, err := srcChain.DumpContractState(ctx, contractAddress, latestHeight) + if err != nil { + return err + } + contractData, err := base64.StdEncoding.DecodeString(res.Models[1].Value) + if err != nil { + return err + } + fmt.Printf("Contract data: %s\n", contractData) + + // run increment a bunch of times. + // Actual mainnet halt included this, but this test shows they are not necessary to cause halt + // for i := 0; i < 5; i++ { + // if err := srcChain.ExecuteContract(ctx, userAccountKeyName, contractAddress, "{\"increment\":{}}"); err != nil { + // return err + // } + // if _, err := srcChain.WaitForBlocks(1); err != nil { + // return err + // } + // } + } + + haltHeight, err := srcChain.Height() + if err != nil { + return err + } + + junoChainAsCosmosChain := srcChain.(*CosmosChain) + + // stop juno chain (2/3 consensus and user node) and relayer + for i := 3; i < len(junoChainAsCosmosChain.ChainNodes); i++ { + node := junoChainAsCosmosChain.ChainNodes[i] + if err := node.StopContainer(); err != nil { + return nil + } + _ = node.Pool.Client.RemoveContainer(docker.RemoveContainerOptions{ID: node.Container.ID}) + } + + // relayer should be stopped by now, but just in case + _ = relayer.StopRelayer(ctx) + + // export state from first validator + newGenesisJson, err := srcChain.ExportState(ctx, haltHeight) + if err != nil { + return err + } + + fmt.Printf("New genesis json: %s\n", newGenesisJson) + + newGenesisJson = strings.ReplaceAll(newGenesisJson, fmt.Sprintf("\"initial_height\":%d", 0), fmt.Sprintf("\"initial_height\":%d", haltHeight+2)) + + juno3Chain, err := GetChain(testName, "juno", "v3.0.0", "juno-1", 10, 1) + if err != nil { + return err + } + + // write modified genesis file to 2/3 vals and fullnode + for i := 3; i < len(junoChainAsCosmosChain.ChainNodes); i++ { + if err := junoChainAsCosmosChain.ChainNodes[i].UnsafeResetAll(ctx); err != nil { + return err + } + if err := ioutil.WriteFile(junoChainAsCosmosChain.ChainNodes[i].GenesisFilePath(), []byte(newGenesisJson), 0644); err != nil { + return err + } + junoChainAsCosmosChain.ChainNodes[i].Chain = juno3Chain + if err := junoChainAsCosmosChain.ChainNodes[i].UnsafeResetAll(ctx); err != nil { + return err + } + } + + if err := junoChainAsCosmosChain.ChainNodes.LogGenesisHashes(); err != nil { + return err + } + + for i := 3; i < len(junoChainAsCosmosChain.ChainNodes); i++ { + node := junoChainAsCosmosChain.ChainNodes[i] + if err := node.CreateNodeContainer(); err != nil { + return err + } + if err := node.StartContainer(ctx); err != nil { + return nil + } + } + + time.Sleep(1 * time.Minute) + + if _, err = srcChain.WaitForBlocks(5); err != nil { + return err + } + + // check IBC again + // note: this requires relayer version with hack to use old RPC for blocks before the halt, and new RPC for blocks after new genesis + if err = relayer.UpdateClients(ctx, testPathName); err != nil { + return err + } + + if err = relayer.StartRelayer(ctx, testPathName); err != nil { + return err + } + + // wait for relayer to start up + time.Sleep(60 * time.Second) + + // send ibc transfer from the user wallet using its fullnode + // timeout is nil so that it will use the default timeout + txHash, err = srcChain.SendIBCTransfer(ctx, channels[0].ChannelID, user.KeyName, testCoin, nil) + if err != nil { + return err + } + + // wait for both chains to produce 10 blocks + if err := WaitForBlocks(srcChain, dstChain, 10); err != nil { + return err + } + + // fetch ibc transfer tx + srcTx2, err := srcChain.GetTransaction(ctx, txHash) + if err != nil { + return err + } + + fmt.Printf("Transaction:\n%v\n", srcTx2) + + // query final balance of user wallet for src chain native denom on the src chain + srcFinalBalance2, err := srcChain.GetBalance(ctx, user.SrcChainAddress, testDenom) + if err != nil { + return err + } + + // query final balance of user wallet for src chain native denom on the dst chain + dstFinalBalance2, err := dstChain.GetBalance(ctx, user.DstChainAddress, dstIbcDenom) + if err != nil { + return err + } + + totalFees = srcChain.GetGasFeesInNativeDenom(srcTx2.GasWanted) + expectedDifference = testCoin.Amount + totalFees + + if srcFinalBalance2 != srcFinalBalance-expectedDifference { + return fmt.Errorf("source balances do not match. expected: %d, actual: %d", srcFinalBalance-expectedDifference, srcFinalBalance2) + } + + if dstFinalBalance2 != dstFinalBalance+testCoin.Amount { + return fmt.Errorf("destination balances do not match. expected: %d, actual: %d", dstFinalBalance+testCoin.Amount, dstFinalBalance2) + } + + return nil + +}