From 0293aee24793a31525867a222d8a2c0a49ac6bb5 Mon Sep 17 00:00:00 2001 From: OFF0 Date: Sat, 24 Dec 2022 00:02:57 +0100 Subject: [PATCH] nip-13: add pow and only invoke noxy in events with valid work added pow to text notes, reactions and metadata events. noxy is now only used if event has valid work proof. --- src/cards.css | 2 -- src/main.js | 64 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/cards.css b/src/cards.css index 2a407b4..c3a7a58 100644 --- a/src/cards.css +++ b/src/cards.css @@ -158,6 +158,4 @@ max-width: 48rem; padding: 1.5rem 1.8rem; width: 100%; - /* TODO: revert when things calm down or we find an alternative */ - display: none; } diff --git a/src/main.js b/src/main.js index e223e7c..cdb2b44 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import {relayPool, generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools'; +import {relayPool, generatePrivateKey, getEventHash, 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/ @@ -40,6 +40,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => { return pubkey; })(); +const difficulty = 16; + const subList = []; const unSubAll = () => { subList.forEach(sub => sub.unsub()); @@ -161,9 +163,9 @@ function renderProfile(evt, relay) { if (content) { profileAbout.textContent = content.about; profileName.textContent = content.name; - const noxyImg = getNoxyUrl('data', content.picture, evt.id, relay); + const noxyImg = validatePow(evt) && getNoxyUrl('data', content.picture, evt.id, relay); if (noxyImg) { - profileImage.setAttribute('src', getNoxyUrl('data', noxyImg, evt.id, relay)); + profileImage.setAttribute('src', noxyImg); profileImage.hidden = false; } } @@ -450,7 +452,7 @@ function createTextNote(evt, relay) { ]), elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [ ...content, - firstLink ? linkPreview(firstLink, evt.id, relay) : '' + (firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : '', ]), elem('button', { className: 'btn-inline', name: 'star', type: 'button', @@ -626,7 +628,7 @@ function setMetadata(evt, relay, content) { } } // update profile images - if (user.picture) { + if (user.picture && validatePow(evt)) { document.body .querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`) .forEach(canvas => (canvas.parentNode.replaceChild(elem('img', {src: user.picture}), canvas))); @@ -686,7 +688,7 @@ function getMetadata(evt, relay) { const name = user?.metadata[relay]?.name; const userName = name || evt.pubkey.slice(0, 8); const userAbout = user?.metadata[relay]?.about || ''; - const img = userImg ? elem('img', { + const img = (userImg && validatePow(evt)) ? elem('img', { alt: `${userName} ${host}`, loading: 'lazy', src: userImg, @@ -761,13 +763,16 @@ function hideNewMessage(hide) { async function upvote(eventId, relay) { const privatekey = localStorage.getItem('private_key'); - const newReaction = { + const newReaction = powEvent({ kind: 7, pubkey, // TODO: lib could check that this is the pubkey of the key to sign with content: '+', - tags: [['e', eventId, relay, 'reply']], + tags: [ + ['nonce', '0', `${difficulty}`], + ['e', eventId, relay, 'reply'], + ], created_at: Math.floor(Date.now() * 0.001), - }; + }, difficulty); const sig = await signEvent(newReaction, privatekey).catch(console.error); if (sig) { const ev = await pool.publish({...newReaction, sig}, (status, url) => { @@ -798,13 +803,13 @@ writeForm.addEventListener('submit', async (e) => { } const replyTo = localStorage.getItem('reply_to'); const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : []; - const newEvent = { + const newEvent = powEvent({ kind: 1, - pubkey, content, - tags, + pubkey, + tags: [['nonce', '0', `${difficulty}`], ...tags], created_at: Math.floor(Date.now() * 0.001), - }; + }, difficulty); const sig = await signEvent(newEvent, privatekey).catch(onSendError); if (sig) { const ev = await pool.publish({...newEvent, sig}, (status, url) => { @@ -940,13 +945,13 @@ profileForm.addEventListener('submit', async (e) => { const form = new FormData(profileForm); const privatekey = localStorage.getItem('private_key'); - const newProfile = { + const newProfile = powEvent({ kind: 0, pubkey, content: JSON.stringify(Object.fromEntries(form)), + tags: [['nonce', '0', `${difficulty}`]], created_at: Math.floor(Date.now() * 0.001), - tags: [], - }; + }, difficulty); const sig = await signEvent(newProfile, privatekey).catch(console.error); if (sig) { const ev = await pool.publish({...newProfile, sig}, (status, url) => { @@ -961,3 +966,30 @@ profileForm.addEventListener('submit', async (e) => { }).catch(console.error); } }); + +function validatePow(evt) { + const tag = evt.tags.find(tag => tag[0] === 'nonce'); + if (!tag) { + return false; + } + const [, , difficulty2] = tag; + if (difficulty2 < 16) { + return false; + } + 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; + } + } +} \ No newline at end of file