|
| 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