Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
63 changes: 63 additions & 0 deletions utils/math/exponential.go
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add tests for this new code?

Original file line number Diff line number Diff line change
@@ -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()
}
61 changes: 1 addition & 60 deletions vms/components/gas/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
}
42 changes: 14 additions & 28 deletions vms/evm/acp176/acp176.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
45 changes: 13 additions & 32 deletions vms/evm/acp226/acp226.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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))
}
51 changes: 51 additions & 0 deletions vms/evm/excess/params.go
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add tests for new code?

Original file line number Diff line number Diff line change
@@ -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,
)
}