diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 2ecfa795..838fa524 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -152,6 +152,15 @@ func (c *HTTP) Block(height *int64) (*ctypes.ResultBlock, error) { return result, nil } +func (c *HTTP) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { + result := new(ctypes.ResultBlockResults) + _, err := c.rpc.Call("block_results", map[string]interface{}{"height": height}, result) + if err != nil { + return nil, errors.Wrap(err, "Block Result") + } + return result, nil +} + func (c *HTTP) Commit(height *int64) (*ctypes.ResultCommit, error) { result := new(ctypes.ResultCommit) _, err := c.rpc.Call("commit", map[string]interface{}{"height": height}, result) diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 063d50e1..70cb4d95 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -45,6 +45,7 @@ type ABCIClient interface { // signatures and prove anything about the chain type SignClient interface { Block(height *int64) (*ctypes.ResultBlock, error) + BlockResults(height *int64) (*ctypes.ResultBlockResults, error) Commit(height *int64) (*ctypes.ResultCommit, error) Validators(height *int64) (*ctypes.ResultValidators, error) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 18c6759d..71f25ef2 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -100,6 +100,10 @@ func (Local) Block(height *int64) (*ctypes.ResultBlock, error) { return core.Block(height) } +func (Local) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { + return core.BlockResults(height) +} + func (Local) Commit(height *int64) (*ctypes.ResultCommit, error) { return core.Commit(height) } diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index c32d08bd..7d6bc8c3 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -155,7 +155,6 @@ func TestAppCalls(t *testing.T) { } // make sure we can lookup the tx with proof - // ptx, err := c.Tx(bres.Hash, true) ptx, err := c.Tx(bres.Hash, true) require.Nil(err, "%d: %+v", i, err) assert.EqualValues(txh, ptx.Height) @@ -168,6 +167,15 @@ func TestAppCalls(t *testing.T) { assert.True(len(appHash) > 0) assert.EqualValues(apph, block.BlockMeta.Header.Height) + // now check the results + blockResults, err := c.BlockResults(&apph) + require.Nil(err, "%d: %+v", i, err) + assert.Equal(apph, blockResults.Height) + if assert.Equal(1, len(blockResults.Results)) { + // check success code + assert.EqualValues(0, blockResults.Results[0].Code) + } + // check blockchain info, now that we know there is info // TODO: is this commented somewhere that they are returned // in order of descending height??? diff --git a/rpc/core/blocks.go b/rpc/core/blocks.go index 9d409845..14d9e9fc 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -312,3 +312,66 @@ func Commit(heightPtr *int64) (*ctypes.ResultCommit, error) { commit := blockStore.LoadBlockCommit(height) return ctypes.NewResultCommit(header, commit, true), nil } + +// BlockResults gets ABCIResults at a given height. +// If no height is provided, it will fetch the latest block. +// +// Results are for the tx of the last block with the same index. +// Thus response.results[5] is the results of executing +// getBlock(h-1).Txs[5] +// +// ```shell +// curl 'localhost:46657/block_results?height=10' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") +// info, err := client.BlockResults(10) +// ``` +// +// +// > The above command returns JSON structured like this: +// +// ```json +// { +// "height": 88, +// "results": [ +// { +// "code": 0, +// "data": "CAFE00F00D" +// }, +// { +// "code": 102, +// "data": "" +// } +// ] +// } +// ``` +func BlockResults(heightPtr *int64) (*ctypes.ResultBlockResults, error) { + var height int64 + if heightPtr != nil { + height = *heightPtr + if height <= 0 { + return nil, fmt.Errorf("Height must be greater than 0") + } + storeHeight := blockStore.Height() + if height > storeHeight { + return nil, fmt.Errorf("Height must be less than or equal to the current blockchain height") + } + } else { + height = blockStore.Height() + } + + // load the results + state := consensusState.GetState() + results, err := state.LoadResults(height) + if err != nil { + return nil, err + } + + res := &ctypes.ResultBlockResults{ + Height: height, + Results: results, + } + return res, nil +} diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 111c010a..fb5a1fd3 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -17,6 +17,7 @@ var Routes = map[string]*rpc.RPCFunc{ "blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"), "genesis": rpc.NewRPCFunc(Genesis, ""), "block": rpc.NewRPCFunc(Block, "height"), + "block_results": rpc.NewRPCFunc(BlockResults, "height"), "commit": rpc.NewRPCFunc(Commit, "height"), "tx": rpc.NewRPCFunc(Tx, "hash,prove"), "tx_search": rpc.NewRPCFunc(TxSearch, "query,prove"), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 3d1e7a21..31d86f94 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -33,6 +33,11 @@ type ResultCommit struct { CanonicalCommit bool `json:"canonical"` } +type ResultBlockResults struct { + Height int64 `json:"height"` + Results types.ABCIResults `json:"results"` +} + // NewResultCommit is a helper to initialize the ResultCommit with // the embedded struct func NewResultCommit(header *types.Header, commit *types.Commit, diff --git a/state/state.go b/state/state.go index 9ef734fc..be9c641c 100644 --- a/state/state.go +++ b/state/state.go @@ -8,13 +8,10 @@ import ( "time" abci "github.com/tendermint/abci/types" - "github.com/tendermint/go-wire/data" - "golang.org/x/crypto/ripemd160" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" "github.com/tendermint/tmlibs/log" - "github.com/tendermint/tmlibs/merkle" wire "github.com/tendermint/go-wire" @@ -79,9 +76,9 @@ type State struct { LastHeightConsensusParamsChanged int64 // Store LastABCIResults along with hash - LastResults ABCIResults // TODO: remove?? - LastResultHash []byte // this is the one for the next block to propose - LastLastResultHash []byte // this verifies the last block? + LastResults types.ABCIResults // TODO: remove?? + LastResultHash []byte // this is the one for the next block to propose + LastLastResultHash []byte // this verifies the last block? // The latest AppHash we've received from calling abci.Commit() AppHash []byte @@ -311,8 +308,8 @@ 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) { +// LoadResults loads the types.ABCIResults for a given height. +func (s *State) LoadResults(height int64) (types.ABCIResults, error) { resInfo := s.loadResults(height) if resInfo == nil { return nil, ErrNoResultsForHeight{height} @@ -320,13 +317,13 @@ func (s *State) LoadResults(height int64) (ABCIResults, error) { return resInfo, nil } -func (s *State) loadResults(height int64) ABCIResults { +func (s *State) loadResults(height int64) types.ABCIResults { buf := s.db.Get(calcResultsKey(height)) if len(buf) == 0 { return nil } - v := new(ABCIResults) + v := new(types.ABCIResults) err := wire.ReadBinaryBytes(buf, v) if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED @@ -396,7 +393,7 @@ func (s *State) SetBlockAndValidators(header *types.Header, blockPartsHeader typ header.Time, nextValSet, nextParams, - NewResults(abciResponses.DeliverTx)) + types.NewResults(abciResponses.DeliverTx)) return nil } @@ -404,7 +401,7 @@ func (s *State) setBlockAndValidators(height int64, newTxs int64, blockID types.BlockID, blockTime time.Time, valSet *types.ValidatorSet, params types.ConsensusParams, - results ABCIResults) { + results types.ABCIResults) { s.LastBlockHeight = height s.LastBlockTotalTx += newTxs @@ -455,64 +452,6 @@ func (a *ABCIResponses) Bytes() []byte { //----------------------------------------------------------------------------- -// ABCIResult is just the essential info to prove -// success/failure of a DeliverTx -type ABCIResult struct { - Code uint32 `json:"code"` - Data data.Bytes `json:"data"` -} - -// Hash creates a canonical json hash of the ABCIResult -func (a ABCIResult) Hash() []byte { - // stupid canonical json output, easy to check in any language - bs := fmt.Sprintf(`{"code":%d,"data":"%s"}`, a.Code, a.Data) - var hasher = ripemd160.New() - hasher.Write([]byte(bs)) - return hasher.Sum(nil) -} - -// ABCIResults wraps the deliver tx results to return a proof -type ABCIResults []ABCIResult - -// NewResults creates ABCIResults from ResponseDeliverTx -func NewResults(del []*abci.ResponseDeliverTx) ABCIResults { - res := make(ABCIResults, len(del)) - for i, d := range del { - res[i] = ABCIResult{ - Code: d.Code, - Data: d.Data, - } - } - 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()) -} - -// ProveResult returns a merkle proof of one result from the set -func (a ABCIResults) ProveResult(i int) merkle.SimpleProof { - _, proofs := merkle.SimpleProofsFromHashables(a.toHashables()) - return *proofs[i] -} - -func (a ABCIResults) toHashables() []merkle.Hashable { - l := len(a) - hashables := make([]merkle.Hashable, l) - for i := 0; i < l; i++ { - hashables[i] = a[i] - } - return hashables -} - -//----------------------------------------------------------------------------- - // ValidatorsInfo represents the latest validator set, or the last height it changed type ValidatorsInfo struct { ValidatorSet *types.ValidatorSet diff --git a/state/state_test.go b/state/state_test.go index 84b8fb9d..df4a2e1c 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -279,40 +279,6 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { } } -func TestABCIResults(t *testing.T) { - a := ABCIResult{Code: 0, Data: nil} - b := ABCIResult{Code: 0, Data: []byte{}} - c := ABCIResult{Code: 0, Data: []byte("one")} - d := ABCIResult{Code: 14, Data: nil} - e := ABCIResult{Code: 14, Data: []byte("foo")} - f := ABCIResult{Code: 14, Data: []byte("bar")} - - // nil and []byte{} should produce same hash - assert.Equal(t, a.Hash(), b.Hash()) - - // a and b should be the same, don't go in results - results := ABCIResults{a, c, d, e, f} - - // make sure each result hashes properly - var last []byte - for i, res := range results { - h := res.Hash() - assert.NotEqual(t, last, h, "%d", i) - last = h - } - - // make sure that we can get a root hash from results - // and verify proofs - root := results.Hash() - assert.NotEmpty(t, root) - - for i, res := range results { - proof := results.ProveResult(i) - valid := proof.Verify(i, len(results), res.Hash(), root) - assert.True(t, valid, "%d", i) - } -} - // TestResultsSaveLoad tests saving and loading abci results. func TestResultsSaveLoad(t *testing.T) { tearDown, _, state := setupTestCase(t) @@ -324,17 +290,17 @@ func TestResultsSaveLoad(t *testing.T) { // height is implied index+2 // as block 1 is created from genesis added []*abci.ResponseDeliverTx - expected ABCIResults + expected types.ABCIResults }{ 0: { []*abci.ResponseDeliverTx{}, - ABCIResults{}, + types.ABCIResults{}, }, 1: { []*abci.ResponseDeliverTx{ {Code: 32, Data: []byte("Hello"), Log: "Huh?"}, }, - ABCIResults{ + types.ABCIResults{ {32, []byte("Hello")}, }}, 2: { @@ -346,13 +312,13 @@ func TestResultsSaveLoad(t *testing.T) { abci.KVPairString("build", "stuff"), }}, }, - ABCIResults{ + types.ABCIResults{ {383, []byte{}}, {0, []byte("Gotcha!")}, }}, 3: { nil, - ABCIResults{}, + types.ABCIResults{}, }, } diff --git a/types/results.go b/types/results.go new file mode 100644 index 00000000..208e26b3 --- /dev/null +++ b/types/results.go @@ -0,0 +1,70 @@ +package types + +import ( + "fmt" + + "golang.org/x/crypto/ripemd160" + + abci "github.com/tendermint/abci/types" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/go-wire/data" + "github.com/tendermint/tmlibs/merkle" +) + +//----------------------------------------------------------------------------- + +// ABCIResult is just the essential info to prove +// success/failure of a DeliverTx +type ABCIResult struct { + Code uint32 `json:"code"` + Data data.Bytes `json:"data"` +} + +// Hash creates a canonical json hash of the ABCIResult +func (a ABCIResult) Hash() []byte { + // stupid canonical json output, easy to check in any language + bs := fmt.Sprintf(`{"code":%d,"data":"%s"}`, a.Code, a.Data) + var hasher = ripemd160.New() + hasher.Write([]byte(bs)) + return hasher.Sum(nil) +} + +// ABCIResults wraps the deliver tx results to return a proof +type ABCIResults []ABCIResult + +// NewResults creates ABCIResults from ResponseDeliverTx +func NewResults(del []*abci.ResponseDeliverTx) ABCIResults { + res := make(ABCIResults, len(del)) + for i, d := range del { + res[i] = ABCIResult{ + Code: d.Code, + Data: d.Data, + } + } + 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()) +} + +// ProveResult returns a merkle proof of one result from the set +func (a ABCIResults) ProveResult(i int) merkle.SimpleProof { + _, proofs := merkle.SimpleProofsFromHashables(a.toHashables()) + return *proofs[i] +} + +func (a ABCIResults) toHashables() []merkle.Hashable { + l := len(a) + hashables := make([]merkle.Hashable, l) + for i := 0; i < l; i++ { + hashables[i] = a[i] + } + return hashables +} diff --git a/types/results_test.go b/types/results_test.go new file mode 100644 index 00000000..88624e8c --- /dev/null +++ b/types/results_test.go @@ -0,0 +1,41 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestABCIResults(t *testing.T) { + a := ABCIResult{Code: 0, Data: nil} + b := ABCIResult{Code: 0, Data: []byte{}} + c := ABCIResult{Code: 0, Data: []byte("one")} + d := ABCIResult{Code: 14, Data: nil} + e := ABCIResult{Code: 14, Data: []byte("foo")} + f := ABCIResult{Code: 14, Data: []byte("bar")} + + // nil and []byte{} should produce same hash + assert.Equal(t, a.Hash(), b.Hash()) + + // a and b should be the same, don't go in results + results := ABCIResults{a, c, d, e, f} + + // make sure each result hashes properly + var last []byte + for i, res := range results { + h := res.Hash() + assert.NotEqual(t, last, h, "%d", i) + last = h + } + + // make sure that we can get a root hash from results + // and verify proofs + root := results.Hash() + assert.NotEmpty(t, root) + + for i, res := range results { + proof := results.ProveResult(i) + valid := proof.Verify(i, len(results), res.Hash(), root) + assert.True(t, valid, "%d", i) + } +}