Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
69 changes: 69 additions & 0 deletions store/types/proxygasmeter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package types

import (
fmt "fmt"
)

var _ GasMeter = &ProxyGasMeter{}

// ProxyGasMeter wraps another GasMeter, but enforces a lower gas limit.
// Gas consumption is delegated to the wrapped GasMeter, so it won't risk losing gas accounting compared to standalone
// gas meter.
type ProxyGasMeter struct {
GasMeter

limit Gas
}

// NewProxyGasMeter returns a new GasMeter which wraps the provided gas meter.
// The remaining is the maximum gas that can be consumed on top of current consumed
// gas of the wrapped gas meter.
//
// If the new remaining is greater than or equal to the existing remaining gas, no wrapping is needed
// and the original gas meter is returned.
func NewProxyGasMeter(gasMeter GasMeter, remaining Gas) GasMeter {
if remaining >= gasMeter.GasRemaining() {
return gasMeter
}

return &ProxyGasMeter{
GasMeter: gasMeter,
limit: remaining + gasMeter.GasConsumed(),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against uint64 overflow when computing the absolute limit.

If the wrapped meter has very high consumption (e.g., infinite meter), consumed + remaining can overflow and silently wrap. Panic consistently with ConsumeGas on overflow.

 func NewProxyGasMeter(gasMeter GasMeter, remaining Gas) GasMeter {
 	if remaining >= gasMeter.GasRemaining() {
 		return gasMeter
 	}
 
-	return &ProxyGasMeter{
-		GasMeter: gasMeter,
-		limit:    remaining + gasMeter.GasConsumed(),
-	}
+	limit, overflow := addUint64Overflow(gasMeter.GasConsumed(), remaining)
+	if overflow {
+		panic(ErrorGasOverflow{Descriptor: "NewProxyGasMeter"})
+	}
+	return &ProxyGasMeter{
+		GasMeter: gasMeter,
+		limit:    limit,
+	}
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In store/types/proxygasmeter.go around lines 24 to 33, the computation limit :=
remaining + gasMeter.GasConsumed() can overflow uint64; detect this case before
summing and panic in the same way ConsumeGas does on overflow. Specifically,
fetch consumed := gasMeter.GasConsumed(), check if consumed > math.MaxUint64 -
remaining (import math or use ^uint64(0)), and if so invoke panic("gas
overflow") (or the exact ConsumeGas overflow message used elsewhere); otherwise
set limit = remaining + consumed and return the ProxyGasMeter.


func (pgm ProxyGasMeter) GasRemaining() Gas {
if pgm.IsPastLimit() {
return 0
}
return pgm.limit - pgm.GasConsumed()
}

func (pgm ProxyGasMeter) Limit() Gas {
return pgm.limit
}

func (pgm ProxyGasMeter) IsPastLimit() bool {
return pgm.GasConsumed() > pgm.limit
}

func (pgm ProxyGasMeter) IsOutOfGas() bool {
return pgm.GasConsumed() >= pgm.limit
}

func (pgm ProxyGasMeter) ConsumeGas(amount Gas, descriptor string) {
consumed, overflow := addUint64Overflow(pgm.GasConsumed(), amount)
if overflow {
panic(ErrorGasOverflow{Descriptor: descriptor})
}

if consumed > pgm.limit {
panic(ErrorOutOfGas{Descriptor: descriptor})
}

pgm.GasMeter.ConsumeGas(amount, descriptor)
}

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

import (
math "math"
"testing"

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

func TestProxyGasMeterBasic(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(700), bgm.GasRemaining())

pgm.RefundGas(1, "test")
require.Equal(t, Gas(299), pgm.GasConsumed())
require.Equal(t, Gas(1), pgm.GasRemaining())
require.False(t, pgm.IsOutOfGas())
require.False(t, pgm.IsPastLimit())
}

func TestProxyGasMeterInfinit(t *testing.T) {
limit := uint64(300)

bgm := NewInfiniteGasMeter()
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(math.MaxUint64), bgm.GasRemaining())
require.True(t, pgm.IsOutOfGas())
require.False(t, pgm.IsPastLimit())

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

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

// WithGasRemaining returns a Context with a lower remaining gas,
// it's used to execute sub-calls with a lower gas limit.
// the gas consumption is still tracked by the parent gas meter in realtime,
// there's no risk of losing gas accounting.
func (c Context) WithGasRemaining(remaining storetypes.Gas) Context {
return c.WithGasMeter(storetypes.NewProxyGasMeter(c.GasMeter(), remaining))
}

// WithBlockGasMeter returns a Context with an updated block GasMeter
func (c Context) WithBlockGasMeter(meter storetypes.GasMeter) Context {
c.blockGasMeter = meter
Expand Down
Loading