Files
platformvm/api/static_service.go
zeekay ede37547f6 refactor(platformvm): consolidate block code, drop atomic_block, add v0 lift
- Drop atomic_block.go + legacy *_block_test.go (consolidated)
- Add block/v0/ subdir + lift_v0.go for old version handling
- Copyright header: 'Lux Industries, Inc.' → 'Lux Industries Inc' (no comma)
- block_adapter.go: new adapter layer
2026-06-01 15:22:27 -07:00

405 lines
13 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package api
import (
"cmp"
"errors"
"fmt"
"net/http"
"github.com/luxfi/address"
"github.com/luxfi/formatting"
"github.com/luxfi/ids"
"github.com/luxfi/math"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/genesis"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/txs/txheap"
"github.com/luxfi/utils"
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/utxo/secp256k1fx"
)
// Note that since a Lux network has exactly one Platform Chain,
// and the Platform Chain defines the genesis state of the network
// (who is staking, which chains exist, etc.), defining the genesis
// state of the Platform Chain is the same as defining the genesis
// state of the network.
var (
errUTXOHasNoValue = errors.New("genesis UTXO has no value")
errValidatorHasZeroWeight = errors.New("validator has zero weight")
errValidatorAlreadyExited = errors.New("validator would have already unstaked")
errStakeOverflow = errors.New("validator stake exceeds limit")
_ utils.Sortable[UTXO] = UTXO{}
)
// StaticService defines the static API methods exposed by the platform VM
type StaticService struct{}
// UTXO is a UTXO on the Platform Chain that exists at the chain's genesis.
type UTXO struct {
Locktime json.Uint64 `json:"locktime"`
Amount json.Uint64 `json:"amount"`
Address string `json:"address"`
Message string `json:"message"`
}
// Compare is defined on UTXO (not *UTXO) because it is used as a
// Sortable[T] value type in slices.SortFunc.
func (utxo UTXO) Compare(other UTXO) int {
if locktimeCmp := cmp.Compare(utxo.Locktime, other.Locktime); locktimeCmp != 0 {
return locktimeCmp
}
if amountCmp := cmp.Compare(utxo.Amount, other.Amount); amountCmp != 0 {
return amountCmp
}
utxoAddr, err := bech32ToID(utxo.Address)
if err != nil {
return 0
}
otherAddr, err := bech32ToID(other.Address)
if err != nil {
return 0
}
return utxoAddr.Compare(otherAddr)
}
// PermissionedValidators + PermissionlessValidators.
// APIStaker is the representation of a staker sent via APIs.
// [TxID] is the txID of the transaction that added this staker.
// [Amount] is the amount of tokens being staked.
// [StartTime] is the Unix time when they start staking
// [Endtime] is the Unix time repr. of when they are done staking
// [NodeID] is the node ID of the staker
// [Uptime] is the observed uptime of this staker
type Staker struct {
TxID ids.ID `json:"txID"`
StartTime json.Uint64 `json:"startTime"`
EndTime json.Uint64 `json:"endTime"`
Weight json.Uint64 `json:"weight"`
NodeID ids.NodeID `json:"nodeID"`
// Deprecated: Use Weight instead
StakeAmount *json.Uint64 `json:"stakeAmount,omitempty"`
}
// GenesisValidator should to be used for genesis validators only.
type GenesisValidator Staker
// Owner is the repr. of a reward owner sent over APIs.
type Owner struct {
Locktime json.Uint64 `json:"locktime"`
Threshold json.Uint32 `json:"threshold"`
Addresses []string `json:"addresses"`
}
// PermissionlessValidator is the repr. of a permissionless validator sent over
// APIs.
type PermissionlessValidator struct {
Staker
BaseL1Validator
// Deprecated: RewardOwner has been replaced by ValidationRewardOwner and
// DelegationRewardOwner.
RewardOwner *Owner `json:"rewardOwner,omitempty"`
// The owner of the rewards from the validation period, if applicable.
ValidationRewardOwner *Owner `json:"validationRewardOwner,omitempty"`
// The owner of the rewards from delegations during the validation period,
// if applicable.
DelegationRewardOwner *Owner `json:"delegationRewardOwner,omitempty"`
PotentialReward *json.Uint64 `json:"potentialReward,omitempty"`
AccruedDelegateeReward *json.Uint64 `json:"accruedDelegateeReward,omitempty"`
DelegationFee json.Float32 `json:"delegationFee"`
ExactDelegationFee *json.Uint32 `json:"exactDelegationFee,omitempty"`
Uptime *json.Float32 `json:"uptime,omitempty"`
Connected *bool `json:"connected,omitempty"`
Staked []UTXO `json:"staked,omitempty"`
Signer *signer.ProofOfPossession `json:"signer,omitempty"`
// The delegators delegating to this validator
DelegatorCount *json.Uint64 `json:"delegatorCount,omitempty"`
DelegatorWeight *json.Uint64 `json:"delegatorWeight,omitempty"`
Delegators *[]PrimaryDelegator `json:"delegators,omitempty"`
}
// GenesisPermissionlessValidator should to be used for genesis validators only.
type GenesisPermissionlessValidator struct {
GenesisValidator
RewardOwner *Owner `json:"rewardOwner,omitempty"`
DelegationFee json.Float32 `json:"delegationFee"`
ExactDelegationFee *json.Uint32 `json:"exactDelegationFee,omitempty"`
Staked []UTXO `json:"staked,omitempty"`
Signer *signer.ProofOfPossession `json:"signer,omitempty"`
}
// PermissionedValidator is the repr. of a permissioned validator sent over APIs.
type PermissionedValidator struct {
Staker
// The owner the staking reward, if applicable, will go to
Connected bool `json:"connected"`
Uptime *json.Float32 `json:"uptime,omitempty"`
}
// PrimaryDelegator is the repr. of a primary network delegator sent over APIs.
type PrimaryDelegator struct {
Staker
RewardOwner *Owner `json:"rewardOwner,omitempty"`
PotentialReward *json.Uint64 `json:"potentialReward,omitempty"`
}
// Chain defines a chain that exists
// at the network's genesis.
// [GenesisData] is the initial state of the chain.
// [VMID] is the ID of the VM this chain runs.
// [FxIDs] are the IDs of the Fxs the chain supports.
// [Name] is a human-readable, non-unique name for the chain.
// [ChainID] is the ID of the net that validates the chain
type Chain struct {
GenesisData string `json:"genesisData"`
VMID ids.ID `json:"vmID"`
FxIDs []ids.ID `json:"fxIDs"`
Name string `json:"name"`
ChainID ids.ID `json:"netID"`
}
// BuildGenesisArgs are the arguments used to create
// the genesis data of the Platform Chain.
// [NetworkID] is the ID of the network
// [UTXOs] are the UTXOs on the Platform Chain that exist at genesis.
// [Validators] are the validators of the primary network at genesis.
// [Chains] are the chains that exist at genesis.
// [Time] is the Platform Chain's time at network genesis.
type BuildGenesisArgs struct {
UTXOAssetID ids.ID `json:"utxoAssetID"`
NetworkID json.Uint32 `json:"networkID"`
UTXOs []UTXO `json:"utxos"`
Validators []GenesisPermissionlessValidator `json:"validators"`
Chains []Chain `json:"chains"`
Time json.Uint64 `json:"time"`
InitialSupply json.Uint64 `json:"initialSupply"`
Message string `json:"message"`
Encoding formatting.Encoding `json:"encoding"`
}
// BuildGenesisReply is the reply from BuildGenesis
type BuildGenesisReply struct {
Bytes string `json:"bytes"`
Encoding formatting.Encoding `json:"encoding"`
}
// bech32ToID takes bech32 address and produces a shortID
func bech32ToID(addrStr string) (ids.ShortID, error) {
_, addrBytes, err := address.ParseBech32(addrStr)
if err != nil {
return ids.ShortID{}, err
}
return ids.ToShortID(addrBytes)
}
// BuildGenesis build the genesis state of the Platform Chain (and thereby the Lux network.)
func (*StaticService) BuildGenesis(_ *http.Request, args *BuildGenesisArgs, reply *BuildGenesisReply) error {
// Specify the UTXOs on the Platform chain that exist at genesis.
utxos := make([]*genesis.UTXO, 0, len(args.UTXOs))
for i, apiUTXO := range args.UTXOs {
if apiUTXO.Amount == 0 {
return errUTXOHasNoValue
}
addrID, err := bech32ToID(apiUTXO.Address)
if err != nil {
return err
}
utxo := lux.UTXO{
UTXOID: lux.UTXOID{
TxID: ids.Empty,
OutputIndex: uint32(i),
},
Asset: lux.Asset{ID: args.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: uint64(apiUTXO.Amount),
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{addrID},
},
},
}
if apiUTXO.Locktime > args.Time {
utxo.Out = &stakeable.LockOut{
Locktime: uint64(apiUTXO.Locktime),
TransferableOut: utxo.Out.(lux.TransferableOut),
}
}
messageBytes, err := formatting.Decode(args.Encoding, apiUTXO.Message)
if err != nil {
return fmt.Errorf("problem decoding UTXO message bytes: %w", err)
}
utxos = append(utxos, &genesis.UTXO{
UTXO: utxo,
Message: messageBytes,
})
}
// Specify the validators that are validating the primary network at genesis.
vdrs := txheap.NewByEndTime()
for _, vdr := range args.Validators {
weight := uint64(0)
stake := make([]*lux.TransferableOutput, len(vdr.Staked))
utils.Sort(vdr.Staked)
for i, apiUTXO := range vdr.Staked {
addrID, err := bech32ToID(apiUTXO.Address)
if err != nil {
return err
}
utxo := &lux.TransferableOutput{
Asset: lux.Asset{ID: args.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: uint64(apiUTXO.Amount),
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{addrID},
},
},
}
if apiUTXO.Locktime > args.Time {
utxo.Out = &stakeable.LockOut{
Locktime: uint64(apiUTXO.Locktime),
TransferableOut: utxo.Out,
}
}
stake[i] = utxo
newWeight, err := math.Add64(weight, uint64(apiUTXO.Amount))
if err != nil {
return errStakeOverflow
}
weight = newWeight
}
if weight == 0 {
return errValidatorHasZeroWeight
}
if uint64(vdr.EndTime) <= uint64(args.Time) {
return errValidatorAlreadyExited
}
owner := &secp256k1fx.OutputOwners{
Locktime: uint64(vdr.RewardOwner.Locktime),
Threshold: uint32(vdr.RewardOwner.Threshold),
}
for _, addrStr := range vdr.RewardOwner.Addresses {
addrID, err := bech32ToID(addrStr)
if err != nil {
return err
}
owner.Addrs = append(owner.Addrs, addrID)
}
utils.Sort(owner.Addrs)
delegationFee := uint32(0)
if vdr.ExactDelegationFee != nil {
delegationFee = uint32(*vdr.ExactDelegationFee)
}
var (
baseTx = txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: uint32(args.NetworkID),
BlockchainID: ids.Empty,
}}
validator = txs.Validator{
NodeID: vdr.NodeID,
Start: uint64(args.Time),
End: uint64(vdr.EndTime),
Wght: weight,
}
tx *txs.Tx
)
if vdr.Signer == nil {
tx = &txs.Tx{Unsigned: &txs.AddValidatorTx{
BaseTx: baseTx,
Validator: validator,
StakeOuts: stake,
RewardsOwner: owner,
DelegationShares: delegationFee,
}}
} else {
tx = &txs.Tx{Unsigned: &txs.AddPermissionlessValidatorTx{
BaseTx: baseTx,
Validator: validator,
Signer: vdr.Signer,
StakeOuts: stake,
ValidatorRewardsOwner: owner,
DelegatorRewardsOwner: owner,
DelegationShares: delegationFee,
}}
}
if err := tx.Initialize(txs.GenesisCodec); err != nil {
return err
}
vdrs.Add(tx)
}
// Specify the chains that exist at genesis.
chains := []*txs.Tx{}
for _, chain := range args.Chains {
genesisBytes, err := formatting.Decode(args.Encoding, chain.GenesisData)
if err != nil {
return fmt.Errorf("problem decoding chain genesis data: %w", err)
}
tx := &txs.Tx{Unsigned: &txs.CreateChainTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: uint32(args.NetworkID),
BlockchainID: ids.Empty,
}},
ChainID: chain.ChainID,
BlockchainName: chain.Name,
VMID: chain.VMID,
FxIDs: chain.FxIDs,
GenesisData: genesisBytes,
ChainAuth: &secp256k1fx.Input{},
}}
if err := tx.Initialize(txs.GenesisCodec); err != nil {
return err
}
chains = append(chains, tx)
}
validatorTxs := vdrs.List()
// genesis holds the genesis state
g := genesis.Genesis{
UTXOs: utxos,
Validators: validatorTxs,
Chains: chains,
Timestamp: uint64(args.Time),
InitialSupply: uint64(args.InitialSupply),
Message: args.Message,
}
// Marshal genesis to bytes
bytes, err := genesis.Codec.Marshal(genesis.CodecVersion, g)
if err != nil {
return fmt.Errorf("couldn't marshal genesis: %w", err)
}
reply.Bytes, err = formatting.Encode(args.Encoding, bytes)
if err != nil {
return fmt.Errorf("couldn't encode genesis as string: %w", err)
}
reply.Encoding = args.Encoding
return nil
}