From 38fb29571a0d1baf20bf58399d0553f3ebe1c016 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 19 Jul 2024 13:02:07 -0400 Subject: [PATCH] Implement ACP-103 fee package (#3203) Signed-off-by: Alberto Benegiamo Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Co-authored-by: Alberto Benegiamo Co-authored-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- go.mod | 2 +- vms/components/fee/config.go | 22 ++ vms/components/fee/dimensions.go | 70 +++++ vms/components/fee/dimensions_test.go | 428 ++++++++++++++++++++++++++ vms/components/fee/gas.go | 108 +++++++ vms/components/fee/gas_test.go | 179 +++++++++++ vms/components/fee/state.go | 63 ++++ vms/components/fee/state_test.go | 159 ++++++++++ 8 files changed, 1030 insertions(+), 1 deletion(-) create mode 100644 vms/components/fee/config.go create mode 100644 vms/components/fee/dimensions.go create mode 100644 vms/components/fee/dimensions_test.go create mode 100644 vms/components/fee/gas.go create mode 100644 vms/components/fee/gas_test.go create mode 100644 vms/components/fee/state.go create mode 100644 vms/components/fee/state_test.go diff --git a/go.mod b/go.mod index 52c55810d0f9..eaedf707a300 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/gorilla/rpc v1.2.0 github.com/gorilla/websocket v1.4.2 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/holiman/uint256 v1.2.4 github.com/huin/goupnp v1.3.0 github.com/jackpal/gateway v1.0.6 github.com/jackpal/go-nat-pmp v1.0.2 @@ -117,7 +118,6 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.4 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/vms/components/fee/config.go b/vms/components/fee/config.go new file mode 100644 index 000000000000..ca56f872b627 --- /dev/null +++ b/vms/components/fee/config.go @@ -0,0 +1,22 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// The fee package implements dynamic gas pricing specified in ACP-103: +// https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/103-dynamic-fees +package fee + +type Config struct { + // Weights to merge fee dimensions into a single gas value. + Weights Dimensions `json:"weights"` + // Maximum amount of gas the chain is allowed to store for future use. + MaxGasCapacity Gas `json:"maxGasCapacity"` + // Maximum amount of gas the chain is allowed to consume per second. + MaxGasPerSecond Gas `json:"maxGasPerSecond"` + // Target amount of gas the chain should consume per second to keep the fees + // stable. + TargetGasPerSecond Gas `json:"targetGasPerSecond"` + // Minimum price per unit of gas. + MinGasPrice GasPrice `json:"minGasPrice"` + // Constant used to convert excess gas to a gas price. + ExcessConversionConstant Gas `json:"excessConversionConstant"` +} diff --git a/vms/components/fee/dimensions.go b/vms/components/fee/dimensions.go new file mode 100644 index 000000000000..3db1db01b3d2 --- /dev/null +++ b/vms/components/fee/dimensions.go @@ -0,0 +1,70 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import "github.com/ava-labs/avalanchego/utils/math" + +const ( + Bandwidth Dimension = iota + DBRead + DBWrite // includes deletes + Compute + + NumDimensions = iota +) + +type ( + Dimension uint + Dimensions [NumDimensions]uint64 +) + +// Add returns d + sum(os...). +// +// If overflow occurs, an error is returned. +func (d Dimensions) Add(os ...Dimensions) (Dimensions, error) { + var err error + for _, o := range os { + for i := range o { + d[i], err = math.Add(d[i], o[i]) + if err != nil { + return d, err + } + } + } + return d, nil +} + +// Sub returns d - sum(os...). +// +// If underflow occurs, an error is returned. +func (d Dimensions) Sub(os ...Dimensions) (Dimensions, error) { + var err error + for _, o := range os { + for i := range o { + d[i], err = math.Sub(d[i], o[i]) + if err != nil { + return d, err + } + } + } + return d, nil +} + +// ToGas returns d ยท weights. +// +// If overflow occurs, an error is returned. +func (d Dimensions) ToGas(weights Dimensions) (Gas, error) { + var res uint64 + for i := range d { + v, err := math.Mul(d[i], weights[i]) + if err != nil { + return 0, err + } + res, err = math.Add(res, v) + if err != nil { + return 0, err + } + } + return Gas(res), nil +} diff --git a/vms/components/fee/dimensions_test.go b/vms/components/fee/dimensions_test.go new file mode 100644 index 000000000000..3a0260934e75 --- /dev/null +++ b/vms/components/fee/dimensions_test.go @@ -0,0 +1,428 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +func Test_Dimensions_Add(t *testing.T) { + tests := []struct { + name string + lhs Dimensions + rhs []Dimensions + expected Dimensions + expectedErr error + }{ + { + name: "no error single entry", + lhs: Dimensions{ + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + }, + expected: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + expectedErr: nil, + }, + { + name: "no error multiple entries", + lhs: Dimensions{ + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + { + Bandwidth: 100, + DBRead: 200, + DBWrite: 300, + Compute: 400, + }, + }, + expected: Dimensions{ + Bandwidth: 111, + DBRead: 222, + DBWrite: 333, + Compute: 444, + }, + expectedErr: nil, + }, + { + name: "bandwidth overflow", + lhs: Dimensions{ + Bandwidth: math.MaxUint64, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + }, + expected: Dimensions{ + Bandwidth: 0, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "db read overflow", + lhs: Dimensions{ + Bandwidth: 1, + DBRead: math.MaxUint64, + DBWrite: 3, + Compute: 4, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + }, + expected: Dimensions{ + Bandwidth: 11, + DBRead: 0, + DBWrite: 3, + Compute: 4, + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "db write overflow", + lhs: Dimensions{ + Bandwidth: 1, + DBRead: 2, + DBWrite: math.MaxUint64, + Compute: 4, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + }, + expected: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 0, + Compute: 4, + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "compute overflow", + lhs: Dimensions{ + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: math.MaxUint64, + }, + rhs: []Dimensions{ + { + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + }, + expected: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 0, + }, + expectedErr: safemath.ErrOverflow, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + actual, err := test.lhs.Add(test.rhs...) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expected, actual) + }) + } +} + +func Test_Dimensions_Sub(t *testing.T) { + tests := []struct { + name string + lhs Dimensions + rhs []Dimensions + expected Dimensions + expectedErr error + }{ + { + name: "no error single entry", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + }, + expected: Dimensions{ + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 40, + }, + expectedErr: nil, + }, + { + name: "no error multiple entries", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + { + Bandwidth: 5, + DBRead: 5, + DBWrite: 5, + Compute: 5, + }, + }, + expected: Dimensions{ + Bandwidth: 5, + DBRead: 15, + DBWrite: 25, + Compute: 35, + }, + expectedErr: nil, + }, + { + name: "bandwidth underflow", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: math.MaxUint64, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + }, + expected: Dimensions{ + Bandwidth: 0, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + expectedErr: safemath.ErrUnderflow, + }, + { + name: "db read underflow", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: 1, + DBRead: math.MaxUint64, + DBWrite: 3, + Compute: 4, + }, + }, + expected: Dimensions{ + Bandwidth: 10, + DBRead: 0, + DBWrite: 33, + Compute: 44, + }, + expectedErr: safemath.ErrUnderflow, + }, + { + name: "db write underflow", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: 1, + DBRead: 2, + DBWrite: math.MaxUint64, + Compute: 4, + }, + }, + expected: Dimensions{ + Bandwidth: 10, + DBRead: 20, + DBWrite: 0, + Compute: 44, + }, + expectedErr: safemath.ErrUnderflow, + }, + { + name: "compute underflow", + lhs: Dimensions{ + Bandwidth: 11, + DBRead: 22, + DBWrite: 33, + Compute: 44, + }, + rhs: []Dimensions{ + { + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: math.MaxUint64, + }, + }, + expected: Dimensions{ + Bandwidth: 10, + DBRead: 20, + DBWrite: 30, + Compute: 0, + }, + expectedErr: safemath.ErrUnderflow, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + actual, err := test.lhs.Sub(test.rhs...) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expected, actual) + }) + } +} + +func Test_Dimensions_ToGas(t *testing.T) { + tests := []struct { + name string + units Dimensions + weights Dimensions + expected Gas + expectedErr error + }{ + { + name: "no error", + units: Dimensions{ + Bandwidth: 1, + DBRead: 2, + DBWrite: 3, + Compute: 4, + }, + weights: Dimensions{ + Bandwidth: 1000, + DBRead: 100, + DBWrite: 10, + Compute: 1, + }, + expected: 1*1000 + 2*100 + 3*10 + 4*1, + expectedErr: nil, + }, + { + name: "multiplication overflow", + units: Dimensions{ + Bandwidth: 2, + DBRead: 1, + DBWrite: 1, + Compute: 1, + }, + weights: Dimensions{ + Bandwidth: math.MaxUint64, + DBRead: 1, + DBWrite: 1, + Compute: 1, + }, + expected: 0, + expectedErr: safemath.ErrOverflow, + }, + { + name: "addition overflow", + units: Dimensions{ + Bandwidth: 1, + DBRead: 1, + DBWrite: 0, + Compute: 0, + }, + weights: Dimensions{ + Bandwidth: math.MaxUint64, + DBRead: math.MaxUint64, + DBWrite: 1, + Compute: 1, + }, + expected: 0, + expectedErr: safemath.ErrOverflow, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + actual, err := test.units.ToGas(test.weights) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expected, actual) + + actual, err = test.weights.ToGas(test.units) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expected, actual) + }) + } +} diff --git a/vms/components/fee/gas.go b/vms/components/fee/gas.go new file mode 100644 index 000000000000..1e0ef2af86fe --- /dev/null +++ b/vms/components/fee/gas.go @@ -0,0 +1,108 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +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 + GasPrice uint64 +) + +// AddPerSecond returns g + gasPerSecond * seconds. +// +// If overflow would occur, MaxUint64 is returned. +func (g Gas) AddPerSecond(gasPerSecond Gas, seconds uint64) Gas { + newGas, err := safemath.Mul(uint64(gasPerSecond), seconds) + if err != nil { + return math.MaxUint64 + } + totalGas, err := safemath.Add(uint64(g), newGas) + if err != nil { + return math.MaxUint64 + } + return Gas(totalGas) +} + +// SubPerSecond returns g - gasPerSecond * seconds. +// +// If underflow would occur, 0 is returned. +func (g Gas) SubPerSecond(gasPerSecond Gas, seconds uint64) Gas { + gasToRemove, err := safemath.Mul(uint64(gasPerSecond), seconds) + if err != nil { + return 0 + } + totalGas, err := safemath.Sub(uint64(g), gasToRemove) + if err != nil { + return 0 + } + return Gas(totalGas) +} + +// MulExp returns an approximation of g * 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 (g GasPrice) MulExp( + excess Gas, + excessConversionConstant Gas, +) GasPrice { + 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(g)) // 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 GasPrice(output.Div(&output, &denominator).Uint64()) +} diff --git a/vms/components/fee/gas_test.go b/vms/components/fee/gas_test.go new file mode 100644 index 000000000000..3e62cdec819b --- /dev/null +++ b/vms/components/fee/gas_test.go @@ -0,0 +1,179 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "fmt" + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +var gasPriceMulExpTests = []struct { + minPrice GasPrice + excess Gas + excessConversionConstant Gas + expected GasPrice +}{ + { + minPrice: 1, + excess: 0, + excessConversionConstant: 1, + expected: 1, + }, + { + minPrice: 1, + excess: 1, + excessConversionConstant: 1, + expected: 2, + }, + { + minPrice: 1, + excess: 2, + excessConversionConstant: 1, + expected: 6, + }, + { + minPrice: 1, + excess: 10_000, + excessConversionConstant: 10_000, + expected: 2, + }, + { + minPrice: 1, + excess: 1_000_000, + excessConversionConstant: 10_000, + expected: math.MaxUint64, + }, + { + minPrice: 10, + excess: 10_000_000, + excessConversionConstant: 1_000_000, + expected: 220_264, + }, + { + minPrice: math.MaxUint64, + excess: math.MaxUint64, + excessConversionConstant: 1, + expected: math.MaxUint64, + }, + { + minPrice: math.MaxUint32, + excess: 1, + excessConversionConstant: 1, + expected: 11_674_931_546, + }, + { + minPrice: 6_786_177_901_268_885_274, // ~ MaxUint64 / e + excess: 1, + excessConversionConstant: 1, + expected: math.MaxUint64 - 11, + }, + { + minPrice: 6_786_177_901_268_885_274, // ~ MaxUint64 / e + excess: math.MaxUint64, + excessConversionConstant: math.MaxUint64, + expected: math.MaxUint64 - 1, + }, +} + +func Test_Gas_AddPerSecond(t *testing.T) { + tests := []struct { + initial Gas + gasPerSecond Gas + seconds uint64 + expected Gas + }{ + { + initial: 5, + gasPerSecond: 1, + seconds: 2, + expected: 7, + }, + { + initial: 5, + gasPerSecond: math.MaxUint64, + seconds: 2, + expected: math.MaxUint64, + }, + { + initial: math.MaxUint64, + gasPerSecond: 1, + seconds: 2, + expected: math.MaxUint64, + }, + { + initial: math.MaxUint64, + gasPerSecond: math.MaxUint64, + seconds: math.MaxUint64, + expected: math.MaxUint64, + }, + } + for _, test := range tests { + t.Run(fmt.Sprintf("%d+%d*%d=%d", test.initial, test.gasPerSecond, test.seconds, test.expected), func(t *testing.T) { + actual := test.initial.AddPerSecond(test.gasPerSecond, test.seconds) + require.Equal(t, test.expected, actual) + }) + } +} + +func Test_Gas_SubPerSecond(t *testing.T) { + tests := []struct { + initial Gas + gasPerSecond Gas + seconds uint64 + expected Gas + }{ + { + initial: 5, + gasPerSecond: 1, + seconds: 2, + expected: 3, + }, + { + initial: 5, + gasPerSecond: math.MaxUint64, + seconds: 2, + expected: 0, + }, + { + initial: 1, + gasPerSecond: 1, + seconds: 2, + expected: 0, + }, + { + initial: math.MaxUint64, + gasPerSecond: math.MaxUint64, + seconds: math.MaxUint64, + expected: 0, + }, + } + for _, test := range tests { + t.Run(fmt.Sprintf("%d-%d*%d=%d", test.initial, test.gasPerSecond, test.seconds, test.expected), func(t *testing.T) { + actual := test.initial.SubPerSecond(test.gasPerSecond, test.seconds) + require.Equal(t, test.expected, actual) + }) + } +} + +func Test_GasPrice_MulExp(t *testing.T) { + for _, test := range gasPriceMulExpTests { + t.Run(fmt.Sprintf("%d*e^(%d/%d)=%d", test.minPrice, test.excess, test.excessConversionConstant, test.expected), func(t *testing.T) { + actual := test.minPrice.MulExp(test.excess, test.excessConversionConstant) + require.Equal(t, test.expected, actual) + }) + } +} + +func Benchmark_GasPrice_MulExp(b *testing.B) { + for _, test := range gasPriceMulExpTests { + b.Run(fmt.Sprintf("%d*e^(%d/%d)=%d", test.minPrice, test.excess, test.excessConversionConstant, test.expected), func(b *testing.B) { + for i := 0; i < b.N; i++ { + test.minPrice.MulExp(test.excess, test.excessConversionConstant) + } + }) + } +} diff --git a/vms/components/fee/state.go b/vms/components/fee/state.go new file mode 100644 index 000000000000..cd6414bba355 --- /dev/null +++ b/vms/components/fee/state.go @@ -0,0 +1,63 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "errors" + "math" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +var ErrInsufficientCapacity = errors.New("insufficient capacity") + +type State struct { + Capacity Gas + Excess Gas +} + +// AdvanceTime adds maxGasPerSecond to capacity and subtracts targetGasPerSecond +// from excess over the provided duration. +// +// Capacity is capped at maxGasCapacity. +// Excess to be removed is capped at excess. +func (s State) AdvanceTime( + maxGasCapacity Gas, + maxGasPerSecond Gas, + targetGasPerSecond Gas, + duration uint64, +) State { + return State{ + Capacity: min( + s.Capacity.AddPerSecond(maxGasPerSecond, duration), + maxGasCapacity, + ), + Excess: s.Excess.SubPerSecond(targetGasPerSecond, duration), + } +} + +// ConsumeGas removes gas from capacity and adds gas to excess. +// +// If the capacity is insufficient, an error is returned. +// If the excess would overflow, it is capped at MaxUint64. +func (s State) ConsumeGas(gas Gas) (State, error) { + newCapacity, err := safemath.Sub(uint64(s.Capacity), uint64(gas)) + if err != nil { + return State{}, ErrInsufficientCapacity + } + + newExcess, err := safemath.Add(uint64(s.Excess), uint64(gas)) + if err != nil { + //nolint:nilerr // excess is capped at MaxUint64 + return State{ + Capacity: Gas(newCapacity), + Excess: math.MaxUint64, + }, nil + } + + return State{ + Capacity: Gas(newCapacity), + Excess: Gas(newExcess), + }, nil +} diff --git a/vms/components/fee/state_test.go b/vms/components/fee/state_test.go new file mode 100644 index 000000000000..2a48f33a4cc0 --- /dev/null +++ b/vms/components/fee/state_test.go @@ -0,0 +1,159 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_State_AdvanceTime(t *testing.T) { + tests := []struct { + name string + initial State + maxGasCapacity Gas + maxGasPerSecond Gas + targetGasPerSecond Gas + duration uint64 + expected State + }{ + { + name: "cap capacity", + initial: State{ + Capacity: 10, + Excess: 0, + }, + maxGasCapacity: 20, + maxGasPerSecond: 10, + targetGasPerSecond: 0, + duration: 2, + expected: State{ + Capacity: 20, + Excess: 0, + }, + }, + { + name: "increase capacity", + initial: State{ + Capacity: 10, + Excess: 0, + }, + maxGasCapacity: 30, + maxGasPerSecond: 10, + targetGasPerSecond: 0, + duration: 1, + expected: State{ + Capacity: 20, + Excess: 0, + }, + }, + { + name: "avoid excess underflow", + initial: State{ + Capacity: 10, + Excess: 10, + }, + maxGasCapacity: 20, + maxGasPerSecond: 10, + targetGasPerSecond: 10, + duration: 2, + expected: State{ + Capacity: 20, + Excess: 0, + }, + }, + { + name: "reduce excess", + initial: State{ + Capacity: 10, + Excess: 10, + }, + maxGasCapacity: 20, + maxGasPerSecond: 10, + targetGasPerSecond: 5, + duration: 1, + expected: State{ + Capacity: 20, + Excess: 5, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.initial.AdvanceTime(test.maxGasCapacity, test.maxGasPerSecond, test.targetGasPerSecond, test.duration) + require.Equal(t, test.expected, actual) + }) + } +} + +func Test_State_ConsumeGas(t *testing.T) { + tests := []struct { + name string + initial State + gas Gas + expected State + expectedErr error + }{ + { + name: "consume some gas", + initial: State{ + Capacity: 10, + Excess: 10, + }, + gas: 5, + expected: State{ + Capacity: 5, + Excess: 15, + }, + expectedErr: nil, + }, + { + name: "consume all gas", + initial: State{ + Capacity: 10, + Excess: 10, + }, + gas: 10, + expected: State{ + Capacity: 0, + Excess: 20, + }, + expectedErr: nil, + }, + { + name: "consume too much gas", + initial: State{ + Capacity: 10, + Excess: 10, + }, + gas: 11, + expected: State{}, + expectedErr: ErrInsufficientCapacity, + }, + { + name: "maximum excess", + initial: State{ + Capacity: 10, + Excess: math.MaxUint64, + }, + gas: 1, + expected: State{ + Capacity: 9, + Excess: math.MaxUint64, + }, + expectedErr: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + actual, err := test.initial.ConsumeGas(test.gas) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expected, actual) + }) + } +}