Skip to content

Bitcoin Anchoring ​

EdgeCoder coordinators anchor checkpoint and issuance epoch hashes to the Bitcoin blockchain using OP_RETURN transactions. This provides cryptographic finality: once anchored, checkpoint integrity can be verified by anyone with access to a Bitcoin node or block explorer.

Architecture ​

How it works ​

  1. Stats checkpoints are computed from the coordinator's stats ledger hash chain. Every 10 minutes, the anchor leader checks for a new finalized checkpoint.

  2. Issuance epochs are finalized via quorum consensus among coordinators. Once finalized, the anchor leader anchors the epoch hash.

  3. The anchor leader encodes the checkpoint hash into an OP_RETURN payload with a 35-byte format: "EC" prefix (2 bytes) + version byte (0x01) + SHA-256 hash (32 bytes).

  4. The OP_RETURN transaction is broadcast via the anchor proxy, which forwards to the Bitcoin Knots node over local JSON-RPC.

  5. The coordinator polls for block confirmations. Once the transaction has the required confirmations (default: 1), the anchor status is promoted from pending to anchored.

Anchor leader election ​

Only one coordinator per mesh broadcasts each checkpoint to avoid duplicate transactions.

The anchor leader is the coordinator with the lexicographically smallest peerId among all reachable mesh peers (including itself). This is deterministic and requires no extra messages. If the current leader goes offline, the next coordinator in sort order automatically takes over.

Non-leader coordinators verify anchors via the public Blockstream or mempool.space API.

Anchor proxy service ​

The anchor proxy is a lightweight authenticated HTTPS service that sits between the coordinator mesh and the Bitcoin node.

Why a proxy?

  • Coordinators never hold bitcoind RPC credentials
  • No IP whitelisting is needed when new coordinators join the mesh
  • Only a single service has access to the wallet private keys
  • Rate limiting prevents runaway broadcast costs

Endpoints:

MethodPathAuthDescription
POST/anchor/broadcastBearerBroadcast an OP_RETURN transaction
GET/anchor/confirm/:txidBearerCheck confirmation status of a txid
GET/anchor/find/:dataHexBearerReconciliation: txid of the wallet tx whose OP_RETURN carries dataHex, or null
GET/anchor/mempool/:txidBearerReconciliation: whether an unconfirmed tx is still in the mempool
POST/anchor/abandonBearerReconciliation: abandontransaction for a dropped tx ({ "txid": "..." })

dataHex is the raw OP_RETURN payload (what /anchor/broadcast embeds), not the full script: even-length hex, at most 160 characters (80 bytes). The three reconciliation routes share a rate limit of MAX_RECONCILE_CALLS_PER_MINUTE (default 30), separate from MAX_BROADCASTS_PER_MINUTE. A node error returns 502 with only an error code, and the detail is logged on the proxy. /anchor/mempool returns inMempool: false only for bitcoind error -5 (not in mempool). Any other node error is a 502, so the reconciler counts it as an attempt instead of treating the tx as dropped. | GET | /health | None | Basic node health (for Fly health checks) | | GET | /health/detailed | Bearer | Full node + wallet + rate limit status |

Environment variables (anchor proxy service):

VariableRequiredDescription
ANCHOR_PROXY_TOKENYesBearer token for authenticating coordinator requests
BITCOIND_RPC_URLYesBitcoin node JSON-RPC endpoint
BITCOIND_RPC_USERYesRPC username
BITCOIND_RPC_PASSWORDYesRPC password
BITCOIND_WALLET_NAMENoWallet name (default: empty)
BITCOIN_NETWORKNoNetwork label (default: signet)
MAX_BROADCASTS_PER_MINUTENoRate limit (default: 10)

Provider types ​

