- package main
-
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "math/rand"
- "os"
- "path/filepath"
- "time"
-
- "github.com/tendermint/tendermint/crypto"
- "github.com/tendermint/tendermint/crypto/tmhash"
- "github.com/tendermint/tendermint/internal/test/factory"
- "github.com/tendermint/tendermint/privval"
- tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
- e2e "github.com/tendermint/tendermint/test/e2e/pkg"
- "github.com/tendermint/tendermint/types"
- "github.com/tendermint/tendermint/version"
- )
-
- // 1 in 4 evidence is light client evidence, the rest is duplicate vote evidence
- const lightClientEvidenceRatio = 4
-
- // InjectEvidence takes a running testnet and generates an amount of valid
- // evidence and broadcasts it to a random node through the rpc endpoint `/broadcast_evidence`.
- // Evidence is random and can be a mixture of LightClientAttackEvidence and
- // DuplicateVoteEvidence.
- func InjectEvidence(ctx context.Context, r *rand.Rand, testnet *e2e.Testnet, amount int) error {
- // select a random node
- var targetNode *e2e.Node
-
- for _, idx := range r.Perm(len(testnet.Nodes)) {
- if !testnet.Nodes[idx].Stateless() {
- targetNode = testnet.Nodes[idx]
- break
- }
- }
-
- if targetNode == nil {
- return errors.New("could not find node to inject evidence into")
- }
-
- logger.Info(fmt.Sprintf("Injecting evidence through %v (amount: %d)...", targetNode.Name, amount))
-
- client, err := targetNode.Client()
- if err != nil {
- return err
- }
-
- // request the latest block and validator set from the node
- blockRes, err := client.Block(ctx, nil)
- if err != nil {
- return err
- }
- evidenceHeight := blockRes.Block.Height - 3
-
- nValidators := 100
- valRes, err := client.Validators(ctx, &evidenceHeight, nil, &nValidators)
- if err != nil {
- return err
- }
-
- valSet, err := types.ValidatorSetFromExistingValidators(valRes.Validators)
- if err != nil {
- return err
- }
-
- // get the private keys of all the validators in the network
- privVals, err := getPrivateValidatorKeys(testnet)
- if err != nil {
- return err
- }
-
- // request the latest block and validator set from the node
- blockRes, err = client.Block(ctx, &evidenceHeight)
- if err != nil {
- return err
- }
-
- var ev types.Evidence
- for i := 1; i <= amount; i++ {
- if i%lightClientEvidenceRatio == 0 {
- ev, err = generateLightClientAttackEvidence(ctx,
- privVals, evidenceHeight, valSet, testnet.Name, blockRes.Block.Time,
- )
- } else {
- ev, err = generateDuplicateVoteEvidence(ctx,
- privVals, evidenceHeight, valSet, testnet.Name, blockRes.Block.Time,
- )
- }
- if err != nil {
- return err
- }
-
- _, err := client.BroadcastEvidence(ctx, ev)
- if err != nil {
- return err
- }
- }
-
- logger.Info("Finished sending evidence",
- "node", testnet.Name,
- "amount", amount,
- "height", evidenceHeight,
- )
-
- wctx, cancel := context.WithTimeout(ctx, time.Minute)
- defer cancel()
-
- // wait for the node to make progress after submitting
- // evidence (3 (forged height) + 1 (progress))
- _, err = waitForNode(wctx, targetNode, evidenceHeight+4)
- if err != nil {
- return err
- }
-
- return nil
- }
-
- func getPrivateValidatorKeys(testnet *e2e.Testnet) ([]types.MockPV, error) {
- privVals := []types.MockPV{}
-
- for _, node := range testnet.Nodes {
- if node.Mode == e2e.ModeValidator {
- privKeyPath := filepath.Join(testnet.Dir, node.Name, PrivvalKeyFile)
- privKey, err := readPrivKey(privKeyPath)
- if err != nil {
- return nil, err
- }
- // Create mock private validators from the validators private key. MockPV is
- // stateless which means we can double vote and do other funky stuff
- privVals = append(privVals, types.NewMockPVWithParams(privKey, false, false))
- }
- }
-
- return privVals, nil
- }
-
- // creates evidence of a lunatic attack. The height provided is the common height.
- // The forged height happens 2 blocks later.
- func generateLightClientAttackEvidence(
- ctx context.Context,
- privVals []types.MockPV,
- height int64,
- vals *types.ValidatorSet,
- chainID string,
- evTime time.Time,
- ) (*types.LightClientAttackEvidence, error) {
- // forge a random header
- forgedHeight := height + 2
- forgedTime := evTime.Add(1 * time.Second)
- header := makeHeaderRandom(chainID, forgedHeight)
- header.Time = forgedTime
-
- // add a new bogus validator and remove an existing one to
- // vary the validator set slightly
- pv, conflictingVals, err := mutateValidatorSet(ctx, privVals, vals)
- if err != nil {
- return nil, err
- }
-
- header.ValidatorsHash = conflictingVals.Hash()
-
- // create a commit for the forged header
- blockID := makeBlockID(header.Hash(), 1000, []byte("partshash"))
- voteSet := types.NewVoteSet(chainID, forgedHeight, 0, tmproto.SignedMsgType(2), conflictingVals)
-
- commit, err := factory.MakeCommit(ctx, blockID, forgedHeight, 0, voteSet, pv, forgedTime)
- if err != nil {
- return nil, err
- }
-
- ev := &types.LightClientAttackEvidence{
- ConflictingBlock: &types.LightBlock{
- SignedHeader: &types.SignedHeader{
- Header: header,
- Commit: commit,
- },
- ValidatorSet: conflictingVals,
- },
- CommonHeight: height,
- TotalVotingPower: vals.TotalVotingPower(),
- Timestamp: evTime,
- }
- ev.ByzantineValidators = ev.GetByzantineValidators(vals, &types.SignedHeader{
- Header: makeHeaderRandom(chainID, forgedHeight),
- })
- return ev, nil
- }
-
- // generateDuplicateVoteEvidence picks a random validator from the val set and
- // returns duplicate vote evidence against the validator
- func generateDuplicateVoteEvidence(
- ctx context.Context,
- privVals []types.MockPV,
- height int64,
- vals *types.ValidatorSet,
- chainID string,
- time time.Time,
- ) (*types.DuplicateVoteEvidence, error) {
- privVal, valIdx, err := getRandomValidatorIndex(privVals, vals)
- if err != nil {
- return nil, err
- }
-
- voteA, err := factory.MakeVote(ctx, privVal, chainID, valIdx, height, 0, 2, makeRandomBlockID(), time)
- if err != nil {
- return nil, err
- }
- voteB, err := factory.MakeVote(ctx, privVal, chainID, valIdx, height, 0, 2, makeRandomBlockID(), time)
- if err != nil {
- return nil, err
- }
- ev, err := types.NewDuplicateVoteEvidence(voteA, voteB, time, vals)
- if err != nil {
- return nil, fmt.Errorf("could not generate evidence: %w", err)
- }
-
- return ev, nil
- }
-
- // getRandomValidatorIndex picks a random validator from a slice of mock PrivVals that's
- // also part of the validator set, returning the PrivVal and its index in the validator set
- func getRandomValidatorIndex(privVals []types.MockPV, vals *types.ValidatorSet) (types.MockPV, int32, error) {
- for _, idx := range rand.Perm(len(privVals)) {
- pv := privVals[idx]
- valIdx, _ := vals.GetByAddress(pv.PrivKey.PubKey().Address())
- if valIdx >= 0 {
- return pv, valIdx, nil
- }
- }
- return types.MockPV{}, -1, errors.New("no private validator found in validator set")
- }
-
- func readPrivKey(keyFilePath string) (crypto.PrivKey, error) {
- keyJSONBytes, err := os.ReadFile(keyFilePath)
- if err != nil {
- return nil, err
- }
- pvKey := privval.FilePVKey{}
- err = json.Unmarshal(keyJSONBytes, &pvKey)
- if err != nil {
- return nil, fmt.Errorf("error reading PrivValidator key from %v: %w", keyFilePath, err)
- }
-
- return pvKey.PrivKey, nil
- }
-
- func makeHeaderRandom(chainID string, height int64) *types.Header {
- return &types.Header{
- Version: version.Consensus{Block: version.BlockProtocol, App: 1},
- ChainID: chainID,
- Height: height,
- Time: time.Now(),
- LastBlockID: makeBlockID([]byte("headerhash"), 1000, []byte("partshash")),
- LastCommitHash: crypto.CRandBytes(tmhash.Size),
- DataHash: crypto.CRandBytes(tmhash.Size),
- ValidatorsHash: crypto.CRandBytes(tmhash.Size),
- NextValidatorsHash: crypto.CRandBytes(tmhash.Size),
- ConsensusHash: crypto.CRandBytes(tmhash.Size),
- AppHash: crypto.CRandBytes(tmhash.Size),
- LastResultsHash: crypto.CRandBytes(tmhash.Size),
- EvidenceHash: crypto.CRandBytes(tmhash.Size),
- ProposerAddress: crypto.CRandBytes(crypto.AddressSize),
- }
- }
-
- func makeRandomBlockID() types.BlockID {
- return makeBlockID(crypto.CRandBytes(tmhash.Size), 100, crypto.CRandBytes(tmhash.Size))
- }
-
- func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID {
- var (
- h = make([]byte, tmhash.Size)
- psH = make([]byte, tmhash.Size)
- )
- copy(h, hash)
- copy(psH, partSetHash)
- return types.BlockID{
- Hash: h,
- PartSetHeader: types.PartSetHeader{
- Total: partSetSize,
- Hash: psH,
- },
- }
- }
-
- func mutateValidatorSet(ctx context.Context, privVals []types.MockPV, vals *types.ValidatorSet,
- ) ([]types.PrivValidator, *types.ValidatorSet, error) {
- newVal, newPrivVal, err := factory.Validator(ctx, 10)
- if err != nil {
- return nil, nil, err
- }
-
- var newVals *types.ValidatorSet
- if vals.Size() > 2 {
- newVals = types.NewValidatorSet(append(vals.Copy().Validators[:vals.Size()-1], newVal))
- } else {
- newVals = types.NewValidatorSet(append(vals.Copy().Validators, newVal))
- }
-
- // we need to sort the priv validators with the same index as the validator set
- pv := make([]types.PrivValidator, newVals.Size())
- for idx, val := range newVals.Validators {
- found := false
- for _, p := range append(privVals, newPrivVal.(types.MockPV)) {
- if bytes.Equal(p.PrivKey.PubKey().Address(), val.Address) {
- pv[idx] = p
- found = true
- break
- }
- }
- if !found {
- return nil, nil, fmt.Errorf("missing priv validator for %v", val.Address)
- }
- }
-
- return pv, newVals, nil
- }
|