Skip to content

Commit

Permalink
fix(gnodev): do not replay failed transaction (#3708)
Browse files Browse the repository at this point in the history
This PR follows #3699 to skip failed transactions on `gnodev` while
reloading.

---------

Signed-off-by: gfanton <[email protected]>
Co-authored-by: Morgan Bazalgette <[email protected]>
  • Loading branch information
gfanton and thehowl authored Feb 10, 2025
1 parent 7ad19d7 commit 1226a21
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 14 deletions.
43 changes: 36 additions & 7 deletions contribs/gnodev/pkg/dev/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
Expand All @@ -20,6 +21,7 @@ import (
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/tm2/pkg/amino"
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
"github.com/gnolang/gno/tm2/pkg/bft/node"
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
Expand Down Expand Up @@ -241,26 +243,53 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata,
int64BlockNum := int64(blockNum)
b, err := n.client.Block(&int64BlockNum)
if err != nil {
return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here
return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err)
}
txs := b.Block.Data.Txs

bres, err := n.client.BlockResults(&int64BlockNum)
if err != nil {
return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err)
}
deliverTxs := bres.Results.DeliverTxs

// Sanity check
if len(txs) != len(deliverTxs) {
panic(fmt.Errorf("invalid block txs len (%d) vs block result txs len (%d)",
len(txs), len(deliverTxs),
))
}

txResults := make([]*abci.ResponseDeliverTx, len(deliverTxs))
for i, tx := range deliverTxs {
txResults[i] = &tx
}

// XXX: Consider replacing a failed transaction with an empty transaction
// to preserve the transaction height ?
// Note that this would also require committing instead of using the
// genesis block.

metaTxs := make([]gnoland.TxWithMetadata, 0, len(txs))
for i, encodedTx := range txs {
if deliverTx := deliverTxs[i]; !deliverTx.IsOK() {
continue // skip failed tx
}

txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs))
for i, encodedTx := range b.Block.Data.Txs {
// fallback on std tx
var tx std.Tx
if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil {
return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr)
}

txs[i] = gnoland.TxWithMetadata{
metaTxs = append(metaTxs, gnoland.TxWithMetadata{
Tx: tx,
Metadata: &gnoland.GnoTxMetadata{
Timestamp: b.BlockMeta.Header.Time.Unix(),
},
}
})
}

return txs, nil
return slices.Clip(metaTxs), nil
}

// GetBlockTransactions returns the transactions contained
Expand Down
96 changes: 89 additions & 7 deletions contribs/gnodev/pkg/dev/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
tm2events "github.com/gnolang/gno/tm2/pkg/events"
"github.com/gnolang/gno/tm2/pkg/log"
tm2std "github.com/gnolang/gno/tm2/pkg/std"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -203,7 +204,6 @@ func Render(_ string) string { return str }
},
}

// Call NewDevNode with no package should work
node, emitter := newTestingDevNode(t, &fooPkg)
assert.Len(t, node.ListPkgs(), 1)

Expand Down Expand Up @@ -243,6 +243,82 @@ func Render(_ string) string { return str }
assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type())
}

func TestTxGasFailure(t *testing.T) {
fooPkg := gnovm.MemPackage{
Name: "foo",
Path: "gno.land/r/dev/foo",
Files: []*gnovm.MemFile{
{
Name: "foo.gno",
Body: `package foo
import "strconv"
var i int
func Inc() { i++ } // method to increment i
func Render(_ string) string { return strconv.Itoa(i) }
`,
},
},
}

node, emitter := newTestingDevNode(t, &fooPkg)
assert.Len(t, node.ListPkgs(), 1)

// Test rendering
render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo")
require.NoError(t, err)
require.Equal(t, "0", render)

// Call `Inc` to update counter
msg := vm.MsgCall{
PkgPath: "gno.land/r/dev/foo",
Func: "Inc",
Args: nil,
Send: nil,
}

res, err := testingCallRealm(t, node, msg)
require.NoError(t, err)
require.NoError(t, res.CheckTx.Error)
require.NoError(t, res.DeliverTx.Error)
assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult)

// Check for correct render update
render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo")
require.NoError(t, err)
require.Equal(t, "1", render)

// Not Enough gas wanted
callCfg := gnoclient.BaseTxCfg{
GasFee: ugnot.ValueString(10000), // Gas fee

// Ensure sufficient gas is provided for the transaction to be committed.
// However, avoid providing too much gas to allow the
// transaction to succeed (OutOfGasError).
GasWanted: 100_000,
}

res, err = testingCallRealmWithConfig(t, node, callCfg, msg)
require.Error(t, err)
require.ErrorAs(t, err, &tm2std.OutOfGasError{})

// Transaction should be committed regardless the error
require.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult,
"(probably) not enough gas for the transaction to be committed")

// Reload the node
err = node.Reload(context.Background())
require.NoError(t, err)
assert.Equal(t, events.EvtReload, emitter.NextEvent().Type())

// Check for correct render update
render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo")
require.NoError(t, err)

// Assert that the previous transaction hasn't succeeded during genesis reload
require.Equal(t, "1", render)
}

func TestTxTimestampRecover(t *testing.T) {
const fooFile = `
package foo
Expand Down Expand Up @@ -455,17 +531,23 @@ func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error
func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) {
t.Helper()

defaultCfg := gnoclient.BaseTxCfg{
GasFee: ugnot.ValueString(1000000), // Gas fee
GasWanted: 3_000_000, // Gas wanted
}

return testingCallRealmWithConfig(t, node, defaultCfg, msgs...)
}

func testingCallRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxCfg, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) {
t.Helper()

signer := newInMemorySigner(t, node.Config().ChainID())
cli := gnoclient.Client{
Signer: signer,
RPCClient: node.Client(),
}

txcfg := gnoclient.BaseTxCfg{
GasFee: ugnot.ValueString(1000000), // Gas fee
GasWanted: 3_000_000, // Gas wanted
}

// Set Caller in the msgs
caller, err := signer.Info()
require.NoError(t, err)
Expand All @@ -474,7 +556,7 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types
vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args))
}

return cli.Call(txcfg, vmMsgs...)
return cli.Call(bcfg, vmMsgs...)
}

func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig {
Expand Down

0 comments on commit 1226a21

Please sign in to comment.