Files
node/wallet/network/primary/examples/deploy-chains/main.go
T
Hanzo AI 1486f1ec47 txs+wallet: deprecate AddChainValidatorTx under LP-018 sovereign-L1
Validators validate networks; chains live on networks. AddValidatorTx
is the universal add-validator-to-a-network tx whether the target
network is Lux primary (1/2/3/1337) or any downstream sovereign L1's
own primary at its chosen networkID. AddChainValidatorTx is legacy
and kept for one release cycle of wire/codec compat with pre-LP-018
binaries.

- vms/platformvm/txs/add_chain_validator_tx.go: file-header + struct
  Deprecated: notice pointing to AddValidatorTx.
- vms/platformvm/txs/chain_validator.go: ChainValidator descriptor
  marked Deprecated.
- vms/platformvm/txs/add_validator_test.go: new
  TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs covers
  Lux primaries (1/2/3/1337) plus four synthetic IDs across the uint32
  range, asserting the tx body has no per-chain field and accepts any
  valid primary networkID.
- wallet/chain/p/wallet/wallet.go + with_options.go: Deprecated on
  IssueAddChainValidatorTx; IssueRemoveChainValidatorTx doc reworded
  to network-centric language.
- wallet/chain/p/builder.go + builder/builder.go: Deprecated on
  NewAddChainValidatorTx interfaces.
- wallet/network/primary/examples/{add-permissioned-chain-validator,
  bootstrap-hanzo, deploy-chains}/main.go: file-header notes that
  these examples exercise the legacy path; new code uses AddValidatorTx.
- node/validator_manager.go: 4 log strings + 1 comment block rewritten
  to talk about network validator (not chain validator).
- vms/platformvm/health.go: error string 'current chain validator of'
  → 'current network validator on'.
2026-06-08 16:02:58 -07:00

352 lines
11 KiB
Go

