nip-13: mine proof async in worker

OFF0 2 years ago
parent 984aee3f80
commit dd59c69170
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -20,6 +20,7 @@ export const options = {
'src/main.css', 'src/main.css',
'src/main.js', 'src/main.js',
'src/manifest.json', 'src/manifest.json',
'src/worker.js',
], ],
outdir: 'dist', outdir: 'dist',
//entryNames: '[name]-[hash]', TODO: replace urls in index.html with hashed paths //entryNames: '[name]-[hash]', TODO: replace urls in index.html with hashed paths

@ -0,0 +1,24 @@
/**
* evaluate the difficulty of hex32 according to nip-13.
* @param hex32 a string of 64 chars - 32 bytes in hex representation
*/
export const zeroLeadingBitsCount = (hex32) => {
let count = 0;
for (let i = 0; i < 64; i += 2) {
const hexbyte = hex32.slice(i, i + 2); // grab next byte
if (hexbyte == '00') {
count += 8;
continue;
}
// reached non-zero byte; count number of 0 bits in hexbyte
const bits = parseInt(hexbyte, 16).toString(2).padStart(8, '0');
for (let b = 0; b < 8; b++) {
if (bits[b] == '1' ) {
break; // reached non-zero bit; stop
}
count += 1;
}
break;
}
return count;
};

@ -1,10 +1,10 @@
import {relayPool, generatePrivateKey, getEventHash, getPublicKey, signEvent} from 'nostr-tools'; import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
import {zeroLeadingBitsCount} from './cryptoutils';
import {elem, parseTextContent} from './domutil.js'; import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js'; import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
const pool = relayPool(); const pool = relayPool();
pool.addRelay('wss://relay.nostr.info', {read: true, write: true}); pool.addRelay('wss://relay.nostr.info', {read: true, write: true});
pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true}); pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true});
// pool.addRelay('wss://relay.damus.io', {read: true, write: true}); // pool.addRelay('wss://relay.damus.io', {read: true, write: true});
@ -41,6 +41,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey; return pubkey;
})(); })();
// arbitrary difficulty, still experimenting.
// measured empirically, takes N sec on average to mine a text note event.
const difficulty = 16; const difficulty = 16;
const subList = []; const subList = [];
@ -764,14 +766,11 @@ function hideNewMessage(hide) {
async function upvote(eventId, relay) { async function upvote(eventId, relay) {
const privatekey = localStorage.getItem('private_key'); const privatekey = localStorage.getItem('private_key');
const newReaction = powEvent({ const newReaction = await powEvent({
kind: 7, kind: 7,
pubkey, // TODO: lib could check that this is the pubkey of the key to sign with pubkey, // TODO: lib could check that this is the pubkey of the key to sign with
content: '+', content: '+',
tags: [ tags: [['e', eventId, relay, 'reply']],
['nonce', '0', `${difficulty}`],
['e', eventId, relay, 'reply'],
],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty);
const sig = await signEvent(newReaction, privatekey).catch(console.error); const sig = await signEvent(newReaction, privatekey).catch(console.error);
@ -804,11 +803,11 @@ writeForm.addEventListener('submit', async (e) => {
} }
const replyTo = localStorage.getItem('reply_to'); const replyTo = localStorage.getItem('reply_to');
const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : []; const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : [];
const newEvent = powEvent({ const newEvent = await powEvent({
kind: 1, kind: 1,
content, content,
pubkey, pubkey,
tags: [['nonce', '0', `${difficulty}`], ...tags], tags,
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty);
const sig = await signEvent(newEvent, privatekey).catch(onSendError); const sig = await signEvent(newEvent, privatekey).catch(onSendError);
@ -946,11 +945,11 @@ profileForm.addEventListener('submit', async (e) => {
const form = new FormData(profileForm); const form = new FormData(profileForm);
const privatekey = localStorage.getItem('private_key'); const privatekey = localStorage.getItem('private_key');
const newProfile = powEvent({ const newProfile = await powEvent({
kind: 0, kind: 0,
pubkey, pubkey,
content: JSON.stringify(Object.fromEntries(form)), content: JSON.stringify(Object.fromEntries(form)),
tags: [['nonce', '0', `${difficulty}`]], tags: [],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}, difficulty); }, difficulty);
const sig = await signEvent(newProfile, privatekey).catch(console.error); const sig = await signEvent(newProfile, privatekey).catch(console.error);
@ -968,29 +967,46 @@ profileForm.addEventListener('submit', async (e) => {
} }
}); });
/**
* check that the event has the id has the desired number of leading zero bits
* @param {EventObj} evt to validate
* @returns boolean
*/
function validatePow(evt) { function validatePow(evt) {
const tag = evt.tags.find(tag => tag[0] === 'nonce'); const tag = evt.tags.find(tag => tag[0] === 'nonce');
if (!tag) { if (!tag) {
return false; return false;
} }
const [, , difficulty2] = tag; const [, , difficultyCommitment] = tag;
if (difficulty2 < 16) { return zeroLeadingBitsCount(evt.id) === difficultyCommitment;
return false; }
}
return evt.id.substring(0, difficulty2 / 4) === '00'.repeat(difficulty2 / 8); /**
} * run proof of work in a worker until at least the specified difficulty.
* if succcessful, the returned event contains the 'nonce' tag
* and the updated created_at timestamp.
*
* powEvent returns a rejected promise if the funtion runs for longer than timeout.
* a zero timeout makes mineEvent run without a time limit.
*/
function powEvent(evt, difficulty, timeout) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.onmessage = (msg) => {
worker.terminate();
if (msg.data.error) {
reject(msg.data.error);
} else {
resolve(msg.data.event);
}
};
function powEvent(newEvent, difficulty) { worker.onerror = (err) => {
const chars = difficulty / 8; worker.terminate();
let n = Number(newEvent.tags[0][1]) + 1; reject(err);
// const until = Date.now() + 15000; };
console.time('pow');
while (true/*Date.now() < until*/) { worker.postMessage({event: evt, difficulty, timeout});
newEvent.tags[0][1] = `${n++}`; });
const id = getEventHash(newEvent, privatekey);
if (id.substring(0, chars * 2) === '00'.repeat(chars)) {
console.timeEnd('pow');
return newEvent;
}
}
} }

@ -0,0 +1,47 @@
import {getEventHash} from 'nostr-tools';
import {zeroLeadingBitsCount} from './cryptoutils.js';
function mine(event, difficulty, timeout) {
const max = 256; // arbitrary
if (!Number.isInteger(difficulty) || difficulty < 0 || difficulty > max) {
throw new Error(`difficulty must be an integer between 0 and ${max}`);
}
// continue with mining
let n = BigInt(0);
event.tags.unshift(['nonce', n.toString(), `${difficulty}`]);
const start = Math.floor(Date.now() * 0.001);
console.time('pow');
while (true) {
const now = Math.floor(Date.now() * 0.001);
// if (now > start + 15) {
// console.timeEnd('pow');
// return false;
// }
if (now !== event.created_at) {
event.created_at = now;
// n = BigInt(0); // could reset nonce as we have a new timestamp
}
event.tags[0][1] = (++n).toString();
const id = getEventHash(event);
if (zeroLeadingBitsCount(id) === difficulty) {
console.log(event.tags[0][1], id);
console.timeEnd('pow');
return event;
}
}
}
addEventListener('message', async (msg) => {
const {
difficulty,
event,
timeout,
} = msg.data;
try {
const minedEvent = mine(event, difficulty, timeout);
postMessage({event: minedEvent});
} catch (err) {
postMessage({error: err});
}
});
Loading…
Cancel
Save