From 3aa0f0b7d81a8aa5c772a2774da625be8aa05f6a Mon Sep 17 00:00:00 2001 From: n0izn0iz Date: Tue, 23 Jul 2024 13:19:38 +0200 Subject: [PATCH] feat(tm2): expose InitChain tx responses (#1941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save and allow to fetch genesis txs responses ### Before ``` ❯ curl 'http://127.0.0.1:26657/block_results?height=0' -s | head -n 20 { "jsonrpc": "2.0", "id": "", "error": { "code": -32603, "message": "Internal error", "data": "height must be greater than 0" } } ``` ### After ``` ❯ curl 'http://127.0.0.1:26657/block_results?height=0' -s | head -n 20 { "jsonrpc": "2.0", "id": "", "result": { "height": "0", "results": { "deliver_tx": [ { "ResponseBase": { "Error": null, "Data": null, "Events": null, "Log": "msg:0,success:true,log:,events:[]", "Info": "" }, "GasWanted": "50000", "GasUsed": "240261825" }, { "ResponseBase": { ```
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: Norman Meier Co-authored-by: Morgan --- gno.land/pkg/gnoland/app.go | 11 ++++- tm2/pkg/bft/abci/types/abci.proto | 1 + tm2/pkg/bft/abci/types/types.go | 1 + tm2/pkg/bft/consensus/replay.go | 7 ++++ tm2/pkg/bft/consensus/replay_test.go | 63 +++++++++++++++++++++------- tm2/pkg/bft/rpc/core/blocks.go | 10 +++-- tm2/pkg/bft/rpc/core/blocks_test.go | 37 ++++++++++++++++ tm2/pkg/bft/state/execution.go | 6 +-- tm2/pkg/bft/state/export_test.go | 6 --- tm2/pkg/bft/state/store.go | 12 ++++-- 10 files changed, 123 insertions(+), 31 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index ac066fa98b8..f4d353411f8 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -193,6 +193,8 @@ func InitChainer( resHandler GenesisTxHandler, ) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + txResponses := []abci.ResponseDeliverTx{} + if req.AppState != nil { // Get genesis state genState := req.AppState.(GnoGenesisState) @@ -219,13 +221,20 @@ func InitChainer( ) } + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: res.ResponseBase, + GasWanted: res.GasWanted, + GasUsed: res.GasUsed, + }) + resHandler(ctx, tx, res) } } // Done! return abci.ResponseInitChain{ - Validators: req.Validators, + Validators: req.Validators, + TxResponses: txResponses, } } } diff --git a/tm2/pkg/bft/abci/types/abci.proto b/tm2/pkg/bft/abci/types/abci.proto index 99e7a584c2e..15b8ffa219e 100644 --- a/tm2/pkg/bft/abci/types/abci.proto +++ b/tm2/pkg/bft/abci/types/abci.proto @@ -112,6 +112,7 @@ message ResponseInitChain { ResponseBase response_base = 1 [json_name = "ResponseBase"]; ConsensusParams consensus_params = 2 [json_name = "ConsensusParams"]; repeated ValidatorUpdate validators = 3 [json_name = "Validators"]; + repeated ResponseDeliverTx tx_responses = 4 [json_name = "TxResponses"]; } message ResponseQuery { diff --git a/tm2/pkg/bft/abci/types/types.go b/tm2/pkg/bft/abci/types/types.go index 8c2764cb1bd..42376e712a6 100644 --- a/tm2/pkg/bft/abci/types/types.go +++ b/tm2/pkg/bft/abci/types/types.go @@ -159,6 +159,7 @@ type ResponseInitChain struct { ResponseBase ConsensusParams *ConsensusParams Validators []ValidatorUpdate + TxResponses []ResponseDeliverTx } type ResponseQuery struct { diff --git a/tm2/pkg/bft/consensus/replay.go b/tm2/pkg/bft/consensus/replay.go index a423d634c2f..02e6dade72c 100644 --- a/tm2/pkg/bft/consensus/replay.go +++ b/tm2/pkg/bft/consensus/replay.go @@ -307,6 +307,13 @@ func (h *Handshaker) ReplayBlocks( return nil, err } + // Save the results by height + abciResponse := sm.NewABCIResponsesFromNum(int64(len(res.TxResponses))) + copy(abciResponse.DeliverTxs, res.TxResponses) + sm.SaveABCIResponses(h.stateDB, 0, abciResponse) + + // NOTE: we don't save results by tx hash since the transactions are in the AppState opaque type + if stateBlockHeight == 0 { // we only update state when we are in initial state // If the app returned validators or consensus params, update the state. if len(res.Validators) > 0 { diff --git a/tm2/pkg/bft/consensus/replay_test.go b/tm2/pkg/bft/consensus/replay_test.go index 4ba091346f0..aff7316f086 100644 --- a/tm2/pkg/bft/consensus/replay_test.go +++ b/tm2/pkg/bft/consensus/replay_test.go @@ -1131,11 +1131,19 @@ func TestHandshakeUpdatesValidators(t *testing.T) { val, _ := types.RandValidator(true, 10) vals := types.NewValidatorSet([]*types.Validator{val}) - app := &initChainApp{vals: vals.ABCIValidatorUpdates()} + appVals := vals.ABCIValidatorUpdates() + // returns the vals on InitChain + app := initChainApp{ + initChain: func(req abci.RequestInitChain) abci.ResponseInitChain { + return abci.ResponseInitChain{ + Validators: appVals, + } + }, + } clientCreator := proxy.NewLocalClientCreator(app) config, genesisFile := ResetConfig("handshake_test_") - defer os.RemoveAll(config.RootDir) + t.Cleanup(func() { require.NoError(t, os.RemoveAll(config.RootDir)) }) stateDB, state, store := makeStateAndStore(config, genesisFile, "v0.0.0-test") oldValAddr := state.Validators.Validators[0].Address @@ -1144,13 +1152,9 @@ func TestHandshakeUpdatesValidators(t *testing.T) { genDoc, _ := sm.MakeGenesisDocFromFile(genesisFile) handshaker := NewHandshaker(stateDB, state, store, genDoc) proxyApp := appconn.NewAppConns(clientCreator) - if err := proxyApp.Start(); err != nil { - t.Fatalf("Error starting proxy app connections: %v", err) - } - defer proxyApp.Stop() - if err := handshaker.Handshake(proxyApp); err != nil { - t.Fatalf("Error on abci handshake: %v", err) - } + require.NoError(t, proxyApp.Start(), "Error starting proxy app connections") + t.Cleanup(func() { require.NoError(t, proxyApp.Stop()) }) + require.NoError(t, handshaker.Handshake(proxyApp), "Error on abci handshake") // reload the state, check the validator set was updated state = sm.LoadState(stateDB) @@ -1161,14 +1165,43 @@ func TestHandshakeUpdatesValidators(t *testing.T) { assert.Equal(t, newValAddr, expectValAddr) } -// returns the vals on InitChain +func TestHandshakeGenesisResponseDeliverTx(t *testing.T) { + t.Parallel() + + const numInitResponses = 42 + + app := initChainApp{ + initChain: func(req abci.RequestInitChain) abci.ResponseInitChain { + return abci.ResponseInitChain{ + TxResponses: make([]abci.ResponseDeliverTx, numInitResponses), + } + }, + } + clientCreator := proxy.NewLocalClientCreator(app) + + config, genesisFile := ResetConfig("handshake_test_") + t.Cleanup(func() { require.NoError(t, os.RemoveAll(config.RootDir)) }) + stateDB, state, store := makeStateAndStore(config, genesisFile, "v0.0.0-test") + + // now start the app using the handshake - it should sync + genDoc, _ := sm.MakeGenesisDocFromFile(genesisFile) + handshaker := NewHandshaker(stateDB, state, store, genDoc) + proxyApp := appconn.NewAppConns(clientCreator) + require.NoError(t, proxyApp.Start(), "Error starting proxy app connections") + t.Cleanup(func() { require.NoError(t, proxyApp.Stop()) }) + require.NoError(t, handshaker.Handshake(proxyApp), "Error on abci handshake") + + // check that the genesis transaction results are saved + res, err := sm.LoadABCIResponses(stateDB, 0) + require.NoError(t, err, "Failed to load genesis ABCI responses") + assert.Len(t, res.DeliverTxs, numInitResponses) +} + type initChainApp struct { abci.BaseApplication - vals []abci.ValidatorUpdate + initChain func(req abci.RequestInitChain) abci.ResponseInitChain } -func (ica *initChainApp) InitChain(req abci.RequestInitChain) abci.ResponseInitChain { - return abci.ResponseInitChain{ - Validators: ica.vals, - } +func (m initChainApp) InitChain(req abci.RequestInitChain) abci.ResponseInitChain { + return m.initChain(req) } diff --git a/tm2/pkg/bft/rpc/core/blocks.go b/tm2/pkg/bft/rpc/core/blocks.go index 06bb3de1174..53ed25ade11 100644 --- a/tm2/pkg/bft/rpc/core/blocks.go +++ b/tm2/pkg/bft/rpc/core/blocks.go @@ -400,7 +400,7 @@ func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, erro // ``` func BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockResults, error) { storeHeight := blockStore.Height() - height, err := getHeight(storeHeight, heightPtr) + height, err := getHeightWithMin(storeHeight, heightPtr, 0) if err != nil { return nil, err } @@ -418,10 +418,14 @@ func BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockR } func getHeight(currentHeight int64, heightPtr *int64) (int64, error) { + return getHeightWithMin(currentHeight, heightPtr, 1) +} + +func getHeightWithMin(currentHeight int64, heightPtr *int64, min int64) (int64, error) { if heightPtr != nil { height := *heightPtr - if height <= 0 { - return 0, fmt.Errorf("height must be greater than 0") + if height < min { + return 0, fmt.Errorf("height must be greater than or equal to %d", min) } if height > currentHeight { return 0, fmt.Errorf("height must be less than or equal to the current blockchain height") diff --git a/tm2/pkg/bft/rpc/core/blocks_test.go b/tm2/pkg/bft/rpc/core/blocks_test.go index 0fcd40f6d14..550cc1542c9 100644 --- a/tm2/pkg/bft/rpc/core/blocks_test.go +++ b/tm2/pkg/bft/rpc/core/blocks_test.go @@ -55,3 +55,40 @@ func TestBlockchainInfo(t *testing.T) { } } } + +func TestGetHeight(t *testing.T) { + t.Parallel() + + cases := []struct { + currentHeight int64 + heightPtr *int64 + min int64 + res int64 + wantErr bool + }{ + // height >= min + {42, int64Ptr(0), 0, 0, false}, + {42, int64Ptr(1), 0, 1, false}, + + // height < min + {42, int64Ptr(0), 1, 0, true}, + + // nil height + {42, nil, 1, 42, false}, + } + + for i, c := range cases { + caseString := fmt.Sprintf("test %d failed", i) + res, err := getHeightWithMin(c.currentHeight, c.heightPtr, c.min) + if c.wantErr { + require.Error(t, err, caseString) + } else { + require.NoError(t, err, caseString) + require.Equal(t, res, c.res, caseString) + } + } +} + +func int64Ptr(v int64) *int64 { + return &v +} diff --git a/tm2/pkg/bft/state/execution.go b/tm2/pkg/bft/state/execution.go index 8b461cdbf6c..15a0f466341 100644 --- a/tm2/pkg/bft/state/execution.go +++ b/tm2/pkg/bft/state/execution.go @@ -106,10 +106,10 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b fail.Fail() // XXX - // Save the results before we commit. - saveABCIResponses(blockExec.db, block.Height, abciResponses) + // Save the results by height + SaveABCIResponses(blockExec.db, block.Height, abciResponses) - // Save the transaction results + // Save the results by tx hash for index, tx := range block.Txs { saveTxResultIndex( blockExec.db, diff --git a/tm2/pkg/bft/state/export_test.go b/tm2/pkg/bft/state/export_test.go index cdebf3e852d..0935236ed92 100644 --- a/tm2/pkg/bft/state/export_test.go +++ b/tm2/pkg/bft/state/export_test.go @@ -42,12 +42,6 @@ func CalcValidatorsKey(height int64) []byte { return calcValidatorsKey(height) } -// SaveABCIResponses is an alias for the private saveABCIResponses method in -// store.go, exported exclusively and explicitly for testing. -func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { - saveABCIResponses(db, height, abciResponses) -} - // SaveConsensusParamsInfo is an alias for the private saveConsensusParamsInfo // method in store.go, exported exclusively and explicitly for testing. func SaveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params abci.ConsensusParams) { diff --git a/tm2/pkg/bft/state/store.go b/tm2/pkg/bft/state/store.go index 804d96842c4..7c23e4eed4a 100644 --- a/tm2/pkg/bft/state/store.go +++ b/tm2/pkg/bft/state/store.go @@ -131,8 +131,13 @@ type ABCIResponses struct { // NewABCIResponses returns a new ABCIResponses func NewABCIResponses(block *types.Block) *ABCIResponses { - resDeliverTxs := make([]abci.ResponseDeliverTx, block.NumTxs) - if block.NumTxs == 0 { + return NewABCIResponsesFromNum(block.NumTxs) +} + +// NewABCIResponsesFromNum returns a new ABCIResponses with a set number of txs +func NewABCIResponsesFromNum(numTxs int64) *ABCIResponses { + resDeliverTxs := make([]abci.ResponseDeliverTx, numTxs) + if numTxs == 0 { // This makes Amino encoding/decoding consistent. resDeliverTxs = nil } @@ -175,7 +180,8 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { // SaveABCIResponses persists the ABCIResponses to the database. // 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) { +// NOTE: this should only be used internally by the bft package and subpackages. +func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { db.Set(CalcABCIResponsesKey(height), abciResponses.Bytes()) }