nip-13: add pow and only invoke noxy in events with valid work #55

Merged
offbyn merged 5 commits from nip-13 into master 2 years ago
offbyn commented 2 years ago
Owner

added pow to text notes, reactions and metadata events.

noxy is now only used if event has valid work proof.

updates #51

added pow to text notes, reactions and metadata events. noxy is now only used if event has valid work proof. updates https://git.qcode.ch/nostr/nostrweb/issues/51
offbyn added 1 commit 2 years ago
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
984aee3f80
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.
x1ddos reviewed 2 years ago
x1ddos left a comment
Owner

good start! i would also mention that nostrweb now does pow in the readme and about page so users know what's going on.

also, made an issue in nostr-tools repo: https://github.com/fiatjaf/nostr-tools/issues/63

good start! i would also mention that nostrweb now does pow in the readme and about page so users know what's going on. also, made an issue in nostr-tools repo: https://github.com/fiatjaf/nostr-tools/issues/63
src/main.js Outdated
@ -40,6 +41,8 @@ let pubkey = localStorage.getItem('pub_key') || (() => {
return pubkey;
})();
const difficulty = 16;
x1ddos commented 2 years ago
Owner

would add a comment why 16, even if it's arbitrary:

// arbitrary difficulty, still experimenting.
// measured empirically, takes N sec on average to mine a text note event.

another idea for the future: make it adjustable? i'd imagine a slider in profile settings.

would add a comment why 16, even if it's arbitrary: ``` // arbitrary difficulty, still experimenting. // measured empirically, takes N sec on average to mine a text note event. ``` another idea for the future: make it adjustable? i'd imagine a slider in profile settings.
offbyn commented 2 years ago
Poster
Owner

done, using input type number for now

