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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (server) [#24720](https://github.com/cosmos/cosmos-sdk/pull/24720) add `verbose_log_level` flag for configuring the log level when switching to verbose logging mode during sensitive operations (such as chain upgrades).
* (crypto) [#24861](https://github.com/cosmos/cosmos-sdk/pull/24861) add `PubKeyFromCometTypeAndBytes` helper function to convert from `comet/v2` PubKeys to the `cryptotypes.Pubkey` interface.
* (abci_utils) [#25008](https://github.com/cosmos/cosmos-sdk/pull/25008) add the ability to assign a custom signer extraction adapter in `DefaultProposalHandler`.
* (context) [#25303](https://github.com/cosmos/cosmos-sdk/pull/25303) Add `WithGasRemaining` to execute sub-calls with a specific gas limit.

### Improvements

Expand Down
4 changes: 4 additions & 0 deletions store/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Ref: https://keepachangelog.com/en/1.0.0/

* [#20425](https://github.com/cosmos/cosmos-sdk/pull/20425) Fix nil pointer panic when querying historical state where a new store does not exist.

### Features

* [#25303](https://github.com/cosmos/cosmos-sdk/pull/25303) Implement ProxyGasMeter to execute sub-calls with a different gas limit.

## v1.1.2 (March 31, 2025)

### Bug Fixes
Expand Down
46 changes: 46 additions & 0 deletions store/types/proxygasmeter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package types

import "fmt"

var _ GasMeter = &ProxyGasMeter{}

// ProxyGasMeter is like a basicGasMeter, but delegates the gas changes (refund and consume) to the parent GasMeter in
// realtime, ensuring accurate gas accounting even during panics.
type ProxyGasMeter struct {
GasMeter

parent GasMeter
}

// NewProxyGasMeter creates a ProxyGasMeter that wraps a parent gas meter, inheriting the minimum of the new limit and the parent's remaining gas.
// It delegates consumption to the parent in real time, ensuring accurate gas accounting even during panics.
//
// Returns the parent directly if the new limit is greater than or equal to its remaining gas.
func NewProxyGasMeter(gasMeter GasMeter, limit Gas) GasMeter {
limit = min(limit, gasMeter.GasRemaining())
return &ProxyGasMeter{
GasMeter: NewGasMeter(limit),
parent: gasMeter,
}
}

// RefundGas will also refund gas to parent gas meter.
func (pgm ProxyGasMeter) RefundGas(amount Gas, descriptor string) {
pgm.parent.RefundGas(amount, descriptor)
pgm.GasMeter.RefundGas(amount, descriptor)
}

// ConsumeGas will also consume gas from parent gas meter.
//
// it consume sub-gasmeter first, which means if sub-gasmeter runs out of gas,
// the gas is not charged in parent gas meter, the assumption for business logic
// is the gas is always charged before the work is done, so when out-of-gas panic happens,
// the actual work is not done yet, so we don't need to consume the gas in parent gas meter.
func (pgm ProxyGasMeter) ConsumeGas(amount Gas, descriptor string) {
pgm.parent.ConsumeGas(amount, descriptor)
pgm.GasMeter.ConsumeGas(amount, descriptor)
}

func (pgm ProxyGasMeter) String() string {
return fmt.Sprintf("ProxyGasMeter{consumed: %d, limit: %d}", pgm.GasConsumed(), pgm.Limit())
}
45 changes: 45 additions & 0 deletions store/types/proxygasmeter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package types

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestProxyGasMeter(t *testing.T) {
baseGas := uint64(1000)
limit := uint64(300)

bgm := NewGasMeter(baseGas)
pgm := NewProxyGasMeter(bgm, limit)

require.Equal(t, Gas(0), pgm.GasConsumed())
require.Equal(t, limit, pgm.Limit())
require.Equal(t, limit, pgm.GasRemaining())

pgm.ConsumeGas(100, "test")
require.Equal(t, Gas(100), pgm.GasConsumed())
require.Equal(t, Gas(100), bgm.GasConsumed())
require.Equal(t, limit-100, pgm.GasRemaining())
require.False(t, pgm.IsOutOfGas())
require.False(t, pgm.IsPastLimit())

pgm.ConsumeGas(200, "test")
require.Equal(t, Gas(300), pgm.GasConsumed())
require.Equal(t, Gas(300), bgm.GasConsumed())
require.Equal(t, Gas(0), pgm.GasRemaining())
require.Equal(t, Gas(700), bgm.GasRemaining())
require.True(t, pgm.IsOutOfGas())
require.False(t, pgm.IsPastLimit())

require.Panics(t, func() {
pgm.ConsumeGas(1, "test")
})
require.Equal(t, Gas(699), bgm.GasRemaining())

pgm.RefundGas(1, "test")
require.Equal(t, Gas(300), pgm.GasConsumed())
require.Equal(t, Gas(0), pgm.GasRemaining())
require.True(t, pgm.IsOutOfGas())
require.False(t, pgm.IsPastLimit())
}
9 changes: 9 additions & 0 deletions types/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ func (c Context) WithGasMeter(meter storetypes.GasMeter) Context {
return c
}

// WithGasLimit replaces the GasMeter with a ProxyGasMeter whose gas limit is set to the minimal of the
// given limit and the remaining gas in the current GasMeter.
//
// ProxyGasMeter will delegate the gas consumption to the parent gas meter in realtime, so there's no need to write
// back the gas consumed to the parent gas meter when the sub-GasMeter is done.
func (c Context) WithGasLimit(limit storetypes.Gas) Context {
return c.WithGasMeter(storetypes.NewProxyGasMeter(c.GasMeter(), limit))
}

// WithBlockGasMeter returns a Context with an updated block GasMeter
func (c Context) WithBlockGasMeter(meter storetypes.GasMeter) Context {
c.blockGasMeter = meter
Expand Down
49 changes: 49 additions & 0 deletions types/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,52 @@ func (s *contextTestSuite) TestUnwrapSDKContext() {
sdkCtx2 = types.UnwrapSDKContext(ctx)
s.Require().Equal(sdkCtx, sdkCtx2)
}

func (s *contextTestSuite) TestProxyGasMeter() {
ctx := types.NewContext(nil, cmtproto.Header{}, false, nil).WithGasMeter(storetypes.NewGasMeter(20))
s.Require().EqualValues(20, ctx.GasMeter().Limit())

ctx.GasMeter().ConsumeGas(5, "test")
s.Require().EqualValues(5, ctx.GasMeter().GasConsumed())
s.Require().EqualValues(15, ctx.GasMeter().GasRemaining())

{
ctx := ctx.WithGasLimit(10)
s.Require().EqualValues(10, ctx.GasMeter().Limit())

ctx.GasMeter().ConsumeGas(5, "test")
s.Require().EqualValues(5, ctx.GasMeter().GasConsumed())

s.Require().Panics(func() {
ctx.GasMeter().ConsumeGas(6, "test")
})
s.Require().EqualValues(11, ctx.GasMeter().GasConsumed())
}

s.Require().EqualValues(10, ctx.GasMeter().GasConsumed())
s.Require().EqualValues(10, ctx.GasMeter().GasRemaining())

{
ctx := ctx.WithGasLimit(5)
s.Require().EqualValues(5, ctx.GasMeter().Limit())

s.Require().Panics(func() {
ctx.GasMeter().ConsumeGas(6, "test")
})
s.Require().EqualValues(6, ctx.GasMeter().GasConsumed())
}

s.Require().EqualValues(10, ctx.GasMeter().GasConsumed())
s.Require().EqualValues(10, ctx.GasMeter().GasRemaining())

{
ctx := ctx.WithGasLimit(15)
s.Require().EqualValues(10, ctx.GasMeter().Limit())

ctx.GasMeter().ConsumeGas(10, "test")
s.Require().EqualValues(10, ctx.GasMeter().GasConsumed())
}

s.Require().EqualValues(20, ctx.GasMeter().GasConsumed())
s.Require().EqualValues(0, ctx.GasMeter().GasRemaining())
}
Loading