Skip to content

feat(oracle): add PowAttestOracleClient for attest.powforge.dev#159

Open
zekebuilds-lab wants to merge 5 commits into
bennyhodl:masterfrom
zekebuilds-lab:feat/pow-attest-oracle
Open

feat(oracle): add PowAttestOracleClient for attest.powforge.dev#159
zekebuilds-lab wants to merge 5 commits into
bennyhodl:masterfrom
zekebuilds-lab:feat/pow-attest-oracle

Conversation

@zekebuilds-lab

Copy link
Copy Markdown

Adds a fourth oracle implementation alongside kormir, nostr, and p2p_derivatives:
PowAttestOracleClient — a thin HTTP client for pow-attest,
a PoW-gated Schnorr attestation oracle implementing the dlcspecs OracleAnnouncement
(type 55332) and OracleAttestation (type 55400) TLV formats.

Discussed in #158.

What this PR adds

  • ddk/src/oracle/pow_attest.rsPowAttestOracleClient implementing ddk_manager::Oracle and crate::Oracle
  • ddk/examples/pow_attest.rs — connects to attest.powforge.dev, fetches and prints a decoded announcement
  • pow-attest feature flag in ddk/Cargo.toml (default off, mirrors the kormir flag pattern, reuses the existing optional reqwest dep)
  • [[example]] registration with required-features = ["pow-attest"]

How it works

The oracle exposes binary TLV endpoints that match the dlcspecs format verbatim:

  • GET /api/v1/info — returns oracle_pubkey (x-only, hex)
  • GET /api/v1/bounty/{event_id}/announcement.tlv — full OracleAnnouncement TLV wire bytes
  • GET /api/v1/bounty/{event_id}/attestation.tlv — full OracleAttestation TLV wire bytes

The response is the full TLV wire format (3-byte BigSize type prefix + 1-byte BigSize length prefix + payload).
OracleAnnouncement::read / OracleAttestation::read expect only the payload, so the
leading 4 bytes are skipped (&bytes[4..]) before reading. There is a comment in
read_tlv_payload calling out that, if message payloads ever grow past 252 bytes,
the BigSize length prefix becomes multi-byte and the offset will need to follow the
BigSize rules in dlcspecs.

Round-trip verification

The included unit test roundtrips_static_announcement decodes a captured
announcement.tlv blob (the static 6ba7b810-9dad-11d1-80b4-00c04fd430c8 bounty
kept on the pow-attest server for downstream testing) through
ddk_messages::oracle_msgs::OracleAnnouncement::read and asserts on the
oracle_event.event_id and oracle_nonces.len(). This is the gate that
confirms the server's TLV wire bytes are dlcspecs-compatible.

Use case

pow-attest signs RELEASED attestations when an external condition is met (e.g.
a GitHub PR is merged, an issue closed, a "dead man's switch" deadline elapsed).
Combined with a DLC, this enables conditional release — for example, trustless
bug-bounty payouts: lock funds in a contract, oracle signs when the PR merges,
anyone can execute using the revealed Schnorr scalar.

Test vectors: https://attest.powforge.dev/test-vectors
Background: https://dev.to/zekebuilds/trustless-bug-bounty-releases-with-a-pow-gated-dlc-oracle-46f0

Notes for review

  • The skeleton follows the kormir.rs shape (HTTP client with optional Arc<Logger>, both ddk_manager::Oracle and crate::Oracle impls).
  • No new top-level deps — the pow-attest feature reuses the existing optional reqwest dep that already backs kormir and p2pderivatives.
  • The example targets attest.powforge.dev over the public internet by default; POW_ATTEST_HOST and EVENT_ID env vars override.
  • The oracle pubkey (2bc7…a0e5) is stable; the test-vectors page gives deterministic anchor values for any future regression test.

Happy to address any feedback on naming, error variants, the static fixture
format, or anything else.

@zekebuilds-lab

Copy link
Copy Markdown
Author

Update: @powforge/attest-client@0.2.1 just published to npm with hand-crafted TypeScript declarations (index.d.ts). TypeScript consumers now get full IDE autocomplete and type errors against the oracle client.

Key types: AttestClient, BountyRecord, SwitchRecord, Attestation, BountyCondition, MineStream (async iterator with .result: Promise<string>), and the bytes[4..] note is documented on getBountyAnnouncementTlv so it's visible from the type signature.

https://www.npmjs.com/package/@powforge/attest-client

@zekebuilds-lab

Copy link
Copy Markdown
Author

Quick update: @powforge/attest-client@0.3.0 just shipped with two new methods directly relevant to testing this PR:

  • contributeDrawEntropy(owner_pubkey) — auto-mines 18-bit PoW and POSTs to /api/v1/draw; returns epoch_id + contribution_count
  • getDrawBeacon(epoch_id, l402Token?) — GETs the signed beacon, throws { invoice } on 402 for inline L402 payment

The oracle nonce (R) returned by the /announcement endpoint feeds directly into getDrawBeacon — you can wire them together to test the full DLC adaptor signing flow from JS before the Rust PR lands.

19 tests green on 0.3.0. Happy to share test patterns if useful.

@bennyhodl bennyhodl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a new oracle 🎉

One comment for a utility

Comment thread ddk/src/oracle/pow_attest.rs Outdated
/// 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> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ddk-messages has a helper exactly for this

ddk_messages::ser_impls::read_as_tlv

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — replaced read_tlv_payload with ddk_messages::ser_impls::read_as_tlv across all three call sites (announcement, attestation, and the test). Removed the local helper entirely.

I also updated the struct-level doc to reference read_as_tlv now that the comment about the 4-byte skip is gone.

Replace local read_tlv_payload with the existing read_as_tlv helper from
ddk-messages as requested by @bennyhodl. read_as_tlv reads BigSize type
and BigSize length fields correctly regardless of payload size, where
the previous implementation hardcoded a 4-byte skip.
@zekebuilds-lab

Copy link
Copy Markdown
Author

Addressed the review — switched to ddk_messages::ser_impls::read_as_tlv, removed the local read_tlv_payload helper. Ready for another look when you get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants