Skip to content
Open
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
50 changes: 33 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Use **alpha** for reliability; use **dev** if you want the newest changes and ca

## Running Mithril

The `run` command starts Mithril as a live full node - it bootstraps from a Solana snapshot and continuously verifies new blocks as they are produced on mainnet-beta.
The `run` command starts Mithril as a live full node - it bootstraps from a Solana snapshot and continuously verifies new blocks as they are produced.

**This branch builds an Alpenglow-only node.** It boots against Alpenglow clusters (`network.cluster = "alpenglow"`, the default), streams live blocks from native turbine shreds by default, and uses Alpenglow certificates as the source of truth for fork choice and durable state. TowerBFT clusters (`mainnet-beta`/`testnet`/`devnet`) need a build from the `dev` branch until those clusters upgrade to Alpenglow.

### Nix (NixOS / nix-darwin / Home Manager)

Expand Down Expand Up @@ -122,25 +124,40 @@ This builds the `mithril` binary with version, commit, and branch information em

### Configuration

Generate a starter config with sensible defaults:
Mithril runs as one of two node types, selected by `[consensus].mode`:

- **Verifying node** (`mode = "verifying"`, the default) — non-voting: observes, executes, and verifies the cluster. No keypairs required.
- **Validator** (`mode = "validator"`) — enforces the full voting-deployment shape at startup: identity + vote-account keypairs, the turbine block source with a gossip entrypoint, and the Votor QUIC listener. The voting engine has not landed yet, so a validator-mode node runs the same verifying pipeline and casts **no votes** — but selecting it now means the deployment is provisioned and the config stays valid when voting activates. Both node types share the same fork choice.

Generate a starter config for your node type:

```bash
# Verifying node (default)
./mithril config init

# Validator (keypair/socket fields laid out and required)
./mithril config init --validator
```

This creates `config.toml`. **We strongly recommend reviewing [`config.example.toml`](config.example.toml)** for all available options and detailed documentation.
This creates `config.toml`. **We strongly recommend reviewing [`config.example.toml`](config.example.toml)** for all available options and detailed documentation. At minimum, set:

- `[network].rpc` — RPC endpoint(s), used for catchup, tip polling, and execution verification
- `[turbine].gossip_entrypoint` — a gossip entrypoint of your Alpenglow cluster. **Required for the default turbine block source**: without it the node cannot join the shred tree.
- Validator profile only: the `[validator]` identity and vote-account keypair paths. Keep the authorized-withdrawer keypair **offline** — it is not needed at runtime.

There is also an interactive wizard (`./mithril setup`) that asks for the node type first and generates the matching config, and `./mithril doctor` validates an existing one.

**Important: RPC Configuration**

The default config uses `api.mainnet-beta.solana.com` as the RPC endpoint, but this public endpoint has low rate limits and is not suitable for getting blocks. For reliable operation, add a dedicated RPC provider (Helius, Triton, etc.) as the primary endpoint:
Public RPC endpoints have low rate limits. For reliable operation, use a dedicated RPC endpoint for your Alpenglow cluster as the primary:

```toml
[network]
# Primary RPC first, public endpoint as fallback
rpc = ["https://your-rpc-provider.com", "https://api.mainnet-beta.solana.com"]
# Primary RPC first, fallbacks after
rpc = ["https://your-rpc-provider.example.com", "https://public-fallback.example.com"]
```

Mithril will use the first endpoint for block fetching and fall back to others if needed.
Mithril uses the first endpoint and fails over to the others if needed.

### Running Mithril

Expand All @@ -164,11 +181,11 @@ You can also specify a config file explicitly:
**Note:** Do not run Mithril with `sudo`. The setup scripts automatically configure directory permissions for your user.

