rpc: /broadcast_evidence (#3481)

* implement broadcast_duplicate_vote endpoint

* fix test_cover

* address comments

* address comments

* Update abci/example/kvstore/persistent_kvstore.go

Co-Authored-By: mossid <torecursedivine@gmail.com>

* Update rpc/client/main_test.go

Co-Authored-By: mossid <torecursedivine@gmail.com>

* address comments in progress

* reformat the code

* make linter happy

* make tests pass

* replace BroadcastDuplicateVote with BroadcastEvidence

* fix test

* fix endpoint name

* improve doc

* fix TestBroadcastEvidenceDuplicateVote

* Update rpc/core/evidence.go

Co-Authored-By: Thane Thomson <connect@thanethomson.com>

* add changelog entry

* fix TestBroadcastEvidenceDuplicateVote
This commit is contained in:
Anton Kaliaev
2019-07-22 12:15:29 +04:00
committed by GitHub
parent 657832a95a
commit c6daa48368
13 changed files with 302 additions and 29 deletions

12
rpc/client/amino.go Normal file
View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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)

39
rpc/core/evidence.go Normal file
View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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{}