The registry contract and anchoring
The registry contract does one thing: it receives a fingerprint and emits an event. What the law reads is the format of the event.
Interface
event Anchored(address indexed author, bytes32 indexed hash);
function anchor(bytes32 hash) external;
function anchorMany(bytes32[] calldata hashes) external;anchor emits one event; anchorMany emits one for each element of the array, and zero for an empty array. The author in the event is the caller’s address. The contract has zero storage, zero owners and zero upgrade paths, and treats every caller alike. The source is base/zikaron-core/contracts/src/ZikaronRegistry.sol, licensed CC0-1.0.
Known deployments
The app comes with four deployments of the same pinned build, with the same code hash; on Ethereum mainnet, Arbitrum One and OP Mainnet they also share one deployer, one set of deployment bytes and one address:
| Network | Chain ID | Registry contract | Start block |
|---|---|---|---|
| Ethereum mainnet (default) | 1 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 26087229 |
| Sepolia testnet | 11155111 | 0xC29410B882c4C3b77e33659d2f06ac563e7B08a3 | 11715660 |
| Arbitrum One | 42161 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 511445184 |
| OP Mainnet | 10 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 157735914 |
The start block is where scanning begins, earlier than the first anchor on that contract. Each identity keeps the one it uses, shared by both roles; “default” means the one the first-run wizard and “New identity” select first. The preset nodes and how they are chosen are in Releasing · Contract and networks.
Two forms of anchor
- Registry form
- The transaction succeeds, and its receipt holds a log emitted by a registry contract with exactly three topics and empty data: topic 0 is the keccak256 of
Anchored(address,bytes32), topic 1 is the transaction’s sender, topic 2 is the fingerprint. The same fingerprint also appears in the transaction’s calldata, at an offset that is a multiple of 32, or 4 plus a multiple of 32. - Self-transfer form
- A transaction sent to oneself whose calldata is a positive multiple of 32 bytes long, each 32 bytes being one fingerprint. It relies on the transaction alone and suits any EVM chain.
The two forms are equally valid. A verifier states in its scan range which registry addresses, which senders and which self-transfers it accepts.
Three requirements on the sender
- The sender is the address recovered from the transaction’s signature. For a transaction sent through a relayer, the sender is the relayer’s address; for a transaction validated by account code, the sender field under the law is empty.
- The sending address is a plain address both before and after the block of the anchor. Anchors sent from an address that carries code or an EIP-7702 delegation are void.
- The transaction’s signature names the chain ID.
These three keep every anchor readable as the key holder’s own act.
The code hash
The contract’s runtime bytecode is 246 bytes long, and its keccak256 is:
It comes from fixed build settings: solc 0.8.28, EVM version cancun, optimizer on with 200 runs, via_ir on, metadata removed from the bytecode. Any change to the source or the settings changes the hash. To recompute it:
cd base/zikaron-core/contracts
forge build
cast keccak $(forge inspect ZikaronRegistry deployedBytecode)Deploying your own
Anyone may deploy the same bytes on any EVM chain that has passed the Shanghai upgrade. After deploying, check that the hash of the code on chain matches the table above, then enter the chain ID, registry contract, start block and nodes for the identity in the app under Settings › Network › “Edit nodes”; to use it only for checking other people’s material, add it as a read-only network.
forge create src/ZikaronRegistry.sol:ZikaronRegistry \
--rpc-url <node> --private-key <deployment key> --broadcast
cast keccak $(cast code <new address> --rpc-url <node>)Deploy with a dedicated key; the identity key signs the ledger and sends anchors.
This pinned build uses the PUSH0 instruction and suits chains that have passed the Shanghai upgrade. An earlier chain needs its own build, with its own code hash. The law reads logs by the contract addresses a verifier declares, so a second build is purely a deployment convenience. The app checks the registry’s code hash in two places: before a read-only network is read, each of its nodes is asked its chain and the code at its registry: the nodes giving the pinned build count, and the network is read on those alone; a node giving no code there (one not yet synced to the block the registry was deployed in) is left out of the count; one node giving other code makes the network sit out that read; every node giving no code there is a fingerprint mismatch; when the main network’s chain settings are saved, a registry outside the built-in table is first checked against the nodes too, and it is saved once it proves to be the pinned build (otherwise: “The registry … is not the pinned build; the chain settings were not saved”); while the nodes are still empty, the settings are saved with the reading “Not checked: no node”, and checked again once nodes are set, with a mismatch reported. The read-only networks and the main network in the app therefore all run on the one pinned code.
Gas, for reference
| Call | Gas used by the contract | Estimate for the whole transaction (Ethereum mainnet) |
|---|---|---|
anchor | 2,352 | about 23,600 |
anchorMany, 1 element | 2,776 | about 24,050 |
anchorMany, 10 elements | 17,746 | about 42,900 |
anchorMany, 50 elements | about 127,300 | |
anchorMany, 83 elements | about 198,000 | |
anchorMany, 100 elements | 167,661 | about 233,800 |
The second column is the contract’s execution as measured in forge; the third is a node’s estimate for the whole transaction, including the 21,000 base cost and the calldata cost, which shifts a little with the hashes’ bytes (each zero byte in a hash saves about 12 gas). The more elements, the less each one costs.
The app and the command line set gas by one rule: when nodes estimate differently the largest is taken, and the transaction carries one and a half times it, rounded up, at most 200,000, and an estimate above 200,000 is refused, the whole batch staying in the queue (the app says the gas cannot be estimated; the command line sending directly reports E_GAS_REFUSED, and sending a data folder’s queue E_SEND_REFUSED with token GAS_REFUSED). On Ethereum mainnet one transaction therefore safely holds 83 entries; the estimate for 84 sits right at 200,000, and whether it passes depends on the zero bytes in the hashes. The fee cap is twice the base fee plus the tip, the tip being the median of the last 20 blocks’ median paid tips (blocks whose median is zero are left out unless all are zero), at most 1 gwei; when the base fee cannot be read, the fallback is 3 gwei and 1 gwei, and the confirmation card says “No base fee from the nodes; cap at the fallback”. A node serving another chain has its fees left out. The balance check before sending asks every node at the lowest block they have all reached and compares them, and a balance counts once a node has given it.
What the chain holds stays unsent. The app’s sender, anchor through the desktop or with the credential, and the command line’s direct anchor --form registry all read the registry’s Anchored logs (from its deployment block to the head) before signing, asking whether this key already anchored a hash given; any one there sends nothing (the app says “Some records in this batch are already on chain; nothing was sent this time.”, the command line answers E_ON_CHAIN). Entries every node reads as on chain then leave the queue, so the next send carries the rest; one node’s word moves none.
A stuck batch. When a batch sits in the nodes’ pools without being included and the price now has risen above the cap it went out with, it can be resent with higher fees: signed again at the same nonce, each fee at least 10% above what it went out with (nodes take a replacement only then), with the old and the new transaction both watched and the first included counting. When the batch has left the nodes’ pools and its nonce is still free, the desktop offers “Send again at the price now” on the ledger’s status line for the user to press, and anchor --ledger with the credential, at “Send automatically”, signs it again as recorded at that nonce and sends it; when another transaction has used its nonce, the batch lapses and its entries go back to the queue. A batch is resent at most 3 times. At most one transaction per nonce can be included, so a batch is paid for once.
Anchoring through other contracts
Any contract that emits a log of the same format can serve as a registry. The log counts as an anchor when three conditions hold together: the transaction is sent by the ledger’s key itself; the fingerprint appears in that transaction’s own calldata, at an offset as stated above; and the log is emitted by the contract that was called. When a contract reaches a registry through an inner call, the log names that contract’s address, and to every ledger it is an ordinary log.
A verifier should list in its scan range only contract addresses whose source it has read and whose code is fixed. The app enforces this for read-only networks: a registry whose code hash differs from the pinned one is not read.
Self-contained anchor proofs
Law §9.7 defines self-contained anchor proofs, and the proofs/ directory of a record kit carries them. With such a proof a verifier checks an anchor from two trusted block hashes alone: the block headers, Merkle proofs of the transaction and of its receipt, and account proofs of the sending address at the two blocks. Such proofs stay valid after nodes prune their history.