mirror of
https://github.com/luxfi/genesis.git
synced 2026-10-30 16:25:45 +00:00
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]>
332 lines
13 KiB
Go
332 lines
13 KiB
Go
// 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)
|
||
}
|
||
}
|
||
}
|
||
}
|