mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-21 07:24:36 +00:00
Revert "delete everything" (includes everything non-go-crypto)
This reverts commit 96a3502
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
)
|
||||
|
||||
type (
|
||||
ErrInvalidBlock error
|
||||
ErrProxyAppConn error
|
||||
|
||||
ErrUnknownBlock struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrBlockHashMismatch struct {
|
||||
CoreHash []byte
|
||||
AppHash []byte
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrAppBlockHeightTooHigh struct {
|
||||
CoreHeight int64
|
||||
AppHeight int64
|
||||
}
|
||||
|
||||
ErrLastStateMismatch struct {
|
||||
Height int64
|
||||
Core []byte
|
||||
App []byte
|
||||
}
|
||||
|
||||
ErrStateMismatch struct {
|
||||
Got *State
|
||||
Expected *State
|
||||
}
|
||||
|
||||
ErrNoValSetForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrNoConsensusParamsForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrNoABCIResponsesForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrUnknownBlock) Error() string {
|
||||
return cmn.Fmt("Could not find block #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrBlockHashMismatch) Error() string {
|
||||
return cmn.Fmt("App block hash (%X) does not match core block hash (%X) for height %d", e.AppHash, e.CoreHash, e.Height)
|
||||
}
|
||||
|
||||
func (e ErrAppBlockHeightTooHigh) Error() string {
|
||||
return cmn.Fmt("App block height (%d) is higher than core (%d)", e.AppHeight, e.CoreHeight)
|
||||
}
|
||||
func (e ErrLastStateMismatch) Error() string {
|
||||
return cmn.Fmt("Latest tendermint block (%d) LastAppHash (%X) does not match app's AppHash (%X)", e.Height, e.Core, e.App)
|
||||
}
|
||||
|
||||
func (e ErrStateMismatch) Error() string {
|
||||
return cmn.Fmt("State after replay does not match saved state. Got ----\n%v\nExpected ----\n%v\n", e.Got, e.Expected)
|
||||
}
|
||||
|
||||
func (e ErrNoValSetForHeight) Error() string {
|
||||
return cmn.Fmt("Could not find validator set for height #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrNoConsensusParamsForHeight) Error() string {
|
||||
return cmn.Fmt("Could not find consensus params for height #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrNoABCIResponsesForHeight) Error() string {
|
||||
return cmn.Fmt("Could not find results for height #%d", e.Height)
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
fail "github.com/ebuchman/fail-test"
|
||||
abci "github.com/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/proxy"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// BlockExecutor handles block execution and state updates.
|
||||
// It exposes ApplyBlock(), which validates & executes the block, updates state w/ ABCI responses,
|
||||
// then commits and updates the mempool atomically, then saves state.
|
||||
|
||||
// BlockExecutor provides the context and accessories for properly executing a block.
|
||||
type BlockExecutor struct {
|
||||
// save state, validators, consensus params, abci responses here
|
||||
db dbm.DB
|
||||
|
||||
// execute the app against this
|
||||
proxyApp proxy.AppConnConsensus
|
||||
|
||||
// events
|
||||
eventBus types.BlockEventPublisher
|
||||
|
||||
// update these with block results after commit
|
||||
mempool Mempool
|
||||
evpool EvidencePool
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// NewBlockExecutor returns a new BlockExecutor with a NopEventBus.
|
||||
// Call SetEventBus to provide one.
|
||||
func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsensus,
|
||||
mempool Mempool, evpool EvidencePool) *BlockExecutor {
|
||||
return &BlockExecutor{
|
||||
db: db,
|
||||
proxyApp: proxyApp,
|
||||
eventBus: types.NopEventBus{},
|
||||
mempool: mempool,
|
||||
evpool: evpool,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventBus - sets the event bus for publishing block related events.
|
||||
// If not called, it defaults to types.NopEventBus.
|
||||
func (blockExec *BlockExecutor) SetEventBus(eventBus types.BlockEventPublisher) {
|
||||
blockExec.eventBus = eventBus
|
||||
}
|
||||
|
||||
// ValidateBlock validates the given block against the given state.
|
||||
// If the block is invalid, it returns an error.
|
||||
// Validation does not mutate state, but does require historical information from the stateDB,
|
||||
// ie. to verify evidence from a validator at an old height.
|
||||
func (blockExec *BlockExecutor) ValidateBlock(state State, block *types.Block) error {
|
||||
return validateBlock(blockExec.db, state, block)
|
||||
}
|
||||
|
||||
// ApplyBlock validates the block against the state, executes it against the app,
|
||||
// fires the relevant events, commits the app, and saves the new state and responses.
|
||||
// It's the only function that needs to be called
|
||||
// from outside this package to process and commit an entire block.
|
||||
// It takes a blockID to avoid recomputing the parts hash.
|
||||
func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, block *types.Block) (State, error) {
|
||||
|
||||
if err := blockExec.ValidateBlock(state, block); err != nil {
|
||||
return state, ErrInvalidBlock(err)
|
||||
}
|
||||
|
||||
abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, state.LastValidators, blockExec.db)
|
||||
if err != nil {
|
||||
return state, ErrProxyAppConn(err)
|
||||
}
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// save the results before we commit
|
||||
saveABCIResponses(blockExec.db, block.Height, abciResponses)
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// update the state with the block and responses
|
||||
state, err = updateState(state, blockID, block.Header, abciResponses)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("Commit failed for application: %v", err)
|
||||
}
|
||||
|
||||
// lock mempool, commit app state, update mempoool
|
||||
appHash, err := blockExec.Commit(block)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("Commit failed for application: %v", err)
|
||||
}
|
||||
|
||||
// Update evpool with the block and state.
|
||||
blockExec.evpool.Update(block, state)
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// update the app hash and save the state
|
||||
state.AppHash = appHash
|
||||
SaveState(blockExec.db, state)
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// events are fired after everything else
|
||||
// NOTE: if we crash between Commit and Save, events wont be fired during replay
|
||||
fireEvents(blockExec.logger, blockExec.eventBus, block, abciResponses)
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// Commit locks the mempool, runs the ABCI Commit message, and updates the mempool.
|
||||
// It returns the result of calling abci.Commit (the AppHash), and an error.
|
||||
// The Mempool must be locked during commit and update because state is typically reset on Commit and old txs must be replayed
|
||||
// against committed state before new txs are run in the mempool, lest they be invalid.
|
||||
func (blockExec *BlockExecutor) Commit(block *types.Block) ([]byte, error) {
|
||||
blockExec.mempool.Lock()
|
||||
defer blockExec.mempool.Unlock()
|
||||
|
||||
// while mempool is Locked, flush to ensure all async requests have completed
|
||||
// in the ABCI app before Commit.
|
||||
err := blockExec.mempool.FlushAppConn()
|
||||
if err != nil {
|
||||
blockExec.logger.Error("Client error during mempool.FlushAppConn", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit block, get hash back
|
||||
res, err := blockExec.proxyApp.CommitSync()
|
||||
if err != nil {
|
||||
blockExec.logger.Error("Client error during proxyAppConn.CommitSync", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
// ResponseCommit has no error code - just data
|
||||
|
||||
blockExec.logger.Info("Committed state",
|
||||
"height", block.Height,
|
||||
"txs", block.NumTxs,
|
||||
"appHash", fmt.Sprintf("%X", res.Data))
|
||||
|
||||
// Update mempool.
|
||||
if err := blockExec.mempool.Update(block.Height, block.Txs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Helper functions for executing blocks and updating state
|
||||
|
||||
// Executes block's transactions on proxyAppConn.
|
||||
// Returns a list of transaction results and updates to the validator set
|
||||
func execBlockOnProxyApp(logger log.Logger, proxyAppConn proxy.AppConnConsensus,
|
||||
block *types.Block, lastValSet *types.ValidatorSet, stateDB dbm.DB) (*ABCIResponses, error) {
|
||||
var validTxs, invalidTxs = 0, 0
|
||||
|
||||
txIndex := 0
|
||||
abciResponses := NewABCIResponses(block)
|
||||
|
||||
// Execute transactions and get hash
|
||||
proxyCb := func(req *abci.Request, res *abci.Response) {
|
||||
switch r := res.Value.(type) {
|
||||
case *abci.Response_DeliverTx:
|
||||
// TODO: make use of res.Log
|
||||
// TODO: make use of this info
|
||||
// Blocks may include invalid txs.
|
||||
txRes := r.DeliverTx
|
||||
if txRes.Code == abci.CodeTypeOK {
|
||||
validTxs++
|
||||
} else {
|
||||
logger.Debug("Invalid tx", "code", txRes.Code, "log", txRes.Log)
|
||||
invalidTxs++
|
||||
}
|
||||
abciResponses.DeliverTx[txIndex] = txRes
|
||||
txIndex++
|
||||
}
|
||||
}
|
||||
proxyAppConn.SetResponseCallback(proxyCb)
|
||||
|
||||
signVals, byzVals := getBeginBlockValidatorInfo(block, lastValSet, stateDB)
|
||||
|
||||
// Begin block
|
||||
_, err := proxyAppConn.BeginBlockSync(abci.RequestBeginBlock{
|
||||
Hash: block.Hash(),
|
||||
Header: types.TM2PB.Header(block.Header),
|
||||
Validators: signVals,
|
||||
ByzantineValidators: byzVals,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error in proxyAppConn.BeginBlock", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Run txs of block
|
||||
for _, tx := range block.Txs {
|
||||
proxyAppConn.DeliverTxAsync(tx)
|
||||
if err := proxyAppConn.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// End block
|
||||
abciResponses.EndBlock, err = proxyAppConn.EndBlockSync(abci.RequestEndBlock{block.Height})
|
||||
if err != nil {
|
||||
logger.Error("Error in proxyAppConn.EndBlock", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("Executed block", "height", block.Height, "validTxs", validTxs, "invalidTxs", invalidTxs)
|
||||
|
||||
valUpdates := abciResponses.EndBlock.ValidatorUpdates
|
||||
if len(valUpdates) > 0 {
|
||||
logger.Info("Updates to validators", "updates", abci.ValidatorsString(valUpdates))
|
||||
}
|
||||
|
||||
return abciResponses, nil
|
||||
}
|
||||
|
||||
func getBeginBlockValidatorInfo(block *types.Block, lastValSet *types.ValidatorSet, stateDB dbm.DB) ([]abci.SigningValidator, []abci.Evidence) {
|
||||
|
||||
// Sanity check that commit length matches validator set size -
|
||||
// only applies after first block
|
||||
if block.Height > 1 {
|
||||
precommitLen := len(block.LastCommit.Precommits)
|
||||
valSetLen := len(lastValSet.Validators)
|
||||
if precommitLen != valSetLen {
|
||||
// sanity check
|
||||
panic(fmt.Sprintf("precommit length (%d) doesn't match valset length (%d) at height %d\n\n%v\n\n%v",
|
||||
precommitLen, valSetLen, block.Height, block.LastCommit.Precommits, lastValSet.Validators))
|
||||
}
|
||||
}
|
||||
|
||||
// determine which validators did not sign last block.
|
||||
signVals := make([]abci.SigningValidator, len(lastValSet.Validators))
|
||||
for i, val := range lastValSet.Validators {
|
||||
var vote *types.Vote
|
||||
if i < len(block.LastCommit.Precommits) {
|
||||
vote = block.LastCommit.Precommits[i]
|
||||
}
|
||||
val := abci.SigningValidator{
|
||||
Validator: types.TM2PB.Validator(val),
|
||||
SignedLastBlock: vote != nil,
|
||||
}
|
||||
signVals[i] = val
|
||||
}
|
||||
|
||||
byzVals := make([]abci.Evidence, len(block.Evidence.Evidence))
|
||||
for i, ev := range block.Evidence.Evidence {
|
||||
// We need the validator set. We already did this in validateBlock.
|
||||
// TODO: Should we instead cache the valset in the evidence itself and add
|
||||
// `SetValidatorSet()` and `ToABCI` methods ?
|
||||
valset, err := LoadValidators(stateDB, ev.Height())
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
byzVals[i] = types.TM2PB.Evidence(ev, valset, block.Time)
|
||||
}
|
||||
|
||||
return signVals, byzVals
|
||||
|
||||
}
|
||||
|
||||
// If more or equal than 1/3 of total voting power changed in one block, then
|
||||
// a light client could never prove the transition externally. See
|
||||
// ./lite/doc.go for details on how a light client tracks validators.
|
||||
func updateValidators(currentSet *types.ValidatorSet, abciUpdates []abci.Validator) error {
|
||||
updates, err := types.PB2TM.Validators(abciUpdates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// these are tendermint types now
|
||||
for _, valUpdate := range updates {
|
||||
address := valUpdate.Address
|
||||
_, val := currentSet.GetByAddress(address)
|
||||
if val == nil {
|
||||
// add val
|
||||
added := currentSet.Add(valUpdate)
|
||||
if !added {
|
||||
return fmt.Errorf("Failed to add new validator %v", valUpdate)
|
||||
}
|
||||
} else if valUpdate.VotingPower == 0 {
|
||||
// remove val
|
||||
_, removed := currentSet.Remove(address)
|
||||
if !removed {
|
||||
return fmt.Errorf("Failed to remove validator %X", address)
|
||||
}
|
||||
} else {
|
||||
// update val
|
||||
updated := currentSet.Update(valUpdate)
|
||||
if !updated {
|
||||
return fmt.Errorf("Failed to update validator %X to %v", address, valUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateState returns a new State updated according to the header and responses.
|
||||
func updateState(state State, blockID types.BlockID, header *types.Header,
|
||||
abciResponses *ABCIResponses) (State, error) {
|
||||
|
||||
// copy the valset so we can apply changes from EndBlock
|
||||
// and update s.LastValidators and s.Validators
|
||||
prevValSet := state.Validators.Copy()
|
||||
nextValSet := prevValSet.Copy()
|
||||
|
||||
// update the validator set with the latest abciResponses
|
||||
lastHeightValsChanged := state.LastHeightValidatorsChanged
|
||||
if len(abciResponses.EndBlock.ValidatorUpdates) > 0 {
|
||||
err := updateValidators(nextValSet, abciResponses.EndBlock.ValidatorUpdates)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("Error changing validator set: %v", err)
|
||||
}
|
||||
// change results from this height but only applies to the next height
|
||||
lastHeightValsChanged = header.Height + 1
|
||||
}
|
||||
|
||||
// Update validator accums and set state variables
|
||||
nextValSet.IncrementAccum(1)
|
||||
|
||||
// update the params with the latest abciResponses
|
||||
nextParams := state.ConsensusParams
|
||||
lastHeightParamsChanged := state.LastHeightConsensusParamsChanged
|
||||
if abciResponses.EndBlock.ConsensusParamUpdates != nil {
|
||||
// NOTE: must not mutate s.ConsensusParams
|
||||
nextParams = state.ConsensusParams.Update(abciResponses.EndBlock.ConsensusParamUpdates)
|
||||
err := nextParams.Validate()
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("Error updating consensus params: %v", err)
|
||||
}
|
||||
// change results from this height but only applies to the next height
|
||||
lastHeightParamsChanged = header.Height + 1
|
||||
}
|
||||
|
||||
// NOTE: the AppHash has not been populated.
|
||||
// It will be filled on state.Save.
|
||||
return State{
|
||||
ChainID: state.ChainID,
|
||||
LastBlockHeight: header.Height,
|
||||
LastBlockTotalTx: state.LastBlockTotalTx + header.NumTxs,
|
||||
LastBlockID: blockID,
|
||||
LastBlockTime: header.Time,
|
||||
Validators: nextValSet,
|
||||
LastValidators: state.Validators.Copy(),
|
||||
LastHeightValidatorsChanged: lastHeightValsChanged,
|
||||
ConsensusParams: nextParams,
|
||||
LastHeightConsensusParamsChanged: lastHeightParamsChanged,
|
||||
LastResultsHash: abciResponses.ResultsHash(),
|
||||
AppHash: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Fire NewBlock, NewBlockHeader.
|
||||
// Fire TxEvent for every tx.
|
||||
// NOTE: if Tendermint crashes before commit, some or all of these events may be published again.
|
||||
func fireEvents(logger log.Logger, eventBus types.BlockEventPublisher, block *types.Block, abciResponses *ABCIResponses) {
|
||||
eventBus.PublishEventNewBlock(types.EventDataNewBlock{block})
|
||||
eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{block.Header})
|
||||
|
||||
for i, tx := range block.Data.Txs {
|
||||
eventBus.PublishEventTx(types.EventDataTx{types.TxResult{
|
||||
Height: block.Height,
|
||||
Index: uint32(i),
|
||||
Tx: tx,
|
||||
Result: *(abciResponses.DeliverTx[i]),
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
// Execute block without state. TODO: eliminate
|
||||
|
||||
// ExecCommitBlock executes and commits a block on the proxyApp without validating or mutating the state.
|
||||
// It returns the application root hash (result of abci.Commit).
|
||||
func ExecCommitBlock(appConnConsensus proxy.AppConnConsensus, block *types.Block,
|
||||
logger log.Logger, lastValSet *types.ValidatorSet, stateDB dbm.DB) ([]byte, error) {
|
||||
_, err := execBlockOnProxyApp(logger, appConnConsensus, block, lastValSet, stateDB)
|
||||
if err != nil {
|
||||
logger.Error("Error executing block on proxy app", "height", block.Height, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
// Commit block, get hash back
|
||||
res, err := appConnConsensus.CommitSync()
|
||||
if err != nil {
|
||||
logger.Error("Client error during proxyAppConn.CommitSync", "err", res)
|
||||
return nil, err
|
||||
}
|
||||
// ResponseCommit has no error or log, just data
|
||||
return res.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/abci/example/kvstore"
|
||||
abci "github.com/tendermint/abci/types"
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/proxy"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var (
|
||||
chainID = "execution_chain"
|
||||
testPartSize = 65536
|
||||
nTxsPerBlock = 10
|
||||
)
|
||||
|
||||
func TestApplyBlock(t *testing.T) {
|
||||
cc := proxy.NewLocalClientCreator(kvstore.NewKVStoreApplication())
|
||||
proxyApp := proxy.NewAppConns(cc, nil)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop()
|
||||
|
||||
state, stateDB := state(1, 1)
|
||||
|
||||
blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(),
|
||||
MockMempool{}, MockEvidencePool{})
|
||||
|
||||
block := makeBlock(state, 1)
|
||||
blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()}
|
||||
|
||||
state, err = blockExec.ApplyBlock(state, blockID, block)
|
||||
require.Nil(t, err)
|
||||
|
||||
// TODO check state and mempool
|
||||
}
|
||||
|
||||
// TestBeginBlockValidators ensures we send absent validators list.
|
||||
func TestBeginBlockValidators(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := proxy.NewLocalClientCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc, nil)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop()
|
||||
|
||||
state, stateDB := state(2, 2)
|
||||
|
||||
prevHash := state.LastBlockID.Hash
|
||||
prevParts := types.PartSetHeader{}
|
||||
prevBlockID := types.BlockID{prevHash, prevParts}
|
||||
|
||||
now := time.Now().UTC()
|
||||
vote0 := &types.Vote{ValidatorIndex: 0, Timestamp: now, Type: types.VoteTypePrecommit}
|
||||
vote1 := &types.Vote{ValidatorIndex: 1, Timestamp: now}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
lastCommitPrecommits []*types.Vote
|
||||
expectedAbsentValidators []int
|
||||
}{
|
||||
{"none absent", []*types.Vote{vote0, vote1}, []int{}},
|
||||
{"one absent", []*types.Vote{vote0, nil}, []int{1}},
|
||||
{"multiple absent", []*types.Vote{nil, nil}, []int{0, 1}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
lastCommit := &types.Commit{BlockID: prevBlockID, Precommits: tc.lastCommitPrecommits}
|
||||
|
||||
// block for height 2
|
||||
block, _ := state.MakeBlock(2, makeTxs(2), lastCommit)
|
||||
_, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), state.Validators, stateDB)
|
||||
require.Nil(t, err, tc.desc)
|
||||
|
||||
// -> app receives a list of validators with a bool indicating if they signed
|
||||
ctr := 0
|
||||
for i, v := range app.Validators {
|
||||
if ctr < len(tc.expectedAbsentValidators) &&
|
||||
tc.expectedAbsentValidators[ctr] == i {
|
||||
|
||||
assert.False(t, v.SignedLastBlock)
|
||||
ctr++
|
||||
} else {
|
||||
assert.True(t, v.SignedLastBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBeginBlockByzantineValidators ensures we send byzantine validators list.
|
||||
func TestBeginBlockByzantineValidators(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := proxy.NewLocalClientCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc, nil)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop()
|
||||
|
||||
state, stateDB := state(2, 12)
|
||||
|
||||
prevHash := state.LastBlockID.Hash
|
||||
prevParts := types.PartSetHeader{}
|
||||
prevBlockID := types.BlockID{prevHash, prevParts}
|
||||
|
||||
height1, idx1, val1 := int64(8), 0, state.Validators.Validators[0].Address
|
||||
height2, idx2, val2 := int64(3), 1, state.Validators.Validators[1].Address
|
||||
ev1 := types.NewMockGoodEvidence(height1, idx1, val1)
|
||||
ev2 := types.NewMockGoodEvidence(height2, idx2, val2)
|
||||
|
||||
now := time.Now()
|
||||
valSet := state.Validators
|
||||
testCases := []struct {
|
||||
desc string
|
||||
evidence []types.Evidence
|
||||
expectedByzantineValidators []abci.Evidence
|
||||
}{
|
||||
{"none byzantine", []types.Evidence{}, []abci.Evidence{}},
|
||||
{"one byzantine", []types.Evidence{ev1}, []abci.Evidence{types.TM2PB.Evidence(ev1, valSet, now)}},
|
||||
{"multiple byzantine", []types.Evidence{ev1, ev2}, []abci.Evidence{
|
||||
types.TM2PB.Evidence(ev1, valSet, now),
|
||||
types.TM2PB.Evidence(ev2, valSet, now)}},
|
||||
}
|
||||
|
||||
vote0 := &types.Vote{ValidatorIndex: 0, Timestamp: now, Type: types.VoteTypePrecommit}
|
||||
vote1 := &types.Vote{ValidatorIndex: 1, Timestamp: now}
|
||||
votes := []*types.Vote{vote0, vote1}
|
||||
lastCommit := &types.Commit{BlockID: prevBlockID, Precommits: votes}
|
||||
for _, tc := range testCases {
|
||||
|
||||
block, _ := state.MakeBlock(10, makeTxs(2), lastCommit)
|
||||
block.Time = now
|
||||
block.Evidence.Evidence = tc.evidence
|
||||
_, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), state.Validators, stateDB)
|
||||
require.Nil(t, err, tc.desc)
|
||||
|
||||
// -> app must receive an index of the byzantine validator
|
||||
assert.Equal(t, tc.expectedByzantineValidators, app.ByzantineValidators, tc.desc)
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
// make some bogus txs
|
||||
func makeTxs(height int64) (txs []types.Tx) {
|
||||
for i := 0; i < nTxsPerBlock; i++ {
|
||||
txs = append(txs, types.Tx([]byte{byte(height), byte(i)}))
|
||||
}
|
||||
return txs
|
||||
}
|
||||
|
||||
func state(nVals, height int) (State, dbm.DB) {
|
||||
vals := make([]types.GenesisValidator, nVals)
|
||||
for i := 0; i < nVals; i++ {
|
||||
secret := []byte(fmt.Sprintf("test%d", i))
|
||||
pk := crypto.GenPrivKeyEd25519FromSecret(secret)
|
||||
vals[i] = types.GenesisValidator{
|
||||
pk.PubKey(), 1000, fmt.Sprintf("test%d", i),
|
||||
}
|
||||
}
|
||||
s, _ := MakeGenesisState(&types.GenesisDoc{
|
||||
ChainID: chainID,
|
||||
Validators: vals,
|
||||
AppHash: nil,
|
||||
})
|
||||
|
||||
// save validators to db for 2 heights
|
||||
stateDB := dbm.NewMemDB()
|
||||
SaveState(stateDB, s)
|
||||
|
||||
for i := 1; i < height; i++ {
|
||||
s.LastBlockHeight += 1
|
||||
SaveState(stateDB, s)
|
||||
}
|
||||
return s, stateDB
|
||||
}
|
||||
|
||||
func makeBlock(state State, height int64) *types.Block {
|
||||
block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit))
|
||||
return block
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
var _ abci.Application = (*testApp)(nil)
|
||||
|
||||
type testApp struct {
|
||||
abci.BaseApplication
|
||||
|
||||
Validators []abci.SigningValidator
|
||||
ByzantineValidators []abci.Evidence
|
||||
}
|
||||
|
||||
func NewKVStoreApplication() *testApp {
|
||||
return &testApp{}
|
||||
}
|
||||
|
||||
func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) {
|
||||
return abci.ResponseInfo{}
|
||||
}
|
||||
|
||||
func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock {
|
||||
app.Validators = req.Validators
|
||||
app.ByzantineValidators = req.ByzantineValidators
|
||||
return abci.ResponseBeginBlock{}
|
||||
}
|
||||
|
||||
func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx {
|
||||
return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}}
|
||||
}
|
||||
|
||||
func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx {
|
||||
return abci.ResponseCheckTx{}
|
||||
}
|
||||
|
||||
func (app *testApp) Commit() abci.ResponseCommit {
|
||||
return abci.ResponseCommit{}
|
||||
}
|
||||
|
||||
func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) {
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
abci "github.com/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//------------------------------------------------------
|
||||
// blockchain services types
|
||||
// NOTE: Interfaces used by RPC must be thread safe!
|
||||
//------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------
|
||||
// mempool
|
||||
|
||||
// Mempool defines the mempool interface as used by the ConsensusState.
|
||||
// Updates to the mempool need to be synchronized with committing a block
|
||||
// so apps can reset their transient state on Commit
|
||||
type Mempool interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
|
||||
Size() int
|
||||
CheckTx(types.Tx, func(*abci.Response)) error
|
||||
Reap(int) types.Txs
|
||||
Update(height int64, txs types.Txs) error
|
||||
Flush()
|
||||
FlushAppConn() error
|
||||
|
||||
TxsAvailable() <-chan int64
|
||||
EnableTxsAvailable()
|
||||
}
|
||||
|
||||
// MockMempool is an empty implementation of a Mempool, useful for testing.
|
||||
type MockMempool struct {
|
||||
}
|
||||
|
||||
func (m MockMempool) Lock() {}
|
||||
func (m MockMempool) Unlock() {}
|
||||
func (m MockMempool) Size() int { return 0 }
|
||||
func (m MockMempool) CheckTx(tx types.Tx, cb func(*abci.Response)) error { return nil }
|
||||
func (m MockMempool) Reap(n int) types.Txs { return types.Txs{} }
|
||||
func (m MockMempool) Update(height int64, txs types.Txs) error { return nil }
|
||||
func (m MockMempool) Flush() {}
|
||||
func (m MockMempool) FlushAppConn() error { return nil }
|
||||
func (m MockMempool) TxsAvailable() <-chan int64 { return make(chan int64) }
|
||||
func (m MockMempool) EnableTxsAvailable() {}
|
||||
|
||||
//------------------------------------------------------
|
||||
// blockstore
|
||||
|
||||
// BlockStoreRPC is the block store interface used by the RPC.
|
||||
type BlockStoreRPC interface {
|
||||
Height() int64
|
||||
|
||||
LoadBlockMeta(height int64) *types.BlockMeta
|
||||
LoadBlock(height int64) *types.Block
|
||||
LoadBlockPart(height int64, index int) *types.Part
|
||||
|
||||
LoadBlockCommit(height int64) *types.Commit
|
||||
LoadSeenCommit(height int64) *types.Commit
|
||||
}
|
||||
|
||||
// BlockStore defines the BlockStore interface used by the ConsensusState.
|
||||
type BlockStore interface {
|
||||
BlockStoreRPC
|
||||
SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------------------
|
||||
// evidence pool
|
||||
|
||||
// EvidencePool defines the EvidencePool interface used by the ConsensusState.
|
||||
type EvidencePool interface {
|
||||
PendingEvidence() []types.Evidence
|
||||
AddEvidence(types.Evidence) error
|
||||
Update(*types.Block, State)
|
||||
}
|
||||
|
||||
// MockMempool is an empty implementation of a Mempool, useful for testing.
|
||||
type MockEvidencePool struct {
|
||||
}
|
||||
|
||||
func (m MockEvidencePool) PendingEvidence() []types.Evidence { return nil }
|
||||
func (m MockEvidencePool) AddEvidence(types.Evidence) error { return nil }
|
||||
func (m MockEvidencePool) Update(*types.Block, State) {}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// database keys
|
||||
var (
|
||||
stateKey = []byte("stateKey")
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// State is a short description of the latest committed block of the Tendermint consensus.
|
||||
// It keeps all information necessary to validate new blocks,
|
||||
// including the last validator set and the consensus params.
|
||||
// All fields are exposed so the struct can be easily serialized,
|
||||
// but none of them should be mutated directly.
|
||||
// Instead, use state.Copy() or state.NextState(...).
|
||||
// NOTE: not goroutine-safe.
|
||||
type State struct {
|
||||
// Immutable
|
||||
ChainID string
|
||||
|
||||
// LastBlockHeight=0 at genesis (ie. block(H=0) does not exist)
|
||||
LastBlockHeight int64
|
||||
LastBlockTotalTx int64
|
||||
LastBlockID types.BlockID
|
||||
LastBlockTime time.Time
|
||||
|
||||
// LastValidators is used to validate block.LastCommit.
|
||||
// Validators are persisted to the database separately every time they change,
|
||||
// so we can query for historical validator sets.
|
||||
// Note that if s.LastBlockHeight causes a valset change,
|
||||
// we set s.LastHeightValidatorsChanged = s.LastBlockHeight + 1
|
||||
Validators *types.ValidatorSet
|
||||
LastValidators *types.ValidatorSet
|
||||
LastHeightValidatorsChanged int64
|
||||
|
||||
// Consensus parameters used for validating blocks.
|
||||
// Changes returned by EndBlock and updated after Commit.
|
||||
ConsensusParams types.ConsensusParams
|
||||
LastHeightConsensusParamsChanged int64
|
||||
|
||||
// Merkle root of the results from executing prev block
|
||||
LastResultsHash []byte
|
||||
|
||||
// The latest AppHash we've received from calling abci.Commit()
|
||||
AppHash []byte
|
||||
}
|
||||
|
||||
// Copy makes a copy of the State for mutating.
|
||||
func (state State) Copy() State {
|
||||
return State{
|
||||
ChainID: state.ChainID,
|
||||
|
||||
LastBlockHeight: state.LastBlockHeight,
|
||||
LastBlockTotalTx: state.LastBlockTotalTx,
|
||||
LastBlockID: state.LastBlockID,
|
||||
LastBlockTime: state.LastBlockTime,
|
||||
|
||||
Validators: state.Validators.Copy(),
|
||||
LastValidators: state.LastValidators.Copy(),
|
||||
LastHeightValidatorsChanged: state.LastHeightValidatorsChanged,
|
||||
|
||||
ConsensusParams: state.ConsensusParams,
|
||||
LastHeightConsensusParamsChanged: state.LastHeightConsensusParamsChanged,
|
||||
|
||||
AppHash: state.AppHash,
|
||||
|
||||
LastResultsHash: state.LastResultsHash,
|
||||
}
|
||||
}
|
||||
|
||||
// Equals returns true if the States are identical.
|
||||
func (state State) Equals(state2 State) bool {
|
||||
sbz, s2bz := state.Bytes(), state2.Bytes()
|
||||
return bytes.Equal(sbz, s2bz)
|
||||
}
|
||||
|
||||
// Bytes serializes the State using go-amino.
|
||||
func (state State) Bytes() []byte {
|
||||
return cdc.MustMarshalBinaryBare(state)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the State is equal to the empty State.
|
||||
func (state State) IsEmpty() bool {
|
||||
return state.Validators == nil // XXX can't compare to Empty
|
||||
}
|
||||
|
||||
// GetValidators returns the last and current validator sets.
|
||||
func (state State) GetValidators() (last *types.ValidatorSet, current *types.ValidatorSet) {
|
||||
return state.LastValidators, state.Validators
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// Create a block from the latest state
|
||||
|
||||
// MakeBlock builds a block with the given txs and commit from the current state.
|
||||
func (state State) MakeBlock(height int64, txs []types.Tx, commit *types.Commit) (*types.Block, *types.PartSet) {
|
||||
// build base block
|
||||
block := types.MakeBlock(height, txs, commit)
|
||||
|
||||
// fill header with state data
|
||||
block.ChainID = state.ChainID
|
||||
block.TotalTxs = state.LastBlockTotalTx + block.NumTxs
|
||||
block.LastBlockID = state.LastBlockID
|
||||
block.ValidatorsHash = state.Validators.Hash()
|
||||
block.AppHash = state.AppHash
|
||||
block.ConsensusHash = state.ConsensusParams.Hash()
|
||||
block.LastResultsHash = state.LastResultsHash
|
||||
|
||||
return block, block.MakePartSet(state.ConsensusParams.BlockGossip.BlockPartSizeBytes)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// Genesis
|
||||
|
||||
// MakeGenesisStateFromFile reads and unmarshals state from the given
|
||||
// file.
|
||||
//
|
||||
// Used during replay and in tests.
|
||||
func MakeGenesisStateFromFile(genDocFile string) (State, error) {
|
||||
genDoc, err := MakeGenesisDocFromFile(genDocFile)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
return MakeGenesisState(genDoc)
|
||||
}
|
||||
|
||||
// MakeGenesisDocFromFile reads and unmarshals genesis doc from the given file.
|
||||
func MakeGenesisDocFromFile(genDocFile string) (*types.GenesisDoc, error) {
|
||||
genDocJSON, err := ioutil.ReadFile(genDocFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't read GenesisDoc file: %v", err)
|
||||
}
|
||||
genDoc, err := types.GenesisDocFromJSON(genDocJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading GenesisDoc: %v", err)
|
||||
}
|
||||
return genDoc, nil
|
||||
}
|
||||
|
||||
// MakeGenesisState creates state from types.GenesisDoc.
|
||||
func MakeGenesisState(genDoc *types.GenesisDoc) (State, error) {
|
||||
err := genDoc.ValidateAndComplete()
|
||||
if err != nil {
|
||||
return State{}, fmt.Errorf("Error in genesis file: %v", err)
|
||||
}
|
||||
|
||||
// Make validators slice
|
||||
validators := make([]*types.Validator, len(genDoc.Validators))
|
||||
for i, val := range genDoc.Validators {
|
||||
pubKey := val.PubKey
|
||||
address := pubKey.Address()
|
||||
|
||||
// Make validator
|
||||
validators[i] = &types.Validator{
|
||||
Address: address,
|
||||
PubKey: pubKey,
|
||||
VotingPower: val.Power,
|
||||
}
|
||||
}
|
||||
|
||||
return State{
|
||||
|
||||
ChainID: genDoc.ChainID,
|
||||
|
||||
LastBlockHeight: 0,
|
||||
LastBlockID: types.BlockID{},
|
||||
LastBlockTime: genDoc.GenesisTime,
|
||||
|
||||
Validators: types.NewValidatorSet(validators),
|
||||
LastValidators: types.NewValidatorSet(nil),
|
||||
LastHeightValidatorsChanged: 1,
|
||||
|
||||
ConsensusParams: *genDoc.ConsensusParams,
|
||||
LastHeightConsensusParamsChanged: 1,
|
||||
|
||||
AppHash: genDoc.AppHash,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/abci/types"
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// setupTestCase does setup common to all test cases
|
||||
func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, State) {
|
||||
config := cfg.ResetTestRoot("state_")
|
||||
dbType := dbm.DBBackendType(config.DBBackend)
|
||||
stateDB := dbm.NewDB("state", dbType, config.DBDir())
|
||||
state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile())
|
||||
assert.NoError(t, err, "expected no error on LoadStateFromDBOrGenesisFile")
|
||||
|
||||
tearDown := func(t *testing.T) {}
|
||||
|
||||
return tearDown, stateDB, state
|
||||
}
|
||||
|
||||
// TestStateCopy tests the correct copying behaviour of State.
|
||||
func TestStateCopy(t *testing.T) {
|
||||
tearDown, _, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
// nolint: vetshadow
|
||||
assert := assert.New(t)
|
||||
|
||||
stateCopy := state.Copy()
|
||||
|
||||
assert.True(state.Equals(stateCopy),
|
||||
cmn.Fmt("expected state and its copy to be identical.\ngot: %v\nexpected: %v\n",
|
||||
stateCopy, state))
|
||||
|
||||
stateCopy.LastBlockHeight++
|
||||
assert.False(state.Equals(stateCopy), cmn.Fmt(`expected states to be different. got same
|
||||
%v`, state))
|
||||
}
|
||||
|
||||
// TestStateSaveLoad tests saving and loading State from a db.
|
||||
func TestStateSaveLoad(t *testing.T) {
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
// nolint: vetshadow
|
||||
assert := assert.New(t)
|
||||
|
||||
state.LastBlockHeight++
|
||||
SaveState(stateDB, state)
|
||||
|
||||
loadedState := LoadState(stateDB)
|
||||
assert.True(state.Equals(loadedState),
|
||||
cmn.Fmt("expected state and its copy to be identical.\ngot: %v\nexpected: %v\n",
|
||||
loadedState, state))
|
||||
}
|
||||
|
||||
// TestABCIResponsesSaveLoad tests saving and loading ABCIResponses.
|
||||
func TestABCIResponsesSaveLoad1(t *testing.T) {
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
// nolint: vetshadow
|
||||
assert := assert.New(t)
|
||||
|
||||
state.LastBlockHeight++
|
||||
|
||||
// build mock responses
|
||||
block := makeBlock(state, 2)
|
||||
abciResponses := NewABCIResponses(block)
|
||||
abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: nil}
|
||||
abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: nil}
|
||||
abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.Validator{
|
||||
types.TM2PB.ValidatorFromPubKeyAndPower(crypto.GenPrivKeyEd25519().PubKey(), 10),
|
||||
}}
|
||||
|
||||
saveABCIResponses(stateDB, block.Height, abciResponses)
|
||||
loadedABCIResponses, err := LoadABCIResponses(stateDB, block.Height)
|
||||
assert.Nil(err)
|
||||
assert.Equal(abciResponses, loadedABCIResponses,
|
||||
cmn.Fmt("ABCIResponses don't match:\ngot: %v\nexpected: %v\n",
|
||||
loadedABCIResponses, abciResponses))
|
||||
}
|
||||
|
||||
// TestResultsSaveLoad tests saving and loading abci results.
|
||||
func TestABCIResponsesSaveLoad2(t *testing.T) {
|
||||
tearDown, stateDB, _ := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
// nolint: vetshadow
|
||||
assert := assert.New(t)
|
||||
|
||||
cases := [...]struct {
|
||||
// height is implied index+2
|
||||
// as block 1 is created from genesis
|
||||
added []*abci.ResponseDeliverTx
|
||||
expected types.ABCIResults
|
||||
}{
|
||||
0: {
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
1: {
|
||||
[]*abci.ResponseDeliverTx{
|
||||
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
|
||||
},
|
||||
types.ABCIResults{
|
||||
{32, []byte("Hello")},
|
||||
}},
|
||||
2: {
|
||||
[]*abci.ResponseDeliverTx{
|
||||
{Code: 383},
|
||||
{Data: []byte("Gotcha!"),
|
||||
Tags: []cmn.KVPair{
|
||||
cmn.KVPair{[]byte("a"), []byte("1")},
|
||||
cmn.KVPair{[]byte("build"), []byte("stuff")},
|
||||
}},
|
||||
},
|
||||
types.ABCIResults{
|
||||
{383, nil},
|
||||
{0, []byte("Gotcha!")},
|
||||
}},
|
||||
3: {
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
// query all before, should return error
|
||||
for i := range cases {
|
||||
h := int64(i + 1)
|
||||
res, err := LoadABCIResponses(stateDB, h)
|
||||
assert.Error(err, "%d: %#v", i, res)
|
||||
}
|
||||
|
||||
// add all cases
|
||||
for i, tc := range cases {
|
||||
h := int64(i + 1) // last block height, one below what we save
|
||||
responses := &ABCIResponses{
|
||||
DeliverTx: tc.added,
|
||||
EndBlock: &abci.ResponseEndBlock{},
|
||||
}
|
||||
saveABCIResponses(stateDB, h, responses)
|
||||
}
|
||||
|
||||
// query all before, should return expected value
|
||||
for i, tc := range cases {
|
||||
h := int64(i + 1)
|
||||
res, err := LoadABCIResponses(stateDB, h)
|
||||
assert.NoError(err, "%d", i)
|
||||
assert.Equal(tc.expected.Hash(), res.ResultsHash(), "%d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatorSimpleSaveLoad tests saving and loading validators.
|
||||
func TestValidatorSimpleSaveLoad(t *testing.T) {
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
// nolint: vetshadow
|
||||
assert := assert.New(t)
|
||||
|
||||
// can't load anything for height 0
|
||||
v, err := LoadValidators(stateDB, 0)
|
||||
assert.IsType(ErrNoValSetForHeight{}, err, "expected err at height 0")
|
||||
|
||||
// should be able to load for height 1
|
||||
v, err = LoadValidators(stateDB, 1)
|
||||
assert.Nil(err, "expected no err at height 1")
|
||||
assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match")
|
||||
|
||||
// increment height, save; should be able to load for next height
|
||||
state.LastBlockHeight++
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
saveValidatorsInfo(stateDB, nextHeight, state.LastHeightValidatorsChanged, state.Validators)
|
||||
v, err = LoadValidators(stateDB, nextHeight)
|
||||
assert.Nil(err, "expected no err")
|
||||
assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match")
|
||||
|
||||
// increment height, save; should be able to load for next height
|
||||
state.LastBlockHeight += 10
|
||||
nextHeight = state.LastBlockHeight + 1
|
||||
saveValidatorsInfo(stateDB, nextHeight, state.LastHeightValidatorsChanged, state.Validators)
|
||||
v, err = LoadValidators(stateDB, nextHeight)
|
||||
assert.Nil(err, "expected no err")
|
||||
assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match")
|
||||
|
||||
// should be able to load for next next height
|
||||
_, err = LoadValidators(stateDB, state.LastBlockHeight+2)
|
||||
assert.IsType(ErrNoValSetForHeight{}, err, "expected err at unknown height")
|
||||
}
|
||||
|
||||
// TestValidatorChangesSaveLoad tests saving and loading a validator set with changes.
|
||||
func TestOneValidatorChangesSaveLoad(t *testing.T) {
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// change vals at these heights
|
||||
changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20}
|
||||
N := len(changeHeights)
|
||||
|
||||
// build the validator history by running updateState
|
||||
// with the right validator set for each height
|
||||
highestHeight := changeHeights[N-1] + 5
|
||||
changeIndex := 0
|
||||
_, val := state.Validators.GetByIndex(0)
|
||||
power := val.VotingPower
|
||||
var err error
|
||||
for i := int64(1); i < highestHeight; i++ {
|
||||
// when we get to a change height,
|
||||
// use the next pubkey
|
||||
if changeIndex < len(changeHeights) && i == changeHeights[changeIndex] {
|
||||
changeIndex++
|
||||
power++
|
||||
}
|
||||
header, blockID, responses := makeHeaderPartsResponsesValPowerChange(state, i, power)
|
||||
state, err = updateState(state, blockID, header, responses)
|
||||
assert.Nil(t, err)
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
saveValidatorsInfo(stateDB, nextHeight, state.LastHeightValidatorsChanged, state.Validators)
|
||||
}
|
||||
|
||||
// on each change height, increment the power by one.
|
||||
testCases := make([]int64, highestHeight)
|
||||
changeIndex = 0
|
||||
power = val.VotingPower
|
||||
for i := int64(1); i < highestHeight+1; i++ {
|
||||
// we we get to the height after a change height
|
||||
// use the next pubkey (note our counter starts at 0 this time)
|
||||
if changeIndex < len(changeHeights) && i == changeHeights[changeIndex]+1 {
|
||||
changeIndex++
|
||||
power++
|
||||
}
|
||||
testCases[i-1] = power
|
||||
}
|
||||
|
||||
for i, power := range testCases {
|
||||
v, err := LoadValidators(stateDB, int64(i+1))
|
||||
assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", i))
|
||||
assert.Equal(t, v.Size(), 1, "validator set size is greater than 1: %d", v.Size())
|
||||
_, val := v.GetByIndex(0)
|
||||
|
||||
assert.Equal(t, val.VotingPower, power, fmt.Sprintf(`unexpected powerat
|
||||
height %d`, i))
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatorChangesSaveLoad tests saving and loading a validator set with
|
||||
// changes.
|
||||
func TestManyValidatorChangesSaveLoad(t *testing.T) {
|
||||
const valSetSize = 7
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
state.Validators = genValSet(valSetSize)
|
||||
SaveState(stateDB, state)
|
||||
defer tearDown(t)
|
||||
|
||||
const height = 1
|
||||
pubkey := crypto.GenPrivKeyEd25519().PubKey()
|
||||
// swap the first validator with a new one ^^^ (validator set size stays the same)
|
||||
header, blockID, responses := makeHeaderPartsResponsesValPubKeyChange(state, height, pubkey)
|
||||
var err error
|
||||
state, err = updateState(state, blockID, header, responses)
|
||||
require.Nil(t, err)
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
saveValidatorsInfo(stateDB, nextHeight, state.LastHeightValidatorsChanged, state.Validators)
|
||||
|
||||
v, err := LoadValidators(stateDB, height+1)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, valSetSize, v.Size())
|
||||
|
||||
index, val := v.GetByAddress(pubkey.Address())
|
||||
assert.NotNil(t, val)
|
||||
if index < 0 {
|
||||
t.Fatal("expected to find newly added validator")
|
||||
}
|
||||
}
|
||||
|
||||
func genValSet(size int) *types.ValidatorSet {
|
||||
vals := make([]*types.Validator, size)
|
||||
for i := 0; i < size; i++ {
|
||||
vals[i] = types.NewValidator(crypto.GenPrivKeyEd25519().PubKey(), 10)
|
||||
}
|
||||
return types.NewValidatorSet(vals)
|
||||
}
|
||||
|
||||
// TestConsensusParamsChangesSaveLoad tests saving and loading consensus params
|
||||
// with changes.
|
||||
func TestConsensusParamsChangesSaveLoad(t *testing.T) {
|
||||
tearDown, stateDB, state := setupTestCase(t)
|
||||
defer tearDown(t)
|
||||
|
||||
// change vals at these heights
|
||||
changeHeights := []int64{1, 2, 4, 5, 10, 15, 16, 17, 20}
|
||||
N := len(changeHeights)
|
||||
|
||||
// each valset is just one validator
|
||||
// create list of them
|
||||
params := make([]types.ConsensusParams, N+1)
|
||||
params[0] = state.ConsensusParams
|
||||
for i := 1; i < N+1; i++ {
|
||||
params[i] = *types.DefaultConsensusParams()
|
||||
params[i].BlockSize.MaxBytes += i
|
||||
}
|
||||
|
||||
// build the params history by running updateState
|
||||
// with the right params set for each height
|
||||
highestHeight := changeHeights[N-1] + 5
|
||||
changeIndex := 0
|
||||
cp := params[changeIndex]
|
||||
var err error
|
||||
for i := int64(1); i < highestHeight; i++ {
|
||||
// when we get to a change height,
|
||||
// use the next params
|
||||
if changeIndex < len(changeHeights) && i == changeHeights[changeIndex] {
|
||||
changeIndex++
|
||||
cp = params[changeIndex]
|
||||
}
|
||||
header, blockID, responses := makeHeaderPartsResponsesParams(state, i, cp)
|
||||
state, err = updateState(state, blockID, header, responses)
|
||||
|
||||
require.Nil(t, err)
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
saveConsensusParamsInfo(stateDB, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams)
|
||||
}
|
||||
|
||||
// make all the test cases by using the same params until after the change
|
||||
testCases := make([]paramsChangeTestCase, highestHeight)
|
||||
changeIndex = 0
|
||||
cp = params[changeIndex]
|
||||
for i := int64(1); i < highestHeight+1; i++ {
|
||||
// we we get to the height after a change height
|
||||
// use the next pubkey (note our counter starts at 0 this time)
|
||||
if changeIndex < len(changeHeights) && i == changeHeights[changeIndex]+1 {
|
||||
changeIndex++
|
||||
cp = params[changeIndex]
|
||||
}
|
||||
testCases[i-1] = paramsChangeTestCase{i, cp}
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
p, err := LoadConsensusParams(stateDB, testCase.height)
|
||||
assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", testCase.height))
|
||||
assert.Equal(t, testCase.params, p, fmt.Sprintf(`unexpected consensus params at
|
||||
height %d`, testCase.height))
|
||||
}
|
||||
}
|
||||
|
||||
func makeParams(blockBytes, blockTx, blockGas, txBytes,
|
||||
txGas, partSize int) types.ConsensusParams {
|
||||
|
||||
return types.ConsensusParams{
|
||||
BlockSize: types.BlockSize{
|
||||
MaxBytes: blockBytes,
|
||||
MaxTxs: blockTx,
|
||||
MaxGas: int64(blockGas),
|
||||
},
|
||||
TxSize: types.TxSize{
|
||||
MaxBytes: txBytes,
|
||||
MaxGas: int64(txGas),
|
||||
},
|
||||
BlockGossip: types.BlockGossip{
|
||||
BlockPartSizeBytes: partSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pk() []byte {
|
||||
return crypto.GenPrivKeyEd25519().PubKey().Bytes()
|
||||
}
|
||||
|
||||
func TestApplyUpdates(t *testing.T) {
|
||||
initParams := makeParams(1, 2, 3, 4, 5, 6)
|
||||
|
||||
cases := [...]struct {
|
||||
init types.ConsensusParams
|
||||
updates abci.ConsensusParams
|
||||
expected types.ConsensusParams
|
||||
}{
|
||||
0: {initParams, abci.ConsensusParams{}, initParams},
|
||||
1: {initParams, abci.ConsensusParams{}, initParams},
|
||||
2: {initParams,
|
||||
abci.ConsensusParams{
|
||||
TxSize: &abci.TxSize{
|
||||
MaxBytes: 123,
|
||||
},
|
||||
},
|
||||
makeParams(1, 2, 3, 123, 5, 6)},
|
||||
3: {initParams,
|
||||
abci.ConsensusParams{
|
||||
BlockSize: &abci.BlockSize{
|
||||
MaxTxs: 44,
|
||||
MaxGas: 55,
|
||||
},
|
||||
},
|
||||
makeParams(1, 44, 55, 4, 5, 6)},
|
||||
4: {initParams,
|
||||
abci.ConsensusParams{
|
||||
BlockSize: &abci.BlockSize{
|
||||
MaxTxs: 789,
|
||||
},
|
||||
TxSize: &abci.TxSize{
|
||||
MaxGas: 888,
|
||||
},
|
||||
BlockGossip: &abci.BlockGossip{
|
||||
BlockPartSizeBytes: 2002,
|
||||
},
|
||||
},
|
||||
makeParams(1, 789, 3, 4, 888, 2002)},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
res := tc.init.Update(&(tc.updates))
|
||||
assert.Equal(t, tc.expected, res, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesValPubKeyChange(state State, height int64,
|
||||
pubkey crypto.PubKey) (*types.Header, types.BlockID, *ABCIResponses) {
|
||||
|
||||
block := makeBlock(state, height)
|
||||
abciResponses := &ABCIResponses{
|
||||
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
|
||||
}
|
||||
|
||||
// if the pubkey is new, remove the old and add the new
|
||||
_, val := state.Validators.GetByIndex(0)
|
||||
if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) {
|
||||
abciResponses.EndBlock = &abci.ResponseEndBlock{
|
||||
ValidatorUpdates: []abci.Validator{
|
||||
types.TM2PB.ValidatorFromPubKeyAndPower(val.PubKey, 0),
|
||||
types.TM2PB.ValidatorFromPubKeyAndPower(pubkey, 10),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesValPowerChange(state State, height int64,
|
||||
power int64) (*types.Header, types.BlockID, *ABCIResponses) {
|
||||
|
||||
block := makeBlock(state, height)
|
||||
abciResponses := &ABCIResponses{
|
||||
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
|
||||
}
|
||||
|
||||
// if the pubkey is new, remove the old and add the new
|
||||
_, val := state.Validators.GetByIndex(0)
|
||||
if val.VotingPower != power {
|
||||
abciResponses.EndBlock = &abci.ResponseEndBlock{
|
||||
ValidatorUpdates: []abci.Validator{
|
||||
types.TM2PB.ValidatorFromPubKeyAndPower(val.PubKey, power),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesParams(state State, height int64,
|
||||
params types.ConsensusParams) (*types.Header, types.BlockID, *ABCIResponses) {
|
||||
|
||||
block := makeBlock(state, height)
|
||||
abciResponses := &ABCIResponses{
|
||||
EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)},
|
||||
}
|
||||
return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
type paramsChangeTestCase struct {
|
||||
height int64
|
||||
params types.ConsensusParams
|
||||
}
|
||||
|
||||
func makeHeaderPartsResults(state State, height int64,
|
||||
results []*abci.ResponseDeliverTx) (*types.Header, types.BlockID, *ABCIResponses) {
|
||||
|
||||
block := makeBlock(state, height)
|
||||
abciResponses := &ABCIResponses{
|
||||
DeliverTx: results,
|
||||
EndBlock: &abci.ResponseEndBlock{},
|
||||
}
|
||||
return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
func calcValidatorsKey(height int64) []byte {
|
||||
return []byte(cmn.Fmt("validatorsKey:%v", height))
|
||||
}
|
||||
|
||||
func calcConsensusParamsKey(height int64) []byte {
|
||||
return []byte(cmn.Fmt("consensusParamsKey:%v", height))
|
||||
}
|
||||
|
||||
func calcABCIResponsesKey(height int64) []byte {
|
||||
return []byte(cmn.Fmt("abciResponsesKey:%v", height))
|
||||
}
|
||||
|
||||
// LoadStateFromDBOrGenesisFile loads the most recent state from the database,
|
||||
// or creates a new one from the given genesisFilePath and persists the result
|
||||
// to the database.
|
||||
func LoadStateFromDBOrGenesisFile(stateDB dbm.DB, genesisFilePath string) (State, error) {
|
||||
state := LoadState(stateDB)
|
||||
if state.IsEmpty() {
|
||||
var err error
|
||||
state, err = MakeGenesisStateFromFile(genesisFilePath)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
SaveState(stateDB, state)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// LoadStateFromDBOrGenesisDoc loads the most recent state from the database,
|
||||
// or creates a new one from the given genesisDoc and persists the result
|
||||
// to the database.
|
||||
func LoadStateFromDBOrGenesisDoc(stateDB dbm.DB, genesisDoc *types.GenesisDoc) (State, error) {
|
||||
state := LoadState(stateDB)
|
||||
if state.IsEmpty() {
|
||||
var err error
|
||||
state, err = MakeGenesisState(genesisDoc)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
SaveState(stateDB, state)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// LoadState loads the State from the database.
|
||||
func LoadState(db dbm.DB) State {
|
||||
return loadState(db, stateKey)
|
||||
}
|
||||
|
||||
func loadState(db dbm.DB, key []byte) (state State) {
|
||||
buf := db.Get(key)
|
||||
if len(buf) == 0 {
|
||||
return state
|
||||
}
|
||||
|
||||
err := cdc.UnmarshalBinaryBare(buf, &state)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
cmn.Exit(cmn.Fmt(`LoadState: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// SaveState persists the State, the ValidatorsInfo, and the ConsensusParamsInfo to the database.
|
||||
func SaveState(db dbm.DB, state State) {
|
||||
saveState(db, state, stateKey)
|
||||
}
|
||||
|
||||
func saveState(db dbm.DB, state State, key []byte) {
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
saveValidatorsInfo(db, nextHeight, state.LastHeightValidatorsChanged, state.Validators)
|
||||
saveConsensusParamsInfo(db, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams)
|
||||
db.SetSync(stateKey, state.Bytes())
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
// ABCIResponses retains the responses
|
||||
// of the various ABCI calls during block processing.
|
||||
// It is persisted to disk for each height before calling Commit.
|
||||
type ABCIResponses struct {
|
||||
DeliverTx []*abci.ResponseDeliverTx
|
||||
EndBlock *abci.ResponseEndBlock
|
||||
}
|
||||
|
||||
// NewABCIResponses returns a new ABCIResponses
|
||||
func NewABCIResponses(block *types.Block) *ABCIResponses {
|
||||
resDeliverTxs := make([]*abci.ResponseDeliverTx, block.NumTxs)
|
||||
if block.NumTxs == 0 {
|
||||
// This makes Amino encoding/decoding consistent.
|
||||
resDeliverTxs = nil
|
||||
}
|
||||
return &ABCIResponses{
|
||||
DeliverTx: resDeliverTxs,
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes serializes the ABCIResponse using go-amino.
|
||||
func (arz *ABCIResponses) Bytes() []byte {
|
||||
return cdc.MustMarshalBinaryBare(arz)
|
||||
}
|
||||
|
||||
func (arz *ABCIResponses) ResultsHash() []byte {
|
||||
results := types.NewResults(arz.DeliverTx)
|
||||
return results.Hash()
|
||||
}
|
||||
|
||||
// LoadABCIResponses loads the ABCIResponses for the given height from the database.
|
||||
// This is useful for recovering from crashes where we called app.Commit and before we called
|
||||
// s.Save(). It can also be used to produce Merkle proofs of the result of txs.
|
||||
func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) {
|
||||
buf := db.Get(calcABCIResponsesKey(height))
|
||||
if len(buf) == 0 {
|
||||
return nil, ErrNoABCIResponsesForHeight{height}
|
||||
}
|
||||
|
||||
abciResponses := new(ABCIResponses)
|
||||
err := cdc.UnmarshalBinaryBare(buf, abciResponses)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
cmn.Exit(cmn.Fmt(`LoadABCIResponses: Data has been corrupted or its spec has
|
||||
changed: %v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return abciResponses, nil
|
||||
}
|
||||
|
||||
// SaveABCIResponses persists the ABCIResponses to the database.
|
||||
// This is useful in case we crash after app.Commit and before s.Save().
|
||||
// Responses are indexed by height so they can also be loaded later to produce Merkle proofs.
|
||||
func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) {
|
||||
db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes())
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// ValidatorsInfo represents the latest validator set, or the last height it changed
|
||||
type ValidatorsInfo struct {
|
||||
ValidatorSet *types.ValidatorSet
|
||||
LastHeightChanged int64
|
||||
}
|
||||
|
||||
// Bytes serializes the ValidatorsInfo using go-amino.
|
||||
func (valInfo *ValidatorsInfo) Bytes() []byte {
|
||||
return cdc.MustMarshalBinaryBare(valInfo)
|
||||
}
|
||||
|
||||
// LoadValidators loads the ValidatorSet for a given height.
|
||||
// Returns ErrNoValSetForHeight if the validator set can't be found for this height.
|
||||
func LoadValidators(db dbm.DB, height int64) (*types.ValidatorSet, error) {
|
||||
valInfo := loadValidatorsInfo(db, height)
|
||||
if valInfo == nil {
|
||||
return nil, ErrNoValSetForHeight{height}
|
||||
}
|
||||
|
||||
if valInfo.ValidatorSet == nil {
|
||||
valInfo2 := loadValidatorsInfo(db, valInfo.LastHeightChanged)
|
||||
if valInfo2 == nil {
|
||||
cmn.PanicSanity(fmt.Sprintf(`Couldn't find validators at height %d as
|
||||
last changed from height %d`, valInfo.LastHeightChanged, height))
|
||||
}
|
||||
valInfo = valInfo2
|
||||
}
|
||||
|
||||
return valInfo.ValidatorSet, nil
|
||||
}
|
||||
|
||||
func loadValidatorsInfo(db dbm.DB, height int64) *ValidatorsInfo {
|
||||
buf := db.Get(calcValidatorsKey(height))
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := new(ValidatorsInfo)
|
||||
err := cdc.UnmarshalBinaryBare(buf, v)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
cmn.Exit(cmn.Fmt(`LoadValidators: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// saveValidatorsInfo persists the validator set for the next block to disk.
|
||||
// It should be called from s.Save(), right before the state itself is persisted.
|
||||
// If the validator set did not change after processing the latest block,
|
||||
// only the last height for which the validators changed is persisted.
|
||||
func saveValidatorsInfo(db dbm.DB, nextHeight, changeHeight int64, valSet *types.ValidatorSet) {
|
||||
valInfo := &ValidatorsInfo{
|
||||
LastHeightChanged: changeHeight,
|
||||
}
|
||||
if changeHeight == nextHeight {
|
||||
valInfo.ValidatorSet = valSet
|
||||
}
|
||||
db.SetSync(calcValidatorsKey(nextHeight), valInfo.Bytes())
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// ConsensusParamsInfo represents the latest consensus params, or the last height it changed
|
||||
type ConsensusParamsInfo struct {
|
||||
ConsensusParams types.ConsensusParams
|
||||
LastHeightChanged int64
|
||||
}
|
||||
|
||||
// Bytes serializes the ConsensusParamsInfo using go-amino.
|
||||
func (params ConsensusParamsInfo) Bytes() []byte {
|
||||
return cdc.MustMarshalBinaryBare(params)
|
||||
}
|
||||
|
||||
// LoadConsensusParams loads the ConsensusParams for a given height.
|
||||
func LoadConsensusParams(db dbm.DB, height int64) (types.ConsensusParams, error) {
|
||||
empty := types.ConsensusParams{}
|
||||
|
||||
paramsInfo := loadConsensusParamsInfo(db, height)
|
||||
if paramsInfo == nil {
|
||||
return empty, ErrNoConsensusParamsForHeight{height}
|
||||
}
|
||||
|
||||
if paramsInfo.ConsensusParams == empty {
|
||||
paramsInfo = loadConsensusParamsInfo(db, paramsInfo.LastHeightChanged)
|
||||
if paramsInfo == nil {
|
||||
cmn.PanicSanity(fmt.Sprintf(`Couldn't find consensus params at height %d as
|
||||
last changed from height %d`, paramsInfo.LastHeightChanged, height))
|
||||
}
|
||||
}
|
||||
|
||||
return paramsInfo.ConsensusParams, nil
|
||||
}
|
||||
|
||||
func loadConsensusParamsInfo(db dbm.DB, height int64) *ConsensusParamsInfo {
|
||||
buf := db.Get(calcConsensusParamsKey(height))
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
paramsInfo := new(ConsensusParamsInfo)
|
||||
err := cdc.UnmarshalBinaryBare(buf, paramsInfo)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
cmn.Exit(cmn.Fmt(`LoadConsensusParams: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return paramsInfo
|
||||
}
|
||||
|
||||
// saveConsensusParamsInfo persists the consensus params for the next block to disk.
|
||||
// It should be called from s.Save(), right before the state itself is persisted.
|
||||
// If the consensus params did not change after processing the latest block,
|
||||
// only the last height for which they changed is persisted.
|
||||
func saveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params types.ConsensusParams) {
|
||||
paramsInfo := &ConsensusParamsInfo{
|
||||
LastHeightChanged: changeHeight,
|
||||
}
|
||||
if changeHeight == nextHeight {
|
||||
paramsInfo.ConsensusParams = params
|
||||
}
|
||||
db.SetSync(calcConsensusParamsKey(nextHeight), paramsInfo.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package txindex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// TxIndexer interface defines methods to index and search transactions.
|
||||
type TxIndexer interface {
|
||||
|
||||
// AddBatch analyzes, indexes and stores a batch of transactions.
|
||||
AddBatch(b *Batch) error
|
||||
|
||||
// Index analyzes, indexes and stores a single transaction.
|
||||
Index(result *types.TxResult) error
|
||||
|
||||
// Get returns the transaction specified by hash or nil if the transaction is not indexed
|
||||
// or stored.
|
||||
Get(hash []byte) (*types.TxResult, error)
|
||||
|
||||
// Search allows you to query for transactions.
|
||||
Search(q *query.Query) ([]*types.TxResult, error)
|
||||
}
|
||||
|
||||
//----------------------------------------------------
|
||||
// Txs are written as a batch
|
||||
|
||||
// Batch groups together multiple Index operations to be performed at the same time.
|
||||
// NOTE: Batch is NOT thread-safe and must not be modified after starting its execution.
|
||||
type Batch struct {
|
||||
Ops []*types.TxResult
|
||||
}
|
||||
|
||||
// NewBatch creates a new Batch.
|
||||
func NewBatch(n int64) *Batch {
|
||||
return &Batch{
|
||||
Ops: make([]*types.TxResult, n),
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update an entry for the given result.Index.
|
||||
func (b *Batch) Add(result *types.TxResult) error {
|
||||
b.Ops[result.Index] = result
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the total number of operations inside the batch.
|
||||
func (b *Batch) Size() int {
|
||||
return len(b.Ops)
|
||||
}
|
||||
|
||||
//----------------------------------------------------
|
||||
// Errors
|
||||
|
||||
// ErrorEmptyHash indicates empty hash
|
||||
var ErrorEmptyHash = errors.New("Transaction hash cannot be empty")
|
||||
@@ -0,0 +1,73 @@
|
||||
package txindex
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
subscriber = "IndexerService"
|
||||
)
|
||||
|
||||
// IndexerService connects event bus and transaction indexer together in order
|
||||
// to index transactions coming from event bus.
|
||||
type IndexerService struct {
|
||||
cmn.BaseService
|
||||
|
||||
idr TxIndexer
|
||||
eventBus *types.EventBus
|
||||
}
|
||||
|
||||
// NewIndexerService returns a new service instance.
|
||||
func NewIndexerService(idr TxIndexer, eventBus *types.EventBus) *IndexerService {
|
||||
is := &IndexerService{idr: idr, eventBus: eventBus}
|
||||
is.BaseService = *cmn.NewBaseService(nil, "IndexerService", is)
|
||||
return is
|
||||
}
|
||||
|
||||
// OnStart implements cmn.Service by subscribing for all transactions
|
||||
// and indexing them by tags.
|
||||
func (is *IndexerService) OnStart() error {
|
||||
blockHeadersCh := make(chan interface{})
|
||||
if err := is.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryNewBlockHeader, blockHeadersCh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txsCh := make(chan interface{})
|
||||
if err := is.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryTx, txsCh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
e, ok := <-blockHeadersCh
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
header := e.(types.EventDataNewBlockHeader).Header
|
||||
batch := NewBatch(header.NumTxs)
|
||||
for i := int64(0); i < header.NumTxs; i++ {
|
||||
e, ok := <-txsCh
|
||||
if !ok {
|
||||
is.Logger.Error("Failed to index all transactions due to closed transactions channel", "height", header.Height, "numTxs", header.NumTxs, "numProcessed", i)
|
||||
return
|
||||
}
|
||||
txResult := e.(types.EventDataTx).TxResult
|
||||
batch.Add(&txResult)
|
||||
}
|
||||
is.idr.AddBatch(batch)
|
||||
is.Logger.Info("Indexed block", "height", header.Height)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements cmn.Service by unsubscribing from all transactions.
|
||||
func (is *IndexerService) OnStop() {
|
||||
if is.eventBus.IsRunning() {
|
||||
_ = is.eventBus.UnsubscribeAll(context.Background(), subscriber)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/state/txindex"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tagKeySeparator = "/"
|
||||
)
|
||||
|
||||
var _ txindex.TxIndexer = (*TxIndex)(nil)
|
||||
|
||||
// TxIndex is the simplest possible indexer, backed by key-value storage (levelDB).
|
||||
type TxIndex struct {
|
||||
store dbm.DB
|
||||
tagsToIndex []string
|
||||
indexAllTags bool
|
||||
}
|
||||
|
||||
// NewTxIndex creates new KV indexer.
|
||||
func NewTxIndex(store dbm.DB, options ...func(*TxIndex)) *TxIndex {
|
||||
txi := &TxIndex{store: store, tagsToIndex: make([]string, 0), indexAllTags: false}
|
||||
for _, o := range options {
|
||||
o(txi)
|
||||
}
|
||||
return txi
|
||||
}
|
||||
|
||||
// IndexTags is an option for setting which tags to index.
|
||||
func IndexTags(tags []string) func(*TxIndex) {
|
||||
return func(txi *TxIndex) {
|
||||
txi.tagsToIndex = tags
|
||||
}
|
||||
}
|
||||
|
||||
// IndexAllTags is an option for indexing all tags.
|
||||
func IndexAllTags() func(*TxIndex) {
|
||||
return func(txi *TxIndex) {
|
||||
txi.indexAllTags = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets transaction from the TxIndex storage and returns it or nil if the
|
||||
// transaction is not found.
|
||||
func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) {
|
||||
if len(hash) == 0 {
|
||||
return nil, txindex.ErrorEmptyHash
|
||||
}
|
||||
|
||||
rawBytes := txi.store.Get(hash)
|
||||
if rawBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
txResult := new(types.TxResult)
|
||||
err := cdc.UnmarshalBinaryBare(rawBytes, &txResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading TxResult: %v", err)
|
||||
}
|
||||
|
||||
return txResult, nil
|
||||
}
|
||||
|
||||
// AddBatch indexes a batch of transactions using the given list of tags.
|
||||
func (txi *TxIndex) AddBatch(b *txindex.Batch) error {
|
||||
storeBatch := txi.store.NewBatch()
|
||||
|
||||
for _, result := range b.Ops {
|
||||
hash := result.Tx.Hash()
|
||||
|
||||
// index tx by tags
|
||||
for _, tag := range result.Result.Tags {
|
||||
if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) {
|
||||
storeBatch.Set(keyForTag(tag, result), hash)
|
||||
}
|
||||
}
|
||||
|
||||
// index tx by hash
|
||||
rawBytes, err := cdc.MarshalBinaryBare(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storeBatch.Set(hash, rawBytes)
|
||||
}
|
||||
|
||||
storeBatch.Write()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index indexes a single transaction using the given list of tags.
|
||||
func (txi *TxIndex) Index(result *types.TxResult) error {
|
||||
b := txi.store.NewBatch()
|
||||
|
||||
hash := result.Tx.Hash()
|
||||
|
||||
// index tx by tags
|
||||
for _, tag := range result.Result.Tags {
|
||||
if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) {
|
||||
b.Set(keyForTag(tag, result), hash)
|
||||
}
|
||||
}
|
||||
|
||||
// index tx by hash
|
||||
rawBytes, err := cdc.MarshalBinaryBare(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Set(hash, rawBytes)
|
||||
|
||||
b.Write()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search performs a search using the given query. It breaks the query into
|
||||
// conditions (like "tx.height > 5"). For each condition, it queries the DB
|
||||
// index. One special use cases here: (1) if "tx.hash" is found, it returns tx
|
||||
// result for it (2) for range queries it is better for the client to provide
|
||||
// both lower and upper bounds, so we are not performing a full scan. Results
|
||||
// from querying indexes are then intersected and returned to the caller.
|
||||
func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) {
|
||||
var hashes [][]byte
|
||||
var hashesInitialized bool
|
||||
|
||||
// get a list of conditions (like "tx.height > 5")
|
||||
conditions := q.Conditions()
|
||||
|
||||
// if there is a hash condition, return the result immediately
|
||||
hash, err, ok := lookForHash(conditions)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error during searching for a hash in the query")
|
||||
} else if ok {
|
||||
res, err := txi.Get(hash)
|
||||
if res == nil {
|
||||
return []*types.TxResult{}, nil
|
||||
}
|
||||
return []*types.TxResult{res}, errors.Wrap(err, "error while retrieving the result")
|
||||
}
|
||||
|
||||
// conditions to skip because they're handled before "everything else"
|
||||
skipIndexes := make([]int, 0)
|
||||
|
||||
// if there is a height condition ("tx.height=3"), extract it for faster lookups
|
||||
height, heightIndex := lookForHeight(conditions)
|
||||
if heightIndex >= 0 {
|
||||
skipIndexes = append(skipIndexes, heightIndex)
|
||||
}
|
||||
|
||||
// extract ranges
|
||||
// if both upper and lower bounds exist, it's better to get them in order not
|
||||
// no iterate over kvs that are not within range.
|
||||
ranges, rangeIndexes := lookForRanges(conditions)
|
||||
if len(ranges) > 0 {
|
||||
skipIndexes = append(skipIndexes, rangeIndexes...)
|
||||
|
||||
for _, r := range ranges {
|
||||
if !hashesInitialized {
|
||||
hashes = txi.matchRange(r, []byte(r.key))
|
||||
hashesInitialized = true
|
||||
} else {
|
||||
hashes = intersect(hashes, txi.matchRange(r, []byte(r.key)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for all other conditions
|
||||
for i, c := range conditions {
|
||||
if cmn.IntInSlice(i, skipIndexes) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !hashesInitialized {
|
||||
hashes = txi.match(c, startKey(c, height))
|
||||
hashesInitialized = true
|
||||
} else {
|
||||
hashes = intersect(hashes, txi.match(c, startKey(c, height)))
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*types.TxResult, len(hashes))
|
||||
i := 0
|
||||
for _, h := range hashes {
|
||||
results[i], err = txi.Get(h)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get Tx{%X}", h)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// sort by height by default
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Height < results[j].Height
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func lookForHash(conditions []query.Condition) (hash []byte, err error, ok bool) {
|
||||
for _, c := range conditions {
|
||||
if c.Tag == types.TxHashKey {
|
||||
decoded, err := hex.DecodeString(c.Operand.(string))
|
||||
return decoded, err, true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lookForHeight(conditions []query.Condition) (height int64, index int) {
|
||||
for i, c := range conditions {
|
||||
if c.Tag == types.TxHeightKey {
|
||||
return c.Operand.(int64), i
|
||||
}
|
||||
}
|
||||
return 0, -1
|
||||
}
|
||||
|
||||
// special map to hold range conditions
|
||||
// Example: account.number => queryRange{lowerBound: 1, upperBound: 5}
|
||||
type queryRanges map[string]queryRange
|
||||
|
||||
type queryRange struct {
|
||||
key string
|
||||
lowerBound interface{} // int || time.Time
|
||||
includeLowerBound bool
|
||||
upperBound interface{} // int || time.Time
|
||||
includeUpperBound bool
|
||||
}
|
||||
|
||||
func (r queryRange) lowerBoundValue() interface{} {
|
||||
if r.lowerBound == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.includeLowerBound {
|
||||
return r.lowerBound
|
||||
} else {
|
||||
switch t := r.lowerBound.(type) {
|
||||
case int64:
|
||||
return t + 1
|
||||
case time.Time:
|
||||
return t.Unix() + 1
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r queryRange) AnyBound() interface{} {
|
||||
if r.lowerBound != nil {
|
||||
return r.lowerBound
|
||||
} else {
|
||||
return r.upperBound
|
||||
}
|
||||
}
|
||||
|
||||
func (r queryRange) upperBoundValue() interface{} {
|
||||
if r.upperBound == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.includeUpperBound {
|
||||
return r.upperBound
|
||||
} else {
|
||||
switch t := r.upperBound.(type) {
|
||||
case int64:
|
||||
return t - 1
|
||||
case time.Time:
|
||||
return t.Unix() - 1
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lookForRanges(conditions []query.Condition) (ranges queryRanges, indexes []int) {
|
||||
ranges = make(queryRanges)
|
||||
for i, c := range conditions {
|
||||
if isRangeOperation(c.Op) {
|
||||
r, ok := ranges[c.Tag]
|
||||
if !ok {
|
||||
r = queryRange{key: c.Tag}
|
||||
}
|
||||
switch c.Op {
|
||||
case query.OpGreater:
|
||||
r.lowerBound = c.Operand
|
||||
case query.OpGreaterEqual:
|
||||
r.includeLowerBound = true
|
||||
r.lowerBound = c.Operand
|
||||
case query.OpLess:
|
||||
r.upperBound = c.Operand
|
||||
case query.OpLessEqual:
|
||||
r.includeUpperBound = true
|
||||
r.upperBound = c.Operand
|
||||
}
|
||||
ranges[c.Tag] = r
|
||||
indexes = append(indexes, i)
|
||||
}
|
||||
}
|
||||
return ranges, indexes
|
||||
}
|
||||
|
||||
func isRangeOperation(op query.Operator) bool {
|
||||
switch op {
|
||||
case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (txi *TxIndex) match(c query.Condition, startKey []byte) (hashes [][]byte) {
|
||||
if c.Op == query.OpEqual {
|
||||
it := dbm.IteratePrefix(txi.store, startKey)
|
||||
defer it.Close()
|
||||
for ; it.Valid(); it.Next() {
|
||||
hashes = append(hashes, it.Value())
|
||||
}
|
||||
} else if c.Op == query.OpContains {
|
||||
// XXX: doing full scan because startKey does not apply here
|
||||
// For example, if startKey = "account.owner=an" and search query = "accoutn.owner CONSISTS an"
|
||||
// we can't iterate with prefix "account.owner=an" because we might miss keys like "account.owner=Ulan"
|
||||
it := txi.store.Iterator(nil, nil)
|
||||
defer it.Close()
|
||||
for ; it.Valid(); it.Next() {
|
||||
if !isTagKey(it.Key()) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) {
|
||||
hashes = append(hashes, it.Value())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic("other operators should be handled already")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (txi *TxIndex) matchRange(r queryRange, prefix []byte) (hashes [][]byte) {
|
||||
// create a map to prevent duplicates
|
||||
hashesMap := make(map[string][]byte)
|
||||
|
||||
lowerBound := r.lowerBoundValue()
|
||||
upperBound := r.upperBoundValue()
|
||||
|
||||
it := dbm.IteratePrefix(txi.store, prefix)
|
||||
defer it.Close()
|
||||
LOOP:
|
||||
for ; it.Valid(); it.Next() {
|
||||
if !isTagKey(it.Key()) {
|
||||
continue
|
||||
}
|
||||
switch r.AnyBound().(type) {
|
||||
case int64:
|
||||
v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
|
||||
if err != nil {
|
||||
continue LOOP
|
||||
}
|
||||
include := true
|
||||
if lowerBound != nil && v < lowerBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
if upperBound != nil && v > upperBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
if include {
|
||||
hashesMap[fmt.Sprintf("%X", it.Value())] = it.Value()
|
||||
}
|
||||
// XXX: passing time in a ABCI Tags is not yet implemented
|
||||
// case time.Time:
|
||||
// v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
|
||||
// if v == r.upperBound {
|
||||
// break
|
||||
// }
|
||||
}
|
||||
}
|
||||
hashes = make([][]byte, len(hashesMap))
|
||||
i := 0
|
||||
for _, h := range hashesMap {
|
||||
hashes[i] = h
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Keys
|
||||
|
||||
func startKey(c query.Condition, height int64) []byte {
|
||||
var key string
|
||||
if height > 0 {
|
||||
key = fmt.Sprintf("%s/%v/%d", c.Tag, c.Operand, height)
|
||||
} else {
|
||||
key = fmt.Sprintf("%s/%v", c.Tag, c.Operand)
|
||||
}
|
||||
return []byte(key)
|
||||
}
|
||||
|
||||
func isTagKey(key []byte) bool {
|
||||
return strings.Count(string(key), tagKeySeparator) == 3
|
||||
}
|
||||
|
||||
func extractValueFromKey(key []byte) string {
|
||||
parts := strings.SplitN(string(key), tagKeySeparator, 3)
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
func keyForTag(tag cmn.KVPair, result *types.TxResult) []byte {
|
||||
return []byte(fmt.Sprintf("%s/%s/%d/%d", tag.Key, tag.Value, result.Height, result.Index))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
|
||||
func intersect(as, bs [][]byte) [][]byte {
|
||||
i := make([][]byte, 0, cmn.MinInt(len(as), len(bs)))
|
||||
for _, a := range as {
|
||||
for _, b := range bs {
|
||||
if bytes.Equal(a, b) {
|
||||
i = append(i, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/abci/types"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
db "github.com/tendermint/tmlibs/db"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/state/txindex"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestTxIndex(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: nil}}
|
||||
hash := tx.Hash()
|
||||
|
||||
batch := txindex.NewBatch(1)
|
||||
if err := batch.Add(txResult); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err := indexer.AddBatch(batch)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedTxResult, err := indexer.Get(hash)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, txResult, loadedTxResult)
|
||||
|
||||
tx2 := types.Tx("BYE BYE WORLD")
|
||||
txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: nil}}
|
||||
hash2 := tx2.Hash()
|
||||
|
||||
err = indexer.Index(txResult2)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedTxResult2, err := indexer.Get(hash2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, txResult2, loadedTxResult2)
|
||||
}
|
||||
|
||||
func TestTxSearch(t *testing.T) {
|
||||
allowedTags := []string{"account.number", "account.owner", "account.date"}
|
||||
indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags))
|
||||
|
||||
txResult := txResultWithTags([]cmn.KVPair{
|
||||
{Key: []byte("account.number"), Value: []byte("1")},
|
||||
{Key: []byte("account.owner"), Value: []byte("Ivan")},
|
||||
{Key: []byte("not_allowed"), Value: []byte("Vlad")},
|
||||
})
|
||||
hash := txResult.Tx.Hash()
|
||||
|
||||
err := indexer.Index(txResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
q string
|
||||
resultsLength int
|
||||
}{
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash), 1},
|
||||
// search by exact match (one tag)
|
||||
{"account.number = 1", 1},
|
||||
// search by exact match (two tags)
|
||||
{"account.number = 1 AND account.owner = 'Ivan'", 1},
|
||||
// search by exact match (two tags)
|
||||
{"account.number = 1 AND account.owner = 'Vlad'", 0},
|
||||
// search by range
|
||||
{"account.number >= 1 AND account.number <= 5", 1},
|
||||
// search by range (lower bound)
|
||||
{"account.number >= 1", 1},
|
||||
// search by range (upper bound)
|
||||
{"account.number <= 5", 1},
|
||||
// search using not allowed tag
|
||||
{"not_allowed = 'boom'", 0},
|
||||
// search for not existing tx result
|
||||
{"account.number >= 2 AND account.number <= 5", 0},
|
||||
// search using not existing tag
|
||||
{"account.date >= TIME 2013-05-03T14:45:00Z", 0},
|
||||
// search using CONTAINS
|
||||
{"account.owner CONTAINS 'an'", 1},
|
||||
// search using CONTAINS
|
||||
{"account.owner CONTAINS 'Vlad'", 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.q, func(t *testing.T) {
|
||||
results, err := indexer.Search(query.MustParse(tc.q))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, results, tc.resultsLength)
|
||||
if tc.resultsLength > 0 {
|
||||
assert.Equal(t, []*types.TxResult{txResult}, results)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
|
||||
allowedTags := []string{"account.number"}
|
||||
indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags))
|
||||
|
||||
txResult := txResultWithTags([]cmn.KVPair{
|
||||
{Key: []byte("account.number"), Value: []byte("1")},
|
||||
{Key: []byte("account.number"), Value: []byte("2")},
|
||||
})
|
||||
|
||||
err := indexer.Index(txResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := indexer.Search(query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, []*types.TxResult{txResult}, results)
|
||||
}
|
||||
|
||||
func TestTxSearchMultipleTxs(t *testing.T) {
|
||||
allowedTags := []string{"account.number"}
|
||||
indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags))
|
||||
|
||||
// indexed first, but bigger height (to test the order of transactions)
|
||||
txResult := txResultWithTags([]cmn.KVPair{
|
||||
{Key: []byte("account.number"), Value: []byte("1")},
|
||||
})
|
||||
txResult.Tx = types.Tx("Bob's account")
|
||||
txResult.Height = 2
|
||||
err := indexer.Index(txResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed second, but smaller height (to test the order of transactions)
|
||||
txResult2 := txResultWithTags([]cmn.KVPair{
|
||||
{Key: []byte("account.number"), Value: []byte("2")},
|
||||
})
|
||||
txResult2.Tx = types.Tx("Alice's account")
|
||||
txResult2.Height = 1
|
||||
err = indexer.Index(txResult2)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := indexer.Search(query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, results, 2)
|
||||
assert.Equal(t, []*types.TxResult{txResult2, txResult}, results)
|
||||
}
|
||||
|
||||
func TestIndexAllTags(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB(), IndexAllTags())
|
||||
|
||||
txResult := txResultWithTags([]cmn.KVPair{
|
||||
cmn.KVPair{[]byte("account.owner"), []byte("Ivan")},
|
||||
cmn.KVPair{[]byte("account.number"), []byte("1")},
|
||||
})
|
||||
|
||||
err := indexer.Index(txResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := indexer.Search(query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, []*types.TxResult{txResult}, results)
|
||||
|
||||
results, err = indexer.Search(query.MustParse("account.owner = 'Ivan'"))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, []*types.TxResult{txResult}, results)
|
||||
}
|
||||
|
||||
func txResultWithTags(tags []cmn.KVPair) *types.TxResult {
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
return &types.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Tags: tags,
|
||||
Fee: cmn.KI64Pair{Key: nil, Value: 0},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkTxIndex(txsCount int64, b *testing.B) {
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
txResult := &types.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Tags: []cmn.KVPair{},
|
||||
Fee: cmn.KI64Pair{Key: []uint8{}, Value: 0},
|
||||
},
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir("", "tx_index_db")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir) // nolint: errcheck
|
||||
|
||||
store := db.NewDB("tx_index", "leveldb", dir)
|
||||
indexer := NewTxIndex(store)
|
||||
|
||||
batch := txindex.NewBatch(txsCount)
|
||||
for i := int64(0); i < txsCount; i++ {
|
||||
if err := batch.Add(txResult); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
txResult.Index++
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = indexer.AddBatch(batch)
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTxIndex1(b *testing.B) { benchmarkTxIndex(1, b) }
|
||||
func BenchmarkTxIndex500(b *testing.B) { benchmarkTxIndex(500, b) }
|
||||
func BenchmarkTxIndex1000(b *testing.B) { benchmarkTxIndex(1000, b) }
|
||||
func BenchmarkTxIndex2000(b *testing.B) { benchmarkTxIndex(2000, b) }
|
||||
func BenchmarkTxIndex10000(b *testing.B) { benchmarkTxIndex(10000, b) }
|
||||
@@ -0,0 +1,10 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-amino"
|
||||
)
|
||||
|
||||
var cdc = amino.NewCodec()
|
||||
|
||||
func init() {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/state/txindex"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ txindex.TxIndexer = (*TxIndex)(nil)
|
||||
|
||||
// TxIndex acts as a /dev/null.
|
||||
type TxIndex struct{}
|
||||
|
||||
// Get on a TxIndex is disabled and panics when invoked.
|
||||
func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) {
|
||||
return nil, errors.New(`Indexing is disabled (set 'tx_index = "kv"' in config)`)
|
||||
}
|
||||
|
||||
// AddBatch is a noop and always returns nil.
|
||||
func (txi *TxIndex) AddBatch(batch *txindex.Batch) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index is a noop and always returns nil.
|
||||
func (txi *TxIndex) Index(result *types.TxResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) {
|
||||
return []*types.TxResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Validate block
|
||||
|
||||
func validateBlock(stateDB dbm.DB, state State, block *types.Block) error {
|
||||
// validate internal consistency
|
||||
if err := block.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// validate basic info
|
||||
if block.ChainID != state.ChainID {
|
||||
return fmt.Errorf("Wrong Block.Header.ChainID. Expected %v, got %v", state.ChainID, block.ChainID)
|
||||
}
|
||||
if block.Height != state.LastBlockHeight+1 {
|
||||
return fmt.Errorf("Wrong Block.Header.Height. Expected %v, got %v", state.LastBlockHeight+1, block.Height)
|
||||
}
|
||||
/* TODO: Determine bounds for Time
|
||||
See blockchain/reactor "stopSyncingDurationMinutes"
|
||||
|
||||
if !block.Time.After(lastBlockTime) {
|
||||
return errors.New("Invalid Block.Header.Time")
|
||||
}
|
||||
*/
|
||||
|
||||
// validate prev block info
|
||||
if !block.LastBlockID.Equals(state.LastBlockID) {
|
||||
return fmt.Errorf("Wrong Block.Header.LastBlockID. Expected %v, got %v", state.LastBlockID, block.LastBlockID)
|
||||
}
|
||||
newTxs := int64(len(block.Data.Txs))
|
||||
if block.TotalTxs != state.LastBlockTotalTx+newTxs {
|
||||
return fmt.Errorf("Wrong Block.Header.TotalTxs. Expected %v, got %v", state.LastBlockTotalTx+newTxs, block.TotalTxs)
|
||||
}
|
||||
|
||||
// validate app info
|
||||
if !bytes.Equal(block.AppHash, state.AppHash) {
|
||||
return fmt.Errorf("Wrong Block.Header.AppHash. Expected %X, got %v", state.AppHash, block.AppHash)
|
||||
}
|
||||
if !bytes.Equal(block.ConsensusHash, state.ConsensusParams.Hash()) {
|
||||
return fmt.Errorf("Wrong Block.Header.ConsensusHash. Expected %X, got %v", state.ConsensusParams.Hash(), block.ConsensusHash)
|
||||
}
|
||||
if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) {
|
||||
return fmt.Errorf("Wrong Block.Header.LastResultsHash. Expected %X, got %v", state.LastResultsHash, block.LastResultsHash)
|
||||
}
|
||||
if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) {
|
||||
return fmt.Errorf("Wrong Block.Header.ValidatorsHash. Expected %X, got %v", state.Validators.Hash(), block.ValidatorsHash)
|
||||
}
|
||||
|
||||
// Validate block LastCommit.
|
||||
if block.Height == 1 {
|
||||
if len(block.LastCommit.Precommits) != 0 {
|
||||
return errors.New("Block at height 1 (first block) should have no LastCommit precommits")
|
||||
}
|
||||
} else {
|
||||
if len(block.LastCommit.Precommits) != state.LastValidators.Size() {
|
||||
return fmt.Errorf("Invalid block commit size. Expected %v, got %v",
|
||||
state.LastValidators.Size(), len(block.LastCommit.Precommits))
|
||||
}
|
||||
err := state.LastValidators.VerifyCommit(
|
||||
state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Each check requires loading an old validator set.
|
||||
// We should cap the amount of evidence per block
|
||||
// to prevent potential proposer DoS.
|
||||
for _, ev := range block.Evidence.Evidence {
|
||||
if err := VerifyEvidence(stateDB, state, ev); err != nil {
|
||||
return types.NewEvidenceInvalidErr(ev, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyEvidence verifies the evidence fully by checking:
|
||||
// - it is sufficiently recent (MaxAge)
|
||||
// - it is from a key who was a validator at the given height
|
||||
// - it is internally consistent
|
||||
// - it was properly signed by the alleged equivocator
|
||||
func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error {
|
||||
height := state.LastBlockHeight
|
||||
|
||||
evidenceAge := height - evidence.Height()
|
||||
maxAge := state.ConsensusParams.EvidenceParams.MaxAge
|
||||
if evidenceAge > maxAge {
|
||||
return fmt.Errorf("Evidence from height %d is too old. Min height is %d",
|
||||
evidence.Height(), height-maxAge)
|
||||
}
|
||||
|
||||
valset, err := LoadValidators(stateDB, evidence.Height())
|
||||
if err != nil {
|
||||
// TODO: if err is just that we cant find it cuz we pruned, ignore.
|
||||
// TODO: if its actually bad evidence, punish peer
|
||||
return err
|
||||
}
|
||||
|
||||
// The address must have been an active validator at the height.
|
||||
// NOTE: we will ignore evidence from H if the key was not a validator
|
||||
// at H, even if it is a validator at some nearby H'
|
||||
ev := evidence
|
||||
height, addr := ev.Height(), ev.Address()
|
||||
_, val := valset.GetByAddress(addr)
|
||||
if val == nil {
|
||||
return fmt.Errorf("Address %X was not a validator at height %d", addr, height)
|
||||
}
|
||||
|
||||
if err := evidence.Verify(state.ChainID, val.PubKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
"github.com/tendermint/tmlibs/log"
|
||||
)
|
||||
|
||||
func TestValidateBlock(t *testing.T) {
|
||||
state, _ := state(1, 1)
|
||||
|
||||
blockExec := NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), nil, nil, nil)
|
||||
|
||||
// proper block must pass
|
||||
block := makeBlock(state, 1)
|
||||
err := blockExec.ValidateBlock(state, block)
|
||||
require.NoError(t, err)
|
||||
|
||||
// wrong chain fails
|
||||
block = makeBlock(state, 1)
|
||||
block.ChainID = "not-the-real-one"
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong height fails
|
||||
block = makeBlock(state, 1)
|
||||
block.Height += 10
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong total tx fails
|
||||
block = makeBlock(state, 1)
|
||||
block.TotalTxs += 10
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong blockid fails
|
||||
block = makeBlock(state, 1)
|
||||
block.LastBlockID.PartsHeader.Total += 10
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong app hash fails
|
||||
block = makeBlock(state, 1)
|
||||
block.AppHash = []byte("wrong app hash")
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong consensus hash fails
|
||||
block = makeBlock(state, 1)
|
||||
block.ConsensusHash = []byte("wrong consensus hash")
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong results hash fails
|
||||
block = makeBlock(state, 1)
|
||||
block.LastResultsHash = []byte("wrong results hash")
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
|
||||
// wrong validators hash fails
|
||||
block = makeBlock(state, 1)
|
||||
block.ValidatorsHash = []byte("wrong validators hash")
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-amino"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
var cdc = amino.NewCodec()
|
||||
|
||||
func init() {
|
||||
crypto.RegisterAmino(cdc)
|
||||
}
|
||||
Reference in New Issue
Block a user