Prover Guide
Shell-Chain supports asynchronous STARK proof generation. Blocks are produced
and broadcast immediately (native Dilithium3 signature verification), and a
ProofAmendment is attached later by a prover node. This lets validators stay
responsive without waiting for expensive proof generation.
Table of Contents
- Node Roles
- How Proving Works
- ProverRegistry
- Running a Prover Node
- Running a ValidatorProver Node
- Configuration Reference
- ProofAmendment Lifecycle
- Monitoring
- Proof Challenge Mechanism
Node Roles
Shell-Chain defines three operational roles:
| Role | CLI value | Block production | Generates proofs | Description |
|---|---|---|---|---|
| Validator | validator (default) |
✅ | ❌ | Standard authority node. Signs and proposes blocks. Does not run the prover. |
| ValidatorProver | validator-prover |
✅ | ✅ (idle slots) | Authority node that also runs the prover service during idle (non-proposing) slots. Useful for small networks where each validator can contribute proof work. |
| Prover | prover |
❌ | ✅ (full time) | Standalone prover — syncs the chain, generates proofs for every incoming block, broadcasts ProofAmendment via P2P. No block-signing key required. |
Set the role via TOML or CLI:
# config.toml
[node]
node_role = "prover"
shell-node run --node-role prover --config config.toml
How Proving Works
Block N sealed by validator
│
▼
Broadcast to network (contains Dilithium3 sigs in WitnessBundle)
│
├──► Peers: verify natively with TxWitness signatures (instant)
│
└──► Prover node receives block
│
▼
prove_sig_batch(entries) → SigBatchProof ← Winterfell STARK
│
▼
Wrap in ProofAmendment { block_hash, block_number, proof, prover, prover_sig }
│
▼
P2P broadcast → peers store at pa/<block_hash>
│
▼
(if proof_replacement_grace == 0) delete w/<block_hash>
← WitnessBundle freed (~4 KB/tx saved)
ProverRegistry
The native ProverRegistry stores governance-approved proof signing identities in canonical chain state, separately from validator membership. Registration grants no block-production or consensus-voting rights.
Version and activation: this interface is an unreleased source feature. It
requires a node build containing shell_proposeRegisterProver and an explicitly
configured prover_registry_height. The default is disabled. Existing databases
accept only a future activation height, and a saved schedule cannot be changed.
All peers must use the same schedule; snapshots must match trusted configuration.
Do not set height zero on an existing network. Before activation, historical
proof admission and settlement rules remain unchanged. At and after activation,
proof admission and settlement require a registered prover, including proofs
from validator-prover nodes.
Registering a prover
First generate the proof signing key as shown below. Submit only its public key and algorithm ID to a funded validator's authenticated governance RPC. Each validator votes through its own node; registration completes when votes represent strictly more than half of the current Registry weight. Repeating a vote does not add weight. No prior transaction from the prover account is required.
For the Dilithium3 key generated in this guide (algorithm = 0), with jq and
curl installed:
PROVER_PUBLIC_KEY=$(jq -r '.public_key' /data/prover-keystore.json)
PROVER_ADDRESS=$(jq -r '.address' /data/prover-keystore.json)
VALIDATOR_RPC=http://127.0.0.1:8545
REQUEST=$(jq -nc --arg key "$PROVER_PUBLIC_KEY" \
'{jsonrpc:"2.0",id:1,method:"shell_proposeRegisterProver",params:[$key,0]}')
curl -sS -H 'Content-Type: application/json' --data "$REQUEST" "$VALIDATOR_RPC"
The result is a transaction hash. Query eth_getTransactionReceipt with that
hash and require status: "0x1". A successful vote receipt alone does not prove
quorum: query the record and wait for its registered_at block to be finalized
(eth_getBlockByNumber with ["finalized", false]) before starting the prover.
REQUEST=$(jq -nc --arg address "$PROVER_ADDRESS" \
'{jsonrpc:"2.0",id:2,method:"shell_getRegisteredProver",params:[$address]}')
curl -sS -H 'Content-Type: application/json' --data "$REQUEST" "$VALIDATOR_RPC"
The query returns null until registration is committed. Match pubkey and
algorithm to your key and check that shell_getValidators is unchanged.
ML-DSA-65 uses algorithm ID 1; SPHINCS+-SHA2-256f uses 2. Addresses are derived
as blake3(algo_id || pubkey), rendered as 0x plus 64 lowercase hex characters.
Malformed keys, unsupported algorithms, non-validator callers, and duplicate
registrations are rejected. shell_proposeAddValidator changes consensus
membership and is not the prover-registration operation.
When the validator RPC listener is non-loopback, the node must be configured
with --rpc-api-key; add Authorization: Bearer <key> to the requests above.
Keep the prover's encrypted key and password on the prover machine.
ProverRecord fields
| Field | Description |
|---|---|
address |
Prover's full 32-byte address |
pubkey |
Public key bound to this proof signing identity |
algorithm |
Signature algorithm ID: 0, 1, or 2 |
registered_at |
Block height where governance reached quorum |
proofs_submitted |
Successfully executed proof settlements since registration |
last_proof_block |
Highest source block covered by those settlements |
Receiving or storing a proof does not increment the count. Settlement updates share the block's state root, roll back with rejected blocks or failed execution, and persist through restart and snapshot recovery. Import and replay use the registry state of the block being executed.
Running a Prover Node
A prover node does not need a validator identity. It uses a separate prover keystore to sign proof amendments and does not propose or vote on blocks. It needs:
- A PQ key for signing
ProofAmendmentmessages - Registration in
ProverRegistry - Access to a synced peer (or full P2P participation)
Step 1: Generate a prover key
shell-node key generate --algorithm dilithium3 --output /data/prover-keystore.json
# Enter passphrase when prompted
Step 2: Config
# prover.toml
[node]
datadir = "/data/prover"
chain_id = 31337
node_role = "prover"
keystore = "/data/prover-keystore.json" # proof signing only; not a validator key
[rpc]
listen_addr = "127.0.0.1:8545" # optional — prover nodes rarely need RPC
[p2p]
enabled = true
listen_addr = "0.0.0.0:30303"
bootnodes = ["/ip4/VALIDATOR_IP/tcp/30303/p2p/QmVALIDATOR..."]
[consensus]
enable_stark_aggregation = true
[prover]
max_concurrent_proofs = 2 # default 1; increase for multi-core machines
proving_priority = "sequential" # "sequential" or "latest-first"
[metrics]
enabled = true
listen_addr = "0.0.0.0:9090"
Step 3: Start
shell-node run --config prover.toml
Enter the passphrase for the prover keystore generated in Step 1. The
[node].keystore setting selects the proof-signing identity; generating a key
alone does not configure it. If omitted, the node creates or loads its development
identity instead, which has a different address from the generated prover key.
Keep node_role = "prover" so loading the key does not enable block production.
Running a ValidatorProver Node
A validator-prover node runs both roles. The prover service starts
automatically during slots where this node is not the block proposer.
[node]
node_role = "validator-prover"
keystore = "/data/keystore.json"
[consensus]
enable_stark_aggregation = true
[prover]
max_concurrent_proofs = 1 # conservative on validator hardware
proving_priority = "sequential"
Tip: On dedicated proving hardware, set
max_concurrent_proofsto the number of physical CPU cores. The Winterfell STARK prover is CPU-bound and benefits from parallelism.
Configuration Reference
[node]
node_role = "validator" # "validator" | "validator-prover" | "prover"
[consensus]
enable_stark_aggregation = false # set true to enable ProofAmendment flow
[prover]
max_concurrent_proofs = 1 # parallel proof jobs
proving_priority = "sequential" # "sequential" (oldest first) | "latest-first"
[pruning]
proof_replacement_grace = 0 # blocks to keep WitnessBundle after proof arrival
# 0 = delete immediately (default, recommended)
max_concurrent_proofs
Set a positive integer (default 1). Zero, negative and non-integer values are
rejected at configuration load. On builds containing the concurrent prover
implementation, this bounds active CPU proof jobs and completed jobs awaiting
ordered delivery. A later range may finish first, but persistence and event-loop
handoff remain in canonical source order. Source reservations stay active until
handoff is acknowledged. Graceful shutdown stops admission and drains active
proofs to storage for recovery without waiting for a full handoff channel.
This is an unreleased implementation change; verify the node build before assuming that an older binary honors this setting.
proving_priority
| Value | Behaviour |
|---|---|
sequential |
Prove blocks in ascending order. Preferred for archival integrity. |
latest-first |
Compute newer eligible ranges first within a bounded contiguous window. Results are persisted and submitted in source order. |
The latest-first scheduler reserves up to twice max_concurrent_proofs eligible
proof ranges from the canonical frontier. At most max_concurrent_proofs CPU
workers take ranges from the newest end of that window. The entire window drains
before more work is admitted, so new arrivals cannot indefinitely postpone older
reserved ranges. Gaps and below-threshold ranges remain queued under the same
strict settlement rules. Even with one worker, newer ranges in the window are
computed first; proof publication still waits for their predecessors.
Priority scheduling is an unreleased implementation change. Older binaries may parse the setting without changing the actual proving order.
ProofAmendment Lifecycle
prove_sig_batch()
│
└─► ProofAmendment {
version: 1,
block_hash,
block_number,
proof: SigBatchProof {
batch_root_bytes: [u8; 16], // final accumulator (STARK public output)
n_sigs: usize, // number of signatures covered
proof_bytes: Vec<u8>, // raw Winterfell STARK proof
},
prover: Address,
prover_signature: Bytes, // PQ sig over sha3(b"proof-amendment" ‖ block_hash ‖ block_number ‖ batch_root)
}
On receipt, the network:
- Checks
proveris inProverRegistry - Verifies
prover_signaturewith the PQ public key bound to the registered address - Verifies the
SigBatchProof(Winterfell STARK verification) - Stores at
pa/<block_hash> - Deletes
w/<block_hash>(after grace window)
Monitoring
Scrape the configured metrics listener (port 9090 by default):
curl --fail --silent http://127.0.0.1:9090/metrics
Use these exact Prometheus series names:
# Locally generated L1 proofs delivered to the node event loop (process lifetime)
shell_stark_proofs_generated_total
# CPU proof-attempt time, excluding queueing and ordered publication waits
shell_stark_proof_duration_seconds_count
shell_stark_proof_duration_seconds_sum
shell_stark_proof_duration_seconds_bucket{le="1"}
# Failed proof jobs; they do not increment the successful-proof counter
shell_stark_proof_failures_total
# Queued proof tasks, including below-threshold work; excludes reserved jobs
shell_stark_backlog_depth
# Generated proofs awaiting canonical settlement
shell_stark_pending_settlements
# Authenticated amendments rejected by admission limits
shell_stark_amendments_rate_limited_total
# Cached storage byte estimates, refreshed every 300 seconds
shell_storage_cf_size_bytes{cf="witness"}
shell_storage_cf_size_bytes{cf="proof"}
# Local production accepts settlements; imported settlements do not increment this counter
shell_stark_settlements_accepted_total
# Amendments rejected during validation
shell_stark_settlements_rejected_total
# Canonical source blocks not yet settled at L1
shell_stark_frontier_lag
Counters and histogram samples reset when the process restarts; durable prover
registration counts are queried separately with shell_getRegisteredProver.
A generated proof is not necessarily accepted into the chain: check its settlement
transaction receipt and the canonical prover record as well. The duration histogram
records successful and failed CPU attempts. Waiting for enough entries or a missing
source does not produce a duration sample or a proof failure.
Storage estimates include keys and values under the witness and proof prefixes. Proof size varies with the source range and encoding; it is not a fixed number of bytes per block. Witness retention and the settled frontier govern pruning, so a nonzero witness estimate alone does not indicate a failure.
The generation-duration, failure-count and live-backlog updates are an unreleased
implementation change. Older binaries expose these series but may leave them at
zero. Previous guide examples using shell_prover_* or unprefixed settlement
names did not match the exported series.
Proof generation remains paused while the node is synchronizing or otherwise
fails its readiness gate. A prover keeps at most two generated amendments in
flight until canonical settlement releases capacity. Sustained growth in
shell_stark_amendments_rate_limited_total, especially together with a stalled
chain head, indicates peer pressure or a settlement-path failure and should be
investigated before increasing any limits.
Durable Settlement Index (v0.22+)
Shell-Chain v0.22.0 adds a persistent SettledSourceIndex that stores accepted
(layer, source_hash) pairs in the KV store (prefix ss/). This replaces the
previous in-memory rebuild that required a full chain scan on every restart:
- Fast-path startup: index is loaded directly — typically instant.
- Slow-path / repair: if the index is empty (first run after upgrade or corruption), the node scans the chain once and backfills the index automatically.
- Durable rejection: duplicate STARK settlements are correctly rejected even after a restart or crash.
No operator action is required — the index is built transparently on first startup after upgrading to v0.22.0.
Proof Challenge Mechanism
If a node receives a ProofAmendment it cannot verify, it may broadcast a
ProofChallenge to the network:
The following Rust example constructs the message using the source API. In a
shell-chain source checkout, save it as
crates/consensus/examples/challenge-message.rs (create examples if needed),
then run cargo run -p shell-consensus --example challenge-message.
It prints the serialized message; constructing it does not broadcast a challenge
or prove that a node accepts it.
use shell_consensus::{ChallengeReason, ProofChallenge};
use shell_primitives::{Address, ShellHash};
fn main() {
let challenge = ProofChallenge::new(
ShellHash::from([0x11; 32]), // replace with the challenged block hash
42, // that block's number
ChallengeReason::VerificationFailed,
Address::from([0x22; 32]), // replace with the challenger's address
1, // sequence supplied by the sender
);
println!("{}", serde_json::to_string(&challenge).unwrap());
}
The message includes block_hash, block_number, reason, challenger and
sequence. Supported reasons are VerificationFailed, BlockNotFound,
MalformedPayload and StateRootMismatch. Batch-root, signature and registration
checks are validation conditions, not additional ChallengeReason variants.
Any node holding the raw proof can respond with a ChallengeResponse carrying
the proof bytes, allowing the challenger to retry verification. Challenges are
rate-limited per-peer to prevent DoS. A prover that accumulates failed
challenges may be removed from ProverRegistry.
Incoming challenges use a separate token bucket per network peer: ten initial
requests, refilling five tokens every 60 seconds up to the initial capacity.
Changing the claimed challenger address does not create another allowance.
Excess requests are dropped before tracking the challenge or reading and sending
proof data; they do not consume the prover's settlement-submission allowance.
Idle peer buckets expire after 600 seconds. These limits reset on process restart.
This receive-side enforcement is an unreleased implementation change; older
binaries do not apply this challenge limit.
On builds containing the challenge recovery fix, restarting with the same node database preserves open challenges, their original opening heights and resolved or timed-out states. The timeout remains 7200 blocks from the original opening height. Timeout status and its consensus penalty are saved together; failed writes leave both unchanged. Recovery restores the saved penalty without applying another percentage reduction, including after terminal records are collected. Later equivocation penalties update the same snapshot. Head rewinds do not reopen resolved challenges or reset their deadlines. A malformed snapshot stops network startup instead of silently discarding challenge history.
This recovery behavior is unreleased. Verify that the node build contains the fix before relying on it; upgrading cannot recover records already lost by an older process. It is separate from the per-peer admission buckets above, which still reset on restart. Process-exit/reopen testing covers database WAL recovery, not machine power-loss durability.
Challenge responses carry a serialized ProofAmendment, including the full STARK
proof and the prover's authenticated signature envelope. The receiver checks the
signature and its binding to the prover address before validating the source range
or writing proof artifacts. When prover_registry_height is active at the next
block height, that prover must also be registered. A relay may forward another
prover's signed amendment; the claimed responder address is not a substitute
for proof authentication. Rejected responses leave the challenge open and do not
write any proof-range artifacts. A valid response for an unsettled range stores
the proof and resolves the challenge; this alone does not settle a reward or
increment registration counts.
This response-admission correction is unreleased; verify the node build before
relying on this behavior in an older binary.
For a challenge to an earlier block in a stored proof range, a prover resolves the
local proof pointer and returns the complete signed amendment for that range.
The response retains the challenged block hash; the receiver checks that the
amendment covers that hash before authentication and source verification. Missing
or inconsistent pointer targets do not produce a response. This range-response
correction is also unreleased; the stored, unsettled range flow is covered by
cargo test -p shell-node event_loop_authenticates_challenge_responses_before_storage.
For an already settled range, a response still requires prover admission,
canonical range checks, and proof verification. When every covered source is
already settled at that layer, a valid response resolves the challenge without
rewriting proof artifacts, settlement links, or rewards. New or partially settled
ranges retain the normal settlement-order checks. This historical-response
correction is unreleased. If L1 witnesses have been pruned, the response's
settlement transaction link identifies the retained canonical StarkReward
payload. The node checks the containing block is still canonical and matches
the source range, original size, entry count, and batch root, then independently
verifies the received proof. Missing or noncanonical settlement links cannot
replace missing witnesses. This path does not rewrite settlement data or award
another reward. The regression above covers unpruned and pruned L1 ranges with
registry activation disabled; its shortened retention window is a test condition.
L2 responses and registry activation changes require separate verification.