diff --git a/state/errors.go b/state/errors.go index 98f44281..1c0b76ab 100644 --- a/state/errors.go +++ b/state/errors.go @@ -41,6 +41,10 @@ type ( ErrNoConsensusParamsForHeight struct { Height int64 } + + ErrNoResultsForHeight struct { + Height int64 + } ) func (e ErrUnknownBlock) Error() string { @@ -69,3 +73,7 @@ func (e ErrNoValSetForHeight) Error() string { func (e ErrNoConsensusParamsForHeight) Error() string { return cmn.Fmt("Could not find consensus params for height #%d", e.Height) } + +func (e ErrNoResultsForHeight) Error() string { + return cmn.Fmt("Could not find results for height #%d", e.Height) +} diff --git a/state/state.go b/state/state.go index 99a022d7..316a3161 100644 --- a/state/state.go +++ b/state/state.go @@ -35,6 +35,10 @@ func calcConsensusParamsKey(height int64) []byte { return []byte(cmn.Fmt("consensusParamsKey:%v", height)) } +func calcResultsKey(height int64) []byte { + return []byte(cmn.Fmt("resultsKey:%v", height)) +} + //----------------------------------------------------------------------------- // State is a short description of the latest committed block of the Tendermint consensus. @@ -75,7 +79,7 @@ type State struct { LastHeightConsensusParamsChanged int64 // Store LastABCIResults along with hash - LastResults ABCIResults + LastResults ABCIResults // TODO: remove?? LastResultHash []byte // The latest AppHash we've received from calling abci.Commit() @@ -163,6 +167,7 @@ func (s *State) Save() { s.saveValidatorsInfo() s.saveConsensusParamsInfo() + s.saveResults() s.db.SetSync(stateKey, s.Bytes()) } @@ -302,6 +307,39 @@ func (s *State) saveConsensusParamsInfo() { s.db.SetSync(calcConsensusParamsKey(nextHeight), paramsInfo.Bytes()) } +// LoadResults loads the ABCIResults for a given height. +func (s *State) LoadResults(height int64) (ABCIResults, error) { + resInfo := s.loadResults(height) + if resInfo == nil { + return nil, ErrNoResultsForHeight{height} + } + return resInfo, nil +} + +func (s *State) loadResults(height int64) ABCIResults { + buf := s.db.Get(calcResultsKey(height)) + if len(buf) == 0 { + return nil + } + + v := new(ABCIResults) + err := wire.ReadBinaryBytes(buf, v) + if err != nil { + // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED + cmn.Exit(cmn.Fmt(`LoadResults: Data has been corrupted or its spec has changed: + %v\n`, err)) + } + return *v +} + +// saveResults persists the results for the last block to disk. +// It should be called from s.Save(), right before the state itself is persisted. +func (s *State) saveResults() { + nextHeight := s.LastBlockHeight + 1 + results := s.LastResults + s.db.SetSync(calcResultsKey(nextHeight), results.Bytes()) +} + // Equals returns true if the States are identical. func (s *State) Equals(s2 *State) bool { return bytes.Equal(s.Bytes(), s2.Bytes()) @@ -444,6 +482,11 @@ func NewResults(del []*abci.ResponseDeliverTx) ABCIResults { return res } +// Bytes serializes the ABCIResponse using go-wire +func (a ABCIResults) Bytes() []byte { + return wire.BinaryBytes(a) +} + // Hash returns a merkle hash of all results func (a ABCIResults) Hash() []byte { return merkle.SimpleHashFromHashables(a.toHashables()) diff --git a/state/state_test.go b/state/state_test.go index d15753fe..84b8fb9d 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -313,6 +313,73 @@ func TestABCIResults(t *testing.T) { } } +// TestResultsSaveLoad tests saving and loading abci results. +func TestResultsSaveLoad(t *testing.T) { + tearDown, _, state := 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 ABCIResults + }{ + 0: { + []*abci.ResponseDeliverTx{}, + ABCIResults{}, + }, + 1: { + []*abci.ResponseDeliverTx{ + {Code: 32, Data: []byte("Hello"), Log: "Huh?"}, + }, + ABCIResults{ + {32, []byte("Hello")}, + }}, + 2: { + []*abci.ResponseDeliverTx{ + {Code: 383}, + {Data: []byte("Gotcha!"), + Tags: []*abci.KVPair{ + abci.KVPairInt("a", 1), + abci.KVPairString("build", "stuff"), + }}, + }, + ABCIResults{ + {383, []byte{}}, + {0, []byte("Gotcha!")}, + }}, + 3: { + nil, + ABCIResults{}, + }, + } + + // query all before, should return error + for i := range cases { + h := int64(i + 2) + res, err := state.LoadResults(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 + header, parts, responses := makeHeaderPartsResults(state, h, tc.added) + state.SetBlockAndValidators(header, parts, responses) + state.Save() + } + + // query all before, should return expected value + for i, tc := range cases { + h := int64(i + 2) + res, err := state.LoadResults(h) + assert.NoError(err, "%d", i) + assert.Equal(tc.expected, res, "%d", i) + } +} + func makeParams(blockBytes, blockTx, blockGas, txBytes, txGas, partSize int) types.ConsensusParams { @@ -510,3 +577,15 @@ type paramsChangeTestCase struct { height int64 params types.ConsensusParams } + +func makeHeaderPartsResults(state *State, height int64, + results []*abci.ResponseDeliverTx) (*types.Header, types.PartSetHeader, *ABCIResponses) { + + block := makeBlock(state, height) + abciResponses := &ABCIResponses{ + Height: height, + DeliverTx: results, + EndBlock: &abci.ResponseEndBlock{}, + } + return block.Header, types.PartSetHeader{}, abciResponses +}