Measured, not asserted

Does obfuscation break meter reading and consumption?

Protection on its own did not change a single meter read in this test. Member renaming did, and the sharpest result was not the renamed name that broke the bill -- it was the second renamed name that switched off the check which had been catching it.

A register reading is two different numbers

An electricity meter is an odometer. Its register counts up and never resets, so the consumption for a month is the difference between this read and the last one. Some meters instead report the interval directly, and the number that arrives looks exactly the same either way. A register value of 98,640 is either a lifetime total -- in which case the customer used the 640 kWh since the last read -- or it is the month's consumption. Only a configuration key says which.

That key is the kind of name this series keeps finding: not a quantity, not a threshold, not a policy, but a statement about what the quantities mean. The sample used here is a small billing engine with a caller that configures a cumulative basis, a five-digit register rollover, and a plausibility rule of its own. The engine itself is copied in unprotected, because an installed billing library is not rebuilt when your bundle is.

The first measurement is the one worth stating before anything else. Protection alone -- five presets, no member renaming -- left every reading in this file identical. So did the other four areas measured this pass, twenty-five comparisons in total. Nothing below is an argument that obfuscating a billing bundle changes what it computes.

The plausibility check caught the basis error

Renaming readingBasis is the option-key case: the name never goes missing, because the library substitutes its own default. The default here is interval, so the engine took the lifetime register to be the month's consumption -- 98,640 kWh instead of 640.

It did not bill it. The caller had supplied a meter-class rule saying that a residential meter on this tariff cannot physically pass 4,000 kWh in a month, and that rule refused the read: BILLED=false read-rejected(meter-class-check(0-4000 kWh)). The bill did not go out, somebody had to look at it, and the fault was recoverable the same day.

This is worth dwelling on, because most of the failures this series has measured were caught by nothing at all. A finite check -- the guard that most codebases actually have -- would have passed 98,640 without comment. It is finite, it is positive, and it is a perfectly ordinary number. What caught it was a RANGE check: a statement about how large this quantity can plausibly be, which a wrong basis violates by two orders of magnitude and a wrong value usually does not.

Then the check itself was renamed

The plausibility rule sits behind an enabling flag, which is how nearly every optional validation is built. Renaming that flag alone, requireValidation, changed nothing about the bills: the basis was still correct, so the reads were still 640 and 800 kWh, and only the line recording that they had been validated moved.

Renaming both -- readingBasis and requireValidation together -- produced this: BILLED=true billed(mtr-res-77120 98640 kWh at $0.184 = $18,149.76 USD -- interval(read taken as consumption)). A $117.76 bill became an $18,149.76 bill, on a residential account, and the system reported success.

The half that would have screamed was switched off by the half that fails quietly. That shape has now appeared in enough of these measurements to be worth stating as a rule of thumb: any reasoning of the form "if this broke we would know" is only sound when the thing that would tell you is not downstream of the thing that broke. Here the validator is downstream of the flag, and the flag went first.

What failed loudly, and what did not

Several arms failed closed, and they are worth reporting because a test that only finds silent disasters is confirming a belief rather than measuring anything. Renaming either register field, registerPrev or registerNow, made the subtraction unreadable and the engine refused: non-finite-consumption(refused before billing). Renaming meterId broke the binding between a read and the meter it came off, and the engine refused that too, with an accurate reason: wrong-meter(read undefined is not mtr-res-77120).

Renaming registerMax, the register size used to correct a rollover, turned the wrapped month into a large negative difference. Negative is finite, so the finite check passed it -- and the range check refused it, for the same reason it refused the basis error. That is twice in one file that a plausibility bound caught something a finite bound could not.

One arm failed open in a way that is easy to miss. Renaming rateUsd, the price, produced BILLED=true billed(mtr-res-77120 640 kWh at $undefined = $NaN USD). The engine checks that the CONSUMPTION is finite before billing, and never checks the CHARGE. The quantity was fine, so the guard was satisfied, and a bill went out with no amount on it.

What to do about it

None of this is an argument against protecting a metering front end. It is an argument about which names you let a rename pattern reach. Member renaming is opt-in, and the pattern is yours to write.

Keep option keys read by an installed library out of the pattern; keep the field names on records that cross into one out of it too. Both are read by code that was not rebuilt with your bundle, so both are matched by text that no longer matches. The mitigation costs nothing at design time and is worth having regardless of obfuscation, because a dropped key over a serialisation hop and a hand-edited config produce exactly the same miss.

And take the specific lesson this file taught: a range check earns its keep. A bound on how large a quantity can plausibly be catches a wrong basis, a wrong unit and a wrong sign, none of which a finite check can see. Put it in front of the money, not only in front of the input, and do not let it sit behind a flag that the same pattern can reach.

Frequently asked questions

Does obfuscation change what a meter read bills?

Not on its own. Across five presets and five areas measured this pass, protection alone left every output identical -- twenty-five comparisons, no differences. What changed behaviour in this test was member renaming, which is a separate opt-in option with a pattern you write yourself.

What actually went wrong in the measured case?

Renaming the option key that says whether a reading is cumulative or an interval made the library substitute its own default. The lifetime register of 98,640 was then taken as one month's consumption instead of the 640 kWh that had actually been used.

Was that caught?

Yes, by a plausibility rule saying a residential meter cannot exceed 4,000 kWh in a month. The read was refused with BILLED=false and never became a bill. A finite check would have passed 98,640 without comment, because it is a perfectly ordinary finite number.

So why is this article worth reading?

Because renaming the enabling flag in front of that plausibility rule, at the same time, produced a bill of $18,149.76 where the correct figure was $117.76. The check that was catching the error was switched off by a second name the same pattern reached, and the system reported success.

Do the register fields themselves fail safely?

They do. Renaming either register field made the subtraction unreadable and the engine refused before billing, and renaming the meter identifier broke the read-to-meter binding and was refused with an accurate reason. Losing a value tends to fail closed; losing what a value means tends to fail open.

Is there anything the guards missed entirely?

Yes. Renaming the price produced a bill of $NaN that still reported success, because the engine checks that the consumption is finite and never checks the charge. Guard the number you are about to act on, not only the number you read in.

What is the practical mitigation?

Scope member renaming to names your own code owns on both sides, and keep out option keys and record fields that an installed library reads. Add a plausibility bound in front of the amount as well as the input, and do not let that bound sit behind a flag the same pattern can reach.

Related reading