Author: Lucas Luhur Last updated: 2026-06-25


Open question:

Pareto shape parameter k (stake inequality). Currently fixed at k=2. Two alternatives:

Pareto k analysis


1. Summary

The consensus message generator is the first module in the agreed build order (consensus → broadcast network → adversaries → anonymity measures). It treats consensus as a random message generator: stake is distributed across nodes, nodes run the stake-proportional leader election each slot, and every win is a block-proposal broadcast. The output is a stream of (slot, node) broadcast events — the input the broadcast-network and adversary modules consume downstream.

A validation harness that simulates the election over a full epoch and checks the empirical statistics against the closed-form formulas is also implemented.

Parameters used

Parameter Symbol Meaning Value
Active-slots coefficient $f$ per-slot win rate → target block time $1/f$ $1/30$
Epoch length $T$ slots per epoch = stake-inference observation window $388{,}800$
Nodes $N$ number of stakeholders $1000$
Pareto shape $k$ stake inequality $2$ (baseline)
Pareto scale $x_m$ cancels after normalisation $1.0$
Seed reproducibility (set at entry point) $0$

2. Stake distribution

Each node $i$ has weight $w_i > 0$; the election uses the relative stake ****$\alpha_i = w_i / \sum_j w_j$, with $\sum_i \alpha_i = 1$. Raw weights are drawn from a Pareto distribution and normalised (Arnold, 2015, p.41):

$$ w_i \sim \mathrm{Pareto}(k, x_m), \qquad \alpha_i = \frac{w_i}{\sum_j w_j}. $$

Stake is quenched disorder — sampled once and held fixed for the experiment, while the per-slot election noise varies on top of it.

def sample_relative_stakes(N, shape=DEFAULT_SHAPE, scale=1.0, rng=None):
    rng = np.random.default_rng(rng)
    w = pareto.rvs(b=shape, scale=scale, size=N, random_state=rng)
    return w / w.sum()       

Why Pareto: Real validator holdings are heavy-tailed. Pareto is the only pure power law, scale-free with a single tail exponent.

Measuring inequality: the Gini coefficient. gini(weights) is implemented to condense a whole stake vector into one interpretable scalar (0 = perfectly equal, → 1 = a single node owns everything), using the standard sorted formulation (Arnold, 2015, p.183:

$$ G = \frac{2\sum_i i\,x_{(i)}}{n\sum_i x_i} - \frac{n+1}{n}, \qquad x_{(i)} \text{ sorted ascending}. $$

def gini(weights):
    x = np.sort(np.asarray(weights, dtype=float))
    n, total = x.size, x.sum()
    idx = np.arange(1, n + 1)
    return float(2.0 * np.sum(idx * x) / (n * total) - (n + 1.0) / n)