diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 290d70818..84bd10ce9 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -14,13 +14,14 @@ program](https://hackerone.com/tendermint). - Apps - Go API -- [libs] \#3811 Remove `db` from libs in favor of `https://github.com/tendermint/tm-cmn` + - [libs] \#3811 Remove `db` from libs in favor of `https://github.com/tendermint/tm-cmn` ### FEATURES: ### IMPROVEMENTS: - [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) +- [rpc] \#2252 Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence - [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable - [p2p] \#3664 p2p/conn: reuse buffer when write/read from secret connection diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 71d0620e1..feb81b35c 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -115,6 +115,7 @@ func (app *KVStoreApplication) Commit() types.ResponseCommit { return types.ResponseCommit{Data: appHash} } +// Returns an associated value or nil if missing. func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { if reqQuery.Prove { value := app.state.db.Get(prefixKey(reqQuery.Data)) diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index 68269dceb..308100b6a 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -9,7 +9,9 @@ import ( "github.com/tendermint/tendermint/abci/example/code" "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-cmn/db" ) @@ -27,6 +29,8 @@ type PersistentKVStoreApplication struct { // validator set ValUpdates []types.ValidatorUpdate + valAddrToPubKeyMap map[string]types.PubKey + logger log.Logger } @@ -40,8 +44,9 @@ func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication state := loadState(db) return &PersistentKVStoreApplication{ - app: &KVStoreApplication{state: state}, - logger: log.NewNopLogger(), + app: &KVStoreApplication{state: state}, + valAddrToPubKeyMap: make(map[string]types.PubKey), + logger: log.NewNopLogger(), } } @@ -83,8 +88,20 @@ func (app *PersistentKVStoreApplication) Commit() types.ResponseCommit { return app.app.Commit() } -func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { - return app.app.Query(reqQuery) +// When path=/val and data={validator address}, returns the validator update (types.ValidatorUpdate) varint encoded. +// For any other path, returns an associated value or nil if missing. +func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + switch reqQuery.Path { + case "/val": + key := []byte("val:" + string(reqQuery.Data)) + value := app.app.state.db.Get(key) + + resQuery.Key = reqQuery.Data + resQuery.Value = value + return + default: + return app.app.Query(reqQuery) + } } // Save the validators in the merkle tree @@ -102,6 +119,20 @@ func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) t func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { // reset valset changes app.ValUpdates = make([]types.ValidatorUpdate, 0) + + for _, ev := range req.ByzantineValidators { + switch ev.Type { + case tmtypes.ABCIEvidenceTypeDuplicateVote: + // decrease voting power by 1 + if ev.TotalVotingPower == 0 { + continue + } + app.updateValidator(types.ValidatorUpdate{ + PubKey: app.valAddrToPubKeyMap[string(ev.Validator.Address)], + Power: ev.TotalVotingPower - 1, + }) + } + } return types.ResponseBeginBlock{} } @@ -174,6 +205,10 @@ func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.Respon // add, update, or remove a validator func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate) types.ResponseDeliverTx { key := []byte("val:" + string(v.PubKey.Data)) + + pubkey := ed25519.PubKeyEd25519{} + copy(pubkey[:], v.PubKey.Data) + if v.Power == 0 { // remove validator if !app.app.state.db.Has(key) { @@ -183,6 +218,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)} } app.app.state.db.Delete(key) + delete(app.valAddrToPubKeyMap, string(pubkey.Address())) } else { // add or update validator value := bytes.NewBuffer(make([]byte, 0)) @@ -192,6 +228,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Error encoding validator: %v", err)} } app.app.state.db.Set(key, value.Bytes()) + app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey } // we only update the changes array if we successfully updated the tree diff --git a/rpc/client/amino.go b/rpc/client/amino.go new file mode 100644 index 000000000..ef1a00ec4 --- /dev/null +++ b/rpc/client/amino.go @@ -0,0 +1,12 @@ +package client + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + types.RegisterEvidences(cdc) +} diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 3fd13da37..85f065b61 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -333,6 +333,15 @@ func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, err return result, nil } +func (c *baseRPCClient) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + result := new(ctypes.ResultBroadcastEvidence) + _, err := c.caller.Call("broadcast_evidence", map[string]interface{}{"evidence": ev}, result) + if err != nil { + return nil, errors.Wrap(err, "BroadcastEvidence") + } + return result, nil +} + //----------------------------------------------------------------------------- // WSEvents diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 8f9ed9372..383e0b480 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -28,9 +28,24 @@ import ( "github.com/tendermint/tendermint/types" ) -// ABCIClient groups together the functionality that principally -// affects the ABCI app. In many cases this will be all we want, -// so we can accept an interface which is easier to mock +// Client wraps most important rpc calls a client would make if you want to +// listen for events, test if it also implements events.EventSwitch. +type Client interface { + cmn.Service + ABCIClient + EventsClient + HistoryClient + NetworkClient + SignClient + StatusClient + EvidenceClient +} + +// ABCIClient groups together the functionality that principally affects the +// ABCI app. +// +// In many cases this will be all we want, so we can accept an interface which +// is easier to mock. type ABCIClient interface { // Reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) @@ -44,8 +59,8 @@ type ABCIClient interface { BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) } -// SignClient groups together the interfaces need to get valid -// signatures and prove anything about the chain +// SignClient groups together the functionality needed to get valid signatures +// and prove anything about the chain. type SignClient interface { Block(height *int64) (*ctypes.ResultBlock, error) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) @@ -55,32 +70,19 @@ type SignClient interface { TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) } -// HistoryClient shows us data from genesis to now in large chunks. +// HistoryClient provides access to data from genesis to now in large chunks. type HistoryClient interface { Genesis() (*ctypes.ResultGenesis, error) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) } +// StatusClient provides access to general chain info. type StatusClient interface { - // General chain info Status() (*ctypes.ResultStatus, error) } -// Client wraps most important rpc calls a client would make -// if you want to listen for events, test if it also -// implements events.EventSwitch -type Client interface { - cmn.Service - ABCIClient - EventsClient - HistoryClient - NetworkClient - SignClient - StatusClient -} - -// NetworkClient is general info about the network state. May not -// be needed usually. +// NetworkClient is general info about the network state. May not be needed +// usually. type NetworkClient interface { NetInfo() (*ctypes.ResultNetInfo, error) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) @@ -110,3 +112,9 @@ type MempoolClient interface { UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) } + +// EvidenceClient is used for submitting an evidence of the malicious +// behaviour. +type EvidenceClient interface { + BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) +} diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 161f44fdf..3c3a1dcc6 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -157,6 +157,10 @@ func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.R return core.TxSearch(c.ctx, query, prove, page, perPage) } +func (c *Local) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(c.ctx, ev) +} + func (c *Local) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) { q, err := tmquery.New(query) if err != nil { diff --git a/rpc/client/main_test.go b/rpc/client/main_test.go index 6ec7b7b0e..d600b32f8 100644 --- a/rpc/client/main_test.go +++ b/rpc/client/main_test.go @@ -1,6 +1,7 @@ package client_test import ( + "io/ioutil" "os" "testing" @@ -13,7 +14,11 @@ var node *nm.Node func TestMain(m *testing.M) { // start a tendermint node (and kvstore) in the background to test against - app := kvstore.NewKVStoreApplication() + dir, err := ioutil.TempDir("/tmp", "rpc-client-test") + if err != nil { + panic(err) + } + app := kvstore.NewPersistentKVStoreApplication(dir) node = rpctest.StartTendermint(app) code := m.Run() diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go index c2e19b6d4..3ec40d6cc 100644 --- a/rpc/client/mock/client.go +++ b/rpc/client/mock/client.go @@ -36,6 +36,7 @@ type Client struct { client.HistoryClient client.StatusClient client.EventsClient + client.EvidenceClient cmn.Service } @@ -147,3 +148,7 @@ func (c Client) Commit(height *int64) (*ctypes.ResultCommit, error) { func (c Client) Validators(height *int64) (*ctypes.ResultValidators, error) { return core.Validators(&rpctypes.Context{}, height) } + +func (c Client) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(&rpctypes.Context{}, ev) +} diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index a1a48abc4..de5e18f11 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -1,7 +1,9 @@ package client_test import ( + "bytes" "fmt" + "math/rand" "net/http" "strings" "sync" @@ -12,7 +14,10 @@ import ( abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/tmhash" cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -446,6 +451,145 @@ func TestTxSearch(t *testing.T) { } } +func deepcpVote(vote *types.Vote) (res *types.Vote) { + res = &types.Vote{ + ValidatorAddress: make([]byte, len(vote.ValidatorAddress)), + ValidatorIndex: vote.ValidatorIndex, + Height: vote.Height, + Round: vote.Round, + Type: vote.Type, + BlockID: types.BlockID{ + Hash: make([]byte, len(vote.BlockID.Hash)), + PartsHeader: vote.BlockID.PartsHeader, + }, + Signature: make([]byte, len(vote.Signature)), + } + copy(res.ValidatorAddress, vote.ValidatorAddress) + copy(res.BlockID.Hash, vote.BlockID.Hash) + copy(res.Signature, vote.Signature) + return +} + +func newEvidence(t *testing.T, val *privval.FilePV, vote *types.Vote, vote2 *types.Vote, chainID string) types.DuplicateVoteEvidence { + var err error + vote2_ := deepcpVote(vote2) + vote2_.Signature, err = val.Key.PrivKey.Sign(vote2_.SignBytes(chainID)) + require.NoError(t, err) + + return types.DuplicateVoteEvidence{ + PubKey: val.Key.PubKey, + VoteA: vote, + VoteB: vote2_, + } +} + +func makeEvidences(t *testing.T, val *privval.FilePV, chainID string) (ev types.DuplicateVoteEvidence, fakes []types.DuplicateVoteEvidence) { + vote := &types.Vote{ + ValidatorAddress: val.Key.Address, + ValidatorIndex: 0, + Height: 1, + Round: 0, + Type: types.PrevoteType, + BlockID: types.BlockID{ + Hash: tmhash.Sum([]byte("blockhash")), + PartsHeader: types.PartSetHeader{ + Total: 1000, + Hash: tmhash.Sum([]byte("partset")), + }, + }, + } + + var err error + vote.Signature, err = val.Key.PrivKey.Sign(vote.SignBytes(chainID)) + require.NoError(t, err) + + vote2 := deepcpVote(vote) + vote2.BlockID.Hash = tmhash.Sum([]byte("blockhash2")) + + ev = newEvidence(t, val, vote, vote2, chainID) + + fakes = make([]types.DuplicateVoteEvidence, 42) + + // different address + vote2 = deepcpVote(vote) + for i := 0; i < 10; i++ { + rand.Read(vote2.ValidatorAddress) // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different index + vote2 = deepcpVote(vote) + for i := 10; i < 20; i++ { + vote2.ValidatorIndex = rand.Int()%100 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different height + vote2 = deepcpVote(vote) + for i := 20; i < 30; i++ { + vote2.Height = rand.Int63()%1000 + 100 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different round + vote2 = deepcpVote(vote) + for i := 30; i < 40; i++ { + vote2.Round = rand.Int()%10 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different type + vote2 = deepcpVote(vote) + vote2.Type = types.PrecommitType + fakes[40] = newEvidence(t, val, vote, vote2, chainID) + // exactly same vote + vote2 = deepcpVote(vote) + fakes[41] = newEvidence(t, val, vote, vote2, chainID) + return +} + +func TestBroadcastEvidenceDuplicateVote(t *testing.T) { + config := rpctest.GetConfig() + chainID := config.ChainID() + pvKeyFile := config.PrivValidatorKeyFile() + pvKeyStateFile := config.PrivValidatorStateFile() + pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) + + ev, fakes := makeEvidences(t, pv, chainID) + + t.Logf("evidence %v", ev) + + for i, c := range GetClients() { + t.Logf("client %d", i) + + result, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) + require.Nil(t, err) + require.Equal(t, ev.Hash(), result.Hash, "Invalid response, result %+v", result) + + status, err := c.Status() + require.NoError(t, err) + client.WaitForHeight(c, status.SyncInfo.LatestBlockHeight+2, nil) + + ed25519pub := ev.PubKey.(ed25519.PubKeyEd25519) + rawpub := ed25519pub[:] + result2, err := c.ABCIQuery("/val", rawpub) + require.Nil(t, err, "Error querying evidence, err %v", err) + qres := result2.Response + require.True(t, qres.IsOK(), "Response not OK") + + var v abci.ValidatorUpdate + err = abci.ReadMessage(bytes.NewReader(qres.Value), &v) + require.NoError(t, err, "Error reading query result, value %v", qres.Value) + + require.EqualValues(t, rawpub, v.PubKey.Data, "Stored PubKey not equal with expected, value %v", string(qres.Value)) + require.Equal(t, int64(9), v.Power, "Stored Power not equal with expected, value %v", string(qres.Value)) + + for _, fake := range fakes { + _, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{ + PubKey: fake.PubKey, + VoteA: fake.VoteA, + VoteB: fake.VoteB}) + require.Error(t, err, "Broadcasting fake evidence succeed: %s", fake.String()) + } + } +} + func TestBatchedJSONRPCCalls(t *testing.T) { c := getHTTPClient() testBatchedJSONRPCCalls(t, c) diff --git a/rpc/core/evidence.go b/rpc/core/evidence.go new file mode 100644 index 000000000..b2dfd097f --- /dev/null +++ b/rpc/core/evidence.go @@ -0,0 +1,39 @@ +package core + +import ( + ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/types" +) + +// Broadcast evidence of the misbehavior. +// +// ```shell +// curl 'localhost:26657/broadcast_evidence?evidence={amino-encoded DuplicateVoteEvidence}' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") +// err := client.Start() +// if err != nil { +// // handle error +// } +// defer client.Stop() +// res, err := client.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) +// ``` +// +// > The above command returns JSON structured like this: +// +// ```json +// ``` +// +// | Parameter | Type | Default | Required | Description | +// |-----------+----------------+---------+----------+-----------------------------| +// | evidence | types.Evidence | nil | true | Amino-encoded JSON evidence | +func BroadcastEvidence(ctx *rpctypes.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + err := evidencePool.AddEvidence(ev) + if err != nil { + return nil, err + } + return &ctypes.ResultBroadcastEvidence{Hash: ev.Hash()}, nil +} diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 736ded607..df7cef905 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -30,7 +30,7 @@ var Routes = map[string]*rpc.RPCFunc{ "unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, "limit"), "num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""), - // broadcast API + // tx broadcast API "broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommit, "tx"), "broadcast_tx_sync": rpc.NewRPCFunc(BroadcastTxSync, "tx"), "broadcast_tx_async": rpc.NewRPCFunc(BroadcastTxAsync, "tx"), @@ -38,6 +38,9 @@ var Routes = map[string]*rpc.RPCFunc{ // abci API "abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"), "abci_info": rpc.NewRPCFunc(ABCIInfo, ""), + + // evidence API + "broadcast_evidence": rpc.NewRPCFunc(BroadcastEvidence, "evidence"), } func AddUnsafeRoutes() { diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index f1ae16a39..f8a9476f3 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -194,6 +194,11 @@ type ResultABCIQuery struct { Response abci.ResponseQuery `json:"response"` } +// Result of broadcasting evidence +type ResultBroadcastEvidence struct { + Hash []byte `json:"hash"` +} + // empty results type ( ResultUnsafeFlushMempool struct{}