Skip to content

Commit

Permalink
feat(tm2): expose InitChain tx responses (gnolang#1941)
Browse files Browse the repository at this point in the history
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": {
```

<details><summary>Contributors' checklist...</summary>

- [ ] 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).
</details>

---------

Signed-off-by: Norman Meier <[email protected]>
Co-authored-by: Morgan <[email protected]>
  • Loading branch information
2 people authored and gfanton committed Jul 23, 2024
1 parent 1594226 commit 01050f1
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 31 deletions.
11 changes: 10 additions & 1 deletion gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tm2/pkg/bft/abci/types/abci.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions tm2/pkg/bft/abci/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ type ResponseInitChain struct {
ResponseBase
ConsensusParams *ConsensusParams
Validators []ValidatorUpdate
TxResponses []ResponseDeliverTx
}

type ResponseQuery struct {
Expand Down
7 changes: 7 additions & 0 deletions tm2/pkg/bft/consensus/replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
63 changes: 48 additions & 15 deletions tm2/pkg/bft/consensus/replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
10 changes: 7 additions & 3 deletions tm2/pkg/bft/rpc/core/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
37 changes: 37 additions & 0 deletions tm2/pkg/bft/rpc/core/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 3 additions & 3 deletions tm2/pkg/bft/state/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 0 additions & 6 deletions tm2/pkg/bft/state/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 9 additions & 3 deletions tm2/pkg/bft/state/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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())
}

Expand Down

0 comments on commit 01050f1

Please sign in to comment.