diff --git a/tests/e2e/p/workflow.go b/tests/e2e/p/workflow.go index 5c8a3a5473ef..caf11ad96c3c 100644 --- a/tests/e2e/p/workflow.go +++ b/tests/e2e/p/workflow.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/avm" "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/txs/fees" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -52,6 +53,7 @@ var _ = e2e.DescribePChain("[Workflow]", func() { xBuilder := xWallet.Builder() xContext := xBuilder.Context() pChainClient := platformvm.NewClient(nodeURI.URI) + xChainClient := avm.NewClient(nodeURI.URI, "X") tests.Outf("{{blue}} fetching minimal stake amounts {{/}}\n") minValStake, minDelStake, err := pChainClient.GetMinStake(e2e.DefaultContext(), constants.PlatformChainID) @@ -188,9 +190,12 @@ var _ = e2e.DescribePChain("[Workflow]", func() { // retrieve fees paid for the tx feeCfg := config.GetDynamicFeesConfig(true /*isEActive*/) + feeRates, _, err := xChainClient.GetFeeRates(e2e.DefaultContext()) + require.NoError(err) + feeCalc := fees.NewDynamicCalculator( xbuilder.Parser.Codec(), - commonfees.NewManager(feeCfg.FeeRate), + commonfees.NewManager(feeRates), feeCfg.BlockMaxComplexity, tx.Creds, ) diff --git a/tests/e2e/x/dynamic_fees.go b/tests/e2e/x/dynamic_fees.go new file mode 100644 index 000000000000..6f78cd463d45 --- /dev/null +++ b/tests/e2e/x/dynamic_fees.go @@ -0,0 +1,126 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package x + +import ( + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/avm" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" + ginkgo "github.com/onsi/ginkgo/v2" +) + +var _ = ginkgo.Describe("[Dynamic Fees]", func() { + require := require.New(ginkgo.GinkgoT()) + + ginkgo.It("should ensure that the dynamic multifees are affected by load", func() { + customDynamicFeesConfig := commonfees.DynamicFeesConfig{ + InitialFeeRate: commonfees.Dimensions{1, 2, 3, 4}, + MinFeeRate: commonfees.Dimensions{1, 1, 1, 1}, + UpdateCoefficient: commonfees.Dimensions{1, 1, 1, 1}, + BlockMaxComplexity: commonfees.Max, + + // BlockUnitsTarget are set to cause an increase of fees while simple transactions are issued + BlockTargetComplexityRate: commonfees.Dimensions{300, 80, 150, 800}, + } + + ginkgo.By("creating a new private network to ensure isolation from other tests") + privateNetwork := &tmpnet.Network{ + Owner: "avalanchego-e2e-dynamic-fees", + ChainConfigs: map[string]tmpnet.FlagsMap{ + "X": { + "dynamic-fees-config": customDynamicFeesConfig, + }, + }, + } + e2e.Env.StartPrivateNetwork(privateNetwork) + + ginkgo.By("setup a wallet and a X-chain client") + node := privateNetwork.Nodes[0] + nodeURI := tmpnet.NodeURI{ + NodeID: node.NodeID, + URI: node.URI, + } + keychain := secp256k1fx.NewKeychain(privateNetwork.PreFundedKeys...) + baseWallet := e2e.NewWallet(keychain, nodeURI) + xWallet := baseWallet.X() + xChainClient := avm.NewClient(nodeURI.URI, "X") + + // retrieve initial balances + xBuilder := xWallet.Builder() + xContext := xBuilder.Context() + avaxAssetID := xContext.AVAXAssetID + xBalances, err := xWallet.Builder().GetFTBalance() + require.NoError(err) + xStartBalance := xBalances[avaxAssetID] + tests.Outf("{{blue}} X-chain initial balance: %d {{/}}\n", xStartBalance) + + ginkgo.By("checking that initial fee values match with configured ones", func() { + currFeeRates, _, err := xChainClient.GetFeeRates(e2e.DefaultContext()) + require.NoError(err) + require.Equal(customDynamicFeesConfig.InitialFeeRate, currFeeRates) + }) + + ginkgo.By("issue expensive transactions so to increase the fee rates to be paid for accepting the transactons", + func() { + currFeeRates := commonfees.Empty + + ginkgo.By("repeatedly change the permissioned subnet owner to increase fee rates", func() { + txsCount := 10 + for i := 0; i < txsCount; i++ { + owner := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keychain.Keys[1].Address(), + }, + } + + _, err := xWallet.IssueCreateAssetTx( + "HI", + "HI", + byte(txsCount), + map[uint32][]verify.State{ + 0: { + &secp256k1fx.TransferOutput{ + Amt: units.Schmeckle, + OutputOwners: owner, + }, + }, + }, + ) + require.NoError(err) + + updatedFeeRates, _, err := xChainClient.GetFeeRates(e2e.DefaultContext()) + require.NoError(err) + tests.Outf("{{blue}} current fee rates: %v {{/}}\n", updatedFeeRates) + + ginkgo.By("check that fee rates components have increased") + require.True(commonfees.Compare(currFeeRates, updatedFeeRates)) + currFeeRates = updatedFeeRates + } + }) + + ginkgo.By("wait for the fee rates to decrease", func() { + initialFeeRates := currFeeRates + e2e.Eventually(func() bool { + var err error + _, currFeeRates, err = xChainClient.GetFeeRates(e2e.DefaultContext()) + require.NoError(err) + tests.Outf("{{blue}} next fee rates: %v {{/}}\n", currFeeRates) + return commonfees.Compare(initialFeeRates, currFeeRates) + }, e2e.DefaultTimeout, e2e.DefaultPollingInterval, "failed to see gas price decrease before timeout") + tests.Outf("\n{{blue}}fee rates have decreased to %v{{/}}\n", currFeeRates) + }) + }, + ) + }) +}) diff --git a/tests/e2e/x/transfer/virtuous.go b/tests/e2e/x/transfer/virtuous.go index fef3aad0a304..940c0844ffe9 100644 --- a/tests/e2e/x/transfer/virtuous.go +++ b/tests/e2e/x/transfer/virtuous.go @@ -172,6 +172,12 @@ var _ = e2e.DescribeXChainSerial("[Virtuous Transfer Tx AVAX]", func() { require.Contains(err.Error(), "insufficient funds") }) + // retrieve the unit fees that will be used for the BaseTx + nodeURI := e2e.Env.GetRandomNodeURI() + xChainClient := avm.NewClient(nodeURI.URI, "X") + _, feeRates, err := xChainClient.GetFeeRates(e2e.DefaultContext()) + require.NoError(err) + tx, err := wallets[fromIdx].X().IssueBaseTx( []*avax.TransferableOutput{{ Asset: avax.Asset{ @@ -193,7 +199,7 @@ var _ = e2e.DescribeXChainSerial("[Virtuous Transfer Tx AVAX]", func() { feeCfg := config.GetDynamicFeesConfig(true /*isEActive*/) feeCalc := fees.NewDynamicCalculator( xbuilder.Parser.Codec(), - commonfees.NewManager(feeCfg.FeeRate), + commonfees.NewManager(feeRates), feeCfg.BlockMaxComplexity, tx.Creds, ) diff --git a/vms/avm/block/builder/builder.go b/vms/avm/block/builder/builder.go index d19ad7b6094d..8679558b0ef6 100644 --- a/vms/avm/block/builder/builder.go +++ b/vms/avm/block/builder/builder.go @@ -16,11 +16,12 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" + "github.com/ava-labs/avalanchego/vms/avm/txs/fees" "github.com/ava-labs/avalanchego/vms/avm/txs/mempool" - "github.com/ava-labs/avalanchego/vms/components/fees" blockexecutor "github.com/ava-labs/avalanchego/vms/avm/block/executor" txexecutor "github.com/ava-labs/avalanchego/vms/avm/txs/executor" + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) // targetBlockSize is the max block size we aim to produce @@ -76,13 +77,10 @@ func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { } preferredHeight := preferred.Height() - preferredTimestamp := preferred.Timestamp() - nextHeight := preferredHeight + 1 - nextTimestamp := b.clk.Time() // [timestamp] = max(now, parentTime) - if preferredTimestamp.After(nextTimestamp) { - nextTimestamp = preferredTimestamp - } + + parentBlkTime := preferred.Timestamp() + nextBlkTime := blockexecutor.NextBlockTime(parentBlkTime, b.clk) stateDiff, err := state.NewDiff(preferredID, b.manager) if err != nil { @@ -94,18 +92,28 @@ func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { inputs set.Set[ids.ID] remainingSize = targetBlockSize - chainTime = stateDiff.GetTimestamp() - isEActive = b.backend.Config.IsEActivated(chainTime) - feesCfg = config.GetDynamicFeesConfig(isEActive) - feeManager = fees.NewManager(feesCfg.FeeRate) + chainTime = stateDiff.GetTimestamp() + isEActive = b.backend.Config.IsEActivated(chainTime) + feesCfg = config.GetDynamicFeesConfig(isEActive) ) + + feeManager, err := fees.UpdatedFeeManager(stateDiff, b.backend.Config, parentBlkTime, nextBlkTime) + if err != nil { + return nil, err + } + for { tx, exists := b.mempool.Peek() - // Invariant: [mempool.MaxTxSize] < [targetBlockSize]. This guarantees - // that we will only stop building a block once there are no - // transactions in the mempool or the block is at least - // [targetBlockSize - mempool.MaxTxSize] bytes full. - if !exists || len(tx.Bytes()) > remainingSize { + if !exists { + break + } + txSize := len(tx.Bytes()) + + // pre e upgrade is active, we fill blocks till a target size + // post e upgrade is active, we fill blocks till a target complexity + done := (!isEActive && txSize > remainingSize) || + (isEActive && !commonfees.Compare(feeManager.GetCumulatedComplexity(), feesCfg.BlockTargetComplexityRate)) + if done { break } b.mempool.Remove(tx) @@ -156,9 +164,13 @@ func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { inputs.Union(executor.Inputs) txDiff.AddTx(tx) + txDiff.SetFeeRates(feeManager.GetFeeRates()) + txDiff.SetLastBlockComplexity(feeManager.GetCumulatedComplexity()) txDiff.Apply(stateDiff) - remainingSize -= len(tx.Bytes()) + if isEActive { + remainingSize -= txSize + } blockTxs = append(blockTxs, tx) } @@ -169,7 +181,7 @@ func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { statelessBlk, err := block.NewStandardBlock( preferredID, nextHeight, - nextTimestamp, + nextBlkTime, blockTxs, b.backend.Codec, ) diff --git a/vms/avm/block/builder/builder_test.go b/vms/avm/block/builder/builder_test.go index e5c939787aba..ca5c1c115aa9 100644 --- a/vms/avm/block/builder/builder_test.go +++ b/vms/avm/block/builder/builder_test.go @@ -36,6 +36,7 @@ import ( blkexecutor "github.com/ava-labs/avalanchego/vms/avm/block/executor" txexecutor "github.com/ava-labs/avalanchego/vms/avm/txs/executor" + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) const trackChecksums = false @@ -122,15 +123,19 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() unsignedTx := txs.NewMockUnsignedTx(ctrl) unsignedTx.EXPECT().Visit(gomock.Any()).Return(errTest) // Fail semantic verification + unsignedTx.EXPECT().SetBytes(gomock.Any()) // needed for the SetBytes below tx := &txs.Tx{Unsigned: unsignedTx} + tx.SetBytes([]byte{0x0, 0x1}, []byte{0xff, 0xff}) mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().Peek().Return(tx, true) @@ -169,16 +174,20 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() unsignedTx := txs.NewMockUnsignedTx(ctrl) unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // Pass semantic verification + unsignedTx.EXPECT().SetBytes(gomock.Any()) // needed for the SetBytes below unsignedTx.EXPECT().Visit(gomock.Any()).Return(errTest) // Fail execution tx := &txs.Tx{Unsigned: unsignedTx} + tx.SetBytes([]byte{0x0, 0x1}, []byte{0xff, 0xff}) mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().Peek().Return(tx, true) @@ -217,17 +226,21 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(errTest) unsignedTx := txs.NewMockUnsignedTx(ctrl) unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // Pass semantic verification unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // Pass execution + unsignedTx.EXPECT().SetBytes(gomock.Any()) // needed for the SetBytes below tx := &txs.Tx{Unsigned: unsignedTx} + tx.SetBytes([]byte{0x0, 0x1}, []byte{0xff, 0xff}) mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().Peek().Return(tx, true) @@ -266,6 +279,8 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) // tx1 and tx2 both consume [inputID]. // tx1 is added to the block first, so tx2 should be dropped. @@ -302,7 +317,7 @@ func TestBuilderBuildBlock(t *testing.T) { manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil) // Assert created block has one tx, tx1, // and other fields are set correctly. @@ -367,11 +382,13 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil) // Assert that the created block has the right timestamp manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn( @@ -383,6 +400,7 @@ func TestBuilderBuildBlock(t *testing.T) { inputID := ids.GenerateTestID() unsignedTx := txs.NewMockUnsignedTx(ctrl) + unsignedTx.EXPECT().SetBytes(gomock.Any()) // needed for the SetBytes below unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // Pass semantic verification unsignedTx.EXPECT().Visit(gomock.Any()).DoAndReturn( // Pass execution func(visitor txs.Visitor) error { @@ -394,6 +412,7 @@ func TestBuilderBuildBlock(t *testing.T) { ) unsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes() tx := &txs.Tx{Unsigned: unsignedTx} + tx.SetBytes([]byte{0x0, 0x1}, []byte{0xff, 0xff}) mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().Peek().Return(tx, true) @@ -444,11 +463,13 @@ func TestBuilderBuildBlock(t *testing.T) { preferredState := state.NewMockChain(ctrl) preferredState.EXPECT().GetLastAccepted().Return(preferredID) preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp) + preferredState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + preferredState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) manager := blkexecutor.NewMockManager(ctrl) manager.EXPECT().Preferred().Return(preferredID) manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil) - manager.EXPECT().GetState(preferredID).Return(preferredState, true) + manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes() manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil) // Assert that the created block has the right timestamp manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn( @@ -460,6 +481,7 @@ func TestBuilderBuildBlock(t *testing.T) { inputID := ids.GenerateTestID() unsignedTx := txs.NewMockUnsignedTx(ctrl) + unsignedTx.EXPECT().SetBytes(gomock.Any()) // needed for the SetBytes below unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // Pass semantic verification unsignedTx.EXPECT().Visit(gomock.Any()).DoAndReturn( // Pass execution func(visitor txs.Visitor) error { @@ -471,6 +493,7 @@ func TestBuilderBuildBlock(t *testing.T) { ) unsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes() tx := &txs.Tx{Unsigned: unsignedTx} + tx.SetBytes([]byte{0x0, 0x1}, []byte{0xff, 0xff}) mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().Peek().Return(tx, true) @@ -550,7 +573,7 @@ func TestBlockBuilderAddLocalTx(t *testing.T) { baseDB := versiondb.New(memdb.New()) - state, err := state.New(baseDB, parser, registerer, trackChecksums) + state, err := state.New(baseDB, parser, registerer, *backend.Config, trackChecksums) require.NoError(err) clk := &mockable.Clock{} diff --git a/vms/avm/block/executor/block.go b/vms/avm/block/executor/block.go index 8c4a6f7fc03c..811633781c6a 100644 --- a/vms/avm/block/executor/block.go +++ b/vms/avm/block/executor/block.go @@ -20,7 +20,7 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs/executor" - "github.com/ava-labs/avalanchego/vms/components/fees" + "github.com/ava-labs/avalanchego/vms/avm/txs/fees" ) const SyncBound = 10 * time.Second @@ -137,7 +137,10 @@ func (b *Block) Verify(context.Context) error { feesCfg = config.GetDynamicFeesConfig(isEActive) ) - feeManager := fees.NewManager(feesCfg.FeeRate) + feeManager, err := fees.UpdatedFeeManager(stateDiff, b.manager.backend.Config, parentChainTime, newChainTime) + if err != nil { + return err + } for _, tx := range txs { // Verify that the tx is valid according to the current state of the @@ -207,6 +210,11 @@ func (b *Block) Verify(context.Context) error { // Now that the block has been executed, we can add the block data to the // state diff. + if isEActive { + stateDiff.SetFeeRates(feeManager.GetFeeRates()) + stateDiff.SetLastBlockComplexity(feeManager.GetCumulatedComplexity()) + } + stateDiff.SetLastAccepted(blkID) stateDiff.AddBlock(b.Block) diff --git a/vms/avm/block/executor/block_test.go b/vms/avm/block/executor/block_test.go index 8e72f4c21ebe..38fb1614288b 100644 --- a/vms/avm/block/executor/block_test.go +++ b/vms/avm/block/executor/block_test.go @@ -28,6 +28,8 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/avm/txs/executor" "github.com/ava-labs/avalanchego/vms/avm/txs/mempool" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) func TestBlockVerify(t *testing.T) { @@ -128,10 +130,16 @@ func TestBlockVerify(t *testing.T) { mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().MarkDropped(errTx.ID(), errTest).Times(1) + + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + return &Block{ Block: mockBlock, manager: &manager{ backend: defaultTestBackend(false, nil), + state: mockState, mempool: mempool, metrics: metrics.NewMockMetrics(ctrl), blkIDToState: map[ids.ID]*blockState{}, @@ -161,6 +169,9 @@ func TestBlockVerify(t *testing.T) { mockState := state.NewMockState(ctrl) mockState.EXPECT().GetBlock(parentID).Return(nil, errTest) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + return &Block{ Block: mockBlock, manager: &manager{ @@ -193,10 +204,13 @@ func TestBlockVerify(t *testing.T) { parentID := ids.GenerateTestID() mockBlock.EXPECT().Parent().Return(parentID).AnyTimes() - mockState := state.NewMockState(ctrl) mockParentBlock := block.NewMockBlock(ctrl) mockParentBlock.EXPECT().Height().Return(blockHeight) // Should be blockHeight - 1 + + mockState := state.NewMockState(ctrl) mockState.EXPECT().GetBlock(parentID).Return(mockParentBlock, nil) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() return &Block{ Block: mockBlock, @@ -238,10 +252,15 @@ func TestBlockVerify(t *testing.T) { mockParentState.EXPECT().GetLastAccepted().Return(parentID) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp.Add(1)) + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + return &Block{ Block: mockBlock, manager: &manager{ backend: defaultTestBackend(false, nil), + state: mockState, blkIDToState: map[ids.ID]*blockState{ parentID: { onAcceptState: mockParentState, @@ -282,14 +301,21 @@ func TestBlockVerify(t *testing.T) { mockParentState := state.NewMockDiff(ctrl) mockParentState.EXPECT().GetLastAccepted().Return(parentID) + mockParentState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + mockParentState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp) + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().MarkDropped(tx.ID(), errTest).Times(1) return &Block{ Block: mockBlock, manager: &manager{ backend: defaultTestBackend(false, nil), + state: mockState, mempool: mempool, metrics: metrics.NewMockMetrics(ctrl), blkIDToState: map[ids.ID]*blockState{ @@ -334,6 +360,12 @@ func TestBlockVerify(t *testing.T) { mockParentState := state.NewMockDiff(ctrl) mockParentState.EXPECT().GetLastAccepted().Return(parentID) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp) + mockParentState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + mockParentState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) + + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().MarkDropped(tx.ID(), errTest).Times(1) @@ -343,6 +375,7 @@ func TestBlockVerify(t *testing.T) { mempool: mempool, metrics: metrics.NewMockMetrics(ctrl), backend: defaultTestBackend(false, nil), + state: mockState, blkIDToState: map[ids.ID]*blockState{ parentID: { onAcceptState: mockParentState, @@ -412,6 +445,12 @@ func TestBlockVerify(t *testing.T) { mockParentState := state.NewMockDiff(ctrl) mockParentState.EXPECT().GetLastAccepted().Return(parentID) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp) + mockParentState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + mockParentState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) + + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() mempool := mempool.NewMockMempool(ctrl) mempool.EXPECT().MarkDropped(tx2.ID(), ErrConflictingBlockTxs).Times(1) @@ -421,6 +460,7 @@ func TestBlockVerify(t *testing.T) { mempool: mempool, metrics: metrics.NewMockMetrics(ctrl), backend: defaultTestBackend(false, nil), + state: mockState, blkIDToState: map[ids.ID]*blockState{ parentID: { onAcceptState: mockParentState, @@ -474,11 +514,18 @@ func TestBlockVerify(t *testing.T) { mockParentState := state.NewMockDiff(ctrl) mockParentState.EXPECT().GetLastAccepted().Return(parentID) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp) + mockParentState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + mockParentState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) + + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() return &Block{ Block: mockBlock, manager: &manager{ backend: defaultTestBackend(false, nil), + state: mockState, blkIDToState: map[ids.ID]*blockState{ parentID: { onAcceptState: mockParentState, @@ -522,6 +569,12 @@ func TestBlockVerify(t *testing.T) { mockParentState := state.NewMockDiff(ctrl) mockParentState.EXPECT().GetLastAccepted().Return(parentID) mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp) + mockParentState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil) + mockParentState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil) + + mockState := state.NewMockState(ctrl) + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() mockMempool := mempool.NewMockMempool(ctrl) mockMempool.EXPECT().Remove([]*txs.Tx{tx}) @@ -531,6 +584,7 @@ func TestBlockVerify(t *testing.T) { mempool: mockMempool, metrics: metrics.NewMockMetrics(ctrl), backend: defaultTestBackend(false, nil), + state: mockState, blkIDToState: map[ids.ID]*blockState{ parentID: { onAcceptState: mockParentState, @@ -847,22 +901,33 @@ func TestBlockReject(t *testing.T) { mempool.EXPECT().RequestBuildBlock() lastAcceptedID := ids.GenerateTestID() + lastAcceptedMockBlock := block.NewMockBlock(ctrl) + lastAcceptedMockBlock.EXPECT().ID().Return(lastAcceptedID).AnyTimes() + lastAcceptedMockBlock.EXPECT().Timestamp().Return(time.Now().Truncate(time.Second)).AnyTimes() + mockState := state.NewMockState(ctrl) mockState.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes() + mockState.EXPECT().GetBlock(lastAcceptedID).Return(lastAcceptedMockBlock, nil).AnyTimes() mockState.EXPECT().GetTimestamp().Return(time.Now()).AnyTimes() + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + + manager := &manager{ + lastAccepted: lastAcceptedID, + mempool: mempool, + clk: &mockable.Clock{}, + metrics: metrics.NewMockMetrics(ctrl), + backend: defaultTestBackend(true, nil), + state: mockState, + blkIDToState: map[ids.ID]*blockState{ + blockID: {}, + }, + } + manager.SetPreference(lastAcceptedID) return &Block{ - Block: mockBlock, - manager: &manager{ - lastAccepted: lastAcceptedID, - mempool: mempool, - metrics: metrics.NewMockMetrics(ctrl), - backend: defaultTestBackend(true, nil), - state: mockState, - blkIDToState: map[ids.ID]*blockState{ - blockID: {}, - }, - }, + Block: mockBlock, + manager: manager, } }, }, @@ -900,22 +965,33 @@ func TestBlockReject(t *testing.T) { mempool.EXPECT().RequestBuildBlock() lastAcceptedID := ids.GenerateTestID() + lastAcceptedMockBlock := block.NewMockBlock(ctrl) + lastAcceptedMockBlock.EXPECT().ID().Return(lastAcceptedID).AnyTimes() + lastAcceptedMockBlock.EXPECT().Timestamp().Return(time.Now().Truncate(time.Second)).AnyTimes() + mockState := state.NewMockState(ctrl) mockState.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes() + mockState.EXPECT().GetBlock(lastAcceptedID).Return(lastAcceptedMockBlock, nil).AnyTimes() mockState.EXPECT().GetTimestamp().Return(time.Now()).AnyTimes() + mockState.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + mockState.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + + manager := &manager{ + lastAccepted: lastAcceptedID, + mempool: mempool, + clk: &mockable.Clock{}, + metrics: metrics.NewMockMetrics(ctrl), + backend: defaultTestBackend(true, nil), + state: mockState, + blkIDToState: map[ids.ID]*blockState{ + blockID: {}, + }, + } + manager.SetPreference(lastAcceptedID) return &Block{ - Block: mockBlock, - manager: &manager{ - lastAccepted: lastAcceptedID, - mempool: mempool, - metrics: metrics.NewMockMetrics(ctrl), - backend: defaultTestBackend(true, nil), - state: mockState, - blkIDToState: map[ids.ID]*blockState{ - blockID: {}, - }, - }, + Block: mockBlock, + manager: manager, } }, }, diff --git a/vms/avm/block/executor/helpers.go b/vms/avm/block/executor/helpers.go new file mode 100644 index 000000000000..9a60a24c1982 --- /dev/null +++ b/vms/avm/block/executor/helpers.go @@ -0,0 +1,20 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package executor + +import ( + "time" + + "github.com/ava-labs/avalanchego/utils/timer/mockable" +) + +func NextBlockTime(parentTime time.Time, clk *mockable.Clock) time.Time { + // [timestamp] = max(now, parentTime) + + timestamp := clk.Time() + if parentTime.After(timestamp) { + timestamp = parentTime + } + return timestamp +} diff --git a/vms/avm/block/executor/manager.go b/vms/avm/block/executor/manager.go index 768b91748fa9..0f7e74cad579 100644 --- a/vms/avm/block/executor/manager.go +++ b/vms/avm/block/executor/manager.go @@ -17,8 +17,8 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/avm/txs/executor" + "github.com/ava-labs/avalanchego/vms/avm/txs/fees" "github.com/ava-labs/avalanchego/vms/avm/txs/mempool" - "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -157,18 +157,28 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { return err } + preferredID := m.Preferred() + preferred, err := m.GetStatelessBlock(preferredID) + if err != nil { + return err + } + parentBlkTime := preferred.Timestamp() + nextBlkTime := NextBlockTime(preferred.Timestamp(), m.clk) + stateDiff, err := state.NewDiff(m.lastAccepted, m) if err != nil { return err } var ( - chainTime = m.state.GetTimestamp() - isEActive = m.backend.Config.IsEActivated(chainTime) + isEActive = m.backend.Config.IsEActivated(parentBlkTime) feesCfg = config.GetDynamicFeesConfig(isEActive) ) - feeManager := fees.NewManager(feesCfg.FeeRate) + feeManager, err := fees.UpdatedFeeManager(m.state, m.backend.Config, parentBlkTime, nextBlkTime) + if err != nil { + return err + } err = tx.Unsigned.Visit(&executor.SemanticVerifier{ Backend: m.backend, diff --git a/vms/avm/block/executor/manager_test.go b/vms/avm/block/executor/manager_test.go index 8c4bf737610f..9a6c711812d7 100644 --- a/vms/avm/block/executor/manager_test.go +++ b/vms/avm/block/executor/manager_test.go @@ -13,9 +13,12 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/avm/block" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -136,9 +139,15 @@ func TestManagerVerifyTx(t *testing.T) { Unsigned: unsigned, } }, - managerF: func(*gomock.Controller) *manager { + managerF: func(ctrl *gomock.Controller) *manager { + state := state.NewMockState(ctrl) + // state.EXPECT().GetTimestamp().Return(time.Time{}) + state.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + state.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() + return &manager{ backend: defaultTestBackend(true, nil), + state: state, } }, expectedErr: errTestSyntacticVerifyFail, @@ -157,17 +166,27 @@ func TestManagerVerifyTx(t *testing.T) { }, managerF: func(ctrl *gomock.Controller) *manager { lastAcceptedID := ids.GenerateTestID() + lastAcceptedMockBlock := block.NewMockBlock(ctrl) + lastAcceptedMockBlock.EXPECT().ID().Return(lastAcceptedID).AnyTimes() + lastAcceptedMockBlock.EXPECT().Timestamp().Return(time.Now().Truncate(time.Second)).AnyTimes() // These values don't matter for this test state := state.NewMockState(ctrl) - state.EXPECT().GetLastAccepted().Return(lastAcceptedID) - state.EXPECT().GetTimestamp().Return(time.Time{}).Times(2) + state.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes() + state.EXPECT().GetBlock(lastAcceptedID).Return(lastAcceptedMockBlock, nil).AnyTimes() + state.EXPECT().GetTimestamp().Return(time.Time{}).AnyTimes() + state.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + state.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() - return &manager{ + m := &manager{ backend: defaultTestBackend(true, nil), + clk: &mockable.Clock{}, state: state, lastAccepted: lastAcceptedID, } + m.SetPreference(lastAcceptedID) + + return m }, expectedErr: errTestSemanticVerifyFail, }, @@ -187,17 +206,26 @@ func TestManagerVerifyTx(t *testing.T) { }, managerF: func(ctrl *gomock.Controller) *manager { lastAcceptedID := ids.GenerateTestID() + lastAcceptedMockBlock := block.NewMockBlock(ctrl) + lastAcceptedMockBlock.EXPECT().ID().Return(lastAcceptedID).AnyTimes() + lastAcceptedMockBlock.EXPECT().Timestamp().Return(time.Now().Truncate(time.Second)).AnyTimes() // These values don't matter for this test state := state.NewMockState(ctrl) - state.EXPECT().GetLastAccepted().Return(lastAcceptedID) - state.EXPECT().GetTimestamp().Return(time.Time{}).Times(2) + state.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes() + state.EXPECT().GetBlock(lastAcceptedID).Return(lastAcceptedMockBlock, nil).AnyTimes() + state.EXPECT().GetTimestamp().Return(time.Time{}).AnyTimes() + state.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + state.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() - return &manager{ + m := &manager{ backend: defaultTestBackend(true, nil), + clk: &mockable.Clock{}, state: state, lastAccepted: lastAcceptedID, } + m.SetPreference(lastAcceptedID) + return m }, expectedErr: errTestExecutionFail, }, @@ -217,17 +245,26 @@ func TestManagerVerifyTx(t *testing.T) { }, managerF: func(ctrl *gomock.Controller) *manager { lastAcceptedID := ids.GenerateTestID() + lastAcceptedMockBlock := block.NewMockBlock(ctrl) + lastAcceptedMockBlock.EXPECT().ID().Return(lastAcceptedID).AnyTimes() + lastAcceptedMockBlock.EXPECT().Timestamp().Return(time.Now().Truncate(time.Second)).AnyTimes() // These values don't matter for this test state := state.NewMockState(ctrl) - state.EXPECT().GetLastAccepted().Return(lastAcceptedID) - state.EXPECT().GetTimestamp().Return(time.Time{}).Times(2) + state.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes() + state.EXPECT().GetBlock(lastAcceptedID).Return(lastAcceptedMockBlock, nil).AnyTimes() + state.EXPECT().GetTimestamp().Return(time.Time{}).AnyTimes() + state.EXPECT().GetFeeRates().Return(commonfees.Empty, nil).AnyTimes() + state.EXPECT().GetLastBlockComplexity().Return(commonfees.Empty, nil).AnyTimes() - return &manager{ + m := &manager{ backend: defaultTestBackend(true, nil), + clk: &mockable.Clock{}, state: state, lastAccepted: lastAcceptedID, } + m.SetPreference(lastAcceptedID) + return m }, expectedErr: nil, }, diff --git a/vms/avm/client.go b/vms/avm/client.go index 63df6543446e..49bfe05209a1 100644 --- a/vms/avm/client.go +++ b/vms/avm/client.go @@ -17,6 +17,8 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/rpc" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var _ Client = (*client)(nil) @@ -74,6 +76,8 @@ type Client interface { // // Deprecated: GetUTXOs should be used instead. GetAllBalances(ctx context.Context, addr ids.ShortID, includePartial bool, options ...rpc.Option) ([]Balance, error) + // GetFeeRates returns the current fee rates and the next fee rates that a transaction must pay to be accepted + GetFeeRates(ctx context.Context, options ...rpc.Option) (commonfees.Dimensions, commonfees.Dimensions, error) // CreateAsset creates a new asset and returns its assetID // // Deprecated: Transactions should be issued using the @@ -406,6 +410,12 @@ func (c *client) GetAllBalances( return res.Balances, err } +func (c *client) GetFeeRates(ctx context.Context, options ...rpc.Option) (commonfees.Dimensions, commonfees.Dimensions, error) { + res := &GetFeeRatesReply{} + err := c.requester.SendRequest(ctx, "avm.getFeeRates", struct{}{}, res, options...) + return res.CurrentFeeRates, res.NextFeeRates, err +} + // ClientHolder describes how much an address owns of an asset type ClientHolder struct { Amount uint64 diff --git a/vms/avm/config/dynamic_fees_config.go b/vms/avm/config/dynamic_fees_config.go index 9fc627793931..3d7c469c204b 100644 --- a/vms/avm/config/dynamic_fees_config.go +++ b/vms/avm/config/dynamic_fees_config.go @@ -13,21 +13,35 @@ import ( commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) -// EUpgradeDynamicFeesConfig to be tuned TODO ABENEGIA +func init() { + if err := eUpgradeDynamicFeesConfig.Validate(); err != nil { + panic(err) + } +} + +// eUpgradeDynamicFeesConfig to be tuned TODO ABENEGIA var ( eUpgradeDynamicFeesConfig = commonfees.DynamicFeesConfig{ - FeeRate: commonfees.Dimensions{ + InitialFeeRate: commonfees.Dimensions{ 1 * units.NanoAvax, 2 * units.NanoAvax, 3 * units.NanoAvax, 4 * units.NanoAvax, }, - - BlockMaxComplexity: commonfees.Max, + MinFeeRate: commonfees.Dimensions{}, + UpdateCoefficient: commonfees.Dimensions{ + 1, + 1, + 1, + 1, + }, + BlockMaxComplexity: commonfees.Max, + BlockTargetComplexityRate: commonfees.Dimensions{1000, 1000, 1000, 2000}, } + // TODO ABENEGIA: decide if and how to validate preEUpgradeDynamicFeesConfig preEUpgradeDynamicFeesConfig = commonfees.DynamicFeesConfig{ - FeeRate: commonfees.Empty, + InitialFeeRate: commonfees.Empty, BlockMaxComplexity: commonfees.Max, } diff --git a/vms/avm/environment_test.go b/vms/avm/environment_test.go index 254caf7ca325..53aa0cbb9e03 100644 --- a/vms/avm/environment_test.go +++ b/vms/avm/environment_test.go @@ -80,13 +80,26 @@ var ( addrs []ids.ShortID // addrs[i] corresponds to keys[i] testFeesCfg = commonfees.DynamicFeesConfig{ - FeeRate: commonfees.Dimensions{ + InitialFeeRate: commonfees.Dimensions{ 5 * units.NanoAvax, 5 * units.NanoAvax, 5 * units.NanoAvax, 5 * units.NanoAvax, }, - BlockMaxComplexity: commonfees.Max, + MinFeeRate: commonfees.Dimensions{ + 1 * units.NanoAvax, + 1 * units.NanoAvax, + 1 * units.NanoAvax, + 1 * units.NanoAvax, + }, + UpdateCoefficient: commonfees.Dimensions{ + 1, + 1, + 1, + 1, + }, + BlockMaxComplexity: commonfees.Max, + BlockTargetComplexityRate: commonfees.Dimensions{1000, 1000, 1000, 10000}, } ) @@ -222,6 +235,7 @@ func setup(tb testing.TB, c *envConfig) *environment { vm.feeAssetID, vm.ctx, &vm.Config, + &vm.clock, vm.state, vm.ctx.SharedMemory, vm.parser.Codec(), @@ -352,7 +366,7 @@ func newTx(tb testing.TB, genesisBytes []byte, chainID ids.ID, parser txs.Parser // Sample from a set of addresses and return them raw and formatted as strings. // The size of the sample is between 1 and len(addrs) // If len(addrs) == 0, returns nil -func sampleAddrs(tb testing.TB, addressFormatter avax.AddressManager, addrs []ids.ShortID) ([]ids.ShortID, []string) { +func sampleAddrs(tb testing.TB, addressFormatter avax.AddressManager, addrs []ids.ShortID) ([]ids.ShortID, []string) { //nolint:unparam require := require.New(tb) sampledAddrs := []ids.ShortID{} diff --git a/vms/avm/service.go b/vms/avm/service.go index e1e9382ed4ef..edd2ef13ed56 100644 --- a/vms/avm/service.go +++ b/vms/avm/service.go @@ -21,7 +21,9 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/avm/block/executor" "github.com/ava-labs/avalanchego/vms/avm/txs" + "github.com/ava-labs/avalanchego/vms/avm/txs/fees" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/keystore" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -30,6 +32,7 @@ import ( avajson "github.com/ava-labs/avalanchego/utils/json" safemath "github.com/ava-labs/avalanchego/utils/math" + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) const ( @@ -675,6 +678,51 @@ func (s *Service) GetAllBalances(_ *http.Request, args *GetAllBalancesArgs, repl return nil } +// GetFeeRatesReply is the response from GetFeeRates +type GetFeeRatesReply struct { + CurrentFeeRates commonfees.Dimensions `json:"currentFeeRates"` + NextFeeRates commonfees.Dimensions `json:"nextFeesRates"` +} + +// GetTimestamp returns the current timestamp on chain. +func (s *Service) GetFeeRates(_ *http.Request, _ *struct{}, reply *GetFeeRatesReply) error { + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getFeeRates"), + ) + + s.vm.ctx.Lock.Lock() + defer s.vm.ctx.Lock.Unlock() + + preferredID := s.vm.chainManager.Preferred() + onAccept, ok := s.vm.chainManager.GetState(preferredID) + if !ok { + return fmt.Errorf("could not retrieve state for block %s", preferredID) + } + + currentFeeRates, err := onAccept.GetFeeRates() + if err != nil { + return err + } + reply.CurrentFeeRates = currentFeeRates + + nextTimestamp := executor.NextBlockTime(onAccept.GetTimestamp(), &s.vm.clock) + isEActivated := s.vm.Config.IsEActivated(nextTimestamp) + + if !isEActivated { + reply.NextFeeRates = reply.CurrentFeeRates + return nil + } + + feeManager, err := fees.UpdatedFeeManager(onAccept, &s.vm.Config, onAccept.GetTimestamp(), nextTimestamp) + if err != nil { + return err + } + + reply.NextFeeRates = feeManager.GetFeeRates() + return nil +} + // Holder describes how much an address owns of an asset type Holder struct { Amount avajson.Uint64 `json:"amount"` diff --git a/vms/avm/service_backend.go b/vms/avm/service_backend.go index 60524e4320a4..e46cf1b6d659 100644 --- a/vms/avm/service_backend.go +++ b/vms/avm/service_backend.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -25,6 +26,7 @@ func newServiceBackend( feeAssetID ids.ID, ctx *snow.Context, cfg *config.Config, + clk *mockable.Clock, state state.State, sharedMemory atomic.SharedMemory, codec codec.Manager, @@ -41,6 +43,7 @@ func newServiceBackend( ctx: backendCtx, xchainID: ctx.XChainID, cfg: cfg, + clk: clk, state: state, sharedMemory: sharedMemory, codec: codec, @@ -51,6 +54,7 @@ type serviceBackend struct { ctx *builder.Context xchainID ids.ID cfg *config.Config + clk *mockable.Clock addrs set.Set[ids.ShortID] state state.State sharedMemory atomic.SharedMemory @@ -69,6 +73,10 @@ func (b *serviceBackend) Codec() codec.Manager { return b.codec } +func (b *serviceBackend) Clock() *mockable.Clock { + return b.clk +} + func (b *serviceBackend) Context() *builder.Context { return b.ctx } diff --git a/vms/avm/service_test.go b/vms/avm/service_test.go index 671ff753b64f..5d325b9ad35b 100644 --- a/vms/avm/service_test.go +++ b/vms/avm/service_test.go @@ -34,7 +34,6 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" - "github.com/ava-labs/avalanchego/vms/avm/txs/fees" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/index" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -43,7 +42,6 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" avajson "github.com/ava-labs/avalanchego/utils/json" - commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) func TestServiceIssueTx(t *testing.T) { @@ -556,6 +554,10 @@ func TestServiceGetTxJSON_BaseTx(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + newTx := newAvaxBaseTxWithOutputs(t, env) issueAndAccept(require, env.vm, env.issuer, newTx) @@ -594,7 +596,7 @@ func TestServiceGetTxJSON_BaseTx(t *testing.T) { "addresses": [ "X-testing1d6kkj0qh4wcmus3tk59npwt3rluc6en72ngurd" ], - "amount": 999990355, + "amount": 999992084, "locktime": 0, "threshold": 1 } @@ -653,6 +655,10 @@ func TestServiceGetTxJSON_ExportTx(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + newTx := buildTestExportTx(t, env, env.vm.ctx.CChainID) issueAndAccept(require, env.vm, env.issuer, newTx) @@ -678,7 +684,7 @@ func TestServiceGetTxJSON_ExportTx(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999990215, + "amount": 999991972, "locktime": 0, "threshold": 1 } @@ -756,6 +762,10 @@ func TestServiceGetTxJSON_CreateAssetTx(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + initialStates := map[uint32][]verify.State{ uint32(0): { &nftfx.MintOutput{ @@ -827,7 +837,7 @@ func TestServiceGetTxJSON_CreateAssetTx(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999990715, + "amount": 999992572, "locktime": 0, "threshold": 1 } @@ -922,7 +932,7 @@ func TestServiceGetTxJSON_CreateAssetTx(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0x11df1cb82f9a5e3b3ced9167654330e7782832c1189a04bb6b6207f7a69b979d55e5e3819744e4a13255ca724697c6a4ecab8cc9e8464cd2ec574e5b4bda1e2701" + "0xe943dfd81049dc87f0acecd7a94f2b42717891f230ce04d73fe501c9d4e29f8b5fcd3c6b763f1074da01799fb782d221bc6f5fdebce41180b18bd6aa1cff91c700" ] } } @@ -953,6 +963,10 @@ func TestServiceGetTxJSON_OperationTxWithNftxMintOp(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(1): { @@ -1002,7 +1016,7 @@ func TestServiceGetTxJSON_OperationTxWithNftxMintOp(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999983115, + "amount": 999988192, "locktime": 0, "threshold": 1 } @@ -1010,12 +1024,12 @@ func TestServiceGetTxJSON_OperationTxWithNftxMintOp(t *testing.T) { ], "inputs": [ { - "txID": "KGWg2g81xZHm3Enyd16GKh79tRgRK1hcFDsJe5eY9RZQAv5QG", + "txID": "L46hctVP2oNMKje6VQdk6bSxbXbi2BCgWvWWmHhFJ3yGZMTJc", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991615, + "amount": 999993292, "signatureIndices": [ 0 ] @@ -1059,7 +1073,7 @@ func TestServiceGetTxJSON_OperationTxWithNftxMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0xfb55fa51ca44aa974c03b5a612beba10e68ac01861f98dc31ff096013ba42dac05991bf05750ad38a283e3d5fbbbb4ac1efe4e9bc578272e5acf9a50927676ab00" + "0x3ca34a1e672a4d34b0ac30df5b90c0afeda87702464d13e9e69fb6173ae3537b5736c2d72848d86d99d7f91b3475abfba9c0cc32b10a9e29393144e4dedaf44800" ] } }, @@ -1067,7 +1081,7 @@ func TestServiceGetTxJSON_OperationTxWithNftxMintOp(t *testing.T) { "fxID": "qd2U4HDWUvMrVUeTcCHp6xH3Qpnn1XbU5MDdnBoiifFqvgXwT", "credential": { "signatures": [ - "0xfb55fa51ca44aa974c03b5a612beba10e68ac01861f98dc31ff096013ba42dac05991bf05750ad38a283e3d5fbbbb4ac1efe4e9bc578272e5acf9a50927676ab00" + "0x3ca34a1e672a4d34b0ac30df5b90c0afeda87702464d13e9e69fb6173ae3537b5736c2d72848d86d99d7f91b3475abfba9c0cc32b10a9e29393144e4dedaf44800" ] } } @@ -1104,6 +1118,10 @@ func TestServiceGetTxJSON_OperationTxWithMultipleNftxMintOp(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(0): { @@ -1156,7 +1174,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleNftxMintOp(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999982390, + "amount": 999987749, "locktime": 0, "threshold": 1 } @@ -1164,12 +1182,12 @@ func TestServiceGetTxJSON_OperationTxWithMultipleNftxMintOp(t *testing.T) { ], "inputs": [ { - "txID": "Pbg8AVMJUZUzPiFnnBvNKvW6Ljjobr43udPirFbfEYuW7t49z", + "txID": "2aTrnk4R7eRdaZjYi6JwwVmKkce8xqbXDubYE54q4ojUbkGx51", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991575, + "amount": 999993260, "signatureIndices": [ 0 ] @@ -1241,7 +1259,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleNftxMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "PLACEHOLDER_SIGNATURE" + "0x659fa1e905ccdb514746cee3f009d138e7a627504d9d1ac1f0bacfb31cef8250040db7ef63d6b941d3ffcbbd1f32ef236210a303d454a63ab9c7a1a51a07496c01" ] } }, @@ -1249,7 +1267,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleNftxMintOp(t *testing.T) { "fxID": "qd2U4HDWUvMrVUeTcCHp6xH3Qpnn1XbU5MDdnBoiifFqvgXwT", "credential": { "signatures": [ - "0x1bdb2512fa35d42023ac640f9e7f70d66f3a8262107fdbf9ef2c8d98a0f4444072f2be6bc7b8a2962c4cc9fe78b375ee9166f8c9410abc2318cbb55e7e97046500" + "0x659fa1e905ccdb514746cee3f009d138e7a627504d9d1ac1f0bacfb31cef8250040db7ef63d6b941d3ffcbbd1f32ef236210a303d454a63ab9c7a1a51a07496c01" ] } }, @@ -1294,6 +1312,10 @@ func TestServiceGetTxJSON_OperationTxWithSecpMintOp(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(0): { @@ -1340,7 +1362,7 @@ func TestServiceGetTxJSON_OperationTxWithSecpMintOp(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999983000, + "amount": 999988127, "locktime": 0, "threshold": 1 } @@ -1348,12 +1370,12 @@ func TestServiceGetTxJSON_OperationTxWithSecpMintOp(t *testing.T) { ], "inputs": [ { - "txID": "2MBRmBRnKGXkCe7Byc5g9xArAW24qjpKL9fDVNEWJht156xYKp", + "txID": "2amsBFNL9FXTY7A3jZegVgC2fkYcoSVAzUsoh4ywmrCbQXYYDt", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991635, + "amount": 999993308, "signatureIndices": [ 0 ] @@ -1401,7 +1423,7 @@ func TestServiceGetTxJSON_OperationTxWithSecpMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "PLACEHOLDER_SIGNATURE" + "0x3398fcc938cf42475a65d1d748e782d0d5be8b0b9039a210e6abe943983953e451fceccc283eda0a93859ac920d0cf45e63b7f326667f7e79ae0573192556f4a00" ] } }, @@ -1409,7 +1431,7 @@ func TestServiceGetTxJSON_OperationTxWithSecpMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0x79237564e745b0c0ccefaaf50902a98badb144645fedd898f0b8d3a5dac73013305b7e9d26dcfed97c56b05bca850c4e5fbb829da42a07c0bc6fb294efaf8a7101" + "0x3398fcc938cf42475a65d1d748e782d0d5be8b0b9039a210e6abe943983953e451fceccc283eda0a93859ac920d0cf45e63b7f326667f7e79ae0573192556f4a00" ] } } @@ -1446,6 +1468,10 @@ func TestServiceGetTxJSON_OperationTxWithMultipleSecpMintOp(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(0): { @@ -1496,7 +1522,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleSecpMintOp(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999982160, + "amount": 999987619, "locktime": 0, "threshold": 1 } @@ -1504,12 +1530,12 @@ func TestServiceGetTxJSON_OperationTxWithMultipleSecpMintOp(t *testing.T) { ], "inputs": [ { - "txID": "rS3zk6KTARY8H6njFhYbMs2tdCqgCnyRXBUUu1ta5jyqfXVq", + "txID": "2XFtZCtpqfcQC8zGDTSfyy8v5ks2WX83Dj4iXfoUjcEcHBA1CJ", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991615, + "amount": 999993292, "signatureIndices": [ 0 ] @@ -1597,7 +1623,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleSecpMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "PLACEHOLDER_SIGNATURE" + "0xd47a9aadf3acb5e46eca69104142c0c9a5b1db36c47255b0b764f56a4e3d0d4769a4a6bb1ca3d5159722635198d1edb85d4914c3732a391a4e8a96cb84205ad001" ] } }, @@ -1605,7 +1631,7 @@ func TestServiceGetTxJSON_OperationTxWithMultipleSecpMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0x06e33ad8322d6e670f8e923ace1a55606c5547527a1232d269f857a3388209a813c8aed7f87296ba04f033ed8e83a9dc4b7a3d9dd50c3b1d8b154e5bf34854b901" + "0xd47a9aadf3acb5e46eca69104142c0c9a5b1db36c47255b0b764f56a4e3d0d4769a4a6bb1ca3d5159722635198d1edb85d4914c3732a391a4e8a96cb84205ad001" ] } } @@ -1642,6 +1668,10 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOp(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(2): { @@ -1683,7 +1713,7 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOp(t *testing.T) { "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999983360, + "amount": 999988387, "locktime": 0, "threshold": 1 } @@ -1691,12 +1721,12 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOp(t *testing.T) { ], "inputs": [ { - "txID": "2JE2HjEceadRG2nPvZP3iaPdcG9N9zLhXy3t7RpDqNKFYitJuE", + "txID": "2qYV13hjDYcy8KQTCT4pUGdN29MTqGHdcgXuZhHhrHAVW4W3Cu", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991855, + "amount": 999993484, "signatureIndices": [ 0 ] @@ -1741,7 +1771,7 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOp(t *testing.T) { "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0xd9b773c483a938dd9bacffa82f5da5b17892d991189556b27665eb115989769a5fa8ae3eebe4fb463898ff4b1a0c536fc0f4b054e560d9cfde97b6e931e6099b00" + "0x5d55e6489ba9884d9fc88e1447d55e6a0fab85957e2afce004d85c5f34ce061e68c905707941fe16f59eb15df1ab1067c90c3102fea9e60d2ab505fbdf2d06fe00" ] } }, @@ -1786,6 +1816,10 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOpMultiple(t *testing.T) env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + key := keys[0] initialStates := map[uint32][]verify.State{ uint32(2): { @@ -1834,7 +1868,7 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOpMultiple(t *testing.T) "addresses": [ "X-testing1lnk637g0edwnqc2tn8tel39652fswa3xk4r65e" ], - "amount": 999982480, + "amount": 999987819, "locktime": 0, "threshold": 1 } @@ -1842,12 +1876,12 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOpMultiple(t *testing.T) ], "inputs": [ { - "txID": "2nDskbBWwToZFJ4F2PaTds76ET7UkNuhsmAsmtPpVyodWBZXS7", + "txID": "vu6reemKheEHERYDUArcb6U1T3CJWP149ux1Liga1LrcoH7ta", "outputIndex": 0, "assetID": "tvLKci3hNoCX4NijS6TfiT6XJJY3gGKd2git6SSVTG5J8Nfby", "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "amount": 999991655, + "amount": 999993324, "signatureIndices": [ 0 ] @@ -1921,7 +1955,7 @@ func TestServiceGetTxJSON_OperationTxWithPropertyFxMintOpMultiple(t *testing.T) "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "credential": { "signatures": [ - "0x10bab49817c5bdd16927979ec334ee7f162f9a5795cb0b01a9e183f7323d411e044ff7155a37a54ce7d928e0c80a120ed04707b067297ef16e80d7b14b4f321101" + "0xbe32eb12a68afae7955b88519c4499ce045f8883076410573fdf00f47139f1533be66ffcce307426a828b8c9a1ea719eb2197bab033e88f0ce05fecdb61d438601" ] } }, @@ -2631,7 +2665,7 @@ func TestNFTWorkflow(t *testing.T) { env.vm.ctx.Lock.Unlock() }() - fromAddrs, fromAddrsStr := sampleAddrs(t, env.vm.AddressManager, addrs) + _, fromAddrsStr := sampleAddrs(t, env.vm.AddressManager, addrs) // Test minting of the created variable cap asset addrStr, err := env.vm.FormatLocalAddress(keys[0].PublicKey().Address()) @@ -2663,61 +2697,6 @@ func TestNFTWorkflow(t *testing.T) { buildAndAccept(require, env.vm, env.issuer, createReply.AssetID) - // Key: Address - // Value: AVAX balance - balances := map[ids.ShortID]uint64{} - for _, addr := range addrs { // get balances for all addresses - addrStr, err := env.vm.FormatLocalAddress(addr) - require.NoError(err) - - reply := &GetBalanceReply{} - require.NoError(env.service.GetBalance(nil, - &GetBalanceArgs{ - Address: addrStr, - AssetID: env.vm.feeAssetID.String(), - }, - reply, - )) - - balances[addr] = uint64(reply.Balance) - } - - fromAddrsTotalBalance := uint64(0) - for _, addr := range fromAddrs { - fromAddrsTotalBalance += balances[addr] - } - - // retrieve tx fee - lastAcceptedBlkID := env.vm.chainManager.LastAccepted() - lastAcceptedBlk, err := env.vm.chainManager.GetStatelessBlock(lastAcceptedBlkID) - require.NoError(err) - txs := lastAcceptedBlk.Txs() - require.Len(txs, 1) - createAssetTx := txs[0] - - var ( - isEActive = env.vm.Config.IsEActivated(env.vm.state.GetTimestamp()) - - feeCalc *fees.Calculator - ) - if !isEActive { - feeCalc = fees.NewStaticCalculator(&env.vm.Config) - } else { - feesCfg := config.GetDynamicFeesConfig(isEActive) - feeCalc = fees.NewDynamicCalculator( - env.service.txBuilderBackend.codec, - commonfees.NewManager(feesCfg.FeeRate), - feesCfg.BlockMaxComplexity, - createAssetTx.Creds, - ) - } - - require.NoError(createAssetTx.Unsigned.Visit(feeCalc)) - expectedFee := feeCalc.Fee - - fromAddrsStartBalance := startBalance * uint64(len(fromAddrs)) - require.Equal(fromAddrsStartBalance-expectedFee, fromAddrsTotalBalance) - assetID := createReply.AssetID payload, err := formatting.Encode(formatting.Hex, []byte{1, 2, 3, 4, 5}) require.NoError(err) diff --git a/vms/avm/state/diff.go b/vms/avm/state/diff.go index 83e9c5e0d590..bdcca0e9c8bd 100644 --- a/vms/avm/state/diff.go +++ b/vms/avm/state/diff.go @@ -13,6 +13,8 @@ import ( "github.com/ava-labs/avalanchego/vms/avm/block" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/components/avax" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -38,8 +40,10 @@ type diff struct { addedBlockIDs map[uint64]ids.ID // map of height -> blockID addedBlocks map[ids.ID]block.Block // map of blockID -> block - lastAccepted ids.ID - timestamp time.Time + lastAccepted ids.ID + timestamp time.Time + feeRates *commonfees.Dimensions + lastBlkComplexity *commonfees.Dimensions } func NewDiff( @@ -161,6 +165,56 @@ func (d *diff) SetTimestamp(t time.Time) { d.timestamp = t } +func (d *diff) GetFeeRates() (commonfees.Dimensions, error) { + if d.feeRates == nil { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return commonfees.Empty, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + parentFeeRates, err := parentState.GetFeeRates() + if err != nil { + return commonfees.Empty, err + } + + d.feeRates = new(commonfees.Dimensions) + *d.feeRates = parentFeeRates + } + + return *d.feeRates, nil +} + +func (d *diff) SetFeeRates(uf commonfees.Dimensions) { + if d.feeRates == nil { + d.feeRates = new(commonfees.Dimensions) + } + *d.feeRates = uf +} + +func (d *diff) GetLastBlockComplexity() (commonfees.Dimensions, error) { + if d.lastBlkComplexity == nil { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return commonfees.Empty, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + parentBlkComplexity, err := parentState.GetLastBlockComplexity() + if err != nil { + return commonfees.Empty, err + } + + d.lastBlkComplexity = new(commonfees.Dimensions) + *d.lastBlkComplexity = parentBlkComplexity + } + + return *d.lastBlkComplexity, nil +} + +func (d *diff) SetLastBlockComplexity(complexity commonfees.Dimensions) { + if d.lastBlkComplexity == nil { + d.lastBlkComplexity = new(commonfees.Dimensions) + } + *d.lastBlkComplexity = complexity +} + func (d *diff) Apply(state Chain) { for utxoID, utxo := range d.modifiedUTXOs { if utxo != nil { @@ -180,4 +234,10 @@ func (d *diff) Apply(state Chain) { state.SetLastAccepted(d.lastAccepted) state.SetTimestamp(d.timestamp) + if d.feeRates != nil { + state.SetFeeRates(*d.feeRates) + } + if d.lastBlkComplexity != nil { + state.SetLastBlockComplexity(*d.lastBlkComplexity) + } } diff --git a/vms/avm/state/mock_state.go b/vms/avm/state/mock_state.go index 13ede9805d0b..d1c911d4338a 100644 --- a/vms/avm/state/mock_state.go +++ b/vms/avm/state/mock_state.go @@ -18,6 +18,7 @@ import ( block "github.com/ava-labs/avalanchego/vms/avm/block" txs "github.com/ava-labs/avalanchego/vms/avm/txs" avax "github.com/ava-labs/avalanchego/vms/components/avax" + fees "github.com/ava-labs/avalanchego/vms/components/fees" gomock "go.uber.org/mock/gomock" ) @@ -122,6 +123,21 @@ func (mr *MockChainMockRecorder) GetBlockIDAtHeight(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockIDAtHeight", reflect.TypeOf((*MockChain)(nil).GetBlockIDAtHeight), arg0) } +// GetFeeRates mocks base method. +func (m *MockChain) GetFeeRates() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeeRates") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFeeRates indicates an expected call of GetFeeRates. +func (mr *MockChainMockRecorder) GetFeeRates() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockChain)(nil).GetFeeRates)) +} + // GetLastAccepted mocks base method. func (m *MockChain) GetLastAccepted() ids.ID { m.ctrl.T.Helper() @@ -136,6 +152,21 @@ func (mr *MockChainMockRecorder) GetLastAccepted() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAccepted", reflect.TypeOf((*MockChain)(nil).GetLastAccepted)) } +// GetLastBlockComplexity mocks base method. +func (m *MockChain) GetLastBlockComplexity() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLastBlockComplexity") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLastBlockComplexity indicates an expected call of GetLastBlockComplexity. +func (mr *MockChainMockRecorder) GetLastBlockComplexity() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastBlockComplexity", reflect.TypeOf((*MockChain)(nil).GetLastBlockComplexity)) +} + // GetTimestamp mocks base method. func (m *MockChain) GetTimestamp() time.Time { m.ctrl.T.Helper() @@ -180,6 +211,18 @@ func (mr *MockChainMockRecorder) GetUTXO(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUTXO", reflect.TypeOf((*MockChain)(nil).GetUTXO), arg0) } +// SetFeeRates mocks base method. +func (m *MockChain) SetFeeRates(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetFeeRates", arg0) +} + +// SetFeeRates indicates an expected call of SetFeeRates. +func (mr *MockChainMockRecorder) SetFeeRates(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeeRates", reflect.TypeOf((*MockChain)(nil).SetFeeRates), arg0) +} + // SetLastAccepted mocks base method. func (m *MockChain) SetLastAccepted(arg0 ids.ID) { m.ctrl.T.Helper() @@ -192,6 +235,18 @@ func (mr *MockChainMockRecorder) SetLastAccepted(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastAccepted", reflect.TypeOf((*MockChain)(nil).SetLastAccepted), arg0) } +// SetLastBlockComplexity mocks base method. +func (m *MockChain) SetLastBlockComplexity(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLastBlockComplexity", arg0) +} + +// SetLastBlockComplexity indicates an expected call of SetLastBlockComplexity. +func (mr *MockChainMockRecorder) SetLastBlockComplexity(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastBlockComplexity", reflect.TypeOf((*MockChain)(nil).SetLastBlockComplexity), arg0) +} + // SetTimestamp mocks base method. func (m *MockChain) SetTimestamp(arg0 time.Time) { m.ctrl.T.Helper() @@ -375,6 +430,21 @@ func (mr *MockStateMockRecorder) GetBlockIDAtHeight(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockIDAtHeight", reflect.TypeOf((*MockState)(nil).GetBlockIDAtHeight), arg0) } +// GetFeeRates mocks base method. +func (m *MockState) GetFeeRates() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeeRates") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFeeRates indicates an expected call of GetFeeRates. +func (mr *MockStateMockRecorder) GetFeeRates() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockState)(nil).GetFeeRates)) +} + // GetLastAccepted mocks base method. func (m *MockState) GetLastAccepted() ids.ID { m.ctrl.T.Helper() @@ -389,6 +459,21 @@ func (mr *MockStateMockRecorder) GetLastAccepted() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAccepted", reflect.TypeOf((*MockState)(nil).GetLastAccepted)) } +// GetLastBlockComplexity mocks base method. +func (m *MockState) GetLastBlockComplexity() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLastBlockComplexity") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLastBlockComplexity indicates an expected call of GetLastBlockComplexity. +func (mr *MockStateMockRecorder) GetLastBlockComplexity() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastBlockComplexity", reflect.TypeOf((*MockState)(nil).GetLastBlockComplexity)) +} + // GetTimestamp mocks base method. func (m *MockState) GetTimestamp() time.Time { m.ctrl.T.Helper() @@ -433,6 +518,20 @@ func (mr *MockStateMockRecorder) GetUTXO(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUTXO", reflect.TypeOf((*MockState)(nil).GetUTXO), arg0) } +// InitFees mocks base method. +func (m *MockState) InitFees() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InitFees") + ret0, _ := ret[0].(error) + return ret0 +} + +// InitFees indicates an expected call of InitFees. +func (mr *MockStateMockRecorder) InitFees() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitFees", reflect.TypeOf((*MockState)(nil).InitFees)) +} + // InitializeChainState mocks base method. func (m *MockState) InitializeChainState(arg0 ids.ID, arg1 time.Time) error { m.ctrl.T.Helper() @@ -462,6 +561,18 @@ func (mr *MockStateMockRecorder) IsInitialized() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInitialized", reflect.TypeOf((*MockState)(nil).IsInitialized)) } +// SetFeeRates mocks base method. +func (m *MockState) SetFeeRates(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetFeeRates", arg0) +} + +// SetFeeRates indicates an expected call of SetFeeRates. +func (mr *MockStateMockRecorder) SetFeeRates(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeeRates", reflect.TypeOf((*MockState)(nil).SetFeeRates), arg0) +} + // SetInitialized mocks base method. func (m *MockState) SetInitialized() error { m.ctrl.T.Helper() @@ -488,6 +599,18 @@ func (mr *MockStateMockRecorder) SetLastAccepted(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastAccepted", reflect.TypeOf((*MockState)(nil).SetLastAccepted), arg0) } +// SetLastBlockComplexity mocks base method. +func (m *MockState) SetLastBlockComplexity(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLastBlockComplexity", arg0) +} + +// SetLastBlockComplexity indicates an expected call of SetLastBlockComplexity. +func (mr *MockStateMockRecorder) SetLastBlockComplexity(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastBlockComplexity", reflect.TypeOf((*MockState)(nil).SetLastBlockComplexity), arg0) +} + // SetTimestamp mocks base method. func (m *MockState) SetTimestamp(arg0 time.Time) { m.ctrl.T.Helper() @@ -628,6 +751,21 @@ func (mr *MockDiffMockRecorder) GetBlockIDAtHeight(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockIDAtHeight", reflect.TypeOf((*MockDiff)(nil).GetBlockIDAtHeight), arg0) } +// GetFeeRates mocks base method. +func (m *MockDiff) GetFeeRates() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeeRates") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFeeRates indicates an expected call of GetFeeRates. +func (mr *MockDiffMockRecorder) GetFeeRates() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockDiff)(nil).GetFeeRates)) +} + // GetLastAccepted mocks base method. func (m *MockDiff) GetLastAccepted() ids.ID { m.ctrl.T.Helper() @@ -642,6 +780,21 @@ func (mr *MockDiffMockRecorder) GetLastAccepted() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAccepted", reflect.TypeOf((*MockDiff)(nil).GetLastAccepted)) } +// GetLastBlockComplexity mocks base method. +func (m *MockDiff) GetLastBlockComplexity() (fees.Dimensions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLastBlockComplexity") + ret0, _ := ret[0].(fees.Dimensions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLastBlockComplexity indicates an expected call of GetLastBlockComplexity. +func (mr *MockDiffMockRecorder) GetLastBlockComplexity() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastBlockComplexity", reflect.TypeOf((*MockDiff)(nil).GetLastBlockComplexity)) +} + // GetTimestamp mocks base method. func (m *MockDiff) GetTimestamp() time.Time { m.ctrl.T.Helper() @@ -686,6 +839,18 @@ func (mr *MockDiffMockRecorder) GetUTXO(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUTXO", reflect.TypeOf((*MockDiff)(nil).GetUTXO), arg0) } +// SetFeeRates mocks base method. +func (m *MockDiff) SetFeeRates(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetFeeRates", arg0) +} + +// SetFeeRates indicates an expected call of SetFeeRates. +func (mr *MockDiffMockRecorder) SetFeeRates(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeeRates", reflect.TypeOf((*MockDiff)(nil).SetFeeRates), arg0) +} + // SetLastAccepted mocks base method. func (m *MockDiff) SetLastAccepted(arg0 ids.ID) { m.ctrl.T.Helper() @@ -698,6 +863,18 @@ func (mr *MockDiffMockRecorder) SetLastAccepted(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastAccepted", reflect.TypeOf((*MockDiff)(nil).SetLastAccepted), arg0) } +// SetLastBlockComplexity mocks base method. +func (m *MockDiff) SetLastBlockComplexity(arg0 fees.Dimensions) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLastBlockComplexity", arg0) +} + +// SetLastBlockComplexity indicates an expected call of SetLastBlockComplexity. +func (mr *MockDiffMockRecorder) SetLastBlockComplexity(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastBlockComplexity", reflect.TypeOf((*MockDiff)(nil).SetLastBlockComplexity), arg0) +} + // SetTimestamp mocks base method. func (m *MockDiff) SetTimestamp(arg0 time.Time) { m.ctrl.T.Helper() diff --git a/vms/avm/state/state.go b/vms/avm/state/state.go index 5005eb3dfc14..948b2be69cf6 100644 --- a/vms/avm/state/state.go +++ b/vms/avm/state/state.go @@ -17,8 +17,11 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/vms/avm/block" + "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/components/avax" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) const ( @@ -34,9 +37,11 @@ var ( blockPrefix = []byte("block") singletonPrefix = []byte("singleton") - isInitializedKey = []byte{0x00} - timestampKey = []byte{0x01} - lastAcceptedKey = []byte{0x02} + isInitializedKey = []byte{0x00} + timestampKey = []byte{0x01} + lastAcceptedKey = []byte{0x02} + feeRatesKey = []byte{0x03} + lastBlkComplexityKey = []byte{0x04} _ State = (*state)(nil) ) @@ -49,6 +54,9 @@ type ReadOnlyChain interface { GetBlock(blkID ids.ID) (block.Block, error) GetLastAccepted() ids.ID GetTimestamp() time.Time + + GetFeeRates() (commonfees.Dimensions, error) + GetLastBlockComplexity() (commonfees.Dimensions, error) } type Chain interface { @@ -60,6 +68,9 @@ type Chain interface { AddBlock(block block.Block) SetLastAccepted(blkID ids.ID) SetTimestamp(t time.Time) + + SetFeeRates(commonfees.Dimensions) + SetLastBlockComplexity(commonfees.Dimensions) } // State persistently maintains a set of UTXOs, transaction, statuses, and @@ -79,6 +90,8 @@ type State interface { // called during startup. InitializeChainState(stopVertexID ids.ID, genesisTimestamp time.Time) error + InitFees() error + // Discard uncommitted changes to the database. Abort() @@ -111,6 +124,7 @@ type State interface { * '-- lastAcceptedKey -> lastAccepted */ type state struct { + cfg config.Config parser block.Parser db *versiondb.Database @@ -135,6 +149,9 @@ type state struct { timestamp, persistedTimestamp time.Time singletonDB database.Database + feeRates *commonfees.Dimensions // pointer, to allow customization for test networks + lastBlkComplexity commonfees.Dimensions + trackChecksum bool txChecksum ids.ID } @@ -143,6 +160,7 @@ func New( db *versiondb.Database, parser block.Parser, metrics prometheus.Registerer, + cfg config.Config, trackChecksums bool, ) (State, error) { utxoDB := prefixdb.New(utxoPrefix, db) @@ -185,6 +203,7 @@ func New( s := &state{ parser: parser, + cfg: cfg, db: db, modifiedUTXOs: make(map[ids.ID]*avax.UTXO), @@ -203,8 +222,7 @@ func New( blockCache: blockCache, blockDB: blockDB, - singletonDB: singletonDB, - + singletonDB: singletonDB, trackChecksum: trackChecksums, } return s, s.initTxChecksum() @@ -345,6 +363,45 @@ func (s *state) InitializeChainState(stopVertexID ids.ID, genesisTimestamp time. return err } +func (s *state) InitFees() error { + s.feeRates = new(commonfees.Dimensions) + switch feeRatesBytes, err := s.singletonDB.Get(feeRatesKey); err { + case nil: + if err := s.feeRates.FromBytes(feeRatesBytes); err != nil { + return err + } + + case database.ErrNotFound: + // fork introducing dynamic fees may not be active yet, + // hence we may have never stored fee rates. Load from config + // TODO: remove once fork is active + isEActivated := s.cfg.IsEActivated(s.GetTimestamp()) + feeCfg := config.GetDynamicFeesConfig(isEActivated) + *s.feeRates = feeCfg.InitialFeeRate + + default: + return err + } + + switch lastBlkComplexityBytes, err := s.singletonDB.Get(lastBlkComplexityKey); err { + case nil: + if err := s.lastBlkComplexity.FromBytes(lastBlkComplexityBytes); err != nil { + return err + } + + case database.ErrNotFound: + // fork introducing dynamic fees may not be active yet, + // hence we may have never stored block complexities. Set to nil + // TODO: remove once fork is active + s.lastBlkComplexity = commonfees.Empty + + default: + return err + } + + return nil +} + func (s *state) initializeChainState(stopVertexID ids.ID, genesisTimestamp time.Time) error { genesis, err := block.NewStandardBlock( stopVertexID, @@ -387,6 +444,26 @@ func (s *state) SetTimestamp(t time.Time) { s.timestamp = t } +func (s *state) GetFeeRates() (commonfees.Dimensions, error) { + if s.feeRates == nil { + return commonfees.Empty, nil + } + return *s.feeRates, nil +} + +func (s *state) SetFeeRates(fr commonfees.Dimensions) { + feeRates := fr + s.feeRates = &feeRates +} + +func (s *state) GetLastBlockComplexity() (commonfees.Dimensions, error) { + return s.lastBlkComplexity, nil +} + +func (s *state) SetLastBlockComplexity(complexity commonfees.Dimensions) { + s.lastBlkComplexity = complexity +} + func (s *state) Commit() error { defer s.Abort() batch, err := s.CommitBatch() @@ -493,6 +570,15 @@ func (s *state) writeMetadata() error { } s.persistedTimestamp = s.timestamp } + + if s.feeRates != nil { + if err := s.singletonDB.Put(feeRatesKey, s.feeRates.Bytes()); err != nil { + return fmt.Errorf("failed to write fee rates: %w", err) + } + } + if err := s.singletonDB.Put(lastBlkComplexityKey, s.lastBlkComplexity.Bytes()); err != nil { + return fmt.Errorf("failed to write fee rates: %w", err) + } if s.persistedLastAccepted != s.lastAccepted { if err := database.PutID(s.singletonDB, lastAcceptedKey, s.lastAccepted); err != nil { return fmt.Errorf("failed to write last accepted: %w", err) diff --git a/vms/avm/state/state_test.go b/vms/avm/state/state_test.go index a6170c62c405..4725893dac59 100644 --- a/vms/avm/state/state_test.go +++ b/vms/avm/state/state_test.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/avm/block" + "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/fxs" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -100,9 +101,10 @@ func (v *versions) GetState(blkID ids.ID) (Chain, bool) { func TestState(t *testing.T) { require := require.New(t) + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) - s, err := New(vdb, parser, prometheus.NewRegistry(), trackChecksums) + s, err := New(vdb, parser, prometheus.NewRegistry(), cfg, trackChecksums) require.NoError(err) s.AddUTXO(populatedUTXO) @@ -110,7 +112,7 @@ func TestState(t *testing.T) { s.AddBlock(populatedBlk) require.NoError(s.Commit()) - s, err = New(vdb, parser, prometheus.NewRegistry(), trackChecksums) + s, err = New(vdb, parser, prometheus.NewRegistry(), cfg, trackChecksums) require.NoError(err) ChainUTXOTest(t, s) @@ -121,9 +123,10 @@ func TestState(t *testing.T) { func TestDiff(t *testing.T) { require := require.New(t) + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) - s, err := New(vdb, parser, prometheus.NewRegistry(), trackChecksums) + s, err := New(vdb, parser, prometheus.NewRegistry(), cfg, trackChecksums) require.NoError(err) s.AddUTXO(populatedUTXO) @@ -281,9 +284,10 @@ func ChainBlockTest(t *testing.T, c Chain) { func TestInitializeChainState(t *testing.T) { require := require.New(t) + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) - s, err := New(vdb, parser, prometheus.NewRegistry(), trackChecksums) + s, err := New(vdb, parser, prometheus.NewRegistry(), cfg, trackChecksums) require.NoError(err) stopVertexID := ids.GenerateTestID() diff --git a/vms/avm/tx.go b/vms/avm/tx.go index e272f8d28de8..5e55b7993141 100644 --- a/vms/avm/tx.go +++ b/vms/avm/tx.go @@ -128,12 +128,16 @@ func (tx *Tx) Verify(context.Context) error { return fmt.Errorf("%w: %s", errTxNotProcessing, s) } + feeRates, err := tx.vm.state.GetFeeRates() + if err != nil { + return fmt.Errorf("failed retrieving fee rates: %w", err) + } + var ( isEActive = tx.vm.txExecutorBackend.Config.IsEActivated(tx.vm.state.GetTimestamp()) feeCfg = config.GetDynamicFeesConfig(isEActive) - feeManager = fees.NewManager(feeCfg.FeeRate) + feeManager = fees.NewManager(feeRates) ) - return tx.tx.Unsigned.Visit(&executor.SemanticVerifier{ Backend: tx.vm.txExecutorBackend, BlkFeeManager: feeManager, diff --git a/vms/avm/tx_builders.go b/vms/avm/tx_builders.go index 0243b98fb529..9da87e5a09fb 100644 --- a/vms/avm/tx_builders.go +++ b/vms/avm/tx_builders.go @@ -10,6 +10,8 @@ import ( "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/avm/block/executor" "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" @@ -20,7 +22,6 @@ import ( "github.com/ava-labs/avalanchego/wallet/chain/x/signer" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" - commonfees "github.com/ava-labs/avalanchego/vms/components/fees" walletbuilder "github.com/ava-labs/avalanchego/wallet/chain/x/builder" ) @@ -31,6 +32,7 @@ type txBuilderBackend interface { State() state.State Config() *config.Config Codec() codec.Manager + Clock() *mockable.Clock Context() *walletbuilder.Context ResetAddresses(addrs set.Set[ids.ShortID]) @@ -44,10 +46,11 @@ func buildCreateAssetTx( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, ids.ShortID, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, ids.ShortEmpty, fmt.Errorf("failed creating fee calculator: %w", err) + } utx, err := pBuilder.NewCreateAssetTx( name, @@ -76,10 +79,11 @@ func buildBaseTx( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, ids.ShortID, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, ids.ShortEmpty, fmt.Errorf("failed creating fee calculator: %w", err) + } utx, err := pBuilder.NewBaseTx( outs, @@ -106,10 +110,11 @@ func mintNFT( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, fmt.Errorf("failed creating fee calculator: %w", err) + } utx, err := pBuilder.NewOperationTxMintNFT( assetID, @@ -131,10 +136,12 @@ func mintFTs( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, fmt.Errorf("failed creating fee calculator: %w", err) + } + utx, err := pBuilder.NewOperationTxMintFT( outputs, feeCalc, @@ -153,10 +160,11 @@ func buildOperation( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, fmt.Errorf("failed creating fee calculator: %w", err) + } utx, err := pBuilder.NewOperationTx( ops, @@ -176,10 +184,11 @@ func buildImportTx( to ids.ShortID, kc *secp256k1fx.Keychain, ) (*txs.Tx, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, fmt.Errorf("failed creating fee calculator: %w", err) + } outOwner := &secp256k1fx.OutputOwners{ Locktime: 0, @@ -208,10 +217,11 @@ func buildExportTx( kc *secp256k1fx.Keychain, changeAddr ids.ShortID, ) (*txs.Tx, ids.ShortID, error) { - var ( - pBuilder, pSigner = builders(backend, kc) - feeCalc = feeCalculator(backend) - ) + pBuilder, pSigner := builders(backend, kc) + feeCalc, err := feeCalculator(backend) + if err != nil { + return nil, ids.ShortEmpty, fmt.Errorf("failed creating fee calculator: %w", err) + } outputs := []*avax.TransferableOutput{{ Asset: avax.Asset{ID: exportedAssetID}, @@ -254,7 +264,7 @@ func builders(backend txBuilderBackend, kc *secp256k1fx.Keychain) (walletbuilder return builder, signer } -func feeCalculator(backend txBuilderBackend) *fees.Calculator { +func feeCalculator(backend txBuilderBackend) (*fees.Calculator, error) { var ( chainTime = backend.State().GetTimestamp() cfg = backend.Config() @@ -266,11 +276,17 @@ func feeCalculator(backend txBuilderBackend) *fees.Calculator { feeCalculator = fees.NewStaticCalculator(cfg) } else { feeCfg := config.GetDynamicFeesConfig(isEActive) - feeMan := commonfees.NewManager(feeCfg.FeeRate) - feeCalculator = fees.NewDynamicCalculator(backend.Codec(), feeMan, feeCfg.BlockMaxComplexity, nil) + nextChainTime := executor.NextBlockTime(chainTime, backend.Clock()) + + feeManager, err := fees.UpdatedFeeManager(backend.State(), backend.Config(), chainTime, nextChainTime) + if err != nil { + return nil, err + } + + feeCalculator = fees.NewDynamicCalculator(backend.Codec(), feeManager, feeCfg.BlockMaxComplexity, nil) } - return feeCalculator + return feeCalculator, nil } func options(changeAddr ids.ShortID, memo []byte) []common.Option { diff --git a/vms/avm/txs/executor/executor_test.go b/vms/avm/txs/executor/executor_test.go index 6be98183c1ca..aee5bdb381da 100644 --- a/vms/avm/txs/executor/executor_test.go +++ b/vms/avm/txs/executor/executor_test.go @@ -17,6 +17,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/avm/block" + "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/fxs" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" @@ -42,10 +43,11 @@ func TestBaseTxExecutor(t *testing.T) { require.NoError(err) codec := parser.Codec() + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) registerer := prometheus.NewRegistry() - state, err := state.New(vdb, parser, registerer, trackChecksums) + state, err := state.New(vdb, parser, registerer, cfg, trackChecksums) require.NoError(err) utxoID := avax.UTXOID{ @@ -149,10 +151,11 @@ func TestCreateAssetTxExecutor(t *testing.T) { require.NoError(err) codec := parser.Codec() + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) registerer := prometheus.NewRegistry() - state, err := state.New(vdb, parser, registerer, trackChecksums) + state, err := state.New(vdb, parser, registerer, cfg, trackChecksums) require.NoError(err) utxoID := avax.UTXOID{ @@ -294,10 +297,11 @@ func TestOperationTxExecutor(t *testing.T) { require.NoError(err) codec := parser.Codec() + cfg := config.Config{} db := memdb.New() vdb := versiondb.New(db) registerer := prometheus.NewRegistry() - state, err := state.New(vdb, parser, registerer, trackChecksums) + state, err := state.New(vdb, parser, registerer, cfg, trackChecksums) require.NoError(err) outputOwners := secp256k1fx.OutputOwners{ diff --git a/vms/avm/txs/executor/semantic_verifier_test.go b/vms/avm/txs/executor/semantic_verifier_test.go index 49f206ccb65a..96d5822ce3ac 100644 --- a/vms/avm/txs/executor/semantic_verifier_test.go +++ b/vms/avm/txs/executor/semantic_verifier_test.go @@ -745,7 +745,7 @@ func TestSemanticVerifierBaseTx(t *testing.T) { err = tx.Unsigned.Visit(&SemanticVerifier{ Backend: backend, - BlkFeeManager: fees.NewManager(feeCfg.FeeRate), + BlkFeeManager: fees.NewManager(feeCfg.InitialFeeRate), BlockMaxComplexity: feeCfg.BlockMaxComplexity, State: state, Tx: tx, @@ -1499,7 +1499,7 @@ func TestSemanticVerifierExportTx(t *testing.T) { err = tx.Unsigned.Visit(&SemanticVerifier{ Backend: backend, - BlkFeeManager: fees.NewManager(feeCfg.FeeRate), + BlkFeeManager: fees.NewManager(feeCfg.InitialFeeRate), BlockMaxComplexity: feeCfg.BlockMaxComplexity, State: state, Tx: tx, @@ -1644,7 +1644,7 @@ func TestSemanticVerifierExportTxDifferentSubnet(t *testing.T) { err = tx.Unsigned.Visit(&SemanticVerifier{ Backend: backend, - BlkFeeManager: fees.NewManager(feeCfg.FeeRate), + BlkFeeManager: fees.NewManager(feeCfg.InitialFeeRate), BlockMaxComplexity: feeCfg.BlockMaxComplexity, State: state, Tx: tx, @@ -2182,7 +2182,7 @@ func TestSemanticVerifierImportTx(t *testing.T) { err = tx.Unsigned.Visit(&SemanticVerifier{ Backend: backend, - BlkFeeManager: fees.NewManager(feeCfg.FeeRate), + BlkFeeManager: fees.NewManager(feeCfg.InitialFeeRate), BlockMaxComplexity: feeCfg.BlockMaxComplexity, State: state, Tx: tx, @@ -2644,7 +2644,7 @@ func TestSemanticVerifierOperationTx(t *testing.T) { tx := test.txFunc(require) err := tx.Unsigned.Visit(&SemanticVerifier{ Backend: backend, - BlkFeeManager: fees.NewManager(feeCfg.FeeRate), + BlkFeeManager: fees.NewManager(feeCfg.InitialFeeRate), BlockMaxComplexity: feeCfg.BlockMaxComplexity, State: state, Tx: tx, diff --git a/vms/avm/txs/fees/calculator.go b/vms/avm/txs/fees/calculator.go index b12b3d6a6033..586bab7bbf75 100644 --- a/vms/avm/txs/fees/calculator.go +++ b/vms/avm/txs/fees/calculator.go @@ -216,6 +216,10 @@ func (fc *Calculator) meterTx( } func (fc *Calculator) AddFeesFor(complexity fees.Dimensions) (uint64, error) { + if fc.feeManager == nil || complexity == fees.Empty { + return 0, nil + } + boundBreached, dimension := fc.feeManager.CumulateComplexity(complexity, fc.blockMaxComplexity) if boundBreached { return 0, fmt.Errorf("%w: breached dimension %d", errFailedConsumedUnitsCumulation, dimension) @@ -231,6 +235,10 @@ func (fc *Calculator) AddFeesFor(complexity fees.Dimensions) (uint64, error) { } func (fc *Calculator) RemoveFeesFor(unitsToRm fees.Dimensions) (uint64, error) { + if fc.feeManager == nil || unitsToRm == fees.Empty { + return 0, nil + } + if err := fc.feeManager.RemoveComplexity(unitsToRm); err != nil { return 0, fmt.Errorf("failed removing units: %w", err) } diff --git a/vms/avm/txs/fees/helpers.go b/vms/avm/txs/fees/helpers.go index bb1b346875d3..3f101e590bab 100644 --- a/vms/avm/txs/fees/helpers.go +++ b/vms/avm/txs/fees/helpers.go @@ -5,8 +5,11 @@ package fees import ( "fmt" + "time" "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/vms/avm/config" + "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/fees" @@ -59,3 +62,33 @@ func FinanceCredential(feeCalc *Calculator, codec codec.Manager, keysCount int) } return addedFees, nil } + +func UpdatedFeeManager(state state.Chain, cfg *config.Config, parentBlkTime, nextBlkTime time.Time) (*fees.Manager, error) { + var ( + isEActive = cfg.IsEActivated(parentBlkTime) + feeCfg = config.GetDynamicFeesConfig(isEActive) + ) + + feeRates, err := state.GetFeeRates() + if err != nil { + return nil, fmt.Errorf("failed retrieving fee rates: %w", err) + } + parentBlkComplexity, err := state.GetLastBlockComplexity() + if err != nil { + return nil, fmt.Errorf("failed retrieving last block complexity: %w", err) + } + + feeManager := fees.NewManager(feeRates) + if isEActive { + if err := feeManager.UpdateFeeRates( + feeCfg, + parentBlkComplexity, + parentBlkTime.Unix(), + nextBlkTime.Unix(), + ); err != nil { + return nil, fmt.Errorf("failed updating fee rates, %w", err) + } + } + + return feeManager, nil +} diff --git a/vms/avm/vm.go b/vms/avm/vm.go index c61294fc85cb..f64359773a96 100644 --- a/vms/avm/vm.go +++ b/vms/avm/vm.go @@ -235,6 +235,7 @@ func (vm *VM) Initialize( vm.db, vm.parser, vm.registerer, + vm.Config, avmConfig.ChecksumsEnabled, ) if err != nil { @@ -346,6 +347,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { vm.feeAssetID, vm.ctx, &vm.Config, + &vm.clock, vm.state, vm.ctx.SharedMemory, vm.parser.Codec(), @@ -413,6 +415,10 @@ func (vm *VM) Linearize(ctx context.Context, stopVertexID ids.ID, toEngine chan< return err } + if err := vm.state.InitFees(); err != nil { + return err + } + mempool, err := xmempool.New("mempool", vm.registerer, toEngine) if err != nil { return fmt.Errorf("failed to create mempool: %w", err) diff --git a/vms/avm/vm_test.go b/vms/avm/vm_test.go index 358a858b0351..8e0279cca24d 100644 --- a/vms/avm/vm_test.go +++ b/vms/avm/vm_test.go @@ -7,6 +7,7 @@ import ( "context" "math" "testing" + "time" "github.com/stretchr/testify/require" @@ -466,7 +467,7 @@ func TestTxAcceptAfterParseTx(t *testing.T) { }, Asset: avax.Asset{ID: env.genesisTx.ID()}, In: &secp256k1fx.TransferInput{ - Amt: 499991395, + Amt: 499998279, Input: secp256k1fx.Input{ SigIndices: []uint32{ 0, @@ -741,6 +742,10 @@ func TestClearForceAcceptedExportTx(t *testing.T) { env.vm.ctx.Lock.Unlock() }() + // to avoid tests flackiness we fix clock time wrt chain time + // so to have stable updated fee rates. + env.vm.clock.Set(env.vm.state.GetTimestamp().Add(time.Second)) + genesisTx := getCreateTxFromGenesisTest(t, env.genesisBytes, "AVAX") var ( diff --git a/vms/avm/wallet_service_backend.go b/vms/avm/wallet_service_backend.go index bc4aef2f8f84..78e208f5a1b4 100644 --- a/vms/avm/wallet_service_backend.go +++ b/vms/avm/wallet_service_backend.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/linked" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/avm/config" "github.com/ava-labs/avalanchego/vms/avm/state" "github.com/ava-labs/avalanchego/vms/avm/txs" @@ -60,6 +61,10 @@ func (b *walletServiceBackend) Codec() codec.Manager { return b.vm.parser.Codec() } +func (b *walletServiceBackend) Clock() *mockable.Clock { + return &b.vm.clock +} + func (b *walletServiceBackend) Context() *builder.Context { return b.ctx } diff --git a/vms/components/fees/config.go b/vms/components/fees/config.go index 20b528703866..eb9b4af12d9a 100644 --- a/vms/components/fees/config.go +++ b/vms/components/fees/config.go @@ -3,13 +3,61 @@ package fees +import "fmt" + type DynamicFeesConfig struct { - // FeeRate contains, per each fee dimension, the fee rate, - // i.e. the fee per unit of complexity. Fee rates are + // InitialFeeRate contains, per each fee dimension, the + // fee rate, i.e. the fee per unit of complexity. Fee rates are // valid as soon as fork introducing dynamic fees activates. - FeeRate Dimensions `json:"fee-rate"` + // Fee rates will be then updated by the dynamic fees algo. + InitialFeeRate Dimensions `json:"initial-fee-rate"` + + // MinFeeRate contains, per each fee dimension, the + // minimal fee rate, i.e. the fee per unit of complexity, + // enforced by the dynamic fees algo. + MinFeeRate Dimensions `json:"minimal-fee-rate"` + + // UpdateCoefficient contains, per each fee dimension, the + // exponential update coefficient. Setting an entry to 0 makes + // the corresponding fee rate constant. + UpdateCoefficient Dimensions `json:"update-coefficient"` + + // BlockTargetComplexityRate contains, per each fee dimension, the + // preferred block complexity that the dynamic fee algo + // strive to converge to, per second. + BlockTargetComplexityRate Dimensions `json:"block-target-complexity-rate"` // BlockMaxComplexity contains, per each fee dimension, the - // maximal complexity a valid block can host. - BlockMaxComplexity Dimensions `json:"block-max-complexity"` + // maximal complexity a valid P-chain block can host. + BlockMaxComplexity Dimensions `json:"block-max-complexity-rate"` +} + +func (c *DynamicFeesConfig) Validate() error { + for i := Dimension(0); i < FeeDimensions; i++ { + // MinFeeRate can be zero, but that is a bit dangerous. If a fee rate ever becomes + // zero, the update mechanism will keep them to zero. + if c.InitialFeeRate[i] < c.MinFeeRate[i] { + return fmt.Errorf("dimension %d, initial fee rate %d smaller than minimal fee rate %d", + i, + c.InitialFeeRate[i], + c.MinFeeRate[i], + ) + } + + if c.BlockTargetComplexityRate[i] > c.BlockMaxComplexity[i] { + return fmt.Errorf("dimension %d, block target complexity rate %d larger than block max complexity rate %d", + i, + c.BlockTargetComplexityRate[i], + c.BlockMaxComplexity[i], + ) + } + + // The update algorithm normalizes complexity delta by [BlockTargetComplexityRate]. + // So we enforce [BlockTargetComplexityRate] to be non-zero. + if c.BlockTargetComplexityRate[i] == 0 { + return fmt.Errorf("dimension %d, block target complexity rate set to zero", i) + } + } + + return nil } diff --git a/vms/components/fees/dimensions.go b/vms/components/fees/dimensions.go index 8184c242768f..36a35f438ab0 100644 --- a/vms/components/fees/dimensions.go +++ b/vms/components/fees/dimensions.go @@ -4,7 +4,9 @@ package fees import ( + "encoding/binary" "errors" + "fmt" "math" safemath "github.com/ava-labs/avalanchego/utils/math" @@ -22,6 +24,8 @@ const ( computeString string = "Compute" FeeDimensions = 4 + + uint64Len = 8 ) var ( @@ -67,3 +71,36 @@ func Add(lhs, rhs Dimensions) (Dimensions, error) { } return res, nil } + +// [Compare] returns true only if rhs[i] >= lhs[i] for each dimensions +// Arrays ordering is not total, so we avoided naming [Compare] as [Less] +// to discourage improper use +func Compare(lhs, rhs Dimensions) bool { + for i := 0; i < FeeDimensions; i++ { + if lhs[i] > rhs[i] { + return false + } + } + return true +} + +func (d *Dimensions) Bytes() []byte { + res := make([]byte, FeeDimensions*uint64Len) + for i := Dimension(0); i < FeeDimensions; i++ { + binary.BigEndian.PutUint64(res[i*uint64Len:], d[i]) + } + return res +} + +func (d *Dimensions) FromBytes(b []byte) error { + if len(b) != FeeDimensions*uint64Len { + return fmt.Errorf("unexpected bytes length: expected %d, actual %d", + FeeDimensions*uint64Len, + len(b), + ) + } + for i := Dimension(0); i < FeeDimensions; i++ { + d[i] = binary.BigEndian.Uint64(b[i*uint64Len : (i+1)*uint64Len]) + } + return nil +} diff --git a/vms/components/fees/dimensions_test.go b/vms/components/fees/dimensions_test.go new file mode 100644 index 000000000000..2f2dcd3d12a9 --- /dev/null +++ b/vms/components/fees/dimensions_test.go @@ -0,0 +1,21 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshalUnmarshalDimensions(t *testing.T) { + require := require.New(t) + + input := Dimensions{0, 1, 2024, math.MaxUint64} + bytes := input.Bytes() + var output Dimensions + require.NoError(output.FromBytes(bytes)) + require.Equal(input, output) +} diff --git a/vms/components/fees/manager.go b/vms/components/fees/manager.go index cf18276c1938..f8a9cba1cdb9 100644 --- a/vms/components/fees/manager.go +++ b/vms/components/fees/manager.go @@ -5,10 +5,14 @@ package fees import ( "fmt" + "math" safemath "github.com/ava-labs/avalanchego/utils/math" ) +// the update fee algorithm has a UpdateCoefficient, normalized to [CoeffDenom] +const CoeffDenom = uint64(20) + type Manager struct { // Avax denominated fee rates, i.e. fees per unit of complexity. feeRates Dimensions @@ -90,3 +94,163 @@ func (m *Manager) RemoveComplexity(unitsToRm Dimensions) error { m.cumulatedComplexity = revertedUnits return nil } + +// UpdateFeeRates calculates next fee rates. +// We update the fee rate with the formula: +// +// feeRate_{t+1} = Max(feeRate_t * exp(k*delta), minFeeRate) +// +// where +// +// delta == (parentComplexity - targetBlkComplexity)/targetBlkComplexity +// +// and [targetBlkComplexity] is the target complexity expected in the elapsed time. +// We update the fee rate trying to guarantee the following stability property: +// +// feeRate(delta) * feeRate(-delta) = 1 +// +// so that fee rates won't change much when block complexity wiggles around target complexity +func (m *Manager) UpdateFeeRates( + feesConfig DynamicFeesConfig, + parentBlkComplexity Dimensions, + parentBlkTime, childBlkTime int64, +) error { + if childBlkTime < parentBlkTime { + return fmt.Errorf("unexpected block times, parentBlkTim %v, childBlkTime %v", parentBlkTime, childBlkTime) + } + + elapsedTime := uint64(childBlkTime - parentBlkTime) + for i := Dimension(0); i < FeeDimensions; i++ { + targetBlkComplexity := targetComplexity( + feesConfig.BlockTargetComplexityRate[i], + elapsedTime, + feesConfig.BlockMaxComplexity[i], + ) + + factorNum, factorDenom := updateFactor( + feesConfig.UpdateCoefficient[i], + parentBlkComplexity[i], + targetBlkComplexity, + ) + nextFeeRates, over := safemath.Mul64(m.feeRates[i], factorNum) + if over != nil { + nextFeeRates = math.MaxUint64 + } + nextFeeRates /= factorDenom + + nextFeeRates = max(nextFeeRates, feesConfig.MinFeeRate[i]) + m.feeRates[i] = nextFeeRates + } + return nil +} + +func targetComplexity(targetComplexityRate, elapsedTime, maxBlockComplexity uint64) uint64 { + // parent and child block may have the same timestamp. In this case targetComplexity will match targetComplexityRate + elapsedTime = max(1, elapsedTime) + targetComplexity, over := safemath.Mul64(targetComplexityRate, elapsedTime) + if over != nil { + targetComplexity = maxBlockComplexity + } + + // regardless how low network load has been, we won't allow + // blocks larger than max block complexity + targetComplexity = min(targetComplexity, maxBlockComplexity) + return targetComplexity +} + +// updateFactor uses the following piece-wise approximation for the exponential function: +// +// if B > T --> exp{k * (B-T)/T} ≈≈ Approx(k,B,T) +// if B < T --> exp{k * (B-T)/T} ≈≈ 1/ Approx(k,B,T) +// +// Note that the approximation guarantees stability, since +// +// factor(k, B=T+X, T)*factor(k, B=T-X, T) == 1 +// +// We express the result with the pair (numerator, denominator) +// to increase precision with small deltas +func updateFactor(k, b, t uint64) (uint64, uint64) { + if b == t { + return 1, 1 // complexity matches target, nothing to update + } + + var ( + increaseFee bool + delta uint64 + ) + + if t < b { + increaseFee = true + delta = b - t + } else { + increaseFee = false + delta = t - b + } + + n, over := safemath.Mul64(k, delta) + if over != nil { + n = math.MaxUint64 + } + d, over := safemath.Mul64(CoeffDenom, t) + if over != nil { + d = math.MaxUint64 + } + + p, q := expPiecewiseApproximation(n, d) + if increaseFee { + return p, q + } + return q, p +} + +// piecewise approximation data. exp(x) ≈≈ m_i * x ± q_i in [i,i+1] +func expPiecewiseApproximation(a, b uint64) (uint64, uint64) { // exported to appease linter. + var ( + m, q uint64 + sign bool + ) + + switch v := a / b; { + case v < 1: + m, q, sign = 2, 1, true + case v < 2: + m, q, sign = 5, 2, false + case v < 3: + m, q, sign = 13, 18, false + case v < 4: + m, q, sign = 35, 84, false + case v < 5: + m, q, sign = 94, 321, false + case v < 6: + m, q, sign = 256, 1131, false + case v < 7: + m, q, sign = 694, 3760, false + case v < 8: + m, q, sign = 1885, 12098, false + case v < 9: + m, q, sign = 5123, 38003, false + default: + m, q, sign = 13924, 117212, false + } + + // m(A/B) - q == (m*A-q*B)/B + n1, over := safemath.Mul64(m, a) + if over != nil { + return math.MaxUint64, b + } + n2, over := safemath.Mul64(q, b) + if over != nil { + return math.MaxUint64, b + } + + var n uint64 + if !sign { + n = n1 - n2 + } else { + n, over = safemath.Add64(n1, n2) + if over != nil { + return math.MaxUint64, b + } + } + return n, b +} diff --git a/vms/components/fees/manager_test.go b/vms/components/fees/manager_test.go new file mode 100644 index 000000000000..922183102f93 --- /dev/null +++ b/vms/components/fees/manager_test.go @@ -0,0 +1,291 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/utils/units" +) + +func TestUpdateFeeRates(t *testing.T) { + require := require.New(t) + + var ( + feesCfg = DynamicFeesConfig{ + MinFeeRate: Dimensions{1, 1, 1, 1}, + UpdateCoefficient: Dimensions{1, 2, 5, 10}, + BlockMaxComplexity: Dimensions{100, 100, 100, 100}, + BlockTargetComplexityRate: Dimensions{25, 25, 25, 25}, + } + parentFeeRate = Dimensions{10, 20, 100, 200} + parentComplexity = Dimensions{100, 25, 20, 10} + + elapsedTime = time.Second + parentBlkTime = time.Now().Truncate(time.Second) + childBlkTime = parentBlkTime.Add(elapsedTime) + ) + + m := &Manager{ + feeRates: parentFeeRate, + } + + require.NoError(m.UpdateFeeRates( + feesCfg, + parentComplexity, + parentBlkTime.Unix(), + childBlkTime.Unix(), + )) + + // Bandwidth complexity are above target, fee rate is pushed up + require.Equal(uint64(13), m.feeRates[Bandwidth]) + + // UTXORead complexity is at target, fee rate does not change + require.Equal(parentFeeRate[UTXORead], m.feeRates[UTXORead]) + + // UTXOWrite complexity is below target, fee rate is pushed down + require.Equal(uint64(90), m.feeRates[UTXOWrite]) + + // Compute complexoty is below target, fee rate is pushed down + require.Equal(uint64(125), m.feeRates[Compute]) +} + +func TestUpdateFeeRatesStability(t *testing.T) { + // The advantage of using an exponential fee update scheme + // (vs e.g. the EIP-1559 scheme we use in the C-chain) is that + // it is more stable against dithering. + // We prove here that if complexity oscillates around the target + // fee rates are unchanged (discounting for some numerical errors) + + require := require.New(t) + + var ( + feesCfg = DynamicFeesConfig{ + MinFeeRate: Dimensions{0, 0, 0, 0}, + UpdateCoefficient: Dimensions{2, 4, 5, 10}, + BlockMaxComplexity: Dimensions{100_000, 100_000, 100_000, 100_000}, + BlockTargetComplexityRate: Dimensions{200, 60, 80, 600}, + } + initialFeeRate = Dimensions{ + 60 * units.NanoAvax, + 8 * units.NanoAvax, + 10 * units.NanoAvax, + 35 * units.NanoAvax, + } + + elapsedTime = time.Second + parentComplexity = Dimensions{50, 45, 70, 500} // less than target complexity rate * elapsedTime + childComplexity = Dimensions{350, 75, 90, 700} // more than target complexity rate * elapsedTime + + parentBlkTime = time.Now().Truncate(time.Second) + childBlkTime = parentBlkTime.Add(elapsedTime) + granChildBlkTime = childBlkTime.Add(elapsedTime) + ) + + // step1: parent complexity is below target. Fee rates will decrease + m1 := &Manager{feeRates: initialFeeRate} + require.NoError(m1.UpdateFeeRates( + feesCfg, + parentComplexity, + parentBlkTime.Unix(), + childBlkTime.Unix(), + )) + + require.Less(m1.feeRates[Bandwidth], initialFeeRate[Bandwidth]) + require.Less(m1.feeRates[UTXORead], initialFeeRate[UTXORead]) + require.Less(m1.feeRates[UTXOWrite], initialFeeRate[UTXOWrite]) + require.Less(m1.feeRates[Compute], initialFeeRate[Compute]) + + // step2: child complexity goes above target, so that average complexity is at target. + // Fee rates go back to the original value + m2 := &Manager{feeRates: m1.feeRates} + require.NoError(m2.UpdateFeeRates( + feesCfg, + childComplexity, + childBlkTime.Unix(), + granChildBlkTime.Unix(), + )) + + require.LessOrEqual(initialFeeRate[Bandwidth]-m2.feeRates[Bandwidth], uint64(1)) + require.LessOrEqual(initialFeeRate[UTXORead]-m2.feeRates[UTXORead], uint64(1)) + require.LessOrEqual(initialFeeRate[UTXOWrite]-m2.feeRates[UTXOWrite], uint64(1)) + require.LessOrEqual(initialFeeRate[Compute]-m2.feeRates[Compute], uint64(1)) +} + +func TestPChainFeeRateIncreaseDueToPeak(t *testing.T) { + // Complexity values comes from the mainnet historical peak as measured + // pre E upgrade activation + + require := require.New(t) + + var ( + feesCfg = DynamicFeesConfig{ + MinFeeRate: Dimensions{ + 60 * units.NanoAvax, + 8 * units.NanoAvax, + 10 * units.NanoAvax, + 35 * units.NanoAvax, + }, + UpdateCoefficient: Dimensions{ // over CoeffDenom + 3, + 1, + 1, + 2, + }, + BlockTargetComplexityRate: Dimensions{ + 2500, + 600, + 1200, + 6500, + }, + BlockMaxComplexity: Dimensions{ + 100_000, + 60_000, + 60_000, + 600_000, + }, + } + + // See mainnet P-chain block 2LJVD1rfEfaJtTwRggFXaUXhME4t5WYGhYP9Aj7eTYqGsfknuC its descendants + blockComplexities = []struct { + blkTime int64 + complexity Dimensions + }{ + {1615237936, Dimensions{28234, 10812, 10812, 106000}}, + {1615237936, Dimensions{17634, 6732, 6732, 66000}}, + {1615237936, Dimensions{12334, 4692, 4692, 46000}}, + {1615237936, Dimensions{5709, 2142, 2142, 21000}}, + {1615237936, Dimensions{15514, 5916, 5916, 58000}}, + {1615237936, Dimensions{12069, 4590, 4590, 45000}}, + {1615237936, Dimensions{8359, 3162, 3162, 31000}}, + {1615237936, Dimensions{5444, 2040, 2040, 20000}}, + {1615237936, Dimensions{1734, 612, 612, 6000}}, + {1615237936, Dimensions{5974, 2244, 2244, 22000}}, + {1615237936, Dimensions{3059, 1122, 1122, 11000}}, + {1615237936, Dimensions{7034, 2652, 2652, 26000}}, + {1615237936, Dimensions{7564, 2856, 2856, 28000}}, + {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, <-- from here on, fee would exceed 100 Avax + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{820, 360, 442, 4000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{34064, 13056, 13056, 128000}}, + // {1615237936, Dimensions{3589, 1326, 1326, 13000}}, + // {1615237936, Dimensions{550, 180, 180, 2000}}, + // {1615237936, Dimensions{413, 102, 102, 1000}}, + } + ) + + m := &Manager{ + feeRates: feesCfg.MinFeeRate, + } + + // PEAK INCOMING + peakFeeRate := feesCfg.MinFeeRate + for i := 1; i < len(blockComplexities); i++ { + parentBlkData := blockComplexities[i-1] + childBlkData := blockComplexities[i] + require.NoError(m.UpdateFeeRates( + feesCfg, + parentBlkData.complexity, + parentBlkData.blkTime, + childBlkData.blkTime, + )) + + // check that fee rates are strictly above minimal + require.False( + Compare(m.feeRates, feesCfg.MinFeeRate), + fmt.Sprintf("failed at %d of %d iteration, \n curr fees %v \n next fees %v", + i, + len(blockComplexities), + peakFeeRate, + m.feeRates, + ), + ) + + // at peak the total fee should be no more than 100 Avax. + fee, err := m.CalculateFee(childBlkData.complexity) + require.NoError(err) + require.Less(fee, 100*units.Avax, fmt.Sprintf("iteration: %d, total: %d", i, len(blockComplexities))) + + peakFeeRate = m.feeRates + } + + // OFF PEAK + offPeakBlkComplexity := Dimensions{1473, 510, 510, 5000} + elapsedTime := time.Unix(1615238881, 0).Sub(time.Unix(1615237936, 0)) + parentBlkTime := time.Now().Truncate(time.Second) + childBlkTime := parentBlkTime.Add(elapsedTime) + + require.NoError(m.UpdateFeeRates( + feesCfg, + offPeakBlkComplexity, + parentBlkTime.Unix(), + childBlkTime.Unix(), + )) + + // check that fee rates decrease off peak + require.Less(m.feeRates[Bandwidth], peakFeeRate[Bandwidth]) + require.Less(m.feeRates[UTXORead], peakFeeRate[UTXORead]) + require.LessOrEqual(m.feeRates[UTXOWrite], peakFeeRate[UTXOWrite]) + require.Less(m.feeRates[Compute], peakFeeRate[Compute]) +} + +func TestFeeUpdateFactor(t *testing.T) { + tests := []struct { + coeff uint64 + parentBlkComplexity uint64 + targetBlkComplexity uint64 + wantNum uint64 + wantDenom uint64 + }{ + // parentBlkComplexity == targetBlkComplexity gives factor 1, no matter what coeff is + {1, 250, 250, 1, 1}, + {math.MaxUint64, 250, 250, 1, 1}, + + // parentBlkComplexity > targetBlkComplexity + {1, 101, 100, 2_002, 2_000}, // should be 1.0005 + {1, 110, 100, 2_020, 2_000}, // should be 1.005 + {1, 200, 100, 2_200, 2_000}, // should be 1.05 + {1, 1_100, 100, 4_000, 2_000}, // should be 1.648 + {1, 2_100, 100, 6000, 2_000}, // should be 2,718 + {1, 3_100, 100, 11_000, 2_000}, // should be 4,48 + {1, 4_100, 100, 16_000, 2_000}, // should be 7,39 + {1, 7_100, 100, 77_000, 2_000}, // should be 33,12 + {1, 8_100, 100, 110_000, 2_000}, // should be 54,6 + {1, 10_100, 100, 298_000, 2_000}, // should be 148,4 + + // parentBlkComplexity < targetBlkComplexity + {1, 100, 101, 2_020, 2_022}, // should be 0,9995 + {1, 100, 110, 2_200, 2_220}, // should be 0,995 + {1, 100, 200, 4_000, 4_200}, // should be 0,975 + {1, 100, 1_100, 22_000, 24_000}, // should be 0,955 + {1, 100, 10_100, 202_000, 222_000}, // should be 0,952 + } + for _, tt := range tests { + haveFactor, haveIncreaseFee := updateFactor( + tt.coeff, + tt.parentBlkComplexity, + tt.targetBlkComplexity, + ) + require.Equal(t, tt.wantNum, haveFactor) + require.Equal(t, tt.wantDenom, haveIncreaseFee) + } +} diff --git a/wallet/chain/x/builder/builder.go b/wallet/chain/x/builder/builder.go index dd5621ac6596..716daa956985 100644 --- a/wallet/chain/x/builder/builder.go +++ b/wallet/chain/x/builder/builder.go @@ -542,12 +542,9 @@ func (b *builder) NewImportTx( if err != nil { return nil, fmt.Errorf("failed calculating output size: %w", err) } - - if feeCalc.IsEActive { - // update fees to target given the extra output added - if _, err := feeCalc.AddFeesFor(outDimensions); err != nil { - return nil, fmt.Errorf("account for output fees: %w", err) - } + // update fees to target given the extra output added + if _, err := feeCalc.AddFeesFor(outDimensions); err != nil { + return nil, fmt.Errorf("account for output fees: %w", err) } switch { @@ -563,14 +560,12 @@ func (b *builder) NewImportTx( return utx, b.initCtx(utx) default: - if feeCalc.IsEActive { - // imported avax are not enough to pay fees - // Drop the changeOut and finance the tx - if _, err := feeCalc.RemoveFeesFor(outDimensions); err != nil { - return nil, fmt.Errorf("failed reverting change output: %w", err) - } - feeCalc.Fee -= importedAVAX + // imported avax are not enough to pay fees + // Drop the changeOut and finance the tx + if _, err := feeCalc.RemoveFeesFor(outDimensions); err != nil { + return nil, fmt.Errorf("failed reverting change output: %w", err) } + feeCalc.Fee -= importedAVAX } } diff --git a/wallet/chain/x/wallet.go b/wallet/chain/x/wallet.go index e1cc14b52257..6b31121fafab 100644 --- a/wallet/chain/x/wallet.go +++ b/wallet/chain/x/wallet.go @@ -170,9 +170,9 @@ type wallet struct { signer signer.Signer client avm.Client - isEForkActive bool - staticFeesConfig *config.Config - feeRates, blockMaxComplexity commonfees.Dimensions + isEForkActive bool + staticFeesConfig *config.Config + nextFeeRates, blockMaxComplexity commonfees.Dimensions } func (w *wallet) Builder() builder.Builder { @@ -187,7 +187,10 @@ func (w *wallet) IssueBaseTx( outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewBaseTx(outputs, feeCalc, options...) if err != nil { @@ -203,7 +206,10 @@ func (w *wallet) IssueCreateAssetTx( initialState map[uint32][]verify.State, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewCreateAssetTx(name, symbol, denomination, initialState, feeCalc, options...) if err != nil { @@ -216,7 +222,10 @@ func (w *wallet) IssueOperationTx( operations []*txs.Operation, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewOperationTx(operations, feeCalc, options...) if err != nil { @@ -229,7 +238,10 @@ func (w *wallet) IssueOperationTxMintFT( outputs map[ids.ID]*secp256k1fx.TransferOutput, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewOperationTxMintFT(outputs, feeCalc, options...) if err != nil { @@ -244,7 +256,10 @@ func (w *wallet) IssueOperationTxMintNFT( owners []*secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewOperationTxMintNFT(assetID, payload, owners, feeCalc, options...) if err != nil { @@ -258,7 +273,10 @@ func (w *wallet) IssueOperationTxMintProperty( owner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewOperationTxMintProperty(assetID, owner, feeCalc, options...) if err != nil { @@ -271,7 +289,10 @@ func (w *wallet) IssueOperationTxBurnProperty( assetID ids.ID, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewOperationTxBurnProperty(assetID, feeCalc, options...) if err != nil { @@ -285,7 +306,10 @@ func (w *wallet) IssueImportTx( to *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewImportTx(chainID, to, feeCalc, options...) if err != nil { @@ -299,7 +323,10 @@ func (w *wallet) IssueExportTx( outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.Tx, error) { - feeCalc := w.feeCalculator(w.builder.Context(), options...) + feeCalc, err := w.feeCalculator(w.builder.Context(), options...) + if err != nil { + return nil, err + } utx, err := w.builder.NewExportTx(chainID, outputs, feeCalc, options...) if err != nil { @@ -356,25 +383,38 @@ func (w *wallet) IssueTx( return nil } -func (w *wallet) feeCalculator(ctx *builder.Context, options ...common.Option) *fees.Calculator { - w.refreshFeesData(ctx, options...) +func (w *wallet) feeCalculator(ctx *builder.Context, options ...common.Option) (*fees.Calculator, error) { + if err := w.refreshFeesData(ctx, options...); err != nil { + return nil, err + } var feeCalculator *fees.Calculator if !w.isEForkActive { feeCalculator = fees.NewStaticCalculator(w.staticFeesConfig) } else { feeCfg := config.GetDynamicFeesConfig(w.isEForkActive) - feeMan := commonfees.NewManager(w.feeRates) + feeMan := commonfees.NewManager(w.nextFeeRates) feeCalculator = fees.NewDynamicCalculator(builder.Parser.Codec(), feeMan, feeCfg.BlockMaxComplexity, nil) } - return feeCalculator + return feeCalculator, nil } -func (w *wallet) refreshFeesData(ctx *builder.Context, _ ...common.Option) { +func (w *wallet) refreshFeesData(ctx *builder.Context, options ...common.Option) error { if w.isEForkActive { // E fork enables dinamic fees and it is active // not need to recheck - return + return nil + } + + var ( + ops = common.NewOptions(options) + opsCtx = ops.Context() + err error + ) + + _, w.nextFeeRates, err = w.client.GetFeeRates(opsCtx) + if err != nil { + return err } eUpgradeTime := version.GetEUpgradeTime(w.builder.Context().NetworkID) @@ -388,9 +428,8 @@ func (w *wallet) refreshFeesData(ctx *builder.Context, _ ...common.Option) { // } chainTime := mockable.MaxTime // assume fork is already active w.isEForkActive = !chainTime.Before(eUpgradeTime) - feeCfg := config.GetDynamicFeesConfig(w.isEForkActive) - w.feeRates = feeCfg.FeeRate - w.blockMaxComplexity = feeCfg.BlockMaxComplexity + w.blockMaxComplexity = config.GetDynamicFeesConfig(w.isEForkActive).BlockMaxComplexity + return nil } func staticFeesConfigFromContext(ctx *builder.Context) *config.Config {