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=