feat(genesis): bootstrap-l2 — preset chain ID (chain:subnetID:chainID) for resume

Extends --existing-subnet-ids parser to accept an optional third field
<chainID>. When present, both IssueCreateNetworkTx and IssueCreateChainTx
are skipped; the tool only waits for isBootstrapped, sends the heartbeat
(if --evm-heartbeat-key is set), and probes eth_blockNumber.

This is the post-partial-failure resume pattern: if hanzo's subnet+chain
were created successfully in a prior run but zoo onwards failed (or were
never attempted), pass

  --existing-subnet-ids=hanzo:2PkWqv...:rHiiTB...,zoo:abc...

and the tool will skip the hanzo P-chain spend, treat hanzo as already
created, and continue with the next chain.

Also gates the AddChainValidatorTx and post-chain wallet re-sync on
presetChainID being unset, since both are unnecessary in resume mode.
This commit is contained in:
Hanzo AI
2026-05-29 18:55:57 -07:00
parent 28b72bed1a
commit 799d5dafef
+74 -45
View File
@@ -148,23 +148,38 @@ func main() {
chains[i] = strings.TrimSpace(chains[i])
}
// Parse --existing-subnet-ids=hanzo:abc,zoo:def
existing := map[string]ids.ID{}
// Parse --existing-subnet-ids=hanzo:<subnetID>[:<chainID>],zoo:<subnetID>...
// If chainID is supplied as the third field, CreateChainTx is also
// skipped — we only heartbeat+probe.
type existingChain struct {
SubnetID ids.ID
ChainID ids.ID // empty if not set
}
existing := map[string]existingChain{}
if strings.TrimSpace(*existingSubnetIDs) != "" {
for _, pair := range strings.Split(*existingSubnetIDs, ",") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
kv := strings.SplitN(pair, ":", 2)
if len(kv) != 2 {
log.Fatalf("--existing-subnet-ids: bad pair %q (want chain:subnetID)", pair)
parts := strings.Split(pair, ":")
if len(parts) < 2 || len(parts) > 3 {
log.Fatalf("--existing-subnet-ids: bad pair %q (want chain:subnetID[:chainID])", pair)
}
sid, err := ids.FromString(strings.TrimSpace(kv[1]))
name := strings.TrimSpace(parts[0])
sid, err := ids.FromString(strings.TrimSpace(parts[1]))
if err != nil {
log.Fatalf("--existing-subnet-ids: bad subnetID for %s: %v", kv[0], err)
log.Fatalf("--existing-subnet-ids: bad subnetID for %s: %v", name, err)
}
existing[strings.TrimSpace(kv[0])] = sid
ec := existingChain{SubnetID: sid}
if len(parts) == 3 {
cid, err := ids.FromString(strings.TrimSpace(parts[2]))
if err != nil {
log.Fatalf("--existing-subnet-ids: bad chainID for %s: %v", name, err)
}
ec.ChainID = cid
}
existing[name] = ec
}
}
@@ -284,9 +299,15 @@ func main() {
log.Printf("[%s] === bootstrapping %s ===", *networkLabel, spec.Name)
var subnetID ids.ID
if sid, ok := existing[spec.Name]; ok {
subnetID = sid
log.Printf("[%s/%s] reusing subnet ID = %s (skipping CreateNetworkTx)", *networkLabel, spec.Name, subnetID)
var presetChainID ids.ID
if ec, ok := existing[spec.Name]; ok {
subnetID = ec.SubnetID
presetChainID = ec.ChainID
if (presetChainID != ids.ID{}) {
log.Printf("[%s/%s] reusing subnet ID = %s + chain ID = %s (skipping both CreateNetworkTx and CreateChainTx)", *networkLabel, spec.Name, subnetID, presetChainID)
} else {
log.Printf("[%s/%s] reusing subnet ID = %s (skipping CreateNetworkTx)", *networkLabel, spec.Name, subnetID)
}
} else {
log.Printf("[%s/%s] IssueCreateNetworkTx", *networkLabel, spec.Name)
createNetTx, err := w.P().IssueCreateNetworkTx(owner)
@@ -298,35 +319,40 @@ func main() {
time.Sleep(*subnetSettleDelay)
}
// Re-sync wallet with the new subnetID tx fetched, so the builder
// can authorize the CreateChain spend.
var w2 primary.Wallet
for i := 0; i < 5; i++ {
w2, err = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: *uri,
LUXKeychain: kc,
EVMKeychain: kc,
PChainTxsToFetch: set.Of(subnetID),
})
if err == nil {
break
var bcID ids.ID
if (presetChainID != ids.ID{}) {
bcID = presetChainID
} else {
// Re-sync wallet with the new subnetID tx fetched, so the builder
// can authorize the CreateChain spend.
var w2 primary.Wallet
for i := 0; i < 5; i++ {
w2, err = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: *uri,
LUXKeychain: kc,
EVMKeychain: kc,
PChainTxsToFetch: set.Of(subnetID),
})
if err == nil {
break
}
log.Printf("[%s] wallet re-sync attempt %d: %v", spec.Name, i+1, err)
time.Sleep(5 * time.Second)
}
log.Printf("[%s] wallet re-sync attempt %d: %v", spec.Name, i+1, err)
time.Sleep(5 * time.Second)
}
if err != nil {
log.Fatalf("[%s] wallet re-sync failed: %v", spec.Name, err)
if err != nil {
log.Fatalf("[%s] wallet re-sync failed: %v", spec.Name, err)
}
log.Printf("[%s/%s] IssueCreateChainTx (vmID=%s)", *networkLabel, spec.Name, vmID)
createChainTx, err := w2.P().IssueCreateChainTx(subnetID, spec.GenesisRaw, vmID, nil, spec.Name)
if err != nil {
log.Fatalf("[%s] create chain: %v", spec.Name, err)
}
bcID = createChainTx.ID()
log.Printf("[%s/%s] blockchain ID = %s", *networkLabel, spec.Name, bcID)
}
log.Printf("[%s/%s] IssueCreateChainTx (vmID=%s)", *networkLabel, spec.Name, vmID)
createChainTx, err := w2.P().IssueCreateChainTx(subnetID, spec.GenesisRaw, vmID, nil, spec.Name)
if err != nil {
log.Fatalf("[%s] create chain: %v", spec.Name, err)
}
bcID := createChainTx.ID()
log.Printf("[%s/%s] blockchain ID = %s", *networkLabel, spec.Name, bcID)
if !*skipValidators && len(nodeIDs) > 0 {
if !*skipValidators && len(nodeIDs) > 0 && (presetChainID == ids.ID{}) {
time.Sleep(3 * time.Second)
w3, err := primary.MakeWallet(ctx, &primary.WalletConfig{
URI: *uri,
@@ -368,14 +394,17 @@ func main() {
// Use w (not w2) for the next chain: w still has the parent wallet
// state for issuing a fresh CreateNetwork. Re-sync w so it picks up
// the spent UTXOs from this iteration.
w, err = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: *uri,
LUXKeychain: kc,
EVMKeychain: kc,
})
if err != nil {
log.Fatalf("[%s] post-chain wallet re-sync: %v", spec.Name, err)
// the spent UTXOs from this iteration. Skip when we didn't touch
// the P-chain (preset chain ID resume mode).
if (presetChainID == ids.ID{}) {
w, err = primary.MakeWallet(ctx, &primary.WalletConfig{
URI: *uri,
LUXKeychain: kc,
EVMKeychain: kc,
})
if err != nil {
log.Fatalf("[%s] post-chain wallet re-sync: %v", spec.Name, err)
}
}
// Wait for bootstrap, send heartbeat if configured, then probe.