diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go new file mode 100644 index 000000000000..3394267eb8aa --- /dev/null +++ b/tests/e2e/p/permissionless_layer_one.go @@ -0,0 +1,104 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + ginkgo "github.com/onsi/ginkgo/v2" +) + +var _ = e2e.DescribePChain("[Permissionless L1]", func() { + tc := e2e.NewTestContext() + require := require.New(tc) + + ginkgo.It("e2e flow", func() { + env := e2e.GetEnv(tc) + + nodeURI := env.GetRandomNodeURI() + + infoClient := info.NewClient(nodeURI.URI) + + tc.By("get upgrade config") + upgrades, err := infoClient.Upgrades(tc.DefaultContext()) + require.NoError(err) + + now := time.Now() + if !upgrades.IsEtnaActivated(now) { + ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") + } + + keychain := env.NewKeychain(1) + 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(), + }, + } + + var subnetID ids.ID + tc.By("create a permissioned subnet", func() { + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + tc.WithDefaultContext(), + ) + + subnetID = subnetTx.ID() + require.NoError(err) + require.NotEqual(subnetID, constants.PrimaryNetworkID) + + res, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + + require.Equal(platformvm.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + }, res) + }) + + chainID := ids.GenerateTestID() + address := []byte{'a', 'd', 'd', 'r', 'e', 's', 's'} + tc.By("convert subnet to permissionless L1", func() { + convertSubnetTx, err := pWallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + tc.WithDefaultContext(), + ) + require.NoError(err) + + require.NoError(platformvm.AwaitTxAccepted(pClient, tc.DefaultContext(), convertSubnetTx.ID(), 100*time.Millisecond)) + + 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, + ManagerChainID: chainID, + ManagerAddress: address, + }, res) + }) + }) +}) diff --git a/vms/components/fee/validator.go b/vms/components/fee/validator.go new file mode 100644 index 000000000000..e01c010e12b3 --- /dev/null +++ b/vms/components/fee/validator.go @@ -0,0 +1,93 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "fmt" + + "github.com/holiman/uint256" +) + +type ValidatorState struct { + Current Gas + Target Gas + Capacity Gas + Excess Gas + MinFee GasPrice + K Gas +} + +func (v ValidatorState) CurrentFeeRate() GasPrice { + return v.MinFee.MulExp(v.Excess, v.K) +} + +func (v ValidatorState) CalculateContinuousFee(seconds uint64) uint64 { + if v.Current == v.Target { + return uint64(v.MinFee.MulExp(v.Excess, v.K)) * seconds + } + + uint256.NewInt(uint64(v.Excess)) + + var totalFee uint64 + if v.Current < v.Target { + secondsTillExcessIsZero := uint64(v.Excess / (v.Target - v.Current)) + + if secondsTillExcessIsZero < seconds { + totalFee += uint64(v.MinFee) * (seconds - secondsTillExcessIsZero) + seconds = secondsTillExcessIsZero + } + } + + x := v.Excess + for i := uint64(1); i <= seconds; i++ { + if v.Current < v.Target { + x = x.SubPerSecond(v.Target-v.Current, 1) + } else { + x = x.AddPerSecond(v.Current-v.Target, 1) + } + + if x == 0 { + totalFee += uint64(v.MinFee) + continue + } + + totalFee += uint64(v.MinFee.MulExp(x, v.K)) + } + + return totalFee +} + +// Returns the first number n where CalculateContinuousFee(n) >= balance +func (v ValidatorState) CalculateTimeTillContinuousFee(balance uint64) (uint64, uint64) { + // Lower bound can be derived from [MinFee]. + n := balance / uint64(v.MinFee) + interval := n + + numIters := 0 + for { + fmt.Printf("n=%d", n) + feeAtN := v.CalculateContinuousFee(n) + feeBeforeN := v.CalculateContinuousFee(n - 1) + if feeAtN == balance { + return n, uint64(numIters) + } + + if feeAtN > balance && feeBeforeN < balance { + return n, uint64(numIters) + } + + if feeAtN > balance { + if interval > 1 { + interval /= 2 + } + n -= interval + } + + if feeAtN < balance { + n += interval + } + + numIters++ + } +} diff --git a/vms/components/fee/validator_test.go b/vms/components/fee/validator_test.go new file mode 100644 index 000000000000..f2b94a622142 --- /dev/null +++ b/vms/components/fee/validator_test.go @@ -0,0 +1,124 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package fee + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ValidatorState_CalculateFee(t *testing.T) { + var ( + minute uint64 = 60 + hour = 60 * minute + day = 24 * hour + week = 7 * day + ) + + tests := []struct { + name string + initial ValidatorState + seconds uint64 + expected uint64 + }{ + { + name: "excess=0, currenttarget, minute", + initial: ValidatorState{ + Current: 15_000, + Target: 10_000, + Capacity: 20_000, + Excess: 0, + MinFee: 2_048, + K: 60_480_000_000, + }, + seconds: minute, + expected: 122_880, + }, + { + name: "excess=K, current=target, minute", + initial: ValidatorState{ + Current: 10_000, + Target: 10_000, + Capacity: 20_000, + Excess: 60_480_000_000, + MinFee: 2_048, + K: 60_480_000_000, + }, + seconds: minute, + expected: 334_020, + }, + { + name: "excess=0, current>target, day", + initial: ValidatorState{ + Current: 15_000, + Target: 10_000, + Capacity: 20_000, + Excess: 0, + MinFee: 2048, + K: 60_480_000_000, + }, + seconds: day, + expected: 177_538_111, + }, + { + name: "excess=K, current=target, day", + initial: ValidatorState{ + Current: 10_000, + Target: 10_000, + Capacity: 20_000, + Excess: 60_480_000_000, + MinFee: 2_048, + K: 60_480_000_000, + }, + seconds: day, + expected: 480_988_800, + }, + { + name: "excess hits 0 during, currenttarget, week", + initial: ValidatorState{ + Current: 15_000, + Target: 10_000, + Capacity: 20_000, + Excess: 0, + MinFee: 2048, + K: 60_480_000_000, + }, + seconds: week, + expected: 1_269_816_464, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := test.initial.CalculateContinuousFee(test.seconds) + require.Equal(t, test.expected, actual) + }) + } +} diff --git a/vms/platformvm/block/codec.go b/vms/platformvm/block/codec.go index dbb446161fb5..92c9be6812ff 100644 --- a/vms/platformvm/block/codec.go +++ b/vms/platformvm/block/codec.go @@ -36,6 +36,7 @@ func init() { txs.RegisterUnsignedTxsTypes(c), RegisterBanffBlockTypes(c), txs.RegisterDurangoUnsignedTxsTypes(c), + txs.RegisterEtnaUnsignedTxsTypes(c), ) } diff --git a/vms/platformvm/block/executor/block.go b/vms/platformvm/block/executor/block.go index 76cf6ff9078f..1ea19e116b67 100644 --- a/vms/platformvm/block/executor/block.go +++ b/vms/platformvm/block/executor/block.go @@ -29,18 +29,24 @@ func (*Block) ShouldVerifyWithContext(context.Context) (bool, error) { return true, nil } -func (b *Block) VerifyWithContext(_ context.Context, ctx *smblock.Context) error { +func (b *Block) VerifyWithContext(ctx context.Context, smCtx *smblock.Context) error { pChainHeight := uint64(0) - if ctx != nil { - pChainHeight = ctx.PChainHeight + if smCtx != nil { + pChainHeight = smCtx.PChainHeight } blkID := b.ID() if blkState, ok := b.manager.blkIDToState[blkID]; ok { if !blkState.verifiedHeights.Contains(pChainHeight) { - // PlatformVM blocks are currently valid regardless of the ProposerVM's - // PChainHeight. If this changes, those validity checks should be done prior - // to adding [pChainHeight] to [verifiedHeights]. + if err := b.Visit(&warpBlockVerifier{ + ctx: ctx, + chainCtx: b.manager.ctx, + state: b.manager.state, + pChainHeight: pChainHeight, + }); err != nil { + return err + } + blkState.verifiedHeights.Add(pChainHeight) } @@ -48,6 +54,15 @@ func (b *Block) VerifyWithContext(_ context.Context, ctx *smblock.Context) error return nil } + if err := b.Visit(&warpBlockVerifier{ + ctx: ctx, + chainCtx: b.manager.ctx, + state: b.manager.state, + pChainHeight: pChainHeight, + }); err != nil { + return err + } + return b.Visit(&verifier{ backend: b.manager.backend, txExecutorBackend: b.manager.txExecutorBackend, diff --git a/vms/platformvm/block/executor/warp_verifier.go b/vms/platformvm/block/executor/warp_verifier.go new file mode 100644 index 000000000000..65a73357b54e --- /dev/null +++ b/vms/platformvm/block/executor/warp_verifier.go @@ -0,0 +1,128 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package executor + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + _ block.Visitor = (*warpBlockVerifier)(nil) + _ txs.Visitor = (*warpTxVerifier)(nil) +) + +// warpBlockVerifier handles the logic for verifying all the warp message +// signatures in a block's transactions. +type warpBlockVerifier struct { + ctx context.Context + chainCtx *snow.Context + state state.State + pChainHeight uint64 +} + +// Pre-Banff did not contain any transactions with warp messages. +func (*warpBlockVerifier) ApricotAbortBlock(*block.ApricotAbortBlock) error { return nil } +func (*warpBlockVerifier) ApricotCommitBlock(*block.ApricotCommitBlock) error { return nil } +func (*warpBlockVerifier) ApricotProposalBlock(*block.ApricotProposalBlock) error { return nil } +func (*warpBlockVerifier) ApricotStandardBlock(*block.ApricotStandardBlock) error { return nil } +func (*warpBlockVerifier) ApricotAtomicBlock(*block.ApricotAtomicBlock) error { return nil } + +// No transactions in these blocks. +func (*warpBlockVerifier) BanffAbortBlock(*block.BanffAbortBlock) error { return nil } +func (*warpBlockVerifier) BanffCommitBlock(*block.BanffCommitBlock) error { return nil } + +func (v *warpBlockVerifier) BanffProposalBlock(b *block.BanffProposalBlock) error { + return v.verifyStandardTxs(b.Transactions) +} + +func (v *warpBlockVerifier) BanffStandardBlock(b *block.BanffStandardBlock) error { + return v.verifyStandardTxs(b.Transactions) +} + +func (v *warpBlockVerifier) verifyStandardTxs(txs []*txs.Tx) error { + for _, tx := range txs { + if err := tx.Unsigned.Visit(&warpTxVerifier{ + ctx: v.ctx, + chainCtx: v.chainCtx, + state: v.state, + pChainHeight: v.pChainHeight, + }); err != nil { + return err + } + } + return nil +} + +// warpTxVerifier handles the logic for verifying the warp message +// signature in a transaction. +type warpTxVerifier struct { + ctx context.Context + chainCtx *snow.Context + state state.State + pChainHeight uint64 +} + +func (*warpTxVerifier) AddDelegatorTx(*txs.AddDelegatorTx) error { return nil } +func (*warpTxVerifier) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { return nil } +func (*warpTxVerifier) AddValidatorTx(*txs.AddValidatorTx) error { return nil } +func (*warpTxVerifier) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return nil } +func (*warpTxVerifier) BaseTx(*txs.BaseTx) error { return nil } +func (*warpTxVerifier) CreateChainTx(*txs.CreateChainTx) error { return nil } +func (*warpTxVerifier) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } +func (*warpTxVerifier) ExportTx(*txs.ExportTx) error { return nil } +func (*warpTxVerifier) ImportTx(*txs.ImportTx) error { return nil } +func (*warpTxVerifier) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return nil } +func (*warpTxVerifier) RewardValidatorTx(*txs.RewardValidatorTx) error { return nil } +func (*warpTxVerifier) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return nil } +func (*warpTxVerifier) TransformSubnetTx(*txs.TransformSubnetTx) error { return nil } +func (*warpTxVerifier) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return nil } +func (*warpTxVerifier) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { + return nil +} + +func (*warpTxVerifier) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { + return nil +} + +func (v *warpTxVerifier) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { + if v.pChainHeight == 0 { + return errors.New("pChainHeight must be > 0") + } + + chainID, addr, err := v.state.GetSubnetManager(tx.ParsedMessage.SubnetID) + if err != nil { + return fmt.Errorf("failed to lookup subnet manager for %s: %w", tx.ParsedMessage.SubnetID, err) + } + + if tx.Message.SourceChainID != chainID { + return fmt.Errorf("mismatched chainIDs: expected %s, got %s", chainID, tx.Message.SourceChainID) + } + + if !bytes.Equal(tx.SourceAddress, addr) { + return fmt.Errorf("mismatched addresses: expected %v, got %v", tx.SourceAddress, addr) + } + + err = tx.Message.Signature.Verify( + v.ctx, + &tx.Message.UnsignedMessage, + v.chainCtx.NetworkID, + v.chainCtx.ValidatorState, + v.pChainHeight, + 1, + 2, + ) + if err != nil { + return fmt.Errorf("failed to verify warp signature: %w", err) + } + + return nil +} diff --git a/vms/platformvm/client.go b/vms/platformvm/client.go index a631176f5e46..a9e2c7feb20e 100644 --- a/vms/platformvm/client.go +++ b/vms/platformvm/client.go @@ -230,6 +230,9 @@ type GetSubnetClientResponse struct { Locktime uint64 // subnet transformation tx ID for a permissionless subnet SubnetTransformationTxID ids.ID + // subnet manager information for a permissionless L1 + ManagerChainID ids.ID + ManagerAddress []byte } func (c *client) GetSubnet(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (GetSubnetClientResponse, error) { @@ -251,6 +254,8 @@ func (c *client) GetSubnet(ctx context.Context, subnetID ids.ID, options ...rpc. Threshold: uint32(res.Threshold), Locktime: uint64(res.Locktime), SubnetTransformationTxID: res.SubnetTransformationTxID, + ManagerChainID: res.ManagerChainID, + ManagerAddress: res.ManagerAddress, }, nil } diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 02f45f011624..7d0af2ad1614 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -132,6 +132,20 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er return nil } +func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { + m.numTxs.With(prometheus.Labels{ + txLabel: "convert_subnet", + }).Inc() + return nil +} + +func (m *txMetrics) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) error { + m.numTxs.With(prometheus.Labels{ + txLabel: "register_subnet_validator", + }).Inc() + return nil +} + func (m *txMetrics) BaseTx(*txs.BaseTx) error { m.numTxs.With(prometheus.Labels{ txLabel: "base", diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index 0299192b6677..e5f1476f4afc 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -435,8 +435,11 @@ type GetSubnetResponse struct { ControlKeys []string `json:"controlKeys"` Threshold avajson.Uint32 `json:"threshold"` Locktime avajson.Uint64 `json:"locktime"` - // subnet transformation tx ID for a permissionless subnet + // subnet transformation tx ID for an elastic subnet SubnetTransformationTxID ids.ID `json:"subnetTransformationTxID"` + // subnet manager information for a permissionless L1 + ManagerChainID ids.ID `json:"managerChainID"` + ManagerAddress []byte `json:"managerAddress"` } func (s *Service) GetSubnet(_ *http.Request, args *GetSubnetArgs, response *GetSubnetResponse) error { @@ -485,6 +488,18 @@ func (s *Service) GetSubnet(_ *http.Request, args *GetSubnetArgs, response *GetS return err } + switch chainID, addr, err := s.vm.state.GetSubnetManager(args.SubnetID); err { + case nil: + response.IsPermissioned = false + response.ManagerChainID = chainID + response.ManagerAddress = addr + case database.ErrNotFound: + response.ManagerChainID = ids.Empty + response.ManagerAddress = []byte(nil) + default: + return err + } + return nil } diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index da2564f55cc2..c0f8fb5a3af0 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -68,6 +68,7 @@ var ( DelegatorPrefix = []byte("delegator") SubnetValidatorPrefix = []byte("subnetValidator") SubnetDelegatorPrefix = []byte("subnetDelegator") + SubnetOnlyValidatorPrefix = []byte("subnetOnlyValidator") ValidatorWeightDiffsPrefix = []byte("flatValidatorDiffs") ValidatorPublicKeyDiffsPrefix = []byte("flatPublicKeyDiffs") TxPrefix = []byte("tx") @@ -233,6 +234,9 @@ type stateBlk struct { * | | '-. subnetDelegator * | | '-. list * | | '-- txID -> potential reward + * | | |-. subnetOnlyValidator + * | | | '-. list + * | | | '-- validationID -> subnet + nodeID + minNonce + balance + endTime * | |-. pending * | | |-. validator * | | | '-. list @@ -267,6 +271,8 @@ type stateBlk struct { * | '-- txID -> nil * |-. subnetOwners * | '-. subnetID -> owner + * |-. subnetManagers + * | '-. subnetID -> manager * |-. chains * | '-. subnetID * | '-. list @@ -291,8 +297,9 @@ type state struct { baseDB *versiondb.Database - currentStakers *baseStakers - pendingStakers *baseStakers + currentStakers *baseStakers + pendingStakers *baseStakers + subnetOnlyValidators *subnetOnlyValidators currentHeight uint64 @@ -304,25 +311,26 @@ type state struct { blockCache cache.Cacher[ids.ID, block.Block] // cache of blockID -> Block; if the entry is nil, it is not in the database blockDB database.Database - validatorsDB database.Database - currentValidatorsDB database.Database - currentValidatorBaseDB database.Database - currentValidatorList linkeddb.LinkedDB - currentDelegatorBaseDB database.Database - currentDelegatorList linkeddb.LinkedDB - currentSubnetValidatorBaseDB database.Database - currentSubnetValidatorList linkeddb.LinkedDB - currentSubnetDelegatorBaseDB database.Database - currentSubnetDelegatorList linkeddb.LinkedDB - pendingValidatorsDB database.Database - pendingValidatorBaseDB database.Database - pendingValidatorList linkeddb.LinkedDB - pendingDelegatorBaseDB database.Database - pendingDelegatorList linkeddb.LinkedDB - pendingSubnetValidatorBaseDB database.Database - pendingSubnetValidatorList linkeddb.LinkedDB - pendingSubnetDelegatorBaseDB database.Database - pendingSubnetDelegatorList linkeddb.LinkedDB + validatorsDB database.Database + currentValidatorsDB database.Database + currentValidatorBaseDB database.Database + currentValidatorList linkeddb.LinkedDB + currentDelegatorBaseDB database.Database + currentDelegatorList linkeddb.LinkedDB + currentSubnetValidatorBaseDB database.Database + currentSubnetValidatorList linkeddb.LinkedDB + currentSubnetDelegatorBaseDB database.Database + currentSubnetDelegatorList linkeddb.LinkedDB + currentSubnetOnlyValidatorBaseDB database.Database + pendingValidatorsDB database.Database + pendingValidatorBaseDB database.Database + pendingValidatorList linkeddb.LinkedDB + pendingDelegatorBaseDB database.Database + pendingDelegatorList linkeddb.LinkedDB + pendingSubnetValidatorBaseDB database.Database + pendingSubnetValidatorList linkeddb.LinkedDB + pendingSubnetDelegatorBaseDB database.Database + pendingSubnetDelegatorList linkeddb.LinkedDB validatorWeightDiffsDB database.Database validatorPublicKeyDiffsDB database.Database @@ -517,6 +525,7 @@ func newState( currentDelegatorBaseDB := prefixdb.New(DelegatorPrefix, currentValidatorsDB) currentSubnetValidatorBaseDB := prefixdb.New(SubnetValidatorPrefix, currentValidatorsDB) currentSubnetDelegatorBaseDB := prefixdb.New(SubnetDelegatorPrefix, currentValidatorsDB) + currentSubnetOnlyValidatorBaseDB := prefixdb.New(SubnetOnlyValidatorPrefix, currentValidatorsDB) pendingValidatorsDB := prefixdb.New(PendingPrefix, validatorsDB) pendingValidatorBaseDB := prefixdb.New(ValidatorPrefix, pendingValidatorsDB) @@ -634,28 +643,41 @@ func newState( currentStakers: newBaseStakers(), pendingStakers: newBaseStakers(), - - validatorsDB: validatorsDB, - currentValidatorsDB: currentValidatorsDB, - currentValidatorBaseDB: currentValidatorBaseDB, - currentValidatorList: linkeddb.NewDefault(currentValidatorBaseDB), - currentDelegatorBaseDB: currentDelegatorBaseDB, - currentDelegatorList: linkeddb.NewDefault(currentDelegatorBaseDB), - currentSubnetValidatorBaseDB: currentSubnetValidatorBaseDB, - currentSubnetValidatorList: linkeddb.NewDefault(currentSubnetValidatorBaseDB), - currentSubnetDelegatorBaseDB: currentSubnetDelegatorBaseDB, - currentSubnetDelegatorList: linkeddb.NewDefault(currentSubnetDelegatorBaseDB), - pendingValidatorsDB: pendingValidatorsDB, - pendingValidatorBaseDB: pendingValidatorBaseDB, - pendingValidatorList: linkeddb.NewDefault(pendingValidatorBaseDB), - pendingDelegatorBaseDB: pendingDelegatorBaseDB, - pendingDelegatorList: linkeddb.NewDefault(pendingDelegatorBaseDB), - pendingSubnetValidatorBaseDB: pendingSubnetValidatorBaseDB, - pendingSubnetValidatorList: linkeddb.NewDefault(pendingSubnetValidatorBaseDB), - pendingSubnetDelegatorBaseDB: pendingSubnetDelegatorBaseDB, - pendingSubnetDelegatorList: linkeddb.NewDefault(pendingSubnetDelegatorBaseDB), - validatorWeightDiffsDB: validatorWeightDiffsDB, - validatorPublicKeyDiffsDB: validatorPublicKeyDiffsDB, + subnetOnlyValidators: newSubnetOnlyValidators( + &fee.ValidatorState{ + Current: 0, + Target: 10_000, + Capacity: 20_000, + Excess: 0, + MinFee: 2048, + K: 60_480_000_000, + }, + linkeddb.NewDefault(currentSubnetOnlyValidatorBaseDB), + cfg.Validators, + ), + + validatorsDB: validatorsDB, + currentValidatorsDB: currentValidatorsDB, + currentValidatorBaseDB: currentValidatorBaseDB, + currentValidatorList: linkeddb.NewDefault(currentValidatorBaseDB), + currentDelegatorBaseDB: currentDelegatorBaseDB, + currentDelegatorList: linkeddb.NewDefault(currentDelegatorBaseDB), + currentSubnetValidatorBaseDB: currentSubnetValidatorBaseDB, + currentSubnetValidatorList: linkeddb.NewDefault(currentSubnetValidatorBaseDB), + currentSubnetDelegatorBaseDB: currentSubnetDelegatorBaseDB, + currentSubnetDelegatorList: linkeddb.NewDefault(currentSubnetDelegatorBaseDB), + currentSubnetOnlyValidatorBaseDB: currentSubnetOnlyValidatorBaseDB, + pendingValidatorsDB: pendingValidatorsDB, + pendingValidatorBaseDB: pendingValidatorBaseDB, + pendingValidatorList: linkeddb.NewDefault(pendingValidatorBaseDB), + pendingDelegatorBaseDB: pendingDelegatorBaseDB, + pendingDelegatorList: linkeddb.NewDefault(pendingDelegatorBaseDB), + pendingSubnetValidatorBaseDB: pendingSubnetValidatorBaseDB, + pendingSubnetValidatorList: linkeddb.NewDefault(pendingSubnetValidatorBaseDB), + pendingSubnetDelegatorBaseDB: pendingSubnetDelegatorBaseDB, + pendingSubnetDelegatorList: linkeddb.NewDefault(pendingSubnetDelegatorBaseDB), + validatorWeightDiffsDB: validatorWeightDiffsDB, + validatorPublicKeyDiffsDB: validatorPublicKeyDiffsDB, addedTxs: make(map[ids.ID]*txAndStatus), txDB: prefixdb.New(TxPrefix, baseDB), @@ -1720,6 +1742,7 @@ func (s *state) write(updateValidators bool, height uint64) error { s.writeTransformedSubnets(), s.writeSubnetSupplies(), s.writeChains(), + s.subnetOnlyValidators.Write(height, s.validatorWeightDiffsDB, s.validatorPublicKeyDiffsDB), s.writeMetadata(), ) } @@ -1733,6 +1756,7 @@ func (s *state) Close() error { s.pendingValidatorsDB.Close(), s.currentSubnetValidatorBaseDB.Close(), s.currentSubnetDelegatorBaseDB.Close(), + s.currentSubnetOnlyValidatorBaseDB.Close(), s.currentDelegatorBaseDB.Close(), s.currentValidatorBaseDB.Close(), s.currentValidatorsDB.Close(), @@ -1963,8 +1987,11 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV // Record that the public key for the validator is being // added. This means the prior value for the public key was // nil. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(constants.PrimaryNetworkID, height, nodeID), + err := writeValidatorPublicKeyDiff( + s.validatorPublicKeyDiffsDB, + constants.PrimaryNetworkID, + height, + nodeID, nil, ) if err != nil { @@ -2012,9 +2039,12 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV // Note: We store the uncompressed public key here as it is // significantly more efficient to parse when applying // diffs. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(constants.PrimaryNetworkID, height, nodeID), - bls.PublicKeyToUncompressedBytes(staker.PublicKey), + err := writeValidatorPublicKeyDiff( + s.validatorPublicKeyDiffsDB, + constants.PrimaryNetworkID, + height, + nodeID, + staker.PublicKey, ) if err != nil { return err @@ -2043,9 +2073,12 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV continue } - err = s.validatorWeightDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - marshalWeightDiff(weightDiff), + err = writeValidatorWeightDiff( + s.validatorWeightDiffsDB, + subnetID, + height, + nodeID, + weightDiff, ) if err != nil { return err @@ -2571,3 +2604,34 @@ func getFeeState(db database.KeyValueReader) (fee.State, error) { } return feeState, nil } + +func writeValidatorWeightDiff( + db database.KeyValueWriter, + subnetID ids.ID, + height uint64, + nodeID ids.NodeID, + weightDiff *ValidatorWeightDiff, +) error { + return db.Put( + marshalDiffKey(subnetID, height, nodeID), + marshalWeightDiff(weightDiff), + ) +} + +func writeValidatorPublicKeyDiff( + db database.KeyValueWriter, + subnetID ids.ID, + height uint64, + nodeID ids.NodeID, + pk *bls.PublicKey, +) error { + value := []byte(nil) + if pk != nil { + value = bls.PublicKeyToUncompressedBytes(pk) + } + + return db.Put( + marshalDiffKey(subnetID, height, nodeID), + value, + ) +} diff --git a/vms/platformvm/state/subnet_only_validators.go b/vms/platformvm/state/subnet_only_validators.go new file mode 100644 index 000000000000..117297d436e1 --- /dev/null +++ b/vms/platformvm/state/subnet_only_validators.go @@ -0,0 +1,383 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "bytes" + "errors" + "fmt" + "math" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/linkeddb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/heap" + "github.com/ava-labs/avalanchego/vms/components/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/block" +) + +var ( + _ SubnetOnlyValidators = (*subnetOnlyValidators)(nil) + + ErrAlreadyValidator = errors.New("already a validator") + ErrValidatorNotFound = errors.New("validator not found") + ErrNonZeroWeight = errors.New("weight must be 0 when minNonce is MaxUint64") +) + +type SubnetOnlyValidators interface { + AddValidator( + validationID ids.ID, + subnetID ids.ID, + nodeID ids.NodeID, + weight uint64, + balance uint64, + endTime uint64, + pk *bls.PublicKey, + ) error + + ExitValidator(validationID ids.ID) (uint64, error) + + SetValidatorWeight(validationID ids.ID, newWeight uint64, newNonce uint64) (uint64, error) + + IncreaseValidatorBalance(validationID ids.ID, toAdd uint64) error + + AdvanceTime(duration uint64, maxToRemove int) ([]ids.ID, bool) + + Prune(maxToRemove int) ([]ids.ID, bool) + + Write( + height uint64, + validatorWeightDiffsDB, + validatorPublicKeyDiffsDB database.KeyValueWriter, + ) error +} + +type subnetOnlyValidator struct { + ValidationID ids.ID + SubnetID ids.ID `serialize:"true"` + NodeID ids.NodeID `serialize:"true"` + MinNonce uint64 `serialize:"true"` + Weight uint64 `serialize:"true"` + Balance uint64 `serialize:"true"` + PublicKey *bls.PublicKey `serialize:"true"` + + // If non-zero, this validator was added via an [Owner] key. + EndTime uint64 `serialize:"true"` +} + +// A *subnetOnlyValidator is considered to be less than another *subnetOnlyValidator when: +// +// 1. If its Balance is less than the other's. +// 2. If the Balances are the same, the one with the lesser NodeID is the +// lesser one. +func (v *subnetOnlyValidator) Less(than *subnetOnlyValidator) bool { + if v.Balance < than.Balance { + return true + } + + if than.Balance < v.Balance { + return false + } + + return bytes.Compare(v.ValidationID[:], than.ValidationID[:]) == -1 +} + +type subnetOnlyValidatorDiff struct { + weightDiff *ValidatorWeightDiff + status diffValidatorStatus +} + +type subnetOnlyValidators struct { + aggregatedBalance uint64 + calculator *fee.ValidatorState + + validators heap.Map[ids.ID, *subnetOnlyValidator] // validationID -> *subnetOnlyValidator + + // validationID -> added for that validator since the last db write + validatorDiffs map[ids.ID]*subnetOnlyValidatorDiff + validatorDB linkeddb.LinkedDB + validatorManager validators.Manager +} + +func newSubnetOnlyValidators( + calculator *fee.ValidatorState, + validatorDB linkeddb.LinkedDB, + validatorManager validators.Manager, +) *subnetOnlyValidators { + return &subnetOnlyValidators{ + aggregatedBalance: 0, + calculator: calculator, + + validators: heap.NewMap[ids.ID, *subnetOnlyValidator]((*subnetOnlyValidator).Less), + validatorDiffs: make(map[ids.ID]*subnetOnlyValidatorDiff), + validatorDB: validatorDB, + validatorManager: validatorManager, + } +} + +func (s *subnetOnlyValidators) AddValidator( + validationID ids.ID, + subnetID ids.ID, + nodeID ids.NodeID, + weight uint64, + balance uint64, + endTime uint64, + pk *bls.PublicKey, +) error { + if s.validators.Contains(validationID) { + return ErrAlreadyValidator + } + + s.validators.Push(validationID, &subnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + MinNonce: 0, + Weight: weight, + Balance: s.aggregatedBalance + balance, + PublicKey: pk, + + EndTime: endTime, + }) + + s.validatorDiffs[validationID] = &subnetOnlyValidatorDiff{ + weightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: weight, + }, + status: added, + } + s.calculator.Current += 1 + return nil +} + +func (s *subnetOnlyValidators) ExitValidator(validationID ids.ID) (uint64, error) { + vdr, ok := s.validators.Remove(validationID) + if !ok { + return 0, ErrValidatorNotFound + } + + // Note: s.validatorDiffs is not updated here as the validator is only + // purged when its weight is set to 0 via SetValidatorWeight() + s.calculator.Current -= 1 + + toRefund := vdr.Balance - s.aggregatedBalance + return toRefund, nil +} + +func (s *subnetOnlyValidators) SetValidatorWeight(validationID ids.ID, newWeight uint64, newNonce uint64) (uint64, error) { + vdr, ok := s.validators.Get(validationID) + if !ok { + return 0, ErrValidatorNotFound + } + + // Silently drop stale modifications. + if vdr.MinNonce <= newNonce { + return 0, nil + } + + if vdr.MinNonce == math.MaxUint64 && newWeight != 0 { + return 0, ErrNonZeroWeight + } + + currentWeight := s.validatorManager.GetWeight(vdr.SubnetID, vdr.NodeID) + + if newWeight == 0 { + s.validators.Remove(validationID) + + s.validatorDiffs[validationID] = &subnetOnlyValidatorDiff{ + weightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: currentWeight, + }, + status: deleted, + } + s.calculator.Current -= 1 + + toRefund := vdr.Balance - s.aggregatedBalance + return toRefund, nil + } + + if newWeight != currentWeight { + var weightDiff *ValidatorWeightDiff + if newWeight > currentWeight { + weightDiff = &ValidatorWeightDiff{ + Decrease: false, + Amount: newWeight - currentWeight, + } + } else { + weightDiff = &ValidatorWeightDiff{ + Decrease: true, + Amount: currentWeight - newWeight, + } + } + + s.validatorDiffs[validationID] = &subnetOnlyValidatorDiff{ + weightDiff: weightDiff, + } + } + + vdr.Weight = newWeight + vdr.MinNonce = newNonce + 1 + return 0, nil +} + +func (s *subnetOnlyValidators) IncreaseValidatorBalance(validationID ids.ID, toAdd uint64) error { + vdr, ok := s.validators.Get(validationID) + if ok { + vdr.Balance += toAdd + s.validators.Fix(validationID) + + return nil + } + + vdr, err := getSubnetOnlyValidator(s.validatorDB, validationID) + if err != nil { + return fmt.Errorf("%w: %w", ErrValidatorNotFound, err) + } + + vdr.Balance = s.aggregatedBalance + toAdd + s.validators.Push(validationID, vdr) + + s.calculator.Current += 1 + return nil +} + +func (s *subnetOnlyValidators) AdvanceTime(duration uint64, maxToRemove int) ([]ids.ID, bool) { + fee := s.calculator.CalculateContinuousFee(duration) + s.aggregatedBalance += fee + + return s.Prune(maxToRemove) +} + +func (s *subnetOnlyValidators) Prune(maxToRemove int) ([]ids.ID, bool) { + removed := []ids.ID{} + for { + if len(removed) == maxToRemove { + break + } + + validationID, vdr, ok := s.validators.Peek() + if !ok { + break + } + + if vdr.Balance > s.aggregatedBalance { + break + } + + s.validators.Pop() + removed = append(removed, validationID) + } + + _, vdr, ok := s.validators.Peek() + moreToRemove := false + if ok { + moreToRemove = vdr.Balance <= s.aggregatedBalance + } + + return removed, moreToRemove +} + +func (s *subnetOnlyValidators) Write( + height uint64, + validatorWeightDiffsDB, + validatorPublicKeyDiffsDB database.KeyValueWriter, +) error { + for validationID, diff := range s.validatorDiffs { + delete(s.validatorDiffs, validationID) + + vdr, ok := s.validators.Get(validationID) + if !ok { + return fmt.Errorf("failed to get subnet only validator: %s", validationID) + } + + err := writeValidatorWeightDiff( + validatorWeightDiffsDB, + vdr.SubnetID, + height, + vdr.NodeID, + diff.weightDiff, + ) + if err != nil { + return err + } + + err = writeValidatorPublicKeyDiff( + validatorPublicKeyDiffsDB, + vdr.SubnetID, + height, + vdr.NodeID, + vdr.PublicKey, + ) + if err != nil { + return err + } + + switch diff.status { + case added: + err := s.validatorManager.AddStaker( + vdr.SubnetID, + vdr.NodeID, + vdr.PublicKey, + validationID, + vdr.Weight, + ) + if err != nil { + return fmt.Errorf("failed to add staker: %w", err) + } + + if err := putSubnetOnlyValidator(s.validatorDB, vdr); err != nil { + return fmt.Errorf("failed to write subnet only validator: %w", err) + } + case deleted: + if err := deleteSubnetOnlyValidator(s.validatorDB, validationID); err != nil { + return fmt.Errorf("failed to delete subnet only validator: %w", err) + } + + continue + } + + if diff.weightDiff.Decrease { + if err := s.validatorManager.RemoveWeight(vdr.SubnetID, vdr.NodeID, diff.weightDiff.Amount); err != nil { + return fmt.Errorf("failed to remove weight: %w", err) + } + } else { + if err := s.validatorManager.AddWeight(vdr.SubnetID, vdr.NodeID, diff.weightDiff.Amount); err != nil { + return fmt.Errorf("failed to add weight: %w", err) + } + } + } + return nil +} + +func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (*subnetOnlyValidator, error) { + vdrBytes, err := db.Get(validationID[:]) + if err != nil { + return nil, err + } + + vdr := &subnetOnlyValidator{ + ValidationID: validationID, + } + if _, err = block.GenesisCodec.Unmarshal(vdrBytes, vdr); err != nil { + return nil, fmt.Errorf("failed to unmarshal subnet only validator: %w", err) + } + return vdr, err +} + +func putSubnetOnlyValidator(db database.KeyValueWriter, vdr *subnetOnlyValidator) error { + vdrBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, vdr) + if err != nil { + return err + } + + return db.Put(vdr.ValidationID[:], vdrBytes) +} + +func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) error { + return db.Delete(validationID[:]) +} diff --git a/vms/platformvm/state/subnet_only_validators_test.go b/vms/platformvm/state/subnet_only_validators_test.go new file mode 100644 index 000000000000..e298671e9565 --- /dev/null +++ b/vms/platformvm/state/subnet_only_validators_test.go @@ -0,0 +1,85 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database/linkeddb" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/vms/components/fee" +) + +var calculator = &fee.ValidatorState{ + Current: 0, + Target: 10_000, + Capacity: 20_000, + Excess: 0, + MinFee: 2048, + K: 60_480_000_000, +} + +func TestAddValidator(t *testing.T) { + var ( + require = require.New(t) + validatorDB = memdb.New() + validatorLinkedDB = linkeddb.New(validatorDB, 2048) + validatorManager = validators.NewManager() + validatorWeightDiffsDB = memdb.New() + validatorPublicKeyDiffsDB = memdb.New() + subnetOnlyValidators = newSubnetOnlyValidators( + calculator, + validatorLinkedDB, + validatorManager, + ) + + validationID = ids.GenerateTestID() + subnetID = ids.GenerateTestID() + nodeID = ids.GenerateTestNodeID() + weight = uint64(10000) + balance = uint64(100) + endTime = uint64(0) + height = uint64(2) + ) + + sk, err := bls.NewSecretKey() + require.NoError(err) + pk := bls.PublicFromSecretKey(sk) + + require.NoError(subnetOnlyValidators.AddValidator(validationID, subnetID, nodeID, weight, balance, endTime, pk)) + + require.Equal(&subnetOnlyValidatorDiff{ + weightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: weight, + }, + status: added, + }, subnetOnlyValidators.validatorDiffs[validationID]) + require.Equal(fee.Gas(1), subnetOnlyValidators.calculator.Current) + + require.NoError(subnetOnlyValidators.Write( + height, + validatorWeightDiffsDB, + validatorPublicKeyDiffsDB, + )) + + got, err := getSubnetOnlyValidator(subnetOnlyValidators.validatorDB, validationID) + require.NoError(err) + require.Equal(&subnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + MinNonce: 0, + Weight: weight, + Balance: balance, + PublicKey: new(bls.PublicKey), // TODO: Populate + + EndTime: endTime, + }, got) +} diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 8fa09606897f..22655ddd1e5b 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -43,6 +43,7 @@ func init() { c.SkipRegistrations(4) errs.Add(RegisterDurangoUnsignedTxsTypes(c)) + errs.Add(RegisterEtnaUnsignedTxsTypes(c)) } Codec = codec.NewDefaultManager() @@ -109,3 +110,10 @@ func RegisterDurangoUnsignedTxsTypes(targetCodec linearcodec.Codec) error { targetCodec.RegisterType(&BaseTx{}), ) } + +func RegisterEtnaUnsignedTxsTypes(targetCodec linearcodec.Codec) error { + return errors.Join( + targetCodec.RegisterType(&ConvertSubnetTx{}), + targetCodec.RegisterType(&RegisterSubnetValidatorTx{}), + ) +} diff --git a/vms/platformvm/txs/convert_subnet_tx.go b/vms/platformvm/txs/convert_subnet_tx.go new file mode 100644 index 000000000000..019893aeb67b --- /dev/null +++ b/vms/platformvm/txs/convert_subnet_tx.go @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/components/verify" +) + +var ( + _ UnsignedTx = (*TransferSubnetOwnershipTx)(nil) + + ErrConvertPermissionlessSubnet = errors.New("cannot convert a permissionless subnet") +) + +type ConvertSubnetTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // ID of the Subnet to transform + Subnet ids.ID `serialize:"true" json:"subnetID"` + // Chain where the Subnet manager lives + ChainID ids.ID `serialize:"true" json:"chainID"` + // Address of the Subnet manager + Address []byte `serialize:"true" json:"address"` + // Authorizes this conversion + SubnetAuth verify.Verifiable `serialize:"true" json:"subnetAuthorization"` +} + +// InitCtx sets the FxID fields in the inputs and outputs of this +// [ConvertSubnetTx]. Also sets the [ctx] to the given [vm.ctx] so +// that the addresses can be json marshalled into human readable format +func (tx *ConvertSubnetTx) InitCtx(ctx *snow.Context) { + tx.BaseTx.InitCtx(ctx) +} + +func (tx *ConvertSubnetTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: + // already passed syntactic verification + return nil + case tx.Subnet == constants.PrimaryNetworkID: + return ErrConvertPermissionlessSubnet + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return err + } + if err := tx.SubnetAuth.Verify(); err != nil { + return err + } + + tx.SyntacticallyVerified = true + return nil +} + +func (tx *ConvertSubnetTx) Visit(visitor Visitor) error { + return visitor.ConvertSubnetTx(tx) +} diff --git a/vms/platformvm/txs/convert_subnet_tx_test.go b/vms/platformvm/txs/convert_subnet_tx_test.go new file mode 100644 index 000000000000..e2b2a793d374 --- /dev/null +++ b/vms/platformvm/txs/convert_subnet_tx_test.go @@ -0,0 +1,658 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" +) + +func TestConvertSubnetTxSerialization(t *testing.T) { + require := require.New(t) + + addr := ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + + avaxAssetID, err := ids.FromString("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") + require.NoError(err) + + customAssetID := ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + + txID := ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + subnetID := ids.ID{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + } + managerChainID := ids.ID{ + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + } + managerAddress := []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xde, 0xad, + } + + simpleConvertSubnetTx := &ConvertSubnetTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.MainnetID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{}, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.MilliAvax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{5}, + }, + }, + }, + }, + Memo: types.JSONByteSlice{}, + }, + }, + Subnet: subnetID, + ChainID: managerChainID, + Address: managerAddress, + SubnetAuth: &secp256k1fx.Input{ + SigIndices: []uint32{3}, + }, + } + require.NoError(simpleConvertSubnetTx.SyntacticVerify(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + })) + + expectedUnsignedSimpleConvertSubnetTxBytes := []byte{ + // Codec version + 0x00, 0x00, + // ConvertSubnetTx Type ID + 0x00, 0x00, 0x00, 0x23, + // Mainnet network ID + 0x00, 0x00, 0x00, 0x01, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x00, + // Number of inputs + 0x00, 0x00, 0x00, 0x01, + // Inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 MilliAvax + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x05, + // length of memo + 0x00, 0x00, 0x00, 0x00, + // subnetID to modify + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + // chainID of the manager + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + // length of the manager address + 0x00, 0x00, 0x00, 0x14, + // address of the manager + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xde, 0xad, + // secp256k1fx authorization type ID + 0x00, 0x00, 0x00, 0x0a, + // number of signatures needed in authorization + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x03, + } + var unsignedSimpleConvertSubnetTx UnsignedTx = simpleConvertSubnetTx + unsignedSimpleConvertSubnetTxBytes, err := Codec.Marshal(CodecVersion, &unsignedSimpleConvertSubnetTx) + require.NoError(err) + require.Equal(expectedUnsignedSimpleConvertSubnetTxBytes, unsignedSimpleConvertSubnetTxBytes) + + complexConvertSubnetTx := &ConvertSubnetTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.MainnetID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, + }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), + }, + }, + Subnet: subnetID, + ChainID: managerChainID, + Address: managerAddress, + SubnetAuth: &secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + } + avax.SortTransferableOutputs(complexConvertSubnetTx.Outs, Codec) + utils.Sort(complexConvertSubnetTx.Ins) + require.NoError(complexConvertSubnetTx.SyntacticVerify(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + })) + + expectedUnsignedComplexConvertSubnetTxBytes := []byte{ + // Codec version + 0x00, 0x00, + // ConvertSubnetTx Type ID + 0x00, 0x00, 0x00, 0x23, + // Mainnet network ID + 0x00, 0x00, 0x00, 0x01, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + // subnetID to modify + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + // chainID of the manager + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + // length of the manager address + 0x00, 0x00, 0x00, 0x14, + // address of the manager + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xde, 0xad, + // secp256k1fx authorization type ID + 0x00, 0x00, 0x00, 0x0a, + // number of signatures needed in authorization + 0x00, 0x00, 0x00, 0x00, + } + var unsignedComplexConvertSubnetTx UnsignedTx = complexConvertSubnetTx + unsignedComplexConvertSubnetTxBytes, err := Codec.Marshal(CodecVersion, &unsignedComplexConvertSubnetTx) + require.NoError(err) + require.Equal(expectedUnsignedComplexConvertSubnetTxBytes, unsignedComplexConvertSubnetTxBytes) + + aliaser := ids.NewAliaser() + require.NoError(aliaser.Alias(constants.PlatformChainID, "P")) + + unsignedComplexConvertSubnetTx.InitCtx(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + BCLookup: aliaser, + }) + + unsignedComplexConvertSubnetTxJSONBytes, err := json.MarshalIndent(unsignedComplexConvertSubnetTx, "", "\t") + require.NoError(err) + require.Equal(`{ + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-avax1g32kvaugnx4tk3z4vemc3xd2hdz92enh972wxr" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "inputs": [ + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1152921504606846976, + "signatureIndices": [] + } + } + ], + "memo": "0xf09f98850a77656c6c2074686174277301234521", + "subnetID": "SkB92YpWm4UpburLz9tEKZw2i67H3FF6YkjaU4BkFUDTG9Xm", + "chainID": "NfebWJbJMmUpduqFCF8i1m5pstbVYLP1gGHbacrevXZMhpVMy", + "address": "AAAAAAAAAAAAAAAAAAAAAAAA3q0=", + "subnetAuthorization": { + "signatureIndices": [] + } +}`, string(unsignedComplexConvertSubnetTxJSONBytes)) +} + +func TestConvertSubnetTxSyntacticVerify(t *testing.T) { + type test struct { + name string + txFunc func(*gomock.Controller) *ConvertSubnetTx + expectedErr error + } + + var ( + networkID = uint32(1337) + chainID = ids.GenerateTestID() + ) + + ctx := &snow.Context{ + ChainID: chainID, + NetworkID: networkID, + } + + // A BaseTx that already passed syntactic verification. + verifiedBaseTx := BaseTx{ + SyntacticallyVerified: true, + } + // Sanity check. + require.NoError(t, verifiedBaseTx.SyntacticVerify(ctx)) + + // A BaseTx that passes syntactic verification. + validBaseTx := BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: networkID, + BlockchainID: chainID, + }, + } + // Sanity check. + require.NoError(t, validBaseTx.SyntacticVerify(ctx)) + // Make sure we're not caching the verification result. + require.False(t, validBaseTx.SyntacticallyVerified) + + // A BaseTx that fails syntactic verification. + invalidBaseTx := BaseTx{} + + tests := []test{ + { + name: "nil tx", + txFunc: func(*gomock.Controller) *ConvertSubnetTx { + return nil + }, + expectedErr: ErrNilTx, + }, + { + name: "already verified", + txFunc: func(*gomock.Controller) *ConvertSubnetTx { + return &ConvertSubnetTx{BaseTx: verifiedBaseTx} + }, + expectedErr: nil, + }, + { + name: "invalid BaseTx", + txFunc: func(*gomock.Controller) *ConvertSubnetTx { + return &ConvertSubnetTx{ + // Set subnetID so we don't error on that check. + Subnet: ids.GenerateTestID(), + BaseTx: invalidBaseTx, + } + }, + expectedErr: avax.ErrWrongNetworkID, + }, + { + name: "invalid subnetID", + txFunc: func(*gomock.Controller) *ConvertSubnetTx { + return &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: constants.PrimaryNetworkID, + } + }, + expectedErr: ErrConvertPermissionlessSubnet, + }, + { + name: "invalid subnetAuth", + txFunc: func(ctrl *gomock.Controller) *ConvertSubnetTx { + // This SubnetAuth fails verification. + invalidSubnetAuth := verify.NewMockVerifiable(ctrl) + invalidSubnetAuth.EXPECT().Verify().Return(errInvalidSubnetAuth) + return &ConvertSubnetTx{ + // Set subnetID so we don't error on that check. + Subnet: ids.GenerateTestID(), + BaseTx: validBaseTx, + SubnetAuth: invalidSubnetAuth, + } + }, + expectedErr: errInvalidSubnetAuth, + }, + { + name: "passes verification", + txFunc: func(ctrl *gomock.Controller) *ConvertSubnetTx { + // This SubnetAuth passes verification. + validSubnetAuth := verify.NewMockVerifiable(ctrl) + validSubnetAuth.EXPECT().Verify().Return(nil) + return &ConvertSubnetTx{ + // Set subnetID so we don't error on that check. + Subnet: ids.GenerateTestID(), + BaseTx: validBaseTx, + SubnetAuth: validSubnetAuth, + } + }, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + tx := tt.txFunc(ctrl) + err := tx.SyntacticVerify(ctx) + require.ErrorIs(err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + require.True(tx.SyntacticallyVerified) + }) + } +} diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index f62abd21f818..190c5ec114c9 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -82,6 +82,10 @@ func (*AtomicTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } +func (*AtomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { + return ErrWrongTxType +} + func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } diff --git a/vms/platformvm/txs/executor/create_chain_test.go b/vms/platformvm/txs/executor/create_chain_test.go index 1f5e4be4f97a..cf74d4ead96c 100644 --- a/vms/platformvm/txs/executor/create_chain_test.go +++ b/vms/platformvm/txs/executor/create_chain_test.go @@ -252,3 +252,40 @@ func TestCreateChainTxAP3FeeChange(t *testing.T) { }) } } + +func TestEtnaCreateChainTxInvalidWithManagedSubnet(t *testing.T) { + require := require.New(t) + env := newEnvironment(t, etna) + env.ctx.Lock.Lock() + defer env.ctx.Lock.Unlock() + + builder, signer := env.factory.NewWallet(testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]) + utx, err := builder.NewCreateChainTx( + testSubnet1.ID(), + nil, + constants.AVMID, + nil, + "chain name", + ) + require.NoError(err) + tx, err := walletsigner.SignUnsigned(context.Background(), signer, utx) + require.NoError(err) + + stateDiff, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + builderDiff, err := state.NewDiffOn(stateDiff) + require.NoError(err) + + stateDiff.SetSubnetManager(testSubnet1.ID(), ids.GenerateTestID(), []byte{'a', 'd', 'd', 'r', 'e', 's', 's'}) + + feeCalculator := state.PickFeeCalculator(env.config, builderDiff) + executor := StandardTxExecutor{ + Backend: &env.backend, + FeeCalculator: feeCalculator, + State: stateDiff, + Tx: tx, + } + err = tx.Unsigned.Visit(&executor) + require.ErrorIs(err, ErrIsImmutable) +} diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index d7b76d10c615..421e57fab120 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -100,6 +100,10 @@ func (*ProposalTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } +func (*ProposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) 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/staker_tx_verification.go b/vms/platformvm/txs/executor/staker_tx_verification.go index b84ad9d6e2b1..1570451b1027 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification.go +++ b/vms/platformvm/txs/executor/staker_tx_verification.go @@ -41,6 +41,7 @@ var ( ErrDurangoUpgradeNotActive = errors.New("attempting to use a Durango-upgrade feature prior to activation") ErrAddValidatorTxPostDurango = errors.New("AddValidatorTx is not permitted post-Durango") ErrAddDelegatorTxPostDurango = errors.New("AddDelegatorTx is not permitted post-Durango") + ErrRemoveValidatorManagedSubnet = errors.New("RemoveSubnetValidatorTx cannot be used to remove a validator from a Subnet with a manager") ) // verifySubnetValidatorPrimaryNetworkRequirements verifies the primary @@ -306,6 +307,13 @@ func verifyRemoveSubnetValidatorTx( return nil, false, err } + if backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { + _, _, err := chainState.GetSubnetManager(tx.Subnet) + if err != database.ErrNotFound { + return nil, false, ErrRemoveValidatorManagedSubnet + } + } + isCurrentValidator := true vdr, err := chainState.GetCurrentValidator(tx.Subnet, tx.NodeID) if err == database.ErrNotFound { diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 1204b7d82502..a8d6e66f5d90 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -28,6 +28,7 @@ var ( errEmptyNodeID = errors.New("validator nodeID cannot be empty") errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") + errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") ) @@ -494,6 +495,57 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error return nil } +func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) 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 + } + + baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + if 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, + baseTxCreds, + map[ids.ID]uint64{ + e.Ctx.AVAXAssetID: fee, + }, + ); 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) + // Set the new Subnet manager in the database + e.State.SetSubnetManager(tx.Subnet, tx.ChainID, tx.Address) + return nil +} + func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( e.Backend, diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index d57955c08b39..926f632961c2 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -892,6 +892,44 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { } } +func TestEtnaStandardTxExecutorAddSubnetValidator(t *testing.T) { + require := require.New(t) + env := newEnvironment(t, etna) + env.ctx.Lock.Lock() + defer env.ctx.Lock.Unlock() + + nodeID := genesisNodeIDs[0] + + builder, signer := env.factory.NewWallet(testSubnet1ControlKeys[0], testSubnet1ControlKeys[1]) + utx, err := builder.NewAddSubnetValidatorTx( + &txs.SubnetValidator{ + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(defaultValidateStartTime.Unix() + 1), + End: uint64(defaultValidateEndTime.Unix()), + Wght: defaultWeight, + }, + Subnet: testSubnet1.ID(), + }, + ) + require.NoError(err) + tx, err := walletsigner.SignUnsigned(context.Background(), signer, utx) + require.NoError(err) + + onAcceptState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAcceptState.SetSubnetManager(testSubnet1.ID(), ids.GenerateTestID(), []byte{'a', 'd', 'd', 'r', 'e', 's', 's'}) + + executor := StandardTxExecutor{ + Backend: &env.backend, + State: onAcceptState, + Tx: tx, + } + err = tx.Unsigned.Visit(&executor) + require.ErrorIs(err, ErrIsImmutable) +} + func TestBanffStandardTxExecutorAddValidator(t *testing.T) { require := require.New(t) env := newEnvironment(t, banff) @@ -1969,6 +2007,30 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { }, expectedErr: ErrFlowCheckFailed, }, + { + name: "attempted to remove subnet validator after subnet manager is set", + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.GenerateTestID(), []byte{'a', 'd', 'd', 'r', 'e', 's', 's'}, nil).AnyTimes() + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + expectedErr: ErrRemoveValidatorManagedSubnet, + }, } for _, tt := range tests { @@ -2192,6 +2254,7 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { subnetOwner := fx.NewMockOwner(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.Empty, nil, database.ErrNotFound).Times(1) env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) env.fx.EXPECT().VerifyPermission(gomock.Any(), env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil) env.flowChecker.EXPECT().VerifySpend( @@ -2219,6 +2282,40 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { }, err: ErrFlowCheckFailed, }, + { + name: "invalid if subnet manager is set", + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + env := newValidTransformSubnetTxVerifyEnv(t, ctrl) + + // Set dependency expectations. + subnetOwner := fx.NewMockOwner(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.GenerateTestID(), make([]byte, 20), nil) + env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) + env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) + + cfg := defaultTestConfig(t, durango, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: ErrIsImmutable, + }, { name: "valid tx", newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { @@ -2228,6 +2325,7 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { subnetOwner := fx.NewMockOwner(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.Empty, nil, database.ErrNotFound).Times(1) env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) env.flowChecker.EXPECT().VerifySpend( @@ -2272,6 +2370,354 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } } +// Returns a ConvertSubnetTx that passes syntactic verification. +// Memo field is empty as required post Durango activation +func newConvertSubnetTx(t *testing.T) (*txs.ConvertSubnetTx, *txs.Tx) { + t.Helper() + + creds := []verify.Verifiable{ + &secp256k1fx.Credential{ + Sigs: make([][65]byte, 1), + }, + &secp256k1fx.Credential{ + Sigs: make([][65]byte, 1), + }, + } + unsignedTx := &txs.ConvertSubnetTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + Ins: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{ + TxID: ids.GenerateTestID(), + }, + Asset: avax.Asset{ + ID: ids.GenerateTestID(), + }, + In: &secp256k1fx.TransferInput{ + Amt: 1, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0, 1}, + }, + }, + }}, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: ids.GenerateTestID(), + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ids.GenerateTestShortID()}, + }, + }, + }, + }, + }, + }, + Subnet: ids.GenerateTestID(), + ChainID: ids.GenerateTestID(), + Address: make([]byte, 24), + SubnetAuth: &secp256k1fx.Credential{ + Sigs: make([][65]byte, 1), + }, + } + tx := &txs.Tx{ + Unsigned: unsignedTx, + Creds: creds, + } + require.NoError(t, tx.Initialize(txs.Codec)) + return unsignedTx, tx +} + +// mock implementations that can be used in tests +// for verifying ConvertSubnetTx. +type convertSubnetTxVerifyEnv struct { + latestForkTime time.Time + fx *fx.MockFx + flowChecker *utxo.MockVerifier + unsignedTx *txs.ConvertSubnetTx + tx *txs.Tx + state *state.MockDiff + staker *state.Staker +} + +// Returns mock implementations that can be used in tests +// for verifying ConvertSubnetTx. +func newValidConvertSubnetTxVerifyEnv(t *testing.T, ctrl *gomock.Controller) convertSubnetTxVerifyEnv { + t.Helper() + + now := time.Now() + mockFx := fx.NewMockFx(ctrl) + mockFlowChecker := utxo.NewMockVerifier(ctrl) + unsignedTx, tx := newConvertSubnetTx(t) + mockState := state.NewMockDiff(ctrl) + return convertSubnetTxVerifyEnv{ + latestForkTime: now, + fx: mockFx, + flowChecker: mockFlowChecker, + unsignedTx: unsignedTx, + tx: tx, + state: mockState, + staker: &state.Staker{ + TxID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + }, + } +} + +func TestStandardExecutorConvertSubnetTx(t *testing.T) { + type test struct { + name string + newExecutor func(*gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) + err error + } + + tests := []test{ + { + name: "invalid prior to E-Upgrade", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + + // Set dependency expectations. + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + + cfg := defaultTestConfig(t, durango, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: errEtnaUpgradeNotActive, + }, + { + name: "tx fails syntactic verification", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + // Setting the tx to nil makes the tx fail syntactic verification + env.tx.Unsigned = (*txs.ConvertSubnetTx)(nil) + env.state = state.NewMockDiff(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: txs.ErrNilTx, + }, + { + name: "fail subnet authorization", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + // Remove credentials + env.tx.Creds = nil + env.state = state.NewMockDiff(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: errWrongNumberOfCredentials, + }, + { + name: "flow checker failed", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + env.state = state.NewMockDiff(ctrl) + subnetOwner := fx.NewMockOwner(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.Empty, nil, database.ErrNotFound) + env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) + env.fx.EXPECT().VerifyPermission(gomock.Any(), env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil) + env.flowChecker.EXPECT().VerifySpend( + gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), + ).Return(ErrFlowCheckFailed) + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: ErrFlowCheckFailed, + }, + { + name: "invalid if subnet is transformed", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + + // Set dependency expectations. + subnetOwner := fx.NewMockOwner(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) + env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(&txs.Tx{Unsigned: &txs.TransformSubnetTx{}}, nil).Times(1) + env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: ErrIsImmutable, + }, + { + name: "invalid if subnet is converted", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + + // Set dependency expectations. + subnetOwner := fx.NewMockOwner(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.GenerateTestID(), make([]byte, 24), nil) + env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) + env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: ErrIsImmutable, + }, + { + name: "valid tx", + newExecutor: func(ctrl *gomock.Controller) (*txs.ConvertSubnetTx, *StandardTxExecutor) { + env := newValidConvertSubnetTxVerifyEnv(t, ctrl) + + // Set dependency expectations. + subnetOwner := fx.NewMockOwner(ctrl) + env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() + env.state.EXPECT().GetSubnetOwner(env.unsignedTx.Subnet).Return(subnetOwner, nil).Times(1) + env.state.EXPECT().GetSubnetManager(env.unsignedTx.Subnet).Return(ids.Empty, nil, database.ErrNotFound) + env.state.EXPECT().GetSubnetTransformation(env.unsignedTx.Subnet).Return(nil, database.ErrNotFound).Times(1) + env.fx.EXPECT().VerifyPermission(env.unsignedTx, env.unsignedTx.SubnetAuth, env.tx.Creds[len(env.tx.Creds)-1], subnetOwner).Return(nil).Times(1) + env.flowChecker.EXPECT().VerifySpend( + env.unsignedTx, env.state, env.unsignedTx.Ins, env.unsignedTx.Outs, env.tx.Creds[:len(env.tx.Creds)-1], gomock.Any(), + ).Return(nil).Times(1) + env.state.EXPECT().SetSubnetManager(env.unsignedTx.Subnet, env.unsignedTx.ChainID, env.unsignedTx.Address) + env.state.EXPECT().DeleteUTXO(gomock.Any()).Times(len(env.unsignedTx.Ins)) + env.state.EXPECT().AddUTXO(gomock.Any()).Times(len(env.unsignedTx.Outs)) + + cfg := defaultTestConfig(t, etna, env.latestForkTime) + cfg.MaxStakeDuration = math.MaxInt64 + + feeCalculator := state.PickFeeCalculator(cfg, env.state) + e := &StandardTxExecutor{ + Backend: &Backend{ + Config: cfg, + Bootstrapped: &utils.Atomic[bool]{}, + Fx: env.fx, + FlowChecker: env.flowChecker, + Ctx: &snow.Context{}, + }, + FeeCalculator: feeCalculator, + Tx: env.tx, + State: env.state, + } + e.Bootstrapped.Set(true) + return env.unsignedTx, e + }, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + unsignedTx, executor := tt.newExecutor(ctrl) + err := executor.ConvertSubnetTx(unsignedTx) + require.ErrorIs(t, err, tt.err) + }) + } +} + func defaultTestConfig(t *testing.T, f fork, tm time.Time) *config.Config { c := &config.Config{ UpgradeConfig: upgrade.Config{ diff --git a/vms/platformvm/txs/executor/subnet_tx_verification.go b/vms/platformvm/txs/executor/subnet_tx_verification.go index f1a75f6f2f3f..6a526c9e46f4 100644 --- a/vms/platformvm/txs/executor/subnet_tx_verification.go +++ b/vms/platformvm/txs/executor/subnet_tx_verification.go @@ -16,8 +16,9 @@ import ( var ( errWrongNumberOfCredentials = errors.New("should have the same number of credentials as inputs") - errIsImmutable = errors.New("is immutable") errUnauthorizedSubnetModification = errors.New("unauthorized subnet modification") + + ErrIsImmutable = errors.New("is immutable") ) // verifyPoASubnetAuthorization carries out the validation for modifying a PoA @@ -37,7 +38,15 @@ func verifyPoASubnetAuthorization( _, err = chainState.GetSubnetTransformation(subnetID) if err == nil { - return nil, fmt.Errorf("%q %w", subnetID, errIsImmutable) + return nil, fmt.Errorf("%q %w", subnetID, ErrIsImmutable) + } + if err != database.ErrNotFound { + return nil, err + } + + _, _, err = chainState.GetSubnetManager(subnetID) + if err == nil { + return nil, fmt.Errorf("%q %w", subnetID, ErrIsImmutable) } if err != database.ErrNotFound { return nil, err diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 2334230078a8..f8079d8cba03 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -175,6 +175,18 @@ var ( fee.DBWrite: 1, fee.Compute: 0, } + IntrinsicConvertSubnetTxComplexities = fee.Dimensions{ + fee.Bandwidth: IntrinsicBaseTxComplexities[fee.Bandwidth] + + ids.IDLen + // subnetID + ids.IDLen + // chainID + wrappers.IntLen + // address length + wrappers.IntLen + // subnetAuth typeID + wrappers.IntLen + // owner typeID + wrappers.IntLen, // subnetAuthCredential typeID + fee.DBRead: 1, + fee.DBWrite: 1, + fee.Compute: 0, + } errUnsupportedOutput = errors.New("unsupported output type") errUnsupportedInput = errors.New("unsupported input type") @@ -580,6 +592,33 @@ func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return err } +func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + authComplexity, err := AuthComplexity(tx.SubnetAuth) + if err != nil { + return err + } + output, err := IntrinsicConvertSubnetTxComplexities.Add( + &baseTxComplexity, + &authComplexity, + ) + if err != nil { + return err + } + output[fee.Bandwidth], err = math.Add( + output[fee.Bandwidth], + uint64(len(tx.Memo)), + ) + if err != nil { + return err + } + c.output = output + return nil +} + func baseTxComplexity(tx *txs.BaseTx) (fee.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 9b79e573db70..e63a5e95ddb2 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -119,3 +119,8 @@ func (c *staticVisitor) ExportTx(*txs.ExportTx) error { c.fee = c.config.TxFee return nil } + +func (c *staticVisitor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { + c.fee = c.config.TxFee + return nil +} diff --git a/vms/platformvm/txs/message/codec.go b/vms/platformvm/txs/message/codec.go new file mode 100644 index 000000000000..abc278a51a63 --- /dev/null +++ b/vms/platformvm/txs/message/codec.go @@ -0,0 +1,29 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "errors" + "math" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" +) + +const CodecVersion = 0 + +var Codec codec.Manager + +func init() { + Codec = codec.NewManager(math.MaxInt) + lc := linearcodec.NewDefault() + + err := errors.Join( + lc.RegisterType(&RegisterSubnetValidator{}), + Codec.RegisterCodec(CodecVersion, lc), + ) + if err != nil { + panic(err) + } +} diff --git a/vms/platformvm/txs/message/payload.go b/vms/platformvm/txs/message/payload.go new file mode 100644 index 000000000000..796b3da6cbd6 --- /dev/null +++ b/vms/platformvm/txs/message/payload.go @@ -0,0 +1,35 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "errors" + "fmt" +) + +var errWrongType = errors.New("wrong payload type") + +type Message interface { + Bytes() []byte + + initialize(b []byte) +} + +func Parse(bytes []byte) (Message, error) { + var payload Message + if _, err := Codec.Unmarshal(bytes, &payload); err != nil { + return nil, err + } + payload.initialize(bytes) + return payload, nil +} + +func initialize(p Message) error { + bytes, err := Codec.Marshal(CodecVersion, &p) + if err != nil { + return fmt.Errorf("couldn't marshal %T payload: %w", p, err) + } + p.initialize(bytes) + return nil +} diff --git a/vms/platformvm/txs/message/register_subnet_validator.go b/vms/platformvm/txs/message/register_subnet_validator.go new file mode 100644 index 000000000000..650f7add2f86 --- /dev/null +++ b/vms/platformvm/txs/message/register_subnet_validator.go @@ -0,0 +1,59 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package message + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ Message = (*RegisterSubnetValidator)(nil) + +type RegisterSubnetValidator struct { + SubnetID ids.ID `serialize:"true"` + NodeID ids.NodeID `serialize:"true"` + Weight uint64 `serialize:"true"` + BlsPubKey []byte `serialize:"true"` + Expiry uint64 `serialize:"true"` + + bytes []byte +} + +func NewRegisterSubnetValidator( + subnetID ids.ID, + nodeID ids.NodeID, + weight uint64, + blsPubKey []byte, + expiry uint64, +) (*RegisterSubnetValidator, error) { + msg := &RegisterSubnetValidator{ + SubnetID: subnetID, + NodeID: nodeID, + Weight: weight, + BlsPubKey: blsPubKey, + Expiry: expiry, + } + return msg, initialize(msg) +} + +func ParseRegisterSubnetValidator(b []byte) (*RegisterSubnetValidator, error) { + payloadIntf, err := Parse(b) + if err != nil { + return nil, err + } + payload, ok := payloadIntf.(*RegisterSubnetValidator) + if !ok { + return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + } + return payload, nil +} + +func (r *RegisterSubnetValidator) Bytes() []byte { + return r.bytes +} + +func (r *RegisterSubnetValidator) initialize(bytes []byte) { + r.bytes = bytes +} diff --git a/vms/platformvm/txs/register_subnet_validator_tx.go b/vms/platformvm/txs/register_subnet_validator_tx.go new file mode 100644 index 000000000000..1be220546446 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx.go @@ -0,0 +1,110 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +var ( + _ UnsignedTx = (*RegisterSubnetValidatorTx)(nil) +) + +type RegisterSubnetValidatorTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Balance <= sum($AVAX inputs) - sum($AVAX outputs) - TxFee. + Balance uint64 `serialize:"true" json:"balance"` + // [Signer] is the BLS key for this validator. + // Note: We do not enforce that the BLS key is unique across all validators. + // This means that validators can share a key if they so choose. + // However, a NodeID does uniquely map to a BLS key + Signer signer.Signer `serialize:"true" json:"signer"` + // Leftover $AVAX from the Subnet Validator's Balance will be issued to + // this owner after it is removed from the validator set. + ChangeOwner fx.Owner `serialize:"true" json:"changeOwner"` + // AddressedCall with Payload: + // - SubnetID + // - NodeID (must be Ed25519 NodeID) + // - Weight + // - BLS public key + // - Expiry + Message warp.Message `serialize:"true" json:"message"` + + // true iff this transaction has already passed syntactic verification + SyntacticallyVerified bool `json:"-"` + + // Populated during syntactic verification + SourceAddress []byte `json:"-"` + ParsedMessage *message.RegisterSubnetValidator `json:"-"` +} + +func (tx *RegisterSubnetValidatorTx) PublicKey() (*bls.PublicKey, bool, error) { + if err := tx.Signer.Verify(); err != nil { + return nil, false, err + } + key := tx.Signer.Key() + return key, key != nil, nil +} + +// SyntacticVerify returns nil iff [tx] is valid +func (tx *RegisterSubnetValidatorTx) 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 fmt.Errorf("failed to verify BaseTx: %w", err) + } + if err := tx.Signer.Verify(); err != nil { + return fmt.Errorf("failed to verify signer: %w", err) + } + + addressedCall, err := payload.ParseAddressedCall(tx.Message.Payload) + if err != nil { + return fmt.Errorf("failed to parse AddressedCall: %w", err) + } + + msg, err := message.ParseRegisterSubnetValidator(addressedCall.Payload) + if err != nil { + return fmt.Errorf("failed to parse RegisterSubnetValidator: %w", err) + } + + switch { + case msg.SubnetID == constants.PrimaryNetworkID: + return errors.New("cannot add Primary Network Validator") + case msg.NodeID == ids.EmptyNodeID: + return errors.New("cannot add EmptyNodeID") + case msg.Weight == 0: + return errors.New("cannot add Subnet Validator with weight of 0") + case msg.Expiry == 0: + return errors.New("cannot add Subnet Validator with expiry of 0") + case len(msg.BlsPubKey) != bls.PublicKeyLen: + return errors.New("invalid bls public key len") + } + + // cache that this is valid + tx.SyntacticallyVerified = true + tx.SourceAddress = addressedCall.SourceAddress + tx.ParsedMessage = msg + return nil +} + +func (tx *RegisterSubnetValidatorTx) Visit(visitor Visitor) error { + return visitor.RegisterSubnetValidatorTx(tx) +} diff --git a/vms/platformvm/txs/register_subnet_validator_tx_test.go b/vms/platformvm/txs/register_subnet_validator_tx_test.go new file mode 100644 index 000000000000..e0cb8d76622b --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx_test.go @@ -0,0 +1,4 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index b3fc55af4cf3..8093b6218023 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -19,5 +19,7 @@ type Visitor interface { AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error + ConvertSubnetTx(*ConvertSubnetTx) error + RegisterSubnetValidatorTx(*RegisterSubnetValidatorTx) error BaseTx(*BaseTx) error } diff --git a/wallet/chain/p/backend_visitor.go b/wallet/chain/p/backend_visitor.go index c7cec9544da1..386bd86c005c 100644 --- a/wallet/chain/p/backend_visitor.go +++ b/wallet/chain/p/backend_visitor.go @@ -70,6 +70,11 @@ func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnersh return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + // TODO: handle subnet conversion + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { return b.baseTx(tx) } diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index e15267893fcf..1ffe739522fd 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -151,6 +151,18 @@ type Builder interface { options ...common.Option, ) (*txs.TransferSubnetOwnershipTx, error) + // NewConvertSubnetTx converts the subnet to a Permissionless L1. + // + // - [subnetID] specifies the subnet to be converted + // - [chainID] specifies which chain the manager is deployed on + // - [address] specifies the address of the manager + NewConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []byte, + options ...common.Option, + ) (*txs.ConvertSubnetTx, error) + // NewImportTx creates an import transaction that attempts to consume all // the available UTXOs and import the funds to [to]. // @@ -767,6 +779,67 @@ func (b *builder) NewTransferSubnetOwnershipTx( return tx, b.initCtx(tx) } +func (b *builder) NewConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []byte, + options ...common.Option, +) (*txs.ConvertSubnetTx, error) { + toBurn := map[ids.ID]uint64{ + b.context.AVAXAssetID: b.context.StaticFeeConfig.TxFee, + } + toStake := map[ids.ID]uint64{} + + ops := common.NewOptions(options) + subnetAuth, err := b.authorizeSubnet(subnetID, ops) + if err != nil { + return nil, err + } + + memo := ops.Memo() + bytesComplexity := feecomponent.Dimensions{ + feecomponent.Bandwidth: uint64(len(memo)) + uint64(len(address)), + } + authComplexity, err := txfee.AuthComplexity(subnetAuth) + if err != nil { + return nil, err + } + complexity, err := txfee.IntrinsicConvertSubnetTxComplexities.Add( + &bytesComplexity, + &authComplexity, + ) + 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.ConvertSubnetTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.context.NetworkID, + BlockchainID: constants.PlatformChainID, + Ins: inputs, + Outs: outputs, + Memo: memo, + }}, + Subnet: subnetID, + ChainID: chainID, + Address: address, + SubnetAuth: subnetAuth, + } + 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 d831e0c76daa..4a473cf50bac 100644 --- a/wallet/chain/p/builder/builder_with_options.go +++ b/wallet/chain/p/builder/builder_with_options.go @@ -155,6 +155,20 @@ func (b *builderWithOptions) NewTransferSubnetOwnershipTx( ) } +func (b *builderWithOptions) NewConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []byte, + options ...common.Option, +) (*txs.ConvertSubnetTx, error) { + return b.builder.NewConvertSubnetTx( + subnetID, + chainID, + address, + 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 5dd4abe2b7d2..38d501c908b0 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -156,6 +156,19 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return sign(s.tx, true, txSigners) } +func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + txSigners = append(txSigners, subnetAuthSigners) + 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.go b/wallet/chain/p/wallet.go index 631553431ef7..56d30f27654f 100644 --- a/wallet/chain/p/wallet.go +++ b/wallet/chain/p/wallet.go @@ -131,6 +131,19 @@ type Wallet interface { options ...common.Option, ) (*txs.Tx, error) + // NewConvertSubnetTx creates, signs, and issues a transaction that converts + // the subnet to a Permissionless L1. + // + // - [subnetID] specifies the subnet to be converted + // - [chainID] specifies which chain the manager is deployed on + // - [address] specifies the address of the manager + IssueConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []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]. // @@ -380,6 +393,19 @@ func (w *wallet) IssueTransferSubnetOwnershipTx( return w.IssueUnsignedTx(utx, options...) } +func (w *wallet) IssueConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []byte, + options ...common.Option, +) (*txs.Tx, error) { + utx, err := w.builder.NewConvertSubnetTx(subnetID, chainID, address, 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 92965f2e4f1f..fb5ab1b0e5a5 100644 --- a/wallet/chain/p/wallet_with_options.go +++ b/wallet/chain/p/wallet_with_options.go @@ -143,6 +143,20 @@ func (w *walletWithOptions) IssueTransferSubnetOwnershipTx( ) } +func (w *walletWithOptions) IssueConvertSubnetTx( + subnetID ids.ID, + chainID ids.ID, + address []byte, + options ...common.Option, +) (*txs.Tx, error) { + return w.wallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + common.UnionOptions(w.options, options)..., + ) +} + func (w *walletWithOptions) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners,