Proof of Work
NIP-13 implements proof-of-work on Nostr by mining event IDs with leading zero bits, making spam economically infeasible while allowing legitimate use.
NIP-13: Proof of Work
Status: Draft Authors: jb55, cameri Category: Relay Features
Overview
NIP-13 defines proof-of-work (PoW) for Nostr events, requiring computational effort to publish, making spam economically infeasible.
Core Concept:
- Mine event IDs with leading zero bits
- Higher difficulty = more computational work
- Relays can require minimum PoW
- Spam becomes expensive to produce
How It Works:
- Add
noncetag to event - Increment nonce and recalculate ID
- Repeat until ID has required leading zeros
- Publish event with proof
Benefits:
- ✅ Spam prevention: Makes bulk posting expensive
- ✅ Sybil resistance: Rate limiting without accounts
- ✅ Fair access: Anyone can post with enough work
- ✅ No registration: No accounts or credentials needed
- ✅ Relay protection: Filters low-quality content
Status: Draft (optional, some relays require it)
Why Proof of Work Matters
Spam Problem
Without PoW:
- Free to spam millions of events
- Relays overwhelmed with junk
- No cost to bad actors
- Rate limiting requires authentication
With PoW:
- Each event requires computational work
- Bulk spam becomes prohibitively expensive
- Legitimate users barely notice cost
- No authentication needed
Real-World Impact
Without PoW:
- 1,000,000 spam events = seconds to generate
With PoW (difficulty 20):
- 1,000,000 spam events = ~11,574 days of CPU time
- Legitimate post = ~1 second
Result: Spam becomes 1,000,000× more expensive
How It Works
Event ID Mining
Nostr event IDs are SHA-256 hashes. PoW requires mining an ID with leading zero bits:
Difficulty 0: (any ID)
Difficulty 8: 00xxxxxx... (8 leading zero bits)
Difficulty 16: 0000xxxx... (16 leading zero bits)
Difficulty 20: 00000xxx... (20 leading zero bits)
Difficulty 32: 00000000... (32 leading zero bits)
Mining Process:
- Create event with
noncetag - Calculate event ID (SHA-256 hash)
- Count leading zero bits in ID
- If insufficient, increment nonce and repeat
Mining Implementation
Basic PoW Miner
import { getEventHash } from 'nostr-tools';
function mineEvent(event, targetDifficulty) {
let nonce = 0;
let minedEvent = { ...event };
while (true) {
// Add nonce tag
minedEvent.tags = event.tags.filter(t => t[0] !== 'nonce');
minedEvent.tags.push(['nonce', String(nonce), String(targetDifficulty)]);
// Calculate event ID
minedEvent.id = getEventHash(minedEvent);
// Check difficulty
const difficulty = countLeadingZeroBits(minedEvent.id);
if (difficulty >= targetDifficulty) {
console.log(`Found valid PoW after ${nonce} attempts`);
return minedEvent;
}
nonce++;
// Progress indicator
if (nonce % 10000 === 0) {
console.log(`Mining... ${nonce} attempts, best: ${difficulty} bits`);
}
}
}
function countLeadingZeroBits(hex) {
let count = 0;
for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16);
if (nibble === 0) {
count += 4;
} else {
// Count leading zeros in this nibble
count += Math.clz32(nibble) - 28;
break;
}
}
return count;
}
// Usage
const event = {
kind: 1,
content: "This post has proof of work!",
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: myPubkey
};
const minedEvent = mineEvent(event, 20); // Target 20 bits
const signedEvent = signEvent(minedEvent, privateKey);
Optimized Mining
class PoWMiner {
constructor(targetDifficulty) {
this.targetDifficulty = targetDifficulty;
this.bestDifficulty = 0;
}
mine(event) {
const startTime = Date.now();
let nonce = 0;
let attempts = 0;
while (true) {
// Update nonce tag
const tags = event.tags.filter(t => t[0] !== 'nonce');
tags.push(['nonce', String(nonce), String(this.targetDifficulty)]);
const testEvent = {
...event,
tags: tags
};
testEvent.id = getEventHash(testEvent);
const difficulty = this.countLeadingZeroBits(testEvent.id);
attempts++;
// Track best found
if (difficulty > this.bestDifficulty) {
this.bestDifficulty = difficulty;
console.log(`New best: ${difficulty} bits (nonce: ${nonce})`);
}
// Check if target met
if (difficulty >= this.targetDifficulty) {
const duration = (Date.now() - startTime) / 1000;
const hashRate = attempts / duration;
console.log(`✅ PoW found! Difficulty: ${difficulty}`);
console.log(`Time: ${duration.toFixed(2)}s, Attempts: ${attempts}`);
console.log(`Hash rate: ${hashRate.toFixed(0)} H/s`);
return testEvent;
}
nonce++;
}
}
countLeadingZeroBits(hexStr) {
let bits = 0;
for (let i = 0; i < hexStr.length; i++) {
const nibble = parseInt(hexStr[i], 16);
if (nibble === 0) {
bits += 4;
} else {
// Count leading zeros in nibble
const binary = nibble.toString(2).padStart(4, '0');
for (const bit of binary) {
if (bit === '0') bits++;
else return bits;
}
}
}
return bits;
}
}
// Usage
const miner = new PoWMiner(20);
const minedEvent = miner.mine(myEvent);
Web Worker Mining
For better UX, mine in a background thread:
// pow-worker.js
self.onmessage = function(e) {
const { event, targetDifficulty } = e.data;
const minedEvent = mineEvent(event, targetDifficulty);
self.postMessage({ success: true, event: minedEvent });
};
function mineEvent(event, targetDifficulty) {
// ... mining implementation ...
}
// main.js
function mineEventAsync(event, targetDifficulty) {
return new Promise((resolve, reject) => {
const worker = new Worker('pow-worker.js');
worker.onmessage = (e) => {
if (e.data.success) {
resolve(e.data.event);
worker.terminate();
}
};
worker.onerror = (error) => {
reject(error);
worker.terminate();
};
worker.postMessage({ event, targetDifficulty });
});
}
// Usage (non-blocking UI)
async function postWithPoW(content) {
console.log("Mining PoW...");
const event = {
kind: 1,
content: content,
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: myPubkey
};
const mined = await mineEventAsync(event, 20);
const signed = signEvent(mined, privateKey);
await publishToRelays(signed);
console.log("Posted with PoW!");
}
Verification
Verify PoW on Event
function verifyPoW(event, minimumDifficulty) {
// Find nonce tag
const nonceTag = event.tags.find(t => t[0] === 'nonce');
if (!nonceTag) {
return { valid: false, difficulty: 0, reason: "No nonce tag" };
}
const [_, nonce, targetDiff] = nonceTag;
// Verify event ID is correct
const calculatedId = getEventHash(event);
if (calculatedId !== event.id) {
return { valid: false, difficulty: 0, reason: "Invalid event ID" };
}
// Count difficulty
const difficulty = countLeadingZeroBits(event.id);
// Check if meets minimum
if (difficulty < minimumDifficulty) {
return {
valid: false,
difficulty: difficulty,
reason: `Insufficient PoW (${difficulty} < ${minimumDifficulty})`
};
}
return { valid: true, difficulty: difficulty };
}
// Usage
const event = receivedEvent;
const result = verifyPoW(event, 20);
if (result.valid) {
console.log(`✅ Valid PoW: ${result.difficulty} bits`);
} else {
console.log(`❌ Invalid PoW: ${result.reason}`);
}
Relay Implementation
PoW-Required Relay
class PoWRelay {
constructor(minimumDifficulty = 16) {
this.minimumDifficulty = minimumDifficulty;
}
handleEvent(event, ws) {
// Verify PoW
const powResult = verifyPoW(event, this.minimumDifficulty);
if (!powResult.valid) {
// Reject event
ws.send(JSON.stringify([
"OK",
event.id,
false,
`pow: ${powResult.reason}`
]));
return;
}
// Log PoW quality
console.log(`Event ${event.id} has PoW: ${powResult.difficulty} bits`);
// Accept and store event
this.storeEvent(event);
ws.send(JSON.stringify([
"OK",
event.id,
true,
""
]));
}
getRequiredDifficulty(eventKind) {
// Different requirements per kind
const difficulties = {
1: 16, // Text notes
3: 20, // Contact lists (more expensive to spam)
4: 12, // DMs
7: 8 // Reactions (less important)
};
return difficulties[eventKind] || this.minimumDifficulty;
}
}
Advertise PoW Requirement
Relays should advertise PoW requirements in NIP-11:
{
"name": "PoW Relay",
"supported_nips": [1, 11, 13],
"limitation": {
"pow_difficulty": 16,
"pow_required": true
}
}
Difficulty Recommendations
By Event Type
const RECOMMENDED_DIFFICULTY = {
// Text notes
KIND_1: 16, // ~65,536 hashes (~0.1s)
// Contact lists
KIND_3: 20, // ~1,048,576 hashes (~1.5s)
// Encrypted DMs
KIND_4: 12, // ~4,096 hashes (~instant)
// Deletions
KIND_5: 20, // Prevent spam deletions
// Reactions
KIND_7: 8, // ~256 hashes (~instant)
// Channel messages
KIND_42: 16
};
function getRecommendedDifficulty(kind) {
return RECOMMENDED_DIFFICULTY[`KIND_${kind}`] || 16;
}
Performance Metrics
Average mining times on modern CPU (~1M hashes/sec):
| Difficulty | Avg Hashes | Avg Time | Use Case |
|---|---|---|---|
| 8 bits | 256 | <1ms | Reactions, likes |
| 12 bits | 4,096 | ~4ms | DMs, low-priority |
| 16 bits | 65,536 | ~65ms | Standard posts |
| 20 bits | 1,048,576 | ~1s | Important posts |
| 24 bits | 16,777,216 | ~17s | Very valuable posts |
| 28 bits | 268,435,456 | ~4.5min | Extreme protection |
Client Integration
Auto-PoW Feature
class NostrClient {
constructor(options = {}) {
this.autoPoW = options.autoPoW || false;
this.defaultDifficulty = options.defaultDifficulty || 16;
}
async publishEvent(event) {
// Check relay requirements
const relayDifficulty = await this.getRelayPoWRequirement();
const targetDifficulty = Math.max(
relayDifficulty,
this.autoPoW ? this.defaultDifficulty : 0
);
if (targetDifficulty > 0) {
console.log(`Mining PoW (difficulty: ${targetDifficulty})...`);
event = await this.mineEvent(event, targetDifficulty);
console.log("PoW complete!");
}
// Sign and publish
const signed = await this.signEvent(event);
return await this.publishToRelays(signed);
}
async getRelayPoWRequirement() {
// Fetch from NIP-11
try {
const info = await fetch(this.relayHttpUrl, {
headers: { 'Accept': 'application/nostr+json' }
});
const data = await info.json();
return data.limitation?.pow_difficulty || 0;
} catch {
return 0;
}
}
}
// Usage
const client = new NostrClient({
autoPoW: true,
defaultDifficulty: 16
});
await client.publishEvent({
kind: 1,
content: "This will automatically include PoW!",
tags: []
});
Progressive Mining UI
function MiningStatus({ difficulty }) {
const [progress, setProgress] = useState(0);
const [attempts, setAttempts] = useState(0);
const [hashRate, setHashRate] = useState(0);
// Estimated attempts for difficulty
const estimated = Math.pow(2, difficulty);
return (
<div className="mining-status">
<h3>Mining Proof of Work...</h3>
<div className="progress-bar">
<div
className="progress"
style={{ width: `${(attempts / estimated) * 100}%` }}
/>
</div>
<div className="stats">
<span>Difficulty: {difficulty} bits</span>
<span>Attempts: {attempts.toLocaleString()}</span>
<span>Hash rate: {hashRate.toLocaleString()} H/s</span>
<span>Est. time: {(estimated / hashRate).toFixed(1)}s</span>
</div>
</div>
);
}
Security Considerations
PoW Attacks
Pre-mining:
- Attacker mines many events in advance
- Mitigation: Check
created_attimestamp is recent
function verifyPoWTimestamp(event, maxAge = 3600) {
const now = Math.floor(Date.now() / 1000);
const age = now - event.created_at;
if (age > maxAge) {
return false; // Event too old (possibly pre-mined)
}
if (age < -300) {
return false; // Event in future (clock skew)
}
return true;
}
Mining Farms:
- Dedicated hardware mines spam
- Mitigation: Increase difficulty for suspicious patterns
Reuse:
- Reuse same mined event multiple times
- Mitigation: Track seen event IDs
Performance Optimization
Multi-threaded Mining
class ParallelMiner {
constructor(numWorkers = 4) {
this.workers = [];
for (let i = 0; i < numWorkers; i++) {
this.workers.push(new Worker('pow-worker.js'));
}
}
async mine(event, targetDifficulty) {
return new Promise((resolve) => {
let resolved = false;
// Divide nonce space among workers
this.workers.forEach((worker, index) => {
worker.onmessage = (e) => {
if (e.data.success && !resolved) {
resolved = true;
// Terminate all workers
this.workers.forEach(w => w.terminate());
resolve(e.data.event);
}
};
worker.postMessage({
event,
targetDifficulty,
nonceStart: index * 1000000,
nonceStep: this.workers.length
});
});
});
}
}
// 4x speedup with 4 cores
const miner = new ParallelMiner(4);
const minedEvent = await miner.mine(event, 20);
Client Support
Clients with PoW Support
- Damus - Automatic PoW for protected relays
- Amethyst - Configurable PoW mining
- Snort - Manual PoW enable
- Coracle - Relay-adaptive PoW
Relay Support
| Relay Software | PoW Support | Configurable | Default |
|---|---|---|---|
| nostream | ✅ | ✅ | Off |
| strfry | ✅ | ✅ | Off |
| nostr-rs-relay | ✅ | ✅ | Off |
| rnostr | ✅ | ✅ | Off |
Check our Relay Directory for PoW-enabled relays.
Common Questions
Does PoW prevent all spam?
No, but it makes spam economically infeasible at scale. A determined attacker with resources can still spam, but the cost is high.
Will PoW drain my battery?
Modern difficulty levels (16-20 bits) take <1 second on most devices. Minimal battery impact.
Can I disable PoW?
Yes, most clients make it optional. However, PoW-required relays won’t accept your events without it.
Why not just use rate limiting?
Rate limiting requires authentication (IP, accounts). PoW works without any registration.
What difficulty should relays require?
Start with 16 bits for regular posts. Increase if spam persists. Too high frustrates legitimate users.
Does PoW work on mobile?
Yes, but be mindful of battery. Use lower difficulties (12-16) for mobile.
Related NIPs
- NIP-01 - Basic protocol (event structure, IDs)
- NIP-11 - Relay information (advertising PoW requirements)
- NIP-42 - Authentication (alternative spam prevention)
Technical Specification
For the complete technical specification, see NIP-13 on GitHub.
Summary
NIP-13 implements proof-of-work on Nostr:
✅ Mine event IDs with leading zero bits ✅ Configurable difficulty per relay/event type ✅ Spam prevention through computational cost ✅ No authentication required - fair access ✅ Optional feature - relay discretion
Mining example:
const miner = new PoWMiner(20); // 20-bit difficulty
const minedEvent = miner.mine(event);
// Event ID now has 20 leading zero bits
console.log(minedEvent.id); // 00000abc...
Status: Draft - optional, growing adoption by spam-protected relays.
Best practice: Use 16 bits for normal posts, higher for valuable content.
Next Steps:
- Learn about URI schemes in NIP-21
- Explore text references in NIP-27
- Understand authentication in NIP-42
- Browse all NIPs in our reference
Last updated: September 2023 Official specification: GitHub
Client Support
This NIP is supported by the following clients: