Verifying an attestation
One quorum rule, enforced identically by every verifier, with no call back to Attestra.
An attestation is a statement plus a bundle of signatures over that statement's digest. Checking it is a local operation. Nothing in the procedure below contacts Attestra, and nothing in it requires Attestra to be running.
The four steps
- Recompute the digest from the attestation's own fields. The rule is on
The canonical digest: nine 32-byte words,
keccak256over the 288-byte concatenation. If your recomputed digest does not match the one the signatures were taken over, every recovery below returns an address nobody authorized, and verification fails, which is the correct outcome. - Recover each signature. Each signature is 65 bytes,
R || S || V. Recovery over the digest yields a 20-byte address, or nothing at all if the signature is malformed. - Check each recovered address against that chain's signer registry. The registry is on-chain and is read at verification time. A recovered address that is not in it fails the whole bundle.
- Count, and accept when the count reaches the threshold. The threshold comes from the same registry.
The rule, in one block
last = 0
for each sig:
rec = recover(digest, sig)
require rec != 0
require rec > last # strictly greater: distinct AND sorted
require registry.isAuthorized(rec)
last = rec
count++
require count >= thresholdFour deployed verifiers implement exactly this, in four languages. A bundle accepted by one is accepted by all of them.
Why signatures must arrive sorted, strictly ascending
The ordering requirement is rec > last, on the recovered signer address, for
every signature after the first. It is not a stylistic preference. It carries
three loads at once.
It proves distinctness in one pass. A strictly increasing sequence cannot contain a repeat, so a single comparison per signature does the work. The alternative is comparing every recovered address against every other, which is O(n²) and, on Ethereum, gas the caller pays for.
Without a distinctness check, one key satisfies any threshold. Submitting the same oracle's signature three times would satisfy a 3-of-5 threshold with a single key. The count would be three; the number of independent opinions would be one. The whole point of a quorum is that the count and the number of opinions are the same number.
It closes signature malleability for free. A malleated signature recovers
to the same address as the original, so rec > last rejects it as a repeat.
No separate low-S check is needed, and none is present in any of the four
verifiers.
This is the Gnosis Safe approach, and it is well travelled. Sorting is the collector's job, not the verifier's, because the collector is the only party that sees the whole bundle. A verifier sees a bundle it did not assemble and can only accept or reject the order it was handed.
There is no single-signature path on any chain
None of the four verifiers exposes a way to check one signature. The reason is
stated flatly in the plan that built them: if a verify(a, sig) overload
remains, a caller can bypass the quorum entirely. The instruction was to
replace, not to add alongside. Every entry point takes a bundle, and a bundle
of one signature is accepted only where the threshold is genuinely 1.
The signer registry
A signer registry is an on-chain list of authorized oracle addresses plus an M-of-N threshold, consulted at the moment of verification rather than baked into the verifier. Verifiers check a recovered address against the registry instead of trusting a hardcoded address. That indirection is what lets keys be rotated and operators be onboarded without redeploying the verifiers.
Each chain keeps its own, and each is authoritative for that chain:
| Chain | Where | How it is administered |
|---|---|---|
| Ethereum | SignerRegistry contract | addSigner, removeSigner, setThreshold, transferOwnership, all onlyOwner |
| Hyperledger Fabric | a SignerSet record in the registry chaincode | PutSigners(signersJSON, threshold), read with GetSigners |
| eIDChain | passed to verify_quorum as authorized: &[[u8; 20]] | the pallet's own storage |
Because each chain holds its own copy, the registries are a governance surface, and they can disagree. Whoever controls a registry controls what that chain will accept.
The same rule in three languages
The pages that follow show the rule as it is actually written, pasted from source, with the administration surface of each chain's registry alongside it.