All inputs are fixed constants. Reproduce with any sha256 implementation to verify a client or integration.
#pow · #outcomes · #released · #checkin · #oracle-pubkey · #attestation-verify · #announcement-tlv · #verify-node · #verify-rust
← back to pow-attestFormat: sha256(challenge + nonce) — direct concatenation, no separator. Output must have 18 leading zero bits (4.5 hex nibbles).
challenge = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" nonce = "122778" input = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4122778" sha256 = 0000260f25c541583b8065698fe685375e15f02604aa5363d35285eb7911dd6e ↑ 18 leading zero bits (first 4 nibbles = 0, 5th nibble 0x2 ≤ 0b0011) Verify: echo -n "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4122778" | sha256sum
Construction: sha256(TAG_BYTES + switch_id_bytes + ascii_decimal_timestamp_bytes)
switch_id = "550e8400-e29b-41d4-a716-446655440000" timestamp = 1700000000 (Unix seconds) ALIVE outcome = sha256("ALIVE" + switch_id + str(timestamp)) = eeeafdcee5cbe81171106e687a9036a443d118c3b7006084f10b5c5034f395d7 DEAD outcome = sha256("DEAD" + switch_id + str(timestamp)) = 9f38ce383c2e97413bc2b37987937967471aac1c163c20c3975e914183969b81
| Input | Value |
|---|---|
| tag (ALIVE) | 41 4c 49 56 45 |
| switch_id | 550e8400-e29b-41d4-a716-446655440000 |
| timestamp | 1700000000 → "1700000000" |
| ALIVE sha256 | eeeafdcee5cbe81171106e687a9036a443d118c3b7006084f10b5c5034f395d7 |
| DEAD sha256 | 9f38ce383c2e97413bc2b37987937967471aac1c163c20c3975e914183969b81 |
Construction: sha256("RELEASED" + bounty_id) — no timestamp; the release event is the condition.
bounty_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" RELEASED outcome = sha256("RELEASED" + bounty_id) = 6ad09a223172420d37ad3512cfa6367ec3dbf4fe38249ee16786202411dec2ad
The owner proves liveness by signing sha256(switch_id + str(bucket_ts)) with BIP-340 Schnorr. Bucket = floor(unix_seconds / 600) * 600 — 10-minute windows prevent replay while tolerating clock skew.
switch_id = "550e8400-e29b-41d4-a716-446655440000" bucket_ts = 1700000000 (= floor(1700000000 / 600) * 600) sig_message = sha256(switch_id + str(bucket_ts)) = 4c3c748a5dcbd4f47ec022d2f09d709a52da53bfde89b59a2ffbc08883770b70 Sign this 32-byte digest with BIP-340 Schnorr using your owner_pubkey private key.
2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5
x-only (BIP-340). All attestations are verifiable offline against this key with schnorr.verify(sig, outcome_hash, oracle_pubkey).
The previous sections show the input to the oracle (the outcome hash). This section closes the loop: given a static (signature, outcome_hash, oracle_pubkey) triple, reproduce the BIP-340 verify steps offline.
Why two tagged-hash layers. The dlcspecs attestation tag DLC/oracle/attestation/v0 binds the signature to "this is a DLC oracle attestation, not some other Schnorr signature." That layer produces a 32-byte message digest. BIP-340 then signs that digest with its own BIP0340/challenge tagged hash over R ‖ P ‖ msg. Skipping either layer breaks verification.
Static test triple (uses a fixed test key, not the live 2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5 oracle key, so the bytes are reproducible by anyone):
test_priv (sha256("pow-attest-test-vectors-v1")) = 3a3bc858abc9bfee35e95f79a5b90e79745a513dff244167e934bdceff85bb7f test_pubkey (x-only) = ef6218b2e12d74ffafa1b6e5217cc4592848c321c28109869903ff88989db23b bounty_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" outcome_hash = sha256("RELEASED" + bounty_id) = 6ad09a223172420d37ad3512cfa6367ec3dbf4fe38249ee16786202411dec2ad attestation_signature (64B, R‖s) = 10c860d54be8ba1a7216ae7385cd3ce4a0f4bed86ba25abfab2f4aec676c934aa6e162b4924374de4dbc91ab808843ca332d2b20e4f28ffcec23633af23f7081 R (32) = 10c860d54be8ba1a7216ae7385cd3ce4a0f4bed86ba25abfab2f4aec676c934a s (32) = a6e162b4924374de4dbc91ab808843ca332d2b20e4f28ffcec23633af23f7081
Layer 1 — DLC attestation tagged hash. The oracle does NOT sign the raw outcome_hash; it signs taggedHash("DLC/oracle/attestation/v0", outcome_hash_bytes). The tagged hash construction is BIP-340 §3.2: sha256(sha256(tag) ‖ sha256(tag) ‖ msg).
tag = "DLC/oracle/attestation/v0" tagHash = sha256(tag) = 0c2fa46216e6e460e5e3f78555b102c5ac6aecabbfb82b430cf36cdfe0442179 msg = bytes-of(6ad09a223172420d37ad3512cfa6367ec3dbf4fe38249ee16786202411dec2ad) layer1_msgHash = sha256(tagHash ‖ tagHash ‖ msg) = 0484919f41ae5bea08b827cde70c0efd832c172e9b829d191eeff45fde14c13b
Layer 2 — BIP-340 verify. Now run standard BIP-340 verification on (sig, layer1_msgHash, test_pubkey):
1. split sig into R (32 bytes) and s (32 bytes) 2. P = lift_x(test_pubkey) # BIP-340 §3.1 3. R_point = lift_x(R) 4. e = int(taggedHash("BIP0340/challenge", R ‖ P ‖ layer1_msgHash)) mod n 5. verify: s·G == R_point + e·P 6. verify also: has_even_y(s·G - e·P) AND x(s·G - e·P) == R
Any BIP-340 library can do this in one call. Node.js example:
const secp = require('@noble/secp256k1');
const { schnorr } = secp;
const sha256 = require('@noble/hashes/sha2.js').sha256;
secp.hashes.sha256 ||= (...m) => { const h = sha256.create(); for (const x of m) h.update(x); return h.digest(); };
const hexToBytes = (s) => Uint8Array.from(Buffer.from(s, 'hex'));
const tag = 'DLC/oracle/attestation/v0';
const tagHash = require('node:crypto').createHash('sha256').update(tag).digest();
const outcomeBytes = hexToBytes('6ad09a223172420d37ad3512cfa6367ec3dbf4fe38249ee16786202411dec2ad');
const layer1 = require('node:crypto').createHash('sha256').update(tagHash).update(tagHash).update(outcomeBytes).digest();
console.assert(layer1.toString('hex') === '0484919f41ae5bea08b827cde70c0efd832c172e9b829d191eeff45fde14c13b');
const ok = schnorr.verify(
hexToBytes('10c860d54be8ba1a7216ae7385cd3ce4a0f4bed86ba25abfab2f4aec676c934aa6e162b4924374de4dbc91ab808843ca332d2b20e4f28ffcec23633af23f7081'),
layer1,
hexToBytes('ef6218b2e12d74ffafa1b6e5217cc4592848c321c28109869903ff88989db23b'),
);
console.assert(ok === true, 'Schnorr verify failed');
console.log('attestation OK');
A GET /api/v1/bounty/6ba7b810-9dad-11d1-80b4-00c04fd430c8/announcement.tlv response is 205 bytes of binary TLV per dlcspecs/Oracle.md. The bytes below are produced from the same fixed test key as the previous section, signing a bounty announcement for bounty_id = 6ba7b810-9dad-11d1-80b4-00c04fd430c8 with created_at = 1700000000.
Full hex (205 bytes):
fdd824c9711cd782ddf632840c17b934e646785eb5418ec1b104436cce98eff8a4ea1557cd5d2e93316d300aa758cefebf02dd23f9a0fdfe08ce807e9b54ac241c80243def6218b2e12d74ffafa1b6e5217cc4592848c321c28109869903ff88989db23bfdd8226500013e0c2dad9737a8fc69f09298317fae26276c6319f65f0c589e57973abf48fbd967352480fdd806150002000852454c4541534544000750454e44494e47002436626137623831302d396461642d313164312d383062342d303063303466643433306338
Annotated breakdown — every byte explained:
# Outer TLV: type 55332 (oracle_announcement) fd d8 24 # BigSize(55332) = 0xFD prefix + 0xd824 BE c9 # BigSize(201) = body length # Body field 1: announcement_signature (64 bytes, BIP-340 Schnorr) # signs sha256(oracle_event_tlv) with the oracle private key 711cd782ddf632840c17b934e646785e b5418ec1b104436cce98eff8a4ea1557 cd5d2e93316d300aa758cefebf02dd23 f9a0fdfe08ce807e9b54ac241c80243d # Body field 2: oracle_pubkey (32 bytes, x-only) ef6218b2e12d74ffafa1b6e5217cc459 2848c321c28109869903ff88989db23b # Body field 3: oracle_event (TLV type 55330, inner) fd d8 22 # BigSize(55330) 65 # BigSize(101) = inner body length # oracle_event.num_nonces (u16 BE) 00 01 # 1 nonce # oracle_event.oracle_nonces[0] (32-byte x-only nonce pubkey R) 3e0c2dad9737a8fc69f09298317fae26 276c6319f65f0c589e57973abf48fbd9 # oracle_event.event_maturity_epoch (u32 BE) 67 35 24 80 # = 1731536000 (created_at + 365d) # oracle_event.event_descriptor (TLV type 55302, EnumerationEventDescriptor) fd d8 06 # BigSize(55302) 15 # BigSize(21) = descriptor body length 00 02 # num_outcomes (u16 BE) = 2 00 08 # outcomes[0].len (u16 BE) = 8 52 45 4c 45 41 53 45 44 # "RELEASED" 00 07 # outcomes[1].len (u16 BE) = 7 50 45 4e 44 49 4e 47 # "PENDING" # oracle_event.event_id_len (u16 BE) 00 24 # = 36 (UUID string length) # oracle_event.event_id (utf-8 bytes of bounty_id) 36 62 61 37 62 38 31 30 2d 39 64 61 64 # "6ba7b810-9dad" 2d 31 31 64 31 2d 38 30 62 34 2d 30 30 63 30 34 # "-11d1-80b4-00c04" 66 64 34 33 30 63 38 # "fd430c8"
The announcement_signature is over sha256(oracle_event_tlv), NOT over the raw event_id. This binds the signature to every TLV byte after the outer header — change one byte of the nonce, maturity, or descriptor and the signature fails. The signing auxRand is sha256("pow-attest-ann-sig-v0" ‖ oracle_priv ‖ bounty_id) so TLV bytes are stable across server restarts for the same bounty.
Cross-check: verify the byte dump above totals 205 = 3 (outer type) + 1 (outer len) + 64 (sig) + 32 (pubkey) + 3 (inner type) + 1 (inner len) + 2 + 32 + 4 + 3 + 1 + 21 + 2 + 36. ✓
Parse the announcement TLV from section 7 with dlc_messages, then verify the Schnorr attestation from section 6.
# Cargo.toml
[dependencies]
dlc-messages = "0.8"
lightning = "0.0.125"
bitcoin = "0.32"
secp256k1 = { version = "0.29", features = ["schnorr"] }
hex = "0.4"
reqwest = { version = "0.12", features = ["blocking"] }
sha2 = "0.10"
// src/main.rs — fetch and parse OracleAnnouncement TLV from /api/v1/bounty/:id/announcement.tlv
use dlc_messages::oracle_msgs::OracleAnnouncement;
use lightning::util::ser::Readable;
use std::io::Cursor;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// (1) Fetch TLV bytes from the oracle (or use the static hex from section 7)
let host = "https://attest.powforge.dev";
let bounty_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
let url = format!("{host}/api/v1/bounty/{bounty_id}/announcement.tlv");
let bytes = reqwest::blocking::get(&url)?.bytes()?;
// (2) Parse the OracleAnnouncement TLV via dlc_messages.
// The endpoint returns a full 205-byte TLV: 3-byte BigSize type (fdd824 = 55332)
// + 1-byte BigSize length (c9 = 201) + 201-byte payload. OracleAnnouncement::read
// expects only the payload, so skip the 4-byte outer header.
let mut cur = Cursor::new(&bytes[4..]);
let ann = OracleAnnouncement::read(&mut cur)
.expect("OracleAnnouncement::read failed -- server TLV is malformed");
println!("oracle_pubkey: {}", ann.oracle_public_key);
println!("event_id: {}", ann.oracle_event.event_id);
println!("nonce count: {}", ann.oracle_event.oracle_nonces.len());
println!("maturity: {}", ann.oracle_event.event_maturity_epoch);
// (3) Verify the announcement_signature offline (no external crate needed --
// dlc_messages exposes the inner OracleEvent for re-hashing)
ann.validate(&secp256k1::Secp256k1::new())
.expect("announcement signature did not verify against oracle_pubkey");
Ok(())
}
For the attestation Schnorr verify (section 6), fetch /api/v1/bounty/<id>/attestation.tlv and use OracleAttestation::read the same way. The attestation contains the raw signature plus the revealed outcome string; reproduce outcome_hash = sha256("RELEASED" + bounty_id) and verify with secp256k1::schnorr::Signature::verify against taggedHash("DLC/oracle/attestation/v0", outcome_hash).
const crypto = require('crypto');
// Reproduce all vectors above
const SWITCH_ID = '550e8400-e29b-41d4-a716-446655440000';
const BOUNTY_ID = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const TIMESTAMP = 1700000000;
// PoW: sha256(challenge + nonce), no separator
const pow = crypto.createHash('sha256')
.update('a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' + '122778').digest('hex');
const alive = crypto.createHash('sha256')
.update('ALIVE').update(SWITCH_ID).update(String(TIMESTAMP)).digest('hex');
const released = crypto.createHash('sha256')
.update('RELEASED').update(BOUNTY_ID).digest('hex');
const checkin = crypto.createHash('sha256')
.update(SWITCH_ID).update(String(TIMESTAMP)).digest('hex');
console.assert(pow === '0000260f25c541583b8065698fe685375e15f02604aa5363d35285eb7911dd6e');
console.assert(alive === 'eeeafdcee5cbe81171106e687a9036a443d118c3b7006084f10b5c5034f395d7');
console.assert(released === '6ad09a223172420d37ad3512cfa6367ec3dbf4fe38249ee16786202411dec2ad');
console.assert(checkin === '4c3c748a5dcbd4f47ec022d2f09d709a52da53bfde89b59a2ffbc08883770b70');
console.log('all vectors match');