From 82bf033c3fb495d9441b752e4f168b3dc58513c8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 28 Aug 2024 16:40:37 -0400 Subject: [PATCH] Add P-chain dynamic fees execution (#3251) --- vms/platformvm/block/builder/builder.go | 209 ++++++++++++++---- vms/platformvm/block/executor/verifier.go | 36 +++ .../block/executor/verifier_test.go | 142 ++++++++++++ vms/platformvm/service_test.go | 28 +-- vms/platformvm/state/chain_time_helpers.go | 16 +- .../state/chain_time_helpers_test.go | 64 ++++++ vms/platformvm/txs/executor/state_changes.go | 18 +- .../txs/executor/state_changes_test.go | 100 +++++++++ vms/platformvm/txs/txstest/context.go | 40 ++-- vms/platformvm/txs/txstest/wallet.go | 2 +- vms/platformvm/vm_test.go | 7 +- wallet/chain/p/builder/context.go | 50 ----- wallet/chain/p/context.go | 84 +++++++ wallet/subnet/primary/api.go | 3 +- 14 files changed, 674 insertions(+), 125 deletions(-) create mode 100644 vms/platformvm/state/chain_time_helpers_test.go create mode 100644 vms/platformvm/txs/executor/state_changes_test.go create mode 100644 wallet/chain/p/context.go diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index 6cecad86184..35ad20adc16 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -19,10 +19,12 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" @@ -244,14 +246,30 @@ func (b *builder) PackAllBlockTxs() ([]*txs.Tx, error) { return nil, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID) } - return packBlockTxs( + timestamp, _, err := state.NextBlockTime(preferredState, b.txExecutorBackend.Clk) + if err != nil { + return nil, fmt.Errorf("could not calculate next staker change time: %w", err) + } + + if !b.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { + return packDurangoBlockTxs( + preferredID, + preferredState, + b.Mempool, + b.txExecutorBackend, + b.blkManager, + timestamp, + math.MaxInt, + ) + } + return packEtnaBlockTxs( preferredID, preferredState, b.Mempool, b.txExecutorBackend, b.blkManager, - b.txExecutorBackend.Clk.Time(), - math.MaxInt, + timestamp, + math.MaxUint64, ) } @@ -264,15 +282,31 @@ func buildBlock( forceAdvanceTime bool, parentState state.Chain, ) (block.Block, error) { - blockTxs, err := packBlockTxs( - parentID, - parentState, - builder.Mempool, - builder.txExecutorBackend, - builder.blkManager, - timestamp, - targetBlockSize, + var ( + blockTxs []*txs.Tx + err error ) + if builder.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { + blockTxs, err = packEtnaBlockTxs( + parentID, + parentState, + builder.Mempool, + builder.txExecutorBackend, + builder.blkManager, + timestamp, + 0, // minCapacity is 0 as we want to honor the capacity in state. + ) + } else { + blockTxs, err = packDurangoBlockTxs( + parentID, + parentState, + builder.Mempool, + builder.txExecutorBackend, + builder.blkManager, + timestamp, + targetBlockSize, + ) + } if err != nil { return nil, fmt.Errorf("failed to pack block txs: %w", err) } @@ -314,7 +348,7 @@ func buildBlock( ) } -func packBlockTxs( +func packDurangoBlockTxs( parentID ids.ID, parentState state.Chain, mempool mempool.Mempool, @@ -346,55 +380,154 @@ func packBlockTxs( if txSize > remainingSize { break } - mempool.Remove(tx) - - // Invariant: [tx] has already been syntactically verified. - txDiff, err := state.NewDiffOn(stateDiff) + shouldAdd, err := executeTx( + parentID, + stateDiff, + mempool, + backend, + manager, + &inputs, + feeCalculator, + tx, + ) if err != nil { return nil, err } + if !shouldAdd { + continue + } + + remainingSize -= txSize + blockTxs = append(blockTxs, tx) + } + + return blockTxs, nil +} + +func packEtnaBlockTxs( + parentID ids.ID, + parentState state.Chain, + mempool mempool.Mempool, + backend *txexecutor.Backend, + manager blockexecutor.Manager, + timestamp time.Time, + minCapacity gas.Gas, +) ([]*txs.Tx, error) { + stateDiff, err := state.NewDiffOn(parentState) + if err != nil { + return nil, err + } + + if _, err := txexecutor.AdvanceTimeTo(backend, stateDiff, timestamp); err != nil { + return nil, err + } + + feeState := stateDiff.GetFeeState() + capacity := max(feeState.Capacity, minCapacity) - executor := &txexecutor.StandardTxExecutor{ - Backend: backend, - State: txDiff, - FeeCalculator: feeCalculator, - Tx: tx, + var ( + blockTxs []*txs.Tx + inputs set.Set[ids.ID] + blockComplexity gas.Dimensions + feeCalculator = state.PickFeeCalculator(backend.Config, stateDiff) + ) + for { + tx, exists := mempool.Peek() + if !exists { + break } - err = tx.Unsigned.Visit(executor) + txComplexity, err := fee.TxComplexity(tx.Unsigned) if err != nil { - txID := tx.ID() - mempool.MarkDropped(txID, err) - continue + return nil, err } - - if inputs.Overlaps(executor.Inputs) { - txID := tx.ID() - mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) - continue + newBlockComplexity, err := blockComplexity.Add(&txComplexity) + if err != nil { + return nil, err } - err = manager.VerifyUniqueInputs(parentID, executor.Inputs) + newBlockGas, err := newBlockComplexity.ToGas(backend.Config.DynamicFeeConfig.Weights) if err != nil { - txID := tx.ID() - mempool.MarkDropped(txID, err) - continue + return nil, err + } + if newBlockGas > capacity { + break } - inputs.Union(executor.Inputs) - txDiff.AddTx(tx, status.Committed) - err = txDiff.Apply(stateDiff) + shouldAdd, err := executeTx( + parentID, + stateDiff, + mempool, + backend, + manager, + &inputs, + feeCalculator, + tx, + ) if err != nil { return nil, err } + if !shouldAdd { + continue + } - remainingSize -= txSize + blockComplexity = newBlockComplexity blockTxs = append(blockTxs, tx) } return blockTxs, nil } +func executeTx( + parentID ids.ID, + stateDiff state.Diff, + mempool mempool.Mempool, + backend *txexecutor.Backend, + manager blockexecutor.Manager, + inputs *set.Set[ids.ID], + feeCalculator fee.Calculator, + tx *txs.Tx, +) (bool, error) { + mempool.Remove(tx) + + // Invariant: [tx] has already been syntactically verified. + + txDiff, err := state.NewDiffOn(stateDiff) + if err != nil { + return false, err + } + + executor := &txexecutor.StandardTxExecutor{ + Backend: backend, + State: txDiff, + FeeCalculator: feeCalculator, + Tx: tx, + } + + err = tx.Unsigned.Visit(executor) + if err != nil { + txID := tx.ID() + mempool.MarkDropped(txID, err) + return false, nil + } + + if inputs.Overlaps(executor.Inputs) { + txID := tx.ID() + mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) + return false, nil + } + err = manager.VerifyUniqueInputs(parentID, executor.Inputs) + if err != nil { + txID := tx.ID() + mempool.MarkDropped(txID, err) + return false, nil + } + inputs.Union(executor.Inputs) + + txDiff.AddTx(tx, status.Committed) + return true, txDiff.Apply(stateDiff) +} + // getNextStakerToReward returns the next staker txID to remove from the staking // set with a RewardValidatorTx rather than an AdvanceTimeTx. [chainTimestamp] // is the timestamp of the chain at the time this validator would be getting diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index e238a35c100..532dc4d4b6f 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -460,6 +461,41 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato func(), error, ) { + // Complexity is limited first to avoid processing too large of a block. + if timestamp := state.GetTimestamp(); v.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { + var blockComplexity gas.Dimensions + for _, tx := range txs { + txComplexity, err := fee.TxComplexity(tx.Unsigned) + if err != nil { + txID := tx.ID() + v.MarkDropped(txID, err) + return nil, nil, nil, err + } + + blockComplexity, err = blockComplexity.Add(&txComplexity) + if err != nil { + return nil, nil, nil, err + } + } + + blockGas, err := blockComplexity.ToGas(v.txExecutorBackend.Config.DynamicFeeConfig.Weights) + if err != nil { + return nil, nil, nil, err + } + + // If this block exceeds the available capacity, ConsumeGas will return + // an error. + feeState := state.GetFeeState() + feeState, err = feeState.ConsumeGas(blockGas) + if err != nil { + return nil, nil, nil, err + } + + // Updating the fee state prior to executing the transactions is fine + // because the fee calculator was already created. + state.SetFeeState(feeState) + } + var ( onAcceptFunc func() inputs set.Set[ids.ID] diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index d2b131a3f2a..6508d4c8862 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -8,30 +8,88 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" + "github.com/ava-labs/avalanchego/vms/platformvm/utxo" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) +func newTestVerifier(t testing.TB, s state.State) *verifier { + require := require.New(t) + + mempool, err := mempool.New("", prometheus.NewRegistry(), nil) + require.NoError(err) + + var ( + upgrades = upgradetest.GetConfig(upgradetest.Latest) + ctx = snowtest.Context(t, constants.PlatformChainID) + clock = &mockable.Clock{} + fx = &secp256k1fx.Fx{} + ) + require.NoError(fx.InitializeVM(&secp256k1fx.TestVM{ + Clk: *clock, + Log: logging.NoLog{}, + })) + + return &verifier{ + backend: &backend{ + Mempool: mempool, + lastAccepted: s.GetLastAccepted(), + blkIDToState: make(map[ids.ID]*blockState), + state: s, + ctx: ctx, + }, + txExecutorBackend: &executor.Backend{ + Config: &config.Config{ + CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, + StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + SybilProtectionEnabled: true, + UpgradeConfig: upgrades, + }, + Ctx: ctx, + Clk: clock, + Fx: fx, + FlowChecker: utxo.NewVerifier( + ctx, + clock, + fx, + ), + Bootstrapped: utils.NewAtomic(true), + }, + } +} + func TestVerifierVisitProposalBlock(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -1042,3 +1100,87 @@ func TestVerifierVisitBanffAbortBlockUnexpectedParentState(t *testing.T) { err = verifier.BanffAbortBlock(blk) require.ErrorIs(err, state.ErrMissingParentState) } + +func TestBlockExecutionWithComplexity(t *testing.T) { + s := statetest.New(t, statetest.Config{}) + verifier := newTestVerifier(t, s) + wallet := txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + s, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + + baseTx0, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + require.NoError(t, err) + baseTx1, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + require.NoError(t, err) + + blockComplexity, err := fee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) + require.NoError(t, err) + blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) + require.NoError(t, err) + + tests := []struct { + name string + timestamp time.Time + expectedErr error + expectedFeeState gas.State + }{ + { + name: "no capacity", + timestamp: genesistest.DefaultValidatorStartTime, + expectedErr: gas.ErrInsufficientCapacity, + }, + { + name: "updates fee state", + timestamp: genesistest.DefaultValidatorStartTime.Add(10 * time.Second), + expectedFeeState: gas.State{ + Capacity: gas.Gas(0).AddPerSecond(verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxPerSecond, 10) - blockGas, + Excess: blockGas, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Clear the state to prevent prior tests from impacting this test. + clear(verifier.blkIDToState) + + verifier.txExecutorBackend.Clk.Set(test.timestamp) + timestamp, _, err := state.NextBlockTime(s, verifier.txExecutorBackend.Clk) + require.NoError(err) + + lastAcceptedID := s.GetLastAccepted() + lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + require.NoError(err) + + blk, err := block.NewBanffStandardBlock( + timestamp, + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{ + baseTx0, + baseTx1, + }, + ) + require.NoError(err) + + blkID := blk.ID() + err = blk.Visit(verifier) + require.ErrorIs(err, test.expectedErr) + if err != nil { + require.NotContains(verifier.blkIDToState, blkID) + return + } + + require.Contains(verifier.blkIDToState, blkID) + onAcceptState := verifier.blkIDToState[blkID].onAcceptState + require.Equal(test.expectedFeeState, onAcceptState.GetFeeState()) + }) + } +} diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 24edafbeca8..c896066142e 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -78,8 +78,8 @@ var ( } ) -func defaultService(t *testing.T) (*Service, *mutableSharedMemory) { - vm, _, mutableSharedMemory := defaultVM(t, upgradetest.Latest) +func defaultService(t *testing.T, fork upgradetest.Fork) (*Service, *mutableSharedMemory) { + vm, _, mutableSharedMemory := defaultVM(t, fork) return &Service{ vm: vm, addrManager: avax.NewAddressManager(vm.ctx), @@ -92,7 +92,7 @@ func defaultService(t *testing.T) (*Service, *mutableSharedMemory) { func TestExportKey(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) service.vm.ctx.Lock.Lock() ks := keystore.New(logging.NoLog{}, memdb.New()) @@ -122,7 +122,7 @@ func TestExportKey(t *testing.T) { // Test issuing a tx and accepted func TestGetTxStatus(t *testing.T) { require := require.New(t) - service, mutableSharedMemory := defaultService(t) + service, mutableSharedMemory := defaultService(t, upgradetest.Latest) service.vm.ctx.Lock.Lock() recipientKey, err := secp256k1.NewPrivateKey() @@ -304,7 +304,7 @@ func TestGetTx(t *testing.T) { ) t.Run(testName, func(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) service.vm.ctx.Lock.Lock() tx := test.createTx(t, service) @@ -366,10 +366,10 @@ func TestGetTx(t *testing.T) { func TestGetBalance(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Durango) feeCalculator := state.PickFeeCalculator(&service.vm.Config, service.vm.state) - createSubnetFee, err := feeCalculator.CalculateFee(&txs.CreateSubnetTx{}) + createSubnetFee, err := feeCalculator.CalculateFee(testSubnet1.Unsigned) require.NoError(err) // Ensure GetStake is correct for each of the genesis validators @@ -405,7 +405,7 @@ func TestGetBalance(t *testing.T) { func TestGetStake(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) // Ensure GetStake is correct for each of the genesis validators genesis := genesistest.New(t, genesistest.Config{}) @@ -605,7 +605,7 @@ func TestGetStake(t *testing.T) { func TestGetCurrentValidators(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) genesis := genesistest.New(t, genesistest.Config{}) @@ -741,7 +741,7 @@ func TestGetCurrentValidators(t *testing.T) { func TestGetTimestamp(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) reply := GetTimestampReply{} require.NoError(service.GetTimestamp(nil, nil, &reply)) @@ -777,7 +777,7 @@ func TestGetBlock(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) service.vm.ctx.Lock.Lock() service.vm.Config.CreateAssetTxFee = 100 * defaultTxFee @@ -1066,7 +1066,7 @@ func TestServiceGetBlockByHeight(t *testing.T) { func TestServiceGetSubnets(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) testSubnet1ID := testSubnet1.ID() @@ -1138,7 +1138,7 @@ func TestGetFeeConfig(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) service.vm.Config.UpgradeConfig.EtnaTime = test.etnaTime var reply gas.Config @@ -1152,7 +1152,7 @@ func FuzzGetFeeState(f *testing.F) { f.Fuzz(func(t *testing.T, capacity, excess uint64) { require := require.New(t) - service, _ := defaultService(t) + service, _ := defaultService(t, upgradetest.Latest) var ( expectedState = gas.State{ diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index 7b5dd6ebe10..8b861f69a26 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) @@ -77,7 +78,20 @@ func GetNextStakerChangeTime(state Chain) (time.Time, error) { // PickFeeCalculator does not modify [state]. func PickFeeCalculator(cfg *config.Config, state Chain) fee.Calculator { timestamp := state.GetTimestamp() - return NewStaticFeeCalculator(cfg, timestamp) + if !cfg.UpgradeConfig.IsEtnaActivated(timestamp) { + return NewStaticFeeCalculator(cfg, timestamp) + } + + feeState := state.GetFeeState() + gasPrice := gas.CalculatePrice( + cfg.DynamicFeeConfig.MinPrice, + feeState.Excess, + cfg.DynamicFeeConfig.ExcessConversionConstant, + ) + return fee.NewDynamicCalculator( + cfg.DynamicFeeConfig.Weights, + gasPrice, + ) } // NewStaticFeeCalculator creates a static fee calculator, with the config set diff --git a/vms/platformvm/state/chain_time_helpers_test.go b/vms/platformvm/state/chain_time_helpers_test.go new file mode 100644 index 00000000000..3a6304aaeea --- /dev/null +++ b/vms/platformvm/state/chain_time_helpers_test.go @@ -0,0 +1,64 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" +) + +func TestPickFeeCalculator(t *testing.T) { + var ( + createAssetTxFee = genesis.LocalParams.CreateAssetTxFee + staticFeeConfig = genesis.LocalParams.StaticFeeConfig + dynamicFeeConfig = genesis.LocalParams.DynamicFeeConfig + ) + + apricotPhase2StaticFeeConfig := staticFeeConfig + apricotPhase2StaticFeeConfig.CreateSubnetTxFee = createAssetTxFee + apricotPhase2StaticFeeConfig.CreateBlockchainTxFee = createAssetTxFee + + tests := []struct { + fork upgradetest.Fork + expected fee.Calculator + }{ + { + fork: upgradetest.ApricotPhase2, + expected: fee.NewStaticCalculator(apricotPhase2StaticFeeConfig), + }, + { + fork: upgradetest.ApricotPhase3, + expected: fee.NewStaticCalculator(staticFeeConfig), + }, + { + fork: upgradetest.Etna, + expected: fee.NewDynamicCalculator( + dynamicFeeConfig.Weights, + dynamicFeeConfig.MinPrice, + ), + }, + } + for _, test := range tests { + t.Run(test.fork.String(), func(t *testing.T) { + var ( + config = &config.Config{ + CreateAssetTxFee: createAssetTxFee, + StaticFeeConfig: staticFeeConfig, + DynamicFeeConfig: dynamicFeeConfig, + UpgradeConfig: upgradetest.GetConfig(test.fork), + } + s = newTestState(t, memdb.New()) + ) + actual := PickFeeCalculator(config, s) + require.Equal(t, test.expected, actual) + }) + } +} diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index 3086358304a..244be246f8a 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -165,12 +165,22 @@ func AdvanceTimeTo( changed = true } - if err := changes.Apply(parentState); err != nil { - return false, err + if backend.Config.UpgradeConfig.IsEtnaActivated(newChainTime) { + previousChainTime := changes.GetTimestamp() + duration := uint64(newChainTime.Sub(previousChainTime) / time.Second) + + feeState := changes.GetFeeState() + feeState = feeState.AdvanceTime( + backend.Config.DynamicFeeConfig.MaxCapacity, + backend.Config.DynamicFeeConfig.MaxPerSecond, + backend.Config.DynamicFeeConfig.TargetPerSecond, + duration, + ) + changes.SetFeeState(feeState) } - parentState.SetTimestamp(newChainTime) - return changed, nil + changes.SetTimestamp(newChainTime) + return changed, changes.Apply(parentState) } func GetRewardsCalculator( diff --git a/vms/platformvm/txs/executor/state_changes_test.go b/vms/platformvm/txs/executor/state_changes_test.go new file mode 100644 index 00000000000..5588f4b7da7 --- /dev/null +++ b/vms/platformvm/txs/executor/state_changes_test.go @@ -0,0 +1,100 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package executor + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/vms/components/gas" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" +) + +func TestAdvanceTimeTo_UpdatesFeeState(t *testing.T) { + const ( + secondsToAdvance = 3 + durationToAdvance = secondsToAdvance * time.Second + ) + + feeConfig := gas.Config{ + MaxCapacity: 1000, + MaxPerSecond: 100, + TargetPerSecond: 50, + } + + tests := []struct { + name string + fork upgradetest.Fork + initialState gas.State + expectedState gas.State + }{ + { + name: "Pre-Etna", + fork: upgradetest.Durango, + initialState: gas.State{}, + expectedState: gas.State{}, // Pre-Etna, fee state should not change + }, + { + name: "Etna with no usage", + initialState: gas.State{ + Capacity: feeConfig.MaxCapacity, + Excess: 0, + }, + expectedState: gas.State{ + Capacity: feeConfig.MaxCapacity, + Excess: 0, + }, + }, + { + name: "Etna with usage", + fork: upgradetest.Etna, + initialState: gas.State{ + Capacity: 1, + Excess: 10_000, + }, + expectedState: gas.State{ + Capacity: min(gas.Gas(1).AddPerSecond(feeConfig.MaxPerSecond, secondsToAdvance), feeConfig.MaxCapacity), + Excess: gas.Gas(10_000).SubPerSecond(feeConfig.TargetPerSecond, secondsToAdvance), + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ( + require = require.New(t) + + s = statetest.New(t, statetest.Config{}) + nextTime = s.GetTimestamp().Add(durationToAdvance) + ) + + // Ensure the invariant that [nextTime <= nextStakerChangeTime] on + // AdvanceTimeTo is maintained. + nextStakerChangeTime, err := state.GetNextStakerChangeTime(s) + require.NoError(err) + require.False(nextTime.After(nextStakerChangeTime)) + + s.SetFeeState(test.initialState) + + validatorsModified, err := AdvanceTimeTo( + &Backend{ + Config: &config.Config{ + DynamicFeeConfig: feeConfig, + UpgradeConfig: upgradetest.GetConfig(test.fork), + }, + }, + s, + nextTime, + ) + require.NoError(err) + require.False(validatorsModified) + require.Equal(test.expectedState, s.GetFeeState()) + require.Equal(nextTime, s.GetTimestamp()) + }) + } +} diff --git a/vms/platformvm/txs/txstest/context.go b/vms/platformvm/txs/txstest/context.go index 5f453429c22..4fcc0f790ec 100644 --- a/vms/platformvm/txs/txstest/context.go +++ b/vms/platformvm/txs/txstest/context.go @@ -4,27 +4,39 @@ package txstest import ( - "time" - "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/wallet/chain/p/builder" ) func newContext( ctx *snow.Context, - cfg *config.Config, - timestamp time.Time, + config *config.Config, + state state.State, ) *builder.Context { - feeConfig := cfg.StaticFeeConfig - if !cfg.UpgradeConfig.IsApricotPhase3Activated(timestamp) { - feeConfig.CreateSubnetTxFee = cfg.CreateAssetTxFee - feeConfig.CreateBlockchainTxFee = cfg.CreateAssetTxFee - } - - return &builder.Context{ - NetworkID: ctx.NetworkID, - AVAXAssetID: ctx.AVAXAssetID, - StaticFeeConfig: feeConfig, + var ( + timestamp = state.GetTimestamp() + builderContext = &builder.Context{ + NetworkID: ctx.NetworkID, + AVAXAssetID: ctx.AVAXAssetID, + } + ) + switch { + case config.UpgradeConfig.IsEtnaActivated(timestamp): + builderContext.ComplexityWeights = config.DynamicFeeConfig.Weights + builderContext.GasPrice = gas.CalculatePrice( + config.DynamicFeeConfig.MinPrice, + state.GetFeeState().Excess, + config.DynamicFeeConfig.ExcessConversionConstant, + ) + case config.UpgradeConfig.IsApricotPhase3Activated(timestamp): + builderContext.StaticFeeConfig = config.StaticFeeConfig + default: + builderContext.StaticFeeConfig = config.StaticFeeConfig + builderContext.StaticFeeConfig.CreateSubnetTxFee = config.CreateAssetTxFee + builderContext.StaticFeeConfig.CreateBlockchainTxFee = config.CreateAssetTxFee } + return builderContext } diff --git a/vms/platformvm/txs/txstest/wallet.go b/vms/platformvm/txs/txstest/wallet.go index 8f16b40719b..0a040eed7d8 100644 --- a/vms/platformvm/txs/txstest/wallet.go +++ b/vms/platformvm/txs/txstest/wallet.go @@ -81,7 +81,7 @@ func NewWallet( owners[subnetID] = owner } - builderContext := newContext(ctx, config, state.GetTimestamp()) + builderContext := newContext(ctx, config, state) backend := wallet.NewBackend( builderContext, common.NewChainUTXOs(constants.PlatformChainID, utxos), diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index d11141c923a..ab31ba76402 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -184,6 +184,9 @@ func defaultVM(t *testing.T, f upgradetest.Fork) (*VM, database.Database, *mutab // align chain time and local clock vm.state.SetTimestamp(vm.clock.Time()) + vm.state.SetFeeState(gas.State{ + Capacity: defaultDynamicFeeConfig.MaxCapacity, + }) require.NoError(vm.SetState(context.Background(), snow.NormalOp)) @@ -245,7 +248,7 @@ func newWallet(t testing.TB, vm *VM, c walletConfig) wallet.Wallet { // Ensure genesis state is parsed from bytes and stored correctly func TestGenesis(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, upgradetest.Latest) + vm, _, _ := defaultVM(t, upgradetest.Durango) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() @@ -2034,7 +2037,7 @@ func TestTransferSubnetOwnershipTx(t *testing.T) { func TestBaseTx(t *testing.T) { require := require.New(t) - vm, _, _ := defaultVM(t, upgradetest.Latest) + vm, _, _ := defaultVM(t, upgradetest.Durango) vm.ctx.Lock.Lock() defer vm.ctx.Lock.Unlock() diff --git a/wallet/chain/p/builder/context.go b/wallet/chain/p/builder/context.go index 2ddf168ad22..bb871a9f294 100644 --- a/wallet/chain/p/builder/context.go +++ b/wallet/chain/p/builder/context.go @@ -4,14 +4,10 @@ package builder import ( - "context" - - "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/vms/avm" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) @@ -26,52 +22,6 @@ type Context struct { GasPrice gas.Price } -func NewContextFromURI(ctx context.Context, uri string) (*Context, error) { - infoClient := info.NewClient(uri) - xChainClient := avm.NewClient(uri, "X") - return NewContextFromClients(ctx, infoClient, xChainClient) -} - -func NewContextFromClients( - ctx context.Context, - infoClient info.Client, - xChainClient avm.Client, -) (*Context, error) { - networkID, err := infoClient.GetNetworkID(ctx) - if err != nil { - return nil, err - } - - asset, err := xChainClient.GetAssetDescription(ctx, "AVAX") - if err != nil { - return nil, err - } - - txFees, err := infoClient.GetTxFee(ctx) - if err != nil { - return nil, err - } - - return &Context{ - NetworkID: networkID, - AVAXAssetID: asset.AssetID, - StaticFeeConfig: fee.StaticConfig{ - TxFee: uint64(txFees.TxFee), - CreateSubnetTxFee: uint64(txFees.CreateSubnetTxFee), - TransformSubnetTxFee: uint64(txFees.TransformSubnetTxFee), - CreateBlockchainTxFee: uint64(txFees.CreateBlockchainTxFee), - AddPrimaryNetworkValidatorFee: uint64(txFees.AddPrimaryNetworkValidatorFee), - AddPrimaryNetworkDelegatorFee: uint64(txFees.AddPrimaryNetworkDelegatorFee), - AddSubnetValidatorFee: uint64(txFees.AddSubnetValidatorFee), - AddSubnetDelegatorFee: uint64(txFees.AddSubnetDelegatorFee), - }, - - // TODO: Populate these fields once they are exposed by the API - ComplexityWeights: gas.Dimensions{}, - GasPrice: 0, - }, nil -} - func NewSnowContext(networkID uint32, avaxAssetID ids.ID) (*snow.Context, error) { lookup := ids.NewAliaser() return &snow.Context{ diff --git a/wallet/chain/p/context.go b/wallet/chain/p/context.go new file mode 100644 index 00000000000..a8685e4ee0d --- /dev/null +++ b/wallet/chain/p/context.go @@ -0,0 +1,84 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "context" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/vms/avm" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" +) + +// gasPriceMultiplier increases the gas price to support multiple transactions +// to be issued. +// +// TODO: Handle this better. Either here or in the mempool. +const gasPriceMultiplier = 2 + +func NewContextFromURI(ctx context.Context, uri string) (*builder.Context, error) { + infoClient := info.NewClient(uri) + xChainClient := avm.NewClient(uri, "X") + pChainClient := platformvm.NewClient(uri) + return NewContextFromClients(ctx, infoClient, xChainClient, pChainClient) +} + +func NewContextFromClients( + ctx context.Context, + infoClient info.Client, + xChainClient avm.Client, + pChainClient platformvm.Client, +) (*builder.Context, error) { + networkID, err := infoClient.GetNetworkID(ctx) + if err != nil { + return nil, err + } + + asset, err := xChainClient.GetAssetDescription(ctx, "AVAX") + if err != nil { + return nil, err + } + + dynamicFeeConfig, err := pChainClient.GetFeeConfig(ctx) + if err != nil { + return nil, err + } + + // TODO: After Etna is activated, assume the gas price is always non-zero. + if dynamicFeeConfig.MinPrice != 0 { + _, gasPrice, _, err := pChainClient.GetFeeState(ctx) + if err != nil { + return nil, err + } + + return &builder.Context{ + NetworkID: networkID, + AVAXAssetID: asset.AssetID, + ComplexityWeights: dynamicFeeConfig.Weights, + GasPrice: gasPriceMultiplier * gasPrice, + }, nil + } + + staticFeeConfig, err := infoClient.GetTxFee(ctx) + if err != nil { + return nil, err + } + + return &builder.Context{ + NetworkID: networkID, + AVAXAssetID: asset.AssetID, + StaticFeeConfig: fee.StaticConfig{ + TxFee: uint64(staticFeeConfig.TxFee), + CreateSubnetTxFee: uint64(staticFeeConfig.CreateSubnetTxFee), + TransformSubnetTxFee: uint64(staticFeeConfig.TransformSubnetTxFee), + CreateBlockchainTxFee: uint64(staticFeeConfig.CreateBlockchainTxFee), + AddPrimaryNetworkValidatorFee: uint64(staticFeeConfig.AddPrimaryNetworkValidatorFee), + AddPrimaryNetworkDelegatorFee: uint64(staticFeeConfig.AddPrimaryNetworkDelegatorFee), + AddSubnetValidatorFee: uint64(staticFeeConfig.AddSubnetValidatorFee), + AddSubnetDelegatorFee: uint64(staticFeeConfig.AddSubnetDelegatorFee), + }, + }, nil +} diff --git a/wallet/subnet/primary/api.go b/wallet/subnet/primary/api.go index 2aedc5c476c..d9f9e5e0624 100644 --- a/wallet/subnet/primary/api.go +++ b/wallet/subnet/primary/api.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/wallet/chain/c" + "github.com/ava-labs/avalanchego/wallet/chain/p" "github.com/ava-labs/avalanchego/wallet/chain/x" pbuilder "github.com/ava-labs/avalanchego/wallet/chain/p/builder" @@ -79,7 +80,7 @@ func FetchState( xClient := avm.NewClient(uri, "X") cClient := evm.NewCChainClient(uri) - pCTX, err := pbuilder.NewContextFromClients(ctx, infoClient, xClient) + pCTX, err := p.NewContextFromClients(ctx, infoClient, xClient, pClient) if err != nil { return nil, err }