diff --git a/Makefile b/Makefile index f3d7f48f2f66..4adf564c09be 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # with Go source code. If you know what GOPATH is then you probably # don't need to bother with make. -.PHONY: geth evm all test lint fmt clean devtools help +.PHONY: geth slave evm all test lint fmt clean devtools help GOBIN = ./build/bin GO ?= latest @@ -14,6 +14,12 @@ geth: @echo "Done building." @echo "Run \"$(GOBIN)/geth\" to launch geth." +#? slave: Build the goshard slave node. +slave: + $(GORUN) build/ci.go install ./cmd/slave + @echo "Done building." + @echo "Run \"$(GOBIN)/slave\" to launch the slave node." + #? evm: Build evm. evm: $(GORUN) build/ci.go install ./cmd/evm diff --git a/cmd/slave/README.md b/cmd/slave/README.md new file mode 100644 index 000000000000..029eea69df95 --- /dev/null +++ b/cmd/slave/README.md @@ -0,0 +1,88 @@ +# slave + +The goshard slave node. It boots from a pyquarkchain-compatible +`cluster_config.json` and hosts the shards assigned to one slave identity. It +performs no network I/O — it is the node, not the protocol (tracking issue +[#17](https://github.com/QuarkChain/goshard/issues/17)). + +## Build + +``` +make slave +``` + +This installs the binary to `./build/bin/slave` (the same convention as `geth`). +The commands below are run from the repo root so the relative config paths resolve. + +## Subcommands + +### `slave config` + +Parse, validate, and print a normalized summary of a cluster config for one slave. +Exits non-zero on an invalid config or an unknown node id. + +``` +./build/bin/slave config --cluster_config ./qkc/config/singularity/mainnet.json --node_id S0 +``` + +``` +slave S0 @ 127.0.0.1:38000 +db path root: ./qkc-data/mainnet +network id: 1 +owns 2 shard(s): + FULL_SHARD_ID CHAIN SHARD CONSENSUS BLOCK_TIME GENESIS_TIME DIFFICULTY GAS_LIMIT ALLOC + 0x00000001 0 0/1 POW_ETHASH 10s 1556639999 5000000000 12000000 3 + 0x00040001 4 0/1 POW_ETHASH 10s 1556639999 5000000000 12000000 3 +config OK +``` + +`--node_id S9` (an id the config does not define) prints +`unknown node id "S9" (config defines: S0)` and exits non-zero. + +### `slave genesis` + +Derive and print the cluster's root genesis block. The printed hash is +byte-identical to pyquarkchain's +`GenesisManager.create_root_block().header.get_hash()`. + +``` +./build/bin/slave genesis --cluster_config ./qkc/config/singularity/mainnet.json +``` + +``` +root genesis block: + version: 0 + height: 0 + timestamp: 1556639999 + difficulty: 10000000000000 + total_difficulty: 10000000000000 + nonce: 0 + hash_prev_block: 0x0000000000000000000000000000000000000000000000000000000000000000 + hash_merkle_root: 0x0000000000000000000000000000000000000000000000000000000000000000 + seal_hash: 0xe7dcdecc09e724ad81e493d70dedcd6d9ea0ee830d7ab2528a5648f2a0cf8178 + hash: 0x4036783e441eb5057bf2be96bf1fd4585ac49824de15c0d92a4c14a97886ca51 +``` + +The running devnet config works the same way: + +``` +./build/bin/slave genesis --cluster_config ./qkc/config/singularity/devnet.json +``` + +prints `hash: 0x5ad443efb7cf5246a3d1bbc1734bd02bf3a5d83bedeccfcfe707d0ebee03780d`. + +## Fixtures and pyquarkchain cross-validation + +Both real (singularity) cluster configs are checked in under +[`qkc/config/singularity/`](../../qkc/config/singularity/) — `mainnet.json` and +`devnet.json`. They are copied verbatim from pyquarkchain; provenance and the +regeneration steps live in [that directory's README](../../qkc/config/singularity/README.md). + +To cross-validate a `slave genesis` run against pyquarkchain, derive the same +header there with the command in that README's +[Pinned root-genesis values](../../qkc/config/singularity/README.md#pinned-root-genesis-values) +section and compare its `hash` output with the `hash:` line printed here. + +The shard-level `chain genesis` printed by `slave inspect` is the config +descriptor's fingerprint, not a pyquarkchain minor-block hash; it becomes the +real shard genesis block hash when the QKC block format (#1) lands. diff --git a/cmd/slave/configcmd.go b/cmd/slave/configcmd.go new file mode 100644 index 000000000000..df69732e348b --- /dev/null +++ b/cmd/slave/configcmd.go @@ -0,0 +1,73 @@ +// Copyright 2026-2027, QuarkChain. + +package main + +import ( + "fmt" + "io" + "os" + "text/tabwriter" + + "github.com/ethereum/go-ethereum/qkc/config" + "github.com/urfave/cli/v2" +) + +var configCommand = &cli.Command{ + Name: "config", + Usage: "Parse, validate, and print a normalized cluster config summary for a slave", + ArgsUsage: " ", + Flags: []cli.Flag{ + clusterConfigFlag, + nodeIDFlag, + }, + Description: `Loads and validates a pyquarkchain cluster_config.json, resolves the shards owned +by --node_id, prints a normalized summary, and reports "config OK". Exits non-zero +on an invalid config or an unknown node id.`, + Action: runConfig, +} + +func runConfig(ctx *cli.Context) error { + cfg, err := loadClusterConfig(ctx) + if err != nil { + return err + } + nodeID := ctx.String(nodeIDFlag.Name) + if nodeID == "" { + return fmt.Errorf("--%s is required (e.g. S0)", nodeIDFlag.Name) + } + slaveCtx, err := cfg.ResolveSlave(nodeID) + if err != nil { + return err + } + printConfigSummary(os.Stdout, slaveCtx) + fmt.Fprintln(os.Stdout, "config OK") + return nil +} + +func printConfigSummary(out io.Writer, c *config.SlaveContext) { + fmt.Fprintf(out, "slave %s @ %s:%d\n", c.ID, c.Slave.IP, c.Slave.Port) + fmt.Fprintf(out, "db path root: %s\n", c.DBPathRoot) + fmt.Fprintf(out, "network id: %d\n", c.Quarkchain.NetworkID) + fmt.Fprintf(out, "owns %d shard(s):\n", len(c.Slave.FullShardList)) + + tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + fmt.Fprintln(tw, " FULL_SHARD_ID\tCHAIN\tSHARD\tCONSENSUS\tBLOCK_TIME\tGENESIS_TIME\tDIFFICULTY\tGAS_LIMIT\tALLOC") + for _, shard := range c.ShardConfigs() { + blockTime := "-" + if shard.ConsensusConfig != nil { + blockTime = fmt.Sprintf("%ds", shard.ConsensusConfig.TargetBlockTime) + } + fmt.Fprintf(tw, " 0x%08x\t%d\t%d/%d\t%s\t%s\t%d\t%d\t%d\t%d\n", + shard.GetFullShardId(), + shard.ChainID, + shard.ShardID, shard.ShardSize, + shard.ConsensusType, + blockTime, + shard.Genesis.Timestamp, + shard.Genesis.Difficulty, + shard.Genesis.GasLimit, + len(shard.Genesis.Alloc), + ) + } + tw.Flush() +} diff --git a/cmd/slave/genesiscmd.go b/cmd/slave/genesiscmd.go new file mode 100644 index 000000000000..d5df7bdbcb47 --- /dev/null +++ b/cmd/slave/genesiscmd.go @@ -0,0 +1,53 @@ +// Copyright 2026-2027, QuarkChain. + +package main + +import ( + "fmt" + "io" + "os" + + "github.com/ethereum/go-ethereum/qkc" + "github.com/ethereum/go-ethereum/qkc/types" + "github.com/urfave/cli/v2" +) + +var genesisCommand = &cli.Command{ + Name: "genesis", + Usage: "Derive and print the root genesis block and its hash", + ArgsUsage: " ", + Flags: []cli.Flag{ + clusterConfigFlag, + }, + Description: `Derives the cluster's root genesis block purely from QUARKCHAIN.ROOT.GENESIS and +prints its fields and hash. The hash is byte-identical to pyquarkchain's +GenesisManager.create_root_block().header.get_hash().`, + Action: runGenesis, +} + +func runGenesis(ctx *cli.Context) error { + cfg, err := loadClusterConfig(ctx) + if err != nil { + return err + } + header, err := qkc.CreateRootBlock(cfg.Quarkchain) + if err != nil { + return err + } + printRootGenesis(os.Stdout, header) + return nil +} + +func printRootGenesis(out io.Writer, h *types.RootBlockHeader) { + fmt.Fprintln(out, "root genesis block:") + fmt.Fprintf(out, " version: %d\n", h.Version) + fmt.Fprintf(out, " height: %d\n", h.Number) + fmt.Fprintf(out, " timestamp: %d\n", h.Time) + fmt.Fprintf(out, " difficulty: %d\n", h.Difficulty) + fmt.Fprintf(out, " total_difficulty: %d\n", h.TotalDifficulty) + fmt.Fprintf(out, " nonce: %d\n", h.Nonce) + fmt.Fprintf(out, " hash_prev_block: %s\n", h.ParentHash.Hex()) + fmt.Fprintf(out, " hash_merkle_root: %s\n", h.MinorHeaderHash.Hex()) + fmt.Fprintf(out, " seal_hash: %s\n", h.SealHash().Hex()) + fmt.Fprintf(out, " hash: %s\n", h.Hash().Hex()) +} diff --git a/cmd/slave/main.go b/cmd/slave/main.go new file mode 100644 index 000000000000..d45eeb5db58a --- /dev/null +++ b/cmd/slave/main.go @@ -0,0 +1,106 @@ +// Copyright 2026-2027, QuarkChain. + +// Command slave is the goshard slave node. It boots from a pyquarkchain-compatible +// cluster_config.json and hosts the shards assigned to one slave identity. +package main + +import ( + "fmt" + "os" + "slices" + + "github.com/ethereum/go-ethereum/internal/debug" + "github.com/ethereum/go-ethereum/internal/flags" + "github.com/ethereum/go-ethereum/qkc/config" + "github.com/urfave/cli/v2" +) + +// Flags shared by the default run action and the subcommands. The names match how +// pyquarkchain's cluster.py launches a slave: +// `slave --cluster_config= --node_id=`. +// +// Neither flag is marked Required even though the actions insist on it: they are +// registered both globally (for the default action) and on the subcommands, and +// cli enforces a required global flag even when the value is passed after the +// subcommand name. The actions validate presence instead. +var ( + clusterConfigFlag = &cli.StringFlag{ + Name: "cluster_config", + Usage: "Path to the pyquarkchain cluster_config.json", + } + nodeIDFlag = &cli.StringFlag{ + Name: "node_id", + Usage: "Slave identity (e.g. S0), matched against SLAVE_LIST[].ID in cluster_config to select which shards this process owns", + } +) + +// debugFlagAllowlist selects which of geth's debug.Flags the slave registers: +// logging and file-based profiling only. The slave performs no network I/O of +// any kind, so the debug flags that open sockets — the pprof HTTP server and +// pyroscope push — are deliberately absent; debug.Setup treats an unregistered +// flag as unset and never starts a listener. An allowlist (rather than a +// blocklist) keeps that boundary fail-closed when a geth merge grows debug.Flags. +var debugFlagAllowlist = map[string]bool{ + "verbosity": true, + "log.vmodule": true, + "vmodule": true, + "log.json": true, + "log.format": true, + "log.file": true, + "log.rotate": true, + "log.maxsize": true, + "log.maxbackups": true, + "log.maxage": true, + "log.compress": true, + "pprof.memprofilerate": true, + "pprof.blockprofilerate": true, + "pprof.cpuprofile": true, + "go-execution-trace": true, +} + +func slaveDebugFlags() []cli.Flag { + var kept []cli.Flag + for _, f := range debug.Flags { + if debugFlagAllowlist[f.Names()[0]] { + kept = append(kept, f) + } + } + return kept +} + +// newApp assembles the slave CLI app; tests construct it directly. +func newApp() *cli.App { + app := flags.NewApp("the goshard slave node") + app.Name = "slave" + app.Flags = slices.Concat([]cli.Flag{clusterConfigFlag, nodeIDFlag}, slaveDebugFlags()) + app.Before = func(ctx *cli.Context) error { + flags.MigrateGlobalFlags(ctx) + return debug.Setup(ctx) + } + app.After = func(ctx *cli.Context) error { + debug.Exit() + return nil + } + app.Commands = []*cli.Command{ + configCommand, + genesisCommand, + } + return app +} + +func main() { + if err := newApp().Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// loadClusterConfig loads and validates the config named by --cluster_config, +// insisting the flag is set. +func loadClusterConfig(ctx *cli.Context) (*config.ClusterConfig, error) { + path := ctx.String(clusterConfigFlag.Name) + if path == "" { + return nil, fmt.Errorf("--%s is required", clusterConfigFlag.Name) + } + return config.LoadClusterConfig(path) +} diff --git a/cmd/slave/slave_test.go b/cmd/slave/slave_test.go new file mode 100644 index 000000000000..aba897e38e2b --- /dev/null +++ b/cmd/slave/slave_test.go @@ -0,0 +1,110 @@ +// Copyright 2026-2027, QuarkChain. + +package main + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/qkc" + "github.com/ethereum/go-ethereum/qkc/config" + "github.com/urfave/cli/v2" +) + +var fixtures = []struct { + name string + path string + consensus string + genesisHash string +}{ + {"mainnet", "../../qkc/config/singularity/mainnet.json", "POW_ETHASH", "0x4036783e441eb5057bf2be96bf1fd4585ac49824de15c0d92a4c14a97886ca51"}, + {"devnet", "../../qkc/config/singularity/devnet.json", "POW_SIMULATE", "0x5ad443efb7cf5246a3d1bbc1734bd02bf3a5d83bedeccfcfe707d0ebee03780d"}, +} + +func TestConfigSummaryOutput(t *testing.T) { + for _, f := range fixtures { + t.Run(f.name, func(t *testing.T) { + cfg, err := config.LoadClusterConfig(f.path) + if err != nil { + t.Fatalf("load: %v", err) + } + sc, err := cfg.ResolveSlave("S0") + if err != nil { + t.Fatalf("resolve: %v", err) + } + + var buf bytes.Buffer + printConfigSummary(&buf, sc) + out := buf.String() + for _, want := range []string{"slave S0", "0x00000001", "0x00040001", f.consensus, "owns 2 shard(s)"} { + if !strings.Contains(out, want) { + t.Errorf("config summary missing %q:\n%s", want, out) + } + } + }) + } +} + +func TestGenesisOutput(t *testing.T) { + for _, f := range fixtures { + t.Run(f.name, func(t *testing.T) { + cfg, err := config.LoadClusterConfig(f.path) + if err != nil { + t.Fatalf("load: %v", err) + } + header, err := qkc.CreateRootBlock(cfg.Quarkchain) + if err != nil { + t.Fatalf("genesis: %v", err) + } + + var buf bytes.Buffer + printRootGenesis(&buf, header) + if out := buf.String(); !strings.Contains(out, f.genesisHash) { + t.Errorf("genesis output missing pinned %s hash:\n%s", f.name, out) + } + }) + } +} + +// TestNoNetworkedDebugFlags pins the no-network-I/O boundary of the CLI: no +// registered debug flag may be able to open a socket, and every allowlisted +// debug flag must still exist upstream so a geth merge that renames or drops +// one fails loudly instead of silently shrinking the slave's flag surface. +func TestNoNetworkedDebugFlags(t *testing.T) { + app := newApp() + registered := map[string]bool{} + collect := func(fs []cli.Flag) { + for _, f := range fs { + for _, name := range f.Names() { + registered[name] = true + } + } + } + collect(app.Flags) + for _, cmd := range app.Commands { + collect(cmd.Flags) + } + + networked := []string{ + "pprof", "pprof.addr", "pprof.port", + "pyroscope", "pyroscope.server", "pyroscope.username", "pyroscope.password", "pyroscope.tags", + } + for _, name := range networked { + if registered[name] { + t.Errorf("networked debug flag --%s is registered", name) + } + } + for name := range debugFlagAllowlist { + if !registered[name] { + t.Errorf("allowlisted debug flag --%s missing from the app (renamed upstream?)", name) + } + } + + app = newApp() + app.Writer, app.ErrWriter = io.Discard, io.Discard + if err := app.Run([]string{"slave", "--pprof"}); err == nil || !strings.Contains(err.Error(), "pprof") { + t.Errorf("--pprof accepted, err = %v, want unknown-flag error", err) + } +} diff --git a/qkc/config/chain_config.go b/qkc/config/chain_config.go index d9ca01f489b1..4c88529c2cf1 100644 --- a/qkc/config/chain_config.go +++ b/qkc/config/chain_config.go @@ -11,8 +11,14 @@ import ( ) type ChainConfig struct { - ChainID uint32 `json:"CHAIN_ID"` - ShardSize uint32 `json:"SHARD_SIZE"` + ChainID uint32 `json:"CHAIN_ID"` + ShardSize uint32 `json:"SHARD_SIZE"` + // EthChainID is pyquarkchain's per-chain ETH_CHAIN_ID (absent in goquarkchain). + // pyquarkchain always derives it as BASE_ETH_CHAIN_ID + CHAIN_ID + 1 and + // overwrites any configured value with that derivation (config.py:534), so a + // present value is only accepted when consistent (checked in Validate). Zero + // means absent: real chain ids start at BASE_ETH_CHAIN_ID + 1 > 0. + EthChainID uint32 `json:"ETH_CHAIN_ID,omitempty"` DefaultChainToken string `json:"DEFAULT_CHAIN_TOKEN"` ConsensusType string `json:"CONSENSUS_TYPE"` diff --git a/qkc/config/config.go b/qkc/config/config.go index a5c5a3f7c557..0bd0a3d85837 100644 --- a/qkc/config/config.go +++ b/qkc/config/config.go @@ -143,8 +143,12 @@ type RootGenesis struct { HashPrevBlock string `json:"HASH_PREV_BLOCK"` HashMerkleRoot string `json:"HASH_MERKLE_ROOT"` Timestamp uint64 `json:"TIMESTAMP"` - Difficulty uint64 `json:"DIFFICULTY"` - Nonce uint32 `json:"NONCE"` + // Difficulty is a biguint and Nonce a uint64 on the QKC wire (RootBlockHeader), + // so the config must preserve those ranges: a uint64/uint32 here would reject + // valid pyquarkchain configs (difficulty 2^64, nonce 2^32) before the genesis + // header is ever hashed. + Difficulty *big.Int `json:"DIFFICULTY"` + Nonce uint64 `json:"NONCE"` } func NewRootGenesis() *RootGenesis { @@ -154,7 +158,7 @@ func NewRootGenesis() *RootGenesis { HashPrevBlock: "", HashMerkleRoot: "", Timestamp: 1519147489, - Difficulty: 1000000, + Difficulty: big.NewInt(1000000), Nonce: 0, } } diff --git a/qkc/config/load.go b/qkc/config/load.go new file mode 100644 index 000000000000..694c7531b85b --- /dev/null +++ b/qkc/config/load.go @@ -0,0 +1,246 @@ +// Copyright 2026-2027, QuarkChain. + +// The slave's load-time entry point onto the reused config types: it adds (1) a +// non-panicking Validate() run before any database is opened, and (2) +// ResolveSlave(), which narrows the full ClusterConfig down to a SlaveContext so +// the SlaveBackend never sees other slaves' data. + +package config + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "math" + "os" + "strings" + + "github.com/ethereum/go-ethereum/common" +) + +// SlaveContext is the narrowed view a single slave boots from: just its own slave +// entry, the shared chain config, and the db path root. It deliberately does not +// carry the whole ClusterConfig (which includes every other slave's assignments). +type SlaveContext struct { + ID string + Slave *SlaveConfig + Quarkchain *QuarkChainConfig + DBPathRoot string +} + +// FullShardIDs returns this slave's owned full shard ids, in config order. +func (c *SlaveContext) FullShardIDs() []uint32 { + return c.Slave.FullShardList +} + +// ShardConfigs returns the resolved shard config for each owned full shard id, in +// config order. Validate guarantees each id resolves, so no entry is nil. +func (c *SlaveContext) ShardConfigs() []*ShardConfig { + out := make([]*ShardConfig, 0, len(c.Slave.FullShardList)) + for _, id := range c.Slave.FullShardList { + out = append(out, c.Quarkchain.GetShardConfigByFullShardID(id)) + } + return out +} + +// LoadClusterConfig reads, parses, and validates a pyquarkchain cluster_config.json +// from path. Validation runs before any database is touched, so a bad config fails +// here rather than mid-boot. A panic from the reused initAndValidate path (the +// goquarkchain config validates by panicking) is converted into an error so the +// binary exits cleanly instead of crashing. +func LoadClusterConfig(path string) (*ClusterConfig, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + cfg, err := unmarshalClusterConfig(content) + if err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if err := cfg.deriveEthChainID(); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + return cfg, nil +} + +func unmarshalClusterConfig(content []byte) (cfg *ClusterConfig, err error) { + defer func() { + if r := recover(); r != nil { + cfg, err = nil, fmt.Errorf("invalid cluster config: %v", r) + } + }() + cfg = new(ClusterConfig) + if e := json.Unmarshal(content, cfg); e != nil { + return nil, e + } + return cfg, nil +} + +// Validate checks the boot-relevant invariants before any database opens: +// - the config carries a chain config and at least one slave; +// - no slave carries the legacy CHAIN_MASK_LIST key, and every slave owns at +// least one shard; +// - every owned full shard id resolves to a configured chain/shard; +// - no slave lists the same full shard id twice (intra-slave only — the same +// shard on multiple slaves is a valid multi-replica deployment, allowed here +// and validated at the master layer); +// - the root and owned-shard genesis hash fields are well-formed hex; +// - each owned shard's genesis ROOT_HEIGHT equals ROOT.GENESIS.HEIGHT (this +// issue derives only the genesis root block). +// +// ETH_CHAIN_ID is filled in and consistency-checked separately by deriveEthChainID. +func (c *ClusterConfig) Validate() error { + if c.Quarkchain == nil { + return fmt.Errorf("missing QUARKCHAIN config") + } + if c.Quarkchain.Root == nil || c.Quarkchain.Root.Genesis == nil { + return fmt.Errorf("missing QUARKCHAIN.ROOT.GENESIS") + } + if len(c.SlaveList) == 0 { + return fmt.Errorf("SLAVE_LIST is empty") + } + + root := c.Quarkchain.Root.Genesis + for _, name := range []struct { + field string + value string + }{ + {"HASH_PREV_BLOCK", root.HashPrevBlock}, + {"HASH_MERKLE_ROOT", root.HashMerkleRoot}, + } { + if err := validateRootHashHex(name.value); err != nil { + return fmt.Errorf("ROOT.GENESIS.%s: %w", name.field, err) + } + } + + for _, slave := range c.SlaveList { + if slave == nil { + continue + } + // The legacy CHAIN_MASK_LIST form is unsupported. Reject it whenever the key + // is present, including an empty list: slave_config.go leaves the field nil + // when the key is absent and a non-nil (possibly empty) slice when present, + // so len()>0 alone would let CHAIN_MASK_LIST:[] slip through as zero shards. + if slave.ChainMaskListForBackward != nil { + return fmt.Errorf("slave %q uses legacy CHAIN_MASK_LIST: legacy config not supported, use FULL_SHARD_ID_LIST", slave.ID) + } + if len(slave.FullShardList) == 0 { + return fmt.Errorf("slave %q owns no shards: FULL_SHARD_ID_LIST is empty", slave.ID) + } + // Duplicates are rejected only within one slave's list. The same id on two + // slaves is a valid multi-replica deployment (pyquarkchain master.py:1122); + // global shard-coverage and replica sanity belong at the master layer. + seen := make(map[uint32]bool) + for _, id := range slave.FullShardList { + shard := c.Quarkchain.GetShardConfigByFullShardID(id) + if shard == nil { + return fmt.Errorf("slave %q full shard id 0x%08x is not configured in any chain", slave.ID, id) + } + if seen[id] { + return fmt.Errorf("slave %q has duplicate full shard id 0x%08x in FULL_SHARD_ID_LIST", slave.ID, id) + } + seen[id] = true + if shard.Genesis == nil { + return fmt.Errorf("full shard id 0x%08x has no GENESIS", id) + } + // Validate the shard genesis hash fields at load, not deferred to shard + // genesis creation, so a malformed hex value fails while reporting config. + for _, h := range []struct { + field string + value string + }{ + {"HASH_PREV_MINOR_BLOCK", shard.Genesis.HashPrevMinorBlock}, + {"HASH_MERKLE_ROOT", shard.Genesis.HashMerkleRoot}, + } { + if err := validateRootHashHex(h.value); err != nil { + return fmt.Errorf("full shard id 0x%08x GENESIS.%s: %w", id, h.field, err) + } + } + if shard.Genesis.RootHeight != root.Height { + return fmt.Errorf("full shard id 0x%08x genesis ROOT_HEIGHT %d != ROOT.GENESIS.HEIGHT %d", id, shard.Genesis.RootHeight, root.Height) + } + } + } + return nil +} + +// deriveEthChainID fills in each chain's ETH_CHAIN_ID the way pyquarkchain does on +// load: ETH_CHAIN_ID = BASE_ETH_CHAIN_ID + CHAIN_ID + 1 (config.py:534). pyquarkchain +// overwrites any configured value with this derivation; goshard is stricter and first +// rejects a present-but-inconsistent value so a hand-edited typo is reported rather +// than silently overwritten. A zero value is treated as absent (a real id is +// BASE_ETH_CHAIN_ID + 1 or greater, so it is never zero). The derivation is applied to +// the chain configs and to the shard configs copied from them (NewShardConfig deep- +// copies each chain config before this runs), so both carry the resolved id. +func (c *ClusterConfig) deriveEthChainID() error { + if c.Quarkchain == nil { + return nil // Validate reports the missing config. + } + derive := func(cc *ChainConfig) error { + // Computed in uint64: pyquarkchain's arithmetic is unbounded, so the + // derivation may exceed uint32 and must not silently wrap. + want := uint64(c.Quarkchain.BaseEthChainID) + uint64(cc.ChainID) + 1 + if cc.EthChainID != 0 && uint64(cc.EthChainID) != want { + return fmt.Errorf("chain %d ETH_CHAIN_ID %d != BASE_ETH_CHAIN_ID %d + CHAIN_ID + 1 = %d", + cc.ChainID, cc.EthChainID, c.Quarkchain.BaseEthChainID, want) + } + if want > math.MaxUint32 { + return fmt.Errorf("chain %d derived ETH_CHAIN_ID %d exceeds uint32", cc.ChainID, want) + } + cc.EthChainID = uint32(want) + return nil + } + for _, chain := range c.Quarkchain.Chains { + if err := derive(chain); err != nil { + return err + } + } + for _, shard := range c.Quarkchain.shards { + if err := derive(shard.ChainConfig); err != nil { + return err + } + } + return nil +} + +// ResolveSlave narrows the cluster config to the single slave identified by nodeID, +// returning a SlaveContext. An unknown id is reported with the ids the config does +// define, so the operator can correct the launch flag. +func (c *ClusterConfig) ResolveSlave(nodeID string) (*SlaveContext, error) { + var ids []string + for _, slave := range c.SlaveList { + if slave == nil { + continue + } + ids = append(ids, slave.ID) + if slave.ID == nodeID { + return &SlaveContext{ + ID: slave.ID, + Slave: slave, + Quarkchain: c.Quarkchain, + DBPathRoot: c.DbPathRoot, + }, nil + } + } + return nil, fmt.Errorf("unknown node id %q (config defines: %s)", nodeID, strings.Join(ids, ", ")) +} + +// validateRootHashHex accepts an empty string (meaning all-zero) or a hex string +// (optionally 0x-prefixed) decoding to exactly 32 bytes. +func validateRootHashHex(s string) error { + s = strings.TrimPrefix(s, "0x") + if s == "" { + return nil + } + b, err := hex.DecodeString(s) + if err != nil { + return fmt.Errorf("invalid hex %q: %w", s, err) + } + if len(b) != common.HashLength { + return fmt.Errorf("expected %d bytes, got %d", common.HashLength, len(b)) + } + return nil +} diff --git a/qkc/config/load_test.go b/qkc/config/load_test.go new file mode 100644 index 000000000000..0b255d97f96f --- /dev/null +++ b/qkc/config/load_test.go @@ -0,0 +1,313 @@ +// Copyright 2026-2027, QuarkChain. + +package config + +import ( + "encoding/json" + "math" + "os" + "strings" + "testing" +) + +const ( + fixtureMainnet = "./singularity/mainnet.json" + fixtureDevnet = "./singularity/devnet.json" + templatePath = "./testdata/cluster_config_template.json" +) + +// network describes the boot-relevant values of one real network config. Both +// singularity networks have 8 chains and 4 slaves S0..S3 each owning 2 shards; +// they differ in network id, consensus, db path, and root difficulty. +type network struct { + name string + path string + dbRoot string + networkID uint32 + s0Consensus string // consensus of S0's shards + chain6 string // consensus of chain 6 (qkchash on mainnet, simulate on devnet) + rootDiff uint64 +} + +var networks = []network{ + {"mainnet", fixtureMainnet, "./qkc-data/mainnet", 1, PoWEthash, PoWQkchash, 10000000000000}, + {"devnet", fixtureDevnet, "./qkc-data/devnet", 255, PoWSimulate, PoWSimulate, 100000}, +} + +// TestLoadClusterConfigFixture asserts the boot-relevant values parsed from each +// real network config: 8 chains, 4 slaves each owning 2 shards. S0's first id is +// written "0x1" in both configs, so leading-zero-tolerant hex must parse it to 1. +func TestLoadClusterConfigFixture(t *testing.T) { + for _, nw := range networks { + t.Run(nw.name, func(t *testing.T) { + cfg, err := LoadClusterConfig(nw.path) + if err != nil { + t.Fatalf("LoadClusterConfig: %v", err) + } + if cfg.DbPathRoot != nw.dbRoot { + t.Errorf("DB_PATH_ROOT = %q, want %q", cfg.DbPathRoot, nw.dbRoot) + } + if cfg.Quarkchain.NetworkID != nw.networkID { + t.Errorf("NETWORK_ID = %d, want %d", cfg.Quarkchain.NetworkID, nw.networkID) + } + if len(cfg.SlaveList) != 4 { + t.Fatalf("SLAVE_LIST len = %d, want 4", len(cfg.SlaveList)) + } + + s0 := cfg.SlaveList[0] + if s0.ID != "S0" { + t.Errorf("slave ID = %q, want S0", s0.ID) + } + wantIDs := []uint32{0x00000001, 0x00040001} + if len(s0.FullShardList) != len(wantIDs) { + t.Fatalf("FULL_SHARD_ID_LIST = %v, want %v", s0.FullShardList, wantIDs) + } + for i, id := range wantIDs { + if s0.FullShardList[i] != id { + t.Errorf("FULL_SHARD_ID_LIST[%d] = 0x%08x, want 0x%08x", i, s0.FullShardList[i], id) + } + // full shard id math: (chain_id<<16) | shard_size | shard_id. + shard := cfg.Quarkchain.GetShardConfigByFullShardID(id) + if shard == nil { + t.Fatalf("shard 0x%08x not configured", id) + } + if got := shard.GetFullShardId(); got != id { + t.Errorf("GetFullShardId = 0x%08x, want 0x%08x", got, id) + } + if shard.ConsensusType != nw.s0Consensus { + t.Errorf("shard 0x%08x consensus = %q, want %q", id, shard.ConsensusType, nw.s0Consensus) + } + } + if c6 := cfg.Quarkchain.GetShardConfigByFullShardID(0x00060001); c6 == nil || c6.ConsensusType != nw.chain6 { + t.Errorf("shard 0x00060001 consensus = %v, want %q", c6, nw.chain6) + } + + root := cfg.Quarkchain.Root.Genesis + if root.Difficulty == nil || root.Difficulty.Uint64() != nw.rootDiff { + t.Errorf("ROOT.GENESIS.DIFFICULTY = %v, want %d", root.Difficulty, nw.rootDiff) + } + if root.Timestamp != 1556639999 { + t.Errorf("ROOT.GENESIS.TIMESTAMP = %d, want 1556639999", root.Timestamp) + } + }) + } +} + +func TestResolveSlave(t *testing.T) { + for _, nw := range networks { + t.Run(nw.name, func(t *testing.T) { + cfg, err := LoadClusterConfig(nw.path) + if err != nil { + t.Fatalf("LoadClusterConfig: %v", err) + } + + sc, err := cfg.ResolveSlave("S0") + if err != nil { + t.Fatalf("ResolveSlave(S0): %v", err) + } + if sc.ID != "S0" || sc.DBPathRoot != nw.dbRoot { + t.Errorf("SlaveContext = %+v, want ID=S0 DBPathRoot=%q", sc, nw.dbRoot) + } + shards := sc.ShardConfigs() + if len(shards) != 2 { + t.Fatalf("ShardConfigs len = %d, want 2", len(shards)) + } + if shards[0].GetFullShardId() != 0x00000001 || shards[1].GetFullShardId() != 0x00040001 { + t.Errorf("ShardConfigs order wrong: %x, %x", shards[0].GetFullShardId(), shards[1].GetFullShardId()) + } + + _, err = cfg.ResolveSlave("S9") + if err == nil { + t.Fatal("ResolveSlave(S9): expected error, got nil") + } + if !strings.Contains(err.Error(), `unknown node id "S9"`) || !strings.Contains(err.Error(), "S3") { + t.Errorf("ResolveSlave(S9) error = %q, want mention of S9 and the configured ids (incl S3)", err) + } + }) + } +} + +// TestWebSocketPortParsing covers the optional single WEBSOCKET_JSON_RPC_PORT field +// (pyquarkchain's form, adapted in slave_config.go): devnet's S0 omits it (null) and +// S1 sets 38591. +func TestWebSocketPortParsing(t *testing.T) { + cfg, err := LoadClusterConfig(fixtureDevnet) + if err != nil { + t.Fatalf("LoadClusterConfig: %v", err) + } + if cfg.SlaveList[0].WSPort != nil { + t.Errorf("S0 WEBSOCKET_JSON_RPC_PORT = %v, want nil", *cfg.SlaveList[0].WSPort) + } + if got := cfg.SlaveList[1].WSPort; got == nil || *got != 38591 { + t.Errorf("S1 WEBSOCKET_JSON_RPC_PORT = %v, want 38591", got) + } +} + +func TestValidateRejectsUnknownShard(t *testing.T) { + cfg := mustLoad(t) + cfg.SlaveList[0].FullShardList = []uint32{0x00990099} + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "not configured") { + t.Fatalf("Validate err = %v, want 'not configured'", err) + } +} + +func TestValidateRejectsDuplicateShard(t *testing.T) { + cfg := mustLoad(t) + cfg.SlaveList[0].FullShardList = []uint32{0x00000001, 0x00000001} + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "duplicate") { + t.Fatalf("Validate err = %v, want 'duplicate'", err) + } +} + +// TestValidateAllowsCrossSlaveReplica asserts that the same full shard id appearing +// on two different slaves is accepted: pyquarkchain treats this as a valid +// multi-replica deployment (master.py:1122), so the duplicate check is intra-slave +// only. Global shard-coverage/replica validation belongs at the master layer. +func TestValidateAllowsCrossSlaveReplica(t *testing.T) { + cfg := mustLoad(t) + // S1 now replicates S0's shards: a valid multi-replica config. + cfg.SlaveList[1].FullShardList = append([]uint32(nil), cfg.SlaveList[0].FullShardList...) + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate rejected a valid multi-replica config: %v", err) + } +} + +// TestDeriveEthChainID: pyquarkchain forces ETH_CHAIN_ID = BASE_ETH_CHAIN_ID + +// CHAIN_ID + 1 on load (config.py:534). Derivation fills every chain and the shard +// copies made before it runs, accepts a consistent explicit value, rejects an +// inconsistent one, and does not wrap the uint64 arithmetic into uint32. +func TestDeriveEthChainID(t *testing.T) { + content, err := os.ReadFile(fixtureMainnet) + if err != nil { + t.Fatal(err) + } + // parse without the LoadClusterConfig normalization, so ETH_CHAIN_ID is still + // absent (the fixtures omit it) and derivation can be exercised in isolation. + parse := func() *ClusterConfig { + cfg, err := unmarshalClusterConfig(content) + if err != nil { + t.Fatalf("parse: %v", err) + } + return cfg + } + + cfg := parse() + if err := cfg.deriveEthChainID(); err != nil { + t.Fatalf("deriveEthChainID: %v", err) + } + for _, chain := range cfg.Quarkchain.Chains { + if want := cfg.Quarkchain.BaseEthChainID + chain.ChainID + 1; chain.EthChainID != want { + t.Errorf("chain %d ETH_CHAIN_ID = %d, want %d", chain.ChainID, chain.EthChainID, want) + } + } + // The shard copies (deep-copied from the chain config before derivation) must + // carry the derived id too, not the pre-derivation zero. + shard := cfg.Quarkchain.GetShardConfigByFullShardID(0x00000001) + if want := cfg.Quarkchain.BaseEthChainID + 1; shard == nil || shard.EthChainID != want { + t.Errorf("shard 0x00000001 ETH_CHAIN_ID = %v, want %d", shard, want) + } + + // A present-but-inconsistent value is rejected rather than silently overwritten. + cfg = parse() + cfg.Quarkchain.Chains[0].EthChainID = 42 + if err := cfg.deriveEthChainID(); err == nil || !strings.Contains(err.Error(), "ETH_CHAIN_ID") { + t.Fatalf("deriveEthChainID err = %v, want ETH_CHAIN_ID mismatch", err) + } + + // The derivation is computed in uint64 and must not wrap into uint32: with + // BASE_ETH_CHAIN_ID = MaxUint32 the true id 4294967296 does not fit a uint32. + cfg = parse() + cfg.Quarkchain.BaseEthChainID = math.MaxUint32 + if err := cfg.deriveEthChainID(); err == nil || !strings.Contains(err.Error(), "exceeds uint32") { + t.Fatalf("deriveEthChainID err = %v, want 'exceeds uint32'", err) + } +} + +func TestValidateRejectsBadRootHash(t *testing.T) { + cfg := mustLoad(t) + cfg.Quarkchain.Root.Genesis.HashPrevBlock = "nothex!!" + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "HASH_PREV_BLOCK") { + t.Fatalf("Validate err = %v, want 'HASH_PREV_BLOCK'", err) + } +} + +func TestValidateRejectsGenesisRootHeightMismatch(t *testing.T) { + cfg := mustLoad(t) + cfg.Quarkchain.GetShardConfigByFullShardID(0x00000001).Genesis.RootHeight = 5 + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "ROOT_HEIGHT") { + t.Fatalf("Validate err = %v, want 'ROOT_HEIGHT'", err) + } +} + +// TestLoadRejectsChainMaskList parses the larger real template (which still uses +// the legacy CHAIN_MASK_LIST assignment form): it must parse without panicking, +// but be rejected with an explicit legacy-config error. +func TestLoadRejectsChainMaskList(t *testing.T) { + content, err := os.ReadFile(templatePath) + if err != nil { + t.Fatalf("read template: %v", err) + } + // Parse stage tolerates the legacy form, nulls, and extra fields. + if _, err := unmarshalClusterConfig(content); err != nil { + t.Fatalf("parse template: %v", err) + } + // Validate stage rejects it. + if _, err := LoadClusterConfig(templatePath); err == nil || + !strings.Contains(err.Error(), "legacy config not supported") { + t.Fatalf("LoadClusterConfig(template) err = %v, want 'legacy config not supported'", err) + } +} + +// TestValidateRejectsBadShardGenesisHash: an owned shard's genesis hash fields are +// validated at load, so a malformed value fails while reporting config rather than +// being deferred (and silently mis-decoded) at shard genesis creation. +func TestValidateRejectsBadShardGenesisHash(t *testing.T) { + cfg := mustLoad(t) + cfg.Quarkchain.GetShardConfigByFullShardID(0x00000001).Genesis.HashPrevMinorBlock = "nothex!!" + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "HASH_PREV_MINOR_BLOCK") { + t.Fatalf("Validate err = %v, want 'HASH_PREV_MINOR_BLOCK'", err) + } +} + +// TestShardGenesisRejectsBadExtraData: EXTRA_DATA is decoded to bytes at parse time, +// so it must be strictly validated there. common.Hex2Bytes silently turns "zz" into +// an empty slice; decodeGenesisHex reports it instead. +func TestShardGenesisRejectsBadExtraData(t *testing.T) { + var g ShardGenesis + if err := json.Unmarshal([]byte(`{"EXTRA_DATA":"zz"}`), &g); err == nil || !strings.Contains(err.Error(), "EXTRA_DATA") { + t.Fatalf("Unmarshal err = %v, want 'EXTRA_DATA'", err) + } + if err := json.Unmarshal([]byte(`{"EXTRA_DATA":"abcd"}`), &g); err != nil { + t.Fatalf("Unmarshal valid EXTRA_DATA: %v", err) + } +} + +// TestValidateRejectsEmptyChainMaskList: a present-but-empty CHAIN_MASK_LIST is a +// non-nil zero-length slice; it must be rejected as the unsupported legacy form +// rather than slipping through len()>0 and reporting a slave with zero shards. +func TestValidateRejectsEmptyChainMaskList(t *testing.T) { + cfg := mustLoad(t) + cfg.SlaveList[0].ChainMaskListForBackward = []uint32{} + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "legacy config not supported") { + t.Fatalf("Validate err = %v, want 'legacy config not supported'", err) + } +} + +// TestValidateRejectsZeroShardSlave: a slave that resolves to no shards (e.g. an +// empty FULL_SHARD_ID_LIST) is meaningless and must fail loudly. +func TestValidateRejectsZeroShardSlave(t *testing.T) { + cfg := mustLoad(t) + cfg.SlaveList[0].FullShardList = nil + if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "owns no shards") { + t.Fatalf("Validate err = %v, want 'owns no shards'", err) + } +} + +func mustLoad(t *testing.T) *ClusterConfig { + t.Helper() + cfg, err := LoadClusterConfig(fixtureMainnet) + if err != nil { + t.Fatalf("LoadClusterConfig: %v", err) + } + return cfg +} diff --git a/qkc/config/shard_config.go b/qkc/config/shard_config.go index d4ea28742f58..2216e29ecf60 100644 --- a/qkc/config/shard_config.go +++ b/qkc/config/shard_config.go @@ -16,6 +16,19 @@ import ( qcom "github.com/ethereum/go-ethereum/qkc/common" ) +// decodeGenesisHex strictly decodes an optional-"0x"-prefixed hex string. Unlike +// common.FromHex/Hex2Bytes (which silently drop invalid input, so "zz" decodes to +// empty and a malformed genesis passes validation), it reports malformed hex so the +// failure surfaces at config load instead of at shard-genesis creation. An empty +// string is a valid empty value. +func decodeGenesisHex(s string) ([]byte, error) { + s = strings.TrimPrefix(s, "0x") + if s == "" { + return nil, nil + } + return hex.DecodeString(s) +} + type ShardGenesis struct { RootHeight uint32 `json:"ROOT_HEIGHT"` Version uint32 `json:"VERSION"` @@ -98,7 +111,11 @@ func (a *Allocation) UnmarshalJSON(input []byte) error { a.Balances = jsonConfig.Balances } if jsonConfig.Code != "" { - a.Code = common.FromHex(jsonConfig.Code) + code, err := decodeGenesisHex(jsonConfig.Code) + if err != nil { + return fmt.Errorf("code: %w", err) + } + a.Code = code } if jsonConfig.Storage != nil { a.Storage = make(map[common.Hash]common.Hash, len(jsonConfig.Storage)) @@ -155,7 +172,11 @@ func (s *ShardGenesis) UnmarshalJSON(input []byte) error { return err } *s = ShardGenesis(jsonConfig.ShardGenesisAlias) - s.ExtraData = common.Hex2Bytes(jsonConfig.ExtraData) + extra, err := decodeGenesisHex(jsonConfig.ExtraData) + if err != nil { + return fmt.Errorf("EXTRA_DATA: %w", err) + } + s.ExtraData = extra s.Alloc = make(map[account.Address]Allocation) for addr, val := range jsonConfig.Alloc { address, err := account.CreatAddressFromBytes(common.FromHex(addr)) diff --git a/qkc/config/singularity/README.md b/qkc/config/singularity/README.md new file mode 100644 index 000000000000..b127f8e8bece --- /dev/null +++ b/qkc/config/singularity/README.md @@ -0,0 +1,53 @@ +# singularity + +The real QuarkChain singularity cluster configs the goshard slave must consume — +the live **mainnet** and the running **devnet**. These are irreplaceable network +configs, so they live here as shipped artifacts rather than under `testdata/`. + +Both: 8 chains (one shard each), 4 slaves `S0..S3` each owning 2 shards, +`FULL_SHARD_ID_LIST` form (S0's first id is written `0x1`), genesis `ALLOC` present. + +## `mainnet.json` + +`NETWORK_ID 1`, mixed `POW_ETHASH` / `POW_QKCHASH` consensus, root difficulty 1e13. +Copied verbatim from pyquarkchain's +[`mainnet/singularity/cluster_config_template.json`](https://github.com/QuarkChain/pyquarkchain/blob/master/mainnet/singularity/cluster_config_template.json). + +## `devnet.json` + +`NETWORK_ID 255`, all `POW_SIMULATE`, root difficulty 100000. Copied verbatim from +pyquarkchain's +[`devnet/singularity/cluster_config.json`](https://github.com/QuarkChain/pyquarkchain/blob/master/devnet/singularity/cluster_config.json). + +## Pinned root-genesis values + +The root genesis header derived from each config (`qkc.CreateRootBlock`) must hash +byte-identically to pyquarkchain's +`GenesisManager.create_root_block().header.get_hash()`. Regenerate with (swap the +path for devnet): + +``` +# from the root of a pyquarkchain checkout, inside a virtualenv with its +# requirements installed (bare system python lacks e.g. aiohttp): +python -c " +import json +from quarkchain.cluster.cluster_config import ClusterConfig +from quarkchain.genesis import GenesisManager +raw = json.load(open('mainnet/singularity/cluster_config_template.json')) +h = GenesisManager(ClusterConfig.from_dict(raw).QUARKCHAIN).create_root_block().header +print('hash ', h.get_hash().hex()) +print('seal_hash', h.get_hash_for_mining().hex()) +print('serialize', h.serialize().hex()) +" +``` + +Current pinned values: + +``` +mainnet hash 4036783e441eb5057bf2be96bf1fd4585ac49824de15c0d92a4c14a97886ca51 + seal e7dcdecc09e724ad81e493d70dedcd6d9ea0ee830d7ab2528a5648f2a0cf8178 +devnet hash 5ad443efb7cf5246a3d1bbc1734bd02bf3a5d83bedeccfcfe707d0ebee03780d + seal 055a7b410a50c098c52c123983e3596c5914ba227bf9e2c0c93309ff8f650d41 +``` + +Consumed by `qkc`, `qkc/config`, `qkc/types`, and `cmd/slave` tests. diff --git a/qkc/config/singularity/devnet.json b/qkc/config/singularity/devnet.json new file mode 100644 index 000000000000..8cdab6d27af2 --- /dev/null +++ b/qkc/config/singularity/devnet.json @@ -0,0 +1,425 @@ +{ + "P2P_PORT": 38291, + "JSON_RPC_PORT": 38391, + "JSON_RPC_HOST": "0.0.0.0", + "PRIVATE_JSON_RPC_PORT": 38491, + "PRIVATE_JSON_RPC_HOST": "0.0.0.0", + "ENABLE_TRANSACTION_HISTORY": true, + "DB_PATH_ROOT": "./qkc-data/devnet", + "LOG_LEVEL": "info", + "START_SIMULATED_MINING": true, + "CLEAN": false, + "GENESIS_DIR": null, + "QUARKCHAIN": { + "MIN_TX_POOL_GAS_PRICE": 0, + "MIN_MINING_GAS_PRICE": 0, + "CHAIN_SIZE": 8, + "BASE_ETH_CHAIN_ID": 110000, + "MAX_NEIGHBORS": 32, + "NETWORK_ID": 255, + "TRANSACTION_QUEUE_SIZE_LIMIT_PER_SHARD": 10000, + "BLOCK_EXTRA_DATA_SIZE_LIMIT": 1024, + "GUARDIAN_PUBLIC_KEY": "6f9ed23452ffb7902345ca8dc53292480274a22cc4625f783e84dd3a6e7082d3e17901c7dc1ba3286fbd1fbd295c17c0722c89e7693220e00587b0d96dd64647", + "ROOT_SIGNER_PRIVATE_KEY": null, + "P2P_PROTOCOL_VERSION": 0, + "P2P_COMMAND_SIZE_LIMIT": 134217728, + "SKIP_ROOT_DIFFICULTY_CHECK": false, + "SKIP_MINOR_DIFFICULTY_CHECK": false, + "GENESIS_TOKEN": "QKC", + "ENABLE_TX_TIMESTAMP": 0, + "ENABLE_EVM_TIMESTAMP": 0, + "ENABLE_EIP155_SIGNER_TIMESTAMP": 0, + "ENABLE_NON_RESERVED_NATIVE_TOKEN_TIMESTAMP": 0, + "ENABLE_GENERAL_NATIVE_TOKEN_TIMESTAMP": 0, + "ROOT": { + "MAX_STALE_ROOT_BLOCK_HEIGHT_DIFF": 22500, + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 60, + "REMOTE_MINE": true + }, + "GENESIS": { + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 100000, + "NONCE": 0 + }, + "COINBASE_ADDRESS": "5C935469C5592Aeeac3372e922d9bCEabDF8830d00000000", + "COINBASE_AMOUNT": 156000000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 40, + "DIFFICULTY_ADJUSTMENT_FACTOR": 1024, + "EPOCH_INTERVAL": 525600, + "POSW_CONFIG": { + "ENABLED": true, + "ENABLE_TIMESTAMP": 1646668800, + "DIFF_DIVIDER": 100, + "WINDOW_SIZE": 512, + "TOTAL_STAKE_PER_BLOCK": 10000000000000000000000, + "BOOST_TIMESTAMP": 1647424800, + "BOOST_MULTIPLIER_PER_STEP": 2, + "BOOST_STEPS": 10, + "BOOST_STEP_INTERVAL": 3600 + } + }, + "CHAINS": [ + { + "CHAIN_ID": 0, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "a92885095A33E45A3C018Df7Aa6242B62Acb971800000000": { + "balances": { + "QKC": 600000000000000000000000000 + } + }, + "5C935469C5592Aeeac3372e922d9bCEabDF8830d00000000": { + "balances": { + "QKC": 600000000000000000000000000 + } + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": false, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 0 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 1, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 20000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 2, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 40000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 3, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 80000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 4, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 160000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 5, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 320000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 6, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 120000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 40000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 7, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_SIMULATE", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": false + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 120000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 160000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + } + ], + "REWARD_TAX_RATE": 0.5, + "BLOCK_REWARD_DECAY_FACTOR": 0.88, + "ROOT_CHAIN_POSW_CONTRACT_BYTECODE_HASH": "ee90e568da573f251d63256e843add8bd7a27cec1f4c2a06ef20380be68df0a3" + }, + "MASTER": { + "MASTER_TO_SLAVE_CONNECT_RETRY_DELAY": 1.0 + }, + "SLAVE_LIST": [ + { + "HOST": "127.0.0.1", + "PORT": 38000, + "ID": "S0", + "FULL_SHARD_ID_LIST": [ + "0x1", "0x00040001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38001, + "WEBSOCKET_JSON_RPC_PORT": 38591, + "ID": "S1", + "FULL_SHARD_ID_LIST": [ + "0x00010001", "0x00050001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38002, + "WEBSOCKET_JSON_RPC_PORT": 38592, + "ID": "S2", + "FULL_SHARD_ID_LIST": [ + "0x00020001", "0x00060001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38003, + "WEBSOCKET_JSON_RPC_PORT": 38593, + "ID": "S3", + "FULL_SHARD_ID_LIST": [ + "0x00030001", "0x00070001" + ] + } + ], + "P2P": { + "NEW_MODULE": true, + "MAX_PEERS": 30, + "PRIV_KEY": "", + "UPNP": true + }, + "MONITORING": { + "NETWORK_NAME": "", + "CLUSTER_ID": "127.0.0.1", + "KAFKA_REST_ADDRESS": "", + "MINER_TOPIC": "qkc_miner", + "PROPAGATION_TOPIC": "block_propagation", + "ERRORS": "error" + } +} diff --git a/qkc/config/singularity/mainnet.json b/qkc/config/singularity/mainnet.json new file mode 100644 index 000000000000..dc33a7831369 --- /dev/null +++ b/qkc/config/singularity/mainnet.json @@ -0,0 +1,517 @@ +{ + "P2P_PORT": 38291, + "JSON_RPC_PORT": 38391, + "JSON_RPC_HOST": "127.0.0.1", + "PRIVATE_JSON_RPC_PORT": 38491, + "PRIVATE_JSON_RPC_HOST": "127.0.0.1", + "ENABLE_TRANSACTION_HISTORY": false, + "DB_PATH_ROOT": "./qkc-data/mainnet", + "LOG_LEVEL": "info", + "START_SIMULATED_MINING": false, + "CLEAN": false, + "GENESIS_DIR": null, + "QUARKCHAIN": { + "CHAIN_SIZE": 8, + "BASE_ETH_CHAIN_ID": 100000, + "MAX_NEIGHBORS": 32, + "NETWORK_ID": 1, + "TRANSACTION_QUEUE_SIZE_LIMIT_PER_SHARD": 10000, + "BLOCK_EXTRA_DATA_SIZE_LIMIT": 1024, + "GUARDIAN_PUBLIC_KEY": "6f9ed23452ffb7902345ca8dc53292480274a22cc4625f783e84dd3a6e7082d3e17901c7dc1ba3286fbd1fbd295c17c0722c89e7693220e00587b0d96dd64647", + "ROOT_SIGNER_PRIVATE_KEY": null, + "P2P_PROTOCOL_VERSION": 0, + "P2P_COMMAND_SIZE_LIMIT": 134217728, + "SKIP_ROOT_DIFFICULTY_CHECK": false, + "SKIP_MINOR_DIFFICULTY_CHECK": false, + "GENESIS_TOKEN": "QKC", + "ENABLE_TX_TIMESTAMP": 1561791600, + "ENABLE_EVM_TIMESTAMP": 1569567600, + "ENABLE_QKCHASHX_HEIGHT": 1480000, + "ENABLE_EIP155_SIGNER_TIMESTAMP": 1631577600, + "ENABLE_NON_RESERVED_NATIVE_TOKEN_TIMESTAMP": 1588291200, + "ENABLE_GENERAL_NATIVE_TOKEN_TIMESTAMP": 1588291200, + "ENABLE_POSW_STAKING_DECAY_TIMESTAMP": 1588291200, + "TX_WHITELIST_SENDERS": [ + "b8C082828F51343299c9A4deEb2503AaC3bA074f", + "3391A1796cB98D79A2Fde326F375DF900C959Ed0", + "f9c9B4991A885cB7889074FA91E04AFe7d36b856", + "cd91b2C43D5943ad83e682eF723001F6B9Ec35F2", + "3A324dc1fb617eeA9b1EBc41408d87Fc44FbDd4a", + "bde653eFF19dE913AF8eA6AC4287a8Ec2c1f1e24", + "b505DBb1153449df0863cf72ace1a2a1898B7Bba", + "70FC830E4bCC9a4Dd15dE3faE7a8DF0a29b43321", + "13A83b461d7c612f5C120979cEf16335806d6EAc", + "62d4971dB0133dAC13dF915Be1D11FB9d0909a8B", + "7963EAcDC3FdD481db6018673A41a633636a3b69", + "Bb9bd7d3937712405cf74d044EcD1733eb8763d3", + "db7FD07891697f74A7E5102Cc2cC522c25dc06e9", + "248Dc97675f46Cb2AeCa53006F647ED94eF5B502", + "A3eBA5dBeC29f813171A1C4861B98DaBB12641C1", + "Ccf9296ed0e118BF3815f4aC27a0544ECE7E731F", + "Be4B98ABE5982AC6E453307c81CB47A316b78d89", + "9Cb0b9B9e707054C862272FC5f61B3F2E8d88BE9", + "26E2Fa524B85072eD7fb5D5d9237804dc6c0A140", + "75980402beDF9dca4d745fd8F2B06aeAAE97A8CB", + "bE9473d4Fd4FeD5F20DF43328da93d6dF4104E09", + "60aE18CedCdDd524Fc3448b5AA76634189625699", + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e", + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97", + "2b7aCC42b0dc2a1562601e2ed9957eAdff7A1347", + "F923ac88fc61837662BACe7E94720C7a071997E6", + "c4fbA3740f95d25B2196C9437fDb005359296D36", + "1f231b489a2d5A1Eb374D363D3Ac851c25Db8626", + "eeb4bBff983536039eda281A0ACCFAe360AeF1fA", + "b9385cA98F102Bd6B180cB76AA0ca8c1615053e7" + ], + "ROOT": { + "MAX_STALE_ROOT_BLOCK_HEIGHT_DIFF": 22500, + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 60, + "REMOTE_MINE": true + }, + "GENESIS": { + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 10000000000000, + "NONCE": 0 + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 156000000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 40, + "DIFFICULTY_ADJUSTMENT_FACTOR": 1024, + "EPOCH_INTERVAL": 525600, + "POSW_CONFIG": { + "ENABLED": true, + "ENABLE_TIMESTAMP": 1569567600, + "DIFF_DIVIDER": 10000, + "WINDOW_SIZE": 512, + "TOTAL_STAKE_PER_BLOCK": 1000000000000000000000000, + "BOOST_TIMESTAMP": 1649736000, + "BOOST_MULTIPLIER_PER_STEP": 2, + "BOOST_STEPS": 13, + "BOOST_STEP_INTERVAL": 86400 + } + }, + "CHAINS": [ + { + "CHAIN_ID": 0, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000075b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000075b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000075b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": false, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 0 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 1, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000175b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000175b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000175b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 20000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 2, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000275b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000275b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000275b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 40000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 3, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000375b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000375b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000375b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 80000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 4, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000475b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000475b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000475b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 160000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 5, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_ETHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 5000000000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000575b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000575b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000575b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 320000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 6, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_QKCHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 120000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000675b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000675b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000675b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 40000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + }, + { + "CHAIN_ID": 7, + "SHARD_SIZE": 1, + "DEFAULT_CHAIN_TOKEN": "QKC", + "CONSENSUS_TYPE": "POW_QKCHASH", + "CONSENSUS_CONFIG": { + "TARGET_BLOCK_TIME": 10, + "REMOTE_MINE": true + }, + "GENESIS": { + "ROOT_HEIGHT": 0, + "VERSION": 0, + "HEIGHT": 0, + "HASH_PREV_MINOR_BLOCK": "0000000000000000000000000000000000000000000000000000000000000000", + "HASH_MERKLE_ROOT": "0000000000000000000000000000000000000000000000000000000000000000", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", + "TIMESTAMP": 1556639999, + "DIFFICULTY": 120000, + "GAS_LIMIT": 12000000, + "NONCE": 0, + "ALLOC": { + "32c53C6c2B57B2026a51C87aDD0695F5AeEd3f2e000775b2": { + "QKC": 600000000000000000000000000 + }, + "7DeB90eF2097D8A9e423516e199b9D95EB2b4D97000775b2": { + "QKC": 100000000000000000000000000 + }, + "c4fbA3740f95d25B2196C9437fDb005359296D36000775b2": { + "QKC": 50000000000000000000000000 + } + } + }, + "COINBASE_ADDRESS": "000000000000000000000000000000000000000000000000", + "COINBASE_AMOUNT": 6500000000000000000, + "DIFFICULTY_ADJUSTMENT_CUTOFF_TIME": 7, + "DIFFICULTY_ADJUSTMENT_FACTOR": 512, + "EXTRA_SHARD_BLOCKS_IN_ROOT_BLOCK": 12, + "POSW_CONFIG": { + "ENABLED": true, + "DIFF_DIVIDER": 20, + "WINDOW_SIZE": 256, + "TOTAL_STAKE_PER_BLOCK": 160000000000000000000000 + }, + "EPOCH_INTERVAL": 3153600 + } + ], + "REWARD_TAX_RATE": 0.5, + "BLOCK_REWARD_DECAY_FACTOR": 0.88, + "ROOT_CHAIN_POSW_CONTRACT_BYTECODE_HASH": "ee90e568da573f251d63256e843add8bd7a27cec1f4c2a06ef20380be68df0a3" + }, + "MASTER": { + "MASTER_TO_SLAVE_CONNECT_RETRY_DELAY": 1.0 + }, + "SLAVE_LIST": [ + { + "HOST": "127.0.0.1", + "PORT": 38000, + "ID": "S0", + "FULL_SHARD_ID_LIST": [ + "0x1", "0x00040001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38001, + "ID": "S1", + "FULL_SHARD_ID_LIST": [ + "0x00010001", "0x00050001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38002, + "ID": "S2", + "FULL_SHARD_ID_LIST": [ + "0x00020001", "0x00060001" + ] + }, + { + "HOST": "127.0.0.1", + "PORT": 38003, + "ID": "S3", + "FULL_SHARD_ID_LIST": [ + "0x00030001", "0x00070001" + ] + } + ], + "P2P": { + "NEW_MODULE": true, + "MAX_PEERS": 30, + "BOOT_NODES": "enode://438d9a2349037e231ae7975f646a32c5b3d2032190a067762b35b8a039568fbb81981e4e2e43f1923a113834a4675919ed27fad68ca48203b4001fee049a9276@35.243.210.122:38291,enode://c093dee29400c0d114c3af600df80a7ad285e8b430f6768749600d55726d4b1562f624526c596b968e4520eaeadaa0d8be98940da065ac710a76d8fac58d5c00@35.246.213.180:38291,enode://48e1af232c290add043118edca45608589ef305f19a2d6d8a6126677ba573c1d5984c15962e8593b32e6ae2f1a89237f9691178e527cba0b07818ad5b01a13dc@52.34.48.64:38291,enode://69a887846c4f6540958c20d654b191b08c39e5624b93d8e94ec8e37da2ae7c0572c0741775e34cd17affa7e68532910e152e16361d22198f04aae2cceb105a03@13.124.15.123:38291,enode://5f81aac576814cac04701d418d9f127903cf75ed26c0433b9b1b35774efe7e2630e377baae156023d2448055e32678a472f6a25f5ead8a690f8cec1e7c9176e6@68.183.247.182:38291,enode://80a7f0960732dae69fa470cf950be636352166b9f016605b79bae4286f06a3fb0361f1293d6ea43df9b87f6e6397498e82c23d913464e2724e5c3adcce796e0d@165.227.240.113:38291", + "PRIV_KEY": "", + "UPNP": true + }, + "MONITORING": { + "NETWORK_NAME": "", + "CLUSTER_ID": "127.0.0.1", + "KAFKA_REST_ADDRESS": "", + "MINER_TOPIC": "qkc_miner", + "PROPAGATION_TOPIC": "block_propagation", + "ERRORS": "error" + } +} diff --git a/qkc/config/test_config.json b/qkc/config/test_config.json index 2677977b92d5..da0d0d4330f4 100644 --- a/qkc/config/test_config.json +++ b/qkc/config/test_config.json @@ -57,7 +57,7 @@ "HEIGHT": 0, "HASH_PREV_MINOR_BLOCK": "", "HASH_MERKLE_ROOT": "", - "EXTRA_DATA": "SXQgd2FzIHRoZSBiZXN0IG9mIHRpbWVzLCBpdCB3YXMgdGhlIHdvcnN0IG9mIHRpbWVzLCAuLi4gLSBDaGFybGVzIERpY2tlbnM=", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", "TIMESTAMP": 1519147489, "DIFFICULTY": 10000, "GAS_LIMIT": 12000000, @@ -95,7 +95,7 @@ "HEIGHT": 0, "HASH_PREV_MINOR_BLOCK": "", "HASH_MERKLE_ROOT": "", - "EXTRA_DATA": "SXQgd2FzIHRoZSBiZXN0IG9mIHRpbWVzLCBpdCB3YXMgdGhlIHdvcnN0IG9mIHRpbWVzLCAuLi4gLSBDaGFybGVzIERpY2tlbnM=", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", "TIMESTAMP": 1519147489, "DIFFICULTY": 10000, "GAS_LIMIT": 12000000, @@ -133,7 +133,7 @@ "HEIGHT": 0, "HASH_PREV_MINOR_BLOCK": "", "HASH_MERKLE_ROOT": "", - "EXTRA_DATA": "SXQgd2FzIHRoZSBiZXN0IG9mIHRpbWVzLCBpdCB3YXMgdGhlIHdvcnN0IG9mIHRpbWVzLCAuLi4gLSBDaGFybGVzIERpY2tlbnM=", + "EXTRA_DATA": "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732c202e2e2e202d20436861726c6573204469636b656e73", "TIMESTAMP": 1519147489, "DIFFICULTY": 10000, "GAS_LIMIT": 12000000, diff --git a/qkc/genesis.go b/qkc/genesis.go new file mode 100644 index 000000000000..97e716f3c0a4 --- /dev/null +++ b/qkc/genesis.go @@ -0,0 +1,80 @@ +// Copyright 2026-2027, QuarkChain. + +// The GenesisManager analog: pure functions of the parsed config that derive the +// cluster's genesis blocks. Only the root genesis block (the anchor every shard +// genesis links to) is derived here; shard genesis state materialization belongs +// to the separate shard-chain task. + +package qkc + +import ( + "encoding/hex" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + qkcCommon "github.com/ethereum/go-ethereum/qkc/common" + "github.com/ethereum/go-ethereum/qkc/config" + "github.com/ethereum/go-ethereum/qkc/types" +) + +// CreateRootBlock derives the genesis root block header purely from +// QUARKCHAIN.ROOT.GENESIS, mirroring pyquarkchain's +// GenesisManager.create_root_block (quarkchain/genesis.py:28). total_difficulty is +// set equal to difficulty; the evm-state root, coinbase, amount map, extra data, +// mixhash, and signature are all empty/zero, exactly as the Python default header. +// The resulting header.Hash() is byte-identical to pyquarkchain's +// create_root_block().header.get_hash(). +func CreateRootBlock(qkc *config.QuarkChainConfig) (*types.RootBlockHeader, error) { + if qkc == nil || qkc.Root == nil || qkc.Root.Genesis == nil { + return nil, fmt.Errorf("genesis: QUARKCHAIN.ROOT.GENESIS is missing") + } + g := qkc.Root.Genesis + + prev, err := parseRootHash(g.HashPrevBlock) + if err != nil { + return nil, fmt.Errorf("genesis: ROOT.GENESIS.HASH_PREV_BLOCK: %w", err) + } + merkle, err := parseRootHash(g.HashMerkleRoot) + if err != nil { + return nil, fmt.Errorf("genesis: ROOT.GENESIS.HASH_MERKLE_ROOT: %w", err) + } + + difficulty := new(big.Int) + if g.Difficulty != nil { + difficulty.Set(g.Difficulty) + } + return &types.RootBlockHeader{ + Version: g.Version, + Number: g.Height, + ParentHash: prev, + MinorHeaderHash: merkle, + Coinbase: account.CreatEmptyAddress(0), + CoinbaseAmount: qkcCommon.NewEmptyTokenBalances(), + Time: g.Timestamp, + Difficulty: difficulty, + TotalDifficulty: new(big.Int).Set(difficulty), + Nonce: g.Nonce, + }, nil +} + +// parseRootHash decodes a config hash field (a hex string such as "0000…0000", +// optionally "0x"-prefixed; empty means all-zero) into a 32-byte hash. Any other +// length is rejected so a malformed config fails loudly here rather than producing +// a silently wrong genesis hash. +func parseRootHash(s string) (common.Hash, error) { + s = strings.TrimPrefix(s, "0x") + if s == "" { + return common.Hash{}, nil + } + b, err := hex.DecodeString(s) + if err != nil { + return common.Hash{}, fmt.Errorf("invalid hex %q: %w", s, err) + } + if len(b) != common.HashLength { + return common.Hash{}, fmt.Errorf("expected %d bytes, got %d", common.HashLength, len(b)) + } + return common.BytesToHash(b), nil +} diff --git a/qkc/genesis_test.go b/qkc/genesis_test.go new file mode 100644 index 000000000000..aee1898f357d --- /dev/null +++ b/qkc/genesis_test.go @@ -0,0 +1,97 @@ +// Copyright 2026-2027, QuarkChain. + +package qkc + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/config" +) + +// TestRootBlockFromFixture pins the root genesis hash derived from each real +// QuarkChain network config (the live mainnet and the running devnet) against +// pyquarkchain's own GenesisManager.create_root_block().header.get_hash(). This is +// the tightest compatibility contract. Regeneration: qkc/config/singularity/README.md. +func TestRootBlockFromFixture(t *testing.T) { + for _, tc := range []struct { + name string + path string + hash string + seal string + }{ + { + name: "mainnet", + path: "config/singularity/mainnet.json", + hash: "0x4036783e441eb5057bf2be96bf1fd4585ac49824de15c0d92a4c14a97886ca51", + seal: "0xe7dcdecc09e724ad81e493d70dedcd6d9ea0ee830d7ab2528a5648f2a0cf8178", + }, + { + name: "devnet", + path: "config/singularity/devnet.json", + hash: "0x5ad443efb7cf5246a3d1bbc1734bd02bf3a5d83bedeccfcfe707d0ebee03780d", + seal: "0x055a7b410a50c098c52c123983e3596c5914ba227bf9e2c0c93309ff8f650d41", + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg, err := config.LoadClusterConfig(tc.path) + if err != nil { + t.Fatalf("load fixture: %v", err) + } + header, err := CreateRootBlock(cfg.Quarkchain) + if err != nil { + t.Fatalf("CreateRootBlock: %v", err) + } + if got := header.Hash(); got != common.HexToHash(tc.hash) { + t.Errorf("root genesis hash mismatch\n got %s\nwant %s", got.Hex(), tc.hash) + } + if got := header.SealHash(); got != common.HexToHash(tc.seal) { + t.Errorf("root genesis seal hash mismatch\n got %s\nwant %s", got.Hex(), tc.seal) + } + // total_difficulty is set equal to difficulty (pyquarkchain genesis.py). + if header.Difficulty.Cmp(header.TotalDifficulty) != 0 { + t.Errorf("total_difficulty %d != difficulty %d", header.TotalDifficulty, header.Difficulty) + } + }) + } +} + +// TestRootBlockSynthetic exercises every ROOT.GENESIS field non-zero, pinned +// against a pyquarkchain RootBlockHeader built with the same fields. See README.md. +func TestRootBlockSynthetic(t *testing.T) { + qkc := config.NewQuarkChainConfig() + qkc.Root.Genesis = &config.RootGenesis{ + Version: 7, + Height: 3, + HashPrevBlock: "1111111111111111111111111111111111111111111111111111111111111111", + HashMerkleRoot: "2222222222222222222222222222222222222222222222222222222222222222", + Timestamp: 1234567890, + Difficulty: big.NewInt(11259375), // 0xabcdef + Nonce: 99, + } + + header, err := CreateRootBlock(qkc) + if err != nil { + t.Fatalf("CreateRootBlock: %v", err) + } + + const ( + wantHash = "0x70dc3e4bcfe83d1bb81f95015550c9a741168677fe711128aec2d811423991c2" + wantSeal = "0xbb6649648cd925271b70a49269257b602366b0a24db14d93038ec04a665ad67e" + ) + if got := header.Hash(); got != common.HexToHash(wantHash) { + t.Errorf("synthetic root genesis hash mismatch\n got %s\nwant %s", got.Hex(), wantHash) + } + if got := header.SealHash(); got != common.HexToHash(wantSeal) { + t.Errorf("synthetic root genesis seal hash mismatch\n got %s\nwant %s", got.Hex(), wantSeal) + } +} + +func TestRootBlockRejectsBadHash(t *testing.T) { + qkc := config.NewQuarkChainConfig() + qkc.Root.Genesis = &config.RootGenesis{HashPrevBlock: "zznothex"} + if _, err := CreateRootBlock(qkc); err == nil { + t.Fatal("expected error for malformed HASH_PREV_BLOCK, got nil") + } +} diff --git a/qkc/types/rootblock.go b/qkc/types/rootblock.go new file mode 100644 index 000000000000..8b445fb6fcf2 --- /dev/null +++ b/qkc/types/rootblock.go @@ -0,0 +1,65 @@ +// Copyright 2026-2027, QuarkChain. + +// RootBlockHeader mirrors goquarkchain's core/types.RootBlockHeader, kept minimal: +// only the fields and the Hash/SealHash needed to derive and pin the root genesis +// block. Mining, signing, and RLP helpers are omitted. +// TODO: more content need to be added later. +// +// Field order and ser tags reproduce pyquarkchain's RootBlockHeader.FIELDS +// (quarkchain/core.py:888) exactly, so qkc/serialize encodes it byte-identically: +// +// version uint32 +// height uint32 +// hash_prev_block hash256 (32 raw bytes) +// hash_merkle_root hash256 +// hash_evm_state_root hash256 +// coinbase_address Address (20-byte recipient + uint32 full_shard_key) +// coinbase_amount_map TokenBalanceMap +// create_time uint64 +// difficulty biguint (1-byte length prefix + big-endian bytes) +// total_difficulty biguint +// nonce uint64 +// extra_data PrependedSizeBytes(2) +// mixhash hash256 +// signature FixedSizeBytes(65) + +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + qkcCommon "github.com/ethereum/go-ethereum/qkc/common" +) + +// RootBlockHeader is the QuarkChain root block header. Only the subset of behavior +// needed to derive, hash, and seal-hash the root genesis is implemented. +type RootBlockHeader struct { + Version uint32 + Number uint32 + ParentHash common.Hash + MinorHeaderHash common.Hash + Root common.Hash + Coinbase account.Address + CoinbaseAmount *qkcCommon.TokenBalances + Time uint64 + Difficulty *big.Int + TotalDifficulty *big.Int + Nonce uint64 + Extra []byte `bytesizeofslicelen:"2"` + MixDigest common.Hash + Signature [65]byte +} + +// Hash returns keccak256 of the full serialized header — the value pyquarkchain +// records as the block hash (RootBlockHeader.get_hash, quarkchain/core.py:938). +func (h *RootBlockHeader) Hash() common.Hash { + return serHash(*h, nil) +} + +// SealHash returns keccak256 of the header serialized without Nonce, MixDigest, +// and Signature — pyquarkchain's get_hash_for_mining (the proof-of-work input). +func (h *RootBlockHeader) SealHash() common.Hash { + return serHash(*h, map[string]bool{"Nonce": true, "MixDigest": true, "Signature": true}) +} diff --git a/qkc/types/rootblock_test.go b/qkc/types/rootblock_test.go new file mode 100644 index 000000000000..1eb70e961557 --- /dev/null +++ b/qkc/types/rootblock_test.go @@ -0,0 +1,129 @@ +// Copyright 2026-2027, QuarkChain. + +package types + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + qkcCommon "github.com/ethereum/go-ethereum/qkc/common" + "github.com/ethereum/go-ethereum/qkc/serialize" + "github.com/holiman/uint256" +) + +// Golden values produced by pyquarkchain (the compatibility source of truth). See +// qkc/config/singularity/README.md for the regeneration one-liner; the synthetic +// case is built directly in quarkchain.core.RootBlockHeader with every field +// non-zero. These pin both the serialized bytes (so a layout regression is caught +// precisely) and the resulting hash/seal-hash. +func rep(b byte, n int) []byte { + s := make([]byte, n) + for i := range s { + s[i] = b + } + return s +} + +func sig65(b byte) (s [65]byte) { + copy(s[:], rep(b, 65)) + return s +} + +func TestRootBlockHeaderSerializeAndHash(t *testing.T) { + cases := []struct { + name string + header *RootBlockHeader + ser string + hash string + sealHash string + }{ + { + // The real QuarkChain mainnet (singularity) root genesis, built exactly + // as qkc.CreateRootBlock builds it from ROOT.GENESIS. + name: "mainnet_root_genesis", + header: &RootBlockHeader{ + Time: 1556639999, + Coinbase: account.CreatEmptyAddress(0), + CoinbaseAmount: qkcCommon.NewEmptyTokenBalances(), + Difficulty: big.NewInt(10000000000000), + TotalDifficulty: big.NewInt(10000000000000), + }, + ser: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005cc870ff0609184e72a0000609184e72a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hash: "4036783e441eb5057bf2be96bf1fd4585ac49824de15c0d92a4c14a97886ca51", + sealHash: "e7dcdecc09e724ad81e493d70dedcd6d9ea0ee830d7ab2528a5648f2a0cf8178", + }, + { + // The QuarkChain devnet (singularity) root genesis. + name: "devnet_root_genesis", + header: &RootBlockHeader{ + Time: 1556639999, + Coinbase: account.CreatEmptyAddress(0), + CoinbaseAmount: qkcCommon.NewEmptyTokenBalances(), + Difficulty: big.NewInt(100000), + TotalDifficulty: big.NewInt(100000), + }, + ser: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005cc870ff030186a0030186a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hash: "5ad443efb7cf5246a3d1bbc1734bd02bf3a5d83bedeccfcfe707d0ebee03780d", + sealHash: "055a7b410a50c098c52c123983e3596c5914ba227bf9e2c0c93309ff8f650d41", + }, + { + // Every field non-zero, including a multi-entry coinbase amount map + // with a zero balance (token id 2) that must be skipped. + name: "synthetic_all_fields", + header: &RootBlockHeader{ + Version: 1, + Number: 2, + ParentHash: common.BytesToHash(rep(0x01, 32)), + MinorHeaderHash: common.BytesToHash(rep(0x02, 32)), + Root: common.BytesToHash(rep(0x03, 32)), + Coinbase: account.NewAddress(common.BytesToAddress(rep(0xaa, 20)), 0x00010001), + CoinbaseAmount: qkcCommon.NewTokenBalancesWithMap(map[uint64]*uint256.Int{ + 1: uint256.NewInt(100), + 2: uint256.NewInt(0), + 1000000: uint256.NewInt(999), + }), + Time: 1600000000, + Difficulty: big.NewInt(1000000), + TotalDifficulty: big.NewInt(2000000), + Nonce: 42, + Extra: []byte("hello"), + MixDigest: common.BytesToHash(rep(0x04, 32)), + Signature: sig65(0x05), + }, + ser: "0000000100000002010101010101010101010101010101010101010101010101010101010101010102020202020202020202020202020202020202020202020202020202020202020303030303030303030303030303030303030303030303030303030303030303aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000100010000000201010164030f42400203e7000000005f5e1000030f4240031e8480000000000000002a000568656c6c6f04040404040404040404040404040404040404040404040404040404040404040505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505", + hash: "316595066fd9795df3ba42b3f0e6b3da03bf0f75e14a9f425270095a8768ed4c", + sealHash: "32e26c6a06cbb68460f4b2c79379e157ac62373cf5f0a7175079ac19d80a9c93", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := serialize.SerializeToBytes(tc.header) + if err != nil { + t.Fatalf("serialize: %v", err) + } + if h := hex.EncodeToString(got); h != tc.ser { + t.Errorf("serialized bytes mismatch\n got %s\nwant %s", h, tc.ser) + } + if h := tc.header.Hash(); h != common.HexToHash(tc.hash) { + t.Errorf("hash mismatch\n got %s\nwant 0x%s", h.Hex(), tc.hash) + } + if h := tc.header.SealHash(); h != common.HexToHash(tc.sealHash) { + t.Errorf("seal hash mismatch\n got %s\nwant 0x%s", h.Hex(), tc.sealHash) + } + }) + } +} + +func TestEmptyTokenBalancesSerialize(t *testing.T) { + var w []byte + if err := qkcCommon.NewEmptyTokenBalances().Serialize(&w); err != nil { + t.Fatalf("serialize: %v", err) + } + if h := hex.EncodeToString(w); h != "00000000" { + t.Fatalf("empty map serialize = %s, want 00000000", h) + } +}