mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Restore ConvertNetworkTx as the promote endomorphism (Create ≠ Convert)
Create and Convert are orthogonal arrows: CreateNetworkTx = ∅→Network (birth, sovereign-or-inherited); ConvertNetworkTx = Network→Network (promote an existing network: inherited→sovereign, re-anchor parent — L2→L1, L3→L1, with owner auth). Only CreateSovereignL1 stays folded (it was birth+sovereign, not a distinct op). Both reuse the NetworkValidator component. Round-trip green incl L2→L1 promote.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/components/verify"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/utils"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/zap"
|
||||
)
|
||||
|
||||
// ConvertNetworkTx PROMOTES an existing network: it changes the network's
|
||||
// security from inherited (restaked from parent) to sovereign (its own
|
||||
// validator set + on-chain manager) and re-anchors its Parent. This is the
|
||||
// endomorphism `Network → Network` — distinct from CreateNetworkTx (the
|
||||
// `∅ → Network` constructor). Examples: an L2 (parent = an L1, inherited)
|
||||
// converts to an L1 (parent = Primary, sovereign); an L3 promotes to L1.
|
||||
//
|
||||
// Requires the existing network owner's authorization (Auth). The genesis
|
||||
// validator set is established here (shared NetworkValidator component); the
|
||||
// manager lives on an existing chain (ManagerChainID) of the network.
|
||||
type ConvertNetworkTx struct {
|
||||
spendingTx
|
||||
}
|
||||
|
||||
var (
|
||||
_ UnsignedTx = (*ConvertNetworkTx)(nil)
|
||||
|
||||
ErrConvertPrimaryNetwork = errors.New("cannot convert the primary network")
|
||||
ErrConvertMustHaveValidators = errors.New("conversion must establish at least one validator")
|
||||
)
|
||||
|
||||
const (
|
||||
offCV_Network = spendSize // 32B — the network being promoted
|
||||
offCV_Parent = spendSize + 32 // 32B — new parent (Primary ⇒ L1)
|
||||
offCV_ManagerChainID = spendSize + 64 // 32B — existing chain hosting manager
|
||||
offCV_ManagerAddress = spendSize + 96 // bytes ptr (8B)
|
||||
offCV_Validators = spendSize + 104 // list ptr (8B)
|
||||
offCV_ValNodeIDPool = spendSize + 112 // bytes ptr (8B)
|
||||
offCV_ValAddrPool = spendSize + 120 // list ptr (8B)
|
||||
offCV_AuthPtr = spendSize + 128 // sig-idx list ptr (8B)
|
||||
sizeCVTx = spendSize + 136
|
||||
)
|
||||
|
||||
func NewConvertNetworkTx(
|
||||
base *lux.BaseTx,
|
||||
network, parent, managerChainID ids.ID,
|
||||
managerAddress []byte,
|
||||
validators []*NetworkValidator,
|
||||
auth verify.Verifiable,
|
||||
) (*ConvertNetworkTx, error) {
|
||||
b := zap.NewBuilder(zap.HeaderSize + 512 + sizeCVTx + len(validators)*nvStride)
|
||||
p, err := writeSpending(b, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vdrOff, vdrCount, nodeIDPool, addrPool := writeNetworkValidators(b, validators)
|
||||
valAddrOff, valAddrCount := 0, 0
|
||||
if len(addrPool) > 0 {
|
||||
alb := b.StartList(addrStride)
|
||||
for _, a := range addrPool {
|
||||
alb.AddBytes(a[:])
|
||||
}
|
||||
valAddrOff, _ = alb.Finish()
|
||||
valAddrCount = len(addrPool)
|
||||
}
|
||||
authOff, authCount, err := writeAuth(b, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ob := b.StartObject(sizeCVTx)
|
||||
setEnvelope(ob, kindConvertNetwork, base, p)
|
||||
setID(ob, offCV_Network, network)
|
||||
setID(ob, offCV_Parent, parent)
|
||||
setID(ob, offCV_ManagerChainID, managerChainID)
|
||||
ob.SetBytes(offCV_ManagerAddress, managerAddress)
|
||||
ob.SetList(offCV_Validators, vdrOff, vdrCount)
|
||||
ob.SetBytes(offCV_ValNodeIDPool, nodeIDPool)
|
||||
ob.SetList(offCV_ValAddrPool, valAddrOff, valAddrCount)
|
||||
ob.SetList(offCV_AuthPtr, authOff, authCount)
|
||||
ob.FinishAsRoot()
|
||||
msg, _ := zap.Parse(b.Finish())
|
||||
return &ConvertNetworkTx{spendingTx{msg: msg}}, nil
|
||||
}
|
||||
|
||||
func (tx *ConvertNetworkTx) Network() ids.ID { return readID(tx.root(), offCV_Network) }
|
||||
func (tx *ConvertNetworkTx) Parent() ids.ID { return readID(tx.root(), offCV_Parent) }
|
||||
func (tx *ConvertNetworkTx) ManagerChainID() ids.ID { return readID(tx.root(), offCV_ManagerChainID) }
|
||||
func (tx *ConvertNetworkTx) ManagerAddress() []byte {
|
||||
if a := tx.root().Bytes(offCV_ManagerAddress); len(a) > 0 {
|
||||
return append([]byte(nil), a...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (tx *ConvertNetworkTx) Validators() []*NetworkValidator {
|
||||
return readNetworkValidators(tx.root(), offCV_Validators, offCV_ValNodeIDPool, offCV_ValAddrPool)
|
||||
}
|
||||
func (tx *ConvertNetworkTx) Auth() verify.Verifiable { return readAuth(tx.root(), offCV_AuthPtr) }
|
||||
|
||||
func (tx *ConvertNetworkTx) SyntacticVerify(rt *runtime.Runtime) error {
|
||||
if tx == nil {
|
||||
return ErrNilTx
|
||||
}
|
||||
vdrs := tx.Validators()
|
||||
switch {
|
||||
case tx.Network() == constants.PrimaryNetworkID:
|
||||
return ErrConvertPrimaryNetwork
|
||||
case len(vdrs) == 0:
|
||||
return ErrConvertMustHaveValidators
|
||||
case !utils.IsSortedAndUnique(vdrs):
|
||||
return ErrValidatorsNotSortedAndUnique
|
||||
case len(tx.ManagerAddress()) > MaxChainAddressLength:
|
||||
return ErrAddressTooLong
|
||||
}
|
||||
if err := verifyBaseTx(tx.baseTx(), rt); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, vdr := range vdrs {
|
||||
if err := vdr.Verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Auth().Verify()
|
||||
}
|
||||
|
||||
func (tx *ConvertNetworkTx) Visit(visitor Visitor) error { return visitor.ConvertNetworkTx(tx) }
|
||||
@@ -368,51 +368,6 @@ func inputComplexity(in *lux.TransferableInput) (gas.Dimensions, error) {
|
||||
return complexity, err
|
||||
}
|
||||
|
||||
// ConvertNetworkToL1ValidatorComplexity returns the complexity the validators
|
||||
// add to a transaction.
|
||||
func ConvertNetworkToL1ValidatorComplexity(l1Validators ...*txs.ConvertNetworkToL1Validator) (gas.Dimensions, error) {
|
||||
var complexity gas.Dimensions
|
||||
for _, l1Validator := range l1Validators {
|
||||
l1ValidatorComplexity, err := convertNetToL1ValidatorComplexity(l1Validator)
|
||||
if err != nil {
|
||||
return gas.Dimensions{}, err
|
||||
}
|
||||
|
||||
complexity, err = complexity.Add(&l1ValidatorComplexity)
|
||||
if err != nil {
|
||||
return gas.Dimensions{}, err
|
||||
}
|
||||
}
|
||||
return complexity, nil
|
||||
}
|
||||
|
||||
func convertNetToL1ValidatorComplexity(l1Validator *txs.ConvertNetworkToL1Validator) (gas.Dimensions, error) {
|
||||
complexity := gas.Dimensions{
|
||||
gas.Bandwidth: intrinsicConvertNetworkToL1ValidatorBandwidth,
|
||||
gas.DBWrite: intrinsicConvertNetworkToL1ValidatorDBWrite,
|
||||
}
|
||||
|
||||
signerComplexity, err := SignerComplexity(&l1Validator.Signer)
|
||||
if err != nil {
|
||||
return gas.Dimensions{}, err
|
||||
}
|
||||
|
||||
numAddresses := uint64(len(l1Validator.RemainingBalanceOwner.Addresses) + len(l1Validator.DeactivationOwner.Addresses))
|
||||
addressBandwidth, err := math.Mul(numAddresses, ids.ShortIDLen)
|
||||
if err != nil {
|
||||
return gas.Dimensions{}, err
|
||||
}
|
||||
return complexity.Add(
|
||||
&gas.Dimensions{
|
||||
gas.Bandwidth: uint64(len(l1Validator.NodeID)),
|
||||
},
|
||||
&signerComplexity,
|
||||
&gas.Dimensions{
|
||||
gas.Bandwidth: addressBandwidth,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// OwnerComplexity returns the complexity an owner adds to a transaction.
|
||||
// It does not include the typeID of the owner.
|
||||
func OwnerComplexity(ownerIntf fx.Owner) (gas.Dimensions, error) {
|
||||
@@ -737,59 +692,6 @@ func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *complexityVisitor) ConvertNetworkToL1Tx(tx *txs.ConvertNetworkToL1Tx) error {
|
||||
baseTxComplexity, err := baseTxComplexity(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatorComplexity, err := ConvertNetworkToL1ValidatorComplexity(tx.Validators()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authComplexity, err := AuthComplexity(tx.ChainAuth())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.output, err = IntrinsicConvertNetworkToL1TxComplexities.Add(
|
||||
&baseTxComplexity,
|
||||
&validatorComplexity,
|
||||
&authComplexity,
|
||||
&gas.Dimensions{
|
||||
gas.Bandwidth: uint64(len(tx.Address())),
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *complexityVisitor) CreateSovereignL1Tx(tx *txs.CreateSovereignL1Tx) error {
|
||||
// Sovereign L1 = network + N validators + K chains. Fee complexity
|
||||
// is approximately the sum: baseTx + ConvertNetworkToL1 (validators)
|
||||
// + sum(CreateChain) (chains). Charge as composite.
|
||||
baseTxComplexity, err := baseTxComplexity(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validatorComplexity, err := ConvertNetworkToL1ValidatorComplexity(tx.Validators()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.output, err = baseTxComplexity.Add(&validatorComplexity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Per-chain genesis adds to the bandwidth dimension.
|
||||
for _, ch := range tx.Chains() {
|
||||
chainBytes := gas.Dimensions{
|
||||
gas.Bandwidth: uint64(len(ch.GenesisData)) + uint64(len(ch.BlockchainName)),
|
||||
}
|
||||
c.output, err = c.output.Add(&chainBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *complexityVisitor) RegisterL1ValidatorTx(tx *txs.RegisterL1ValidatorTx) error {
|
||||
baseTxComplexity, err := baseTxComplexity(tx)
|
||||
if err != nil {
|
||||
|
||||
@@ -125,14 +125,6 @@ func (c *staticVisitor) ExportTx(*txs.ExportTx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*staticVisitor) ConvertNetworkToL1Tx(*txs.ConvertNetworkToL1Tx) error {
|
||||
return ErrUnsupportedTx
|
||||
}
|
||||
|
||||
func (*staticVisitor) CreateSovereignL1Tx(*txs.CreateSovereignL1Tx) error {
|
||||
return ErrUnsupportedTx
|
||||
}
|
||||
|
||||
func (*staticVisitor) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error {
|
||||
return ErrUnsupportedTx
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
kindSetL1ValidatorWeight
|
||||
kindIncreaseL1ValidatorBalance
|
||||
kindDisableL1Validator
|
||||
kindConvertNetwork // promote an existing network: inherited → sovereign, re-anchor parent (L2→L1, L3→L1)
|
||||
)
|
||||
|
||||
// offKind is the fixed wire position of the discriminator (object offset 0).
|
||||
|
||||
@@ -77,6 +77,8 @@ func parseUnsigned(buf []byte) (UnsignedTx, error) {
|
||||
return &IncreaseL1ValidatorBalanceTx{spendingTx{msg: msg}}, nil
|
||||
case kindDisableL1Validator:
|
||||
return &DisableL1ValidatorTx{spendingTx{msg: msg}}, nil
|
||||
case kindConvertNetwork:
|
||||
return &ConvertNetworkTx{spendingTx{msg: msg}}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("zap: unknown tx kind %d", k)
|
||||
}
|
||||
|
||||
@@ -131,6 +131,31 @@ func TestRoundTrip_CreateNetwork_InheritedL2(t *testing.T) {
|
||||
require.Equal([]byte("l2mgr"), got.ManagerAddress(), "inherited L2 still holds a local manager")
|
||||
}
|
||||
|
||||
// TestRoundTrip_ConvertNetwork exercises the promote endomorphism: an existing
|
||||
// network re-anchors to Primary (→L1) and establishes its own validator set +
|
||||
// manager, authorized by its owner.
|
||||
func TestRoundTrip_ConvertNetwork(t *testing.T) {
|
||||
require := require.New(t)
|
||||
base := spendBase()
|
||||
vdr := sampleNetworkValidator()
|
||||
in, err := NewConvertNetworkTx(base,
|
||||
ids.GenerateTestID(), // the L2/L3 network being promoted
|
||||
ids.Empty, // new parent = Primary ⇒ becomes an L1
|
||||
ids.GenerateTestID(), // manager chain
|
||||
[]byte("0xmgr"), // manager address
|
||||
[]*NetworkValidator{vdr}, // sovereign validator set
|
||||
&secp256k1fx.Input{SigIndices: []uint32{0}}, // owner auth
|
||||
)
|
||||
require.NoError(err)
|
||||
got := roundTrip(t, in).(*ConvertNetworkTx)
|
||||
require.Equal(in.Network(), got.Network())
|
||||
require.Equal(ids.Empty, got.Parent(), "re-anchored to Primary ⇒ L1")
|
||||
require.Equal(in.ManagerChainID(), got.ManagerChainID())
|
||||
require.Equal([]byte("0xmgr"), got.ManagerAddress())
|
||||
require.Equal([]*NetworkValidator{vdr}, got.Validators(), "sovereign set established on promote")
|
||||
require.Equal(in.Auth(), got.Auth())
|
||||
}
|
||||
|
||||
func TestRoundTrip_SignedWithCreds(t *testing.T) {
|
||||
require := require.New(t)
|
||||
in, err := NewBaseTx(spendBase())
|
||||
|
||||
@@ -9,6 +9,7 @@ type Visitor interface {
|
||||
AddChainValidatorTx(*AddChainValidatorTx) error
|
||||
AddDelegatorTx(*AddDelegatorTx) error
|
||||
CreateNetworkTx(*CreateNetworkTx) error
|
||||
ConvertNetworkTx(*ConvertNetworkTx) error
|
||||
CreateChainTx(*CreateChainTx) error
|
||||
ImportTx(*ImportTx) error
|
||||
ExportTx(*ExportTx) error
|
||||
|
||||
Reference in New Issue
Block a user