diff --git a/pkg/rpc/core/tx.go b/pkg/rpc/core/tx.go index 8e5a1bd..8e86632 100644 --- a/pkg/rpc/core/tx.go +++ b/pkg/rpc/core/tx.go @@ -5,10 +5,14 @@ import ( "fmt" "sort" + "github.com/cometbft/cometbft/crypto/merkle" + "github.com/cometbft/cometbft/crypto/tmhash" cmtquery "github.com/cometbft/cometbft/libs/pubsub/query" ctypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" - "github.com/cometbft/cometbft/types" + cmttypes "github.com/cometbft/cometbft/types" + + rlktypes "github.com/rollkit/rollkit/types" ) // Tx allows you to query the transaction results. `nil` could mean the @@ -28,17 +32,12 @@ func Tx(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error height := res.Height index := res.Index - var proof types.TxProof - // if prove { - // //_, data, _ := env.Adapter.RollkitStore.GetBlockData(unwrappedCtx, uint64(height)) - // //blockProof := data.Txs.Proof(int(index)) // TODO: Add proof method to Txs - // // proof = types.TxProof{ - // // RootHash: blockProof.RootHash, - // // Data: types.Tx(blockProof.Data), - // // Proof: blockProof.Proof, - // // } - // } - + var proof cmttypes.TxProof + if prove { + if proof, err = buildProof(ctx, height, index); err != nil { + return nil, err + } + } return &ctypes.ResultTx{ Hash: hash, Height: height, @@ -106,14 +105,14 @@ func TxSearch( for i := skipCount; i < skipCount+pageSize; i++ { r := results[i] - var proof types.TxProof - /*if prove { - block := nil //env.BlockStore.GetBlock(r.Height) - proof = block.Data.Txs.Proof(int(r.Index)) // XXX: overflow on 32-bit machines - }*/ - + var proof cmttypes.TxProof + if prove { + if proof, err = buildProof(ctx, r.Height, r.Index); err != nil { + return nil, err + } + } apiResults = append(apiResults, &ctypes.ResultTx{ - Hash: types.Tx(r.Tx).Hash(), + Hash: cmttypes.Tx(r.Tx).Hash(), Height: r.Height, Index: r.Index, TxResult: r.Result, @@ -124,3 +123,30 @@ func TxSearch( return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil } + +func buildProof(ctx *rpctypes.Context, blockHeight int64, txIndex uint32) (cmttypes.TxProof, error) { + _, data, err := env.Adapter.RollkitStore.GetBlockData(ctx.Context(), uint64(blockHeight)) + if err != nil { + return cmttypes.TxProof{}, fmt.Errorf("failed to get block data: %w", err) + } + return proofTXExists(data.Txs, txIndex), nil +} + +func proofTXExists(txs []rlktypes.Tx, i uint32) cmttypes.TxProof { + hl := hashList(txs) + root, proofs := merkle.ProofsFromByteSlices(hl) + + return cmttypes.TxProof{ + RootHash: root, + Data: cmttypes.Tx(txs[i]), + Proof: *proofs[i], + } +} + +func hashList(txs []rlktypes.Tx) [][]byte { + hl := make([][]byte, len(txs)) + for i := 0; i < len(txs); i++ { + hl[i] = tmhash.Sum(txs[i]) + } + return hl +} diff --git a/pkg/rpc/core/tx_test.go b/pkg/rpc/core/tx_test.go index b8a8d84..94b1176 100644 --- a/pkg/rpc/core/tx_test.go +++ b/pkg/rpc/core/tx_test.go @@ -12,6 +12,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + rktypes "github.com/rollkit/rollkit/types" + "github.com/rollkit/go-execution-abci/pkg/adapter" ) @@ -22,10 +24,14 @@ func TestTx(t *testing.T) { ctx := newTestRPCContext() // Assumes newTestRPCContext is available or define it mockTxIndexer := new(MockTxIndexer) + mockStore := new(MockRollkitStore) + env = &Environment{ TxIndexer: mockTxIndexer, Logger: cmtlog.NewNopLogger(), - Adapter: &adapter.Adapter{}, // Minimal adapter needed? Add mocks if GetBlockData is used (for prove=true) + Adapter: &adapter.Adapter{ + RollkitStore: mockStore, + }, } sampleTx := cmttypes.Tx("sample_tx_data") @@ -44,11 +50,32 @@ func TestTx(t *testing.T) { Result: sampleResult, } - t.Run("Success", func(t *testing.T) { + t.Run("Success with proofs", func(t *testing.T) { mockTxIndexer.On("Get", sampleHash).Return(sampleTxResult, nil).Once() + mockStore.On("GetBlockData", mock.Anything, uint64(sampleHeight)).Return(nil, + &rktypes.Data{Txs: rktypes.Txs{[]byte{0}, []byte{1}}}, nil).Once() + result, err := Tx(ctx, sampleHash, true) - result, err := Tx(ctx, sampleHash, false) // prove = false + require.NoError(err) + require.NotNil(result) + assert.Equal(sampleHash, []byte(result.Hash)) + assert.Equal(sampleHeight, result.Height) + assert.Equal(sampleIndex, result.Index) + assert.Equal(sampleResult, result.TxResult) + assert.Equal(sampleTx, result.Tx) + assert.Equal(int64(2), result.Proof.Proof.Total) + assert.Equal(int64(1), result.Proof.Proof.Index) + assert.NotEmpty(result.Proof.Proof.LeafHash) + + mockTxIndexer.AssertExpectations(t) + mockStore.AssertExpectations(t) + }) + t.Run("result without proofs", func(t *testing.T) { + mockTxIndexer.On("Get", sampleHash).Return(sampleTxResult, nil).Once() + // when + result, err := Tx(ctx, sampleHash, false) + // then require.NoError(err) require.NotNil(result) assert.Equal(sampleHash, []byte(result.Hash)) @@ -57,9 +84,10 @@ func TestTx(t *testing.T) { assert.Equal(sampleResult, result.TxResult) assert.Equal(sampleTx, result.Tx) // Proof is expected to be empty when prove is false - assert.Empty(result.Proof.Proof) // Check specific fields if needed + assert.Empty(result.Proof.Proof) mockTxIndexer.AssertExpectations(t) + mockStore.AssertExpectations(t) }) t.Run("NotFound", func(t *testing.T) { @@ -99,10 +127,11 @@ func TestTxSearch(t *testing.T) { ctx := newTestRPCContext() mockTxIndexer := new(MockTxIndexer) + mockStore := new(MockRollkitStore) env = &Environment{ TxIndexer: mockTxIndexer, Logger: cmtlog.NewNopLogger(), - Adapter: &adapter.Adapter{}, + Adapter: &adapter.Adapter{RollkitStore: mockStore}, } // Sample transactions for search results @@ -201,6 +230,53 @@ func TestTxSearch(t *testing.T) { mockTxIndexer.AssertExpectations(t) }) + t.Run("with proofs", func(t *testing.T) { + query := "tx.height >= 10" + orderBy := "asc" + mockTxIndexer.On("Search", mock.Anything, mock.AnythingOfType("*query.Query")).Run(func(args mock.Arguments) { + // Basic check if the query seems right (optional) + q := args.Get(1).(*cmtquery.Query) + require.NotNil(q) + }).Return(searchResults, nil).Once() + mockStore.On("GetBlockData", mock.Anything, uint64(res1.Height)).Return(nil, + &rktypes.Data{Txs: rktypes.Txs{[]byte{0}, []byte{1}}}, nil).Once() + mockStore.On("GetBlockData", mock.Anything, uint64(res2.Height)).Return(nil, + &rktypes.Data{Txs: rktypes.Txs{[]byte{2}}}, nil).Once() + mockStore.On("GetBlockData", mock.Anything, uint64(res3.Height)).Return(nil, + &rktypes.Data{Txs: rktypes.Txs{[]byte{3}, []byte{4}, []byte{5}}}, nil).Once() + + result, err := TxSearch(ctx, query, true, &defaultPage, &defaultPerPage, orderBy) + + require.NoError(err) + require.NotNil(result) + assert.Equal(3, result.TotalCount) + require.Len(result.Txs, 3) + + // Check order: (h10, i0), (h10, i1), (h11, i0) + assert.Equal(int64(10), result.Txs[0].Height) + assert.Equal(uint32(0), result.Txs[0].Index) + assert.Equal(tx3.Hash(), []byte(result.Txs[0].Hash)) + assert.Equal(int64(2), result.Txs[0].Proof.Proof.Total) + assert.Equal(int64(0), result.Txs[0].Proof.Proof.Index) + assert.NotEmpty(result.Txs[0].Proof.Proof.LeafHash) + + assert.Equal(int64(10), result.Txs[1].Height) + assert.Equal(uint32(1), result.Txs[1].Index) + assert.Equal(tx1.Hash(), []byte(result.Txs[1].Hash)) + assert.Equal(int64(3), result.Txs[1].Proof.Proof.Total) + assert.Equal(int64(1), result.Txs[1].Proof.Proof.Index) + assert.NotEmpty(result.Txs[1].Proof.Proof.LeafHash) + + assert.Equal(int64(11), result.Txs[2].Height) + assert.Equal(uint32(0), result.Txs[2].Index) + assert.Equal(tx2.Hash(), []byte(result.Txs[2].Hash)) + assert.Equal(int64(1), result.Txs[2].Proof.Proof.Total) + assert.Equal(int64(0), result.Txs[2].Proof.Proof.Index) + assert.NotEmpty(result.Txs[2].Proof.Proof.LeafHash) + + mockTxIndexer.AssertExpectations(t) + }) + t.Run("Error_InvalidQuery", func(t *testing.T) { invalidQuery := "invalid query string!!!" orderBy := "asc"