Skip to content

Commit

Permalink
Add light client evidence generation
Browse files Browse the repository at this point in the history
  • Loading branch information
p-offtermatt committed Aug 30, 2023
1 parent 09b964b commit c0b7c6f
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 75 deletions.
219 changes: 148 additions & 71 deletions cometmock/abci_client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions cometmock/rpc_server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit c0b7c6f

Please sign in to comment.