Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions cmd/slave/README.md
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 73 additions & 0 deletions cmd/slave/configcmd.go
Original file line number Diff line number Diff line change
@@ -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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error returned by tw.Flush() is silently ignored.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring Flush() errors is consistent with surrounding Fprintf calls, both of which write targeting os.Stdout. This is common in geth, so it's acceptable and not a defect.

}
53 changes: 53 additions & 0 deletions cmd/slave/genesiscmd.go
Original file line number Diff line number Diff line change
@@ -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())
}
106 changes: 106 additions & 0 deletions cmd/slave/main.go
Original file line number Diff line number Diff line change
@@ -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=<file> --node_id=<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)
}
Loading