From 4763b0ee9922b0d07e59633b00bf81b6a839880e Mon Sep 17 00:00:00 2001 From: William Banfield Date: Tue, 15 Mar 2022 11:12:15 -0400 Subject: [PATCH] resurrect tx.Proof method --- internal/rpc/core/tx.go | 15 +----- types/tx.go | 28 ++++++++--- types/tx_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/internal/rpc/core/tx.go b/internal/rpc/core/tx.go index 48501f148..73fa6d2c8 100644 --- a/internal/rpc/core/tx.go +++ b/internal/rpc/core/tx.go @@ -6,7 +6,6 @@ import ( "fmt" "sort" - "github.com/tendermint/tendermint/crypto/merkle" tmquery "github.com/tendermint/tendermint/internal/pubsub/query" "github.com/tendermint/tendermint/internal/state/indexer" "github.com/tendermint/tendermint/libs/bytes" @@ -40,12 +39,7 @@ func (env *Environment) Tx(ctx context.Context, hash bytes.HexBytes, prove bool) var proof types.TxProof if prove { block := env.BlockStore.LoadBlock(r.Height) - root, proofs := merkle.ProofsFromByteSlices(block.Data.Txs.ToSliceOfBytes()) - proof = types.TxProof{ - RootHash: root, - Proof: *proofs[int(r.Index)], - Data: block.Data.Txs[int(r.Index)], - } + proof = block.Data.Txs.Proof(int(r.Index)) } return &coretypes.ResultTx{ @@ -130,12 +124,7 @@ func (env *Environment) TxSearch( var proof types.TxProof if prove { block := env.BlockStore.LoadBlock(r.Height) - root, proofs := merkle.ProofsFromByteSlices(block.Data.Txs.ToSliceOfBytes()) - proof = types.TxProof{ - RootHash: root, - Proof: *proofs[int(r.Index)], - Data: block.Data.Txs[int(r.Index)], - } + proof = block.Data.Txs.Proof(int(r.Index)) } apiResults = append(apiResults, &coretypes.ResultTx{ diff --git a/types/tx.go b/types/tx.go index 265f24cb2..107f9c4e6 100644 --- a/types/tx.go +++ b/types/tx.go @@ -34,13 +34,8 @@ type Txs []Tx // Hash returns the Merkle root hash of the transaction hashes. // i.e. the leaves of the tree are the hashes of the txs. func (txs Txs) Hash() []byte { - // These allocations will be removed once Txs is switched to [][]byte, - // ref #2603. This is because golang does not allow type casting slices without unsafe - txBzs := make([][]byte, len(txs)) - for i := 0; i < len(txs); i++ { - txBzs[i] = txs[i].Hash() - } - return merkle.HashFromByteSlices(txBzs) + hl := txs.hashList() + return merkle.HashFromByteSlices(hl) } // Index returns the index of this transaction in the list, or -1 if not found @@ -63,6 +58,25 @@ func (txs Txs) IndexByHash(hash []byte) int { return -1 } +func (txs Txs) Proof(i int) TxProof { + hl := txs.hashList() + root, proofs := merkle.ProofsFromByteSlices(hl) + + return TxProof{ + RootHash: root, + Data: txs[i], + Proof: *proofs[i], + } +} + +func (txs Txs) hashList() [][]byte { + hl := make([][]byte, len(txs)) + for i := 0; i < len(txs); i++ { + hl[i] = txs[i].Hash() + } + return hl +} + // Txs is a slice of transactions. Sorting a Txs value orders the transactions // lexicographically. func (txs Txs) Len() int { return len(txs) } diff --git a/types/tx_test.go b/types/tx_test.go index 105b03e86..00ef29e14 100644 --- a/types/tx_test.go +++ b/types/tx_test.go @@ -1,13 +1,17 @@ package types import ( + "bytes" + "math/rand" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + ctest "github.com/tendermint/tendermint/internal/libs/test" tmrand "github.com/tendermint/tendermint/libs/rand" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) func makeTxs(cnt, size int) Txs { @@ -197,3 +201,107 @@ func TestValidateTxRecordSet(t *testing.T) { } }) } + +func TestValidTxProof(t *testing.T) { + cases := []struct { + txs Txs + }{ + {Txs{{1, 4, 34, 87, 163, 1}}}, + {Txs{{5, 56, 165, 2}, {4, 77}}}, + {Txs{Tx("foo"), Tx("bar"), Tx("baz")}}, + {makeTxs(20, 5)}, + {makeTxs(7, 81)}, + {makeTxs(61, 15)}, + } + + for h, tc := range cases { + txs := tc.txs + root := txs.Hash() + // make sure valid proof for every tx + for i := range txs { + tx := []byte(txs[i]) + proof := txs.Proof(i) + assert.EqualValues(t, i, proof.Proof.Index, "%d: %d", h, i) + assert.EqualValues(t, len(txs), proof.Proof.Total, "%d: %d", h, i) + assert.EqualValues(t, root, proof.RootHash, "%d: %d", h, i) + assert.EqualValues(t, tx, proof.Data, "%d: %d", h, i) + assert.EqualValues(t, txs[i].Hash(), proof.Leaf(), "%d: %d", h, i) + assert.Nil(t, proof.Validate(root), "%d: %d", h, i) + assert.NotNil(t, proof.Validate([]byte("foobar")), "%d: %d", h, i) + + // read-write must also work + var ( + p2 TxProof + pb2 tmproto.TxProof + ) + pbProof := proof.ToProto() + bin, err := pbProof.Marshal() + require.NoError(t, err) + + err = pb2.Unmarshal(bin) + require.NoError(t, err) + + p2, err = TxProofFromProto(pb2) + if assert.NoError(t, err, "%d: %d: %+v", h, i, err) { + assert.Nil(t, p2.Validate(root), "%d: %d", h, i) + } + } + } +} + +func TestTxProofUnchangable(t *testing.T) { + // run the other test a bunch... + for i := 0; i < 40; i++ { + testTxProofUnchangable(t) + } +} + +func testTxProofUnchangable(t *testing.T) { + // make some proof + txs := makeTxs(randInt(2, 100), randInt(16, 128)) + root := txs.Hash() + i := randInt(0, len(txs)-1) + proof := txs.Proof(i) + + // make sure it is valid to start with + assert.Nil(t, proof.Validate(root)) + pbProof := proof.ToProto() + bin, err := pbProof.Marshal() + require.NoError(t, err) + + // try mutating the data and make sure nothing breaks + for j := 0; j < 500; j++ { + bad := ctest.MutateByteSlice(bin) + if !bytes.Equal(bad, bin) { + assertBadProof(t, root, bad, proof) + } + } +} + +// This makes sure that the proof doesn't deserialize into something valid. +func assertBadProof(t *testing.T, root []byte, bad []byte, good TxProof) { + + var ( + proof TxProof + pbProof tmproto.TxProof + ) + err := pbProof.Unmarshal(bad) + if err == nil { + proof, err = TxProofFromProto(pbProof) + if err == nil { + err = proof.Validate(root) + if err == nil { + // XXX Fix simple merkle proofs so the following is *not* OK. + // This can happen if we have a slightly different total (where the + // path ends up the same). If it is something else, we have a real + // problem. + assert.NotEqual(t, proof.Proof.Total, good.Proof.Total, "bad: %#v\ngood: %#v", proof, good) + } + } + } +} + +func randInt(low, high int) int { + off := rand.Int() % (high - low) + return low + off +}