diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index de4aaa11a08..43d67d271f1 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -50,6 +50,7 @@ const ( genesisWeight = units.Schmeckle genesisBalance = units.Avax registerWeight = genesisWeight / 10 + updatedWeight = 2 * registerWeight registerBalance = 0 ) @@ -278,6 +279,7 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { registerWeight, ) require.NoError(err) + registerValidationID := registerSubnetValidatorMessage.ValidationID() tc.By("registering the validator", func() { tc.By("creating the unsigned warp message") @@ -346,6 +348,100 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) }) + var nextNonce uint64 + setWeight := func(validationID ids.ID, weight uint64) { + tc.By("creating the unsigned warp message") + unsignedSubnetValidatorWeight := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + chainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + address, + must[*warpmessage.SubnetValidatorWeight](tc)(warpmessage.NewSubnetValidatorWeight( + validationID, + nextNonce, + weight, + )).Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + setSubnetValidatorWeightRequest, err := wrapWarpSignatureRequest( + unsignedSubnetValidatorWeight, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), setSubnetValidatorWeightRequest)) + }) + + tc.By("getting the signature response") + setSubnetValidatorWeightSignature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + + tc.By("creating the signed warp message to increase the weight of the validator") + signers := set.NewBits() + signers.Add(0) // [signers] has weight from the genesis peer + + var sigBytes [bls.SignatureLen]byte + copy(sigBytes[:], bls.SignatureToBytes(setSubnetValidatorWeightSignature)) + registerSubnetValidator, err := warp.NewMessage( + unsignedSubnetValidatorWeight, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + require.NoError(err) + + tc.By("issuing a SetSubnetValidatorWeightTx", func() { + _, err := pWallet.IssueSetSubnetValidatorWeightTx( + registerSubnetValidator.Bytes(), + ) + require.NoError(err) + }) + + nextNonce++ + } + + tc.By("increasing the weight of the validator", func() { + setWeight(registerValidationID, updatedWeight) + }) + + tc.By("verifying the validator weight was increased", func() { + tc.By("verifying the validator set was updated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + ids.EmptyNodeID: { // The validator is not active + NodeID: ids.EmptyNodeID, + Weight: updatedWeight, + }, + }) + }) + }) + + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) + + tc.By("removing the registered validator", func() { + setWeight(registerValidationID, 0) + }) + + tc.By("verifying the validator was removed", func() { + tc.By("verifying the validator set was updated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + }) + }) + }) + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) }) }) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 1da84206cf0..c194f603f97 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -152,3 +152,10 @@ func (m *txMetrics) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) er }).Inc() return nil } + +func (m *txMetrics) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) error { + m.numTxs.With(prometheus.Labels{ + txLabel: "set_subnet_validator_weight", + }).Inc() + return nil +} diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 4c389275355..51e56e380e9 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -124,5 +124,6 @@ func RegisterEtnaTypes(targetCodec linearcodec.Codec) error { return errors.Join( targetCodec.RegisterType(&ConvertSubnetTx{}), targetCodec.RegisterType(&RegisterSubnetValidatorTx{}), + targetCodec.RegisterType(&SetSubnetValidatorWeightTx{}), ) } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 24bfeda7a8f..7327766b536 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -90,6 +90,10 @@ func (*AtomicTxExecutor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorT return ErrWrongTxType } +func (*AtomicTxExecutor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) error { + return ErrWrongTxType +} + func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 31a84bafecc..93b2f5aff50 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -107,6 +107,10 @@ func (*ProposalTxExecutor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidato return ErrWrongTxType } +func (*ProposalTxExecutor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) 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 497928a9c2a..fc87daeefb3 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -16,7 +16,6 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" @@ -28,6 +27,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + safemath "github.com/ava-labs/avalanchego/utils/math" ) const ( @@ -47,6 +49,9 @@ var ( errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errRemovingLastValidator = errors.New("attempting to remove the last SoV from a converted subnet") + + errStateCorruption = errors.New("state corruption") ) type StandardTxExecutor struct { @@ -585,12 +590,12 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return errMaxNumActiveValidators } - sov.EndAccumulatedFee, err = math.Add(vdr.Balance, currentFees) + sov.EndAccumulatedFee, err = safemath.Add(vdr.Balance, currentFees) if err != nil { return err } - fee, err = math.Add(fee, vdr.Balance) + fee, err = safemath.Add(fee, vdr.Balance) if err != nil { return err } @@ -664,7 +669,7 @@ func (e *StandardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal if err != nil { return err } - fee, err = math.Add(fee, tx.Balance) + fee, err = safemath.Add(fee, tx.Balance) if err != nil { return err } @@ -718,7 +723,7 @@ func (e *StandardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal if msg.Expiry <= currentTimestampUnix { return fmt.Errorf("expected expiry to be after %d but got %d", currentTimestampUnix, msg.Expiry) } - maxAllowedExpiry, err := math.Add(currentTimestampUnix, RegisterSubnetValidatorTxExpiryWindow) + maxAllowedExpiry, err := safemath.Add(currentTimestampUnix, RegisterSubnetValidatorTxExpiryWindow) if err != nil { // This should never happen, as it would imply that either // currentTimestampUnix or RegisterSubnetValidatorTxExpiryWindow is @@ -784,7 +789,7 @@ func (e *StandardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal } currentFees := e.State.GetAccruedFees() - sov.EndAccumulatedFee, err = math.Add(tx.Balance, currentFees) + sov.EndAccumulatedFee, err = safemath.Add(tx.Balance, currentFees) if err != nil { return err } @@ -805,6 +810,146 @@ func (e *StandardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return nil } +func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { + var ( + currentTimestamp = e.State.GetTimestamp() + upgrades = e.Backend.Config.UpgradeConfig + ) + if !upgrades.IsEtnaActivated(currentTimestamp) { + return errEtnaUpgradeNotActive + } + + if err := e.Tx.SyntacticVerify(e.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 + } + + if err := e.Backend.FlowChecker.VerifySpend( + tx, + e.State, + tx.Ins, + tx.Outs, + e.Tx.Creds, + map[ids.ID]uint64{ + e.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + + warpMessage, err := warp.ParseMessage(tx.Message) + if err != nil { + return err + } + if warpMessage.NetworkID != e.Ctx.NetworkID { + return fmt.Errorf("expected networkID %d but got %d", e.Ctx.NetworkID, warpMessage.NetworkID) + } + + addressedCall, err := payload.ParseAddressedCall(warpMessage.Payload) + if err != nil { + return err + } + + msg, err := message.ParseSubnetValidatorWeight(addressedCall.Payload) + if err != nil { + return err + } + if err := msg.Verify(); err != nil { + return err + } + + sov, err := e.State.GetSubnetOnlyValidator(msg.ValidationID) + if err != nil { + return err + } + if msg.Nonce < sov.MinNonce { + return fmt.Errorf("expected nonce to be at least %d but got %d", sov.MinNonce, msg.Nonce) + } + + subnetConversion, err := e.State.GetSubnetConversion(sov.SubnetID) + if err != nil { + return err + } + if warpMessage.SourceChainID != subnetConversion.ChainID { + return fmt.Errorf("expected chainID %s but got %s", subnetConversion.ChainID, warpMessage.SourceChainID) + } + if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { + return fmt.Errorf("expected address %s but got %s", subnetConversion.Addr, addressedCall.SourceAddress) + } + + txID := e.Tx.ID() + + // We are removing the validator + if msg.Weight == 0 { + weight, err := e.State.WeightOfSubnetOnlyValidators(sov.SubnetID) + if err != nil { + return err + } + if weight == sov.Weight { + return errRemovingLastValidator + } + + // The validator is currently active, we need to refund the remaining + // balance. + if sov.EndAccumulatedFee != 0 { + var remainingBalanceOwner message.PChainOwner + if _, err := txs.Codec.Unmarshal(sov.RemainingBalanceOwner, &remainingBalanceOwner); err != nil { + return err + } + + accruedFees := e.State.GetAccruedFees() + if sov.EndAccumulatedFee <= accruedFees { + // This check should be unreachable. However, including it ensures + // that AVAX can't get minted out of thin air due to state + // corruption. + return fmt.Errorf("%w: validator should have already been disabled", errStateCorruption) + } + remainingBalance := sov.EndAccumulatedFee - accruedFees + + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(tx.Outs)), + }, + Asset: avax.Asset{ + ID: e.Ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: remainingBalance, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: remainingBalanceOwner.Threshold, + Addrs: remainingBalanceOwner.Addresses, + }, + }, + } + e.State.AddUTXO(utxo) + } + } + + // If the weight is being set to 0, the validator is being removed and the + // nonce doesn't matter. + sov.MinNonce = msg.Nonce + 1 + sov.Weight = msg.Weight + if err := e.State.PutSubnetOnlyValidator(sov); err != nil { + return err + } + + // Consume the UTXOS + avax.Consume(e.State, tx.Ins) + // Produce the UTXOS + avax.Produce(e.State, txID, tx.Outs) + return nil +} + func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( e.Backend, diff --git a/vms/platformvm/txs/fee/calculator_test.go b/vms/platformvm/txs/fee/calculator_test.go index dd71649fecd..9a3a3f2d150 100644 --- a/vms/platformvm/txs/fee/calculator_test.go +++ b/vms/platformvm/txs/fee/calculator_test.go @@ -243,5 +243,17 @@ var ( }, expectedDynamicFee: 151_000, }, + { + name: "SetSubnetValidatorWeightTx", + tx: "00000000002500003039000000000000000000000000000000000000000000000000000000000000000000000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007002386f1f88b5100000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001389c41b6ed301e4c118bd23673268fd2054b772efcf25685a117b74bab7ae5e400000000dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000005002386f1f88b552a000000010000000000000000000000d7000000003039705f3d4415f990225d3df5ce437d7af2aa324b1bbce854ee34ab6f39882250d200000044000000000001000000000000003600000000000338e6e9fe31c6d070a8c792dbacf6d0aefb8eac2aded49cc0aa9f422d1fdd9ecd0000000000000001000000000000000500000000000000010187f4bb2c42869c56f023a1ca81045aff034acd490b8f15b5069025f982e605e077007fc588f7d56369a65df7574df3b70ff028ea173739c789525ab7eebfcb5c115b13cca8f02b362104b700c75bc95234109f3f1360ddcb4ec3caf6b0e821cb0000000100000009000000010a29f3c86d52908bf2efbc3f918a363df704c429d66c8d6615712a2a584a2a5f264a9e7b107c07122a06f31cadc2f51285884d36fe8df909a07467417f1d64cf00", + expectedStaticFeeErr: ErrUnsupportedTx, + expectedComplexity: gas.Dimensions{ + gas.Bandwidth: 518, // The length of the tx in bytes + gas.DBRead: IntrinsicSetSubnetValidatorWeightTxComplexities[gas.DBRead] + intrinsicInputDBRead, + gas.DBWrite: IntrinsicSetSubnetValidatorWeightTxComplexities[gas.DBWrite] + intrinsicInputDBWrite + intrinsicOutputDBWrite, + gas.Compute: 0, // TODO: implement + }, + expectedDynamicFee: 131_800, + }, } ) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 0c553f488bb..9601d7ed5a2 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -205,6 +205,13 @@ var ( gas.DBWrite: 0, // TODO gas.Compute: 0, // TODO: Include PoP verification time } + IntrinsicSetSubnetValidatorWeightTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + wrappers.IntLen, // message length + gas.DBRead: 0, // TODO + gas.DBWrite: 0, // TODO + gas.Compute: 0, + } errUnsupportedOutput = errors.New("unsupported output type") errUnsupportedInput = errors.New("unsupported input type") @@ -719,6 +726,22 @@ func (c *complexityVisitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVali return err } +func (c *complexityVisitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + warpComplexity, err := WarpComplexity(tx.Message) + if err != nil { + return err + } + c.output, err = IntrinsicSetSubnetValidatorWeightTxComplexities.Add( + &baseTxComplexity, + &warpComplexity, + ) + 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 b2ddd0f91f9..8e58827dcb3 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -55,6 +55,10 @@ func (*staticVisitor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) return ErrUnsupportedTx } +func (*staticVisitor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) error { + return ErrUnsupportedTx +} + func (c *staticVisitor) AddValidatorTx(*txs.AddValidatorTx) error { c.fee = c.config.AddPrimaryNetworkValidatorFee return nil diff --git a/vms/platformvm/txs/set_subnet_validator_weight_tx.go b/vms/platformvm/txs/set_subnet_validator_weight_tx.go new file mode 100644 index 00000000000..519e0bd3bd9 --- /dev/null +++ b/vms/platformvm/txs/set_subnet_validator_weight_tx.go @@ -0,0 +1,40 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/types" +) + +var _ UnsignedTx = (*SetSubnetValidatorWeightTx)(nil) + +type SetSubnetValidatorWeightTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Message is expected to be a signed Warp message containing an + // AddressedCall payload with the SetSubnetValidatorWeight message. + Message types.JSONByteSlice `serialize:"true" json:"message"` +} + +func (tx *SetSubnetValidatorWeightTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: + // already passed syntactic verification + return nil + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return err + } + + tx.SyntacticallyVerified = true + return nil +} + +func (tx *SetSubnetValidatorWeightTx) Visit(visitor Visitor) error { + return visitor.SetSubnetValidatorWeightTx(tx) +} diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 02627c198f1..6aa766e1e3e 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -29,4 +29,5 @@ type Visitor interface { // Etna Transactions: ConvertSubnetTx(*ConvertSubnetTx) error RegisterSubnetValidatorTx(*RegisterSubnetValidatorTx) error + SetSubnetValidatorWeightTx(*SetSubnetValidatorWeightTx) error } diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index 03e2f2da5e2..bca4f12b2e3 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -179,6 +179,16 @@ type Builder interface { options ...common.Option, ) (*txs.RegisterSubnetValidatorTx, error) + // NewSetSubnetValidatorWeightTx sets the weight of a validator on a + // Permissionless L1. + // + // - [message] is the Warp message that authorizes this validator's weight + // to be changed + NewSetSubnetValidatorWeightTx( + message []byte, + options ...common.Option, + ) (*txs.SetSubnetValidatorWeightTx, error) + // NewImportTx creates an import transaction that attempts to consume all // the available UTXOs and import the funds to [to]. // @@ -933,6 +943,56 @@ func (b *builder) NewRegisterSubnetValidatorTx( return tx, b.initCtx(tx) } +func (b *builder) NewSetSubnetValidatorWeightTx( + message []byte, + options ...common.Option, +) (*txs.SetSubnetValidatorWeightTx, error) { + var ( + toBurn = map[ids.ID]uint64{} + toStake = map[ids.ID]uint64{} + ops = common.NewOptions(options) + memo = ops.Memo() + memoComplexity = gas.Dimensions{ + gas.Bandwidth: uint64(len(memo)), + } + ) + warpComplexity, err := fee.WarpComplexity(message) + if err != nil { + return nil, err + } + complexity, err := fee.IntrinsicRegisterSubnetValidatorTxComplexities.Add( + &memoComplexity, + &warpComplexity, + ) + 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.SetSubnetValidatorWeightTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.context.NetworkID, + BlockchainID: constants.PlatformChainID, + Ins: inputs, + Outs: outputs, + Memo: memo, + }}, + Message: message, + } + return tx, b.initCtx(tx) +} + func (b *builder) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/builder/builder_with_options.go b/wallet/chain/p/builder/builder_with_options.go index 4982eb3a752..bb19782573f 100644 --- a/wallet/chain/p/builder/builder_with_options.go +++ b/wallet/chain/p/builder/builder_with_options.go @@ -186,6 +186,16 @@ func (b *builderWithOptions) NewRegisterSubnetValidatorTx( ) } +func (b *builderWithOptions) NewSetSubnetValidatorWeightTx( + message []byte, + options ...common.Option, +) (*txs.SetSubnetValidatorWeightTx, error) { + return b.builder.NewSetSubnetValidatorWeightTx( + message, + common.UnionOptions(b.options, options)..., + ) +} + func (b *builderWithOptions) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index 4a2f1a98f62..8a6f1bf0e78 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -177,6 +177,14 @@ func (s *visitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) e return sign(s.tx, true, txSigners) } +func (s *visitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, true, txSigners) +} + func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index 3b6bcfaef3a..db0b544d4f8 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -78,6 +78,10 @@ func (b *backendVisitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidat return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { return b.baseTx(tx) } diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index dfc75be5010..b2ec508c075 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -166,6 +166,16 @@ type Wallet interface { options ...common.Option, ) (*txs.Tx, error) + // IssueSetSubnetValidatorWeightTx creates, signs, and issues a transaction + // that sets the weight of a validator on a Permissionless L1. + // + // - [message] is the Warp message that authorizes this validator's weight + // to be changed + IssueSetSubnetValidatorWeightTx( + message []byte, + 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]. // @@ -433,6 +443,17 @@ func (w *wallet) IssueRegisterSubnetValidatorTx( return w.IssueUnsignedTx(utx, options...) } +func (w *wallet) IssueSetSubnetValidatorWeightTx( + message []byte, + options ...common.Option, +) (*txs.Tx, error) { + utx, err := w.builder.NewSetSubnetValidatorWeightTx(message, 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 4e5c451f099..0b1c64c091d 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -174,6 +174,16 @@ func (w *withOptions) IssueRegisterSubnetValidatorTx( ) } +func (w *withOptions) IssueSetSubnetValidatorWeightTx( + message []byte, + options ...common.Option, +) (*txs.Tx, error) { + return w.wallet.IssueSetSubnetValidatorWeightTx( + message, + common.UnionOptions(w.options, options)..., + ) +} + func (w *withOptions) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/subnet/primary/examples/create-chain/main.go b/wallet/subnet/primary/examples/create-chain/main.go index 61526d9cf38..9a4ac92782d 100644 --- a/wallet/subnet/primary/examples/create-chain/main.go +++ b/wallet/subnet/primary/examples/create-chain/main.go @@ -22,7 +22,7 @@ func main() { key := genesis.EWOQKey uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) - subnetIDStr := "2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof" + subnetIDStr := "2eZYSgCU738xN7aRw47NsBUPqnKkoqJMYUJexTsX19VdTNSZc9" genesis := &xsgenesis.Genesis{ Timestamp: time.Now().Unix(), Allocations: []xsgenesis.Allocation{ diff --git a/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go new file mode 100644 index 00000000000..35e7ce26410 --- /dev/null +++ b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go @@ -0,0 +1,126 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "log" + "os" + "time" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "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) + chainID := ids.FromStringOrPanic("2BMFrJ9xeh5JdwZEx6uuFcjfZC2SV2hdbMT8ee5HrvjtfJb5br") + addressHex := "" + validationID := ids.FromStringOrPanic("2Y3ZZZXxpzm46geqVuqFXeSFVbeKihgrfeXRDaiF4ds6R2N8M5") + nonce := uint64(1) + weight := uint64(2) + + address, err := hex.DecodeString(addressHex) + if err != nil { + log.Fatalf("failed to decode address %q: %s\n", addressHex, err) + } + + skBytes, err := os.ReadFile("/Users/stephen/.avalanchego/staking/signer.key") + if err != nil { + log.Fatalf("failed to read signer key: %s\n", err) + } + + sk, err := bls.SecretKeyFromBytes(skBytes) + if err != nil { + log.Fatalf("failed to parse secret key: %s\n", err) + } + + // MakeWallet fetches the available UTXOs owned by [kc] on the network that + // [uri] is hosting and registers [subnetID]. + walletSyncStartTime := time.Now() + ctx := context.Background() + 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() + context := pWallet.Builder().Context() + + addressedCallPayload, err := message.NewSubnetValidatorWeight( + validationID, + nonce, + weight, + ) + if err != nil { + log.Fatalf("failed to create SubnetValidatorWeight message: %s\n", err) + } + addressedCallPayloadJSON, err := json.MarshalIndent(addressedCallPayload, "", "\t") + if err != nil { + log.Fatalf("failed to marshal SubnetValidatorWeight message: %s\n", err) + } + log.Println(string(addressedCallPayloadJSON)) + + addressedCall, err := payload.NewAddressedCall( + address, + addressedCallPayload.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + context.NetworkID, + chainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + signers := set.NewBits() + signers.Add(0) // [signers] has weight from [vdr[0]] + + unsignedBytes := unsignedWarp.Bytes() + sig := bls.Sign(sk, unsignedBytes) + sigBytes := [bls.SignatureLen]byte{} + copy(sigBytes[:], bls.SignatureToBytes(sig)) + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + if err != nil { + log.Fatalf("failed to create Warp message: %s\n", err) + } + + setWeightStartTime := time.Now() + setWeightTx, err := pWallet.IssueSetSubnetValidatorWeightTx( + warp.Bytes(), + ) + if err != nil { + log.Fatalf("failed to issue set subnet validator weight transaction: %s\n", err) + } + log.Printf("issued set weight of validationID %s to %d with nonce %d and txID %s in %s\n", validationID, weight, nonce, setWeightTx.ID(), time.Since(setWeightStartTime)) +}