Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
77 changes: 77 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,77 @@
// 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)

// CalculateExponential 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 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 CalculateExponential(
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) // range is [0, MaxUint64]
denom.SetUint64(denominator) // range is [0, MaxUint64]

i.SetOne()
numeratorAccum.SetUint64(factor) // range is [0, MaxUint64]
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.CalculateExponential(uint64(minPrice), uint64(excess), uint64(excessConversionConstant)))
}
66 changes: 20 additions & 46 deletions vms/evm/acp176/acp176.go → vms/evm/upgrades/acp176/acp176.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +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"

safemath "github.com/ava-labs/avalanchego/utils/math"
"github.com/ava-labs/avalanchego/vms/evm/upgrades/common"
)

const (
Expand All @@ -41,7 +39,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 = common.TargetExcessParams{
MinTarget: MinTargetPerSecond,
TargetConversion: TargetConversion,
MaxExcessDiff: MaxTargetExcessDiff,
MaxExcess: maxTargetExcess,
}
)

// State represents the current state of the gas pricing and constraints.
type State struct {
Expand Down Expand Up @@ -74,34 +81,30 @@ 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.CalculateTarget(uint64(s.TargetExcess)))
}

// MaxCapacity returns the maximum possible accrued gas capacity, `C`.
func (s *State) MaxCapacity() gas.Gas {
targetPerSecond := s.Target()
return mulWithUpperBound(targetPerSecond, TargetToMaxCapacity)
return gas.Gas(common.MulWithUpperBound(uint64(targetPerSecond), TargetToMaxCapacity))
}

// GasPrice returns the current required fee per gas.
//
// GasPrice = MinGasPrice * e^(Excess / (Target() * TargetToPriceUpdateConversion))
func (s *State) GasPrice() gas.Price {
targetPerSecond := s.Target()
priceUpdateConversion := mulWithUpperBound(targetPerSecond, TargetToPriceUpdateConversion) // K
return gas.CalculatePrice(MinGasPrice, s.Gas.Excess, priceUpdateConversion)
priceUpdateConversion := common.MulWithUpperBound(uint64(targetPerSecond), TargetToPriceUpdateConversion) // K
return gas.CalculatePrice(MinGasPrice, s.Gas.Excess, gas.Gas(priceUpdateConversion))
}

// AdvanceTime increases the gas capacity and decreases the gas excess based on
// the elapsed seconds.
func (s *State) AdvanceTime(seconds uint64) {
targetPerSecond := s.Target()
maxPerSecond := mulWithUpperBound(targetPerSecond, TargetToMax) // R
maxCapacity := mulWithUpperBound(maxPerSecond, TimeToFillCapacity) // C
maxPerSecond := gas.Gas(common.MulWithUpperBound(uint64(targetPerSecond), TargetToMax)) // R
maxCapacity := gas.Gas(common.MulWithUpperBound(uint64(maxPerSecond), TimeToFillCapacity)) // C
s.Gas = s.Gas.AdvanceTime(
maxCapacity,
maxPerSecond,
Expand Down Expand Up @@ -145,7 +148,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.TargetExcess(uint64(s.TargetExcess), uint64(desiredTargetExcess)))
newTargetPerSecond := s.Target()
s.Gas.Excess = scaleExcess(
s.Gas.Excess,
Expand All @@ -154,7 +157,7 @@ func (s *State) UpdateTargetExcess(desiredTargetExcess gas.Gas) {
)

// Ensure the gas capacity does not exceed the maximum capacity.
newMaxCapacity := mulWithUpperBound(newTargetPerSecond, TargetToMaxCapacity) // C
newMaxCapacity := gas.Gas(common.MulWithUpperBound(uint64(newTargetPerSecond), TargetToMaxCapacity)) // C
s.Gas.Capacity = min(s.Gas.Capacity, newMaxCapacity)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this now uses golang builtin min, would it be a problem? cc @StephenButtolph

}

Expand All @@ -170,26 +173,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.DesiredTargetExcess(uint64(desiredTarget)))
}

// scaleExcess scales the excess during gas target modifications to keep the
Expand All @@ -213,13 +197,3 @@ func scaleExcess(
}
return gas.Gas(bigExcess.Uint64())
}

// mulWithUpperBound multiplies two numbers and returns the result. If the
// result overflows, it returns [math.MaxUint64].
func mulWithUpperBound(a, b gas.Gas) gas.Gas {
product, err := safemath.Mul(a, b)
if err != nil {
return math.MaxUint64
}
return product
}
File renamed without changes.
40 changes: 12 additions & 28 deletions vms/evm/acp226/acp226.go → vms/evm/upgrades/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/upgrades/common"

const (
// MinDelayMilliseconds (M) is the minimum block delay in milliseconds
Expand All @@ -24,24 +18,28 @@ const (
maxDelayExcess = 46_516_320 // ConversionRate * ln(MaxUint64 / MinDelayMilliseconds) + 1
)

// acp226Params is the params used for the acp226 upgrade.
var acp226Params = common.TargetExcessParams{
MinTarget: MinDelayMilliseconds,
TargetConversion: ConversionRate,
MaxExcessDiff: MaxDelayExcessDiff,
MaxExcess: maxDelayExcess,
}

// DelayExcess represents the excess for delay calculation in the dynamic minimum block delay mechanism.
type DelayExcess uint64

// Delay returns the minimum block delay in milliseconds, `m`.
//
// Delay = MinDelayMilliseconds * e^(DelayExcess / ConversionRate)
func (t DelayExcess) Delay() uint64 {
return uint64(gas.CalculatePrice(
MinDelayMilliseconds,
gas.Gas(t),
ConversionRate,
))
return acp226Params.CalculateTarget(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 uint64) {
*t = DelayExcess(calculateDelayExcess(uint64(*t), desiredDelayExcess))
*t = DelayExcess(acp226Params.TargetExcess(uint64(*t), desiredDelayExcess))
}

// DesiredDelayExcess calculates the optimal delay excess given the desired
Expand All @@ -50,19 +48,5 @@ func DesiredDelayExcess(desiredDelay uint64) uint64 {
// 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 uint64(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 uint64) uint64 {
change := safemath.AbsDiff(excess, desired)
change = min(change, MaxDelayExcessDiff)
if excess < desired {
return excess + change
}
return excess - change
return acp226Params.DesiredTargetExcess(desiredDelay)
}
File renamed without changes.
Loading
Loading