5 Sources of Nondeterminism That Can Break Decentralized Verification
Every decentralized network eventually runs into the same wall. You have N independent parties who m 2026-10-5 16:11:1 Author: hackernoon.com(查看原文) 阅读量:0 收藏

Every decentralized network eventually runs into the same wall. You have N independent parties who must reach the same conclusion about the same fact, without coordinating, and then act on it — usually by moving money. If any two of them disagree, you do not have a network. You have a fork.

Stated that way it sounds like a solved problem. It is not. Almost every interesting fact a network wants to verify lives outside the network, and the outside world is not deterministic. What follows are the five ways I keep seeing verification layers break, and the patterns that fix them. The examples are drawn from networks that pay for externally verifiable work — Bittensor subnets, price oracles, proof-of-storage systems — where two verifiers disagreeing is not a logging problem but a payout dispute. None of this is exotic. All of it is easy to get wrong.

1. Anything you fetch is a moving target

The moment your verifier calls out to the live web, you have introduced a variable that no two nodes will observe identically. Two verifiers hitting the same URL thirty seconds apart can legitimately receive different bytes: A/B tests, edge-cache variance, geo-personalization, an ad slot that filled on one request and not the other, a rate limit that tripped for one client.

The naive fix is to fetch and compare hashes. That fails immediately, because exact byte equality across independent fetches of a real webpage essentially never holds.

The pattern that works is content anchoring. One party — usually the one making the claim — captures a snapshot of the content and submits it. Every verifier then fetches independently and checks that the snapshot is contained in what they see, using a fuzzy measure rather than equality. Shingle containment works well: break the snapshot into overlapping word n-grams, break your own fetch into the same, and compute the fraction of the snapshot's shingles present in yours.

def containment(needle: str, haystack: str, n: int = 5) -> float:
    nw, hw = normalize(needle).split(), normalize(haystack).split()
    if not nw:
        return 0.0
    n = min(n, len(nw))
    ns = {" ".join(nw[i:i+n]) for i in range(len(nw) - n + 1)}
    hs = {" ".join(hw[i:i+n]) for i in range(len(hw) - n + 1)}
    return len(ns & hs) / len(ns)

Above a threshold, the snapshot is accepted as authentic — and then here is the important part: every subsequent check runs against the snapshot bytes, not against each verifier's own fetch. The anchor is the only step that touches the variable input. Everything downstream operates on identical data, so it produces identical results by construction.

That inversion is the whole trick. You cannot make the network deterministic. You can shrink the nondeterministic surface to a single fuzzy check and make everything after it exact.

2. LLM judges are a consensus fork waiting to happen

This one is increasingly common and genuinely tempting. You need a semantic judgment — is this document on topic, is this answer correct, is this content promotional — and a language model gives you flexible, human-like classification for pennies.

It also gives you nondeterminism at the exact point where your system decides who gets paid.

Same prompt and same model do not guarantee the same output. Temperature zero reduces variance but does not eliminate it: batching, hardware differences, kernel versions, and silent provider-side model updates all move the result. And a model that gets deprecated mid-quarter takes your historical reproducibility with it.

If a judgment decides money, it has to be exact string, set, or date arithmetic. That is a real constraint and it will feel primitive. You will find yourself replacing "is this article about renewable energy" with "does the normalized text contain any of these keywords," and it will be cruder than you want.

Take the crude version. A verification layer that is 90% as smart and 100% reproducible beats one that is smarter and forks. If you genuinely need model-based judgment, the only safe shape I know is to pin an exact model version, require every node to run it, and treat any disagreement as a hard failure rather than a vote — which in practice means most teams should just not do it.

3. Floating-point addition is not commutative

This one bites late and it bites quietly.

IEEE 754 addition is not associative. (a + b) + c and a + (b + c) can differ in the low bits. So if two verifiers sum the same set of payouts while iterating a hash map in different insertion orders, they get slightly different totals. Divide by that total to produce normalized weights and the difference propagates into every entry in the vector.

Whether that matters depends on your tolerance. If weights are compared for exact equality anywhere, or hashed, or committed on chain and checked against a quorum, it matters enormously.

The fix is two lines and no excuse not to:

import math

total = math.fsum(sorted(values))       # sort for order-independence,
                                        # fsum for exact rounding

Sorting removes the iteration-order dependency. math.fsum performs compensated summation and returns the correctly-rounded result, so the answer no longer depends on grouping at all. Apply the same discipline everywhere a collection is reduced — and iterate dictionaries as for k in sorted(d), never in native order, anywhere the result feeds a consensus value.

4. Missing data is a decision, so make it explicit

Real inputs have holes. A field you expect is absent, a date fails to parse, an endpoint returns a shape you did not anticipate. Each of those is a branch, and if you do not write the branch deliberately, the language writes it for you — usually as "falsy, so skip the check."

That default is the dangerous one, because it means the verifier that fails to parse something is more permissive than the one that succeeds. Two nodes with slightly different library versions now disagree about whether a claim is valid, and the more broken node is the one paying out.

Fail closed. If a required timestamp will not parse, the claim is rejected — not accepted, not skipped. If a document is missing the field that proves it qualifies, it does not qualify. Write these as explicit early returns with named reasons, so that a rejection is a decision you can read in a log rather than an accident of control flow:

if published_ts is None:
    return reject(claim, "unparseable_publish_date")

The bonus is operational: named rejection reasons turn "the network disagrees" into a diff you can actually inspect.

time.time() returns something different on every machine. Any rule expressed in wall-clock terms — a deadline, a window, an expiry — is a rule that different nodes will evaluate differently near the boundary. Under load, "different near the boundary" happens constantly.

Use a shared, monotonic, network-visible counter as your clock. On a blockchain, that is the block height, usually bucketed into epochs. Every node reads the same height and derives the same epoch, so a rule like "installment N releases in epoch N" evaluates identically everywhere, and a node that was offline can replay the exact same schedule when it catches up.

Bittensor subnets are built this way: each derives its evaluation epoch by integer-dividing the current block height, so every validator scoring a given epoch agrees on where that epoch starts and ends without exchanging a single message about it. The clock is a consensus value like any other, and reading it costs nothing.

When you must convert to real time — because an external document carries a real timestamp — pull the reference time from the chain rather than the local clock, so the comparison is anchored to something all nodes share.

The underlying rule

Every one of these is the same principle wearing a different hat. Any input to a consensus decision must be one of:

  1. On-chain — every node reads the identical value, or
  2. Content-addressed and anchored — variable at the edge, fixed after one fuzzy check, or
  3. Computed by exact arithmetic over the first two.

Anything that does not fit those three categories is a fork you have not observed yet.

The uncomfortable part is that this constraint propagates backward through your architecture. It determines what facts you are able to verify, which determines what your network can pay for, which determines what it can be used for. Teams routinely design the product first and discover the constraint during implementation, at which point the honest options are to weaken the product or rebuild the verification layer.

Decide it first. Ask "can two strangers compute this identically, from data they can both obtain, using arithmetic neither can influence?" If the answer is no, the feature does not belong in the consensus path — put it in an advisory layer, or design a different feature.

Verification is not a component you add at the end. It is the shape of the whole thing.


文章来源: https://hackernoon.com/5-sources-of-nondeterminism-that-can-break-decentralized-verification?source=rss
如有侵权请联系:admin#unsafe.sh