Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ddk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "ddk"
version = "1.0.11"
edition = "2021"
license = "MIT"
description = "application tooling for DLCs 🌊"
description = "application tooling for DLCs 🌊"
documentation = "https://docs.rs/crate/ddk"
repository = "https://github.com/bennyhodl/dlcdevkit"
homepage = "https://dlcdevkit.com"
Expand All @@ -18,6 +18,7 @@ lightning = ["dep:lightning-net-tokio"]
kormir = ["dep:reqwest"]
p2pderivatives = ["dep:reqwest"]
nostr-oracle = ["dep:nostr-database", "nostr", "kormir", "kormir/nostr"]
pow-attest = ["dep:reqwest"]

# storage features
sled = ["dep:sled"]
Expand Down Expand Up @@ -87,3 +88,8 @@ required-features = ["nostr"]
name = "postgres"
path = "examples/postgres.rs"
required-features = ["postgres", "lightning", "kormir"]

[[example]]
name = "pow_attest"
path = "examples/pow_attest.rs"
required-features = ["pow-attest"]
50 changes: 50 additions & 0 deletions ddk/examples/pow_attest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Connects to the live pow-attest oracle and prints a decoded
//! `OracleAnnouncement`.
//!
//! Run with:
//!
//! ```bash
//! cargo run --example pow_attest --features pow-attest
//! ```
//!
//! The default event id below is the static bounty kept on the pow-attest
//! server for downstream-test purposes. Override with the `EVENT_ID`
//! environment variable to point at any registered bounty.

use std::sync::Arc;

use ddk::error::Error;
use ddk::logger::{LogLevel, Logger};
use ddk::oracle::pow_attest::PowAttestOracleClient;

const DEFAULT_HOST: &str = "https://attest.powforge.dev";
const DEFAULT_EVENT_ID: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";

#[tokio::main]
async fn main() -> Result<(), Error> {
let logger = Arc::new(Logger::console(
"pow_attest_example".to_string(),
LogLevel::Info,
));

let host = std::env::var("POW_ATTEST_HOST").unwrap_or_else(|_| DEFAULT_HOST.to_string());
let event_id =
std::env::var("EVENT_ID").unwrap_or_else(|_| DEFAULT_EVENT_ID.to_string());

let client = PowAttestOracleClient::new(&host, logger).await?;

// ddk_manager::Oracle is the trait that exposes get_announcement /
// get_attestation. Bring it into scope so the methods resolve.
use ddk_manager::Oracle as _;

let announcement = client.get_announcement(&event_id).await?;
println!("oracle_pubkey: {}", announcement.oracle_public_key);
println!("event_id: {}", announcement.oracle_event.event_id);
println!("nonces: {}", announcement.oracle_event.oracle_nonces.len());
println!(
"maturity_epoch: {}",
announcement.oracle_event.event_maturity_epoch
);

Ok(())
}
2 changes: 2 additions & 0 deletions ddk/src/oracle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ pub mod memory;
pub mod nostr;
#[cfg(feature = "p2pderivatives")]
pub mod p2p_derivatives;
#[cfg(feature = "pow-attest")]
pub mod pow_attest;
224 changes: 224 additions & 0 deletions ddk/src/oracle/pow_attest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//! HTTP client for the pow-attest oracle at <https://attest.powforge.dev>.
//!
//! pow-attest is a PoW-gated Schnorr attestation oracle implementing the
//! dlcspecs OracleAnnouncement (type 55332) and OracleAttestation (type 55400)
//! TLV formats. Bytes are read directly through
//! `ddk_messages::oracle_msgs::OracleAnnouncement::read` / `OracleAttestation::read`
//! with no JSON translation layer.
//!
//! See discussion at <https://github.com/bennyhodl/dlcdevkit/issues/158>.

use std::io::Cursor;
use std::sync::Arc;

use crate::error::OracleError;
use crate::logger::{log_error, log_info, Logger, WriteLog};
use bitcoin::key::XOnlyPublicKey;
use ddk_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation};
use lightning::util::ser::Readable;
use serde::Deserialize;

/// Client for the pow-attest oracle.
///
/// The oracle exposes binary TLV endpoints that match the dlcspecs format
/// verbatim, so deserialization uses `lightning::util::ser::Readable` on the
/// response payload after stripping the outer TLV header.
#[derive(Debug)]
pub struct PowAttestOracleClient {
host: String,
pubkey: XOnlyPublicKey,
client: reqwest::Client,
logger: Arc<Logger>,
}

#[derive(Debug, Deserialize)]
struct InfoResponse {
oracle_pubkey: XOnlyPublicKey,
}

impl PowAttestOracleClient {
/// Connects to a pow-attest oracle at `host` (e.g. `https://attest.powforge.dev`).
///
/// Fetches `/api/v1/info` to learn the oracle's x-only public key.
pub async fn new(host: &str, logger: Arc<Logger>) -> Result<Self, OracleError> {
if host.is_empty() {
return Err(OracleError::Init("Invalid host".to_string()));
}
let host = host.trim_end_matches('/').to_string();
let client = reqwest::Client::new();
let info: InfoResponse = client
.get(format!("{host}/api/v1/info"))
.send()
.await
.map_err(|e| {
OracleError::Init(format!("Could not reach pow-attest: {e}"))
})?
.json()
.await
.map_err(|e| {
OracleError::Init(format!("Could not parse /api/v1/info: {e}"))
})?;
log_info!(
logger,
"Connected to pow-attest oracle. host={} pubkey={}",
host,
info.oracle_pubkey
);
Ok(Self {
host,
pubkey: info.oracle_pubkey,
client,
logger,
})
}
}