ProviderUse caseEnv value
anchor-proxyProduction (recommended)BITCOIN_ANCHOR_PROVIDER=anchor-proxy
bitcoindDirect RPC access to a local nodeBITCOIN_ANCHOR_PROVIDER=bitcoind
blockstreamRead-only confirmation lookups. Not accepted for anchoring (it can't broadcast); the factory rejects it with blockstream_cannot_anchor (#102).(used internally for lookups only)
mockDevelopment and testingBITCOIN_ANCHOR_PROVIDER=mock

Coordinator environment variables ​

VariableRequiredDescription
BITCOIN_ANCHOR_PROVIDERYesProvider type (see above)
BITCOIN_NETWORKYesMust match across all coordinators in the mesh
ANCHOR_PROXY_URLWhen using anchor-proxyURL of the anchor proxy service
ANCHOR_PROXY_TOKENWhen using anchor-proxyBearer token matching the proxy
ANCHOR_CONFIRMATION_POLL_MSNoPoll interval for pending anchors (default: 60000)
STATS_ANCHOR_MIN_CONFIRMATIONSNoRequired confirmations (default: 1)
BITCOIN_ANCHOR_CONFIRMATIONSNoConfirmations before an anchor counts as anchored (default: 6)
BITCOIN_ANCHOR_DAILY_BUDGET_SATSNoDaily anchoring budget in sats (default: 200000). A broadcast is refused when (broadcasts in the last 24h + 1) × BITCOIN_ANCHOR_MAX_FEE_SATS would exceed it, so the default allows 10 anchors/day at the default fee cap. 0 disables broadcasting.
BITCOIN_ANCHOR_MAX_FEE_SATSNoAbsolute fee cap per anchor tx (default: 20000)
BITCOIN_ANCHOR_MIN_FEE_SAT_VBNoFee-rate floor in sat/vB (default: 2)
BITCOIN_ANCHOR_MAINNETFor mainnetMust be 1 to broadcast when BITCOIN_NETWORK=bitcoin; mainnet anchoring is opt-in
ANCHOR_RECONCILE_MSNoHow often to reconcile in-flight and stuck anchors; also runs at startup (default: 900000)
ANCHOR_RECONCILE_MAX_ATTEMPTSNoFailed lookups before a broadcasting row is flagged needs_admin (default: 5)
ANCHOR_STUCK_AFTER_MSNoA pending tx this old that is unconfirmed and not in the mempool is marked failed and re-anchored (default: 21600000, 6h)
ANCHOR_ABANDON_WATCH_MSNoHow long an abandoned tx is watched in case it confirms anyway (default: 259200000, 72h)

Reconciliation (#103) ​

A broadcasting row means the coordinator stopped between broadcasting and storing the txid. On startup and every ANCHOR_RECONCILE_MS, the coordinator searches the bitcoind wallet for a transaction whose OP_RETURN carries that checkpoint. If it finds one, it stores the txid and tracks confirmations as usual. If the wallet definitely has no such transaction, the row becomes failed and is re-anchored. If the node can't answer, the coordinator counts an attempt. After ANCHOR_RECONCILE_MAX_ATTEMPTS failed attempts the row becomes needs_admin (logged as bitcoin_anchor_needs_admin_reconcile). That checkpoint is then never re-broadcast automatically. An operator checks the wallet and sets the row's tx_ref/status by hand. Wallet lookup needs BITCOIN_ANCHOR_PROVIDER=bitcoind or anchor-proxy. With other providers, in-flight rows go to needs_admin.

Before a stuck pending tx is marked failed, the coordinator calls bitcoind abandontransaction on it so its inputs are released and a re-anchor can't conflict with the old tx. If that call fails, or the provider can't abandon transactions, the row stays pending and the failure counts as an attempt. Each reconcile pass re-reads a row under the checkpoint lock and skips it if it changed since it was listed. An abandoned transaction keeps its txid and is watched for ANCHOR_ABANDON_WATCH_MS (default 259200000, 72h). If it confirms anyway, the row goes back to pending and then anchored, and its fee counts toward the daily budget for the 24h after it confirmed. needs_admin rows also count toward the budget, since they may have spent sats. The final fee of a recovered transaction is not re-checked against BITCOIN_ANCHOR_MAX_FEE_SATS.

Verification ​

Operators can verify any checkpoint hash on-chain:

bash
# Check coordinator anchor status
curl -s -H "x-mesh-token: $MESH_AUTH_TOKEN" \
  "https://coordinator.edgecoder.io/stats/anchors/verify?checkpointHash=<HASH>"

# Check anchor proxy health
curl -s -H "Authorization: Bearer $ANCHOR_PROXY_TOKEN" \
  https://edgecoder-anchor-proxy.fly.dev/health/detailed

# View transaction on Blockstream
open "https://blockstream.info/tx/<TXID>"

Security considerations ​

  • The Bitcoin wallet private keys never leave the Bitcoin Knots node. Signing happens server-side via JSON-RPC.
  • The anchor proxy only accepts requests with a valid Bearer token.
  • Rate limiting prevents runaway broadcast costs (default: 10 per minute).
  • Cross-network checkpoint messages from peers with a different BITCOIN_NETWORK are rejected with a peer score penalty.
  • Chain reorganization detection automatically demotes anchors if their transaction disappears from the longest chain.