diff --git a/utils/math/exponential.go b/utils/math/exponential.go new file mode 100644 index 000000000000..12ccba626cc0 --- /dev/null +++ b/utils/math/exponential.go @@ -0,0 +1,63 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package math + +import ( + "math" + + "github.com/holiman/uint256" +) + +var max256Uint64 = new(uint256.Int).SetUint64(math.MaxUint64) + +// ApproximateExponential implements the EIP-4844 fake exponential formula and +// returns the approximate exponential result given the factor, the numerator, and the denominator. +// +// It is defined as an approximation of: +// +// factor * e^(numerator / denominator) +// +// This implementation is optimized with the knowledge that any value greater +// than MaxUint64 gets returned as MaxUint64. This means that every intermediate +// value is guaranteed to be at most MaxUint193. So, we can safely use +// uint256.Int. +// +// This function does not perform any memory allocations. +func ApproximateExponential( + factor uint64, + numerator uint64, + denominator uint64, +) uint64 { + var ( + num uint256.Int + denom uint256.Int + + i uint256.Int + output uint256.Int + numeratorAccum uint256.Int + + maxOutput uint256.Int + ) + num.SetUint64(numerator) + denom.SetUint64(denominator) + + i.SetOne() + numeratorAccum.SetUint64(factor) + numeratorAccum.Mul(&numeratorAccum, &denom) // range is [0, MaxUint128] + + maxOutput.Mul(&denom, max256Uint64) // range is [0, MaxUint128] + for numeratorAccum.Sign() > 0 { + output.Add(&output, &numeratorAccum) // range is [0, MaxUint192+MaxUint128] + if output.Cmp(&maxOutput) >= 0 { + return math.MaxUint64 + } + // maxOutput < MaxUint128 so numeratorAccum < MaxUint128. + numeratorAccum.Mul(&numeratorAccum, &num) // range is [0, MaxUint192] + numeratorAccum.Div(&numeratorAccum, &denom) + numeratorAccum.Div(&numeratorAccum, &i) + + i.AddUint64(&i, 1) + } + return output.Div(&output, &denom).Uint64() +} diff --git a/vms/components/gas/gas.go b/vms/components/gas/gas.go index ef8d63963596..3e942c9cbe7b 100644 --- a/vms/components/gas/gas.go +++ b/vms/components/gas/gas.go @@ -6,13 +6,9 @@ package gas import ( "math" - "github.com/holiman/uint256" - safemath "github.com/ava-labs/avalanchego/utils/math" ) -var maxUint64 = new(uint256.Int).SetUint64(math.MaxUint64) - type ( Gas uint64 Price uint64 @@ -57,65 +53,10 @@ func (g Gas) SubOverTime(gasRate Gas, duration uint64) Gas { // CalculatePrice returns the gas price given the minimum gas price, the // excess gas, and the excess conversion constant. -// -// It is defined as an approximation of: -// -// minPrice * e^(excess / excessConversionConstant) -// -// This implements the EIP-4844 fake exponential formula: -// -// def fake_exponential(factor: int, numerator: int, denominator: int) -> int: -// i = 1 -// output = 0 -// numerator_accum = factor * denominator -// while numerator_accum > 0: -// output += numerator_accum -// numerator_accum = (numerator_accum * numerator) // (denominator * i) -// i += 1 -// return output // denominator -// -// This implementation is optimized with the knowledge that any value greater -// than MaxUint64 gets returned as MaxUint64. This means that every intermediate -// value is guaranteed to be at most MaxUint193. So, we can safely use -// uint256.Int. -// -// This function does not perform any memory allocations. -// -//nolint:dupword // The python is copied from the EIP-4844 specification func CalculatePrice( minPrice Price, excess Gas, excessConversionConstant Gas, ) Price { - var ( - numerator uint256.Int - denominator uint256.Int - - i uint256.Int - output uint256.Int - numeratorAccum uint256.Int - - maxOutput uint256.Int - ) - numerator.SetUint64(uint64(excess)) // range is [0, MaxUint64] - denominator.SetUint64(uint64(excessConversionConstant)) // range is [0, MaxUint64] - - i.SetOne() - numeratorAccum.SetUint64(uint64(minPrice)) // range is [0, MaxUint64] - numeratorAccum.Mul(&numeratorAccum, &denominator) // range is [0, MaxUint128] - - maxOutput.Mul(&denominator, maxUint64) // range is [0, MaxUint128] - for numeratorAccum.Sign() > 0 { - output.Add(&output, &numeratorAccum) // range is [0, MaxUint192+MaxUint128] - if output.Cmp(&maxOutput) >= 0 { - return math.MaxUint64 - } - // maxOutput < MaxUint128 so numeratorAccum < MaxUint128. - numeratorAccum.Mul(&numeratorAccum, &numerator) // range is [0, MaxUint192] - numeratorAccum.Div(&numeratorAccum, &denominator) - numeratorAccum.Div(&numeratorAccum, &i) - - i.AddUint64(&i, 1) - } - return Price(output.Div(&output, &denominator).Uint64()) + return Price(safemath.ApproximateExponential(uint64(minPrice), uint64(excess), uint64(excessConversionConstant))) } diff --git a/vms/evm/acp176/acp176.go b/vms/evm/acp176/acp176.go index 66a58a427547..45e0de849907 100644 --- a/vms/evm/acp176/acp176.go +++ b/vms/evm/acp176/acp176.go @@ -11,12 +11,12 @@ import ( "fmt" "math" "math/big" - "sort" "github.com/holiman/uint256" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/evm/excess" safemath "github.com/ava-labs/avalanchego/utils/math" ) @@ -41,7 +41,16 @@ const ( maxTargetExcess = 1_024_950_627 // TargetConversion * ln(MaxUint64 / MinTargetPerSecond) + 1 ) -var ErrStateInsufficientLength = errors.New("insufficient length for fee state") +var ( + ErrStateInsufficientLength = errors.New("insufficient length for fee state") + + acp176Params = excess.Params{ + MinValue: MinTargetPerSecond, // P + ConversionRate: TargetConversion, // D + MaxExcessDiff: MaxTargetExcessDiff, // Q + MaxExcess: maxTargetExcess, + } +) // State represents the current state of the gas pricing and constraints. type State struct { @@ -74,11 +83,7 @@ func ParseState(bytes []byte) (State, error) { // // Target = MinTargetPerSecond * e^(TargetExcess / TargetConversion) func (s *State) Target() gas.Gas { - return gas.Gas(gas.CalculatePrice( - MinTargetPerSecond, - s.TargetExcess, - TargetConversion, - )) + return gas.Gas(acp176Params.CalculateValue(uint64(s.TargetExcess))) } // MaxCapacity returns the maximum possible accrued gas capacity, `C`. @@ -163,7 +168,7 @@ func (s *State) ConsumeGas( // desiredTargetExcess without exceeding the maximum targetExcess change. func (s *State) UpdateTargetExcess(desiredTargetExcess gas.Gas) { previousTargetPerSecond := s.Target() - s.TargetExcess = targetExcess(s.TargetExcess, desiredTargetExcess) + s.TargetExcess = gas.Gas(acp176Params.AdjustExcess(uint64(s.TargetExcess), uint64(desiredTargetExcess))) newTargetPerSecond := s.Target() s.Gas.Excess = scaleExcess( s.Gas.Excess, @@ -188,26 +193,7 @@ func (s *State) Bytes() []byte { // DesiredTargetExcess calculates the optimal desiredTargetExcess given the // desired target. func DesiredTargetExcess(desiredTarget gas.Gas) gas.Gas { - // This could be solved directly by calculating D * ln(desiredTarget / P) - // using floating point math. However, it introduces inaccuracies. So, we - // use a binary search to find the closest integer solution. - return gas.Gas(sort.Search(maxTargetExcess, func(targetExcessGuess int) bool { - state := State{ - TargetExcess: gas.Gas(targetExcessGuess), - } - return state.Target() >= desiredTarget - })) -} - -// targetExcess calculates the optimal new targetExcess for a block proposer to -// include given the current and desired excess values. -func targetExcess(excess, desired gas.Gas) gas.Gas { - change := safemath.AbsDiff(excess, desired) - change = min(change, MaxTargetExcessDiff) - if excess < desired { - return excess + change - } - return excess - change + return gas.Gas(acp176Params.DesiredExcess(uint64(desiredTarget))) } // scaleExcess scales the excess during gas target modifications to keep the diff --git a/vms/evm/acp226/acp226.go b/vms/evm/acp226/acp226.go index bb36dfc99667..816104d256f9 100644 --- a/vms/evm/acp226/acp226.go +++ b/vms/evm/acp226/acp226.go @@ -5,13 +5,7 @@ // https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/226-dynamic-minimum-block-times/README.md package acp226 -import ( - "sort" - - "github.com/ava-labs/avalanchego/vms/components/gas" - - safemath "github.com/ava-labs/avalanchego/utils/math" -) +import "github.com/ava-labs/avalanchego/vms/evm/excess" const ( // MinDelayMilliseconds (M) is the minimum block delay in milliseconds @@ -28,6 +22,14 @@ const ( maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1 ) +// acp226Params is the params used for the acp226 upgrade. +var acp226Params = excess.Params{ + MinValue: MinDelayMilliseconds, // M + ConversionRate: ConversionRate, // D + MaxExcessDiff: MaxDelayExcessDiff, // Q + MaxExcess: maxDelayExcess, +} + // DelayExcess represents the excess for delay calculation in the dynamic // minimum block delay mechanism. type DelayExcess uint64 @@ -36,38 +38,17 @@ type DelayExcess uint64 // // Delay = MinDelayMilliseconds * e^(DelayExcess / ConversionRate) func (t DelayExcess) Delay() uint64 { - return uint64(gas.CalculatePrice( - MinDelayMilliseconds, - gas.Gas(t), - ConversionRate, - )) + return acp226Params.CalculateValue(uint64(t)) } // UpdateDelayExcess updates the DelayExcess to be as close as possible to the // desiredDelayExcess without exceeding the maximum DelayExcess change. func (t *DelayExcess) UpdateDelayExcess(desiredDelayExcess DelayExcess) { - *t = calculateDelayExcess(*t, desiredDelayExcess) + *t = DelayExcess(acp226Params.AdjustExcess(uint64(*t), uint64(desiredDelayExcess))) } // DesiredDelayExcess calculates the optimal delay excess given the desired -// delay in milliseconds. +// delay. func DesiredDelayExcess(desiredDelay uint64) DelayExcess { - // This could be solved directly by calculating D * ln(desired / M) - // using floating point math. However, it introduces inaccuracies. So, we - // use a binary search to find the closest integer solution. - return DelayExcess(sort.Search(maxDelayExcess, func(delayExcessGuess int) bool { - excess := DelayExcess(delayExcessGuess) - return excess.Delay() >= desiredDelay - })) -} - -// calculateDelayExcess calculates the optimal new DelayExcess for a block -// proposer to include given the current and desired excess values. -func calculateDelayExcess(excess, desired DelayExcess) DelayExcess { - change := safemath.AbsDiff(excess, desired) - change = min(change, MaxDelayExcessDiff) - if excess < desired { - return excess + change - } - return excess - change + return DelayExcess(acp226Params.DesiredExcess(desiredDelay)) } diff --git a/vms/evm/excess/params.go b/vms/evm/excess/params.go new file mode 100644 index 000000000000..7ccfc4dbfac0 --- /dev/null +++ b/vms/evm/excess/params.go @@ -0,0 +1,51 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package excess + +import ( + "sort" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +// Params contains the parameters needed for excess calculations. +type Params struct { + MinValue uint64 // Minimum value + ConversionRate uint64 // Conversion factor for exponential calculations + MaxExcessDiff uint64 // Maximum change in excess per update + MaxExcess uint64 // Maximum possible excess value +} + +// AdjustExcess calculates the optimal new excess given the current and desired excess values, +// ensuring the change does not exceed the maximum excess difference. +func (p Params) AdjustExcess(excess, desired uint64) uint64 { + change := safemath.AbsDiff(excess, desired) + change = min(change, p.MaxExcessDiff) + if excess < desired { + return excess + change + } + return excess - change +} + +// DesiredExcess calculates the optimal desiredExcess given the +// desired value using binary search. +func (p Params) DesiredExcess(desiredValue uint64) uint64 { + // This could be solved directly by calculating ConversionRate * ln(desiredValue / MinValue) + // using floating point math. However, it introduces inaccuracies. So, we + // use a binary search to find the closest integer solution. + return uint64(sort.Search(int(p.MaxExcess), func(targetExcessGuess int) bool { + calculatedValue := p.CalculateValue(uint64(targetExcessGuess)) + return calculatedValue >= desiredValue + })) +} + +// CalculateValue calculates the value using exponential formula: +// Value = MinValue * e^(Excess / ConversionRate) +func (p Params) CalculateValue(excess uint64) uint64 { + return safemath.ApproximateExponential( + p.MinValue, + excess, + p.ConversionRate, + ) +}