diff --git a/README.md b/README.md index f597bb3..f7b5bc6 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# CometMock \ No newline at end of file +# CometMock + +To run CometMock: +`docker stop simapp; docker rm simapp; docker run --add-host=host.docker.internal:host-gateway --name simapp -p 26658:26658 -ti informalofftermatt/testnet:tendermock simd start --transport=grpc --with-tendermint=false --grpc-only --rpc.laddr=tcp://host.docker.internal:99999` +`docker stop simapp2; docker rm simapp2; docker run --add-host=host.docker.internal:host-gateway --name simapp2 -p 36658:26658 -ti informalofftermatt/testnet:tendermock simd start --transport=grpc --with-tendermint=false --grpc-only --rpc.laddr=tcp://host.docker.internal:99999` +`go run ./cometmock localhost:36658,localhost:26658 genesis.json tcp://localhost:26657 /Users/offtermatt/simapp1,/Users/offtermatt/simapp2` +`curl -H 'Content-Type: application/json' -H 'Accept:application/json' --data '{"jsonrpc":"2.0","method":"commit","params":{},"id":1}' 127.0.0.1:26657` \ No newline at end of file diff --git a/cometmock/abci_client/client.go b/cometmock/abci_client/client.go index d700a69..8261162 100644 --- a/cometmock/abci_client/client.go +++ b/cometmock/abci_client/client.go @@ -3,28 +3,47 @@ package abci_client import ( "fmt" "reflect" + "sync" "time" abciclient "github.com/cometbft/cometbft/abci/client" abcitypes "github.com/cometbft/cometbft/abci/types" cryptoenc "github.com/cometbft/cometbft/crypto/encoding" "github.com/cometbft/cometbft/crypto/merkle" - "github.com/cometbft/cometbft/libs/bytes" cometlog "github.com/cometbft/cometbft/libs/log" + cmtmath "github.com/cometbft/cometbft/libs/math" cmtstate "github.com/cometbft/cometbft/proto/tendermint/state" - ttypes "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/proto/tendermint/types" "github.com/cometbft/cometbft/state" + blockindexkv "github.com/cometbft/cometbft/state/indexer/block/kv" + "github.com/cometbft/cometbft/state/txindex" + indexerkv "github.com/cometbft/cometbft/state/txindex/kv" "github.com/cometbft/cometbft/types" + "github.com/p-offtermatt/CometMock/cometmock/storage" + "github.com/p-offtermatt/CometMock/cometmock/utils" ) var GlobalClient *AbciClient +// store a mutex that allows only running one block at a time +var blockMutex = sync.Mutex{} + +var verbose = false + // AbciClient facilitates calls to the ABCI interface of multiple nodes. // It also tracks the current state and a common logger. type AbciClient struct { - Clients []abciclient.Client - Logger cometlog.Logger - CurState state.State + Clients []abciclient.Client + Logger cometlog.Logger + CurState state.State + EventBus types.EventBus + LastBlock *types.Block + LastCommit *types.Commit + Storage storage.Storage + PrivValidators map[string]types.PrivValidator + IndexerService *txindex.IndexerService + TxIndex *indexerkv.TxIndex + BlockIndex *blockindexkv.BlockerIndexer // if this is true, then an error will be returned if the responses from the clients are not all equal. // can be used to check for nondeterminism in apps, but also slows down execution a bit, @@ -32,10 +51,12 @@ type AbciClient struct { ErrorOnUnequalResponses bool } -func (a *AbciClient) SendBeginBlock() (*abcitypes.ResponseBeginBlock, error) { - a.Logger.Info("Sending BeginBlock to clients") +func (a *AbciClient) SendBeginBlock(block *types.Block) (*abcitypes.ResponseBeginBlock, error) { + if verbose { + a.Logger.Info("Sending BeginBlock to clients") + } // build the BeginBlock request - beginBlockRequest := CreateBeginBlockRequest(a.CurState, time.Now(), *a.CurState.LastValidators.Proposer) + beginBlockRequest := CreateBeginBlockRequest(&block.Header, block.LastCommit) // send BeginBlock to all clients and collect the responses responses := []*abcitypes.ResponseBeginBlock{} @@ -59,26 +80,18 @@ func (a *AbciClient) SendBeginBlock() (*abcitypes.ResponseBeginBlock, error) { return responses[0], nil } -func CreateBeginBlockRequest(curState state.State, curTime time.Time, proposer types.Validator) *abcitypes.RequestBeginBlock { +func CreateBeginBlockRequest(header *types.Header, lastCommit *types.Commit) *abcitypes.RequestBeginBlock { return &abcitypes.RequestBeginBlock{ - LastCommitInfo: abcitypes.CommitInfo{ - Round: 0, - Votes: []abcitypes.VoteInfo{}, - }, - Header: ttypes.Header{ - ChainID: curState.ChainID, - Version: curState.Version.Consensus, - Height: curState.LastBlockHeight + 1, - Time: curTime, - LastBlockId: curState.LastBlockID.ToProto(), - LastCommitHash: curState.LastResultsHash, - ProposerAddress: proposer.Address, - }, + // TODO: fill in Votes + LastCommitInfo: abcitypes.LastCommitInfo{Round: lastCommit.Round, Votes: []abcitypes.VoteInfo{}}, + Header: *header.ToProto(), } } func (a *AbciClient) SendInitChain(genesisState state.State, genesisDoc *types.GenesisDoc) error { - a.Logger.Info("Sending InitChain to clients") + if verbose { + a.Logger.Info("Sending InitChain to clients") + } // build the InitChain request initChainRequest := CreateInitChainRequest(genesisState, genesisDoc) @@ -111,7 +124,7 @@ func (a *AbciClient) SendInitChain(genesisState state.State, genesisDoc *types.G } func CreateInitChainRequest(genesisState state.State, genesisDoc *types.GenesisDoc) *abcitypes.RequestInitChain { - consensusParams := genesisState.ConsensusParams.ToProto() + consensusParams := types.TM2PB.ConsensusParams(&genesisState.ConsensusParams) genesisValidators := genesisDoc.Validators @@ -127,7 +140,7 @@ func CreateInitChainRequest(genesisState state.State, genesisDoc *types.GenesisD InitialHeight: genesisState.InitialHeight, Time: genesisDoc.GenesisTime, ChainId: genesisState.ChainID, - ConsensusParams: &consensusParams, + ConsensusParams: consensusParams, AppStateBytes: genesisDoc.AppState, } return &initChainRequest @@ -153,8 +166,8 @@ func (a *AbciClient) UpdateStateFromInit(res *abcitypes.ResponseInitChain) error // if response specified consensus params, update the consensus params, otherwise we keep the ones from the genesis file if res.ConsensusParams != nil { - a.CurState.ConsensusParams = a.CurState.ConsensusParams.Update(res.ConsensusParams) - a.CurState.Version.Consensus.App = a.CurState.ConsensusParams.Version.App + a.CurState.ConsensusParams = types.UpdateConsensusParams(a.CurState.ConsensusParams, res.ConsensusParams) + a.CurState.Version.Consensus.App = a.CurState.ConsensusParams.Version.AppVersion } // to conform with RFC-6962 @@ -164,7 +177,9 @@ func (a *AbciClient) UpdateStateFromInit(res *abcitypes.ResponseInitChain) error } func (a *AbciClient) SendEndBlock() (*abcitypes.ResponseEndBlock, error) { - a.Logger.Info("Sending EndBlock to clients") + if verbose { + a.Logger.Info("Sending EndBlock to clients") + } // build the EndBlock request endBlockRequest := abcitypes.RequestEndBlock{ Height: a.CurState.LastBlockHeight + 1, @@ -214,6 +229,34 @@ func (a *AbciClient) SendCommit() (*abcitypes.ResponseCommit, error) { return responses[0], nil } +func (a *AbciClient) SendCheckTx(tx *[]byte) (*abcitypes.ResponseCheckTx, error) { + // build the CheckTx request + checkTxRequest := abcitypes.RequestCheckTx{ + Tx: *tx, + } + + // send CheckTx to all clients and collect the responses + responses := []*abcitypes.ResponseCheckTx{} + for _, client := range a.Clients { + response, err := client.CheckTxSync(checkTxRequest) + if err != nil { + return nil, err + } + responses = append(responses, response) + } + + if a.ErrorOnUnequalResponses { + // return an error if the responses are not all equal + for i := 1; i < len(responses); i++ { + if !reflect.DeepEqual(responses[i], responses[0]) { + return nil, fmt.Errorf("responses are not all equal: %v is not equal to %v", responses[i], responses[0]) + } + } + } + + return responses[0], nil +} + func (a *AbciClient) SendDeliverTx(tx *[]byte) (*abcitypes.ResponseDeliverTx, error) { // build the DeliverTx request deliverTxRequest := abcitypes.RequestDeliverTx{ @@ -256,20 +299,116 @@ func (a *AbciClient) SendAbciQuery(data []byte, path string, height int64, prove // RunBlock runs a block with a specified transaction through the ABCI application. // It calls BeginBlock, DeliverTx, EndBlock, Commit and then // updates the state. -func (a *AbciClient) RunBlock(tx *[]byte) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) { +// RunBlock is safe for use by multiple goroutines simultaneously. +func (a *AbciClient) RunBlock(tx *[]byte, blockTime time.Time, proposer *types.Validator) (*abcitypes.ResponseBeginBlock, *abcitypes.ResponseCheckTx, *abcitypes.ResponseDeliverTx, *abcitypes.ResponseEndBlock, *abcitypes.ResponseCommit, error) { + // lock mutex to avoid running two blocks at the same time + blockMutex.Lock() + a.Logger.Info("Running block") - a.Logger.Info("State at start of block", "state", a.CurState) + if verbose { + a.Logger.Info("State at start of block", "state", a.CurState) + } + + newHeight := a.CurState.LastBlockHeight + 1 - resBeginBlock, err := a.SendBeginBlock() + txs := make([]types.Tx, 0) + if tx != nil { + txs = append(txs, *tx) + } + + var resCheckTx *abcitypes.ResponseCheckTx + var err error + if tx != nil { + resCheckTx, err = a.SendCheckTx(tx) + if err != nil { + return nil, nil, nil, nil, nil, err + } + } + + // TODO: handle special case where proposer is nil + var proposerAddress types.Address + if proposer != nil { + proposerAddress = proposer.Address + } + + block, _ := a.CurState.MakeBlock(a.CurState.LastBlockHeight+1, txs, a.LastCommit, []types.Evidence{}, proposerAddress) + // override the block time, since we do not actually get votes from peers to median the time out of + block.Time = blockTime + blockId, err := utils.GetBlockIdFromBlock(block) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err + } + + a.LastBlock = block + + commitSigs := []types.CommitSig{} + + for index, val := range a.CurState.Validators.Validators { + privVal := a.PrivValidators[val.Address.String()] + + // create and sign a precommit + vote := &cmttypes.Vote{ + ValidatorAddress: val.Address, + ValidatorIndex: int32(index), + Height: block.Height, + Round: 1, + Timestamp: time.Now(), + Type: cmttypes.PrecommitType, + BlockID: blockId.ToProto(), + } + + privVal.SignVote(a.CurState.ChainID, vote) + + convertedVote, err := types.VoteFromProto(vote) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + commitSig := convertedVote.CommitSig() + + commitSigs = append(commitSigs, commitSig) + } + + a.LastCommit = types.NewCommit( + block.Height, + 1, + *blockId, + commitSigs, + ) + + // sanity check that the commit is signed correctly + err = a.CurState.Validators.VerifyCommitLightTrusting(a.CurState.ChainID, a.LastCommit, cmtmath.Fraction{Numerator: 1, Denominator: 3}) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // sanity check that the commit makes a proper light block + signedHeader := types.SignedHeader{ + Header: &block.Header, + Commit: a.LastCommit, + } + + lightBlock := types.LightBlock{ + SignedHeader: &signedHeader, + ValidatorSet: a.CurState.Validators, + } + + err = lightBlock.ValidateBasic(a.CurState.ChainID) + if err != nil { + a.Logger.Error("Light block validation failed", "err", err) + return nil, nil, nil, nil, nil, err + } + + resBeginBlock, err := a.SendBeginBlock(block) + if err != nil { + return nil, nil, nil, nil, nil, err } var resDeliverTx *abcitypes.ResponseDeliverTx if tx != nil { resDeliverTx, err = a.SendDeliverTx(tx) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } } else { resDeliverTx = nil @@ -277,21 +416,61 @@ func (a *AbciClient) RunBlock(tx *[]byte) (*abcitypes.ResponseBeginBlock, *abcit resEndBlock, err := a.SendEndBlock() if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } - resCommit, err := a.SendCommit() + deliverTxResponses := []*abcitypes.ResponseDeliverTx{} + if tx != nil { + deliverTxResponses = append(deliverTxResponses, resDeliverTx) + } + + // insert entries into the storage + err = a.Storage.InsertBlock(newHeight, block) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + err = a.Storage.InsertCommit(newHeight, a.LastCommit) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err + } + + // copy state so that the historical state is not mutated + state := a.CurState.Copy() + + err = a.Storage.InsertState(newHeight, &state) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // build components of the state update, then call the update function + abciResponses := cmtstate.ABCIResponses{ + DeliverTxs: deliverTxResponses, + EndBlock: resEndBlock, + BeginBlock: resBeginBlock, + } + + err = a.Storage.InsertResponses(newHeight, &abciResponses) + if err != nil { + return nil, nil, nil, nil, nil, err } // updates state as a side effect. returns an error if the state update fails - err = a.UpdateStateFromBlock(resBeginBlock, resEndBlock, resCommit) + err = a.UpdateStateFromBlock(blockId, block, abciResponses) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + resCommit, err := a.SendCommit() if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, err } + a.CurState.AppHash = resCommit.Data + + // unlock mutex + blockMutex.Unlock() - return resBeginBlock, resDeliverTx, resEndBlock, resCommit, nil + return resBeginBlock, resCheckTx, resDeliverTx, resEndBlock, resCommit, nil } // UpdateStateFromBlock updates the AbciClients state @@ -300,40 +479,12 @@ func (a *AbciClient) RunBlock(tx *[]byte) (*abcitypes.ResponseBeginBlock, *abcit // block results hash, validators, consensus // params, and app hash. func (a *AbciClient) UpdateStateFromBlock( - beginBlockResponse *abcitypes.ResponseBeginBlock, - endBlockResponse *abcitypes.ResponseEndBlock, - commitResponse *abcitypes.ResponseCommit, + blockId *types.BlockID, + block *types.Block, + abciResponses cmtstate.ABCIResponses, ) error { // build components of the state update, then call the update function - - // TODO: if necessary, construct actual block id - blockID := types.BlockID{} - - // TODO: if necessary, refine header to be more true to CometBFT behaviour - header := types.Header{ - Version: a.CurState.Version.Consensus, - ChainID: a.CurState.ChainID, - Height: a.CurState.LastBlockHeight + 1, - Time: time.Now(), - LastBlockID: a.CurState.LastBlockID, - LastCommitHash: a.CurState.LastResultsHash, - DataHash: bytes.HexBytes{}, - ValidatorsHash: a.CurState.Validators.Hash(), - NextValidatorsHash: a.CurState.NextValidators.Hash(), - ConsensusHash: a.CurState.ConsensusParams.Hash(), - AppHash: a.CurState.AppHash, - LastResultsHash: commitResponse.Data, - EvidenceHash: bytes.HexBytes{}, - ProposerAddress: a.CurState.Validators.Proposer.Address, - } - - abciResponses := cmtstate.ABCIResponses{ - DeliverTxs: []*abcitypes.ResponseDeliverTx{}, - EndBlock: endBlockResponse, - BeginBlock: beginBlockResponse, - } - - abciValidatorUpdates := endBlockResponse.ValidatorUpdates + abciValidatorUpdates := abciResponses.EndBlock.ValidatorUpdates err := validateValidatorUpdates(abciValidatorUpdates, a.CurState.ConsensusParams.Validator) if err != nil { return fmt.Errorf("error in validator updates: %v", err) @@ -346,8 +497,8 @@ func (a *AbciClient) UpdateStateFromBlock( newState, err := UpdateState( a.CurState, - blockID, - &header, + blockId, + &block.Header, &abciResponses, validatorUpdates, ) @@ -356,6 +507,10 @@ func (a *AbciClient) UpdateStateFromBlock( } a.CurState = newState + + // Events are fired after everything else. + // NOTE: if we crash between Commit and Save, events wont be fired during replay + fireEvents(a.Logger, &a.EventBus, block, &abciResponses, validatorUpdates) return nil } @@ -363,8 +518,8 @@ func (a *AbciClient) UpdateStateFromBlock( // updateState returns a new State updated according to the header and responses. func UpdateState( curState state.State, - blockID types.BlockID, - header *types.Header, + blockId *types.BlockID, + blockHeader *types.Header, abciResponses *cmtstate.ABCIResponses, validatorUpdates []*types.Validator, ) (state.State, error) { @@ -380,7 +535,7 @@ func UpdateState( return curState, fmt.Errorf("error changing validator set: %v", err) } // Change results from this height but only applies to the next next height. - lastHeightValsChanged = header.Height + 1 + 1 + lastHeightValsChanged = blockHeader.Height + 1 + 1 } // Update validator proposer priority and set state variables. @@ -391,29 +546,27 @@ func UpdateState( lastHeightParamsChanged := curState.LastHeightConsensusParamsChanged if abciResponses.EndBlock.ConsensusParamUpdates != nil { // NOTE: must not mutate s.ConsensusParams - nextParams = curState.ConsensusParams.Update(abciResponses.EndBlock.ConsensusParamUpdates) - err := nextParams.ValidateBasic() + nextParams = types.UpdateConsensusParams(curState.ConsensusParams, abciResponses.EndBlock.ConsensusParamUpdates) + err := types.ValidateConsensusParams(nextParams) if err != nil { return curState, fmt.Errorf("error updating consensus params: %v", err) } - curState.Version.Consensus.App = nextParams.Version.App + curState.Version.Consensus.App = nextParams.Version.AppVersion // Change results from this height but only applies to the next height. - lastHeightParamsChanged = header.Height + 1 + lastHeightParamsChanged = blockHeader.Height + 1 } nextVersion := curState.Version - // NOTE: the AppHash has not been populated. - // It will be filled on state.Save. return state.State{ Version: nextVersion, ChainID: curState.ChainID, InitialHeight: curState.InitialHeight, - LastBlockHeight: header.Height, - LastBlockID: blockID, - LastBlockTime: header.Time, + LastBlockHeight: blockHeader.Height, + LastBlockID: *blockId, + LastBlockTime: blockHeader.Time, NextValidators: nValSet, Validators: curState.NextValidators.Copy(), LastValidators: curState.Validators.Copy(), @@ -421,14 +574,15 @@ func UpdateState( ConsensusParams: nextParams, LastHeightConsensusParamsChanged: lastHeightParamsChanged, LastResultsHash: state.ABCIResponsesResultsHash(abciResponses), - AppHash: nil, + // app hash will be populated after commit + AppHash: nil, }, nil } // adapted from https://github.com/cometbft/cometbft/blob/9267594e0a17c01cc4a97b399ada5eaa8a734db5/state/execution.go#L452 func validateValidatorUpdates( abciUpdates []abcitypes.ValidatorUpdate, - params types.ValidatorParams, + params cmttypes.ValidatorParams, ) error { for _, valUpdate := range abciUpdates { if valUpdate.GetPower() < 0 { @@ -452,3 +606,59 @@ func validateValidatorUpdates( } return nil } + +func fireEvents( + logger cometlog.Logger, + eventBus types.BlockEventPublisher, + block *types.Block, + abciResponses *cmtstate.ABCIResponses, + validatorUpdates []*types.Validator, +) { + if err := eventBus.PublishEventNewBlock(types.EventDataNewBlock{ + Block: block, + ResultBeginBlock: *abciResponses.BeginBlock, + ResultEndBlock: *abciResponses.EndBlock, + }); err != nil { + logger.Error("failed publishing new block", "err", err) + } + + eventDataNewBlockHeader := types.EventDataNewBlockHeader{ + Header: block.Header, + NumTxs: int64(len(block.Txs)), + ResultBeginBlock: *abciResponses.BeginBlock, + ResultEndBlock: *abciResponses.EndBlock, + } + + if err := eventBus.PublishEventNewBlockHeader(eventDataNewBlockHeader); err != nil { + logger.Error("failed publishing new block header", "err", err) + } + + if len(block.Evidence.Evidence) != 0 { + for _, ev := range block.Evidence.Evidence { + if err := eventBus.PublishEventNewEvidence(types.EventDataNewEvidence{ + Evidence: ev, + Height: block.Height, + }); err != nil { + logger.Error("failed publishing new evidence", "err", err) + } + } + } + + for i, tx := range block.Data.Txs { + if err := eventBus.PublishEventTx(types.EventDataTx{TxResult: abcitypes.TxResult{ + Height: block.Height, + Index: uint32(i), + Tx: tx, + Result: *(abciResponses.DeliverTxs[i]), + }}); err != nil { + logger.Error("failed publishing event TX", "err", err) + } + } + + if len(validatorUpdates) > 0 { + if err := eventBus.PublishEventValidatorSetUpdates( + types.EventDataValidatorSetUpdates{ValidatorUpdates: validatorUpdates}); err != nil { + logger.Error("failed publishing event", "err", err) + } + } +} diff --git a/cometmock/main.go b/cometmock/main.go index 314c86f..2216869 100644 --- a/cometmock/main.go +++ b/cometmock/main.go @@ -3,19 +3,63 @@ package main import ( "os" "strings" + "time" + db "github.com/cometbft/cometbft-db" comet_abciclient "github.com/cometbft/cometbft/abci/client" cometlog "github.com/cometbft/cometbft/libs/log" + "github.com/cometbft/cometbft/privval" "github.com/cometbft/cometbft/state" + blockindexkv "github.com/cometbft/cometbft/state/indexer/block/kv" + "github.com/cometbft/cometbft/state/txindex" + indexerkv "github.com/cometbft/cometbft/state/txindex/kv" + "github.com/cometbft/cometbft/types" "github.com/p-offtermatt/CometMock/cometmock/abci_client" "github.com/p-offtermatt/CometMock/cometmock/rpc_server" + "github.com/p-offtermatt/CometMock/cometmock/storage" ) +func CreateAndStartEventBus(logger cometlog.Logger) (*types.EventBus, error) { + eventBus := types.NewEventBus() + eventBus.SetLogger(logger.With("module", "events")) + if err := eventBus.Start(); err != nil { + return nil, err + } + return eventBus, nil +} + +// GetMockPVsFromNodeHomes returns a list of MockPVs, created with the priv_validator_key's from the specified node homes +// We use MockPV because they do not do sanity checks that would e.g. prevent double signing +func GetMockPVsFromNodeHomes(nodeHomes []string) []types.PrivValidator { + mockPVs := make([]types.PrivValidator, 0) + + for _, nodeHome := range nodeHomes { + privValidatorKeyFile := nodeHome + "/config/priv_validator_key.json" + privValidatorStateFile := nodeHome + "/data/priv_validator_state.json" + validator := privval.LoadFilePV(privValidatorKeyFile, privValidatorStateFile) + + mockPV := types.NewMockPVWithParams(validator.Key.PrivKey, false, false) + mockPVs = append(mockPVs, mockPV) + } + + return mockPVs +} + +func CreateAndStartIndexerService(eventBus *types.EventBus, logger cometlog.Logger) (*txindex.IndexerService, *indexerkv.TxIndex, *blockindexkv.BlockerIndexer, error) { + txIndexer := indexerkv.NewTxIndex(db.NewMemDB()) + blockIndexer := blockindexkv.New(db.NewMemDB()) + + indexerService := txindex.NewIndexerService(txIndexer, blockIndexer, eventBus, false) + indexerService.SetLogger(logger.With("module", "txindex")) + + return indexerService, txIndexer, blockIndexer, indexerService.Start() +} + func main() { logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)) - if len(os.Args) != 4 { - logger.Error("Usage: ") + if len(os.Args) != 5 { + logger.Error("Usage: ") } args := os.Args[1:] @@ -23,6 +67,13 @@ func main() { appAddresses := strings.Split(args[0], ",") genesisFile := args[1] cometMockListenAddress := args[2] + nodeHomesString := args[3] + + // read node homes from args + nodeHomes := strings.Split(nodeHomesString, ",") + + // get priv validators from node Homes + privVals := GetMockPVsFromNodeHomes(nodeHomes) genesisDoc, err := state.MakeGenesisDocFromFile(genesisFile) if err != nil { @@ -44,11 +95,42 @@ func main() { clients = append(clients, client) } + eventBus, err := CreateAndStartEventBus(logger) + if err != nil { + logger.Error(err.Error()) + panic(err) + } + + indexerService, txIndex, blockIndex, err := CreateAndStartIndexerService(eventBus, logger) + if err != nil { + logger.Error(err.Error()) + panic(err) + } + + privValsMap := make(map[string]types.PrivValidator) + for _, privVal := range privVals { + pubkey, err := privVal.GetPubKey() + if err != nil { + logger.Error(err.Error()) + panic(err) + } + addr := pubkey.Address() + + privValsMap[addr.String()] = privVal + } + abci_client.GlobalClient = &abci_client.AbciClient{ Clients: clients, Logger: logger, CurState: curState, ErrorOnUnequalResponses: true, + EventBus: *eventBus, + LastCommit: &types.Commit{}, + Storage: &storage.MapStorage{}, + PrivValidators: privValsMap, + IndexerService: indexerService, + TxIndex: txIndex, + BlockIndex: blockIndex, } // initialize chain @@ -59,7 +141,17 @@ func main() { } // run an empty block - abci_client.GlobalClient.RunBlock(nil) + _, _, _, _, _, err = abci_client.GlobalClient.RunBlock(nil, time.Now(), abci_client.GlobalClient.CurState.LastValidators.Proposer) + if err != nil { + logger.Error(err.Error()) + panic(err) + } - rpc_server.StartRPCServerWithDefaultConfig(cometMockListenAddress, logger) + go rpc_server.StartRPCServerWithDefaultConfig(cometMockListenAddress, logger) + + // produce a block every second + for { + abci_client.GlobalClient.RunBlock(nil, time.Now(), abci_client.GlobalClient.CurState.LastValidators.Proposer) + time.Sleep(1 * time.Second) + } } diff --git a/cometmock/rpc_server/routes.go b/cometmock/rpc_server/routes.go index 872dd35..83beed3 100644 --- a/cometmock/rpc_server/routes.go +++ b/cometmock/rpc_server/routes.go @@ -1,15 +1,23 @@ package rpc_server import ( + "errors" "fmt" + "sort" + "time" "github.com/cometbft/cometbft/libs/bytes" cmtmath "github.com/cometbft/cometbft/libs/math" + cmtquery "github.com/cometbft/cometbft/libs/pubsub/query" + "github.com/cometbft/cometbft/p2p" + cometp2p "github.com/cometbft/cometbft/p2p" ctypes "github.com/cometbft/cometbft/rpc/core/types" rpc "github.com/cometbft/cometbft/rpc/jsonrpc/server" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" "github.com/cometbft/cometbft/types" + "github.com/cometbft/cometbft/version" "github.com/p-offtermatt/CometMock/cometmock/abci_client" + "github.com/p-offtermatt/CometMock/cometmock/utils" ) const ( @@ -18,9 +26,23 @@ const ( ) var Routes = map[string]*rpc.RPCFunc{ + // websocket + "subscribe": rpc.NewWSRPCFunc(Subscribe, "query"), + "unsubscribe": rpc.NewWSRPCFunc(Unsubscribe, "query"), + "unsubscribe_all": rpc.NewWSRPCFunc(UnsubscribeAll, ""), + // info API - "validators": rpc.NewRPCFunc(Validators, "height,page,per_page"), - "block": rpc.NewRPCFunc(Block, "height", rpc.Cacheable("height")), + "health": rpc.NewRPCFunc(Health, ""), + "status": rpc.NewRPCFunc(Status, ""), + "validators": rpc.NewRPCFunc(Validators, "height,page,per_page"), + "block": rpc.NewRPCFunc(Block, "height", rpc.Cacheable("height")), + "consensus_params": rpc.NewRPCFunc(ConsensusParams, "height", rpc.Cacheable("height")), + // "header": rpc.NewRPCFunc(Header, "height", rpc.Cacheable("height")), // not available in 0.34.x + "commit": rpc.NewRPCFunc(Commit, "height", rpc.Cacheable("height")), + "block_results": rpc.NewRPCFunc(BlockResults, "height", rpc.Cacheable("height")), + "tx": rpc.NewRPCFunc(Tx, "hash,prove", rpc.Cacheable()), + "tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page,order_by"), + "block_search": rpc.NewRPCFunc(BlockSearch, "query,page,per_page,order_by"), // // tx broadcast API "broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommit, "tx"), @@ -31,15 +53,327 @@ var Routes = map[string]*rpc.RPCFunc{ "abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"), } +// BlockSearch searches for a paginated set of blocks matching BeginBlock and +// EndBlock event search criteria. +func BlockSearch( + ctx *rpctypes.Context, + query string, + pagePtr, perPagePtr *int, + orderBy string, +) (*ctypes.ResultBlockSearch, error) { + q, err := cmtquery.New(query) + if err != nil { + return nil, err + } + + results, err := abci_client.GlobalClient.BlockIndex.Search(ctx.Context(), q) + if err != nil { + return nil, err + } + + // sort results (must be done before pagination) + switch orderBy { + case "desc", "": + sort.Slice(results, func(i, j int) bool { return results[i] > results[j] }) + + case "asc": + sort.Slice(results, func(i, j int) bool { return results[i] < results[j] }) + + default: + return nil, errors.New("expected order_by to be either `asc` or `desc` or empty") + } + + // paginate results + totalCount := len(results) + perPage := validatePerPage(perPagePtr) + + page, err := validatePage(pagePtr, perPage, totalCount) + if err != nil { + return nil, err + } + + skipCount := validateSkipCount(page, perPage) + pageSize := cmtmath.MinInt(perPage, totalCount-skipCount) + + apiResults := make([]*ctypes.ResultBlock, 0, pageSize) + for i := skipCount; i < skipCount+pageSize; i++ { + block, err := abci_client.GlobalClient.Storage.GetBlock(results[i]) + if err != nil { + return nil, err + } + if block != nil { + if err != nil { + return nil, err + } + blockId, err := utils.GetBlockIdFromBlock(block) + if err != nil { + return nil, err + } + + apiResults = append(apiResults, &ctypes.ResultBlock{ + Block: block, + BlockID: *blockId, + }) + } + } + + return &ctypes.ResultBlockSearch{Blocks: apiResults, TotalCount: totalCount}, nil +} + +// Tx allows you to query the transaction results. `nil` could mean the +// transaction is in the mempool, invalidated, or was not sent in the first +// place. +// More: https://docs.tendermint.com/v0.34/rpc/#/Info/tx +func Tx(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { + txIndexer := abci_client.GlobalClient.TxIndex + + r, err := txIndexer.Get(hash) + if err != nil { + return nil, err + } + + if r == nil { + return nil, fmt.Errorf("tx (%X) not found", hash) + } + + height := r.Height + index := r.Index + + var proof types.TxProof + if prove { + block, err := abci_client.GlobalClient.Storage.GetBlock(height) + if err != nil { + return nil, err + } + proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines + } + + return &ctypes.ResultTx{ + Hash: hash, + Height: height, + Index: index, + TxResult: r.Result, + Tx: r.Tx, + Proof: proof, + }, nil +} + +// TxSearch allows you to query for multiple transactions results. It returns a +// list of transactions (maximum ?per_page entries) and the total count. +// More: https://docs.tendermint.com/v0.34/rpc/#/Info/tx_search +func TxSearch( + ctx *rpctypes.Context, + query string, + prove bool, + pagePtr, perPagePtr *int, + orderBy string, +) (*ctypes.ResultTxSearch, error) { + if len(query) > maxQueryLength { + return nil, errors.New("maximum query length exceeded") + } + + q, err := cmtquery.New(query) + if err != nil { + return nil, err + } + + results, err := abci_client.GlobalClient.TxIndex.Search(ctx.Context(), q) + if err != nil { + return nil, err + } + + // sort results (must be done before pagination) + switch orderBy { + case "desc": + sort.Slice(results, func(i, j int) bool { + if results[i].Height == results[j].Height { + return results[i].Index > results[j].Index + } + return results[i].Height > results[j].Height + }) + case "asc", "": + sort.Slice(results, func(i, j int) bool { + if results[i].Height == results[j].Height { + return results[i].Index < results[j].Index + } + return results[i].Height < results[j].Height + }) + default: + return nil, errors.New("expected order_by to be either `asc` or `desc` or empty") + } + + // paginate results + totalCount := len(results) + perPage := validatePerPage(perPagePtr) + + page, err := validatePage(pagePtr, perPage, totalCount) + if err != nil { + return nil, err + } + + skipCount := validateSkipCount(page, perPage) + pageSize := cmtmath.MinInt(perPage, totalCount-skipCount) + + apiResults := make([]*ctypes.ResultTx, 0, pageSize) + for i := skipCount; i < skipCount+pageSize; i++ { + r := results[i] + + var proof types.TxProof + if prove { + block, err := abci_client.GlobalClient.Storage.GetBlock(r.Height) + if err != nil { + return nil, err + } + proof = block.Data.Txs.Proof(int(r.Index)) // XXX: overflow on 32-bit machines + } + + apiResults = append(apiResults, &ctypes.ResultTx{ + Hash: types.Tx(r.Tx).Hash(), + Height: r.Height, + Index: r.Index, + TxResult: r.Result, + Tx: r.Tx, + Proof: proof, + }) + } + + return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil +} + +func getHeight(latestHeight int64, heightPtr *int64) (int64, error) { + if heightPtr != nil { + height := *heightPtr + if height <= 0 { + return 0, fmt.Errorf("height must be greater than 0, but got %d", height) + } + if height > latestHeight { + return 0, fmt.Errorf("height %d must be less than or equal to the current blockchain height %d", + height, latestHeight) + } + return height, nil + } + return latestHeight, nil +} + +// // Header gets block header at a given height. +// // If no height is provided, it will fetch the latest header. +// // More: https://docs.cometbft.com/v0.37/rpc/#/Info/header +// func Header(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultHeader, error) { +// height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) +// if err != nil { +// return nil, err +// } + +// block, err := abci_client.GlobalClient.Storage.GetBlock(height) +// if err != nil { +// return nil, err +// } + +// return &ctypes.ResultHeader{Header: &block.Header}, nil +// } + +// Commit gets block commit at a given height. +// If no height is provided, it will fetch the commit for the latest block. +// More: https://docs.cometbft.com/main/rpc/#/Info/commit +func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, error) { + height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) + if err != nil { + return nil, err + } + + commit, err := abci_client.GlobalClient.Storage.GetCommit(height) + if err != nil { + return nil, err + } + + block, err := abci_client.GlobalClient.Storage.GetBlock(height) + if err != nil { + return nil, err + } + + return ctypes.NewResultCommit(&block.Header, commit, true), nil +} + +// ConsensusParams gets the consensus parameters at the given block height. +// If no height is provided, it will fetch the latest consensus params. +// More: https://docs.cometbft.com/v0.37/rpc/#/Info/consensus_params +func ConsensusParams(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultConsensusParams, error) { + height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) + if err != nil { + return nil, err + } + + stateForHeight, err := abci_client.GlobalClient.Storage.GetState(height) + if err != nil { + return nil, err + } + + consensusParams := stateForHeight.ConsensusParams + + return &ctypes.ResultConsensusParams{ + BlockHeight: height, + ConsensusParams: consensusParams, + }, nil +} + +// Status returns CometBFT status including node info, pubkey, latest block +// hash, app hash, block height and time. +// More: https://docs.cometbft.com/v0.37/rpc/#/Info/status +func Status(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { + // return status as if we are the first validator + curState := abci_client.GlobalClient.CurState + validator := curState.Validators.Validators[0] + + nodeInfo := cometp2p.DefaultNodeInfo{ + DefaultNodeID: cometp2p.PubKeyToID(validator.PubKey), + Network: abci_client.GlobalClient.CurState.ChainID, + Other: cometp2p.DefaultNodeInfoOther{ + TxIndex: "on", + }, + Version: "v0.37.1", + ProtocolVersion: p2p.NewProtocolVersion( + version.P2PProtocol, // global + curState.Version.Consensus.Block, + curState.Version.Consensus.App, + ), + } + syncInfo := ctypes.SyncInfo{ + LatestBlockHash: abci_client.GlobalClient.LastBlock.Hash(), + LatestAppHash: abci_client.GlobalClient.LastBlock.AppHash, + LatestBlockHeight: abci_client.GlobalClient.LastBlock.Height, + LatestBlockTime: abci_client.GlobalClient.CurState.LastBlockTime, + CatchingUp: false, + } + validatorInfo := ctypes.ValidatorInfo{ + Address: validator.Address, + PubKey: validator.PubKey, + VotingPower: validator.VotingPower, + } + result := &ctypes.ResultStatus{ + NodeInfo: nodeInfo, + SyncInfo: syncInfo, + ValidatorInfo: validatorInfo, + } + + return result, nil +} + +// Health gets node health. Returns empty result (200 OK) on success, no +// response - in case of an error. +func Health(ctx *rpctypes.Context) (*ctypes.ResultHealth, error) { + return &ctypes.ResultHealth{}, nil +} + // BroadcastTxCommit broadcasts a transaction, // and wait until it is included in a block and and comitted. // In our case, this means running a block with just the the transition, // then return. func BroadcastTxCommit(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { abci_client.GlobalClient.Logger.Info( - "BroadcastTxCommut called", "tx", tx) + "BroadcastTxCommit called", "tx", tx) - return BroadcastTx(&tx) + res, err := BroadcastTx(&tx) + return res, err } // BroadcastTxSync would normally broadcast a transaction and wait until it gets the result from CheckTx. @@ -49,12 +383,18 @@ func BroadcastTxSync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcas abci_client.GlobalClient.Logger.Info( "BroadcastTxSync called", "tx", tx) - _, err := BroadcastTx(&tx) + resBroadcastTx, err := BroadcastTx(&tx) if err != nil { return nil, err } - return &ctypes.ResultBroadcastTx{}, nil + return &ctypes.ResultBroadcastTx{ + Code: resBroadcastTx.CheckTx.Code, + Data: resBroadcastTx.CheckTx.Data, + Log: resBroadcastTx.CheckTx.Log, + Hash: resBroadcastTx.Hash, + Codespace: resBroadcastTx.CheckTx.Codespace, + }, nil } // BroadcastTxAsync would normally broadcast a transaction and return immediately. @@ -79,13 +419,18 @@ func BroadcastTx(tx *types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { byteTx := []byte(*tx) - _, _, _, _, err := abci_client.GlobalClient.RunBlock(&byteTx) + _, responseCheckTx, responseDeliverTx, _, _, err := abci_client.GlobalClient.RunBlock(&byteTx, time.Now(), abci_client.GlobalClient.CurState.LastValidators.Proposer) if err != nil { return nil, err } // TODO: fill the return value if necessary - return &ctypes.ResultBroadcastTxCommit{}, nil + return &ctypes.ResultBroadcastTxCommit{ + CheckTx: *responseCheckTx, + DeliverTx: *responseDeliverTx, + Height: abci_client.GlobalClient.LastBlock.Height, + Hash: tx.Hash(), + }, nil } func ABCIQuery( @@ -99,18 +444,24 @@ func ABCIQuery( "ABCIQuery called", "path", "data", "height", "prove", path, data, height, prove) response, err := abci_client.GlobalClient.SendAbciQuery(data, path, height, prove) + + abci_client.GlobalClient.Logger.Info( + "Response to ABCI query", response.String()) return &ctypes.ResultABCIQuery{Response: *response}, err } func Validators(ctx *rpctypes.Context, heightPtr *int64, pagePtr, perPagePtr *int) (*ctypes.ResultValidators, error) { - // only the last height is available, since we do not keep past heights at the moment - if heightPtr != nil { - return nil, fmt.Errorf("height parameter is not supported, use version of the function without height") + height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) + if err != nil { + return nil, err } - height := abci_client.GlobalClient.CurState.LastBlockHeight + pastState, err := abci_client.GlobalClient.Storage.GetState(height) + if err != nil { + return nil, err + } - validators := abci_client.GlobalClient.CurState.LastValidators + validators := pastState.Validators totalCount := len(validators.Validators) perPage := validatePerPage(perPagePtr) @@ -179,15 +530,48 @@ func validateSkipCount(page, perPage int) int { } func Block(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlock, error) { - // only the last height is available, since we do not keep past heights at the moment - if heightPtr != nil { - return nil, fmt.Errorf("height parameter is not supported, use version of the function without height") + height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) + if err != nil { + return nil, err } - blockID := abci_client.GlobalClient.CurState.LastBlockID + block, err := abci_client.GlobalClient.Storage.GetBlock(height) + if err != nil { + return nil, err + } - // TODO: return an actual block if it is needed, for now return en empty block - block := &types.Block{Header: types.Header{Height: abci_client.GlobalClient.CurState.LastBlockHeight}} + blockID, err := utils.GetBlockIdFromBlock(block) + if err != nil { + return nil, err + } + + return &ctypes.ResultBlock{BlockID: *blockID, Block: abci_client.GlobalClient.LastBlock}, nil +} - return &ctypes.ResultBlock{BlockID: blockID, Block: block}, nil +// BlockResults gets ABCIResults at a given height. +// If no height is provided, it will fetch results for the latest block. +// +// Results are for the height of the block containing the txs. +// Thus response.results.deliver_tx[5] is the results of executing +// getBlock(h).Txs[5] +// More: https://docs.cometbft.com/v0.37/rpc/#/Info/block_results +func BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockResults, error) { + height, err := getHeight(abci_client.GlobalClient.LastBlock.Height, heightPtr) + if err != nil { + return nil, err + } + + results, err := abci_client.GlobalClient.Storage.GetResponses(height) + if err != nil { + return nil, err + } + + return &ctypes.ResultBlockResults{ + Height: height, + TxsResults: results.DeliverTxs, + BeginBlockEvents: results.BeginBlock.Events, + EndBlockEvents: results.EndBlock.Events, + ValidatorUpdates: results.EndBlock.ValidatorUpdates, + ConsensusParamUpdates: results.EndBlock.ConsensusParamUpdates, + }, nil } diff --git a/cometmock/rpc_server/rpc_server.go b/cometmock/rpc_server/rpc_server.go index c161706..a8c5114 100644 --- a/cometmock/rpc_server/rpc_server.go +++ b/cometmock/rpc_server/rpc_server.go @@ -1,14 +1,12 @@ package rpc_server import ( + "bytes" "encoding/json" - "errors" "fmt" - "net" + "io" + "io/ioutil" "net/http" - "os" - "runtime/debug" - "time" "github.com/cometbft/cometbft/libs/log" rpcserver "github.com/cometbft/cometbft/rpc/jsonrpc/server" @@ -37,7 +35,7 @@ func StartRPCServer(listenAddr string, logger log.Logger, config *rpcserver.Conf var rootHandler http.Handler = mux if err := rpcserver.Serve( listener, - rootHandler, + ExtraLogHandler(rootHandler, rpcLogger), rpcLogger, config, ); err != nil { @@ -50,92 +48,25 @@ func StartRPCServerWithDefaultConfig(listenAddr string, logger log.Logger) { StartRPCServer(listenAddr, logger, rpcserver.DefaultConfig()) } -func ServeRPC(listener net.Listener, handler http.Handler, logger log.Logger, config *rpcserver.Config) error { - logger.Info("serve", "msg", log.NewLazySprintf("Starting RPC HTTP server on %s", listener.Addr())) - s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), - ReadTimeout: config.ReadTimeout, - ReadHeaderTimeout: config.ReadTimeout, - WriteTimeout: config.WriteTimeout, - MaxHeaderBytes: config.MaxHeaderBytes, - } - err := s.Serve(listener) - logger.Info("RPC HTTP server stopped", "err", err) - return err -} - // RecoverAndLogHandler wraps an HTTP handler, adding error logging. // If the inner function panics, the outer function recovers, logs, sends an // HTTP 500 error response. -func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler { +func ExtraLogHandler(handler http.Handler, logger log.Logger) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Wrap the ResponseWriter to remember the status - rww := &responseWriterWrapper{-1, w} - begin := time.Now() - - rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) - - defer func() { - // Handle any panics in the panic handler below. Does not use the logger, since we want - // to avoid any further panics. However, we try to return a 500, since it otherwise - // defaults to 200 and there is no other way to terminate the connection. If that - // should panic for whatever reason then the Go HTTP server will handle it and - // terminate the connection - panicing is the de-facto and only way to get the Go HTTP - // server to terminate the request and close the connection/stream: - // https://github.com/golang/go/issues/17790#issuecomment-258481416 - if e := recover(); e != nil { - fmt.Fprintf(os.Stderr, "Panic during RPC panic recovery: %v\n%v\n", e, string(debug.Stack())) - w.WriteHeader(500) - } - }() - - defer func() { - // Send a 500 error if a panic happens during a handler. - // Without this, Chrome & Firefox were retrying aborted ajax requests, - // at least to my localhost. - if e := recover(); e != nil { - // If RPCResponse - if res, ok := e.(types.RPCResponse); ok { - if wErr := WriteRPCResponseHTTP(rww, res); wErr != nil { - logger.Error("failed to write response", "res", res, "err", wErr) - } - } else { - // Panics can contain anything, attempt to normalize it as an error. - var err error - switch e := e.(type) { - case error: - err = e - case string: - err = errors.New(e) - case fmt.Stringer: - err = errors.New(e.String()) - default: - } - - logger.Error("panic in RPC HTTP handler", "err", e, "stack", string(debug.Stack())) - - res := types.RPCInternalError(types.JSONRPCIntID(-1), err) - if wErr := WriteRPCResponseHTTPError(rww, http.StatusInternalServerError, res); wErr != nil { - logger.Error("failed to write response", "res", res, "err", wErr) - } - } + if r.Body != nil { + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Error("failed to read request body", "err", err) + } else { + logger.Debug("served RPC HTTP request", + "body", string(body), + ) } - // Finally, log. - durationMS := time.Since(begin).Nanoseconds() / 1000000 - if rww.Status == -1 { - rww.Status = 200 - } - logger.Debug("served RPC HTTP response", - "method", r.Method, - "url", r.URL, - "status", rww.Status, - "duration", durationMS, - "remoteAddr", r.RemoteAddr, - ) - }() - - handler.ServeHTTP(rww, r) + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + + handler.ServeHTTP(w, r) }) } diff --git a/cometmock/rpc_server/websocket.go b/cometmock/rpc_server/websocket.go new file mode 100644 index 0000000..6a7a0d0 --- /dev/null +++ b/cometmock/rpc_server/websocket.go @@ -0,0 +1,125 @@ +package rpc_server + +import ( + "context" + "errors" + "fmt" + "time" + + cmtpubsub "github.com/cometbft/cometbft/libs/pubsub" + cmtquery "github.com/cometbft/cometbft/libs/pubsub/query" + ctypes "github.com/cometbft/cometbft/rpc/core/types" + rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" + "github.com/p-offtermatt/CometMock/cometmock/abci_client" +) + +const ( + // maxQueryLength is the maximum length of a query string that will be + // accepted. This is just a safety check to avoid outlandish queries. + maxQueryLength = 512 + SubscribeTimeout = 10 * time.Second + SubscriptionBufferSize = 100 +) + +// Subscribe for events via WebSocket. +// More: https://docs.cometbft.com/v0.37/rpc/#/Websocket/subscribe +func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) { + addr := ctx.RemoteAddr() + + abci_client.GlobalClient.Logger.Info("Subscribe to query", "remote", addr, "query", query) + + q, err := cmtquery.New(query) + if err != nil { + return nil, fmt.Errorf("failed to parse query: %w", err) + } + + subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout) + defer cancel() + + sub, err := abci_client.GlobalClient.EventBus.Subscribe(subCtx, addr, q, SubscriptionBufferSize) + if err != nil { + return nil, err + } + + closeIfSlow := false + + // Capture the current ID, since it can change in the future. + subscriptionID := ctx.JSONReq.ID + go func() { + for { + select { + case msg := <-sub.Out(): + var ( + resultEvent = &ctypes.ResultEvent{Query: query, Data: msg.Data(), Events: msg.Events()} + resp = rpctypes.NewRPCSuccessResponse(subscriptionID, resultEvent) + ) + writeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := ctx.WSConn.WriteRPCResponse(writeCtx, resp); err != nil { + abci_client.GlobalClient.Logger.Info("Can't write response (slow client)", + "to", addr, "subscriptionID", subscriptionID, "err", err) + + if closeIfSlow { + var ( + err = errors.New("subscription was canceled (reason: slow client)") + resp = rpctypes.RPCServerError(subscriptionID, err) + ) + if !ctx.WSConn.TryWriteRPCResponse(resp) { + abci_client.GlobalClient.Logger.Info("Can't write response (slow client)", + "to", addr, "subscriptionID", subscriptionID, "err", err) + } + return + } + } + case <-sub.Cancelled(): + if sub.Err() != cmtpubsub.ErrUnsubscribed { + var reason string + if sub.Err() == nil { + reason = "CometBFT exited" + } else { + reason = sub.Err().Error() + } + var ( + err = fmt.Errorf("subscription was canceled (reason: %s)", reason) + resp = rpctypes.RPCServerError(subscriptionID, err) + ) + if !ctx.WSConn.TryWriteRPCResponse(resp) { + abci_client.GlobalClient.Logger.Info("Can't write response (slow client)", + "to", addr, "subscriptionID", subscriptionID, "err", err) + } + } + return + } + } + }() + + return &ctypes.ResultSubscribe{}, nil +} + +// Unsubscribe from events via WebSocket. +// More: https://docs.cometbft.com/v0.37/rpc/#/Websocket/unsubscribe +func Unsubscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) { + addr := ctx.RemoteAddr() + abci_client.GlobalClient.Logger.Info("Unsubscribe from query", "remote", addr, "query", query) + q, err := cmtquery.New(query) + if err != nil { + return nil, fmt.Errorf("failed to parse query: %w", err) + } + err = abci_client.GlobalClient.EventBus.Unsubscribe(context.Background(), addr, q) + if err != nil { + return nil, err + } + return &ctypes.ResultUnsubscribe{}, nil +} + +// UnsubscribeAll from all events via WebSocket. +// More: https://docs.cometbft.com/v0.37/rpc/#/Websocket/unsubscribe_all +func UnsubscribeAll(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) { + addr := ctx.RemoteAddr() + abci_client.GlobalClient.Logger.Info("Unsubscribe from all", "remote", addr) + err := abci_client.GlobalClient.EventBus.UnsubscribeAll(context.Background(), addr) + if err != nil { + return nil, err + } + return &ctypes.ResultUnsubscribe{}, nil +} diff --git a/cometmock/storage/storage.go b/cometmock/storage/storage.go new file mode 100644 index 0000000..38ab2e7 --- /dev/null +++ b/cometmock/storage/storage.go @@ -0,0 +1,127 @@ +package storage + +import ( + "fmt" + + protostate "github.com/cometbft/cometbft/proto/tendermint/state" + cometstate "github.com/cometbft/cometbft/state" + "github.com/cometbft/cometbft/types" +) + +// Storage is an interface for storing blocks, commits and states by height. +type Storage interface { + // InsertBlock inserts a block at a given height. + // If there is already a block at that height, it should be overwritten. + InsertBlock(height int64, block *types.Block) error + // GetBlock returns the block at a given height. + GetBlock(height int64) (*types.Block, error) + + // InsertCommit inserts a commit at a given height. + // If there is already a commit at that height, it should be overwritten. + InsertCommit(height int64, commit *types.Commit) error + // GetCommit returns the commit at a given height. + GetCommit(height int64) (*types.Commit, error) + + // InsertState inserts a state at a given height. This is the state after + // applying the block at that height. + // If there is already a state at that height, it should be overwritten. + InsertState(height int64, state *cometstate.State) error + // GetState returns the state at a given height. This is the state after + // applying the block at that height. + GetState(height int64) (*cometstate.State, error) + + // InsertResponses inserts the ABCI responses from a given height. + // If there are already responses at that height, they should be overwritten. + InsertResponses(height int64, responses *protostate.ABCIResponses) error + // GetResponses returns the ABCI responses from a given height. + GetResponses(height int64) (*protostate.ABCIResponses, error) +} + +// MapStorage is a simple in-memory implementation of Storage. +type MapStorage struct { + blocks map[int64]*types.Block + commits map[int64]*types.Commit + states map[int64]*cometstate.State + responses map[int64]*protostate.ABCIResponses +} + +// ensure MapStorage implements Storage +var _ Storage = (*MapStorage)(nil) + +func (m *MapStorage) InsertBlock(height int64, block *types.Block) error { + if m.blocks == nil { + m.blocks = make(map[int64]*types.Block) + } + m.blocks[height] = block + return nil +} + +func (m *MapStorage) GetBlock(height int64) (*types.Block, error) { + if m.blocks == nil { + m.blocks = make(map[int64]*types.Block) + } + if block, ok := m.blocks[height]; ok { + return block, nil + } + return nil, fmt.Errorf("block for height %v not found", height) +} + +func (m *MapStorage) InsertCommit(height int64, commit *types.Commit) error { + if m.commits == nil { + m.commits = make(map[int64]*types.Commit) + } + + m.commits[height] = commit + return nil +} + +func (m *MapStorage) GetCommit(height int64) (*types.Commit, error) { + if m.commits == nil { + m.commits = make(map[int64]*types.Commit) + } + + if commit, ok := m.commits[height]; ok { + return commit, nil + } + return nil, fmt.Errorf("commit for height %v not found", height) +} + +func (m *MapStorage) InsertState(height int64, state *cometstate.State) error { + if m.states == nil { + m.states = make(map[int64]*cometstate.State) + } + + m.states[height] = state + return nil +} + +func (m *MapStorage) GetState(height int64) (*cometstate.State, error) { + if m.states == nil { + m.states = make(map[int64]*cometstate.State) + } + + if state, ok := m.states[height]; ok { + return state, nil + } + return nil, fmt.Errorf("state for height %v not found", height) +} + +func (m *MapStorage) InsertResponses(height int64, responses *protostate.ABCIResponses) error { + if m.responses == nil { + m.responses = make(map[int64]*protostate.ABCIResponses) + } + + m.responses[height] = responses + return nil +} + +func (m *MapStorage) GetResponses(height int64) (*protostate.ABCIResponses, error) { + if m.responses == nil { + m.responses = make(map[int64]*protostate.ABCIResponses) + } + + if responses, ok := m.responses[height]; ok { + return responses, nil + } + return nil, fmt.Errorf("responses for height %v not found", height) +} diff --git a/cometmock/test_utils/helpers.go b/cometmock/test_utils/helpers.go new file mode 100644 index 0000000..fa3140b --- /dev/null +++ b/cometmock/test_utils/helpers.go @@ -0,0 +1,248 @@ +package test_utils + +import ( + "time" + + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/ed25519" + "github.com/cometbft/cometbft/crypto/tmhash" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmtversion "github.com/cometbft/cometbft/proto/tendermint/version" + "github.com/cometbft/cometbft/types" + cmttime "github.com/cometbft/cometbft/types/time" + "github.com/cometbft/cometbft/version" +) + +// file is adapted from https://github.com/cometbft/cometbft/blob/9267594e0a17c01cc4a97b399ada5eaa8a734db5/light/helpers_test.go#L16 + +// privKeys is a helper type for testing. +// +// It lets us simulate signing with many keys. The main use case is to create +// a set, and call GenSignedHeader to get properly signed header for testing. +// +// You can set different weights of validators each time you call ToValidators, +// and can optionally extend the validator set later with Extend. +type privKeys []crypto.PrivKey + +// genPrivKeys produces an array of private keys to generate commits. +func GenPrivKeys(n int) privKeys { + res := make(privKeys, n) + for i := range res { + res[i] = ed25519.GenPrivKey() + } + return res +} + +// // Change replaces the key at index i. +// func (pkz privKeys) Change(i int) privKeys { +// res := make(privKeys, len(pkz)) +// copy(res, pkz) +// res[i] = ed25519.GenPrivKey() +// return res +// } + +// Extend adds n more keys (to remove, just take a slice). +func (pkz privKeys) Extend(n int) privKeys { + extra := GenPrivKeys(n) + return append(pkz, extra...) +} + +// // GenSecpPrivKeys produces an array of secp256k1 private keys to generate commits. +// func GenSecpPrivKeys(n int) privKeys { +// res := make(privKeys, n) +// for i := range res { +// res[i] = secp256k1.GenPrivKey() +// } +// return res +// } + +// // ExtendSecp adds n more secp256k1 keys (to remove, just take a slice). +// func (pkz privKeys) ExtendSecp(n int) privKeys { +// extra := GenSecpPrivKeys(n) +// return append(pkz, extra...) +// } + +// ToValidators produces a valset from the set of keys. +// The first key has weight `init` and it increases by `inc` every step +// so we can have all the same weight, or a simple linear distribution +// (should be enough for testing). +func (pkz privKeys) ToValidators(init, inc int64) *types.ValidatorSet { + res := make([]*types.Validator, len(pkz)) + for i, k := range pkz { + res[i] = types.NewValidator(k.PubKey(), init+int64(i)*inc) + } + return types.NewValidatorSet(res) +} + +// signHeader properly signs the header with all keys from first to last exclusive. +func (pkz privKeys) signHeader(header *types.Header, valSet *types.ValidatorSet, first, last int) *types.Commit { + commitSigs := make([]types.CommitSig, len(pkz)) + for i := 0; i < len(pkz); i++ { + commitSigs[i] = types.NewCommitSigAbsent() + } + + blockID := types.BlockID{ + Hash: header.Hash(), + PartSetHeader: types.PartSetHeader{Total: 1, Hash: crypto.CRandBytes(32)}, + } + + // Fill in the votes we want. + for i := first; i < last && i < len(pkz); i++ { + vote := makeVote(header, valSet, pkz[i], blockID) + commitSigs[vote.ValidatorIndex] = vote.CommitSig() + } + + return types.NewCommit(header.Height, 1, blockID, commitSigs) +} + +func makeVote(header *types.Header, valset *types.ValidatorSet, + key crypto.PrivKey, blockID types.BlockID, +) *types.Vote { + addr := key.PubKey().Address() + idx, _ := valset.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: header.Height, + Round: 1, + Timestamp: cmttime.Now(), + Type: cmtproto.PrecommitType, + BlockID: blockID, + } + + v := vote.ToProto() + // Sign it + signBytes := types.VoteSignBytes(header.ChainID, v) + sig, err := key.Sign(signBytes) + if err != nil { + panic(err) + } + + vote.Signature = sig + + return vote +} + +func genHeader(chainID string, height int64, bTime time.Time, txs types.Txs, + valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, +) *types.Header { + return &types.Header{ + Version: cmtversion.Consensus{Block: version.BlockProtocol, App: 0}, + ChainID: chainID, + Height: height, + Time: bTime, + // LastBlockID + // LastCommitHash + ValidatorsHash: valset.Hash(), + NextValidatorsHash: nextValset.Hash(), + DataHash: txs.Hash(), + AppHash: appHash, + ConsensusHash: consHash, + LastResultsHash: resHash, + ProposerAddress: valset.Validators[0].Address, + } +} + +// GenSignedHeader calls genHeader and signHeader and combines them into a SignedHeader. +func (pkz privKeys) GenSignedHeader(chainID string, height int64, bTime time.Time, txs types.Txs, + valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int, +) *types.SignedHeader { + header := genHeader(chainID, height, bTime, txs, valset, nextValset, appHash, consHash, resHash) + return &types.SignedHeader{ + Header: header, + Commit: pkz.signHeader(header, valset, first, last), + } +} + +// GenSignedHeaderLastBlockID calls genHeader and signHeader and combines them into a SignedHeader. +func (pkz privKeys) GenSignedHeaderLastBlockID(chainID string, height int64, bTime time.Time, txs types.Txs, + valset, nextValset *types.ValidatorSet, appHash, consHash, resHash []byte, first, last int, + lastBlockID types.BlockID, +) *types.SignedHeader { + header := genHeader(chainID, height, bTime, txs, valset, nextValset, appHash, consHash, resHash) + header.LastBlockID = lastBlockID + return &types.SignedHeader{ + Header: header, + Commit: pkz.signHeader(header, valset, first, last), + } +} + +func (pkz privKeys) ChangeKeys(delta int) privKeys { + newKeys := pkz[delta:] + return newKeys.Extend(delta) +} + +// Generates the header and validator set to create a full entire mock node with blocks to height ( +// blockSize) and with variation in validator sets. BlockIntervals are in per minute. +// NOTE: Expected to have a large validator set size ~ 100 validators. +func genMockNodeWithKeys( + chainID string, + blockSize int64, + valSize int, + valVariation float32, + bTime time.Time) ( + map[int64]*types.SignedHeader, + map[int64]*types.ValidatorSet, + map[int64]privKeys, +) { + var ( + headers = make(map[int64]*types.SignedHeader, blockSize) + valset = make(map[int64]*types.ValidatorSet, blockSize+1) + keymap = make(map[int64]privKeys, blockSize+1) + keys = GenPrivKeys(valSize) + totalVariation = valVariation + valVariationInt int + newKeys privKeys + ) + + valVariationInt = int(totalVariation) + totalVariation = -float32(valVariationInt) + newKeys = keys.ChangeKeys(valVariationInt) + keymap[1] = keys + keymap[2] = newKeys + + // genesis header and vals + lastHeader := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Minute), nil, + keys.ToValidators(2, 0), newKeys.ToValidators(2, 0), hash("app_hash"), hash("cons_hash"), + hash("results_hash"), 0, len(keys)) + currentHeader := lastHeader + headers[1] = currentHeader + valset[1] = keys.ToValidators(2, 0) + keys = newKeys + + for height := int64(2); height <= blockSize; height++ { + totalVariation += valVariation + valVariationInt = int(totalVariation) + totalVariation = -float32(valVariationInt) + newKeys = keys.ChangeKeys(valVariationInt) + currentHeader = keys.GenSignedHeaderLastBlockID(chainID, height, bTime.Add(time.Duration(height)*time.Minute), + nil, + keys.ToValidators(2, 0), newKeys.ToValidators(2, 0), hash("app_hash"), hash("cons_hash"), + hash("results_hash"), 0, len(keys), types.BlockID{Hash: lastHeader.Hash()}) + headers[height] = currentHeader + valset[height] = keys.ToValidators(2, 0) + lastHeader = currentHeader + keys = newKeys + keymap[height+1] = keys + } + + return headers, valset, keymap +} + +func genMockNode( + chainID string, + blockSize int64, + valSize int, + valVariation float32, + bTime time.Time) ( + string, + map[int64]*types.SignedHeader, + map[int64]*types.ValidatorSet, +) { + headers, valset, _ := genMockNodeWithKeys(chainID, blockSize, valSize, valVariation, bTime) + return chainID, headers, valset +} + +func hash(s string) []byte { + return tmhash.Sum([]byte(s)) +} diff --git a/cometmock/utils/blocks.go b/cometmock/utils/blocks.go new file mode 100644 index 0000000..458cc03 --- /dev/null +++ b/cometmock/utils/blocks.go @@ -0,0 +1,14 @@ +package utils + +import "github.com/cometbft/cometbft/types" + +func GetBlockIdFromBlock(block *types.Block) (*types.BlockID, error) { + partSet := block.MakePartSet(2) + + partSetHeader := partSet.Header() + blockID := types.BlockID{ + Hash: block.Hash(), + PartSetHeader: partSetHeader, + } + return &blockID, nil +} diff --git a/cometmock/utils/votes.go b/cometmock/utils/votes.go new file mode 100644 index 0000000..4a10b0d --- /dev/null +++ b/cometmock/utils/votes.go @@ -0,0 +1,44 @@ +package utils + +import ( + "time" + + cmttypes "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cometbft/cometbft/types" +) + +// MakeVote creates a signed vote. +// Adapted from https://github.com/cometbft/cometbft/blob/9267594e0a17c01cc4a97b399ada5eaa8a734db5/internal/test/vote.go#L10. +func MakeVote( + val types.PrivValidator, // PrivValidator is the validator that will sign the vote. + chainID string, + valIndex int32, + height int64, + round int32, + step int, // StepType is the step in the consensus process, see https://github.com/cometbft/cometbft/blob/9267594e0a17c01cc4a97b399ada5eaa8a734db5/proto/tendermint/types/types.pb.go#L68 + blockID types.BlockID, + time time.Time, +) (*types.Vote, error) { + pubKey, err := val.GetPubKey() + if err != nil { + return nil, err + } + + v := &types.Vote{ + ValidatorAddress: pubKey.Address(), + ValidatorIndex: valIndex, + Height: height, + Round: round, + Type: cmttypes.SignedMsgType(step), + BlockID: blockID, + Timestamp: time, + } + + vpb := v.ToProto() + if err := val.SignVote(chainID, vpb); err != nil { + return nil, err + } + + v.Signature = vpb.Signature + return v, nil +} diff --git a/genesis.json b/genesis.json new file mode 100644 index 0000000..d212185 --- /dev/null +++ b/genesis.json @@ -0,0 +1,270 @@ +{ + "genesis_time": "2022-09-27T14:43:24.802423659Z", + "chain_id": "tendermock", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": {} + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos134r9s82qv8fprz3y7fw5lv40yuvsh285vxev02", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos153rpdnp3jcq4kpac8njlyf4gmf724hm6repu72", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos1x63y2p7wzsyf9ln0at56vdpe3x66jaf9qzh86t", + "pub_key": null, + "account_number": "0", + "sequence": "0" + } + ] + }, + "authz": { + "authorization": [] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "cosmos1x63y2p7wzsyf9ln0at56vdpe3x66jaf9qzh86t", + "coins": [ + { + "denom": "stake", + "amount": "5000000000" + } + ] + }, + { + "address": "cosmos134r9s82qv8fprz3y7fw5lv40yuvsh285vxev02", + "coins": [ + { + "denom": "stake", + "amount": "5000000000" + } + ] + }, + { + "address": "cosmos153rpdnp3jcq4kpac8njlyf4gmf724hm6repu72", + "coins": [ + { + "denom": "stake", + "amount": "5000000000" + } + ] + } + ], + "supply": [ + { + "denom": "stake", + "amount": "15000000000" + } + ], + "denom_metadata": [] + }, + "capability": { + "index": "1", + "owners": [] + }, + "crisis": { + "constant_fee": { + "denom": "stake", + "amount": "1000" + } + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evidence": { + "evidence": [] + }, + "feegrant": { + "allowances": [] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "node", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "cosmos1x63y2p7wzsyf9ln0at56vdpe3x66jaf9qzh86t", + "validator_address": "cosmosvaloper1x63y2p7wzsyf9ln0at56vdpe3x66jaf99krjkc", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "pZR7fq8nmVbyJaUhV9jvlzHOG01vJQjCHi8Pb5k8m/8=" + }, + "value": { + "denom": "stake", + "amount": "5000000000" + } + } + ], + "memo": "919555b1567d03bc0ff2a6a885cc9a3e8098db49@172.17.0.2:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsBiSVZ2Ht/+o6qgBrjlRbkfRw3sJDkb7ew1GedJCbsM" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [], + "gas_limit": "200000", + "payer": "", + "granter": "" + } + }, + "signatures": [ + "LT99RZPm6rpPsJ0ERVQkCnkI85pk57QYUkoPoTAYnX8QghrUFRIAIqFHx/SgxiRpq6XBl3hZTusNRgyqxweyNA==" + ] + } + ] + }, + "gov": { + "starting_proposal_id": "1", + "deposits": [], + "votes": [], + "proposals": [], + "deposit_params": { + "min_deposit": [ + { + "denom": "stake", + "amount": "10000000" + } + ], + "max_deposit_period": "172800s" + }, + "voting_params": { + "voting_period": "172800s" + }, + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + } + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "stake", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "params": null, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "1814400s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "stake" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + }, + "upgrade": {}, + "vesting": {} + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 828dfec..49a5b73 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,16 @@ module github.com/p-offtermatt/CometMock go 1.20 -require github.com/cometbft/cometbft v0.37.1 +require ( + github.com/cometbft/cometbft v0.34.27-alpha.1 + github.com/cometbft/cometbft-db v0.7.0 +) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/cometbft/cometbft-db v0.7.0 // indirect - github.com/cosmos/gogoproto v1.4.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect @@ -19,9 +20,11 @@ require ( github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/orderedcode v0.0.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/jmhodges/levigo v1.0.0 // indirect @@ -29,6 +32,8 @@ require ( github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect + github.com/onsi/ginkgo v1.16.4 // indirect + github.com/onsi/gomega v1.19.0 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect diff --git a/go.sum b/go.sum index 0d30300..f2560b7 100644 --- a/go.sum +++ b/go.sum @@ -62,16 +62,14 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cometbft/cometbft v0.37.1 h1:KLxkQTK2hICXYq21U2hn1W5hOVYUdQgDQ1uB+90xPIg= -github.com/cometbft/cometbft v0.37.1/go.mod h1:Y2MMMN//O5K4YKd8ze4r9jmk4Y7h0ajqILXbH5JQFVs= +github.com/cometbft/cometbft v0.34.27-alpha.1 h1:GvmlB422TYhj+6a/A7VV33iRxp/F2IT3aiqExc3s2Ew= +github.com/cometbft/cometbft v0.34.27-alpha.1/go.mod h1:hct3hasQ2hIF3HoD7foVw4RaqTNSSeJ/lgcrVK6uDvs= github.com/cometbft/cometbft-db v0.7.0 h1:uBjbrBx4QzU0zOEnU8KxoDl18dMNgDh+zZRUE0ucsbo= github.com/cometbft/cometbft-db v0.7.0/go.mod h1:yiKJIm2WKrt6x8Cyxtq9YTEcIMPcEe4XPxhgX59Fzf0= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= -github.com/cosmos/gogoproto v1.4.1 h1:WoyH+0/jbCTzpKNvyav5FL1ZTWsp1im1MxEpJEzKUB8= -github.com/cosmos/gogoproto v1.4.1/go.mod h1:Ac9lzL4vFpBMcptJROQ6dQ4M3pOEK5Z/l0Q9p+LoCr4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -116,7 +114,10 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -165,6 +166,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= +github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -197,6 +200,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= @@ -225,15 +229,18 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= @@ -291,6 +298,7 @@ github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -301,6 +309,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -378,6 +387,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -398,6 +408,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -439,7 +450,9 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -499,13 +512,15 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=