diff --git a/wallet/chain/p/backend.go b/wallet/chain/p/backend.go index d06c7a1f9cf9..6bc4bc64909d 100644 --- a/wallet/chain/p/backend.go +++ b/wallet/chain/p/backend.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) @@ -32,16 +33,30 @@ type backend struct { Context common.ChainUTXOs - txsLock sync.RWMutex - // txID -> tx - txs map[ids.ID]*txs.Tx + subnetOwnerLock sync.RWMutex + subnetOwner map[ids.ID]fx.Owner // subnetID -> owner } -func NewBackend(ctx Context, utxos common.ChainUTXOs, txs map[ids.ID]*txs.Tx) Backend { +func NewBackend(ctx Context, utxos common.ChainUTXOs, subnetTxs map[ids.ID]*txs.Tx) Backend { + subnetOwner := make(map[ids.ID]fx.Owner) + for txID, tx := range subnetTxs { // first get owners from the CreateSubnetTx + createSubnetTx, ok := tx.Unsigned.(*txs.CreateSubnetTx) + if !ok { + continue + } + subnetOwner[txID] = createSubnetTx.Owner + } + for _, tx := range subnetTxs { // then check for TransferSubnetOwnershipTx + transferSubnetOwnershipTx, ok := tx.Unsigned.(*txs.TransferSubnetOwnershipTx) + if !ok { + continue + } + subnetOwner[transferSubnetOwnershipTx.Subnet] = transferSubnetOwnershipTx.Owner + } return &backend{ - Context: ctx, - ChainUTXOs: utxos, - txs: txs, + Context: ctx, + ChainUTXOs: utxos, + subnetOwner: subnetOwner, } } @@ -57,16 +72,7 @@ func (b *backend) AcceptTx(ctx stdcontext.Context, tx *txs.Tx) error { } producedUTXOSlice := tx.UTXOs() - err = b.addUTXOs(ctx, constants.PlatformChainID, producedUTXOSlice) - if err != nil { - return err - } - - b.txsLock.Lock() - defer b.txsLock.Unlock() - - b.txs[txID] = tx - return nil + return b.addUTXOs(ctx, constants.PlatformChainID, producedUTXOSlice) } func (b *backend) addUTXOs(ctx stdcontext.Context, destinationChainID ids.ID, utxos []*avax.UTXO) error { @@ -87,13 +93,20 @@ func (b *backend) removeUTXOs(ctx stdcontext.Context, sourceChain ids.ID, utxoID return nil } -func (b *backend) GetTx(_ stdcontext.Context, txID ids.ID) (*txs.Tx, error) { - b.txsLock.RLock() - defer b.txsLock.RUnlock() +func (b *backend) GetSubnetOwner(_ stdcontext.Context, subnetID ids.ID) (fx.Owner, error) { + b.subnetOwnerLock.RLock() + defer b.subnetOwnerLock.RUnlock() - tx, exists := b.txs[txID] + owner, exists := b.subnetOwner[subnetID] if !exists { return nil, database.ErrNotFound } - return tx, nil + return owner, nil +} + +func (b *backend) setSubnetOwner(subnetID ids.ID, owner fx.Owner) { + b.subnetOwnerLock.Lock() + defer b.subnetOwnerLock.Unlock() + + b.subnetOwner[subnetID] = owner } diff --git a/wallet/chain/p/backend_visitor.go b/wallet/chain/p/backend_visitor.go index 1a0c8e39e8da..c5b8951c70f6 100644 --- a/wallet/chain/p/backend_visitor.go +++ b/wallet/chain/p/backend_visitor.go @@ -46,6 +46,10 @@ func (b *backendVisitor) CreateChainTx(tx *txs.CreateChainTx) error { } func (b *backendVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { + b.b.setSubnetOwner( + b.txID, + tx.Owner, + ) return b.baseTx(&tx.BaseTx) } @@ -54,7 +58,10 @@ func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx } func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - // TODO: Correctly track subnet owners in [getSubnetSigners] + b.b.setSubnetOwner( + tx.Subnet, + tx.Owner, + ) return b.baseTx(&tx.BaseTx) } diff --git a/wallet/chain/p/builder.go b/wallet/chain/p/builder.go index 9a8189c4db6b..a2c7208ffe8a 100644 --- a/wallet/chain/p/builder.go +++ b/wallet/chain/p/builder.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -25,7 +26,6 @@ import ( var ( errNoChangeAddress = errors.New("no possible change address") - errWrongTxType = errors.New("wrong tx type") errUnknownOwnerType = errors.New("unknown owner type") errInsufficientAuthorization = errors.New("insufficient authorization") errInsufficientFunds = errors.New("insufficient funds") @@ -134,6 +134,17 @@ type Builder interface { options ...common.Option, ) (*txs.CreateSubnetTx, error) + // NewTransferSubnetOwnershipTx changes the owner of the named subnet. + // + // - [subnetID] specifies the subnet to be modified + // - [owner] specifies who has the ability to create new chains and add new + // validators to the subnet. + NewTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + options ...common.Option, + ) (*txs.TransferSubnetOwnershipTx, error) + // NewImportTx creates an import transaction that attempts to consume all // the available UTXOs and import the funds to [to]. // @@ -250,7 +261,7 @@ type Builder interface { type BuilderBackend interface { Context UTXOs(ctx stdcontext.Context, sourceChainID ids.ID) ([]*avax.UTXO, error) - GetTx(ctx stdcontext.Context, txID ids.ID) (*txs.Tx, error) + GetSubnetOwner(ctx stdcontext.Context, subnetID ids.ID) (fx.Owner, error) } type builder struct { @@ -532,6 +543,42 @@ func (b *builder) NewCreateSubnetTx( return tx, b.initCtx(tx) } +func (b *builder) NewTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + options ...common.Option, +) (*txs.TransferSubnetOwnershipTx, error) { + toBurn := map[ids.ID]uint64{ + b.backend.AVAXAssetID(): b.backend.BaseTxFee(), + } + toStake := map[ids.ID]uint64{} + ops := common.NewOptions(options) + inputs, outputs, _, err := b.spend(toBurn, toStake, ops) + if err != nil { + return nil, err + } + + subnetAuth, err := b.authorizeSubnet(subnetID, ops) + if err != nil { + return nil, err + } + + utils.Sort(owner.Addrs) + tx := &txs.TransferSubnetOwnershipTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.backend.NetworkID(), + BlockchainID: constants.PlatformChainID, + Ins: inputs, + Outs: outputs, + Memo: ops.Memo(), + }}, + Subnet: subnetID, + Owner: owner, + SubnetAuth: subnetAuth, + } + return tx, b.initCtx(tx) +} + func (b *builder) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, @@ -1100,20 +1147,15 @@ func (b *builder) spend( } func (b *builder) authorizeSubnet(subnetID ids.ID, options *common.Options) (*secp256k1fx.Input, error) { - subnetTx, err := b.backend.GetTx(options.Context(), subnetID) + ownerIntf, err := b.backend.GetSubnetOwner(options.Context(), subnetID) if err != nil { return nil, fmt.Errorf( - "failed to fetch subnet %q: %w", + "failed to fetch subnet owner for %q: %w", subnetID, err, ) } - subnet, ok := subnetTx.Unsigned.(*txs.CreateSubnetTx) - if !ok { - return nil, errWrongTxType - } - - owner, ok := subnet.Owner.(*secp256k1fx.OutputOwners) + owner, ok := ownerIntf.(*secp256k1fx.OutputOwners) if !ok { return nil, errUnknownOwnerType } diff --git a/wallet/chain/p/builder_with_options.go b/wallet/chain/p/builder_with_options.go index 46deab976577..a402355b9e01 100644 --- a/wallet/chain/p/builder_with_options.go +++ b/wallet/chain/p/builder_with_options.go @@ -129,6 +129,18 @@ func (b *builderWithOptions) NewCreateSubnetTx( ) } +func (b *builderWithOptions) NewTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + options ...common.Option, +) (*txs.TransferSubnetOwnershipTx, error) { + return b.Builder.NewTransferSubnetOwnershipTx( + subnetID, + owner, + common.UnionOptions(b.options, options)..., + ) +} + func (b *builderWithOptions) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/signer.go b/wallet/chain/p/signer.go index be2db8ddd2c0..331d8056c111 100644 --- a/wallet/chain/p/signer.go +++ b/wallet/chain/p/signer.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) @@ -21,7 +22,7 @@ type Signer interface { type SignerBackend interface { GetUTXO(ctx stdcontext.Context, chainID, utxoID ids.ID) (*avax.UTXO, error) - GetTx(ctx stdcontext.Context, txID ids.ID) (*txs.Tx, error) + GetSubnetOwner(ctx stdcontext.Context, subnetID ids.ID) (fx.Owner, error) } type txSigner struct { diff --git a/wallet/chain/p/signer_visitor.go b/wallet/chain/p/signer_visitor.go index c5c444a97f13..d4475427fc95 100644 --- a/wallet/chain/p/signer_visitor.go +++ b/wallet/chain/p/signer_visitor.go @@ -246,20 +246,15 @@ func (s *signerVisitor) getSubnetSigners(subnetID ids.ID, subnetAuth verify.Veri return nil, errUnknownSubnetAuthType } - subnetTx, err := s.backend.GetTx(s.ctx, subnetID) + ownerIntf, err := s.backend.GetSubnetOwner(s.ctx, subnetID) if err != nil { return nil, fmt.Errorf( - "failed to fetch subnet %q: %w", + "failed to fetch subnet owner for %q: %w", subnetID, err, ) } - subnet, ok := subnetTx.Unsigned.(*txs.CreateSubnetTx) - if !ok { - return nil, errWrongTxType - } - - owner, ok := subnet.Owner.(*secp256k1fx.OutputOwners) + owner, ok := ownerIntf.(*secp256k1fx.OutputOwners) if !ok { return nil, errUnknownOwnerType } diff --git a/wallet/chain/p/wallet.go b/wallet/chain/p/wallet.go index e982a204a9f8..c5b9ecb0604b 100644 --- a/wallet/chain/p/wallet.go +++ b/wallet/chain/p/wallet.go @@ -122,6 +122,18 @@ type Wallet interface { options ...common.Option, ) (*txs.Tx, error) + // IssueTransferSubnetOwnershipTx creates, signs, and issues a transaction that + // changes the owner of the named subnet. + // + // - [subnetID] specifies the subnet to be modified + // - [owner] specifies who has the ability to create new chains and add new + // validators to the subnet. + IssueTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + 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]. // @@ -359,6 +371,18 @@ func (w *wallet) IssueCreateSubnetTx( return w.IssueUnsignedTx(utx, options...) } +func (w *wallet) IssueTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + options ...common.Option, +) (*txs.Tx, error) { + utx, err := w.builder.NewTransferSubnetOwnershipTx(subnetID, owner, 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 33faa1a86bb0..4982e77f8a51 100644 --- a/wallet/chain/p/wallet_with_options.go +++ b/wallet/chain/p/wallet_with_options.go @@ -124,6 +124,18 @@ func (w *walletWithOptions) IssueCreateSubnetTx( ) } +func (w *walletWithOptions) IssueTransferSubnetOwnershipTx( + subnetID ids.ID, + owner *secp256k1fx.OutputOwners, + options ...common.Option, +) (*txs.Tx, error) { + return w.Wallet.IssueTransferSubnetOwnershipTx( + subnetID, + owner, + common.UnionOptions(w.options, options)..., + ) +} + func (w *walletWithOptions) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/subnet/primary/examples/get-p-chain-balance/main.go b/wallet/subnet/primary/examples/get-p-chain-balance/main.go index fd14eb4ea588..336c25d2dd1b 100644 --- a/wallet/subnet/primary/examples/get-p-chain-balance/main.go +++ b/wallet/subnet/primary/examples/get-p-chain-balance/main.go @@ -8,11 +8,9 @@ import ( "log" "time" - "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/set" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/wallet/chain/p" "github.com/ava-labs/avalanchego/wallet/subnet/primary" ) @@ -38,7 +36,7 @@ func main() { log.Printf("fetched state of %s in %s\n", addrStr, time.Since(fetchStartTime)) pUTXOs := primary.NewChainUTXOs(constants.PlatformChainID, state.UTXOs) - pBackend := p.NewBackend(state.PCTX, pUTXOs, make(map[ids.ID]*txs.Tx)) + pBackend := p.NewBackend(state.PCTX, pUTXOs, nil) pBuilder := p.NewBuilder(addresses, pBackend) currentBalances, err := pBuilder.GetBalance()