From c0b7c6fd7b3e58b7d4281508e255b967fe68d963 Mon Sep 17 00:00:00 2001
From: Philip Offtermatt
Date: Wed, 30 Aug 2023 15:07:19 +0200
Subject: [PATCH] Add light client evidence generation
---
cometmock/abci_client/client.go | 219 +++++++++++++++++++++-----------
cometmock/rpc_server/routes.go | 16 ++-
go.mod | 1 +
go.sum | 2 +
4 files changed, 163 insertions(+), 75 deletions(-)
diff --git a/cometmock/abci_client/client.go b/cometmock/abci_client/client.go
index 90a1f16..99ead85 100644
--- a/cometmock/abci_client/client.go
+++ b/cometmock/abci_client/client.go
@@ -6,6 +6,7 @@ import (
"sync"
"time"
+ "github.com/barkimedes/go-deepcopy"
db "github.com/cometbft/cometbft-db"
abcitypes "github.com/cometbft/cometbft/abci/types"
cryptoenc "github.com/cometbft/cometbft/crypto/encoding"
@@ -33,6 +34,15 @@ var stateUpdateMutex = sync.Mutex{}
var verbose = false
+type MisbehaviourType int
+
+const (
+ DuplicateVote MisbehaviourType = iota
+ Lunatic
+ Amnesia
+ Equivocation
+)
+
// AbciClient facilitates calls to the ABCI interface of multiple nodes.
// It also tracks the current state and a common logger.
type AbciClient struct {
@@ -582,15 +592,138 @@ func (a *AbciClient) RunEmptyBlocks(numBlocks int) error {
// RunBlock runs a block with a specified transaction through the ABCI application.
// It calls RunBlockWithTimeAndProposer with the current time and the LastValidators.Proposer.
func (a *AbciClient) RunBlock(tx *[]byte) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
- return a.RunBlockWithTimeAndProposer(tx, time.Now().Add(a.timeOffset), a.CurState.LastValidators.Proposer, make([]*types.Validator, 0))
+ return a.RunBlockWithTimeAndProposer(tx, time.Now().Add(a.timeOffset), a.CurState.LastValidators.Proposer, make(map[*types.Validator]MisbehaviourType, 0))
}
// RunBlockWithEvidence runs a block with a specified transaction through the ABCI application.
-// It also produces duplicate vote evidence for the specified misbehaving validators.
-func (a *AbciClient) RunBlockWithEvidence(tx *[]byte, misbehavingValidators []*types.Validator) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
+// It also produces the specified evidence for the specified misbehaving validators.
+func (a *AbciClient) RunBlockWithEvidence(tx *[]byte, misbehavingValidators map[*types.Validator]MisbehaviourType) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
return a.RunBlockWithTimeAndProposer(tx, time.Now().Add(a.timeOffset), a.CurState.LastValidators.Proposer, misbehavingValidators)
}
+func (a *AbciClient) ConstructDuplicateVoteEvidence(v *types.Validator) (*types.DuplicateVoteEvidence, error) {
+ privVal := a.PrivValidators[v.Address.String()]
+ lastBlock := a.LastBlock
+ blockId, err := utils.GetBlockIdFromBlock(lastBlock)
+ if err != nil {
+ return nil, err
+ }
+
+ lastState, err := a.Storage.GetState(lastBlock.Height)
+ if err != nil {
+ return nil, err
+ }
+
+ // get the index of the validator in the last state
+ index, valInLastState := lastState.Validators.GetByAddress(v.Address)
+
+ // produce vote A.
+ voteA := &cmttypes.Vote{
+ ValidatorAddress: v.Address,
+ ValidatorIndex: int32(index),
+ Height: lastBlock.Height,
+ Round: 1,
+ Timestamp: time.Now().Add(a.timeOffset),
+ Type: cmttypes.PrecommitType,
+ BlockID: blockId.ToProto(),
+ }
+
+ // produce vote B, which just has a different round.
+ voteB := &cmttypes.Vote{
+ ValidatorAddress: v.Address,
+ ValidatorIndex: int32(index),
+ Height: lastBlock.Height,
+ Round: 2, // this is what differentiates the votes
+ Timestamp: time.Now().Add(a.timeOffset),
+ Type: cmttypes.PrecommitType,
+ BlockID: blockId.ToProto(),
+ }
+
+ // sign the votes
+ privVal.SignVote(a.CurState.ChainID, voteA)
+ privVal.SignVote(a.CurState.ChainID, voteB)
+
+ // votes need to pass validation rules
+ convertedVoteA, err := types.VoteFromProto(voteA)
+ err = convertedVoteA.ValidateBasic()
+ if err != nil {
+ a.Logger.Error("Error validating vote A", "error", err)
+ return nil, err
+ }
+
+ convertedVoteB, err := types.VoteFromProto(voteB)
+ err = convertedVoteB.ValidateBasic()
+ if err != nil {
+ a.Logger.Error("Error validating vote B", "error", err)
+ return nil, err
+ }
+
+ // build the actual evidence
+ evidence := types.DuplicateVoteEvidence{
+ VoteA: convertedVoteA,
+ VoteB: convertedVoteB,
+
+ TotalVotingPower: lastState.Validators.TotalVotingPower(),
+ ValidatorPower: valInLastState.VotingPower,
+ Timestamp: lastBlock.Time,
+ }
+ return &evidence, nil
+}
+
+func (a *AbciClient) ConstructLightClientAttackEvidence(
+ v *types.Validator,
+ misbehaviourType MisbehaviourType,
+) (*types.LightClientAttackEvidence, error) {
+ lastBlock := a.LastBlock
+
+ lastState, err := a.Storage.GetState(lastBlock.Height)
+ if err != nil {
+ return nil, err
+ }
+
+ // deepcopy the last block so we can modify it
+ cp, err := deepcopy.Anything(lastBlock)
+ if err != nil {
+ return nil, err
+ }
+
+ // force the type conversion into a block
+ var conflictingBlock *types.Block
+ conflictingBlock = cp.(*types.Block)
+
+ switch misbehaviourType {
+ case Lunatic:
+ // modify the app hash to be invalid
+ conflictingBlock.AppHash = []byte("some other app hash")
+ case Amnesia:
+ // TODO not sure how to handle this yet, just leave the block intact for now
+ case Equivocation:
+ // get another valid block by making it have a different time
+ conflictingBlock.Time = conflictingBlock.Time.Add(1 * time.Second)
+ default:
+ return nil, fmt.Errorf("unknown misbehaviour type %v for light client misbehaviour", misbehaviourType)
+ }
+
+ // make the conflicting block into a light block
+ signedHeader := types.SignedHeader{
+ Header: &conflictingBlock.Header,
+ Commit: a.LastCommit,
+ }
+
+ conflictingLightBlock := types.LightBlock{
+ SignedHeader: &signedHeader,
+ ValidatorSet: a.CurState.Validators,
+ }
+
+ return &types.LightClientAttackEvidence{
+ TotalVotingPower: lastState.Validators.TotalVotingPower(),
+ Timestamp: lastBlock.Time,
+ ByzantineValidators: []*types.Validator{v},
+ CommonHeight: lastBlock.Height - 1,
+ ConflictingBlock: &conflictingLightBlock,
+ }, nil
+}
+
// RunBlock runs a block with a specified transaction through the ABCI application.
// It calls BeginBlock, DeliverTx, EndBlock, Commit and then
// updates the state.
@@ -599,7 +732,7 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
tx *[]byte,
blockTime time.Time,
proposer *types.Validator,
- misbehavingValidators []*types.Validator,
+ misbehavingValidators map[*types.Validator]MisbehaviourType,
) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) {
// lock mutex to avoid running two blocks at the same time
a.Logger.Debug("Locking mutex")
@@ -636,79 +769,23 @@ func (a *AbciClient) RunBlockWithTimeAndProposer(
}
evidences := make([]types.Evidence, 0)
- for _, v := range misbehavingValidators {
- privVal := a.PrivValidators[v.Address.String()]
- // produce evidence of misbehaviour.
-
- // assemble a duplicate vote evidence for this validator,
- // claiming it voted twice on the last block.
-
- lastBlock := a.LastBlock
- blockId, err := utils.GetBlockIdFromBlock(lastBlock)
- if err != nil {
- return nil, nil, nil, nil, nil, err
- }
-
- lastState, err := a.Storage.GetState(lastBlock.Height)
- if err != nil {
- return nil, nil, nil, nil, nil, err
- }
-
- // get the index of the validator in the last state
- index, valInLastState := lastState.Validators.GetByAddress(v.Address)
-
- // produce vote A.
- voteA := &cmttypes.Vote{
- ValidatorAddress: v.Address,
- ValidatorIndex: int32(index),
- Height: lastBlock.Height,
- Round: 1,
- Timestamp: time.Now().Add(a.timeOffset),
- Type: cmttypes.PrecommitType,
- BlockID: blockId.ToProto(),
- }
-
- // produce vote B, which just has a different round.
- voteB := &cmttypes.Vote{
- ValidatorAddress: v.Address,
- ValidatorIndex: int32(index),
- Height: lastBlock.Height,
- Round: 2, // this is what differentiates the votes
- Timestamp: time.Now().Add(a.timeOffset),
- Type: cmttypes.PrecommitType,
- BlockID: blockId.ToProto(),
- }
-
- // sign the votes
- privVal.SignVote(a.CurState.ChainID, voteA)
- privVal.SignVote(a.CurState.ChainID, voteB)
-
- // votes need to pass validation rules
- convertedVoteA, err := types.VoteFromProto(voteA)
- err = convertedVoteA.ValidateBasic()
- if err != nil {
- a.Logger.Error("Error validating vote A", "error", err)
- return nil, nil, nil, nil, nil, err
+ for v, misbehaviourType := range misbehavingValidators {
+ // match the misbehaviour type to call the correct function
+ var evidence types.Evidence
+ var err error
+ if misbehaviourType == DuplicateVote {
+ // create double-sign evidence
+ evidence, err = a.ConstructDuplicateVoteEvidence(v)
+ } else {
+ // create light client attack evidence
+ evidence, err = a.ConstructLightClientAttackEvidence(v, misbehaviourType)
}
- convertedVoteB, err := types.VoteFromProto(voteB)
- err = convertedVoteB.ValidateBasic()
if err != nil {
- a.Logger.Error("Error validating vote B", "error", err)
return nil, nil, nil, nil, nil, err
}
- // build the actual evidence
- evidence := types.DuplicateVoteEvidence{
- VoteA: convertedVoteA,
- VoteB: convertedVoteB,
-
- TotalVotingPower: lastState.Validators.TotalVotingPower(),
- ValidatorPower: valInLastState.VotingPower,
- Timestamp: lastBlock.Time,
- }
-
- evidences = append(evidences, &evidence)
+ evidences = append(evidences, evidence)
}
block := a.CurState.MakeBlock(a.CurState.LastBlockHeight+1, txs, a.LastCommit, evidences, proposerAddress)
diff --git a/cometmock/rpc_server/routes.go b/cometmock/rpc_server/routes.go
index 96ed401..ca4c30c 100644
--- a/cometmock/rpc_server/routes.go
+++ b/cometmock/rpc_server/routes.go
@@ -53,10 +53,18 @@ var Routes = map[string]*rpc.RPCFunc{
"abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"),
// cometmock specific API
- "advance_blocks": rpc.NewRPCFunc(AdvanceBlocks, "num_blocks"),
- "set_signing_status": rpc.NewRPCFunc(SetSigningStatus, "private_key_address,status"),
- "advance_time": rpc.NewRPCFunc(AdvanceTime, "duration_in_seconds"),
- "cause_double_sign": rpc.NewRPCFunc(CauseDoubleSign, "private_key_address"),
+ "advance_blocks": rpc.NewRPCFunc(AdvanceBlocks, "num_blocks"),
+ "set_signing_status": rpc.NewRPCFunc(SetSigningStatus, "private_key_address,status"),
+ "advance_time": rpc.NewRPCFunc(AdvanceTime, "duration_in_seconds"),
+ "cause_double_sign": rpc.NewRPCFunc(CauseDoubleSign, "private_key_address"),
+ "cause_light_client_attack": rpc.NewRPCFunc(CauseLightClientAttack, "private_key_address"),
+}
+
+type ResultCauseLightClientAttack struct{}
+
+func CauseLightClientAttack(ctx *rpctypes.Context, privateKeyAddress string) (*ResultCauseLightClientAttack, error) {
+ err := abci_client.GlobalClient.CauseLightClientAttack(privateKeyAddress)
+ return &ResultCauseLightClientAttack{}, err
}
type ResultCauseDoubleSign struct{}
diff --git a/go.mod b/go.mod
index afa1265..63fe9b6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/informalsystems/CometMock
go 1.20
require (
+ github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df
github.com/cometbft/cometbft v0.37.2
github.com/cometbft/cometbft-db v0.7.0
)
diff --git a/go.sum b/go.sum
index c7a78cd..b27f725 100644
--- a/go.sum
+++ b/go.sum
@@ -43,6 +43,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
+github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=