diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 74f129f3a58d..f52a36ffb58f 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -4,6 +4,7 @@ package p import ( + "bytes" "context" "errors" "math" @@ -516,7 +517,7 @@ var _ = e2e.DescribePChain("[L1]", func() { }) var nextNonce uint64 - setWeight := func(validationID ids.ID, weight uint64) { + setWeight := func(validationID ids.ID, weight uint64, genesisValidatorBit int) { tc.By("creating the unsigned SubnetValidatorWeightMessage") unsignedSubnetValidatorWeight := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( networkID, @@ -550,7 +551,7 @@ var _ = e2e.DescribePChain("[L1]", func() { setSubnetValidatorWeight, err := warp.NewMessage( unsignedSubnetValidatorWeight, &warp.BitSetSignature{ - Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer + Signers: set.NewBits(genesisValidatorBit).Bytes(), // [signers] has weight from the genesis validator Signature: ([bls.SignatureLen]byte)( bls.SignatureToBytes(setSubnetValidatorWeightSignature), ), @@ -585,7 +586,9 @@ var _ = e2e.DescribePChain("[L1]", func() { } tc.By("increasing the weight of the validator", func() { - setWeight(registerValidationID, updatedWeight) + // Because registerValidationID is not active, the genesis validator + // is guaranteed to be index 0. + setWeight(registerValidationID, updatedWeight, 0) }) tc.By("verifying the validator weight was increased", func() { @@ -660,10 +663,43 @@ var _ = e2e.DescribePChain("[L1]", func() { }) }) + tc.By("issuing an IncreaseBalanceTx", func() { + _, err := pWallet.IssueIncreaseBalanceTx( + registerValidationID, + units.Avax, + ) + require.NoError(err) + }) + + tc.By("verifying the validator was activated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + subnetRegisterNode.NodeID: { + NodeID: subnetRegisterNode.NodeID, + PublicKey: registerNodePK, + Weight: updatedWeight, + }, + }) + }) + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) tc.By("removing the registered validator", func() { - setWeight(registerValidationID, 0) + // Because registerValidationID is active, we must calculate which + // bit the genesis validator should be in the warp message. + var ( + genesisValidatorPKBytes = bls.PublicKeyToUncompressedBytes(genesisNodePK) + registerValidatorPKBytes = bls.PublicKeyToUncompressedBytes(registerNodePK) + genesisValidatorBit int + ) + if bytes.Compare(genesisValidatorPKBytes, registerValidatorPKBytes) > 0 { + genesisValidatorBit = 1 + } + setWeight(registerValidationID, 0, genesisValidatorBit) }) tc.By("verifying the validator was removed", func() { diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index c194f603f97b..5077b63c15df 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -159,3 +159,10 @@ func (m *txMetrics) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) }).Inc() return nil } + +func (m *txMetrics) IncreaseBalanceTx(*txs.IncreaseBalanceTx) error { + m.numTxs.With(prometheus.Labels{ + txLabel: "increase_balance", + }).Inc() + return nil +} diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 51e56e380e99..dbe84b196a1a 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -125,5 +125,6 @@ func RegisterEtnaTypes(targetCodec linearcodec.Codec) error { targetCodec.RegisterType(&ConvertSubnetTx{}), targetCodec.RegisterType(&RegisterSubnetValidatorTx{}), targetCodec.RegisterType(&SetSubnetValidatorWeightTx{}), + targetCodec.RegisterType(&IncreaseBalanceTx{}), ) } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index a821cbfece1e..c4dcf2e20916 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -120,6 +120,10 @@ func (*atomicTxExecutor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeigh return ErrWrongTxType } +func (*atomicTxExecutor) IncreaseBalanceTx(*txs.IncreaseBalanceTx) error { + return ErrWrongTxType +} + func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error { return e.atomicTx() } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 21bfc1d249e8..101821bb0719 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -139,6 +139,10 @@ func (*proposalTxExecutor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWei return ErrWrongTxType } +func (*proposalTxExecutor) IncreaseBalanceTx(*txs.IncreaseBalanceTx) error { + return ErrWrongTxType +} + func (e *proposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { // AddValidatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddValidatorTxs must be issued into diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index b132409b12da..c78a803b3622 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -1093,6 +1093,78 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return nil } +func (e *standardTxExecutor) IncreaseBalanceTx(tx *txs.IncreaseBalanceTx) error { + var ( + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig + ) + if !upgrades.IsEtnaActivated(currentTimestamp) { + return errEtnaUpgradeNotActive + } + + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err + } + + fee, err = math.Add(fee, tx.Balance) + if err != nil { + return err + } + + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + tx.Outs, + e.tx.Creds, + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + + sov, err := e.state.GetSubnetOnlyValidator(tx.ValidationID) + if err != nil { + return err + } + + // If the validator is currently inactive, we are activating it. + if sov.EndAccumulatedFee == 0 { + if gas.Gas(e.state.NumActiveSubnetOnlyValidators()) >= e.backend.Config.ValidatorFeeConfig.Capacity { + return errMaxNumActiveValidators + } + + sov.EndAccumulatedFee = e.state.GetAccruedFees() + } + sov.EndAccumulatedFee, err = math.Add(sov.EndAccumulatedFee, tx.Balance) + if err != nil { + return err + } + + if err := e.state.PutSubnetOnlyValidator(sov); err != nil { + return err + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + return nil +} + // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 8682c3b06e2f..43d53c284636 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3720,6 +3720,301 @@ func TestStandardExecutorSetSubnetValidatorWeightTx(t *testing.T) { } } +func TestStandardExecutorIncreaseBalanceTx(t *testing.T) { + var ( + fx = &secp256k1fx.Fx{} + vm = &secp256k1fx.TestVM{ + Log: logging.NoLog{}, + } + ) + require.NoError(t, fx.InitializeVM(vm)) + + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) + defaultConfig = &config.Internal{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + baseState = statetest.New(t, statetest.Config{ + Upgrades: defaultConfig.UpgradeConfig, + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // validationIDs + nil, // chainIDs + ) + flowChecker = utxo.NewVerifier( + ctx, + &vm.Clk, + fx, + ) + + backend = &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + } + feeCalculator = state.PickFeeCalculator(defaultConfig, baseState) + ) + + // Create the initial state + diff, err := state.NewDiffOn(baseState) + require.NoError(t, err) + + // Create the subnet + createSubnetTx, err := wallet.IssueCreateSubnetTx( + &secp256k1fx.OutputOwners{}, + ) + require.NoError(t, err) + + // Execute the subnet creation + _, _, _, err = StandardTx( + backend, + feeCalculator, + createSubnetTx, + diff, + ) + require.NoError(t, err) + + // Create the subnet conversion + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + weight = 1 + initialBalance uint64 = 0 + ) + var ( + subnetID = createSubnetTx.ID() + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + validator = &txs.ConvertSubnetValidator{ + NodeID: ids.GenerateTestNodeID().Bytes(), + Weight: weight, + Balance: initialBalance, + Signer: *signer.NewProofOfPossession(sk), + // RemainingBalanceOwner and DeactivationOwner are initialized so + // that later reflect based equality checks pass. + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 0, + Addresses: []ids.ShortID{}, + }, + DeactivationOwner: message.PChainOwner{ + Threshold: 0, + Addresses: []ids.ShortID{}, + }, + } + validationID = subnetID.Append(0) + ) + + convertSubnetTx, err := wallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + validator, + }, + ) + require.NoError(t, err) + + // Execute the subnet conversion + _, _, _, err = StandardTx( + backend, + feeCalculator, + convertSubnetTx, + diff, + ) + require.NoError(t, err) + require.NoError(t, diff.Apply(baseState)) + require.NoError(t, baseState.Commit()) + + initialSoV, err := baseState.GetSubnetOnlyValidator(validationID) + require.NoError(t, err) + + const balanceIncrease = units.NanoAvax + tests := []struct { + name string + validationID ids.ID + builderOptions []common.Option + updateTx func(*txs.IncreaseBalanceTx) + updateExecutor func(*standardTxExecutor) error + expectedBalance uint64 + expectedErr error + }{ + { + name: "invalid prior to E-Upgrade", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Internal{ + UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), + } + return nil + }, + expectedErr: errEtnaUpgradeNotActive, + }, + { + name: "tx fails syntactic verification", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + return nil + }, + expectedErr: avax.ErrWrongChainID, + }, + { + name: "invalid memo length", + builderOptions: []common.Option{ + common.WithMemo([]byte("memo!")), + }, + expectedErr: avax.ErrMemoTooLarge, + }, + { + name: "invalid fee calculation", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) + return nil + }, + expectedErr: txfee.ErrUnsupportedTx, + }, + { + name: "fee overflow", + updateTx: func(tx *txs.IncreaseBalanceTx) { + tx.Balance = math.MaxUint64 + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "insufficient fee", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, + 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, + ) + return nil + }, + expectedErr: utxo.ErrInsufficientUnlockedFunds, + }, + { + name: "unknown validationID", + validationID: ids.GenerateTestID(), + expectedErr: database.ErrNotFound, + }, + { + name: "too many active validators", + validationID: validationID, + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Internal{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: validatorfee.Config{ + Capacity: 0, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + return nil + }, + expectedErr: errMaxNumActiveValidators, + }, + { + name: "accumulated fees overflow", + validationID: validationID, + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetAccruedFees(math.MaxUint64) + return nil + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "increase balance", + validationID: validationID, + expectedBalance: baseState.GetAccruedFees() + balanceIncrease, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Create the IncreaseBalanceTx + wallet := txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // validationIDs + nil, // chainIDs + ) + + increaseBalanceTx, err := wallet.IssueIncreaseBalanceTx( + test.validationID, + balanceIncrease, + test.builderOptions..., + ) + require.NoError(err) + + unsignedTx := increaseBalanceTx.Unsigned.(*txs.IncreaseBalanceTx) + if test.updateTx != nil { + test.updateTx(unsignedTx) + } + + diff, err := state.NewDiffOn(baseState) + require.NoError(err) + + executor := &standardTxExecutor{ + backend: &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + }, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: increaseBalanceTx, + state: diff, + } + if test.updateExecutor != nil { + require.NoError(test.updateExecutor(executor)) + } + + err = unsignedTx.Visit(executor) + require.ErrorIs(err, test.expectedErr) + if err != nil { + return + } + + for utxoID := range increaseBalanceTx.InputIDs() { + _, err := diff.GetUTXO(utxoID) + require.ErrorIs(err, database.ErrNotFound) + } + + baseTxOutputUTXOs := increaseBalanceTx.UTXOs() + for _, expectedUTXO := range baseTxOutputUTXOs { + utxoID := expectedUTXO.InputID() + utxo, err := diff.GetUTXO(utxoID) + require.NoError(err) + require.Equal(expectedUTXO, utxo) + } + + sov, err := diff.GetSubnetOnlyValidator(validationID) + require.NoError(err) + + expectedSoV := initialSoV + expectedSoV.EndAccumulatedFee = test.expectedBalance + require.Equal(expectedSoV, sov) + }) + } +} + func must[T any](t require.TestingT) func(T, error) T { return func(val T, err error) T { require.NoError(t, err) diff --git a/vms/platformvm/txs/executor/warp_verifier.go b/vms/platformvm/txs/executor/warp_verifier.go index 731cac7b3827..133456699e00 100644 --- a/vms/platformvm/txs/executor/warp_verifier.go +++ b/vms/platformvm/txs/executor/warp_verifier.go @@ -106,6 +106,10 @@ func (*warpVerifier) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return nil } +func (*warpVerifier) IncreaseBalanceTx(*txs.IncreaseBalanceTx) error { + return nil +} + func (w *warpVerifier) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { return w.verify(tx.Message) } diff --git a/vms/platformvm/txs/executor/warp_verifier_test.go b/vms/platformvm/txs/executor/warp_verifier_test.go index fd925f4f7825..d7fe9b8e5e9f 100644 --- a/vms/platformvm/txs/executor/warp_verifier_test.go +++ b/vms/platformvm/txs/executor/warp_verifier_test.go @@ -190,6 +190,10 @@ func TestVerifyWarpMessages(t *testing.T) { Message: validWarpMessage.Bytes(), }, }, + { + name: "IncreaseBalanceTx", + tx: &txs.IncreaseBalanceTx{}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/vms/platformvm/txs/fee/calculator_test.go b/vms/platformvm/txs/fee/calculator_test.go index 9a3a3f2d1508..cfd17b97330f 100644 --- a/vms/platformvm/txs/fee/calculator_test.go +++ b/vms/platformvm/txs/fee/calculator_test.go @@ -255,5 +255,17 @@ var ( }, expectedDynamicFee: 131_800, }, + { + name: "IncreaseBalanceTx", + tx: "00000000002600003039000000000000000000000000000000000000000000000000000000000000000000000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007002386f1f88b4e52000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001f61ea7e3bb6d33da9901644f3c623e4537b7d1c276e9ef23bcc8e4150e494d6600000000dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000005002386f1f88b510000000001000000000000000038e6e9fe31c6d070a8c792dbacf6d0aefb8eac2aded49cc0aa9f422d1fdd9ecd0000000000000002000000010000000900000001cb56b56387be9186d86430fad5418db4d13e991b6805b6ba178b719e3f47ce001da52d6ed3173bfdd8b69940a135432abce493a10332e881f6c34cea3617595e00", + expectedStaticFeeErr: ErrUnsupportedTx, + expectedComplexity: gas.Dimensions{ + gas.Bandwidth: 339, // The length of the tx in bytes + gas.DBRead: IntrinsicIncreaseBalanceTxComplexities[gas.DBRead] + intrinsicInputDBRead, + gas.DBWrite: IntrinsicIncreaseBalanceTxComplexities[gas.DBWrite] + intrinsicInputDBWrite + intrinsicOutputDBWrite, + gas.Compute: 0, // TODO: implement + }, + expectedDynamicFee: 113_900, + }, } ) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 9bf0b2e2b3c2..dadd51b16552 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -214,6 +214,14 @@ var ( gas.DBWrite: 0, // TODO gas.Compute: 0, } + IntrinsicIncreaseBalanceTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + ids.IDLen + // validationID + wrappers.LongLen, // balance + gas.DBRead: 0, // TODO + gas.DBWrite: 0, // TODO + gas.Compute: 0, + } errUnsupportedOutput = errors.New("unsupported output type") errUnsupportedInput = errors.New("unsupported input type") @@ -744,6 +752,17 @@ func (c *complexityVisitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidato return err } +func (c *complexityVisitor) IncreaseBalanceTx(tx *txs.IncreaseBalanceTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + c.output, err = IntrinsicIncreaseBalanceTxComplexities.Add( + &baseTxComplexity, + ) + return err +} + func baseTxComplexity(tx *txs.BaseTx) (gas.Dimensions, error) { outputsComplexity, err := OutputComplexity(tx.Outs...) if err != nil { diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index c3846708f6bc..1a77602898a1 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -59,6 +59,10 @@ func (*staticVisitor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx return ErrUnsupportedTx } +func (*staticVisitor) IncreaseBalanceTx(*txs.IncreaseBalanceTx) error { + return ErrUnsupportedTx +} + func (c *staticVisitor) AddValidatorTx(*txs.AddValidatorTx) error { c.fee = c.config.AddPrimaryNetworkValidatorFee return nil diff --git a/vms/platformvm/txs/increase_balance_tx.go b/vms/platformvm/txs/increase_balance_tx.go new file mode 100644 index 000000000000..fd2cc25abcb0 --- /dev/null +++ b/vms/platformvm/txs/increase_balance_tx.go @@ -0,0 +1,49 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" +) + +var ( + _ UnsignedTx = (*IncreaseBalanceTx)(nil) + + ErrZeroBalance = errors.New("balance must be greater than 0") +) + +type IncreaseBalanceTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // ID corresponding to the validator + ValidationID ids.ID `serialize:"true" json:"validationID"` + // Balance <= sum($AVAX inputs) - sum($AVAX outputs) - TxFee + Balance uint64 `serialize:"true" json:"balance"` +} + +func (tx *IncreaseBalanceTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: + // already passed syntactic verification + return nil + case tx.Balance == 0: + return ErrZeroBalance + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return err + } + + tx.SyntacticallyVerified = true + return nil +} + +func (tx *IncreaseBalanceTx) Visit(visitor Visitor) error { + return visitor.IncreaseBalanceTx(tx) +} diff --git a/vms/platformvm/txs/increase_balance_tx_test.go b/vms/platformvm/txs/increase_balance_tx_test.go new file mode 100644 index 000000000000..2a5fd1a20a64 --- /dev/null +++ b/vms/platformvm/txs/increase_balance_tx_test.go @@ -0,0 +1,389 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + _ "embed" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" +) + +//go:embed increase_balance_tx_test.json +var increaseBalanceTxJSON []byte + +func TestIncreaseBalanceTxSerialization(t *testing.T) { + require := require.New(t) + + var ( + validationID = ids.ID{ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + } + balance uint64 = 0xfedcba9876543210 + addr = ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + avaxAssetID = ids.ID{ + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + } + customAssetID = ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + txID = ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + ) + + var unsignedTx UnsignedTx = &IncreaseBalanceTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.UnitTestID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, + }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), + }, + }, + ValidationID: validationID, + Balance: balance, + } + txBytes, err := Codec.Marshal(CodecVersion, &unsignedTx) + require.NoError(err) + + expectedBytes := []byte{ + // Codec version + 0x00, 0x00, + // IncreaseBalanceTx Type ID + 0x00, 0x00, 0x00, 0x26, + // Network ID + 0x00, 0x00, 0x00, 0x0a, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + // validation ID + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + // balance + 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10, + } + require.Equal(expectedBytes, txBytes) + + ctx := snowtest.Context(t, constants.PlatformChainID) + unsignedTx.InitCtx(ctx) + + txJSON, err := json.MarshalIndent(unsignedTx, "", "\t") + require.NoError(err) + require.Equal( + // Normalize newlines for Windows + strings.ReplaceAll(string(increaseBalanceTxJSON), "\r\n", "\n"), + string(txJSON), + ) +} + +func TestIncreaseBalanceTxSyntacticVerify(t *testing.T) { + var ( + ctx = snowtest.Context(t, ids.GenerateTestID()) + validBaseTx = BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }, + } + ) + tests := []struct { + name string + tx *IncreaseBalanceTx + expectedErr error + }{ + { + name: "nil tx", + tx: nil, + expectedErr: ErrNilTx, + }, + { + name: "already verified", + // The tx includes invalid data to verify that a cached result is + // returned. + tx: &IncreaseBalanceTx{ + BaseTx: BaseTx{ + SyntacticallyVerified: true, + }, + Balance: 0, + }, + expectedErr: nil, + }, + { + name: "zero balance", + tx: &IncreaseBalanceTx{ + BaseTx: validBaseTx, + Balance: 0, + }, + expectedErr: ErrZeroBalance, + }, + { + name: "invalid BaseTx", + tx: &IncreaseBalanceTx{ + BaseTx: BaseTx{}, + Balance: 1, + }, + expectedErr: avax.ErrWrongNetworkID, + }, + { + name: "passes verification", + tx: &IncreaseBalanceTx{ + BaseTx: validBaseTx, + Balance: 1, + }, + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.tx.SyntacticVerify(ctx) + require.ErrorIs(err, test.expectedErr) + if test.expectedErr != nil { + return + } + require.True(test.tx.SyntacticallyVerified) + }) + } +} diff --git a/vms/platformvm/txs/increase_balance_tx_test.json b/vms/platformvm/txs/increase_balance_tx_test.json new file mode 100644 index 000000000000..662d39a82008 --- /dev/null +++ b/vms/platformvm/txs/increase_balance_tx_test.json @@ -0,0 +1,77 @@ +{ + "networkID": 10, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-testing1g32kvaugnx4tk3z4vemc3xd2hdz92enhgrdu9n" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "inputs": [ + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1152921504606846976, + "signatureIndices": [] + } + } + ], + "memo": "0xf09f98850a77656c6c2074686174277301234521", + "validationID": "W4exHFQ41XUp8noMjTtReLTLt5X7fBcbNUzERLBHVgnd65HY", + "balance": 18364758544493064720 +} \ No newline at end of file diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 6aa766e1e3ea..fbaf2a12124d 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -30,4 +30,5 @@ type Visitor interface { ConvertSubnetTx(*ConvertSubnetTx) error RegisterSubnetValidatorTx(*RegisterSubnetValidatorTx) error SetSubnetValidatorWeightTx(*SetSubnetValidatorWeightTx) error + IncreaseBalanceTx(*IncreaseBalanceTx) error } diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index cb58b2dc2485..3d4ad8b8e381 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -188,6 +188,17 @@ type Builder interface { options ...common.Option, ) (*txs.SetSubnetValidatorWeightTx, error) + // NewIncreaseBalanceTx increases the balance of a validator on an L1 for + // the continuous fee. + // + // - [validationID] of the validator + // - [balance] amount to increase the validator's balance by + NewIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, + ) (*txs.IncreaseBalanceTx, error) + // NewImportTx creates an import transaction that attempts to consume all // the available UTXOs and import the funds to [to]. // @@ -994,6 +1005,55 @@ func (b *builder) NewSetSubnetValidatorWeightTx( return tx, b.initCtx(tx) } +func (b *builder) NewIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, +) (*txs.IncreaseBalanceTx, error) { + var ( + toBurn = map[ids.ID]uint64{ + b.context.AVAXAssetID: balance, + } + toStake = map[ids.ID]uint64{} + ops = common.NewOptions(options) + memo = ops.Memo() + memoComplexity = gas.Dimensions{ + gas.Bandwidth: uint64(len(memo)), + } + ) + complexity, err := fee.IntrinsicIncreaseBalanceTxComplexities.Add( + &memoComplexity, + ) + if err != nil { + return nil, err + } + + inputs, outputs, _, err := b.spend( + toBurn, + toStake, + 0, + complexity, + nil, + ops, + ) + if err != nil { + return nil, err + } + + tx := &txs.IncreaseBalanceTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.context.NetworkID, + BlockchainID: constants.PlatformChainID, + Ins: inputs, + Outs: outputs, + Memo: memo, + }}, + ValidationID: validationID, + Balance: balance, + } + return tx, b.initCtx(tx) +} + func (b *builder) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/builder/with_options.go b/wallet/chain/p/builder/with_options.go index 86dab193c07b..71534764915e 100644 --- a/wallet/chain/p/builder/with_options.go +++ b/wallet/chain/p/builder/with_options.go @@ -195,6 +195,18 @@ func (w *withOptions) NewSetSubnetValidatorWeightTx( ) } +func (w *withOptions) NewIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, +) (*txs.IncreaseBalanceTx, error) { + return w.builder.NewIncreaseBalanceTx( + validationID, + balance, + common.UnionOptions(w.options, options)..., + ) +} + func (w *withOptions) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index 9a3cb1001401..74be5a8324ce 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -931,6 +931,44 @@ func TestSetSubnetValidatorWeightTx(t *testing.T) { } } +func TestIncreaseIncreaseBalanceTx(t *testing.T) { + const balance = units.Avax + validationID := ids.GenerateTestID() + for _, e := range testEnvironmentPostEtna { + 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 = wallet.NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewIncreaseBalanceTx( + validationID, + balance, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(validationID, utx.ValidationID) + require.Equal(balance, utx.Balance) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + map[ids.ID]uint64{ + e.context.AVAXAssetID: balance, // Balance increase + }, + ) + }) + } +} + 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 diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index a155b351faa3..673890158b9a 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -214,6 +214,14 @@ func (s *visitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) return sign(s.tx, true, txSigners) } +func (s *visitor) IncreaseBalanceTx(tx *txs.IncreaseBalanceTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, true, txSigners) +} + func (s *visitor) getSigners(sourceChainID ids.ID, ins []*avax.TransferableInput) ([][]keychain.Signer, error) { txSigners := make([][]keychain.Signer, len(ins)) for credIndex, transferInput := range ins { diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index 8df2ba7962bf..a89844db2f26 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -164,6 +164,10 @@ func (b *backendVisitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWe return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) IncreaseBalanceTx(tx *txs.IncreaseBalanceTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index 26ad5a4b5a97..f959aa8ac189 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -176,6 +176,17 @@ type Wallet interface { options ...common.Option, ) (*txs.Tx, error) + // IssueIncreaseBalanceTx creates, signs, and issues a transaction that + // increases the balance of a validator on an L1 for the continuous fee. + // + // - [validationID] of the validator + // - [balance] amount to increase the validator's balance by + IssueIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, + ) (*txs.Tx, error) + // IssueImportTx creates, signs, and issues an import transaction that // attempts to consume all the available UTXOs and import the funds to [to]. // @@ -454,6 +465,18 @@ func (w *wallet) IssueSetSubnetValidatorWeightTx( return w.IssueUnsignedTx(utx, options...) } +func (w *wallet) IssueIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, +) (*txs.Tx, error) { + utx, err := w.builder.NewIncreaseBalanceTx(validationID, balance, options...) + if err != nil { + return nil, err + } + return w.IssueUnsignedTx(utx, options...) +} + func (w *wallet) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/wallet/with_options.go b/wallet/chain/p/wallet/with_options.go index 1b43e3832b15..50aa1191f09a 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -184,6 +184,18 @@ func (w *withOptions) IssueSetSubnetValidatorWeightTx( ) } +func (w *withOptions) IssueIncreaseBalanceTx( + validationID ids.ID, + balance uint64, + options ...common.Option, +) (*txs.Tx, error) { + return w.wallet.IssueIncreaseBalanceTx( + validationID, + balance, + common.UnionOptions(w.options, options)..., + ) +} + func (w *withOptions) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/subnet/primary/examples/increase-balance/main.go b/wallet/subnet/primary/examples/increase-balance/main.go new file mode 100644 index 000000000000..8374c4888a93 --- /dev/null +++ b/wallet/subnet/primary/examples/increase-balance/main.go @@ -0,0 +1,56 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "log" + "time" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +func main() { + key := genesis.EWOQKey + uri := primary.LocalAPIURI + kc := secp256k1fx.NewKeychain(key) + validationID := ids.FromStringOrPanic("9FAftNgNBrzHUMMApsSyV6RcFiL9UmCbvsCu28xdLV2mQ7CMo") + balance := uint64(2) + + ctx := context.Background() + + // MakeWallet fetches the available UTXOs owned by [kc] on the network that + // [uri] is hosting. + walletSyncStartTime := time.Now() + wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ + URI: uri, + AVAXKeychain: kc, + EthKeychain: kc, + }) + if err != nil { + log.Fatalf("failed to initialize wallet: %s\n", err) + } + log.Printf("synced wallet in %s\n", time.Since(walletSyncStartTime)) + + // Get the P-chain wallet + pWallet := wallet.P() + + increaseBalanceStartTime := time.Now() + increaseBalanceTx, err := pWallet.IssueIncreaseBalanceTx( + validationID, + balance, + ) + if err != nil { + log.Fatalf("failed to issue increase balance transaction: %s\n", err) + } + log.Printf("increased balance of validationID %s by %d with %s in %s\n", + validationID, + balance, + increaseBalanceTx.ID(), + time.Since(increaseBalanceStartTime), + ) +}