From 984aee3f80eb459eb2880b4d01192d932816d34b 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 | 65 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 49 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..f5366f5 100644 --- a/src/main.js +++ b/src/main.js @@ -1,9 +1,10 @@ -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/ const pool = relayPool(); + pool.addRelay('wss://relay.nostr.info', {read: true, write: true}); pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true}); // pool.addRelay('wss://relay.damus.io', {read: true, write: true}); @@ -40,6 +41,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => { return pubkey; })(); +const difficulty = 16; + const subList = []; const unSubAll = () => { subList.forEach(sub => sub.unsub()); @@ -161,9 +164,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 +453,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 +629,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 +689,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 +764,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 +804,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 +946,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 +967,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