abci: implement finalize block (#9468)

Adds the `FinalizeBlock` method which replaces `BeginBlock`, `DeliverTx`, and `EndBlock` in a single call.
This commit is contained in:
Callum Waters
2022-11-28 23:12:28 +01:00
committed by GitHub
parent 001cd50fc7
commit c5c2aafad2
142 changed files with 6717 additions and 8420 deletions
+1 -1
View File
@@ -103,4 +103,4 @@ func (e ErrNoABCIResponsesForHeight) Error() string {
return fmt.Sprintf("could not find results for height #%d", e.Height)
}
var ErrABCIResponsesNotPersisted = errors.New("node is not persisting abci responses")
var ErrFinalizeBlockResponsesNotPersisted = errors.New("node is not persisting finalize block responses")
+82 -123
View File
@@ -1,7 +1,7 @@
package state
import (
"errors"
"context"
"fmt"
"time"
@@ -10,7 +10,6 @@ import (
"github.com/tendermint/tendermint/libs/fail"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/mempool"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
"github.com/tendermint/tendermint/proxy"
"github.com/tendermint/tendermint/types"
)
@@ -117,8 +116,8 @@ func (blockExec *BlockExecutor) CreateProposalBlock(
block := state.MakeBlock(height, txs, commit, evidence, proposerAddr)
localLastCommit := buildLastCommitInfo(block, blockExec.store, state.InitialHeight)
rpp, err := blockExec.proxyApp.PrepareProposalSync(
abci.RequestPrepareProposal{
rpp, err := blockExec.proxyApp.PrepareProposal(context.TODO(),
&abci.RequestPrepareProposal{
MaxTxBytes: maxDataBytes,
Txs: block.Txs.ToSliceOfBytes(),
LocalLastCommit: extendedCommitInfo(localLastCommit, votes),
@@ -153,7 +152,7 @@ func (blockExec *BlockExecutor) ProcessProposal(
block *types.Block,
state State,
) (bool, error) {
resp, err := blockExec.proxyApp.ProcessProposalSync(abci.RequestProcessProposal{
resp, err := blockExec.proxyApp.ProcessProposal(context.TODO(), &abci.RequestProcessProposal{
Hash: block.Header.Hash(),
Height: block.Header.Height,
Time: block.Header.Time,
@@ -199,33 +198,48 @@ func (blockExec *BlockExecutor) ApplyBlock(
return state, ErrInvalidBlock(err)
}
commitInfo := buildLastCommitInfo(block, blockExec.store, state.InitialHeight)
startTime := time.Now().UnixNano()
abciResponses, err := execBlockOnProxyApp(
blockExec.logger, blockExec.proxyApp, block, blockExec.store, state.InitialHeight,
)
abciResponse, err := blockExec.proxyApp.FinalizeBlock(context.TODO(), &abci.RequestFinalizeBlock{
Hash: block.Hash(),
NextValidatorsHash: block.NextValidatorsHash,
ProposerAddress: block.ProposerAddress,
Height: block.Height,
DecidedLastCommit: commitInfo,
Misbehavior: block.Evidence.Evidence.ToABCI(),
Txs: block.Txs.ToSliceOfBytes(),
})
endTime := time.Now().UnixNano()
blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000)
if err != nil {
return state, ErrProxyAppConn(err)
blockExec.logger.Error("error in proxyAppConn.FinalizeBlock", "err", err)
return state, err
}
// Assert that the application correctly returned tx results for each of the transactions provided in the block
if len(block.Data.Txs) != len(abciResponse.TxResults) {
return state, fmt.Errorf("expected tx results length to match size of transactions in block. Expected %d, got %d", len(block.Data.Txs), len(abciResponse.TxResults))
}
blockExec.logger.Info("executed block", "height", block.Height, "agreed_app_data", abciResponse.AgreedAppData)
fail.Fail() // XXX
// Save the results before we commit.
if err := blockExec.store.SaveABCIResponses(block.Height, abciResponses); err != nil {
if err := blockExec.store.SaveFinalizeBlockResponse(block.Height, abciResponse); err != nil {
return state, err
}
fail.Fail() // XXX
// validate the validator updates and convert to tendermint types
abciValUpdates := abciResponses.EndBlock.ValidatorUpdates
err = validateValidatorUpdates(abciValUpdates, state.ConsensusParams.Validator)
err = validateValidatorUpdates(abciResponse.ValidatorUpdates, state.ConsensusParams.Validator)
if err != nil {
return state, fmt.Errorf("error in validator updates: %v", err)
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates)
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponse.ValidatorUpdates)
if err != nil {
return state, err
}
@@ -233,18 +247,18 @@ func (blockExec *BlockExecutor) ApplyBlock(
blockExec.logger.Debug("updates to validators", "updates", types.ValidatorListString(validatorUpdates))
blockExec.metrics.ValidatorSetUpdates.Add(1)
}
if abciResponses.EndBlock.ConsensusParamUpdates != nil {
if abciResponse.ConsensusParamUpdates != nil {
blockExec.metrics.ConsensusParamUpdates.Add(1)
}
// Update the state with the block and responses.
state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
state, err = updateState(state, blockID, &block.Header, abciResponse, validatorUpdates)
if err != nil {
return state, fmt.Errorf("commit failed for application: %v", err)
}
// Lock mempool, commit app state, update mempoool.
appHash, retainHeight, err := blockExec.Commit(state, block, abciResponses.DeliverTxs)
retainHeight, err := blockExec.Commit(state, block, abciResponse)
if err != nil {
return state, fmt.Errorf("commit failed for application: %v", err)
}
@@ -255,7 +269,7 @@ func (blockExec *BlockExecutor) ApplyBlock(
fail.Fail() // XXX
// Update the app hash and save the state.
state.AppHash = appHash
state.AppHash = abciResponse.AgreedAppData
if err := blockExec.store.Save(state); err != nil {
return state, err
}
@@ -274,7 +288,7 @@ func (blockExec *BlockExecutor) ApplyBlock(
// 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, validatorUpdates)
fireEvents(blockExec.logger, blockExec.eventBus, block, blockID, abciResponse, validatorUpdates)
return state, nil
}
@@ -288,8 +302,8 @@ func (blockExec *BlockExecutor) ApplyBlock(
func (blockExec *BlockExecutor) Commit(
state State,
block *types.Block,
deliverTxResponses []*abci.ResponseDeliverTx,
) ([]byte, int64, error) {
abciResponse *abci.ResponseFinalizeBlock,
) (int64, error) {
blockExec.mempool.Lock()
defer blockExec.mempool.Unlock()
@@ -298,114 +312,37 @@ func (blockExec *BlockExecutor) Commit(
err := blockExec.mempool.FlushAppConn()
if err != nil {
blockExec.logger.Error("client error during mempool.FlushAppConn", "err", err)
return nil, 0, err
return 0, err
}
// Commit block, get hash back
res, err := blockExec.proxyApp.CommitSync()
res, err := blockExec.proxyApp.Commit(context.TODO())
if err != nil {
blockExec.logger.Error("client error during proxyAppConn.CommitSync", "err", err)
return nil, 0, err
return 0, err
}
// ResponseCommit has no error code - just data
blockExec.logger.Info(
"committed state",
"height", block.Height,
"num_txs", len(block.Txs),
"app_hash", fmt.Sprintf("%X", res.Data),
)
// Update mempool.
err = blockExec.mempool.Update(
block.Height,
block.Txs,
deliverTxResponses,
abciResponse.TxResults,
TxPreCheck(state),
TxPostCheck(state),
)
return res.Data, res.RetainHeight, err
return res.RetainHeight, err
}
//---------------------------------------------------------
// 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,
store Store,
initialHeight int64,
) (*tmstate.ABCIResponses, error) {
var validTxs, invalidTxs = 0, 0
txIndex := 0
abciResponses := new(tmstate.ABCIResponses)
dtxs := make([]*abci.ResponseDeliverTx, len(block.Txs))
abciResponses.DeliverTxs = dtxs
// Execute transactions and get hash.
proxyCb := func(req *abci.Request, res *abci.Response) {
if r, ok := res.Value.(*abci.Response_DeliverTx); ok {
// 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.DeliverTxs[txIndex] = txRes
txIndex++
}
}
proxyAppConn.SetResponseCallback(proxyCb)
commitInfo := buildLastCommitInfo(block, store, initialHeight)
// Begin block
var err error
pbh := block.Header.ToProto()
if pbh == nil {
return nil, errors.New("nil header")
}
abciResponses.BeginBlock, err = proxyAppConn.BeginBlockSync(abci.RequestBeginBlock{
Hash: block.Hash(),
Header: *pbh,
LastCommitInfo: commitInfo,
ByzantineValidators: block.Evidence.Evidence.ToABCI(),
})
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(abci.RequestDeliverTx{Tx: tx})
if err := proxyAppConn.Error(); err != nil {
return nil, err
}
}
// End block.
abciResponses.EndBlock, err = proxyAppConn.EndBlockSync(abci.RequestEndBlock{Height: block.Height})
if err != nil {
logger.Error("error in proxyAppConn.EndBlock", "err", err)
return nil, err
}
logger.Info("executed block", "height", block.Height, "num_valid_txs", validTxs, "num_invalid_txs", invalidTxs)
return abciResponses, nil
}
func buildLastCommitInfo(block *types.Block, store Store, initialHeight int64) abci.CommitInfo {
if block.Height == initialHeight {
// there is no last commit for the initial height.
@@ -495,7 +432,7 @@ func updateState(
state State,
blockID types.BlockID,
header *types.Header,
abciResponses *tmstate.ABCIResponses,
abciResponse *abci.ResponseFinalizeBlock,
validatorUpdates []*types.Validator,
) (State, error) {
@@ -503,7 +440,7 @@ func updateState(
// and update s.LastValidators and s.Validators.
nValSet := state.NextValidators.Copy()
// Update the validator set with the latest abciResponses.
// Update the validator set with the latest abciResponse.
lastHeightValsChanged := state.LastHeightValidatorsChanged
if len(validatorUpdates) > 0 {
err := nValSet.UpdateWithChangeSet(validatorUpdates)
@@ -517,12 +454,12 @@ func updateState(
// Update validator proposer priority and set state variables.
nValSet.IncrementProposerPriority(1)
// Update the params with the latest abciResponses.
// Update the params with the latest abciResponse.
nextParams := state.ConsensusParams
lastHeightParamsChanged := state.LastHeightConsensusParamsChanged
if abciResponses.EndBlock.ConsensusParamUpdates != nil {
if abciResponse.ConsensusParamUpdates != nil {
// NOTE: must not mutate s.ConsensusParams
nextParams = state.ConsensusParams.Update(abciResponses.EndBlock.ConsensusParamUpdates)
nextParams = state.ConsensusParams.Update(abciResponse.ConsensusParamUpdates)
err := nextParams.ValidateBasic()
if err != nil {
return state, fmt.Errorf("error updating consensus params: %v", err)
@@ -551,7 +488,7 @@ func updateState(
LastHeightValidatorsChanged: lastHeightValsChanged,
ConsensusParams: nextParams,
LastHeightConsensusParamsChanged: lastHeightParamsChanged,
LastResultsHash: ABCIResponsesResultsHash(abciResponses),
LastResultsHash: TxResultsHash(abciResponse.TxResults),
AppHash: nil,
}, nil
}
@@ -563,26 +500,32 @@ func fireEvents(
logger log.Logger,
eventBus types.BlockEventPublisher,
block *types.Block,
abciResponses *tmstate.ABCIResponses,
blockID types.BlockID,
abciResponse *abci.ResponseFinalizeBlock,
validatorUpdates []*types.Validator,
) {
if err := eventBus.PublishEventNewBlock(types.EventDataNewBlock{
Block: block,
ResultBeginBlock: *abciResponses.BeginBlock,
ResultEndBlock: *abciResponses.EndBlock,
Block: block,
BlockID: blockID,
ResultFinalizeBlock: *abciResponse,
}); err != nil {
logger.Error("failed publishing new block", "err", err)
}
if err := eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
Header: block.Header,
NumTxs: int64(len(block.Txs)),
ResultBeginBlock: *abciResponses.BeginBlock,
ResultEndBlock: *abciResponses.EndBlock,
Header: block.Header,
}); err != nil {
logger.Error("failed publishing new block header", "err", err)
}
if err := eventBus.PublishEventNewBlockEvents(types.EventDataNewBlockEvents{
Height: block.Height,
Events: abciResponse.Events,
NumTxs: int64(len(block.Txs)),
}); err != nil {
logger.Error("failed publishing new block events", "err", err)
}
if len(block.Evidence.Evidence) != 0 {
for _, ev := range block.Evidence.Evidence {
if err := eventBus.PublishEventNewEvidence(types.EventDataNewEvidence{
@@ -599,7 +542,7 @@ func fireEvents(
Height: block.Height,
Index: uint32(i),
Tx: tx,
Result: *(abciResponses.DeliverTxs[i]),
Result: *(abciResponse.TxResults[i]),
}}); err != nil {
logger.Error("failed publishing event TX", "err", err)
}
@@ -625,21 +568,37 @@ func ExecCommitBlock(
store Store,
initialHeight int64,
) ([]byte, error) {
_, err := execBlockOnProxyApp(logger, appConnConsensus, block, store, initialHeight)
commitInfo := buildLastCommitInfo(block, store, initialHeight)
resp, err := appConnConsensus.FinalizeBlock(context.TODO(), &abci.RequestFinalizeBlock{
Hash: block.Hash(),
NextValidatorsHash: block.NextValidatorsHash,
ProposerAddress: block.ProposerAddress,
Height: block.Height,
DecidedLastCommit: commitInfo,
Misbehavior: block.Evidence.Evidence.ToABCI(),
Txs: block.Txs.ToSliceOfBytes(),
})
if err != nil {
logger.Error("failed executing block on proxy app", "height", block.Height, "err", err)
logger.Error("error in proxyAppConn.FinalizeBlock", "err", err)
return nil, err
}
// Assert that the application correctly returned tx results for each of the transactions provided in the block
if len(block.Data.Txs) != len(resp.TxResults) {
return nil, fmt.Errorf("expected tx results length to match size of transactions in block. Expected %d, got %d", len(block.Data.Txs), len(resp.TxResults))
}
logger.Info("executed block", "height", block.Height, "agreed_app_data", resp.AgreedAppData)
// Commit block, get hash back
res, err := appConnConsensus.CommitSync()
_, err = appConnConsensus.Commit(context.TODO())
if err != nil {
logger.Error("client error during proxyAppConn.CommitSync", "err", res)
logger.Error("client error during proxyAppConn.CommitSync", "err", err)
return nil, err
}
// ResponseCommit has no error or log, just data
return res.Data, nil
return resp.AgreedAppData, nil
}
func (blockExec *BlockExecutor) pruneBlocks(retainHeight int64, state State) (uint64, error) {
+115 -38
View File
@@ -77,13 +77,90 @@ func TestApplyBlock(t *testing.T) {
assert.EqualValues(t, 1, state.Version.Consensus.App, "App version wasn't updated")
}
// TestBeginBlockValidators ensures we send absent validators list.
func TestBeginBlockValidators(t *testing.T) {
// TestFinalizeBlockDecidedLastCommit ensures we correctly send the
// DecidedLastCommit to the application. The test ensures that the
// DecidedLastCommit properly reflects which validators signed the preceding
// block.
func TestFinalizeBlockDecidedLastCommit(t *testing.T) {
app := &testApp{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
require.Nil(t, err)
require.NoError(t, err)
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
state, stateDB, privVals := makeState(7, 1)
stateStore := sm.NewStore(stateDB, sm.StoreOptions{
DiscardABCIResponses: false,
})
absentSig := types.NewCommitSigAbsent()
testCases := []struct {
name string
absentCommitSigs map[int]bool
}{
{"none absent", map[int]bool{}},
{"one absent", map[int]bool{1: true}},
{"multiple absent", map[int]bool{1: true, 3: true}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
blockStore := store.NewBlockStore(dbm.NewMemDB())
evpool := &mocks.EvidencePool{}
evpool.On("PendingEvidence", mock.Anything).Return([]types.Evidence{}, 0)
evpool.On("Update", mock.Anything, mock.Anything).Return()
evpool.On("CheckEvidence", mock.Anything).Return(nil)
mp := &mpmocks.Mempool{}
mp.On("Lock").Return()
mp.On("Unlock").Return()
mp.On("FlushAppConn", mock.Anything).Return(nil)
mp.On("Update",
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything,
mock.Anything).Return(nil)
eventBus := types.NewEventBus()
require.NoError(t, eventBus.Start())
blockExec := sm.NewBlockExecutor(stateStore, log.NewNopLogger(), proxyApp.Consensus(), mp, evpool, blockStore)
state, _, lastCommit, err := makeAndCommitGoodBlock(state, 1, new(types.Commit), state.NextValidators.Validators[0].Address, blockExec, privVals, nil)
require.NoError(t, err)
for idx, isAbsent := range tc.absentCommitSigs {
if isAbsent {
lastCommit.Signatures[idx] = absentSig
}
}
// block for height 2
block := makeBlock(state, 2, lastCommit)
bps, err := block.MakePartSet(testPartSize)
require.NoError(t, err)
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
_, err = blockExec.ApplyBlock(state, blockID, block)
require.NoError(t, err)
// -> app receives a list of validators with a bool indicating if they signed
for i, v := range app.CommitVotes {
_, absent := tc.absentCommitSigs[i]
assert.Equal(t, !absent, v.SignedLastBlock)
}
})
}
}
// TestFinalizeBlockValidators ensures we send absent validators list.
func TestFinalizeBlockValidators(t *testing.T) {
app := &testApp{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
require.NoError(t, err)
defer proxyApp.Stop() //nolint:errcheck // no need to check error again
state, stateDB, _ := makeState(2, 2)
@@ -142,13 +219,13 @@ func TestBeginBlockValidators(t *testing.T) {
}
}
// TestBeginBlockByzantineValidators ensures we send byzantine validators list.
func TestBeginBlockByzantineValidators(t *testing.T) {
// TestFinalizeBlockMisbehavior ensures we send misbehavior list.
func TestFinalizeBlockMisbehavior(t *testing.T) {
app := &testApp{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
require.Nil(t, err)
require.NoError(t, err)
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
state, stateDB, privVals := makeState(1, 1)
@@ -247,8 +324,8 @@ func TestBeginBlockByzantineValidators(t *testing.T) {
blockID = types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
state, err = blockExec.ApplyBlock(state, blockID, block)
require.Nil(t, err)
_, err = blockExec.ApplyBlock(state, blockID, block)
require.NoError(t, err)
// TODO check state and mempool
assert.Equal(t, abciMb, app.Misbehavior)
@@ -259,8 +336,8 @@ func TestProcessProposal(t *testing.T) {
txs := test.MakeNTxs(height, 10)
logger := log.NewNopLogger()
app := abcimocks.NewBaseMock()
app.On("ProcessProposal", mock.Anything).Return(abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT})
app := &abcimocks.Application{}
app.On("ProcessProposal", mock.Anything, mock.Anything).Return(&abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil)
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
@@ -316,7 +393,7 @@ func TestProcessProposal(t *testing.T) {
})
block1.Txs = txs
expectedRpp := abci.RequestProcessProposal{
expectedRpp := &abci.RequestProcessProposal{
Txs: block1.Txs.ToSliceOfBytes(),
Hash: block1.Hash(),
Height: block1.Header.Height,
@@ -334,7 +411,7 @@ func TestProcessProposal(t *testing.T) {
require.NoError(t, err)
require.True(t, acceptBlock)
app.AssertExpectations(t)
app.AssertCalled(t, "ProcessProposal", expectedRpp)
app.AssertCalled(t, "ProcessProposal", context.TODO(), expectedRpp)
}
func TestValidateValidatorUpdates(t *testing.T) {
@@ -467,13 +544,13 @@ func TestUpdateValidators(t *testing.T) {
}
}
// TestEndBlockValidatorUpdates ensures we update validator set and send an event.
func TestEndBlockValidatorUpdates(t *testing.T) {
// TestFinalizeBlockValidatorUpdates ensures we update validator set and send an event.
func TestFinalizeBlockValidatorUpdates(t *testing.T) {
app := &testApp{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
require.Nil(t, err)
require.NoError(t, err)
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
state, stateDB, _ := makeState(1, 1)
@@ -530,7 +607,7 @@ func TestEndBlockValidatorUpdates(t *testing.T) {
}
state, err = blockExec.ApplyBlock(state, blockID, block)
require.Nil(t, err)
require.NoError(t, err)
// test new validator was added to NextValidators
if assert.Equal(t, state.Validators.Size()+1, state.NextValidators.Size()) {
idx, _ := state.NextValidators.GetByAddress(pubkey.Address())
@@ -555,14 +632,14 @@ func TestEndBlockValidatorUpdates(t *testing.T) {
}
}
// TestEndBlockValidatorUpdatesResultingInEmptySet checks that processing validator updates that
// TestFinalizeBlockValidatorUpdatesResultingInEmptySet checks that processing validator updates that
// would result in empty set causes no panic, an error is raised and NextValidators is not updated
func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) {
func TestFinalizeBlockValidatorUpdatesResultingInEmptySet(t *testing.T) {
app := &testApp{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
require.Nil(t, err)
require.NoError(t, err)
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
state, stateDB, _ := makeState(1, 1)
@@ -592,14 +669,14 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) {
}
assert.NotPanics(t, func() { state, err = blockExec.ApplyBlock(state, blockID, block) })
assert.NotNil(t, err)
assert.Error(t, err)
assert.NotEmpty(t, state.NextValidators.Validators)
}
func TestEmptyPrepareProposal(t *testing.T) {
const height = 2
app := abcimocks.NewBaseMock()
app := &abci.BaseApplication{}
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
@@ -654,12 +731,12 @@ func TestPrepareProposalTxsAllIncluded(t *testing.T) {
txs := test.MakeNTxs(height, 10)
mp := &mpmocks.Mempool{}
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(types.Txs(txs[2:]))
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(txs[2:])
app := abcimocks.NewBaseMock()
app.On("PrepareProposal", mock.Anything).Return(abci.ResponsePrepareProposal{
Txs: types.Txs(txs).ToSliceOfBytes(),
})
app := &abcimocks.Application{}
app.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{
Txs: txs.ToSliceOfBytes(),
}, nil)
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
err := proxyApp.Start()
@@ -703,15 +780,15 @@ func TestPrepareProposalReorderTxs(t *testing.T) {
txs := test.MakeNTxs(height, 10)
mp := &mpmocks.Mempool{}
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(types.Txs(txs))
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(txs)
txs = txs[2:]
txs = append(txs[len(txs)/2:], txs[:len(txs)/2]...)
app := abcimocks.NewBaseMock()
app.On("PrepareProposal", mock.Anything).Return(abci.ResponsePrepareProposal{
Txs: types.Txs(txs).ToSliceOfBytes(),
})
app := &abcimocks.Application{}
app.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{
Txs: txs.ToSliceOfBytes(),
}, nil)
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
@@ -761,12 +838,12 @@ func TestPrepareProposalErrorOnTooManyTxs(t *testing.T) {
maxDataBytes := types.MaxDataBytes(state.ConsensusParams.Block.MaxBytes, 0, nValidators)
txs := test.MakeNTxs(height, maxDataBytes/bytesPerTx+2) // +2 so that tx don't fit
mp := &mpmocks.Mempool{}
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(types.Txs(txs))
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(txs)
app := abcimocks.NewBaseMock()
app.On("PrepareProposal", mock.Anything).Return(abci.ResponsePrepareProposal{
Txs: types.Txs(txs).ToSliceOfBytes(),
})
app := &abcimocks.Application{}
app.On("PrepareProposal", mock.Anything, mock.Anything).Return(&abci.ResponsePrepareProposal{
Txs: txs.ToSliceOfBytes(),
}, nil)
cc := proxy.NewLocalClientCreator(app)
proxyApp := proxy.NewAppConns(cc, proxy.NopMetrics())
@@ -809,13 +886,13 @@ func TestPrepareProposalErrorOnPrepareProposalError(t *testing.T) {
txs := test.MakeNTxs(height, 10)
mp := &mpmocks.Mempool{}
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(types.Txs(txs))
mp.On("ReapMaxBytesMaxGas", mock.Anything, mock.Anything).Return(txs)
cm := &abciclientmocks.Client{}
cm.On("SetLogger", mock.Anything).Return()
cm.On("Start").Return(nil)
cm.On("Quit").Return(nil)
cm.On("PrepareProposalSync", mock.Anything).Return(nil, errors.New("an injected error")).Once()
cm.On("PrepareProposal", mock.Anything, mock.Anything).Return(nil, errors.New("an injected error")).Once()
cm.On("Stop").Return(nil)
cc := &pmocks.ClientCreator{}
cc.On("NewABCIClient").Return(cm, nil)
+2 -3
View File
@@ -4,7 +4,6 @@ import (
dbm "github.com/tendermint/tm-db"
abci "github.com/tendermint/tendermint/abci/types"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
"github.com/tendermint/tendermint/types"
)
@@ -27,10 +26,10 @@ func UpdateState(
state State,
blockID types.BlockID,
header *types.Header,
abciResponses *tmstate.ABCIResponses,
resp *abci.ResponseFinalizeBlock,
validatorUpdates []*types.Validator,
) (State, error) {
return updateState(state, blockID, header, abciResponses, validatorUpdates)
return updateState(state, blockID, header, resp, validatorUpdates)
}
// ValidateValidatorUpdates is an alias for validateValidatorUpdates exported
+33 -53
View File
@@ -2,6 +2,7 @@ package state_test
import (
"bytes"
"context"
"fmt"
"testing"
"time"
@@ -12,7 +13,6 @@ import (
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/internal/test"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
"github.com/tendermint/tendermint/proxy"
sm "github.com/tendermint/tendermint/state"
@@ -153,21 +153,16 @@ func makeHeaderPartsResponsesValPubKeyChange(
t *testing.T,
state sm.State,
pubkey crypto.PubKey,
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
) (types.Header, types.BlockID, *abci.ResponseFinalizeBlock) {
block := makeBlock(state, state.LastBlockHeight+1, new(types.Commit))
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
abciResponses := &abci.ResponseFinalizeBlock{}
// If the pubkey is new, remove the old and add the new.
_, val := state.NextValidators.GetByIndex(0)
if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) {
abciResponses.EndBlock = &abci.ResponseEndBlock{
ValidatorUpdates: []abci.ValidatorUpdate{
types.TM2PB.NewValidatorUpdate(val.PubKey, 0),
types.TM2PB.NewValidatorUpdate(pubkey, 10),
},
abciResponses.ValidatorUpdates = []abci.ValidatorUpdate{
types.TM2PB.NewValidatorUpdate(val.PubKey, 0),
types.TM2PB.NewValidatorUpdate(pubkey, 10),
}
}
@@ -178,21 +173,16 @@ func makeHeaderPartsResponsesValPowerChange(
t *testing.T,
state sm.State,
power int64,
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
) (types.Header, types.BlockID, *abci.ResponseFinalizeBlock) {
block := makeBlock(state, state.LastBlockHeight+1, new(types.Commit))
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
abciResponses := &abci.ResponseFinalizeBlock{}
// If the pubkey is new, remove the old and add the new.
_, val := state.NextValidators.GetByIndex(0)
if val.VotingPower != power {
abciResponses.EndBlock = &abci.ResponseEndBlock{
ValidatorUpdates: []abci.ValidatorUpdate{
types.TM2PB.NewValidatorUpdate(val.PubKey, power),
},
abciResponses.ValidatorUpdates = []abci.ValidatorUpdate{
types.TM2PB.NewValidatorUpdate(val.PubKey, power),
}
}
@@ -203,12 +193,11 @@ func makeHeaderPartsResponsesParams(
t *testing.T,
state sm.State,
params tmproto.ConsensusParams,
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
) (types.Header, types.BlockID, *abci.ResponseFinalizeBlock) {
block := makeBlock(state, state.LastBlockHeight+1, new(types.Commit))
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: &params},
abciResponses := &abci.ResponseFinalizeBlock{
ConsensusParamUpdates: &params,
}
return block.Header, types.BlockID{Hash: block.Hash(), PartSetHeader: types.PartSetHeader{}}, abciResponses
}
@@ -238,49 +227,40 @@ type testApp struct {
CommitVotes []abci.VoteInfo
Misbehavior []abci.Misbehavior
ValidatorUpdates []abci.ValidatorUpdate
AgreedAppData []byte
}
var _ abci.Application = (*testApp)(nil)
func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) {
return abci.ResponseInfo{}
}
func (app *testApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
app.CommitVotes = req.DecidedLastCommit.Votes
app.Misbehavior = req.Misbehavior
txResults := make([]*abci.ExecTxResult, len(req.Txs))
for idx := range req.Txs {
txResults[idx] = &abci.ExecTxResult{
Code: abci.CodeTypeOK,
}
}
func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock {
app.CommitVotes = req.LastCommitInfo.Votes
app.Misbehavior = req.ByzantineValidators
return abci.ResponseBeginBlock{}
}
func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock {
return abci.ResponseEndBlock{
return &abci.ResponseFinalizeBlock{
ValidatorUpdates: app.ValidatorUpdates,
ConsensusParamUpdates: &tmproto.ConsensusParams{
Version: &tmproto.VersionParams{
App: 1}}}
App: 1}},
TxResults: txResults,
AgreedAppData: app.AgreedAppData,
}, nil
}
func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
return abci.ResponseDeliverTx{Events: []abci.Event{}}
func (app *testApp) Commit(_ context.Context, _ *abci.RequestCommit) (*abci.ResponseCommit, error) {
return &abci.ResponseCommit{RetainHeight: 1}, nil
}
func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
return abci.ResponseCheckTx{}
}
func (app *testApp) Commit() abci.ResponseCommit {
return abci.ResponseCommit{RetainHeight: 1}
}
func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) {
return
}
func (app *testApp) ProcessProposal(req abci.RequestProcessProposal) abci.ResponseProcessProposal {
func (app *testApp) ProcessProposal(_ context.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
for _, tx := range req.Txs {
if len(tx) == 0 {
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
}
return abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
}
+4 -4
View File
@@ -15,10 +15,10 @@ type BlockIndexer interface {
// upon database query failure.
Has(height int64) (bool, error)
// Index indexes BeginBlock and EndBlock events for a given block by its height.
Index(types.EventDataNewBlockHeader) error
// Index indexes FinalizeBlock events for a given block by its height.
Index(types.EventDataNewBlockEvents) error
// Search performs a query for block heights that match a given BeginBlock
// and Endblock event search criteria.
// Search performs a query for block heights that match a given FinalizeBlock
// event search criteria.
Search(ctx context.Context, q *query.Query) ([]int64, error)
}
+10 -16
View File
@@ -20,7 +20,7 @@ import (
var _ indexer.BlockIndexer = (*BlockerIndexer)(nil)
// BlockerIndexer implements a block indexer, indexing BeginBlock and EndBlock
// BlockerIndexer implements a block indexer, indexing FinalizeBlock
// events with an underlying KV store. Block events are indexed by their height,
// such that matching search criteria returns the respective block height(s).
type BlockerIndexer struct {
@@ -44,17 +44,16 @@ func (idx *BlockerIndexer) Has(height int64) (bool, error) {
return idx.store.Has(key)
}
// Index indexes BeginBlock and EndBlock events for a given block by its height.
// Index indexes FinalizeBlock events for a given block by its height.
// The following is indexed:
//
// primary key: encode(block.height | height) => encode(height)
// BeginBlock events: encode(eventType.eventAttr|eventValue|height|begin_block) => encode(height)
// EndBlock events: encode(eventType.eventAttr|eventValue|height|end_block) => encode(height)
func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockHeader) error {
// FinalizeBlock events: encode(eventType.eventAttr|eventValue|height|finalize_block) => encode(height)
func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockEvents) error {
batch := idx.store.NewBatch()
defer batch.Close()
height := bh.Header.Height
height := bh.Height
// 1. index by height
key, err := heightKey(height)
@@ -65,21 +64,16 @@ func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockHeader) error {
return err
}
// 2. index BeginBlock events
if err := idx.indexEvents(batch, bh.ResultBeginBlock.Events, "begin_block", height); err != nil {
return fmt.Errorf("failed to index BeginBlock events: %w", err)
}
// 3. index EndBlock events
if err := idx.indexEvents(batch, bh.ResultEndBlock.Events, "end_block", height); err != nil {
return fmt.Errorf("failed to index EndBlock events: %w", err)
// 2. index block events
if err := idx.indexEvents(batch, bh.Events, "finalize_block", height); err != nil {
return fmt.Errorf("failed to index FinalizeBlock events: %w", err)
}
return batch.WriteSync()
}
// Search performs a query for block heights that match a given BeginBlock
// and Endblock event search criteria. The given query can match against zero,
// Search performs a query for block heights that match a given FinalizeBlock
// event search criteria. The given query can match against zero,
// one or more block heights. In the case of height queries, i.e. block.height=H,
// if the height is indexed, that height alone will be returned. An error and
// nil slice is returned. Otherwise, a non-nil slice and nil error is returned.
+34 -46
View File
@@ -18,32 +18,26 @@ func TestBlockIndexer(t *testing.T) {
store := db.NewPrefixDB(db.NewMemDB(), []byte("block_events"))
indexer := blockidxkv.New(store)
require.NoError(t, indexer.Index(types.EventDataNewBlockHeader{
Header: types.Header{Height: 1},
ResultBeginBlock: abci.ResponseBeginBlock{
Events: []abci.Event{
{
Type: "begin_event",
Attributes: []abci.EventAttribute{
{
Key: "proposer",
Value: "FCAA001",
Index: true,
},
require.NoError(t, indexer.Index(types.EventDataNewBlockEvents{
Height: 1,
Events: []abci.Event{
{
Type: "begin_event",
Attributes: []abci.EventAttribute{
{
Key: "proposer",
Value: "FCAA001",
Index: true,
},
},
},
},
ResultEndBlock: abci.ResponseEndBlock{
Events: []abci.Event{
{
Type: "end_event",
Attributes: []abci.EventAttribute{
{
Key: "foo",
Value: "100",
Index: true,
},
{
Type: "end_event",
Attributes: []abci.EventAttribute{
{
Key: "foo",
Value: "100",
Index: true,
},
},
},
@@ -56,32 +50,26 @@ func TestBlockIndexer(t *testing.T) {
index = true
}
require.NoError(t, indexer.Index(types.EventDataNewBlockHeader{
Header: types.Header{Height: int64(i)},
ResultBeginBlock: abci.ResponseBeginBlock{
Events: []abci.Event{
{
Type: "begin_event",
Attributes: []abci.EventAttribute{
{
Key: "proposer",
Value: "FCAA001",
Index: true,
},
require.NoError(t, indexer.Index(types.EventDataNewBlockEvents{
Height: int64(i),
Events: []abci.Event{
{
Type: "begin_event",
Attributes: []abci.EventAttribute{
{
Key: "proposer",
Value: "FCAA001",
Index: true,
},
},
},
},
ResultEndBlock: abci.ResponseEndBlock{
Events: []abci.Event{
{
Type: "end_event",
Attributes: []abci.EventAttribute{
{
Key: "foo",
Value: fmt.Sprintf("%d", i),
Index: index,
},
{
Type: "end_event",
Attributes: []abci.EventAttribute{
{
Key: "foo",
Value: fmt.Sprintf("%d", i),
Index: index,
},
},
},
+1 -1
View File
@@ -18,7 +18,7 @@ func (idx *BlockerIndexer) Has(height int64) (bool, error) {
return false, errors.New(`indexing is disabled (set 'tx_index = "kv"' in config)`)
}
func (idx *BlockerIndexer) Index(types.EventDataNewBlockHeader) error {
func (idx *BlockerIndexer) Index(types.EventDataNewBlockEvents) error {
return nil
}
+2 -2
View File
@@ -39,11 +39,11 @@ func (_m *BlockIndexer) Has(height int64) (bool, error) {
}
// Index provides a mock function with given fields: _a0
func (_m *BlockIndexer) Index(_a0 types.EventDataNewBlockHeader) error {
func (_m *BlockIndexer) Index(_a0 types.EventDataNewBlockEvents) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(types.EventDataNewBlockHeader) error); ok {
if rf, ok := ret.Get(0).(func(types.EventDataNewBlockEvents) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
+2 -3
View File
@@ -24,8 +24,7 @@ import (
)
const (
eventTypeBeginBlock = "begin_block"
eventTypeEndBlock = "end_block"
eventTypeFinalizeBlock = "finaliz_block"
)
// TxIndexer returns a bridge from es to the Tendermint v0.34 transaction indexer.
@@ -77,7 +76,7 @@ func (BackportBlockIndexer) Has(height int64) (bool, error) {
// Index indexes block begin and end events for the specified block. It is
// part of the BlockIndexer interface.
func (b BackportBlockIndexer) Index(block types.EventDataNewBlockHeader) error {
func (b BackportBlockIndexer) Index(block types.EventDataNewBlockEvents) error {
return b.psql.IndexBlockEvents(block)
}
+5 -8
View File
@@ -139,7 +139,7 @@ func makeIndexedEvent(compositeKey, value string) abci.Event {
// IndexBlockEvents indexes the specified block header, part of the
// indexer.EventSink interface.
func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error {
func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockEvents) error {
ts := time.Now().UTC()
return runInTransaction(es.store, func(dbtx *sql.Tx) error {
@@ -150,7 +150,7 @@ INSERT INTO `+tableBlocks+` (height, chain_id, created_at)
VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
RETURNING rowid;
`, h.Header.Height, es.chainID, ts)
`, h.Height, es.chainID, ts)
if err == sql.ErrNoRows {
return nil // we already saw this block; quietly succeed
} else if err != nil {
@@ -159,16 +159,13 @@ INSERT INTO `+tableBlocks+` (height, chain_id, created_at)
// Insert the special block meta-event for height.
if err := insertEvents(dbtx, blockID, 0, []abci.Event{
makeIndexedEvent(types.BlockHeightKey, fmt.Sprint(h.Header.Height)),
makeIndexedEvent(types.BlockHeightKey, fmt.Sprint(h.Height)),
}); err != nil {
return fmt.Errorf("block meta-events: %w", err)
}
// Insert all the block events. Order is important here,
if err := insertEvents(dbtx, blockID, 0, h.ResultBeginBlock.Events); err != nil {
return fmt.Errorf("begin-block events: %w", err)
}
if err := insertEvents(dbtx, blockID, 0, h.ResultEndBlock.Events); err != nil {
return fmt.Errorf("end-block events: %w", err)
if err := insertEvents(dbtx, blockID, 0, h.Events); err != nil {
return fmt.Errorf("finalizeblock events: %w", err)
}
return nil
})
+21 -34
View File
@@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
tmlog "github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/state/txindex"
"github.com/tendermint/tendermint/types"
@@ -140,7 +141,7 @@ func TestMain(m *testing.M) {
func TestIndexing(t *testing.T) {
t.Run("IndexBlockEvents", func(t *testing.T) {
indexer := &EventSink{store: testDB(), chainID: chainID}
require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
require.NoError(t, indexer.IndexBlockEvents(newTestBlockEvents()))
verifyBlock(t, 1)
verifyBlock(t, 2)
@@ -156,7 +157,7 @@ func TestIndexing(t *testing.T) {
require.NoError(t, verifyTimeStamp(tableBlocks))
// Attempting to reindex the same events should gracefully succeed.
require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
require.NoError(t, indexer.IndexBlockEvents(newTestBlockEvents()))
})
t.Run("IndexTxEvents", func(t *testing.T) {
@@ -212,6 +213,7 @@ func TestIndexing(t *testing.T) {
})
service := txindex.NewIndexerService(indexer.TxIndexer(), indexer.BlockIndexer(), eventBus, true)
service.SetLogger(tmlog.TestingLogger())
err = service.Start()
require.NoError(t, err)
t.Cleanup(func() {
@@ -221,16 +223,16 @@ func TestIndexing(t *testing.T) {
})
// publish block with txs
err = eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
Header: types.Header{Height: 1},
NumTxs: int64(2),
err = eventBus.PublishEventNewBlockEvents(types.EventDataNewBlockEvents{
Height: 1,
NumTxs: 2,
})
require.NoError(t, err)
txResult1 := &abci.TxResult{
Height: 1,
Index: uint32(0),
Tx: types.Tx("foo"),
Result: abci.ResponseDeliverTx{Code: 0},
Result: abci.ExecTxResult{Code: 0},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1})
require.NoError(t, err)
@@ -238,7 +240,7 @@ func TestIndexing(t *testing.T) {
Height: 1,
Index: uint32(1),
Tx: types.Tx("bar"),
Result: abci.ResponseDeliverTx{Code: 1},
Result: abci.ExecTxResult{Code: 1},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2})
require.NoError(t, err)
@@ -253,22 +255,16 @@ func TestStop(t *testing.T) {
require.NoError(t, indexer.Stop())
}
// newTestBlockHeader constructs a fresh copy of a block header containing
// newTestBlock constructs a fresh copy of a new block event containing
// known test values to exercise the indexer.
func newTestBlockHeader() types.EventDataNewBlockHeader {
return types.EventDataNewBlockHeader{
Header: types.Header{Height: 1},
ResultBeginBlock: abci.ResponseBeginBlock{
Events: []abci.Event{
makeIndexedEvent("begin_event.proposer", "FCAA001"),
makeIndexedEvent("thingy.whatzit", "O.O"),
},
},
ResultEndBlock: abci.ResponseEndBlock{
Events: []abci.Event{
makeIndexedEvent("end_event.foo", "100"),
makeIndexedEvent("thingy.whatzit", "-.O"),
},
func newTestBlockEvents() types.EventDataNewBlockEvents {
return types.EventDataNewBlockEvents{
Height: 1,
Events: []abci.Event{
makeIndexedEvent("begin_event.proposer", "FCAA001"),
makeIndexedEvent("thingy.whatzit", "O.O"),
makeIndexedEvent("end_event.foo", "100"),
makeIndexedEvent("thingy.whatzit", "-.O"),
},
}
}
@@ -307,7 +303,7 @@ func txResultWithEvents(events []abci.Event) *abci.TxResult {
Height: 1,
Index: 0,
Tx: types.Tx("HELLO WORLD"),
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK,
Log: "",
@@ -355,17 +351,8 @@ SELECT height FROM `+tableBlocks+` WHERE height = $1;
if err := testDB().QueryRow(`
SELECT type, height, chain_id FROM `+viewBlockEvents+`
WHERE height = $1 AND type = $2 AND chain_id = $3;
`, height, eventTypeBeginBlock, chainID).Err(); err == sql.ErrNoRows {
t.Errorf("No %q event found for height=%d", eventTypeBeginBlock, height)
} else if err != nil {
t.Fatalf("Database query failed: %v", err)
}
if err := testDB().QueryRow(`
SELECT type, height, chain_id FROM `+viewBlockEvents+`
WHERE height = $1 AND type = $2 AND chain_id = $3;
`, height, eventTypeEndBlock, chainID).Err(); err == sql.ErrNoRows {
t.Errorf("No %q event found for height=%d", eventTypeEndBlock, height)
`, height, eventTypeFinalizeBlock, chainID).Err(); err == sql.ErrNoRows {
t.Errorf("No %q event found for height=%d", eventTypeFinalizeBlock, height)
} else if err != nil {
t.Fatalf("Database query failed: %v", err)
}
+1 -1
View File
@@ -18,7 +18,7 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics {
Namespace: namespace,
Subsystem: MetricsSubsystem,
Name: "block_processing_time",
Help: "Time between BeginBlock and EndBlock in ms.",
Help: "Time spent processig finalize block.",
Buckets: stdprometheus.LinearBuckets(1, 10, 10),
}, labels).With(labelsAndValues...),
+1 -1
View File
@@ -14,7 +14,7 @@ const (
// Metrics contains metrics exposed by this package.
type Metrics struct {
// Time between BeginBlock and EndBlock in ms.
// Time spent processing FinalizeBlock
BlockProcessingTime metrics.Histogram `metrics_buckettype:"lin" metrics_bucketsizes:"1, 10, 10"`
// ConsensusParamUpdates is the total number of times the application has
+22 -22
View File
@@ -4,9 +4,9 @@ package mocks
import (
mock "github.com/stretchr/testify/mock"
state "github.com/tendermint/tendermint/state"
abcitypes "github.com/tendermint/tendermint/abci/types"
tendermintstate "github.com/tendermint/tendermint/proto/tendermint/state"
state "github.com/tendermint/tendermint/state"
types "github.com/tendermint/tendermint/types"
)
@@ -65,17 +65,15 @@ func (_m *Store) Load() (state.State, error) {
return r0, r1
}
// LoadABCIResponses provides a mock function with given fields: _a0
func (_m *Store) LoadABCIResponses(_a0 int64) (*tendermintstate.ABCIResponses, error) {
// LoadConsensusParams provides a mock function with given fields: _a0
func (_m *Store) LoadConsensusParams(_a0 int64) (types.ConsensusParams, error) {
ret := _m.Called(_a0)
var r0 *tendermintstate.ABCIResponses
if rf, ok := ret.Get(0).(func(int64) *tendermintstate.ABCIResponses); ok {
var r0 types.ConsensusParams
if rf, ok := ret.Get(0).(func(int64) types.ConsensusParams); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*tendermintstate.ABCIResponses)
}
r0 = ret.Get(0).(types.ConsensusParams)
}
var r1 error
@@ -88,15 +86,17 @@ func (_m *Store) LoadABCIResponses(_a0 int64) (*tendermintstate.ABCIResponses, e
return r0, r1
}
// LoadConsensusParams provides a mock function with given fields: _a0
func (_m *Store) LoadConsensusParams(_a0 int64) (types.ConsensusParams, error) {
// LoadFinalizeBlockResponse provides a mock function with given fields: _a0
func (_m *Store) LoadFinalizeBlockResponse(_a0 int64) (*abcitypes.ResponseFinalizeBlock, error) {
ret := _m.Called(_a0)
var r0 types.ConsensusParams
if rf, ok := ret.Get(0).(func(int64) types.ConsensusParams); ok {
var r0 *abcitypes.ResponseFinalizeBlock
if rf, ok := ret.Get(0).(func(int64) *abcitypes.ResponseFinalizeBlock); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(types.ConsensusParams)
if ret.Get(0) != nil {
r0 = ret.Get(0).(*abcitypes.ResponseFinalizeBlock)
}
}
var r1 error
@@ -151,16 +151,16 @@ func (_m *Store) LoadFromDBOrGenesisFile(_a0 string) (state.State, error) {
return r0, r1
}
// LoadLastABCIResponse provides a mock function with given fields: _a0
func (_m *Store) LoadLastABCIResponse(_a0 int64) (*tendermintstate.ABCIResponses, error) {
// LoadLastFinalizeBlockResponse provides a mock function with given fields: _a0
func (_m *Store) LoadLastFinalizeBlockResponse(_a0 int64) (*abcitypes.ResponseFinalizeBlock, error) {
ret := _m.Called(_a0)
var r0 *tendermintstate.ABCIResponses
if rf, ok := ret.Get(0).(func(int64) *tendermintstate.ABCIResponses); ok {
var r0 *abcitypes.ResponseFinalizeBlock
if rf, ok := ret.Get(0).(func(int64) *abcitypes.ResponseFinalizeBlock); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*tendermintstate.ABCIResponses)
r0 = ret.Get(0).(*abcitypes.ResponseFinalizeBlock)
}
}
@@ -225,12 +225,12 @@ func (_m *Store) Save(_a0 state.State) error {
return r0
}
// SaveABCIResponses provides a mock function with given fields: _a0, _a1
func (_m *Store) SaveABCIResponses(_a0 int64, _a1 *tendermintstate.ABCIResponses) error {
// SaveFinalizeBlockResponse provides a mock function with given fields: _a0, _a1
func (_m *Store) SaveFinalizeBlockResponse(_a0 int64, _a1 *abcitypes.ResponseFinalizeBlock) error {
ret := _m.Called(_a0, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(int64, *tendermintstate.ABCIResponses) error); ok {
if rf, ok := ret.Get(0).(func(int64, *abcitypes.ResponseFinalizeBlock) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
+1 -1
View File
@@ -68,7 +68,7 @@ type State struct {
LastHeightValidatorsChanged int64
// Consensus parameters used for validating blocks.
// Changes returned by EndBlock and updated after Commit.
// Changes returned by FinalizeBlock and updated after Commit.
ConsensusParams types.ConsensusParams
LastHeightConsensusParamsChanged int64
+59 -91
View File
@@ -18,7 +18,6 @@ import (
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
"github.com/tendermint/tendermint/internal/test"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
sm "github.com/tendermint/tendermint/state"
"github.com/tendermint/tendermint/types"
)
@@ -94,8 +93,8 @@ func TestStateSaveLoad(t *testing.T) {
loadedState, state))
}
// TestABCIResponsesSaveLoad tests saving and loading ABCIResponses.
func TestABCIResponsesSaveLoad1(t *testing.T) {
// TestFinalizeBlockResponsesSaveLoad1 tests saving and loading ABCIResponses.
func TestFinalizeBlockResponsesSaveLoad1(t *testing.T) {
tearDown, stateDB, state := setupTestCase(t)
defer tearDown(t)
stateStore := sm.NewStore(stateDB, sm.StoreOptions{
@@ -108,27 +107,25 @@ func TestABCIResponsesSaveLoad1(t *testing.T) {
// Build mock responses.
block := makeBlock(state, 2, new(types.Commit))
abciResponses := new(tmstate.ABCIResponses)
dtxs := make([]*abci.ResponseDeliverTx, 2)
abciResponses.DeliverTxs = dtxs
abciResponses := new(abci.ResponseFinalizeBlock)
dtxs := make([]*abci.ExecTxResult, 2)
abciResponses.TxResults = dtxs
abciResponses.DeliverTxs[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Events: nil}
abciResponses.DeliverTxs[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Events: nil}
abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{
abciResponses.TxResults[0] = &abci.ExecTxResult{Data: []byte("foo"), Events: nil}
abciResponses.TxResults[1] = &abci.ExecTxResult{Data: []byte("bar"), Log: "ok", Events: nil}
abciResponses.ValidatorUpdates = []abci.ValidatorUpdate{
types.TM2PB.NewValidatorUpdate(ed25519.GenPrivKey().PubKey(), 10),
}}
}
err := stateStore.SaveABCIResponses(block.Height, abciResponses)
err := stateStore.SaveFinalizeBlockResponse(block.Height, abciResponses)
require.NoError(t, err)
loadedABCIResponses, err := stateStore.LoadABCIResponses(block.Height)
assert.Nil(err)
assert.Equal(abciResponses, loadedABCIResponses,
fmt.Sprintf("ABCIResponses don't match:\ngot: %v\nexpected: %v\n",
loadedABCIResponses, abciResponses))
loadedABCIResponses, err := stateStore.LoadFinalizeBlockResponse(block.Height)
assert.NoError(err)
assert.Equal(abciResponses, loadedABCIResponses)
}
// TestResultsSaveLoad tests saving and loading ABCI results.
func TestABCIResponsesSaveLoad2(t *testing.T) {
// TestResultsSaveLoad tests saving and loading FinalizeBlock results.
func TestFinalizeBlockResponsesSaveLoad2(t *testing.T) {
tearDown, stateDB, _ := setupTestCase(t)
defer tearDown(t)
assert := assert.New(t)
@@ -140,22 +137,22 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
cases := [...]struct {
// Height is implied to equal index+2,
// as block 1 is created from genesis.
added []*abci.ResponseDeliverTx
expected []*abci.ResponseDeliverTx
added []*abci.ExecTxResult
expected []*abci.ExecTxResult
}{
0: {
nil,
nil,
},
1: {
[]*abci.ResponseDeliverTx{
[]*abci.ExecTxResult{
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
},
[]*abci.ResponseDeliverTx{
[]*abci.ExecTxResult{
{Code: 32, Data: []byte("Hello")},
}},
2: {
[]*abci.ResponseDeliverTx{
[]*abci.ExecTxResult{
{Code: 383},
{
Data: []byte("Gotcha!"),
@@ -165,7 +162,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
},
},
},
[]*abci.ResponseDeliverTx{
[]*abci.ExecTxResult{
{Code: 383, Data: nil},
{Code: 0, Data: []byte("Gotcha!"), Events: []abci.Event{
{Type: "type1", Attributes: []abci.EventAttribute{{Key: "a", Value: "1"}}},
@@ -177,7 +174,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
nil,
},
4: {
[]*abci.ResponseDeliverTx{nil},
[]*abci.ExecTxResult{nil},
nil,
},
}
@@ -185,34 +182,32 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
// Query all before, this should return error.
for i := range cases {
h := int64(i + 1)
res, err := stateStore.LoadABCIResponses(h)
res, err := stateStore.LoadFinalizeBlockResponse(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 := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: tc.added,
EndBlock: &abci.ResponseEndBlock{},
responses := &abci.ResponseFinalizeBlock{
TxResults: tc.added,
AgreedAppData: []byte(fmt.Sprintf("%d", h)),
}
err := stateStore.SaveABCIResponses(h, responses)
err := stateStore.SaveFinalizeBlockResponse(h, responses)
require.NoError(t, err)
}
// Query all before, should return expected value.
for i, tc := range cases {
h := int64(i + 1)
res, err := stateStore.LoadABCIResponses(h)
res, err := stateStore.LoadFinalizeBlockResponse(h)
if assert.NoError(err, "%d", i) {
t.Log(res)
responses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: tc.expected,
EndBlock: &abci.ResponseEndBlock{},
responses := &abci.ResponseFinalizeBlock{
TxResults: tc.expected,
AgreedAppData: []byte(fmt.Sprintf("%d", h)),
}
assert.Equal(sm.ABCIResponsesResultsHash(responses), sm.ABCIResponsesResultsHash(res), "%d", i)
assert.Equal(sm.TxResultsHash(responses.TxResults), sm.TxResultsHash(res.TxResults), "%d", i)
}
}
}
@@ -281,7 +276,7 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) {
power++
}
header, blockID, responses := makeHeaderPartsResponsesValPowerChange(t, state, power)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.ValidatorUpdates)
require.NoError(t, err)
state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates)
require.NoError(t, err)
@@ -458,11 +453,8 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) {
bps, err := block.MakePartSet(testPartSize)
require.NoError(t, err)
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
assert.NoError(t, err)
@@ -575,11 +567,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
require.NoError(t, err)
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
// no updates:
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
@@ -639,7 +628,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
updatedVal2,
)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
updatedState3, err := sm.UpdateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates)
@@ -678,11 +667,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
// no changes in voting power and both validators have same voting power
// -> proposers should alternate:
oldState := updatedState3
abciResponses = &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses = &abci.ResponseFinalizeBlock{}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
oldState, err = sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates)
@@ -694,11 +680,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
for i := 0; i < 1000; i++ {
// no validator updates:
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates)
@@ -752,11 +735,8 @@ func TestLargeGenesisValidator(t *testing.T) {
oldState := state
for i := 0; i < 10; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
block := makeBlock(oldState, oldState.LastBlockHeight+1, new(types.Commit))
@@ -786,9 +766,8 @@ func TestLargeGenesisValidator(t *testing.T) {
firstAddedVal := abci.ValidatorUpdate{PubKey: fvp, Power: firstAddedValVotingPower}
validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{firstAddedVal})
assert.NoError(t, err)
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}},
abciResponses := &abci.ResponseFinalizeBlock{
ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal},
}
block := makeBlock(oldState, oldState.LastBlockHeight+1, new(types.Commit))
@@ -802,11 +781,8 @@ func TestLargeGenesisValidator(t *testing.T) {
lastState := updatedState
for i := 0; i < 200; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
block := makeBlock(lastState, lastState.LastBlockHeight+1, new(types.Commit))
@@ -842,9 +818,8 @@ func TestLargeGenesisValidator(t *testing.T) {
validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{addedVal})
assert.NoError(t, err)
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}},
abciResponses := &abci.ResponseFinalizeBlock{
ValidatorUpdates: []abci.ValidatorUpdate{addedVal},
}
block := makeBlock(oldState, oldState.LastBlockHeight+1, new(types.Commit))
bps, err := block.MakePartSet(testPartSize)
@@ -860,9 +835,8 @@ func TestLargeGenesisValidator(t *testing.T) {
gp, err := cryptoenc.PubKeyToProto(genesisPubKey)
require.NoError(t, err)
removeGenesisVal := abci.ValidatorUpdate{PubKey: gp, Power: 0}
abciResponses = &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}},
abciResponses = &abci.ResponseFinalizeBlock{
ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal},
}
block = makeBlock(oldState, oldState.LastBlockHeight+1, new(types.Commit))
@@ -872,7 +846,7 @@ func TestLargeGenesisValidator(t *testing.T) {
require.NoError(t, err)
blockID = types.BlockID{Hash: block.Hash(), PartSetHeader: bps.Header()}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
updatedState, err = sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
require.NoError(t, err)
@@ -885,11 +859,8 @@ func TestLargeGenesisValidator(t *testing.T) {
count := 0
isProposerUnchanged := true
for isProposerUnchanged {
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
block = makeBlock(curState, curState.LastBlockHeight+1, new(types.Commit))
@@ -913,11 +884,8 @@ func TestLargeGenesisValidator(t *testing.T) {
proposers := make([]*types.Validator, numVals)
for i := 0; i < 100; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
abciResponses := &abci.ResponseFinalizeBlock{}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.ValidatorUpdates)
require.NoError(t, err)
block := makeBlock(updatedState, updatedState.LastBlockHeight+1, new(types.Commit))
@@ -988,7 +956,7 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) {
// Save state etc.
var validatorUpdates []*types.Validator
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.ValidatorUpdates)
require.NoError(t, err)
state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates)
require.Nil(t, err)
@@ -1068,7 +1036,7 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) {
cp = params[changeIndex]
}
header, blockID, responses := makeHeaderPartsResponsesParams(t, state, cp.ToProto())
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates)
validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.ValidatorUpdates)
require.NoError(t, err)
state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates)
+72 -38
View File
@@ -58,17 +58,17 @@ type Store interface {
Load() (State, error)
// LoadValidators loads the validator set at a given height
LoadValidators(int64) (*types.ValidatorSet, error)
// LoadABCIResponses loads the abciResponse for a given height
LoadABCIResponses(int64) (*tmstate.ABCIResponses, error)
// LoadFinalizeBlockResponse loads the abciResponse for a given height
LoadFinalizeBlockResponse(int64) (*abci.ResponseFinalizeBlock, error)
// LoadLastABCIResponse loads the last abciResponse for a given height
LoadLastABCIResponse(int64) (*tmstate.ABCIResponses, error)
LoadLastFinalizeBlockResponse(int64) (*abci.ResponseFinalizeBlock, error)
// LoadConsensusParams loads the consensus params for a given height
LoadConsensusParams(int64) (types.ConsensusParams, error)
// Save overwrites the previous state with the updated one
Save(State) error
// SaveABCIResponses saves ABCIResponses for a given height
SaveABCIResponses(int64, *tmstate.ABCIResponses) error
// Bootstrap is used for bootstrapping state when not starting from a initial height
// SaveFinalizeBlockResponse saves ABCIResponses for a given height
SaveFinalizeBlockResponse(int64, *abci.ResponseFinalizeBlock) error
// Bootstrap is used for bootstrapping state when not starting from a initial height.
Bootstrap(State) error
// PruneStates takes the height from which to start pruning and which height stop at
PruneStates(int64, int64, int64) error
@@ -85,7 +85,7 @@ type dbStore struct {
type StoreOptions struct {
// DiscardABCIResponses determines whether or not the store
// retains all ABCIResponses. If DiscardABCiResponses is enabled,
// retains all ABCIResponses. If DiscardABCIResponses is enabled,
// the store will maintain only the response object from the latest
// height.
DiscardABCIResponses bool
@@ -367,20 +367,20 @@ func (store dbStore) PruneStates(from int64, to int64, evidenceThresholdHeight i
//------------------------------------------------------------------------
// ABCIResponsesResultsHash returns the root hash of a Merkle tree of
// ResponseDeliverTx responses (see ABCIResults.Hash)
// TxResultsHash returns the root hash of a Merkle tree of
// ExecTxResulst responses (see ABCIResults.Hash)
//
// See merkle.SimpleHashFromByteSlices
func ABCIResponsesResultsHash(ar *tmstate.ABCIResponses) []byte {
return types.NewResults(ar.DeliverTxs).Hash()
func TxResultsHash(txResults []*abci.ExecTxResult) []byte {
return types.NewResults(txResults).Hash()
}
// LoadABCIResponses loads the ABCIResponses for the given height from the
// database. If the node has DiscardABCIResponses set to true, ErrABCIResponsesNotPersisted
// LoadFinalizeBlockResponse loads the DiscardABCIResponses for the given height from the
// database. If the node has D set to true, ErrABCIResponsesNotPersisted
// is persisted. If not found, ErrNoABCIResponsesForHeight is returned.
func (store dbStore) LoadABCIResponses(height int64) (*tmstate.ABCIResponses, error) {
func (store dbStore) LoadFinalizeBlockResponse(height int64) (*abci.ResponseFinalizeBlock, error) {
if store.DiscardABCIResponses {
return nil, ErrABCIResponsesNotPersisted
return nil, ErrFinalizeBlockResponsesNotPersisted
}
buf, err := store.db.Get(calcABCIResponsesKey(height))
@@ -388,29 +388,39 @@ func (store dbStore) LoadABCIResponses(height int64) (*tmstate.ABCIResponses, er
return nil, err
}
if len(buf) == 0 {
return nil, ErrNoABCIResponsesForHeight{height}
}
abciResponses := new(tmstate.ABCIResponses)
err = abciResponses.Unmarshal(buf)
resp := new(abci.ResponseFinalizeBlock)
err = resp.Unmarshal(buf)
if err != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has
changed: %v\n`, err))
// The data might be of the legacy ABCI response type, so
// we try to unmarshal that
legacyResp := new(tmstate.LegacyABCIResponses)
rerr := legacyResp.Unmarshal(buf)
if rerr != nil {
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
tmos.Exit(fmt.Sprintf(`LoadFinalizeBlockResponse: Data has been corrupted or its spec has
changed: %v\n`, err))
}
// The state store contains the old format. Migrate to
// the new ResponseFinalizeBlock format. Note that the
// new struct expects the AgreedAppData which we don't have.
return responseFinalizeBlockFromLegacy(legacyResp), nil
}
// TODO: ensure that buf is completely read.
return abciResponses, nil
return resp, nil
}
// LoadLastABCIResponses loads the ABCIResponses from the most recent height.
// LoadLastFinalizeBlockResponses loads the FinalizeBlockResponses from the most recent height.
// The height parameter is used to ensure that the response corresponds to the latest height.
// If not, an error is returned.
//
// This method is used for recovering in the case that we called the Commit ABCI
// method on the application but crashed before persisting the results.
func (store dbStore) LoadLastABCIResponse(height int64) (*tmstate.ABCIResponses, error) {
func (store dbStore) LoadLastFinalizeBlockResponse(height int64) (*abci.ResponseFinalizeBlock, error) {
bz, err := store.db.Get(lastABCIResponseKey)
if err != nil {
return nil, err
@@ -420,41 +430,52 @@ func (store dbStore) LoadLastABCIResponse(height int64) (*tmstate.ABCIResponses,
return nil, errors.New("no last ABCI response has been persisted")
}
abciResponse := new(tmstate.ABCIResponsesInfo)
err = abciResponse.Unmarshal(bz)
info := new(tmstate.ABCIResponsesInfo)
err = info.Unmarshal(bz)
if err != nil {
tmos.Exit(fmt.Sprintf(`LoadLastABCIResponses: Data has been corrupted or its spec has
tmos.Exit(fmt.Sprintf(`LoadLastFinalizeBlockResponse: Data has been corrupted or its spec has
changed: %v\n`, err))
}
// Here we validate the result by comparing its height to the expected height.
if height != abciResponse.GetHeight() {
return nil, errors.New("expected height %d but last stored abci responses was at height %d")
if height != info.GetHeight() {
return nil, fmt.Errorf("expected height %d but last stored abci responses was at height %d", height, info.GetHeight())
}
return abciResponse.AbciResponses, nil
// It is possible if this is called directly after an upgrade that
// ResponseFinalizeBlock is nil. In which case we use the legacy
// ABCI responses
if info.ResponseFinalizeBlock == nil {
// sanity check
if info.LegacyAbciResponses == nil {
panic("state store contains last abci response but it is empty")
}
return responseFinalizeBlockFromLegacy(info.LegacyAbciResponses), nil
}
return info.ResponseFinalizeBlock, nil
}
// SaveABCIResponses persists the ABCIResponses to the database.
// SaveFinalizeBlockResponse persists the ResponseFinalizeBlock 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.
//
// CONTRACT: height must be monotonically increasing every time this is called.
func (store dbStore) SaveABCIResponses(height int64, abciResponses *tmstate.ABCIResponses) error {
var dtxs []*abci.ResponseDeliverTx
func (store dbStore) SaveFinalizeBlockResponse(height int64, resp *abci.ResponseFinalizeBlock) error {
var dtxs []*abci.ExecTxResult
// strip nil values,
for _, tx := range abciResponses.DeliverTxs {
for _, tx := range resp.TxResults {
if tx != nil {
dtxs = append(dtxs, tx)
}
}
abciResponses.DeliverTxs = dtxs
resp.TxResults = dtxs
// If the flag is false then we save the ABCIResponse. This can be used for the /BlockResults
// query or to reindex an event using the command line.
if !store.DiscardABCIResponses {
bz, err := abciResponses.Marshal()
bz, err := resp.Marshal()
if err != nil {
return err
}
@@ -466,8 +487,8 @@ func (store dbStore) SaveABCIResponses(height int64, abciResponses *tmstate.ABCI
// We always save the last ABCI response for crash recovery.
// This overwrites the previous saved ABCI Response.
response := &tmstate.ABCIResponsesInfo{
AbciResponses: abciResponses,
Height: height,
ResponseFinalizeBlock: resp,
Height: height,
}
bz, err := response.Marshal()
if err != nil {
@@ -671,3 +692,16 @@ func min(a int64, b int64) int64 {
}
return b
}
// responseFinalizeBlockFromLegacy is a convenience function that takes the old abci responses and morphs
// it to the finalize block response. Note that the agreed app data is missing
func responseFinalizeBlockFromLegacy(legacyResp *tmstate.LegacyABCIResponses) *abci.ResponseFinalizeBlock {
return &abci.ResponseFinalizeBlock{
TxResults: legacyResp.DeliverTxs,
ValidatorUpdates: legacyResp.EndBlock.ValidatorUpdates,
ConsensusParamUpdates: legacyResp.EndBlock.ConsensusParamUpdates,
Events: append(legacyResp.BeginBlock.Events, legacyResp.EndBlock.Events...),
// NOTE: AgreedAppData is missing in the response but will
// be caught and filled in consensus/replay.go
}
}
+70 -33
View File
@@ -154,8 +154,8 @@ func TestPruneStates(t *testing.T) {
err := stateStore.Save(state)
require.NoError(t, err)
err = stateStore.SaveABCIResponses(h, &tmstate.ABCIResponses{
DeliverTxs: []*abci.ResponseDeliverTx{
err = stateStore.SaveFinalizeBlockResponse(h, &abci.ResponseFinalizeBlock{
TxResults: []*abci.ExecTxResult{
{Data: []byte{1}},
{Data: []byte{2}},
{Data: []byte{3}},
@@ -195,7 +195,7 @@ func TestPruneStates(t *testing.T) {
require.Empty(t, params)
}
abci, err := stateStore.LoadABCIResponses(h)
abci, err := stateStore.LoadFinalizeBlockResponse(h)
if expectABCI[h] {
require.NoError(t, err, "abci height %v", h)
require.NotNil(t, abci)
@@ -208,22 +208,18 @@ func TestPruneStates(t *testing.T) {
}
}
func TestABCIResponsesResultsHash(t *testing.T) {
responses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: []*abci.ResponseDeliverTx{
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
},
EndBlock: &abci.ResponseEndBlock{},
func TestTxResultsHash(t *testing.T) {
txResults := []*abci.ExecTxResult{
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
}
root := sm.ABCIResponsesResultsHash(responses)
root := sm.TxResultsHash(txResults)
// root should be Merkle tree root of DeliverTxs responses
results := types.NewResults(responses.DeliverTxs)
// root should be Merkle tree root of ExecTxResult responses
results := types.NewResults(txResults)
assert.Equal(t, root, results.Hash())
// test we can prove first DeliverTx
// test we can prove first ExecTxResult
proof := results.ProveResult(0)
bz, err := results[0].Marshal()
require.NoError(t, err)
@@ -238,41 +234,39 @@ func sliceToMap(s []int64) map[int64]bool {
return m
}
func TestLastABCIResponses(t *testing.T) {
func TestLastFinalizeBlockResponses(t *testing.T) {
// create an empty state store.
t.Run("Not persisting responses", func(t *testing.T) {
stateDB := dbm.NewMemDB()
stateStore := sm.NewStore(stateDB, sm.StoreOptions{
DiscardABCIResponses: false,
})
responses, err := stateStore.LoadABCIResponses(1)
responses, err := stateStore.LoadFinalizeBlockResponse(1)
require.Error(t, err)
require.Nil(t, responses)
// stub the abciresponses.
response1 := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: []*abci.ResponseDeliverTx{
response1 := &abci.ResponseFinalizeBlock{
TxResults: []*abci.ExecTxResult{
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
},
EndBlock: &abci.ResponseEndBlock{},
}
// create new db and state store and set discard abciresponses to false.
stateDB = dbm.NewMemDB()
stateStore = sm.NewStore(stateDB, sm.StoreOptions{DiscardABCIResponses: false})
height := int64(10)
// save the last abci response.
err = stateStore.SaveABCIResponses(height, response1)
err = stateStore.SaveFinalizeBlockResponse(height, response1)
require.NoError(t, err)
// search for the last abciresponse and check if it has saved.
lastResponse, err := stateStore.LoadLastABCIResponse(height)
// search for the last finalize block response and check if it has saved.
lastResponse, err := stateStore.LoadLastFinalizeBlockResponse(height)
require.NoError(t, err)
// check to see if the saved response height is the same as the loaded height.
assert.Equal(t, lastResponse, response1)
// use an incorret height to make sure the state store errors.
_, err = stateStore.LoadLastABCIResponse(height + 1)
_, err = stateStore.LoadLastFinalizeBlockResponse(height + 1)
assert.Error(t, err)
// check if the abci response didnt save in the abciresponses.
responses, err = stateStore.LoadABCIResponses(height)
responses, err = stateStore.LoadFinalizeBlockResponse(height)
require.NoError(t, err, responses)
require.Equal(t, response1, responses)
})
@@ -281,28 +275,71 @@ func TestLastABCIResponses(t *testing.T) {
stateDB := dbm.NewMemDB()
height := int64(10)
// stub the second abciresponse.
response2 := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: []*abci.ResponseDeliverTx{
response2 := &abci.ResponseFinalizeBlock{
TxResults: []*abci.ExecTxResult{
{Code: 44, Data: []byte("Hello again"), Log: "????"},
},
EndBlock: &abci.ResponseEndBlock{},
}
// create a new statestore with the responses on.
stateStore := sm.NewStore(stateDB, sm.StoreOptions{
DiscardABCIResponses: true,
})
// save an additional response.
err := stateStore.SaveABCIResponses(height+1, response2)
err := stateStore.SaveFinalizeBlockResponse(height+1, response2)
require.NoError(t, err)
// check to see if the response saved by calling the last response.
lastResponse2, err := stateStore.LoadLastABCIResponse(height + 1)
lastResponse2, err := stateStore.LoadLastFinalizeBlockResponse(height + 1)
require.NoError(t, err)
// check to see if the saved response height is the same as the loaded height.
assert.Equal(t, response2, lastResponse2)
// should error as we are no longer saving the response.
_, err = stateStore.LoadABCIResponses(height + 1)
assert.Equal(t, sm.ErrABCIResponsesNotPersisted, err)
_, err = stateStore.LoadFinalizeBlockResponse(height + 1)
assert.Equal(t, sm.ErrFinalizeBlockResponsesNotPersisted, err)
})
}
func TestFinalizeBlockRecoveryUsingLegacyABCIResponses(t *testing.T) {
var (
height int64 = 10
lastABCIResponseKey = []byte("lastABCIResponseKey")
memDB = dbm.NewMemDB()
cp = types.DefaultConsensusParams().ToProto()
legacyResp = tmstate.ABCIResponsesInfo{
LegacyAbciResponses: &tmstate.LegacyABCIResponses{
BeginBlock: &tmstate.ResponseBeginBlock{
Events: []abci.Event{{
Type: "begin_block",
Attributes: []abci.EventAttribute{{
Key: "key",
Value: "value",
}},
}},
},
DeliverTxs: []*abci.ExecTxResult{{
Events: []abci.Event{{
Type: "tx",
Attributes: []abci.EventAttribute{{
Key: "key",
Value: "value",
}},
}},
}},
EndBlock: &tmstate.ResponseEndBlock{
ConsensusParamUpdates: &cp,
},
},
Height: height,
}
)
bz, err := legacyResp.Marshal()
require.NoError(t, err)
// should keep this in parity with state/store.go
require.NoError(t, memDB.Set(lastABCIResponseKey, bz))
stateStore := sm.NewStore(memDB, sm.StoreOptions{DiscardABCIResponses: false})
resp, err := stateStore.LoadLastFinalizeBlockResponse(height)
require.NoError(t, err)
require.Equal(t, resp.ConsensusParamUpdates, &cp)
require.Equal(t, resp.Events, legacyResp.LegacyAbciResponses.BeginBlock.Events)
require.Equal(t, resp.TxResults[0], legacyResp.LegacyAbciResponses.DeliverTxs[0])
}
+11 -9
View File
@@ -44,10 +44,10 @@ func (is *IndexerService) OnStart() error {
// Use SubscribeUnbuffered here to ensure both subscriptions does not get
// canceled due to not pulling messages fast enough. Cause this might
// sometimes happen when there are no other subscribers.
blockHeadersSub, err := is.eventBus.SubscribeUnbuffered(
blockSub, err := is.eventBus.SubscribeUnbuffered(
context.Background(),
subscriber,
types.EventQueryNewBlockHeader)
types.EventQueryNewBlockEvents)
if err != nil {
return err
}
@@ -59,12 +59,14 @@ func (is *IndexerService) OnStart() error {
go func() {
for {
msg := <-blockHeadersSub.Out()
eventDataHeader := msg.Data().(types.EventDataNewBlockHeader)
height := eventDataHeader.Header.Height
batch := NewBatch(eventDataHeader.NumTxs)
msg := <-blockSub.Out()
eventNewBlockEvents := msg.Data().(types.EventDataNewBlockEvents)
height := eventNewBlockEvents.Height
numTxs := eventNewBlockEvents.NumTxs
for i := int64(0); i < eventDataHeader.NumTxs; i++ {
batch := NewBatch(numTxs)
for i := int64(0); i < numTxs; i++ {
msg2 := <-txsSub.Out()
txResult := msg2.Data().(types.EventDataTx).TxResult
@@ -85,7 +87,7 @@ func (is *IndexerService) OnStart() error {
}
}
if err := is.blockIdxr.Index(eventDataHeader); err != nil {
if err := is.blockIdxr.Index(eventNewBlockEvents); err != nil {
is.Logger.Error("failed to index block", "height", height, "err", err)
if is.terminateOnError {
if err := is.Stop(); err != nil {
@@ -106,7 +108,7 @@ func (is *IndexerService) OnStart() error {
return
}
} else {
is.Logger.Debug("indexed transactions", "height", height, "num_txs", eventDataHeader.NumTxs)
is.Logger.Debug("indexed block txs", "height", height, "num_txs", numTxs)
}
}
}()
+17 -5
View File
@@ -42,9 +42,21 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
}
})
// publish block with txs
err = eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
Header: types.Header{Height: 1},
// publish block with events
err = eventBus.PublishEventNewBlockEvents(types.EventDataNewBlockEvents{
Height: 1,
Events: []abci.Event{
{
Type: "begin_event",
Attributes: []abci.EventAttribute{
{
Key: "proposer",
Value: "FCAA001",
Index: true,
},
},
},
},
NumTxs: int64(2),
})
require.NoError(t, err)
@@ -52,7 +64,7 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
Height: 1,
Index: uint32(0),
Tx: types.Tx("foo"),
Result: abci.ResponseDeliverTx{Code: 0},
Result: abci.ExecTxResult{Code: 0},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1})
require.NoError(t, err)
@@ -60,7 +72,7 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) {
Height: 1,
Index: uint32(1),
Tx: types.Tx("bar"),
Result: abci.ResponseDeliverTx{Code: 0},
Result: abci.ExecTxResult{Code: 0},
}
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2})
require.NoError(t, err)
+1 -1
View File
@@ -47,7 +47,7 @@ func BenchmarkTxSearch(b *testing.B) {
Height: int64(i),
Index: 0,
Tx: types.Tx(string(txBz)),
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK,
Log: "",
+10 -10
View File
@@ -27,7 +27,7 @@ func TestTxIndex(t *testing.T) {
Height: 1,
Index: 0,
Tx: tx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK, Log: "", Events: nil,
},
@@ -50,7 +50,7 @@ func TestTxIndex(t *testing.T) {
Height: 1,
Index: 0,
Tx: tx2,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK, Log: "", Events: nil,
},
@@ -273,7 +273,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 1,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK,
},
},
@@ -281,7 +281,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 2,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK + 1,
},
},
@@ -293,7 +293,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 1,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK + 1,
},
},
@@ -301,7 +301,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 2,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK + 1,
},
},
@@ -313,7 +313,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 1,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK,
},
},
@@ -321,7 +321,7 @@ func TestTxIndexDuplicatePreviouslySuccessful(t *testing.T) {
Height: 2,
Index: 0,
Tx: mockTx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Code: abci.CodeTypeOK,
},
},
@@ -415,7 +415,7 @@ func txResultWithEvents(events []abci.Event) *abci.TxResult {
Height: 1,
Index: 0,
Tx: tx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK,
Log: "",
@@ -441,7 +441,7 @@ func benchmarkTxIndex(txsCount int64, b *testing.B) {
Height: 1,
Index: txIndex,
Tx: tx,
Result: abci.ResponseDeliverTx{
Result: abci.ExecTxResult{
Data: []byte{0},
Code: abci.CodeTypeOK,
Log: "",