Notation
All arithmetic happens on secp256k1, the same curve Bitcoin and Ethereum use for signatures, which means every implementation language already has a hardened library for it. n is the order of the group. Scalars are elements of Z_n; points are written in capitals.
G the standard secp256k1 generator
H a second generator, derived by hash-to-curve
n the group order (≈ 2^256)
v the committed value, a scalar
r the blinding factor, a uniform scalar
C the commitment, a point
‖ byte concatenationCommitment scheme
A Pedersen commitment to v under randomness r is one curve point:
C = v·G + r·HIt is perfectly hiding: for any C and any candidate v, there is exactly one r that produces it, so an observer with unbounded computing power learns nothing about v. It is computationally binding: opening the same C to two different values requires knowing log_G(H), which is exactly the discrete log problem.
That second property rests entirely on nobody knowing log_G(H), which is why H is not chosen. It is derived by hashing a fixed string onto the curve with the standard SSWU map:
H = hash_to_curve("Oracleum/v1/pedersen-H")
DST = "Oracleum-v1-secp256k1_XMD:SHA-256_SSWU_RO_"Two strings fully determine H. Recompute it in any library that implements RFC 9380 and compare against the value printed in the proof lab. If they differ, something is wrong and you should not trust any of this.
Proof system
A commitment alone proves nothing, since anyone can publish a random point. The prover also has to demonstrate they know a valid opening, without revealing it. That is Okamoto’s protocol, a sigma protocol for knowledge of a representation in base (G, H), made non-interactive with the Fiat-Shamir transform.
Prove(v, r, ctx):
k₁, k₂ ←$ Z_n fresh nonces, never reused
T = k₁·G + k₂·H
e = H2S(domain ‖ G ‖ H ‖ C ‖ T ‖ ctx)
s₁ = k₁ + e·v (mod n)
s₂ = k₂ + e·r (mod n)
→ (T, e, s₁, s₂)
Verify(C, T, e, s₁, s₂, ctx):
e' = H2S(domain ‖ G ‖ H ‖ C ‖ T ‖ ctx)
require e' == e
require s₁·G + s₂·H == T + e·CThe verifier recomputes the challenge rather than trusting the one in the proof. This matters: a proof with a hand-edited challenge that happens to satisfy the algebra would otherwise slip through. Recomputing binds the proof to its exact transcript.
H2S hashes to a scalar by expanding to 64 bytes with two SHA-256 invocations under distinct counter prefixes, then reducing mod n. Reducing a single 32-byte digest would be measurably biased; this is not.
ctx is the statement the proof is bound to. For a window anchor it is the canonical encoding of the block range and both state roots, which is what stops a valid proof for one window being presented as a proof for another.
Window rules
A window is well-formed when all of the following hold:
windowStart ≥ 0
windowEnd ≥ windowStart
windowEnd − windowStart + 1 ≤ 2048
rootBefore, rootAfter are exactly 32 bytes
ctx = "Oracleum/v1/window" ‖ start ‖ end ‖ rootBefore ‖ rootAfter
v = H2S(ctx)
r ←$ Z_nA sequence of windows is contiguous when each window starts at the block immediately after the previous ended, and its before-root equals the previous window’s after-root. Both conditions are checked in isContiguous() off-chain and the first is enforced on-chain, where it cannot be skipped.
The 2,048-block cap is a cost decision, not a security one. A larger window is not less secure; it just takes longer to produce and makes any gap in coverage coarser.
Verifier contract
ProofAnchor.sol stores anchors and refuses malformed ones. It does not verify the zero-knowledge proof on-chain, and the contract says so in its own comments. the EVM has no secp256k1 scalar-multiplication precompile, so the two multiplications a verification needs would cost more than the anchor is worth.
So the division of labour is: the chain guarantees the shape of the history, meaning append-only, contiguous and bounded, and the published proof guarantees the contents. Anyone can fetch a proof, hash it, compare against the on-chain proofHash, and verify it themselves in milliseconds. Moving verification on-chain is Phase 04.
function anchor(
uint256 windowStart,
uint256 windowEnd,
bytes32 commitment,
bytes32 proofHash
) external;
// reverts WindowNotContiguous(expected, actual)
// reverts WindowTooLarge(span, 2048)
// reverts WindowReversed(start, end)
// emits Anchored(windowStart, windowEnd, commitment, proofHash, prover)Verify this yourself
Only the SHA-256 hash of each proof is stored on chain. The proof itself is published here. Three commands close the loop: fetch the proof, verify it, then confirm its hash is the one the contract recorded. Nothing below needs a wallet, an API key, or our software.
- 1
Fetch the published proof
Every anchor is listed in the index, with the full proof bundle beside it.
curl -s https://oracleum.xyz/proofs/index.json curl -sO https://oracleum.xyz/proofs/oracleum-anchor-62346484-62348531.json - 2
Verify the proof locally
This recomputes the Fiat-Shamir challenge from the transcript and checks s₁·G + s₂·H == T + e·C. It never sees the prover’s secrets, because the bundle does not contain them.
# No repository published yet, so verify with any secp256k1 library. # This is the whole check, in Node, with one dependency: npm i @noble/curves @noble/hashes node -e ' const { secp256k1, hashToCurve } = require("@noble/curves/secp256k1"); const { sha256 } = require("@noble/hashes/sha2"); const { bytesToHex, hexToBytes, concatBytes, utf8ToBytes } = require("@noble/hashes/utils"); const P = secp256k1.ProjectivePoint, n = secp256k1.CURVE.n; const b = require("./oracleum-anchor-62346484-62348531.json").proof; const H = hashToCurve(utf8ToBytes("Oracleum/v1/pedersen-H"), { DST: "Oracleum-v1-secp256k1_XMD:SHA-256_SSWU_RO_" }); const num = u => u.reduce((a, x) => (a << 8n) | BigInt(x), 0n); const mod = x => ((x % n) + n) % n; const t = concatBytes( utf8ToBytes("Oracleum/v1/okamoto-fs"), utf8ToBytes("|G:"), P.BASE.toRawBytes(true), utf8ToBytes("|H:"), H.toRawBytes(true), utf8ToBytes("|C:"), hexToBytes(b.C), utf8ToBytes("|T:"), hexToBytes(b.T), utf8ToBytes("|ctx:"), utf8ToBytes(b.ctx)); const e = mod(num(concatBytes( sha256(concatBytes(Uint8Array.of(0), t)), sha256(concatBytes(Uint8Array.of(1), t))))); const lhs = P.BASE.multiply(num(hexToBytes(b.s1))).add(H.multiply(num(hexToBytes(b.s2)))); const rhs = P.fromHex(b.T).add(P.fromHex(b.C).multiply(e)); console.log("challenge matches:", bytesToHex(new Uint8Array(32).map((_, i) => Number((e >> BigInt(8 * (31 - i))) & 0xffn))) === b.e); console.log("equation holds :", lhs.equals(rhs)); 'Expected:
· Proof verifies ✓ · Hash matches file ✓ · On-chain proofHash ✓ matches · Anchored in block 62348727 · Prover 0x515cCC7c362E03D4c504673FBFb13a64608Ef26F - 3
Hash it and compare against the chain
The independent version of step 2’s last line, with no Oracleum code involved. Hash the canonical proof encoding yourself, then read what the contract stored.
# the proof hash, computed from the bundle node -e ' const p = require("./oracleum-anchor-62346484-62348531.json").proof; const enc = [p.scheme, p.version, p.C, p.T, p.e, p.s1, p.s2, p.ctx].join("|"); console.log(require("crypto").createHash("sha256").update(enc).digest("hex")); ' # what the contract recorded for that window cast logs --rpc-url https://rpc.mainnet.chain.robinhood.com \ --address 0xe3459a8da895f24115b26c59393a87bc0454a18b \ 'Anchored(uint256,uint256,bytes32,bytes32,address)' \ --from-block 62348340The third topic-free data word in the log is proofHash. It must equal the digest you just computed. If it does, the anchor on chain is describing exactly this proof, and the proof is valid.
Contract source
The deployed bytecode is an exact match for contracts/src/ProofAnchor.sol, verified publicly on Sourcify. You can confirm the match without trusting us:
curl -s https://sourcify.dev/server/v2/contract/4663/0xe3459a8da895f24115b26c59393a87bc0454a18b \
| grep -o '"match":"[a-z_]*"'
# -> "match":"exact_match"Compiler 0.8.24+commit.e11b9ed9, optimizer enabled at 200 runs, no constructor arguments.
What this does not prove
Stating the limits plainly, because a protocol page that only lists strengths is marketing.
- A proof shows the prover knew an opening of C and formed it from a declared block range. It does not by itself show those roots match what Robinhood Chain published. That comparison requires the roots, which the verifier supplies from their own node or from Ethereum.
- It does not prove the state transition inside the window was valid. That is a different and much larger problem, requiring a proof of execution over every transaction. Oracleum proves the history is complete and consistently recorded, not that it was correct.
- The commitment stored on-chain is the x-coordinate of C, which determines the point up to sign. The full compressed point lives in the published proof.
- No part of this has been audited, and the anchor set is only as complete as the provers who choose to cover it.
Implementation status
lib/zk.tsLivelib/zk.test.tsLiveapp/proveLiveapp/api/proofsLivecontracts/ProofAnchor.solLivescripts/Live0xe3459a8da895f24115b26c59393a87bc0454a18bLivephase 04Plannednot commissionedPlannedThis page is generated from the same config the rest of the site reads, so it cannot drift out of sync with what is actually deployed.