From 64547283353b83ff405052bb823e513c9af6213f Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Wed, 14 May 2025 12:20:52 +0200 Subject: [PATCH 1/4] Use store block data; introduce common package --- pkg/adapter/adapter_test.go | 69 +++++++++--------- pkg/common/convert.go | 112 +++++++++++++++++++++++++++++ pkg/rpc/core/blocks.go | 16 +++-- pkg/rpc/core/utils.go | 136 +----------------------------------- pkg/rpc/core/utils_test.go | 12 ++-- 5 files changed, 165 insertions(+), 180 deletions(-) create mode 100644 pkg/common/convert.go diff --git a/pkg/adapter/adapter_test.go b/pkg/adapter/adapter_test.go index 3f55cb42..ecb9ed9b 100644 --- a/pkg/adapter/adapter_test.go +++ b/pkg/adapter/adapter_test.go @@ -17,12 +17,10 @@ import ( cmtypes "github.com/cometbft/cometbft/types" servertypes "github.com/cosmos/cosmos-sdk/server/types" ds "github.com/ipfs/go-datastore" - kt "github.com/ipfs/go-datastore/keytransform" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - rollnode "github.com/rollkit/rollkit/node" - rstore "github.com/rollkit/rollkit/pkg/store" + "github.com/rollkit/rollkit/types" ) func TestExecuteFiresEvents(t *testing.T) { @@ -76,37 +74,16 @@ func TestExecuteFiresEvents(t *testing.T) { capturedBlockEvents, blockMx := captureEvents(ctx, eventBus, "tm.event='NewBlock'", 1) capturedTxEvents, txMx := captureEvents(ctx, eventBus, "tm.event='Tx'", 2) - mockApp := &MockABCIApp{ - ProcessProposalFn: func(proposal *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { - return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil - }, - FinalizeBlockFn: func(block *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { - return &abci.ResponseFinalizeBlock{ - TxResults: myExecResult, - }, nil - }, - CommitFn: func() (*abci.ResponseCommit, error) { - return &abci.ResponseCommit{}, nil - }, - } - spec.mockMutator(mockApp) + myMockApp := mockApp(myExecResult, spec.mockMutator) originStore := ds.NewMapDatastore() - rollkitPrefixStore := kt.Wrap(originStore, &kt.PrefixTransform{ - Prefix: ds.NewKey(rollnode.RollkitPrefix), - }) - rollkitStore := rstore.New(rollkitPrefixStore) - abciStore := NewStore(originStore) - adapter := &Adapter{ - App: mockApp, - Store: abciStore, - RollkitStore: rollkitStore, - EventBus: eventBus, - Metrics: NopMetrics(), - Logger: log.NewTestLogger(t), - MempoolIDs: newMempoolIDs(), - Mempool: &mempool.NopMempool{}, - } + adapter := NewABCIExecutor(myMockApp, originStore, nil, nil, log.NewTestLogger(t), nil, nil, NopMetrics()) + adapter.EventBus = eventBus + adapter.MempoolIDs = newMempoolIDs() + adapter.Mempool = &mempool.NopMempool{} + + var sig types.Signature = make([]byte, 32) + require.NoError(t, adapter.RollkitStore.SaveBlockData(ctx, headerFixture(), &types.Data{Txs: make(types.Txs, 0)}, &sig)) require.NoError(t, adapter.Store.SaveState(ctx, stateFixture())) // when @@ -154,6 +131,24 @@ func TestExecuteFiresEvents(t *testing.T) { } } +func mockApp(myExecResult []*abci.ExecTxResult, mutator ...func(*MockABCIApp)) *MockABCIApp { + r := &MockABCIApp{ + ProcessProposalFn: func(proposal *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil + }, + FinalizeBlockFn: func(block *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { + return &abci.ResponseFinalizeBlock{TxResults: myExecResult}, nil + }, + CommitFn: func() (*abci.ResponseCommit, error) { + return &abci.ResponseCommit{}, nil + }, + } + for _, m := range mutator { + m(r) + } + return r +} + func captureEvents(ctx context.Context, eventBus *cmtypes.EventBus, query string, numEventsExpected int) (*[]cmtpubsub.Message, *sync.RWMutex) { subscriber := fmt.Sprintf("test-%d", time.Now().UnixNano()) evSub, err := eventBus.Subscribe(ctx, subscriber, cmtquery.MustCompile(query), numEventsExpected) @@ -181,6 +176,16 @@ func captureEvents(ctx context.Context, eventBus *cmtypes.EventBus, query string return &capturedEvents, &mx } +func headerFixture() *types.SignedHeader { + return &types.SignedHeader{ + Header: types.Header{ + BaseHeader: types.BaseHeader{Height: 2, Time: uint64(time.Now().UnixNano())}, + ProposerAddress: []byte("proposer1"), + AppHash: []byte("apphash1"), + }, + } +} + type MockABCIApp struct { servertypes.ABCI // satisfy the interface ProcessProposalFn func(*abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) diff --git a/pkg/common/convert.go b/pkg/common/convert.go new file mode 100644 index 00000000..0f71045a --- /dev/null +++ b/pkg/common/convert.go @@ -0,0 +1,112 @@ +package common + +import ( + "errors" + "time" + + cmbytes "github.com/cometbft/cometbft/libs/bytes" + cmversion "github.com/cometbft/cometbft/proto/tendermint/version" + cmttypes "github.com/cometbft/cometbft/types" + + rlktypes "github.com/rollkit/rollkit/types" +) + +// ToABCIHeader converts Rollkit header to Header format defined in ABCI. +// Caller should fill all the fields that are not available in Rollkit header (like ChainID). +func ToABCIHeader(header *rlktypes.Header) (cmttypes.Header, error) { + return cmttypes.Header{ + Version: cmversion.Consensus{ + Block: header.Version.Block, + App: header.Version.App, + }, + Height: int64(header.Height()), //nolint:gosec + Time: header.Time(), + LastBlockID: cmttypes.BlockID{ + Hash: cmbytes.HexBytes(header.LastHeaderHash[:]), + PartSetHeader: cmttypes.PartSetHeader{ + Total: 0, + Hash: nil, + }, + }, + LastCommitHash: cmbytes.HexBytes(header.LastCommitHash), + DataHash: cmbytes.HexBytes(header.DataHash), + ConsensusHash: cmbytes.HexBytes(header.ConsensusHash), + AppHash: cmbytes.HexBytes(header.AppHash), + LastResultsHash: cmbytes.HexBytes(header.LastResultsHash), + EvidenceHash: new(cmttypes.EvidenceData).Hash(), + ProposerAddress: header.ProposerAddress, + ChainID: header.ChainID(), + ValidatorsHash: cmbytes.HexBytes(header.ValidatorHash), + NextValidatorsHash: cmbytes.HexBytes(header.ValidatorHash), + }, nil +} + +// ToABCIBlock converts Rolkit block into block format defined by ABCI. +// Returned block should pass `ValidateBasic`. +func ToABCIBlock(header *rlktypes.SignedHeader, data *rlktypes.Data) (*cmttypes.Block, error) { + abciHeader, err := ToABCIHeader(&header.Header) + if err != nil { + return nil, err + } + + // we have one validator + if len(header.ProposerAddress) == 0 { + return nil, errors.New("proposer address is not set") + } + + abciCommit := ToABCICommit(header.Height(), header.Hash(), header.ProposerAddress, header.Time(), header.Signature) + + // This assumes that we have only one signature + if len(abciCommit.Signatures) == 1 { + abciCommit.Signatures[0].ValidatorAddress = header.ProposerAddress + } + abciBlock := cmttypes.Block{ + Header: abciHeader, + Evidence: cmttypes.EvidenceData{ + Evidence: nil, + }, + LastCommit: abciCommit, + } + abciBlock.Txs = make([]cmttypes.Tx, len(data.Txs)) + for i := range data.Txs { + abciBlock.Txs[i] = cmttypes.Tx(data.Txs[i]) + } + abciBlock.DataHash = cmbytes.HexBytes(header.DataHash) + + return &abciBlock, nil +} + +// ToABCIBlockMeta converts Rollkit block into BlockMeta format defined by ABCI +func ToABCIBlockMeta(header *rlktypes.SignedHeader, data *rlktypes.Data) (*cmttypes.BlockMeta, error) { + cmblock, err := ToABCIBlock(header, data) + if err != nil { + return nil, err + } + blockID := cmttypes.BlockID{Hash: cmblock.Hash()} + + return &cmttypes.BlockMeta{ + BlockID: blockID, + BlockSize: cmblock.Size(), + Header: cmblock.Header, + NumTxs: len(cmblock.Txs), + }, nil +} + +// ToABCICommit returns a commit format defined by ABCI. +// Other fields (especially ValidatorAddress and Timestamp of Signature) have to be filled by caller. +func ToABCICommit(height uint64, hash rlktypes.Hash, val cmttypes.Address, time time.Time, signature rlktypes.Signature) *cmttypes.Commit { + return &cmttypes.Commit{ + Height: int64(height), //nolint:gosec + Round: 0, + BlockID: cmttypes.BlockID{ + Hash: cmbytes.HexBytes(hash), + PartSetHeader: cmttypes.PartSetHeader{}, + }, + Signatures: []cmttypes.CommitSig{{ + BlockIDFlag: cmttypes.BlockIDFlagCommit, + Signature: signature, + ValidatorAddress: val, + Timestamp: time, + }}, + } +} diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index b89dd59e..e3960767 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -14,6 +14,8 @@ import ( "github.com/rollkit/rollkit/block" rlktypes "github.com/rollkit/rollkit/types" + + "github.com/rollkit/go-execution-abci/pkg/common" ) // BlockSearch searches for a paginated set of blocks matching BeginBlock and @@ -69,7 +71,7 @@ func BlockSearch( if err != nil { return nil, err } - block, err := ToABCIBlock(header, data) + block, err := common.ToABCIBlock(header, data) if err != nil { return nil, err } @@ -114,7 +116,7 @@ func Block(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlock, error) } hash := header.Hash() - abciBlock, err := ToABCIBlock(header, data) + abciBlock, err := common.ToABCIBlock(header, data) if err != nil { return nil, err } @@ -138,7 +140,7 @@ func BlockByHash(ctx *rpctypes.Context, hash []byte) (*ctypes.ResultBlock, error return nil, err } - abciBlock, err := ToABCIBlock(header, data) + abciBlock, err := common.ToABCIBlock(header, data) if err != nil { return nil, err } @@ -171,9 +173,9 @@ func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, erro } val := header.ProposerAddress - commit := GetABCICommit(heightValue, header.Hash(), val, header.Time(), header.Signature) + commit := common.ToABCICommit(heightValue, header.Hash(), val, header.Time(), header.Signature) - block, err := ToABCIBlock(header, data) + block, err := common.ToABCIBlock(header, data) if err != nil { return nil, err } @@ -238,7 +240,7 @@ func HeaderByHash(ctx *rpctypes.Context, hash cmbytes.HexBytes) (*ctypes.ResultH return nil, err } - blockMeta, err := ToABCIBlockMeta(header, data) + blockMeta, err := common.ToABCIBlockMeta(header, data) if err != nil { return nil, err } @@ -280,7 +282,7 @@ func BlockchainInfo(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes. return nil, err } if header != nil && data != nil { - cmblockmeta, err := ToABCIBlockMeta(header, data) + cmblockmeta, err := common.ToABCIBlockMeta(header, data) if err != nil { return nil, err } diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index d66ca7b3..782c90b7 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -5,121 +5,12 @@ import ( "encoding/hex" "errors" "fmt" - "time" - - cmbytes "github.com/cometbft/cometbft/libs/bytes" - cmversion "github.com/cometbft/cometbft/proto/tendermint/version" cmttypes "github.com/cometbft/cometbft/types" - - rlktypes "github.com/rollkit/rollkit/types" + "github.com/rollkit/go-execution-abci/pkg/common" ) const NodeIDByteLength = 20 -// ToABCIHeader converts Rollkit header to Header format defined in ABCI. -// Caller should fill all the fields that are not available in Rollkit header (like ChainID). -func ToABCIHeader(header *rlktypes.Header) (cmttypes.Header, error) { - return cmttypes.Header{ - Version: cmversion.Consensus{ - Block: header.Version.Block, - App: header.Version.App, - }, - Height: int64(header.Height()), //nolint:gosec - Time: header.Time(), - LastBlockID: cmttypes.BlockID{ - Hash: cmbytes.HexBytes(header.LastHeaderHash[:]), - PartSetHeader: cmttypes.PartSetHeader{ - Total: 0, - Hash: nil, - }, - }, - LastCommitHash: cmbytes.HexBytes(header.LastCommitHash), - DataHash: cmbytes.HexBytes(header.DataHash), - ConsensusHash: cmbytes.HexBytes(header.ConsensusHash), - AppHash: cmbytes.HexBytes(header.AppHash), - LastResultsHash: cmbytes.HexBytes(header.LastResultsHash), - EvidenceHash: new(cmttypes.EvidenceData).Hash(), - ProposerAddress: header.ProposerAddress, - ChainID: header.ChainID(), - ValidatorsHash: cmbytes.HexBytes(header.ValidatorHash), - NextValidatorsHash: cmbytes.HexBytes(header.ValidatorHash), - }, nil -} - -// ToABCIBlock converts Rolkit block into block format defined by ABCI. -// Returned block should pass `ValidateBasic`. -func ToABCIBlock(header *rlktypes.SignedHeader, data *rlktypes.Data) (*cmttypes.Block, error) { - abciHeader, err := ToABCIHeader(&header.Header) - if err != nil { - return nil, err - } - - // we have one validator - if len(header.ProposerAddress) == 0 { - return nil, errors.New("proposer address is not set") - } - - abciCommit := getABCICommit(header.Height(), header.Hash(), header.ProposerAddress, header.Time(), header.Signature) - - // This assumes that we have only one signature - if len(abciCommit.Signatures) == 1 { - abciCommit.Signatures[0].ValidatorAddress = header.ProposerAddress - } - abciBlock := cmttypes.Block{ - Header: abciHeader, - Evidence: cmttypes.EvidenceData{ - Evidence: nil, - }, - LastCommit: abciCommit, - } - abciBlock.Txs = make([]cmttypes.Tx, len(data.Txs)) - for i := range data.Txs { - abciBlock.Txs[i] = cmttypes.Tx(data.Txs[i]) - } - abciBlock.DataHash = cmbytes.HexBytes(header.DataHash) - - return &abciBlock, nil -} - -// ToABCIBlockMeta converts Rollkit block into BlockMeta format defined by ABCI -func ToABCIBlockMeta(header *rlktypes.SignedHeader, data *rlktypes.Data) (*cmttypes.BlockMeta, error) { - cmblock, err := ToABCIBlock(header, data) - if err != nil { - return nil, err - } - blockID := cmttypes.BlockID{Hash: cmblock.Hash()} - - return &cmttypes.BlockMeta{ - BlockID: blockID, - BlockSize: cmblock.Size(), - Header: cmblock.Header, - NumTxs: len(cmblock.Txs), - }, nil -} - -// getABCICommit returns a commit format defined by ABCI. -// Other fields (especially ValidatorAddress and Timestamp of Signature) have to be filled by caller. -func getABCICommit(height uint64, hash []byte, val cmttypes.Address, time time.Time, signature []byte) *cmttypes.Commit { - tmCommit := cmttypes.Commit{ - Height: int64(height), //nolint:gosec - Round: 0, - BlockID: cmttypes.BlockID{ - Hash: cmbytes.HexBytes(hash), - PartSetHeader: cmttypes.PartSetHeader{}, - }, - Signatures: make([]cmttypes.CommitSig, 1), - } - commitSig := cmttypes.CommitSig{ - BlockIDFlag: cmttypes.BlockIDFlagCommit, - Signature: signature, - ValidatorAddress: val, - Timestamp: time, - } - tmCommit.Signatures[0] = commitSig - - return &tmCommit -} - func normalizeHeight(height *int64) uint64 { var heightValue uint64 if height == nil { @@ -159,7 +50,7 @@ func getBlockMeta(ctx context.Context, n uint64) *cmttypes.BlockMeta { return nil } // Assuming ToABCIBlockMeta is now in pkg/rpc/provider/provider_utils.go - bmeta, err := ToABCIBlockMeta(header, data) // Removed rpc. prefix + bmeta, err := common.ToABCIBlockMeta(header, data) // Removed rpc. prefix if err != nil { env.Logger.Error("Failed to convert block to ABCI block meta", "height", n, "err", err) return nil @@ -199,29 +90,6 @@ func filterMinMax(base, height, mini, maxi, limit int64) (int64, int64, error) { return mini, maxi, nil } -// GetABCICommit returns a commit format defined by ABCI. -// Other fields (especially ValidatorAddress and Timestamp of Signature) have to be filled by caller. -func GetABCICommit(height uint64, hash rlktypes.Hash, val cmttypes.Address, time time.Time, signature rlktypes.Signature) *cmttypes.Commit { - tmCommit := cmttypes.Commit{ - Height: int64(height), //nolint:gosec - Round: 0, - BlockID: cmttypes.BlockID{ - Hash: cmbytes.HexBytes(hash), - PartSetHeader: cmttypes.PartSetHeader{}, - }, - Signatures: make([]cmttypes.CommitSig, 1), - } - commitSig := cmttypes.CommitSig{ - BlockIDFlag: cmttypes.BlockIDFlagCommit, - Signature: signature, - ValidatorAddress: val, - Timestamp: time, - } - tmCommit.Signatures[0] = commitSig - - return &tmCommit -} - // TruncateNodeID from rollkit we receive a 32 bytes node id, but we only need the first 20 bytes // to be compatible with the ABCI node info func TruncateNodeID(idStr string) (string, error) { diff --git a/pkg/rpc/core/utils_test.go b/pkg/rpc/core/utils_test.go index cb60a5d3..db91a9a1 100644 --- a/pkg/rpc/core/utils_test.go +++ b/pkg/rpc/core/utils_test.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "github.com/stretchr/testify/require" "strings" "testing" @@ -21,7 +22,7 @@ func TestTruncateNodeID(t *testing.T) { expectedPrefix := fullNodeIDStr[:NodeIDByteLength*2] // First 20 bytes -> 40 chars truncatedNodeID, err := TruncateNodeID(fullNodeIDStr) - assert.NoError(t, err, "TruncateNodeID should not return an error for a valid ID") + require.NoError(t, err, "TruncateNodeID should not return an error for a valid ID") assert.Equal(t, NodeIDByteLength*2, len(truncatedNodeID), fmt.Sprintf("Truncated Node ID should be %d characters long", NodeIDByteLength*2)) assert.Equal(t, expectedPrefix, truncatedNodeID, "Truncated Node ID should be the first 20 bytes of the original ID") }) @@ -37,12 +38,9 @@ func TestTruncateNodeID(t *testing.T) { // Test case 3: Invalid hex string t.Run("invalid hex string", func(t *testing.T) { invalidHexStr := "not-a-hex-string-at-all-and-should-be-long-enough-to-pass-length-check" - // Ensure it would be long enough if it were hex - if len(invalidHexStr) < NodeIDByteLength*2 { - invalidHexStr = strings.Repeat("x", NodeIDByteLength*2) // Make it long enough but still invalid hex - } + require.GreaterOrEqual(t, len(invalidHexStr), NodeIDByteLength*2) _, err := TruncateNodeID(invalidHexStr) - assert.Error(t, err, "TruncateNodeID should return an error for an invalid hex string") + require.Error(t, err, "TruncateNodeID should return an error for an invalid hex string") assert.Contains(t, err.Error(), "failed to decode node ID", "Error message should indicate hex decoding failure") }) @@ -56,7 +54,7 @@ func TestTruncateNodeID(t *testing.T) { t.Run("exact length node ID", func(t *testing.T) { exactLengthNodeIDStr := strings.Repeat("a", NodeIDByteLength*2) // 40 chars truncatedNodeID, err := TruncateNodeID(exactLengthNodeIDStr) - assert.NoError(t, err, "TruncateNodeID should not return an error for an ID of exact length") + require.NoError(t, err, "TruncateNodeID should not return an error for an ID of exact length") assert.Equal(t, exactLengthNodeIDStr, truncatedNodeID, "Truncated Node ID should be the same as input for exact length") }) } From 70515e70e4313194840d9094416636abd7553474 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Wed, 14 May 2025 12:23:47 +0200 Subject: [PATCH 2/4] Gci only --- pkg/rpc/core/utils.go | 2 ++ pkg/rpc/core/utils_test.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/rpc/core/utils.go b/pkg/rpc/core/utils.go index 782c90b7..a9bd6282 100644 --- a/pkg/rpc/core/utils.go +++ b/pkg/rpc/core/utils.go @@ -5,7 +5,9 @@ import ( "encoding/hex" "errors" "fmt" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/rollkit/go-execution-abci/pkg/common" ) diff --git a/pkg/rpc/core/utils_test.go b/pkg/rpc/core/utils_test.go index db91a9a1..81946547 100644 --- a/pkg/rpc/core/utils_test.go +++ b/pkg/rpc/core/utils_test.go @@ -4,11 +4,11 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "github.com/stretchr/testify/require" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTruncateNodeID(t *testing.T) { From 5d76b9abd391a4327ff1ba6a9d5ab70490b2ad97 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Wed, 14 May 2025 17:25:31 +0200 Subject: [PATCH 3/4] Add proofs to rpc response --- pkg/rpc/core/tx.go | 64 +++++++++++++++++++++--------- pkg/rpc/core/tx_test.go | 86 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 126 insertions(+), 24 deletions(-) diff --git a/pkg/rpc/core/tx.go b/pkg/rpc/core/tx.go index 8e5a1bd6..8e866323 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 b8a8d848..94b1176c 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" From c5f954de2aa1303e47e6fa74b4cc9e8008e0952e Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Thu, 15 May 2025 08:54:19 +0200 Subject: [PATCH 4/4] Review feedback --- pkg/rpc/core/tx_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/rpc/core/tx_test.go b/pkg/rpc/core/tx_test.go index 94b1176c..fe9c3135 100644 --- a/pkg/rpc/core/tx_test.go +++ b/pkg/rpc/core/tx_test.go @@ -239,11 +239,9 @@ func TestTxSearch(t *testing.T) { 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() + &rktypes.Data{Txs: rktypes.Txs{[]byte{0}, []byte{1}}}, nil).Twice() 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) @@ -263,7 +261,7 @@ func TestTxSearch(t *testing.T) { 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(2), result.Txs[1].Proof.Proof.Total) assert.Equal(int64(1), result.Txs[1].Proof.Proof.Index) assert.NotEmpty(result.Txs[1].Proof.Proof.LeafHash)