@fairseal/verify
v0.1.1
Published
Independent verification for VEO-2 objects — structure, integrity, Ed25519 signatures, and on-chain Merkle anchors. Zero dependencies beyond @fairseal/core.
Maintainers
Readme
@fairseal/verify
Part of FairSeal — formerly OpenRNG.
Independent verification for VEO-2 (Verifiable Execution Object) records.
A VEO is only worth something if a third party can check it without trusting the
party that issued it. This package is that checker. It has zero dependencies
beyond @fairseal/core — an independent verifier shouldn't require you to
install more than the thing it verifies.
npm install @fairseal/verifyQuick Start
import { verifyVEO } from '@fairseal/verify';
const result = verifyVEO(veo, { trustedKeys: [OUR_PUBLIC_KEY] });
if (!result.valid) console.error(result.errors);Looking to verify a Committed Selection Receipt (CSReceipt)? That lives in
@fairseal/commit— use itsverifyReceipt(receipt), which returnsstatus: 'VALID' | 'PARTIAL' | 'INVALID'. This package (@fairseal/verify) verifies VEO objects — structure, integrity, signature, and on-chain anchor.
Four separate questions
| Function | Question it answers | Network? |
|---|---|---|
| verifyStructure(veo) | Is this a well-formed VEO-2? | no |
| verifyIntegrity(veo) | Has it been modified since it was signed? | no |
| verifySignature(veo, trustedKeys?) | Was it signed by a key I trust? | no |
| verifyAnchor(veo, rpcUrl?) | Is its Merkle root really on chain? | yes |
verifyVEO() runs the first three together and is what most callers want.
Quick start
import { verifyVEO } from '@fairseal/verify';
const result = verifyVEO(veo, { trustedKeys: [OUR_PUBLIC_KEY] });
if (result.valid) {
console.log('verified');
} else {
console.error(result.errors);
console.error(result.checks); // per-check pass / fail / skipped
}verifyVEO() is synchronous and never touches the network, so it is safe to run
on untrusted input inside a request handler.
verifyVEO(veo, options?)
interface VerifyOptions {
trustedKeys?: string[]; // PEM or raw 32-byte hex
requireSignature?: boolean; // default: true
}
interface VerificationResult {
valid: boolean;
checks: {
structure: { status: 'pass' | 'fail' | 'skipped'; detail?: string };
integrity: { status: 'pass' | 'fail' | 'skipped'; detail?: string };
signature: { status: 'pass' | 'fail' | 'skipped'; detail?: string };
};
errors: string[]; // why it failed
warnings: string[]; // what the result does *not* prove
}Defaults are fail-closed. An unsigned object is not valid unless you pass
{ requireSignature: false }, and an empty trustedKeys array trusts nobody.
What a signature actually proves
This is the part most verifiers get wrong, so the API is explicit about it:
// Proves: not modified since signing.
// Does NOT prove: who signed it. Anyone can generate a keypair.
verifyVEO(veo);
// → valid: true, warnings: ['Signature was verified against the public key
// embedded in the object…']
// Proves: signed by a key you decided to trust, and not modified since.
verifyVEO(veo, { trustedKeys: [OUR_PUBLIC_KEY] });
// → valid: true, warnings: []If you are verifying someone else's VEO, always pass trustedKeys. A VEO
that verifies against its own embedded key is self-consistent, not authentic.
verifyAnchor(veo, rpcUrl?)
Anchored VEOs (class VEO-2C) claim "this Merkle root was written to chain X in
transaction Y". A signature over that claim only proves the issuer said so —
confirming it means reading the chain back.
import { verifyAnchor } from '@fairseal/verify';
const result = await verifyAnchor(veo); // public RPC for the chain
const result = await verifyAnchor(veo, 'https://my-rpc.example'); // your own node
const result = await verifyAnchor(veo, { offline: true }); // local checks onlyThree independent checks, strongest last:
merkleProof— the object's leaf hashes up toanchor.merkle_root(local, no network).transaction— the anchor transaction succeeded and emitted aBatchAnchoredevent carrying that exact root, from the expected contract.contractState—getBatchRoot(anchor.batch_id)on the contract still returns that root. Skipped whenbatch_idis absent from the anchor record.
interface AnchorVerifyOptions {
rpcUrl?: string; // defaults to a public endpoint for anchor.chain
offline?: boolean; // skip all network calls
timeoutMs?: number; // per request, default 15000
fetch?: FetchLike; // inject a transport (tests, proxies)
leafHash?: string; // defaults to metadata._content_hash, then entropy_hash
}verifyAnchor() never throws on network failure. An unreachable RPC yields
valid: false with the check marked skipped, so you can tell "could not
check" apart from "checked and wrong":
if (!result.valid && result.checks.transaction.status === 'skipped') {
// RPC problem, not a bad anchor — retry later.
}Supported chain
Anchoring currently runs on Polygon Amoy testnet against the MerkleAnchor
contract at 0xA79E149C35Ad47Ed270Bf4b16B80170eBF7B88F8.
Mainnet deployment is planned. See the root README for the full anchoring
architecture.
Default RPC endpoints (DEFAULT_RPC_URLS) are provided for polygon-amoy and
polygon-mainnet. For any other chain, pass rpcUrl explicitly.
Merkle helpers
Exported for building your own tooling:
import { verifyMerklePath, computeMerkleRoot } from '@fairseal/verify';
verifyMerklePath(leafHash, [{ hash: sibling, position: 'right' }], root);Nodes are lowercase hex strings, and a parent is sha256(leftHex + rightHex)
over the concatenated hex text — matching FairSeal's tree construction.
position names the side the sibling sits on.
Verifying without this package
Everything here is deliberately reproducible from the spec. A verifier in
another language needs: SHA-256, Ed25519, canonical JSON (sorted keys, undefined
dropped), and an Ethereum JSON-RPC client. See docs/rfc/RFC-0002-VEO2.md and
packages/core/veo-2.schema.json.
License
MIT
