diff --git a/tests/systemtests/cli.go b/tests/systemtests/cli.go index 486e1c203a..ae0ee0e9ac 100644 --- a/tests/systemtests/cli.go +++ b/tests/systemtests/cli.go @@ -15,11 +15,6 @@ import ( "github.com/tidwall/gjson" "golang.org/x/exp/slices" - "github.com/cosmos/cosmos-sdk/client/grpc/cmtservice" - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/std" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -287,7 +282,7 @@ func (c CLIWrapper) AddKeyFromSeed(name, mnemoic string) string { return addr } -// GetKeyAddr returns address +// GetKeyAddr returns Acc address func (c CLIWrapper) GetKeyAddr(name string) string { cmd := c.withKeyringFlags("keys", "show", name, "-a") out, _ := c.run(cmd) @@ -296,6 +291,15 @@ func (c CLIWrapper) GetKeyAddr(name string) string { return addr } +// GetKeyAddrPrefix returns key address with Beach32 prefix encoding for a key (acc|val|cons) +func (c CLIWrapper) GetKeyAddrPrefix(name, prefix string) string { + cmd := c.withKeyringFlags("keys", "show", name, "-a", "--bech="+prefix) + out, _ := c.run(cmd) + addr := strings.Trim(out, "\n") + require.NotEmpty(c.t, addr, "got %q", out) + return addr +} + const defaultSrcAddr = "node0" // FundAddress sends the token amount to the destination address @@ -330,33 +334,6 @@ func (c CLIWrapper) QueryTotalSupply(denom string) int64 { return gjson.Get(raw, fmt.Sprintf("supply.#(denom==%q).amount", denom)).Int() } -func (c CLIWrapper) GetCometBFTValidatorSet() cmtservice.GetLatestValidatorSetResponse { - args := []string{"q", "comet-validator-set"} - got := c.CustomQuery(args...) - - // still using amino here as the SDK - amino := codec.NewLegacyAmino() - std.RegisterLegacyAminoCodec(amino) - std.RegisterInterfaces(codectypes.NewInterfaceRegistry()) - - var res cmtservice.GetLatestValidatorSetResponse - require.NoError(c.t, amino.UnmarshalJSON([]byte(got), &res), got) - return res -} - -// IsInCometBftValset returns true when the given pub key is in the current active tendermint validator set -func (c CLIWrapper) IsInCometBftValset(valPubKey cryptotypes.PubKey) (cmtservice.GetLatestValidatorSetResponse, bool) { - valResult := c.GetCometBFTValidatorSet() - var found bool - for _, v := range valResult.Validators { - if v.PubKey.Equal(valPubKey) { - found = true - break - } - } - return valResult, found -} - // SubmitGovProposal submit a gov v1 proposal func (c CLIWrapper) SubmitGovProposal(proposalJson string, args ...string) string { if len(args) == 0 { diff --git a/tests/systemtests/fraud_test.go b/tests/systemtests/fraud_test.go new file mode 100644 index 0000000000..675f3969e4 --- /dev/null +++ b/tests/systemtests/fraud_test.go @@ -0,0 +1,62 @@ +//go:build system_test + +package systemtests + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) + +func TestValidatorDoubleSign(t *testing.T) { + // Scenario: + // given: a running chain + // when: a second instance with the same val key signs a block + // then: the validator is removed from the active set and jailed forever + sut.ResetChain(t) + cli := NewCLIWrapper(t, sut, verbose) + sut.StartChain(t) + + // Check the validator is in the active set + rsp := cli.CustomQuery("q", "staking", "validators") + t.Log(rsp) + nodePowerBefore := QueryCometValidatorPowerForNode(t, sut, 0) + require.NotEmpty(t, nodePowerBefore) + + var validatorPubKey cryptotypes.PubKey + newNode := sut.AddFullnode(t, "0.001stake", func(nodeNumber int, nodePath string) { + valKeyFile := filepath.Join(WorkDir, nodePath, "config", "priv_validator_key.json") + _ = os.Remove(valKeyFile) + _, err := copyFile(filepath.Join(WorkDir, sut.nodePath(0), "config", "priv_validator_key.json"), valKeyFile) + require.NoError(t, err) + validatorPubKey = LoadValidatorPubKeyForNode(t, sut, nodeNumber) + }) + sut.AwaitNodeUp(t, fmt.Sprintf("http://%s:%d", newNode.IP, newNode.RPCPort)) + + // let's wait some blocks to have evidence and update persisted + rpc := sut.RPCClient(t) + pkBz := validatorPubKey.Bytes() + for i := 0; i < 20; i++ { + sut.AwaitNextBlock(t) + if QueryCometValidatorPower(rpc, pkBz) == 0 { + break + } + } + sut.AwaitNextBlock(t) + + // then comet status updated + nodePowerAfter := QueryCometValidatorPowerForNode(t, sut, 0) + require.Empty(t, nodePowerAfter) + + // and sdk status updated + byzantineOperatorAddr := cli.GetKeyAddrPrefix("node0", "val") + rsp = cli.CustomQuery("q", "staking", "validator", byzantineOperatorAddr) + assert.True(t, gjson.Get(rsp, "validator.jailed").Bool(), rsp) +} diff --git a/tests/systemtests/node_utils.go b/tests/systemtests/node_utils.go new file mode 100644 index 0000000000..f503a0f390 --- /dev/null +++ b/tests/systemtests/node_utils.go @@ -0,0 +1,46 @@ +package systemtests + +import ( + "bytes" + "path/filepath" + "testing" + + "github.com/cometbft/cometbft/privval" + "github.com/stretchr/testify/require" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" +) + +// LoadValidatorPubKeyForNode load validator nodes consensus pub key for given node number +func LoadValidatorPubKeyForNode(t *testing.T, sut *SystemUnderTest, nodeNumber int) cryptotypes.PubKey { + t.Helper() + return LoadValidatorPubKey(t, filepath.Join(WorkDir, sut.nodePath(nodeNumber), "config", "priv_validator_key.json")) +} + +// LoadValidatorPubKey load validator nodes consensus pub key from disk +func LoadValidatorPubKey(t *testing.T, keyFile string) cryptotypes.PubKey { + t.Helper() + filePV := privval.LoadFilePVEmptyState(keyFile, "") + pubKey, err := filePV.GetPubKey() + require.NoError(t, err) + valPubKey, err := cryptocodec.FromCmtPubKeyInterface(pubKey) + require.NoError(t, err) + return valPubKey +} + +// QueryCometValidatorPowerForNode returns the validator's power from tendermint RPC endpoint. 0 when not found +func QueryCometValidatorPowerForNode(t *testing.T, sut *SystemUnderTest, nodeNumber int) int64 { + t.Helper() + pubKebBz := LoadValidatorPubKeyForNode(t, sut, nodeNumber).Bytes() + return QueryCometValidatorPower(sut.RPCClient(t), pubKebBz) +} + +func QueryCometValidatorPower(c RPCClient, pubKebBz []byte) int64 { + for _, v := range c.Validators() { + if bytes.Equal(v.PubKey.Bytes(), pubKebBz) { + return v.VotingPower + } + } + return 0 +} diff --git a/tests/systemtests/system.go b/tests/systemtests/system.go index a9aec879a7..355854cfdd 100644 --- a/tests/systemtests/system.go +++ b/tests/systemtests/system.go @@ -154,7 +154,7 @@ func (s *SystemUnderTest) StartChain(t *testing.T, xargs ...string) { t.Helper() s.Log("Start chain\n") s.ChainStarted = true - s.startNodesAsync(t, append([]string{"start", "--trace", "--log_level=info"}, xargs...)...) + s.startNodesAsync(t, append([]string{"start", "--trace", "--log_level=info", "--log_no_color"}, xargs...)...) s.AwaitNodeUp(t, s.rpcAddr) @@ -635,9 +635,12 @@ func AllNodes(t *testing.T, s *SystemUnderTest) []Node { t.Helper() result := make([]Node, s.nodesCount) outs := s.ForEachNodeExecAndWait(t, []string{"comet", "show-node-id"}) - ip, err := server.ExternalIP() - require.NoError(t, err) - + ip := "127.0.0.1" + if false { // is there still a use case for external ip? + var err error + ip, err = server.ExternalIP() + require.NoError(t, err) + } for i, out := range outs { result[i] = Node{ ID: strings.TrimSpace(out[0]), @@ -655,7 +658,7 @@ func (s *SystemUnderTest) resetBuffers() { } // AddFullnode starts a new fullnode that connects to the existing chain but is not a validator. -func (s *SystemUnderTest) AddFullnode(t *testing.T, beforeStart ...func(nodeNumber int, nodePath string)) Node { +func (s *SystemUnderTest) AddFullnode(t *testing.T, minGasPrices string, beforeStart ...func(nodeNumber int, nodePath string)) Node { t.Helper() s.MarkDirty() s.nodesCount++ @@ -698,9 +701,11 @@ func (s *SystemUnderTest) AddFullnode(t *testing.T, beforeStart ...func(nodeNumb fmt.Sprintf("--p2p.laddr=tcp://localhost:%d", node.P2PPort), fmt.Sprintf("--rpc.laddr=tcp://localhost:%d", node.RPCPort), fmt.Sprintf("--grpc.address=localhost:%d", 9090+nodeNumber), - fmt.Sprintf("--grpc-web.address=localhost:%d", 8090+nodeNumber), + fmt.Sprintf("--minimum-gas-prices=%s", minGasPrices), + "--p2p.pex=false", "--moniker=" + moniker, "--log_level=info", + "--log_no_color", "--home", nodePath, } s.Logf("Execute `%s %s`\n", s.execBinary, strings.Join(args, " "))