mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-05-05 00:00:14 +02:00
nip13: check and generate proof of work
implementation as per NIP-13 https://github.com/nostr-protocol/nips/blob/e79c84ae/13.md some benchmark results: $ go test -bench . ./nip13 goos: linux goarch: amd64 pkg: github.com/nbd-wtf/go-nostr/nip13 cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz BenchmarkCheck-8 144905385 8.328 ns/op BenchmarkGenerateOneIteration-8 467874 2451 ns/op BenchmarkGenerate/8bits-8 4045 1033196 ns/op BenchmarkGenerate/16bits-8 16 101939954 ns/op BenchmarkGenerate/24bits-8 1 11411513764 ns/op PASS ok github.com/nbd-wtf/go-nostr/nip13 22.220s
This commit is contained in:
parent
bb1138a2fa
commit
5bfb398f4d
77
nip13/nip13.go
Normal file
77
nip13/nip13.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Package nip13 implements NIP-13
|
||||||
|
// See https://github.com/nostr-protocol/nips/blob/master/13.md for details.
|
||||||
|
package nip13
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
nostr "github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDifficultyTooLow = errors.New("nip13: insufficient difficulty")
|
||||||
|
ErrGenerateTimeout = errors.New("nip13: generating proof of work took too long")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Difficulty counts the number of leading zero bits in an event ID.
|
||||||
|
// It returns a negative number if the event ID is malformed.
|
||||||
|
func Difficulty(eventID string) int {
|
||||||
|
if len(eventID) != 64 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
var zeros int
|
||||||
|
for i := 0; i < 64; i += 2 {
|
||||||
|
if eventID[i:i+2] == "00" {
|
||||||
|
zeros += 8
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var b [1]byte
|
||||||
|
if _, err := hex.Decode(b[:], []byte{eventID[i], eventID[i+1]}); err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
zeros += bits.LeadingZeros8(b[0])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return zeros
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check reports whether the event ID demonstrates a sufficient proof of work difficulty.
|
||||||
|
// 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 {
|
||||||
|
return ErrDifficultyTooLow
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate performs proof of work on the specified event until either the target
|
||||||
|
// difficulty is reached or the function runs for longer than the timeout.
|
||||||
|
// The latter case results in ErrGenerateTimeout.
|
||||||
|
//
|
||||||
|
// Upon success, the returned event always contains a "nonce" tag with the target difficulty
|
||||||
|
// commitment, and an updated event.CreatedAt.
|
||||||
|
func Generate(event *nostr.Event, targetDifficulty int, timeout time.Duration) (*nostr.Event, error) {
|
||||||
|
tag := nostr.Tag{"nonce", "", strconv.Itoa(targetDifficulty)}
|
||||||
|
event.Tags = append(event.Tags, tag)
|
||||||
|
var nonce uint64
|
||||||
|
start := time.Now()
|
||||||
|
for {
|
||||||
|
nonce++
|
||||||
|
tag[1] = strconv.FormatUint(nonce, 10)
|
||||||
|
event.CreatedAt = time.Now()
|
||||||
|
if Difficulty(event.GetID()) >= targetDifficulty {
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
// benchmarks show one iteration is approx 3000ns on i7-8565U @ 1.8GHz.
|
||||||
|
// so, check every 3ms; arbitrary
|
||||||
|
if nonce%1000 == 0 && time.Since(start) > timeout {
|
||||||
|
return nil, ErrGenerateTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
nip13/nip13_test.go
Normal file
149
nip13/nip13_test.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package nip13
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
nostr "github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheck(t *testing.T) {
|
||||||
|
const eventID = "000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"
|
||||||
|
tests := []struct {
|
||||||
|
minDifficulty int
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{-1, nil},
|
||||||
|
{0, nil},
|
||||||
|
{1, nil},
|
||||||
|
{35, nil},
|
||||||
|
{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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateShort(t *testing.T) {
|
||||||
|
event := &nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: "It's just me mining my own business",
|
||||||
|
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
|
||||||
|
}
|
||||||
|
pow, err := Generate(event, 0, 3*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testNonceTag(t, pow, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateLong(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("too consuming for short mode")
|
||||||
|
}
|
||||||
|
for _, difficulty := range []int{8, 16} {
|
||||||
|
difficulty := difficulty
|
||||||
|
t.Run(fmt.Sprintf("%dbits", difficulty), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
event := &nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: "It's just me mining my own business",
|
||||||
|
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
|
||||||
|
}
|
||||||
|
pow, err := Generate(event, difficulty, time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Check(pow.GetID(), difficulty); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
testNonceTag(t, pow, difficulty)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNonceTag(t *testing.T, event *nostr.Event, commitment int) {
|
||||||
|
t.Helper()
|
||||||
|
tagptr := event.Tags.GetFirst([]string{"nonce"})
|
||||||
|
if tagptr == nil {
|
||||||
|
t.Fatal("no nonce tag")
|
||||||
|
}
|
||||||
|
tag := *tagptr
|
||||||
|
if tag[0] != "nonce" {
|
||||||
|
t.Errorf("tag[0] = %q; want 'nonce'", tag[0])
|
||||||
|
}
|
||||||
|
if n, err := strconv.ParseInt(tag[1], 10, 64); err != nil || n < 1 {
|
||||||
|
t.Errorf("tag[1] = %q; want an int greater than 0", tag[1])
|
||||||
|
}
|
||||||
|
if n, err := strconv.Atoi(tag[2]); err != nil || n != commitment {
|
||||||
|
t.Errorf("tag[2] = %q; want %d", tag[2], commitment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateTimeout(t *testing.T) {
|
||||||
|
event := &nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: "It's just me mining my own business",
|
||||||
|
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
|
||||||
|
}
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
_, err := Generate(event, 256, time.Millisecond)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Generate took too long to timeout")
|
||||||
|
case err := <-done:
|
||||||
|
if !errors.Is(err, ErrGenerateTimeout) {
|
||||||
|
t.Errorf("Generate returned %v; want ErrGenerateTimeout", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCheck(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
Check("000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d", 36)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGenerateOneIteration(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
event := &nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: "It's just me mining my own business",
|
||||||
|
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
|
||||||
|
}
|
||||||
|
if _, err := Generate(event, 0, time.Minute); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGenerate(b *testing.B) {
|
||||||
|
if testing.Short() {
|
||||||
|
b.Skip("too consuming for short mode")
|
||||||
|
}
|
||||||
|
for _, difficulty := range []int{8, 16, 24} {
|
||||||
|
difficulty := difficulty
|
||||||
|
b.Run(fmt.Sprintf("%dbits", difficulty), func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
event := &nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: "It's just me mining my own business",
|
||||||
|
PubKey: "a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243",
|
||||||
|
}
|
||||||
|
if _, err := Generate(event, difficulty, time.Minute); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user