nip-13: add pow and only invoke noxy in events with valid work
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

added pow to text notes, reactions and metadata events.

noxy is now only used if event has valid work proof.
OFF0 2 years ago
parent 89c54ac08f
commit 984aee3f80
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -158,6 +158,4 @@
max-width: 48rem; max-width: 48rem;
padding: 1.5rem 1.8rem; padding: 1.5rem 1.8rem;
width: 100%; width: 100%;
/* TODO: revert when things calm down or we find an alternative */
display: none;
} }

@ -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 {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});
@ -40,6 +41,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey; return pubkey;
})(); })();
const difficulty = 16;
const subList = []; const subList = [];
const unSubAll = () => { const unSubAll = () => {
subList.forEach(sub => sub.unsub()); subList.forEach(sub => sub.unsub());
@ -161,9 +164,9 @@ function renderProfile(evt, relay) {
if (content) { if (content) {
profileAbout.textContent = content.about; profileAbout.textContent = content.about;
profileName.textContent = content.name; 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) { if (noxyImg) {
profileImage.setAttribute('src', getNoxyUrl('data', noxyImg, evt.id, relay)); profileImage.setAttribute('src', noxyImg);
profileImage.hidden = false; profileImage.hidden = false;
} }
} }
@ -450,7 +453,7 @@ function createTextNote(evt, relay) {
]), ]),
elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [ elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [
...content, ...content,
firstLink ? linkPreview(firstLink, evt.id, relay) : '' (firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : '',
]), ]),
elem('button', { elem('button', {
className: 'btn-inline', name: 'star', type: 'button', className: 'btn-inline', name: 'star', type: 'button',
@ -626,7 +629,7 @@ function setMetadata(evt, relay, content) {
} }
} }
// update profile images // update profile images
if (user.picture) { if (user.picture && validatePow(evt)) {
document.body document.body
.querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`) .querySelectorAll(`canvas[data-pubkey="${evt.pubkey}"]`)
.forEach(canvas => (canvas.parentNode.replaceChild(elem('img', {src: user.picture}), canvas))); .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 name = user?.metadata[relay]?.name;
const userName = name || evt.pubkey.slice(0, 8); const userName = name || evt.pubkey.slice(0, 8);
const userAbout = user?.metadata[relay]?.about || ''; const userAbout = user?.metadata[relay]?.about || '';
const img = userImg ? elem('img', { const img = (userImg && validatePow(evt)) ? elem('img', {
alt: `${userName} ${host}`, alt: `${userName} ${host}`,
loading: 'lazy', loading: 'lazy',
src: userImg, src: userImg,
@ -761,13 +764,16 @@ 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 = { const newReaction = 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: [['e', eventId, relay, 'reply']], tags: [
['nonce', '0', `${difficulty}`],
['e', eventId, relay, 'reply'],
],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}; }, difficulty);
const sig = await signEvent(newReaction, privatekey).catch(console.error); const sig = await signEvent(newReaction, privatekey).catch(console.error);
if (sig) { if (sig) {
const ev = await pool.publish({...newReaction, sig}, (status, url) => { 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 replyTo = localStorage.getItem('reply_to');
const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : []; const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : [];
const newEvent = { const newEvent = powEvent({
kind: 1, kind: 1,
pubkey,
content, content,
tags, pubkey,
tags: [['nonce', '0', `${difficulty}`], ...tags],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
}; }, difficulty);
const sig = await signEvent(newEvent, privatekey).catch(onSendError); const sig = await signEvent(newEvent, privatekey).catch(onSendError);
if (sig) { if (sig) {
const ev = await pool.publish({...newEvent, sig}, (status, url) => { const ev = await pool.publish({...newEvent, sig}, (status, url) => {
@ -940,13 +946,13 @@ 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 = { const newProfile = powEvent({
kind: 0, kind: 0,
pubkey, pubkey,
content: JSON.stringify(Object.fromEntries(form)), content: JSON.stringify(Object.fromEntries(form)),
tags: [['nonce', '0', `${difficulty}`]],
created_at: Math.floor(Date.now() * 0.001), created_at: Math.floor(Date.now() * 0.001),
tags: [], }, difficulty);
};
const sig = await signEvent(newProfile, privatekey).catch(console.error); const sig = await signEvent(newProfile, privatekey).catch(console.error);
if (sig) { if (sig) {
const ev = await pool.publish({...newProfile, sig}, (status, url) => { const ev = await pool.publish({...newProfile, sig}, (status, url) => {
@ -961,3 +967,30 @@ profileForm.addEventListener('submit', async (e) => {
}).catch(console.error); }).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;
}
}
}
Loading…
Cancel
Save