mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 13:22:56 +01:00
141 lines
3.6 KiB
Go
141 lines
3.6 KiB
Go
package nip13
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"math/bits"
|
|
"runtime"
|
|
"strconv"
|
|
|
|
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")
|
|
ErrMissingPubKey = errors.New("nip13: attempting to work on an event without a pubkey, which makes no sense")
|
|
)
|
|
|
|
// CommittedDifficulty returns the Difficulty but checks the "nonce" tag for a target.
|
|
//
|
|
// if the target is smaller than the actual difficulty then the value of the target is used.
|
|
// if the target is bigger than the actual difficulty then it returns 0.
|
|
func CommittedDifficulty(event *nostr.Event) int {
|
|
work := 0
|
|
if nonceTag := event.Tags.Find("nonce"); nonceTag != nil && len(nonceTag) >= 3 {
|
|
work = Difficulty(event.ID)
|
|
target, _ := strconv.Atoi(nonceTag[2])
|
|
if target <= work {
|
|
work = target
|
|
} else {
|
|
work = 0
|
|
}
|
|
}
|
|
return work
|
|
}
|
|
|
|
// Difficulty counts the number of leading zero bits in an event ID.
|
|
func Difficulty(id string) int {
|
|
var zeros int
|
|
var b [1]byte
|
|
for i := 0; i < 64; i += 2 {
|
|
if id[i:i+2] == "00" {
|
|
zeros += 8
|
|
continue
|
|
}
|
|
if _, err := hex.Decode(b[:], []byte{id[i], id[i+1]}); err != nil {
|
|
return -1
|
|
}
|
|
zeros += bits.LeadingZeros8(b[0])
|
|
break
|
|
}
|
|
return zeros
|
|
}
|
|
|
|
func difficultyBytes(id [32]byte) int {
|
|
var zeroBits int
|
|
for _, idByte := range id {
|
|
if idByte == 0 {
|
|
zeroBits += 8
|
|
continue
|
|
}
|
|
zeroBits += bits.LeadingZeros8(idByte)
|
|
break
|
|
}
|
|
return zeroBits
|
|
}
|
|
|
|
// 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(id string, minDifficulty int) error {
|
|
if Difficulty(id) < minDifficulty {
|
|
return ErrDifficultyTooLow
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DoWork() performs work in multiple threads (given by runtime.NumCPU()) and returns the first
|
|
// nonce (as a nostr.Tag) that yields the required work.
|
|
// Returns an error if the context expires before that.
|
|
func DoWork(ctx context.Context, event nostr.Event, targetDifficulty int) (nostr.Tag, error) {
|
|
if event.PubKey == "" {
|
|
return nil, ErrMissingPubKey
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
nthreads := runtime.NumCPU()
|
|
tagCh := make(chan nostr.Tag)
|
|
|
|
for i := 0; i < nthreads; i++ {
|
|
// we must copy the tags here otherwise only a pointer to them will be copied below
|
|
// and that will cause all sorts of weird races when computing the difficulty
|
|
event.Tags = append(nostr.Tags{}, event.Tags...)
|
|
|
|
go func(event nostr.Event, nonce uint64) {
|
|
// we make a tag here and add it -- later we will just overwrite it on each iteration
|
|
tag := nostr.Tag{"nonce", "", strconv.Itoa(targetDifficulty)}
|
|
event.Tags = append(event.Tags, tag)
|
|
|
|
for {
|
|
// try 10000 times (~30ms)
|
|
for n := 0; n < 10000; n++ {
|
|
tag[1] = strconv.FormatUint(nonce, 10)
|
|
|
|
if difficultyBytes(sha256.Sum256(event.Serialize())) >= targetDifficulty {
|
|
// must select{} here otherwise a goroutine that finds a good nonce
|
|
// right after the first will get stuck in the ch forever
|
|
select {
|
|
case tagCh <- tag:
|
|
case <-ctx.Done():
|
|
}
|
|
cancel()
|
|
return
|
|
}
|
|
|
|
nonce += uint64(nthreads)
|
|
}
|
|
|
|
// then check if the context was canceled
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
// otherwise keep trying
|
|
}
|
|
}
|
|
}(event, uint64(i))
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ErrGenerateTimeout
|
|
case tag := <-tagCh:
|
|
return tag, nil
|
|
}
|
|
}
|