mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM. Executor semantics (standard_tx_executor): - registerOwnSet(): shared primitive — per-validator state.L1Validator with native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion manager-authority recording (byte-for-byte legacy tail). - ConvertNetworkTx: promote endomorphism (owner-authorized, folds old ConvertNetworkToL1Tx), gated by security.Mode.Manager. - CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set. - Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx. - txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec. All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free; wallet/network/primary off txs.Codec. KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads tx.Chains() only to derive managerChainID — it does NOT create the genesis chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
1135 lines
32 KiB
Go
1135 lines
32 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package p
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
stdcontext "context"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/node/vms/platformvm/security"
|
|
"github.com/luxfi/node/vms/platformvm/signer"
|
|
"github.com/luxfi/node/vms/platformvm/stakeable"
|
|
"github.com/luxfi/node/vms/platformvm/txs"
|
|
"github.com/luxfi/node/wallet/chain/p/builder"
|
|
"github.com/luxfi/node/wallet/network/primary/common"
|
|
"github.com/luxfi/utils"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
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")
|
|
|
|
_ Builder = (*txBuilder)(nil)
|
|
)
|
|
|
|
// Builder provides a convenient interface for building unsigned P-chain
|
|
// transactions.
|
|
type Builder interface {
|
|
// GetBalance calculates the amount of each asset that this builder has
|
|
// control over.
|
|
GetBalance(
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error)
|
|
|
|
// GetImportableBalance calculates the amount of each asset that this
|
|
// builder could import from the provided chain.
|
|
//
|
|
// - [chainID] specifies the chain the funds are from.
|
|
GetImportableBalance(
|
|
chainID ids.ID,
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error)
|
|
|
|
// NewBaseTx creates a new simple value transfer.
|
|
//
|
|
// - [outputs] specifies all the recipients and amounts that should be sent
|
|
// from this transaction.
|
|
NewBaseTx(
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.BaseTx, error)
|
|
|
|
// NewAddValidatorTx creates a new validator of the primary network.
|
|
//
|
|
// - [vdr] specifies all the details of the validation period such as the
|
|
// startTime, endTime, stake weight, and nodeID.
|
|
// - [rewardsOwner] specifies the owner of all the rewards this validator
|
|
// may accrue during its validation period.
|
|
// - [shares] specifies the fraction (out of 1,000,000) that this validator
|
|
// will take from delegation rewards. If 1,000,000 is provided, 100% of
|
|
// the delegation reward will be sent to the validator's [rewardsOwner].
|
|
NewAddValidatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddValidatorTx, error)
|
|
|
|
// NewAddChainValidatorTx builds an unsigned legacy per-chain
|
|
// validator registration tx.
|
|
//
|
|
// - [vdr] specifies all the details of the validation period such as the
|
|
// startTime, endTime, sampling weight, nodeID, and netID.
|
|
//
|
|
// Deprecated: Use NewAddValidatorTx. Under LP-018 sovereign-L1,
|
|
// validators join a network — never a chain. A sovereign L1 IS a
|
|
// primary network at its own networkID. Retained for one release
|
|
// cycle for wire/codec compat with pre-LP-018 binaries.
|
|
NewAddChainValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
options ...common.Option,
|
|
) (*txs.AddChainValidatorTx, error)
|
|
|
|
// NewRemoveChainValidatorTx removes [nodeID] from the validator
|
|
// set [netID].
|
|
NewRemoveChainValidatorTx(
|
|
nodeID ids.NodeID,
|
|
netID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.RemoveChainValidatorTx, error)
|
|
|
|
// NewAddDelegatorTx creates a new delegator to a validator on the primary
|
|
// network.
|
|
//
|
|
// - [vdr] specifies all the details of the delegation period such as the
|
|
// startTime, endTime, stake weight, and validator's nodeID.
|
|
// - [rewardsOwner] specifies the owner of all the rewards this delegator
|
|
// may accrue at the end of its delegation period.
|
|
NewAddDelegatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddDelegatorTx, error)
|
|
|
|
// NewCreateChainTx creates a new chain in the named chain.
|
|
//
|
|
// - [netID] specifies the net to launch the chain in.
|
|
// - [genesis] specifies the initial state of the new chain.
|
|
// - [vmID] specifies the vm that the new chain will run.
|
|
// - [fxIDs] specifies all the feature extensions that the vm should be
|
|
// running with.
|
|
// - [chainName] specifies a human readable name for the chain.
|
|
NewCreateChainTx(
|
|
netID ids.ID,
|
|
genesis []byte,
|
|
vmID ids.ID,
|
|
fxIDs []ids.ID,
|
|
chainName string,
|
|
options ...common.Option,
|
|
) (*txs.CreateChainTx, error)
|
|
|
|
// NewCreateNetworkTx creates a new network with the specified owner.
|
|
//
|
|
// - [owner] specifies who has the ability to create new chains and add new
|
|
// validators to the network.
|
|
NewCreateNetworkTx(
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.CreateNetworkTx, error)
|
|
|
|
// NewImportTx creates an import transaction that attempts to consume all
|
|
// the available UTXOs and import the funds to [to].
|
|
//
|
|
// - [chainID] specifies the chain to be importing funds from.
|
|
// - [to] specifies where to send the imported funds to.
|
|
NewImportTx(
|
|
chainID ids.ID,
|
|
to *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.ImportTx, error)
|
|
|
|
// NewExportTx creates an export transaction that attempts to send all the
|
|
// provided [outputs] to the requested [chainID].
|
|
//
|
|
// - [chainID] specifies the chain to be exporting the funds to.
|
|
// - [outputs] specifies the outputs to send to the [chainID].
|
|
NewExportTx(
|
|
chainID ids.ID,
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.ExportTx, error)
|
|
|
|
// NewTransformChainTx creates a transform net transaction that attempts
|
|
// to convert the provided [netID] from a permissioned net to a
|
|
// permissionless chain. This transaction will convert
|
|
// [maxSupply] - [initialSupply] of [assetID] to staking rewards.
|
|
//
|
|
// - [netID] specifies the net to transform.
|
|
// - [assetID] specifies the asset to use to reward stakers on the chain.
|
|
// - [initialSupply] is the amount of [assetID] that will be in circulation
|
|
// after this transaction is accepted.
|
|
// - [maxSupply] is the maximum total amount of [assetID] that should ever
|
|
// exist.
|
|
// - [minConsumptionRate] is the rate that a staker will receive rewards
|
|
// if they stake with a duration of 0.
|
|
// - [maxConsumptionRate] is the maximum rate that staking rewards should be
|
|
// consumed from the reward pool per year.
|
|
// - [minValidatorStake] is the minimum amount of funds required to become a
|
|
// validator.
|
|
// - [maxValidatorStake] is the maximum amount of funds a single validator
|
|
// can be allocated, including delegated funds.
|
|
// - [minStakeDuration] is the minimum number of seconds a staker can stake
|
|
// for.
|
|
// - [maxStakeDuration] is the maximum number of seconds a staker can stake
|
|
// for.
|
|
// - [minValidatorStake] is the minimum amount of funds required to become a
|
|
// delegator.
|
|
// - [maxValidatorWeightFactor] is the factor which calculates the maximum
|
|
// amount of delegation a validator can receive. A value of 1 effectively
|
|
// disables delegation.
|
|
// - [uptimeRequirement] is the minimum percentage a validator must be
|
|
// online and responsive to receive a reward.
|
|
NewTransformChainTx(
|
|
netID ids.ID,
|
|
assetID ids.ID,
|
|
initialSupply uint64,
|
|
maxSupply uint64,
|
|
minConsumptionRate uint64,
|
|
maxConsumptionRate uint64,
|
|
minValidatorStake uint64,
|
|
maxValidatorStake uint64,
|
|
minStakeDuration time.Duration,
|
|
maxStakeDuration time.Duration,
|
|
minDelegationFee uint32,
|
|
minDelegatorStake uint64,
|
|
maxValidatorWeightFactor byte,
|
|
uptimeRequirement uint32,
|
|
options ...common.Option,
|
|
) (*txs.TransformChainTx, error)
|
|
|
|
// NewAddPermissionlessValidatorTx creates a new validator of the specified
|
|
// chain.
|
|
//
|
|
// - [vdr] specifies all the details of the validation period such as the
|
|
// netID, startTime, endTime, stake weight, and nodeID.
|
|
// - [signer] if the netID is the primary network, this is the BLS key
|
|
// for this validator. Otherwise, this value should be the empty signer.
|
|
// - [assetID] specifies the asset to stake.
|
|
// - [validationRewardsOwner] specifies the owner of all the rewards this
|
|
// validator earns for its validation period.
|
|
// - [delegationRewardsOwner] specifies the owner of all the rewards this
|
|
// validator earns for delegations during its validation period.
|
|
// - [shares] specifies the fraction (out of 1,000,000) that this validator
|
|
// will take from delegation rewards. If 1,000,000 is provided, 100% of
|
|
// the delegation reward will be sent to the validator's [rewardsOwner].
|
|
NewAddPermissionlessValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
signer signer.Signer,
|
|
assetID ids.ID,
|
|
validationRewardsOwner *secp256k1fx.OutputOwners,
|
|
delegationRewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessValidatorTx, error)
|
|
|
|
// NewAddPermissionlessDelegatorTx creates a new delegator of the specified
|
|
// net on the specified nodeID.
|
|
//
|
|
// - [vdr] specifies all the details of the delegation period such as the
|
|
// netID, startTime, endTime, stake weight, and nodeID.
|
|
// - [assetID] specifies the asset to stake.
|
|
// - [rewardsOwner] specifies the owner of all the rewards this delegator
|
|
// earns during its delegation period.
|
|
NewAddPermissionlessDelegatorTx(
|
|
vdr *txs.ChainValidator,
|
|
assetID ids.ID,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessDelegatorTx, error)
|
|
}
|
|
|
|
// BuilderBackend specifies the required information needed to build unsigned
|
|
// P-chain transactions.
|
|
type BuilderBackend interface {
|
|
builder.Backend
|
|
GetTx(ctx stdcontext.Context, txID ids.ID) (*txs.Tx, error)
|
|
}
|
|
|
|
type txBuilder struct {
|
|
addrs set.Set[ids.ShortID]
|
|
context *builder.Context
|
|
backend BuilderBackend
|
|
}
|
|
|
|
// NewBuilder returns a new transaction builder.
|
|
//
|
|
// - [addrs] is the set of addresses that the builder assumes can be used when
|
|
// signing the transactions in the future.
|
|
// - [context] provides the chain's configuration.
|
|
// - [backend] provides the required access to the chain's state
|
|
// to build out the transactions.
|
|
func NewBuilder(addrs set.Set[ids.ShortID], context *builder.Context, backend BuilderBackend) Builder {
|
|
return &txBuilder{
|
|
addrs: addrs,
|
|
context: context,
|
|
backend: backend,
|
|
}
|
|
}
|
|
|
|
func (b *txBuilder) GetBalance(
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(constants.PlatformChainID, ops)
|
|
}
|
|
|
|
func (b *txBuilder) GetImportableBalance(
|
|
chainID ids.ID,
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(chainID, ops)
|
|
}
|
|
|
|
func (b *txBuilder) NewBaseTx(
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.BaseTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
|
|
}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
|
|
ops := common.NewOptions(options)
|
|
inputs, changeOutputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputs = append(outputs, changeOutputs...)
|
|
lux.SortTransferableOutputs(outputs) // sort the outputs
|
|
|
|
return txs.NewBaseTx(&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
})
|
|
}
|
|
|
|
func (b *txBuilder) NewAddValidatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddValidatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{
|
|
utxoAssetID: b.context.StaticFeeConfig.AddNetworkValidatorFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{
|
|
utxoAssetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
return txs.NewAddValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
*vdr,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
shares,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewAddChainValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
options ...common.Option,
|
|
) (*txs.AddChainValidatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.AddChainValidatorFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainAuth, err := b.authorizeNet(vdr.Chain, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewAddChainValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewRemoveChainValidatorTx(
|
|
nodeID ids.NodeID,
|
|
netID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.RemoveChainValidatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainAuth, err := b.authorizeNet(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewRemoveChainValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
nodeID,
|
|
netID,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewAddDelegatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddDelegatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{
|
|
utxoAssetID: b.context.StaticFeeConfig.AddNetworkDelegatorFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
return txs.NewAddDelegatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
*vdr,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewCreateChainTx(
|
|
netID ids.ID,
|
|
genesis []byte,
|
|
vmID ids.ID,
|
|
fxIDs []ids.ID,
|
|
chainName string,
|
|
options ...common.Option,
|
|
) (*txs.CreateChainTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.CreateChainTxFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainAuth, err := b.authorizeNet(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(fxIDs)
|
|
return txs.NewCreateChainTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
netID,
|
|
chainName,
|
|
vmID,
|
|
fxIDs,
|
|
genesis,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewCreateNetworkTx(
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.CreateNetworkTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.CreateNetworkTxFee,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(owner.Addrs)
|
|
// Plain "create an empty network under the primary network": no own
|
|
// validators or chains, no manager. Parent = primary network;
|
|
// security inherited (restaked from parent).
|
|
return txs.NewCreateNetworkTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
constants.PrimaryNetworkID, // parent
|
|
owner,
|
|
security.Mode{RestakeParent: true, Manager: security.PChain}, // restaked L2 under primary
|
|
nil, // validators
|
|
nil, // chains
|
|
0, // managerChainIdx
|
|
nil, // managerAddress
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewImportTx(
|
|
sourceChainID ids.ID,
|
|
to *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.ImportTx, error) {
|
|
ops := common.NewOptions(options)
|
|
utxos, err := b.backend.UTXOs(ops.Context(), sourceChainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
addrs = ops.Addresses(b.addrs)
|
|
minIssuanceTime = ops.MinIssuanceTime()
|
|
utxoAssetID = b.context.UTXOAssetID
|
|
txFee = b.context.StaticFeeConfig.TxFee
|
|
|
|
importedInputs = make([]*lux.TransferableInput, 0, len(utxos))
|
|
importedAmounts = make(map[ids.ID]uint64)
|
|
)
|
|
// Iterate over the unlocked UTXOs
|
|
for _, utxo := range utxos {
|
|
out, ok := utxo.Out.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
importedInputs = append(importedInputs, &lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
|
|
assetID := utxo.AssetID()
|
|
newImportedAmount, err := math.Add64(importedAmounts[assetID], out.Amt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
importedAmounts[assetID] = newImportedAmount
|
|
}
|
|
utils.Sort(importedInputs) // sort imported inputs
|
|
|
|
if len(importedInputs) == 0 {
|
|
return nil, fmt.Errorf(
|
|
"%w: no UTXOs available to import",
|
|
errInsufficientFunds,
|
|
)
|
|
}
|
|
|
|
var (
|
|
inputs []*lux.TransferableInput
|
|
outputs = make([]*lux.TransferableOutput, 0, len(importedAmounts))
|
|
importedLUX = importedAmounts[utxoAssetID]
|
|
)
|
|
if importedLUX > txFee {
|
|
importedAmounts[utxoAssetID] -= txFee
|
|
} else {
|
|
if importedLUX < txFee { // imported amount goes toward paying tx fee
|
|
toBurn := map[ids.ID]uint64{
|
|
utxoAssetID: txFee - importedLUX,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
var err error
|
|
inputs, outputs, _, err = b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
|
|
}
|
|
}
|
|
delete(importedAmounts, utxoAssetID)
|
|
}
|
|
|
|
for assetID, amount := range importedAmounts {
|
|
outputs = append(outputs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amount,
|
|
OutputOwners: *to,
|
|
},
|
|
})
|
|
}
|
|
|
|
lux.SortTransferableOutputs(outputs) // sort imported outputs
|
|
return txs.NewImportTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
sourceChainID,
|
|
importedInputs,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewExportTx(
|
|
chainID ids.ID,
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.ExportTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
|
|
}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, changeOutputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lux.SortTransferableOutputs(outputs) // sort exported outputs
|
|
return txs.NewExportTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: changeOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
chainID,
|
|
outputs,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewTransformChainTx(
|
|
netID ids.ID,
|
|
assetID ids.ID,
|
|
initialSupply uint64,
|
|
maxSupply uint64,
|
|
minConsumptionRate uint64,
|
|
maxConsumptionRate uint64,
|
|
minValidatorStake uint64,
|
|
maxValidatorStake uint64,
|
|
minStakeDuration time.Duration,
|
|
maxStakeDuration time.Duration,
|
|
minDelegationFee uint32,
|
|
minDelegatorStake uint64,
|
|
maxValidatorWeightFactor byte,
|
|
uptimeRequirement uint32,
|
|
options ...common.Option,
|
|
) (*txs.TransformChainTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: b.context.StaticFeeConfig.TransformChainTxFee,
|
|
assetID: maxSupply - initialSupply,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chainAuth, err := b.authorizeNet(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewTransformChainTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
netID,
|
|
assetID,
|
|
initialSupply,
|
|
maxSupply,
|
|
minConsumptionRate,
|
|
maxConsumptionRate,
|
|
minValidatorStake,
|
|
maxValidatorStake,
|
|
uint32(minStakeDuration/time.Second),
|
|
uint32(maxStakeDuration/time.Second),
|
|
minDelegationFee,
|
|
minDelegatorStake,
|
|
maxValidatorWeightFactor,
|
|
uptimeRequirement,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewAddPermissionlessValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
signer signer.Signer,
|
|
assetID ids.ID,
|
|
validationRewardsOwner *secp256k1fx.OutputOwners,
|
|
delegationRewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessValidatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{}
|
|
if vdr.Chain == constants.PrimaryNetworkID {
|
|
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddNetworkValidatorFee
|
|
} else {
|
|
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddChainValidatorFee
|
|
}
|
|
toStake := map[ids.ID]uint64{
|
|
assetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(validationRewardsOwner.Addrs)
|
|
utils.Sort(delegationRewardsOwner.Addrs)
|
|
return txs.NewAddPermissionlessValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
signer,
|
|
stakeOutputs,
|
|
validationRewardsOwner,
|
|
delegationRewardsOwner,
|
|
shares,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) NewAddPermissionlessDelegatorTx(
|
|
vdr *txs.ChainValidator,
|
|
assetID ids.ID,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessDelegatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{}
|
|
if vdr.Chain == constants.PrimaryNetworkID {
|
|
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddNetworkDelegatorFee
|
|
} else {
|
|
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddChainDelegatorFee
|
|
}
|
|
toStake := map[ids.ID]uint64{
|
|
assetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
return txs.NewAddPermissionlessDelegatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) getBalance(
|
|
chainID ids.ID,
|
|
options *common.Options,
|
|
) (
|
|
balance map[ids.ID]uint64,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), chainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
balance = make(map[ids.ID]uint64)
|
|
|
|
// Iterate over the UTXOs
|
|
for _, utxo := range utxos {
|
|
outIntf := utxo.Out
|
|
if lockedOut, ok := outIntf.(*stakeable.LockOut); ok {
|
|
if !options.AllowStakeableLocked() && lockedOut.Locktime > minIssuanceTime {
|
|
// This output is currently locked, so this output can't be
|
|
// burned.
|
|
continue
|
|
}
|
|
outIntf = lockedOut.TransferableOut
|
|
}
|
|
|
|
out, ok := outIntf.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
return nil, errUnknownOutputType
|
|
}
|
|
|
|
_, ok = common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
assetID := utxo.AssetID()
|
|
balance[assetID], err = math.Add64(balance[assetID], out.Amt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return balance, nil
|
|
}
|
|
|
|
// spend takes in the requested burn amounts and the requested stake amounts.
|
|
//
|
|
// - [amountsToBurn] maps assetID to the amount of the asset to spend without
|
|
// producing an output. This is typically used for fees. However, it can
|
|
// also be used to consume some of an asset that will be produced in
|
|
// separate outputs, such as ExportedOutputs. Only unlocked UTXOs are able
|
|
// to be burned here.
|
|
// - [amountsToStake] maps assetID to the amount of the asset to spend and
|
|
// place into the staked outputs. First locked UTXOs are attempted to be
|
|
// used for these funds, and then unlocked UTXOs will be attempted to be
|
|
// used. There is no preferential ordering on the unlock times.
|
|
func (b *txBuilder) spend(
|
|
amountsToBurn map[ids.ID]uint64,
|
|
amountsToStake map[ids.ID]uint64,
|
|
options *common.Options,
|
|
) (
|
|
inputs []*lux.TransferableInput,
|
|
changeOutputs []*lux.TransferableOutput,
|
|
stakeOutputs []*lux.TransferableOutput,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), constants.PlatformChainID)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
addr, ok := addrs.Peek()
|
|
if !ok {
|
|
return nil, nil, nil, errNoChangeAddress
|
|
}
|
|
changeOwner := options.ChangeOwner(&secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{addr},
|
|
})
|
|
|
|
// Iterate over the locked UTXOs
|
|
for _, utxo := range utxos {
|
|
assetID := utxo.AssetID()
|
|
remainingAmountToStake := amountsToStake[assetID]
|
|
|
|
// If we have staked enough of the asset, then we have no need burn
|
|
// more.
|
|
if remainingAmountToStake == 0 {
|
|
continue
|
|
}
|
|
|
|
outIntf := utxo.Out
|
|
lockedOut, ok := outIntf.(*stakeable.LockOut)
|
|
if !ok {
|
|
// This output isn't locked, so it will be handled during the next
|
|
// iteration of the UTXO set
|
|
continue
|
|
}
|
|
if minIssuanceTime >= lockedOut.Locktime {
|
|
// This output isn't locked, so it will be handled during the next
|
|
// iteration of the UTXO set
|
|
continue
|
|
}
|
|
|
|
out, ok := lockedOut.TransferableOut.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
return nil, nil, nil, errUnknownOutputType
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
inputs = append(inputs, &lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &stakeable.LockIn{
|
|
Locktime: lockedOut.Locktime,
|
|
TransferableIn: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
// Stake any value that should be staked
|
|
amountToStake := min(
|
|
remainingAmountToStake, // Amount we still need to stake
|
|
out.Amt, // Amount available to stake
|
|
)
|
|
|
|
// Add the output to the staked outputs
|
|
stakeOutputs = append(stakeOutputs, &lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &stakeable.LockOut{
|
|
Locktime: lockedOut.Locktime,
|
|
TransferableOut: &secp256k1fx.TransferOutput{
|
|
Amt: amountToStake,
|
|
OutputOwners: out.OutputOwners,
|
|
},
|
|
},
|
|
})
|
|
|
|
amountsToStake[assetID] -= amountToStake
|
|
if remainingAmount := out.Amt - amountToStake; remainingAmount > 0 {
|
|
// This input had extra value, so some of it must be returned
|
|
changeOutputs = append(changeOutputs, &lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &stakeable.LockOut{
|
|
Locktime: lockedOut.Locktime,
|
|
TransferableOut: &secp256k1fx.TransferOutput{
|
|
Amt: remainingAmount,
|
|
OutputOwners: out.OutputOwners,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// Iterate over the unlocked UTXOs
|
|
for _, utxo := range utxos {
|
|
assetID := utxo.AssetID()
|
|
remainingAmountToStake := amountsToStake[assetID]
|
|
remainingAmountToBurn := amountsToBurn[assetID]
|
|
|
|
// If we have consumed enough of the asset, then we have no need burn
|
|
// more.
|
|
if remainingAmountToStake == 0 && remainingAmountToBurn == 0 {
|
|
continue
|
|
}
|
|
|
|
outIntf := utxo.Out
|
|
if lockedOut, ok := outIntf.(*stakeable.LockOut); ok {
|
|
if lockedOut.Locktime > minIssuanceTime {
|
|
// This output is currently locked, so this output can't be
|
|
// burned.
|
|
continue
|
|
}
|
|
outIntf = lockedOut.TransferableOut
|
|
}
|
|
|
|
out, ok := outIntf.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
return nil, nil, nil, errUnknownOutputType
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
inputs = append(inputs, &lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Burn any value that should be burned
|
|
amountToBurn := min(
|
|
remainingAmountToBurn, // Amount we still need to burn
|
|
out.Amt, // Amount available to burn
|
|
)
|
|
amountsToBurn[assetID] -= amountToBurn
|
|
|
|
amountAvalibleToStake := out.Amt - amountToBurn
|
|
// Burn any value that should be burned
|
|
amountToStake := min(
|
|
remainingAmountToStake, // Amount we still need to stake
|
|
amountAvalibleToStake, // Amount available to stake
|
|
)
|
|
amountsToStake[assetID] -= amountToStake
|
|
if amountToStake > 0 {
|
|
// Some of this input was put for staking
|
|
stakeOutputs = append(stakeOutputs, &lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amountToStake,
|
|
OutputOwners: *changeOwner,
|
|
},
|
|
})
|
|
}
|
|
if remainingAmount := amountAvalibleToStake - amountToStake; remainingAmount > 0 {
|
|
// This input had extra value, so some of it must be returned
|
|
changeOutputs = append(changeOutputs, &lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: remainingAmount,
|
|
OutputOwners: *changeOwner,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
for assetID, amount := range amountsToStake {
|
|
if amount != 0 {
|
|
return nil, nil, nil, fmt.Errorf(
|
|
"%w: provided UTXOs need %d more units of asset %q to stake",
|
|
errInsufficientFunds,
|
|
amount,
|
|
assetID,
|
|
)
|
|
}
|
|
}
|
|
for assetID, amount := range amountsToBurn {
|
|
if amount != 0 {
|
|
return nil, nil, nil, fmt.Errorf(
|
|
"%w: provided UTXOs need %d more units of asset %q",
|
|
errInsufficientFunds,
|
|
amount,
|
|
assetID,
|
|
)
|
|
}
|
|
}
|
|
|
|
utils.Sort(inputs) // sort inputs
|
|
lux.SortTransferableOutputs(changeOutputs) // sort the change outputs
|
|
lux.SortTransferableOutputs(stakeOutputs) // sort stake outputs
|
|
return inputs, changeOutputs, stakeOutputs, nil
|
|
}
|
|
|
|
func (b *txBuilder) authorizeNet(netID ids.ID, options *common.Options) (*secp256k1fx.Input, error) {
|
|
netTx, err := b.backend.GetTx(options.Context(), netID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to fetch net %q: %w",
|
|
netID,
|
|
err,
|
|
)
|
|
}
|
|
network, ok := netTx.Unsigned.(*txs.CreateNetworkTx)
|
|
if !ok {
|
|
return nil, errWrongTxType
|
|
}
|
|
|
|
owner, ok := network.Owner().(*secp256k1fx.OutputOwners)
|
|
if !ok {
|
|
return nil, errUnknownOwnerType
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
inputSigIndices, ok := common.MatchOwners(owner, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We can't authorize the chain
|
|
return nil, errInsufficientAuthorization
|
|
}
|
|
return &secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
}, nil
|
|
}
|