mirror of
https://github.com/luxfi/genesis.git
synced 2026-07-27 04:11:41 +00:00
fix(genesis): canonical 0x prefix on all devnet alloc keys + fail-loud bootstrap-l2 (#72)
Two-part fix for the alloc-key shape that bootstrap-l2 consumes:
## 1. Genesis configs patched at source (4 files)
Every alloc key in:
- configs/hanzo-devnet/genesis.json
- configs/pars-devnet/genesis.json
- configs/spc-devnet/genesis.json
- configs/zoo-devnet/genesis.json
now matches the canonical /^0x[0-9a-fA-F]{40}$/ shape. The EVM
genesis loader rejects unprefixed keys; previously bootstrap-l2's
in-memory repair was masking the issue. Fixing at source makes the
configs self-describing and stops the "works for me but won't load
on a fresh node" failure mode.
## 2. bootstrap-l2 normalization is now fail-loud (cmd/bootstrap-l2/main.go)
Pre-flight repair upgrades from silent fix to:
- Normalize 0X → 0x (preserve body case for EIP-55 checksum signal)
- Add missing 0x prefix (repair, log count)
- Reject anything that doesn't match /^0x[0-9a-fA-F]{40}$/ AFTER
repair via log.Fatalf with up to 5 sample keys
Per the user's directive:
> "normalize alloc keys on load/write or fail loudly before chain
> creation. I prefer normalization plus a validation log, not
> silent mutation with no signal."
Log line now shows both healthy and repaired counts:
[devnet] hanzo: alloc keys ok=2 0x-repaired=0 (in-memory only, file unchanged)
A genuinely malformed key (non-hex, wrong length) aborts the entire
bootstrap before any CreateNetworkTx burns LUX:
[devnet] hanzo: 1 malformed alloc keys (require canonical
/^0x[0-9a-fA-F]{40}$/); sample: ["0xZZZ..."]
## 3. Single source of truth: normalizeAllocKey() helper
New pure function returns `(canonical, repaired, valid)`. Every
consumer (the in-memory repair, future validation tooling, any
on-disk rewriter) routes through this one function. Routes:
- body-case PRESERVED (EIP-55 checksum)
- prefix-case NORMALIZED (`0X` → `0x`)
- shape-validated (length 40, hex only)
- idempotent
## Tests
9 new tests in cmd/bootstrap-l2/main_test.go:
- already-canonical roundtrip
- missing-prefix repair flag
- 0X-prefix normalization
- body-case preservation (EIP-55 invariant)
- short body rejection (39/38/empty)
- long body rejection (41)
- non-hex rejection (g, _, space)
- empty string rejection
- idempotency under repeated normalize calls
All pass. Build clean.
This commit is contained in:
+263
-105
@@ -1,19 +1,28 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Command bootstrap-l2 creates one or more L2 EVM subnets on a luxd
|
||||
// devnet/testnet network using the canonical BIP44 m/44'/9000'/0'/0/<idx>
|
||||
// derivation. For each chain it:
|
||||
// Command bootstrap-l2 creates one or more chains on a luxd network using the
|
||||
// canonical BIP44 m/44'/9000'/0'/0/<idx> derivation. For each chain it:
|
||||
//
|
||||
// 1. IssueCreateNetworkTx (P-chain CreateSubnetTx) — owner threshold=1
|
||||
// 2. IssueCreateChainTx — vmID=nyGCobireNhxFB7iM5bxV74hAY6j9nQX6wizxfWomnMMtztkr,
|
||||
// genesis read from the genesis configs dir
|
||||
// 1. IssueCreateNetworkTx (upstream P-chain CreateSubnetTx, exposed in our
|
||||
// wallet as a chain-namespace operation) — owner threshold=1
|
||||
// 2. IssueCreateChainTx — vmID=<--vm-id>, genesis read from the configs dir
|
||||
// 3. IssueAddChainValidatorTx — adds every primary network validator
|
||||
// 4. Probes eth_blockNumber and info.isBootstrapped — fails if either is bad
|
||||
//
|
||||
// Designed to bootstrap the four canonical Lux devnet L2 EVMs (hanzo, zoo,
|
||||
// pars, spc) in one pass, but the chains list is data-driven so it can
|
||||
// bootstrap any subset.
|
||||
// Designed to bootstrap the four canonical Lux devnet chains (hanzo, zoo,
|
||||
// pars, spc) in one pass, but the list is data-driven so it can bootstrap
|
||||
// any subset.
|
||||
//
|
||||
// Idempotency: before any P-chain spend, the tool queries
|
||||
// platform.getBlockchains and skips any chain whose alias already exists.
|
||||
// Re-running the tool is safe — already-bootstrapped chains are detected and
|
||||
// only probed for liveness, never re-created.
|
||||
//
|
||||
// Vocabulary: this tool speaks "chain" — the polymorphic primitive produced
|
||||
// by CreateChainTx, irrespective of L1/L2/L3 level. Upstream luxfi/node API
|
||||
// names (e.g. ConvertSubnetToL1Tx) keep their literal types as imported but
|
||||
// are wrapped here under chain-namespace naming.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
@@ -23,7 +32,7 @@
|
||||
// --bip44-idx=5 \
|
||||
// --network-label=devnet \
|
||||
// --configs-dir=/path/to/genesis/configs \
|
||||
// --chains=hanzo,zoo,pars,spc \
|
||||
// --track-chain-ids=hanzo,zoo,pars,spc \
|
||||
// --output=/dev/stdout
|
||||
package main
|
||||
|
||||
@@ -58,25 +67,23 @@ import (
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// defaultSubnetEVMID is the canonical subnet-evm VM ID present in the luxd
|
||||
// node image's plugin dir. Devnet, testnet, mainnet all expose this plugin
|
||||
// natively (verified via `ls /data/plugins`). Mainnet's L2 EVM subnets
|
||||
// (hanzo/zoo/pars/spc) were created against this same VM ID.
|
||||
//
|
||||
// The brand-namespaced alias nyGCobireNhxFB7iM5bxV74hAY6j9nQX6wizxfWomnMMtztkr
|
||||
// referenced in the 2026-05-27 devnet chain-aliases CM required a runtime
|
||||
// symlink that didn't survive PVC remount; using the native VM ID removes
|
||||
// that failure mode.
|
||||
const defaultSubnetEVMID = "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6"
|
||||
// defaultEVMID is the canonical EVM VM ID present in the luxd node image's
|
||||
// plugin dir. Devnet, testnet, mainnet all expose this plugin natively
|
||||
// (verified via `ls /data/plugins`). The brand-namespaced alias
|
||||
// nyGCobireNhxFB7iM5bxV74hAY6j9nQX6wizxfWomnMMtztkr referenced in the
|
||||
// 2026-05-27 devnet chain-aliases CM required a runtime symlink that didn't
|
||||
// survive PVC remount; using the native VM ID removes that failure mode.
|
||||
const defaultEVMID = "mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6"
|
||||
|
||||
// resultChain is the per-chain output written to --output.
|
||||
type resultChain struct {
|
||||
Name string `json:"name"`
|
||||
SubnetID string `json:"subnetId"`
|
||||
ChainOwnerID string `json:"chainOwnerId"` // upstream "subnetID" — the CreateNetwork tx id
|
||||
BlockchainID string `json:"blockchainId"`
|
||||
EVMChainID uint64 `json:"evmChainId"`
|
||||
FirstBlockHex string `json:"firstBlockHex"`
|
||||
BootstrappedAt string `json:"bootstrappedAt"`
|
||||
Reused bool `json:"reused"` // true when the chain pre-existed and was only probed
|
||||
}
|
||||
|
||||
type result struct {
|
||||
@@ -95,16 +102,19 @@ func main() {
|
||||
hrp := flag.String("hrp", "dev", "P-chain bech32 HRP: test|dev")
|
||||
bipIdx := flag.Uint("bip44-idx", 5, "BIP44 derivation index at m/44'/9000'/0'/0/<idx>")
|
||||
configsDir := flag.String("configs-dir", "", "directory containing <chain>-<network>/genesis.json files (required)")
|
||||
chainsCSV := flag.String("chains", "hanzo,zoo,pars,spc", "comma-separated chain names")
|
||||
vmIDStr := flag.String("vm-id", defaultSubnetEVMID, "subnet-evm VM ID present in luxd's --plugin-dir")
|
||||
// --track-chain-ids mirrors luxd's existing --track-chain-ids flag. One
|
||||
// concept, one flag — this is the declared list of chains this network
|
||||
// serves. The tool reads it, queries the P-chain, and idempotently
|
||||
// creates the missing ones.
|
||||
trackChainIDs := flag.String("track-chain-ids", "hanzo,zoo,pars,spc", "comma-separated chain names (matches luxd --track-chain-ids)")
|
||||
vmIDStr := flag.String("vm-id", defaultEVMID, "EVM VM ID present in luxd's --plugin-dir")
|
||||
output := flag.String("output", "/dev/stdout", "result JSON output path")
|
||||
skipValidators := flag.Bool("skip-validators", false, "skip adding primary validators as subnet validators")
|
||||
skipValidators := flag.Bool("skip-validators", false, "skip adding primary validators as chain validators")
|
||||
probeTimeout := flag.Duration("probe-timeout", 90*time.Second, "max wait per chain for eth_blockNumber>0 and isBootstrapped")
|
||||
probeInterval := flag.Duration("probe-interval", 3*time.Second, "polling interval inside probe-timeout")
|
||||
subnetSettleDelay := flag.Duration("subnet-settle-delay", 10*time.Second, "delay after IssueCreateNetworkTx before re-syncing wallet")
|
||||
chainSettleDelay := flag.Duration("chain-settle-delay", 10*time.Second, "delay after IssueCreateNetworkTx before re-syncing wallet")
|
||||
printAddrOnly := flag.Bool("print-addr-only", false, "derive the BIP44 key from MNEMONIC, print the P-chain address, exit")
|
||||
existingSubnetIDs := flag.String("existing-subnet-ids", "", "comma-separated <chain>:<subnetID> mappings — skip CreateNetworkTx for these chains and reuse the supplied subnet")
|
||||
evmHeartbeatKeyHex := flag.String("evm-heartbeat-key", "", "hex-encoded secp256k1 key funded on every L2 EVM (LUX_PRIVATE_KEY). If set, the tool sends a 0-value self-tx after CreateChainTx to roll block 1 before probing eth_blockNumber>0")
|
||||
evmHeartbeatKeyHex := flag.String("evm-heartbeat-key", "", "hex-encoded secp256k1 key funded on every EVM chain (LUX_PRIVATE_KEY). If set, the tool sends a 0-value self-tx after CreateChainTx to roll block 1 before probing eth_blockNumber>0")
|
||||
probeBootstrapOnly := flag.Bool("probe-bootstrap-only", false, "accept the chain as healthy if info.isBootstrapped=true regardless of eth_blockNumber; use when --evm-heartbeat-key is unavailable and the operator will trigger heartbeats out-of-band")
|
||||
flag.Parse()
|
||||
|
||||
@@ -143,49 +153,16 @@ func main() {
|
||||
controlKey := formatPAddr(*hrp, addr)
|
||||
log.Printf("[%s] derived key m/44'/9000'/0'/0/%d -> %s", *networkLabel, *bipIdx, controlKey)
|
||||
|
||||
chains := strings.Split(*chainsCSV, ",")
|
||||
chains := strings.Split(*trackChainIDs, ",")
|
||||
for i := range chains {
|
||||
chains[i] = strings.TrimSpace(chains[i])
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
parts := strings.Split(pair, ":")
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
log.Fatalf("--existing-subnet-ids: bad pair %q (want chain:subnetID[:chainID])", pair)
|
||||
}
|
||||
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", name, err)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-flight: load every genesis up front. A bad path is fatal before
|
||||
// we burn any P-chain LUX on a CreateNetwork that we can't follow with
|
||||
// a CreateChain.
|
||||
// a CreateChain. The loader also validates 0x-prefixing on alloc keys
|
||||
// and auto-fixes in-memory (without rewriting the file on disk) so a
|
||||
// historical 0x-less file doesn't block live work.
|
||||
type chainSpec struct {
|
||||
Name string
|
||||
GenesisRaw []byte
|
||||
@@ -217,6 +194,59 @@ func main() {
|
||||
default:
|
||||
log.Fatalf("genesis %s: .config.chainId not a number (got %T)", path, v)
|
||||
}
|
||||
// Defensive 0x prefix + hex shape validation on alloc keys.
|
||||
// The EVM genesis loader rejects unprefixed keys, but historical
|
||||
// configs forgot the prefix. We normalize-and-log for missing
|
||||
// prefixes (recoverable), and fail loudly for genuinely malformed
|
||||
// keys (non-hex chars, wrong length) — never silently mutate
|
||||
// something we can't prove is just a missing prefix.
|
||||
//
|
||||
// Acceptance shape after this block:
|
||||
// - every alloc key matches /^0x[0-9a-fA-F]{40}$/
|
||||
// - if any key fails that AND can't be fixed by prepending
|
||||
// `0x`, the whole bootstrap aborts before any CreateChainTx
|
||||
// burns LUX
|
||||
alloc, _ := doc["alloc"].(map[string]any)
|
||||
if alloc != nil {
|
||||
fixed := 0
|
||||
ok := 0
|
||||
bad := make([]string, 0)
|
||||
repaired := make(map[string]any, len(alloc))
|
||||
for k, v := range alloc {
|
||||
canonical, repair, valid := normalizeAllocKey(k)
|
||||
if !valid {
|
||||
bad = append(bad, k)
|
||||
continue
|
||||
}
|
||||
repaired[canonical] = v
|
||||
if repair {
|
||||
fixed++
|
||||
} else {
|
||||
ok++
|
||||
}
|
||||
}
|
||||
if len(bad) > 0 {
|
||||
// Fail loud + give the operator everything they need to
|
||||
// fix the genesis at source. Show up to 5 bad keys so
|
||||
// the log line stays bounded but useful.
|
||||
preview := bad
|
||||
if len(preview) > 5 {
|
||||
preview = preview[:5]
|
||||
}
|
||||
log.Fatalf(
|
||||
"[%s] %s: %d malformed alloc keys (require canonical /^0x[0-9a-fA-F]{40}$/); sample: %v",
|
||||
*networkLabel, name, len(bad), preview,
|
||||
)
|
||||
}
|
||||
doc["alloc"] = repaired
|
||||
if patched, perr := json.Marshal(doc); perr == nil {
|
||||
raw = patched
|
||||
log.Printf(
|
||||
"[%s] %s: alloc keys ok=%d 0x-repaired=%d (in-memory only, file unchanged)",
|
||||
*networkLabel, name, ok, fixed,
|
||||
)
|
||||
}
|
||||
}
|
||||
specs = append(specs, chainSpec{Name: name, GenesisRaw: raw, EVMChainID: evmChainID})
|
||||
log.Printf("[%s] loaded %s genesis (evm chainId=%d, %d bytes)", *networkLabel, name, evmChainID, len(raw))
|
||||
}
|
||||
@@ -236,6 +266,36 @@ func main() {
|
||||
log.Printf("[%s] P-chain balance of control key: %+v", *networkLabel, balResp)
|
||||
}
|
||||
|
||||
// Single source of truth for "already created" — query the live
|
||||
// P-chain for the current blockchain set, then look up by chain name.
|
||||
// This removes the operator burden of maintaining a --existing-X flag
|
||||
// in lockstep with cluster reality.
|
||||
existingByName := map[string]struct {
|
||||
ChainOwnerID ids.ID // upstream "subnetID"
|
||||
BlockchainID ids.ID
|
||||
}{}
|
||||
{
|
||||
blockchains, perr := platformGetBlockchains(ctx, *uri)
|
||||
if perr != nil {
|
||||
log.Printf("WARN: platform.getBlockchains failed: %v (assuming no pre-existing chains)", perr)
|
||||
} else {
|
||||
byName := map[string]platformBlockchain{}
|
||||
for _, b := range blockchains {
|
||||
byName[strings.ToLower(b.Name)] = b
|
||||
}
|
||||
for _, spec := range specs {
|
||||
if b, ok := byName[strings.ToLower(spec.Name)]; ok {
|
||||
existingByName[spec.Name] = struct {
|
||||
ChainOwnerID ids.ID
|
||||
BlockchainID ids.ID
|
||||
}{ChainOwnerID: b.SubnetID, BlockchainID: b.ID}
|
||||
log.Printf("[%s/%s] pre-existing chain found via platform.getBlockchains: blockchainID=%s ownerID=%s",
|
||||
*networkLabel, spec.Name, b.ID, b.SubnetID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nodeIDs []ids.NodeID
|
||||
var minPrimaryEnd uint64
|
||||
if !*skipValidators {
|
||||
@@ -254,8 +314,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Initial wallet sync. We re-sync after every subnet creation so the
|
||||
// builder sees the new subnetID UTXOs.
|
||||
// Initial wallet sync. We re-sync after every chain creation so the
|
||||
// builder sees the new chain-owner UTXOs.
|
||||
w, err := primary.MakeWallet(ctx, &primary.WalletConfig{
|
||||
URI: *uri,
|
||||
LUXKeychain: kc,
|
||||
@@ -271,13 +331,21 @@ func main() {
|
||||
}
|
||||
log.Printf("[%s] initial wallet P-chain LUX = %d nLUX", *networkLabel, pBal[luxAssetID])
|
||||
|
||||
// Empirical: ~1 LUX for createSubnet + ~0.5 LUX for createChain per L2,
|
||||
// plus ~0.005 LUX per addValidatorTx. Floor at 1.5 LUX * len(chains)
|
||||
// to refuse work we cannot fund.
|
||||
minRequired := uint64(1_500_000_000) * uint64(len(specs))
|
||||
// Refuse work we cannot fund. We only charge for the chains that don't
|
||||
// exist yet — pre-existing chains are skipped entirely.
|
||||
pendingCount := 0
|
||||
for _, s := range specs {
|
||||
if _, ok := existingByName[s.Name]; !ok {
|
||||
pendingCount++
|
||||
}
|
||||
}
|
||||
// Empirical: ~1 LUX for createNetwork + ~0.5 LUX for createChain per
|
||||
// pending chain, plus ~0.005 LUX per addValidator. Floor at 1.5 LUX *
|
||||
// pendingCount.
|
||||
minRequired := uint64(1_500_000_000) * uint64(pendingCount)
|
||||
if pBal[luxAssetID] < minRequired {
|
||||
log.Fatalf("insufficient P-chain balance for %d chains, need >= %d nLUX, have %d nLUX",
|
||||
len(specs), minRequired, pBal[luxAssetID])
|
||||
log.Fatalf("insufficient P-chain balance for %d pending chains, need >= %d nLUX, have %d nLUX",
|
||||
pendingCount, minRequired, pBal[luxAssetID])
|
||||
}
|
||||
|
||||
owner := &secp256k1fx.OutputOwners{
|
||||
@@ -298,40 +366,35 @@ func main() {
|
||||
for _, spec := range specs {
|
||||
log.Printf("[%s] === bootstrapping %s ===", *networkLabel, spec.Name)
|
||||
|
||||
var subnetID ids.ID
|
||||
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)
|
||||
}
|
||||
var chainOwnerID ids.ID
|
||||
var bcID ids.ID
|
||||
reused := false
|
||||
|
||||
if ec, ok := existingByName[spec.Name]; ok {
|
||||
chainOwnerID = ec.ChainOwnerID
|
||||
bcID = ec.BlockchainID
|
||||
reused = true
|
||||
log.Printf("[%s/%s] REUSE: ownerID=%s blockchainID=%s (skipping both CreateNetworkTx and CreateChainTx)",
|
||||
*networkLabel, spec.Name, chainOwnerID, bcID)
|
||||
} else {
|
||||
log.Printf("[%s/%s] IssueCreateNetworkTx", *networkLabel, spec.Name)
|
||||
createNetTx, err := w.P().IssueCreateNetworkTx(owner)
|
||||
if err != nil {
|
||||
log.Fatalf("[%s] create network: %v", spec.Name, err)
|
||||
}
|
||||
subnetID = createNetTx.ID()
|
||||
log.Printf("[%s/%s] subnet ID = %s", *networkLabel, spec.Name, subnetID)
|
||||
time.Sleep(*subnetSettleDelay)
|
||||
}
|
||||
chainOwnerID = createNetTx.ID()
|
||||
log.Printf("[%s/%s] chain owner ID = %s", *networkLabel, spec.Name, chainOwnerID)
|
||||
time.Sleep(*chainSettleDelay)
|
||||
|
||||
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.
|
||||
// Re-sync wallet with the new chain-owner 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),
|
||||
PChainTxsToFetch: set.Of(chainOwnerID),
|
||||
})
|
||||
if err == nil {
|
||||
break
|
||||
@@ -344,7 +407,7 @@ func main() {
|
||||
}
|
||||
|
||||
log.Printf("[%s/%s] IssueCreateChainTx (vmID=%s)", *networkLabel, spec.Name, vmID)
|
||||
createChainTx, err := w2.P().IssueCreateChainTx(subnetID, spec.GenesisRaw, vmID, nil, spec.Name)
|
||||
createChainTx, err := w2.P().IssueCreateChainTx(chainOwnerID, spec.GenesisRaw, vmID, nil, spec.Name)
|
||||
if err != nil {
|
||||
log.Fatalf("[%s] create chain: %v", spec.Name, err)
|
||||
}
|
||||
@@ -352,13 +415,13 @@ func main() {
|
||||
log.Printf("[%s/%s] blockchain ID = %s", *networkLabel, spec.Name, bcID)
|
||||
}
|
||||
|
||||
if !*skipValidators && len(nodeIDs) > 0 && (presetChainID == ids.ID{}) {
|
||||
if !*skipValidators && len(nodeIDs) > 0 && !reused {
|
||||
time.Sleep(3 * time.Second)
|
||||
w3, err := primary.MakeWallet(ctx, &primary.WalletConfig{
|
||||
URI: *uri,
|
||||
LUXKeychain: kc,
|
||||
EVMKeychain: kc,
|
||||
PChainTxsToFetch: set.Of(subnetID),
|
||||
PChainTxsToFetch: set.Of(chainOwnerID),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[%s] WARN validator wallet sync failed: %v", spec.Name, err)
|
||||
@@ -380,12 +443,12 @@ func main() {
|
||||
End: uint64(endTime.Unix()),
|
||||
Wght: 20,
|
||||
},
|
||||
Chain: subnetID,
|
||||
Chain: chainOwnerID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[%s] WARN add validator %s: %v", spec.Name, nid, err)
|
||||
} else {
|
||||
log.Printf("[%s] added subnet validator: %s", spec.Name, nid)
|
||||
log.Printf("[%s] added chain validator: %s", spec.Name, nid)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
@@ -395,8 +458,8 @@ 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. Skip when we didn't touch
|
||||
// the P-chain (preset chain ID resume mode).
|
||||
if (presetChainID == ids.ID{}) {
|
||||
// the P-chain (reused chain).
|
||||
if !reused {
|
||||
w, err = primary.MakeWallet(ctx, &primary.WalletConfig{
|
||||
URI: *uri,
|
||||
LUXKeychain: kc,
|
||||
@@ -418,7 +481,13 @@ func main() {
|
||||
if *evmHeartbeatKeyHex != "" {
|
||||
log.Printf("[%s/%s] sending EVM heartbeat tx to roll block 1", *networkLabel, spec.Name)
|
||||
if err := evmHeartbeat(ctx, *uri, bcID.String(), spec.EVMChainID, *evmHeartbeatKeyHex); err != nil {
|
||||
log.Fatalf("[%s] heartbeat failed: %v", spec.Name, err)
|
||||
// Heartbeat failure on a reused chain is non-fatal: the
|
||||
// chain may already have blocks. Log and continue.
|
||||
if reused {
|
||||
log.Printf("[%s/%s] WARN heartbeat on reused chain failed (non-fatal): %v", *networkLabel, spec.Name, err)
|
||||
} else {
|
||||
log.Fatalf("[%s] heartbeat failed: %v", spec.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,8 +499,9 @@ func main() {
|
||||
}
|
||||
log.Printf("[%s/%s] PROBE OK: first eth_blockNumber=%s", *networkLabel, spec.Name, firstBlock)
|
||||
} else {
|
||||
// In bootstrap-only mode we still capture whatever eth_blockNumber
|
||||
// currently reports; the caller knows it may be 0x0.
|
||||
// In bootstrap-only mode we still capture whatever
|
||||
// eth_blockNumber currently reports; the caller knows it may
|
||||
// be 0x0.
|
||||
httpc := &http.Client{Timeout: 5 * time.Second}
|
||||
if blk, berr := ethBlockNumber(ctx, httpc, *uri, bcID.String()); berr == nil {
|
||||
firstBlock = blk
|
||||
@@ -441,11 +511,12 @@ func main() {
|
||||
|
||||
out.Chains = append(out.Chains, resultChain{
|
||||
Name: spec.Name,
|
||||
SubnetID: subnetID.String(),
|
||||
ChainOwnerID: chainOwnerID.String(),
|
||||
BlockchainID: bcID.String(),
|
||||
EVMChainID: spec.EVMChainID,
|
||||
FirstBlockHex: firstBlock,
|
||||
BootstrappedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Reused: reused,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -468,6 +539,93 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// platformBlockchain is the subset of platform.getBlockchains we care about.
|
||||
type platformBlockchain struct {
|
||||
ID ids.ID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SubnetID ids.ID `json:"subnetID"` // upstream wire field — semantically "chain owner"
|
||||
VMID ids.ID `json:"vmID"`
|
||||
}
|
||||
|
||||
// platformGetBlockchains queries the P-chain for the current set of
|
||||
// blockchains. Returns the parsed list. This is the canonical
|
||||
// already-exists check for idempotency.
|
||||
func platformGetBlockchains(ctx context.Context, uri string) ([]platformBlockchain, error) {
|
||||
body := strings.NewReader(
|
||||
`{"jsonrpc":"2.0","id":1,"method":"platform.getBlockchains","params":{}}`)
|
||||
req, _ := http.NewRequestWithContext(ctx, "POST", uri+"/ext/P", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
httpc := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := httpc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
var parsed struct {
|
||||
Result struct {
|
||||
Blockchains []platformBlockchain `json:"blockchains"`
|
||||
} `json:"result"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return nil, fmt.Errorf("decode platform.getBlockchains: %w (body=%s)", err, string(raw))
|
||||
}
|
||||
if parsed.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error: %s", parsed.Error.Message)
|
||||
}
|
||||
return parsed.Result.Blockchains, nil
|
||||
}
|
||||
|
||||
// normalizeAllocKey returns the canonical 0x-prefixed form of an EVM
|
||||
// genesis alloc key, a flag indicating whether the input needed repair,
|
||||
// and a validity flag.
|
||||
//
|
||||
// Acceptance shape: /^0x[0-9a-fA-F]{40}$/ after normalization.
|
||||
//
|
||||
// Returns:
|
||||
// - canonical: the 0x-prefixed lowercase-or-mixed-case form (we don't
|
||||
// lowercase the hex itself — Ethereum addresses preserve case as a
|
||||
// checksum signal; if the caller mixed cases they want to keep it).
|
||||
// - repaired: true if the input was missing a 0x prefix.
|
||||
// - valid: false if the key fails the hex+length shape AFTER any prefix
|
||||
// repair attempt. Callers must fail loudly on !valid.
|
||||
//
|
||||
// Single source of truth for "is this a usable alloc key?" — both the
|
||||
// in-memory repair in main and any future validation tooling should
|
||||
// route through this function.
|
||||
func normalizeAllocKey(k string) (canonical string, repaired bool, valid bool) {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "0x"):
|
||||
canonical = k
|
||||
case strings.HasPrefix(k, "0X"):
|
||||
// Normalize the prefix to lowercase even when the body cases are
|
||||
// preserved — `0X` is non-standard for EVM addresses.
|
||||
canonical = "0x" + k[2:]
|
||||
repaired = true
|
||||
default:
|
||||
canonical = "0x" + k
|
||||
repaired = true
|
||||
}
|
||||
body := canonical[2:]
|
||||
if len(body) != 40 {
|
||||
return canonical, repaired, false
|
||||
}
|
||||
for _, c := range body {
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
case c >= 'a' && c <= 'f':
|
||||
case c >= 'A' && c <= 'F':
|
||||
default:
|
||||
return canonical, repaired, false
|
||||
}
|
||||
}
|
||||
valid = true
|
||||
return
|
||||
}
|
||||
|
||||
// deriveLuxKey derives a secp256k1 private key from a BIP39 mnemonic at the
|
||||
// canonical Lux web-wallet hardened path m/44'/9000'/0'/0/<idx>.
|
||||
func deriveLuxKey(mnemonic string, idx uint32) (*secp256k1.PrivateKey, error) {
|
||||
@@ -529,7 +687,7 @@ func waitBootstrap(ctx context.Context, uri, chainID string, timeout, interval t
|
||||
|
||||
// evmHeartbeat sends a 0-value self-transfer using the supplied secp256k1 key.
|
||||
// Signs with the EVM chainID's EIP-155 v. Returns nil iff eth_sendRawTransaction
|
||||
// returns a tx hash. The first tx on a fresh subnet-evm chain produces block 1.
|
||||
// returns a tx hash. The first tx on a fresh EVM chain produces block 1.
|
||||
func evmHeartbeat(ctx context.Context, uri, chainID string, evmChainID uint64, keyHex string) error {
|
||||
keyHex = strings.TrimSpace(strings.TrimPrefix(keyHex, "0x"))
|
||||
keyBytes, err := hex.DecodeString(keyHex)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Unit tests for bootstrap-l2's defensive alloc key normalization.
|
||||
//
|
||||
// The whole point of normalizeAllocKey is to keep the load-and-validate
|
||||
// path single-sourced: every consumer that touches an alloc key (the
|
||||
// in-memory repair in main, future validation tooling, future on-disk
|
||||
// rewriters) must route through this function. These tests pin the
|
||||
// contract — a regression here would let a malformed key slip past the
|
||||
// pre-flight check and burn LUX on a CreateChainTx that the node will
|
||||
// later reject at genesis-load time.
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeAllocKey_AlreadyCanonical(t *testing.T) {
|
||||
canonical, repaired, valid := normalizeAllocKey("0x1111111111111111111111111111111111111111")
|
||||
if !valid {
|
||||
t.Fatalf("canonical 0x... key must validate; got !valid")
|
||||
}
|
||||
if repaired {
|
||||
t.Error("canonical key must not flag repaired=true")
|
||||
}
|
||||
if canonical != "0x1111111111111111111111111111111111111111" {
|
||||
t.Errorf("canonical roundtrip mismatch: %q", canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_AddsMissingPrefix(t *testing.T) {
|
||||
canonical, repaired, valid := normalizeAllocKey("1111111111111111111111111111111111111111")
|
||||
if !valid {
|
||||
t.Fatal("missing-prefix key must validate after repair")
|
||||
}
|
||||
if !repaired {
|
||||
t.Error("missing-prefix repair must flag repaired=true")
|
||||
}
|
||||
if canonical != "0x1111111111111111111111111111111111111111" {
|
||||
t.Errorf("expected 0x-prefixed form, got %q", canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_NormalizesCapitalPrefix(t *testing.T) {
|
||||
// `0X` is non-standard for EVM address strings. We lowercase the
|
||||
// PREFIX while preserving body case (in case the caller is using
|
||||
// EIP-55 checksum casing on the address itself).
|
||||
canonical, repaired, valid := normalizeAllocKey("0XaBcDeF0123456789aBcDeF0123456789aBcDeF01")
|
||||
if !valid {
|
||||
t.Fatal("0X-prefixed key with valid body must validate")
|
||||
}
|
||||
if !repaired {
|
||||
t.Error("0X repair must flag repaired=true")
|
||||
}
|
||||
if canonical != "0xaBcDeF0123456789aBcDeF0123456789aBcDeF01" {
|
||||
t.Errorf("prefix should normalize to 0x while preserving body case; got %q", canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_PreservesBodyCase(t *testing.T) {
|
||||
// EIP-55 checksum addresses encode the checksum in upper/lower case
|
||||
// of the hex digits. Stripping the case would destroy the checksum,
|
||||
// so we must preserve body case verbatim.
|
||||
canonical, _, valid := normalizeAllocKey("0xaBcDeF0123456789AbCdEf0123456789AbCdEf01")
|
||||
if !valid {
|
||||
t.Fatal("mixed-case body must validate")
|
||||
}
|
||||
if canonical != "0xaBcDeF0123456789AbCdEf0123456789AbCdEf01" {
|
||||
t.Errorf("body case must be preserved verbatim; got %q", canonical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_RejectsShortBody(t *testing.T) {
|
||||
// 39 hex chars after 0x — one short of a valid address. Must fail.
|
||||
cases := []string{
|
||||
"0x111111111111111111111111111111111111111", // 39
|
||||
"0x11111111111111111111111111111111111111", // 38
|
||||
"0x", // empty body
|
||||
}
|
||||
for _, k := range cases {
|
||||
_, _, valid := normalizeAllocKey(k)
|
||||
if valid {
|
||||
t.Errorf("short-body key %q must fail validation", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_RejectsLongBody(t *testing.T) {
|
||||
// 41 hex chars — one over. Must fail.
|
||||
_, _, valid := normalizeAllocKey("0x11111111111111111111111111111111111111111")
|
||||
if valid {
|
||||
t.Error("long-body key must fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_RejectsNonHex(t *testing.T) {
|
||||
cases := []string{
|
||||
// `g` is not a hex char
|
||||
"0x111111111111111111111111111111111111111g",
|
||||
// Underscore
|
||||
"0x_aBcDeF0123456789aBcDeF0123456789aBcDeF0",
|
||||
// Spaces
|
||||
"0x 111111111111111111111111111111111111111",
|
||||
}
|
||||
for _, k := range cases {
|
||||
_, _, valid := normalizeAllocKey(k)
|
||||
if valid {
|
||||
t.Errorf("non-hex key %q must fail validation", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_EmptyString(t *testing.T) {
|
||||
_, _, valid := normalizeAllocKey("")
|
||||
if valid {
|
||||
t.Error("empty string must fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAllocKey_IsIdempotent(t *testing.T) {
|
||||
// Calling normalize twice in a row must produce the same result —
|
||||
// every consumer should be safe to defensive-call.
|
||||
first, _, valid := normalizeAllocKey("aBcDeF0123456789aBcDeF0123456789aBcDeF01")
|
||||
if !valid {
|
||||
t.Fatal("first call must validate")
|
||||
}
|
||||
second, repairedSecond, validSecond := normalizeAllocKey(first)
|
||||
if !validSecond {
|
||||
t.Error("second call must also validate (idempotent)")
|
||||
}
|
||||
if repairedSecond {
|
||||
t.Error("second call must report repaired=false; the input was already canonical")
|
||||
}
|
||||
if second != first {
|
||||
t.Errorf("second-call output differs from first: %q vs %q", second, first)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"alloc": {
|
||||
"0200000000000000000000000000000000000005": {
|
||||
"0x0200000000000000000000000000000000000005": {
|
||||
"balance": "0x0",
|
||||
"code": "0x01",
|
||||
"nonce": "0x1"
|
||||
},
|
||||
"9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"0x9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"balance": "0x193e5939a08ce9dbd480000000"
|
||||
}
|
||||
},
|
||||
@@ -284,4 +284,4 @@
|
||||
"number": "0x0",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp": "0x0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"alloc": {
|
||||
"0200000000000000000000000000000000000005": {
|
||||
"0x0200000000000000000000000000000000000005": {
|
||||
"balance": "0x0",
|
||||
"code": "0x01",
|
||||
"nonce": "0x1"
|
||||
@@ -284,4 +284,4 @@
|
||||
"number": "0x0",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp": "0x696ffa12"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"alloc": {
|
||||
"0200000000000000000000000000000000000005": {
|
||||
"0x0200000000000000000000000000000000000005": {
|
||||
"balance": "0x0",
|
||||
"code": "0x01",
|
||||
"nonce": "0x1"
|
||||
},
|
||||
"9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"0x9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"balance": "0x33b2e3c9fd0803ce8000000"
|
||||
}
|
||||
},
|
||||
@@ -284,4 +284,4 @@
|
||||
"number": "0x0",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp": "0x0"
|
||||
}
|
||||
}
|
||||
+102
-102
@@ -1,308 +1,308 @@
|
||||
{
|
||||
"alloc": {
|
||||
"0200000000000000000000000000000000000005": {
|
||||
"0x0200000000000000000000000000000000000005": {
|
||||
"balance": "0x00",
|
||||
"code": "0x01",
|
||||
"nonce": "0x01"
|
||||
},
|
||||
"02aCB0ee16fd2B1bF465F85D7Ee112fA80C055FC": {
|
||||
"0x02aCB0ee16fd2B1bF465F85D7Ee112fA80C055FC": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"03B8Bd0A20b05AADC9E457A4c967f45eEeC163c4": {
|
||||
"0x03B8Bd0A20b05AADC9E457A4c967f45eEeC163c4": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"054D2e1dA5A1B5b9Da158acbA42Aed1d45F5CF02": {
|
||||
"0x054D2e1dA5A1B5b9Da158acbA42Aed1d45F5CF02": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"05CAE5842A08165d4d736dA9E49438574283aaE9": {
|
||||
"0x05CAE5842A08165d4d736dA9E49438574283aaE9": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"0698495959f5570420BAA955dae5eA2864933ACf": {
|
||||
"0x0698495959f5570420BAA955dae5eA2864933ACf": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"07283AA2c30523E9190A17b9598ff65599BAAe17": {
|
||||
"0x07283AA2c30523E9190A17b9598ff65599BAAe17": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"111BDcfe8C5398C7764BC1220B99cA1EdBcF5AF7": {
|
||||
"0x111BDcfe8C5398C7764BC1220B99cA1EdBcF5AF7": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"16204223fe4470f4B1F1dA19A368dC815736a3d7": {
|
||||
"0x16204223fe4470f4B1F1dA19A368dC815736a3d7": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1882D237E2FD5d4F3CCE29215317847D2b75f3eB": {
|
||||
"0x1882D237E2FD5d4F3CCE29215317847D2b75f3eB": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1B475A4C983DfE4f32bbA4dE8DA8fd2c37f3A2A6": {
|
||||
"0x1B475A4C983DfE4f32bbA4dE8DA8fd2c37f3A2A6": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1D2Eb1F0D268401B7C910d18e3F06fe1606964f7": {
|
||||
"0x1D2Eb1F0D268401B7C910d18e3F06fe1606964f7": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1EB0b3E6cb2dE3B25b45d6e49C33e683271D7bd5": {
|
||||
"0x1EB0b3E6cb2dE3B25b45d6e49C33e683271D7bd5": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1FCDa7bc336de60FAb3BE7B2188cefE5AA590CBD": {
|
||||
"0x1FCDa7bc336de60FAb3BE7B2188cefE5AA590CBD": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"1a845Da3412a6D6231AB0E85FaEd72c260Da55bC": {
|
||||
"0x1a845Da3412a6D6231AB0E85FaEd72c260Da55bC": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"2099389940444768C7738D4C45FF965CF36ECa52": {
|
||||
"0x2099389940444768C7738D4C45FF965CF36ECa52": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"20d3f6B4aE88224Ab9CD8ddE689FFb646B14e4c1": {
|
||||
"0x20d3f6B4aE88224Ab9CD8ddE689FFb646B14e4c1": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"22EbCe08E9f967F227F6E83d2eDB6b600f1370E3": {
|
||||
"0x22EbCe08E9f967F227F6E83d2eDB6b600f1370E3": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"2458cB853c406cDda14C6aD1Ebc8b106A829529E": {
|
||||
"0x2458cB853c406cDda14C6aD1Ebc8b106A829529E": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"2E2Ba4324533fD6fB077450403D5fAb25Cd934f9": {
|
||||
"0x2E2Ba4324533fD6fB077450403D5fAb25Cd934f9": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"2ba3d932FAAE51EBAF2173b78c874d422a0Cc186": {
|
||||
"0x2ba3d932FAAE51EBAF2173b78c874d422a0Cc186": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"30341F789A0A8C81378Ec169f77125A76660662F": {
|
||||
"0x30341F789A0A8C81378Ec169f77125A76660662F": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"313CF291c069C58D6bd61B0D672673462B8951bD": {
|
||||
"0x313CF291c069C58D6bd61B0D672673462B8951bD": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"314fac4Bd7D6a283985451202f25c6BcfaA037dE": {
|
||||
"0x314fac4Bd7D6a283985451202f25c6BcfaA037dE": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"341813ADC2de5fa20982E592A136D0b26bD35228": {
|
||||
"0x341813ADC2de5fa20982E592A136D0b26bD35228": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"3690f692D17554F90a9Bf7819d5b987E605245fA": {
|
||||
"0x3690f692D17554F90a9Bf7819d5b987E605245fA": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"3B084e285EEf960873f9070B916B8B857e30Cbe1": {
|
||||
"0x3B084e285EEf960873f9070B916B8B857e30Cbe1": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"3C334af45A2f41274aA63fd9348fAeBA8966e279": {
|
||||
"0x3C334af45A2f41274aA63fd9348fAeBA8966e279": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"3f39f5bfc9E99a7C7455fB101cdBdA70D0595160": {
|
||||
"0x3f39f5bfc9E99a7C7455fB101cdBdA70D0595160": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4154BD5e8E15822Ab96359d309166EC91f009248": {
|
||||
"0x4154BD5e8E15822Ab96359d309166EC91f009248": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4315c1e150a8825403135Ab705738dFe04f32183": {
|
||||
"0x4315c1e150a8825403135Ab705738dFe04f32183": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"43Ba86de6709D76Fa015E3cC3b7DDe31703504F8": {
|
||||
"0x43Ba86de6709D76Fa015E3cC3b7DDe31703504F8": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"471217e35Cf573133b840c53E93Ed1EaCC51Ac1c": {
|
||||
"0x471217e35Cf573133b840c53E93Ed1EaCC51Ac1c": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"49660D0A746255ACCfeE4aA004279F7B27179111": {
|
||||
"0x49660D0A746255ACCfeE4aA004279F7B27179111": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4F566Da11B0711010abD9181631f6DaFc1f0c6B8": {
|
||||
"0x4F566Da11B0711010abD9181631f6DaFc1f0c6B8": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4e256878ED0Aa6Aea20394015c6F66443a6047e0": {
|
||||
"0x4e256878ED0Aa6Aea20394015c6F66443a6047e0": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4e79e052C5130d3918Fcb9D6825158aeEeF4890e": {
|
||||
"0x4e79e052C5130d3918Fcb9D6825158aeEeF4890e": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"4f49A5bbe8A6f673BD06261E9CCf1D23a004FC4d": {
|
||||
"0x4f49A5bbe8A6f673BD06261E9CCf1D23a004FC4d": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"53163A5922f1531F5C98D69aC2Dd130660c3c855": {
|
||||
"0x53163A5922f1531F5C98D69aC2Dd130660c3c855": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"53bCA7fF5dd0441DB410bdC6FFD58703406434bf": {
|
||||
"0x53bCA7fF5dd0441DB410bdC6FFD58703406434bf": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"55AA096feBd81Cdf27A0c2dA2F857D3F3e7C55eF": {
|
||||
"0x55AA096feBd81Cdf27A0c2dA2F857D3F3e7C55eF": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"568AA29eD57e8575A794EC3e5FEe20f886411E1C": {
|
||||
"0x568AA29eD57e8575A794EC3e5FEe20f886411E1C": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"59f8F59199C0B3C6e752941ff67bd015E73829Ec": {
|
||||
"0x59f8F59199C0B3C6e752941ff67bd015E73829Ec": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"5a777e9b95F3a1706017F9b6fDc501cEceD84248": {
|
||||
"0x5a777e9b95F3a1706017F9b6fDc501cEceD84248": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"5b72d3ff49B12c84a101D17ec98d87bf7B70b6bC": {
|
||||
"0x5b72d3ff49B12c84a101D17ec98d87bf7B70b6bC": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"66D94a1014Fa808eE9Eab3Ac38C0810ab7d0009d": {
|
||||
"0x66D94a1014Fa808eE9Eab3Ac38C0810ab7d0009d": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"6D84Cdd6a387584922c7FDB1f70acF146f25C04B": {
|
||||
"0x6D84Cdd6a387584922c7FDB1f70acF146f25C04B": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"71aECaCe53bC0849A14F0b0a10224B00F6153352": {
|
||||
"0x71aECaCe53bC0849A14F0b0a10224B00F6153352": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"721762eD0C130CE07d3Dc56A1fff1f26499bE8A6": {
|
||||
"0x721762eD0C130CE07d3Dc56A1fff1f26499bE8A6": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"7337840B9d8cf2C78295FBC3373f6ceA9CB9F286": {
|
||||
"0x7337840B9d8cf2C78295FBC3373f6ceA9CB9F286": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"74aB2B6E01bDf141FA73BC2355b58ceaC38C0Ac9": {
|
||||
"0x74aB2B6E01bDf141FA73BC2355b58ceaC38C0Ac9": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"7792a945C604B6A8991bC37F0BBa48eD3220e8d8": {
|
||||
"0x7792a945C604B6A8991bC37F0BBa48eD3220e8d8": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"7EDbd481118E071FE9c4b85e404c56CCC351292C": {
|
||||
"0x7EDbd481118E071FE9c4b85e404c56CCC351292C": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"7a05645cAc4f3FDA00b100924562D0a9085A082D": {
|
||||
"0x7a05645cAc4f3FDA00b100924562D0a9085A082D": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"80924B34750C897c34c25b3bA13dBE95E3224EfD": {
|
||||
"0x80924B34750C897c34c25b3bA13dBE95E3224EfD": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"80E6653cb1BA2a0302207A0a21521CAf81fcc763": {
|
||||
"0x80E6653cb1BA2a0302207A0a21521CAf81fcc763": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"85a6ad52261990cf1E0e05D1dc471A8c4Dda94e7": {
|
||||
"0x85a6ad52261990cf1E0e05D1dc471A8c4Dda94e7": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8A19c91f810421d347DDBBE8158ab7F8693eae14": {
|
||||
"0x8A19c91f810421d347DDBBE8158ab7F8693eae14": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8B8A5e59c1D430ab508dD6C45178f048C2aFAcEd": {
|
||||
"0x8B8A5e59c1D430ab508dD6C45178f048C2aFAcEd": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8C21B1Cb91C4FFf11f69f6B21103E2d4b003035c": {
|
||||
"0x8C21B1Cb91C4FFf11f69f6B21103E2d4b003035c": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8C7B7ff7196515a8f3cf04bD9E8A33C4FFC8c9C3": {
|
||||
"0x8C7B7ff7196515a8f3cf04bD9E8A33C4FFC8c9C3": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8Da4C887aaFcEDDF2B4D543e538bE7215D3401c3": {
|
||||
"0x8Da4C887aaFcEDDF2B4D543e538bE7215D3401c3": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8E22f2F37e7a5c643d883c956C31A4bCe08991F6": {
|
||||
"0x8E22f2F37e7a5c643d883c956C31A4bCe08991F6": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84": {
|
||||
"0x8d5081153aE1cfb41f5c932fe0b6Beb7E159cF84": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"0x9011E888251AB053B7bD1cdB598Db4f9DEd94714": {
|
||||
"balance": "0x193e5939a08ce9dbd480000000"
|
||||
},
|
||||
"944aB0665466A10f3C7E82096e2A7a6afF128378": {
|
||||
"0x944aB0665466A10f3C7E82096e2A7a6afF128378": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"96B995fF4847Ffb5436132B4a86502C07789c7Bf": {
|
||||
"0x96B995fF4847Ffb5436132B4a86502C07789c7Bf": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"96cdbBe896a7f51342d8177ACE00eF3475FaADc3": {
|
||||
"0x96cdbBe896a7f51342d8177ACE00eF3475FaADc3": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"97f688735eD6D896FE740CbFD26983767A4cb9AA": {
|
||||
"0x97f688735eD6D896FE740CbFD26983767A4cb9AA": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"980ca20F732c2217A3C7cc6cA252EbA56e088A61": {
|
||||
"0x980ca20F732c2217A3C7cc6cA252EbA56e088A61": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"9C864a2884eD27c376e499AA75f5B064c2Ef51a0": {
|
||||
"0x9C864a2884eD27c376e499AA75f5B064c2Ef51a0": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"9cF4d8Bf5F1EC1997510C2126441c813A8c7Cc6f": {
|
||||
"0x9cF4d8Bf5F1EC1997510C2126441c813A8c7Cc6f": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"A105a413c349b175d8b445Da814A6a766843F6aC": {
|
||||
"0xA105a413c349b175d8b445Da814A6a766843F6aC": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"AC994e2fcb6aE6078B27937B1b5374B891669fb8": {
|
||||
"0xAC994e2fcb6aE6078B27937B1b5374B891669fb8": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"Ada3736A876A7fa4a454ab0605536e0A859D2A30": {
|
||||
"0xAda3736A876A7fa4a454ab0605536e0A859D2A30": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"B5ACC73B22612BC1B99f98C6da7B3E175E8F7539": {
|
||||
"0xB5ACC73B22612BC1B99f98C6da7B3E175E8F7539": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"B5B325df519eB58B7223d85aaeac8b56aB05f3d6": {
|
||||
"0xB5B325df519eB58B7223d85aaeac8b56aB05f3d6": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"B5c2a4867D6096F0F4e3ae76c48FD92C412154f0": {
|
||||
"0xB5c2a4867D6096F0F4e3ae76c48FD92C412154f0": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"BB927d5e0b19631F6536B284444213CDDDB93Cdf": {
|
||||
"0xBB927d5e0b19631F6536B284444213CDDDB93Cdf": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"C25d00876B6bB80a64E3032C2872440Ebfaf5b80": {
|
||||
"0xC25d00876B6bB80a64E3032C2872440Ebfaf5b80": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"CA92ad0C91bd8DE640B9dAFfEB338ac908725142": {
|
||||
"0xCA92ad0C91bd8DE640B9dAFfEB338ac908725142": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"CBac896B5DcDFB87f1ebf80A0D033a3Ef4f76f06": {
|
||||
"0xCBac896B5DcDFB87f1ebf80A0D033a3Ef4f76f06": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"E374867773455c36Db90776a6a4E4B5B21743763": {
|
||||
"0xE374867773455c36Db90776a6a4E4B5B21743763": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"EAbCC110fAcBfebabC66Ad6f9E7B67288e720B59": {
|
||||
"0xEAbCC110fAcBfebabC66Ad6f9E7B67288e720B59": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"Fb66808f708e1d4D7D43a8c75596e84f94e06806": {
|
||||
"0xFb66808f708e1d4D7D43a8c75596e84f94e06806": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"a8722395CaECe005f613644502d9655Ab8C42B17": {
|
||||
"0xa8722395CaECe005f613644502d9655Ab8C42B17": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"bB9bA2b3ECE6Ddc84a988D07d1d1Dd34b48C66f8": {
|
||||
"0xbB9bA2b3ECE6Ddc84a988D07d1d1Dd34b48C66f8": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"c19d8D7bc52C9F36DA48B0a80eb8015c7Add7916": {
|
||||
"0xc19d8D7bc52C9F36DA48B0a80eb8015c7Add7916": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"c1ED5cef75C4285614c8b2878C60A92454DdF5d1": {
|
||||
"0xc1ED5cef75C4285614c8b2878C60A92454DdF5d1": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"c805912a1e62828c0C36e674164e6A9E948722eF": {
|
||||
"0xc805912a1e62828c0C36e674164e6A9E948722eF": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"cb4E59591354111EFB8C7b78543E1aDa3bEdd077": {
|
||||
"0xcb4E59591354111EFB8C7b78543E1aDa3bEdd077": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"cf5288bEe8d8F63511C389D5015185FDEDe30e54": {
|
||||
"0xcf5288bEe8d8F63511C389D5015185FDEDe30e54": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"e8829fdc3c12Fd95a48dF248D028166d6C2d3598": {
|
||||
"0xe8829fdc3c12Fd95a48dF248D028166d6C2d3598": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"e945dbB0bA699FCcA469153aC6937943014C2b6F": {
|
||||
"0xe945dbB0bA699FCcA469153aC6937943014C2b6F": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f106e8a89D982Dc13B25c933BEEfBcBa266ffF59": {
|
||||
"0xf106e8a89D982Dc13B25c933BEEfBcBa266ffF59": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f483a8a0Ee70147A18EBCFAd7Bf1DE0E48B1fA51": {
|
||||
"0xf483a8a0Ee70147A18EBCFAd7Bf1DE0E48B1fA51": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f5AbBd1b2384C16AA6A4bD114b399dE396c579A6": {
|
||||
"0xf5AbBd1b2384C16AA6A4bD114b399dE396c579A6": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f5b14e0Ae37324877F8C03abe351C4743C95B5B4": {
|
||||
"0xf5b14e0Ae37324877F8C03abe351C4743C95B5B4": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f7f52257a6143cE6BbD12A98eF2B0a3d0C648079": {
|
||||
"0xf7f52257a6143cE6BbD12A98eF2B0a3d0C648079": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"f8f12D0592e6d1bFe92ee16CaBCC4a6F26dAAe23": {
|
||||
"0xf8f12D0592e6d1bFe92ee16CaBCC4a6F26dAAe23": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
},
|
||||
"fc4e8955441c7DEf35c017d20127744736362164": {
|
||||
"0xfc4e8955441c7DEf35c017d20127744736362164": {
|
||||
"balance": "0x033b2e3c9fd0803ce8000000"
|
||||
}
|
||||
},
|
||||
@@ -581,4 +581,4 @@
|
||||
"number": "0x00",
|
||||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"timestamp": "0x6727ea31"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user