09 Distributed Consensus & Task Allocation · #42 of 46

When Robots Lie

Reaching agreement when some of the voters are adversaries

Every consensus algorithm you've met so far quietly assumed everyone tells the truth. Cut that assumption, and a single robot whispering nonsense can poison the agreement of a thousand honest ones.

The fix isn't to detect the liar. It's to build a vote that doesn't care whether one exists.

A failed robot is easy. It goes dark, stops broadcasting, and the rest of the swarm routes around the silence. The dangerous failure is the one that keeps talking: the sensor that's drifted, the unit that's been spoofed, the node that has, for whatever reason, decided to broadcast a number that is confidently, fluently wrong. In the literature these are called Byzantine failures, and they are the reason robust consensus is its own field.

The liar at the table

Picture a handful of robots trying to agree on a single number: the temperature of a room, the bearing to a target, the price of a trade. The protocol is simple: tell your neighbors your value, listen to theirs, combine. Now drop one liar into the group who, instead of its true reading, broadcasts whatever it likes.

How you combine the numbers is suddenly the whole ballgame.

Picture it running

This track has live canvases elsewhere; here, run the simulation in your head and watch the failure modes appear.

The lesson lands in one sentence: robust aggregation functions ignore outliers, and that's the entire trick.

Where the limit comes from

The median's "fewer than half" tolerance is a heuristic. The hard theoretical floor is stricter and worth knowing.

The three combine rules, written out

Mean consensus, the standard, fragile update. Each agent ii averages over its neighborhood NiN_i:

xi1NijNixjx_i \leftarrow \frac{1}{|N_i|} \sum_{j \in N_i} x_j

A single malicious xjx_j pushed to infinity sends xix_i to infinity with it. Breakdown point: zero.

Median consensus: replace the sum with the order statistic:

ximedian({xj:jNi})x_i \leftarrow \operatorname{median}(\{x_j : j \in N_i\})

Tolerates corruption of up to half the values in NiN_i before the result can be moved.

Trimmed mean: drop the ff highest and ff lowest readings, then average the survivors:

xi1Ni2fjtrimmed(Ni)xjx_i \leftarrow \frac{1}{|N_i| - 2f} \sum_{j \in \operatorname{trimmed}(N_i)} x_j

where ff is the number of outliers trimmed from each tail. Choosing ff trades robustness (larger ff tolerates more liars) against resolution (larger ff discards more honest data).

Why three agents and one liar is already hopeless

The n>3fn > 3f bound feels arbitrary until you try to beat the smallest broken case: one commander and two lieutenants, with exactly one traitor among the three. Watch it fail.

Say the commander is the traitor. He tells lieutenant B "attack" and lieutenant C "retreat." Honest B and C compare notes, each forwarding what they heard. Now B holds two conflicting orders, "attack" (from the commander) and "retreat" (relayed from C), and has no way to tell whether the commander lied or whether C is the traitor inventing a relay. C is stuck in the mirror image. Neither can act on the truth, because from the inside the two stories are identical.

Bump it to four agents with one traitor, and the honest three can outvote the lie. That gap, three fails, four works, is the n>3fn > 3f line in miniature. Lamport, Shostak, and Pease proved the small case is the whole case: if you cannot solve one-commander-two-lieutenants, you cannot solve any group where liars reach a third.

Robust consensus in Rust
fn robust_consensus_step(agents: &mut [Agent], neighbors: &[Vec<usize>],
                        method: RobustMethod) {
    for i in 0..agents.len() {
        if agents[i].malicious { continue; } // honest agents only update honestly

        let values: Vec<f32> = neighbors[i].iter()
            .map(|&j| agents[j].value)
            .collect();

        agents[i].value = match method {
            RobustMethod::Mean => values.iter().sum::<f32>() / values.len() as f32,
            RobustMethod::Median => median(&values),
            RobustMethod::TrimmedMean(f) => trimmed_mean(&values, f),
        };
    }
}

A prompt to hand an LLM if you want to push past robust statistics into provable agreement:

"Implement Byzantine agreement algorithm:
- Requires >2/3 honest robots
- Uses voting and majority rule
- Test with varying malicious fractions
- Compare to median consensus"

Key takeaways

There is something quietly profound here. The robust swarm never identifies its traitor, never accuses anyone, never even asks who lied. It simply structures its decision so that no single liar can matter, and then proceeds, unbothered, toward the truth.

That same idea, scaled to a planet, is what keeps a blockchain honest: Byzantine fault tolerance running across thousands of strangers, none of whom trust each other, all of whom agree anyway. The deepest robustness isn't catching the liar. It's making the lie powerless.

full glossary →