Skip to content

Commit 1226a21

Browse files
gfantonthehowl
andauthored
fix(gnodev): do not replay failed transaction (#3708)
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]>
1 parent 7ad19d7 commit 1226a21

File tree

2 files changed

+125
-14
lines changed

2 files changed

+125
-14
lines changed

contribs/gnodev/pkg/dev/node.go

+36-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log/slog"
77
"os"
88
"path/filepath"
9+
"slices"
910
"strings"
1011
"sync"
1112
"time"
@@ -20,6 +21,7 @@ import (
2021
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
2122
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
2223
"github.com/gnolang/gno/tm2/pkg/amino"
24+
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
2325
tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
2426
"github.com/gnolang/gno/tm2/pkg/bft/node"
2527
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
@@ -241,26 +243,53 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata,
241243
int64BlockNum := int64(blockNum)
242244
b, err := n.client.Block(&int64BlockNum)
243245
if err != nil {
244-
return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here
246+
return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err)
245247
}
248+
txs := b.Block.Data.Txs
249+
250+
bres, err := n.client.BlockResults(&int64BlockNum)
251+
if err != nil {
252+
return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err)
253+
}
254+
deliverTxs := bres.Results.DeliverTxs
255+
256+
// Sanity check
257+
if len(txs) != len(deliverTxs) {
258+
panic(fmt.Errorf("invalid block txs len (%d) vs block result txs len (%d)",
259+
len(txs), len(deliverTxs),
260+
))
261+
}
262+
263+
txResults := make([]*abci.ResponseDeliverTx, len(deliverTxs))
264+
for i, tx := range deliverTxs {
265+
txResults[i] = &tx
266+
}
267+
268+
// XXX: Consider replacing a failed transaction with an empty transaction
269+
// to preserve the transaction height ?
270+
// Note that this would also require committing instead of using the
271+
// genesis block.
272+
273+
metaTxs := make([]gnoland.TxWithMetadata, 0, len(txs))
274+
for i, encodedTx := range txs {
275+
if deliverTx := deliverTxs[i]; !deliverTx.IsOK() {
276+
continue // skip failed tx
277+
}
246278

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

255-
txs[i] = gnoland.TxWithMetadata{
284+
metaTxs = append(metaTxs, gnoland.TxWithMetadata{
256285
Tx: tx,
257286
Metadata: &gnoland.GnoTxMetadata{
258287
Timestamp: b.BlockMeta.Header.Time.Unix(),
259288
},
260-
}
289+
})
261290
}
262291

263-
return txs, nil
292+
return slices.Clip(metaTxs), nil
264293
}
265294

266295
// GetBlockTransactions returns the transactions contained

contribs/gnodev/pkg/dev/node_test.go

+89-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
2121
tm2events "github.com/gnolang/gno/tm2/pkg/events"
2222
"github.com/gnolang/gno/tm2/pkg/log"
23+
tm2std "github.com/gnolang/gno/tm2/pkg/std"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
)
@@ -203,7 +204,6 @@ func Render(_ string) string { return str }
203204
},
204205
}
205206

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

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

246+
func TestTxGasFailure(t *testing.T) {
247+
fooPkg := gnovm.MemPackage{
248+
Name: "foo",
249+
Path: "gno.land/r/dev/foo",
250+
Files: []*gnovm.MemFile{
251+
{
252+
Name: "foo.gno",
253+
Body: `package foo
254+
import "strconv"
255+
256+
var i int
257+
func Inc() { i++ } // method to increment i
258+
func Render(_ string) string { return strconv.Itoa(i) }
259+
`,
260+
},
261+
},
262+
}
263+
264+
node, emitter := newTestingDevNode(t, &fooPkg)
265+
assert.Len(t, node.ListPkgs(), 1)
266+
267+
// Test rendering
268+
render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo")
269+
require.NoError(t, err)
270+
require.Equal(t, "0", render)
271+
272+
// Call `Inc` to update counter
273+
msg := vm.MsgCall{
274+
PkgPath: "gno.land/r/dev/foo",
275+
Func: "Inc",
276+
Args: nil,
277+
Send: nil,
278+
}
279+
280+
res, err := testingCallRealm(t, node, msg)
281+
require.NoError(t, err)
282+
require.NoError(t, res.CheckTx.Error)
283+
require.NoError(t, res.DeliverTx.Error)
284+
assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult)
285+
286+
// Check for correct render update
287+
render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo")
288+
require.NoError(t, err)
289+
require.Equal(t, "1", render)
290+
291+
// Not Enough gas wanted
292+
callCfg := gnoclient.BaseTxCfg{
293+
GasFee: ugnot.ValueString(10000), // Gas fee
294+
295+
// Ensure sufficient gas is provided for the transaction to be committed.
296+
// However, avoid providing too much gas to allow the
297+
// transaction to succeed (OutOfGasError).
298+
GasWanted: 100_000,
299+
}
300+
301+
res, err = testingCallRealmWithConfig(t, node, callCfg, msg)
302+
require.Error(t, err)
303+
require.ErrorAs(t, err, &tm2std.OutOfGasError{})
304+
305+
// Transaction should be committed regardless the error
306+
require.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult,
307+
"(probably) not enough gas for the transaction to be committed")
308+
309+
// Reload the node
310+
err = node.Reload(context.Background())
311+
require.NoError(t, err)
312+
assert.Equal(t, events.EvtReload, emitter.NextEvent().Type())
313+
314+
// Check for correct render update
315+
render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo")
316+
require.NoError(t, err)
317+
318+
// Assert that the previous transaction hasn't succeeded during genesis reload
319+
require.Equal(t, "1", render)
320+
}
321+
246322
func TestTxTimestampRecover(t *testing.T) {
247323
const fooFile = `
248324
package foo
@@ -455,17 +531,23 @@ func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error
455531
func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) {
456532
t.Helper()
457533

534+
defaultCfg := gnoclient.BaseTxCfg{
535+
GasFee: ugnot.ValueString(1000000), // Gas fee
536+
GasWanted: 3_000_000, // Gas wanted
537+
}
538+
539+
return testingCallRealmWithConfig(t, node, defaultCfg, msgs...)
540+
}
541+
542+
func testingCallRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxCfg, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) {
543+
t.Helper()
544+
458545
signer := newInMemorySigner(t, node.Config().ChainID())
459546
cli := gnoclient.Client{
460547
Signer: signer,
461548
RPCClient: node.Client(),
462549
}
463550

464-
txcfg := gnoclient.BaseTxCfg{
465-
GasFee: ugnot.ValueString(1000000), // Gas fee
466-
GasWanted: 3_000_000, // Gas wanted
467-
}
468-
469551
// Set Caller in the msgs
470552
caller, err := signer.Info()
471553
require.NoError(t, err)
@@ -474,7 +556,7 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types
474556
vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args))
475557
}
476558

477-
return cli.Call(txcfg, vmMsgs...)
559+
return cli.Call(bcfg, vmMsgs...)
478560
}
479561

480562
func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig {

0 commit comments

Comments
 (0)