Skip to content

Commit 13ad8fe

Browse files
authored
feat: gastime package (#9)
Introduces the gas clock, an extension of a `proxytime.Time[gas.Gas]` that also tracks a "continuous" equivalent of ACP-176 gas excess at gas-unit resolution instead of per second. Closes #11
1 parent 3b70781 commit 13ad8fe

File tree

9 files changed

+811
-6
lines changed

9 files changed

+811
-6
lines changed

gastime/cmpopt.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
//go:build !prod && !nocmpopts
5+
6+
package gastime
7+
8+
import (
9+
"github.com/ava-labs/avalanchego/vms/components/gas"
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/google/go-cmp/cmp/cmpopts"
12+
13+
"github.com/ava-labs/strevm/proxytime"
14+
)
15+
16+
// CmpOpt returns a configuration for [cmp.Diff] to compare [Time] instances in
17+
// tests.
18+
func CmpOpt() cmp.Option {
19+
return cmp.Options{
20+
cmp.AllowUnexported(TimeMarshaler{}),
21+
cmpopts.IgnoreTypes(canotoData_TimeMarshaler{}),
22+
proxytime.CmpOpt[gas.Gas](proxytime.CmpRateInvariantsByValue),
23+
}
24+
}

gastime/gastime.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
// Package gastime measures time based on the consumption of gas.
5+
package gastime
6+
7+
import (
8+
"math"
9+
10+
"github.com/ava-labs/avalanchego/vms/components/gas"
11+
"github.com/holiman/uint256"
12+
13+
"github.com/ava-labs/strevm/intmath"
14+
"github.com/ava-labs/strevm/proxytime"
15+
)
16+
17+
// Time represents an instant in time, its passage measured in [gas.Gas]
18+
// consumption. It is not thread safe nor is the zero value valid.
19+
//
20+
// In addition to the passage of time, it also tracks excess consumption above a
21+
// target, as described in [ACP-194] as a "continuous" version of [ACP-176].
22+
//
23+
// Copying a Time, either directly or by dereferencing a pointer, will result in
24+
// undefined behaviour. Use [Time.Clone] instead as it reestablishes internal
25+
// invariants.
26+
//
27+
// [ACP-176]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/176-dynamic-evm-gas-limit-and-price-discovery-updates
28+
// [ACP-194]: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/194-streaming-asynchronous-execution
29+
type Time struct {
30+
TimeMarshaler
31+
}
32+
33+
// makeTime is a constructor shared by [New] and [Time.Clone].
34+
func makeTime(t *proxytime.Time[gas.Gas], target, excess gas.Gas) *Time {
35+
tm := &Time{
36+
TimeMarshaler: TimeMarshaler{
37+
Time: t,
38+
target: target,
39+
excess: excess,
40+
},
41+
}
42+
tm.establishInvariants()
43+
return tm
44+
}
45+
46+
func (tm *Time) establishInvariants() {
47+
tm.Time.SetRateInvariants(&tm.target, &tm.excess)
48+
}
49+
50+
// New returns a new [Time], set from a Unix timestamp. The consumption of
51+
// `target` * [TargetToRate] units of [gas.Gas] is equivalent to a tick of 1
52+
// second. Targets are clamped to [MaxTarget].
53+
func New(unixSeconds uint64, target, startingExcess gas.Gas) *Time {
54+
target = clampTarget(target)
55+
return makeTime(proxytime.New(unixSeconds, rateOf(target)), target, startingExcess)
56+
}
57+
58+
// TargetToRate is the ratio between [Time.Target] and [proxytime.Time.Rate].
59+
const TargetToRate = 2
60+
61+
// MaxTarget is the maximum allowable [Time.Target] to avoid overflows of the
62+
// associated [proxytime.Time.Rate]. Values above this are silently clamped.
63+
const MaxTarget = gas.Gas(math.MaxUint64 / TargetToRate)
64+
65+
func rateOf(target gas.Gas) gas.Gas { return target * TargetToRate }
66+
func clampTarget(t gas.Gas) gas.Gas { return min(t, MaxTarget) }
67+
func roundRate(r gas.Gas) gas.Gas { return (r / TargetToRate) * TargetToRate }
68+
69+
// Clone returns a deep copy of the time.
70+
func (tm *Time) Clone() *Time {
71+
// [proxytime.Time.Clone] explicitly does NOT clone the rate invariants, so
72+
// we reestablish them as if we were constructing a new instance.
73+
return makeTime(tm.Time.Clone(), tm.target, tm.excess)
74+
}
75+
76+
// Target returns the `T` parameter of ACP-176.
77+
func (tm *Time) Target() gas.Gas {
78+
return tm.target
79+
}
80+
81+
// Excess returns the `x` variable of ACP-176.
82+
func (tm *Time) Excess() gas.Gas {
83+
return tm.excess
84+
}
85+
86+
// Price returns the price of a unit of gas, i.e. the "base fee".
87+
func (tm *Time) Price() gas.Price {
88+
return gas.CalculatePrice(1 /* M */, tm.excess, tm.excessScalingFactor())
89+
}
90+
91+
// excessScalingFactor returns the K variable of ACP-103/176, i.e. 87*T, capped
92+
// at [math.MaxUint64].
93+
func (tm *Time) excessScalingFactor() gas.Gas {
94+
const (
95+
targetToK = 87
96+
overflowThreshold = math.MaxUint64 / targetToK
97+
)
98+
if tm.target > overflowThreshold {
99+
return math.MaxUint64
100+
}
101+
return targetToK * tm.target
102+
}
103+
104+
// BaseFee is equivalent to [Time.Price], returning the result as a uint256 for
105+
// compatibility with geth/libevm objects.
106+
func (tm *Time) BaseFee() *uint256.Int {
107+
return uint256.NewInt(uint64(tm.Price()))
108+
}
109+
110+
// SetRate changes the gas rate per second, rounding down the argument if it is
111+
// not a multiple of [TargetToRate]. See [Time.SetTarget] re potential error(s).
112+
func (tm *Time) SetRate(r gas.Gas) error {
113+
_, err := tm.TimeMarshaler.SetRate(roundRate(r))
114+
return err
115+
}
116+
117+
// SetTarget changes the target gas consumption per second, clamping the
118+
// argument to [MaxTarget]. It returns an error if the scaled [Time.Excess]
119+
// overflows as a result of the scaling.
120+
func (tm *Time) SetTarget(t gas.Gas) error {
121+
_, err := tm.TimeMarshaler.SetRate(rateOf(clampTarget(t))) // also updates [Time.Target] as it was passed to [proxytime.Time.SetRateInvariants]
122+
return err
123+
}
124+
125+
// Tick is equivalent to [proxytime.Time.Tick] except that it also updates the
126+
// gas excess.
127+
func (tm *Time) Tick(g gas.Gas) {
128+
tm.Time.Tick(g)
129+
130+
R, T := tm.Rate(), tm.Target()
131+
quo, _, _ := intmath.MulDiv(g, R-T, R) // overflow is impossible as (R-T)/R < 1
132+
tm.excess += quo
133+
}
134+
135+
// FastForwardTo is equivalent to [proxytime.Time.FastForwardTo] except that it
136+
// may also update the gas excess.
137+
func (tm *Time) FastForwardTo(to uint64) {
138+
sec, frac := tm.Time.FastForwardTo(to)
139+
if sec == 0 && frac.Numerator == 0 {
140+
return
141+
}
142+
143+
R, T := tm.Rate(), tm.Target()
144+
145+
// Excess is reduced by the amount of gas skipped (g), multiplied by T/R.
146+
// However, to avoid overflow, the implementation needs to be a bit more
147+
// complicated. The reduction in excess can be calculated as follows (math
148+
// notation, not code, and ignoring the bounding at zero):
149+
//
150+
// s := seconds fast-forwarded (`sec`)
151+
// f := `frac.Numerator`
152+
// x := excess
153+
//
154+
// dx = -g·T/R
155+
// = -(sR + f)·T/R
156+
// = -sR·T/R - fT/R
157+
// = -sT - fT/R
158+
//
159+
// Note that this is equivalent to the ACP reduction of T·dt because dt is
160+
// equal to s + f/R since `frac.Denominator == R` is a documented invariant.
161+
// Therefore dx = -(s + f/R)·T, but we separate the terms differently for
162+
// our implementation.
163+
164+
// -sT
165+
if s := gas.Gas(sec); tm.excess/T >= s { // sT <= x; division is safe because T > 0
166+
tm.excess -= s * T
167+
} else { // sT > x
168+
tm.excess = 0
169+
}
170+
171+
// -fT/R
172+
quo, _, _ := intmath.MulDiv(frac.Numerator, T, R) // overflow is impossible as T/R < 1
173+
tm.excess = intmath.BoundedSubtract(tm.excess, quo, 0)
174+
}

0 commit comments

Comments
 (0)