Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions gno.land/pkg/keyscli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command {
cfg.OnTxSuccess = func(tx std.Tx, res *ctypes.ResultBroadcastTxCommit) {
PrintTxInfo(tx, res, io)
}
// OnTxFailure prints metrics (gas, storage, etc.) when a tx fails.
cfg.OnTxFailure = func(tx std.Tx, res *ctypes.ResultBroadcastTxCommit) {
PrintTxMetrics(tx, res, io)
}
cmd.AddSubCommands(
client.NewAddCmd(cfg, io),
client.NewDeleteCmd(cfg, io),
Expand Down Expand Up @@ -64,6 +68,13 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command {
func PrintTxInfo(tx std.Tx, res *ctypes.ResultBroadcastTxCommit, io commands.IO) {
io.Println(string(res.DeliverTx.Data))
io.Println("OK!")
PrintTxMetrics(tx, res, io)
}

// PrintTxMetrics prints common tx metrics (gas, storage, events, info, hash).
// This is used for both success and failure cases so users always see the
// relevant numbers.
func PrintTxMetrics(tx std.Tx, res *ctypes.ResultBroadcastTxCommit, io commands.IO) {
io.Println("GAS WANTED:", res.DeliverTx.GasWanted)
io.Println("GAS USED: ", res.DeliverTx.GasUsed)
io.Println("HEIGHT: ", res.Height)
Expand Down
3 changes: 3 additions & 0 deletions tm2/pkg/crypto/keys/client/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type BaseOptions struct {
// OnTxSuccess is called when the transaction tx succeeds. It can, for example,
// print info in the result. If OnTxSuccess is nil, print basic info.
OnTxSuccess func(tx std.Tx, res *ctypes.ResultBroadcastTxCommit)
// OnTxFailure is called when the transaction tx fails. If nil, failure output
// is minimal.
OnTxFailure func(tx std.Tx, res *ctypes.ResultBroadcastTxCommit)
}

var DefaultBaseOptions = BaseOptions{
Expand Down
17 changes: 14 additions & 3 deletions tm2/pkg/crypto/keys/client/maketx.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ func ExecSignAndBroadcast(
return errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log)
}
if bres.DeliverTx.IsErr() {
io.Println("TX HASH: ", base64.StdEncoding.EncodeToString(bres.Hash))
io.Println("INFO: ", bres.DeliverTx.Info)
return errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log)
return handleDeliverResult(cfg.RootCfg, tx, bres, io)
}

if cfg.RootCfg.OnTxSuccess != nil {
Expand All @@ -238,3 +236,16 @@ func ExecSignAndBroadcast(

return nil
}

// handleDeliverResult handles a failed DeliverTx by invoking OnTxFailure or printing defaults.
func handleDeliverResult(cfg *BaseCfg, tx std.Tx, bres *types.ResultBroadcastTxCommit, io commands.IO) error {
if cfg.OnTxFailure != nil {
cfg.OnTxFailure(tx, bres)
} else {
io.Println("GAS WANTED:", bres.DeliverTx.GasWanted)
io.Println("GAS USED: ", bres.DeliverTx.GasUsed)
io.Println("EVENTS: ", string(bres.DeliverTx.EncodeEvents()))
io.Println("INFO: ", bres.DeliverTx.Info)
}
return errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log)
}
61 changes: 61 additions & 0 deletions tm2/pkg/crypto/keys/client/maketx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package client

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"

abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/std"
)

func TestHandleDeliverResultCallsOnFailure(t *testing.T) {
called := false
cfg := &BaseCfg{BaseOptions: BaseOptions{OnTxFailure: func(tx std.Tx, res *ctypes.ResultBroadcastTxCommit) {
called = true
}}}

tx := std.Tx{}
bres := &ctypes.ResultBroadcastTxCommit{
DeliverTx: abci.ResponseDeliverTx{
ResponseBase: abci.ResponseBase{Error: abci.StringError("fail")},
GasWanted: 10,
GasUsed: 20,
},
}

io := commands.NewTestIO()
io.SetOut(commands.WriteNopCloser(&bytes.Buffer{}))
err := handleDeliverResult(cfg, tx, bres, io)

require.True(t, called, "OnTxFailure should be invoked")
require.Error(t, err)
}

func TestHandleDeliverResultPrintsDefaultWhenNoCallback(t *testing.T) {
cfg := &BaseCfg{BaseOptions: BaseOptions{}}
tx := std.Tx{}
bres := &ctypes.ResultBroadcastTxCommit{
DeliverTx: abci.ResponseDeliverTx{
ResponseBase: abci.ResponseBase{Error: abci.StringError("fail"), Info: "hint"},
GasWanted: 7,
GasUsed: 9,
},
}

buf := &bytes.Buffer{}
io := commands.NewTestIO()
io.SetOut(commands.WriteNopCloser(buf))

err := handleDeliverResult(cfg, tx, bres, io)
require.Error(t, err)

output := buf.String()
require.Contains(t, output, "GAS WANTED: 7")
require.Contains(t, output, "GAS USED: 9")
require.Contains(t, output, "INFO:")
require.Contains(t, output, "hint")
}
18 changes: 17 additions & 1 deletion tm2/pkg/sdk/auth/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func NewAnteHandler(ak AccountKeeper, bank BankKeeperI, sigGasConsumer Signature
switch ex := r.(type) {
case store.OutOfGasError:
log := fmt.Sprintf(
"out of gas in location: %v; gasWanted: %d, gasUsed: %d",
"out of gas in location: %v; gasWanted: %d, gasUsed: %d (until panic)",
ex.Descriptor, tx.Fee.GasWanted, newCtx.GasMeter().GasConsumed(),
)
res = abciResult(std.ErrOutOfGas(log))
Expand Down Expand Up @@ -408,6 +408,22 @@ func EnsureSufficientMempoolFees(ctx sdk.Context, fee std.Fee) sdk.Result {

// SetGasMeter returns a new context with a gas meter set from a given context.
func SetGasMeter(ctx sdk.Context, gasLimit int64) sdk.Context {
if ctx.Mode() == sdk.RunTxModeSimulate {
// Cap simulation gas to avoid unbounded consumption; use consensus maxGas
// as a ceiling. Ignore the tx gasLimit here so we can measure full gas usage.
maxGas := int64(-1)
if cp := ctx.ConsensusParams(); cp != nil {
maxGas = cp.Block.MaxGas
}

if maxGas != int64(-1) {
return ctx.WithGasMeter(store.NewGasMeter(maxGas))
}

// If no limit is configured (maxGas == -1), fall back to infinite.
return ctx.WithGasMeter(store.NewInfiniteGasMeter())
}

// In various cases such as simulation and during the genesis block, we do not
// meter any gas utilization.
if ctx.BlockHeight() == 0 {
Expand Down
54 changes: 54 additions & 0 deletions tm2/pkg/sdk/auth/ante_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -948,3 +948,57 @@ func TestInvalidUserFee(t *testing.T) {
require.False(t, res2.IsOK())
assert.Contains(t, res2.Log, "Gas price denominations should be equal;")
}

func TestSetGasMeterSimulationCap(t *testing.T) {
t.Parallel()

env := setupTestEnv()
cp := env.ctx.ConsensusParams()
require.NotNil(t, cp)
require.NotNil(t, cp.Block)
maxGas := cp.Block.MaxGas
require.True(t, maxGas > 0)

ctxSim := env.ctx.WithMode(sdk.RunTxModeSimulate)

t.Run("caps to consensus maxGas when higher gas wanted", func(t *testing.T) {
t.Parallel()
ctx := SetGasMeter(ctxSim, maxGas+500)
meter := ctx.GasMeter()
require.Equal(t, maxGas, meter.Limit())
meter.ConsumeGas(maxGas, "fill to max")
require.Panics(t, func() {
meter.ConsumeGas(1, "over maxGas")
})
})

t.Run("ignores lower gas wanted in simulation and uses consensus cap", func(t *testing.T) {
t.Parallel()
const gasWanted = int64(500)
ctx := SetGasMeter(ctxSim, gasWanted)
meter := ctx.GasMeter()
require.Equal(t, maxGas, meter.Limit())
meter.ConsumeGas(maxGas, "fill to maxGas cap")
require.Panics(t, func() {
meter.ConsumeGas(1, "over maxGas")
})
})
}

// When no consensus maxGas is configured, simulation should use an infinite meter.
func TestSimulationGasMeterInfiniteWhenNoMaxGas(t *testing.T) {
t.Parallel()

env := setupTestEnv()
ctx := env.ctx.WithConsensusParams(nil).WithMode(sdk.RunTxModeSimulate)

ctx = SetGasMeter(ctx, 1) // should be ignored and infinite meter used
meter := ctx.GasMeter()
require.Equal(t, int64(0), meter.Limit())

// Consuming a very large amount should not panic.
require.NotPanics(t, func() {
meter.ConsumeGas(1_000_000_000, "huge consume")
})
require.False(t, meter.IsOutOfGas())
}
30 changes: 27 additions & 3 deletions tm2/pkg/sdk/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,16 +762,26 @@ func (app *BaseApp) runTx(ctx Context, tx Tx) (result Result) {
if r := recover(); r != nil {
switch ex := r.(type) {
case store.OutOfGasError:
gasUsed := ctx.GasMeter().GasConsumed()
maxGas := int64(-1)
if cp := ctx.ConsensusParams(); cp != nil {
maxGas = cp.Block.MaxGas
}
var detail string
if maxGas > 0 && gasUsed >= maxGas {
detail = fmt.Sprintf("(hit consensus maxGas %d)", maxGas)
}
log := fmt.Sprintf(
"out of gas, gasWanted: %d, gasUsed: %d location: %v",
"out of gas %s, gasWanted: %d, gasUsed: %d (until panic) location: %v",
detail,
gasWanted,
ctx.GasMeter().GasConsumed(),
gasUsed,
ex.Descriptor,
)
result.Error = ABCIError(std.ErrOutOfGas(log))
result.Log = log
result.GasWanted = gasWanted
result.GasUsed = ctx.GasMeter().GasConsumed()
result.GasUsed = gasUsed
return
default:
log := fmt.Sprintf("recovered: %v\nstack:\n%v", r, string(debug.Stack()))
Expand Down Expand Up @@ -862,6 +872,20 @@ func (app *BaseApp) runTx(ctx Context, tx Tx) (result Result) {
result = app.runMsgs(runMsgCtx, msgs, mode)
result.GasWanted = gasWanted

// Special case for simulation mode where the gas meter is infinite:
// if we used more gas than was requested, return an out of gas error.
if mode == RunTxModeSimulate && result.Error == nil &&
gasWanted > 0 && result.GasUsed > gasWanted {
log := fmt.Sprintf(
"out of gas during simulation; gasWanted: %d, gasUsed: %d",
gasWanted, result.GasUsed,
)
result.Error = ABCIError(std.ErrOutOfGas(log))
result.Log = log

return result
}

// Safety check: don't write the cache state unless we're in DeliverTx.
if mode != RunTxModeDeliver {
return result
Expand Down
Loading
Loading