diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index ee127ed71c22..e15267893fcf 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -22,6 +22,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + + feecomponent "github.com/ava-labs/avalanchego/vms/components/fee" + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) var ( @@ -325,10 +328,27 @@ func (b *builder) NewBaseTx( toStake := map[ids.ID]uint64{} ops := common.NewOptions(options) + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + outputComplexity, err := txfee.OutputComplexity(outputs...) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicBaseTxComplexities.Add( + &memoComplexity, + &outputComplexity, + ) + if err != nil { + return nil, err + } + inputs, changeOutputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -343,7 +363,7 @@ func (b *builder) NewBaseTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }} return tx, b.initCtx(tx) } @@ -366,6 +386,7 @@ func (b *builder) NewAddValidatorTx( toBurn, toStake, 0, + feecomponent.Dimensions{}, nil, ops, ) @@ -405,10 +426,27 @@ func (b *builder) NewAddSubnetValidatorTx( return nil, err } + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + authComplexity, err := txfee.AuthComplexity(subnetAuth) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicAddSubnetValidatorTxComplexities.Add( + &memoComplexity, + &authComplexity, + ) + if err != nil { + return nil, err + } + inputs, outputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -422,7 +460,7 @@ func (b *builder) NewAddSubnetValidatorTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }}, SubnetValidator: *vdr, SubnetAuth: subnetAuth, @@ -446,10 +484,27 @@ func (b *builder) NewRemoveSubnetValidatorTx( return nil, err } + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + authComplexity, err := txfee.AuthComplexity(subnetAuth) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicRemoveSubnetValidatorTxComplexities.Add( + &memoComplexity, + &authComplexity, + ) + if err != nil { + return nil, err + } + inputs, outputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -489,6 +544,7 @@ func (b *builder) NewAddDelegatorTx( toBurn, toStake, 0, + feecomponent.Dimensions{}, nil, ops, ) @@ -531,10 +587,43 @@ func (b *builder) NewCreateChainTx( return nil, err } + memo := ops.Memo() + bandwidth, err := math.Mul(uint64(len(fxIDs)), ids.IDLen) + if err != nil { + return nil, err + } + bandwidth, err = math.Add(bandwidth, uint64(len(chainName))) + if err != nil { + return nil, err + } + bandwidth, err = math.Add(bandwidth, uint64(len(genesis))) + if err != nil { + return nil, err + } + bandwidth, err = math.Add(bandwidth, uint64(len(memo))) + if err != nil { + return nil, err + } + dynamicComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: bandwidth, + } + authComplexity, err := txfee.AuthComplexity(subnetAuth) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicCreateChainTxComplexities.Add( + &dynamicComplexity, + &authComplexity, + ) + if err != nil { + return nil, err + } + inputs, outputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -549,7 +638,7 @@ func (b *builder) NewCreateChainTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }}, SubnetID: subnetID, ChainName: chainName, @@ -569,11 +658,29 @@ func (b *builder) NewCreateSubnetTx( b.context.AVAXAssetID: b.context.StaticFeeConfig.CreateSubnetTxFee, } toStake := map[ids.ID]uint64{} + ops := common.NewOptions(options) + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + ownerComplexity, err := txfee.OwnerComplexity(owner) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicCreateSubnetTxComplexities.Add( + &memoComplexity, + &ownerComplexity, + ) + if err != nil { + return nil, err + } + inputs, outputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -588,7 +695,7 @@ func (b *builder) NewCreateSubnetTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }}, Owner: owner, } @@ -611,10 +718,32 @@ func (b *builder) NewTransferSubnetOwnershipTx( return nil, err } + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + authComplexity, err := txfee.AuthComplexity(subnetAuth) + if err != nil { + return nil, err + } + ownerComplexity, err := txfee.OwnerComplexity(owner) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicTransferSubnetOwnershipTxComplexities.Add( + &memoComplexity, + &authComplexity, + &ownerComplexity, + ) + if err != nil { + return nil, err + } + inputs, outputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -629,7 +758,7 @@ func (b *builder) NewTransferSubnetOwnershipTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }}, Subnet: subnetID, Owner: owner, @@ -713,6 +842,27 @@ func (b *builder) NewImportTx( }) } + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + inputComplexity, err := txfee.InputComplexity(importedInputs...) + if err != nil { + return nil, err + } + outputComplexity, err := txfee.OutputComplexity(outputs...) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicImportTxComplexities.Add( + &memoComplexity, + &inputComplexity, + &outputComplexity, + ) + if err != nil { + return nil, err + } + var ( toBurn = map[ids.ID]uint64{} toStake = map[ids.ID]uint64{} @@ -728,6 +878,7 @@ func (b *builder) NewImportTx( toBurn, toStake, excessAVAX, + complexity, to, ops, ) @@ -743,7 +894,7 @@ func (b *builder) NewImportTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: outputs, - Memo: ops.Memo(), + Memo: memo, }}, SourceChain: sourceChainID, ImportedInputs: importedInputs, @@ -770,10 +921,27 @@ func (b *builder) NewExportTx( toStake := map[ids.ID]uint64{} ops := common.NewOptions(options) + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + outputComplexity, err := txfee.OutputComplexity(outputs...) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicExportTxComplexities.Add( + &memoComplexity, + &outputComplexity, + ) + if err != nil { + return nil, err + } + inputs, changeOutputs, _, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -788,7 +956,7 @@ func (b *builder) NewExportTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: changeOutputs, - Memo: ops.Memo(), + Memo: memo, }}, DestinationChain: chainID, ExportedOutputs: outputs, @@ -829,6 +997,7 @@ func (b *builder) NewTransformSubnetTx( toBurn, toStake, 0, + feecomponent.Dimensions{}, nil, ops, ) @@ -882,11 +1051,39 @@ func (b *builder) NewAddPermissionlessValidatorTx( toStake := map[ids.ID]uint64{ assetID: vdr.Wght, } + ops := common.NewOptions(options) + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + signerComplexity, err := txfee.SignerComplexity(signer) + if err != nil { + return nil, err + } + validatorOwnerComplexity, err := txfee.OwnerComplexity(validationRewardsOwner) + if err != nil { + return nil, err + } + delegatorOwnerComplexity, err := txfee.OwnerComplexity(delegationRewardsOwner) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicAddPermissionlessValidatorTxComplexities.Add( + &memoComplexity, + &signerComplexity, + &validatorOwnerComplexity, + &delegatorOwnerComplexity, + ) + if err != nil { + return nil, err + } + inputs, baseOutputs, stakeOutputs, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -902,7 +1099,7 @@ func (b *builder) NewAddPermissionlessValidatorTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: baseOutputs, - Memo: ops.Memo(), + Memo: memo, }}, Validator: vdr.Validator, Subnet: vdr.Subnet, @@ -931,11 +1128,29 @@ func (b *builder) NewAddPermissionlessDelegatorTx( toStake := map[ids.ID]uint64{ assetID: vdr.Wght, } + ops := common.NewOptions(options) + memo := ops.Memo() + memoComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)), + } + ownerComplexity, err := txfee.OwnerComplexity(rewardsOwner) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicAddPermissionlessDelegatorTxComplexities.Add( + &memoComplexity, + &ownerComplexity, + ) + if err != nil { + return nil, err + } + inputs, baseOutputs, stakeOutputs, err := b.spend( toBurn, toStake, 0, + complexity, nil, ops, ) @@ -950,7 +1165,7 @@ func (b *builder) NewAddPermissionlessDelegatorTx( BlockchainID: constants.PlatformChainID, Ins: inputs, Outs: baseOutputs, - Memo: ops.Memo(), + Memo: memo, }}, Validator: vdr.Validator, Subnet: vdr.Subnet, @@ -1021,6 +1236,8 @@ func (b *builder) getBalance( // preferential ordering on the unlock times. // - [excessAVAX] contains the amount of extra AVAX that spend can produce in // the change outputs in addition to the consumed and not burned AVAX. +// - [complexity] contains the currently accrued transaction complexity that +// will be used to calculate the required fees to be burned. // - [ownerOverride] optionally specifies the output owners to use for the // unlocked AVAX change output if no additional AVAX was needed to be // burned. If this value is nil, the default change owner is used. @@ -1028,6 +1245,7 @@ func (b *builder) spend( toBurn map[ids.ID]uint64, toStake map[ids.ID]uint64, excessAVAX uint64, + complexity feecomponent.Dimensions, ownerOverride *secp256k1fx.OutputOwners, options *common.Options, ) ( @@ -1057,8 +1275,12 @@ func (b *builder) spend( } s := spendHelper{ - toBurn: toBurn, - toStake: toStake, + weights: b.context.ComplexityWeights, + gasPrice: b.context.GasPrice, + + toBurn: toBurn, + toStake: toStake, + complexity: complexity, // Initialize the return values with empty slices to preserve backward // compatibility of the json representation of transactions with no @@ -1086,7 +1308,7 @@ func (b *builder) spend( continue } - s.addInput(&avax.TransferableInput{ + err = s.addInput(&avax.TransferableInput{ UTXOID: utxo.UTXOID, Asset: utxo.Asset, In: &stakeable.LockIn{ @@ -1099,9 +1321,12 @@ func (b *builder) spend( }, }, }) + if err != nil { + return nil, nil, nil, err + } excess := s.consumeLockedAsset(assetID, out.Amt) - s.addStakedOutput(&avax.TransferableOutput{ + err = s.addStakedOutput(&avax.TransferableOutput{ Asset: utxo.Asset, Out: &stakeable.LockOut{ Locktime: locktime, @@ -1111,13 +1336,16 @@ func (b *builder) spend( }, }, }) + if err != nil { + return nil, nil, nil, err + } if excess == 0 { continue } // This input had extra value, so some of it must be returned - s.addChangeOutput(&avax.TransferableOutput{ + err = s.addChangeOutput(&avax.TransferableOutput{ Asset: utxo.Asset, Out: &stakeable.LockOut{ Locktime: locktime, @@ -1127,6 +1355,9 @@ func (b *builder) spend( }, }, }) + if err != nil { + return nil, nil, nil, err + } } // Add all the remaining stake amounts assuming unlocked UTXOs. @@ -1135,7 +1366,7 @@ func (b *builder) spend( continue } - s.addStakedOutput(&avax.TransferableOutput{ + err = s.addStakedOutput(&avax.TransferableOutput{ Asset: avax.Asset{ ID: assetID, }, @@ -1144,6 +1375,9 @@ func (b *builder) spend( OutputOwners: *changeOwner, }, }) + if err != nil { + return nil, nil, nil, err + } } // AVAX is handled last to account for fees. @@ -1165,7 +1399,7 @@ func (b *builder) spend( continue } - s.addInput(&avax.TransferableInput{ + err = s.addInput(&avax.TransferableInput{ UTXOID: utxo.UTXOID, Asset: utxo.Asset, In: &secp256k1fx.TransferInput{ @@ -1175,6 +1409,9 @@ func (b *builder) spend( }, }, }) + if err != nil { + return nil, nil, nil, err + } excess := s.consumeAsset(assetID, out.Amt) if excess == 0 { @@ -1182,19 +1419,28 @@ func (b *builder) spend( } // This input had extra value, so some of it must be returned - s.addChangeOutput(&avax.TransferableOutput{ + err = s.addChangeOutput(&avax.TransferableOutput{ Asset: utxo.Asset, Out: &secp256k1fx.TransferOutput{ Amt: excess, OutputOwners: *changeOwner, }, }) + if err != nil { + return nil, nil, nil, err + } } for _, utxo := range utxosByAVAXAssetID.requested { - // If we have consumed enough of the asset, then we have no need burn - // more. - if !s.shouldConsumeAsset(b.context.AVAXAssetID) { + requiredFee, err := s.calculateFee() + if err != nil { + return nil, nil, nil, err + } + + // If we don't need to burn or stake additional AVAX and we have + // consumed enough AVAX to pay the required fee, we should stop + // consuming UTXOs. + if !s.shouldConsumeAsset(b.context.AVAXAssetID) && excessAVAX >= requiredFee { break } @@ -1209,7 +1455,7 @@ func (b *builder) spend( continue } - s.addInput(&avax.TransferableInput{ + err = s.addInput(&avax.TransferableInput{ UTXOID: utxo.UTXOID, Asset: utxo.Asset, In: &secp256k1fx.TransferInput{ @@ -1219,6 +1465,9 @@ func (b *builder) spend( }, }, }) + if err != nil { + return nil, nil, nil, err + } excess := s.consumeAsset(b.context.AVAXAssetID, out.Amt) excessAVAX, err = math.Add(excessAVAX, excess) @@ -1235,17 +1484,41 @@ func (b *builder) spend( return nil, nil, nil, err } - if excessAVAX > 0 { - newOutput := &avax.TransferableOutput{ - Asset: avax.Asset{ - ID: b.context.AVAXAssetID, - }, - Out: &secp256k1fx.TransferOutput{ - Amt: excessAVAX, - OutputOwners: *ownerOverride, - }, - } - s.changeOutputs = append(s.changeOutputs, newOutput) + requiredFee, err := s.calculateFee() + if err != nil { + return nil, nil, nil, err + } + if excessAVAX < requiredFee { + return nil, nil, nil, fmt.Errorf( + "%w: provided UTXOs needed %d more nAVAX (%q)", + ErrInsufficientFunds, + requiredFee-excessAVAX, + b.context.AVAXAssetID, + ) + } + + secpExcessAVAXOutput := &secp256k1fx.TransferOutput{ + Amt: 0, // Populated later if used + OutputOwners: *ownerOverride, + } + excessAVAXOutput := &avax.TransferableOutput{ + Asset: avax.Asset{ + ID: b.context.AVAXAssetID, + }, + Out: secpExcessAVAXOutput, + } + if err := s.addOutputComplexity(excessAVAXOutput); err != nil { + return nil, nil, nil, err + } + + requiredFeeWithChange, err := s.calculateFee() + if err != nil { + return nil, nil, nil, err + } + if excessAVAX > requiredFeeWithChange { + // It is worth adding the change output + secpExcessAVAXOutput.Amt = excessAVAX - requiredFeeWithChange + s.changeOutputs = append(s.changeOutputs, excessAVAXOutput) } utils.Sort(s.inputs) // sort inputs @@ -1291,24 +1564,49 @@ func (b *builder) initCtx(tx txs.UnsignedTx) error { } type spendHelper struct { - toBurn map[ids.ID]uint64 - toStake map[ids.ID]uint64 + weights feecomponent.Dimensions + gasPrice feecomponent.GasPrice + + toBurn map[ids.ID]uint64 + toStake map[ids.ID]uint64 + complexity feecomponent.Dimensions inputs []*avax.TransferableInput changeOutputs []*avax.TransferableOutput stakeOutputs []*avax.TransferableOutput } -func (s *spendHelper) addInput(input *avax.TransferableInput) { +func (s *spendHelper) addInput(input *avax.TransferableInput) error { + newInputComplexity, err := txfee.InputComplexity(input) + if err != nil { + return err + } + s.complexity, err = s.complexity.Add(&newInputComplexity) + if err != nil { + return err + } + s.inputs = append(s.inputs, input) + return nil } -func (s *spendHelper) addChangeOutput(output *avax.TransferableOutput) { +func (s *spendHelper) addChangeOutput(output *avax.TransferableOutput) error { s.changeOutputs = append(s.changeOutputs, output) + return s.addOutputComplexity(output) } -func (s *spendHelper) addStakedOutput(output *avax.TransferableOutput) { +func (s *spendHelper) addStakedOutput(output *avax.TransferableOutput) error { s.stakeOutputs = append(s.stakeOutputs, output) + return s.addOutputComplexity(output) +} + +func (s *spendHelper) addOutputComplexity(output *avax.TransferableOutput) error { + newOutputComplexity, err := txfee.OutputComplexity(output) + if err != nil { + return err + } + s.complexity, err = s.complexity.Add(&newOutputComplexity) + return err } func (s *spendHelper) shouldConsumeLockedAsset(assetID ids.ID) bool { @@ -1341,6 +1639,14 @@ func (s *spendHelper) consumeAsset(assetID ids.ID, amount uint64) uint64 { return s.consumeLockedAsset(assetID, amount-toBurn) } +func (s *spendHelper) calculateFee() (uint64, error) { + gas, err := s.complexity.ToGas(s.weights) + if err != nil { + return 0, err + } + return gas.Cost(s.gasPrice) +} + func (s *spendHelper) verifyAssetsConsumed() error { for assetID, amount := range s.toStake { if amount == 0 { diff --git a/wallet/chain/p/builder/context.go b/wallet/chain/p/builder/context.go index 33eb5ecf0af5..3729edca2deb 100644 --- a/wallet/chain/p/builder/context.go +++ b/wallet/chain/p/builder/context.go @@ -12,15 +12,19 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/avm" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + + feecomponent "github.com/ava-labs/avalanchego/vms/components/fee" + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) const Alias = "P" type Context struct { - NetworkID uint32 - AVAXAssetID ids.ID - StaticFeeConfig fee.StaticConfig + NetworkID uint32 + AVAXAssetID ids.ID + StaticFeeConfig txfee.StaticConfig + ComplexityWeights feecomponent.Dimensions + GasPrice feecomponent.GasPrice } func NewContextFromURI(ctx context.Context, uri string) (*Context, error) { @@ -52,7 +56,7 @@ func NewContextFromClients( return &Context{ NetworkID: networkID, AVAXAssetID: asset.AssetID, - StaticFeeConfig: fee.StaticConfig{ + StaticFeeConfig: txfee.StaticConfig{ TxFee: uint64(txFees.TxFee), CreateSubnetTxFee: uint64(txFees.CreateSubnetTxFee), TransformSubnetTxFee: uint64(txFees.TransformSubnetTxFee), @@ -62,6 +66,10 @@ func NewContextFromClients( AddSubnetValidatorFee: uint64(txFees.AddSubnetValidatorFee), AddSubnetDelegatorFee: uint64(txFees.AddSubnetDelegatorFee), }, + + // TODO: Populate these fields once they are exposed by the API + ComplexityWeights: feecomponent.Dimensions{}, + GasPrice: 0, }, nil } diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index 67d51eaa7fcd..97ae0992025b 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "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/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" @@ -20,24 +21,81 @@ import ( "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/fee" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" "github.com/ava-labs/avalanchego/wallet/chain/p/builder" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common/utxotest" + + feecomponent "github.com/ava-labs/avalanchego/vms/components/fee" + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) var ( - testKeys = secp256k1.TestKeys() + subnetID = ids.GenerateTestID() + nodeID = ids.GenerateTestNodeID() + + testKeys = secp256k1.TestKeys() + subnetAuthKey = testKeys[0] + subnetAuthAddr = subnetAuthKey.Address() + subnetOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{subnetAuthAddr}, + } + importKey = testKeys[0] + importAddr = importKey.Address() + importOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{importAddr}, + } + rewardKey = testKeys[0] + rewardAddr = rewardKey.Address() + rewardsOwner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + } + utxoKey = testKeys[1] + utxoAddr = utxoKey.Address() + utxoOwner = secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{utxoAddr}, + } - // We hard-code [avaxAssetID] and [subnetAssetID] to make - // ordering of UTXOs generated by [testUTXOsList] is reproducible + // We hard-code [avaxAssetID] and [subnetAssetID] to make ordering of UTXOs + // generated by [makeTestUTXOs] reproducible. avaxAssetID = ids.Empty.Prefix(1789) subnetAssetID = ids.Empty.Prefix(2024) + utxos = makeTestUTXOs(utxoKey) + + avaxOutput = &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 7 * units.Avax, + OutputOwners: utxoOwner, + }, + } - testContext = &builder.Context{ + subnets = map[ids.ID]*txs.Tx{ + subnetID: { + Unsigned: &txs.CreateSubnetTx{ + Owner: subnetOwner, + }, + }, + } + + primaryNetworkPermissionlessStaker = &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + End: uint64(time.Now().Add(time.Hour).Unix()), + Wght: 2 * units.Avax, + }, + Subnet: constants.PrimaryNetworkID, + } + + testContextPreEtna = &builder.Context{ NetworkID: constants.UnitTestID, AVAXAssetID: avaxAssetID, - StaticFeeConfig: fee.StaticConfig{ + StaticFeeConfig: txfee.StaticConfig{ TxFee: units.MicroAvax, CreateSubnetTxFee: 19 * units.MicroAvax, TransformSubnetTxFee: 789 * units.MicroAvax, @@ -48,509 +106,442 @@ var ( AddSubnetDelegatorFee: 9 * units.Avax, }, } -) - -// These tests create a tx, then verify that utxos included in the tx are -// exactly necessary to pay fees for it. - -func TestBaseTx(t *testing.T) { - var ( - require = require.New(t) + staticFeeCalculator = txfee.NewStaticCalculator( + testContextPreEtna.StaticFeeConfig, + ) - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, - }) - backend = NewBackend(testContext, chainUTXOs, nil) + testContextPostEtna = &builder.Context{ + NetworkID: constants.UnitTestID, + AVAXAssetID: avaxAssetID, - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr), testContext, backend) + ComplexityWeights: feecomponent.Dimensions{ + feecomponent.Bandwidth: 1, + feecomponent.DBRead: 10, + feecomponent.DBWrite: 100, + feecomponent.Compute: 1000, + }, + GasPrice: 1, + } + dynamicFeeCalculator = txfee.NewDynamicCalculator( + testContextPostEtna.ComplexityWeights, + testContextPostEtna.GasPrice, + ) - // data to build the transaction - outputToMove = &avax.TransferableOutput{ - Asset: avax.Asset{ID: avaxAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: 7 * units.Avax, - OutputOwners: secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{utxoAddr}, - }, - }, - } + testEnvironmentPreEtna = []environment{ + { + name: "Pre-Etna", + context: testContextPreEtna, + feeCalculator: staticFeeCalculator, + }, + { + name: "Pre-Etna with memo", + context: testContextPreEtna, + feeCalculator: staticFeeCalculator, + memo: []byte("memo"), + }, + } + testEnvironmentPostEtna = []environment{ + { + name: "Post-Etna", + context: testContextPostEtna, + feeCalculator: dynamicFeeCalculator, + }, + { + name: "Post-Etna with memo", + context: testContextPostEtna, + feeCalculator: dynamicFeeCalculator, + memo: []byte("memo"), + }, + } + testEnvironment = utils.Join( + testEnvironmentPreEtna, + testEnvironmentPostEtna, ) +) - utx, err := builder.NewBaseTx([]*avax.TransferableOutput{outputToMove}) - require.NoError(err) +type environment struct { + name string + context *builder.Context + feeCalculator txfee.Calculator + memo []byte +} - // check that the output is included in the transaction - require.Contains(utx.Outs, outputToMove) +// These tests create a tx, then verify that utxos included in the tx are +// exactly necessary to pay fees for it. - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TxFee, - }, - ), - addInputAmounts(utx.Ins), - ) +func TestBaseTx(t *testing.T) { + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewBaseTx( + []*avax.TransferableOutput{avaxOutput}, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Contains(utx.Outs, avaxOutput) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx, + nil, + nil, + nil, + ) + }) + } } func TestAddSubnetValidatorTx(t *testing.T) { - var ( - require = require.New(t) + subnetValidator := &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + End: uint64(time.Now().Add(time.Hour).Unix()), + }, + Subnet: subnetID, + } - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewAddSubnetValidatorTx( + subnetValidator, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(*subnetValidator, utx.SubnetValidator) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, - }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - - // data to build the transaction - subnetValidator = &txs.SubnetValidator{ - Validator: txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - End: uint64(time.Now().Add(time.Hour).Unix()), - }, - Subnet: subnetID, - } - ) - - // build the transaction - utx, err := builder.NewAddSubnetValidatorTx(subnetValidator) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.AddSubnetValidatorFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func TestRemoveSubnetValidatorTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewRemoveSubnetValidatorTx( + nodeID, + subnetID, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(nodeID, utx.NodeID) + require.Equal(subnetID, utx.Subnet) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, - }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - ) - - // build the transaction - utx, err := builder.NewRemoveSubnetValidatorTx( - ids.GenerateTestNodeID(), - subnetID, - ) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TxFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func TestCreateChainTx(t *testing.T) { var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, - }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, - }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - - // data to build the transaction genesisBytes = []byte{'a', 'b', 'c'} vmID = ids.GenerateTestID() fxIDs = []ids.ID{ids.GenerateTestID()} chainName = "dummyChain" ) - // build the transaction - utx, err := builder.NewCreateChainTx( - subnetID, - genesisBytes, - vmID, - fxIDs, - chainName, - ) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.CreateBlockchainTxFee, - }, - ), - addInputAmounts(utx.Ins), - ) + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewCreateChainTx( + subnetID, + genesisBytes, + vmID, + fxIDs, + chainName, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(subnetID, utx.SubnetID) + require.Equal(genesisBytes, utx.GenesisData) + require.Equal(vmID, utx.VMID) + require.ElementsMatch(fxIDs, utx.FxIDs) + require.Equal(chainName, utx.ChainName) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) + }) + } } func TestCreateSubnetTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewCreateSubnetTx( + subnetOwner, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(subnetOwner, utx.Owner) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, - }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - ) - - // build the transaction - utx, err := builder.NewCreateSubnetTx(subnetOwner) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.CreateSubnetTxFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func TestTransferSubnetOwnershipTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewTransferSubnetOwnershipTx( + subnetID, + subnetOwner, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(subnetID, utx.Subnet) + require.Equal(subnetOwner, utx.Owner) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, - }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - ) - - // build the transaction - utx, err := builder.NewTransferSubnetOwnershipTx( - subnetID, - subnetOwner, - ) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TxFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func TestImportTx(t *testing.T) { var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) sourceChainID = ids.GenerateTestID() importedUTXOs = utxos[:1] - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, - sourceChainID: importedUTXOs, - }) - - backend = NewBackend(testContext, chainUTXOs, nil) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr), testContext, backend) - - // data to build the transaction - importKey = testKeys[0] - importTo = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - importKey.Address(), - }, - } ) - // build the transaction - utx, err := builder.NewImportTx( - sourceChainID, - importTo, - ) - require.NoError(err) - - require.Empty(utx.Ins) // we spend the imported input (at least partially) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TxFee, - }, - ), - addInputAmounts(utx.Ins, utx.ImportedInputs), - ) + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + sourceChainID: importedUTXOs, + }) + backend = NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewImportTx( + sourceChainID, + importOwner, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(sourceChainID, utx.SourceChain) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + require.Empty(utx.Ins) // The imported input should be sufficient for fees + require.Len(utx.ImportedInputs, len(importedUTXOs)) // All utxos should be imported + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + utx.ImportedInputs, + nil, + nil, + ) + }) + } } func TestExportTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + exportedOutputs := []*avax.TransferableOutput{avaxOutput} + + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewExportTx( + subnetID, + exportedOutputs, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(subnetID, utx.DestinationChain) + require.ElementsMatch(exportedOutputs, utx.ExportedOutputs) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + utx.ExportedOutputs, + nil, + ) }) - backend = NewBackend(testContext, chainUTXOs, nil) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr), testContext, backend) - - // data to build the transaction - subnetID = ids.GenerateTestID() - exportedOutputs = []*avax.TransferableOutput{{ - Asset: avax.Asset{ID: avaxAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: 7 * units.Avax, - OutputOwners: secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{utxoAddr}, - }, - }, - }} - ) - - // build the transaction - utx, err := builder.NewExportTx( - subnetID, - exportedOutputs, - ) - require.NoError(err) - - require.Equal(utx.ExportedOutputs, exportedOutputs) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs, utx.ExportedOutputs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TxFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func TestTransformSubnetTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, - }) - - subnetID = ids.GenerateTestID() - subnetAuthKey = testKeys[0] - subnetAuthAddr = subnetAuthKey.Address() - subnetOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{subnetAuthAddr}, - } - subnets = map[ids.ID]*txs.Tx{ - subnetID: { - Unsigned: &txs.CreateSubnetTx{ - Owner: subnetOwner, + const ( + initialSupply = 40 * units.MegaAvax + maxSupply = 100 * units.MegaAvax + minConsumptionRate uint64 = reward.PercentDenominator + maxConsumptionRate uint64 = reward.PercentDenominator + minValidatorStake uint64 = 1 + maxValidatorStake = 100 * units.MegaAvax + minStakeDuration = time.Second + maxStakeDuration = 365 * 24 * time.Hour + minDelegationFee uint32 = 0 + minDelegatorStake uint64 = 1 + maxValidatorWeightFactor byte = 5 + uptimeRequirement uint32 = .80 * reward.PercentDenominator + ) + + // TransformSubnetTx is not valid to be issued post-Etna + for _, e := range testEnvironmentPreEtna { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, subnets) + builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), e.context, backend) + ) + + utx, err := builder.NewTransformSubnetTx( + subnetID, + subnetAssetID, + initialSupply, + maxSupply, + minConsumptionRate, + maxConsumptionRate, + minValidatorStake, + maxValidatorStake, + minStakeDuration, + maxStakeDuration, + minDelegationFee, + minDelegatorStake, + maxValidatorWeightFactor, + uptimeRequirement, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(subnetID, utx.Subnet) + require.Equal(subnetAssetID, utx.AssetID) + require.Equal(initialSupply, utx.InitialSupply) + require.Equal(maxSupply, utx.MaximumSupply) + require.Equal(minConsumptionRate, utx.MinConsumptionRate) + require.Equal(minValidatorStake, utx.MinValidatorStake) + require.Equal(maxValidatorStake, utx.MaxValidatorStake) + require.Equal(uint32(minStakeDuration/time.Second), utx.MinStakeDuration) + require.Equal(uint32(maxStakeDuration/time.Second), utx.MaxStakeDuration) + require.Equal(minDelegationFee, utx.MinDelegationFee) + require.Equal(minDelegatorStake, utx.MinDelegatorStake) + require.Equal(maxValidatorWeightFactor, utx.MaxValidatorWeightFactor) + require.Equal(uptimeRequirement, utx.UptimeRequirement) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + map[ids.ID]uint64{ + subnetAssetID: maxSupply - initialSupply, }, - }, - } - - backend = NewBackend(testContext, chainUTXOs, subnets) - - // builder - utxoAddr = utxosKey.Address() - builder = builder.New(set.Of(utxoAddr, subnetAuthAddr), testContext, backend) - - // data to build the transaction - initialSupply = 40 * units.MegaAvax - maxSupply = 100 * units.MegaAvax - ) - - // build the transaction - utx, err := builder.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 - ) - require.NoError(err) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.TransformSubnetTxFee, - subnetAssetID: maxSupply - initialSupply, - }, - ), - addInputAmounts(utx.Ins), - ) + ) + }) + } } func TestAddPermissionlessValidatorTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosOffset uint64 = 2024 - utxosKey = testKeys[1] - utxosAddr = utxosKey.Address() - ) + var utxosOffset uint64 = 2024 makeUTXO := func(amount uint64) *avax.UTXO { utxosOffset++ return &avax.UTXO{ @@ -560,159 +551,128 @@ func TestAddPermissionlessValidatorTx(t *testing.T) { }, Asset: avax.Asset{ID: avaxAssetID}, Out: &secp256k1fx.TransferOutput{ - Amt: amount, - OutputOwners: secp256k1fx.OutputOwners{ - Locktime: 0, - Addrs: []ids.ShortID{utxosAddr}, - Threshold: 1, - }, + Amt: amount, + OutputOwners: utxoOwner, }, } } var ( utxos = []*avax.UTXO{ - makeUTXO(testContext.StaticFeeConfig.AddPrimaryNetworkValidatorFee), // UTXO to pay the fee - makeUTXO(1 * units.NanoAvax), // small UTXO - makeUTXO(9 * units.Avax), // large UTXO + makeUTXO(testContextPreEtna.StaticFeeConfig.AddPrimaryNetworkValidatorFee), // UTXO to pay the fee + makeUTXO(1 * units.NanoAvax), // small UTXO + makeUTXO(9 * units.Avax), // large UTXO } - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, - }) - backend = NewBackend(testContext, chainUTXOs, nil) - - // builder - utxoAddr = utxosKey.Address() - rewardKey = testKeys[0] - rewardAddr = rewardKey.Address() - builder = builder.New(set.Of(utxoAddr, rewardAddr), testContext, backend) - - // data to build the transaction - validationRewardsOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - rewardAddr, - }, - } - delegationRewardsOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - rewardAddr, - }, - } - ) - - sk, err := bls.NewSecretKey() - require.NoError(err) - - // build the transaction - utx, err := builder.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, - ) - require.NoError(err) - - // check stake amount - require.Equal( - map[ids.ID]uint64{ - avaxAssetID: 2 * units.Avax, - }, - addOutputAmounts(utx.StakeOuts), - ) - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs, utx.StakeOuts), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.AddPrimaryNetworkValidatorFee, - }, - ), - addInputAmounts(utx.Ins), + validationRewardsOwner = rewardsOwner + delegationRewardsOwner = rewardsOwner + delegationShares uint32 = reward.PercentDenominator ) - // Outputs should be merged if possible. For example, if there are two - // unlocked inputs consumed for staking, this should only produce one staked - // output. - require.Len(utx.StakeOuts, 1) + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + pop := signer.NewProofOfPossession(sk) + + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr, rewardAddr), e.context, backend) + ) + + utx, err := builder.NewAddPermissionlessValidatorTx( + primaryNetworkPermissionlessStaker, + pop, + avaxAssetID, + validationRewardsOwner, + delegationRewardsOwner, + delegationShares, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(primaryNetworkPermissionlessStaker.Validator, utx.Validator) + require.Equal(primaryNetworkPermissionlessStaker.Subnet, utx.Subnet) + require.Equal(pop, utx.Signer) + // Outputs should be merged if possible. For example, if there are two + // unlocked inputs consumed for staking, this should only produce one staked + // output. + require.Len(utx.StakeOuts, 1) + // check stake amount + require.Equal( + map[ids.ID]uint64{ + avaxAssetID: primaryNetworkPermissionlessStaker.Wght, + }, + addOutputAmounts(utx.StakeOuts), + ) + require.Equal(validationRewardsOwner, utx.ValidatorRewardsOwner) + require.Equal(delegationRewardsOwner, utx.DelegatorRewardsOwner) + require.Equal(delegationShares, utx.DelegationShares) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + utx.StakeOuts, + nil, + ) + }) + } } func TestAddPermissionlessDelegatorTx(t *testing.T) { - var ( - require = require.New(t) - - // backend - utxosKey = testKeys[1] - utxos = makeTestUTXOs(utxosKey) - chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ - constants.PlatformChainID: utxos, + for _, e := range testEnvironment { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr, rewardAddr), e.context, backend) + ) + + utx, err := builder.NewAddPermissionlessDelegatorTx( + primaryNetworkPermissionlessStaker, + avaxAssetID, + rewardsOwner, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(primaryNetworkPermissionlessStaker.Validator, utx.Validator) + require.Equal(primaryNetworkPermissionlessStaker.Subnet, utx.Subnet) + // check stake amount + require.Equal( + map[ids.ID]uint64{ + avaxAssetID: primaryNetworkPermissionlessStaker.Wght, + }, + addOutputAmounts(utx.StakeOuts), + ) + require.Equal(rewardsOwner, utx.DelegationRewardsOwner) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + utx.StakeOuts, + nil, + ) }) - backend = NewBackend(testContext, chainUTXOs, nil) - - // builder - utxoAddr = utxosKey.Address() - rewardKey = testKeys[0] - rewardAddr = rewardKey.Address() - builder = builder.New(set.Of(utxoAddr, rewardAddr), testContext, backend) - - // data to build the transaction - rewardsOwner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - rewardAddr, - }, - } - ) - - // build the transaction - utx, err := builder.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, - ) - require.NoError(err) - - // check stake amount - require.Equal( - map[ids.ID]uint64{ - avaxAssetID: 2 * units.Avax, - }, - addOutputAmounts(utx.StakeOuts), - ) - - // check fee calculation - require.Equal( - addAmounts( - addOutputAmounts(utx.Outs, utx.StakeOuts), - map[ids.ID]uint64{ - avaxAssetID: testContext.StaticFeeConfig.AddPrimaryNetworkDelegatorFee, - }, - ), - addInputAmounts(utx.Ins), - ) + } } func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*avax.UTXO { - // 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. + // 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. const utxosOffset uint64 = 2024 utxosAddr := utxosKey.Address() @@ -799,6 +759,32 @@ func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*avax.UTXO { } } +// requireFeeIsCorrect calculates the required fee for the unsigned transaction +// and verifies that the burned amount is exactly the required fee. +func requireFeeIsCorrect( + require *require.Assertions, + feeCalculator txfee.Calculator, + utx txs.UnsignedTx, + baseTx *avax.BaseTx, + additionalIns []*avax.TransferableInput, + additionalOuts []*avax.TransferableOutput, + additionalFee map[ids.ID]uint64, +) { + amountConsumed := addInputAmounts(baseTx.Ins, additionalIns) + amountProduced := addOutputAmounts(baseTx.Outs, additionalOuts) + + expectedFee, err := feeCalculator.CalculateFee(utx) + require.NoError(err) + expectedAmountBurned := addAmounts( + map[ids.ID]uint64{ + avaxAssetID: expectedFee, + }, + additionalFee, + ) + expectedAmountConsumed := addAmounts(amountProduced, expectedAmountBurned) + require.Equal(expectedAmountConsumed, amountConsumed) +} + func addAmounts(allAmounts ...map[ids.ID]uint64) map[ids.ID]uint64 { amounts := make(map[ids.ID]uint64) for _, amountsToAdd := range allAmounts {