Files
genesis/configs/chain_shards_test.go
zeekayandHanzo Dev f8c2794d35 Mainnet and testnet boot the genesis their validators were born from
The shipped mainnet genesis was assembled from shards and matched the
live network only at C-Chain block 0. Its X-Chain denomination was 6
where the live one is 9, its allocations were in 6-decimal units, its
letter chains carried chainId fields and an F/M split the network never
had, and its C-Chain rules differed (targetGas 500M against 60M, and a
fee split the network never scheduled). Every blockchain ID differed, so
a node started without --genesis-file founded C-Chain TZEa3jDM... beside
the live 2Hx3UMuW.... Testnet shipped another network's stakers.

A network with history cannot have its genesis changed. mainnet and
testnet now ship genesis.json, the document in their luxd-genesis
ConfigMap, and GetGenesis returns it byte for byte. Their shards are
gone, allocations handed to them are refused, and the shard contract
and 6-decimal units stay with devnet, localnet and new networks.
GetCanonicalGenesisBytes, a second assembler nothing called, is gone.

A test pins both documents' digests and staker sets. luxfi/node pins
the C-Chain IDs its builder derives from them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 19:08:42 -07:00

332 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package configs
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// TestGetGenesis_CChainShardPresentEmbedsCChainGenesis confirms that for
// every embedded network that ships a cchain.json shard, the marshalled
// primary genesis JSON contains a non-empty cChainGenesis field — i.e. the
// builder will emit a C-Chain CreateChainTx for that network.
//
// This is the new contract that replaced the LUX_DISABLE_CCHAIN env knob:
// chain presence is data-driven (does the shard exist?) rather than
// runtime-toggled (is an env set?). Adding/removing a chain from a
// network's primary genesis is a file-tree edit, not an operator switch.
func TestGetGenesis_CChainShardPresentEmbedsCChainGenesis(t *testing.T) {
for _, name := range []string{"mainnet", "testnet", "localnet"} {
t.Run(name, func(t *testing.T) {
data, err := GetGenesis(networkIDFromName(t, name))
if err != nil {
t.Fatalf("GetGenesis(%s): %v", name, err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
got, _ := m["cChainGenesis"].(string)
if got == "" {
t.Fatalf("%s: expected non-empty cChainGenesis (embedded shard present)", name)
}
})
}
}
// TestGetGenesis_XChainShardPresentEmbedsXChainGenesis is the X-Chain
// equivalent of the C-Chain test above. Every Lux network ships an
// xchain.json shard (the small asset descriptor
// `{"symbol":"LUX","name":"Lux","denomination":6}`); the loader must
// thread it into the marshalled primary genesis JSON as the
// xChainGenesis field, where the builder later parses it to construct
// the XVM genesis and CreateChainTx.
//
// Together with TestBuildGenesisFromDir_AbsentShardEmptyOptIn (which
// covers the absent-shard case for X), this enforces the same data-
// driven contract for X that we already have for C/Q/Z: chain
// presence is a file-tree edit, not an operator switch. The previous
// hardcoded `Symbol: "LUX", Name: "Lux", Denomination: 6` literal in
// builder.FromConfig is now sourced from the shard, so a P-only
// network (P-only shape) can opt out by simply omitting the file.
func TestGetGenesis_XChainShardPresentEmbedsXChainGenesis(t *testing.T) {
for _, name := range []string{"localnet"} {
t.Run(name, func(t *testing.T) {
data, err := GetGenesis(networkIDFromName(t, name))
if err != nil {
t.Fatalf("GetGenesis(%s): %v", name, err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
got, _ := m["xChainGenesis"].(string)
if got == "" {
t.Fatalf("%s: expected non-empty xChainGenesis (embedded shard present)", name)
}
// Sanity-check the shard parses and pins the canonical Lux asset.
var asset struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Denomination byte `json:"denomination"`
}
if err := json.Unmarshal([]byte(got), &asset); err != nil {
t.Fatalf("%s: xChainGenesis shard unparseable: %v", name, err)
}
if asset.Symbol != "LUX" || asset.Name != "Lux" || asset.Denomination != 6 {
t.Fatalf("%s: xChainGenesis shard drift: got %+v want {LUX Lux 6}", name, asset)
}
})
}
}
// TestBuildGenesisFromDir_AbsentShardEmptyOptIn confirms the FS-fallback
// loader honours the same "shard absent → opt-out" semantics as the
// embedded loader. An operator who wants a P+X-only network just omits
// the relevant chain shards from their dir; no env knob, no flag.
func TestBuildGenesisFromDir_AbsentShardEmptyOptIn(t *testing.T) {
dir := t.TempDir()
write := func(name, body string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
// Minimal shards: network + P-Chain only. No X/C/Q/Z. This is the
// P-only shape — every primary-network chain (X included)
// is opt-in by shard presence.
write("network.json", `{"networkID":1337,"startTime":1735689600,"message":"P-only test"}`)
write("pchain.json", `{
"allocations":[],
"initialStakeDuration":31536000,
"initialStakeDurationOffset":5400,
"initialStakedFunds":[],
"initialStakers":[]
}`)
data, err := buildGenesisFromDir(dir)
if err != nil {
t.Fatalf("buildGenesisFromDir: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse: %v", err)
}
for _, field := range []string{"xChainGenesis", "cChainGenesis", "qChainGenesis", "zChainGenesis"} {
if got, _ := m[field].(string); got != "" {
t.Fatalf("absent shard should yield empty %s; got %q", field, got)
}
}
}
// networkIDFromName resolves a network directory name to its canonical
// network ID. Test helper that mirrors networkNameFromID's inverse.
func networkIDFromName(t *testing.T, name string) uint32 {
t.Helper()
switch name {
case "mainnet":
return MainnetID
case "testnet":
return TestnetID
case "devnet":
return DevnetID
case "localnet":
return LocalID
default:
t.Fatalf("unknown network name %q", name)
return 0
}
}
// TestGetGenesis_AllPrimaryChainsBakedIn locks the contract that every
// primary network (mainnet, testnet, devnet, localnet) ships ALL 11
// primary-network chain shards baked into its genesis. This means the
// chains spawn at network startup with NO post-launch CreateBlockchainTx —
// chain set is fully data-driven by which shards are committed.
//
// The 11 primary-network chains, in canonical order (LP-134: the legacy
// T-Chain/ThresholdVM is retired and split into F-Chain + M-Chain, both
// served by the same ThresholdVM substrate in FHE / MPC mode respectively):
//
// X XVM exchange / UTXO asset
// C EVM contracts
// D DexVM decentralized exchange (CLOB + AMM + perps)
// Q QuantumVM post-quantum consensus (Quasar / ML-DSA / ML-KEM)
// A AIVM AI verification / attestation
// B BridgeVM cross-chain bridge (MPC signing)
// F ThresholdVM threshold-FHE confidential compute (LP-134; FHE mode, T's slot)
// Z ZKVM zero-knowledge proofs / private state
// G GraphVM graph database
// K KeyVM KMS — key management
// M ThresholdVM threshold-MPC bridge-custody signing (LP-134; MPC mode)
//
// P-Chain is the primary network itself and carries the validator set + chain
// registry; it has no shard slot because it isn't a CreateChainTx entry.
//
// If a future genesis intentionally needs to omit a chain (e.g. a P+X-only
// regulated-securities L1), that's a NEW config-tree, not a regression on the
// canonical Lux primary networks. Editing this test to drop a chain on
// devnet is a load-bearing decision — bring it to design review. mainnet and
// testnet are recorded: their chain set is the one they were born with.
func TestGetGenesis_AllPrimaryChainsBakedIn(t *testing.T) {
required := []string{
"xChainGenesis",
"cChainGenesis",
"dChainGenesis",
"qChainGenesis",
"aChainGenesis",
"bChainGenesis",
"fChainGenesis",
"zChainGenesis",
"gChainGenesis",
"kChainGenesis",
"mChainGenesis",
}
for _, name := range assembledNetworks {
t.Run(name, func(t *testing.T) {
data, err := GetGenesis(networkIDFromName(t, name))
if err != nil {
t.Fatalf("GetGenesis(%s): %v", name, err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
for _, field := range required {
got, _ := m[field].(string)
if got == "" {
t.Fatalf("%s: required chain field %q is empty — primary network must ship every letter-chain shard", name, field)
}
}
})
}
}
// TestPrimaryChainShards_PerChainCanonicalChainID locks the canonical
// per-letter EVM chainId scheme so any drift between the network configs
// is caught at test time. Pattern:
//
// letter base = 96369 + 100*(idx_in_alphabet_relative_to_C)
// per-network offset: testnet = base-1, devnet = base+1 (mirrors C-Chain)
//
// Exception: the C-Chain devnet EVM chainID is the owner-locked canonical
// value 96367 (see DevnetChainID), NOT base+1 (96370). The +1 rule holds
// for the D/Q/A/... shards below; C is the one cell pinned by the
// canonical map rather than derived from the arithmetic.
//
// LP-134: the legacy T-Chain (idx 5, 96869-series) is retired. F-Chain and
// M-Chain take the next free indices 9 and 10 — fresh IDs, T's are not
// recycled. (localnet base = 31337 + 110*idx.)
//
// This produces:
//
// C(96369/96368/96367), D(96469/96468/96470), Q(96569/96568/96570),
// A(96669/96668/96670), B(96769/96768/96770), F(97269/97268/97270),
// Z(96969/96968/96970), G(97069/97068/97070), K(97169/97168/97170),
// M(97369/97368/97370)
//
// Each ID is unique across {network × letter} so a misrouted tx cannot
// be replayed against the wrong chain.
//
// WHAT THESE NUMBERS ARE, AND WHY THEY STAY. An EVM chain id is EIP-155 replay
// protection: it means something exactly where EVM transactions are signed,
// which on the primary network is the C-Chain and nowhere else. The other nine
// letters run AIVM, BridgeVM, DexVM, ThresholdVM, GraphVM, KeyVM, mpcvm,
// QuantumVM and ZKVM — none of them an EVM, none of them signing a transaction
// this number could protect. P and X carry no chainId at all, which is the
// shape the other nine would have if the field had been added where it applies.
//
// Nothing reads them. Grep the estate: these nine values appear in the network
// configs and in this test, and in no other code.
//
// They stay anyway, and this test is why. They are in the genesis of a live
// network, and a chain's genesis bytes are the network's identity — editing one
// mints a different network whose nodes cannot talk to the one that exists. So
// the cost of the nine is nine dead numbers, and the cost of tidying them is a
// fork. This test holds them still.
//
// If work needs to name the chain that produced it rather than the chain it
// settles on, the value is that chain's blockchainID from its CreateChainTx, or
// its letter. Not a number in this range.
func TestPrimaryChainShards_PerChainCanonicalChainID(t *testing.T) {
want := map[string]map[string]int{
"devnet": {
"d": 96470, "q": 96570, "a": 96670, "b": 96770,
"f": 97270, "z": 96970, "g": 97070, "k": 97170, "m": 97370,
},
// localnet is absent on purpose. It is the one network not frozen by a
// running validator set, so it is the one that got to drop the field —
// see TestNonEVMShardsCarryNoChainID, which now requires its absence.
}
for net, byLetter := range want {
net := net
t.Run(net, func(t *testing.T) {
for letter, wantID := range byLetter {
path := filepath.Join("..", "configs", net, letter+"chain.json")
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("%s/%schain.json: %v", net, letter, err)
}
var shard map[string]any
if err := json.Unmarshal(data, &shard); err != nil {
t.Fatalf("%s/%schain.json parse: %v", net, letter, err)
}
// chainId is stored as a JSON number → unmarshals to float64
gotF, ok := shard["chainId"].(float64)
if !ok {
t.Fatalf("%s/%schain.json: chainId missing or wrong type, got %T", net, letter, shard["chainId"])
}
if int(gotF) != wantID {
t.Fatalf("%s/%schain.json: chainId got %d want %d", net, letter, int(gotF), wantID)
}
}
})
}
}
// TestNonEVMShardsCarryNoChainID is the rule, going forward: a chain that is
// not an EVM does not get an EVM chain id.
//
// An EVM chain id is EIP-155 replay protection. It protects a signed EVM
// transaction from being replayed on another EVM, and it means exactly nothing
// on a chain running AIVM, BridgeVM, DexVM, ThresholdVM, GraphVM, KeyVM, mpcvm,
// QuantumVM or ZKVM — none of which signs such a transaction. P and X have
// always carried none, which is the shape the rest should have had.
//
// mainnet, testnet and devnet are exempt and always will be. Their shard JSON
// goes into a CreateChainTx verbatim (builder.go: []byte(config.AChainGenesis)),
// so the field is part of the chain's genesis bytes, which decide its blockchain
// ID, which decides the network's genesis hash. Removing it there does not tidy
// those networks — it defines different ones, which the running nodes would
// refuse to join. Those numbers are frozen by arithmetic, not by preference,
// and TestPrimaryChainShards_PerChainCanonicalChainID holds them still.
//
// Every network minted after this test exists starts without them.
func TestNonEVMShardsCarryNoChainID(t *testing.T) {
nonEVM := []string{"achain", "bchain", "dchain", "fchain", "gchain", "kchain", "mchain", "qchain", "zchain"}
for _, net := range []string{"localnet", "local"} {
for _, letter := range nonEVM {
path := filepath.Join(net, letter+".json")
raw, err := os.ReadFile(path)
if os.IsNotExist(err) {
continue
}
if err != nil {
t.Fatalf("%s: %v", path, err)
}
var shard map[string]any
if err := json.Unmarshal(raw, &shard); err != nil {
t.Fatalf("%s: %v", path, err)
}
if _, present := shard["chainId"]; present {
t.Errorf("%s declares chainId, but its VM signs no EVM transaction for one to protect. "+
"Only the C-Chain has an EVM chain id; give this chain none.", path)
}
}
}
}