**What happens:**
1. Mithril queries the Solana cluster to find reliable snapshot sources
1. Mithril queries the cluster to find reliable snapshot sources
2. The full snapshot is streamed and processed (optionally saved to disk for faster restarts)
3. An incremental snapshot is fetched to bring the state closer to the tip
4. Mithril block execution (aka replay) is initiated and blocks are retrieved with RPC `getBlock` calls and verified
5. Mithril keeps up very close to the tip of the chain with recommended hardware specs
4. Replay catches up toward the tip with RPC `getBlock` calls, then hands off to live blocks reconstructed from native turbine shreds (which carry the Alpenglow block ids and footer certificates)
5. Blocks execute the moment they are assembled; Alpenglow certificates drive fork choice and gate what is promoted to durable storage, and a trailing verifier cross-checks execution results against finalized RPC blocks

### Mithril's Simple RPC Server

Expand Down Expand Up @@ -206,16 +223,15 @@ We're actively expanding RPC method coverage. Upcoming methods include transacti

### Current Limitations

- **Block Catchup**: Mithril currently relies on `getBlock` RPC calls to catch up to the tip of mainnet-beta. This dependency is temporary — we are actively working on direct shred replay, which will eliminate the need for external RPC sources entirely.
- **RPC still required**: live near-tip blocks stream from turbine shreds, but RPC `getBlock` is still used for catchup and by the trailing execution verifier (Alpenglow certificates attest block *data*, not execution results, so an external oracle cross-checks execution until peer bankhash cross-checking lands).
- **Voting engine not yet active**: validator mode provisions and enforces the full voting deployment shape, but the node runs verify-only until the voting engine lands. Block production, repair serving, and Rotor relay duty are also future work.

### RPC Sources

Mithril fetches blocks via `getBlock` RPC calls during catchup. For **short-term testing**, most free Solana RPC plans are sufficient to try out Mithril.
Mithril fetches blocks via `getBlock` RPC calls during catchup and uses RPC for trailing execution verification. For **short-term testing**, most free Solana RPC plans are sufficient to try out Mithril.

For **extended testing** or if you'd like to help with longer-running nodes, reach out to us on the [Overclock Validator Discord](https://discord.gg/overclock) — we can provide access to our RPC endpoints.

Once direct shred replay is implemented, external RPC sources will no longer be required for block fetching.

### Updating Mithril

