Skip to content

Protocol

The whole scheme, in about a thousand words.

Nothing here is novel cryptography, and that is the point. Pedersen commitments date to 1991 and Okamoto’s protocol to 1992; both have been analysed for three decades. Oracleum’s contribution is the plumbing: binding them to block windows and enforcing that the windows leave no gaps.

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 concatenation

Commitment scheme

A Pedersen commitment to v under randomness r is one curve point:

C = v·G + r·H

It 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·C

The 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_n

A 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. 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. 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. 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 62348340

    The 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

Commitment + proof scheme
lib/zk.tsLive
Unit tests over commit / prove / verify / tamper
lib/zk.test.tsLive
Browser proof lab
app/proveLive
Proof feed with server-side re-verification
app/api/proofsLive
Verifier contract source
contracts/ProofAnchor.solLive
Deploy + anchor scripts
scripts/Live
Contract deployed to mainnet
0xe3459a8da895f24115b26c59393a87bc0454a18bLive
On-chain proof verification
phase 04Planned
External audit
not commissionedPlanned

This 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.