Browse Source

[block#LastResultsHash] add Events + GasWanted/Used (#4845)

Closes #1007
pull/5000/head
Anton Kaliaev 5 years ago
committed by GitHub
parent
commit
a8d8600308
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 99 deletions
  1. +5
    -2
      CHANGELOG_PENDING.md
  2. +1
    -3
      proto/types/validator.pb.go
  3. +0
    -0
      scripts/protocgen.sh
  4. +1
    -1
      state/execution.go
  5. +6
    -3
      state/helpers_test.go
  6. +39
    -17
      state/state_test.go
  7. +36
    -8
      state/store.go
  8. +23
    -0
      state/store_test.go
  9. +23
    -41
      types/results.go
  10. +21
    -24
      types/results_test.go

+ 5
- 2
CHANGELOG_PENDING.md View File

@ -65,6 +65,10 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi
- [crypto] [\#4721](https://github.com/tendermint/tendermint/pull/4721) Remove `SimpleHashFromMap()` and `SimpleProofsFromMap()` (@erikgrinaker)
- [types] \#4798 Simplify `VerifyCommitTrusting` func + remove extra validation (@melekes)
- [libs] \#4831 Remove `Bech32` pkg from Tendermint. This pkg now lives in the [cosmos-sdk](https://github.com/cosmos/cosmos-sdk/tree/4173ea5ebad906dd9b45325bed69b9c655504867/types/bech32)
- [rpc/client] \#4947 `Validators`, `TxSearch` `page`/`per_page` params become pointers (@melekes)
`UnconfirmedTxs` `limit` param is a pointer
- [types] \#4845 Remove `ABCIResult`
- Blockchain Protocol
- [types] [\#4792](https://github.com/tendermint/tendermint/pull/4792) Sort validators by voting power to enable faster commit verification (@melekes)
@ -72,8 +76,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi
Add `max_num` to consensus evidence parameters (default: 50 items).
- [mempool] \#4940 Migrate mempool from amino binary encoding to Protobuf
- [statesync] \#4943 Migrate statesync reactor from amino binary encoding to Protobuf
- [rpc/client] \#4947 `Validators`, `TxSearch` `page`/`per_page` params become pointers (@melekes)
`UnconfirmedTxs` `limit` param is a pointer
- [state] \#4845 Include BeginBlock#Events, EndBlock#Events, DeliverTx#Events, GasWanted and GasUsed into `LastResultsHash` (@melekes)
### FEATURES:


+ 1
- 3
proto/types/validator.pb.go View File

@ -162,9 +162,7 @@ func init() {
}
func init() { proto.RegisterFile("proto/types/validator.proto", fileDescriptor_2e7c6b38c20e5406) }
func init() {
golang_proto.RegisterFile("proto/types/validator.proto", fileDescriptor_2e7c6b38c20e5406)
}
func init() { golang_proto.RegisterFile("proto/types/validator.proto", fileDescriptor_2e7c6b38c20e5406) }
var fileDescriptor_2e7c6b38c20e5406 = []byte{
// 358 bytes of a gzipped FileDescriptorProto


+ 0
- 0
scripts/protocgen.sh View File


+ 1
- 1
state/execution.go View File

@ -452,7 +452,7 @@ func updateState(
LastHeightValidatorsChanged: lastHeightValsChanged,
ConsensusParams: nextParams,
LastHeightConsensusParamsChanged: lastHeightParamsChanged,
LastResultsHash: ABCIResponsesResultsHash(*abciResponses),
LastResultsHash: ABCIResponsesResultsHash(abciResponses),
AppHash: nil,
}, nil
}


+ 6
- 3
state/helpers_test.go View File

@ -172,7 +172,8 @@ func makeHeaderPartsResponsesValPubKeyChange(
block := makeBlock(state, state.LastBlockHeight+1)
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
// If the pubkey is new, remove the old and add the new.
_, val := state.NextValidators.GetByIndex(0)
@ -195,7 +196,8 @@ func makeHeaderPartsResponsesValPowerChange(
block := makeBlock(state, state.LastBlockHeight+1)
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
// If the pubkey is new, remove the old and add the new.
@ -218,7 +220,8 @@ func makeHeaderPartsResponsesParams(
block := makeBlock(state, state.LastBlockHeight+1)
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(&params)},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(&params)},
}
return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses
}


+ 39
- 17
state/state_test.go View File

@ -124,7 +124,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
// Height is implied to equal index+2,
// as block 1 is created from genesis.
added []*abci.ResponseDeliverTx
expected types.ABCIResults
expected []*abci.ResponseDeliverTx
}{
0: {
nil,
@ -134,7 +134,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
[]*abci.ResponseDeliverTx{
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
},
types.ABCIResults{
[]*abci.ResponseDeliverTx{
{Code: 32, Data: []byte("Hello")},
}},
2: {
@ -148,9 +148,12 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
},
},
},
types.ABCIResults{
[]*abci.ResponseDeliverTx{
{Code: 383, Data: nil},
{Code: 0, Data: []byte("Gotcha!")},
{Code: 0, Data: []byte("Gotcha!"), Events: []abci.Event{
{Type: "type1", Attributes: []abci.EventAttribute{{Key: []byte("a"), Value: []byte("1")}}},
{Type: "type2", Attributes: []abci.EventAttribute{{Key: []byte("build"), Value: []byte("stuff")}}},
}},
}},
3: {
nil,
@ -173,6 +176,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
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{},
}
@ -183,8 +187,15 @@ func TestABCIResponsesSaveLoad2(t *testing.T) {
for i, tc := range cases {
h := int64(i + 1)
res, err := sm.LoadABCIResponses(stateDB, h)
assert.NoError(err, "%d", i)
assert.Equal(tc.expected.Hash(), sm.ABCIResponsesResultsHash(*res), "%d", i)
if assert.NoError(err, "%d", i) {
t.Log(res)
responses := &tmstate.ABCIResponses{
BeginBlock: &abci.ResponseBeginBlock{},
DeliverTxs: tc.expected,
EndBlock: &abci.ResponseEndBlock{},
}
assert.Equal(sm.ABCIResponsesResultsHash(responses), sm.ABCIResponsesResultsHash(res), "%d", i)
}
}
}
@ -420,7 +431,8 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) {
block := makeBlock(state, state.LastBlockHeight+1)
blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()}
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -534,7 +546,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()}
// no updates:
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -636,7 +649,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
// -> proposers should alternate:
oldState := updatedState3
abciResponses = &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -651,7 +665,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) {
for i := 0; i < 1000; i++ {
// no validator updates:
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -708,7 +723,8 @@ func TestLargeGenesisValidator(t *testing.T) {
for i := 0; i < 10; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -739,7 +755,8 @@ func TestLargeGenesisValidator(t *testing.T) {
validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{firstAddedVal})
assert.NoError(t, err)
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}},
}
block := makeBlock(oldState, oldState.LastBlockHeight+1)
blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()}
@ -750,7 +767,8 @@ func TestLargeGenesisValidator(t *testing.T) {
for i := 0; i < 200; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -785,7 +803,8 @@ func TestLargeGenesisValidator(t *testing.T) {
assert.NoError(t, err)
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}},
}
block := makeBlock(oldState, oldState.LastBlockHeight+1)
blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()}
@ -799,7 +818,8 @@ func TestLargeGenesisValidator(t *testing.T) {
require.NoError(t, err)
removeGenesisVal := abci.ValidatorUpdate{PubKey: gp, Power: 0}
abciResponses = &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}},
}
block = makeBlock(oldState, oldState.LastBlockHeight+1)
blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()}
@ -817,7 +837,8 @@ func TestLargeGenesisValidator(t *testing.T) {
isProposerUnchanged := true
for isProposerUnchanged {
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)
@ -840,7 +861,8 @@ func TestLargeGenesisValidator(t *testing.T) {
for i := 0; i < 100; i++ {
// no updates:
abciResponses := &tmstate.ABCIResponses{
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
BeginBlock: &abci.ResponseBeginBlock{},
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
}
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates)
require.NoError(t, err)


+ 36
- 8
state/store.go View File

@ -7,6 +7,7 @@ import (
dbm "github.com/tendermint/tm-db"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/merkle"
tmmath "github.com/tendermint/tendermint/libs/math"
tmos "github.com/tendermint/tendermint/libs/os"
tmstate "github.com/tendermint/tendermint/proto/state"
@ -138,8 +139,6 @@ func BootstrapState(db dbm.DB, state State) error {
return db.SetSync(stateKey, state.Bytes())
}
//------------------------------------------------------------------------
// PruneStates deletes states between the given heights (including from, excluding to). It is not
// guaranteed to delete all states, since the last checkpointed state and states being pointed to by
// e.g. `LastHeightChanged` must remain. The state at to must also exist.
@ -253,15 +252,44 @@ func PruneStates(db dbm.DB, from int64, to int64) error {
return nil
}
// ABCIResponsesResultsHash returns the merkle hash of the deliverTxs within ABCIResponses
func ABCIResponsesResultsHash(ar tmstate.ABCIResponses) []byte {
//------------------------------------------------------------------------
// ABCIResponsesResultsHash returns the root hash of a Merkle tree with 3 leafs:
// 1) proto encoded ResponseBeginBlock.Events
// 2) root hash of a Merkle tree of ResponseDeliverTx responses (see ABCIResults.Hash)
// 3) proto encoded ResponseEndBlock.Events
//
// See merkle.SimpleHashFromByteSlices
func ABCIResponsesResultsHash(ar *tmstate.ABCIResponses) []byte {
// proto-encode BeginBlock events.
bbeBytes, err := proto.Marshal(&abci.ResponseBeginBlock{
Events: ar.BeginBlock.Events,
})
if err != nil {
panic(err)
}
// Build a Merkle tree of proto-encoded DeliverTx results and get a hash.
results := types.NewResults(ar.DeliverTxs)
return results.Hash()
// proto-encode EndBlock events.
ebeBytes, err := proto.Marshal(&abci.ResponseEndBlock{
Events: ar.EndBlock.Events,
})
if err != nil {
panic(err)
}
// Build a Merkle tree out of the above 3 binary slices.
return merkle.HashFromByteSlices([][]byte{bbeBytes, results.Hash(), ebeBytes})
}
// LoadABCIResponses loads the ABCIResponses for the given height from the database.
// This is useful for recovering from crashes where we called app.Commit and before we called
// s.Save(). It can also be used to produce Merkle proofs of the result of txs.
// LoadABCIResponses loads the ABCIResponses for the given height from the
// database. If not found, ErrNoABCIResponsesForHeight is returned.
//
// This is useful for recovering from crashes where we called app.Commit and
// before we called s.Save(). It can also be used to produce Merkle proofs of
// the result of txs.
func LoadABCIResponses(db dbm.DB, height int64) (*tmstate.ABCIResponses, error) {
buf, err := db.Get(calcABCIResponsesKey(height))
if err != nil {


+ 23
- 0
state/store_test.go View File

@ -5,6 +5,7 @@ import (
"os"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -14,6 +15,7 @@ import (
cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/merkle"
tmrand "github.com/tendermint/tendermint/libs/rand"
tmstate "github.com/tendermint/tendermint/proto/state"
tmproto "github.com/tendermint/tendermint/proto/types"
@ -187,6 +189,27 @@ 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{},
}
root := sm.ABCIResponsesResultsHash(responses)
bbeBytes, _ := proto.Marshal(responses.BeginBlock)
results := types.NewResults(responses.DeliverTxs)
ebeBytes, _ := proto.Marshal(responses.EndBlock)
root2, proofs := merkle.ProofsFromByteSlices([][]byte{bbeBytes, results.Hash(), ebeBytes})
assert.Equal(t, root2, root)
assert.NoError(t, proofs[1].Verify(root, results.Hash()))
}
func sliceToMap(s []int64) map[int64]bool {
m := make(map[int64]bool, len(s))
for _, i := range s {


+ 23
- 41
types/results.go View File

@ -3,57 +3,23 @@ package types
import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/libs/bytes"
)
//-----------------------------------------------------------------------------
// ABCIResults wraps the deliver tx results to return a proof.
type ABCIResults []*abci.ResponseDeliverTx
// ABCIResult is the deterministic component of a ResponseDeliverTx.
// TODO: add tags and other fields
// https://github.com/tendermint/tendermint/issues/1007
type ABCIResult struct {
Code uint32 `json:"code"`
Data bytes.HexBytes `json:"data"`
}
// Bytes returns the amino encoded ABCIResult
func (a ABCIResult) Bytes() []byte {
return cdcEncode(a)
}
// ABCIResults wraps the deliver tx results to return a proof
type ABCIResults []ABCIResult
// NewResults creates ABCIResults from the list of ResponseDeliverTx.
// NewResults strips non-deterministic fields from ResponseDeliverTx responses
// and returns ABCIResults.
func NewResults(responses []*abci.ResponseDeliverTx) ABCIResults {
res := make(ABCIResults, len(responses))
for i, d := range responses {
res[i] = NewResultFromResponse(d)
res[i] = deterministicResponseDeliverTx(d)
}
return res
}
// NewResultFromResponse creates ABCIResult from ResponseDeliverTx.
func NewResultFromResponse(response *abci.ResponseDeliverTx) ABCIResult {
return ABCIResult{
Code: response.Code,
Data: response.Data,
}
}
// Bytes serializes the ABCIResponse using amino
func (a ABCIResults) Bytes() []byte {
bz, err := cdc.MarshalBinaryLengthPrefixed(a)
if err != nil {
panic(err)
}
return bz
}
// Hash returns a merkle hash of all results
// Hash returns a merkle hash of all results.
func (a ABCIResults) Hash() []byte {
// NOTE: we copy the impl of the merkle tree for txs -
// we should be consistent and either do it for both or not.
return merkle.HashFromByteSlices(a.toByteSlices())
}
@ -67,7 +33,23 @@ func (a ABCIResults) toByteSlices() [][]byte {
l := len(a)
bzs := make([][]byte, l)
for i := 0; i < l; i++ {
bzs[i] = a[i].Bytes()
bz, err := a[i].Marshal()
if err != nil {
panic(err)
}
bzs[i] = bz
}
return bzs
}
// deterministicResponseDeliverTx strips non-deterministic fields from
// ResponseDeliverTx and returns another ResponseDeliverTx.
func deterministicResponseDeliverTx(response *abci.ResponseDeliverTx) *abci.ResponseDeliverTx {
return &abci.ResponseDeliverTx{
Code: response.Code,
Data: response.Data,
GasWanted: response.GasWanted,
GasUsed: response.GasUsed,
Events: response.Events,
}
}

+ 21
- 24
types/results_test.go View File

@ -10,26 +10,31 @@ import (
)
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")}
a := &abci.ResponseDeliverTx{Code: 0, Data: nil}
b := &abci.ResponseDeliverTx{Code: 0, Data: []byte{}}
c := &abci.ResponseDeliverTx{Code: 0, Data: []byte("one")}
d := &abci.ResponseDeliverTx{Code: 14, Data: nil}
e := &abci.ResponseDeliverTx{Code: 14, Data: []byte("foo")}
f := &abci.ResponseDeliverTx{Code: 14, Data: []byte("bar")}
// Nil and []byte{} should produce the same bytes
require.Equal(t, a.Bytes(), a.Bytes())
require.Equal(t, b.Bytes(), b.Bytes())
require.Equal(t, a.Bytes(), b.Bytes())
bzA, err := a.Marshal()
require.NoError(t, err)
bzB, err := b.Marshal()
require.NoError(t, err)
require.Equal(t, bzA, bzB)
// a and b should be the same, don't go in results.
results := ABCIResults{a, c, d, e, f}
// Make sure each result serializes differently
var last []byte
assert.Equal(t, last, a.Bytes()) // first one is empty
last := []byte{}
assert.Equal(t, last, bzA) // first one is empty
for i, res := range results[1:] {
bz := res.Bytes()
bz, err := res.Marshal()
require.NoError(t, err)
assert.NotEqual(t, last, bz, "%d", i)
last = bz
}
@ -39,19 +44,11 @@ func TestABCIResults(t *testing.T) {
assert.NotEmpty(t, root)
for i, res := range results {
bz, err := res.Marshal()
require.NoError(t, err)
proof := results.ProveResult(i)
valid := proof.Verify(root, res.Bytes())
valid := proof.Verify(root, bz)
assert.NoError(t, valid, "%d", i)
}
}
func TestABCIResultsBytes(t *testing.T) {
results := NewResults([]*abci.ResponseDeliverTx{
{Code: 0, Data: []byte{}},
{Code: 0, Data: []byte("one")},
{Code: 14, Data: nil},
{Code: 14, Data: []byte("foo")},
{Code: 14, Data: []byte("bar")},
})
assert.NotNil(t, results.Bytes())
}

Loading…
Cancel
Save