DEV Community

Zeke
Zeke

Posted on

Why block hashes are dangerous for DLC randomness (and the fix)

A DLC (Discreet Log Contract) is only as fair as its randomness source. If you're using a Bitcoin block hash as your oracle input for anything with money on it, you've got a miner front-running problem that won't care how tight the rest of your contract is.

This ain't theoretical. PancakeSwap lost $1.8M in 2021 to an attack that precomputed the block hash used as a random seed (SWC-120). DeFi, but the same class of attack applies to any protocol where the person producing the "randomness" can see the downstream payoff first.

Block hashes are predictable to miners.

Why block hashes fail for DLCs specifically

Here's the attack in plain terms:

  1. A DLC has two outcomes: Alice wins if the coin lands heads, Bob wins tails. The oracle will sign "heads" or "tails" based on the block hash at time T.
  2. A miner with enough hashrate can, before publishing a block, check: does this hash make me money (if I'm a counterparty) or does it favor the other side? If it favors the other side, they can try again: mine another nonce, or selectively withhold.
  3. Even without being a counterparty, miners can be bribed to produce favorable hashes, or simply front-run if the DLC outcome is observable on-chain before the block is confirmed.

Block hashes work fine for non-adversarial randomness. They're terrible for anything adversarial where the miner has a stake.

The professional gambling industry learned this. Stake.com publishes "public seeding events" that mix a future block hash with additional entropy they commit to in advance. They separate who knows what from who controls what. That's the pattern.

What a verifiable beacon provides instead

A proper randomness beacon closes the attack surface by:

  1. Collecting entropy from multiple independent contributors before the result is computed
  2. Computing the result only after contributions are frozen (end of epoch)
  3. Signing the result with a Schnorr key so you can verify it offline
  4. Providing a payment receipt (L402 macaroon + preimage) that ties your specific fetch to a specific epoch

That last point matters for DLCs. A DLC contract can reference the oracle's pubkey and the epoch ID in the announcement. When the epoch closes, the signed beacon is the attestation. Any counterparty can verify it independently with the oracle pubkey.

The PowForge draw beacon

The /draw endpoint at attest.powforge.dev does exactly this for Bitcoin. Every 5-minute epoch:

  • Contributors submit SHA-256 proof-of-work entropy (free)
  • Oracle aggregates deterministically: sha256("DRAW" || epoch_id || sha256(epoch_id || sorted_contribution_hashes))
  • Oracle signs with BIP-340 Schnorr and seals the epoch
  • Result is fixed forever and publicly verifiable

The Schnorr format is the same family used by DLC oracle specs (dlcspecs/Oracle.md). You're not adding a new trust assumption. You're delegating randomness to a PoW-seeded ceremony that's independent of the miner who eventually mines your settlement block.

Fetching a sealed beacon costs 50 sats via L402:

# Trigger the 402 to get the invoice
curl -si https://attest.powforge.dev/api/v1/draw/EPOCH_ID

# Pay the 50-sat Lightning invoice, get a preimage
# Then fetch with the credential
curl -s https://attest.powforge.dev/api/v1/draw/EPOCH_ID \
  -H "Authorization: L402 <macaroon>:<preimage>"
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "epoch_id": 5934397,
  "contribution_count": 2,
  "oracle_pubkey": "2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5",
  "attestation": {
    "beacon_random": "62eb805f...32 bytes hex...",
    "signature": "f2925b93...64 bytes hex...",
    "oracle_pubkey": "2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5"
  }
}
Enter fullscreen mode Exit fullscreen mode

Verify offline (oracle_pubkey is x-only, 32 bytes, no prefix):

const crypto = require('crypto');
const { schnorr } = require('@noble/curves/secp256k1');

// The oracle signs taggedHash("DLC/oracle/attestation/v0", beacon_bytes)
// not the raw beacon bytes directly. BIP-340 ยง3.2 tagged hash construction.
function taggedHash(tag, msg) {
  const tagHash = crypto.createHash('sha256').update(tag).digest();
  return crypto.createHash('sha256').update(tagHash).update(tagHash).update(msg).digest();
}

const { attestation } = result;
const beaconBytes = Buffer.from(attestation.beacon_random, 'hex');
const msgHash = taggedHash('DLC/oracle/attestation/v0', beaconBytes);
const sig = Buffer.from(attestation.signature, 'hex');
const pubkey = Buffer.from(attestation.oracle_pubkey, 'hex'); // already x-only
const valid = schnorr.verify(sig, msgHash, pubkey);
Enter fullscreen mode Exit fullscreen mode

Using the beacon as a DLC oracle input

In a dlcspecs-compliant DLC:

  1. Pick the epoch ID for your event close time (5-minute windows, Math.floor(Date.now() / 300000))
  2. Include the oracle pubkey and epoch ID in your DLC announcement as the "event descriptor"
  3. At settlement, fetch the sealed beacon and use attestation.beacon_random as the outcome scalar input
  4. Verify the Schnorr signature before settling (attestation.signature against attestation.oracle_pubkey)

The oracle pubkey is stable and published at attest.powforge.dev. The beacon is seeded by contributors independent of both counterparties and miners. Nobody controls the output.


If you're building DLCs and randomness is part of your oracle design, the block hash pattern has a known attack class. There's a Bitcoin-native alternative that costs 50 sats per event and verifies with any secp256k1 library.

  • Endpoint: https://attest.powforge.dev/api/v1/draw/{epoch_id}
  • Landing: https://powforge.dev/draw/

Top comments (0)