-
Notifications
You must be signed in to change notification settings - Fork 721
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
1,722 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package fee | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
safemath "github.com/ava-labs/avalanchego/utils/math" | ||
) | ||
|
||
var errGasBoundBreached = errors.New("gas bound breached") | ||
|
||
// Calculator performs fee-related operations that are shared among P-chain and X-chain | ||
// Calculator is supposed to be embedded within chain specific calculators. | ||
type Calculator struct { | ||
// feeWeights help consolidating complexity into gas | ||
feeWeights Dimensions | ||
|
||
// gas cap enforced with adding gas via AddFeesFor | ||
gasCap Gas | ||
|
||
// Avax denominated gas price, i.e. fee per unit of gas. | ||
gasPrice GasPrice | ||
|
||
// cumulatedGas helps aggregating the gas consumed in a single block | ||
// so that we can verify it's not too big/build it properly. | ||
cumulatedGas Gas | ||
|
||
// latestTxComplexity tracks complexity of latest tx being processed. | ||
// latestTxComplexity is especially helpful while building a tx. | ||
latestTxComplexity Dimensions | ||
} | ||
|
||
func NewCalculator(feeWeights Dimensions, gasPrice GasPrice, gasCap Gas) *Calculator { | ||
return &Calculator{ | ||
feeWeights: feeWeights, | ||
gasCap: gasCap, | ||
gasPrice: gasPrice, | ||
} | ||
} | ||
|
||
func (c *Calculator) GetGasPrice() GasPrice { | ||
return c.gasPrice | ||
} | ||
|
||
func (c *Calculator) GetBlockGas() (Gas, error) { | ||
txGas, err := ToGas(c.feeWeights, c.latestTxComplexity) | ||
if err != nil { | ||
return ZeroGas, err | ||
} | ||
return c.cumulatedGas + txGas, nil | ||
} | ||
|
||
func (c *Calculator) GetGasCap() Gas { | ||
return c.gasCap | ||
} | ||
|
||
// AddFeesFor updates latest tx complexity. It should be called once when tx is being verified | ||
// and may be called multiple times when tx is being built (and tx components are added in time). | ||
// AddFeesFor checks that gas cap is not breached. It also returns the updated tx fee for convenience. | ||
func (c *Calculator) AddFeesFor(complexity Dimensions) (uint64, error) { | ||
if complexity == Empty { | ||
return c.GetLatestTxFee() | ||
} | ||
|
||
// Ensure we can consume (don't want partial update of values) | ||
uc, err := Add(c.latestTxComplexity, complexity) | ||
if err != nil { | ||
return 0, fmt.Errorf("%w: %w", errGasBoundBreached, err) | ||
} | ||
c.latestTxComplexity = uc | ||
|
||
totalGas, err := c.GetBlockGas() | ||
if err != nil { | ||
return 0, fmt.Errorf("%w: %w", errGasBoundBreached, err) | ||
} | ||
if totalGas > c.gasCap { | ||
return 0, fmt.Errorf("%w: %w", errGasBoundBreached, err) | ||
} | ||
|
||
return c.GetLatestTxFee() | ||
} | ||
|
||
// Sometimes, e.g. while building a tx, we'd like freedom to speculatively add complexity | ||
// and to remove it later on. [RemoveFeesFor] grants this freedom | ||
func (c *Calculator) RemoveFeesFor(complexity Dimensions) (uint64, error) { | ||
if complexity == Empty { | ||
return c.GetLatestTxFee() | ||
} | ||
|
||
rc, err := Remove(c.latestTxComplexity, complexity) | ||
if err != nil { | ||
return 0, fmt.Errorf("%w: current Gas %d, gas to revert %d", err, c.cumulatedGas, complexity) | ||
} | ||
c.latestTxComplexity = rc | ||
return c.GetLatestTxFee() | ||
} | ||
|
||
// DoneWithLatestTx should be invoked one a tx has been fully processed, before moving to the next one | ||
func (c *Calculator) DoneWithLatestTx() error { | ||
txGas, err := ToGas(c.feeWeights, c.latestTxComplexity) | ||
if err != nil { | ||
return err | ||
} | ||
c.cumulatedGas += txGas | ||
c.latestTxComplexity = Empty | ||
return nil | ||
} | ||
|
||
// CalculateFee must be a stateless method | ||
func (c *Calculator) GetLatestTxFee() (uint64, error) { | ||
gas, err := ToGas(c.feeWeights, c.latestTxComplexity) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return safemath.Mul64(uint64(c.gasPrice), uint64(gas)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package fee | ||
|
||
import ( | ||
"math" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/ava-labs/avalanchego/utils/units" | ||
|
||
safemath "github.com/ava-labs/avalanchego/utils/math" | ||
) | ||
|
||
var ( | ||
testDynamicFeeCfg = DynamicFeesConfig{ | ||
GasPrice: GasPrice(10 * units.NanoAvax), | ||
FeeDimensionWeights: Dimensions{6, 10, 10, 1}, | ||
} | ||
testGasCap = Gas(math.MaxUint64) | ||
) | ||
|
||
func TestAddAndRemoveFees(t *testing.T) { | ||
require := require.New(t) | ||
|
||
var ( | ||
fc = NewCalculator(testDynamicFeeCfg.FeeDimensionWeights, testDynamicFeeCfg.GasPrice, testGasCap) | ||
|
||
complexity = Dimensions{1, 2, 3, 4} | ||
extraComplexity = Dimensions{2, 3, 4, 5} | ||
overComplexity = Dimensions{math.MaxUint64, math.MaxUint64, math.MaxUint64, math.MaxUint64} | ||
) | ||
|
||
fee0, err := fc.GetLatestTxFee() | ||
require.NoError(err) | ||
require.Zero(fee0) | ||
require.NoError(fc.DoneWithLatestTx()) | ||
gas, err := fc.GetBlockGas() | ||
require.NoError(err) | ||
require.Zero(gas) | ||
|
||
fee1, err := fc.AddFeesFor(complexity) | ||
require.NoError(err) | ||
require.Greater(fee1, fee0) | ||
gas, err = fc.GetBlockGas() | ||
require.NoError(err) | ||
require.NotZero(gas) | ||
|
||
// complexity can't overflow | ||
_, err = fc.AddFeesFor(overComplexity) | ||
require.ErrorIs(err, safemath.ErrOverflow) | ||
gas, err = fc.GetBlockGas() | ||
require.NoError(err) | ||
require.NotZero(gas) | ||
|
||
// can't remove more complexity than it was added | ||
_, err = fc.RemoveFeesFor(extraComplexity) | ||
require.ErrorIs(err, safemath.ErrUnderflow) | ||
gas, err = fc.GetBlockGas() | ||
require.NoError(err) | ||
require.NotZero(gas) | ||
|
||
rFee, err := fc.RemoveFeesFor(complexity) | ||
require.NoError(err) | ||
require.Equal(rFee, fee0) | ||
gas, err = fc.GetBlockGas() | ||
require.NoError(err) | ||
require.Zero(gas) | ||
} | ||
|
||
func TestGasCap(t *testing.T) { | ||
require := require.New(t) | ||
|
||
var ( | ||
now = time.Now().Truncate(time.Second) | ||
parentBlkTime = now | ||
// childBlkTime = parentBlkTime.Add(time.Second) | ||
// grandChildBlkTime = childBlkTime.Add(5 * time.Second) | ||
|
||
cfg = DynamicFeesConfig{ | ||
MaxGasPerSecond: Gas(1_000), | ||
LeakGasCoeff: Gas(5), | ||
} | ||
|
||
currCap = cfg.MaxGasPerSecond | ||
) | ||
|
||
// A block whose gas matches cap, will consume full available cap | ||
blkGas := cfg.MaxGasPerSecond | ||
currCap = UpdateGasCap(currCap, blkGas) | ||
require.Equal(ZeroGas, currCap) | ||
|
||
// capacity grows linearly in time till MaxGas | ||
for i := 1; i <= 5; i++ { | ||
childBlkTime := parentBlkTime.Add(time.Duration(i) * time.Second) | ||
nextCap, err := GasCap(cfg, currCap, parentBlkTime, childBlkTime) | ||
require.NoError(err) | ||
require.Equal(Gas(i)*cfg.MaxGasPerSecond/cfg.LeakGasCoeff, nextCap) | ||
} | ||
|
||
// capacity won't grow beyond MaxGas | ||
childBlkTime := parentBlkTime.Add(time.Duration(6) * time.Second) | ||
nextCap, err := GasCap(cfg, currCap, parentBlkTime, childBlkTime) | ||
require.NoError(err) | ||
require.Equal(cfg.MaxGasPerSecond, nextCap) | ||
|
||
// Arrival of a block will reduce GasCap of block Gas content | ||
blkGas = cfg.MaxGasPerSecond / 4 | ||
currCap = UpdateGasCap(nextCap, blkGas) | ||
require.Equal(3*cfg.MaxGasPerSecond/4, currCap) | ||
|
||
// capacity keeps growing again in time after block | ||
childBlkTime = parentBlkTime.Add(time.Second) | ||
nextCap, err = GasCap(cfg, currCap, parentBlkTime, childBlkTime) | ||
require.NoError(err) | ||
require.Equal(currCap+cfg.MaxGasPerSecond/cfg.LeakGasCoeff, nextCap) | ||
|
||
// time can only grow forward with capacity | ||
childBlkTime = parentBlkTime.Add(-1 * time.Second) | ||
_, err = GasCap(cfg, currCap, parentBlkTime, childBlkTime) | ||
require.ErrorIs(err, errUnexpectedBlockTimes) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package fee | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"time" | ||
) | ||
|
||
var ( | ||
errZeroLeakGasCoeff = errors.New("zero leak gas coefficient") | ||
errUnexpectedBlockTimes = errors.New("unexpected block times") | ||
) | ||
|
||
type DynamicFeesConfig struct { | ||
// At this state this is the fixed gas price applied to each block | ||
// In the next PRs, gas price will float and this will become the | ||
// minimum gas price | ||
GasPrice GasPrice `json:"gas-price"` | ||
|
||
// weights to merge fees dimensions complexities into a single gas value | ||
FeeDimensionWeights Dimensions `json:"fee-dimension-weights"` | ||
|
||
// Leaky bucket parameters to calculate gas cap | ||
MaxGasPerSecond Gas // techically the unit of measure is Gas/sec, but picking Gas reduces casts needed | ||
LeakGasCoeff Gas // techically the unit of measure is sec^{-1}, but picking Gas reduces casts needed | ||
} | ||
|
||
func (c *DynamicFeesConfig) Validate() error { | ||
if c.LeakGasCoeff == 0 { | ||
return errZeroLeakGasCoeff | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// We cap the maximum gas consumed by time with a leaky bucket approach | ||
// GasCap = min (GasCap + MaxGasPerSecond/LeakGasCoeff*ElapsedTime, MaxGasPerSecond) | ||
func GasCap(cfg DynamicFeesConfig, currentGasCapacity Gas, parentBlkTime, childBlkTime time.Time) (Gas, error) { | ||
if parentBlkTime.Compare(childBlkTime) > 0 { | ||
return ZeroGas, fmt.Errorf("%w, parentBlkTim %v, childBlkTime %v", errUnexpectedBlockTimes, parentBlkTime, childBlkTime) | ||
} | ||
|
||
elapsedTime := uint64(childBlkTime.Unix() - parentBlkTime.Unix()) | ||
if elapsedTime > uint64(cfg.LeakGasCoeff) { | ||
return cfg.MaxGasPerSecond, nil | ||
} | ||
|
||
return min(cfg.MaxGasPerSecond, currentGasCapacity+cfg.MaxGasPerSecond*Gas(elapsedTime)/cfg.LeakGasCoeff), nil | ||
} | ||
|
||
func UpdateGasCap(currentGasCap, blkGas Gas) Gas { | ||
nextGasCap := Gas(0) | ||
if currentGasCap > blkGas { | ||
nextGasCap = currentGasCap - blkGas | ||
} | ||
return nextGasCap | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. | ||
// See the file LICENSE for licensing terms. | ||
|
||
package fee | ||
|
||
import ( | ||
safemath "github.com/ava-labs/avalanchego/utils/math" | ||
) | ||
|
||
const ( | ||
Bandwidth Dimension = 0 | ||
DBRead Dimension = 1 | ||
DBWrite Dimension = 2 // includes deletes | ||
Compute Dimension = 3 | ||
|
||
FeeDimensions = 4 | ||
) | ||
|
||
var ( | ||
ZeroGas = Gas(0) | ||
ZeroGasPrice = GasPrice(0) | ||
Empty = Dimensions{} | ||
) | ||
|
||
type ( | ||
GasPrice uint64 | ||
Gas uint64 | ||
|
||
Dimension int | ||
Dimensions [FeeDimensions]uint64 | ||
) | ||
|
||
func Add(lhs, rhs Dimensions) (Dimensions, error) { | ||
var res Dimensions | ||
for i := 0; i < FeeDimensions; i++ { | ||
v, err := safemath.Add64(lhs[i], rhs[i]) | ||
if err != nil { | ||
return res, err | ||
} | ||
res[i] = v | ||
} | ||
return res, nil | ||
} | ||
|
||
func Remove(lhs, rhs Dimensions) (Dimensions, error) { | ||
var res Dimensions | ||
for i := 0; i < FeeDimensions; i++ { | ||
v, err := safemath.Sub(lhs[i], rhs[i]) | ||
if err != nil { | ||
return res, err | ||
} | ||
res[i] = v | ||
} | ||
return res, nil | ||
} | ||
|
||
func ToGas(weights, dimensions Dimensions) (Gas, error) { | ||
res := uint64(0) | ||
for i := 0; i < FeeDimensions; i++ { | ||
v, err := safemath.Mul64(weights[i], dimensions[i]) | ||
if err != nil { | ||
return ZeroGas, err | ||
} | ||
res, err = safemath.Add64(res, v) | ||
if err != nil { | ||
return ZeroGas, err | ||
} | ||
} | ||
return Gas(res) / 10, nil | ||
} |
Oops, something went wrong.