Skip to content

Commit 287d07f

Browse files
author
HuangYi
committed
feat: support executing sub-calls with a specific gas limit
- It's a common pattern to execute a sub-call with a lower gas limit than the whole tx gas limit, this PR introduce a primitive method in `Context` for that use case.
1 parent b070d0b commit 287d07f

File tree

5 files changed

+165
-0
lines changed

5 files changed

+165
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
5555
* (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).
5656
* (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.
5757
* (abci_utils) [#25008](https://github.com/cosmos/cosmos-sdk/pull/25008) add the ability to assign a custom signer extraction adapter in `DefaultProposalHandler`.
58+
* (context) [#]() Add `WithGasRemaining` to execute sub-calls with a specific gas limit.
5859

5960
### Improvements
6061

store/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
2929

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

32+
### Features
33+
34+
* [#]() Implement ProxyGasMeter to execute sub-calls with a different gas limit.
35+
3236
## v1.1.2 (March 31, 2025)
3337

3438
### Bug Fixes

store/types/proxygasmeter.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package types
2+
3+
import (
4+
fmt "fmt"
5+
)
6+
7+
var _ GasMeter = &ProxyGasMeter{}
8+
9+
// ProxyGasMeter wraps another GasMeter, but enforces a lower gas limit.
10+
// Gas consumption is delegated to the wrapped GasMeter, so it won't risk losing gas accounting compared to standalone
11+
// gas meter.
12+
type ProxyGasMeter struct {
13+
GasMeter
14+
15+
limit Gas
16+
}
17+
18+
// NewProxyGasMeter returns a new GasMeter which wraps the provided gas meter.
19+
// The remaining is the maximum gas that can be consumed on top of current consumed
20+
// gas of the wrapped gas meter.
21+
//
22+
// If the new remaining is greater than or equal to the existing remaining gas, no wrapping is needed
23+
// and the original gas meter is returned.
24+
func NewProxyGasMeter(gasMeter GasMeter, remaining Gas) GasMeter {
25+
if remaining >= gasMeter.GasRemaining() {
26+
return gasMeter
27+
}
28+
29+
return &ProxyGasMeter{
30+
GasMeter: gasMeter,
31+
limit: remaining + gasMeter.GasConsumed(),
32+
}
33+
}
34+
35+
func (pgm ProxyGasMeter) GasRemaining() Gas {
36+
if pgm.IsPastLimit() {
37+
return 0
38+
}
39+
return pgm.limit - pgm.GasConsumed()
40+
}
41+
42+
func (pgm ProxyGasMeter) Limit() Gas {
43+
return pgm.limit
44+
}
45+
46+
func (pgm ProxyGasMeter) IsPastLimit() bool {
47+
return pgm.GasConsumed() > pgm.limit
48+
}
49+
50+
func (pgm ProxyGasMeter) IsOutOfGas() bool {
51+
return pgm.GasConsumed() >= pgm.limit
52+
}
53+
54+
func (pgm ProxyGasMeter) ConsumeGas(amount Gas, descriptor string) {
55+
consumed, overflow := addUint64Overflow(pgm.GasMeter.GasConsumed(), amount)
56+
if overflow {
57+
panic(ErrorGasOverflow{Descriptor: descriptor})
58+
}
59+
60+
if consumed > pgm.limit {
61+
panic(ErrorOutOfGas{Descriptor: descriptor})
62+
}
63+
64+
pgm.GasMeter.ConsumeGas(amount, descriptor)
65+
}
66+
67+
func (pgm ProxyGasMeter) String() string {
68+
return fmt.Sprintf("ProxyGasMeter{consumed: %d, limit: %d}", pgm.GasConsumed(), pgm.limit)
69+
}

store/types/proxygasmeter_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package types
2+
3+
import (
4+
math "math"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestProxyGasMeterBasic(t *testing.T) {
11+
baseGas := uint64(1000)
12+
limit := uint64(300)
13+
14+
bgm := NewGasMeter(baseGas)
15+
pgm := NewProxyGasMeter(bgm, limit)
16+
17+
require.Equal(t, Gas(0), pgm.GasConsumed())
18+
require.Equal(t, limit, pgm.Limit())
19+
require.Equal(t, limit, pgm.GasRemaining())
20+
21+
pgm.ConsumeGas(100, "test")
22+
require.Equal(t, Gas(100), pgm.GasConsumed())
23+
require.Equal(t, Gas(100), bgm.GasConsumed())
24+
require.Equal(t, limit-100, pgm.GasRemaining())
25+
require.False(t, pgm.IsOutOfGas())
26+
require.False(t, pgm.IsPastLimit())
27+
28+
pgm.ConsumeGas(200, "test")
29+
require.Equal(t, Gas(300), pgm.GasConsumed())
30+
require.Equal(t, Gas(300), bgm.GasConsumed())
31+
require.Equal(t, Gas(0), pgm.GasRemaining())
32+
require.Equal(t, Gas(700), bgm.GasRemaining())
33+
require.True(t, pgm.IsOutOfGas())
34+
require.False(t, pgm.IsPastLimit())
35+
36+
require.Panics(t, func() {
37+
pgm.ConsumeGas(1, "test")
38+
})
39+
require.Equal(t, Gas(700), bgm.GasRemaining())
40+
41+
pgm.RefundGas(1, "test")
42+
require.Equal(t, Gas(299), pgm.GasConsumed())
43+
require.Equal(t, Gas(1), pgm.GasRemaining())
44+
require.False(t, pgm.IsOutOfGas())
45+
require.False(t, pgm.IsPastLimit())
46+
}
47+
48+
func TestProxyGasMeterInfinit(t *testing.T) {
49+
limit := uint64(300)
50+
51+
bgm := NewInfiniteGasMeter()
52+
pgm := NewProxyGasMeter(bgm, limit)
53+
54+
require.Equal(t, Gas(0), pgm.GasConsumed())
55+
require.Equal(t, limit, pgm.Limit())
56+
require.Equal(t, limit, pgm.GasRemaining())
57+
58+
pgm.ConsumeGas(100, "test")
59+
require.Equal(t, Gas(100), pgm.GasConsumed())
60+
require.Equal(t, Gas(100), bgm.GasConsumed())
61+
require.Equal(t, limit-100, pgm.GasRemaining())
62+
require.False(t, pgm.IsOutOfGas())
63+
require.False(t, pgm.IsPastLimit())
64+
65+
pgm.ConsumeGas(200, "test")
66+
require.Equal(t, Gas(300), pgm.GasConsumed())
67+
require.Equal(t, Gas(300), bgm.GasConsumed())
68+
require.Equal(t, Gas(0), pgm.GasRemaining())
69+
require.Equal(t, Gas(math.MaxUint64), bgm.GasRemaining())
70+
require.True(t, pgm.IsOutOfGas())
71+
require.False(t, pgm.IsPastLimit())
72+
73+
require.Panics(t, func() {
74+
pgm.ConsumeGas(1, "test")
75+
})
76+
require.Equal(t, Gas(math.MaxUint64), bgm.GasRemaining())
77+
78+
pgm.RefundGas(1, "test")
79+
require.Equal(t, Gas(299), pgm.GasConsumed())
80+
require.Equal(t, Gas(1), pgm.GasRemaining())
81+
require.False(t, pgm.IsOutOfGas())
82+
require.False(t, pgm.IsPastLimit())
83+
}

types/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ func (c Context) WithGasMeter(meter storetypes.GasMeter) Context {
224224
return c
225225
}
226226

227+
// WithGasRemaining returns a Context with a lower remaining gas,
228+
// it's used to execute sub-calls with a lower gas limit.
229+
// the gas consumption is still tracked by the parent gas meter in realtime,
230+
// there's no risk of losing gas accounting.
231+
func (c Context) WithGasRemaining(remaining storetypes.Gas) Context {
232+
return c.WithGasMeter(storetypes.NewProxyGasMeter(c.GasMeter(), remaining))
233+
}
234+
227235
// WithBlockGasMeter returns a Context with an updated block GasMeter
228236
func (c Context) WithBlockGasMeter(meter storetypes.GasMeter) Context {
229237
c.blockGasMeter = meter

0 commit comments

Comments
 (0)