Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transfer subnet ownership functionality to wallet #2659

Merged
merged 16 commits into from
Jan 30, 2024
57 changes: 35 additions & 22 deletions wallet/chain/p/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
}
}

Expand All @@ -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 {
Expand All @@ -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
}
9 changes: 8 additions & 1 deletion wallet/chain/p/backend_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand Down
62 changes: 52 additions & 10 deletions wallet/chain/p/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -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].
//
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions wallet/chain/p/builder_with_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion wallet/chain/p/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand Down
11 changes: 3 additions & 8 deletions wallet/chain/p/signer_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
24 changes: 24 additions & 0 deletions wallet/chain/p/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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].
//
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions wallet/chain/p/wallet_with_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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()
Expand Down
Loading