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:
Hanzo AI
2026-05-29 20:02:05 -07:00
parent 799d5dafef
commit 631a9b9dd1
6 changed files with 507 additions and 215 deletions
+263 -105
View File
@@ -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)
+134
View File
@@ -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)
}
}
+3 -3
View File
@@ -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"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"alloc": {
"0200000000000000000000000000000000000005": {
"0x0200000000000000000000000000000000000005": {
"balance": "0x0",
"code": "0x01",
"nonce": "0x1"
@@ -284,4 +284,4 @@
"number": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x696ffa12"
}
}
+3 -3
View File
@@ -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
View File
@@ -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"
}
}