nip-13: mine proof async in worker
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details

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

@ -20,6 +20,7 @@ export const options = {
'src/main.css',
'src/main.js',
'src/manifest.json',
'src/worker.js',
],
outdir: 'dist',
//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,4 +1,4 @@
import {relayPool, generatePrivateKey, getEventHash, getPublicKey, signEvent} from 'nostr-tools';
import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools';
import {elem, parseTextContent} from './domutil.js';
import {dateTime, formatTime} from './timeutil.js';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
@ -41,6 +41,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey;
})();
// arbitrary difficulty, still experimenting.
// measured empirically, takes N sec on average to mine a text note event.
const difficulty = 16;
const subList = [];
@ -764,14 +766,11 @@ function hideNewMessage(hide) {
async function upvote(eventId, relay) {
const privatekey = localStorage.getItem('private_key');
const newReaction = powEvent({
const newReaction = await powEvent({
kind: 7,
pubkey, // TODO: lib could check that this is the pubkey of the key to sign with
content: '+',
tags: [
['nonce', '0', `${difficulty}`],
['e', eventId, relay, 'reply'],
],
tags: [['e', eventId, relay, 'reply']],
created_at: Math.floor(Date.now() * 0.001),
}, difficulty);
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 tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : [];
const newEvent = powEvent({
const newEvent = await powEvent({
kind: 1,
content,
pubkey,
tags: [['nonce', '0', `${difficulty}`], ...tags],
tags,
created_at: Math.floor(Date.now() * 0.001),
}, difficulty);
const sig = await signEvent(newEvent, privatekey).catch(onSendError);
@ -946,11 +945,11 @@ profileForm.addEventListener('submit', async (e) => {
const form = new FormData(profileForm);
const privatekey = localStorage.getItem('private_key');
const newProfile = powEvent({
const newProfile = await powEvent({
kind: 0,
pubkey,
content: JSON.stringify(Object.fromEntries(form)),
tags: [['nonce', '0', `${difficulty}`]],
tags: [],
created_at: Math.floor(Date.now() * 0.001),
}, difficulty);
const sig = await signEvent(newProfile, privatekey).catch(console.error);
@ -980,17 +979,36 @@ function validatePow(evt) {
return evt.id.substring(0, difficulty2 / 4) === '00'.repeat(difficulty2 / 8);
}
function powEvent(newEvent, difficulty) {
const chars = difficulty / 8;
let n = Number(newEvent.tags[0][1]) + 1;
// const until = Date.now() + 15000;
console.time('pow');
while (true/*Date.now() < until*/) {
newEvent.tags[0][1] = `${n++}`;
const id = getEventHash(newEvent, privatekey);
if (id.substring(0, chars * 2) === '00'.repeat(chars)) {
console.timeEnd('pow');
return newEvent;
}
/**
* 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) {
const privatekey = localStorage.getItem('private_key');
return new Promise((resolve, reject) => {
// const webWorkerURL = URL.createObjectURL(new Blob(['(', powEventWorker(), ')()'], {type: 'application/javascript'}));
// const worker = new Worker(webWorkerURL);
const worker = new Worker('./worker.js');
worker.onmessage = (msg) => {
worker.terminate();
if (msg.data.error) {
reject(msg.data.error);
} else {
resolve(msg.data.event);
}
};
worker.onerror = (err) => {
worker.terminate();
reject(err);
};
worker.postMessage({event: evt, difficulty, privatekey, timeout});
// URL.revokeObjectURL(webWorkerURL); // one-time worker; no longer need the URL obj
});
}

@ -0,0 +1,48 @@
import {getEventHash} from 'nostr-tools';
import {zeroLeadingBitsCount} from './cryptoutils.js';
function mine(event, difficulty, privatekey, 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, privatekey);
if (zeroLeadingBitsCount(id) === difficulty) {
console.log(event.tags[0][1], id);
console.timeEnd('pow');
return event;
}
}
}
addEventListener('message', async (msg) => {
const {
difficulty,
event,
privatekey,
timeout,
} = msg.data;
try {
const minedEvent = mine(event, difficulty, privatekey, timeout);
postMessage({event: minedEvent});
} catch (err) {
postMessage({error: err});
}
});
Loading…
Cancel
Save