Files
zeekay ede37547f6 refactor(platformvm): consolidate block code, drop atomic_block, add v0 lift
- Drop atomic_block.go + legacy *_block_test.go (consolidated)
- Add block/v0/ subdir + lift_v0.go for old version handling
- Copyright header: 'Lux Industries, Inc.' → 'Lux Industries Inc' (no comma)
- block_adapter.go: new adapter layer
2026-06-01 15:22:27 -07:00

572 lines
13 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package fee
import (
"math"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/node/vms/components/gas"
safemath "github.com/luxfi/math"
)
const (
second = 1
minute = 60 * second
hour = 60 * minute
day = 24 * hour
week = 7 * day
minPrice = 2_048
capacity = 20_000
target = 10_000
maxExcessIncreasePerSecond = capacity - target
doubleEvery = day
excessIncreasePerDoubling = maxExcessIncreasePerSecond * doubleEvery
excessConversionConstantFloat = excessIncreasePerDoubling / math.Ln2
)
var (
excessConversionConstant = floatToGas(excessConversionConstantFloat)
tests = []struct {
name string
state State
config Config
// expectedSeconds and expectedCost are used as inputs for some tests
// and outputs for other tests.
expectedSeconds uint64
expectedCost uint64
expectedExcess gas.Gas
}{
{
name: "excess=0, current<target, minute",
state: State{
Current: 10,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: minute,
expectedCost: 122_880,
expectedExcess: 0, // Should not underflow
},
{
name: "excess=0, current=target, minute",
state: State{
Current: 10_000,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: minute,
expectedCost: 122_880,
expectedExcess: 0,
},
{
name: "excess=excessIncreasePerDoubling, current=target, minute",
state: State{
Current: 10_000,
Excess: excessIncreasePerDoubling,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: minute,
expectedCost: 245_760,
expectedExcess: excessIncreasePerDoubling,
},
{
name: "excess=K, current=target, minute",
state: State{
Current: 10_000,
Excess: excessConversionConstant,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: minute,
expectedCost: 334_020,
expectedExcess: excessConversionConstant,
},
{
name: "excess=0, current>target, minute",
state: State{
Current: 15_000,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: minute,
expectedCost: 122_880,
expectedExcess: 5_000 * minute,
},
{
name: "excess hits 0 during, current<target, day",
state: State{
Current: 9_000,
Excess: 6 * hour * 1_000,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: day,
expectedCost: 177_321_939,
expectedExcess: 0, // Should not underflow
},
{
name: "excess=K, current=target, day",
state: State{
Current: 10_000,
Excess: excessConversionConstant,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: day,
expectedCost: 480_988_800,
expectedExcess: excessConversionConstant,
},
{
name: "excess=0, current>target, day",
state: State{
Current: 15_000,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: day,
expectedCost: 211_438_809,
expectedExcess: 5_000 * day,
},
{
name: "excess=0, current=target, week",
state: State{
Current: 10_000,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: week,
expectedCost: 1_238_630_400,
expectedExcess: 0,
},
{
name: "excess=0, current>target, week",
state: State{
Current: 15_000,
Excess: 0,
},
config: Config{
Target: 10_000,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: week,
expectedCost: 5_265_492_669,
expectedExcess: 5_000 * week,
},
{
name: "excess=1, current>>target, second",
state: State{
Current: math.MaxUint64,
Excess: 1,
},
config: Config{
Target: 0,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: 1,
expectedCost: math.MaxUint64, // Should not overflow
expectedExcess: math.MaxUint64, // Should not overflow
},
{
name: "excess=0, current>>target, 10 seconds",
state: State{
Current: math.MaxUint32,
Excess: 0,
},
config: Config{
Target: 0,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
},
expectedSeconds: 10,
expectedCost: 1_948_429_840_780_833_612,
expectedExcess: math.MaxUint32 * 10,
},
}
)
func TestStateAdvanceTime(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(
t,
State{
Current: test.state.Current,
Excess: test.expectedExcess,
},
test.state.AdvanceTime(test.config.Target, test.expectedSeconds),
)
})
}
}
func TestStateCostOf(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(
t,
test.expectedCost,
test.state.CostOf(test.config, test.expectedSeconds),
)
})
}
}
func TestStateCostOfOverflow(t *testing.T) {
const target = 10_000
config := Config{
Target: target,
MinPrice: minPrice,
ExcessConversionConstant: excessConversionConstant,
}
tests := []struct {
name string
state State
seconds uint64
}{
{
name: "current > target",
state: State{
Current: math.MaxUint32,
Excess: 0,
},
seconds: math.MaxUint64,
},
{
name: "current == target",
state: State{
Current: target,
Excess: 0,
},
seconds: math.MaxUint64,
},
{
name: "current < target",
state: State{
Current: 0,
Excess: 0,
},
seconds: math.MaxUint64,
},
{
name: "current < target and reasonable excess",
state: State{
Current: 0,
Excess: target + 1,
},
seconds: math.MaxUint64/minPrice + 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(
t,
uint64(math.MaxUint64), // Should not overflow
test.state.CostOf(config, test.seconds),
)
})
}
}
func TestStateSecondsRemaining(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(
t,
test.expectedSeconds,
test.state.SecondsRemaining(test.config, week, test.expectedCost),
)
})
}
}
func TestStateSecondsRemainingLimit(t *testing.T) {
const target = 10_000
tests := []struct {
name string
state State
minPrice gas.Price
costLimit uint64
}{
{
name: "zero price",
state: State{
Current: math.MaxUint32,
Excess: 0,
},
minPrice: 0,
costLimit: 0,
},
{
name: "current > target",
state: State{
Current: target + 1,
Excess: 0,
},
minPrice: minPrice,
costLimit: math.MaxUint64,
},
{
name: "current == target",
state: State{
Current: target,
Excess: 0,
},
minPrice: minPrice,
costLimit: minPrice * (week + 1),
},
{
name: "current < target",
state: State{
Current: 0,
Excess: 0,
},
minPrice: minPrice,
costLimit: minPrice * (week + 1),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
config := Config{
Target: target,
MinPrice: test.minPrice,
ExcessConversionConstant: excessConversionConstant,
}
require.Equal(
t,
uint64(week),
test.state.SecondsRemaining(config, week, test.costLimit),
)
})
}
}
func BenchmarkStateCostOf(b *testing.B) {
benchmarks := []struct {
name string
costOf func(
s State,
c Config,
seconds uint64,
) uint64
}{
{
name: "unoptimized",
costOf: State.unoptimizedCostOf,
},
{
name: "optimized",
costOf: State.CostOf,
},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
for _, benchmark := range benchmarks {
b.Run(benchmark.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
benchmark.costOf(test.state, test.config, test.expectedSeconds)
}
})
}
})
}
}
func BenchmarkStateSecondsRemaining(b *testing.B) {
benchmarks := []struct {
name string
secondsRemaining func(
s State,
c Config,
maxSeconds uint64,
targetCost uint64,
) uint64
}{
{
name: "unoptimized",
secondsRemaining: State.unoptimizedSecondsRemaining,
},
{
name: "optimized",
secondsRemaining: State.SecondsRemaining,
},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
for _, benchmark := range benchmarks {
b.Run(benchmark.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
benchmark.secondsRemaining(test.state, test.config, week, test.expectedCost)
}
})
}
})
}
}
func FuzzStateCostOf(f *testing.F) {
for _, test := range tests {
f.Add(
uint64(test.state.Current),
uint64(test.state.Excess),
uint64(test.config.Target),
uint64(test.config.MinPrice),
uint64(test.config.ExcessConversionConstant),
test.expectedSeconds,
)
}
f.Fuzz(
func(
t *testing.T,
current uint64,
excess uint64,
target uint64,
minPrice uint64,
excessConversionConstant uint64,
seconds uint64,
) {
s := State{
Current: gas.Gas(current),
Excess: gas.Gas(excess),
}
c := Config{
Target: gas.Gas(target),
MinPrice: gas.Price(minPrice),
ExcessConversionConstant: gas.Gas(max(excessConversionConstant, 1)),
}
seconds = min(seconds, hour)
require.Equal(
t,
s.unoptimizedCostOf(c, seconds),
s.CostOf(c, seconds),
)
},
)
}
func FuzzStateSecondsRemaining(f *testing.F) {
for _, test := range tests {
f.Add(
uint64(test.state.Current),
uint64(test.state.Excess),
uint64(test.config.Target),
uint64(test.config.MinPrice),
uint64(test.config.ExcessConversionConstant),
uint64(hour),
test.expectedCost,
)
}
f.Fuzz(
func(
t *testing.T,
current uint64,
excess uint64,
target uint64,
minPrice uint64,
excessConversionConstant uint64,
maxSeconds uint64,
targetCost uint64,
) {
s := State{
Current: gas.Gas(current),
Excess: gas.Gas(excess),
}
c := Config{
Target: gas.Gas(target),
MinPrice: gas.Price(minPrice),
ExcessConversionConstant: gas.Gas(max(excessConversionConstant, 1)),
}
maxSeconds = min(maxSeconds, hour)
require.Equal(
t,
s.unoptimizedSecondsRemaining(c, maxSeconds, targetCost),
s.SecondsRemaining(c, maxSeconds, targetCost),
)
},
)
}
// unoptimizedCostOf is a naive implementation of CostOf that is used for
// differential fuzzing.
func (s State) unoptimizedCostOf(c Config, seconds uint64) uint64 {
var (
cost uint64
err error
)
for i := uint64(0); i < seconds; i++ {
s = s.AdvanceTime(c.Target, 1)
price := gas.CalculatePrice(c.MinPrice, s.Excess, c.ExcessConversionConstant)
cost, err = safemath.Add(cost, uint64(price))
if err != nil {
return math.MaxUint64
}
}
return cost
}
// unoptimizedSecondsRemaining is a naive implementation of SecondsRemaining
// that is used for differential fuzzing.
func (s State) unoptimizedSecondsRemaining(c Config, maxSeconds uint64, fundsRemaining uint64) uint64 {
for seconds := uint64(0); seconds < maxSeconds; seconds++ {
s = s.AdvanceTime(c.Target, 1)
price := uint64(gas.CalculatePrice(c.MinPrice, s.Excess, c.ExcessConversionConstant))
if price > fundsRemaining {
return seconds
}
fundsRemaining -= price
}
return maxSeconds
}
// floatToGas converts f to gas.Gas by truncation. `gas.Gas(f)` is preferred and
// floatToGas should only be used if its argument is a `const`.
func floatToGas(f float64) gas.Gas {
return gas.Gas(f)
}