/// Strips the outer TLV header (BigSize type + BigSize length) from the
/// response body and reads the inner payload.
///
/// The pow-attest endpoints return the full TLV wire format including the
/// 3-byte BigSize type (`fdd824` for announcements, `fdd868` for attestations)
/// and the 1-byte BigSize length prefix. `OracleAnnouncement::read` and
/// `OracleAttestation::read` expect only the payload, so the leading 4 bytes
/// are skipped here.
fn read_tlv_payload<T: Readable>(bytes: &[u8]) -> Result<T, lightning::ln::msgs::DecodeError> {
// Outer type+len wrapper is 4 bytes for the message sizes pow-attest emits
// (3-byte BigSize type + 1-byte BigSize length <= 252). If the server ever
// grows a message past 252 bytes of payload, the length prefix becomes
// multi-byte and this offset will need to follow the BigSize length rules
// in dlcspecs.
let payload = if bytes.len() > 4 { &bytes[4..] } else { bytes };
let mut cur = Cursor::new(payload);
T::read(&mut cur)
}

#[async_trait::async_trait]
impl ddk_manager::Oracle for PowAttestOracleClient {
fn get_public_key(&self) -> XOnlyPublicKey {
self.pubkey
}

#[tracing::instrument(skip(self))]
async fn get_announcement(
&self,
event_id: &str,
) -> Result<OracleAnnouncement, ddk_manager::error::Error> {
let url = format!(
"{}/api/v1/bounty/{}/announcement.tlv",
self.host, event_id
);
let bytes = self
.client
.get(&url)
.send()
.await
.map_err(|e| {
log_error!(
self.logger,
"Could not fetch pow-attest announcement. error={}",
e
);
ddk_manager::error::Error::OracleError(format!(
"Could not fetch announcement: {e}"
))
})?
.bytes()
.await
.map_err(|e| {
ddk_manager::error::Error::OracleError(format!(
"Could not read announcement body: {e}"
))
})?;
let announcement = read_tlv_payload::<OracleAnnouncement>(&bytes).map_err(|e| {
log_error!(
self.logger,
"Could not decode pow-attest announcement TLV. error={:?}",
e
);
ddk_manager::error::Error::OracleError(format!("Could not decode announcement: {e:?}"))
})?;
log_info!(
self.logger,
"pow-attest announcement. event_id={} nonces={}",
event_id,
announcement.oracle_event.oracle_nonces.len()
);
Ok(announcement)
}

#[tracing::instrument(skip(self))]
async fn get_attestation(
&self,
event_id: &str,
) -> Result<OracleAttestation, ddk_manager::error::Error> {
let url = format!(
"{}/api/v1/bounty/{}/attestation.tlv",
self.host, event_id
);
let bytes = self
.client
.get(&url)
.send()
.await
.map_err(|e| {
log_error!(
self.logger,
"Could not fetch pow-attest attestation. error={}",
e
);
ddk_manager::error::Error::OracleError(format!(
"Could not fetch attestation: {e}"
))
})?
.bytes()
.await
.map_err(|e| {
ddk_manager::error::Error::OracleError(format!(
"Could not read attestation body: {e}"
))
})?;
let attestation = read_tlv_payload::<OracleAttestation>(&bytes).map_err(|e| {
log_error!(
self.logger,
"Could not decode pow-attest attestation TLV. error={:?}",
e
);
ddk_manager::error::Error::OracleError(format!("Could not decode attestation: {e:?}"))
})?;
log_info!(
self.logger,
"pow-attest attestation. event_id={} outcomes={:?}",
event_id,
attestation.outcomes
);
Ok(attestation)
}
}

impl crate::Oracle for PowAttestOracleClient {
fn name(&self) -> String {
"pow-attest".into()
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Captured OracleAnnouncement TLV from
/// `https://attest.powforge.dev/api/v1/bounty/<static-id>/announcement.tlv`.
///
/// The 4-byte outer header is `fdd824 c9`:
/// - `fdd824` = 3-byte BigSize for type `55332` (OracleAnnouncement)
/// - `c9` = 1-byte BigSize for length `201`
const STATIC_ANNOUNCEMENT_TLV_HEX: &str = "fdd824c9711cd782ddf632840c17b934e646785eb5418ec1b104436cce98eff8a4ea1557cd5d2e93316d300aa758cefebf02dd23f9a0fdfe08ce807e9b54ac241c80243def6218b2e12d74ffafa1b6e5217cc4592848c321c28109869903ff88989db23bfdd8226500013e0c2dad9737a8fc69f09298317fae26276c6319f65f0c589e57973abf48fbd967352480fdd806150002000852454c4541534544000750454e44494e47002436626137623831302d396461642d313164312d383062342d303063303466643433306338";

#[test]
fn roundtrips_static_announcement() {
let bytes = hex::decode(STATIC_ANNOUNCEMENT_TLV_HEX).expect("hex");
let ann = read_tlv_payload::<OracleAnnouncement>(&bytes)
.expect("OracleAnnouncement::read failed on captured pow-attest TLV");
assert_eq!(ann.oracle_event.oracle_nonces.len(), 1);
assert!(ann.oracle_event.event_id.contains("6ba7b810"));
}
}