// deploy-chains creates chains on the Lux network using the SDK wallet.
//
// Usage:
//
// MNEMONIC="word1 word2 ..." go run . --network=mainnet
// go run . --network=mainnet mainnet-key-01
// MNEMONIC="word1 word2 ..." go run . --network=mainnet --chain=zoo
//
// Deprecated post-chain-create flow: after CreateChainTx, this binary
// adds validators to the created network via the legacy
// IssueAddChainValidatorTx. Under LP-018 sovereign-L1, validators join
// a network via AddValidatorTx — chains live on networks, validators
// do not register per-chain. The legacy call is preserved for one
// release cycle for wire/codec compat with pre-LP-018 binaries.
package main
import (
"context"
"flag"
"fmt"
"github.com/go-json-experiment/json"
"log"
"os"
"path/filepath"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/wallet/network/primary"
"github.com/luxfi/node/wallet/network/primary/examples/keyutil"
"github.com/luxfi/sdk/info"
"github.com/luxfi/sdk/platformvm"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
var evmVMID = ids.FromStringOrPanic("ag3GReYPNuSR17rUP8acMdZipQBikdXNRKDyFszAysmy3vDXE")
type networkConfig struct {
URI string
Suffix string
}
func main() {
network := flag.String("network", "", "network: mainnet, testnet, devnet")
chainFilter := flag.String("chain", "", "deploy only this chain (zoo, hanzo, spc, pars)")
chainNameOverride := flag.String("chain-name", "", "override blockchain name on P-chain (for redeployment)")
uri := flag.String("uri", "", "override node URI")
skipValidators := flag.Bool("skip-validators", false, "skip adding chain validators")
stateDir := flag.String("state-dir", "", "path to state/chains directory")
flag.Parse()
if *network == "" {
log.Fatal("--network required (mainnet, testnet, devnet)")
}
// Resolve state dir
if *stateDir == "" {
home, _ := os.UserHomeDir()
*stateDir = filepath.Join(home, "work/lux/state/chains")
}
nets := map[string]networkConfig{
"mainnet": {URI: "https://api.lux.network", Suffix: "mainnet"},
"testnet": {URI: "https://api.lux-test.network", Suffix: "testnet"},
"devnet": {URI: "https://api.lux-dev.network", Suffix: "devnet"},
}
nc, ok := nets[*network]
if !ok {
log.Fatalf("unknown network: %s", *network)
}
if *uri != "" {
nc.URI = *uri
}
chains := []string{"zoo", "hanzo", "spc", "pars"}
if *chainFilter != "" {
chains = []string{*chainFilter}
}
// Load key
key := keyutil.MustLoadKey()
kc := primary.NewKeychainAdapter(secp256k1fx.NewKeychain(key))
addr := key.PublicKey().Address()
log.Printf("Deploying to %s via %s (addr: %s, hex: %x)", *network, nc.URI, addr, addr[:])
ctx := context.Background()
// Fetch ALL validator node IDs from P-chain
var nodeIDs []ids.NodeID
var minPrimaryEnd uint64
if !*skipValidators {
pClient := platformvm.NewClient(nc.URI)
validators, err := pClient.GetCurrentValidators(ctx, ids.Empty, nil)
if err != nil {
log.Printf("WARNING: could not fetch validators: %v (skipping validators)", err)
*skipValidators = true
} else {
for _, v := range validators {
nodeIDs = append(nodeIDs, v.NodeID)
log.Printf("Validator: %s (end: %d)", v.NodeID, v.EndTime)
if minPrimaryEnd == 0 || v.EndTime < minPrimaryEnd {
minPrimaryEnd = v.EndTime
}
}
log.Printf("Found %d validators (min primary end: %d)", len(nodeIDs), minPrimaryEnd)
}
}
// Also get our connected node ID for info
infoClient := info.NewClient(nc.URI)
myNodeID, _, err := infoClient.GetNodeID(ctx)
if err == nil {
log.Printf("Connected to: %s", myNodeID)
}
// Check P-chain balance via direct RPC first
log.Println("Checking P-chain balance...")
{
pClient := platformvm.NewClient(nc.URI)
balResp, err := pClient.GetBalance(ctx, []ids.ShortID{addr})
if err != nil {
log.Printf("WARNING: direct getBalance failed: %v", err)
} else {
log.Printf("Direct P-chain balance (via RPC): %+v", balResp)
}
}
fundWallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{
URI: nc.URI,
LUXKeychain: kc,
EVMKeychain: kc,
})
if err != nil {
log.Fatalf("wallet sync failed: %v", err)
}
utxoAssetID := fundWallet.X().Builder().Context().UTXOAssetID
pBalanceMap, err := fundWallet.P().Builder().GetBalance()
if err != nil {
log.Fatalf("P-chain balance check failed: %v", err)
}
pBalance := pBalanceMap[utxoAssetID]
log.Printf("Wallet P-chain balance: %d nLUX (%.2f LUX)", pBalance, float64(pBalance)/1e9)
if pBalance < 1_000_000_000 { // Need at least 1 LUX for tx fees
log.Printf("P-chain balance low (%d nLUX). Checking X-chain...", pBalance)
xBalanceMap, _ := fundWallet.X().Builder().GetFTBalance()
xBalance := xBalanceMap[utxoAssetID]
log.Printf("X-chain balance: %d nLUX (%.2f LUX)", xBalance, float64(xBalance)/1e9)
if xBalance > 0 {
exportAmount := xBalance
if exportAmount > 10_000_000_000 {
exportAmount = 10_000_000_000 // Cap at 10 LUX
}
log.Printf("Exporting %d nLUX from X→P...", exportAmount)
xChainID := fundWallet.X().Builder().Context().BlockchainID
exportTx, err := fundWallet.X().IssueExportTx(
constants.PlatformChainID,
[]*lux.TransferableOutput{{
Asset: lux.Asset{ID: utxoAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: exportAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
},
},
}},
)
if err != nil {
log.Printf("X→P export failed: %v", err)
} else {
log.Printf("X→P export tx: %s", exportTx.ID())
time.Sleep(3 * time.Second)
importTx, err := fundWallet.P().IssueImportTx(
xChainID,
&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
},
)
if err != nil {
log.Printf("P-chain import failed: %v", err)
} else {
log.Printf("P-chain import tx: %s", importTx.ID())
time.Sleep(3 * time.Second)
// Re-sync wallet
fundWallet, _ = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: nc.URI,
LUXKeychain: kc,
EVMKeychain: kc,
})
pBalanceMap, _ = fundWallet.P().Builder().GetBalance()
pBalance = pBalanceMap[utxoAssetID]
log.Printf("P-Chain balance after import: %d nLUX (%.2f LUX)", pBalance, float64(pBalance)/1e9)
}
}
}
}
// Deploy each chain
for _, chainName := range chains {
genesisPath := filepath.Join(*stateDir, chainName+"-"+nc.Suffix, "genesis.json")
genesisBytes, err := os.ReadFile(genesisPath)
if err != nil {
log.Printf("SKIP %s: %v", chainName, err)
continue
}
var g map[string]interface{}
if err := json.Unmarshal(genesisBytes, &g); err != nil {
log.Fatalf("invalid genesis for %s: %v", chainName, err)
}
evmChainID := g["config"].(map[string]interface{})["chainId"]
log.Printf("\n=== %s (EVM chainId: %v) ===", chainName, evmChainID)
// Sync wallet
log.Println("Syncing wallet...")
wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{
URI: nc.URI,
LUXKeychain: kc,
EVMKeychain: kc,
})
if err != nil {
log.Fatalf("wallet sync failed: %v", err)
}
pWallet := wallet.P()
// Create chain (network)
log.Println("Creating chain...")
owner := &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
}
createNetTx, err := pWallet.IssueCreateNetworkTx(owner)
if err != nil {
log.Fatalf("create chain failed: %v", err)
}
networkID := createNetTx.ID()
log.Printf("Network: %s", networkID)
// Wait for tx acceptance (mainnet needs longer)
log.Println("Waiting 10s for network tx acceptance...")
time.Sleep(10 * time.Second)
// Re-sync wallet with network tx (retry up to 5 times)
var wallet2 primary.Wallet
for attempt := 0; attempt < 5; attempt++ {
wallet2, err = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: nc.URI,
LUXKeychain: kc,
EVMKeychain: kc,
PChainTxsToFetch: set.Of(networkID),
})
if err == nil {
break
}
log.Printf("Wallet re-sync attempt %d failed: %v, retrying in 5s...", attempt+1, err)
time.Sleep(5 * time.Second)
}
if err != nil {
log.Fatalf("wallet re-sync failed after retries: %v", err)
}
// Create blockchain
deployName := chainName
if *chainNameOverride != "" {
deployName = *chainNameOverride
}
log.Printf("Creating blockchain %s (P-chain name: %s)...", chainName, deployName)
createChainTx, err := wallet2.P().IssueCreateChainTx(
networkID,
genesisBytes,
evmVMID,
nil,
deployName,
)
if err != nil {
log.Fatalf("create chain failed: %v", err)
}
blockchainID := createChainTx.ID()
log.Printf("Blockchain: %s", blockchainID)
// Add ALL validators to the chain network
if !*skipValidators && len(nodeIDs) > 0 {
time.Sleep(2 * time.Second)
wallet3, err := primary.MakeWallet(ctx, &primary.WalletConfig{
URI: nc.URI,
LUXKeychain: kc,
EVMKeychain: kc,
PChainTxsToFetch: set.Of(networkID),
})
if err != nil {
log.Printf("WARNING: validator wallet sync failed: %v", err)
} else {
startTime := time.Now().Add(60 * time.Second)
// Use primary validator end time minus 1 hour buffer, or 300 days
endTime := startTime.Add(300 * 24 * time.Hour)
if minPrimaryEnd > 0 {
primaryEnd := time.Unix(int64(minPrimaryEnd), 0)
safeEnd := primaryEnd.Add(-1 * time.Hour) // 1 hour buffer
if safeEnd.Before(endTime) {
endTime = safeEnd
}
log.Printf("Network validator: start=%s end=%s (primary ends %s)", startTime.Format(time.RFC3339), endTime.Format(time.RFC3339), primaryEnd.Format(time.RFC3339))
}
for _, nodeID := range nodeIDs {
log.Printf("Adding validator %s to network %s...", nodeID, networkID)
_, err := wallet3.P().IssueAddChainValidatorTx(&txs.ChainValidator{
Validator: txs.Validator{
NodeID: nodeID,
Start: uint64(startTime.Unix()),
End: uint64(endTime.Unix()),
Wght: 20,
},
Chain: networkID,
})
if err != nil {
log.Printf("WARNING: add validator %s failed: %v", nodeID, err)
} else {
log.Printf("Validator added: %s", nodeID)
}
time.Sleep(1 * time.Second) // Space out validator txs
}
}
}
fmt.Printf("\n--- %s %s ---\n", chainName, nc.Suffix)
fmt.Printf("Network ID: %s\n", networkID)
fmt.Printf("Blockchain ID: %s\n", blockchainID)
fmt.Printf("VM ID: %s\n", evmVMID)
fmt.Printf("EVM Chain ID: %v\n", evmChainID)
fmt.Println()
}
log.Println("Done!")
log.Println("Next steps:")
log.Println(" 1. Update Helm values with new chain/blockchain IDs")
log.Println(" 2. Restart nodes to track new chains")
log.Println(" 3. Import RLP blocks if available")
}