Skip to content

Commit

Permalink
Merge pull request #47 from informalsystems/ph/light-client
Browse files Browse the repository at this point in the history
Add light client evidence generation
  • Loading branch information
p-offtermatt authored Aug 30, 2023
2 parents 99ae11c + bf336c7 commit 6b66447
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 76 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ Example usage:
curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{"jsonrpc":"2.0","method":"advance_time","params":{"duration_in_seconds": "36000000"},"id":1}' 127.0.0.1:22331
```

* `cause_double_sign(private_key_address)`: Causes the validator with the given private key to double sign. This is done by signing two blocks with the same height. This will produce DuplicateVoteEvidence and propagate it to the app via ABCI.

* `cause_light_client_attack(private_key_address, misbehaviour_type)`: Will LightClientAttackEvidence for the validator with the given private key. This will produce evidence in one of three different ways. Misbehaviour type can be:
* Equivocation: The evidence has a conflicting block that has the same height, but a non-deterministic field is different, e.g. time.
* Lunatic: The evidence has a conflicting block that differs in the app hash.
* Amnesia: The evidence has a conflicting block that is the same as the original block.

## Limitations

### Not all CometBFT RPC endpoints are implemented
Expand Down
246 changes: 174 additions & 72 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 @@ -78,6 +88,31 @@ func (a *AbciClient) IncrementTimeOffset(additionalOffset time.Duration) error {
return nil
}

func (a *AbciClient) CauseLightClientAttack(address string, misbehaviourType string) error {
a.Logger.Info("Causing double sign", "address", address)

validator, err := a.GetValidatorFromAddress(address)
if err != nil {
return err
}

// get the misbehaviour type from the string
var misbehaviour MisbehaviourType
switch misbehaviourType {
case "Lunatic":
misbehaviour = Lunatic
case "Amnesia":
misbehaviour = Amnesia
case "Equivocation":
misbehaviour = Equivocation
default:
return fmt.Errorf("unknown misbehaviour type %s, possible types are: Equivocation, Lunatic, Amnesia", misbehaviourType)
}

_, _, _, _, _, err = a.RunBlockWithEvidence(nil, map[*types.Validator]MisbehaviourType{validator: misbehaviour})
return err
}

func (a *AbciClient) CauseDoubleSign(address string) error {
a.Logger.Info("Causing double sign", "address", address)

Expand All @@ -86,7 +121,7 @@ func (a *AbciClient) CauseDoubleSign(address string) error {
return err
}

_, _, _, _, _, err = a.RunBlockWithEvidence(nil, []*types.Validator{validator})
_, _, _, _, _, err = a.RunBlockWithEvidence(nil, map[*types.Validator]MisbehaviourType{validator: DuplicateVote})
return err
}

Expand Down Expand Up @@ -582,15 +617,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 +757,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 +794,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,misbehaviour_type"),
}

type ResultCauseLightClientAttack struct{}

func CauseLightClientAttack(ctx *rpctypes.Context, privateKeyAddress, misbehaviourType string) (*ResultCauseLightClientAttack, error) {
err := abci_client.GlobalClient.CauseLightClientAttack(privateKeyAddress, misbehaviourType)
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 6b66447

Please sign in to comment.