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:
zeekay
2026-07-10 12:13:15 -07:00
parent 4610978bcc
commit 20d36b36aa
7 changed files with 162 additions and 106 deletions
+133
View File
@@ -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) }
-98
View File
@@ -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
}
+1
View File
@@ -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).
+2
View File
@@ -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)
}
+25
View File
@@ -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())
+1
View File
@@ -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