Stateless Solidity drand verification stack for two BLS12-381 networks:
- Quicknet (
DrandOracleQuicknetdeployable oracle,DrandVerifierQuicknetinternal library) - Default network (
DrandOracleDefaultdeployable oracle,DrandVerifierDefaultinternal library)
The project currently uses both vendored bls-solidity (BLS2) and an in-repo internal library (LibBLS) for cryptographic operations.
DISCLAIMER: This code has not been professionally audited. It was developed, tested, and self-audited with AI. The bls-solidity library may have been audited, but include any of this within your audit scope if you're getting one.
For many apps, the value is not just “getting a provably random value", but getting randomness that is publicly retrievable and independently verifiable without a privileged oracle callback path. This is randomness your users can copy and paste into your app.
With drand, beacon data is public (round, signature) and can be fetched from public endpoints, then submitted onchain by anyone. That means integrators are not forced into a provider-managed callback flow with subscription/premium mechanics, and users can still supply signature data directly (including via a block explorer) if a frontend is unavailable. In this model, you pay normal transaction gas for your own app flow and verification, not an additional oracle fulfillment callback into your contract. You can then use this signature as a random number after hashing it.
Security-wise, this only gives the intended “external randomness” properties if integration is done correctly: commit to a specific future round before reveal, stop accepting user inputs that could be adapted after commitment, and enforce freshness/replay policy in the consuming contract.
src/verifiers/DrandVerifierQuicknet.sol- Internal quicknet verifier library (for embedding in other contracts).
- Accepts compressed (48-byte) and uncompressed (96-byte) G1 signatures.
src/verifiers/DrandVerifierDefault.sol- Internal default network verifier library (for embedding in other contracts).
- Verifies chained beacons with
sha256(previous_signature || uint64(round)). - Accepts compressed (96-byte) and uncompressed (192-byte) G2 signatures.
- Requires
previousSignature.length == 96(compressed previous round signature bytes).
src/oracles/DrandOracleQuicknet.sol- Deployable quicknet oracle contract exposing the external verifier API.
src/oracles/DrandOracleDefault.sol- Deployable default network oracle contract exposing the external verifier API.
src/utils/LibBLS.sol- Internal BLS12-381 helper library used by default verification paths.
src/interfaces/IDrandOracleQuicknet.sol- Quicknet oracle interface.
src/interfaces/IDrandOracleDefault.sol- Default oracle interface.
test/DrandVerifierQuicknet.t.sol- Quicknet unit/adversarial/fuzz/live-FFI coverage.
test/DrandVerifierDefault.t.sol- Default network unit/adversarial/fuzz/live-FFI coverage.
test/LibBLS.t.sol- Direct coverage for LibBLS decoding/math/hash-to-curve/pairing wiring paths.
The codebase separates internal-use verification logic from deployable entrypoints:
src/verifiers/*are libraries with internal functions. Use these when integrating drand verification directly inside your own contracts.src/oracles/*are deployable contracts that expose the same external API and are suitable when you want a standalone oracle/verifier address.
- Message digest input:
uint64(round) - Message digest:
sha256(uint64(round)) - Signature group: G1
- Public key group: G2
- Contract flow:
BLS2.hashToPoint(...)on G1 +BLS2.verifySingle(...)
- Message digest input:
previous_signature || uint64(round) - Message digest:
sha256(previous_signature || uint64(round)) - Signature group: G2
- Public key group: G1
- Contract flow:
LibBLS.verifyDefaultSignature(...)
From an onchain randomness perspective, both networks are functionally usable. Both provide publicly verifiable drand beacons you can verify onchain.
- Use Quicknet for most new integrations when you want faster cadence (3s rounds) and simpler verification inputs (
round + signature). - Use Default when you specifically need the chained scheme (
previous_signaturelinked into the message).
LibBLS is the internal library that powers DrandVerifierDefault. It exists because the default network uses a G2-signature / G1-public-key path with chained message construction, which is not the same turnkey path used by Quicknet.
In this repository, LibBLS provides the default network-specific cryptographic flow: compressed G2 decoding, canonical checks, G2 subgroup validation, hash-to-G2 mapping for the chained digest, and pairing-precompile wiring for final verification. DrandVerifierDefault.verify(...) computes the chained digest, then calls LibBLS.verifyDefaultSignature(...), decompression paths call LibBLS.decompressG2Signature(...).
LibBLS does not replace Quicknet’s BLS2 G1-verification flow. Quicknet verification still uses BLS2 directly.
| Property | Quicknet | Default |
|---|---|---|
| Oracle contract | DrandOracleQuicknet |
DrandOracleDefault |
| Internal library | DrandVerifierQuicknet |
DrandVerifierDefault |
| drand scheme | bls-unchained-g1-rfc9380 |
pedersen-bls-chained |
| Hash input | round |
previous_signature + round |
| Signature bytes accepted | 48 (compressed G1) / 96 (uncompressed G1) | 96 (compressed G2) / 192 (uncompressed G2) |
| Signature group | G1 | G2 |
| Public key group | G2 | G1 |
| Verification backend in this repo | bls-solidity (BLS2) |
LibBLS |
- Fetch round and signature from the Quicknet API.
- Call
verify(round, sig)with either compressed (48-byte) or uncompressed (96-byte) signature. - Use
decompressSignature(...)offchain only if you explicitly need uncompressed bytes. - Or pass raw API JSON directly to
verifyAPI(apiResponse)for simpler integration (with extra gas for JSON parsing). - Use
safeVerifyif you want to ensure no reverts occur due to malformed signature data or precompile failures, and return false instead. - Use
verifyNormalized(round, sig)when you need consistent randomness outputs across compressed/uncompressed signature representations.
- Fetch
round,signature, andprevious_signaturefrom the Default chain API. - Pass
previous_signatureexactly as 96-byte compressed bytes. - Call
verify(round, previousSignature, sig)with either compressed (96-byte) or uncompressed (192-byte) signature. decompressSignature(...)can be used offchain when you need uncompressed form.- Or pass raw API JSON directly to
verifyAPI(apiResponse)for simpler integration (with extra gas for JSON parsing). - Use
safeVerifyif you want to ensure no reverts occur due to malformed signature data or precompile failures, and return false instead. - Use
verifyNormalized(round, previousSignature, sig)when you need consistent randomness outputs across compressed/uncompressed signature representations.
If previous_signature is omitted, malformed, or from the wrong round, verification fails by design.
roundMessageHash(uint64 round) -> bytes32verify(uint64 round, bytes sig) -> boolsafeVerify(uint64 round, bytes sig) -> boolverifyAPI(string apiResponse) -> boolverifyNormalized(uint64 round, bytes sig) -> (bool verified, bytes32 normalizedRoundHash, bytes32 chainScopedHash)decompressSignature(bytes compressedSig) -> bytes- constants/metadata:
DST,COMPRESSED_G1_SIG_LENGTH,UNCOMPRESSED_G1_SIG_LENGTH,PUBLIC_KEY
roundMessageHash(uint64 round, bytes previousSignature) -> bytes32verify(uint64 round, bytes previousSignature, bytes signature) -> boolsafeVerify(uint64 round, bytes previousSignature, bytes signature) -> boolverifyAPI(string apiResponse) -> boolverifyNormalized(uint64 round, bytes previousSignature, bytes signature) -> (bool verified, bytes32 normalizedRoundHash, bytes32 chainScopedHash)decompressSignature(bytes compressedSig) -> bytes- constants/metadata:
DST,COMPRESSED_G2_SIG_LENGTH,UNCOMPRESSED_G2_SIG_LENGTH,PUBLIC_KEY
- Known good values
- Wrong round / wrong previous signature / wrong signature negatives
- Adversarial malformed/non-canonical input coverage
- Fuzzing for bit flips and random payloads
- Live FFI tests against drand APIs
- Dedicated LibBLS coverage via harness tests
foundry.toml enables ffi = true for live tests (curl + local conversion helpers). In CI/security-sensitive environments, disable or gate FFI appropriately.
- Both oracle contracts are stateless verifiers only (no freshness tracking or replay prevention).
- Both rely on target-chain support for required BLS12-381 precompiles included in the Pectra hard fork.
- Quicknet and Default use different precompile paths. Chain compatibility must be validated for your deployment target.
- For state-changing use, caller contracts should define freshness/replay policy explicitly.
verify(...) only answers signature validity. If an app derives randomness directly from raw signature bytes, compressed and uncompressed encodings of the same valid point hash to different values. To avoid that integration footgun, verifyNormalized(...) verifies first, then hashes the canonical signature point bytes plus round.
Both oracles return:
normalizedRoundHash = keccak256(canonicalSignaturePoint || round)chainScopedHash = keccak256(keccak256("DRAND_NORMALIZED_CHAIN_V1") || normalizedRoundHash || address(this) || block.chainid)
This gives one consistent random value (normalizedRoundHash) and one chain/contract-scoped value (chainScopedHash).
drand nodes collectively produce threshold BLS signatures for each round. Anyone who has the network root-of-trust parameters (public key, period, genesis, scheme) can verify a beacon signature. Onchain, this contract family checks the same signature validity that offchain clients check.
This gives public verifiability without trusting a single node or a private API response. The critical assumption is threshold honesty: drand’s security model states malicious control must stay below threshold for unpredictability; if an attacker controls at least threshold shares, they can derive future chain beacons, while randomness remains unbiasable. Because multiple parties are involved in signing, it is impossible for any single party to influence the final signature.
- Quicknet (
bls-unchained-g1-rfc9380): unchained mode, signatures on G1, 3s period, and per-round verification without needing previous signature bytes. - Default (
pedersen-bls-chained): chained mode, signatures on G2, 30s period, and verification depends onprevious_signaturelinkage.
Both can serve as onchain randomness sources; the practical choice is mostly integration shape and cadence: Quicknet is usually simpler/faster for new apps, while Default is chosen for chained-scheme compatibility requirements.
In this repo that means:
DrandOracleQuicknetverifies a round with(round, signature)via theDrandVerifierQuicknetlibrary.DrandOracleDefaultverifies with(round, previousSignature, signature)via theDrandVerifierDefaultlibrary and enforcespreviousSignature.length == 96.
| Dimension | drand (this repo’s model) | Chainlink VRF | block.prevrandao |
|---|---|---|---|
| Delivery pattern | Public beacon + user/relayer submits | Oracle callback fulfillment | Native block field |
| Cost shape | Gas for your call + verification, no VRF premium/subscription flow | Gas + VRF premium + callback path, subscription/funding management | Minimal read cost |
| Influence surface | External threshold network, unpredictability requires < threshold corruption | Validator reorg/re-roll considerations + callback ordering/funding concerns | Proposer has bounded influence per slot (EIP-4399) |
| Commitment style | Clean when app commits to specific future round before reveal | Request/fulfill lifecycle, asynchronous callback semantics | Must use lookahead/cutoff discipline to reduce predictability/bias risk |
- If your app uses drand, commit to the target round before reveal and stop accepting user inputs that could be adapted after commitment.
- Treat validator influence as mostly a timing/censorship issue on submission, not direct control of drand beacon value itself.
- Enforce freshness/replay policy in your stateful consumer contract (these verifier contracts are intentionally stateless).
- Handle round progression explicitly: drand can stall and later recover, and applications should define behavior for delayed/missed target rounds.
- Verify chain compatibility up front: this repo’s verifiers use BLS12-381 precompile paths, while drand
evmnetexists specifically for BN254 EVM-precompile compatibility. These verifiers do not implement drand'sevmnetscheme. - Either enforce use of either compressed or uncompressed signatures, as either form will derive different random values, or used
verifyNormalizedto ensure you get a consistent result.
lib/bls-solidity(still used directly by Quicknet verifier and BLS2 types)lib/forge-stdlib/solady(JSON parsing in verifierverifyAPI(...)helpers and FFI live tests)
- drand: Why decentralized randomness is important
- drand developer docs
- drand security model
- drand protocol specification
- drand timelock encryption
- Chainlink VRF security considerations
- Chainlink VRF billing
- EIP-4399 (
PREVRANDAO) - randa-mu/bls-solidity
VPL
