Consensus Details

Shell Chain uses WPoA (Weighted Proof of Authority) as its current consensus model, paired with async STARK proof aggregation.


Table of Contents


WPoA Engine

Shell Chain's live WPoaEngine uses weighted authority selection with deterministic slot scheduling. A block is valid if:

  1. The proposer is in the current ValidatorSet.
  2. The block header authority field matches the proposer's address.
  3. All transactions have valid ML-DSA-65 signatures (or supported legacy/fallback PQ signatures) verified via WitnessBundle.
  4. The block timestamp is within the allowed drift window.
  5. State root and witness root match the executed result.

Demand-driven production and rewards

Testnet/mainnet production ticks every 2 seconds, but validators only build a normal block when executable transactions are available. When the mempool is idle, validators skip empty slots and produce only a 600-second heartbeat block. Heartbeat blocks carry no user transactions, consume zero gas, and do not pay block or STARK rewards.

For normal transaction blocks, the reward model is:

  • the block producer receives 100% of effective gas fees as a deterministic blockGasReward system transaction (Σ gas_used × effective_gas_price);
  • L1 STARK settlement receives a mint-only reward of 100 SHELL / 2^1 × source_count (no gas share); source_count = number of covered source blocks with ≥1 user tx (min 1);
  • L2+ recursive STARK settlements receive 100 SHELL / 2^L × source_count (mint only).

Reward records are not fake signed transactions. They are first-class system transactions with deterministic hashes, receipts, block inclusion indexes, and address-history indexing.

Configuration

[consensus]
engine = "wpoa"
enable_stark_aggregation = false  # enable async STARK proofs (see PROVER_GUIDE.md)

Base authority scheduling

WPoaEngine builds on deterministic authority-slot scheduling. Validators accrue stake-weight over time, and the fork-choice rule favours the chain with highest cumulative weight rather than longest chain.

WPoaConfig fields:

Field Default Description
slot_duration_ms 2000 Slot length in milliseconds
min_validators 1 Minimum validators to produce blocks
max_missed_slots 10 Missed slots before offline detection

WPoA configuration:

[consensus]
engine = "wpoa"

Validator Set

The validator set is maintained in the ValidatorRegistry system contract (0x0000…0001) and the in-memory ValidatorSet. Changes take effect at the next epoch boundary.

ValidatorStatus

Status Description
Active Participating in block production
Pending Added via governance, not yet active
Suspended Temporarily suspended (missed slots)
Slashed Permanently removed due to misbehaviour

Epoch management

ValidatorSetConfig controls epoch length. At each epoch boundary:

  • Pending additions become active
  • Slashed validators are removed
  • Validator weights are recalculated

Finality

Shell Chain uses a threshold attestation model for finality on top of WPoA. Blocks become final when ≥ 2/3 of validators have attested to them via Attestation messages.

FinalityState tracks:

  • finalized_height — highest finalized block
  • attestation_counts — votes per block hash
  • quorum_threshold — ceil(2/3 × validator_count)

A block at height H is safe once ≥ 1/3 + 1 validators have attested. A block is finalized once ≥ 2/3 have attested.

RPC block tags map to these states:

Tag Meaning
latest Most recent sealed block
safe Block with ≥ 1/3+1 attestations
finalized Block with ≥ 2/3 attestations
pending Not yet sealed
earliest Genesis block

Fork Choice

ForkChoice assigns a BlockScore to each candidate chain head. The rule prefers the chain with:

  1. Highest finalized height (safety over liveness)
  2. Highest cumulative validator weight (WPoA tiebreak)
  3. Lowest block hash (deterministic last tiebreak)

If validator weights are equal, rule 2 degenerates to longest-chain.


Slashing

The slashing system detects two categories of misbehaviour:

Double-sign (equivocation)

Detected by detect_double_sign(h1, h2) — triggered when the same validator proposes two different blocks at the same height with different hashes.

SlashType::DoubleSign
SlashEvidence::Equivocation { h1: BlockHeader, h2: BlockHeader }

EquivocationProof can be broadcast by any node that observes two conflicting headers from the same authority.

Offline

Detected by detect_offline(addr, last_proposed, current_block, config) — triggered when a validator hasn't proposed a block for more than SlashingConfig::offline_threshold slots.

SlashType::Offline

SlashingConfig defaults

Field Default Description
offline_threshold 100 Slots without a proposal before offline detection
slash_on_double_sign true Slash immediately on equivocation
slash_on_offline false Offline triggers suspension, not slash (configurable)

SlashRecord

SlashRecord {
    validator: Address,
    slash_type: SlashType,       // DoubleSign | Offline
    evidence: SlashEvidence,
    block_height: u64,
    epoch: u64,
}

Slash records are written to ValidatorSet and change the validator's status to Slashed. The validator is removed at the next epoch boundary.


Proof Challenges

When enable_stark_aggregation = true, received ProofAmendment messages are verified by all peers. If verification fails, the peer broadcasts a ProofChallenge:

ChallengeReason

Value Description
VerificationFailed Winterfell STARK verification returned false
InvalidBatchRoot batch_root_bytes doesn't match expected public output
InvalidProverSignature Prover's PQ signature on the amendment is invalid
UnregisteredProver Prover address not in ProverRegistry

Rate limiting

Challenges are rate-limited per-challenger via ProofRateLimiter to prevent DoS. RateLimiterConfig sets:

  • max_challenges_per_window — max challenges in any rolling window
  • window_seconds — rolling window duration

A challenger that exceeds the limit has its challenges silently dropped by peers.

Challenge flow

Node A cannot verify ProofAmendment for block #N
  │
  └─► Broadcast ProofChallenge { block_hash: N, reason, challenger: A, sequence: k }
          │
          └─► Any peer holding proof bytes broadcasts:
                ChallengeResponse { block_hash: N, proof_bytes: [...] }
                      │
                      └─► Node A retries verification with raw proof bytes