done, using input type number for now
src/main.js Outdated
@ -767,2 +770,3 @@
content: '+',
tags: [['e', eventId, relay, 'reply']],
tags: [
['nonce', '0', `${difficulty}`],
x1ddos commented 2 years ago
Owner

i would add this tag in the powEvent function. otherwise, you need to insert it everywhere. it's also easier to read and verify correctness when pow-related things are in the single function imho:

function powEvent(evt, difficulty) {
  const max = 256; // arbitrary
  if (!Number.isInteger(difficulty) || difficulty < 0 || difficulty > max) {
    throw new Error(`difficulty must be an integer between 0 and ${max}`);
  }
  evt.tags.push(['nonce', '0', `${difficulty}`]
  // continue with mining
  // ...
}
i would add this tag in the `powEvent` function. otherwise, you need to insert it everywhere. it's also easier to read and verify correctness when pow-related things are in the single function imho: ```js function powEvent(evt, difficulty) { const max = 256; // arbitrary if (!Number.isInteger(difficulty) || difficulty < 0 || difficulty > max) { throw new Error(`difficulty must be an integer between 0 and ${max}`); } evt.tags.push(['nonce', '0', `${difficulty}`] // continue with mining // ... } ```
src/main.js Outdated
@ -964,0 +980,4 @@
return evt.id.substring(0, difficulty2 / 4) === '00'.repeat(difficulty2 / 8);
}
function powEvent(newEvent, difficulty) {
x1ddos commented 2 years ago
Owner

is it possible to make it async? i'm hoping when invoked as await powEvent(...) it UI thread in browser's javascript won't be blocked.

also, i think it needs a timeout arg, to run at most that long. otherwise, it could run indefinitely and go crazy with the cpu fan :)

another idea is to run pow in a web worker. i imagine something like this:

/**
 * 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(event, difficulty, timeout) {
  return new Promise((resolve, reject) => {
    const webWorkerURL = URL.createObjectURL(new Blob(['(', powEventWorker(), ')()'], {type: 'application/javascript'}));
    const worker = new Worker(webWorkerURL);
    
    worker.onmessage = (msg) => {
      worker.terminate();
      if (msg.error) {
        reject(msg.error);
      } else {
        resolve(msg.event);
      }
    };

    worker.onerror = (err) => {
      worker.terminate();
      reject(err);
    };

    worker.postMessage({event, difficulty, timeout});
    URL.revokeObjectURL(webWorkerURL); // one-time worker; no longer need the URL obj 
  });
}

function powEventWorker() {
  return () => {
    function mine(event, difficulty, timeout) {
      // do the actual mining here
      // ...
      // throw err if running longer than the timeout
    }
    
    addEventListener('message', async (msg) => {
      let nostrEvent = msg.data.event;
      const difficulty = msg.data.difficulty;
      const timeout = msg.data.timeout;
      try {
        const minedEvent = mine(nostrEvent, difficulty, timeout);
        postMessage({event: minedEvent});
      } catch (err) {
        postMessage({error: err});
      }
    });
  }
}
is it possible to make it async? i'm hoping when invoked as `await powEvent(...)` it UI thread in browser's javascript won't be blocked. also, i think it needs a `timeout` arg, to run at most that long. otherwise, it could run indefinitely and go crazy with the cpu fan :) another idea is to run pow in a web worker. i imagine something like this: ```js /** * 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(event, difficulty, timeout) { return new Promise((resolve, reject) => { const webWorkerURL = URL.createObjectURL(new Blob(['(', powEventWorker(), ')()'], {type: 'application/javascript'})); const worker = new Worker(webWorkerURL); worker.onmessage = (msg) => { worker.terminate(); if (msg.error) { reject(msg.error); } else { resolve(msg.event); } }; worker.onerror = (err) => { worker.terminate(); reject(err); }; worker.postMessage({event, difficulty, timeout}); URL.revokeObjectURL(webWorkerURL); // one-time worker; no longer need the URL obj }); } function powEventWorker() { return () => { function mine(event, difficulty, timeout) { // do the actual mining here // ... // throw err if running longer than the timeout } addEventListener('message', async (msg) => { let nostrEvent = msg.data.event; const difficulty = msg.data.difficulty; const timeout = msg.data.timeout; try { const minedEvent = mine(nostrEvent, difficulty, timeout); postMessage({event: minedEvent}); } catch (err) { postMessage({error: err}); } }); } } ```
offbyn commented 2 years ago
Poster
Owner

I played around with the worker a bit, unfortunately it seems passing a worker by URL.createObjectURL doesn't include getEventHash by nostr-tools, this is why I added a worker.js file.

I played around with the worker a bit, unfortunately it seems passing a worker by URL.createObjectURL doesn't include `getEventHash` by `nostr-tools`, this is why I added a worker.js file.
offbyn commented 2 years ago
Poster
Owner

A bigger issue is that getEventHash needs the private-key, which would be hidden from JavaScript with window.nostr.

A bigger issue is that `getEventHash` needs the private-key, which would be hidden from JavaScript with `window.nostr`.
x1ddos commented 2 years ago
Owner

wait, i'm confused. why does getEventHash need a private key? here it takes only the Event: https://github.com/nbd-wtf/nostr-tools/blob/4b36848b/event.ts#L56

maybe we're using an older version?

wait, i'm confused. why does `getEventHash` need a private key? here it takes only the `Event`: https://github.com/nbd-wtf/nostr-tools/blob/4b36848b/event.ts#L56 maybe we're using an older version?
x1ddos commented 2 years ago
Owner

the Event should indeed contain the pubkey, for hashing, but i would expect the caller to set it before passing the event on to the worker.

the `Event` should indeed contain the pubkey, for hashing, but i would expect the caller to set it before passing the event on to the worker.
offbyn commented 2 years ago
Poster
Owner

indeed, now I am confused why did I add it. thanks for pointing this out. will remove privatekey.

indeed, now I am confused why did I add it. thanks for pointing this out. will remove privatekey.
src/main.js Outdated
@ -964,0 +986,4 @@
// const until = Date.now() + 15000;
console.time('pow');
while (true/*Date.now() < until*/) {
newEvent.tags[0][1] = `${n++}`;
x1ddos commented 2 years ago
Owner

i'm afraid this results in exponent format for large n:

>> `${2**128}`
<- "3.402823669209385e+38" 

but we always want regular format based 10, right?

>> `${BigInt(2**128).toString()}`
<- "340282366920938463463374607431768211456"
i'm afraid this results in exponent format for large `n`: ``` >> `${2**128}` <- "3.402823669209385e+38" ``` but we always want regular format based 10, right? ``` >> `${BigInt(2**128).toString()}` <- "340282366920938463463374607431768211456" ```
offbyn commented 2 years ago
Poster
Owner

changed to using BigInt

changed to using BigInt
src/main.js Outdated
@ -964,0 +987,4 @@
console.time('pow');
while (true/*Date.now() < until*/) {
newEvent.tags[0][1] = `${n++}`;
const id = getEventHash(newEvent, privatekey);
x1ddos commented 2 years ago
Owner

before hashing the event, i believe you'll want to update the created_at timestamp. from nip-13:

It is recommended to update the created_at as well during this process.

before hashing the event, i believe you'll want to update the `created_at` timestamp. from [nip-13](https://github.com/nostr-protocol/nips/blob/master/13.md): > It is recommended to update the created_at as well during this process.
offbyn commented 2 years ago
Poster
Owner

done

done
src/main.js Outdated
@ -964,0 +988,4 @@
while (true/*Date.now() < until*/) {
newEvent.tags[0][1] = `${n++}`;
const id = getEventHash(newEvent, privatekey);
if (id.substring(0, chars * 2) === '00'.repeat(chars)) {
x1ddos commented 2 years ago
Owner

this unfortunately won't count the exact number of bits in all cases. taking the event id from nip-13 example, it's binary representation is:

>> const id = '000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d';
>> parseInt(id, 16).toString(2).padStart(8*32, '0')
<- '0000000000000000000000000000000000001110100111...

or for better readability:

00000000 00000000 00000000 00000000 00001110 100111...

so, we have 8 + 8 + 8 + 8 + 4 = 36 leading 0 bits but the current impl will count only 8*4 = 32.

i saw you difficulty / 8 to make it round but i think it's relatively easy to do it at the bit level. played around a bit. here's a version i came up with that counts exact number:

/**
  * evaluate the difficulty of hex32 according to nip-13.
  * @param hex32 a string of 64 chars - 32 bytes in hex representation
  */
function 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;
}

and here's another version. it looks simpler but i don't know which one is faster. it should be possible to benchmark cpu and memory performance, and pick the best one?

/**
  * evaluate the difficulty of hex32 according to nip-13.
  * @param hex32 a string of 64 chars - 32 bytes in hex representation
  */
function zeroLeadingBitsCount(hex32) {
  const bits = parseInt(hex32, 16).toString(2).padStart(8*32, '0');
  let count = 0;
  for (let i = 0; i < 8*32; i++) {
    if (bits[i] == '1') {
      break;
    }
    count += 1;
  }
  return count;
}
this unfortunately won't count the exact number of bits in all cases. taking the event id from [nip-13](https://github.com/nostr-protocol/nips/blob/master/13.md) example, it's binary representation is: ``` >> const id = '000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d'; >> parseInt(id, 16).toString(2).padStart(8*32, '0') <- '0000000000000000000000000000000000001110100111... ``` or for better readability: ``` 00000000 00000000 00000000 00000000 00001110 100111... ``` so, we have `8 + 8 + 8 + 8 + 4 = 36` leading 0 bits but the current impl will count only `8*4 = 32`. i saw you `difficulty / 8` to make it round but i think it's relatively easy to do it at the bit level. played around a bit. here's a version i came up with that counts exact number: ```js /** * evaluate the difficulty of hex32 according to nip-13. * @param hex32 a string of 64 chars - 32 bytes in hex representation */ function 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; } ``` and here's another version. it looks simpler but i don't know which one is faster. it should be possible to benchmark cpu and memory performance, and pick the best one? ```js /** * evaluate the difficulty of hex32 according to nip-13. * @param hex32 a string of 64 chars - 32 bytes in hex representation */ function zeroLeadingBitsCount(hex32) { const bits = parseInt(hex32, 16).toString(2).padStart(8*32, '0'); let count = 0; for (let i = 0; i < 8*32; i++) { if (bits[i] == '1') { break; } count += 1; } return count; } ```
offbyn commented 2 years ago
Poster
Owner

first function is faster. It varies with how many 0's, tested a bit with https://jsbench.me/

first function is faster. It varies with how many 0's, tested a bit with https://jsbench.me/
x1ddos commented 2 years ago
Owner

👍 same in go. i added a similar function to go-nostr in https://github.com/nbd-wtf/go-nostr/pull/27

👍 same in go. i added a similar function to go-nostr in https://github.com/nbd-wtf/go-nostr/pull/27
offbyn commented 2 years ago
Poster
Owner

thanks for the feedback. sorry I got sick a few days ago, today is the first time I am up again. will still take a few more days.

thanks for the feedback. sorry I got sick a few days ago, today is the first time I am up again. will still take a few more days.
offbyn added 1 commit 2 years ago
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
da269debc3
nip-13: mine proof async in worker
x1ddos approved these changes 2 years ago
src/main.js Outdated
@ -964,0 +973,4 @@
return false;
}
const [, , difficulty2] = tag;
if (difficulty2 < 16) {
x1ddos commented 2 years ago
Owner

there's a const difficulty = 16; elsewhere in main.js. i think you'll want to compare with it here?

maybe a better way to name the const is something like const acceptedDifficulty = 16 so that it reads more clear here:

if (difficulty2 < acceptedDifficulty) {
  return false
}
there's a `const difficulty = 16;` elsewhere in main.js. i think you'll want to compare with it here? maybe a better way to name the const is something like `const acceptedDifficulty = 16` so that it reads more clear here: ```js if (difficulty2 < acceptedDifficulty) { return false } ```
offbyn commented 2 years ago
Poster
Owner

done

done
src/main.js Outdated
@ -964,0 +976,4 @@
if (difficulty2 < 16) {
return false;
}
return evt.id.substring(0, difficulty2 / 4) === '00'.repeat(difficulty2 / 8);
x1ddos commented 2 years ago
Owner

looks like that return assumes a difficulty is always divisible by 4 or 8 but it isn't always the case. could be 3, 7, 9, 11... plus, this is only commitment which can be fake.

i believe you'll want to use zeroLeadingBitsCount here too. for example:

function validatePow(evt) {
  // acceptedDifficulty is set by the user.
  // let the event pass if user doesn't care about pow difficulty.
  if (!acceptedDifficulty) {
    return true;
  }
  
  const tag = evt.tags.find(tag => tag[0] === 'nonce');
  const committedDifficulty = tag && parseInt(tag[2]);
  // discard events which committed to lower difficulty.
  if (!committedDifficulty || committedDifficulty < acceptedDifficulty) {
    return false;
  }
  
  // difficulty commitment might be fake.
  // the final check is the actual event pow difficulty. 
  return zeroLeadingBitsCount(evt.ID) >= acceptedDifficulty;
}
looks like that return assumes a difficulty is always divisible by 4 or 8 but it isn't always the case. could be 3, 7, 9, 11... plus, this is only commitment which can be fake. i believe you'll want to use `zeroLeadingBitsCount` here too. for example: ```js function validatePow(evt) { // acceptedDifficulty is set by the user. // let the event pass if user doesn't care about pow difficulty. if (!acceptedDifficulty) { return true; } const tag = evt.tags.find(tag => tag[0] === 'nonce'); const committedDifficulty = tag && parseInt(tag[2]); // discard events which committed to lower difficulty. if (!committedDifficulty || committedDifficulty < acceptedDifficulty) { return false; } // difficulty commitment might be fake. // the final check is the actual event pow difficulty. return zeroLeadingBitsCount(evt.ID) >= acceptedDifficulty; } ```
offbyn marked this conversation as resolved
src/worker.js Outdated
@ -0,0 +18,4 @@
// console.timeEnd('pow');
// return false;
// }
if (now !== event.created_at) {
x1ddos commented 2 years ago
Owner

i think it shoudn't matter? what if you always event.created_at = now() on each iteration?

i think it shoudn't matter? what if you always `event.created_at = now()` on each iteration?
src/worker.js Outdated
@ -0,0 +24,4 @@
}
event.tags[0][1] = (++n).toString();
const id = getEventHash(event, privatekey);
if (zeroLeadingBitsCount(id) === difficulty) {
x1ddos commented 2 years ago
Owner

one can get luckly and produce a higher difficulty. so, the check should be an inequality:

if (zeroLeadingBitsCount(id) >= difficulty) {
  // done: reached at least the desired difficulty, but maybe higher
  return event;
}
one can get luckly and produce a higher difficulty. so, the check should be an inequality: ```js if (zeroLeadingBitsCount(id) >= difficulty) { // done: reached at least the desired difficulty, but maybe higher return event; } ```
offbyn commented 2 years ago
Poster
Owner

The nip-13 has difficulty commitment target / lucky protection :)

This allows clients to protect against situations where bulk spammers targeting a lower difficulty get lucky and match a higher difficulty.

https://github.com/nostr-protocol/nips/blob/master/13.md#mining

The nip-13 has difficulty commitment target / lucky protection :) > This allows clients to protect against situations where bulk spammers targeting a lower difficulty get lucky and match a higher difficulty. https://github.com/nostr-protocol/nips/blob/master/13.md#mining
offbyn commented 2 years ago
Poster
Owner

I tried modifying go-nostr and added ErrDifficultyTooHigh, but somehow the tests always pass with go test or go test -run nip13_test.go (even if I modify the test so it should fail), doing something wrong 🤔

diff --git a/nip13/nip13.go b/nip13/nip13.go
index 35a7d1f..ef88368 100644
--- a/nip13/nip13.go
+++ b/nip13/nip13.go
@@ -14,6 +14,7 @@ import (
 
 var (
 	ErrDifficultyTooLow = errors.New("nip13: insufficient difficulty")
+	ErrDifficultyTooHigh = errors.New("nip13: does not match committed difficulty target")
 	ErrGenerateTimeout  = errors.New("nip13: generating proof of work took too long")
 )
 
@@ -43,10 +44,13 @@ func Difficulty(eventID string) int {
 // Note that Check performs no validation other than counting leading zero bits
 // in an event ID. It is up to the callers to verify the event with other methods,
 // such as [nostr.Event.CheckSignature].
-func Check(eventID string, minDifficulty int) error {
-	if Difficulty(eventID) < minDifficulty {
+func Check(eventID string, targetDifficulty int) error {
+	if Difficulty(eventID) < targetDifficulty {
 		return ErrDifficultyTooLow
 	}
+	if Difficulty(eventID) > targetDifficulty {
+		return ErrDifficultyTooHigh
+	}
 	return nil
 }
 
diff --git a/nip13/nip13_test.go b/nip13/nip13_test.go
index b1e4409..cc1ae6a 100644
--- a/nip13/nip13_test.go
+++ b/nip13/nip13_test.go
@@ -13,20 +13,20 @@ import (
 func TestCheck(t *testing.T) {
 	const eventID = "000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"
 	tests := []struct {
-		minDifficulty int
+		difficulty int
 		wantErr       error
 	}{
-		{-1, nil},
-		{0, nil},
-		{1, nil},
-		{35, nil},
+		{-1, ErrDifficultyTooHigh},
+		{0, ErrDifficultyTooHigh},
+		{1, ErrDifficultyTooHigh},
+		{35, ErrDifficultyTooHigh},
 		{36, nil},
 		{37, ErrDifficultyTooLow},
 		{42, ErrDifficultyTooLow},
 	}
 	for i, tc := range tests {
-		if err := Check(eventID, tc.minDifficulty); err != tc.wantErr {
-			t.Errorf("%d: Check(%q, %d) returned %v; want err: %v", i, eventID, tc.minDifficulty, err, tc.wantErr)
+		if err := Check(eventID, tc.difficulty); err != tc.wantErr {
+			t.Errorf("%d: Check(%q, %d) returned %v; want err: %v", i, eventID, tc.difficulty, err, tc.wantErr)
 		}
 	}
 }
I tried modifying `go-nostr` and added ErrDifficultyTooHigh, but somehow the tests always pass with `go test` or `go test -run nip13_test.go` (even if I modify the test so it should fail), doing something wrong 🤔 ```diff diff --git a/nip13/nip13.go b/nip13/nip13.go index 35a7d1f..ef88368 100644 --- a/nip13/nip13.go +++ b/nip13/nip13.go @@ -14,6 +14,7 @@ import ( var ( ErrDifficultyTooLow = errors.New("nip13: insufficient difficulty") + ErrDifficultyTooHigh = errors.New("nip13: does not match committed difficulty target") ErrGenerateTimeout = errors.New("nip13: generating proof of work took too long") ) @@ -43,10 +44,13 @@ func Difficulty(eventID string) int { // Note that Check performs no validation other than counting leading zero bits // in an event ID. It is up to the callers to verify the event with other methods, // such as [nostr.Event.CheckSignature]. -func Check(eventID string, minDifficulty int) error { - if Difficulty(eventID) < minDifficulty { +func Check(eventID string, targetDifficulty int) error { + if Difficulty(eventID) < targetDifficulty { return ErrDifficultyTooLow } + if Difficulty(eventID) > targetDifficulty { + return ErrDifficultyTooHigh + } return nil } diff --git a/nip13/nip13_test.go b/nip13/nip13_test.go index b1e4409..cc1ae6a 100644 --- a/nip13/nip13_test.go +++ b/nip13/nip13_test.go @@ -13,20 +13,20 @@ import ( func TestCheck(t *testing.T) { const eventID = "000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d" tests := []struct { - minDifficulty int + difficulty int wantErr error }{ - {-1, nil}, - {0, nil}, - {1, nil}, - {35, nil}, + {-1, ErrDifficultyTooHigh}, + {0, ErrDifficultyTooHigh}, + {1, ErrDifficultyTooHigh}, + {35, ErrDifficultyTooHigh}, {36, nil}, {37, ErrDifficultyTooLow}, {42, ErrDifficultyTooLow}, } for i, tc := range tests { - if err := Check(eventID, tc.minDifficulty); err != tc.wantErr { - t.Errorf("%d: Check(%q, %d) returned %v; want err: %v", i, eventID, tc.minDifficulty, err, tc.wantErr) + if err := Check(eventID, tc.difficulty); err != tc.wantErr { + t.Errorf("%d: Check(%q, %d) returned %v; want err: %v", i, eventID, tc.difficulty, err, tc.wantErr) } } } ```
x1ddos commented 2 years ago
Owner

this part doesn't make much sense to me:

+	if Difficulty(eventID) > targetDifficulty {
+		return ErrDifficultyTooHigh
+	}

you'll always want to accept pow difficulty higher than target, except when committed is lower, so in pseudo-code:

if (committedToDifficulty < targetDifficulty) {
  fail
}
if (eventDifficulty < targetDifficulty) {
  fail
}

the function i wrote in #55 should work, no?

this part doesn't make much sense to me: ```patch + if Difficulty(eventID) > targetDifficulty { + return ErrDifficultyTooHigh + } ``` you'll always want to accept pow difficulty higher than target, except when **committed** is lower, so in pseudo-code: ```js if (committedToDifficulty < targetDifficulty) { fail } if (eventDifficulty < targetDifficulty) { fail } ``` the function i wrote in https://git.qcode.ch/nostr/nostrweb/pulls/55#issuecomment-565 should work, no?
offbyn commented 2 years ago
Poster
Owner

you'll always want to accept pow difficulty higher than target, except when committed is lower, so in pseudo-code:

not according to nip-13, see my other comment about the lucky protection

> you'll always want to accept pow difficulty higher than target, except when committed is lower, so in pseudo-code: not according to nip-13, see my other comment about the lucky protection - https://git.qcode.ch/nostr/nostrweb/pulls/55#issuecomment-570 - and here the nip-13 https://github.com/nostr-protocol/nips/blob/master/13.md#mining
x1ddos commented 2 years ago
Owner

right. the lucky protection is about difficulty commitment, not the actual event ID pow. so i don't understand if Difficulty(eventID) > targetDifficulty { return err-too-high } - you'll want to compare that to commitment, not the actual event difficulty.

right. the lucky protection is about difficulty commitment, not the actual event ID pow. so i don't understand `if Difficulty(eventID) > targetDifficulty { return err-too-high }` - you'll want to compare that to **commitment**, not the actual event difficulty.
offbyn commented 2 years ago
Poster
Owner

I expected that the difficulty arg passed to the Check function comes from the nonce tag where target difficultly is specified.

anyway I just tried to modify the go-nostr, but kind of gave up.

I expected that the difficulty arg passed to the Check function comes from the nonce tag where target difficultly is specified. anyway I just tried to modify the go-nostr, but kind of gave up.
x1ddos commented 2 years ago
Owner

I expected that the difficulty arg passed to the Check function comes from the nonce tag where target difficultly is specified.

not really. look, you as a user set a minimal difficulty at which you'll accept events. let's call it acceptedDifficulty. then you verify pow for each event like so:

  1. compare commitment from nonce against the acceptedDifficulty.
  2. compare event pow (event ID) again with the acceptedDifficulty.

you don't really need to compare commitment and event pow between each other.

> I expected that the difficulty arg passed to the Check function comes from the nonce tag where target difficultly is specified. not really. look, you as a user set a minimal difficulty at which you'll accept events. let's call it `acceptedDifficulty`. then you verify pow for each event like so: 1. compare **commitment** from nonce against the `acceptedDifficulty`. 2. compare event pow (**event ID**) again with the `acceptedDifficulty`. you don't really need to compare commitment and event pow between each other.
offbyn commented 2 years ago
Poster
Owner

look, you as a user set a minimal difficulty at which you'll accept events. let's call it acceptedDifficulty. then you verify pow for each event like so:

this makes sense, but I read the nip differently that a client may or may not accept an event if the commitment and event pow do not match. so some clients may reject if the actual difficulty is higher than target difficulty.

https://github.com/nostr-protocol/nips/blob/master/13.md#mining

> look, you as a user set a minimal difficulty at which you'll accept events. let's call it acceptedDifficulty. then you verify pow for each event like so: this makes sense, but I read the nip differently that a client may or may not accept an event if the commitment and event pow do not match. so some clients may reject if the actual difficulty is higher than target difficulty. https://github.com/nostr-protocol/nips/blob/master/13.md#mining
offbyn commented 2 years ago
Poster
Owner

but I am thinking in client-world, please ignore my go-nostr. I'll drop and resolve this.

maybe the go-nostr library should have an option to opt in into lucky prevention, but that is not part of this PR. :)

but I am thinking in client-world, please ignore my go-nostr. I'll drop and resolve this. maybe the go-nostr library should have an option to opt in into lucky prevention, but that is not part of this PR. :)
offbyn marked this conversation as resolved
x1ddos requested review from x1ddos 2 years ago
offbyn commented 2 years ago
Poster
Owner

@x1ddos I am not sure what to do in case of timeout, and also how long to wait.

silently dismiss is not great UX.

trying again with lower difficulty seems wrong somehow.

I guess it should show an error to the user maybe with a retry button, but that error can pop up from various places, i.e. posting, upvoting, replying etc. WDYT?

@x1ddos I am not sure what to do in case of timeout, and also how long to wait. silently dismiss is not great UX. trying again with lower difficulty seems wrong somehow. I guess it should show an error to the user maybe with a retry button, but that error can pop up from various places, i.e. posting, upvoting, replying etc. WDYT?
x1ddos commented 2 years ago
Owner

I guess it should show an error to the user maybe with a retry button, but that error can pop up from various places, i.e. posting, upvoting, replying etc. WDYT?

yeah, i too would expect some kind of an error. a retry button would be definitely very nice 👍

> I guess it should show an error to the user maybe with a retry button, but that error can pop up from various places, i.e. posting, upvoting, replying etc. WDYT? yeah, i too would expect some kind of an error. a retry button would be definitely very nice 👍
offbyn added 1 commit 2 years ago
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
cf92997ac5
fix into last commit
cleanup, and updated validatePow, but this commit does not yet
have acceptedDifficulty, it only checks if the pow is as desired.

next commit should also add timeout and a userfacing error.
offbyn added 1 commit 2 years ago
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
41efd4626f
nip-13: add timeout and show user facing error if it exceeds
offbyn commented 2 years ago
Poster
Owner

added a user facing error, planing to need to do some more testing and squash some of the commits prob tomorrow. need to rest a bit now zzzz

added a user facing error, planing to need to do some more testing and squash some of the commits prob tomorrow. need to rest a bit now zzzz
offbyn force-pushed nip-13 from 41efd4626f to 67920b1cb6 2 years ago
offbyn force-pushed nip-13 from 67920b1cb6 to daf57d2160 2 years ago
offbyn force-pushed nip-13 from daf57d2160 to f834268a67 2 years ago
offbyn force-pushed nip-13 from f834268a67 to b231c4cb61 2 years ago
offbyn force-pushed nip-13 from b231c4cb61 to 5b53824606 2 years ago
offbyn force-pushed nip-13 from 5b53824606 to 85dd6c11d8 2 years ago
offbyn added 1 commit 2 years ago
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
cd92145107
nip-13: add settings for target difficulty and timeout
adding settings to change difficulty target and timeout, so users
can change or disable pow. also added some explanation and link
to nip-13.

setting arbitrary low default to 16 zero target difficulty and
5 seconds timeout.
offbyn added 1 commit 2 years ago
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details
3929e572c3
nip-13: error overlay should also cover settings
moving error overlay last so it is able to cover everything
including settings, as mining timeout can also occur when
changing profile metadata.
offbyn force-pushed nip-13 from 3929e572c3 to 67e175cd89 2 years ago
offbyn force-pushed nip-13 from 67e175cd89 to 18c0dc62a6 2 years ago
offbyn commented 2 years ago
Poster
Owner

I'm planing to add 1 commit to give visual feedback when posting/reacting, maybe with the option to cancel mining.

also text of the settings and timeout error are very simple and could be better.

I'm planing to add 1 commit to give visual feedback when posting/reacting, maybe with the option to cancel mining. also text of the settings and timeout error are very simple and could be better.
offbyn force-pushed nip-13 from 18c0dc62a6 to 06a4ffb340 2 years ago
offbyn force-pushed nip-13 from 06a4ffb340 to a9be17ad42 2 years ago
x1ddos reviewed 2 years ago
x1ddos left a comment
Owner

very nice! there's one important bug to resolve here - see comments around validatePow.

very nice! there's one important bug to resolve here - see comments around `validatePow`.
src/index.html Outdated
@ -86,0 +99,4 @@
<label class="number" for="miningTimeout">
<span>
mining timeout<br>
<small>abord trying to find a proof if timeout (in seconds) exceeds. use 0 to mine without a time limit.</small>
x1ddos commented 2 years ago
Owner

abord -> abort typo

abord -> abort typo
offbyn marked this conversation as resolved
src/main.js Outdated
@ -749,2 +751,4 @@
}
// arbitrary difficulty default, still experimenting.
let difficulty = JSON.parse(localStorage.getItem('difficutly_target')) ?? 16;
x1ddos commented 2 years ago
Owner

maybe just parseInt(localStorage.getItem('difficutly_target'))? feels a bit overkill to feed a number to a json parser. same for timeout.

also, how about renaming it to something with a word "mine", like mine_target? otherwise, i see it a bit confusing when thinking about validating others' events where an "accepted" difficulty can be different from this client's mining difficulty.

maybe just `parseInt(localStorage.getItem('difficutly_target'))`? feels a bit overkill to feed a number to a json parser. same for timeout. also, how about renaming it to something with a word "mine", like `mine_target`? otherwise, i see it a bit confusing when thinking about validating others' events where an "accepted" difficulty can be different from this client's mining difficulty.
offbyn commented 2 years ago
Poster
Owner

JSON.parse just deals with null and "0" in the best way (both valid JSON), but I guess it is a bit overkill 😥. what is convenient is: localStorage returns null for missing keys and JSON.parse just returns null as well. then it can just fallback with "nullish coalescing operator (??)", which I find cool 🤓.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing

compared to parseInt(null) returns NaN and Number(null) returns 0 which both are bit a pain to try to add a fallback.

also parseInt seems wrong, i.e. if the value for some reason would be "2e1" it would parse it into "2". Number would correctly convert to "20".
https://stackoverflow.com/questions/4090518/what-is-the-difference-between-parseint-and-number

`JSON.parse` just deals with `null` and `"0"` in the best way (both valid JSON), but I guess it is a bit overkill 😥. what is convenient is: localStorage returns `null` for missing keys and JSON.parse just returns `null` as well. then it can just fallback with "nullish coalescing operator (??)", which I find cool 🤓. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing compared to `parseInt(null)` returns `NaN` and `Number(null)` returns `0` which both are bit a pain to try to add a fallback. also `parseInt` seems wrong, i.e. if the value for some reason would be "2e1" it would parse it into "2". `Number` would correctly convert to "20". https://stackoverflow.com/questions/4090518/what-is-the-difference-between-parseint-and-number
src/main.js Outdated
@ -960,0 +1038,4 @@
* @param {EventObj} evt to validate
* @returns boolean
*/
function validatePow(evt) {
x1ddos commented 2 years ago
Owner

i would add a second parameter, difficulty:

/**
 * validate proof-of-work of a nostr event per nip-13.
 * the validation always requires difficulty commitment in the nonce tag.
 * a difficulty param value below 1 always returns true without actual validation.
 *
 * @param {EventObj} evt event to validate
 * @param {number} difficulty target proof-of-work difficulty
 */
function validatePow(evt, difficulty)
i would add a second parameter, `difficulty`: ```js /** * validate proof-of-work of a nostr event per nip-13. * the validation always requires difficulty commitment in the nonce tag. * a difficulty param value below 1 always returns true without actual validation. * * @param {EventObj} evt event to validate * @param {number} difficulty target proof-of-work difficulty */ function validatePow(evt, difficulty) ```
offbyn commented 2 years ago
Poster
Owner

will add this in a later change, together with when the client actually drops events based on this.

will add this in a later change, together with when the client actually drops events based on this.
src/main.js Outdated
@ -960,0 +1044,4 @@
return false;
}
const [, , difficultyCommitment] = tag;
return zeroLeadingBitsCount(evt.id) >= difficultyCommitment;
x1ddos commented 2 years ago
Owner

this is still incomplete validation. it would return true for any difficutly as high as the commitment, but you'll want the validation to fail if the difficulty is lower than some expected value.

here's how i see this working correctly. note the second parameter:

/**
 * validate proof-of-work of a nostr event per nip-13.
 * the validation always requires difficulty commitment in the nonce tag.
 * if targetDifficulty value is below 1, validatePow always returns true without
 * actual validation.
 *
 * @param {EventObj} evt event to validate
 * @param {number} targetDifficulty target proof-of-work difficulty
 */
function validatePow(evt, targetDifficulty) {
  // skip validation for a zero target difficulty
  if (!targetDifficulty || targetDifficulty < 1) {
    return true;
  }
  
  // require nonce tag and the commitment
  const tag = evt.tags.find(tag => tag[0] === 'nonce');
  if (!tag) {
    return false;
  }
  const [, , commitment] = tag;
  if (!commitment || commitment < targetDifficulty) {
    return false;
  }
  
  // finally, check the event pow difficulty
  return zeroLeadingBitsCount(evt.id) >= targetDifficulty;
}

you see, there are two importan checks against a target difficulty: first the commitment, then the event pow. but not between each other.

this is still incomplete validation. it would return true for any difficutly as high as the commitment, but you'll want the validation to fail if the difficulty is lower than some expected value. here's how i see this working correctly. note the second parameter: ```js /** * validate proof-of-work of a nostr event per nip-13. * the validation always requires difficulty commitment in the nonce tag. * if targetDifficulty value is below 1, validatePow always returns true without * actual validation. * * @param {EventObj} evt event to validate * @param {number} targetDifficulty target proof-of-work difficulty */ function validatePow(evt, targetDifficulty) { // skip validation for a zero target difficulty if (!targetDifficulty || targetDifficulty < 1) { return true; } // require nonce tag and the commitment const tag = evt.tags.find(tag => tag[0] === 'nonce'); if (!tag) { return false; } const [, , commitment] = tag; if (!commitment || commitment < targetDifficulty) { return false; } // finally, check the event pow difficulty return zeroLeadingBitsCount(evt.id) >= targetDifficulty; } ``` you see, there are two importan checks against a target difficulty: first the commitment, then the event pow. but not between each other.
x1ddos commented 2 years ago
Owner

oh, i forgot: isn't the commitment in the nonce tag a string? then need to parse it into a number:

const commitment = parseInt(tag[2]);
if (!commitment || commitment < targetDifficulty) {
  return false;
}
oh, i forgot: isn't the commitment in the nonce tag a string? then need to parse it into a number: ```js const commitment = parseInt(tag[2]); if (!commitment || commitment < targetDifficulty) { return false; } ```
offbyn commented 2 years ago
Poster
Owner

this is still incomplete validation. it would return true for any difficutly as high as the commitment, but you'll want the validation to fail if the difficulty is lower than some expected value.

I forgot to check validatePow and will fix zero difficulty bug.

but I was aware that at this point it doesn't require a specific target. so far the only objective was that there is nonce at all. the only change client does differently, in this PR, is conditional noxy involvement.

I assumed requiredTargetDifficulty should come in a later step as it's not clear what to do with it. but if you want it now please propose:

  • what should it do, keep only-noxy-invoking or something else i.e. drop events?

  • what targetDifficulty would you propose?

  • should it be configurable in the client?

prob there is more questions, alternative we keep objective to only noxy-invoking for now and get this out.

> this is still incomplete validation. it would return true for any difficutly as high as the commitment, but you'll want the validation to fail if the difficulty is lower than some expected value. I forgot to check validatePow and will fix zero difficulty bug. but I was aware that at this point it doesn't require a specific target. so far the only objective was that there is nonce at all. the only change client does differently, in this PR, is conditional noxy involvement. I assumed requiredTargetDifficulty should come in a later step as it's not clear what to do with it. but if you want it now please propose: - what should it do, keep only-noxy-invoking or something else i.e. drop events? - what targetDifficulty would you propose? - should it be configurable in the client? prob there is more questions, alternative we keep objective to only noxy-invoking for now and get this out.
x1ddos commented 2 years ago
Owner

ah then i guess i got confused because the title says:

only invoke noxy in events with valid work

but the way it checks pow at the moment cannot say anything about "valid work". then rename the commit title? :)

another confusion for me was that the client now sends pow'ed events to a relay but expects essential no proof when receiving events. feels kind of unfair. i don't mind either way, as long as the changes are clear so i get less confused in the future!

re: what to do. those are easy questions in my head:

what should it do, keep only-noxy-invoking or something else i.e. drop events?

of course, it should drop events below a requiredTargetDifficulty. it's the whole point of nip-13, right?

what targetDifficulty would you propose?

i'd initially set it to the same as what the client mines when posting events. basically, expect the same difficulty both ways.

should it be configurable in the client?

definitely. there are many scenarios where receiving target difficuly can be different from sending. for example, slow client like an iot device. in the

future, i would also expect to be able to set separate difficulty for each relay. for example, i'd set a higher pow for a relay with many "spammy" users and lower or zero for an expensive relay. but can do this after, in a separate commit.

keep objective to only noxy-invoking for now and get this out.

yes, ship it!

ah then i guess i got confused because the title says: > only invoke noxy in events with valid work but the way it checks pow at the moment cannot say anything about "valid work". then rename the commit title? :) another confusion for me was that the client now sends pow'ed events to a relay but expects essential no proof when receiving events. feels kind of unfair. i don't mind either way, as long as the changes are clear so i get less confused in the future! re: what to do. those are easy questions in my head: > what should it do, keep only-noxy-invoking or something else i.e. drop events? of course, it should drop events below a requiredTargetDifficulty. it's the whole point of nip-13, right? > what targetDifficulty would you propose? i'd initially set it to the same as what the client mines when posting events. basically, expect the same difficulty both ways. > should it be configurable in the client? definitely. there are many scenarios where receiving target difficuly can be different from sending. for example, slow client like an iot device. in the future, i would also expect to be able to set separate difficulty for each relay. for example, i'd set a higher pow for a relay with many "spammy" users and lower or zero for an expensive relay. but can do this after, in a separate commit. > keep objective to only noxy-invoking for now and get this out. yes, ship it!
offbyn commented 2 years ago
Poster
Owner

I forgot to check validatePow and will fix zero difficulty bug.

It now checks that there is a greater than 0 difficulty commitment and that the string is a number. made a test with not a number commitment here:

https://nostr.ch/6b0c020d91a0148ee19550f30a9362429ccb2f87e789ded7edea4ca7fd886213

but the way it checks pow at the moment cannot say anything about "valid work". then rename the commit title? :)

yes I didn't push as I wanted to clarify if it should already drop events in the first step. now it should check for valid work although it's already valid if it has 1 zero :)

> I forgot to check validatePow and will fix zero difficulty bug. It now checks that there is a greater than 0 difficulty commitment and that the string is a number. made a test with not a number commitment here: https://nostr.ch/6b0c020d91a0148ee19550f30a9362429ccb2f87e789ded7edea4ca7fd886213 > but the way it checks pow at the moment cannot say anything about "valid work". then rename the commit title? :) yes I didn't push as I wanted to clarify if it should already drop events in the first step. now it should check for valid work although it's already valid if it has 1 zero :)
offbyn commented 2 years ago
Poster
Owner

another confusion for me was that the client now sends pow'ed events to a relay but expects essential no proof when receiving events.

yes a bit unfair. but honest clients that put in work may see noxy preview again one day (if we enable it, i.e. for events with difficulty 16).

i don't mind either way, as long as the changes are clear so i get less confused in the future!

I'll update the commit message one more time, in some commit it still mentions target difficulty, but I'll change it to mining commitment.

> another confusion for me was that the client now sends pow'ed events to a relay but expects essential no proof when receiving events. yes a bit unfair. but honest clients that put in work may see noxy preview again one day (if we enable it, i.e. for events with difficulty 16). > i don't mind either way, as long as the changes are clear so i get less confused in the future! I'll update the commit message one more time, in some commit it still mentions target difficulty, but I'll change it to mining commitment.
offbyn commented 2 years ago
Poster
Owner

of course, it should drop events below a requiredTargetDifficulty. it's the whole point of nip-13, right?

this would have the effect that on nostr.ch only events from nostr.ch would be visible as no other clients implemented nip-13.

> of course, it should drop events below a requiredTargetDifficulty. it's the whole point of nip-13, right? this would have the effect that on nostr.ch only events from nostr.ch would be visible as no other clients implemented nip-13.
x1ddos commented 2 years ago
Owner

this would have the effect that on nostr.ch only events from nostr.ch would be visible as no other clients implemented nip-13.

for public feed, yes. it's one of the main points to reduce spam - the second bullet in n.2 in #51

my hope is other clients will start doing it too. then public feed on nostrweb will start showing more by default.

anyway, that's for default public feed only. users can then tweak settings and even set pow check/mining to zero, to disable, if they want to see spam. i guess one could call it a spam gauge control? :D

> this would have the effect that on nostr.ch only events from nostr.ch would be visible as no other clients implemented nip-13. for public feed, yes. it's one of the main points to reduce spam - the second bullet in n.2 in https://git.qcode.ch/nostr/nostrweb/issues/51#issuecomment-519 my hope is other clients will start doing it too. then public feed on nostrweb will start showing more by default. anyway, that's for default public feed only. users can then tweak settings and even set pow check/mining to zero, to disable, if they want to see spam. i guess one could call it a spam gauge control? :D
x1ddos approved these changes 2 years ago
x1ddos left a comment
Owner

lgtm. good start! can't wait 'till the cycle is closed and receiving side also validates pow.

lgtm. good start! can't wait 'till the cycle is closed and receiving side also validates pow.
offbyn force-pushed nip-13 from a9be17ad42 to 825d4b25b4 2 years ago
offbyn force-pushed nip-13 from 825d4b25b4 to e1a1381a87 2 years ago
offbyn force-pushed nip-13 from e1a1381a87 to 37f0a07cf3 2 years ago
offbyn merged commit 37f0a07cf3 into master 2 years ago
offbyn deleted branch nip-13 2 years ago

Reviewers

x1ddos approved these changes 2 years ago
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
The pull request has been merged as 37f0a07cf3.
You can also view command line instructions.

Step 1:

From your project repository, check out a new branch and test the changes.
git checkout -b nip-13 master
git pull origin nip-13

Step 2:

Merge the changes and update on Gitea.
git checkout master
git merge --no-ff nip-13
git push origin master
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: nostr/nostrweb#55
Loading…
There is no content yet.