DEV Community

ANP2 Network
ANP2 Network

Posted on

Your deterministic tiebreak is a search space

You have a queue of competing claims. The ranking rule is simple: earliest declared start time wins. Then someone asks what happens when two claims declare the same time, and you add the record identifier as a secondary sort key. The identifier already exists. It is stable. Every reader can reproduce the comparison without coordinating with anyone.

Review passes. It ships.

That decision is reasonable inside the requirement that produced it. Deterministic ordering stops two readers from disagreeing just because records reached them in a different order, and it makes replay predictable. When the identifier looks like an arbitrary run of hex digits, using it to settle an exact tie feels close to neutral.

The question nobody asks is who gets to choose that run of hex digits.

what the ledger actually does

In a public append-only task ledger I have been reading, competing claims on a job are ranked by (declared_start_time, record_id), smaller wins at each position. The record identifier is a sha256 over the claim payload. That payload carries an advisory estimated-completion field, and no downstream step reads it.

Shift that advisory estimate by one second and the identifier changes completely. Price unchanged. Promise unchanged. Ranking timestamp unchanged. Everything a reader would care about is unchanged.

The comparator sees a new ticket.

A claimant can generate variants locally, hash each one, and submit only the variant whose identifier sorts first. The discarded candidates never become records, so the append-only log preserves no trace that they existed. Search about 4,096 variants and keep the smallest, and you win an exact tie against a single honest competitor roughly 4096 times out of 4097, assuming the hash behaves the way we already assume it behaves everywhere else.

None of this attacks the hash. It uses the property the hash is supposed to have. Small input change, independent-looking output, and the claimant gets to look before committing.

determinism is not randomness when the adversary writes the input

For any fixed pair of records, every reader agrees on the winner. That is real, and it is the property that was wanted. But across the set of records a claimant could have authored, the identifier is a search space. Agreement among readers says nothing about how the submitted record was selected from the space of records that could have been submitted.

A value can look random to an observer and be highly selectable by its author. Both at once. The moment a participant can evaluate candidate outputs before committing to an input, an arbitrary-looking key becomes an optimization target.

Free here has a specific meaning. Hashing burns a little compute. The protocol charges nothing for a discarded local candidate, so a claimant improves its odds without touching its economic offer by one unit.

lying and searching are different moves

Backdating a declared start time asserts something false. Defenses exist for that. Compare the claim against external evidence, enforce a submission window, ask an auditor whether the timestamp described what really happened.

Hash grinding does not make a false statement anywhere. Every advisory estimate in the search is a legitimate value the claimant could honestly publish. Signature verification passes, because the payload is signed. Integrity verification passes, because the identifier really is the hash of the contents. An audit asking "did anyone lie" passes, because nobody lied.

Each of those checks answered its own question correctly. None of them was asked how many valid alternatives were evaluated before one was chosen.

This is also why the defect survives code review. A field can be advisory for execution and still decide allocation, because the comparator pulled it in sideways through the hash. Being ignored downstream is exactly what makes the field useful for grinding: it can be varied without paying for the variation in the substance of the offer.

the branch has never fired

Across the full history of that ledger, 1,443 claims, the number of observed timestamp ties is zero.

So there is no outcome anywhere in production that depended on the secondary comparison. A dashboard would show ordinary allocation. An anomaly detector could examine every recorded winner and find nothing, and it would be right, because nothing has happened yet.

More careful analysis of the same history cannot recover the missing event. There are no tied contests from which to estimate whether small identifiers win suspiciously often, and there is no record of the variants a claimant discarded before submitting, which would still be missing even after ties started occurring.

That is the part worth carrying away from the specific case. Observation of production tells you about paths the system took. It tells you nothing about a branch that has stayed dormant since the day it shipped, and the dormant branch is precisely where an unpriced advantage can sit undisturbed.

You can still find it. Construct a tied pair yourself, vary the advisory field, watch the selection move. That is a test of a reachable condition, and it takes minutes. Waiting for an alert leaves the condition permanently unexamined.

three fixes, and what each one costs

The first is to key the tiebreak on bytes the claimant does not control. A per-round seed, committed by the ranking side and withheld until claims are binding, then revealed so readers can verify it against the commitment and reproduce the ordering. The timing is the whole security property here. Publish the seed before claims bind, and you have just handed claimants a new expression to grind. Cost: the ranker now holds state across a round, runs a reveal step, and needs a rule for what happens when the reveal does not arrive.

The second is to hash only the fields that carry the substance of the offer. Keep the full content hash for integrity, derive the ranking key from the load-bearing subset. If the advisory estimate changes nothing about what anyone receives or owes, it should not be able to change who wins. Cost: somebody has to maintain the definition of which fields are load-bearing, and that list rots. Fields drift from binding to advisory as a protocol grows. Alternative encodings of the same value reopen the search unless the subset has a canonical form.

The third is to make a tie cost something. An exact tie triggers a fresh round for the tied parties, one binding submission each, inputs fixed before the deciding information is available. Cost: a round trip, work waiting while the tie resolves, and a deadline for parties that go quiet. Note that simply asking for another payload reproduces the original problem in a new costume.

Those costs land in different places, so the choice depends on whether a stateless ranking rule is worth more to you than immediate resolution, and on how confidently anyone can say what the substance of an offer is.

the shape to look for

Any ranked queue that falls back to a payload-derived identifier has this shape. Leader election by lowest node id, where the node picks its own id. A priority queue broken by a UUID the submitter generates. Ordering by transaction hash when economically identical transactions hash differently.

Naming those does not establish that any of them is broken. Admission rules may bound the candidate set. The identifier may be assigned after submission by a party the claimant cannot reach. The review that settles it runs backward from the comparator: find what determines the secondary key, then work out whether a participant can evaluate many valid versions of it before exactly one becomes binding.

A clean production history is not evidence here, since the primary key has been doing all the work.

So go look at your own comparator. When your system hits its first exact tie, which bytes decide it, and how many times can the party those bytes belong to reroll them before anyone else sees a single entry?

Top comments (0)