# The Anatomy of a Fork: Simulating Slot Collisions in Substrate

**URL:** <https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514>\
**Category:** Tech Talk\
**Created:** [April 14, 2026, 2:00am UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514 "2026-04-14T02:00:27Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![Kanasjnr](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/kanasjnr/32/10899_2.png) [@Kanasjnr](https://forum.polkadot.network/u/Kanasjnr)\
**Post date:** [April 14, 2026, 2:00am UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/1 "2026-04-14T02:00:27Z")

</div>

Hi Polkadot community 👋

I’ve been deep in Substrate for a while now, but I kept running into the same wall: sure, I could use the SDK, spin up the node template, throw in some pallets, and even get a working chain out the door… but I still didn’t _really_ understand how the consensus stuff actually works under the hood.

I already know the theory. I just really wanted to **see it actually work** from first principles. So I stripped everything back and built a little Consensus Lab from scratch nothing fancy, no magic, just the exact same DNA as Substrate (SCALE codec + Blake3 hashing). Now I can literally watch every single byte move around in real time.

## Why the Black-Box Approach Wasn’t Enough

Most Substrate tutorials start with `substrate-node-template` and `cargo expand` on the macros. That’s great for speed, but terrible for intuition. You end up treating `sp_consensus_babe` and `sp_consensus_grandpa` like black boxes.

When you’re trying to build production logic or debug why your chain behaves strangely under network stress, a black box isn’t enough. I wanted to _feel_ the data flow, the determinism, the probabilistic lottery, and see exactly when and why a fork is born.

So I built a tiny discrete-event simulator in pure Rust. No Substrate runtime, no FRAME, no `#[pallet]`. Just the primitives that actually matter for consensus.

## Architecture

The project is deliberately structured like a miniature Polkadot SDK:

- **`primitives/`** — Zero-dependency protocol types (Header, Block, Hash, Extrinsic)
- **`core/`**
  - `runtime/` — State Transition Function (STF) + deterministic state roots
  - `consensus/` — BABE-style slot-based leadership + Longest-Chain fork selection
  - `node/` — Actor that handles block import, proposal, and re-orgs

- **`network/`** — Discrete-event message simulator with configurable propagation latency

Everything is built for maximum fidelity:

- Parity SCALE Codec everywhere
- Blake3 for hashing (same as Substrate)
- Deterministic state roots via sorted `BTreeMap` + SCALE encoding
- Probabilistic slot claiming (VRF-style approximation)

## The “Eureka” Moment: Slot Collisions in Real Time

I ran a 3-node simulation for 20 slots with ~33% slot-claim probability per validator (realistic BABE-style threshold) and 1-slot network latency.

Here’s exactly what the logs showed (real output):

```auto
[INFO] ---------------- Slot 1 ----------------
[INFO] [node_0] ⚡ Proposed block at height 1 (hash: d30c23..b9a2)

[INFO] ---------------- Slot 2 ----------------
[INFO] [node_1] ⚡ Proposed block at height 2 (hash: 6518e1..1d2f)
[INFO] [node_2] ⚡ Proposed block at height 2 (hash: e977a5..107e)

[INFO] ---------------- Slot 4 ----------------
[INFO] [node_0] ⚡ Proposed block at height 3 (hash: 5db3af..c748)
[INFO] [node_2] ⚡ Proposed block at height 3 (hash: 4c2d21..2fe7)

[INFO] ---------------- Slot 7 ----------------
[INFO] [node_0] ⚡ Proposed block at height 5 (hash: 99172d..1588)
[INFO] [node_2] ⚡ Proposed block at height 5 (hash: a2fd36..61b1)

... (more collisions in slots 9, 10, 12, 18, 20)

[INFO] Simulation complete.
[INFO] Node node_0 canonical head: 1803eb..c69d (Blocks discovered: 24)
[INFO] Node node_1 canonical head: 13fd3e..f8bf (Blocks discovered: 25)
[INFO] Node node_2 canonical head: 0eca66..24df (Blocks discovered: 25)

```

**This is the anatomy of a fork in action.**

In Slot 2, **two nodes both won the lottery** → two cryptographically valid blocks at the same height. Because of the 1-slot latency, they arrived out of order on different nodes. Forks were born instantly. The longest-chain rule kicked in independently on each node. By the end of 20 slots the network had produced **25 blocks** (instead of 20) and the nodes were still sitting on slightly different canonical heads.

In a real Substrate chain you’d never see this in the logs just a quiet re-org. In my terminaI i watched it in slow motion with full visibility into every node’s block DAG.

That single run taught me more about fork resolution than three months of reading the BABE paper.

## How the Core Pieces Actually Work

**Deterministic State Roots**

```rust
pub fn root(&self) -> Hash {
    let bytes = self.encode(); // BTreeMap is sorted → always identical
    Hash::from_bytes(blake3::hash(&bytes).into())
}

```

**Probabilistic Slot Claiming (BABE-style lottery)**

```rust
pub fn claim_slot(&self, slot: Slot, randomness: [u8; 32]) -> bool {
    let mut rng = ChaCha20Rng::from_seed(seed);
    let val: u64 = rng.gen();
    val < self.threshold
}

```

**Longest-Chain Fork Choice**

```rust
pub fn find_best_head<'a>(&self, headers: &'a [Header]) -> Option<&'a Header> {
    headers.iter().max_by_key(|h| (h.number, -(h.slot as i64)))
}

```

**Network Simulator** every block gets a configurable propagation delay so forks can actually stay alive.

## What This Taught Me

1. **Slot collisions aren’t bugs — they’re a feature** of probabilistic finality.
2. **Network latency is the real enemy** of fast convergence.
3. **Determinism is everything** — state roots must match on every node or the chain can never heal.
4. **Longest-chain heals forks, but it takes time** — and sometimes leaves temporary divergence.
5. **GRANDPA exists for a reason** — longest-chain alone is not enough for strong finality.

## What’s Next (still building live)

- Real gossip topology with neighbor meshes (no more instant broadcast)
- Configurable network partitions
- Upgrade to proper GHOST fork-choice
- Full VRF + signature verification
- GRANDPA-style finality gadget integration

## Why This Matters

If you’re serious about building on Polkadot/Substrate, understanding consensus at this level changes everything. You stop treating the chain as magic and start seeing it as a deterministic, probabilistic, eventually-consistent distributed system.

I’m still learning, still iterating, but this little lab has already given me more intuition than any tutorial ever could.

**Would love your feedback** especially if you’ve built similar minimal simulators or have ideas for the next experiments.

Drop a comment if you want the repo link (happy to share once it’s cleaned up a bit more). Or if you want to pair on the next iteration I’m all in.

Happy forking! 🔬

* * *

_All code is open for inspection and can be ported back into real pallets._

**Repo structure**

```auto
primitives/ → types, Header, Block, Hash
core/
  ├── runtime/ → STF + deterministic state roots
  ├── consensus/ → BABE lottery + fork choice
  └── node/ → block import, proposal, re-org logic
network/ → discrete-event latency simulator

```

## References and Further Reading

- **[BABE Paper](https://research.web3.foundation/en/latest/polkadot/BABE1)**

- **[GRANDPA Paper](https://research.web3.foundation/en/latest/polkadot/GRANDPA1)**

- **[Substrate Docs](https://docs.substrate.io)**

- **[Parity SCALE Codec](https://github.com/paritytech/parity-scale-codec)**

- **[Blake3 Hash Function](https://github.com/BLAKE3-team/BLAKE3)**

---

<div class="post-metadata">

**Author:** ![kukabi](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/kukabi/32/181_2.png) [@kukabi](https://forum.polkadot.network/u/kukabi)\
**Post date:** [April 14, 2026, 6:57am UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/2 "2026-04-14T06:57:32Z")

</div>

Great work. I also find it important to understand any technology I’m working with, down to the bits. So this is something I’ve thought of doing for quite some time. One note: I wasn’t able to find the link to your code base in your post.

---

<div class="post-metadata">

**Author:** ![Kanasjnr](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/kanasjnr/32/10899_2.png) [@Kanasjnr](https://forum.polkadot.network/u/Kanasjnr)\
**Post date:** [April 14, 2026, 7:54am UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/3 "2026-04-14T07:54:29Z")

</div>

Here

> **[GitHub - Kanasjnr/substrate-consensus-lab](https://github.com/Kanasjnr/substrate-consensus-lab)**
>
> Contribute to Kanasjnr/substrate-consensus-lab development by creating an account on GitHub.

---

<div class="post-metadata">

**Author:** ![kianenigma](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/kianenigma/32/6153_2.png) [@kianenigma](https://forum.polkadot.network/u/kianenigma)\
**Post date:** [April 14, 2026, 12:19pm UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/4 "2026-04-14T12:19:19Z")

</div>

I believe BABE which is currently being used always has a possibility of:

- 2 winners per slot =\> fork
- no winner per slot =\> why there is an Aura back up in place (called secondary slot)

You should check out how Sassafras is used in JAM. It addresses both of the issues + retains privacy of not knowing who the future block authors are.

> **[RFC-0026: Sassafras Consensus Protocol - Polkadot Fellowship RFCs](https://polkadot-fellows.github.io/RFCs/approved/0026-sassafras-consensus.html)**
>
> An online book of RFCs approved or proposed within the Polkadot Fellowship.

---

<div class="post-metadata">

**Author:** ![bkchr](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/bkchr/32/27_2.png) [@bkchr](https://forum.polkadot.network/u/bkchr)\
**Post date:** [April 14, 2026, 12:34pm UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/5 "2026-04-14T12:34:07Z")

</div>

> [@kianenigma](#):
>
> I believe BABE which is currently being used always has a possibility of:
> 
> - 2 winners per slot =\> fork
> - no winner per slot =\> why there is an Aura back up in place (called secondary slot)

You can actually have more than 2 winners per slot. Right now most of the forks are probably coming from the Aura fallback, because this means that there are at least always 2 forks being build when a primary author exists.

---

<div class="post-metadata">

**Author:** ![Kanasjnr](https://dub1.discourse-cdn.com/flex005/user_avatar/forum.polkadot.network/kanasjnr/32/10899_2.png) [@Kanasjnr](https://forum.polkadot.network/u/Kanasjnr)\
**Post date:** [April 14, 2026, 1:00pm UTC](https://forum.polkadot.network/t/the-anatomy-of-a-fork-simulating-slot-collisions-in-substrate/17514/6 "2026-04-14T13:00:29Z")

</div>

Kian, Bastien thanks for stopping by. It’s an honor to have the core team looking at this.

Bastien, to your point about standard tutorials, that’s exactly what drove me to build this. the “black box” nature of the macros is excellent for production speed, but it often masks the underlying distributed systems logic. This codebase is my attempt to **decouple the Consensus Mechanics from the FRAME machinery** so I can verify invariants in a controlled, bare-metal environment.

Kian, I’m actually iterating on this specifically to explore more advanced slot-leadership models. Some of you might remember my previous series on **[Sassafras](https://forum.polkadot.network/t/can-sassafras-make-polkadot-faster-and-safer)**.

The “Eureka” moment I had with these slot collisions really highlighted why Sassafras’s attempt to achieve **Single Secret Leader Election (SSLE)** is such a game-changer. My plan for Phase 3/4 is to move beyond the probabilistic BABE threshold and simulate a “Semi-Sassafras” model where we can see the impact of exact leadership on fork density during high network jitter.

**What’s currently in progress:**

- **Phase 2 Complete** : [Just finished the P2P Gossip Mesh and observed “Visibility Lag” across a line topology.](https://forum.polkadot.network/t/beyond-the-broadcast-simulating-p2p-gossip-and-visibility-lag/17517)
- **Next Up** : Simulating network partitions and then moving towards GHOST-lite fork choice.

The repo is now live for anyone who wants to audit my logic (and likely tell me where my SCALE encoding is slightly off!): [GitHub - Kanasjnr/substrate-consensus-lab · GitHub](https://github.com/Kanasjnr/substrate-consensus-lab)

Looking forward to hearing any specific experiments/edge cases the team would love to see simulated in a “bare-bones” environment like this.
