learn-web3-with-phoebe / Practitioner session 9 of 10
Learn Web3 with Phoebe · Practitioner session 9 of 10

The bench that fooled itself

Every number on the reorganisation side of this course comes from one bench, and an earlier version of that bench was wrong. It treated a finality lag and a retracting loader as the same thing, so any lag above zero also switched on deletion. The table it produced said a 1 block lag eliminated every wrong row against a 3 block reorganisation. That is false, and it would have taught something worse than nothing: that a token precaution is sufficient. Tonight is the fault, the reasoning that caught it, and the three habits that generalise well past this bench.

🔧 Practitioner track Anyone who writes the test that proves a control works Anyone who reads one 45 min
0-3 · Welcome 3-18 · The fault and the false table 18-30 · Why the tests passed 30-45 · Build-along and Q&A
Part 0

A result too clean to be about the world

Session 7 spent forty five minutes on the difference between holding a row back and taking a row back, because they cost opposite things and fail in different circumstances. The bench those numbers came from could not tell them apart. One parameter switched on both, and the sweep it produced looked better than the sweep that is true. Nothing errored, the code read correctly, and the self test passed. This session is what happened next.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. The false table and the real one side by side: 511, 0, 0, 0 against 511, 346, 172, 0. The measurement that makes this session worth a slot, which is that all three of the harness's own properties pass under the faulty code, and the fourth property that would have caught it in one line. Three habits: test a defence at a strength where it should fail, never put two mechanisms behind one parameter, and treat a result that is too clean as evidence about your harness rather than about the world.

LiveWhy a course session is being spent on a bug4 min▶

Not for confession, and not because bugs are interesting. This one is here because the shape of it is common and the catch was cheap, and because the alternative was worse than an ordinary wrong number. A bench that overstates a risk gets argued with. A bench that says a cheap precaution gives complete protection gets believed, acted on, and never revisited, because it told everyone what they wanted to hear about a control they had already installed.

The second reason is that the fault was not in the algorithm. The retraction code was correct. The lag code was correct. The reorganisation logic was correct. What was wrong was that one input drove two outputs, so a measurement of one of them was read as a measurement of the other. That failure has nothing to do with ledgers and will find you in any pipeline with a safety switch in it.

Real world

The false table was not spotted by a test. It was spotted by somebody reading a row and finding it implausible in a specific way: a defence two blocks shallower than the thing it defends against had come out perfect. That is not a small anomaly to be explained, it is an impossibility, and the distinction is what made the next twenty minutes productive rather than reassuring.

Self-studyThe words used tonight3 min▶
  • Harness. The code that runs a thing and reports what happened. Distinct from the thing. Most of tonight is about the harness being wrong while the thing was right.
  • Conflation. Two distinct mechanisms driven by one input, so no result can say which of them produced it. The fault tonight, in one word.
  • Self test. Properties the engine checks on load. There are three, they all pass now, and all three also passed under the faulty code, which is the measurement this session turns on.
  • Property. A statement that must hold for any correct run, checked rather than assumed. Stronger than an expected value, because it does not depend on a seed.
  • Should-fail case. An input where a correct implementation is required to produce a bad outcome. The test nobody writes, and the only one that would have caught this.
  • Determinism. The same seed gives the same answer. True of a correct bench and equally true of a broken one, which is why stability across seeds proved nothing here.
  • Reorganisation depth. How many blocks deep a replacement goes. Bench B uses 3 unless the widget below is told otherwise. It is the thing the lag is being defended against.
Part 1 · covers the fault

One parameter, two mechanisms, and a table that lied

15 min live

Draw the wiring and the fault is obvious in about four seconds, which is the usual relationship between a diagram and a bug that survived review. The two defences need two inputs because they are two decisions. The bench gave them one, and derived the second from the first with a comparison that looked like a reasonable default.

Two decisions need two inputs. The bench derived the second one from the first. HOW THE TWO DEFENCES ACTUALLY WORK TWO INPUTS, TWO MECHANISMS finalityLag THE GATE retract THE REMOVER WAREHOUSE Set independently, so each one can be wrong alone. A lag of 1 with no retraction leaves 346 wrong rows, and the table says 346. HOW THE BENCH WIRED THEM ONE INPUT, FORKED finalityLag THE GATE THE REMOVER WAREHOUSE A lag of 1 reported 0 wrong rows, and 346 deleted. The deletions were counted the whole time, in a column the published table did not print. The whole fault is one line: the loader retracted whenever the lag was above zero. Holding a row back and taking a row back are different mechanisms with different costs, and session 7 is entirely about that difference. Behind a single parameter they cannot be told apart, so a result about one of them was read as a result about the other.
🔍 Click to zoom - nothing in either mechanism is wrong. The only difference between the two panels is how many inputs there are.
LiveThe table it produced, and the table that is true4 min▶

