Skip to content

Add proofs to rpc response #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 45 additions & 19 deletions pkg/rpc/core/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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],
}
}
Comment on lines +135 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return early when txs is empty to prevent invalid proofs

merkle.ProofsFromByteSlices panics on an empty slice. A quick guard keeps the
node stable when encountering an unexpected empty block.

 func proofTXExists(txs []rlktypes.Tx, i uint32) cmttypes.TxProof {
+    if len(txs) == 0 {
+        return cmttypes.TxProof{}
+    }
     hl := hashList(txs)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 proofTXExists(txs []rlktypes.Tx, i uint32) cmttypes.TxProof {
if len(txs) == 0 {
return 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
}
86 changes: 81 additions & 5 deletions pkg/rpc/core/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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")
Expand All @@ -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))
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading