Skip to content

Commit

Permalink
ACP-77: Implement SetSubnetValidatorWeightTx (#3421)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenButtolph authored Nov 12, 2024
1 parent a01eaee commit b505c48
Show file tree
Hide file tree
Showing 22 changed files with 1,611 additions and 9 deletions.
93 changes: 93 additions & 0 deletions tests/e2e/p/l1.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
genesisWeight = units.Schmeckle
genesisBalance = units.Avax
registerWeight = genesisWeight / 10
updatedWeight = 2 * registerWeight
registerBalance = 0

// Validator registration attempts expire 5 minutes after they are created
Expand Down Expand Up @@ -300,6 +301,7 @@ var _ = e2e.DescribePChain("[L1]", func() {
registerWeight,
)
require.NoError(err)
registerValidationID := registerSubnetValidatorMessage.ValidationID()

tc.By("registering the validator", func() {
tc.By("creating the unsigned warp message")
Expand Down Expand Up @@ -365,6 +367,97 @@ var _ = e2e.DescribePChain("[L1]", func() {
})
})

var nextNonce uint64
setWeight := func(validationID ids.ID, weight uint64) {
tc.By("creating the unsigned SubnetValidatorWeightMessage")
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")
setSubnetValidatorWeight, err := warp.NewMessage(
unsignedSubnetValidatorWeight,
&warp.BitSetSignature{
Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer
Signature: ([bls.SignatureLen]byte)(
bls.SignatureToBytes(setSubnetValidatorWeightSignature),
),
},
)
require.NoError(err)

tc.By("issuing a SetSubnetValidatorWeightTx", func() {
_, err := pWallet.IssueSetSubnetValidatorWeightTx(
setSubnetValidatorWeight.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,
},
})
})
})

genesisPeerMessages.Close()
genesisPeer.StartClose()
require.NoError(genesisPeer.AwaitClosed(tc.DefaultContext()))
Expand Down
7 changes: 7 additions & 0 deletions vms/platformvm/metrics/tx_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions vms/platformvm/txs/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,6 @@ func RegisterEtnaTypes(targetCodec linearcodec.Codec) error {
return errors.Join(
targetCodec.RegisterType(&ConvertSubnetTx{}),
targetCodec.RegisterType(&RegisterSubnetValidatorTx{}),
targetCodec.RegisterType(&SetSubnetValidatorWeightTx{}),
)
}
4 changes: 4 additions & 0 deletions vms/platformvm/txs/executor/atomic_tx_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func (*atomicTxExecutor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorT
return ErrWrongTxType
}

func (*atomicTxExecutor) SetSubnetValidatorWeightTx(*txs.SetSubnetValidatorWeightTx) error {
return ErrWrongTxType
}

func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error {
return e.atomicTx()
}
Expand Down
4 changes: 4 additions & 0 deletions vms/platformvm/txs/executor/proposal_tx_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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
Expand Down
173 changes: 164 additions & 9 deletions vms/platformvm/txs/executor/standard_tx_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,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"
)

// TODO: Before Etna, ensure that the maximum number of expiries to track is
Expand Down Expand Up @@ -55,6 +56,10 @@ var (
errWarpMessageExpired = errors.New("warp message expired")
errWarpMessageNotYetAllowed = errors.New("warp message not yet allowed")
errWarpMessageAlreadyIssued = errors.New("warp message already issued")
errCouldNotLoadSoV = errors.New("could not load SoV")
errWarpMessageContainsStaleNonce = errors.New("warp message contains stale nonce")
errRemovingLastValidator = errors.New("attempting to remove the last SoV from a converted subnet")
errStateCorruption = errors.New("state corruption")
)

// StandardTx executes the standard transaction [tx].
Expand Down Expand Up @@ -859,15 +864,8 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal

// Verify that the warp message was sent from the expected chain and
// address.
subnetConversion, err := e.state.GetSubnetConversion(msg.SubnetID)
if err != nil {
return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, msg.SubnetID, err)
}
if warpMessage.SourceChainID != subnetConversion.ChainID {
return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, warpMessage.SourceChainID)
}
if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) {
return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, addressedCall.SourceAddress)
if err := verifyL1Conversion(e.state, msg.SubnetID, warpMessage.SourceChainID, addressedCall.SourceAddress); err != nil {
return err
}

// Verify that the message contains a valid expiry time.
Expand Down Expand Up @@ -959,6 +957,142 @@ 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.backend.Ctx); err != nil {
return err
}

if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil {
return err
}

// Verify the flowcheck
fee, err := e.feeCalculator.CalculateFee(tx)
if err != nil {
return err
}

if err := e.backend.FlowChecker.VerifySpend(
tx,
e.state,
tx.Ins,
tx.Outs,
e.tx.Creds,
map[ids.ID]uint64{
e.backend.Ctx.AVAXAssetID: fee,
},
); err != nil {
return err
}

// Parse the warp message.
warpMessage, err := warp.ParseMessage(tx.Message)
if err != nil {
return err
}
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
}

// Verify that the message contains a valid nonce for a current validator.
sov, err := e.state.GetSubnetOnlyValidator(msg.ValidationID)
if err != nil {
return fmt.Errorf("%w: %w", errCouldNotLoadSoV, err)
}
if msg.Nonce < sov.MinNonce {
return fmt.Errorf("%w %d must be at least %d", errWarpMessageContainsStaleNonce, msg.Nonce, sov.MinNonce)
}

// Verify that the warp message was sent from the expected chain and
// address.
if err := verifyL1Conversion(e.state, sov.SubnetID, warpMessage.SourceChainID, addressedCall.SourceAddress); err != nil {
return err
}

txID := e.tx.ID()

// Check if we are removing the validator.
if msg.Weight == 0 {
// Verify that we are not removing the last validator.
weight, err := e.state.WeightOfSubnetOnlyValidators(sov.SubnetID)
if err != nil {
return fmt.Errorf("could not load SoV weights: %w", err)
}
if weight == sov.Weight {
return errRemovingLastValidator
}

// If 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 fmt.Errorf("%w: remaining balance owner is malformed", errStateCorruption)
}

accruedFees := e.state.GetAccruedFees()
if sov.EndAccumulatedFee <= accruedFees {
// This check should be unreachable. However, it prevents AVAX
// from being minted due to state corruption. This also prevents
// invalid UTXOs from being created (with 0 value).
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.backend.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, it is possible for the nonce increment
// to overflow. However, the validator is being removed and the nonce
// doesn't matter. If weight is not 0, [msg.Nonce] is enforced by
// [msg.Verify()] to be less than MaxUInt64 and can therefore be incremented
// without overflow.
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
}

// Creates the staker as defined in [stakerTx] and adds it to [e.State].
func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error {
var (
Expand Down Expand Up @@ -1030,3 +1164,24 @@ func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error {
}
return nil
}

// verifyL1Conversion verifies that the L1 conversion of [subnetID] references
// the [expectedChainID] and [expectedAddress].
func verifyL1Conversion(
state state.Chain,
subnetID ids.ID,
expectedChainID ids.ID,
expectedAddress []byte,
) error {
subnetConversion, err := state.GetSubnetConversion(subnetID)
if err != nil {
return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, subnetID, err)
}
if expectedChainID != subnetConversion.ChainID {
return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, expectedChainID)
}
if !bytes.Equal(expectedAddress, subnetConversion.Addr) {
return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, expectedAddress)
}
return nil
}
Loading

0 comments on commit b505c48

Please sign in to comment.