Under the faulty wiring the wrong-row count went 511 at lag 0 and then 0, 0, 0 for every lag above it. Under the separated mechanisms it goes 511, 346, 172, 0. The two tables agree at lag 0, because at a lag of zero the faulty comparison is false and no retraction happens, and that agreement is part of what made the rest believable. The baseline was right, the sweep started from a real number, and then it fell off a cliff that was not there.

What a reader takes from the false table is a specific and dangerous lesson: set a lag, any lag, and you are covered. That is the opposite of session 7's conclusion, which is that a lag shallower than the depth is the worst configuration available because it buys partial protection at full freshness cost while removing the pressure to look again. A course built on the false table would have taught people to install exactly that and stop.

The 346 and the 172 did not go anywhere in the faulty run. They were deleted, and the run counted them as corrected rows, which is a field the engine returns and the published table did not show. The evidence was in the output the whole time, in a column nobody had a reason to read, because a single parameter had made the distinction between the two columns invisible.

The same sweep, run twice. Only the wiring changed. What the bench reported 640 320 0 511 0 0 0 perfect, at a third of the depth REORGANISATION DEPTH 0 1 2 3 finality lag, in blocks What was actually true 640 320 0 511 346 172 0 REORGANISATION DEPTH 0 1 2 3 finality lag, in blocks A defence shallower than the thing it defends against should not be perfect. A lag of 1 lets two of the three replaced blocks through before the replacement lands. Those rows exist, and they have to be somewhere. The left chart says they are nowhere. That is not a result about reorganisations, it is a result about the harness.
🔍 Click to zoom - the two charts agree at lag 0 and nowhere else, and the agreement at lag 0 is part of why the rest of the left chart was believed.
LiveWhat made it implausible, stated precisely4 min▶

The catch was not a feeling that the number looked odd. It was a specific statement that can be made before running anything: a defence that is shallower than the thing it defends against cannot be complete. A reorganisation replaces three blocks. A lag of one writes two of those three before the replacement arrives. Those rows are in the warehouse. For the wrong-row count to be zero, something must have taken them out, and a gate does not take anything out.

That is the reasoning worth copying, because it does not require knowing where the bug is or even that there is one. It starts from the mechanism, derives what the result has to look like, and compares. A result that contradicts the mechanism is either a discovery or a defect, and the prior on defect is very high.

Notice also what it is not. It is not "this seems too good", which is a mood and leads to a shrug. It is "this specific number cannot be zero, and here is the arithmetic". The first gets overruled in a review. The second ends the conversation.

Self-studyWhere serious people disagree5 min▶

One camp says the real lesson is about parameter design and nothing else. Two mechanisms behind one input is a design fault that no amount of testing reliably compensates for, because the tests are written by the person holding the same mental model that produced the conflation. Their prescription is structural: one input per decision, always, even when one of them is nearly always derivable from the other, and a review that rejects a boolean derived from a numeric threshold on sight. On this view the missing should-fail test is a symptom, and fixing symptoms is how you get a second bug of the same shape next quarter.

The other camp says that is unfalsifiable advice. Every parameter is derivable from something and every codebase has defaults; a rule against derived switches would reject half of the useful ergonomics in any library, and you would still be relying on somebody noticing which derivations are dangerous. Their prescription is behavioural: for every control you claim works, write the test where it should fail, and make the expected value non-zero. That test is cheap, it is mechanical, and it does not require anybody to have the right mental model, because it is derived from the mechanism rather than from the code.

Both are held by people who have shipped test suites that missed real defects. Notice that the evidence here splits them: the conflation was invisible in three passing properties, which supports the second camp, and the reason those three properties were the ones written at all is that the author's mental model had one dial in it, which supports the first. This bench now has both fixes, and no honest account of the episode says which one would have been sufficient alone.

Part 2 · covers why the tests passed

Three properties, all passing, all blind

12 min live

The engine checked three properties on every load while the fault was in it, and the course quoted their passing as a reason to believe the numbers. That claim was true and much narrower than it sounded, because all three of them also pass under the faulty code. There are five now, and the fourth is the one that matters. This has been re-run rather than assumed, and the measurement is the reason this session exists.

