NAP-POW / PR #39
NIP-13 Proof-of-Work Miner
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-POW
NIP-13 Proof-of-Work Miner
draft
NAP ID: NAP-POW
Domain: pow
Depends:
identity— layering · required — the runtime stamps the user pubkey (identity domain) into every mined eventrelay— capability · optional — amineresult may be published viarelayoutbox— capability · optional —mineAndPublishuses outbox-aware fanout; same publish-consent policy asoutboxWeb binding (NIP-5D):window.napplet.pow; domain presence signals availability.
Description
NAP-POW provides napplets with a shell-mediated NIP-13 proof-of-work miner: the napplet hands over an event template and a target difficulty, the runtime mines a nonce until the event id carries the required number of leading zero bits, and returns the mined event. A napplet could grind nonces itself, but it runs in a single sandboxed context where a tight hashing loop blocks its own UI; the runtime can spread the search across 1..n web workers (the web binding) or native threads (other bindings), report aggregate and per-worker progress, and arbitrate CPU across competing jobs. Difficulty mining is a long-running, cancellable, pausable operation, so NAP-POW models each request as a job the napplet can observe and control, not a single blocking call.
Mining commits the user's pubkey and created_at into the event id (NIP-13), so the runtime — which owns identity (NAP-IDENTITY) and signing — stamps those fields before the search begins; the napplet supplies only kind, content, and tags. Two entry points cover the common cases: mine returns the mined event to the napplet for it to use or publish later, and mineAndPublish mines, signs, and publishes the result through the shell's outbox-aware fanout in one step. As with all NAPs, napplets never receive signing keys; signing and publishing remain shell operations.
API Surface
| Operation | Parameters | Result | Wire |
|---|---|---|---|
mine |
template (EventTemplate), target (uint), optional opts (PowOptions) |
PowJob handle |
pow.mine plus push messages |
mineAndPublish |
template (EventTemplate), target (uint), optional opts (PowOptions) |
PowJob handle |
pow.mineAndPublish plus push messages |
queue |
none | list of PowJobSummary |
pow.queue / pow.queue.result |
job |
jobId (tstr) |
PowProgress |
pow.job / pow.job.result |
hashrate |
none | PowHashrate |
pow.hashrate / pow.hashrate.result |
cancel |
jobId (tstr) |
bool |
pow.cancel / pow.cancel.result |
pause |
optional jobId (tstr) |
none | pow.pause / pow.pause.result |
resume |
optional jobId (tstr) |
none | pow.resume / pow.resume.result |
formatHashRate |
hashesPerSecond (number) |
tstr |
local helper; no wire message |
PowJob is a local handle for jobId and target. It exposes started,
completed, event listeners for state, progress, done, and error, and
local cancel, pause, and resume helpers backed by the matching operations.
Schemas
Primitive references:
| Name | Meaning |
|---|---|
NostrEvent |
NIP-01 signed or unsigned event object. |
NostrTag |
NIP-01 event tag list. |
Enumerations:
| Name | Values |
|---|---|
PowState |
queued, mining, paused, done, cancelled, error |
PowJobSummary.mode |
mine, mineAndPublish |
EventTemplate:
| Field | Type | Required | Description |
|---|---|---|---|
kind |
uint |
yes | Event kind to mine. |
content |
tstr |
yes | Event content to mine. |
tags |
list of NostrTag |
no | Event tags to mine. |
created_at |
uint |
no | Requested event timestamp. |
PowOptions:
| Field | Type | Required | Description |
|---|---|---|---|
workers |
uint |
no | Requested worker count. |
priority |
int |
no | Scheduling priority. |
timeoutMs |
uint |
no | Mining timeout in milliseconds. |
commitCreatedAt |
bool |
no | Whether the runtime should commit created_at at mining start. |
PowStateChange:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
state |
PowState |
yes | New job state. |
position |
uint |
no | Queue position when state is queued. |
WorkerStat:
| Field | Type | Required | Description |
|---|---|---|---|
workerId |
uint |
yes | Worker identifier. |
bestPow |
uint |
yes | Best leading-zero-bit count found by this worker. |
hashes |
uint |
yes | Hashes attempted by this worker. |
hashRate |
number |
yes | Worker hash rate in hashes per second. |
PowProgress:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
target |
uint |
yes | Target leading-zero-bit count. |
state |
PowState |
yes | Current job state. |
bestPow |
uint |
yes | Best leading-zero-bit count found so far. |
bestNonce |
tstr |
no | Nonce that produced bestPow. |
hashes |
uint |
yes | Total hashes attempted for this job. |
hashRate |
number |
yes | Job hash rate in hashes per second. |
workers |
list of WorkerStat |
yes | Per-worker progress. |
elapsedMs |
uint |
yes | Elapsed mining time in milliseconds. |
PowWorkerHashrate:
| Field | Type | Required | Description |
|---|---|---|---|
workerId |
uint |
yes | Worker identifier. |
hashRate |
number |
yes | Worker hash rate in hashes per second. |
PowJobHashrate:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
hashRate |
number |
yes | Job hash rate in hashes per second. |
PowHashrate:
| Field | Type | Required | Description |
|---|---|---|---|
hashRate |
number |
yes | Total hash rate in hashes per second. |
workers |
uint |
yes | Active worker count. |
perWorker |
list of PowWorkerHashrate |
yes | Per-worker hash rates. |
byJob |
list of PowJobHashrate |
yes | Per-job hash rates. |
PowResult:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
ok |
bool |
yes | Whether mining succeeded. |
event |
NostrEvent |
yes | Mined event. |
pow |
uint |
yes | Leading-zero-bit count achieved. |
nonce |
tstr |
yes | Nonce value that satisfied the target. |
hashes |
uint |
yes | Total hashes attempted. |
elapsedMs |
uint |
yes | Elapsed mining time in milliseconds. |
published |
PowPublishResult |
no | Publish outcome for mineAndPublish. |
error |
tstr |
no | Error reason when mining or publishing failed. |
PowPublishResult:
| Field | Type | Required | Description |
|---|---|---|---|
eventId |
tstr |
yes | Published event ID. |
relays |
map of relay URL tstr to bool |
yes | Per-relay publish result. |
PowJobSummary:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
target |
uint |
yes | Target leading-zero-bit count. |
state |
PowState |
yes | Current job state. |
priority |
int |
yes | Scheduling priority. |
position |
uint |
no | Queue position when queued. |
bestPow |
uint |
yes | Best leading-zero-bit count found so far. |
hashRate |
number |
yes | Job hash rate in hashes per second. |
kind |
uint |
yes | Event kind being mined. |
mode |
mine or mineAndPublish |
yes | Job mode. |
PowError:
| Field | Type | Required | Description |
|---|---|---|---|
jobId |
tstr |
yes | Mining job identifier. |
error |
tstr |
yes | Error reason. |
mine(template, target, opts?) — Submits a mining job and returns a PowJob handle immediately. The job is enqueued: depending on the shell's concurrency policy it begins mining at once or waits behind other jobs, reporting state: "queued" (with a queue position) until a worker slot frees. job.started resolves when mining actually begins; job.completed resolves with the mined unsigned event once target is met. The handle is valid the moment it is returned — cancel, pause, and queue inspection all work while the job is still queued.
When mining begins (not at submit time), the shell stamps the user pubkey and created_at, replaces any nonce tag with ["nonce", "<nonce>", "<target>"], and searches for a nonce whose event id has at least target leading zero bits, fanned across opts.workers workers (capped by shell policy). Stamping created_at at mining start — not enqueue — keeps a job that waited in the queue from committing a stale timestamp; commitCreatedAt: true (default) then freezes it for the whole search. opts.timeoutMs bounds mining time only and does not count queue wait. The mined event is returned unsigned; the napplet may publish it through NAP-RELAY/NAP-OUTBOX, and the shell MUST sign the committed event verbatim, since altering pubkey, created_at, tags, content, or kind would invalidate the proof of work.
mineAndPublish(template, target, opts?) — Identical lifecycle and queue semantics to mine (submit → queued → mining → done), but on success the shell signs the mined event and publishes it using outbox-aware fanout (the user's write relays, plus recipient inbox relays for directed events). There is no relay argument: relay selection is shell policy, identical to NAP-OUTBOX publish. PowResult.published carries the fanout outcome; job.completed rejects with "publish failed"/"publish denied" if the proof of work succeeded but publishing did not.
queue() — Returns a summary of all of the napplet's tracked jobs (queued, mining, paused) including each job's priority and, for queued jobs, its position. For a miner/status napplet building a queue view. Ordering reflects the shell's current schedule.
job(jobId) — Returns a one-shot PowProgress snapshot for a single job: its target, bestPow, and per-worker bestPow / hashRate.
hashrate() — Returns the live miner-wide hash rate: the total, the per-worker breakdown, and the per-job breakdown. Rates are raw hashes per second; scaling to kH/s, MH/s, GH/s is a display concern — formatHashRate is local SDK sugar and sends no message.
cancel(jobId) — Stops a job and frees its workers. Resolves true if a job was running. The job's completed promise rejects with "cancelled".
pause(jobId?) / resume(jobId?) — Suspend or continue a job, preserving its best-nonce-so-far and hash count. With jobId omitted, the whole miner pauses/resumes (e.g. to yield the CPU). A paused job reports state: "paused" and hashRate: 0.
Wire Protocol
pow.* messages use the NIP-5D wire format ({ "type": "domain.action", ...payload }).
| Type | Direction | Payload fields |
|---|---|---|
pow.mine |
napplet -> shell | id, jobId, template, target, options? |
pow.mine.result |
shell -> napplet | id, jobId, accepted, state, position?, error? |
pow.mineAndPublish |
napplet -> shell | id, jobId, template, target, options? |
pow.mineAndPublish.result |
shell -> napplet | id, jobId, accepted, state, position?, error? |
pow.state |
shell -> napplet | jobId, state, position? |
pow.progress |
shell -> napplet | jobId, progress |
pow.done |
shell -> napplet | jobId, result |
pow.error |
shell -> napplet | jobId, error |
pow.queue |
napplet -> shell | id |
pow.queue.result |
shell -> napplet | id, jobs |
pow.job |
napplet -> shell | id, jobId |
pow.job.result |
shell -> napplet | id, progress, error? |
pow.hashrate |
napplet -> shell | id |
pow.hashrate.result |
shell -> napplet | id, hashrate |
pow.cancel |
napplet -> shell | id, jobId |
pow.cancel.result |
shell -> napplet | id, jobId, cancelled, error? |
pow.pause |
napplet -> shell | id, jobId? |
pow.pause.result |
shell -> napplet | id, jobId?, error? |
pow.resume |
napplet -> shell | id, jobId? |
pow.resume.result |
shell -> napplet | id, jobId?, error? |
Key design notes:
- The napplet generates
jobIdand uses it to correlate the shell'spow.progress/pow.done/pow.errorpush messages, exactly as NAP-OUTBOX usessubId. The shell rejects a duplicatejobIdwithaccepted: false. pow.mine/pow.mineAndPublishare acknowledged by their.result, which reports the job's initialstate—"queued"(withposition) if it is waiting, or"mining"if it started immediately. The mined event arrives later inpow.done, not in the ack.pow.stateis a shell push (noid) emitted on every lifecycle transition (queued→mining→paused/done/cancelled/error), carrying the newstateand, while queued, the currentposition. It is how a napplet observes a queued job starting without polling;job.startedresolves on the first transition to"mining".created_atis committed when the job starts mining, not when it is submitted, so queue wait never produces a stale timestamp.options.timeoutMslikewise measures mining time only.options.priorityis a scheduling hint (higher runs sooner); the shell MAY honor or ignore it. It never lets one napplet preempt another's jobs beyond shell policy.pow.progressis a shell push with noid. The shell SHOULD throttle it (e.g. at most a few per second) regardless of hash rate.targetis the required leading-zero-bit count (NIP-13);bestPow/poware achieved leading-zero bits of the event id.pause/resumewithoutjobIdact on the whole miner.
Examples
Mine a note to difficulty 21 with 4 workers — enqueued behind another job, then started and read back:
-> {
"type": "pow.mine",
"id": "m1",
"jobId": "job-1",
"template": { "kind": 1, "content": "gm", "tags": [] },
"target": 21,
"options": { "workers": 4, "priority": 0 }
}
<- { "type": "pow.mine.result", "id": "m1", "jobId": "job-1", "accepted": true, "state": "queued", "position": 1 }
<- { "type": "pow.state", "jobId": "job-1", "state": "mining" }
<- {
"type": "pow.progress",
"jobId": "job-1",
"progress": {
"jobId": "job-1", "target": 21, "state": "mining",
"bestPow": 18, "bestNonce": "773201", "hashes": 2310000, "hashRate": 412000,
"workers": [
{ "workerId": 0, "bestPow": 18, "hashes": 590000, "hashRate": 104000 },
{ "workerId": 1, "bestPow": 16, "hashes": 580000, "hashRate": 103000 },
{ "workerId": 2, "bestPow": 17, "hashes": 575000, "hashRate": 102500 },
{ "workerId": 3, "bestPow": 15, "hashes": 565000, "hashRate": 102500 }
],
"elapsedMs": 5600
}
}
<- {
"type": "pow.done",
"jobId": "job-1",
"result": {
"jobId": "job-1", "ok": true, "pow": 21, "nonce": "1099148",
"hashes": 4980000, "elapsedMs": 12100,
"event": {
"id": "000007abc…", "pubkey": "user…", "kind": 1, "content": "gm",
"tags": [["nonce", "1099148", "21"]], "created_at": 1234567890
}
}
}
Mine and publish (outbox-aware, no relay argument):
-> {
"type": "pow.mineAndPublish",
"id": "p1",
"jobId": "job-2",
"template": { "kind": 1, "content": "powered note", "tags": [] },
"target": 24
}
<- { "type": "pow.mineAndPublish.result", "id": "p1", "jobId": "job-2", "accepted": true, "state": "mining" }
<- {
"type": "pow.done",
"jobId": "job-2",
"result": {
"jobId": "job-2", "ok": true, "pow": 24, "nonce": "55230914",
"hashes": 31200000, "elapsedMs": 74000,
"event": { "id": "000000def…", "pubkey": "user…", "kind": 1, "content": "powered note", "tags": [["nonce", "55230914", "24"]], "created_at": 1234567999, "sig": "…" },
"published": { "eventId": "000000def…", "relays": { "wss://relay.example.com": true } }
}
}
Inspect the queue, then pause one job:
-> { "type": "pow.queue", "id": "q1" }
<- { "type": "pow.queue.result", "id": "q1",
"jobs": [
{ "jobId": "job-2", "target": 24, "state": "mining", "priority": 0, "bestPow": 22, "hashRate": 418000, "kind": 1, "mode": "mineAndPublish" },
{ "jobId": "job-3", "target": 28, "state": "queued", "priority": 5, "position": 0, "bestPow": 0, "hashRate": 0, "kind": 1, "mode": "mine" }
] }
-> { "type": "pow.pause", "id": "ps1", "jobId": "job-2" }
<- { "type": "pow.pause.result", "id": "ps1", "jobId": "job-2" }
Live miner-wide hash rate:
-> { "type": "pow.hashrate", "id": "h1" }
<- { "type": "pow.hashrate.result", "id": "h1",
"hashrate": {
"hashRate": 824000, "workers": 8,
"perWorker": [ { "workerId": 0, "hashRate": 103000 } ],
"byJob": [ { "jobId": "job-1", "hashRate": 412000 }, { "jobId": "job-2", "hashRate": 412000 } ]
} }
Error Handling
Result and pow.error messages MAY include error. Common errors: "duplicate jobId", "invalid target" (negative, or above the shell's cap), "invalid template", "timeout" (target not met within timeoutMs), "cancelled", "unknown jobId", "queue full", "publish denied" and "publish failed" (mineAndPublish only), and "policy denied".
A timeout or cancel ends the job with state: "error" / "cancelled", emits pow.error, and rejects the handle's completed promise. Best-effort progress emitted before the failure remains valid.
Shell Behavior
- The shell MAY queue submitted jobs and schedule them under its own concurrency policy; it MUST accept a job into the queue (or reject it with
accepted: false) synchronously in the.result, reporting the initialstateand, when"queued", theposition. - The shell MUST emit
pow.stateon every lifecycle transition so a queued job's start is observable without polling, and MUST stamp the committedpubkey(from user identity) andcreated_atat the moment mining begins — not at submit — then mine over that exact serialization;commitCreatedAt: true(default) freezescreated_atfor the whole search. - The shell SHOULD honor
options.priorityas an ordering hint where its policy allows, but MUST NOT let priority become a way for one napplet to starve or preempt another's jobs beyond policy. - The shell MUST insert or replace the
["nonce", "<nonce>", "<target>"]tag per NIP-13 and MUST NOT return a result whose id has fewer thantargetleading zero bits. - The shell MUST treat
options.workersas a hint and MAY clamp it to available cores and per-napplet policy. - The shell MUST respond to every request with a result carrying the same
id, and MUST key every push (pow.progress/pow.done/pow.error) to the napplet-suppliedjobId. - The shell MUST sign mined events only through its normal signing path; for a previously-
mined event submitted for publish, the shell MUST sign it verbatim without re-stamping committed fields, or reject it. - For
mineAndPublish, the shell MUST publish using outbox-aware fanout (no napplet-supplied relays) and SHOULD apply the same publish consent policy as NAP-OUTBOX. - The shell SHOULD throttle
pow.progressand SHOULD preserve best-nonce and hash counters acrosspause/resume. - The shell MAY queue jobs, cap concurrent jobs, total workers, and maximum
target, and MAY enforce ACL checks on mine, publish, and queue inspection.
Security Considerations
- Mining is unbounded CPU work, and difficulty cost is exponential in
target. The shell MUST cap maximumtarget, concurrent jobs, and total workers, and SHOULD enforcetimeoutMs, so a napplet cannot pin the device or drain a battery by requesting an astronomically hard target or flooding the queue. - Signing and publishing remain shell-mediated; napplets never receive keys.
minereturns an unsigned event — the proof of work is public and carries no secret — and signing happens only at publish time, gated by shell consent. - Proof of work commits
pubkeyandcreated_at. The shell MUST preserve every committed field when signing a previously-mined event; a shell that re-stampscreated_ator reorders tags silently destroys the proof of work the napplet paid for. - Hash-rate and queue telemetry expose device capability and user activity. Shells MAY coarsen or rate-limit
pow.hashrate/pow.queuefor untrusted napplets. - A napplet could request mining of content it intends to attribute to the user. Because the id commits the user pubkey,
mineAndPublishMUST run the same content/consent checks the shell applies to any signed publish.
References
Implementations
- (none yet)
Changelog
f01fb31- Introduced NAP-POW as a runtime-owned NIP-13 proof-of-work miner surface.fbedfa2- Adopted injected-domain availability and linked the current upstream web binding.