Anchor Worker

The AdPriva Anchor Worker is the service that batches cryptographic engagement proofs, generates zero-knowledge proofs (zk-SNARKs), and anchors Merkle roots to Horizen mainnet — making every ad interaction permanently verifiable on-chain.

What It Does

At a high level, the Anchor Worker runs a continuous three-stage pipeline every 5 minutes:

  1. Batch — Query the database for unprocessed (pending) engagement proofs, group them into a Merkle tree, and compute the Merkle root.
  2. Verify — Generate a Groth16 zk-SNARK for the batch and submit it to zkVerify for attestation on the zkVerify blockchain.
  3. Anchor — Submit the attested Merkle root to the Anchor.sol smart contract on Horizen mainnet, emitting an Anchored event that creates a permanent, public record.

This pipeline runs as a single Node.js worker process on DigitalOcean App Platform, replacing the previous three-Lambda AWS architecture.

Architecture

Go Server
  │
  ▼ INSERT anchor_jobs (job_type='zkverify_submit', status='pending')
MySQL (anchor_jobs table)
  │
  ▼ Poll every 5 minutes
DO Anchor Worker (Node.js)
  │
  ├─► Cloudflare R2 ──► Download circuit WASM + zkey
  │
  ├─► zkVerify (Kurier API)
  │     └─► Submit Groth16 proof
  │     └─► Poll until attestationId finalized
  │
  └─► Horizen Mainnet (Anchor.sol)
        └─► anchorBatch(merkleRoot)
        └─► Anchored event emitted (permanent on-chain record)
  │
  ▼ UPDATE anchor_jobs (status='completed', result={txHash})
MySQL

Why a Single Worker?

The previous AWS Lambda approach required three separate services (batch creator, zkVerify submitter, Horizen anchor) connected by SQS queues. This added operational complexity and latency.

The Anchor Worker consolidates all three stages into a single stateful process:

Before (AWS Lambda)After (DO Anchor Worker)
3 Lambda functions1 Node.js process
3 SQS queues + 3 DLQsNo queues — DB polling
CloudWatch + Secrets ManagerDO App Platform env vars
Cold-start circuit downloadsIn-memory circuit cache
~$3.56/month (10K proofs)~$6/month (DO plan)

The simplification reduces failure surface area, makes logs centralized, and eliminates SQS propagation delays.

Proof Lifecycle

Every proof follows this path through the Anchor Worker:

  1. Engagement event → Go server captures view/click/conversion, creates HMAC-signed proof, inserts row into proofs table with zkverify_status = 'pending'.

  2. Batch → Worker queries unbatched proofs from the proofs table, organizes them into a Poseidon Merkle tree, computes the root, and creates a proof_batches row + an anchor_jobs row with job_type = 'zkverify_submit'.

  3. ZK proof generation → Worker claims the zkverify_submit job, downloads circuit artifacts (proof_verification_v2.0.0.wasm, *.zkey) from Cloudflare R2 if not already cached, and generates a Groth16 zk-SNARK over the Merkle tree.

  4. zkVerify submission → Worker calls the Kurier API with the Groth16 proof and waits for an attestationId. The proof is verified on the zkVerify blockchain (~25 seconds, ~$0.05/batch). On success, creates an anchor_submit job.

  5. On-chain anchoring → Worker claims the anchor_submit job and calls Anchor.sol.anchorBatch(merkleRoot) on Horizen mainnet. The contract emits Anchored(merkleRoot) and stores nothing (gas-optimized, ~50k gas/anchor, ~$0.001).

  6. Status update → Worker marks anchor job as status = 'completed' with the txHash in the result field. Proofs in the batch are updated to zkverify_status = 'verified'.

Public Verification

Anyone can verify a proof by:

  1. Looking up the proof ID in the AdPriva Explorer.
  2. Tracing the Merkle root to the on-chain Anchored event on Horizen mainnet.
  3. Cross-referencing the zkVerify attestationId for the zero-knowledge proof attestation.

The Anchor contract (Anchor.sol) is deployed on Horizen mainnet (Chain ID: 26514, RPC: https://horizen.calderachain.xyz/http).

Zero-Knowledge Proof Details

AdPriva uses Groth16 zk-SNARKs with a custom Circom circuit (proof_verification_v2.0.0):

  • Circuit inputs: proof leaf hashes, Merkle path, Merkle root, timestamp
  • Output: single bit — proof is valid and belongs to the committed Merkle root
  • Proving key: stored in Cloudflare R2 (circuits/v2.0.0/)
  • Verification: performed by zkVerify’s on-chain verifier

This means that even the Anchor Worker never sees raw user data — it only processes cryptographic hashes produced by the Go server.

Deployment

The Anchor Worker runs on DigitalOcean App Platform as a background worker (no inbound HTTP). Infrastructure is managed via OpenTofu in the infrastructure/ repository.

Key environment variables:

VariableDescription
DATABASE_URLMySQL connection string (private VPC endpoint)
WORKER_START_DATEProcess only proofs after this date (migration cutoff)
KURIER_API_KEYzkVerify Kurier API key
HORIZEN_ANCHOR_SUBMITTER_KEYHorizen wallet private key for anchoring
HORIZEN_ANCHOR_CONTRACT_ADDRESSAnchor.sol address on Horizen mainnet
R2_PUBLIC_ENDPOINTCloudflare R2 endpoint for circuit artifacts
CIRCUIT_VERSIONCircuit version to use (e.g. v2.0.0)

CI/CD deployment is triggered automatically on push to the main branch of AdPriva/proofs via GitHub Actions (.github/workflows/deploy-anchor-worker.yml).