The harness probed the two lags where the answer is obvious, and nothing in between. WHERE THE SELF TEST LOOKED, ALONG THE LAG IT WAS TESTING TESTED TESTED NEVER TESTED THE DEPTH 0 1 2 3 4 5 6 No defence at all, and a defence twice as deep as it needs to be. Both answers are obvious before the run, which is why they were the two written. PROPERTY THE HARNESS CHECKS UNDER THE REAL CODE UNDER THE FAULTY CODE No co-spends leaves every address its own entity pass pass A lag deeper than the reorganisation orphans nothing 0, pass 0, pass With no lag, a reorganisation does orphan rows 380, pass 380, pass The property that was missing: a lag shallower than the reorganisation must still orphan rows 259 0, would have failed Every property the harness checked passed under the faulty code. That is what a blind spot looks like from inside. The three checks probed a lag of 0, where there is no defence to get wrong, and a lag of 6, where the defence is twice as deep as it needs to be. The one region where a bundled parameter could show itself was the band in between, and nothing looked there.
🔍 Click to zoom - the fourth row is one line of code and it is the only one of the four that can tell the two mechanisms apart.
Live Both versions of Bench B running in this page. Change the depth and watch which column refuses to move.

...

...
LiveThe tell the widget makes obvious4 min▶

Change the depth and watch the reported column. At depth 2, at depth 3 and at depth 4 it says the same thing: a real number at lag 0 and zero everywhere above it. The column the mechanisms were separated in moves a great deal, because it has to. At depth 4 a lag of 3 still leaves 170 wrong rows, and under the faulty wiring it reported zero, exactly as it did for depth 2.

A measurement that does not respond to the quantity it is supposed to be measuring is not a measurement. That is a stronger and much more usable diagnostic than "the number looks too good", because you can run it on a bench you did not write and do not understand. Vary the thing the control is defending against. If the reported protection does not change, the harness is not connected to the world in the way the label claims.

It is also the diagnostic that would have caught this without anyone knowing what retraction is. The false table's independence from the depth is visible from the outside, in the output, with no access to the wiring diagram in Part 1.

LiveFour identical seeds, four identical wrong answers3 min▶

The faulty table was checked across four seeds before it was published, which felt like diligence at the time. Every one of them reported 0 wrong rows at a lag of 1. Under the separated mechanisms the same four seeds report 346, 337, 333 and 336, which is exactly the kind of small spread a real quantity has.

So the stability that was taken as evidence of correctness was evidence of determinism and nothing else. A seeded generator plus a deterministic bug gives the same answer every time, with the same confidence, and reproducing it four times reproduces the bug four times. Agreement across seeds tells you the pipeline is deterministic. It tells you nothing at all about whether it is right.

What would have been informative is the shape of the answers rather than their agreement. A quantity produced by counting real events has a distribution. An exact zero, repeated, is either a structural fact or a structural bug, and the way to tell them apart is to ask which structure makes it zero and check that the structure is really there.

Self-studyWriting the should-fail case4 min▶

The missing property is one sentence: against a reorganisation of depth 3, a finality lag of 1 with retraction off must leave more than zero wrong rows. On the harness's own configuration of 120 blocks it leaves 259, so the assertion can be written as a strict inequality and never needs updating when the generator changes.

Three things make that test different from the three that already existed. It has a non-zero expected value, so it cannot be satisfied by a mechanism that removes everything. It sits in the region where the control is supposed to be partially effective, which is the only region where partial and complete can be distinguished. And it is derived from the mechanism rather than from the implementation, so it would have been written the same way by somebody who had not read the code.

The generalisation is worth stating in the form you can apply on Monday. For every control you have claimed works, there exists a strength at which it must not work. Write that case down, assert the bad outcome, and make the assertion strict. A suite in which every expected value is zero or pass is a suite that cannot distinguish a working control from a control that has been wired to something that hides the evidence.

One honest caveat. This test would have caught this bug, and it is not a general proof of anything. A different conflation, wired to a parameter the suite never varies, would survive it. The habit that survives the caveat is the one in the closing band of the diagram above: look at the region between your obvious cases, because that is where the tests are not.

Part 3 · build-along

Audit one control you believe in

15 min hands on

Six steps on a real control in your own estate: a retry, a circuit breaker, a validation gate, a rate limiter, a feature flag, a guardrail on a model. Bring the result you flagged as too good for homework if you have one.

Name the control and the thing it defends against

Two nouns, written down. If the second one is vague, the control cannot be tested, because you cannot vary a thing you have not named. Most of the value in this exercise is in the second noun.

Find the strength at which it must fail

Turn the second noun up until the control cannot cope by construction. Deeper, faster, larger, more concurrent. You are looking for the setting where a correct implementation is required to produce a bad outcome.

Predict the bad number before you run it

Write the expected value down first, with the arithmetic. A lag of 1 against a depth of 3 lets two of three blocks through, so the answer is roughly two thirds of the undefended number. Predicting first is what turns a surprise into a finding.

Count the inputs behind the control

How many settings switch it on or shape it, and is any one of them derived from another? A boolean computed from a numeric threshold is the exact shape of tonight's fault. If you find one, say out loud what result would look the same under both mechanisms.

