From 40827ae8dec54ef0e9a3974d17d0d7c1d9082c7b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jan 2024 16:04:30 +0100 Subject: [PATCH 1/9] Save tx results when applying the block --- tm2/pkg/bft/state/errors.go | 8 ++++ tm2/pkg/bft/state/execution.go | 18 +++++++-- tm2/pkg/bft/state/store.go | 27 +++++++++++++ tm2/pkg/bft/state/store_test.go | 2 +- tm2/pkg/bft/state/tx_result_test.go | 60 +++++++++++++++++++++++++++++ tm2/pkg/bft/types/tx.go | 5 +++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 tm2/pkg/bft/state/tx_result_test.go diff --git a/tm2/pkg/bft/state/errors.go b/tm2/pkg/bft/state/errors.go index 48e01cce451..ffabc577d68 100644 --- a/tm2/pkg/bft/state/errors.go +++ b/tm2/pkg/bft/state/errors.go @@ -43,6 +43,10 @@ type ( NoABCIResponsesForHeightError struct { Height int64 } + + NoTxResultForHashError struct { + Hash []byte + } ) func (e UnknownBlockError) Error() string { @@ -76,3 +80,7 @@ func (e NoConsensusParamsForHeightError) Error() string { func (e NoABCIResponsesForHeightError) Error() string { return fmt.Sprintf("Could not find results for height #%d", e.Height) } + +func (e NoTxResultForHashError) Error() string { + return fmt.Sprintf("Could not find tx result for hash #%X", e.Hash) +} diff --git a/tm2/pkg/bft/state/execution.go b/tm2/pkg/bft/state/execution.go index e7920459172..544517c1f96 100644 --- a/tm2/pkg/bft/state/execution.go +++ b/tm2/pkg/bft/state/execution.go @@ -17,7 +17,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/log" ) -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // BlockExecutor handles block execution and state updates. // It exposes ApplyBlock(), which validates & executes the block, updates state w/ ABCI responses, // then commits and updates the mempool atomically, then saves state. @@ -109,6 +109,18 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b // Save the results before we commit. saveABCIResponses(blockExec.db, block.Height, abciResponses) + // Save the transaction results + for index, tx := range block.Txs { + txResult := &types.TxResult{ + Height: block.Height, + Index: uint32(index), + Tx: tx, + Response: abciResponses.DeliverTxs[index], + } + + saveTxResult(blockExec.db, txResult) + } + fail.Fail() // XXX // validate the validator updates and convert to tendermint types @@ -200,7 +212,7 @@ func (blockExec *BlockExecutor) Commit( return res.Data, err } -//--------------------------------------------------------- +// --------------------------------------------------------- // Helper functions for executing blocks and updating state // Executes block's transactions on proxyAppConn. @@ -425,7 +437,7 @@ func fireEvents(logger log.Logger, evsw events.EventSwitch, block *types.Block, } } -//---------------------------------------------------------------------------------------------------- +// ---------------------------------------------------------------------------------------------------- // Execute block without state. TODO: eliminate // ExecCommitBlock executes and commits a block on the proxyApp without validating or mutating the state. diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 0f54ec547d2..044f123398a 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -33,6 +33,10 @@ func calcABCIResponsesKey(height int64) []byte { return []byte(fmt.Sprintf("abciResponsesKey:%v", height)) } +func calcTxResultKey(hash []byte) []byte { + return []byte(fmt.Sprintf("txResultKey:%v", hash)) +} + // LoadStateFromDBOrGenesisFile loads the most recent state from the database, // or creates a new one from the given genesisFilePath and persists the result // to the database. @@ -172,6 +176,29 @@ func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes()) } +// LoadTxResult loads the tx result associated with the given +// tx hash from the database, if any +func LoadTxResult(db dbm.DB, txHash []byte) (*types.TxResult, error) { + buf := db.Get(calcTxResultKey(txHash)) + if buf == nil { + return nil, NoTxResultForHashError{txHash} + } + + txResult := new(types.TxResult) + if err := amino.Unmarshal(buf, txResult); err != nil { + // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED + osm.Exit(fmt.Sprintf(`LoadTxResult: Data has been corrupted or its spec has + changed: %v\n`, err)) + } + + return txResult, nil +} + +// saveTxResult persists the transaction result to the database +func saveTxResult(db dbm.DB, txResult *types.TxResult) { + db.SetSync(calcTxResultKey(txResult.Tx.Hash()), txResult.Bytes()) +} + // ----------------------------------------------------------------------------- // ValidatorsInfo represents the latest validator set, or the last height it changed diff --git a/tm2/pkg/bft/state/store_test.go b/tm2/pkg/bft/state/store_test.go index ed3b8e63311..f33206ae3f8 100644 --- a/tm2/pkg/bft/state/store_test.go +++ b/tm2/pkg/bft/state/store_test.go @@ -5,10 +5,10 @@ import ( "os" "testing" + cfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - cfg "github.com/gnolang/gno/tm2/pkg/bft/config" sm "github.com/gnolang/gno/tm2/pkg/bft/state" "github.com/gnolang/gno/tm2/pkg/bft/types" dbm "github.com/gnolang/gno/tm2/pkg/db" diff --git a/tm2/pkg/bft/state/tx_result_test.go b/tm2/pkg/bft/state/tx_result_test.go new file mode 100644 index 00000000000..16e85e3a126 --- /dev/null +++ b/tm2/pkg/bft/state/tx_result_test.go @@ -0,0 +1,60 @@ +package state + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + dbm "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func generateTxResults(t *testing.T, count int) []*types.TxResult { + t.Helper() + + results := make([]*types.TxResult, count) + + for i := 0; i < count; i++ { + tx := &std.Tx{ + Memo: fmt.Sprintf("tx %d", i), + } + + marshalledTx, err := amino.Marshal(tx) + require.NoError(t, err) + + results[i] = &types.TxResult{ + Height: 10, + Index: uint32(i), + Tx: marshalledTx, + Response: abci.ResponseDeliverTx{}, + } + } + + return results +} + +func TestStoreLoadTxResult(t *testing.T) { + t.Parallel() + + var ( + stateDB = dbm.NewMemDB() + txResults = generateTxResults(t, 100) + ) + + // Save the results + for _, txResult := range txResults { + saveTxResult(stateDB, txResult) + } + + // Verify they are saved correctly + for _, txResult := range txResults { + dbResult, err := LoadTxResult(stateDB, txResult.Tx.Hash()) + require.NoError(t, err) + + assert.Equal(t, txResult, dbResult) + } +} diff --git a/tm2/pkg/bft/types/tx.go b/tm2/pkg/bft/types/tx.go index 0b3a7dc5503..318491f21e9 100644 --- a/tm2/pkg/bft/types/tx.go +++ b/tm2/pkg/bft/types/tx.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/crypto/merkle" "github.com/gnolang/gno/tm2/pkg/crypto/tmhash" @@ -116,3 +117,7 @@ type TxResult struct { Tx Tx `json:"tx"` Response abci.ResponseDeliverTx `json:"response"` } + +func (tx *TxResult) Bytes() []byte { + return amino.MustMarshal(tx) +} From 27d9f31a68e71bf4c8d77a82f9a77d903aa86cea Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jan 2024 16:19:01 +0100 Subject: [PATCH 2/9] Add tx result fetch endpoint --- tm2/pkg/bft/rpc/core/routes.go | 19 ++- tm2/pkg/bft/rpc/core/tx.go | 231 ++------------------------------- 2 files changed, 20 insertions(+), 230 deletions(-) diff --git a/tm2/pkg/bft/rpc/core/routes.go b/tm2/pkg/bft/rpc/core/routes.go index 127d9367f95..8d210f67985 100644 --- a/tm2/pkg/bft/rpc/core/routes.go +++ b/tm2/pkg/bft/rpc/core/routes.go @@ -8,16 +8,15 @@ import ( // NOTE: Amino is registered in rpc/core/types/codec.go. var Routes = map[string]*rpc.RPCFunc{ // info API - "health": rpc.NewRPCFunc(Health, ""), - "status": rpc.NewRPCFunc(Status, ""), - "net_info": rpc.NewRPCFunc(NetInfo, ""), - "blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"), - "genesis": rpc.NewRPCFunc(Genesis, ""), - "block": rpc.NewRPCFunc(Block, "height"), - "block_results": rpc.NewRPCFunc(BlockResults, "height"), - "commit": rpc.NewRPCFunc(Commit, "height"), - //"tx": rpc.NewRPCFunc(Tx, "hash,prove"), - //"tx_search": rpc.NewRPCFunc(TxSearch, "query,prove,page,per_page"), + "health": rpc.NewRPCFunc(Health, ""), + "status": rpc.NewRPCFunc(Status, ""), + "net_info": rpc.NewRPCFunc(NetInfo, ""), + "blockchain": rpc.NewRPCFunc(BlockchainInfo, "minHeight,maxHeight"), + "genesis": rpc.NewRPCFunc(Genesis, ""), + "block": rpc.NewRPCFunc(Block, "height"), + "block_results": rpc.NewRPCFunc(BlockResults, "height"), + "commit": rpc.NewRPCFunc(Commit, "height"), + "tx": rpc.NewRPCFunc(Tx, "hash"), "validators": rpc.NewRPCFunc(Validators, "height"), "dump_consensus_state": rpc.NewRPCFunc(DumpConsensusState, ""), "consensus_state": rpc.NewRPCFunc(ConsensusState, ""), diff --git a/tm2/pkg/bft/rpc/core/tx.go b/tm2/pkg/bft/rpc/core/tx.go index d471e4fa52a..cdffe43035e 100644 --- a/tm2/pkg/bft/rpc/core/tx.go +++ b/tm2/pkg/bft/rpc/core/tx.go @@ -1,238 +1,29 @@ package core -/* - import ( "fmt" - cmn "github.com/gnolang/gno/tm2/pkg/common" - - tmquery "github.com/gnolang/gno/tm2/pkg/pubsub/query" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/bft/state/txindex/null" - "github.com/gnolang/gno/tm2/pkg/bft/types" + sm "github.com/gnolang/gno/tm2/pkg/bft/state" ) // Tx allows you to query the transaction results. `nil` could mean the // transaction is in the mempool, invalidated, or was not sent in the first -// place. -// -// ```shell -// curl "localhost:26657/tx?hash=0xF87370F68C82D9AC7201248ECA48CEC5F16FFEC99C461C1B2961341A2FE9C1C8" -// ``` -// -// ```go -// client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") -// err := client.Start() -// if err != nil { -// // handle error -// } -// defer client.Stop() -// hashBytes, err := hex.DecodeString("F87370F68C82D9AC7201248ECA48CEC5F16FFEC99C461C1B2961341A2FE9C1C8") -// tx, err := client.Tx(hashBytes, true) -// ``` -// -// > The above command returns JSON structured like this: -// -// ```json -// { -// "error": "", -// "result": { -// "proof": { -// "Proof": { -// "aunts": [] -// }, -// "Data": "YWJjZA==", -// "RootHash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF", -// "Total": "1", -// "Index": "0" -// }, -// "tx": "YWJjZA==", -// "tx_result": { -// "log": "", -// "data": "", -// "code": "0" -// }, -// "index": "0", -// "height": "52", -// "hash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF" -// }, -// "id": "", -// "jsonrpc": "2.0" -// } -// ``` -// -// Returns a transaction matching the given transaction hash. -// -// ### Query Parameters -// -// | Parameter | Type | Default | Required | Description | -// |-----------+--------+---------+----------+-----------------------------------------------------------| -// | hash | []byte | nil | true | The transaction hash | -// | prove | bool | false | false | Include a proof of the transaction inclusion in the block | -// -// ### Returns -// -// - `proof`: the `types.TxProof` object -// - `tx`: `[]byte` - the transaction -// - `tx_result`: the `abci.Result` object -// - `index`: `int` - index of the transaction -// - `height`: `int` - height of the block where this transaction was in -// - `hash`: `[]byte` - hash of the transaction -func Tx(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { - - // if index is disabled, return error - if _, ok := txIndexer.(*null.TxIndex); ok { - return nil, fmt.Errorf("Transaction indexing is disabled") - } - - r, err := txIndexer.Get(hash) +// place +func Tx(_ *rpctypes.Context, hash []byte) (*ctypes.ResultTx, error) { + // Get the result from storage, if any + result, err := sm.LoadTxResult(stateDB, hash) if err != nil { - return nil, err - } - - if r == nil { - return nil, fmt.Errorf("Tx (%X) not found", hash) - } - - height := r.Height - index := r.Index - - var proof types.TxProof - if prove { - block := blockStore.LoadBlock(height) - proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines + return nil, fmt.Errorf("tx (%X) not found", hash) } + // Return the response return &ctypes.ResultTx{ Hash: hash, - Height: height, - Index: index, - TxResult: r.Result, - Tx: r.Tx, - Proof: proof, + Height: result.Height, + Index: result.Index, + TxResult: result.Response, + Tx: result.Tx, }, nil } - -// TxSearch allows you to query for multiple transactions results. It returns a -// list of transactions (maximum ?per_page entries) and the total count. -// -// ```shell -// curl "localhost:26657/tx_search?query=\"account.owner='Ivan'\"&prove=true" -// ``` -// -// ```go -// client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") -// err := client.Start() -// if err != nil { -// // handle error -// } -// defer client.Stop() -// q, err := tmquery.New("account.owner='Ivan'") -// tx, err := client.TxSearch(q, true) -// ``` -// -// > The above command returns JSON structured like this: -// -// ```json -// { -// "jsonrpc": "2.0", -// "id": "", -// "result": { -// "txs": [ -// { -// "proof": { -// "Proof": { -// "aunts": [ -// "J3LHbizt806uKnABNLwG4l7gXCA=", -// "iblMO/M1TnNtlAefJyNCeVhjAb0=", -// "iVk3ryurVaEEhdeS0ohAJZ3wtB8=", -// "5hqMkTeGqpct51ohX0lZLIdsn7Q=", -// "afhsNxFnLlZgFDoyPpdQSe0bR8g=" -// ] -// }, -// "Data": "mvZHHa7HhZ4aRT0xMDA=", -// "RootHash": "F6541223AA46E428CB1070E9840D2C3DF3B6D776", -// "Total": "32", -// "Index": "31" -// }, -// "tx": "mvZHHa7HhZ4aRT0xMDA=", -// "tx_result": {}, -// "index": "31", -// "height": "12", -// "hash": "2B8EC32BA2579B3B8606E42C06DE2F7AFA2556EF" -// } -// ], -// "total_count": "1" -// } -// } -// ``` -// -// ### Query Parameters -// -// | Parameter | Type | Default | Required | Description | -// |-----------+--------+---------+----------+-----------------------------------------------------------| -// | query | string | "" | true | Query | -// | prove | bool | false | false | Include proofs of the transactions inclusion in the block | -// | page | int | 1 | false | Page number (1-based) | -// | per_page | int | 30 | false | Number of entries per page (max: 100) | -// -// ### Returns -// -// - `proof`: the `types.TxProof` object -// - `tx`: `[]byte` - the transaction -// - `tx_result`: the `abci.Result` object -// - `index`: `int` - index of the transaction -// - `height`: `int` - height of the block where this transaction was in -// - `hash`: `[]byte` - hash of the transaction -func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { - // if index is disabled, return error - if _, ok := txIndexer.(*null.TxIndex); ok { - return nil, fmt.Errorf("Transaction indexing is disabled") - } - - q, err := tmquery.New(query) - if err != nil { - return nil, err - } - - results, err := txIndexer.Search(q) - if err != nil { - return nil, err - } - - totalCount := len(results) - perPage = validatePerPage(perPage) - page, err = validatePage(page, perPage, totalCount) - if err != nil { - return nil, err - } - skipCount := validateSkipCount(page, perPage) - - apiResults := make([]*ctypes.ResultTx, cmn.MinInt(perPage, totalCount-skipCount)) - var proof types.TxProof - // if there's no tx in the results array, we don't need to loop through the apiResults array - for i := 0; i < len(apiResults); i++ { - r := results[skipCount+i] - height := r.Height - index := r.Index - - if prove { - block := blockStore.LoadBlock(height) - proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines - } - - apiResults[i] = &ctypes.ResultTx{ - Hash: r.Tx.Hash(), - Height: height, - Index: index, - TxResult: r.Result, - Tx: r.Tx, - Proof: proof, - } - } - - return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil -} -*/ From 20cacdaaa072c23e98fc9252591c981df9e30a84 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jan 2024 13:21:11 +0100 Subject: [PATCH 3/9] Add additional unit tests for storage loading --- tm2/pkg/bft/state/store.go | 7 +-- tm2/pkg/bft/state/tx_result_test.go | 71 +++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 044f123398a..a1519e7d5a9 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -1,6 +1,7 @@ package state import ( + "errors" "fmt" "github.com/gnolang/gno/tm2/pkg/amino" @@ -19,6 +20,8 @@ const ( valSetCheckpointInterval = 100000 ) +var errTxResultCorrupted = errors.New("tx result corrupted") + // ------------------------------------------------------------------------ func calcValidatorsKey(height int64) []byte { @@ -186,9 +189,7 @@ func LoadTxResult(db dbm.DB, txHash []byte) (*types.TxResult, error) { txResult := new(types.TxResult) if err := amino.Unmarshal(buf, txResult); err != nil { - // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - osm.Exit(fmt.Sprintf(`LoadTxResult: Data has been corrupted or its spec has - changed: %v\n`, err)) + return nil, fmt.Errorf("%w, %w", errTxResultCorrupted, err) } return txResult, nil diff --git a/tm2/pkg/bft/state/tx_result_test.go b/tm2/pkg/bft/state/tx_result_test.go index 16e85e3a126..cd96c78e86f 100644 --- a/tm2/pkg/bft/state/tx_result_test.go +++ b/tm2/pkg/bft/state/tx_result_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" ) +// generateTxResults generates test transaction results func generateTxResults(t *testing.T, count int) []*types.TxResult { t.Helper() @@ -40,21 +41,63 @@ func generateTxResults(t *testing.T, count int) []*types.TxResult { func TestStoreLoadTxResult(t *testing.T) { t.Parallel() - var ( - stateDB = dbm.NewMemDB() - txResults = generateTxResults(t, 100) - ) + t.Run("results found", func(t *testing.T) { + t.Parallel() - // Save the results - for _, txResult := range txResults { - saveTxResult(stateDB, txResult) - } + var ( + stateDB = dbm.NewMemDB() + txResults = generateTxResults(t, 100) + ) - // Verify they are saved correctly - for _, txResult := range txResults { - dbResult, err := LoadTxResult(stateDB, txResult.Tx.Hash()) - require.NoError(t, err) + // Save the results + for _, txResult := range txResults { + saveTxResult(stateDB, txResult) + } - assert.Equal(t, txResult, dbResult) - } + // Verify they are saved correctly + for _, txResult := range txResults { + dbResult, err := LoadTxResult(stateDB, txResult.Tx.Hash()) + require.NoError(t, err) + + assert.Equal(t, txResult, dbResult) + } + }) + + t.Run("results not found", func(t *testing.T) { + t.Parallel() + + var ( + stateDB = dbm.NewMemDB() + txResults = generateTxResults(t, 10) + ) + + // Verify they are not present + for _, txResult := range txResults { + _, err := LoadTxResult(stateDB, txResult.Tx.Hash()) + + expectedErr := NoTxResultForHashError{ + Hash: txResult.Tx.Hash(), + } + + require.ErrorContains(t, err, expectedErr.Error()) + } + }) + + t.Run("results corrupted", func(t *testing.T) { + t.Parallel() + + var ( + stateDB = dbm.NewMemDB() + corruptedResult = "totally valid amino" + hash = []byte("tx hash") + ) + + // Save the "corrupted" result to the DB + stateDB.SetSync(calcTxResultKey(hash), []byte(corruptedResult)) + + txResult, err := LoadTxResult(stateDB, hash) + require.Nil(t, txResult) + + assert.ErrorIs(t, err, errTxResultCorrupted) + }) } From dc9f1e551337eedcf59238a9c2eaee5e41ad6bad Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jan 2024 13:46:26 +0100 Subject: [PATCH 4/9] Add unit tests for RPC handler --- tm2/pkg/bft/rpc/core/tx.go | 4 +- tm2/pkg/bft/rpc/core/tx_test.go | 75 +++++++++++++++++++++++++++++ tm2/pkg/bft/state/store.go | 7 +-- tm2/pkg/bft/state/tx_result_test.go | 2 +- 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 tm2/pkg/bft/rpc/core/tx_test.go diff --git a/tm2/pkg/bft/rpc/core/tx.go b/tm2/pkg/bft/rpc/core/tx.go index cdffe43035e..2f490a82bbd 100644 --- a/tm2/pkg/bft/rpc/core/tx.go +++ b/tm2/pkg/bft/rpc/core/tx.go @@ -1,8 +1,6 @@ package core import ( - "fmt" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" sm "github.com/gnolang/gno/tm2/pkg/bft/state" @@ -15,7 +13,7 @@ func Tx(_ *rpctypes.Context, hash []byte) (*ctypes.ResultTx, error) { // Get the result from storage, if any result, err := sm.LoadTxResult(stateDB, hash) if err != nil { - return nil, fmt.Errorf("tx (%X) not found", hash) + return nil, err } // Return the response diff --git a/tm2/pkg/bft/rpc/core/tx_test.go b/tm2/pkg/bft/rpc/core/tx_test.go new file mode 100644 index 00000000000..fb16d25f0a8 --- /dev/null +++ b/tm2/pkg/bft/rpc/core/tx_test.go @@ -0,0 +1,75 @@ +package core + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/state" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTxHandler(t *testing.T) { + // Tests are not run in parallel because the JSON-RPC + // handlers utilize global package-level variables, + // that are not friendly with concurrent test runs (or anything, really) + t.Run("result found", func(t *testing.T) { + // Prepare the transaction + tx := &std.Tx{ + Memo: "example tx", + } + + marshalledTx, err := amino.Marshal(tx) + require.NoError(t, err) + + res := &types.TxResult{ + Height: 1, + Index: 0, + Tx: marshalledTx, + Response: abci.ResponseDeliverTx{}, + } + + // Prepare the DB + sdb := db.NewMemDB() + sdb.Set(state.CalcTxResultKey(res.Tx.Hash()), res.Bytes()) + + // Set the GLOBALLY referenced db + SetStateDB(sdb) + + // Load the result + loadedTxResult, err := Tx(nil, res.Tx.Hash()) + + require.NoError(t, err) + require.NotNil(t, loadedTxResult) + + // Compare the result + assert.Equal(t, res.Height, loadedTxResult.Height) + assert.Equal(t, res.Index, loadedTxResult.Index) + assert.Equal(t, res.Response, loadedTxResult.TxResult) + assert.Equal(t, res.Tx, loadedTxResult.Tx) + assert.Equal(t, res.Tx.Hash(), loadedTxResult.Tx.Hash()) + }) + + t.Run("result not found", func(t *testing.T) { + var ( + sdb = db.NewMemDB() + hash = []byte("hash") + expectedErr = state.NoTxResultForHashError{ + Hash: hash, + } + ) + + // Set the GLOBALLY referenced db + SetStateDB(sdb) + + // Load the result + loadedTxResult, err := Tx(nil, hash) + require.Nil(t, loadedTxResult) + + assert.Equal(t, expectedErr, err) + }) +} diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index a1519e7d5a9..cf5c0707c05 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -36,7 +36,8 @@ func calcABCIResponsesKey(height int64) []byte { return []byte(fmt.Sprintf("abciResponsesKey:%v", height)) } -func calcTxResultKey(hash []byte) []byte { +// CalcTxResultKey calculates the storage key for the transaction result +func CalcTxResultKey(hash []byte) []byte { return []byte(fmt.Sprintf("txResultKey:%v", hash)) } @@ -182,7 +183,7 @@ func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { // LoadTxResult loads the tx result associated with the given // tx hash from the database, if any func LoadTxResult(db dbm.DB, txHash []byte) (*types.TxResult, error) { - buf := db.Get(calcTxResultKey(txHash)) + buf := db.Get(CalcTxResultKey(txHash)) if buf == nil { return nil, NoTxResultForHashError{txHash} } @@ -197,7 +198,7 @@ func LoadTxResult(db dbm.DB, txHash []byte) (*types.TxResult, error) { // saveTxResult persists the transaction result to the database func saveTxResult(db dbm.DB, txResult *types.TxResult) { - db.SetSync(calcTxResultKey(txResult.Tx.Hash()), txResult.Bytes()) + db.SetSync(CalcTxResultKey(txResult.Tx.Hash()), txResult.Bytes()) } // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/state/tx_result_test.go b/tm2/pkg/bft/state/tx_result_test.go index cd96c78e86f..e5b4d0163e4 100644 --- a/tm2/pkg/bft/state/tx_result_test.go +++ b/tm2/pkg/bft/state/tx_result_test.go @@ -93,7 +93,7 @@ func TestStoreLoadTxResult(t *testing.T) { ) // Save the "corrupted" result to the DB - stateDB.SetSync(calcTxResultKey(hash), []byte(corruptedResult)) + stateDB.SetSync(CalcTxResultKey(hash), []byte(corruptedResult)) txResult, err := LoadTxResult(stateDB, hash) require.Nil(t, txResult) From 13e66e191bdd1a26e0b07b28d174101877a57040 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jan 2024 14:00:19 +0100 Subject: [PATCH 5/9] Add client support for Tx result fetching --- tm2/pkg/bft/rpc/client/httpclient.go | 20 ++--------------- tm2/pkg/bft/rpc/client/interface.go | 6 +++++- tm2/pkg/bft/rpc/client/localclient.go | 10 ++------- tm2/pkg/bft/rpc/client/mock/client.go | 1 + tm2/pkg/bft/rpc/client/rpc_test.go | 31 +++++++++------------------ 5 files changed, 20 insertions(+), 48 deletions(-) diff --git a/tm2/pkg/bft/rpc/client/httpclient.go b/tm2/pkg/bft/rpc/client/httpclient.go index c7295e327ed..51d2e1c3fca 100644 --- a/tm2/pkg/bft/rpc/client/httpclient.go +++ b/tm2/pkg/bft/rpc/client/httpclient.go @@ -307,11 +307,10 @@ func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { return result, nil } -func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { +func (c *baseRPCClient) Tx(hash []byte) (*ctypes.ResultTx, error) { result := new(ctypes.ResultTx) params := map[string]interface{}{ - "hash": hash, - "prove": prove, + "hash": hash, } _, err := c.caller.Call("tx", params, result) if err != nil { @@ -320,21 +319,6 @@ func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { return result, nil } -func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { - result := new(ctypes.ResultTxSearch) - params := map[string]interface{}{ - "query": query, - "prove": prove, - "page": page, - "per_page": perPage, - } - _, err := c.caller.Call("tx_search", params, result) - if err != nil { - return nil, errors.Wrap(err, "TxSearch") - } - return result, nil -} - func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) { result := new(ctypes.ResultValidators) params := map[string]interface{}{} diff --git a/tm2/pkg/bft/rpc/client/interface.go b/tm2/pkg/bft/rpc/client/interface.go index a24e95f94bd..a8f42ddc955 100644 --- a/tm2/pkg/bft/rpc/client/interface.go +++ b/tm2/pkg/bft/rpc/client/interface.go @@ -32,13 +32,13 @@ import ( // first synchronously consumes the events from the node's synchronous event // switch, or reads logged events from the filesystem. type Client interface { - // service.Service ABCIClient HistoryClient NetworkClient SignClient StatusClient MempoolClient + TxClient } // ABCIClient groups together the functionality that principally affects the @@ -94,3 +94,7 @@ type MempoolClient interface { UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) } + +type TxClient interface { + Tx(hash []byte) (*ctypes.ResultTx, error) +} diff --git a/tm2/pkg/bft/rpc/client/localclient.go b/tm2/pkg/bft/rpc/client/localclient.go index 20209f74071..fc164b680f8 100644 --- a/tm2/pkg/bft/rpc/client/localclient.go +++ b/tm2/pkg/bft/rpc/client/localclient.go @@ -137,12 +137,6 @@ func (c *Local) Validators(height *int64) (*ctypes.ResultValidators, error) { return core.Validators(c.ctx, height) } -/* -func (c *Local) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { - return core.Tx(c.ctx, hash, prove) -} - -func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { - return core.TxSearch(c.ctx, query, prove, page, perPage) +func (c *Local) Tx(hash []byte) (*ctypes.ResultTx, error) { + return core.Tx(c.ctx, hash) } -*/ diff --git a/tm2/pkg/bft/rpc/client/mock/client.go b/tm2/pkg/bft/rpc/client/mock/client.go index 46db69debb3..5dc048fa5ff 100644 --- a/tm2/pkg/bft/rpc/client/mock/client.go +++ b/tm2/pkg/bft/rpc/client/mock/client.go @@ -36,6 +36,7 @@ type Client struct { client.HistoryClient client.StatusClient client.MempoolClient + client.TxClient service.Service } diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go index e09ae8d4466..18eb3338a5c 100644 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ b/tm2/pkg/bft/rpc/client/rpc_test.go @@ -366,10 +366,7 @@ func TestNumUnconfirmedTxs(t *testing.T) { mempool.Flush() } -/* func TestTx(t *testing.T) { - t.Parallel() - // first we broadcast a tx c := getHTTPClient() _, _, tx := MakeTxKV() @@ -384,24 +381,21 @@ func TestTx(t *testing.T) { cases := []struct { valid bool hash []byte - prove bool }{ // only valid if correct hash provided - {true, txHash, false}, - {true, txHash, true}, - {false, anotherTxHash, false}, - {false, anotherTxHash, true}, - {false, nil, false}, - {false, nil, true}, + {true, txHash}, + {true, txHash}, + {false, anotherTxHash}, + {false, anotherTxHash}, + {false, nil}, + {false, nil}, } - for i, c := range GetClients() { - for j, tc := range cases { - t.Logf("client %d, case %d", i, j) - + for _, c := range GetClients() { + for _, tc := range cases { // now we query for the tx. // since there's only one tx, we know index=0. - ptx, err := c.Tx(tc.hash, tc.prove) + ptx, err := c.Tx(tc.hash) if !tc.valid { require.NotNil(t, err) @@ -412,17 +406,12 @@ func TestTx(t *testing.T) { assert.Zero(t, ptx.Index) assert.True(t, ptx.TxResult.IsOK()) assert.EqualValues(t, txHash, ptx.Hash) - - // time to verify the proof - proof := ptx.Proof - if tc.prove && assert.EqualValues(t, tx, proof.Data) { - assert.NoError(t, proof.Proof.Verify(proof.RootHash, txHash)) - } } } } } +/* func TestTxSearch(t *testing.T) { t.Parallel() From 382e65c9a644d01a50f7e7b8e5f80bfda8d71a74 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 24 Jan 2024 14:51:16 +0100 Subject: [PATCH 6/9] Clean up legacy test --- tm2/pkg/bft/rpc/client/rpc_test.go | 125 ++++++++--------------------- 1 file changed, 34 insertions(+), 91 deletions(-) diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go index 18eb3338a5c..07816a001c0 100644 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ b/tm2/pkg/bft/rpc/client/rpc_test.go @@ -367,118 +367,61 @@ func TestNumUnconfirmedTxs(t *testing.T) { } func TestTx(t *testing.T) { - // first we broadcast a tx + // Prepare the transaction + // by broadcasting it to the chain c := getHTTPClient() _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err, "%+v", err) - txHeight := bres.Height - txHash := bres.Hash + response, err := c.BroadcastTxCommit(tx) + require.NoError(t, err) + require.NotNil(t, response) - anotherTxHash := types.Tx("a different tx").Hash() + var ( + txHeight = response.Height + txHash = response.Hash + ) cases := []struct { + name string valid bool hash []byte }{ - // only valid if correct hash provided - {true, txHash}, - {true, txHash}, - {false, anotherTxHash}, - {false, anotherTxHash}, - {false, nil}, - {false, nil}, + { + "tx result found", + true, + txHash, + }, + { + "tx result not found", + false, + types.Tx("a different tx").Hash(), + }, } for _, c := range GetClients() { for _, tc := range cases { - // now we query for the tx. - // since there's only one tx, we know index=0. - ptx, err := c.Tx(tc.hash) - - if !tc.valid { - require.NotNil(t, err) - } else { - require.Nil(t, err, "%+v", err) + t.Run(tc.name, func(t *testing.T) { + // now we query for the tx. + // since there's only one tx, we know index=0. + ptx, err := c.Tx(tc.hash) + + if !tc.valid { + require.Error(t, err) + + return + } + + require.NoError(t, err) + assert.EqualValues(t, txHeight, ptx.Height) assert.EqualValues(t, tx, ptx.Tx) assert.Zero(t, ptx.Index) assert.True(t, ptx.TxResult.IsOK()) assert.EqualValues(t, txHash, ptx.Hash) - } - } - } -} - -/* -func TestTxSearch(t *testing.T) { - t.Parallel() - - // first we broadcast a tx - c := getHTTPClient() - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err, "%+v", err) - - txHeight := bres.Height - txHash := bres.Hash - - anotherTxHash := types.Tx("a different tx").Hash() - - for i, c := range GetClients() { - t.Logf("client %d", i) - - // now we query for the tx. - // since there's only one tx, we know index=0. - result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", txHash), true, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 1) - - ptx := result.Txs[0] - assert.EqualValues(t, txHeight, ptx.Height) - assert.EqualValues(t, tx, ptx.Tx) - assert.Zero(t, ptx.Index) - assert.True(t, ptx.TxResult.IsOK()) - assert.EqualValues(t, txHash, ptx.Hash) - - // time to verify the proof - proof := ptx.Proof - if assert.EqualValues(t, tx, proof.Data) { - assert.NoError(t, proof.Proof.Verify(proof.RootHash, txHash)) - } - - // query by height - result, err = c.TxSearch(fmt.Sprintf("tx.height=%d", txHeight), true, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 1) - - // query for non existing tx - result, err = c.TxSearch(fmt.Sprintf("tx.hash='%X'", anotherTxHash), false, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 0) - - // query using a tag (see kvstore application) - result, err = c.TxSearch("app.creator='Cosmoshi Netowoko'", false, 1, 30) - require.Nil(t, err, "%+v", err) - if len(result.Txs) == 0 { - t.Fatal("expected a lot of transactions") + }) } - - // query using a tag (see kvstore application) and height - result, err = c.TxSearch("app.creator='Cosmoshi Netowoko' AND tx.height<10000", true, 1, 30) - require.Nil(t, err, "%+v", err) - if len(result.Txs) == 0 { - t.Fatal("expected a lot of transactions") - } - - // query a non existing tx with page 1 and txsPerPage 1 - result, err = c.TxSearch("app.creator='Cosmoshi Neetowoko'", true, 1, 1) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 0) } } -*/ func TestBatchedJSONRPCCalls(t *testing.T) { c := getHTTPClient() From 7f6a025850886f95edc4043de42f3256e32a20e2 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:58:19 +0200 Subject: [PATCH 7/9] Swap DB key format --- tm2/pkg/bft/state/store.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 378bf41a3ed..7f31dd4aa07 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -24,20 +24,20 @@ var errTxResultCorrupted = errors.New("tx result corrupted") // ------------------------------------------------------------------------ func calcValidatorsKey(height int64) []byte { - return []byte(fmt.Sprintf("validatorsKey:%v", height)) + return []byte(fmt.Sprintf("validatorsKey:%x", height)) } func calcConsensusParamsKey(height int64) []byte { - return []byte(fmt.Sprintf("consensusParamsKey:%v", height)) + return []byte(fmt.Sprintf("consensusParamsKey:%x", height)) } func calcABCIResponsesKey(height int64) []byte { - return []byte(fmt.Sprintf("abciResponsesKey:%v", height)) + return []byte(fmt.Sprintf("abciResponsesKey:%x", height)) } // CalcTxResultKey calculates the storage key for the transaction result func CalcTxResultKey(hash []byte) []byte { - return []byte(fmt.Sprintf("txResultKey:%v", hash)) + return []byte(fmt.Sprintf("txResultKey:%x", hash)) } // LoadStateFromDBOrGenesisFile loads the most recent state from the database, From c627995589676bb95a7a2a3b6f341a25a72c02cc Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 12:04:31 +0200 Subject: [PATCH 8/9] Use Set instead of SetSync --- gno.land/pkg/gnoclient/mock_test.go | 10 ++++++++++ tm2/pkg/bft/rpc/core/tx_test.go | 6 +++--- tm2/pkg/bft/state/store.go | 2 +- tm2/pkg/bft/state/tx_result_test.go | 8 ++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/gno.land/pkg/gnoclient/mock_test.go b/gno.land/pkg/gnoclient/mock_test.go index 4a12dfd2d88..385eeb0916e 100644 --- a/gno.land/pkg/gnoclient/mock_test.go +++ b/gno.land/pkg/gnoclient/mock_test.go @@ -118,6 +118,7 @@ type ( mockStatus func() (*ctypes.ResultStatus, error) mockUnconfirmedTxs func(limit int) (*ctypes.ResultUnconfirmedTxs, error) mockNumUnconfirmedTxs func() (*ctypes.ResultUnconfirmedTxs, error) + mockTx func(hash []byte) (*ctypes.ResultTx, error) ) type mockRPCClient struct { @@ -141,6 +142,7 @@ type mockRPCClient struct { status mockStatus unconfirmedTxs mockUnconfirmedTxs numUnconfirmedTxs mockNumUnconfirmedTxs + tx mockTx } func (m *mockRPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { @@ -282,3 +284,11 @@ func (m *mockRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error } return nil, nil } + +func (m *mockRPCClient) Tx(hash []byte) (*ctypes.ResultTx, error) { + if m.tx != nil { + return m.tx(hash) + } + + return nil, nil +} diff --git a/tm2/pkg/bft/rpc/core/tx_test.go b/tm2/pkg/bft/rpc/core/tx_test.go index fb16d25f0a8..1cc2519ec92 100644 --- a/tm2/pkg/bft/rpc/core/tx_test.go +++ b/tm2/pkg/bft/rpc/core/tx_test.go @@ -7,7 +7,7 @@ import ( abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/state" "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,7 +34,7 @@ func TestTxHandler(t *testing.T) { } // Prepare the DB - sdb := db.NewMemDB() + sdb := memdb.NewMemDB() sdb.Set(state.CalcTxResultKey(res.Tx.Hash()), res.Bytes()) // Set the GLOBALLY referenced db @@ -56,7 +56,7 @@ func TestTxHandler(t *testing.T) { t.Run("result not found", func(t *testing.T) { var ( - sdb = db.NewMemDB() + sdb = memdb.NewMemDB() hash = []byte("hash") expectedErr = state.NoTxResultForHashError{ Hash: hash, diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 7f31dd4aa07..668331145cb 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -176,7 +176,7 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { // This is useful in case we crash after app.Commit and before s.Save(). // Responses are indexed by height so they can also be loaded later to produce Merkle proofs. func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { - db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes()) + db.Set(calcABCIResponsesKey(height), abciResponses.Bytes()) } // LoadTxResult loads the tx result associated with the given diff --git a/tm2/pkg/bft/state/tx_result_test.go b/tm2/pkg/bft/state/tx_result_test.go index e5b4d0163e4..83b3172ef3f 100644 --- a/tm2/pkg/bft/state/tx_result_test.go +++ b/tm2/pkg/bft/state/tx_result_test.go @@ -7,7 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/types" - dbm "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -45,7 +45,7 @@ func TestStoreLoadTxResult(t *testing.T) { t.Parallel() var ( - stateDB = dbm.NewMemDB() + stateDB = memdb.NewMemDB() txResults = generateTxResults(t, 100) ) @@ -67,7 +67,7 @@ func TestStoreLoadTxResult(t *testing.T) { t.Parallel() var ( - stateDB = dbm.NewMemDB() + stateDB = memdb.NewMemDB() txResults = generateTxResults(t, 10) ) @@ -87,7 +87,7 @@ func TestStoreLoadTxResult(t *testing.T) { t.Parallel() var ( - stateDB = dbm.NewMemDB() + stateDB = memdb.NewMemDB() corruptedResult = "totally valid amino" hash = []byte("tx hash") ) From 937139934843d835ce3ea02eef57f41f3e363ced Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 19:32:45 +0200 Subject: [PATCH 9/9] Swap what's being indexed for the tx hash --- tm2/pkg/bft/rpc/core/mock_test.go | 78 ++++++++++++ tm2/pkg/bft/rpc/core/tx.go | 53 ++++++-- tm2/pkg/bft/rpc/core/tx_test.go | 187 +++++++++++++++++++++++++--- tm2/pkg/bft/state/execution.go | 16 +-- tm2/pkg/bft/state/store.go | 36 ++++-- tm2/pkg/bft/state/tx_result_test.go | 25 +++- 6 files changed, 341 insertions(+), 54 deletions(-) create mode 100644 tm2/pkg/bft/rpc/core/mock_test.go diff --git a/tm2/pkg/bft/rpc/core/mock_test.go b/tm2/pkg/bft/rpc/core/mock_test.go new file mode 100644 index 00000000000..a6ffe948d00 --- /dev/null +++ b/tm2/pkg/bft/rpc/core/mock_test.go @@ -0,0 +1,78 @@ +package core + +import "github.com/gnolang/gno/tm2/pkg/bft/types" + +type ( + heightDelegate func() int64 + loadBlockMetaDelegate func(int64) *types.BlockMeta + loadBlockDelegate func(int64) *types.Block + loadBlockPartDelegate func(int64, int) *types.Part + loadBlockCommitDelegate func(int64) *types.Commit + loadSeenCommitDelegate func(int64) *types.Commit + + saveBlockDelegate func(*types.Block, *types.PartSet, *types.Commit) +) + +type mockBlockStore struct { + heightFn heightDelegate + loadBlockMetaFn loadBlockMetaDelegate + loadBlockFn loadBlockDelegate + loadBlockPartFn loadBlockPartDelegate + loadBlockCommitFn loadBlockCommitDelegate + loadSeenCommitFn loadSeenCommitDelegate + saveBlockFn saveBlockDelegate +} + +func (m *mockBlockStore) Height() int64 { + if m.heightFn != nil { + return m.heightFn() + } + + return 0 +} + +func (m *mockBlockStore) LoadBlockMeta(height int64) *types.BlockMeta { + if m.loadBlockMetaFn != nil { + return m.loadBlockMetaFn(height) + } + + return nil +} + +func (m *mockBlockStore) LoadBlock(height int64) *types.Block { + if m.loadBlockFn != nil { + return m.loadBlockFn(height) + } + + return nil +} + +func (m *mockBlockStore) LoadBlockPart(height int64, index int) *types.Part { + if m.loadBlockPartFn != nil { + return m.loadBlockPartFn(height, index) + } + + return nil +} + +func (m *mockBlockStore) LoadBlockCommit(height int64) *types.Commit { + if m.loadBlockCommitFn != nil { + return m.loadBlockCommitFn(height) + } + + return nil +} + +func (m *mockBlockStore) LoadSeenCommit(height int64) *types.Commit { + if m.loadSeenCommitFn != nil { + return m.loadSeenCommitFn(height) + } + + return nil +} + +func (m *mockBlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { + if m.saveBlockFn != nil { + m.saveBlockFn(block, blockParts, seenCommit) + } +} diff --git a/tm2/pkg/bft/rpc/core/tx.go b/tm2/pkg/bft/rpc/core/tx.go index 2f490a82bbd..255e33ca499 100644 --- a/tm2/pkg/bft/rpc/core/tx.go +++ b/tm2/pkg/bft/rpc/core/tx.go @@ -1,6 +1,8 @@ package core import ( + "fmt" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" sm "github.com/gnolang/gno/tm2/pkg/bft/state" @@ -10,18 +12,55 @@ import ( // transaction is in the mempool, invalidated, or was not sent in the first // place func Tx(_ *rpctypes.Context, hash []byte) (*ctypes.ResultTx, error) { - // Get the result from storage, if any - result, err := sm.LoadTxResult(stateDB, hash) + // Get the result index from storage, if any + resultIndex, err := sm.LoadTxResultIndex(stateDB, hash) + if err != nil { + return nil, err + } + + // Sanity check the block height + height, err := getHeight(blockStore.Height(), &resultIndex.BlockNum) if err != nil { return nil, err } - // Return the response + // Load the block + block := blockStore.LoadBlock(height) + numTxs := len(block.Txs) + + if int(resultIndex.TxIndex) > numTxs || numTxs == 0 { + return nil, fmt.Errorf( + "unable to get block transaction for block %d, index %d", + resultIndex.BlockNum, + resultIndex.TxIndex, + ) + } + + rawTx := block.Txs[resultIndex.TxIndex] + + // Fetch the block results + blockResults, err := sm.LoadABCIResponses(stateDB, resultIndex.BlockNum) + if err != nil { + return nil, fmt.Errorf("unable to load block results, %w", err) + } + + // Grab the block deliver response + if len(blockResults.DeliverTxs) < int(resultIndex.TxIndex) { + return nil, fmt.Errorf( + "unable to get deliver result for block %d, index %d", + resultIndex.BlockNum, + resultIndex.TxIndex, + ) + } + + deliverResponse := blockResults.DeliverTxs[resultIndex.TxIndex] + + // Craft the response return &ctypes.ResultTx{ Hash: hash, - Height: result.Height, - Index: result.Index, - TxResult: result.Response, - Tx: result.Tx, + Height: resultIndex.BlockNum, + Index: resultIndex.TxIndex, + TxResult: deliverResponse, + Tx: rawTx, }, nil } diff --git a/tm2/pkg/bft/rpc/core/tx_test.go b/tm2/pkg/bft/rpc/core/tx_test.go index 1cc2519ec92..5024980e0d7 100644 --- a/tm2/pkg/bft/rpc/core/tx_test.go +++ b/tm2/pkg/bft/rpc/core/tx_test.go @@ -17,44 +17,81 @@ func TestTxHandler(t *testing.T) { // Tests are not run in parallel because the JSON-RPC // handlers utilize global package-level variables, // that are not friendly with concurrent test runs (or anything, really) - t.Run("result found", func(t *testing.T) { - // Prepare the transaction - tx := &std.Tx{ - Memo: "example tx", - } + t.Run("tx result generated", func(t *testing.T) { + var ( + height = int64(10) + + stdTx = &std.Tx{ + Memo: "example tx", + } + + txResultIndex = state.TxResultIndex{ + BlockNum: height, + TxIndex: 0, + } + + responses = &state.ABCIResponses{ + DeliverTxs: []abci.ResponseDeliverTx{ + { + GasWanted: 100, + }, + }, + } + ) - marshalledTx, err := amino.Marshal(tx) + // Prepare the transaction + marshalledTx, err := amino.Marshal(stdTx) require.NoError(t, err) - res := &types.TxResult{ - Height: 1, - Index: 0, - Tx: marshalledTx, - Response: abci.ResponseDeliverTx{}, - } + tx := types.Tx(marshalledTx) // Prepare the DB sdb := memdb.NewMemDB() - sdb.Set(state.CalcTxResultKey(res.Tx.Hash()), res.Bytes()) + + // Save the result index to the DB + sdb.Set(state.CalcTxResultKey(tx.Hash()), txResultIndex.Bytes()) + + // Save the ABCI response to the DB + sdb.Set(state.CalcABCIResponsesKey(height), responses.Bytes()) // Set the GLOBALLY referenced db SetStateDB(sdb) + // Set the GLOBALLY referenced blockstore + blockStore := &mockBlockStore{ + heightFn: func() int64 { + return height + }, + loadBlockFn: func(h int64) *types.Block { + require.Equal(t, height, h) + + return &types.Block{ + Data: types.Data{ + Txs: []types.Tx{ + tx, + }, + }, + } + }, + } + + SetBlockStore(blockStore) + // Load the result - loadedTxResult, err := Tx(nil, res.Tx.Hash()) + loadedTxResult, err := Tx(nil, tx.Hash()) require.NoError(t, err) require.NotNil(t, loadedTxResult) // Compare the result - assert.Equal(t, res.Height, loadedTxResult.Height) - assert.Equal(t, res.Index, loadedTxResult.Index) - assert.Equal(t, res.Response, loadedTxResult.TxResult) - assert.Equal(t, res.Tx, loadedTxResult.Tx) - assert.Equal(t, res.Tx.Hash(), loadedTxResult.Tx.Hash()) + assert.Equal(t, txResultIndex.BlockNum, loadedTxResult.Height) + assert.Equal(t, txResultIndex.TxIndex, loadedTxResult.Index) + assert.Equal(t, responses.DeliverTxs[0], loadedTxResult.TxResult) + assert.Equal(t, tx, loadedTxResult.Tx) + assert.Equal(t, tx.Hash(), loadedTxResult.Tx.Hash()) }) - t.Run("result not found", func(t *testing.T) { + t.Run("tx result index not found", func(t *testing.T) { var ( sdb = memdb.NewMemDB() hash = []byte("hash") @@ -72,4 +109,114 @@ func TestTxHandler(t *testing.T) { assert.Equal(t, expectedErr, err) }) + + t.Run("invalid block transaction index", func(t *testing.T) { + var ( + height = int64(10) + + stdTx = &std.Tx{ + Memo: "example tx", + } + + txResultIndex = state.TxResultIndex{ + BlockNum: height, + TxIndex: 0, + } + ) + + // Prepare the transaction + marshalledTx, err := amino.Marshal(stdTx) + require.NoError(t, err) + + tx := types.Tx(marshalledTx) + + // Prepare the DB + sdb := memdb.NewMemDB() + + // Save the result index to the DB + sdb.Set(state.CalcTxResultKey(tx.Hash()), txResultIndex.Bytes()) + + // Set the GLOBALLY referenced db + SetStateDB(sdb) + + // Set the GLOBALLY referenced blockstore + blockStore := &mockBlockStore{ + heightFn: func() int64 { + return height + }, + loadBlockFn: func(h int64) *types.Block { + require.Equal(t, height, h) + + return &types.Block{ + Data: types.Data{ + Txs: []types.Tx{}, // empty + }, + } + }, + } + + SetBlockStore(blockStore) + + // Load the result + loadedTxResult, err := Tx(nil, tx.Hash()) + require.Nil(t, loadedTxResult) + + assert.ErrorContains(t, err, "unable to get block transaction") + }) + + t.Run("invalid ABCI response index (corrupted state)", func(t *testing.T) { + var ( + height = int64(10) + + stdTx = &std.Tx{ + Memo: "example tx", + } + + txResultIndex = state.TxResultIndex{ + BlockNum: height, + TxIndex: 0, + } + ) + + // Prepare the transaction + marshalledTx, err := amino.Marshal(stdTx) + require.NoError(t, err) + + tx := types.Tx(marshalledTx) + + // Prepare the DB + sdb := memdb.NewMemDB() + + // Save the result index to the DB + sdb.Set(state.CalcTxResultKey(tx.Hash()), txResultIndex.Bytes()) + + // Set the GLOBALLY referenced db + SetStateDB(sdb) + + // Set the GLOBALLY referenced blockstore + blockStore := &mockBlockStore{ + heightFn: func() int64 { + return height + }, + loadBlockFn: func(h int64) *types.Block { + require.Equal(t, height, h) + + return &types.Block{ + Data: types.Data{ + Txs: []types.Tx{ + tx, + }, + }, + } + }, + } + + SetBlockStore(blockStore) + + // Load the result + loadedTxResult, err := Tx(nil, tx.Hash()) + require.Nil(t, loadedTxResult) + + assert.ErrorContains(t, err, "unable to load block results") + }) } diff --git a/tm2/pkg/bft/state/execution.go b/tm2/pkg/bft/state/execution.go index b51ffd0a4b2..da1735e3fae 100644 --- a/tm2/pkg/bft/state/execution.go +++ b/tm2/pkg/bft/state/execution.go @@ -111,14 +111,14 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b // Save the transaction results for index, tx := range block.Txs { - txResult := &types.TxResult{ - Height: block.Height, - Index: uint32(index), - Tx: tx, - Response: abciResponses.DeliverTxs[index], - } - - saveTxResult(blockExec.db, txResult) + saveTxResultIndex( + blockExec.db, + tx.Hash(), + TxResultIndex{ + BlockNum: block.Height, + TxIndex: uint32(index), + }, + ) } fail.Fail() // XXX diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 668331145cb..804d96842c4 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -19,7 +19,7 @@ const ( valSetCheckpointInterval = 100000 ) -var errTxResultCorrupted = errors.New("tx result corrupted") +var errTxResultIndexCorrupted = errors.New("tx result index corrupted") // ------------------------------------------------------------------------ @@ -31,7 +31,7 @@ func calcConsensusParamsKey(height int64) []byte { return []byte(fmt.Sprintf("consensusParamsKey:%x", height)) } -func calcABCIResponsesKey(height int64) []byte { +func CalcABCIResponsesKey(height int64) []byte { return []byte(fmt.Sprintf("abciResponsesKey:%x", height)) } @@ -155,7 +155,7 @@ func (arz *ABCIResponses) ResultsHash() []byte { // 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) (*ABCIResponses, error) { - buf := db.Get(calcABCIResponsesKey(height)) + buf := db.Get(CalcABCIResponsesKey(height)) if buf == nil { return nil, NoABCIResponsesForHeightError{height} } @@ -176,28 +176,38 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { // This is useful in case we crash after app.Commit and before s.Save(). // Responses are indexed by height so they can also be loaded later to produce Merkle proofs. func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { - db.Set(calcABCIResponsesKey(height), abciResponses.Bytes()) + db.Set(CalcABCIResponsesKey(height), abciResponses.Bytes()) } -// LoadTxResult loads the tx result associated with the given +// TxResultIndex keeps the result index information for a transaction +type TxResultIndex struct { + BlockNum int64 // the block number the tx was contained in + TxIndex uint32 // the index of the transaction within the block +} + +func (t *TxResultIndex) Bytes() []byte { + return amino.MustMarshal(t) +} + +// LoadTxResultIndex loads the tx result associated with the given // tx hash from the database, if any -func LoadTxResult(db dbm.DB, txHash []byte) (*types.TxResult, error) { +func LoadTxResultIndex(db dbm.DB, txHash []byte) (*TxResultIndex, error) { buf := db.Get(CalcTxResultKey(txHash)) if buf == nil { return nil, NoTxResultForHashError{txHash} } - txResult := new(types.TxResult) - if err := amino.Unmarshal(buf, txResult); err != nil { - return nil, fmt.Errorf("%w, %w", errTxResultCorrupted, err) + txResultIndex := new(TxResultIndex) + if err := amino.Unmarshal(buf, txResultIndex); err != nil { + return nil, fmt.Errorf("%w, %w", errTxResultIndexCorrupted, err) } - return txResult, nil + return txResultIndex, nil } -// saveTxResult persists the transaction result to the database -func saveTxResult(db dbm.DB, txResult *types.TxResult) { - db.SetSync(CalcTxResultKey(txResult.Tx.Hash()), txResult.Bytes()) +// saveTxResultIndex persists the transaction result index to the database +func saveTxResultIndex(db dbm.DB, txHash []byte, resultIndex TxResultIndex) { + db.Set(CalcTxResultKey(txHash), resultIndex.Bytes()) } // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/state/tx_result_test.go b/tm2/pkg/bft/state/tx_result_test.go index 83b3172ef3f..e5b53fa0950 100644 --- a/tm2/pkg/bft/state/tx_result_test.go +++ b/tm2/pkg/bft/state/tx_result_test.go @@ -51,15 +51,28 @@ func TestStoreLoadTxResult(t *testing.T) { // Save the results for _, txResult := range txResults { - saveTxResult(stateDB, txResult) + saveTxResultIndex( + stateDB, + txResult.Tx.Hash(), + TxResultIndex{ + BlockNum: txResult.Height, + TxIndex: txResult.Index, + }, + ) } // Verify they are saved correctly for _, txResult := range txResults { - dbResult, err := LoadTxResult(stateDB, txResult.Tx.Hash()) + result := TxResultIndex{ + BlockNum: txResult.Height, + TxIndex: txResult.Index, + } + + dbResult, err := LoadTxResultIndex(stateDB, txResult.Tx.Hash()) require.NoError(t, err) - assert.Equal(t, txResult, dbResult) + assert.Equal(t, result.BlockNum, dbResult.BlockNum) + assert.Equal(t, result.TxIndex, dbResult.TxIndex) } }) @@ -73,7 +86,7 @@ func TestStoreLoadTxResult(t *testing.T) { // Verify they are not present for _, txResult := range txResults { - _, err := LoadTxResult(stateDB, txResult.Tx.Hash()) + _, err := LoadTxResultIndex(stateDB, txResult.Tx.Hash()) expectedErr := NoTxResultForHashError{ Hash: txResult.Tx.Hash(), @@ -95,9 +108,9 @@ func TestStoreLoadTxResult(t *testing.T) { // Save the "corrupted" result to the DB stateDB.SetSync(CalcTxResultKey(hash), []byte(corruptedResult)) - txResult, err := LoadTxResult(stateDB, hash) + txResult, err := LoadTxResultIndex(stateDB, hash) require.Nil(t, txResult) - assert.ErrorIs(t, err, errTxResultCorrupted) + assert.ErrorIs(t, err, errTxResultIndexCorrupted) }) }