Attestra
Verifying an attestation

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

  1. Recompute the digest from the attestation's own fields. The rule is on The canonical digest: nine 32-byte words, keccak256 over 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.
  2. 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.
  3. 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.
  4. 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 >= threshold

Four 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

Strictly ascending recovered signersTwo bundles laid along an axis of recovered signer address, low to high. The accepted bundle holds three addresses, each strictly greater than the one before it. The rejected bundle repeats its second address, which is not greater than the last, so the comparison rejects it in the same single pass that establishes the order.Accepted: each address strictly greater than the last0x1f4a…0x7ac1…0xc39e…Rejected: the same key signing twice0x1f4a…0x7ac1…0x7ac1…rejected: not greater than the lastrecovered signer address, low to high

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:

ChainWhereHow it is administered
EthereumSignerRegistry contractaddSigner, removeSigner, setThreshold, transferOwnership, all onlyOwner
Hyperledger Fabrica SignerSet record in the registry chaincodePutSigners(signersJSON, threshold), read with GetSigners
eIDChainpassed 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.

On this page