Vary the threat and check the report responds

Run at two strengths and compare what the control reports. If the reported protection is identical at both, stop the exercise and investigate, because a reading that ignores its input is not a reading.

Read the fields your dashboard does not print

The 346 was in the output the whole time, in a corrected-rows field nobody displayed. List everything your control returns, then list what is on the dashboard. The difference is where a fault like this lives.

The habit to keep. When a control appears to give perfect protection, reach for the case where it should not. Not to be cynical about the control, but because perfect is a claim about the mechanism, and a mechanism that is genuinely perfect will survive being asked. If it does not survive, you have found something at the cost of one test. If it does, your evidence is now worth quoting, which it was not before.

Check yourself

Three questions

The faulty bench reported 0 wrong rows at a finality lag of 1 against a depth 3 reorganisation. Which single observation identifies this as a harness fault rather than a finding?

The argument is from the mechanism and needs no access to the code. Option A is the reverse of what happened: the faulty answer was stable across four seeds, which is what made it look trustworthy. Zero is a perfectly plausible value at lag 3, which is why the shape of the sweep rather than the presence of a zero is the tell.

All three of the engine's self test properties pass under the faulty code. What does that say about the three properties?

The three properties are still in the engine and still worth having. The gap was their placement: a lag of 0 has no defence to get wrong and a lag of 6 is over-deep, so both endpoints look identical whichever mechanism is doing the work. Only the partial region tells them apart.

Under the faulty wiring, the reported wrong-row count is zero at every lag above zero for depth 2, depth 3 and depth 4 alike. Why is that the most useful diagnostic of the three discussed tonight?

It needs no access to the wiring and no theory of what went wrong. Vary the thing the control defends against; if the reported protection is unchanged, the harness is not connected to the world the way its labels claim. The course takes no position on what any depth is in reality, and none of these figures describes a real network.

Before session 10

Homework

Twenty five minutes. One test written, and one question you want answered on a bench you can drive yourself.

Write one should-fail test and run it

For the control you audited: the strength at which it must not work, the non-zero value you predicted, and the value you got. Bring all three, including the case where the prediction was wrong, which is the more interesting outcome.

Bring a question for the benches

Session 10 puts both benches in your hands with every parameter exposed. Write down one thing you have wanted to check across the last nine sessions, as a question with a number in the answer.

Covered tonight

What this session covers

✓The conflation, drawnOne parameter forked into a gate and a remover, so no result could say which one produced it.
✓The false table and the real one511, 0, 0, 0 against 511, 346, 172, 0. They agree at lag 0 and nowhere else.
✓The catch, stated as arithmeticA lag of 1 writes two of three replaced blocks, so a zero requires a remover and a gate removes nothing.
✓Three passing properties, all blindRe-run under the faulty code: pass, 0 pass, 380 pass. The endpoints cannot distinguish the two mechanisms.
✓The property that was missingA lag shallower than the reorganisation must still orphan rows. 259 real against 0 faulty, on the harness's own setup.
✓A reading that ignores its inputThe reported column is unchanged at depth 2, 3 and 4, which is visible from the output with no access to the code.
✓Determinism is not correctnessFour seeds, four identical zeros. The real answers are 346, 337, 333 and 336, which is what a counted quantity looks like.
◐The two defences and their costsPriced in session 7. Tonight is only about a bench that could not tell them apart.
◐Driving both benches yourselfEvery parameter exposed in session 10, including the ones that produced tonight's tables.
○Chain mechanics and why branches get replacedThe prerequisite is learn-blockchain-with-phoebe and it is assumed rather than repeated.
○Any real network's reorganisation behaviourEvery figure here comes from the generator in w3-live.js and describes no real network.
○Anything to do with price, markets or holdingsOut of scope by design, and nothing here is advice of any kind.
Take this with you

Cheat sheet

The one sentence. A defence shallower than the thing it defends against should not be perfect. When it looks perfect, suspect the harness.
The fault. One parameter drove two mechanisms, so a result about the remover was published as a result about the gate.
The two tables. Reported 511, 0, 0, 0. True 511, 346, 172, 0. They agree only at lag 0, where the fault is inactive.
The evidence was printed. The 346 sat in a corrected-rows field the published table did not show.
Passing is not covering. All three properties pass under the faulty code, because all three sit where the answer is obvious.
The missing test. Lag shallower than the depth must leave more than zero wrong rows. 259 real, 0 faulty, one strict inequality.
Vary the threat. If reported protection is identical at depth 2, 3 and 4, the reading is not connected to the thing it names.
Four seeds agreeing. Evidence of determinism, not of correctness. A deterministic bug reproduces perfectly.