From ef71d413e581b0eafee8d5ff3801442a7bb81398 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 26 Sep 2024 15:10:03 -0400 Subject: [PATCH 01/18] ACP-77: Implement SetSubnetValidatorWeightTx --- vms/platformvm/metrics/tx_metrics.go | 7 ++ vms/platformvm/txs/codec.go | 1 + .../txs/executor/atomic_tx_executor.go | 4 + .../txs/executor/proposal_tx_executor.go | 4 + .../txs/executor/standard_tx_executor.go | 107 +++++++++++++++++- vms/platformvm/txs/fee/complexity.go | 23 ++++ vms/platformvm/txs/fee/static_calculator.go | 4 + .../txs/set_subnet_validator_weight_tx.go | 40 +++++++ vms/platformvm/txs/visitor.go | 1 + wallet/chain/p/builder/builder.go | 60 ++++++++++ .../chain/p/builder/builder_with_options.go | 10 ++ wallet/chain/p/signer/visitor.go | 8 ++ wallet/chain/p/wallet/backend_visitor.go | 4 + wallet/chain/p/wallet/wallet.go | 21 ++++ wallet/chain/p/wallet/with_options.go | 10 ++ .../primary/examples/convert-subnet/main.go | 4 +- .../primary/examples/create-chain/main.go | 2 +- .../register-subnet-validator/main.go | 8 +- .../set-subnet-validator-weight/main.go | 96 ++++++++++++++++ 19 files changed, 401 insertions(+), 13 deletions(-) create mode 100644 vms/platformvm/txs/set_subnet_validator_weight_tx.go create mode 100644 wallet/subnet/primary/examples/set-subnet-validator-weight/main.go 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 b2a0895ed26..87849ae19fb 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "math" "time" "go.uber.org/zap" @@ -17,7 +18,6 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/hashing" - "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" @@ -29,6 +29,8 @@ 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" + + safemath "github.com/ava-labs/avalanchego/utils/math" ) const ( @@ -570,12 +572,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 } @@ -629,7 +631,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 } @@ -680,7 +682,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 @@ -735,7 +737,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 } @@ -756,6 +758,99 @@ 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.ParseSetSubnetValidatorWeight(addressedCall.Payload) + if err != nil { + return err + } + if msg.Nonce == math.MaxUint64 && msg.Weight != 0 { + return fmt.Errorf("setting nonce to %d can only be done when removing the validator", msg.Nonce) + } + + 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) + } + + expectedChainID, expectedAddress, err := e.State.GetSubnetManager(sov.SubnetID) + if err != nil { + return err + } + if warpMessage.SourceChainID != expectedChainID { + return fmt.Errorf("expected chainID %s but got %s", expectedChainID, warpMessage.SourceChainID) + } + if !bytes.Equal(addressedCall.SourceAddress, expectedAddress) { + return fmt.Errorf("expected address %s but got %s", expectedAddress, addressedCall.SourceAddress) + } + + // If the nonce calculation overflows, weight must be 0, so the validator is + // being removed anyways. + sov.MinNonce = msg.Nonce + 1 + sov.Weight = msg.Weight + 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 +} + func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( e.Backend, diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 575cb4c6b74..a5a746cdbca 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -204,6 +204,13 @@ var ( gas.DBWrite: 0, // TODO gas.Compute: 0, } + 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") @@ -716,6 +723,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 b15a567d369..e707366416a 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -182,6 +182,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]. // @@ -944,6 +954,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 fd83ce62f0a..d3a55de058d 100644 --- a/wallet/chain/p/builder/builder_with_options.go +++ b/wallet/chain/p/builder/builder_with_options.go @@ -188,6 +188,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 7811ad3b6f8..bf0a614a0ea 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -169,6 +169,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]. // @@ -437,6 +447,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 ab456d7d469..2beb789ebfd 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -176,6 +176,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/convert-subnet/main.go b/wallet/subnet/primary/examples/convert-subnet/main.go index 14b6f6b588c..9d117e72c69 100644 --- a/wallet/subnet/primary/examples/convert-subnet/main.go +++ b/wallet/subnet/primary/examples/convert-subnet/main.go @@ -22,8 +22,8 @@ func main() { key := genesis.EWOQKey uri := "http://localhost:9700" kc := secp256k1fx.NewKeychain(key) - subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") - chainID := ids.FromStringOrPanic("2f23iBApzAwJy1LRrgGZ3pGGK4S6UrakHMejZsQiwKy4rKfnzx") + subnetID := ids.FromStringOrPanic("2eZYSgCU738xN7aRw47NsBUPqnKkoqJMYUJexTsX19VdTNSZc9") + chainID := ids.FromStringOrPanic("2ko3NCPzHeneKWcYfy55pgAgU1LV9Q9XNrNv2sWG4W2XzE3ViV") addressHex := "" weight := units.Schmeckle 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/register-subnet-validator/main.go b/wallet/subnet/primary/examples/register-subnet-validator/main.go index 2eb92b164ff..7e6f9ebdf0d 100644 --- a/wallet/subnet/primary/examples/register-subnet-validator/main.go +++ b/wallet/subnet/primary/examples/register-subnet-validator/main.go @@ -25,8 +25,8 @@ func main() { key := genesis.EWOQKey uri := "http://localhost:9710" kc := secp256k1fx.NewKeychain(key) - subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") - chainID := ids.FromStringOrPanic("2f23iBApzAwJy1LRrgGZ3pGGK4S6UrakHMejZsQiwKy4rKfnzx") + subnetID := ids.FromStringOrPanic("2eZYSgCU738xN7aRw47NsBUPqnKkoqJMYUJexTsX19VdTNSZc9") + chainID := ids.FromStringOrPanic("2ko3NCPzHeneKWcYfy55pgAgU1LV9Q9XNrNv2sWG4W2XzE3ViV") addressHex := "" weight := units.Schmeckle @@ -109,6 +109,6 @@ func main() { log.Fatalf("failed to issue add subnet validator transaction: %s\n", err) } - validationID := hashing.ComputeHash256Array(addressedCallPayload.Bytes()) - log.Printf("added new subnet validator %s to %s with %s as %s in %s\n", nodeID, subnetID, addValidatorTx.ID(), validationID, time.Since(convertSubnetStartTime)) + var validationID ids.ID = hashing.ComputeHash256Array(addressedCallPayload.Bytes()) + log.Printf("added new subnet validator %s to subnet %s with txID %s as validationID %s in %s\n", nodeID, subnetID, addValidatorTx.ID(), validationID, time.Since(convertSubnetStartTime)) } 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..082268505a6 --- /dev/null +++ b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go @@ -0,0 +1,96 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/hex" + "log" + "time" + + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "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("2ko3NCPzHeneKWcYfy55pgAgU1LV9Q9XNrNv2sWG4W2XzE3ViV") + addressHex := "" + validationID := ids.FromStringOrPanic("225kHLzuaBd6rhxZ8aq91kmgLJyPTFtTFVAWJDaPyKRdDiTpQo") + nonce := uint64(1) + weight := uint64(0) + + address, err := hex.DecodeString(addressHex) + if err != nil { + log.Fatalf("failed to decode address %q: %s\n", addressHex, 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.NewSetSubnetValidatorWeight( + validationID, + nonce, + weight, + ) + if err != nil { + log.Fatalf("failed to create SetSubnetValidatorWeight message: %s\n", err) + } + + 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) + } + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{}, + ) + 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)) +} From d881881040bf6fa3a283f53d6afea0de98a17e4c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 30 Sep 2024 13:25:23 -0400 Subject: [PATCH 02/18] Refund balance --- .../txs/executor/standard_tx_executor.go | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 87849ae19fb..26cf72b40bc 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -834,16 +835,54 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return fmt.Errorf("expected address %s but got %s", expectedAddress, addressedCall.SourceAddress) } - // If the nonce calculation overflows, weight must be 0, so the validator is - // being removed anyways. + txID := e.Tx.ID() + if msg.Weight == 0 && sov.EndAccumulatedFee != 0 { + // If we are removing an active validator, we need to refund the + // remaining balance. + var remainingBalanceOwner fx.Owner + if _, err := txs.Codec.Unmarshal(sov.RemainingBalanceOwner, &remainingBalanceOwner); err != nil { + return err + } + + accruedFees := e.State.GetAccruedFees() + if sov.EndAccumulatedFee <= accruedFees { + // This should never happen as the validator should have been + // evicted. + return fmt.Errorf("validator has insufficient funds to cover accrued fees") + } + remainingBalance := sov.EndAccumulatedFee - accruedFees + + outIntf, err := e.Fx.CreateOutput(remainingBalance, remainingBalanceOwner) + if err != nil { + return fmt.Errorf("failed to create output: %w", err) + } + + out, ok := outIntf.(verify.State) + if !ok { + return ErrInvalidState + } + + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(tx.Outs)), + }, + Asset: avax.Asset{ + ID: e.Ctx.AVAXAssetID, + }, + Out: out, + } + 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 } - txID := e.Tx.ID() - // Consume the UTXOS avax.Consume(e.State, tx.Ins) // Produce the UTXOS From ab6bb9dce639fbc4709d2726afce6a4afc9e55a6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 1 Oct 2024 14:19:44 -0400 Subject: [PATCH 03/18] Fix compilation --- vms/platformvm/txs/executor/standard_tx_executor.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 38205aed297..77e1eaf1d1c 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "math" "time" "go.uber.org/zap" @@ -816,12 +815,12 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return err } - msg, err := message.ParseSetSubnetValidatorWeight(addressedCall.Payload) + msg, err := message.ParseSubnetValidatorWeight(addressedCall.Payload) if err != nil { return err } - if msg.Nonce == math.MaxUint64 && msg.Weight != 0 { - return fmt.Errorf("setting nonce to %d can only be done when removing the validator", msg.Nonce) + if err := msg.Verify(); err != nil { + return err } sov, err := e.State.GetSubnetOnlyValidator(msg.ValidationID) From 6e06c0c22a3919cb89e1adf9d321ea3b342f9445 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 1 Oct 2024 16:59:29 -0400 Subject: [PATCH 04/18] Update setSubnetValidatorWeightTx --- .../txs/executor/standard_tx_executor.go | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 790d4164ee1..9b3a0a8c60e 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -21,7 +21,6 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -29,6 +28,7 @@ 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" ) @@ -857,7 +857,7 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat if msg.Weight == 0 && sov.EndAccumulatedFee != 0 { // If we are removing an active validator, we need to refund the // remaining balance. - var remainingBalanceOwner fx.Owner + var remainingBalanceOwner message.PChainOwner if _, err := txs.Codec.Unmarshal(sov.RemainingBalanceOwner, &remainingBalanceOwner); err != nil { return err } @@ -870,16 +870,6 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat } remainingBalance := sov.EndAccumulatedFee - accruedFees - outIntf, err := e.Fx.CreateOutput(remainingBalance, remainingBalanceOwner) - if err != nil { - return fmt.Errorf("failed to create output: %w", err) - } - - out, ok := outIntf.(verify.State) - if !ok { - return ErrInvalidState - } - utxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: txID, @@ -888,7 +878,13 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat Asset: avax.Asset{ ID: e.Ctx.AVAXAssetID, }, - Out: out, + Out: &secp256k1fx.TransferOutput{ + Amt: remainingBalance, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: remainingBalanceOwner.Threshold, + Addrs: remainingBalanceOwner.Addresses, + }, + }, } e.State.AddUTXO(utxo) } From 904eeca8560ad9483c3c04f7d17102468d5ed707 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 2 Oct 2024 15:51:25 -0400 Subject: [PATCH 05/18] merged --- vms/platformvm/txs/executor/standard_tx_executor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index d4c0df1e0df..e523bcfd3c6 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -860,7 +860,7 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return fmt.Errorf("expected nonce to be at least %d but got %d", sov.MinNonce, msg.Nonce) } - expectedChainID, expectedAddress, err := e.State.GetSubnetManager(sov.SubnetID) + _, expectedChainID, expectedAddress, err := e.State.GetSubnetConversion(sov.SubnetID) if err != nil { return err } From 4a38f38d5f2b1cb6c71525a0c382017af8f796f6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 3 Oct 2024 12:32:55 -0400 Subject: [PATCH 06/18] fix merge --- wallet/subnet/primary/examples/convert-subnet/main.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/wallet/subnet/primary/examples/convert-subnet/main.go b/wallet/subnet/primary/examples/convert-subnet/main.go index 1c13769b4f7..b7133488f6e 100644 --- a/wallet/subnet/primary/examples/convert-subnet/main.go +++ b/wallet/subnet/primary/examples/convert-subnet/main.go @@ -23,13 +23,8 @@ func main() { key := genesis.EWOQKey uri := "http://localhost:9700" kc := secp256k1fx.NewKeychain(key) -<<<<<<< HEAD - subnetID := ids.FromStringOrPanic("2eZYSgCU738xN7aRw47NsBUPqnKkoqJMYUJexTsX19VdTNSZc9") - chainID := ids.FromStringOrPanic("2ko3NCPzHeneKWcYfy55pgAgU1LV9Q9XNrNv2sWG4W2XzE3ViV") -======= subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") chainID := ids.FromStringOrPanic("E8nTR9TtRwfkS7XFjTYUYHENQ91mkPMtDUwwCeu7rNgBBtkqu") ->>>>>>> implement-acp-77-register-subnet-validator-tx addressHex := "" weight := units.Schmeckle From 0ceeb16762305f26a0f1ba05410a7749dc178ec9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 4 Oct 2024 14:46:03 -0400 Subject: [PATCH 07/18] Fix lint --- .../set-subnet-validator-weight/main.go | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go index 082268505a6..35e7ce26410 100644 --- a/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go +++ b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go @@ -6,11 +6,15 @@ 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" @@ -22,17 +26,27 @@ func main() { key := genesis.EWOQKey uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) - chainID := ids.FromStringOrPanic("2ko3NCPzHeneKWcYfy55pgAgU1LV9Q9XNrNv2sWG4W2XzE3ViV") + chainID := ids.FromStringOrPanic("2BMFrJ9xeh5JdwZEx6uuFcjfZC2SV2hdbMT8ee5HrvjtfJb5br") addressHex := "" - validationID := ids.FromStringOrPanic("225kHLzuaBd6rhxZ8aq91kmgLJyPTFtTFVAWJDaPyKRdDiTpQo") + validationID := ids.FromStringOrPanic("2Y3ZZZXxpzm46geqVuqFXeSFVbeKihgrfeXRDaiF4ds6R2N8M5") nonce := uint64(1) - weight := uint64(0) + 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() @@ -51,14 +65,19 @@ func main() { pWallet := wallet.P() context := pWallet.Builder().Context() - addressedCallPayload, err := message.NewSetSubnetValidatorWeight( + addressedCallPayload, err := message.NewSubnetValidatorWeight( validationID, nonce, weight, ) if err != nil { - log.Fatalf("failed to create SetSubnetValidatorWeight message: %s\n", err) + 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, @@ -77,9 +96,20 @@ func main() { 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{}, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, ) if err != nil { log.Fatalf("failed to create Warp message: %s\n", err) From 3178ec0f830a791d52b0415d68f62c0f56b81a5d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 4 Oct 2024 15:07:05 -0400 Subject: [PATCH 08/18] fix lint --- vms/platformvm/txs/executor/standard_tx_executor.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 1cced69c5c0..f3ff2ab9355 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -49,6 +49,8 @@ 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") + + errStateCorruption = errors.New("state corruption") ) type StandardTxExecutor struct { @@ -887,9 +889,10 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat accruedFees := e.State.GetAccruedFees() if sov.EndAccumulatedFee <= accruedFees { - // This should never happen as the validator should have been - // evicted. - return fmt.Errorf("validator has insufficient funds to cover accrued fees") + // 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 From ea0411790cd192465532d4ba8f210285eabf3e84 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 7 Oct 2024 15:14:17 -0400 Subject: [PATCH 09/18] Disallow removal of last validator --- .../txs/executor/standard_tx_executor.go | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index f3ff2ab9355..ff4ae2dcde1 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -49,6 +49,7 @@ 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") ) @@ -879,40 +880,52 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat } txID := e.Tx.ID() - if msg.Weight == 0 && sov.EndAccumulatedFee != 0 { - // If we are removing an active validator, we need to refund the - // remaining balance. - var remainingBalanceOwner message.PChainOwner - if _, err := txs.Codec.Unmarshal(sov.RemainingBalanceOwner, &remainingBalanceOwner); err != nil { + + // We are removing the validator + if msg.Weight == 0 { + weight, err := e.State.WeightOfSubnetOnlyValidators(sov.SubnetID) + if 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) + if weight == sov.Weight { + return errRemovingLastValidator } - 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, + // 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) } - e.State.AddUTXO(utxo) } // If the weight is being set to 0, the validator is being removed and the From ad980128681c26e79fa3b780a215944690c7807f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 12:01:32 -0400 Subject: [PATCH 10/18] Test L1 removal --- tests/e2e/p/permissionless_layer_one.go | 491 ++++++++++++++---------- 1 file changed, 298 insertions(+), 193 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index cf02eb70659..379a6ece289 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -45,38 +45,47 @@ import ( warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" ) +const ( + genesisWeight = units.Schmeckle + genesisBalance = units.Avax + registerWeight = genesisWeight / 10 + registerBalance = 0 +) + var _ = e2e.DescribePChain("[Permissionless L1]", func() { tc := e2e.NewTestContext() require := require.New(tc) - ginkgo.It("creates a Permissionless L1", func() { + ginkgo.It("creates and updates Permissionless L1", func() { env := e2e.GetEnv(tc) nodeURI := env.GetRandomNodeURI() - infoClient := info.NewClient(nodeURI.URI) - - tc.By("fetching upgrade config") - upgrades, err := infoClient.Upgrades(tc.DefaultContext()) - require.NoError(err) - tc.By("verifying Etna is activated") - now := time.Now() - if !upgrades.IsEtnaActivated(now) { - ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") - } - - keychain := env.NewKeychain() - baseWallet := e2e.NewWallet(tc, keychain, nodeURI) + tc.By("verifying Etna is activated", func() { + infoClient := info.NewClient(nodeURI.URI) + upgrades, err := infoClient.Upgrades(tc.DefaultContext()) + require.NoError(err) - pWallet := baseWallet.P() - pClient := platformvm.NewClient(nodeURI.URI) + now := time.Now() + if !upgrades.IsEtnaActivated(now) { + ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") + } + }) - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - } + tc.By("loading the wallet") + var ( + keychain = env.NewKeychain() + baseWallet = e2e.NewWallet(tc, keychain, nodeURI) + pWallet = baseWallet.P() + pClient = platformvm.NewClient(nodeURI.URI) + owner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + } + ) + tc.By("creating the chain genesis") genesisKey, err := secp256k1.NewPrivateKey() require.NoError(err) @@ -91,40 +100,48 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) require.NoError(err) - tc.By("issuing a CreateSubnetTx") - subnetTx, err := pWallet.IssueCreateSubnetTx( - owner, - tc.WithDefaultContext(), - ) - require.NoError(err) + var subnetID ids.ID + tc.By("issuing a CreateSubnetTx", func() { + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + tc.WithDefaultContext(), + ) + require.NoError(err) - tc.By("verifying a Permissioned Subnet was successfully created") - subnetID := subnetTx.ID() - require.NotEqual(subnetID, constants.PrimaryNetworkID) + subnetID = subnetTx.ID() + }) - res, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - require.Equal( - platformvm.GetSubnetClientResponse{ - IsPermissioned: true, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), + tc.By("verifying a Permissioned Subnet was successfully created", func() { + require.NotEqual(constants.PrimaryNetworkID, subnetID) + + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, }, - Threshold: 1, - }, - res, - ) + subnet, + ) + }) - tc.By("issuing a CreateChainTx") - chainTx, err := pWallet.IssueCreateChainTx( - subnetID, - genesisBytes, - constants.XSVMID, - nil, - "No Permissions", - tc.WithDefaultContext(), - ) - require.NoError(err) + var chainID ids.ID + tc.By("issuing a CreateChainTx", func() { + chainTx, err := pWallet.IssueCreateChainTx( + subnetID, + genesisBytes, + constants.XSVMID, + nil, + "No Permissions", + tc.WithDefaultContext(), + ) + require.NoError(err) + + chainID = chainTx.ID() + }) tc.By("creating the genesis validator") subnetGenesisNode := e2e.AddEphemeralNode(tc, env.GetNetwork(), tmpnet.FlagsMap{ @@ -153,91 +170,96 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { ) require.NoError(err) defer func() { + genesisPeerMessages.Close() genesisPeer.StartClose() require.NoError(genesisPeer.AwaitClosed(tc.DefaultContext())) }() - const genesisWeight = 100 - var ( - chainID = chainTx.ID() - address = []byte{} - ) - tc.By("issuing a ConvertSubnetTx") - _, err = pWallet.IssueConvertSubnetTx( - subnetID, - chainID, - address, - []*txs.ConvertSubnetValidator{ - { - NodeID: subnetGenesisNode.NodeID.Bytes(), - Weight: genesisWeight, - Balance: units.Avax, - Signer: *genesisNodePoP, - }, - }, - tc.WithDefaultContext(), - ) - require.NoError(err) - - expectedConversionID, err := warpmessage.SubnetConversionID(warpmessage.SubnetConversionData{ - SubnetID: subnetID, - ManagerChainID: chainID, - ManagerAddress: address, - Validators: []warpmessage.SubnetConversionValidatorData{ - { - NodeID: subnetGenesisNode.NodeID.Bytes(), - BLSPublicKey: genesisNodePoP.PublicKey, - Weight: genesisWeight, + address := []byte{} + tc.By("issuing a ConvertSubnetTx", func() { + _, err := pWallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + { + NodeID: subnetGenesisNode.NodeID.Bytes(), + Weight: genesisWeight, + Balance: genesisBalance, + Signer: *genesisNodePoP, + }, }, - }, - }) - require.NoError(err) - - tc.By("waiting to update the proposervm P-chain height") - time.Sleep((5 * platformvmvalidators.RecentlyAcceptedWindowTTL) / 4) - - tc.By("issuing random transactions to update the proposervm P-chain height") - for range 2 { - _, err = pWallet.IssueCreateSubnetTx( - owner, tc.WithDefaultContext(), ) require.NoError(err) - } + }) - tc.By("verifying the Permissioned Subnet was converted to a Permissionless L1") - res, err = pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - require.Equal( - platformvm.GetSubnetClientResponse{ - IsPermissioned: false, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - Threshold: 1, - ConversionID: expectedConversionID, + tc.By("verifying the Permissioned Subnet was converted to a Permissionless L1", func() { + expectedConversionID, err := warpmessage.SubnetConversionID(warpmessage.SubnetConversionData{ + SubnetID: subnetID, ManagerChainID: chainID, ManagerAddress: address, - }, - res, - ) + Validators: []warpmessage.SubnetConversionValidatorData{ + { + NodeID: subnetGenesisNode.NodeID.Bytes(), + BLSPublicKey: genesisNodePoP.PublicKey, + Weight: genesisWeight, + }, + }, + }) + require.NoError(err) - tc.By("verifying the Permissionless L1 reports the correct validator set") - height, err := pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: false, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + ConversionID: expectedConversionID, + ManagerChainID: chainID, + ManagerAddress: address, + }, + subnet, + ) + }) - subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal( - map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, + tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) + + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal( + map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, }, - }, - subnetValidators, - ) + subnetValidators, + ) + }) + + advanceProposerVMPChainHeight := func() { + // We first must wait at least [RecentlyAcceptedWindowTTL] to ensure + // the next block will evict the prior block from the windower. + time.Sleep((5 * platformvmvalidators.RecentlyAcceptedWindowTTL) / 4) + + // Now we must: + // 1. issue a block which should include the old P-chain height. + // 2. issue a block which should include the new P-chain height. + for range 2 { + _, err = pWallet.IssueBaseTx(nil, tc.WithDefaultContext()) + require.NoError(err) + } + // Now that a block has been issued with the new P-chain height, the + // next block will use that height for warp message verification. + } + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) tc.By("creating the validator to register") subnetRegisterNode := e2e.AddEphemeralNode(tc, env.GetNetwork(), tmpnet.FlagsMap{ @@ -250,87 +272,170 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { registerNodePK, err := bls.PublicKeyFromCompressedBytes(registerNodePoP.PublicKey[:]) require.NoError(err) - tc.By("ensures the subnet nodes are healthy") - e2e.WaitForHealthy(tc, subnetGenesisNode) - e2e.WaitForHealthy(tc, subnetRegisterNode) - - const registerWeight = 1 - tc.By("create the unsigned warp message to register the validator") - unsignedRegisterSubnetValidator := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( - networkID, - chainID, - must[*payload.AddressedCall](tc)(payload.NewAddressedCall( - address, - must[*warpmessage.RegisterSubnetValidator](tc)(warpmessage.NewRegisterSubnetValidator( - subnetID, - subnetRegisterNode.NodeID, - registerNodePoP.PublicKey, - uint64(time.Now().Add(5*time.Minute).Unix()), - warpmessage.PChainOwner{}, - warpmessage.PChainOwner{}, - registerWeight, // weight - )).Bytes(), - )).Bytes(), - )) + tc.By("ensuring the subnet nodes are healthy", func() { + e2e.WaitForHealthy(tc, subnetGenesisNode) + e2e.WaitForHealthy(tc, subnetRegisterNode) + }) - registerSubnetValidatorRequest, err := wrapWarpSignatureRequest( - chainID, - unsignedRegisterSubnetValidator, - nil, + tc.By("creating the RegisterSubnetValidatorMessage") + registerSubnetValidatorMessage, err := warpmessage.NewRegisterSubnetValidator( + subnetID, + subnetRegisterNode.NodeID, + registerNodePoP.PublicKey, + uint64(time.Now().Add(5*time.Minute).Unix()), + warpmessage.PChainOwner{}, + warpmessage.PChainOwner{}, + registerWeight, ) require.NoError(err) + registerValidationID := registerSubnetValidatorMessage.ValidationID() + + tc.By("registering the validator", func() { + tc.By("creating the unsigned warp message") + unsignedRegisterSubnetValidator := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + chainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + address, + registerSubnetValidatorMessage.Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + registerSubnetValidatorRequest, err := wrapWarpSignatureRequest( + chainID, + unsignedRegisterSubnetValidator, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), registerSubnetValidatorRequest)) + }) + + tc.By("getting the signature response") + registerSubnetValidatorSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.True(ok) + + tc.By("creating the signed warp message to register the validator") + signers := set.NewBits() + signers.Add(0) // [signers] has weight from the genesis peer + + var sigBytes [bls.SignatureLen]byte + copy(sigBytes[:], bls.SignatureToBytes(registerSubnetValidatorSignature)) + registerSubnetValidator, err := warp.NewMessage( + unsignedRegisterSubnetValidator, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + require.NoError(err) - tc.By("send the request to sign the warp message") - require.True(genesisPeer.Send(tc.DefaultContext(), registerSubnetValidatorRequest)) + tc.By("issuing a RegisterSubnetValidatorTx", func() { + _, err := pWallet.IssueRegisterSubnetValidatorTx( + registerBalance, + registerNodePoP.ProofOfPossession, + registerSubnetValidator.Bytes(), + ) + require.NoError(err) + }) + }) - tc.By("get the signature response") - registerSubnetValidatorSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) - require.True(ok) + tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) - tc.By("create the signed warp message to register the validator") - signers := set.NewBits() - signers.Add(0) // [signers] has weight from the genesis peer + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal( + map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + subnetRegisterNode.NodeID: { + NodeID: subnetRegisterNode.NodeID, + PublicKey: registerNodePK, + Weight: registerWeight, + }, + }, + subnetValidators, + ) + }) - var sigBytes [bls.SignatureLen]byte - copy(sigBytes[:], bls.SignatureToBytes(registerSubnetValidatorSignature)) - registerSubnetValidator, err := warp.NewMessage( - unsignedRegisterSubnetValidator, - &warp.BitSetSignature{ - Signers: signers.Bytes(), - Signature: sigBytes, - }, - ) - require.NoError(err) + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) + + tc.By("removing the registered validator", func() { + 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( + registerValidationID, + 0, // nonce can be anything here + 0, // weight of 0 means the validator is removed + )).Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + setSubnetValidatorWeightRequest, err := wrapWarpSignatureRequest( + chainID, + unsignedSubnetValidatorWeight, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), setSubnetValidatorWeightRequest)) + }) + + tc.By("getting the signature response") + setSubnetValidatorWeightSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.True(ok) + + tc.By("creating the signed warp message to remove 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("register the validator") - _, err = pWallet.IssueRegisterSubnetValidatorTx( - 1, - registerNodePoP.ProofOfPossession, - registerSubnetValidator.Bytes(), - ) - require.NoError(err) + tc.By("issuing a SetSubnetValidatorWeightTx", func() { + _, err := pWallet.IssueSetSubnetValidatorWeightTx( + registerSubnetValidator.Bytes(), + ) + require.NoError(err) + }) + }) - tc.By("verify that the validator was registered") - height, err = pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) + tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) - subnetValidators, err = pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal( - map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, - }, - subnetRegisterNode.NodeID: { - NodeID: subnetRegisterNode.NodeID, - PublicKey: registerNodePK, - Weight: registerWeight, + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal( + map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, }, - }, - subnetValidators, - ) + subnetValidators, + ) + }) }) }) From 8453ee10f9c9b4d1b07ef71400314b2799ea147c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 12:31:38 -0400 Subject: [PATCH 11/18] fix test --- tests/e2e/p/permissionless_layer_one.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 379a6ece289..633cb645143 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -354,10 +354,9 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { PublicKey: genesisNodePK, Weight: genesisWeight, }, - subnetRegisterNode.NodeID: { - NodeID: subnetRegisterNode.NodeID, - PublicKey: registerNodePK, - Weight: registerWeight, + ids.EmptyNodeID: { // The validator is not active + NodeID: ids.EmptyNodeID, + Weight: registerWeight, }, }, subnetValidators, From 665c49961a13005888df35dcdda1fd219b0e2f30 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 12:37:02 -0400 Subject: [PATCH 12/18] nit --- tests/e2e/p/permissionless_layer_one.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 633cb645143..459b2fcd963 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -269,9 +269,6 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { registerNodePoP, err := subnetRegisterNode.GetProofOfPossession() require.NoError(err) - registerNodePK, err := bls.PublicKeyFromCompressedBytes(registerNodePoP.PublicKey[:]) - require.NoError(err) - tc.By("ensuring the subnet nodes are healthy", func() { e2e.WaitForHealthy(tc, subnetGenesisNode) e2e.WaitForHealthy(tc, subnetRegisterNode) From ad50b635f3146db6b3037e579b994b674b8f89d4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 15:33:30 -0400 Subject: [PATCH 13/18] Update the weight --- tests/e2e/p/permissionless_layer_one.go | 106 +++++++++++++----------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 459b2fcd963..21f7a616e41 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -49,6 +49,7 @@ const ( genesisWeight = units.Schmeckle genesisBalance = units.Avax registerWeight = genesisWeight / 10 + updatedWeight = 2 * registerWeight registerBalance = 0 ) @@ -226,22 +227,22 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { ) }) - tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { height, err := pClient.GetHeight(tc.DefaultContext()) require.NoError(err) subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) require.NoError(err) - require.Equal( - map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, - }, + require.Equal(expectedValidators, subnetValidators) + } + tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, }, - subnetValidators, - ) + }) }) advanceProposerVMPChainHeight := func() { @@ -339,30 +340,21 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) tc.By("verifying the Permissionless L1 reports the correct validator set", func() { - height, err := pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) - - subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal( - 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: registerWeight, - }, + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, }, - subnetValidators, - ) + ids.EmptyNodeID: { // The validator is not active + NodeID: ids.EmptyNodeID, + Weight: registerWeight, + }, + }) }) - tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) - - tc.By("removing the registered validator", 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, @@ -370,9 +362,9 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { must[*payload.AddressedCall](tc)(payload.NewAddressedCall( address, must[*warpmessage.SubnetValidatorWeight](tc)(warpmessage.NewSubnetValidatorWeight( - registerValidationID, - 0, // nonce can be anything here - 0, // weight of 0 means the validator is removed + validationID, + nextNonce, + weight, )).Bytes(), )).Bytes(), )) @@ -392,7 +384,7 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { setSubnetValidatorWeightSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) require.True(ok) - tc.By("creating the signed warp message to remove the validator") + 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 @@ -413,24 +405,42 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { ) require.NoError(err) }) + + nextNonce++ + } + + tc.By("increasing the weight of the validator", func() { + setWeight(registerValidationID, updatedWeight) }) tc.By("verifying the Permissionless L1 reports the correct validator set", func() { - height, err := pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) + 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, + }, + }) + }) - subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal( - map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, - }, + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) + + tc.By("removing the registered validator", func() { + setWeight(registerValidationID, 0) + }) + + tc.By("verifying the Permissionless L1 reports the correct validator set", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, }, - subnetValidators, - ) + }) }) }) }) From 21d732ba80be144bfd1bb67d8256d7e853ac7a27 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 16:36:50 -0400 Subject: [PATCH 14/18] backport tests --- tests/e2e/p/permissionless_layer_one.go | 182 +++++++++++++----------- 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 22c4b8f7154..e7a9b64c61a 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -5,6 +5,7 @@ package p import ( "context" + "errors" "math" "slices" "time" @@ -31,7 +32,6 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" - "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" @@ -41,6 +41,7 @@ import ( p2psdk "github.com/ava-labs/avalanchego/network/p2p" p2ppb "github.com/ava-labs/avalanchego/proto/pb/p2p" snowvalidators "github.com/ava-labs/avalanchego/snow/validators" + platformvmsdk "github.com/ava-labs/avalanchego/vms/platformvm" platformvmvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" ) @@ -77,7 +78,7 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { keychain = env.NewKeychain() baseWallet = e2e.NewWallet(tc, keychain, nodeURI) pWallet = baseWallet.P() - pClient = platformvm.NewClient(nodeURI.URI) + pClient = platformvmsdk.NewClient(nodeURI.URI) owner = &secp256k1fx.OutputOwners{ Threshold: 1, Addrs: []ids.ShortID{ @@ -118,7 +119,7 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) require.NoError(err) require.Equal( - platformvm.GetSubnetClientResponse{ + platformvmsdk.GetSubnetClientResponse{ IsPermissioned: true, ControlKeys: []ids.ShortID{ keychain.Keys[0].Address(), @@ -195,6 +196,14 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { require.NoError(err) }) + verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) + + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal(expectedValidators, subnetValidators) + } tc.By("verifying the Permissioned Subnet was converted to a Permissionless L1", func() { expectedConversionID, err := warpmessage.SubnetConversionID(warpmessage.SubnetConversionData{ SubnetID: subnetID, @@ -210,38 +219,32 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) require.NoError(err) - subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - require.Equal( - platformvm.GetSubnetClientResponse{ - IsPermissioned: false, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), + tc.By("verifying the subnet reports as being converted", func() { + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvmsdk.GetSubnetClientResponse{ + IsPermissioned: false, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + ConversionID: expectedConversionID, + ManagerChainID: chainID, + ManagerAddress: address, }, - Threshold: 1, - ConversionID: expectedConversionID, - ManagerChainID: chainID, - ManagerAddress: address, - }, - subnet, - ) - }) - - verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { - height, err := pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) + subnet, + ) + }) - subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal(expectedValidators, subnetValidators) - } - tc.By("verifying the Permissionless L1 reports the correct validator set", func() { - verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, - }, + tc.By("verifying the validator set was updated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + }) }) }) @@ -301,7 +304,6 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { tc.By("sending the request to sign the warp message", func() { registerSubnetValidatorRequest, err := wrapWarpSignatureRequest( - chainID, unsignedRegisterSubnetValidator, nil, ) @@ -311,7 +313,8 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) tc.By("getting the signature response") - registerSubnetValidatorSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) + registerSubnetValidatorSignature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) require.True(ok) tc.By("creating the signed warp message to register the validator") @@ -339,17 +342,19 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) }) - tc.By("verifying the Permissionless L1 reports the correct validator set", 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: registerWeight, - }, + tc.By("verifying the validator was registered", 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: registerWeight, + }, + }) }) }) @@ -371,7 +376,6 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { tc.By("sending the request to sign the warp message", func() { setSubnetValidatorWeightRequest, err := wrapWarpSignatureRequest( - chainID, unsignedSubnetValidatorWeight, nil, ) @@ -381,7 +385,8 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) tc.By("getting the signature response") - setSubnetValidatorWeightSignature, ok := findMessage(genesisPeerMessages, unwrapWarpSignature) + 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") @@ -413,17 +418,19 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { setWeight(registerValidationID, updatedWeight) }) - tc.By("verifying the Permissionless L1 reports the correct validator set", 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("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, + }, + }) }) }) @@ -433,20 +440,21 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { setWeight(registerValidationID, 0) }) - tc.By("verifying the Permissionless L1 reports the correct validator set", func() { - verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ - subnetGenesisNode.NodeID: { - NodeID: subnetGenesisNode.NodeID, - PublicKey: genesisNodePK, - Weight: genesisWeight, - }, + 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, + }, + }) }) }) }) }) func wrapWarpSignatureRequest( - chainID ids.ID, msg *warp.UnsignedMessage, justification []byte, ) (p2pmessage.OutboundMessage, error) { @@ -470,7 +478,7 @@ func wrapWarpSignatureRequest( } return p2pMessageFactory.AppRequest( - chainID, + msg.SourceChainID, 0, time.Hour, p2psdk.PrefixMessage( @@ -482,8 +490,8 @@ func wrapWarpSignatureRequest( func findMessage[T any]( q buffer.BlockingDeque[p2pmessage.InboundMessage], - parser func(p2pmessage.InboundMessage) (T, bool), -) (T, bool) { + parser func(p2pmessage.InboundMessage) (T, bool, error), +) (T, bool, error) { var messagesToReprocess []p2pmessage.InboundMessage defer func() { slices.Reverse(messagesToReprocess) @@ -495,35 +503,41 @@ func findMessage[T any]( for { msg, ok := q.PopLeft() if !ok { - return utils.Zero[T](), false + return utils.Zero[T](), false, nil } - parsed, ok := parser(msg) + parsed, ok, err := parser(msg) + if err != nil { + return utils.Zero[T](), false, err + } if ok { - return parsed, true + return parsed, true, nil } messagesToReprocess = append(messagesToReprocess, msg) } } -func unwrapWarpSignature(msg p2pmessage.InboundMessage) (*bls.Signature, bool) { - appResponse, ok := msg.Message().(*p2ppb.AppResponse) - if !ok { - return nil, false +// unwrapWarpSignature assumes the only type of AppResponses that will be +// received are ACP-118 compliant responses. +func unwrapWarpSignature(msg p2pmessage.InboundMessage) (*bls.Signature, bool, error) { + var appResponse *p2ppb.AppResponse + switch msg := msg.Message().(type) { + case *p2ppb.AppResponse: + appResponse = msg + case *p2ppb.AppError: + return nil, false, errors.New(msg.ErrorMessage) + default: + return nil, false, nil } var response sdk.SignatureResponse - err := proto.Unmarshal(appResponse.AppBytes, &response) - if err != nil { - return nil, false + if err := proto.Unmarshal(appResponse.AppBytes, &response); err != nil { + return nil, false, err } warpSignature, err := bls.SignatureFromBytes(response.Signature) - if err != nil { - return nil, false - } - return warpSignature, true + return warpSignature, true, err } func must[T any](t require.TestingT) func(T, error) T { From 99c24ad69b4e6430ad134f65b218d1b164cf7f65 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 17:01:13 -0400 Subject: [PATCH 15/18] merge --- tests/e2e/p/permissionless_layer_one.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 8879ad53455..e7a9b64c61a 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 ) From ae5c7128dc3df3d274708798dad4c74d4baac7fd Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 8 Oct 2024 18:29:19 -0400 Subject: [PATCH 16/18] Add SetSubnetValidatorWeightTx complexity tests --- vms/platformvm/txs/fee/calculator_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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, + }, } ) From d64794b61b7d17e6320e1531a5e1b40cc13afad4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 19 Oct 2024 14:08:41 -0400 Subject: [PATCH 17/18] merged --- tests/e2e/p/permissionless_layer_one.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go index 8922a11423a..8dfad5a9f21 100644 --- a/tests/e2e/p/permissionless_layer_one.go +++ b/tests/e2e/p/permissionless_layer_one.go @@ -358,7 +358,6 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) }) -<<<<<<< HEAD var nextNonce uint64 setWeight := func(validationID ids.ID, weight uint64) { tc.By("creating the unsigned warp message") @@ -452,9 +451,8 @@ var _ = e2e.DescribePChain("[Permissionless L1]", func() { }) }) }) -======= + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) ->>>>>>> implement-acp-77-register-subnet-validator-tx }) }) From 2d722fa2dfa4800119d4729c6befbbf948aac02f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 20 Oct 2024 18:20:01 -0400 Subject: [PATCH 18/18] fix merge --- vms/platformvm/txs/executor/standard_tx_executor.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 12a29372a35..fc87daeefb3 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -875,15 +875,15 @@ func (e *StandardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return fmt.Errorf("expected nonce to be at least %d but got %d", sov.MinNonce, msg.Nonce) } - _, expectedChainID, expectedAddress, err := e.State.GetSubnetConversion(sov.SubnetID) + subnetConversion, err := e.State.GetSubnetConversion(sov.SubnetID) if err != nil { return err } - if warpMessage.SourceChainID != expectedChainID { - return fmt.Errorf("expected chainID %s but got %s", expectedChainID, warpMessage.SourceChainID) + if warpMessage.SourceChainID != subnetConversion.ChainID { + return fmt.Errorf("expected chainID %s but got %s", subnetConversion.ChainID, warpMessage.SourceChainID) } - if !bytes.Equal(addressedCall.SourceAddress, expectedAddress) { - return fmt.Errorf("expected address %s but got %s", expectedAddress, addressedCall.SourceAddress) + if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { + return fmt.Errorf("expected address %s but got %s", subnetConversion.Addr, addressedCall.SourceAddress) } txID := e.Tx.ID()