pow-attest test vectors

Deterministic inputs and expected outputs for every cryptographic construction

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-attest

1. Proof-of-Work

Format: 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

2. Dead man's switch outcome hashes

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
InputValue
tag (ALIVE)41 4c 49 56 45
switch_id550e8400-e29b-41d4-a716-446655440000
timestamp1700000000 → "1700000000"
ALIVE sha256eeeafdcee5cbe81171106e687a9036a443d118c3b7006084f10b5c5034f395d7
DEAD sha2569f38ce383c2e97413bc2b37987937967471aac1c163c20c3975e914183969b81

3. Bug bounty released outcome hash

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

4. Check-in signature message

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.

5. Oracle pubkey

2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5

x-only (BIP-340). All attestations are verifiable offline against this key with schnorr.verify(sig, outcome_hash, oracle_pubkey).


6. Schnorr attestation verify walkthrough

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');

7. OracleAnnouncement TLV byte dump

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. ✓


8. Rust verification snippet

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).


Node.js verification snippet

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');

pow-attestmanifestPowForge