Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.GasMeter.GasConsumed(), amount)

Check failure on line 55 in store/types/proxygasmeter.go

View workflow job for this annotation

GitHub Actions / golangci-lint

QF1008: could remove embedded field "GasMeter" from selector (staticcheck)
if overflow {
panic(ErrorGasOverflow{Descriptor: descriptor})
}

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

pgm.GasMeter.ConsumeGas(amount, descriptor)
}

Comment on lines 39 to 43
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix CI lint failure (staticcheck QF1008) by removing the embedded selector.

This is currently failing the lint job; use the promoted method instead.

-func (pgm ProxyGasMeter) ConsumeGas(amount Gas, descriptor string) {
-	consumed, overflow := addUint64Overflow(pgm.GasMeter.GasConsumed(), amount)
+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)
 }
🧰 Tools
🪛 GitHub Check: golangci-lint

[failure] 55-55:
QF1008: could remove embedded field "GasMeter" from selector (staticcheck)

🪛 GitHub Actions: Lint

[error] 55-55: Staticcheck: QF1008 - could remove embedded field 'GasMeter' from selector.

🤖 Prompt for AI Agents
In store/types/proxygasmeter.go around lines 54 to 66, the code uses the
embedded selector (pgm.GasMeter.GasConsumed() and pgm.GasMeter.ConsumeGas(...))
which triggers staticcheck QF1008; replace those calls with the promoted methods
on the embedding type (use pgm.GasConsumed() and pgm.ConsumeGas(amount,
descriptor)) so you no longer reference the embedded field directly and the
linter error is resolved.

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