diff --git a/vms/components/fee/calculator.go b/vms/components/fee/calculator.go new file mode 100644 index 000000000000..903638495961 --- /dev/null +++ b/vms/components/fee/calculator.go @@ -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: total gas %d, gas cap %d", errGasBoundBreached, totalGas, c.gasCap) + } + + 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)) +} diff --git a/vms/components/fee/calculator_test.go b/vms/components/fee/calculator_test.go new file mode 100644 index 000000000000..f2589fcc2ba4 --- /dev/null +++ b/vms/components/fee/calculator_test.go @@ -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) +} diff --git a/vms/components/fee/config.go b/vms/components/fee/config.go new file mode 100644 index 000000000000..28a2059b65e0 --- /dev/null +++ b/vms/components/fee/config.go @@ -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 +} diff --git a/vms/components/fee/dimensions.go b/vms/components/fee/dimensions.go new file mode 100644 index 000000000000..286dc29813f0 --- /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 ( + 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 +} diff --git a/vms/components/fee/helpers.go b/vms/components/fee/helpers.go new file mode 100644 index 000000000000..26cc300b3cc9 --- /dev/null +++ b/vms/components/fee/helpers.go @@ -0,0 +1,66 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +func MeterInput(c codec.Manager, v uint16, in *avax.TransferableInput) (Dimensions, error) { + cost, err := in.In.Cost() + if err != nil { + return Empty, fmt.Errorf("failed retrieving cost of input %s: %w", in.ID, err) + } + + inSize, err := c.Size(v, in) + if err != nil { + return Empty, fmt.Errorf("failed retrieving size of input %s: %w", in.ID, err) + } + uInSize := uint64(inSize) + + complexity := Empty + complexity[Bandwidth] += uInSize - codec.VersionSize + complexity[DBRead] += uInSize // inputs are read + complexity[DBWrite] += uInSize // inputs are deleted + complexity[Compute] += cost + return complexity, nil +} + +func MeterOutput(c codec.Manager, v uint16, out *avax.TransferableOutput) (Dimensions, error) { + outSize, err := c.Size(v, out) + if err != nil { + return Empty, fmt.Errorf("failed retrieving size of output %s: %w", out.ID, err) + } + uOutSize := uint64(outSize) + + complexity := Empty + complexity[Bandwidth] += uOutSize - codec.VersionSize + complexity[DBWrite] += uOutSize + return complexity, nil +} + +func MeterCredential(c codec.Manager, v uint16, keysCount int) (Dimensions, error) { + // Ensure that codec picks interface instead of the pointer to evaluate size. + creds := make([]verify.Verifiable, 0, 1) + creds = append(creds, &secp256k1fx.Credential{ + Sigs: make([][secp256k1.SignatureLen]byte, keysCount), + }) + + credSize, err := c.Size(v, creds) + if err != nil { + return Empty, fmt.Errorf("failed retrieving size of credentials: %w", err) + } + credSize -= wrappers.IntLen // length of the slice, we want the single credential + + complexity := Empty + complexity[Bandwidth] += uint64(credSize) - codec.VersionSize + return complexity, nil +} diff --git a/vms/platformvm/txs/fee/calculator.go b/vms/platformvm/txs/fee/calculator.go index f33db9c1520f..1cfc62c581a7 100644 --- a/vms/platformvm/txs/fee/calculator.go +++ b/vms/platformvm/txs/fee/calculator.go @@ -3,9 +3,23 @@ package fee -import "github.com/ava-labs/avalanchego/vms/platformvm/txs" +import ( + "github.com/ava-labs/avalanchego/vms/components/fee" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) // Calculator is the interfaces that any fee Calculator must implement type Calculator interface { CalculateFee(tx *txs.Tx) (uint64, error) + + GetFee() uint64 + ResetFee(newFee uint64) + AddFeesFor(complexity fee.Dimensions) (uint64, error) + RemoveFeesFor(unitsToRm fee.Dimensions) (uint64, error) + GetGasPrice() fee.GasPrice + GetBlockGas() (fee.Gas, error) + GetGasCap() fee.Gas + setCredentials(creds []verify.Verifiable) + IsEActive() bool } diff --git a/vms/platformvm/txs/fee/calculator_test.go b/vms/platformvm/txs/fee/calculator_test.go index 454072e9df8d..f0652a90accf 100644 --- a/vms/platformvm/txs/fee/calculator_test.go +++ b/vms/platformvm/txs/fee/calculator_test.go @@ -4,18 +4,84 @@ package fee import ( + "errors" "testing" "time" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/upgrade" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) +var ( + testFeeWeights = fee.Dimensions{1, 1, 1, 1} + testGasPrice = fee.GasPrice(10 * units.NanoAvax) + testBlockMaxGas = fee.Gas(100_000) + + preFundedKeys = secp256k1.TestKeys() + feeTestSigners = [][]*secp256k1.PrivateKey{preFundedKeys} + feeTestDefaultStakeWeight = uint64(2024) + + errFailedComplexityCumulation = errors.New("failed cumulating complexity") +) + +func TestAddAndRemoveFees(t *testing.T) { + r := require.New(t) + + fc := NewDynamicCalculator(fee.NewCalculator(testFeeWeights, testGasPrice, testBlockMaxGas)) + + var ( + units = fee.Dimensions{1, 2, 3, 4} + gas = fee.Gas(1) + doubleGas = fee.Gas(2) + ) + + feeDelta, err := fc.AddFeesFor(units) + r.NoError(err) + + haveGas, err := fc.GetBlockGas() + r.NoError(err) + r.Equal(gas, haveGas) + r.NotZero(feeDelta) + r.Equal(feeDelta, fc.GetFee()) + + feeDelta2, err := fc.AddFeesFor(units) + r.NoError(err) + haveGas, err = fc.GetBlockGas() + r.NoError(err) + r.Equal(doubleGas, haveGas) + r.Equal(feeDelta, feeDelta2) + r.Equal(feeDelta+feeDelta2, fc.GetFee()) + + feeDelta3, err := fc.RemoveFeesFor(units) + r.NoError(err) + haveGas, err = fc.GetBlockGas() + r.NoError(err) + r.Equal(gas, haveGas) + r.Equal(feeDelta, feeDelta3) + r.Equal(feeDelta, fc.GetFee()) + + feeDelta4, err := fc.RemoveFeesFor(units) + r.NoError(err) + r.Zero(fc.GetBlockGas()) + r.Equal(feeDelta, feeDelta4) + r.Zero(fc.GetFee()) +} + func TestTxFees(t *testing.T) { feeTestsDefaultCfg := StaticConfig{ TxFee: 1 * units.Avax, @@ -40,214 +106,897 @@ func TestTxFees(t *testing.T) { } // chain times needed to have specific upgrades active + postEUpgradeTime := upgrades.EUpgradeTime.Add(time.Second) preEUpgradeTime := upgrades.EUpgradeTime.Add(-1 * time.Second) preApricotPhase3Time := upgrades.ApricotPhase3Time.Add(-1 * time.Second) tests := []struct { - name string - chainTime time.Time - unsignedTx func() txs.UnsignedTx - expected uint64 + name string + chainTime time.Time + signedTxF func(t *testing.T) *txs.Tx + gasCapF func() fee.Gas + expectedError error + checksF func(*testing.T, Calculator) }{ { - name: "AddValidatorTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: addValidatorTx, - expected: feeTestsDefaultCfg.AddPrimaryNetworkValidatorFee, + name: "AddValidatorTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: addValidatorTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddPrimaryNetworkValidatorFee, c.GetFee()) + }, + }, + { + name: "AddValidatorTx post EUpgrade", + chainTime: postEUpgradeTime, + expectedError: errFailedFeeCalculation, + signedTxF: addValidatorTx, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "AddSubnetValidatorTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: addSubnetValidatorTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddSubnetValidatorFee, c.GetFee()) + }, + }, + { + name: "AddSubnetValidatorTx post EUpgrade, success", + chainTime: postEUpgradeTime, + expectedError: nil, + signedTxF: addSubnetValidatorTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 2_910*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(291), haveGas) + }, + }, + { + name: "AddSubnetValidatorTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: addSubnetValidatorTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "AddDelegatorTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: addDelegatorTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddPrimaryNetworkDelegatorFee, c.GetFee()) + }, + }, + { + name: "AddDelegatorTx post EUpgrade", + chainTime: postEUpgradeTime, + expectedError: errFailedFeeCalculation, + signedTxF: addDelegatorTx, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "CreateChainTx pre ApricotPhase3", + chainTime: preApricotPhase3Time, + signedTxF: createChainTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.CreateAssetTxFee, c.GetFee()) + }, + }, + { + name: "CreateChainTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: createChainTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.CreateBlockchainTxFee, c.GetFee()) + }, + }, + { + name: "CreateChainTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: createChainTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 1_950*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(195), haveGas) + }, + }, + { + name: "CreateChainTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + signedTxF: createChainTx, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "CreateSubnetTx pre ApricotPhase3", + chainTime: preApricotPhase3Time, + signedTxF: createSubnetTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.CreateAssetTxFee, c.GetFee()) + }, + }, + { + name: "CreateSubnetTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: createSubnetTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.CreateSubnetTxFee, c.GetFee()) + }, + }, + { + name: "CreateSubnetTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: createSubnetTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 1_850*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(185), haveGas) + }, }, { - name: "AddSubnetValidatorTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: addSubnetValidatorTx, - expected: feeTestsDefaultCfg.AddSubnetValidatorFee, + name: "CreateSubnetTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + signedTxF: createSubnetTx, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, }, { - name: "AddDelegatorTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: addDelegatorTx, - expected: feeTestsDefaultCfg.AddPrimaryNetworkDelegatorFee, + name: "RemoveSubnetValidatorTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: removeSubnetValidatorTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TxFee, c.GetFee()) + }, }, { - name: "CreateChainTx pre ApricotPhase3", - chainTime: preApricotPhase3Time, - unsignedTx: createChainTx, - expected: feeTestsDefaultCfg.CreateAssetTxFee, + name: "RemoveSubnetValidatorTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: removeSubnetValidatorTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 2_880*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(288), haveGas) + }, }, { - name: "CreateChainTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: createChainTx, - expected: feeTestsDefaultCfg.CreateBlockchainTxFee, + name: "RemoveSubnetValidatorTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: removeSubnetValidatorTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, }, { - name: "CreateSubnetTx pre ApricotPhase3", - chainTime: preApricotPhase3Time, - unsignedTx: createSubnetTx, - expected: feeTestsDefaultCfg.CreateAssetTxFee, + name: "TransformSubnetTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: transformSubnetTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TransformSubnetTxFee, c.GetFee()) + }, }, { - name: "CreateSubnetTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: createSubnetTx, - expected: feeTestsDefaultCfg.CreateSubnetTxFee, + name: "TransformSubnetTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: transformSubnetTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 1_970*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(197), haveGas) + }, }, { - name: "RemoveSubnetValidatorTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: removeSubnetValidatorTx, - expected: feeTestsDefaultCfg.TxFee, + name: "TransformSubnetTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: transformSubnetTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "TransferSubnetOwnershipTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: transferSubnetOwnershipTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TxFee, c.GetFee()) + }, }, { - name: "TransformSubnetTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: transformSubnetTx, - expected: feeTestsDefaultCfg.TransformSubnetTxFee, + name: "TransferSubnetOwnershipTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: transferSubnetOwnershipTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 1_900*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(190), haveGas) + }, }, { - name: "TransferSubnetOwnershipTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: transferSubnetOwnershipTx, - expected: feeTestsDefaultCfg.TxFee, + name: "TransferSubnetOwnershipTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: transferSubnetOwnershipTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, }, { name: "AddPermissionlessValidatorTx Primary Network pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { - return addPermissionlessValidatorTx(constants.PrimaryNetworkID) + chainTime: preEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + return addPermissionlessValidatorTx(t, constants.PrimaryNetworkID) + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddPrimaryNetworkValidatorFee, c.GetFee()) }, - expected: feeTestsDefaultCfg.AddPrimaryNetworkValidatorFee, }, { name: "AddPermissionlessValidatorTx Subnet pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { + chainTime: preEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + subnetID := ids.GenerateTestID() + require.NotEqual(t, constants.PrimaryNetworkID, subnetID) + return addPermissionlessValidatorTx(t, subnetID) + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddSubnetValidatorFee, c.GetFee()) + }, + }, + { + name: "AddPermissionlessValidatorTx Primary Network post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + return addPermissionlessValidatorTx(t, constants.PrimaryNetworkID) + }, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 3_310*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(331), haveGas) + }, + }, + { + name: "AddPermissionlessValidatorTx Subnet post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { subnetID := ids.GenerateTestID() require.NotEqual(t, constants.PrimaryNetworkID, subnetID) - return addPermissionlessValidatorTx(subnetID) + return addPermissionlessValidatorTx(t, subnetID) + }, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 3_310*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(331), haveGas) }, - expected: feeTestsDefaultCfg.AddSubnetValidatorFee, + }, + { + name: "AddPermissionlessValidatorTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: func(t *testing.T) *txs.Tx { + subnetID := ids.GenerateTestID() + return addPermissionlessValidatorTx(t, subnetID) + }, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, }, { name: "AddPermissionlessDelegatorTx Primary Network pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { - return addPermissionlessDelegatorTx(constants.PrimaryNetworkID) + chainTime: preEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + return addPermissionlessDelegatorTx(t, constants.PrimaryNetworkID) + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddPrimaryNetworkDelegatorFee, c.GetFee()) }, - expected: feeTestsDefaultCfg.AddPrimaryNetworkDelegatorFee, }, { name: "AddPermissionlessDelegatorTx pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { + chainTime: preEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { subnetID := ids.GenerateTestID() require.NotEqual(t, constants.PrimaryNetworkID, subnetID) - return addPermissionlessDelegatorTx(subnetID) + return addPermissionlessDelegatorTx(t, subnetID) + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.AddSubnetDelegatorFee, c.GetFee()) }, - expected: feeTestsDefaultCfg.AddSubnetDelegatorFee, }, { - name: "BaseTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: baseTx, - expected: feeTestsDefaultCfg.TxFee, + name: "AddPermissionlessDelegatorTx Primary Network post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + return addPermissionlessDelegatorTx(t, constants.PrimaryNetworkID) + }, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 3_120*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(312), haveGas) + }, }, { - name: "ImportTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: importTx, - expected: feeTestsDefaultCfg.TxFee, + name: "AddPermissionlessDelegatorTx Subnet post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: func(t *testing.T) *txs.Tx { + return addPermissionlessDelegatorTx(t, constants.PrimaryNetworkID) + }, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 3_120*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(312), haveGas) + }, }, { - name: "ExportTx pre EUpgrade", - chainTime: preEUpgradeTime, - unsignedTx: exportTx, - expected: feeTestsDefaultCfg.TxFee, + name: "AddPermissionlessDelegatorTx Subnet post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: func(t *testing.T) *txs.Tx { + subnetID := ids.GenerateTestID() + require.NotEqual(t, constants.PrimaryNetworkID, subnetID) + return addPermissionlessValidatorTx(t, subnetID) + }, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "BaseTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: baseTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TxFee, c.GetFee()) + }, + }, + { + name: "BaseTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: baseTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 1_810*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(181), haveGas) + }, + }, + { + name: "BaseTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: baseTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "ImportTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: importTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TxFee, c.GetFee()) + }, + }, + { + name: "ImportTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: importTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 3_120*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(312), haveGas) + }, + }, + { + name: "ImportTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: importTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, + }, + { + name: "ExportTx pre EUpgrade", + chainTime: preEUpgradeTime, + signedTxF: exportTx, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, feeTestsDefaultCfg.TxFee, c.GetFee()) + }, + }, + { + name: "ExportTx post EUpgrade, success", + chainTime: postEUpgradeTime, + signedTxF: exportTx, + expectedError: nil, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, 2_040*units.NanoAvax, c.GetFee()) + haveGas, err := c.GetBlockGas() + require.NoError(t, err) + require.Equal(t, fee.Gas(204), haveGas) + }, + }, + { + name: "ExportTx post EUpgrade, utxos read cap breached", + chainTime: postEUpgradeTime, + gasCapF: func() fee.Gas { + return testBlockMaxGas - 1 + }, + signedTxF: exportTx, + expectedError: errFailedComplexityCumulation, + checksF: func(*testing.T, Calculator) {}, }, { name: "RewardValidatorTx pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { - return &txs.RewardValidatorTx{ - TxID: ids.GenerateTestID(), + chainTime: preEUpgradeTime, + signedTxF: func(_ *testing.T) *txs.Tx { + return &txs.Tx{ + Unsigned: &txs.RewardValidatorTx{ + TxID: ids.GenerateTestID(), + }, + } + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, uint64(0), c.GetFee()) + }, + }, + { + name: "RewardValidatorTx post EUpgrade", + chainTime: postEUpgradeTime, + signedTxF: func(_ *testing.T) *txs.Tx { + return &txs.Tx{ + Unsigned: &txs.RewardValidatorTx{ + TxID: ids.GenerateTestID(), + }, } }, - expected: 0, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, uint64(0), c.GetFee()) + }, }, { name: "AdvanceTimeTx pre EUpgrade", - chainTime: upgrades.EUpgradeTime.Add(-1 * time.Second), - unsignedTx: func() txs.UnsignedTx { - return &txs.AdvanceTimeTx{ - Time: uint64(time.Now().Unix()), + chainTime: preEUpgradeTime, + signedTxF: func(_ *testing.T) *txs.Tx { + return &txs.Tx{ + Unsigned: &txs.AdvanceTimeTx{ + Time: uint64(time.Now().Unix()), + }, + } + }, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, uint64(0), c.GetFee()) + }, + }, + { + name: "AdvanceTimeTx post EUpgrade", + chainTime: postEUpgradeTime, + signedTxF: func(_ *testing.T) *txs.Tx { + return &txs.Tx{ + Unsigned: &txs.AdvanceTimeTx{ + Time: uint64(time.Now().Unix()), + }, } }, - expected: 0, + checksF: func(t *testing.T, c Calculator) { + require.Equal(t, uint64(0), c.GetFee()) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - uTx := tt.unsignedTx() - fc := NewStaticCalculator(feeTestsDefaultCfg, upgrades, tt.chainTime) - fee, err := fc.CalculateFee(&txs.Tx{Unsigned: uTx}) - require.NoError(t, err) - require.Equal(t, tt.expected, fee) + gasCap := testBlockMaxGas + if tt.gasCapF != nil { + gasCap = tt.gasCapF() + } + + var c Calculator + if !upgrades.IsEActivated(tt.chainTime) { + c = NewStaticCalculator(feeTestsDefaultCfg, upgrades, tt.chainTime) + } else { + c = NewDynamicCalculator(fee.NewCalculator(testFeeWeights, testGasPrice, gasCap)) + } + + sTx := tt.signedTxF(t) + _, _ = c.CalculateFee(sTx) + tt.checksF(t, c) }) } } -func addValidatorTx() txs.UnsignedTx { - return &txs.AddValidatorTx{} +func addValidatorTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddValidatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + StakeOuts: stakes, + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + return sTx } -func addSubnetValidatorTx() txs.UnsignedTx { - return &txs.AddSubnetValidatorTx{} +func addSubnetValidatorTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + subnetID := ids.GenerateTestID() + baseTx, _, subnetAuth := txsCreationHelpers(defaultCtx) + uTx := &txs.AddSubnetValidatorTx{ + BaseTx: baseTx, + SubnetValidator: txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + Subnet: subnetID, + }, + SubnetAuth: subnetAuth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func addDelegatorTx() txs.UnsignedTx { - return &txs.AddDelegatorTx{} +func addDelegatorTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddDelegatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + StakeOuts: stakes, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func createChainTx() txs.UnsignedTx { - return &txs.CreateChainTx{} +func createChainTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, subnetAuth := txsCreationHelpers(defaultCtx) + uTx := &txs.CreateChainTx{ + BaseTx: baseTx, + SubnetID: ids.GenerateTestID(), + ChainName: "testingStuff", + VMID: ids.GenerateTestID(), + FxIDs: []ids.ID{ids.GenerateTestID()}, + GenesisData: []byte{0xff}, + SubnetAuth: subnetAuth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func createSubnetTx() txs.UnsignedTx { - return &txs.CreateSubnetTx{} +func createSubnetTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.CreateSubnetTx{ + BaseTx: baseTx, + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func removeSubnetValidatorTx() txs.UnsignedTx { - return &txs.RemoveSubnetValidatorTx{} +func removeSubnetValidatorTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, auth := txsCreationHelpers(defaultCtx) + uTx := &txs.RemoveSubnetValidatorTx{ + BaseTx: baseTx, + NodeID: ids.GenerateTestNodeID(), + Subnet: ids.GenerateTestID(), + SubnetAuth: auth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func transformSubnetTx() txs.UnsignedTx { - return &txs.TransformSubnetTx{} +func transformSubnetTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, auth := txsCreationHelpers(defaultCtx) + uTx := &txs.TransformSubnetTx{ + BaseTx: baseTx, + Subnet: ids.GenerateTestID(), + AssetID: ids.GenerateTestID(), + InitialSupply: 0x1000000000000000, + MaximumSupply: 0x1000000000000000, + MinConsumptionRate: 0, + MaxConsumptionRate: 0, + MinValidatorStake: 1, + MaxValidatorStake: 0x1000000000000000, + MinStakeDuration: 1, + MaxStakeDuration: 1, + MinDelegationFee: 0, + MinDelegatorStake: 0xffffffffffffffff, + MaxValidatorWeightFactor: 255, + UptimeRequirement: 0, + SubnetAuth: auth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func transferSubnetOwnershipTx() txs.UnsignedTx { - return &txs.TransferSubnetOwnershipTx{} +func transferSubnetOwnershipTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.TransferSubnetOwnershipTx{ + BaseTx: baseTx, + Subnet: ids.GenerateTestID(), + SubnetAuth: &secp256k1fx.Input{ + SigIndices: []uint32{3}, + }, + Owner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func addPermissionlessValidatorTx(subnetID ids.ID) txs.UnsignedTx { - return &txs.AddPermissionlessValidatorTx{ - Subnet: subnetID, +func addPermissionlessValidatorTx(t *testing.T, subnetID ids.ID) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + sk, err := bls.NewSecretKey() + r.NoError(err) + uTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: baseTx, + Subnet: subnetID, + Signer: signer.NewProofOfPossession(sk), + StakeOuts: stakes, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DelegationShares: reward.PercentDenominator, } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func addPermissionlessDelegatorTx(subnetID ids.ID) txs.UnsignedTx { - return &txs.AddPermissionlessDelegatorTx{ - Subnet: subnetID, +func addPermissionlessDelegatorTx(t *testing.T, subnetID ids.ID) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddPermissionlessDelegatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 12345, + End: 12345 + 200*24*60*60, + Wght: 2 * units.KiloAvax, + }, + Subnet: subnetID, + StakeOuts: stakes, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx +} + +func baseTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &baseTx + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func baseTx() txs.UnsignedTx { - return &txs.BaseTx{} +func importTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.ImportTx{ + BaseTx: baseTx, + SourceChain: ids.GenerateTestID(), + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(1), + OutputIndex: 1, + }, + Asset: avax.Asset{ID: ids.ID{'a', 's', 's', 'e', 'r', 't'}}, + In: &secp256k1fx.TransferInput{ + Amt: 50000, + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + }}, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func importTx() txs.UnsignedTx { - return &txs.ImportTx{} +func exportTx(t *testing.T) *txs.Tx { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, outputs, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.ExportTx{ + BaseTx: baseTx, + DestinationChain: ids.GenerateTestID(), + ExportedOutputs: outputs, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + return sTx } -func exportTx() txs.UnsignedTx { - return &txs.ExportTx{} +func txsCreationHelpers(defaultCtx *snow.Context) ( + baseTx txs.BaseTx, + stakes []*avax.TransferableOutput, + auth *secp256k1fx.Input, +) { + inputs := []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + }} + outputs := []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }} + stakes = []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Second).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: feeTestDefaultStakeWeight, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }, + }} + auth = &secp256k1fx.Input{ + SigIndices: []uint32{0, 1}, + } + baseTx = txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: defaultCtx.NetworkID, + BlockchainID: defaultCtx.ChainID, + Ins: inputs, + Outs: outputs, + }, + } + + return baseTx, stakes, auth } diff --git a/vms/platformvm/txs/fee/dynamic_calculator.go b/vms/platformvm/txs/fee/dynamic_calculator.go new file mode 100644 index 000000000000..3b28858733a0 --- /dev/null +++ b/vms/platformvm/txs/fee/dynamic_calculator.go @@ -0,0 +1,312 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fee" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +const StakerLookupCost uint64 = 1000 // equal to secp256k1fx.CostPerSignature + +var ( + _ Calculator = (*dynamicCalculator)(nil) + _ txs.Visitor = (*staticCalculator)(nil) + + errFailedFeeCalculation = errors.New("failed fee calculation") +) + +func NewDynamicCalculator(fc *fee.Calculator) Calculator { + return &dynamicCalculator{ + fc: fc, + buildingTx: false, + // credentials are set when computeFee is called + } +} + +func NewBuildingDynamicCalculator(fc *fee.Calculator) Calculator { + return &dynamicCalculator{ + fc: fc, + buildingTx: true, + // credentials are set when computeFee is called + } +} + +type dynamicCalculator struct { + // inputs + fc *fee.Calculator + cred []verify.Verifiable + + buildingTx bool + + // outputs of visitor execution + fee uint64 +} + +func (c *dynamicCalculator) CalculateFee(tx *txs.Tx) (uint64, error) { + c.setCredentials(tx.Creds) + c.fee = 0 // zero fee among different calculateFee invocations (unlike gas which gets cumulated) + err := tx.Unsigned.Visit(c) + if !c.buildingTx { + err = errors.Join(err, c.fc.DoneWithLatestTx()) + } + return c.fee, err +} + +func (c *dynamicCalculator) AddFeesFor(complexity fee.Dimensions) (uint64, error) { + fee, err := c.fc.AddFeesFor(complexity) + if err != nil { + return 0, fmt.Errorf("failed cumulating complexity: %w", err) + } + + extraFee := fee - c.fee + c.fee = fee + return extraFee, nil +} + +func (c *dynamicCalculator) RemoveFeesFor(unitsToRm fee.Dimensions) (uint64, error) { + fee, err := c.fc.RemoveFeesFor(unitsToRm) + if err != nil { + return 0, fmt.Errorf("failed removing complexity: %w", err) + } + + removedFee := c.fee - fee + c.fee = fee + return removedFee, nil +} + +func (c *dynamicCalculator) GetFee() uint64 { return c.fee } + +func (c *dynamicCalculator) ResetFee(newFee uint64) { + c.fee = newFee +} + +func (c *dynamicCalculator) GetGasPrice() fee.GasPrice { return c.fc.GetGasPrice() } + +func (c *dynamicCalculator) GetBlockGas() (fee.Gas, error) { return c.fc.GetBlockGas() } + +func (c *dynamicCalculator) GetGasCap() fee.Gas { return c.fc.GetGasCap() } + +func (c *dynamicCalculator) setCredentials(creds []verify.Verifiable) { + c.cred = creds +} + +func (*dynamicCalculator) IsEActive() bool { return true } + +func (*dynamicCalculator) AddValidatorTx(*txs.AddValidatorTx) error { + // AddValidatorTx is banned following Durango activation + return errFailedFeeCalculation +} + +func (c *dynamicCalculator) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + complexity[fee.Compute] += StakerLookupCost + + _, err = c.AddFeesFor(complexity) + return err +} + +func (*dynamicCalculator) AddDelegatorTx(*txs.AddDelegatorTx) error { + // AddDelegatorTx is banned following Durango activation + return errFailedFeeCalculation +} + +func (c *dynamicCalculator) CreateChainTx(tx *txs.CreateChainTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) CreateSubnetTx(tx *txs.CreateSubnetTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) AdvanceTimeTx(*txs.AdvanceTimeTx) error { + c.fee = 0 // no fees + return nil +} + +func (c *dynamicCalculator) RewardValidatorTx(*txs.RewardValidatorTx) error { + c.fee = 0 // no fees + return nil +} + +func (c *dynamicCalculator) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + complexity[fee.Compute] += StakerLookupCost + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + complexity, err := c.meterTx(tx, outs, tx.Ins) + if err != nil { + return err + } + complexity[fee.Compute] += StakerLookupCost + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + complexity, err := c.meterTx(tx, outs, tx.Ins) + if err != nil { + return err + } + complexity[fee.Compute] += StakerLookupCost + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) BaseTx(tx *txs.BaseTx) error { + complexity, err := c.meterTx(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) ImportTx(tx *txs.ImportTx) error { + ins := make([]*avax.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) + copy(ins, tx.Ins) + copy(ins[len(tx.Ins):], tx.ImportedInputs) + + complexity, err := c.meterTx(tx, tx.Outs, ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) ExportTx(tx *txs.ExportTx) error { + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.ExportedOutputs)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.ExportedOutputs) + + complexity, err := c.meterTx(tx, outs, tx.Ins) + if err != nil { + return err + } + + _, err = c.AddFeesFor(complexity) + return err +} + +func (c *dynamicCalculator) meterTx( + uTx txs.UnsignedTx, + allOuts []*avax.TransferableOutput, + allIns []*avax.TransferableInput, +) (fee.Dimensions, error) { + var complexity fee.Dimensions + + uTxSize, err := txs.Codec.Size(txs.CodecVersion, uTx) + if err != nil { + return complexity, fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err) + } + complexity[fee.Bandwidth] = uint64(uTxSize) + + // meter credentials, one by one. Then account for the extra bytes needed to + // serialize a slice of credentials (codec version bytes + slice size bytes) + for i, cred := range c.cred { + c, ok := cred.(*secp256k1fx.Credential) + if !ok { + return complexity, fmt.Errorf("don't know how to calculate complexity of %T", cred) + } + credDimensions, err := fee.MeterCredential(txs.Codec, txs.CodecVersion, len(c.Sigs)) + if err != nil { + return complexity, fmt.Errorf("failed adding credential %d: %w", i, err) + } + complexity, err = fee.Add(complexity, credDimensions) + if err != nil { + return complexity, fmt.Errorf("failed adding credentials: %w", err) + } + } + complexity[fee.Bandwidth] += wrappers.IntLen // length of the credentials slice + complexity[fee.Bandwidth] += codec.VersionSize + + for _, in := range allIns { + inputDimensions, err := fee.MeterInput(txs.Codec, txs.CodecVersion, in) + if err != nil { + return complexity, fmt.Errorf("failed retrieving size of inputs: %w", err) + } + inputDimensions[fee.Bandwidth] = 0 // inputs bandwidth is already accounted for above, so we zero it + complexity, err = fee.Add(complexity, inputDimensions) + if err != nil { + return complexity, fmt.Errorf("failed adding inputs: %w", err) + } + } + + for _, out := range allOuts { + outputDimensions, err := fee.MeterOutput(txs.Codec, txs.CodecVersion, out) + if err != nil { + return complexity, fmt.Errorf("failed retrieving size of outputs: %w", err) + } + outputDimensions[fee.Bandwidth] = 0 // outputs bandwidth is already accounted for above, so we zero it + complexity, err = fee.Add(complexity, outputDimensions) + if err != nil { + return complexity, fmt.Errorf("failed adding outputs: %w", err) + } + } + + return complexity, nil +} diff --git a/vms/platformvm/txs/fee/dynamic_config.go b/vms/platformvm/txs/fee/dynamic_config.go new file mode 100644 index 000000000000..94372c9a19f1 --- /dev/null +++ b/vms/platformvm/txs/fee/dynamic_config.go @@ -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" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" + + commonfee "github.com/ava-labs/avalanchego/vms/components/fee" +) + +var ( + errDynamicFeeConfigNotAvailable = errors.New("dynamic fee config not available") + + eUpgradeDynamicFeesConfig = commonfee.DynamicFeesConfig{ + GasPrice: commonfee.GasPrice(10 * units.NanoAvax), + FeeDimensionWeights: commonfee.Dimensions{1, 1, 1, 1}, + MaxGasPerSecond: commonfee.Gas(1_000_000), + LeakGasCoeff: commonfee.Gas(1), + } + + customDynamicFeesConfig *commonfee.DynamicFeesConfig +) + +func init() { + if err := eUpgradeDynamicFeesConfig.Validate(); err != nil { + panic(err) + } +} + +func GetDynamicConfig(isEActive bool) (commonfee.DynamicFeesConfig, error) { + if !isEActive { + return commonfee.DynamicFeesConfig{}, errDynamicFeeConfigNotAvailable + } + + if customDynamicFeesConfig != nil { + return *customDynamicFeesConfig, nil + } + return eUpgradeDynamicFeesConfig, nil +} + +func ResetDynamicConfig(ctx *snow.Context, customFeesConfig *commonfee.DynamicFeesConfig) error { + if customFeesConfig == nil { + return nil // nothing to do + } + if ctx.NetworkID == constants.MainnetID || ctx.NetworkID == constants.FujiID { + return fmt.Errorf("forbidden resetting dynamic fee rates config for network %s", constants.NetworkName(ctx.NetworkID)) + } + if err := customFeesConfig.Validate(); err != nil { + return fmt.Errorf("invalid custom fee config: %w", err) + } + + customDynamicFeesConfig = customFeesConfig + return nil +} diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 7bfb5cf799a0..601ff27dd620 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -4,9 +4,12 @@ package fee import ( + "errors" "time" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/components/fee" + "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/upgrade" ) @@ -14,6 +17,8 @@ import ( var ( _ Calculator = (*staticCalculator)(nil) _ txs.Visitor = (*staticCalculator)(nil) + + errComplexityNotPriced = errors.New("complexity not priced") ) func NewStaticCalculator( @@ -44,6 +49,32 @@ func (c *staticCalculator) CalculateFee(tx *txs.Tx) (uint64, error) { return c.fee, err } +func (c *staticCalculator) GetFee() uint64 { + return c.fee +} + +func (c *staticCalculator) ResetFee(newFee uint64) { + c.fee = newFee +} + +func (*staticCalculator) AddFeesFor(fee.Dimensions) (uint64, error) { + return 0, errComplexityNotPriced +} + +func (*staticCalculator) RemoveFeesFor(fee.Dimensions) (uint64, error) { + return 0, errComplexityNotPriced +} + +func (*staticCalculator) GetGasPrice() fee.GasPrice { return fee.ZeroGasPrice } + +func (*staticCalculator) GetBlockGas() (fee.Gas, error) { return fee.ZeroGas, nil } + +func (*staticCalculator) GetGasCap() fee.Gas { return fee.ZeroGas } + +func (*staticCalculator) setCredentials([]verify.Verifiable) {} + +func (*staticCalculator) IsEActive() bool { return false } + func (c *staticCalculator) AddValidatorTx(*txs.AddValidatorTx) error { c.fee = c.staticCfg.AddPrimaryNetworkValidatorFee return nil