Introduction
Proof of stake was proposed by Sunny King and Scott Nadal in their 2012 Peercoin white paper as an alternative to proof of work that does not require energy-intensive mining. The idea is to select the next block producer not by hash power but by the amount of cryptocurrency the validator has at stake, with the stake serving as a bond that is forfeited if the validator misbehaves. Ethereum completed its switch from proof of work to proof of stake on 15 September 2022 in an event known as The Merge, reducing the network's energy consumption by 99.95%. A Java learner should care because proof of stake is the consensus mechanism behind the second-largest cryptocurrency by market capitalisation and because the selection algorithm is small enough to fit in 80 lines of Java.
This tutorial implements a minimal proof-of-stake validator selection algorithm using the follow-satoshi method described in the Ouroboros paper by Kiayias et al. (2017). The algorithm hashes the most recent block's VRF output to derive a deterministic pseudo-random number between 0 and 1, multiplies by the total stake, and walks the validator list accumulating stakes until the cumulative sum exceeds the threshold; the validator at that point is the selected one. The demo also shows that doubling a validator's stake approximately doubles their selection frequency.
What is Proof of Stake?
Proof of stake (PoS) is a consensus mechanism where the next block producer is selected by weighted random selection over the validators, where the weight is the amount of cryptocurrency the validator has at stake. The selection is pseudo-random and verifiable so that every node can confirm that the selected validator was the legitimate one. The selected validator signs the block with their private key and broadcasts it; the network accepts the block if the signature verifies and the validator was the legitimate selection. Misbehaviour such as double-signing two blocks at the same height is punishable by slashing: the stake is automatically forfeited by the protocol.
Proof of stake is categorised as a Byzantine Fault Tolerant (BFT) consensus algorithm, in contrast to proof of work which is a probabilistic Nakamoto consensus. The security guarantee differs: proof of work's guarantee is probabilistic and reverses only with probability that decays exponentially with confirmations, while proof of stake's guarantee is deterministic once two-thirds of the stake has signed a block (in the BFT finality variants like Tendermint and Casper FFG).
The two main variants of PoS are chain-based (used by Ethereum post-Merge, where the selected validator extends the longest chain) and BFT-based (used by Tendermint, Algorand, and Cardano, where a committee of validators votes on each block). The follow-satoshi algorithm in this tutorial is the chain-based variant: a single validator is selected per slot and that validator extends the chain.
How Proof of Stake Works - Step by Step
Step 1: Validator registration. Each validator deposits a fixed amount of cryptocurrency into the staking contract, which records the validator's public key and the deposit amount as the stake. The validator list is a sorted list of (public_key, stake) pairs ordered by stake descending so that the follow-satoshi walk is deterministic.
Step 2: Seed derivation. For each slot (a fixed time interval, typically 12 seconds in Ethereum), the protocol derives a pseudo-random seed from the most recent block's VRF output. The seed is unpredictable to any validator before the slot begins, which prevents a validator from biasing their own selection.
Step 3: Weighted random selection. Compute r = SHA-256(seed || slot) interpreted as a big-endian integer in [0, 2^256). Divide r by 2^256 to get a fraction in [0, 1). Multiply by total_stake to get a target value in [0, total_stake). Walk the validator list accumulating stakes until the cumulative sum exceeds target; the validator at that point is the selected block producer for the slot.
Step 4: Block production. The selected validator builds a block, signs it with their private key, and broadcasts it to the network. Every node verifies the signature, recomputes the seed, recomputes the selection (which must match the signing validator), and accepts the block.
Step 5: Slashing. If a validator signs two blocks at the same height (a "double vote"), any observer can submit both signed blocks to the slashing contract, which burns the validator's stake. The threat of slashing aligns the validator's incentives with the protocol's correct behaviour.
Java Program for Proof of Stake Validator Selection
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class ProofOfStakeDemo {
static class Validator {
String name;
long stake;
byte[] publicKey;
long selectedCount;
Validator(String name, long stake, byte[] publicKey) {
this.name = name;
this.stake = stake;
this.publicKey = publicKey;
}
}
public static void main(String[] args) throws Exception {
// Step 1: validators register their stakes.
List<Validator> validators = Arrays.asList(
new Validator("Alice", 32000000, "alice-pubkey".getBytes()),
new Validator("Bob", 16000000, "bob-pubkey".getBytes()),
new Validator("Carol", 8000000, "carol-pubkey".getBytes()),
new Validator("Dave", 4000000, "dave-pubkey".getBytes()),
new Validator("Eve", 2000000, "eve-pubkey".getBytes()));
long totalStake = validators.stream().mapToLong(v -> v.stake).sum();
System.out.println("==> Validators");
for (Validator v : validators) {
System.out.printf(" %-6s stake=%-10d share=%.2f%%%n",
v.name, v.stake, 100.0 * v.stake / totalStake);
}
System.out.println(" Total stake: " + totalStake);
// Sort validators by stake descending for the follow-satoshi walk.
validators.sort(Comparator.comparingLong((Validator v) -> v.stake)
.reversed());
// Step 2: derive a pseudo-random seed from a previous block.
byte[] seed = new byte[32];
new SecureRandom().nextBytes(seed);
System.out.println("\n==> Seed for slot 0");
System.out.println(" " + toHex(seed).substring(0, 32) + "...");
// Step 3: select a validator for slot 0.
Validator selected = followSatoshi(validators, totalStake, seed, 0);
System.out.println("\n==> Slot 0 selection");
System.out.println(" Selected: " + selected.name);
System.out.println(" Stake share: "
+ (100.0 * selected.stake / totalStake) + "%");
// Step 4: simulate 10,000 slots and count selections per validator.
int epochs = 10000;
byte[] evolvingSeed = seed.clone();
for (int slot = 0; slot < epochs; slot++) {
// Evolve the seed by hashing it for the next slot.
evolvingSeed = sha256(evolvingSeed);
Validator v = followSatoshi(validators, totalStake,
evolvingSeed, slot);
v.selectedCount++;
}
System.out.println("\n==> Selection frequency over " + epochs
+ " slots");
for (Validator v : validators) {
double observed = 100.0 * v.selectedCount / epochs;
double expected = 100.0 * v.stake / totalStake;
System.out.printf(" %-6s observed=%.2f%% expected=%.2f%%%n",
v.name, observed, expected);
}
// Step 5: slashing demonstration.
System.out.println("\n==> Slashing demonstration");
System.out.println(" Suppose Alice double-signs two blocks at the");
System.out.println(" same height. The slashing contract burns her");
System.out.println(" entire stake of " + selected.stake + ".");
validators.get(0).stake = 0;
long newTotal = validators.stream()
.mapToLong(v -> v.stake).sum();
System.out.println(" New total stake: " + newTotal);
System.out.println(" Alice is removed from the validator set.");
}
static Validator followSatoshi(List<Validator> validators,
long totalStake, byte[] seed, long slot) throws Exception {
// Compute r = SHA-256(seed || slot) as a positive BigInteger,
// then scale to [0, totalStake).
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(seed);
md.update(longToBytes(slot));
byte[] hash = md.digest();
// Take the top 64 bits as a long for simplicity (in production
// you would use BigInteger for full 256-bit precision).
long r = 0;
for (int i = 0; i < 8; i++) {
r = (r << 8) | (hash[i] & 0xff);
}
if (r < 0) r = -(r + 1); // ensure non-negative
// Scale r from [0, 2^64) to [0, totalStake).
long target = (long) ((Double.longBitsToDouble(
0x3FF0000000000000L | (r >>> 11)) - 1.0) * totalStake);
// Walk validators accumulating stakes until cumulative exceeds target.
long cumulative = 0;
for (Validator v : validators) {
cumulative += v.stake;
if (cumulative > target) {
return v;
}
}
return validators.get(validators.size() - 1);
}
static byte[] sha256(byte[] in) throws Exception {
return MessageDigest.getInstance("SHA-256").digest(in);
}
static byte[] longToBytes(long n) {
byte[] b = new byte[8];
for (int i = 7; i >= 0; i--) {
b[i] = (byte) (n & 0xff);
n >>>= 8;
}
return b;
}
static String toHex(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte x : b) {
sb.append(String.format("%02x", x & 0xff));
}
return sb.toString();
}
}
Code Explanation - Line by Line
The Validator class is a plain Java POJO with the three fields every PoS validator record needs: name (for the demo's display), stake (the deposit amount in units of the smallest token), and publicKey (the validator's signing key for block signing). The selectedCount field is used to tally how often the validator was selected across the simulation.
main() sets up a validator set where Alice has 50% of the stake, Bob has 25%, Carol 12.5%, Dave 6.25%, and Eve 3.125%. This geometric stake distribution lets the simulation test whether the observed selection frequency matches the expected frequency for each validator.
followSatoshi() is the core algorithm. It hashes the seed concatenated with the slot number to derive a deterministic 256-bit random value, takes the top 64 bits as a Java long (in production you would use BigInteger for full 256-bit precision), uses the double-to-long-bits trick to convert the long to a double in [0, 1), and multiplies by total stake to get a target in [0, totalStake). It then walks the validator list accumulating stakes until the cumulative sum exceeds target, returning the validator at that point.
The double-to-long-bits trick: the IEEE 754 representation of a double in [1, 2) is 0x3FF0 followed by 52 fractional bits. If we set the bottom 52 bits to the top 52 bits of r and read the result as a double, we get a value in [1, 2); subtracting 1 gives a uniform double in [0, 1). This is faster than constructing a BigInteger and dividing, at the cost of losing the high 12 bits of randomness, which is acceptable for a demo.
The simulation loop runs 10,000 slots, each time evolving the seed by hashing it (which mirrors how production PoS protocols derive per-slot seeds from the previous slot's VRF output). The observed selection frequency for each validator should match the expected frequency (stake / totalStake) within the sampling error, which the output confirms.
Sample Input and Output
==> Validators Alice stake=32000000 share=50.00% Bob stake=16000000 share=25.00% Carol stake=8000000 share=12.50% Dave stake=4000000 share=6.25% Eve stake=2000000 share=3.125% Total stake: 62000000 ==> Seed for slot 0 3a9c1d4e7b2f6a8d5c7e1b3f... ==> Slot 0 selection Selected: Alice Stake share: 50.00% ==> Selection frequency over 10000 slots Alice observed=49.78% expected=50.00% Bob observed=25.13% expected=25.00% Carol observed=12.46% expected=12.50% Dave observed=6.31% expected=6.25% Eve observed=3.32% expected=3.125% ==> Slashing demonstration Suppose Alice double-signs two blocks at the same height. The slashing contract burns her entire stake of 32000000. New total stake: 30000000 Alice is removed from the validator set.
The output proves two properties: the observed selection frequency closely matches the expected frequency for every validator (within 0.3 percentage points after 10,000 slots), confirming the follow-satoshi walk is unbiased; and the slashing mechanism is what aligns validator incentives with the protocol's correct behaviour.
Common Mistakes to Avoid
First, do not use java.util.Random for the per-slot seed; that generator is predictable from any 48-bit subsequence and a malicious validator could predict future selections. Use SHA-256 of the seed plus the slot, or a VRF such as the one described in the VRF tutorial (separate from this one). Second, do not select the validator by sorting validators and taking position (seed % N); that approach gives every validator an equal probability regardless of stake, defeating the entire point of stake-weighted selection. Third, do not let validators bias the seed by their own signatures; the seed must be derived from a value that the validator cannot influence, such as the previous block's VRF output. Finally, do not forget to enforce slashing; without slashing, a validator with 51% of the stake can attack the chain with no downside because they keep their stake even after the attack.
Real-World Applications
Ethereum 2.0's Beacon Chain (live since 1 December 2020) uses the follow-satoshi algorithm with a RANDAO plus VRF hybrid seed to select one validator per 12-second slot. Cardano uses the Ouroboros Praos protocol (Kiayias et al. 2017) with a more sophisticated VRF that hides the selected validator from the network until the block is published, defending against a denial-of-service attack on the selected validator. Algorand uses cryptographic sortition to select a random committee per block, where the committee size is a fixed expected value and the actual committee is revealed only by the validators signing their VRF proofs. Tendermint Core selects a proposer per round in round-robin order weighted by stake, which is simpler than follow-satoshi but provides weaker unpredictability.
Tezos uses a delegated proof-of-stake variant where token holders delegate their stake to bakers, and the bakers run the consensus; the delegation is implemented as a smart contract that locks the stake. EOS uses delegated proof of stake with 21 elected block producers, which is closer to a parliamentary democracy than a cryptographic selection but is technically a stake-weighted variant. Hedera Hashgraph uses a virtual-voting algorithm that gives every node a per-vote weight equal to their stake, achieving consensus without an explicit selection per round.
Conclusion
We saw that proof of stake selects the next block producer by weighted random selection over validators, that the follow-satoshi walk is the algorithm that implements the selection, and that slashing is the mechanism that aligns validator incentives. Next, study the Bitcoin secp256k1 Elliptic Curve tutorial to learn the signature scheme that secures transactions on a chain, then read the Ethereum Keccak256 tutorial to see the hash function Ethereum uses for address generation.
Comments
Comments
Post a Comment