mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
572 lines
13 KiB
Go
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)
|
|
}
|