To update Mithril to a newer version:
Expand Down Expand Up @@ -267,15 +283,15 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for supported networks and feature gate
### Milestone 3 (In Progress): Alpha Release and System Optimization
- First formal audit (https://runtimeverification.com/ team is nearing end of audit). Includes development and intensive use of a robust and comprehensive 'conformance suite' for verification of compliance of the VM, interpreter, and runtime as a complete unit. Differential fuzzing will be used to detect differences versus relevant versions of the Labs client, and guided fuzzing will be used generally to uncover security and loss-of-availability issues. Any bugs identified during this phase will be remediated.
- Thorough optimization work on entire system, including on components such as the Virtual Machine and AccountsDB.
- Consensus verification implementation.
- Direct shred replay support (alternative to RPC-based block fetching and requires consensus implementation).
- Consensus verification implementation (landed on this branch: the Alpenglow certificate engine drives fork choice and durable-state promotion).
- Direct shred replay support (landed on this branch: native turbine shred streaming is the default block source).
- Achieve multi-epoch runs without bugs (e.g. bankhash mismatches with mainnet)
- Transaction simulation and transaction sending
- Earlier testing on testnet environments.
- **Target**: More polished release midway through Q1 2026.

### Future Directions
- Implement Alpenglow consensus verification.
- Complete Alpenglow validator mode: the voting engine (Votor event loop, BLS vote signing, durable vote history) on top of the existing fork choice, plus block production, repair serving, and Rotor relay duty.
- Add Agave ledger-tool type features for Mithril
- gRPC interface support.
- Expanded RPC feature set.
Expand Down
97 changes: 71 additions & 26 deletions cmd/mithril/configcmd/configcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ var (
The generated config has all parameters with good defaults - you only need to
customize the storage paths for your setup.

Two profiles:
mithril config init Verifying node (non-voting) — the default.
mithril config init --validator Validator — consensus.mode=validator with the
required keypair/socket fields laid out
(identity + vote-account keypairs, turbine
gossip entrypoint, Votor QUIC listener).
The voting engine is not yet active; the
node runs verify-only until it lands.

If config.toml already exists, this command will not overwrite it.`,
Run: func(cmd *cobra.Command, args []string) {
runConfigInit()
Expand Down Expand Up @@ -77,15 +86,17 @@ Examples:
},
}

outputPath string
configFile string
outputPath string
initValidator bool
configFile string
)

func init() {
ConfigCmd.AddCommand(&InitCmd)
ConfigCmd.AddCommand(&SetCmd)
ConfigCmd.AddCommand(&GetCmd)
InitCmd.Flags().StringVarP(&outputPath, "output", "o", "config.toml", "Output path for config file")
InitCmd.Flags().BoolVar(&initValidator, "validator", false, "Generate a validator config (consensus.mode=validator with required keypair/socket fields)")
SetCmd.Flags().StringVarP(&configFile, "config", "c", "config.toml", "Path to config file")
GetCmd.Flags().StringVarP(&configFile, "config", "c", "config.toml", "Path to config file")
}
Expand All @@ -98,7 +109,7 @@ func runConfigInit() {
}

// Generate the config content
config := generateStarterConfig()
config := generateStarterConfig(initValidator)

// Write to file
if err := tui.AtomicWriteFile(outputPath, []byte(config), 0600); err != nil {
Expand All @@ -111,16 +122,58 @@ func runConfigInit() {
fmt.Println()
fmt.Println("Next steps:")
fmt.Println(" 1. Edit the [storage] paths for your setup")
fmt.Println(" 2. Run: mithril run --config config.toml")
fmt.Println(" 2. Set [network].rpc and [turbine].gossip_entrypoint for your Alpenglow cluster")
if initValidator {
fmt.Println(" 3. Set [validator].identity_keypair and vote_account_keypair —")
fmt.Println(" validator mode refuses to start without them")
fmt.Println(" (keep the authorized withdrawer keypair OFFLINE; it is not needed at runtime)")
} else {
fmt.Println(" 3. For a staked node, set [validator].identity_keypair and")
fmt.Println(" [consensus].alpenglow_observer_bind_addr (Votor QUIC cert feed)")
}
fmt.Println(" 4. Run: mithril run --config config.toml")
fmt.Println()
fmt.Println("See config.example.toml for detailed documentation of all options.")
}

func generateStarterConfig() string {
func generateStarterConfig(validator bool) string {
// Pick storage paths that work for the current environment: production
// /mnt/mithril-* when scripts/disk-setup.sh has been run, ~/.mithril/*
// otherwise. See pkg/config/defaults.go for detection details.
s := config.DefaultStoragePaths()

// The [validator] + [consensus] sections are the profile split: a
// verifying node needs neither keypairs nor the Votor listener; validator
// mode REQUIRES identity + vote-account keypairs, a turbine gossip
// entrypoint, and the Votor QUIC listener (enforced at startup).
nodeSections := `[validator]
identity_keypair = "" # Validator identity — advertises this node into turbine gossip; set for a staked Alpenglow node
vote_account_keypair = "" # Vote account keypair path (used once voting activates)
authorized_withdrawer_keypair = "" # Authorized withdrawer keypair path (diagnostics only)

[consensus]
mode = "verifying" # "verifying" (default, non-voting) | "validator"
alpenglow_observer_bind_addr = "" # Votor QUIC cert listener, e.g. "0.0.0.0:8010" (empty = rely on footer certs in shreds)
alpenglow_max_message_bytes = 0 # 0 = default
alpenglow_bls_dst = "" # BLS DST override (must match cluster solana-bls version)`
if validator {
nodeSections = `[validator]
# REQUIRED in validator mode — the node refuses to start without these two.
identity_keypair = "/path/to/validator-keypair.json" # Signs gossip/turbine identity and, once voting activates, votes
vote_account_keypair = "/path/to/vote-account-keypair.json" # The vote account votes are cast for
# NOT required at runtime — keep the withdrawer keypair OFFLINE.
authorized_withdrawer_keypair = ""

[consensus]
# Validator mode enforces the full voting-deployment shape (keypairs above,
# turbine source + gossip entrypoint, Votor QUIC listener below) so the
# deployment is provisioned before the voting engine activates. Until it
# lands the node runs the same verifying pipeline and casts NO votes.
mode = "validator"
alpenglow_observer_bind_addr = "0.0.0.0:8010" # REQUIRED: Votor QUIC vote/cert listener
alpenglow_max_message_bytes = 0 # 0 = default
alpenglow_bls_dst = "" # BLS DST override (must match cluster solana-bls version)`
}
return fmt.Sprintf(`# Mithril Configuration
# Generated by: mithril config init
# See config.example.toml for detailed documentation of all options.
Expand All @@ -137,17 +190,20 @@ snapshots = %q # ~100GB for full + incremental
logs = %q # Log files (created if missing)

[network]
cluster = "mainnet-beta" # Required: "mainnet-beta" | "testnet" | "devnet" | "alpenglow"
rpc = ["https://api.mainnet-beta.solana.com"]
cluster = "alpenglow" # This build boots Alpenglow only (TowerBFT clusters need a dev-branch build)
rpc = ["https://alpenglow.rpcpool.com"]

[block]
source = "rpc" # "rpc" | "lightbringer" | "turbine"
# "turbine" is the live mode: shreds carry the Alpenglow block ids and footer
# certificates that gate durable state. "rpc" is catch-up/debug only — RPC
# blocks carry no certificates, so near-tip operation cannot adjudicate them
# and durable folds stall without a Votor QUIC cert feed ([consensus] below).
source = "turbine" # "turbine" (live) | "rpc" (catch-up/debug) | "lightbringer"
turbine_bind_addr = "0.0.0.0:8001"
# lightbringer_endpoint = "localhost:9000"
# turbine_bind_addr = "0.0.0.0:8001"

# [turbine]
# bind_addr = "0.0.0.0:8001"
# gossip_entrypoint = "1.2.3.4:8000"
[turbine]
gossip_entrypoint = "" # REQUIRED for turbine: a gossip entrypoint of your Alpenglow cluster
# gossip_bind_addr = "0.0.0.0:65401"
# advertised_ip = "203.0.113.10"
# shred_version = 0
Expand All @@ -164,18 +220,7 @@ source = "rpc" # "rpc" | "lightbringer" | "turbine"
[tuning]
txpar = 24 # Recommended: 2x your CPU core count

[validator]
identity_keypair = "" # Optional validator identity for native turbine gossip
vote_account_keypair = "" # Optional vote account keypair path for diagnostics/future voting
authorized_withdrawer_keypair = "" # Optional authorized withdrawer keypair path for diagnostics

[consensus]
mode = "classic" # "classic" | "alpenglow-observer" | "alpenglow"
alpenglow_observer_bind_addr = "" # Optional Votor QUIC listener for observer mode
alpenglow_max_message_bytes = 0 # 0 = default
unresolved_policy = "halt" # "halt" | "warn"
skip_path_max_depth = 64
enforce_on_source = "stream"
%s

[rpc]
port = 8899 # Mithril's RPC server (binds to all interfaces)
Expand All @@ -185,11 +230,11 @@ dir = %q # Log files (created if missing)
level = "info" # "debug" | "info" | "warn" | "error"
to_stdout = true # Also write to stdout
max_size_mb = 100 # Max log file size before rotation
max_age_days = 7 # Delete logs older than this
# max_age_days = 0 # Delete logs older than N days (0/unset = never delete by age)

# Advanced options (defaults work well for most setups)
# See config.example.toml for: [tuning], [debug], [snapshot], [reporting]
`, s.Accounts, s.Shredstore, s.Snapshots, s.Logs, s.Logs)
`, s.Accounts, s.Shredstore, s.Snapshots, s.Logs, nodeSections, s.Logs)
}

// runConfigSet updates a key in the config file
Expand Down
17 changes: 9 additions & 8 deletions cmd/mithril/configcmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ type editModel struct {
func newEditModel(cf string, v *viper.Viper) editModel {
cluster := v.GetString("network.cluster")
if cluster == "" {
cluster = "mainnet-beta"
cluster = "alpenglow" // the only cluster this build boots
}
rpcSlice := v.GetStringSlice("network.rpc")
rpcEndpoint := ""
Expand Down Expand Up @@ -275,10 +275,10 @@ func (m editModel) currentItems() []edItem {
}
case edScrCluster:
return []edItem{
{label: "mainnet-beta", value: "mainnet-beta"},
{label: "testnet", value: "testnet"},
{label: "devnet", value: "devnet"},
{label: "alpenglow", value: "alpenglow"},
{label: "alpenglow", value: "alpenglow", desc: "The only cluster this build boots"},
{label: "mainnet-beta", value: "mainnet-beta", desc: "Requires a dev-branch (TowerBFT) build"},
{label: "testnet", value: "testnet", desc: "Requires a dev-branch (TowerBFT) build"},
{label: "devnet", value: "devnet", desc: "Requires a dev-branch (TowerBFT) build"},
{isSep: true},
{label: "← Back", value: "_back"},
}
Expand Down Expand Up @@ -675,10 +675,11 @@ func (m *editModel) saveConfig() {
content = setTomlValue(content, "lightbringer", "quiet", "false")
}
} else {
// Only force block.source="rpc" if no external lightbringer_endpoint is configured.
// External LB mode (enabled=false + endpoint set) is a valid runtime config.
// Only force a source change if no external lightbringer_endpoint is
// configured. External LB mode (enabled=false + endpoint set) is a valid
// runtime config. Turbine is the default live Alpenglow source.
if m.v.GetString("block.lightbringer_endpoint") == "" {
content = setTomlValue(content, "block", "source", "\"rpc\"")
content = setTomlValue(content, "block", "source", "\"turbine\"")
}
if strings.Contains(content, "[lightbringer]") {
content = setTomlValue(content, "lightbringer", "enabled", "false")
Expand Down
16 changes: 6 additions & 10 deletions cmd/mithril/dashboardcmd/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,17 +363,13 @@ func runDoctorChecks(configFile string, cfg *configData) []checkResult {
results = append(results, checkResult{"RPC endpoint", "fail", "no RPC endpoints configured"})
}

consensusMode := cfg.consensusMode
if consensusMode == "" {
consensusMode = "classic"
}
switch strings.ToLower(strings.TrimSpace(consensusMode)) {
case "classic", "legacy", "alpenglow-observer":
results = append(results, checkResult{"Consensus", "pass", consensusMode})
case "alpenglow":
results = append(results, checkResult{"Consensus", "warn", "alpenglow voting mode is not implemented yet"})
switch cfg.consensusMode {
case "", "verifying":
results = append(results, checkResult{"Node mode", "pass", "verifying (non-voting)"})
case "validator":
results = append(results, checkResult{"Node mode", "pass", "validator (voting engine not yet active)"})
default:
results = append(results, checkResult{"Consensus", "fail", "invalid mode: " + consensusMode})
results = append(results, checkResult{"Node mode", "fail", "invalid consensus.mode: " + cfg.consensusMode})
}
if cfg.alpenglowBindAddr != "" {
if _, _, err := net.SplitHostPort(cfg.alpenglowBindAddr); err != nil {
Expand Down
Loading
Loading