DEV Community

Zeke
Zeke

Posted on

Proving a proof-of-work solve happened, at a specific time, without trusting the server's logs

The problem: a solve is a fact that only lives in one server's memory

If you gate a route with proof-of-work, every accepted solve is a real event. Someone burned real compute, at a real wall-clock moment, to get past the gate. That is exactly the property you wanted: the gate costs the caller something.

But where does that fact live? In the verifier's memory, for as long as the process stays up. The server knows a valid solve came in. Nobody else does, and nothing stops the server from claiming a solve happened that didn't, or forgetting one that did. If you ever need to prove "this specific solve happened before this point in time" to somebody who does not trust your logs, you have nothing. A row in your own database is not evidence to an adversary.

So the question is narrow and concrete: can you make an accepted PoW solve durable and independently auditable, without asking anyone to trust the server that recorded it?

The mechanism: Merkle-fold the window, anchor one root to Bitcoin

@powforge/solve-witness does one thing. Every accepted solve is reduced to a leaf hash and appended to the current batch. On a fixed cadence (default 10 minutes) the batch is sealed: the leaves are folded into a Merkle root, and that single 32-byte root is the witness_hash for that window. Submit the root to an OpenTimestamps calendar and the whole batch is anchored to Bitcoin with one timestamp.

The economics are the point. One Bitcoin timestamp attests to an unbounded number of solves, because the Merkle fold makes the on-chain cost O(1) regardless of how many solves landed in the window. A thousand solves and one solve cost the same to anchor.

Later, anyone can prove one specific solve was in the batch by presenting three things:

leaf  ->  Merkle inclusion path  ->  witness_hash (in the OTS proof, anchored to a Bitcoin block)
Enter fullscreen mode Exit fullscreen mode

Every step recomputes with plain SHA-256, and the final OTS proof verifies against Bitcoin with the reference ots tooling, not against any PowForge server.

The leaf: what actually gets committed

A leaf has to commit to which challenge was solved, not just that some solve happened. The PowForge rate-limit layer identifies a solve by the challenge salt and the winning nonce, plus the server's HMAC signature over the salt. All three go into the leaf, domain-separated so a leaf hash can never collide with an internal Merkle node hash:

function leafForSolve({ salt, nonce, signature = '' }) {
  if (salt == null || nonce == null) {
    throw new TypeError('leafForSolve requires { salt, nonce }');
  }
  return sha256(Buffer.from(`leaf:${salt}:${nonce}:${signature}`, 'utf8'));
}

// internal nodes use a different prefix, so a leaf can never be
// reinterpreted as a node (or vice versa)
function nodeHash(left, right) {
  return sha256(Buffer.concat([Buffer.from('node:', 'utf8'), left, right]));
}
Enter fullscreen mode Exit fullscreen mode

The Merkle core is deliberately pure and synchronous. Append, seal, root, inclusion proof, verify — no network, no heavy dependencies, fully unit-testable offline. The only networked piece, the OpenTimestamps calendar submission, lazy-loads javascript-opentimestamps so the core stays dependency-light and the network call is exercised by a live script, not by CI.

Wiring it into a live gate

The collector is the glue that decides when to seal, where to persist the proof, and how to be fed from a running server. It's on npm, so grab it and the rate-limiter it hangs off:

npm install @powforge/solve-witness @powforge/ratelimit
Enter fullscreen mode Exit fullscreen mode

It drops straight onto the rate-limiter's onSolve hook:

const { createWitnessCollector } = require('@powforge/solve-witness/collector');
const { powRateLimit } = require('@powforge/ratelimit');

const witness = createWitnessCollector({
  logPath: '/srv/data/solve-witness-log.jsonl',
});
witness.start();                       // seal every 10 min in the background

app.use('/api', powRateLimit({
  difficulty: 20,
  onSolve: witness.onSolve,            // every verified solve gets witnessed
}));
Enter fullscreen mode Exit fullscreen mode

Two design decisions matter because this sits on a request hot path. First, onSolve is synchronous and cannot throw into the request — a malformed solve is dropped and logged, never propagated to the caller. Second, sealing swaps in a fresh batch before the slow, networked OTS call, so solves arriving mid-stamp land in the next window and are never lost. The seal is also re-entrancy guarded, so a slow calendar submission can't overlap itself.

Does the whole chain actually hold together? Yes, end to end

The part I care about is not "the unit tests pass." It is: a real running service, a real solve, a real OTS proof. So there's a demo server that boots an Express app with the real middleware wired to the real collector, then drives itself:

  1. An unauthenticated GET /api/protected returns a 429 with a PoW challenge.
  2. A client grinds a real SHA-256 nonce until it clears the difficulty threshold.
  3. It retries with the X-PoW-Proof header, the middleware verifies it, serves the route, and fires onSolve.
  4. onSolve folds that exact solve into the live batch.
  5. POST /admin/seal seals the window and submits the Merkle root to a real OpenTimestamps calendar.
  6. The client rebuilds the leaf from its own (salt, nonce, signature) and verifies Merkle inclusion under the sealed witness_hash the OTS proof commits to.

On the last run that produced a genuine 665-byte .ots proof carrying pending calendar attestations — the real serialized OpenTimestamps format, not a stub. Once the calendars aggregate and the anchoring transaction confirms (hours, not seconds), upgradeProof() pulls the Bitcoin attestation and the proof tells you which block height attests to the root.

What this is not — yet

This is infrastructure, and I want to be exact about where the line is.

What exists and is verified: the Merkle batch core, the onSolve wiring into the rate-limiter, live OTS calendar submission, and an end-to-end demo that produces a real proof. What does not exist: any customer-facing product around it. There is no payment flow, no sats price on witnessing, no hosted endpoint where you submit solves and get certificates back, no dashboard. The batch persists to a JSONL log and writes .ots proof files to disk. That is the whole surface today.

So this is not a finished service you can point users at. It is the primitive that a service would be built on. I am publishing it at the primitive stage on purpose, because the interesting claim is the mechanism — one Bitcoin timestamp making an arbitrary number of PoW solves independently provable — and that claim is either sound or it isn't, regardless of whether there's a checkout page in front of it.

If you wire it up and the inclusion proof verifies, or if it doesn't, I want to hear about it. The package is MIT. The OpenTimestamps proof format is an open standard and the calendars are run by the OpenTimestamps project and independent parties, not by me.

Top comments (0)