diff --git a/scripts/mocks.mockgen.txt b/scripts/mocks.mockgen.txt index ba2be886b0b6..701da0a02ac9 100644 --- a/scripts/mocks.mockgen.txt +++ b/scripts/mocks.mockgen.txt @@ -40,5 +40,7 @@ github.com/ava-labs/avalanchego/vms/proposervm=PostForkBlock=vms/proposervm/mock github.com/ava-labs/avalanchego/vms/registry=VMGetter=vms/registry/mock_vm_getter.go github.com/ava-labs/avalanchego/vms/registry=VMRegistry=vms/registry/mock_vm_registry.go github.com/ava-labs/avalanchego/vms=Factory,Manager=vms/mock_manager.go +github.com/ava-labs/avalanchego/wallet/chain/p=BuilderBackend=wallet/chain/p/mocks/mock_builder_backend.go +github.com/ava-labs/avalanchego/wallet/chain/p=SignerBackend=wallet/chain/p/mocks/mock_signer_backend.go github.com/ava-labs/avalanchego/x/sync=Client=x/sync/mock_client.go github.com/ava-labs/avalanchego/x/sync=NetworkClient=x/sync/mock_network_client.go diff --git a/tests/e2e/p/staking_rewards.go b/tests/e2e/p/staking_rewards.go index 43b64456de96..c8fa4b032520 100644 --- a/tests/e2e/p/staking_rewards.go +++ b/tests/e2e/p/staking_rewards.go @@ -237,6 +237,17 @@ var _ = ginkgo.Describe("[Staking Rewards]", func() { delegatorData := data[0].Delegators[0] actualGammaDelegationPeriod := time.Duration(delegatorData.EndTime-delegatorData.StartTime) * time.Second + preRewardBalances := make(map[ids.ShortID]uint64, len(rewardKeys)) + for _, rewardKey := range rewardKeys { + keychain := secp256k1fx.NewKeychain(rewardKey) + baseWallet := e2e.NewWallet(keychain, nodeURI) + pWallet := baseWallet.P() + balances, err := pWallet.Builder().GetBalance() + require.NoError(err) + preRewardBalances[rewardKey.Address()] = balances[pWallet.AVAXAssetID()] + } + require.Len(preRewardBalances, len(rewardKeys)) + ginkgo.By("waiting until all validation periods are over") // The beta validator was the last added and so has the latest end time. The // delegation periods are shorter than the validation periods. @@ -290,13 +301,14 @@ var _ = ginkgo.Describe("[Staking Rewards]", func() { ginkgo.By("checking expected rewards against actual rewards") expectedRewardBalances := map[ids.ShortID]uint64{ - alphaValidationRewardKey.Address(): expectedValidationReward, - alphaDelegationRewardKey.Address(): expectedDelegationFee, - betaValidationRewardKey.Address(): 0, // Validator didn't meet uptime requirement - betaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement - gammaDelegationRewardKey.Address(): expectedDelegatorReward, - deltaDelegationRewardKey.Address(): 0, // Validator didn't meet uptime requirement + alphaValidationRewardKey.Address(): preRewardBalances[alphaValidationRewardKey.Address()] + expectedValidationReward, + alphaDelegationRewardKey.Address(): preRewardBalances[alphaDelegationRewardKey.Address()] + expectedDelegationFee, + betaValidationRewardKey.Address(): preRewardBalances[betaValidationRewardKey.Address()] + 0, // Validator didn't meet uptime requirement + betaDelegationRewardKey.Address(): preRewardBalances[betaDelegationRewardKey.Address()] + 0, // Validator didn't meet uptime requirement + gammaDelegationRewardKey.Address(): preRewardBalances[gammaDelegationRewardKey.Address()] + expectedDelegatorReward, + deltaDelegationRewardKey.Address(): preRewardBalances[deltaDelegationRewardKey.Address()] + 0, // Validator didn't meet uptime requirement } + for address := range expectedRewardBalances { require.Equal(expectedRewardBalances[address], rewardBalances[address]) } diff --git a/tests/e2e/p/workflow.go b/tests/e2e/p/workflow.go index 8bf7efca2c2c..a8b30579ec3d 100644 --- a/tests/e2e/p/workflow.go +++ b/tests/e2e/p/workflow.go @@ -19,8 +19,12 @@ import ( "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) // PChainWorkflow is an integration test for normal P-Chain operations @@ -49,25 +53,18 @@ var _ = e2e.DescribePChain("[Workflow]", func() { tests.Outf("{{green}} minimal validator stake: %d {{/}}\n", minValStake) tests.Outf("{{green}} minimal delegator stake: %d {{/}}\n", minDelStake) - tests.Outf("{{blue}} fetching tx fee {{/}}\n") + tests.Outf("{{blue}} fetching X-chain tx fee {{/}}\n") infoClient := info.NewClient(nodeURI.URI) - fees, err := infoClient.GetTxFee(e2e.DefaultContext()) + xchainFees, err := infoClient.GetTxFee(e2e.DefaultContext()) require.NoError(err) - txFees := uint64(fees.TxFee) - tests.Outf("{{green}} txFee: %d {{/}}\n", txFees) + xChainTxFees := uint64(xchainFees.TxFee) + tests.Outf("{{green}} X-chain TxFee: %d {{/}}\n", xChainTxFees) // amount to transfer from P to X chain toTransfer := 1 * units.Avax pShortAddr := keychain.Keys[0].Address() xTargetAddr := keychain.Keys[1].Address() - ginkgo.By("check selected keys have sufficient funds", func() { - pBalances, err := pWallet.Builder().GetBalance() - pBalance := pBalances[avaxAssetID] - minBalance := minValStake + txFees + minDelStake + txFees + toTransfer + txFees - require.NoError(err) - require.GreaterOrEqual(pBalance, minBalance) - }) // Use a random node ID to ensure that repeated test runs // will succeed against a network that persists across runs. @@ -126,8 +123,9 @@ var _ = e2e.DescribePChain("[Workflow]", func() { OutputOwners: outputOwner, } + pChainExportFee := uint64(0) ginkgo.By("export avax from P to X chain", func() { - _, err := pWallet.IssueExportTx( + tx, err := pWallet.IssueExportTx( xWallet.BlockchainID(), []*avax.TransferableOutput{ { @@ -140,6 +138,18 @@ var _ = e2e.DescribePChain("[Workflow]", func() { e2e.WithDefaultContext(), ) require.NoError(err) + + // retrieve fees paid for the tx + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: tx.Creds, + } + + require.NoError(tx.Unsigned.Visit(&feeCalc)) + pChainExportFee = feeCalc.Fee }) // check balances post export @@ -154,7 +164,7 @@ var _ = e2e.DescribePChain("[Workflow]", func() { tests.Outf("{{blue}} X-chain balance after P->X export: %d {{/}}\n", xPreImportBalance) require.Equal(xPreImportBalance, xStartBalance) // import not performed yet - require.Equal(pPreImportBalance, pStartBalance-toTransfer-txFees) + require.Equal(pPreImportBalance, pStartBalance-toTransfer-pChainExportFee) ginkgo.By("import avax from P into X chain", func() { _, err := xWallet.IssueImportTx( @@ -176,7 +186,7 @@ var _ = e2e.DescribePChain("[Workflow]", func() { xFinalBalance := xBalances[avaxAssetID] tests.Outf("{{blue}} X-chain balance after P->X import: %d {{/}}\n", xFinalBalance) - require.Equal(xFinalBalance, xPreImportBalance+toTransfer-txFees) // import not performed yet + require.Equal(xFinalBalance, xPreImportBalance+toTransfer-xChainTxFees) // import not performed yet require.Equal(pFinalBalance, pPreImportBalance) }) }) diff --git a/vms/components/fees/dimensions.go b/vms/components/fees/dimensions.go new file mode 100644 index 000000000000..31e46e246c7b --- /dev/null +++ b/vms/components/fees/dimensions.go @@ -0,0 +1,62 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "encoding/binary" + "fmt" + + "github.com/ava-labs/avalanchego/utils/math" +) + +const ( + Bandwidth Dimension = 0 + UTXORead Dimension = 1 + UTXOWrite Dimension = 2 // includes delete + Compute Dimension = 3 // signatures checks, tx-specific + + FeeDimensions = 4 + + uint64Len = 8 +) + +var Empty = Dimensions{} // helps avoiding reading unit fees from db for some pre E fork processing + +type ( + Dimension int + Dimensions [FeeDimensions]uint64 +) + +func Add(lhs, rhs Dimensions) (Dimensions, error) { + var res Dimensions + for i := 0; i < FeeDimensions; i++ { + v, err := math.Add64(lhs[i], rhs[i]) + if err != nil { + return res, err + } + res[i] = v + } + return res, nil +} + +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/helpers.go b/vms/components/fees/helpers.go new file mode 100644 index 000000000000..dfdee15325eb --- /dev/null +++ b/vms/components/fees/helpers.go @@ -0,0 +1,71 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/wrappers" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +func GetInputsDimensions(c codec.Manager, v uint16, ins []*avax.TransferableInput) (Dimensions, error) { + var consumedUnits Dimensions + for _, in := range ins { + cost, err := in.In.Cost() + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving cost of input %s: %w", in.ID, err) + } + + inSize, err := c.Size(v, in) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of input %s: %w", in.ID, err) + } + uInSize := uint64(inSize) + + consumedUnits[Bandwidth] += uInSize - codec.CodecVersionSize + consumedUnits[UTXORead] += cost + uInSize // inputs are read + consumedUnits[UTXOWrite] += uInSize // inputs are deleted + } + return consumedUnits, nil +} + +func GetOutputsDimensions(c codec.Manager, v uint16, outs []*avax.TransferableOutput) (Dimensions, error) { + var consumedUnits Dimensions + for _, out := range outs { + outSize, err := c.Size(v, out) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of output %s: %w", out.ID, err) + } + uOutSize := uint64(outSize) + + consumedUnits[Bandwidth] += uOutSize - codec.CodecVersionSize + consumedUnits[UTXOWrite] += uOutSize + } + return consumedUnits, nil +} + +func GetCredentialsDimensions(c codec.Manager, v uint16, inputSigIndices []uint32) (Dimensions, error) { + var consumedUnits Dimensions + + // Workaround to ensure that codec picks interface instead of the pointer to evaluate size. + // TODO ABENEGIA: fix this + creds := make([]verify.Verifiable, 0, 1) + creds = append(creds, &secp256k1fx.Credential{ + Sigs: make([][secp256k1.SignatureLen]byte, len(inputSigIndices)), + }) + + credSize, err := c.Size(v, creds) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of credentials: %w", err) + } + credSize -= wrappers.IntLen // length of the slice, we want the single credential + + consumedUnits[Bandwidth] += uint64(credSize) - codec.CodecVersionSize + return consumedUnits, nil +} diff --git a/vms/components/fees/manager.go b/vms/components/fees/manager.go new file mode 100644 index 000000000000..644cf37fa9b1 --- /dev/null +++ b/vms/components/fees/manager.go @@ -0,0 +1,92 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "fmt" + + safemath "github.com/ava-labs/avalanchego/utils/math" +) + +type Manager struct { + // Avax denominated unit fees for all fee dimensions + unitFees Dimensions + + // cumulatedUnits helps aggregating the units consumed by a block + // so that we can verify it's not too big/build it properly. + cumulatedUnits Dimensions +} + +func NewManager(unitFees Dimensions) *Manager { + return &Manager{ + unitFees: unitFees, + } +} + +func (m *Manager) GetUnitFees() Dimensions { + return m.unitFees +} + +// CalculateFee must be a stateless method +func (m *Manager) CalculateFee(units Dimensions) (uint64, error) { + fee := uint64(0) + + for i := Dimension(0); i < FeeDimensions; i++ { + contribution, err := safemath.Mul64(m.unitFees[i], units[i]) + if err != nil { + return 0, err + } + fee, err = safemath.Add64(contribution, fee) + if err != nil { + return 0, err + } + } + return fee, nil +} + +// CumulateUnits tries to cumulate the consumed units [units]. Before +// actually cumulating them, it checks whether the result would breach [bounds]. +// If so, it returns the first dimension to breach bounds. +func (m *Manager) CumulateUnits(units, bounds Dimensions) (bool, Dimension) { + // Ensure we can consume (don't want partial update of values) + for i := Dimension(0); i < FeeDimensions; i++ { + consumed, err := safemath.Add64(m.cumulatedUnits[i], units[i]) + if err != nil { + return true, i + } + if consumed > bounds[i] { + return true, i + } + } + + // Commit to consumption + for i := Dimension(0); i < FeeDimensions; i++ { + consumed, err := safemath.Add64(m.cumulatedUnits[i], units[i]) + if err != nil { + return true, i + } + m.cumulatedUnits[i] = consumed + } + return false, 0 +} + +// Sometimes, e.g. while building a tx, we'd like freedom to speculatively add units +// and to remove them later on. [RemoveUnits] grants this freedom +func (m *Manager) RemoveUnits(unitsToRm Dimensions) error { + var revertedUnits Dimensions + for i := Dimension(0); i < FeeDimensions; i++ { + prev, err := safemath.Sub(m.cumulatedUnits[i], unitsToRm[i]) + if err != nil { + return fmt.Errorf("%w: dimension %d", err, i) + } + revertedUnits[i] = prev + } + + m.cumulatedUnits = revertedUnits + return nil +} + +func (m *Manager) GetCumulatedUnits() Dimensions { + return m.cumulatedUnits +} diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index d19f5d902e12..6a5be6cf3ad8 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -17,7 +17,9 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -352,6 +354,9 @@ func packBlockTxs( } var ( + feeCfg = config.EUpgradeDynamicFeesConfig + feeMan = fees.NewManager(feeCfg.UnitFees) + blockTxs []*txs.Tx inputs set.Set[ids.ID] ) @@ -375,9 +380,11 @@ func packBlockTxs( } executor := &txexecutor.StandardTxExecutor{ - Backend: backend, - State: txDiff, - Tx: tx, + Backend: backend, + BlkFeeManager: feeMan, + UnitCaps: feeCfg.BlockUnitsCap, + State: txDiff, + Tx: tx, } err = tx.Unsigned.Visit(executor) diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index e277474eb9b3..8a8515257252 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -36,6 +36,7 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -254,10 +255,13 @@ func addSubnet(t *testing.T, env *environment) { stateDiff, err := state.NewDiff(genesisID, env.blkManager) require.NoError(err) + feeCfg := config.EUpgradeDynamicFeesConfig executor := txexecutor.StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: testSubnet1, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(feeCfg.UnitFees), + UnitCaps: feeCfg.BlockUnitsCap, + State: stateDiff, + Tx: testSubnet1, } require.NoError(testSubnet1.Unsigned.Visit(&executor)) diff --git a/vms/platformvm/block/executor/helpers_test.go b/vms/platformvm/block/executor/helpers_test.go index f134ecd73dd8..ccd320e774a6 100644 --- a/vms/platformvm/block/executor/helpers_test.go +++ b/vms/platformvm/block/executor/helpers_test.go @@ -38,6 +38,7 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -274,10 +275,13 @@ func addSubnet(env *environment) { panic(err) } + feeCfg := config.EUpgradeDynamicFeesConfig executor := executor.StandardTxExecutor{ - Backend: env.backend, - State: stateDiff, - Tx: testSubnet1, + Backend: env.backend, + BlkFeeManager: fees.NewManager(feeCfg.UnitFees), + UnitCaps: feeCfg.BlockUnitsCap, + State: stateDiff, + Tx: testSubnet1, } err = testSubnet1.Unsigned.Visit(&executor) if err != nil { diff --git a/vms/platformvm/block/executor/manager.go b/vms/platformvm/block/executor/manager.go index 27d35a7641ad..cc6b72a2813e 100644 --- a/vms/platformvm/block/executor/manager.go +++ b/vms/platformvm/block/executor/manager.go @@ -9,7 +9,9 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -142,10 +144,13 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { return err } + feesCfg := config.EUpgradeDynamicFeesConfig err = tx.Unsigned.Visit(&executor.StandardTxExecutor{ - Backend: m.txExecutorBackend, - State: stateDiff, - Tx: tx, + Backend: m.txExecutorBackend, + BlkFeeManager: fees.NewManager(feesCfg.UnitFees), + UnitCaps: feesCfg.BlockUnitsCap, + State: stateDiff, + Tx: tx, }) // We ignore [errFutureStakeTime] here because the time will be advanced // when this transaction is issued. diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index 6205dacfa29c..c092558fcf69 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -140,6 +140,7 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { env.clk.Set(defaultGenesisTime) env.config.BanffTime = time.Time{} // activate Banff env.config.DurangoTime = mockable.MaxTime // deactivate Durango + env.config.EForkTime = mockable.MaxTime // create parentBlock. It's a standard one for simplicity parentTime := defaultGenesisTime diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index 3e0122c9123d..8faf5796554c 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -87,6 +87,8 @@ func TestBanffStandardBlockTimeVerification(t *testing.T) { now := env.clk.Time() env.clk.Set(now) env.config.BanffTime = time.Time{} // activate Banff + env.config.DurangoTime = time.Time{} + env.config.EForkTime = time.Time{} // setup and store parent block // it's a standard block for simplicity diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index b35d2ecdd55c..800a433db5f3 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -10,7 +10,9 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -438,16 +440,21 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, state state.Diff, parentID error, ) { var ( + feesCfg = config.EUpgradeDynamicFeesConfig + onAcceptFunc func() inputs set.Set[ids.ID] funcs = make([]func(), 0, len(txs)) atomicRequests = make(map[ids.ID]*atomic.Requests) ) + for _, tx := range txs { txExecutor := executor.StandardTxExecutor{ - Backend: v.txExecutorBackend, - State: state, - Tx: tx, + Backend: v.txExecutorBackend, + BlkFeeManager: fees.NewManager(feesCfg.UnitFees), + UnitCaps: feesCfg.BlockUnitsCap, + State: state, + Tx: tx, } if err := tx.Unsigned.Visit(&txExecutor); err != nil { txID := tx.ID() diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index ccac3da3b7e3..b22b2765753a 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -232,6 +232,9 @@ func TestVerifierVisitStandardBlock(t *testing.T) { Config: &config.Config{ ApricotPhase5Time: time.Now().Add(time.Hour), BanffTime: mockable.MaxTime, // banff is not activated + CortinaTime: mockable.MaxTime, + DurangoTime: mockable.MaxTime, + EForkTime: mockable.MaxTime, }, Clk: &mockable.Clock{}, }, @@ -714,6 +717,9 @@ func TestVerifierVisitStandardBlockWithDuplicateInputs(t *testing.T) { Config: &config.Config{ ApricotPhase5Time: time.Now().Add(time.Hour), BanffTime: mockable.MaxTime, // banff is not activated + CortinaTime: mockable.MaxTime, + DurangoTime: mockable.MaxTime, + EForkTime: mockable.MaxTime, }, Clk: &mockable.Clock{}, }, diff --git a/vms/platformvm/config/config.go b/vms/platformvm/config/config.go index 292981ddc021..aca79f446024 100644 --- a/vms/platformvm/config/config.go +++ b/vms/platformvm/config/config.go @@ -41,31 +41,31 @@ type Config struct { // Set of subnets that this node is validating TrackedSubnets set.Set[ids.ID] - // Fee that is burned by every non-state creating transaction + // Pre E Fork, fee that is burned by every non-state creating transaction TxFee uint64 - // Fee that must be burned by every state creating transaction before AP3 + // Pre E Fork, fee that must be burned by every state creating transaction before AP3 CreateAssetTxFee uint64 - // Fee that must be burned by every subnet creating transaction after AP3 + // Pre E Fork, fee that must be burned by every subnet creating transaction after AP3 CreateSubnetTxFee uint64 - // Fee that must be burned by every transform subnet transaction + // Pre E Fork, fee that must be burned by every transform subnet transaction TransformSubnetTxFee uint64 - // Fee that must be burned by every blockchain creating transaction after AP3 + // Pre E Fork, fee that must be burned by every blockchain creating transaction after AP3 CreateBlockchainTxFee uint64 - // Transaction fee for adding a primary network validator + // Pre E Fork, transaction fee for adding a primary network validator AddPrimaryNetworkValidatorFee uint64 - // Transaction fee for adding a primary network delegator + // Pre E Fork, transaction fee for adding a primary network delegator AddPrimaryNetworkDelegatorFee uint64 - // Transaction fee for adding a subnet validator + // Pre E Fork, transaction fee for adding a subnet validator AddSubnetValidatorFee uint64 - // Transaction fee for adding a subnet delegator + // Pre E Fork, transaction fee for adding a subnet delegator AddSubnetDelegatorFee uint64 // The minimum amount of tokens one must bond to be a validator diff --git a/vms/platformvm/config/dynamic_fees_config.go b/vms/platformvm/config/dynamic_fees_config.go new file mode 100644 index 000000000000..d7994896c7da --- /dev/null +++ b/vms/platformvm/config/dynamic_fees_config.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "math" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" +) + +// Dynamic fees configs become relevant with dynamic fees introduction in E-fork +// We cannot easily include then in Config since they do not come from genesis +// They don't feel like an execution config either, since we need a fork upgrade +// to update them (testing is a different story). +// I am setting them in a separate config object, but will access it via Config +// so to have fork control over which dynamic fees is picked + +// EUpgradeDynamicFeesConfig to be tuned TODO ABENEGIA +var EUpgradeDynamicFeesConfig = DynamicFeesConfig{ + UnitFees: commonfees.Dimensions{ + 1, + 2, + 3, + 4, + }, + + BlockUnitsCap: commonfees.Dimensions{ + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + }, +} + +type DynamicFeesConfig struct { + // UnitFees contains, per each fee dimension, the + // unit fees valid as soon as fork introducing dynamic fees + // activates. Unit fees will be then updated by the dynamic fees algo. + UnitFees commonfees.Dimensions + + // BlockUnitsCap contains, per each fee dimension, the + // maximal complexity a valid P-chain block can host + BlockUnitsCap commonfees.Dimensions +} diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 907c3c56ef7d..ebdd3363f705 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -14,6 +14,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -35,6 +37,8 @@ type diff struct { timestamp time.Time + unitFees commonfees.Dimensions + // Subnet ID --> supply of native asset of the subnet currentSupply map[ids.ID]uint64 @@ -89,6 +93,10 @@ func NewDiffOn(parentState Chain) (Diff, error) { }) } +func (d *diff) GetUnitFees() commonfees.Dimensions { + return d.unitFees +} + func (d *diff) GetTimestamp() time.Time { return d.timestamp } diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 4ae66d03c575..e71bc550eb62 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -371,6 +371,7 @@ type state struct { // The persisted fields represent the current database value timestamp, persistedTimestamp time.Time currentSupply, persistedCurrentSupply uint64 + // [lastAccepted] is the most recently accepted block. lastAccepted, persistedLastAccepted ids.ID indexedHeights *heightRange diff --git a/vms/platformvm/txs/builder/builder.go b/vms/platformvm/txs/builder/builder.go index 665342dab5b7..7335e81a8084 100644 --- a/vms/platformvm/txs/builder/builder.go +++ b/vms/platformvm/txs/builder/builder.go @@ -18,8 +18,11 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) // Max number of items allowed in a page @@ -224,6 +227,16 @@ func (b *builder) NewImportTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { + // 1. Build core transaction without utxos + utx := &txs.ImportTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.ctx.NetworkID, + BlockchainID: b.ctx.ChainID, + }}, + SourceChain: from, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) kc := secp256k1fx.NewKeychain(keys...) atomicUTXOs, _, _, err := b.GetAtomicUTXOs(from, kc.Addresses(), ids.ShortEmpty, ids.Empty, MaxPageSize) @@ -231,11 +244,14 @@ func (b *builder) NewImportTx( return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err) } - importedInputs := []*avax.TransferableInput{} - signers := [][]*secp256k1.PrivateKey{} + var ( + importedInputs = []*avax.TransferableInput{} + signers = [][]*secp256k1.PrivateKey{} + outs = []*avax.TransferableOutput{} - importedAmounts := make(map[ids.ID]uint64) - now := b.clk.Unix() + importedAmounts = make(map[ids.ID]uint64) + now = b.clk.Unix() + ) for _, utxo := range atomicUTXOs { inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now) if err != nil { @@ -257,32 +273,21 @@ func (b *builder) NewImportTx( }) signers = append(signers, utxoSigners) } - avax.SortTransferableInputsWithSigners(importedInputs, signers) - if len(importedAmounts) == 0 { return nil, ErrNoFunds // No imported UTXOs were spendable } - importedAVAX := importedAmounts[b.ctx.AVAXAssetID] - - ins := []*avax.TransferableInput{} - outs := []*avax.TransferableOutput{} - switch { - case importedAVAX < b.cfg.TxFee: // imported amount goes toward paying tx fee - var baseSigners [][]*secp256k1.PrivateKey - ins, outs, _, baseSigners, err = b.Spend(b.state, keys, 0, b.cfg.TxFee-importedAVAX, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - signers = append(baseSigners, signers...) - delete(importedAmounts, b.ctx.AVAXAssetID) - case importedAVAX == b.cfg.TxFee: - delete(importedAmounts, b.ctx.AVAXAssetID) - default: - importedAmounts[b.ctx.AVAXAssetID] -= b.cfg.TxFee - } + // Sort and add imported txs to utx. Imported txs must not be + // changed here in after + avax.SortTransferableInputsWithSigners(importedInputs, signers) + utx.ImportedInputs = importedInputs + // add non avax-denominated outputs. Avax-denominated utxos + // are used to pay fees whose amount is calculated later on for assetID, amount := range importedAmounts { + if assetID == b.ctx.AVAXAssetID { + continue + } outs = append(outs, &avax.TransferableOutput{ Asset: avax.Asset{ID: assetID}, Out: &secp256k1fx.TransferOutput{ @@ -294,21 +299,127 @@ func (b *builder) NewImportTx( }, }, }) + delete(importedAmounts, assetID) } - avax.SortTransferableOutputs(outs, txs.Codec) // sort imported outputs + var ( + ins []*avax.TransferableInput - // Create the transaction - utx := &txs.ImportTx{ - BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Outs: outs, - Ins: ins, - }}, - SourceChain: from, - ImportedInputs: importedInputs, + importedAVAX = importedAmounts[b.ctx.AVAXAssetID] // the only entry left in importedAmounts + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ) + if isEForkActive { + // while outs are not ordered we add them to get current fees. We'll fix ordering later on + utx.BaseTx.Outs = outs + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.ImportTx(utx); err != nil { + return nil, err + } + + if feeCalc.Fee >= importedAVAX { + // all imported avax will be burned to pay taxes. + // Fees are scaled back accordingly. + feeCalc.Fee -= importedAVAX + } else { + // imported inputs may be enough to pay taxes by themselves + changeOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: b.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + // Amt: importedAVAX, // SET IT AFTER CONSIDERING ITS OWN FEES + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{to}, + }, + }, + } + + // update fees to target given the extra input added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{changeOut}) + if err != nil { + return nil, fmt.Errorf("failed calculating output size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(outDimensions) + if err != nil { + return nil, fmt.Errorf("account for output fees: %w", err) + } + + if addedFees >= 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 + + var ( + financeOut []*avax.TransferableOutput + financeSigner [][]*secp256k1.PrivateKey + ) + ins, financeOut, _, financeSigner, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + outs = append(financeOut, outs...) + signers = append(financeSigner, signers...) + } else { + changeOut.Out.(*secp256k1fx.TransferOutput).Amt = importedAVAX - feeCalc.Fee + outs = append(outs, changeOut) + } + } + } else { + switch { + case importedAVAX < b.cfg.TxFee: // imported amount goes toward paying tx fee + var ( + baseOuts []*avax.TransferableOutput + baseSigners [][]*secp256k1.PrivateKey + ) + ins, baseOuts, _, baseSigners, err = b.Spend(b.state, keys, 0, b.cfg.TxFee-importedAVAX, changeAddr) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + outs = append(baseOuts, outs...) + signers = append(baseSigners, signers...) + delete(importedAmounts, b.ctx.AVAXAssetID) + case importedAVAX == b.cfg.TxFee: + delete(importedAmounts, b.ctx.AVAXAssetID) + default: + importedAVAX -= b.cfg.TxFee + outs = append(outs, &avax.TransferableOutput{ + Asset: avax.Asset{ID: b.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: importedAVAX, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{to}, + }, + }, + }) + } } + + avax.SortTransferableOutputs(outs, txs.Codec) // sort imported outputs + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -324,22 +435,11 @@ func (b *builder) NewExportTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - toBurn, err := math.Add64(amount, b.cfg.TxFee) - if err != nil { - return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) - } - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - // Create the transaction + // 1. Build core transaction without utxos utx := &txs.ExportTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, // Non-exported outputs }}, DestinationChain: chainID, ExportedOutputs: []*avax.TransferableOutput{{ // Exported to X-Chain @@ -354,6 +454,53 @@ func (b *builder) NewExportTx( }, }}, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + err error + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.ExportTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + var toBurn uint64 + toBurn, err = math.Add64(amount, b.cfg.TxFee) + if err != nil { + return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) + } + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, toBurn, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -370,29 +517,18 @@ func (b *builder) NewCreateChainTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - timestamp := b.state.GetTimestamp() - createBlockchainTxFee := b.cfg.GetCreateBlockchainTxFee(timestamp) - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createBlockchainTxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - + // 1. Build core transaction without utxos subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) if err != nil { return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) } - signers = append(signers, subnetSigners) - // Sort the provided fxIDs - utils.Sort(fxIDs) + utils.Sort(fxIDs) // sort the provided fxIDs - // Create the tx utx := &txs.CreateChainTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, }}, SubnetID: subnetID, ChainName: chainName, @@ -401,6 +537,50 @@ func (b *builder) NewCreateChainTx( GenesisData: genesisData, SubnetAuth: subnetAuth, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.CreateChainTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + timestamp := b.state.GetTimestamp() + createBlockchainTxFee := b.cfg.GetCreateBlockchainTxFee(timestamp) + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, createBlockchainTxFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx + signers = append(signers, subnetSigners) tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -414,29 +594,62 @@ func (b *builder) NewCreateSubnetTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - timestamp := b.state.GetTimestamp() - createSubnetTxFee := b.cfg.GetCreateSubnetTxFee(timestamp) - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createSubnetTxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - - // Sort control addresses - utils.Sort(ownerAddrs) + // 1. Build core transaction without utxos + utils.Sort(ownerAddrs) // sort control addresses - // Create the tx utx := &txs.CreateSubnetTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, }}, Owner: &secp256k1fx.OutputOwners{ Threshold: threshold, Addrs: ownerAddrs, }, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + err error + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.CreateSubnetTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + createSubnetTxFee := b.cfg.GetCreateSubnetTxFee(chainTime) + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, createSubnetTxFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -454,17 +667,11 @@ func (b *builder) NewAddValidatorTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - ins, unstakedOuts, stakedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkValidatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx + // 1. Build core transaction without utxos utx := &txs.AddValidatorTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unstakedOuts, }}, Validator: txs.Validator{ NodeID: nodeID, @@ -472,7 +679,6 @@ func (b *builder) NewAddValidatorTx( End: endTime, Wght: stakeAmount, }, - StakeOuts: stakedOuts, RewardsOwner: &secp256k1fx.OutputOwners{ Locktime: 0, Threshold: 1, @@ -480,6 +686,50 @@ func (b *builder) NewAddValidatorTx( }, DelegationShares: shares, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + stakedOuts []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + err error + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.AddValidatorTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + ins, outs, stakedOuts, signers, err = b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkValidatorFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + utx.StakeOuts = stakedOuts + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -496,17 +746,11 @@ func (b *builder) NewAddDelegatorTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - ins, unlockedOuts, lockedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkDelegatorFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - // Create the tx + // 1. Build core transaction without utxos utx := &txs.AddDelegatorTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: unlockedOuts, }}, Validator: txs.Validator{ NodeID: nodeID, @@ -514,13 +758,56 @@ func (b *builder) NewAddDelegatorTx( End: endTime, Wght: stakeAmount, }, - StakeOuts: lockedOuts, DelegationRewardsOwner: &secp256k1fx.OutputOwners{ Locktime: 0, Threshold: 1, Addrs: []ids.ShortID{rewardAddress}, }, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + stakedOuts []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + err error + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.AddDelegatorTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + ins, outs, stakedOuts, signers, err = b.Spend(b.state, keys, stakeAmount, b.cfg.AddPrimaryNetworkDelegatorFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + utx.StakeOuts = stakedOuts + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -537,24 +824,15 @@ func (b *builder) NewAddSubnetValidatorTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - + // 1. Build core transaction without utxos subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) if err != nil { return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) } - signers = append(signers, subnetSigners) - - // Create the tx utx := &txs.AddSubnetValidatorTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, }}, SubnetValidator: txs.SubnetValidator{ Validator: txs.Validator{ @@ -567,6 +845,48 @@ func (b *builder) NewAddSubnetValidatorTx( }, SubnetAuth: subnetAuth, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.AddSubnetValidatorTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx + signers = append(signers, subnetSigners) tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -580,29 +900,62 @@ func (b *builder) NewRemoveSubnetValidatorTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - + // 1. Build core transaction without utxos subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) if err != nil { return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) } - signers = append(signers, subnetSigners) - - // Create the tx utx := &txs.RemoveSubnetValidatorTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, }}, Subnet: subnetID, NodeID: nodeID, SubnetAuth: subnetAuth, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.RemoveSubnetValidatorTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx + signers = append(signers, subnetSigners) tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -627,23 +980,15 @@ func (b *builder) NewTransferSubnetOwnershipTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) - if err != nil { - return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - } - + // 1. Build core transaction without utxos subnetAuth, subnetSigners, err := b.Authorize(b.state, subnetID, keys) if err != nil { return nil, fmt.Errorf("couldn't authorize tx's subnet restrictions: %w", err) } - signers = append(signers, subnetSigners) - utx := &txs.TransferSubnetOwnershipTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.ctx.NetworkID, BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, }}, Subnet: subnetID, SubnetAuth: subnetAuth, @@ -652,6 +997,48 @@ func (b *builder) NewTransferSubnetOwnershipTx( Addrs: ownerAddrs, }, } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.TransferSubnetOwnershipTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) + } + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx + signers = append(signers, subnetSigners) tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err @@ -665,11 +1052,52 @@ func (b *builder) NewBaseTx( keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) { - toBurn, err := math.Add64(amount, b.cfg.TxFee) - if err != nil { - return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) + // 1. Build core transaction without utxos + utx := &txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: b.ctx.NetworkID, + BlockchainID: b.ctx.ChainID, + }, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + var ( + chainTime = b.state.GetTimestamp() + isEForkActive = b.cfg.IsEForkActivated(chainTime) + ins []*avax.TransferableInput + outs []*avax.TransferableOutput + signers [][]*secp256k1.PrivateKey + err error + ) + if isEForkActive { + feeCfg := config.EUpgradeDynamicFeesConfig + feeCalc := &fees.Calculator{ + IsEForkActive: isEForkActive, + FeeManager: commonfees.NewManager(feeCfg.UnitFees), + ConsumedUnitsCap: feeCfg.BlockUnitsCap, + Credentials: txs.EmptyCredentials(signers), + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.BaseTx(utx); err != nil { + return nil, err + } + + ins, outs, _, signers, err = b.FinanceTx( + b.state, + keys, + 0, + feeCalc, + changeAddr, + ) + } else { + var toBurn uint64 + toBurn, err = math.Add64(amount, b.cfg.TxFee) + if err != nil { + return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) + } + ins, outs, _, signers, err = b.Spend(b.state, keys, 0, toBurn, changeAddr) } - ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr) if err != nil { return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) } @@ -681,17 +1109,12 @@ func (b *builder) NewBaseTx( OutputOwners: owner, }, }) - avax.SortTransferableOutputs(outs, txs.Codec) - utx := &txs.BaseTx{ - BaseTx: avax.BaseTx{ - NetworkID: b.ctx.NetworkID, - BlockchainID: b.ctx.ChainID, - Ins: ins, - Outs: outs, - }, - } + utx.BaseTx.Ins = ins + utx.BaseTx.Outs = outs + + // 3. Sign the tx tx, err := txs.NewSigned(utx, txs.Codec, signers) if err != nil { return nil, err diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 2a35cb45eeab..a00d8381d7db 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -9,6 +9,8 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var _ txs.Visitor = (*AtomicTxExecutor)(nil) @@ -99,9 +101,11 @@ func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { e.OnAccept = onAccept executor := StandardTxExecutor{ - Backend: e.Backend, - State: e.OnAccept, - Tx: e.Tx, + Backend: e.Backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: e.OnAccept, + Tx: e.Tx, } err = tx.Visit(&executor) e.Inputs = executor.Inputs diff --git a/vms/platformvm/txs/executor/create_chain_test.go b/vms/platformvm/txs/executor/create_chain_test.go index 0342c8dc1cfe..eabf9d3f9239 100644 --- a/vms/platformvm/txs/executor/create_chain_test.go +++ b/vms/platformvm/txs/executor/create_chain_test.go @@ -20,6 +20,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) // Ensure Execute fails when there are not enough control sigs @@ -47,9 +49,11 @@ func TestCreateChainTxInsufficientControlSigs(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, errUnauthorizedSubnetModification) @@ -86,9 +90,11 @@ func TestCreateChainTxWrongControlSig(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, errUnauthorizedSubnetModification) @@ -119,9 +125,11 @@ func TestCreateChainTxNoSuchSubnet(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, database.ErrNotFound) @@ -149,9 +157,11 @@ func TestCreateChainTxValid(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } require.NoError(tx.Unsigned.Visit(&executor)) } @@ -220,9 +230,11 @@ func TestCreateChainTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, test.expectedError) diff --git a/vms/platformvm/txs/executor/create_subnet_test.go b/vms/platformvm/txs/executor/create_subnet_test.go index 6d968daa4df0..d084fe473506 100644 --- a/vms/platformvm/txs/executor/create_subnet_test.go +++ b/vms/platformvm/txs/executor/create_subnet_test.go @@ -16,6 +16,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) func TestCreateSubnetTxAP3FeeChange(t *testing.T) { @@ -76,9 +78,11 @@ func TestCreateSubnetTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: commonfees.NewManager(commonfees.Empty), + UnitCaps: commonfees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, test.expectedErr) diff --git a/vms/platformvm/txs/executor/helpers_test.go b/vms/platformvm/txs/executor/helpers_test.go index ce59a27e1467..a80d81053e3a 100644 --- a/vms/platformvm/txs/executor/helpers_test.go +++ b/vms/platformvm/txs/executor/helpers_test.go @@ -35,6 +35,7 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm/api" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -228,10 +229,13 @@ func addSubnet( stateDiff, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) + feeCfg := config.EUpgradeDynamicFeesConfig executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: testSubnet1, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(feeCfg.UnitFees), + UnitCaps: feeCfg.BlockUnitsCap, + State: stateDiff, + Tx: testSubnet1, } require.NoError(testSubnet1.Unsigned.Visit(&executor)) diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 0f082cb8b296..0aaab27c5b4b 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -16,6 +16,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/reward" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) const ( @@ -45,7 +47,8 @@ var ( type ProposalTxExecutor struct { // inputs, to be filled before visitor methods are called *Backend - Tx *txs.Tx + BlkFeeManager *commonfees.Manager + Tx *txs.Tx // [OnCommitState] is the state used for validation. // [OnCommitState] is modified by this struct's methods to // reflect changes made to the state if the proposal is committed. @@ -114,6 +117,8 @@ func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { onAbortOuts, err := verifyAddValidatorTx( e.Backend, + e.BlkFeeManager, + commonfees.Empty, e.OnCommitState, e.Tx, tx, @@ -161,6 +166,8 @@ func (e *ProposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) if err := verifyAddSubnetValidatorTx( e.Backend, + e.BlkFeeManager, + commonfees.Empty, e.OnCommitState, e.Tx, tx, @@ -207,6 +214,8 @@ func (e *ProposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { onAbortOuts, err := verifyAddDelegatorTx( e.Backend, + e.BlkFeeManager, + commonfees.Empty, e.OnCommitState, e.Tx, tx, diff --git a/vms/platformvm/txs/executor/staker_tx_verification.go b/vms/platformvm/txs/executor/staker_tx_verification.go index fdeb9afb7219..21011866e65f 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification.go +++ b/vms/platformvm/txs/executor/staker_tx_verification.go @@ -18,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" safemath "github.com/ava-labs/avalanchego/utils/math" + commonFees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -89,6 +90,8 @@ func verifySubnetValidatorPrimaryNetworkRequirements( // added to the staking set. func verifyAddValidatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.AddValidatorTx, @@ -167,8 +170,12 @@ func verifyAddValidatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: currentTimestamp, + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return nil, err @@ -196,6 +203,8 @@ func verifyAddValidatorTx( // AddSubnetValidatorTx. func verifyAddSubnetValidatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.AddSubnetValidatorTx, @@ -265,8 +274,12 @@ func verifyAddSubnetValidatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: currentTimestamp, + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -300,6 +313,8 @@ func verifyAddSubnetValidatorTx( // * The flow checker passes. func verifyRemoveSubnetValidatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.RemoveSubnetValidatorTx, @@ -350,8 +365,12 @@ func verifyRemoveSubnetValidatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: chainState.GetTimestamp(), + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return nil, false, err @@ -378,6 +397,8 @@ func verifyRemoveSubnetValidatorTx( // added to the staking set. func verifyAddDelegatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.AddDelegatorTx, @@ -476,8 +497,12 @@ func verifyAddDelegatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: chainState.GetTimestamp(), + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return nil, err @@ -505,6 +530,8 @@ func verifyAddDelegatorTx( // AddPermissionlessValidatorTx. func verifyAddPermissionlessValidatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.AddPermissionlessValidatorTx, @@ -603,8 +630,12 @@ func verifyAddPermissionlessValidatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: currentTimestamp, + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -632,6 +663,8 @@ func verifyAddPermissionlessValidatorTx( // AddPermissionlessDelegatorTx. func verifyAddPermissionlessDelegatorTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.AddPermissionlessDelegatorTx, @@ -755,14 +788,17 @@ func verifyAddPermissionlessDelegatorTx( // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: currentTimestamp, + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err } - // Verify the flowcheck if err := backend.FlowChecker.VerifySpend( tx, chainState, @@ -788,6 +824,8 @@ func verifyAddPermissionlessDelegatorTx( // * The flow checker passes. func verifyTransferSubnetOwnershipTx( backend *Backend, + feeManager *commonFees.Manager, + unitCaps commonFees.Dimensions, chainState state.Chain, sTx *txs.Tx, tx *txs.TransferSubnetOwnershipTx, @@ -816,10 +854,16 @@ func verifyTransferSubnetOwnershipTx( } // Verify the flowcheck + currentTimestamp := chainState.GetTimestamp() feeCalculator := fees.Calculator{ - Config: backend.Config, - ChainTime: chainState.GetTimestamp(), + IsEForkActive: backend.Config.IsEForkActivated(currentTimestamp), + Config: backend.Config, + ChainTime: currentTimestamp, + FeeManager: feeManager, + ConsumedUnitsCap: unitCaps, + Credentials: sTx.Creds, } + if err := tx.Visit(&feeCalculator); err != nil { return err } diff --git a/vms/platformvm/txs/executor/staker_tx_verification_test.go b/vms/platformvm/txs/executor/staker_tx_verification_test.go index c874e884561d..11d1e3bd8bc6 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification_test.go +++ b/vms/platformvm/txs/executor/staker_tx_verification_test.go @@ -19,6 +19,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/state" @@ -103,7 +104,10 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { Creds: []verify.Verifiable{}, } ) - verifiedSignedTx.SetBytes([]byte{1}, []byte{2}) + + unsignedBytes := []byte{1} + signedBytes := []byte{2} + verifiedSignedTx.SetBytes(unsignedBytes, signedBytes) tests := []test{ { @@ -507,10 +511,10 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { return &Backend{ FlowChecker: flowChecker, Config: &config.Config{ + AddSubnetValidatorFee: 1, CortinaTime: activeForkTime, DurangoTime: mockable.MaxTime, EForkTime: mockable.MaxTime, - AddSubnetValidatorFee: 1, }, Ctx: ctx, Bootstrapped: bootstrapped, @@ -542,7 +546,7 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { expectedErr: ErrFutureStakeTime, }, { - name: "success", + name: "success pre EFork", backendF: func(ctrl *gomock.Controller) *Backend { bootstrapped := &utils.Atomic[bool]{} bootstrapped.Set(true) @@ -596,12 +600,14 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { var ( backend = tt.backendF(ctrl) - state = tt.stateF(ctrl) - sTx = tt.sTxF() - tx = tt.txF() + + feeManager = fees.NewManager(fees.Empty) + state = tt.stateF(ctrl) + sTx = tt.sTxF() + tx = tt.txF() ) - err := verifyAddPermissionlessValidatorTx(backend, state, sTx, tx) + err := verifyAddPermissionlessValidatorTx(backend, feeManager, fees.Empty, state, sTx, tx) require.ErrorIs(t, err, tt.expectedErr) }) } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index b42f66969b56..29c2224a9311 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -20,6 +20,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" + + commonFees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -33,8 +35,10 @@ var ( type StandardTxExecutor struct { // inputs, to be filled before visitor methods are called *Backend - State state.Diff // state is expected to be modified - Tx *txs.Tx + BlkFeeManager *commonFees.Manager + UnitCaps commonFees.Dimensions + State state.Diff // state is expected to be modified + Tx *txs.Tx // outputs of visitor execution OnAccept func() // may be nil @@ -70,8 +74,12 @@ func (e *StandardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: e.Backend.Config, - ChainTime: e.State.GetTimestamp(), + IsEForkActive: e.Backend.Config.IsEForkActivated(currentTimestamp), + Config: e.Backend.Config, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -123,8 +131,12 @@ func (e *StandardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: e.Backend.Config, - ChainTime: e.State.GetTimestamp(), + IsEForkActive: e.Backend.Config.IsEForkActivated(currentTimestamp), + Config: e.Backend.Config, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -210,9 +222,17 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { copy(ins[len(tx.Ins):], tx.ImportedInputs) // Verify the flowcheck + var ( + cfg = e.Backend.Config + currentTimestamp = e.State.GetTimestamp() + ) feeCalculator := fees.Calculator{ - Config: e.Backend.Config, - ChainTime: e.State.GetTimestamp(), + IsEForkActive: cfg.IsEForkActivated(currentTimestamp), + Config: cfg, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -275,8 +295,12 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { // Verify the flowcheck feeCalculator := fees.Calculator{ - Config: e.Backend.Config, - ChainTime: e.State.GetTimestamp(), + IsEForkActive: e.Backend.Config.IsEForkActivated(currentTimestamp), + Config: e.Backend.Config, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err @@ -346,6 +370,8 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { if _, err := verifyAddValidatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -375,6 +401,8 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { if err := verifyAddSubnetValidatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -395,6 +423,8 @@ func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { if _, err := verifyAddDelegatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -420,6 +450,8 @@ func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { func (e *StandardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -468,6 +500,17 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error } totalRewardAmount := tx.MaximumSupply - tx.InitialSupply + feeCalculator := fees.Calculator{ + IsEForkActive: e.Backend.Config.IsEForkActivated(currentTimestamp), + Config: e.Backend.Config, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, + } + if err := tx.Visit(&feeCalculator); err != nil { + return err + } if err := e.Backend.FlowChecker.VerifySpend( tx, e.State, @@ -478,7 +521,7 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error // entry in this map literal from being overwritten by the // second entry. map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: e.Config.TransformSubnetTxFee, + e.Ctx.AVAXAssetID: feeCalculator.Fee, tx.AssetID: totalRewardAmount, }, ); err != nil { @@ -500,6 +543,8 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -532,6 +577,8 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { if err := verifyAddPermissionlessDelegatorTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -556,6 +603,8 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl func (e *StandardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { err := verifyTransferSubnetOwnershipTx( e.Backend, + e.BlkFeeManager, + e.UnitCaps, e.State, e.Tx, tx, @@ -587,9 +636,17 @@ func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { } // Verify the flowcheck + var ( + cfg = e.Backend.Config + currentTimestamp = e.State.GetTimestamp() + ) feeCalculator := fees.Calculator{ - Config: e.Backend.Config, - ChainTime: e.State.GetTimestamp(), + IsEForkActive: cfg.IsEForkActivated(currentTimestamp), + Config: cfg, + ChainTime: currentTimestamp, + FeeManager: e.BlkFeeManager, + ConsumedUnitsCap: e.UnitCaps, + Credentials: e.Tx.Creds, } if err := tx.Visit(&feeCalculator); err != nil { return err diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 08f01c6ea592..f50b7f643c96 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -25,6 +25,7 @@ import ( "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/fx" @@ -90,9 +91,11 @@ func TestStandardTxExecutorAddValidatorTxEmptyID(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: stateDiff, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, test.expectedError) @@ -346,9 +349,11 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { freshTH.config.BanffTime = onAcceptState.GetTimestamp() executor := StandardTxExecutor{ - Backend: &freshTH.backend, - State: onAcceptState, - Tx: tx, + Backend: &freshTH.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, tt.expectedExecutionErr) @@ -384,9 +389,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrPeriodMismatch) @@ -412,9 +419,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } require.NoError(tx.Unsigned.Visit(&executor)) } @@ -454,9 +463,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrNotValidator) @@ -497,9 +508,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrPeriodMismatch) @@ -523,9 +536,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrPeriodMismatch) @@ -548,9 +563,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { onAcceptState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } require.NoError(tx.Unsigned.Visit(&executor)) } @@ -576,9 +593,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) @@ -632,9 +651,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: duplicateSubnetTx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: duplicateSubnetTx, } err = duplicateSubnetTx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrDuplicateValidator) @@ -669,9 +690,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, secp256k1fx.ErrInputIndicesNotSortedUnique) @@ -702,9 +725,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, errUnauthorizedSubnetModification) @@ -733,9 +758,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, errUnauthorizedSubnetModification) @@ -774,9 +801,11 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrDuplicateValidator) @@ -809,9 +838,11 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) @@ -835,9 +866,11 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { require.NoError(err) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrFutureStakeTime) @@ -874,9 +907,11 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrAlreadyValidator) @@ -910,9 +945,11 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrAlreadyValidator) @@ -945,9 +982,11 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { } executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, + Backend: &env.backend, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + State: onAcceptState, + Tx: tx, } err = tx.Unsigned.Visit(&executor) require.ErrorIs(err, ErrFlowCheckFailed) @@ -1697,7 +1736,7 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Set dependency expectations. - env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).Times(2) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime) env.state.EXPECT().GetCurrentValidator(env.unsignedTx.Subnet, env.unsignedTx.NodeID).Return(env.staker, nil).Times(1) subnetOwner := fx.NewMockOwner(ctrl) env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) @@ -1723,8 +1762,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1753,8 +1794,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1784,8 +1827,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1818,8 +1863,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1850,8 +1897,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1881,8 +1930,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1914,8 +1965,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -1927,7 +1980,7 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) - env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).Times(2) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime) env.state.EXPECT().GetCurrentValidator(env.unsignedTx.Subnet, env.unsignedTx.NodeID).Return(env.staker, nil) subnetOwner := fx.NewMockOwner(ctrl) env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil) @@ -1950,8 +2003,10 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -2110,8 +2165,10 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -2140,8 +2197,10 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -2172,8 +2231,10 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -2209,8 +2270,10 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e @@ -2251,8 +2314,10 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - Tx: env.tx, - State: env.state, + BlkFeeManager: fees.NewManager(fees.Empty), + UnitCaps: fees.Empty, + Tx: env.tx, + State: env.state, } e.Bootstrapped.Set(true) return env.unsignedTx, e diff --git a/vms/platformvm/txs/fees/calculator.go b/vms/platformvm/txs/fees/calculator.go index b35568d4229e..e7bdbbe00f4d 100644 --- a/vms/platformvm/txs/fees/calculator.go +++ b/vms/platformvm/txs/fees/calculator.go @@ -4,47 +4,125 @@ package fees import ( + "errors" + "fmt" "time" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" + "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) -var _ txs.Visitor = (*Calculator)(nil) +var ( + _ txs.Visitor = (*Calculator)(nil) + + errFailedFeeCalculation = errors.New("failed fee calculation") + errFailedConsumedUnitsCumulation = errors.New("failed cumulating consumed units") +) type Calculator struct { - // setup, to be filled before visitor methods are called + // setup + IsEForkActive bool + + // Pre E-fork inputs Config *config.Config ChainTime time.Time + // Post E-fork inputs + FeeManager *fees.Manager + ConsumedUnitsCap fees.Dimensions + + // common inputs + Credentials []verify.Verifiable + // outputs of visitor execution Fee uint64 } -func (fc *Calculator) AddValidatorTx(*txs.AddValidatorTx) error { - fc.Fee = fc.Config.AddPrimaryNetworkValidatorFee - return nil +func (fc *Calculator) AddValidatorTx(tx *txs.AddValidatorTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.AddPrimaryNetworkValidatorFee + return nil + } + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + consumedUnits, err := fc.commonConsumedUnits(tx, outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { - fc.Fee = fc.Config.AddSubnetValidatorFee - return nil +func (fc *Calculator) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.AddSubnetValidatorFee + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) AddDelegatorTx(*txs.AddDelegatorTx) error { - fc.Fee = fc.Config.AddPrimaryNetworkDelegatorFee - return nil +func (fc *Calculator) AddDelegatorTx(tx *txs.AddDelegatorTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.AddPrimaryNetworkDelegatorFee + return nil + } + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + consumedUnits, err := fc.commonConsumedUnits(tx, outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) CreateChainTx(*txs.CreateChainTx) error { - fc.Fee = fc.Config.GetCreateBlockchainTxFee(fc.ChainTime) - return nil +func (fc *Calculator) CreateChainTx(tx *txs.CreateChainTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.GetCreateBlockchainTxFee(fc.ChainTime) + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) CreateSubnetTx(*txs.CreateSubnetTx) error { - fc.Fee = fc.Config.GetCreateSubnetTxFee(fc.ChainTime) - return nil +func (fc *Calculator) CreateSubnetTx(tx *txs.CreateSubnetTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.GetCreateSubnetTxFee(fc.ChainTime) + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } func (*Calculator) AdvanceTimeTx(*txs.AdvanceTimeTx) error { @@ -55,50 +133,215 @@ func (*Calculator) RewardValidatorTx(*txs.RewardValidatorTx) error { return nil // no fees } -func (fc *Calculator) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { - fc.Fee = fc.Config.TxFee - return nil +func (fc *Calculator) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TxFee + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) TransformSubnetTx(*txs.TransformSubnetTx) error { - fc.Fee = fc.Config.TransformSubnetTxFee - return nil +func (fc *Calculator) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TransformSubnetTxFee + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { - fc.Fee = fc.Config.TxFee - return nil +func (fc *Calculator) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TxFee + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } func (fc *Calculator) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - if tx.Subnet != constants.PrimaryNetworkID { - fc.Fee = fc.Config.AddSubnetValidatorFee - } else { - fc.Fee = fc.Config.AddPrimaryNetworkValidatorFee + if !fc.IsEForkActive { + if tx.Subnet != constants.PrimaryNetworkID { + fc.Fee = fc.Config.AddSubnetValidatorFee + } else { + fc.Fee = fc.Config.AddPrimaryNetworkValidatorFee + } + return nil } - return nil + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + consumedUnits, err := fc.commonConsumedUnits(tx, outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } func (fc *Calculator) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - if tx.Subnet != constants.PrimaryNetworkID { - fc.Fee = fc.Config.AddSubnetDelegatorFee - } else { - fc.Fee = fc.Config.AddPrimaryNetworkDelegatorFee + if !fc.IsEForkActive { + if tx.Subnet != constants.PrimaryNetworkID { + fc.Fee = fc.Config.AddSubnetDelegatorFee + } else { + fc.Fee = fc.Config.AddPrimaryNetworkDelegatorFee + } + return nil + } + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.StakeOuts) + + consumedUnits, err := fc.commonConsumedUnits(tx, outs, tx.Ins) + if err != nil { + return err } - return nil + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) BaseTx(*txs.BaseTx) error { - fc.Fee = fc.Config.TxFee - return nil +func (fc *Calculator) BaseTx(tx *txs.BaseTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TxFee + return nil + } + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) ImportTx(*txs.ImportTx) error { - fc.Fee = fc.Config.TxFee - return nil +func (fc *Calculator) ImportTx(tx *txs.ImportTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TxFee + return nil + } + + ins := make([]*avax.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) + copy(ins, tx.Ins) + copy(ins[len(tx.Ins):], tx.ImportedInputs) + + consumedUnits, err := fc.commonConsumedUnits(tx, tx.Outs, ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err +} + +func (fc *Calculator) ExportTx(tx *txs.ExportTx) error { + if !fc.IsEForkActive { + fc.Fee = fc.Config.TxFee + return nil + } + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.ExportedOutputs)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.ExportedOutputs) + + consumedUnits, err := fc.commonConsumedUnits(tx, outs, tx.Ins) + if err != nil { + return err + } + + _, err = fc.AddFeesFor(consumedUnits) + return err } -func (fc *Calculator) ExportTx(*txs.ExportTx) error { - fc.Fee = fc.Config.TxFee - return nil +func (fc *Calculator) commonConsumedUnits( + uTx txs.UnsignedTx, + allOuts []*avax.TransferableOutput, + allIns []*avax.TransferableInput, +) (fees.Dimensions, error) { + var consumedUnits fees.Dimensions + + uTxSize, err := txs.Codec.Size(txs.CodecVersion, uTx) + if err != nil { + return consumedUnits, fmt.Errorf("couldn't calculate UnsignedTx marshal length: %w", err) + } + credsSize, err := txs.Codec.Size(txs.CodecVersion, fc.Credentials) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of credentials: %w", err) + } + consumedUnits[fees.Bandwidth] = uint64(uTxSize + credsSize) + + inputDimensions, err := fees.GetInputsDimensions(txs.Codec, txs.CodecVersion, allIns) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of inputs: %w", err) + } + inputDimensions[fees.Bandwidth] = 0 // inputs bandwidth is already accounted for above, so we zero it + consumedUnits, err = fees.Add(consumedUnits, inputDimensions) + if err != nil { + return consumedUnits, fmt.Errorf("failed adding inputs: %w", err) + } + + outputDimensions, err := fees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, allOuts) + if err != nil { + return consumedUnits, fmt.Errorf("failed retrieving size of outputs: %w", err) + } + outputDimensions[fees.Bandwidth] = 0 // outputs bandwidth is already accounted for above, so we zero it + consumedUnits, err = fees.Add(consumedUnits, outputDimensions) + if err != nil { + return consumedUnits, fmt.Errorf("failed adding outputs: %w", err) + } + + return consumedUnits, nil +} + +func (fc *Calculator) AddFeesFor(consumedUnits fees.Dimensions) (uint64, error) { + boundBreached, dimension := fc.FeeManager.CumulateUnits(consumedUnits, fc.ConsumedUnitsCap) + if boundBreached { + return 0, fmt.Errorf("%w: breached dimension %d", errFailedConsumedUnitsCumulation, dimension) + } + + fee, err := fc.FeeManager.CalculateFee(consumedUnits) + if err != nil { + return 0, fmt.Errorf("%w: %w", errFailedFeeCalculation, err) + } + + fc.Fee += fee + return fee, nil +} + +func (fc *Calculator) RemoveFeesFor(unitsToRm fees.Dimensions) (uint64, error) { + if err := fc.FeeManager.RemoveUnits(unitsToRm); err != nil { + return 0, fmt.Errorf("failed removing units: %w", err) + } + + fee, err := fc.FeeManager.CalculateFee(unitsToRm) + if err != nil { + return 0, fmt.Errorf("%w: %w", errFailedFeeCalculation, err) + } + + fc.Fee -= fee + return fee, nil } diff --git a/vms/platformvm/txs/fees/calculator_test.go b/vms/platformvm/txs/fees/calculator_test.go new file mode 100644 index 000000000000..fcde6cb205ab --- /dev/null +++ b/vms/platformvm/txs/fees/calculator_test.go @@ -0,0 +1,1736 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fees + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +var ( + testUnitFees = fees.Dimensions{ + 1 * units.MicroAvax, + 2 * units.MicroAvax, + 3 * units.MicroAvax, + 4 * units.MicroAvax, + } + testBlockMaxConsumedUnits = fees.Dimensions{ + 3000, + 3500, + 1000, + 1000, + } + + feeTestsDefaultCfg = config.Config{ + TxFee: 1 * units.Avax, + CreateAssetTxFee: 2 * units.Avax, + CreateSubnetTxFee: 3 * units.Avax, + TransformSubnetTxFee: 4 * units.Avax, + CreateBlockchainTxFee: 5 * units.Avax, + AddPrimaryNetworkValidatorFee: 6 * units.Avax, + AddPrimaryNetworkDelegatorFee: 7 * units.Avax, + AddSubnetValidatorFee: 8 * units.Avax, + AddSubnetDelegatorFee: 9 * units.Avax, + } + + preFundedKeys = secp256k1.TestKeys() + feeTestSigners = [][]*secp256k1.PrivateKey{preFundedKeys} + feeTestDefaultStakeWeight = uint64(2024) + durangoTime = time.Time{} // assume durango is active in these tests +) + +type feeTests struct { + description string + cfgAndChainTimeF func() (*config.Config, time.Time) + consumedUnitCapsF func() fees.Dimensions + expectedError error + checksF func(*testing.T, *Calculator) +} + +func TestPartiallyFulledTransactionsSizes(t *testing.T) { + var uTx *txs.AddValidatorTx + uTxSize, err := txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 2) + + uTx = &txs.AddValidatorTx{} + uTxSize, err = txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 102) + + // array of nil elements has size 0. + creds := make([]verify.Verifiable, 10) + uTxSize, err = txs.Codec.Size(txs.CodecVersion, creds) + require.NoError(t, err) + require.Equal(t, uTxSize, 6) + + creds[0] = &secp256k1fx.Credential{ + Sigs: make([][secp256k1.SignatureLen]byte, 5), + } + uTxSize, err = txs.Codec.Size(txs.CodecVersion, creds) + require.NoError(t, err) + require.Equal(t, uTxSize, 339) + + var sTx *txs.Tx + uTxSize, err = txs.Codec.Size(txs.CodecVersion, sTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 2) + + sTx = &txs.Tx{} + uTxSize, err = txs.Codec.Size(txs.CodecVersion, sTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 6) + + sTx = &txs.Tx{ + Unsigned: uTx, + } + uTxSize, err = txs.Codec.Size(txs.CodecVersion, sTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 110) + + sTx = &txs.Tx{ + Unsigned: uTx, + Creds: creds, + } + uTxSize, err = txs.Codec.Size(txs.CodecVersion, sTx) + require.NoError(t, err) + require.Equal(t, uTxSize, 443) +} + +func TestAddAndRemoveFees(t *testing.T) { + r := require.New(t) + + fc := &Calculator{ + IsEForkActive: true, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + } + + var ( + units = fees.Dimensions{1, 2, 3, 4} + doubleUnits = fees.Dimensions{2, 4, 6, 8} + ) + + feeDelta, err := fc.AddFeesFor(units) + r.NoError(err) + r.Equal(units, fc.FeeManager.GetCumulatedUnits()) + r.NotZero(feeDelta) + r.Equal(feeDelta, fc.Fee) + + feeDelta2, err := fc.AddFeesFor(units) + r.NoError(err) + r.Equal(doubleUnits, fc.FeeManager.GetCumulatedUnits()) + r.Equal(feeDelta, feeDelta2) + r.Equal(feeDelta+feeDelta2, fc.Fee) + + feeDelta3, err := fc.RemoveFeesFor(units) + r.NoError(err) + r.Equal(units, fc.FeeManager.GetCumulatedUnits()) + r.Equal(feeDelta, feeDelta3) + r.Equal(feeDelta, fc.Fee) + + feeDelta4, err := fc.RemoveFeesFor(units) + r.NoError(err) + r.Zero(fc.FeeManager.GetCumulatedUnits()) + r.Equal(feeDelta, feeDelta4) + r.Zero(fc.Fee) +} + +func TestUTXOsAreAdditiveInSize(t *testing.T) { + // Show that including utxos of size [S] into a tx of size [T] + // result in a tx of size [S+T-CodecVersion] + // This is key to calculate fees correctly while building a tx + + uTx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: rand.Uint32(), //#nosec G404 + BlockchainID: ids.GenerateTestID(), + Memo: []byte{'a', 'b', 'c'}, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + }, + }, + } + + uTxNakedSize := 105 + uTxSize, err := txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxNakedSize, uTxSize) + + // input to add + input := &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: ids.GenerateTestID()}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + } + inSize, err := txs.Codec.Size(txs.CodecVersion, input) + require.NoError(t, err) + + // include input in uTx and check that sizes add + uTx.BaseTx.BaseTx.Ins = append(uTx.BaseTx.BaseTx.Ins, input) + uTxSize, err = txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxNakedSize+(inSize-codec.CodecVersionSize), uTxSize) + + // output to add + output := &avax.TransferableOutput{ + Asset: avax.Asset{ + ID: ids.GenerateTestID(), + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + } + outSize, err := txs.Codec.Size(txs.CodecVersion, output) + require.NoError(t, err) + + // include output in uTx and check that sizes add + uTx.BaseTx.BaseTx.Outs = append(uTx.BaseTx.BaseTx.Outs, output) + uTxSize, err = txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxNakedSize+(inSize-codec.CodecVersionSize)+(outSize-codec.CodecVersionSize), uTxSize) + + // include output in uTx as stake and check that sizes add + uTx.StakeOuts = append(uTx.StakeOuts, output) + uTxSize, err = txs.Codec.Size(txs.CodecVersion, uTx) + require.NoError(t, err) + require.Equal(t, uTxNakedSize+(inSize-codec.CodecVersionSize)+(outSize-codec.CodecVersionSize)+(outSize-codec.CodecVersionSize), uTxSize) +} + +func TestAddValidatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddValidatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + StakeOuts: stakes, + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.AddPrimaryNetworkValidatorFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3719*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 741, + 1090, + 266, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, bandwidth cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.Bandwidth] = 741 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestAddSubnetValidatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + subnetID := ids.GenerateTestID() + baseTx, _, subnetAuth := txsCreationHelpers(defaultCtx) + uTx := &txs.AddSubnetValidatorTx{ + BaseTx: baseTx, + SubnetValidator: txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + Subnet: subnetID, + }, + SubnetAuth: subnetAuth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.AddSubnetValidatorFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3345*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 649, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestAddDelegatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddDelegatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: defaultCtx.NodeID, + Start: uint64(time.Now().Truncate(time.Second).Unix()), + End: uint64(time.Now().Truncate(time.Second).Add(time.Hour).Unix()), + Wght: feeTestDefaultStakeWeight, + }, + StakeOuts: stakes, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.AddPrimaryNetworkDelegatorFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3715*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 737, + 1090, + 266, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestCreateChainTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, subnetAuth := txsCreationHelpers(defaultCtx) + uTx := &txs.CreateChainTx{ + BaseTx: baseTx, + SubnetID: ids.GenerateTestID(), + ChainName: "testingStuff", + VMID: ids.GenerateTestID(), + FxIDs: []ids.ID{ids.GenerateTestID()}, + GenesisData: []byte{0xff}, + SubnetAuth: subnetAuth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.CreateBlockchainTxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3388*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 692, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestCreateSubnetTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.CreateSubnetTx{ + BaseTx: baseTx, + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.CreateSubnetTxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3293*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 597, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestRemoveSubnetValidatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, auth := txsCreationHelpers(defaultCtx) + uTx := &txs.RemoveSubnetValidatorTx{ + BaseTx: baseTx, + NodeID: ids.GenerateTestNodeID(), + Subnet: ids.GenerateTestID(), + SubnetAuth: auth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3321*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 625, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestTransformSubnetTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, auth := txsCreationHelpers(defaultCtx) + uTx := &txs.TransformSubnetTx{ + BaseTx: baseTx, + Subnet: ids.GenerateTestID(), + AssetID: ids.GenerateTestID(), + InitialSupply: 0x1000000000000000, + MaximumSupply: 0x1000000000000000, + MinConsumptionRate: 0, + MaxConsumptionRate: 0, + MinValidatorStake: 1, + MaxValidatorStake: 0x1000000000000000, + MinStakeDuration: 1, + MaxStakeDuration: 1, + MinDelegationFee: 0, + MinDelegatorStake: 0xffffffffffffffff, + MaxValidatorWeightFactor: 255, + UptimeRequirement: 0, + SubnetAuth: auth, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TransformSubnetTxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3406*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 710, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestTransferSubnetOwnershipTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.TransferSubnetOwnershipTx{ + BaseTx: baseTx, + Subnet: ids.GenerateTestID(), + SubnetAuth: &secp256k1fx.Input{ + SigIndices: []uint32{3}, + }, + Owner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3337*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 641, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestAddPermissionlessValidatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + sk, err := bls.NewSecretKey() + r.NoError(err) + uTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: baseTx, + Subnet: ids.GenerateTestID(), + Signer: signer.NewProofOfPossession(sk), + StakeOuts: stakes, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DelegationShares: reward.PercentDenominator, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.AddSubnetValidatorFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3939*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 961, + 1090, + 266, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestAddPermissionlessDelegatorTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, stakes, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.AddPermissionlessDelegatorTx{ + BaseTx: baseTx, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 12345, + End: 12345 + 200*24*60*60, + Wght: 2 * units.KiloAvax, + }, + Subnet: ids.GenerateTestID(), + StakeOuts: stakes, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.AddSubnetDelegatorFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3747*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 769, + 1090, + 266, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestBaseTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &baseTx + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3253*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 557, + 1090, + 172, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestImportTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, _, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.ImportTx{ + BaseTx: baseTx, + SourceChain: ids.GenerateTestID(), + ImportedInputs: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(1), + OutputIndex: 1, + }, + Asset: avax.Asset{ID: ids.ID{'a', 's', 's', 'e', 'r', 't'}}, + In: &secp256k1fx.TransferInput{ + Amt: 50000, + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + }}, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 5827*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 681, + 2180, + 262, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func TestExportTxFees(t *testing.T) { + r := require.New(t) + + defaultCtx := snowtest.Context(t, snowtest.PChainID) + + baseTx, outputs, _ := txsCreationHelpers(defaultCtx) + uTx := &txs.ExportTx{ + BaseTx: baseTx, + DestinationChain: ids.GenerateTestID(), + ExportedOutputs: outputs, + } + sTx, err := txs.NewSigned(uTx, txs.Codec, feeTestSigners) + r.NoError(err) + + tests := []feeTests{ + { + description: "pre E fork", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(-1 * time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, fc.Config.TxFee, fc.Fee) + }, + }, + { + description: "post E fork, success", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + expectedError: nil, + checksF: func(t *testing.T, fc *Calculator) { + require.Equal(t, 3663*units.MicroAvax, fc.Fee) + require.Equal(t, + fees.Dimensions{ + 685, + 1090, + 266, + 0, + }, + fc.FeeManager.GetCumulatedUnits(), + ) + }, + }, + { + description: "post E fork, utxos read cap breached", + cfgAndChainTimeF: func() (*config.Config, time.Time) { + eForkTime := time.Now().Truncate(time.Second) + chainTime := eForkTime.Add(time.Second) + + cfg := feeTestsDefaultCfg + cfg.DurangoTime = durangoTime + cfg.EForkTime = eForkTime + + return &cfg, chainTime + }, + consumedUnitCapsF: func() fees.Dimensions { + caps := testBlockMaxConsumedUnits + caps[fees.UTXORead] = 1090 - 1 + return caps + }, + expectedError: errFailedConsumedUnitsCumulation, + checksF: func(t *testing.T, fc *Calculator) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cfg, chainTime := tt.cfgAndChainTimeF() + + consumedUnitCaps := testBlockMaxConsumedUnits + if tt.consumedUnitCapsF != nil { + consumedUnitCaps = tt.consumedUnitCapsF() + } + + fc := &Calculator{ + IsEForkActive: cfg.IsEForkActivated(chainTime), + Config: cfg, + ChainTime: chainTime, + FeeManager: fees.NewManager(testUnitFees), + ConsumedUnitsCap: consumedUnitCaps, + Credentials: sTx.Creds, + } + err := uTx.Visit(fc) + r.ErrorIs(err, tt.expectedError) + tt.checksF(t, fc) + }) + } +} + +func txsCreationHelpers(defaultCtx *snow.Context) ( + baseTx txs.BaseTx, + stakes []*avax.TransferableOutput, + auth *secp256k1fx.Input, +) { + inputs := []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + }} + outputs := []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }} + stakes = []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: defaultCtx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Second).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: feeTestDefaultStakeWeight, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }, + }} + auth = &secp256k1fx.Input{ + SigIndices: []uint32{0, 1}, + } + baseTx = txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: defaultCtx.NetworkID, + BlockchainID: defaultCtx.ChainID, + Ins: inputs, + Outs: outputs, + }, + } + + return baseTx, stakes, auth +} diff --git a/vms/platformvm/txs/tx.go b/vms/platformvm/txs/tx.go index 9874f66e0468..c33f67c8cba3 100644 --- a/vms/platformvm/txs/tx.go +++ b/vms/platformvm/txs/tx.go @@ -138,8 +138,10 @@ func (tx *Tx) Sign(c codec.Manager, signers [][]*secp256k1.PrivateKey) error { } // Attach credentials + tx.Creds = EmptyCredentials(signers) + hash := hashing.ComputeHash256(unsignedBytes) - for _, keys := range signers { + for i, keys := range signers { cred := &secp256k1fx.Credential{ Sigs: make([][secp256k1.SignatureLen]byte, len(keys)), } @@ -150,7 +152,7 @@ func (tx *Tx) Sign(c codec.Manager, signers [][]*secp256k1.PrivateKey) error { } copy(cred.Sigs[i][:], sig) } - tx.Creds = append(tx.Creds, cred) // Attach credential + tx.Creds[i] = cred // Attach credential } signedBytes, err := c.Marshal(CodecVersion, tx) @@ -160,3 +162,13 @@ func (tx *Tx) Sign(c codec.Manager, signers [][]*secp256k1.PrivateKey) error { tx.SetBytes(unsignedBytes, signedBytes) return nil } + +func EmptyCredentials(signers [][]*secp256k1.PrivateKey) []verify.Verifiable { + creds := make([]verify.Verifiable, 0, len(signers)) + for i := 0; i < len(signers); i++ { + creds = append(creds, &secp256k1fx.Credential{ + Sigs: make([][secp256k1.SignatureLen]byte, len(signers)), + }) + } + return creds +} diff --git a/vms/platformvm/utxo/handler.go b/vms/platformvm/utxo/handler.go index f22a76fd0b8f..c2179f921ad3 100644 --- a/vms/platformvm/utxo/handler.go +++ b/vms/platformvm/utxo/handler.go @@ -22,7 +22,10 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var ( @@ -69,6 +72,20 @@ type Spender interface { error, ) + FinanceTx( + utxoReader avax.UTXOReader, + keys []*secp256k1.PrivateKey, + amount uint64, + feeCalc *fees.Calculator, + changeAddr ids.ShortID, + ) ( + []*avax.TransferableInput, // inputs + []*avax.TransferableOutput, // returnedOutputs + []*avax.TransferableOutput, // stakedOutputs + [][]*secp256k1.PrivateKey, // signers + error, + ) + // Authorize an operation on behalf of the named subnet with the provided // keys. Authorize( @@ -390,6 +407,344 @@ func (h *handler) Spend( return ins, returnedOuts, stakedOuts, signers, nil } +func (h *handler) FinanceTx( + utxoReader avax.UTXOReader, + keys []*secp256k1.PrivateKey, + amount uint64, + feeCalc *fees.Calculator, + changeAddr ids.ShortID, +) ( + []*avax.TransferableInput, // inputs + []*avax.TransferableOutput, // returnedOutputs + []*avax.TransferableOutput, // stakedOutputs + [][]*secp256k1.PrivateKey, // signers + error, +) { + addrs := set.NewSet[ids.ShortID](len(keys)) // The addresses controlled by [keys] + for _, key := range keys { + addrs.Add(key.PublicKey().Address()) + } + utxos, err := avax.GetAllUTXOs(utxoReader, addrs) // The UTXOs controlled by [keys] + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("couldn't get UTXOs: %w", err) + } + + kc := secp256k1fx.NewKeychain(keys...) // Keychain consumes UTXOs and creates new ones + + // Minimum time this transaction will be issued at + now := uint64(h.clk.Time().Unix()) + + ins := []*avax.TransferableInput{} + returnedOuts := []*avax.TransferableOutput{} + stakedOuts := []*avax.TransferableOutput{} + signers := [][]*secp256k1.PrivateKey{} + + targetFee := feeCalc.Fee + + // Amount of AVAX that has been staked + amountStaked := uint64(0) + + // Consume locked UTXOs + for _, utxo := range utxos { + // If we have consumed more AVAX than we are trying to stake, then we + // have no need to consume more locked AVAX + if amountStaked >= amount { + break + } + + if assetID := utxo.AssetID(); assetID != h.ctx.AVAXAssetID { + continue // We only care about staking AVAX, so ignore other assets + } + + out, ok := utxo.Out.(*stakeable.LockOut) + if !ok { + // This output isn't locked, so it will be handled during the next + // iteration of the UTXO set + continue + } + if out.Locktime <= now { + // This output is no longer locked, so it will be handled during the + // next iteration of the UTXO set + continue + } + + inner, ok := out.TransferableOut.(*secp256k1fx.TransferOutput) + if !ok { + // We only know how to clone secp256k1 outputs for now + continue + } + + inIntf, inSigners, err := kc.Spend(out.TransferableOut, now) + if err != nil { + // We couldn't spend the output, so move on to the next one + continue + } + in, ok := inIntf.(avax.TransferableIn) + if !ok { // should never happen + h.ctx.Log.Warn("wrong input type", + zap.String("expectedType", "avax.TransferableIn"), + zap.String("actualType", fmt.Sprintf("%T", inIntf)), + ) + continue + } + input := &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + In: &stakeable.LockIn{ + Locktime: out.Locktime, + TransferableIn: in, + }, + } + + // The remaining value is initially the full value of the input + remainingValue := in.Amount() + + // update fees to target given the extra input added + insDimensions, err := commonfees.GetInputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableInput{input}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating input size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(insDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for input fees: %w", err) + } + targetFee += addedFees + + // Stake any value that should be staked + amountToStake := math.Min( + amount-amountStaked, // Amount we still need to stake + remainingValue, // Amount available to stake + ) + amountStaked += amountToStake + remainingValue -= amountToStake + + // Add the input to the consumed inputs + ins = append(ins, input) + + stakedOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: out.Locktime, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: amountToStake, + OutputOwners: inner.OutputOwners, + }, + }, + } + + // update fees to target given the staked output added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{stakedOut}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating stakedOut size: %w", err) + } + addedFees, err = feeCalc.AddFeesFor(outDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for stakedOut fees: %w", err) + } + targetFee += addedFees + + // Add the output to the staked outputs + stakedOuts = append(stakedOuts, stakedOut) + + if remainingValue > 0 { + // This input provided more value than was needed to be locked. + // Some of it must be returned + changeOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: out.Locktime, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: remainingValue, + OutputOwners: inner.OutputOwners, + }, + }, + } + + // update fees to target given the change output added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{changeOut}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating changeOut size: %w", err) + } + addedFees, err = feeCalc.AddFeesFor(outDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for stakedOut fees: %w", err) + } + targetFee += addedFees + + returnedOuts = append(returnedOuts, changeOut) + } + + // Add the signers needed for this input to the set of signers + signers = append(signers, inSigners) + } + + // Amount of AVAX that has been burned + amountBurned := uint64(0) + + for _, utxo := range utxos { + // If we have consumed more AVAX than we are trying to stake, + // and we have burned more AVAX than we need to, + // then we have no need to consume more AVAX + if amountBurned >= targetFee && amountStaked >= amount { + break + } + + if assetID := utxo.AssetID(); assetID != h.ctx.AVAXAssetID { + continue // We only care about burning AVAX, so ignore other assets + } + + out := utxo.Out + inner, ok := out.(*stakeable.LockOut) + if ok { + if inner.Locktime > now { + // This output is currently locked, so this output can't be + // burned. Additionally, it may have already been consumed + // above. Regardless, we skip to the next UTXO + continue + } + out = inner.TransferableOut + } + + inIntf, inSigners, err := kc.Spend(out, now) + if err != nil { + // We couldn't spend this UTXO, so we skip to the next one + continue + } + in, ok := inIntf.(avax.TransferableIn) + if !ok { + // Because we only use the secp Fx right now, this should never + // happen + continue + } + input := &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + In: in, + } + + // The remaining value is initially the full value of the input + remainingValue := in.Amount() + + // update fees to target given the extra input added + insDimensions, err := commonfees.GetInputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableInput{input}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating input size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(insDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for input fees: %w", err) + } + targetFee += addedFees + + // Burn any value that should be burned + amountToBurn := math.Min( + targetFee-amountBurned, // Amount we still need to burn + remainingValue, // Amount available to burn + ) + amountBurned += amountToBurn + remainingValue -= amountToBurn + + // Stake any value that should be staked + amountToStake := math.Min( + amount-amountStaked, // Amount we still need to stake + remainingValue, // Amount available to stake + ) + amountStaked += amountToStake + remainingValue -= amountToStake + + // Add the input to the consumed inputs + ins = append(ins, input) + + if amountToStake > 0 { + // Some of this input was put for staking + stakedOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amountToStake, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, + }, + } + + // update fees to target given the extra input added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{stakedOut}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating output size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(outDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + targetFee += addedFees + + amountToBurn := math.Min( + targetFee-amountBurned, // Amount we still need to burn + remainingValue, // Amount available to burn + ) + amountBurned += amountToBurn + remainingValue -= amountToBurn + + stakedOuts = append(stakedOuts, stakedOut) + } + + if remainingValue > 0 { + changeOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: h.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + // Amt: remainingValue, // SET IT AFTER CONSIDERING ITS OWN FEES + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + }, + }, + } + + // update fees to target given the extra input added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{changeOut}) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed calculating output size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(outDimensions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + + if remainingValue > addedFees { + targetFee += addedFees + amountBurned += addedFees + remainingValue -= addedFees + + changeOut.Out.(*secp256k1fx.TransferOutput).Amt = remainingValue + // This input had extra value, so some of it must be returned + returnedOuts = append(returnedOuts, changeOut) + } + + // If this UTXO has not enough value to cover for its own taxes, + // we fully consume it (no output) and move to the next UTXO to pay for it. + } + + // Add the signers needed for this input to the set of signers + signers = append(signers, inSigners) + } + + if amountBurned < targetFee || amountStaked < amount { + return nil, nil, nil, nil, fmt.Errorf( + "%w (unlocked, locked) (%d, %d) but need (%d, %d)", + ErrInsufficientFunds, amountBurned, amountStaked, targetFee, amount, + ) + } + + avax.SortTransferableInputsWithSigners(ins, signers) // sort inputs and keys + avax.SortTransferableOutputs(returnedOuts, txs.Codec) // sort outputs + avax.SortTransferableOutputs(stakedOuts, txs.Codec) // sort outputs + + return ins, returnedOuts, stakedOuts, signers, nil +} + func (h *handler) Authorize( state state.Chain, subnetID ids.ID, diff --git a/vms/platformvm/utxo/handler_test.go b/vms/platformvm/utxo/handler_test.go index d0224ed4666a..cc9a30cb5b8c 100644 --- a/vms/platformvm/utxo/handler_test.go +++ b/vms/platformvm/utxo/handler_test.go @@ -9,18 +9,24 @@ import ( "time" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/snowtest" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" "github.com/ava-labs/avalanchego/vms/secp256k1fx" safemath "github.com/ava-labs/avalanchego/utils/math" + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" ) var _ txs.UnsignedTx = (*dummyUnsignedTx)(nil) @@ -33,6 +39,680 @@ func (*dummyUnsignedTx) Visit(txs.Visitor) error { return nil } +func TestVerifyFinanceTx(t *testing.T) { + fx := &secp256k1fx.Fx{} + require.NoError(t, fx.InitializeVM(&secp256k1fx.TestVM{})) + require.NoError(t, fx.Bootstrapped()) + + ctx := snowtest.Context(t, snowtest.PChainID) + keys := secp256k1.TestKeys() + + h := &handler{ + ctx: ctx, + clk: &mockable.Clock{}, + fx: fx, + } + + testUnitFees := commonfees.Dimensions{ + 1 * units.MicroAvax, + 2 * units.MicroAvax, + 3 * units.MicroAvax, + 4 * units.MicroAvax, + } + testBlockMaxConsumedUnits := commonfees.Dimensions{ + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + } + + var ( + amountToStake = units.MilliAvax + + bigUtxoTxID = ids.ID{0x0, 0x1} + bigUtxoKey = keys[0] + bigUtxoAddr = bigUtxoKey.PublicKey().Address() + bigUtxoAmount = 10 * units.MilliAvax + bigUtxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: bigUtxoTxID, + OutputIndex: 0, + }, + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: bigUtxoAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{bigUtxoAddr}, + Threshold: 1, + }, + }, + } + bigUtxoID = bigUtxo.InputID() + + smallUtxoTxID = ids.ID{0x0, 0x2} + smallUtxoKey = keys[1] + smallUtxoAddr = smallUtxoKey.PublicKey().Address() + smallUtxoAmount = 2 * units.MilliAvax + smallUtxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: smallUtxoTxID, + OutputIndex: 0, + }, + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: smallUtxoAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{smallUtxoAddr}, + Threshold: 1, + }, + }, + } + smallUtxoID = smallUtxo.InputID() + + lockedUtxoTxID = ids.ID{'c'} + lockedUtxoKey = keys[2] + lockedUtxoAddr = lockedUtxoKey.PublicKey().Address() + lockedUtxoAmount = amountToStake + + lockedUtxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: lockedUtxoTxID, + OutputIndex: 0, + }, + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Hour).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: lockedUtxoAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{lockedUtxoAddr}, + Threshold: 1, + }, + }, + }, + } + lockedUtxoID = lockedUtxo.InputID() + + bigLockedUtxoTxID = ids.ID{'d'} + bigLockedUtxoKey = keys[2] + bigLockedUtxoAddr = bigLockedUtxoKey.PublicKey().Address() + bigLockedUtxoAmount = amountToStake * 10 + + bigLockedUtxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: bigLockedUtxoTxID, + OutputIndex: 0, + }, + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Hour).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: bigLockedUtxoAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{bigLockedUtxoAddr}, + Threshold: 1, + }, + }, + }, + } + bigLockedUtxoID = bigLockedUtxo.InputID() + ) + + // this UTXOs ordering ensures that smallUtxo will be picked first, + // even if bigUtxo would be enough finance the whole tx + require.True(t, smallUtxoID.Compare(bigUtxoID) < 0) + + tests := []struct { + description string + utxoReaderF func(ctrl *gomock.Controller) avax.UTXOReader + + // keysF simplifies the utxoReade mock setup. We just specify here + // the only keys referenced by the test scenario + keysF func() []*secp256k1.PrivateKey + amountToStake uint64 + uTxF func(t *testing.T) txs.UnsignedTx + + expectedErr error + checksF func(*testing.T, txs.UnsignedTx, []*avax.TransferableInput, []*avax.TransferableOutput, []*avax.TransferableOutput) + }{ + { + description: "Tx, stake outputs, single locked UTXO and multiple UTXOs", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(smallUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{smallUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(smallUtxoID).Return(smallUtxo, nil).AnyTimes() + + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + + s.EXPECT().UTXOIDs(bigLockedUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigLockedUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigLockedUtxoID).Return(bigLockedUtxo, nil).AnyTimes() + + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{smallUtxoKey, bigUtxoKey, bigLockedUtxoKey} + }, + + amountToStake: units.MilliAvax, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 0, + End: uint64(time.Now().Unix()), + Wght: amountToStake, + }, + StakeOuts: make([]*avax.TransferableOutput, 0), + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 8911 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.AddValidatorTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + addVal.StakeOuts = staked + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 3) + r.Len(staked, 1) + r.Len(outs, 2) + + r.Equal(amountToStake, staked[0].Out.Amount()) + r.Equal(amountToStake, ins[0].In.Amount()-outs[1].Out.Amount()) + r.Equal(expectedFee, ins[1].In.Amount()+ins[2].In.Amount()-outs[0].Out.Amount()) + }, + }, + { + description: "Tx, stake outputs, single locked and unlocked UTXOs", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(lockedUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{lockedUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(lockedUtxoID).Return(lockedUtxo, nil).AnyTimes() + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{lockedUtxoKey, bigUtxoKey} + }, + + amountToStake: units.MilliAvax, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 0, + End: uint64(time.Now().Unix()), + Wght: amountToStake, + }, + StakeOuts: make([]*avax.TransferableOutput, 0), + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 5999 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.AddValidatorTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + addVal.StakeOuts = staked + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 2) + r.Len(staked, 1) + r.Len(outs, 1) + + r.Equal(amountToStake, staked[0].Out.Amount()) + r.Equal(amountToStake, ins[1].In.Amount()) + r.Equal(expectedFee, ins[0].In.Amount()-outs[0].Out.Amount()) + }, + }, + { + description: "Tx, stake outputs, multiple UTXOs", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(smallUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{smallUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(smallUtxoID).Return(smallUtxo, nil).AnyTimes() + + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{smallUtxoKey, bigUtxoKey} + }, + + amountToStake: units.MilliAvax, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 0, + End: uint64(time.Now().Unix()), + Wght: amountToStake, + }, + StakeOuts: make([]*avax.TransferableOutput, 0), + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 5879 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.AddValidatorTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + addVal.StakeOuts = staked + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 2) + r.Len(staked, 1) + r.Len(outs, 1) + + r.Equal(amountToStake, staked[0].Out.Amount()) + r.Equal(expectedFee, ins[0].In.Amount()+ins[1].In.Amount()-amountToStake-outs[0].Out.Amount()) + }, + }, + { + description: "Tx, stake outputs, single UTXO", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{bigUtxoKey} + }, + + amountToStake: units.MilliAvax, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: 0, + End: uint64(time.Now().Unix()), + Wght: amountToStake, + }, + StakeOuts: make([]*avax.TransferableOutput, 0), + RewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + DelegationShares: reward.PercentDenominator, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 3341 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.AddValidatorTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + addVal.StakeOuts = staked + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 1) + r.Len(staked, 1) + r.Len(outs, 1) + + r.Equal(amountToStake, staked[0].Out.Amount()) + r.Equal(expectedFee, ins[0].In.Amount()-amountToStake-outs[0].Out.Amount()) + }, + }, + { + description: "Tx, no stake outputs, single UTXO", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{bigUtxoKey} + }, + + amountToStake: 0, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.CreateChainTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + SubnetID: ids.GenerateTestID(), + ChainName: "testChain", + VMID: ids.GenerateTestID(), + SubnetAuth: &secp256k1fx.Input{}, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 3014 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.CreateChainTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 1) + r.Len(outs, 1) + r.Equal(expectedFee, ins[0].In.Amount()-outs[0].Out.Amount()) + r.Empty(staked) + }, + }, + { + description: "Tx, no stake outputs, multiple UTXOs", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(smallUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{smallUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(smallUtxoID).Return(smallUtxo, nil).AnyTimes() + + s.EXPECT().UTXOIDs(bigUtxoAddr.Bytes(), gomock.Any(), gomock.Any()).Return([]ids.ID{bigUtxoID}, nil).AnyTimes() + s.EXPECT().GetUTXO(bigUtxoID).Return(bigUtxo, nil).AnyTimes() + + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{smallUtxoKey, bigUtxoKey} + }, + + amountToStake: 0, + uTxF: func(t *testing.T) txs.UnsignedTx { + uTx := &txs.CreateChainTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + Ins: make([]*avax.TransferableInput, 0), + Outs: make([]*avax.TransferableOutput, 0), + Memo: []byte{'a', 'b', 'c'}, + }, + }, + SubnetID: ids.GenerateTestID(), + ChainName: "testChain", + VMID: ids.GenerateTestID(), + SubnetAuth: &secp256k1fx.Input{}, + } + + bytes, err := txs.Codec.Marshal(txs.CodecVersion, uTx) + require.NoError(t, err) + + uTx.SetBytes(bytes) + return uTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + expectedFee := 5552 * units.MicroAvax + + // complete uTx with the utxos + addVal, ok := uTx.(*txs.CreateChainTx) + r.True(ok) + + addVal.Ins = ins + addVal.Outs = outs + + r.NoError(uTx.Visit(calc)) + r.Equal(expectedFee, calc.Fee) + + r.Len(ins, 2) + r.Len(outs, 1) + r.Equal(expectedFee, ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) + r.Empty(staked) + }, + }, + { + description: "no inputs, no outputs, no fee", + utxoReaderF: func(ctrl *gomock.Controller) avax.UTXOReader { + s := state.NewMockState(ctrl) + s.EXPECT().UTXOIDs(gomock.Any(), gomock.Any(), gomock.Any()).Return([]ids.ID{}, nil).AnyTimes() + return s + }, + keysF: func() []*secp256k1.PrivateKey { + return []*secp256k1.PrivateKey{} + }, + amountToStake: 0, + uTxF: func(t *testing.T) txs.UnsignedTx { + unsignedTx := dummyUnsignedTx{ + BaseTx: txs.BaseTx{}, + } + unsignedTx.SetBytes([]byte{0}) + return &unsignedTx + }, + expectedErr: nil, + checksF: func(t *testing.T, uTx txs.UnsignedTx, ins []*avax.TransferableInput, outs, staked []*avax.TransferableOutput) { + r := require.New(t) + + fm := commonfees.NewManager(testUnitFees) + calc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + r.NoError(uTx.Visit(calc)) + r.Zero(calc.Fee) + + r.Empty(ins) + r.Empty(outs) + r.Empty(staked) + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + r := require.New(t) + ctrl := gomock.NewController(t) + + uTx := test.uTxF(t) + + fm := commonfees.NewManager(testUnitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: fm, + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: []verify.Verifiable{}, + } + + // init fee calc with the uTx data + r.NoError(uTx.Visit(feeCalc)) + + ins, outs, staked, _, err := h.FinanceTx( + test.utxoReaderF(ctrl), + test.keysF(), + test.amountToStake, + feeCalc, + ids.GenerateTestShortID(), + ) + r.ErrorIs(err, test.expectedErr) + test.checksF(t, uTx, ins, outs, staked) + }) + } +} + func TestVerifySpendUTXOs(t *testing.T) { fx := &secp256k1fx.Fx{} diff --git a/vms/platformvm/validator_set_property_test.go b/vms/platformvm/validator_set_property_test.go index c2894e594f7e..ca161f744011 100644 --- a/vms/platformvm/validator_set_property_test.go +++ b/vms/platformvm/validator_set_property_test.go @@ -730,6 +730,7 @@ func buildVM(t *testing.T) (*VM, ids.ID, error) { ApricotPhase5Time: forkTime, BanffTime: forkTime, CortinaTime: forkTime, + DurangoTime: forkTime, EForkTime: mockable.MaxTime, }} vm.clock.Set(forkTime.Add(time.Second)) diff --git a/wallet/chain/p/builder_dynamic_fees.go b/wallet/chain/p/builder_dynamic_fees.go new file mode 100644 index 000000000000..2a0e9c9469cb --- /dev/null +++ b/wallet/chain/p/builder_dynamic_fees.go @@ -0,0 +1,1210 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "fmt" + "time" + + "golang.org/x/exp/slices" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" +) + +type DynamicFeesBuilder struct { + addrs set.Set[ids.ShortID] + backend BuilderBackend +} + +func NewDynamicFeesBuilder(addrs set.Set[ids.ShortID], backend BuilderBackend) *DynamicFeesBuilder { + return &DynamicFeesBuilder{ + addrs: addrs, + backend: backend, + } +} + +func (b *DynamicFeesBuilder) NewBaseTx( + outputs []*avax.TransferableOutput, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.BaseTx, error) { + // 1. Build core transaction without utxos + ops := common.NewOptions(options) + + utx := &txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + Outs: outputs, // not sorted yet, we'll sort later on when we have all the outputs + }, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + for _, out := range outputs { + assetID := out.AssetID() + amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount()) + if err != nil { + return nil, err + } + toBurn[assetID] = amountToBurn + } + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.BaseTx(utx); err != nil { + return nil, err + } + + inputs, changeOuts, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + outputs = append(outputs, changeOuts...) + avax.SortTransferableOutputs(outputs, txs.Codec) + utx.Ins = inputs + utx.Outs = outputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewAddValidatorTx( + vdr *txs.Validator, + rewardsOwner *secp256k1fx.OutputOwners, + shares uint32, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.AddValidatorTx, error) { + ops := common.NewOptions(options) + utils.Sort(rewardsOwner.Addrs) + utx := &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Validator: *vdr, + RewardsOwner: rewardsOwner, + DelegationShares: shares, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{ + b.backend.AVAXAssetID(): vdr.Wght, + } + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.AddValidatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, stakeOutputs, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + utx.StakeOuts = stakeOutputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewAddSubnetValidatorTx( + vdr *txs.SubnetValidator, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.AddSubnetValidatorTx, error) { + ops := common.NewOptions(options) + + subnetAuth, err := b.authorizeSubnet(vdr.Subnet, ops) + if err != nil { + return nil, err + } + + utx := &txs.AddSubnetValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + SubnetValidator: *vdr, + SubnetAuth: subnetAuth, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // update fees to account for the auth credentials to be added upon tx signing + if _, err = financeCredential(feeCalc, subnetAuth.SigIndices); err != nil { + return nil, fmt.Errorf("account for credential fees: %w", err) + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.AddSubnetValidatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewRemoveSubnetValidatorTx( + nodeID ids.NodeID, + subnetID ids.ID, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.RemoveSubnetValidatorTx, error) { + ops := common.NewOptions(options) + + subnetAuth, err := b.authorizeSubnet(subnetID, ops) + if err != nil { + return nil, err + } + + utx := &txs.RemoveSubnetValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Subnet: subnetID, + NodeID: nodeID, + SubnetAuth: subnetAuth, + } + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // update fees to account for the auth credentials to be added upon tx signing + if _, err = financeCredential(feeCalc, subnetAuth.SigIndices); err != nil { + return nil, fmt.Errorf("account for credential fees: %w", err) + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.RemoveSubnetValidatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewAddDelegatorTx( + vdr *txs.Validator, + rewardsOwner *secp256k1fx.OutputOwners, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.AddDelegatorTx, error) { + ops := common.NewOptions(options) + utils.Sort(rewardsOwner.Addrs) + utx := &txs.AddDelegatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Validator: *vdr, + DelegationRewardsOwner: rewardsOwner, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{ + b.backend.AVAXAssetID(): vdr.Wght, + } + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.AddDelegatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, stakeOutputs, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + utx.StakeOuts = stakeOutputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewCreateChainTx( + subnetID ids.ID, + genesis []byte, + vmID ids.ID, + fxIDs []ids.ID, + chainName string, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.CreateChainTx, error) { + // 1. Build core transaction without utxos + ops := common.NewOptions(options) + subnetAuth, err := b.authorizeSubnet(subnetID, ops) + if err != nil { + return nil, err + } + + utils.Sort(fxIDs) + + uTx := &txs.CreateChainTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + SubnetID: subnetID, + ChainName: chainName, + VMID: vmID, + FxIDs: fxIDs, + GenesisData: genesis, + SubnetAuth: subnetAuth, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // update fees to account for the auth credentials to be added upon tx signing + if _, err = financeCredential(feeCalc, subnetAuth.SigIndices); err != nil { + return nil, fmt.Errorf("account for credential fees: %w", err) + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err = feeCalc.CreateChainTx(uTx); err != nil { + return nil, err + } + + inputs, outputs, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + uTx.Ins = inputs + uTx.Outs = outputs + + return uTx, b.initCtx(uTx) +} + +func (b *DynamicFeesBuilder) NewCreateSubnetTx( + owner *secp256k1fx.OutputOwners, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.CreateSubnetTx, error) { + // 1. Build core transaction without utxos + ops := common.NewOptions(options) + + utx := &txs.CreateSubnetTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Owner: owner, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.CreateSubnetTx(utx); err != nil { + return nil, err + } + + inputs, outputs, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewImportTx( + sourceChainID ids.ID, + to *secp256k1fx.OutputOwners, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.ImportTx, error) { + ops := common.NewOptions(options) + // 1. Build core transaction + utx := &txs.ImportTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + SourceChain: sourceChainID, + } + + // 2. Add imported inputs first + utxos, err := b.backend.UTXOs(ops.Context(), sourceChainID) + if err != nil { + return nil, err + } + + var ( + addrs = ops.Addresses(b.addrs) + minIssuanceTime = ops.MinIssuanceTime() + avaxAssetID = b.backend.AVAXAssetID() + + importedInputs = make([]*avax.TransferableInput, 0, len(utxos)) + importedSigIndices = make([][]uint32, 0) + importedAmounts = make(map[ids.ID]uint64) + ) + + for _, utxo := range utxos { + out, ok := utxo.Out.(*secp256k1fx.TransferOutput) + if !ok { + continue + } + + inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime) + if !ok { + // We couldn't spend this UTXO, so we skip to the next one + continue + } + + importedInputs = append(importedInputs, &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: &secp256k1fx.TransferInput{ + Amt: out.Amt, + Input: secp256k1fx.Input{ + SigIndices: inputSigIndices, + }, + }, + }) + + assetID := utxo.AssetID() + newImportedAmount, err := math.Add64(importedAmounts[assetID], out.Amt) + if err != nil { + return nil, err + } + importedAmounts[assetID] = newImportedAmount + importedSigIndices = append(importedSigIndices, inputSigIndices) + } + if len(importedInputs) == 0 { + return nil, fmt.Errorf( + "%w: no UTXOs available to import", + errInsufficientFunds, + ) + } + + utils.Sort(importedInputs) // sort imported inputs + utx.ImportedInputs = importedInputs + + // 3. Add an output for all non-avax denominated inputs. + for assetID, amount := range importedAmounts { + if assetID == avaxAssetID { + // Avax-denominated inputs may be used to fully or partially pay fees, + // so we'll handle them later on. + continue + } + + utx.Outs = append(utx.Outs, &avax.TransferableOutput{ + Asset: avax.Asset{ID: assetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: *to, + }, + }) // we'll sort them later on + } + + // 3. Finance fees as much as possible with imported, Avax-denominated UTXOs + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.ImportTx(utx); err != nil { + return nil, err + } + + for _, sigIndices := range importedSigIndices { + if _, err = financeCredential(feeCalc, sigIndices); err != nil { + return nil, fmt.Errorf("account for credential fees: %w", err) + } + } + + switch importedAVAX := importedAmounts[avaxAssetID]; { + case importedAVAX == feeCalc.Fee: + // imported inputs match exactly the fees to be paid + avax.SortTransferableOutputs(utx.Outs, txs.Codec) // sort imported outputs + return utx, b.initCtx(utx) + + case importedAVAX < feeCalc.Fee: + // imported inputs can partially pay fees + feeCalc.Fee -= importedAmounts[avaxAssetID] + + default: + // imported inputs may be enough to pay taxes by themselves + changeOut := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + OutputOwners: *to, // we set amount after considering own fees + }, + } + + // update fees to target given the extra output added + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{changeOut}) + if err != nil { + return nil, fmt.Errorf("failed calculating output size: %w", err) + } + if _, err := feeCalc.AddFeesFor(outDimensions); err != nil { + return nil, fmt.Errorf("account for output fees: %w", err) + } + + switch { + case feeCalc.Fee < importedAVAX: + changeOut.Out.(*secp256k1fx.TransferOutput).Amt = importedAVAX - feeCalc.Fee + utx.Outs = append(utx.Outs, changeOut) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) // sort imported outputs + return utx, b.initCtx(utx) + + case feeCalc.Fee == importedAVAX: + // imported fees pays exactly the tx cost. We don't include the outputs + avax.SortTransferableOutputs(utx.Outs, txs.Codec) // sort imported outputs + return utx, b.initCtx(utx) + + default: + // 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 + } + } + + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} + inputs, changeOuts, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = append(utx.Outs, changeOuts...) + avax.SortTransferableOutputs(utx.Outs, txs.Codec) // sort imported outputs + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewExportTx( + chainID ids.ID, + outputs []*avax.TransferableOutput, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.ExportTx, error) { + // 1. Build core transaction without utxos + ops := common.NewOptions(options) + avax.SortTransferableOutputs(outputs, txs.Codec) // sort exported outputs + + utx := &txs.ExportTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + DestinationChain: chainID, + ExportedOutputs: outputs, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + for _, out := range outputs { + assetID := out.AssetID() + amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount()) + if err != nil { + return nil, err + } + toBurn[assetID] = amountToBurn + } + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.ExportTx(utx); err != nil { + return nil, err + } + + inputs, changeOuts, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = changeOuts + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewTransformSubnetTx( + subnetID ids.ID, + assetID ids.ID, + initialSupply uint64, + maxSupply uint64, + minConsumptionRate uint64, + maxConsumptionRate uint64, + minValidatorStake uint64, + maxValidatorStake uint64, + minStakeDuration time.Duration, + maxStakeDuration time.Duration, + minDelegationFee uint32, + minDelegatorStake uint64, + maxValidatorWeightFactor byte, + uptimeRequirement uint32, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.TransformSubnetTx, error) { + // 1. Build core transaction without utxos + ops := common.NewOptions(options) + + subnetAuth, err := b.authorizeSubnet(subnetID, ops) + if err != nil { + return nil, err + } + + utx := &txs.TransformSubnetTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Subnet: subnetID, + AssetID: assetID, + InitialSupply: initialSupply, + MaximumSupply: maxSupply, + MinConsumptionRate: minConsumptionRate, + MaxConsumptionRate: maxConsumptionRate, + MinValidatorStake: minValidatorStake, + MaxValidatorStake: maxValidatorStake, + MinStakeDuration: uint32(minStakeDuration / time.Second), + MaxStakeDuration: uint32(maxStakeDuration / time.Second), + MinDelegationFee: minDelegationFee, + MinDelegatorStake: minDelegatorStake, + MaxValidatorWeightFactor: maxValidatorWeightFactor, + UptimeRequirement: uptimeRequirement, + SubnetAuth: subnetAuth, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{} + toBurn := map[ids.ID]uint64{ + assetID: maxSupply - initialSupply, + } // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // update fees to account for the auth credentials to be added upon tx signing + if _, err = financeCredential(feeCalc, subnetAuth.SigIndices); err != nil { + return nil, fmt.Errorf("account for credential fees: %w", err) + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.TransformSubnetTx(utx); err != nil { + return nil, err + } + + inputs, outputs, _, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewAddPermissionlessValidatorTx( + vdr *txs.SubnetValidator, + signer signer.Signer, + assetID ids.ID, + validationRewardsOwner *secp256k1fx.OutputOwners, + delegationRewardsOwner *secp256k1fx.OutputOwners, + shares uint32, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.AddPermissionlessValidatorTx, error) { + ops := common.NewOptions(options) + utils.Sort(validationRewardsOwner.Addrs) + utils.Sort(delegationRewardsOwner.Addrs) + + utx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Validator: vdr.Validator, + Subnet: vdr.Subnet, + Signer: signer, + ValidatorRewardsOwner: validationRewardsOwner, + DelegatorRewardsOwner: delegationRewardsOwner, + DelegationShares: shares, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{ + assetID: vdr.Wght, + } + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.AddPermissionlessValidatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, stakeOutputs, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + utx.StakeOuts = stakeOutputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) NewAddPermissionlessDelegatorTx( + vdr *txs.SubnetValidator, + assetID ids.ID, + rewardsOwner *secp256k1fx.OutputOwners, + unitFees, unitCaps commonfees.Dimensions, + options ...common.Option, +) (*txs.AddPermissionlessDelegatorTx, error) { + ops := common.NewOptions(options) + utils.Sort(rewardsOwner.Addrs) + + utx := &txs.AddPermissionlessDelegatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Memo: ops.Memo(), + }}, + Validator: vdr.Validator, + Subnet: vdr.Subnet, + DelegationRewardsOwner: rewardsOwner, + } + + // 2. Finance the tx by building the utxos (inputs, outputs and stakes) + toStake := map[ids.ID]uint64{ + assetID: vdr.Wght, + } + toBurn := map[ids.ID]uint64{} // fees are calculated in financeTx + + feesMan := commonfees.NewManager(unitFees) + feeCalc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: feesMan, + ConsumedUnitsCap: unitCaps, + } + + // feesMan cumulates consumed units. Let's init it with utx filled so far + if err := feeCalc.AddPermissionlessDelegatorTx(utx); err != nil { + return nil, err + } + + inputs, outputs, stakeOutputs, err := b.financeTx(toBurn, toStake, feeCalc, ops) + if err != nil { + return nil, err + } + + utx.Ins = inputs + utx.Outs = outputs + utx.StakeOuts = stakeOutputs + + return utx, b.initCtx(utx) +} + +func (b *DynamicFeesBuilder) financeTx( + amountsToBurn map[ids.ID]uint64, + amountsToStake map[ids.ID]uint64, + feeCalc *fees.Calculator, + options *common.Options, +) ( + inputs []*avax.TransferableInput, + changeOutputs []*avax.TransferableOutput, + stakeOutputs []*avax.TransferableOutput, + err error, +) { + avaxAssetID := b.backend.AVAXAssetID() + utxos, err := b.backend.UTXOs(options.Context(), constants.PlatformChainID) + if err != nil { + return nil, nil, nil, err + } + + // we can only pay fees in avax, so we sort avax-denominated UTXOs last + // to maximize probability of being able to pay fees. + slices.SortFunc(utxos, func(lhs, rhs *avax.UTXO) int { + switch { + case lhs.Asset.AssetID() == avaxAssetID && rhs.Asset.AssetID() != avaxAssetID: + return 1 + case lhs.Asset.AssetID() != avaxAssetID && rhs.Asset.AssetID() == avaxAssetID: + return -1 + default: + return 0 + } + }) + + addrs := options.Addresses(b.addrs) + minIssuanceTime := options.MinIssuanceTime() + + addr, ok := addrs.Peek() + if !ok { + return nil, nil, nil, errNoChangeAddress + } + changeOwner := options.ChangeOwner(&secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + }) + + amountsToBurn[avaxAssetID] += feeCalc.Fee + + // Iterate over the locked UTXOs + for _, utxo := range utxos { + assetID := utxo.AssetID() + + // If we have staked enough of the asset, then we have no need burn + // more. + if amountsToStake[assetID] == 0 { + continue + } + + outIntf := utxo.Out + lockedOut, ok := outIntf.(*stakeable.LockOut) + if !ok { + // This output isn't locked, so it will be handled during the next + // iteration of the UTXO set + continue + } + if minIssuanceTime >= lockedOut.Locktime { + // This output isn't locked, so it will be handled during the next + // iteration of the UTXO set + continue + } + + out, ok := lockedOut.TransferableOut.(*secp256k1fx.TransferOutput) + if !ok { + return nil, nil, nil, errUnknownOutputType + } + + inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime) + if !ok { + // We couldn't spend this UTXO, so we skip to the next one + continue + } + + input := &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: &stakeable.LockIn{ + Locktime: lockedOut.Locktime, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: out.Amt, + Input: secp256k1fx.Input{ + SigIndices: inputSigIndices, + }, + }, + }, + } + + addedFees, err := financeInput(feeCalc, input) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for input fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + addedFees, err = financeCredential(feeCalc, inputSigIndices) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for input fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + inputs = append(inputs, input) + + // Stake any value that should be staked + amountToStake := math.Min( + amountsToStake[assetID], // Amount we still need to stake + out.Amt, // Amount available to stake + ) + + // Add the output to the staked outputs + stakeOut := &avax.TransferableOutput{ + Asset: utxo.Asset, + Out: &stakeable.LockOut{ + Locktime: lockedOut.Locktime, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: amountToStake, + OutputOwners: out.OutputOwners, + }, + }, + } + + addedFees, err = financeOutput(feeCalc, stakeOut) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + stakeOutputs = append(stakeOutputs, stakeOut) + + amountsToStake[assetID] -= amountToStake + if remainingAmount := out.Amt - amountToStake; remainingAmount > 0 { + // This input had extra value, so some of it must be returned + changeOut := &avax.TransferableOutput{ + Asset: utxo.Asset, + Out: &stakeable.LockOut{ + Locktime: lockedOut.Locktime, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: remainingAmount, + OutputOwners: out.OutputOwners, + }, + }, + } + + // update fees to account for the change output + addedFees, err = financeOutput(feeCalc, changeOut) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + changeOutputs = append(changeOutputs, changeOut) + } + } + + // Iterate over the unlocked UTXOs + for _, utxo := range utxos { + assetID := utxo.AssetID() + + // If we have consumed enough of the asset, then we have no need burn + // more. + if amountsToStake[assetID] == 0 && amountsToBurn[assetID] == 0 { + continue + } + + outIntf := utxo.Out + if lockedOut, ok := outIntf.(*stakeable.LockOut); ok { + if lockedOut.Locktime > minIssuanceTime { + // This output is currently locked, so this output can't be + // burned. + continue + } + outIntf = lockedOut.TransferableOut + } + + out, ok := outIntf.(*secp256k1fx.TransferOutput) + if !ok { + return nil, nil, nil, errUnknownOutputType + } + + inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime) + if !ok { + // We couldn't spend this UTXO, so we skip to the next one + continue + } + + input := &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: &secp256k1fx.TransferInput{ + Amt: out.Amt, + Input: secp256k1fx.Input{ + SigIndices: inputSigIndices, + }, + }, + } + + addedFees, err := financeInput(feeCalc, input) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for input fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + addedFees, err = financeCredential(feeCalc, inputSigIndices) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for credential fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + + inputs = append(inputs, input) + + // Burn any value that should be burned + amountToBurn := math.Min( + amountsToBurn[assetID], // Amount we still need to burn + out.Amt, // Amount available to burn + ) + amountsToBurn[assetID] -= amountToBurn + + amountAvalibleToStake := out.Amt - amountToBurn + // Burn any value that should be burned + amountToStake := math.Min( + amountsToStake[assetID], // Amount we still need to stake + amountAvalibleToStake, // Amount available to stake + ) + amountsToStake[assetID] -= amountToStake + if amountToStake > 0 { + // Some of this input was put for staking + stakeOut := &avax.TransferableOutput{ + Asset: utxo.Asset, + Out: &secp256k1fx.TransferOutput{ + Amt: amountToStake, + OutputOwners: *changeOwner, + }, + } + + stakeOutputs = append(stakeOutputs, stakeOut) + + addedFees, err = financeOutput(feeCalc, stakeOut) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + amountsToBurn[avaxAssetID] += addedFees + } + + if remainingAmount := amountAvalibleToStake - amountToStake; remainingAmount > 0 { + // This input had extra value, so some of it must be returned, once fees are removed + changeOut := &avax.TransferableOutput{ + Asset: utxo.Asset, + Out: &secp256k1fx.TransferOutput{ + OutputOwners: *changeOwner, + }, + } + + // update fees to account for the change output + addedFees, err = financeOutput(feeCalc, changeOut) + if err != nil { + return nil, nil, nil, fmt.Errorf("account for output fees: %w", err) + } + + if assetID != avaxAssetID { + changeOut.Out.(*secp256k1fx.TransferOutput).Amt = remainingAmount + amountsToBurn[avaxAssetID] += addedFees + changeOutputs = append(changeOutputs, changeOut) + } else { + // here assetID == b.backend.AVAXAssetID() + switch { + case addedFees < remainingAmount: + changeOut.Out.(*secp256k1fx.TransferOutput).Amt = remainingAmount - addedFees + changeOutputs = append(changeOutputs, changeOut) + case addedFees >= remainingAmount: + amountsToBurn[assetID] += addedFees - remainingAmount + } + } + } + } + + for assetID, amount := range amountsToStake { + if amount != 0 { + return nil, nil, nil, fmt.Errorf( + "%w: provided UTXOs need %d more units of asset %q to stake", + errInsufficientFunds, + amount, + assetID, + ) + } + } + for assetID, amount := range amountsToBurn { + if amount != 0 { + return nil, nil, nil, fmt.Errorf( + "%w: provided UTXOs need %d more units of asset %q", + errInsufficientFunds, + amount, + assetID, + ) + } + } + + utils.Sort(inputs) // sort inputs + avax.SortTransferableOutputs(changeOutputs, txs.Codec) // sort the change outputs + avax.SortTransferableOutputs(stakeOutputs, txs.Codec) // sort stake outputs + return inputs, changeOutputs, stakeOutputs, nil +} + +// TODO ABENEGIA: remove duplication with builder method +func (b *DynamicFeesBuilder) authorizeSubnet(subnetID ids.ID, options *common.Options) (*secp256k1fx.Input, error) { + subnetTx, err := b.backend.GetTx(options.Context(), subnetID) + if err != nil { + return nil, fmt.Errorf( + "failed to fetch subnet %q: %w", + subnetID, + err, + ) + } + subnet, ok := subnetTx.Unsigned.(*txs.CreateSubnetTx) + if !ok { + return nil, errWrongTxType + } + + owner, ok := subnet.Owner.(*secp256k1fx.OutputOwners) + if !ok { + return nil, errUnknownOwnerType + } + + addrs := options.Addresses(b.addrs) + minIssuanceTime := options.MinIssuanceTime() + inputSigIndices, ok := common.MatchOwners(owner, addrs, minIssuanceTime) + if !ok { + // We can't authorize the subnet + return nil, errInsufficientAuthorization + } + return &secp256k1fx.Input{ + SigIndices: inputSigIndices, + }, nil +} + +func (b *DynamicFeesBuilder) initCtx(tx txs.UnsignedTx) error { + ctx, err := newSnowContext(b.backend) + if err != nil { + return err + } + + tx.InitCtx(ctx) + return nil +} + +func financeInput(feeCalc *fees.Calculator, input *avax.TransferableInput) (uint64, error) { + insDimensions, err := commonfees.GetInputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableInput{input}) + if err != nil { + return 0, fmt.Errorf("failed calculating input size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(insDimensions) + if err != nil { + return 0, fmt.Errorf("account for input fees: %w", err) + } + return addedFees, nil +} + +func financeOutput(feeCalc *fees.Calculator, output *avax.TransferableOutput) (uint64, error) { + outDimensions, err := commonfees.GetOutputsDimensions(txs.Codec, txs.CodecVersion, []*avax.TransferableOutput{output}) + if err != nil { + return 0, fmt.Errorf("failed calculating changeOut size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(outDimensions) + if err != nil { + return 0, fmt.Errorf("account for stakedOut fees: %w", err) + } + return addedFees, nil +} + +func financeCredential(feeCalc *fees.Calculator, inputSigIndices []uint32) (uint64, error) { + credsDimensions, err := commonfees.GetCredentialsDimensions(txs.Codec, txs.CodecVersion, inputSigIndices) + if err != nil { + return 0, fmt.Errorf("failed calculating input size: %w", err) + } + addedFees, err := feeCalc.AddFeesFor(credsDimensions) + if err != nil { + return 0, fmt.Errorf("account for input fees: %w", err) + } + return addedFees, nil +} diff --git a/wallet/chain/p/builder_dynamic_fees_test.go b/wallet/chain/p/builder_dynamic_fees_test.go new file mode 100644 index 000000000000..b08f992f2b30 --- /dev/null +++ b/wallet/chain/p/builder_dynamic_fees_test.go @@ -0,0 +1,1044 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + stdcontext "context" + "math" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/fees" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p/mocks" + + commonfees "github.com/ava-labs/avalanchego/vms/components/fees" +) + +var ( + testKeys = secp256k1.TestKeys() + testUnitFees = commonfees.Dimensions{ + 1 * units.MicroAvax, + 2 * units.MicroAvax, + 3 * units.MicroAvax, + 4 * units.MicroAvax, + } + testBlockMaxConsumedUnits = commonfees.Dimensions{ + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + math.MaxUint64, + } +) + +// These tests create and sign a tx, then verify that utxos included +// in the tx are exactly necessary to pay fees for it + +func TestBaseTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + utxosKey = testKeys[1] + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + + outputsToMove = []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 7 * units.Avax, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + }, + }, + }} + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewBaseTx( + outputsToMove, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5930*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 2) + require.Equal(fc.Fee+outputsToMove[0].Out.Amount(), ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) + require.Equal(outputsToMove[0], outs[1]) +} + +func TestAddValidatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + rewardKey = testKeys[0] + utxosKey = testKeys[1] + rewardAddr = rewardKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + rewardOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + rewardKey.Address(), + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, rewardAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewAddValidatorTx( + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: uint64(time.Now().Add(time.Hour).Unix()), + Wght: 2 * units.Avax, + }, + rewardOwner, + reward.PercentDenominator, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(12184*units.MicroAvax, fc.Fee) + + ins := utx.Ins + staked := utx.StakeOuts + outs := utx.Outs + require.Len(ins, 4) + require.Len(staked, 2) + require.Len(outs, 2) + require.Equal(utx.Validator.Weight(), staked[0].Out.Amount()+staked[1].Out.Amount()) + require.Equal(fc.Fee, ins[1].In.Amount()+ins[3].In.Amount()-outs[0].Out.Amount()) +} + +func TestAddSubnetValidatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + subnetAuthKey = testKeys[0] + utxosKey = testKeys[1] + subnetAuthAddr = subnetAuthKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + subnetID = ids.GenerateTestID() + subnetTx = &txs.Tx{ + Unsigned: &txs.CreateSubnetTx{ + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{subnetAuthKey.PublicKey().Address()}, + }, + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, subnetAuthAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + be.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + utx, err := b.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: uint64(time.Now().Add(time.Hour).Unix()), + }, + Subnet: subnetID, + }, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + sbe.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5765*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 1) + require.Equal(fc.Fee, ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) +} + +func TestRemoveSubnetValidatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + subnetAuthKey = testKeys[0] + utxosKey = testKeys[1] + subnetAuthAddr = subnetAuthKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + subnetID = ids.GenerateTestID() + subnetTx = &txs.Tx{ + Unsigned: &txs.CreateSubnetTx{ + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{subnetAuthKey.PublicKey().Address()}, + }, + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, subnetAuthAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + be.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + utx, err := b.NewRemoveSubnetValidatorTx( + ids.GenerateTestNodeID(), + subnetID, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + sbe.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5741*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 1) + require.Equal(fc.Fee, ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) +} + +func TestAddDelegatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + rewardKey = testKeys[0] + utxosKey = testKeys[1] + rewardAddr = rewardKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + rewardOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + rewardKey.Address(), + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, rewardAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewAddDelegatorTx( + &txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: uint64(time.Now().Add(time.Hour).Unix()), + Wght: 2 * units.Avax, + }, + rewardOwner, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(12180*units.MicroAvax, fc.Fee) + + ins := utx.Ins + staked := utx.StakeOuts + outs := utx.Outs + require.Len(ins, 4) + require.Len(staked, 2) + require.Len(outs, 2) + require.Equal(utx.Validator.Weight(), staked[0].Out.Amount()+staked[1].Out.Amount()) + require.Equal(fc.Fee, ins[1].In.Amount()+ins[3].In.Amount()-outs[0].Out.Amount()) +} + +func TestCreateChainTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + subnetAuthKey = testKeys[0] + utxosKey = testKeys[1] + subnetAuthAddr = subnetAuthKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + subnetID = ids.GenerateTestID() + genesisBytes = []byte{'a', 'b', 'c'} + vmID = ids.GenerateTestID() + fxIDs = []ids.ID{ids.GenerateTestID()} + chainName = "dummyChain" + subnetTx = &txs.Tx{ + Unsigned: &txs.CreateSubnetTx{ + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{subnetAuthKey.PublicKey().Address()}, + }, + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, subnetAuthAddr), + backend: be, + } + + be.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + utxos, avaxAssetID, _ := testUTXOsList(utxosKey) + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewCreateChainTx( + subnetID, + genesisBytes, + vmID, + fxIDs, + chainName, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + sbe.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5808*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 1) + require.Equal(fc.Fee, ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) +} + +func TestCreateSubnetTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + subnetAuthKey = testKeys[0] + utxosKey = testKeys[1] + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + subnetOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + subnetAuthKey.Address(), + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewCreateSubnetTx( + subnetOwner, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5644*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 1) + require.Equal(fc.Fee, ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) +} + +func TestImportTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + utxosKey = testKeys[1] + utxoAddr = utxosKey.PublicKey().Address() + sourceChainID = ids.GenerateTestID() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + + importKey = testKeys[0] + importTo = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + importKey.Address(), + }, + } + ) + + importedUtxo := utxos[0] + utxos = utxos[1:] + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), sourceChainID).Return([]*avax.UTXO{importedUtxo}, nil) + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewImportTx( + sourceChainID, + importTo, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), importedUtxo.InputID()).Return(importedUtxo, nil).AnyTimes() + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5640*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + importedIns := utx.ImportedInputs + require.Len(ins, 1) + require.Len(importedIns, 1) + require.Len(outs, 1) + require.Equal(fc.Fee, importedIns[0].In.Amount()+ins[0].In.Amount()-outs[0].Out.Amount()) +} + +func TestExportTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + utxosKey = testKeys[1] + utxoAddr = utxosKey.PublicKey().Address() + subnetID = ids.GenerateTestID() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + + exportedOutputs = []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 7 * units.Avax, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + }, + }, + }} + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewExportTx( + subnetID, + exportedOutputs, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(5966*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 2) + require.Len(outs, 1) + require.Equal(fc.Fee+exportedOutputs[0].Out.Amount(), ins[0].In.Amount()+ins[1].In.Amount()-outs[0].Out.Amount()) + require.Equal(utx.ExportedOutputs, exportedOutputs) +} + +func TestTransformSubnetTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + subnetAuthKey = testKeys[0] + utxosKey = testKeys[1] + subnetAuthAddr = subnetAuthKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + subnetID = ids.GenerateTestID() + utxos, avaxAssetID, subnetAssetID = testUTXOsList(utxosKey) + subnetTx = &txs.Tx{ + Unsigned: &txs.CreateSubnetTx{ + Owner: &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{subnetAuthKey.PublicKey().Address()}, + }, + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, subnetAuthAddr), + backend: be, + } + be.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + var ( + initialSupply = 40 * units.MegaAvax + maxSupply = 100 * units.MegaAvax + ) + + utx, err := b.NewTransformSubnetTx( + subnetID, + subnetAssetID, + initialSupply, // initial supply + maxSupply, // max supply + reward.PercentDenominator, // min consumption rate + reward.PercentDenominator, // max consumption rate + 1, // min validator stake + 100*units.MegaAvax, // max validator stake + time.Second, // min stake duration + 365*24*time.Hour, // max stake duration + 0, // min delegation fee + 1, // min delegator stake + 5, // max validator weight factor + .80*reward.PercentDenominator, // uptime requirement + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + sbe.EXPECT().GetTx(gomock.Any(), subnetID).Return(subnetTx, nil) + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(8763*units.MicroAvax, fc.Fee) + + ins := utx.Ins + outs := utx.Outs + require.Len(ins, 3) + require.Len(outs, 2) + require.Equal(maxSupply-initialSupply, ins[0].In.Amount()-outs[0].Out.Amount()) + require.Equal(fc.Fee, ins[1].In.Amount()+ins[2].In.Amount()-outs[1].Out.Amount()) +} + +func TestAddPermissionlessValidatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + rewardKey = testKeys[0] + utxosKey = testKeys[1] + rewardAddr = rewardKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + validationRewardsOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + rewardKey.Address(), + }, + } + delegationRewardsOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + rewardKey.Address(), + }, + } + ) + + sk, err := bls.NewSecretKey() + require.NoError(err) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, rewardAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewAddPermissionlessValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: uint64(time.Now().Add(time.Hour).Unix()), + Wght: 2 * units.Avax, + }, + Subnet: constants.PrimaryNetworkID, + }, + signer.NewProofOfPossession(sk), + avaxAssetID, + validationRewardsOwner, + delegationRewardsOwner, + reward.PercentDenominator, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(12404*units.MicroAvax, fc.Fee) + + ins := utx.Ins + staked := utx.StakeOuts + outs := utx.Outs + require.Len(ins, 4) + require.Len(staked, 2) + require.Len(outs, 2) + require.Equal(utx.Validator.Weight(), staked[0].Out.Amount()+staked[1].Out.Amount()) + require.Equal(fc.Fee, ins[1].In.Amount()+ins[3].In.Amount()-outs[0].Out.Amount()) +} + +func TestAddPermissionlessDelegatorTx(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + be := mocks.NewMockBuilderBackend(ctrl) + + var ( + rewardKey = testKeys[0] + utxosKey = testKeys[1] + rewardAddr = rewardKey.PublicKey().Address() + utxoAddr = utxosKey.PublicKey().Address() + utxos, avaxAssetID, _ = testUTXOsList(utxosKey) + rewardsOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + rewardKey.Address(), + }, + } + ) + + b := &DynamicFeesBuilder{ + addrs: set.Of(utxoAddr, rewardAddr), + backend: be, + } + be.EXPECT().AVAXAssetID().Return(avaxAssetID).AnyTimes() + be.EXPECT().NetworkID().Return(constants.MainnetID).AnyTimes() + be.EXPECT().UTXOs(gomock.Any(), constants.PlatformChainID).Return(utxos, nil) + + utx, err := b.NewAddPermissionlessDelegatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: uint64(time.Now().Add(time.Hour).Unix()), + Wght: 2 * units.Avax, + }, + Subnet: constants.PrimaryNetworkID, + }, + avaxAssetID, + rewardsOwner, + testUnitFees, + testBlockMaxConsumedUnits, + ) + require.NoError(err) + + var ( + kc = secp256k1fx.NewKeychain(utxosKey) + sbe = mocks.NewMockSignerBackend(ctrl) + s = NewSigner(kc, sbe) + ) + + for _, utxo := range utxos { + sbe.EXPECT().GetUTXO(gomock.Any(), gomock.Any(), utxo.InputID()).Return(utxo, nil).AnyTimes() + } + + tx, err := s.SignUnsigned(stdcontext.Background(), utx) + require.NoError(err) + + fc := &fees.Calculator{ + IsEForkActive: true, + FeeManager: commonfees.NewManager(testUnitFees), + ConsumedUnitsCap: testBlockMaxConsumedUnits, + Credentials: tx.Creds, + } + require.NoError(utx.Visit(fc)) + require.Equal(12212*units.MicroAvax, fc.Fee) + + ins := utx.Ins + staked := utx.StakeOuts + outs := utx.Outs + require.Len(ins, 4) + require.Len(staked, 2) + require.Len(outs, 2) + require.Equal(utx.Validator.Weight(), staked[0].Out.Amount()+staked[1].Out.Amount()) + require.Equal(fc.Fee, ins[1].In.Amount()+ins[3].In.Amount()-outs[0].Out.Amount()) +} + +func testUTXOsList(utxosKey *secp256k1.PrivateKey) ( + []*avax.UTXO, + ids.ID, // avaxAssetID, + ids.ID, // subnetAssetID +) { + // Note: we avoid ids.GenerateTestNodeID here to make sure that UTXO IDs won't change + // run by run. This simplifies checking what utxos are included in the built txs. + utxosOffset := uint64(2024) + + var ( + avaxAssetID = ids.Empty.Prefix(utxosOffset) + subnetAssetID = ids.Empty.Prefix(utxosOffset + 1) + ) + + return []*avax.UTXO{ // currently, the wallet scans UTXOs in the order provided here + { // a small UTXO first, which should not be enough to pay fees + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(utxosOffset), + OutputIndex: uint32(utxosOffset), + }, + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2 * units.MilliAvax, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + Threshold: 1, + }, + }, + }, + { // a locked, small UTXO + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(utxosOffset + 1), + OutputIndex: uint32(utxosOffset + 1), + }, + Asset: avax.Asset{ID: avaxAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Hour).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 3 * units.MilliAvax, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + }, + }, + }, + }, + { // a subnetAssetID denominated UTXO + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(utxosOffset + 2), + OutputIndex: uint32(utxosOffset + 2), + }, + Asset: avax.Asset{ID: subnetAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 99 * units.MegaAvax, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + Threshold: 1, + }, + }, + }, + { // a locked, large UTXO + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(utxosOffset + 3), + OutputIndex: uint32(utxosOffset + 3), + }, + Asset: avax.Asset{ID: avaxAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(time.Now().Add(time.Hour).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 88 * units.Avax, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + }, + }, + }, + }, + { // a large UTXO last, which should be enough to pay any fee by itself + UTXOID: avax.UTXOID{ + TxID: ids.Empty.Prefix(utxosOffset + 4), + OutputIndex: uint32(utxosOffset + 4), + }, + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 9 * units.Avax, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Addrs: []ids.ShortID{utxosKey.PublicKey().Address()}, + Threshold: 1, + }, + }, + }, + }, + avaxAssetID, + subnetAssetID +} diff --git a/wallet/chain/p/mocks/mock_builder_backend.go b/wallet/chain/p/mocks/mock_builder_backend.go new file mode 100644 index 000000000000..5f838501f8ce --- /dev/null +++ b/wallet/chain/p/mocks/mock_builder_backend.go @@ -0,0 +1,213 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/wallet/chain/p (interfaces: BuilderBackend) +// +// Generated by this command: +// +// mockgen -package=mocks -destination=wallet/chain/p/mocks/mock_builder_backend.go github.com/ava-labs/avalanchego/wallet/chain/p BuilderBackend +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + ids "github.com/ava-labs/avalanchego/ids" + avax "github.com/ava-labs/avalanchego/vms/components/avax" + txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" + gomock "go.uber.org/mock/gomock" +) + +// MockBuilderBackend is a mock of BuilderBackend interface. +type MockBuilderBackend struct { + ctrl *gomock.Controller + recorder *MockBuilderBackendMockRecorder +} + +// MockBuilderBackendMockRecorder is the mock recorder for MockBuilderBackend. +type MockBuilderBackendMockRecorder struct { + mock *MockBuilderBackend +} + +// NewMockBuilderBackend creates a new mock instance. +func NewMockBuilderBackend(ctrl *gomock.Controller) *MockBuilderBackend { + mock := &MockBuilderBackend{ctrl: ctrl} + mock.recorder = &MockBuilderBackendMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBuilderBackend) EXPECT() *MockBuilderBackendMockRecorder { + return m.recorder +} + +// AVAXAssetID mocks base method. +func (m *MockBuilderBackend) AVAXAssetID() ids.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AVAXAssetID") + ret0, _ := ret[0].(ids.ID) + return ret0 +} + +// AVAXAssetID indicates an expected call of AVAXAssetID. +func (mr *MockBuilderBackendMockRecorder) AVAXAssetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AVAXAssetID", reflect.TypeOf((*MockBuilderBackend)(nil).AVAXAssetID)) +} + +// AddPrimaryNetworkDelegatorFee mocks base method. +func (m *MockBuilderBackend) AddPrimaryNetworkDelegatorFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrimaryNetworkDelegatorFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// AddPrimaryNetworkDelegatorFee indicates an expected call of AddPrimaryNetworkDelegatorFee. +func (mr *MockBuilderBackendMockRecorder) AddPrimaryNetworkDelegatorFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrimaryNetworkDelegatorFee", reflect.TypeOf((*MockBuilderBackend)(nil).AddPrimaryNetworkDelegatorFee)) +} + +// AddPrimaryNetworkValidatorFee mocks base method. +func (m *MockBuilderBackend) AddPrimaryNetworkValidatorFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddPrimaryNetworkValidatorFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// AddPrimaryNetworkValidatorFee indicates an expected call of AddPrimaryNetworkValidatorFee. +func (mr *MockBuilderBackendMockRecorder) AddPrimaryNetworkValidatorFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPrimaryNetworkValidatorFee", reflect.TypeOf((*MockBuilderBackend)(nil).AddPrimaryNetworkValidatorFee)) +} + +// AddSubnetDelegatorFee mocks base method. +func (m *MockBuilderBackend) AddSubnetDelegatorFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddSubnetDelegatorFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// AddSubnetDelegatorFee indicates an expected call of AddSubnetDelegatorFee. +func (mr *MockBuilderBackendMockRecorder) AddSubnetDelegatorFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSubnetDelegatorFee", reflect.TypeOf((*MockBuilderBackend)(nil).AddSubnetDelegatorFee)) +} + +// AddSubnetValidatorFee mocks base method. +func (m *MockBuilderBackend) AddSubnetValidatorFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddSubnetValidatorFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// AddSubnetValidatorFee indicates an expected call of AddSubnetValidatorFee. +func (mr *MockBuilderBackendMockRecorder) AddSubnetValidatorFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSubnetValidatorFee", reflect.TypeOf((*MockBuilderBackend)(nil).AddSubnetValidatorFee)) +} + +// BaseTxFee mocks base method. +func (m *MockBuilderBackend) BaseTxFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BaseTxFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// BaseTxFee indicates an expected call of BaseTxFee. +func (mr *MockBuilderBackendMockRecorder) BaseTxFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BaseTxFee", reflect.TypeOf((*MockBuilderBackend)(nil).BaseTxFee)) +} + +// CreateBlockchainTxFee mocks base method. +func (m *MockBuilderBackend) CreateBlockchainTxFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateBlockchainTxFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// CreateBlockchainTxFee indicates an expected call of CreateBlockchainTxFee. +func (mr *MockBuilderBackendMockRecorder) CreateBlockchainTxFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBlockchainTxFee", reflect.TypeOf((*MockBuilderBackend)(nil).CreateBlockchainTxFee)) +} + +// CreateSubnetTxFee mocks base method. +func (m *MockBuilderBackend) CreateSubnetTxFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSubnetTxFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// CreateSubnetTxFee indicates an expected call of CreateSubnetTxFee. +func (mr *MockBuilderBackendMockRecorder) CreateSubnetTxFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubnetTxFee", reflect.TypeOf((*MockBuilderBackend)(nil).CreateSubnetTxFee)) +} + +// GetTx mocks base method. +func (m *MockBuilderBackend) GetTx(arg0 context.Context, arg1 ids.ID) (*txs.Tx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTx", arg0, arg1) + ret0, _ := ret[0].(*txs.Tx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTx indicates an expected call of GetTx. +func (mr *MockBuilderBackendMockRecorder) GetTx(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTx", reflect.TypeOf((*MockBuilderBackend)(nil).GetTx), arg0, arg1) +} + +// NetworkID mocks base method. +func (m *MockBuilderBackend) NetworkID() uint32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NetworkID") + ret0, _ := ret[0].(uint32) + return ret0 +} + +// NetworkID indicates an expected call of NetworkID. +func (mr *MockBuilderBackendMockRecorder) NetworkID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkID", reflect.TypeOf((*MockBuilderBackend)(nil).NetworkID)) +} + +// TransformSubnetTxFee mocks base method. +func (m *MockBuilderBackend) TransformSubnetTxFee() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransformSubnetTxFee") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// TransformSubnetTxFee indicates an expected call of TransformSubnetTxFee. +func (mr *MockBuilderBackendMockRecorder) TransformSubnetTxFee() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransformSubnetTxFee", reflect.TypeOf((*MockBuilderBackend)(nil).TransformSubnetTxFee)) +} + +// UTXOs mocks base method. +func (m *MockBuilderBackend) UTXOs(arg0 context.Context, arg1 ids.ID) ([]*avax.UTXO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UTXOs", arg0, arg1) + ret0, _ := ret[0].([]*avax.UTXO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UTXOs indicates an expected call of UTXOs. +func (mr *MockBuilderBackendMockRecorder) UTXOs(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UTXOs", reflect.TypeOf((*MockBuilderBackend)(nil).UTXOs), arg0, arg1) +} diff --git a/wallet/chain/p/mocks/mock_signer_backend.go b/wallet/chain/p/mocks/mock_signer_backend.go new file mode 100644 index 000000000000..13f3527c4099 --- /dev/null +++ b/wallet/chain/p/mocks/mock_signer_backend.go @@ -0,0 +1,73 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/wallet/chain/p (interfaces: SignerBackend) +// +// Generated by this command: +// +// mockgen -package=mocks -destination=wallet/chain/p/mocks/mock_signer_backend.go github.com/ava-labs/avalanchego/wallet/chain/p SignerBackend +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + ids "github.com/ava-labs/avalanchego/ids" + avax "github.com/ava-labs/avalanchego/vms/components/avax" + txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" + gomock "go.uber.org/mock/gomock" +) + +// MockSignerBackend is a mock of SignerBackend interface. +type MockSignerBackend struct { + ctrl *gomock.Controller + recorder *MockSignerBackendMockRecorder +} + +// MockSignerBackendMockRecorder is the mock recorder for MockSignerBackend. +type MockSignerBackendMockRecorder struct { + mock *MockSignerBackend +} + +// NewMockSignerBackend creates a new mock instance. +func NewMockSignerBackend(ctrl *gomock.Controller) *MockSignerBackend { + mock := &MockSignerBackend{ctrl: ctrl} + mock.recorder = &MockSignerBackendMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSignerBackend) EXPECT() *MockSignerBackendMockRecorder { + return m.recorder +} + +// GetTx mocks base method. +func (m *MockSignerBackend) GetTx(arg0 context.Context, arg1 ids.ID) (*txs.Tx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTx", arg0, arg1) + ret0, _ := ret[0].(*txs.Tx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTx indicates an expected call of GetTx. +func (mr *MockSignerBackendMockRecorder) GetTx(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTx", reflect.TypeOf((*MockSignerBackend)(nil).GetTx), arg0, arg1) +} + +// GetUTXO mocks base method. +func (m *MockSignerBackend) GetUTXO(arg0 context.Context, arg1, arg2 ids.ID) (*avax.UTXO, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUTXO", arg0, arg1, arg2) + ret0, _ := ret[0].(*avax.UTXO) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUTXO indicates an expected call of GetUTXO. +func (mr *MockSignerBackendMockRecorder) GetUTXO(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUTXO", reflect.TypeOf((*MockSignerBackend)(nil).GetUTXO), arg0, arg1, arg2) +} diff --git a/wallet/chain/p/wallet.go b/wallet/chain/p/wallet.go index e982a204a9f8..e25e3dada140 100644 --- a/wallet/chain/p/wallet.go +++ b/wallet/chain/p/wallet.go @@ -9,8 +9,11 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fees" "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -247,23 +250,31 @@ type Wallet interface { func NewWallet( builder Builder, + dynFeesBuilder *DynamicFeesBuilder, signer Signer, client platformvm.Client, backend Backend, ) Wallet { return &wallet{ - Backend: backend, - builder: builder, - signer: signer, - client: client, + Backend: backend, + builder: builder, + dynamicBuilder: dynFeesBuilder, + unitFees: config.EUpgradeDynamicFeesConfig.UnitFees, + unitCaps: config.EUpgradeDynamicFeesConfig.BlockUnitsCap, + signer: signer, + client: client, } } type wallet struct { Backend - builder Builder - signer Signer - client platformvm.Client + signer Signer + client platformvm.Client + + isEForkActive bool + builder Builder + dynamicBuilder *DynamicFeesBuilder + unitFees, unitCaps fees.Dimensions } func (w *wallet) Builder() Builder { @@ -278,10 +289,23 @@ func (w *wallet) IssueBaseTx( outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewBaseTx(outputs, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewBaseTx(outputs, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewBaseTx(outputs, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -291,10 +315,23 @@ func (w *wallet) IssueAddValidatorTx( shares uint32, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewAddValidatorTx(vdr, rewardsOwner, shares, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewAddValidatorTx(vdr, rewardsOwner, shares, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewAddValidatorTx(vdr, rewardsOwner, shares, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -302,10 +339,23 @@ func (w *wallet) IssueAddSubnetValidatorTx( vdr *txs.SubnetValidator, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewAddSubnetValidatorTx(vdr, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewAddSubnetValidatorTx(vdr, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewAddSubnetValidatorTx(vdr, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -314,10 +364,23 @@ func (w *wallet) IssueRemoveSubnetValidatorTx( subnetID ids.ID, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewRemoveSubnetValidatorTx(nodeID, subnetID, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewRemoveSubnetValidatorTx(nodeID, subnetID, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewRemoveSubnetValidatorTx(nodeID, subnetID, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -326,10 +389,23 @@ func (w *wallet) IssueAddDelegatorTx( rewardsOwner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewAddDelegatorTx(vdr, rewardsOwner, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewAddDelegatorTx(vdr, rewardsOwner, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewAddDelegatorTx(vdr, rewardsOwner, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -341,10 +417,23 @@ func (w *wallet) IssueCreateChainTx( chainName string, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewCreateChainTx(subnetID, genesis, vmID, fxIDs, chainName, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewCreateChainTx(subnetID, genesis, vmID, fxIDs, chainName, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewCreateChainTx(subnetID, genesis, vmID, fxIDs, chainName, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -352,10 +441,23 @@ func (w *wallet) IssueCreateSubnetTx( owner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewCreateSubnetTx(owner, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewCreateSubnetTx(owner, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewCreateSubnetTx(owner, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -364,10 +466,23 @@ func (w *wallet) IssueImportTx( to *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewImportTx(sourceChainID, to, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewImportTx(sourceChainID, to, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewImportTx(sourceChainID, to, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -376,10 +491,23 @@ func (w *wallet) IssueExportTx( outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewExportTx(chainID, outputs, options...) + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error + ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewExportTx(chainID, outputs, w.unitFees, w.unitCaps, options...) + } else { + utx, err = w.builder.NewExportTx(chainID, outputs, options...) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -400,26 +528,57 @@ func (w *wallet) IssueTransformSubnetTx( uptimeRequirement uint32, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewTransformSubnetTx( - subnetID, - assetID, - initialSupply, - maxSupply, - minConsumptionRate, - maxConsumptionRate, - minValidatorStake, - maxValidatorStake, - minStakeDuration, - maxStakeDuration, - minDelegationFee, - minDelegatorStake, - maxValidatorWeightFactor, - uptimeRequirement, - options..., + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewTransformSubnetTx( + subnetID, + assetID, + initialSupply, + maxSupply, + minConsumptionRate, + maxConsumptionRate, + minValidatorStake, + maxValidatorStake, + minStakeDuration, + maxStakeDuration, + minDelegationFee, + minDelegatorStake, + maxValidatorWeightFactor, + uptimeRequirement, + w.unitFees, + w.unitCaps, + options..., + ) + } else { + utx, err = w.builder.NewTransformSubnetTx( + subnetID, + assetID, + initialSupply, + maxSupply, + minConsumptionRate, + maxConsumptionRate, + minValidatorStake, + maxValidatorStake, + minStakeDuration, + maxStakeDuration, + minDelegationFee, + minDelegatorStake, + maxValidatorWeightFactor, + uptimeRequirement, + options..., + ) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -432,18 +591,41 @@ func (w *wallet) IssueAddPermissionlessValidatorTx( shares uint32, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewAddPermissionlessValidatorTx( - vdr, - signer, - assetID, - validationRewardsOwner, - delegationRewardsOwner, - shares, - options..., + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewAddPermissionlessValidatorTx( + vdr, + signer, + assetID, + validationRewardsOwner, + delegationRewardsOwner, + shares, + w.unitFees, + w.unitCaps, + options..., + ) + } else { + utx, err = w.builder.NewAddPermissionlessValidatorTx( + vdr, + signer, + assetID, + validationRewardsOwner, + delegationRewardsOwner, + shares, + options..., + ) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -453,15 +635,35 @@ func (w *wallet) IssueAddPermissionlessDelegatorTx( rewardsOwner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewAddPermissionlessDelegatorTx( - vdr, - assetID, - rewardsOwner, - options..., + if err := w.refreshFork(options...); err != nil { + return nil, err + } + + var ( + utx txs.UnsignedTx + err error ) + if w.isEForkActive { + utx, err = w.dynamicBuilder.NewAddPermissionlessDelegatorTx( + vdr, + assetID, + rewardsOwner, + w.unitFees, + w.unitCaps, + options..., + ) + } else { + utx, err = w.builder.NewAddPermissionlessDelegatorTx( + vdr, + assetID, + rewardsOwner, + options..., + ) + } if err != nil { return nil, err } + return w.IssueUnsignedTx(utx, options...) } @@ -512,3 +714,25 @@ func (w *wallet) IssueTx( } return nil } + +func (w *wallet) refreshFork(options ...common.Option) error { + if w.isEForkActive { + // E fork enables dinamic fees and it is active + // not need to recheck + return nil + } + + var ( + ops = common.NewOptions(options) + ctx = ops.Context() + eForkTime = version.GetEForkTime(w.NetworkID()) + ) + + chainTime, err := w.client.GetTimestamp(ctx) + if err != nil { + return err + } + + w.isEForkActive = !chainTime.Before(eForkTime) + return nil +} diff --git a/wallet/subnet/primary/wallet.go b/wallet/subnet/primary/wallet.go index 3bb3e9965684..0825ab1b370f 100644 --- a/wallet/subnet/primary/wallet.go +++ b/wallet/subnet/primary/wallet.go @@ -119,6 +119,7 @@ func MakeWallet(ctx context.Context, config *WalletConfig) (Wallet, error) { pUTXOs := NewChainUTXOs(constants.PlatformChainID, avaxState.UTXOs) pBackend := p.NewBackend(avaxState.PCTX, pUTXOs, pChainTxs) pBuilder := p.NewBuilder(avaxAddrs, pBackend) + pDynamicFeesBuilder := p.NewDynamicFeesBuilder(avaxAddrs, pBackend) pSigner := p.NewSigner(config.AVAXKeychain, pBackend) xChainID := avaxState.XCTX.BlockchainID() @@ -134,7 +135,7 @@ func MakeWallet(ctx context.Context, config *WalletConfig) (Wallet, error) { cSigner := c.NewSigner(config.AVAXKeychain, config.EthKeychain, cBackend) return NewWallet( - p.NewWallet(pBuilder, pSigner, avaxState.PClient, pBackend), + p.NewWallet(pBuilder, pDynamicFeesBuilder, pSigner, avaxState.PClient, pBackend), x.NewWallet(xBuilder, xSigner, avaxState.XClient, xBackend), c.NewWallet(cBuilder, cSigner, avaxState.CClient, ethState.Client, cBackend), ), nil