github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/test/e2e/runner/evidence.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"math/rand"
     9  	"os"
    10  	"path/filepath"
    11  	"time"
    12  
    13  	"github.com/badrootd/nibiru-cometbft/crypto"
    14  	"github.com/badrootd/nibiru-cometbft/crypto/tmhash"
    15  	"github.com/badrootd/nibiru-cometbft/internal/test"
    16  	cmtjson "github.com/badrootd/nibiru-cometbft/libs/json"
    17  	"github.com/badrootd/nibiru-cometbft/privval"
    18  	cmtproto "github.com/badrootd/nibiru-cometbft/proto/tendermint/types"
    19  	cmtversion "github.com/badrootd/nibiru-cometbft/proto/tendermint/version"
    20  	e2e "github.com/badrootd/nibiru-cometbft/test/e2e/pkg"
    21  	"github.com/badrootd/nibiru-cometbft/types"
    22  	"github.com/badrootd/nibiru-cometbft/version"
    23  )
    24  
    25  // 1 in 4 evidence is light client evidence, the rest is duplicate vote evidence
    26  const lightClientEvidenceRatio = 4
    27  
    28  // InjectEvidence takes a running testnet and generates an amount of valid
    29  // evidence and broadcasts it to a random node through the rpc endpoint `/broadcast_evidence`.
    30  // Evidence is random and can be a mixture of LightClientAttackEvidence and
    31  // DuplicateVoteEvidence.
    32  func InjectEvidence(ctx context.Context, r *rand.Rand, testnet *e2e.Testnet, amount int) error {
    33  	// select a random node
    34  	var targetNode *e2e.Node
    35  
    36  	for _, idx := range r.Perm(len(testnet.Nodes)) {
    37  		targetNode = testnet.Nodes[idx]
    38  
    39  		if targetNode.Mode == e2e.ModeSeed || targetNode.Mode == e2e.ModeLight {
    40  			targetNode = nil
    41  			continue
    42  		}
    43  
    44  		break
    45  	}
    46  
    47  	if targetNode == nil {
    48  		return errors.New("could not find node to inject evidence into")
    49  	}
    50  
    51  	logger.Info(fmt.Sprintf("Injecting evidence through %v (amount: %d)...", targetNode.Name, amount))
    52  
    53  	client, err := targetNode.Client()
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	// request the latest block and validator set from the node
    59  	blockRes, err := client.Block(ctx, nil)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	evidenceHeight := blockRes.Block.Height
    64  	waitHeight := blockRes.Block.Height + 3
    65  
    66  	nValidators := 100
    67  	valRes, err := client.Validators(ctx, &evidenceHeight, nil, &nValidators)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	valSet, err := types.ValidatorSetFromExistingValidators(valRes.Validators)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	// get the private keys of all the validators in the network
    78  	privVals, err := getPrivateValidatorKeys(testnet)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	// wait for the node to reach the height above the forged height so that
    84  	// it is able to validate the evidence
    85  	_, err = waitForNode(targetNode, waitHeight, time.Minute)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	var ev types.Evidence
    91  	for i := 1; i <= amount; i++ {
    92  		if i%lightClientEvidenceRatio == 0 {
    93  			ev, err = generateLightClientAttackEvidence(
    94  				ctx, privVals, evidenceHeight, valSet, testnet.Name, blockRes.Block.Time,
    95  			)
    96  		} else {
    97  			ev, err = generateDuplicateVoteEvidence(
    98  				ctx, privVals, evidenceHeight, valSet, testnet.Name, blockRes.Block.Time,
    99  			)
   100  		}
   101  		if err != nil {
   102  			return err
   103  		}
   104  
   105  		_, err := client.BroadcastEvidence(ctx, ev)
   106  		if err != nil {
   107  			return err
   108  		}
   109  	}
   110  
   111  	// wait for the node to reach the height above the forged height so that
   112  	// it is able to validate the evidence
   113  	_, err = waitForNode(targetNode, blockRes.Block.Height+2, 30*time.Second)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	logger.Info(fmt.Sprintf("Finished sending evidence (height %d)", blockRes.Block.Height+2))
   119  
   120  	return nil
   121  }
   122  
   123  func getPrivateValidatorKeys(testnet *e2e.Testnet) ([]types.MockPV, error) {
   124  	privVals := []types.MockPV{}
   125  
   126  	for _, node := range testnet.Nodes {
   127  		if node.Mode == e2e.ModeValidator {
   128  			privKeyPath := filepath.Join(testnet.Dir, node.Name, PrivvalKeyFile)
   129  			privKey, err := readPrivKey(privKeyPath)
   130  			if err != nil {
   131  				return nil, err
   132  			}
   133  			// Create mock private validators from the validators private key. MockPV is
   134  			// stateless which means we can double vote and do other funky stuff
   135  			privVals = append(privVals, types.NewMockPVWithParams(privKey, false, false))
   136  		}
   137  	}
   138  
   139  	return privVals, nil
   140  }
   141  
   142  // creates evidence of a lunatic attack. The height provided is the common height.
   143  // The forged height happens 2 blocks later.
   144  func generateLightClientAttackEvidence(
   145  	ctx context.Context,
   146  	privVals []types.MockPV,
   147  	height int64,
   148  	vals *types.ValidatorSet,
   149  	chainID string,
   150  	evTime time.Time,
   151  ) (*types.LightClientAttackEvidence, error) {
   152  	// forge a random header
   153  	forgedHeight := height + 2
   154  	forgedTime := evTime.Add(1 * time.Second)
   155  	header := makeHeaderRandom(chainID, forgedHeight)
   156  	header.Time = forgedTime
   157  
   158  	// add a new bogus validator and remove an existing one to
   159  	// vary the validator set slightly
   160  	pv, conflictingVals, err := mutateValidatorSet(ctx, privVals, vals)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	header.ValidatorsHash = conflictingVals.Hash()
   166  
   167  	// create a commit for the forged header
   168  	blockID := makeBlockID(header.Hash(), 1000, []byte("partshash"))
   169  	voteSet := types.NewVoteSet(chainID, forgedHeight, 0, cmtproto.SignedMsgType(2), conflictingVals)
   170  	commit, err := test.MakeCommitFromVoteSet(blockID, voteSet, pv, forgedTime)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	ev := &types.LightClientAttackEvidence{
   176  		ConflictingBlock: &types.LightBlock{
   177  			SignedHeader: &types.SignedHeader{
   178  				Header: header,
   179  				Commit: commit,
   180  			},
   181  			ValidatorSet: conflictingVals,
   182  		},
   183  		CommonHeight:     height,
   184  		TotalVotingPower: vals.TotalVotingPower(),
   185  		Timestamp:        evTime,
   186  	}
   187  	ev.ByzantineValidators = ev.GetByzantineValidators(vals, &types.SignedHeader{
   188  		Header: makeHeaderRandom(chainID, forgedHeight),
   189  	})
   190  	return ev, nil
   191  }
   192  
   193  // generateDuplicateVoteEvidence picks a random validator from the val set and
   194  // returns duplicate vote evidence against the validator
   195  func generateDuplicateVoteEvidence(
   196  	ctx context.Context,
   197  	privVals []types.MockPV,
   198  	height int64,
   199  	vals *types.ValidatorSet,
   200  	chainID string,
   201  	time time.Time,
   202  ) (*types.DuplicateVoteEvidence, error) {
   203  	privVal, valIdx, err := getRandomValidatorIndex(privVals, vals)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	voteA, err := test.MakeVote(privVal, chainID, valIdx, height, 0, 2, makeRandomBlockID(), time)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	voteB, err := test.MakeVote(privVal, chainID, valIdx, height, 0, 2, makeRandomBlockID(), time)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	ev, err := types.NewDuplicateVoteEvidence(voteA, voteB, time, vals)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("could not generate evidence: %w", err)
   218  	}
   219  
   220  	return ev, nil
   221  }
   222  
   223  // getRandomValidatorIndex picks a random validator from a slice of mock PrivVals that's
   224  // also part of the validator set, returning the PrivVal and its index in the validator set
   225  func getRandomValidatorIndex(privVals []types.MockPV, vals *types.ValidatorSet) (types.MockPV, int32, error) {
   226  	for _, idx := range rand.Perm(len(privVals)) {
   227  		pv := privVals[idx]
   228  		valIdx, _ := vals.GetByAddress(pv.PrivKey.PubKey().Address())
   229  		if valIdx >= 0 {
   230  			return pv, valIdx, nil
   231  		}
   232  	}
   233  	return types.MockPV{}, -1, errors.New("no private validator found in validator set")
   234  }
   235  
   236  func readPrivKey(keyFilePath string) (crypto.PrivKey, error) {
   237  	keyJSONBytes, err := os.ReadFile(keyFilePath)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	pvKey := privval.FilePVKey{}
   242  	err = cmtjson.Unmarshal(keyJSONBytes, &pvKey)
   243  	if err != nil {
   244  		return nil, fmt.Errorf("error reading PrivValidator key from %v: %w", keyFilePath, err)
   245  	}
   246  
   247  	return pvKey.PrivKey, nil
   248  }
   249  
   250  func makeHeaderRandom(chainID string, height int64) *types.Header {
   251  	return &types.Header{
   252  		Version:            cmtversion.Consensus{Block: version.BlockProtocol, App: 1},
   253  		ChainID:            chainID,
   254  		Height:             height,
   255  		Time:               time.Now(),
   256  		LastBlockID:        makeBlockID([]byte("headerhash"), 1000, []byte("partshash")),
   257  		LastCommitHash:     crypto.CRandBytes(tmhash.Size),
   258  		DataHash:           crypto.CRandBytes(tmhash.Size),
   259  		ValidatorsHash:     crypto.CRandBytes(tmhash.Size),
   260  		NextValidatorsHash: crypto.CRandBytes(tmhash.Size),
   261  		ConsensusHash:      crypto.CRandBytes(tmhash.Size),
   262  		AppHash:            crypto.CRandBytes(tmhash.Size),
   263  		LastResultsHash:    crypto.CRandBytes(tmhash.Size),
   264  		EvidenceHash:       crypto.CRandBytes(tmhash.Size),
   265  		ProposerAddress:    crypto.CRandBytes(crypto.AddressSize),
   266  	}
   267  }
   268  
   269  func makeRandomBlockID() types.BlockID {
   270  	return makeBlockID(crypto.CRandBytes(tmhash.Size), 100, crypto.CRandBytes(tmhash.Size))
   271  }
   272  
   273  func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID {
   274  	var (
   275  		h   = make([]byte, tmhash.Size)
   276  		psH = make([]byte, tmhash.Size)
   277  	)
   278  	copy(h, hash)
   279  	copy(psH, partSetHash)
   280  	return types.BlockID{
   281  		Hash: h,
   282  		PartSetHeader: types.PartSetHeader{
   283  			Total: partSetSize,
   284  			Hash:  psH,
   285  		},
   286  	}
   287  }
   288  
   289  func mutateValidatorSet(ctx context.Context, privVals []types.MockPV, vals *types.ValidatorSet,
   290  ) ([]types.PrivValidator, *types.ValidatorSet, error) {
   291  	newVal, newPrivVal, err := test.Validator(ctx, 10)
   292  	if err != nil {
   293  		return nil, nil, err
   294  	}
   295  
   296  	var newVals *types.ValidatorSet
   297  	if vals.Size() > 2 {
   298  		newVals = types.NewValidatorSet(append(vals.Copy().Validators[:vals.Size()-1], newVal))
   299  	} else {
   300  		newVals = types.NewValidatorSet(append(vals.Copy().Validators, newVal))
   301  	}
   302  
   303  	// we need to sort the priv validators with the same index as the validator set
   304  	pv := make([]types.PrivValidator, newVals.Size())
   305  	for idx, val := range newVals.Validators {
   306  		found := false
   307  		for _, p := range append(privVals, newPrivVal.(types.MockPV)) {
   308  			if bytes.Equal(p.PrivKey.PubKey().Address(), val.Address) {
   309  				pv[idx] = p
   310  				found = true
   311  				break
   312  			}
   313  		}
   314  		if !found {
   315  			return nil, nil, fmt.Errorf("missing priv validator for %v", val.Address)
   316  		}
   317  	}
   318  
   319  	return pv, newVals, nil
   320  }