Anchoring
Not yet availableBuilt and tested, rehearsed on a test network, and starting on Base mainnet at launch. What the two contracts do, what the second channel is for, and how to confirm for yourself whether it is on.
Where this actually stands
The badge on this page is Not yet available, and unlike the placeholder badges of an earlier draft it is about the feature, not about the page. The page itself is accurate; what it describes is switched off.
This is documented rather than omitted for one reason: the vocabulary appears in responses you will actually receive. A proof bundle has an anchors object. A checkpoint in the overview has a channels array. If those fields are undocumented, the first reasonable guess about what they mean is wrong in the flattering direction.
Confirm it yourself, in one request
Do not take this page's word for it. The status is reported on an unauthenticated endpoint.
curl -s "$UNIFYID_API/v1/public/transparency/overview" \
| jq '.data.channels'[
{
"channel": "unifyid_contract",
"label": "UnifyID contract",
"role": "authority",
"enabled": false,
"network": null,
"chainId": null,
"contractAddress": null,
"contractUrl": null,
"hookAddress": null,
"schemaId": null,
"deployment": null
},
{
"channel": "sign_protocol",
"label": "Sign Protocol",
"role": "witness",
"enabled": false,
"network": null,
"chainId": null,
"contractAddress": null,
"contractUrl": null,
"hookAddress": null,
"schemaId": null,
"deployment": null
}
]Every checkpoint in the same response carries state: "sealed" with both channels at status: "pending", confirmations: 0 and every identifier null. enabled: false is the field that settles it, and it will change to true with a real network, chainId and contractAddress on the day this stops being true. On Base mainnet that is chain id "8453", with deployment naming the bound contract and governance reporting the contract's owner, its timelock, minDelaySeconds and whether it is paused.
Internally the effect is total rather than partial: with no publisher enabled, the delivery job short-circuits and reports the channel as disabled, so the outbox of unsent statements is never even drained. There is no half-anchored state to reason about.
What is built
Two contracts, tested against a simulated chain, fuzzed, checked with static analysis, and rehearsed on Base Sepolia. The self-audit and its refresh of 18 October 2026 are what they rest on; there was no external audit.
UnifyIDAnchor.solthe authorityanchor(bytes statement, bytes signature). It emits the full 256-byte statement and 64-byte signature, so a reader reconstructs everything from the event rather than from an API. Both an allow-listed submitter and a valid low-S P-256 signature by a registered key are required.UnifyIDSignHook.solthe witness- A Sign Protocol schema hook. It reads the anchor contract's own submitter and key lists and refuses anything the anchor would refuse, so the second record cannot say something the first would not accept.
The design decision worth knowing is that the P-256 signature is intended to be checked by the contract, through the P256VERIFY precompile, rather than stored for somebody to check later. The consequence is that a stolen gas wallet could only submit statements the checkpoint key actually signed — it could not occupy a sequence with 256 bytes of its own choosing and fork the log.
The continuity rules the anchor is written to enforce, and the error each one raises.
| Rule | Error |
|---|---|
version must be 1 | StatementMalformed |
logId must equal the deployment's immutable log id | WrongLog |
sequence must be head.sequence + 1 — no gaps | OutOfSequence |
periodStart must equal head.periodEnd | PeriodNotContinuous |
periodEnd must be after periodStart and not in the future | InvalidPeriod, PeriodInFuture |
combinedRoot must not be zero | EmptyCombinedRoot |
previousCombinedRoot must equal head.combinedRoot — no forks | ContinuityBroken |
The signing key must be registered, not revoked, not retired before periodEnd | — |
| The signature must be low-S | SignatureNotLowS |
Governance is written the same way. The owner is an OpenZeppelin TimelockController driven by a Safe multisig, with a 48-hour delay on Base mainnet, so any change to the rules is visible on chain two days before it takes effect; a guardian may only pause() and nothing else; and renounceOwnership() always reverts, so the contract cannot be abandoned into a state with no one able to rotate a key.
Two channels, one chain
The design publishes the identical 256 bytes and the identical 64-byte signature to two places: UnifyID's own contract as the authority, and Sign Protocol as a corroborating second witness. Two records, two different parties storing them, the same bytes.
The production target is Base mainnet, chain id 8453, from launch. Base Sepolia, chain id 84532, is the test network the deployment was rehearsed on: a record there is described as "anchored on Base Sepolia (testnet)" and never as anything more, because a testnet record is a rehearsal.
A mainnet deployment cannot happen by accident or by a flag. The deployment script deploys to Base mainnet only for the production log, only with a 48-hour timelock, a separate guardian and two gas wallets, only after checking the live log, and only with its own confirmation. The API refuses to anchor the production log anywhere but Base mainnet, and refuses Base mainnet for any other log.
The fields you will meet meanwhile
These appear in responses today, populated with nulls and pending states. Read them as the placeholders they are.
stateon a checkpoint or a bundlerecorded,sealed,anchored_one_channeloranchored_both. Only the first two are reachable today.channels[].statusnot_configured,not_anchorable,pending,submitted,confirming,anchoredorfailed. Recorded today:pending. On Base mainnet a delivery isconfirminguntil its block is finalized, and only thenanchored.not_anchorablemeans the channel's contract begins after this checkpoint, so it will never carry it.anchorson a proof bundle{ unifyid_contract, sign_protocol }. Bothnull, or present with nothing in them.confirmations0.transactionHash, transactionUrl, attestationId, signscanUrl, anchoredAtnull. Once a checkpoint is anchored,transactionUrlis its Basescan page and, on the Sign Protocol channel,signscanUrlis the attestation's page on SignScan (scan.sign.globalfor Base,testnet-scan.sign.globalfor Base Sepolia).downloadPath/v1/public/transparency/anchored-checkpoints/{sequence}— a real, fetchable path. The checkpoint in it is real; the anchors in it are empty.
A field that will one day carry a transaction hash is not evidence that a transaction exists. Check enabled on the channel before you read anything into a status.
What this does not do
- Nothing on this page is in effect for UnifyID's log yet. It starts on Base mainnet at launch. Until the overview says
enabled: true, do not describe any of it in the present tense, and do not let a security questionnaire answer imply it. - Two channels is not two chains. Both are designed for one chain, and production refuses to start if the configured chain ids differ.
- A testnet record is not a guarantee. Base Sepolia is the rehearsal network. Anything anchored there should be described as a testnet record, in those words.
- Every check that depends on this is check 9 and check 10. Eight checks work today and stop where the chain starts. See Proof bundles.
- Switching this on is a founder action, not a configuration change you can request. It happens at launch, after a key ceremony with a new Safe, a separate guardian and fresh gas wallets.
Verified against API commit 234dba22 on 2026-10-18. The routes, scopes, checklist, events and limits on these pages are read from that export, not typed.