Skip to content
This repository has been archived by the owner on Jun 20, 2024. It is now read-only.

Events with Pow #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/fiatjaf/go-nostr v0.5.0
github.com/mitchellh/go-homedir v1.1.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
gopkg.in/yaml.v2 v2.2.2
)

Expand All @@ -19,5 +20,4 @@ require (
github.com/kr/pretty v0.2.1 // indirect
github.com/tyler-smith/go-bip32 v1.0.0 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
)
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Usage:
noscl sign <event-json>
noscl verify <event-json>
noscl public
noscl publish [--reference=<id>...] [--profile=<id>...] <content>
noscl publish [--reference=<id>...] [--profile=<id>...] [--pow=<n>] <content>
noscl pow <n> <id>
noscl metadata --name=<name> [--description=<description>] [--image=<image>]
noscl profile <key>
noscl follow <key> [--name=<name>]
Expand Down Expand Up @@ -80,6 +81,8 @@ func main() {
showPublicKey(opts)
case opts["publish"].(bool):
publish(opts)
case opts["pow"].(bool):
pow(opts)
case opts["share-contacts"].(bool):
shareContacts(opts)
case opts["key-gen"].(bool):
Expand Down
151 changes: 151 additions & 0 deletions pow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package main

import (
"bytes"
"encoding/hex"
"encoding/json"
"log"
"math/rand"
"time"

"github.com/docopt/docopt-go"
"github.com/fiatjaf/go-nostr"
"golang.org/x/crypto/scrypt"
)

type checkedPow struct {
nonce string
hash []byte
}

func checkPowVote(evt nostr.Event) (map[string]checkedPow, bool) {
var e string
for _, tag := range evt.Tags {
if len(tag) < 2 {
continue
}
currentTagName, ok := tag[0].(string)
if !ok || currentTagName != "e" {
continue
}
currentTagValue, ok := tag[1].(string)
if !ok {
continue
}
e = currentTagValue
break
}
if e == "" {
log.Printf("No referenced event,\n")
return nil, false
}
evt.ID = e

pow := make([][]string, 0, 1)
if err := json.Unmarshal([]byte(evt.Content), &pow); err != nil {
log.Printf("Could not decode POW: %s", evt.Content)
return nil, false
}
evt.Pow = pow

return checkPow(evt)
}

func checkPow(evt nostr.Event) (map[string]checkedPow, bool) {
id, err := hex.DecodeString(evt.ID)
if err != nil {
log.Printf("Could not decode id: %s.\n", err.Error())
return nil, false
}
result := make(map[string]checkedPow, 1)
for _, pow := range evt.Pow {
switch pow[0] {
case "scrypt":
nonce, err := hex.DecodeString(pow[1])
if err != nil {
log.Printf("Could not decode nonce: %s.\n", err.Error())
return nil, false
}
hash, err := scrypt.Key(id, nonce, 32768, 8, 1, 32)
if err != nil {
log.Printf("Error hashsing scrypt: %s.\n", err.Error())
return nil, false
}
if opow, ok := result["scrypt"]; ok {
if bytes.Compare(hash, opow.hash) < 0 {
// hash less than opow.hash
result["scrypt"] = checkedPow{pow[1], hash}
}
} else {
result["scrypt"] = checkedPow{pow[1], hash}
}
}
}
return result, true
}

func powScrypt(message []byte, n int) ([]byte, error) {

bestnonce := make([]byte, 8)
nonce := make([]byte, 8)
best := []byte{
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255}
for i := 0; i < n; i++ {
rand.Read(nonce)
other, err := scrypt.Key(message, nonce, 32768, 8, 1, 32)
if err != nil {
return bestnonce, err
}
if bytes.Compare(other, best) < 0 { // other less than best
best = other
copy(bestnonce, nonce)
}
}

return bestnonce, nil
}

func pow(opts docopt.Opts) {
initNostr()
rand.Seed(time.Now().UnixNano())

e := opts["<id>"].(string)
message, err := hex.DecodeString(e)
if err != nil {
log.Printf("Could not decode event id: %s.\n", err.Error())
return
}

tags := make(nostr.Tags, 0, 1)
tags = append(tags, nostr.Tag([]interface{}{"e", e}))

n, err := opts.Int("<n>")
if err != nil {
log.Printf("Not a number of hashes to perform: %s.\n", err.Error())
return
}

nonce, err := powScrypt(message, n)
if err != nil {
log.Printf("POW error: %s.\n", err.Error())
return
}

pow, _ := json.Marshal([][]string{{"scrypt", hex.EncodeToString(nonce)}})

event, statuses, err := pool.PublishEvent(&nostr.Event{
CreatedAt: uint32(time.Now().Unix()),
Kind: nostr.KindPow,
Tags: tags,
Content: string(pow),
})
if err != nil {
log.Printf("Error publishing: %s.\n", err.Error())
return
}

printPublishStatus(event, statuses)
}
38 changes: 34 additions & 4 deletions printer.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package main

import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/dustin/go-humanize"
"github.com/fiatjaf/go-nostr"
"gopkg.in/yaml.v2"
"math/big"
"strings"
"time"
)

var kindNames = map[int]string{
Expand All @@ -18,6 +19,7 @@ var kindNames = map[int]string{
nostr.KindContactList: "Contact List",
nostr.KindEncryptedDirectMessage: "Encrypted Message",
nostr.KindDeletion: "Deletion Notice",
nostr.KindPow: "Proof of Work",
}

func printEvent(evt nostr.Event, nick *string) {
Expand All @@ -41,7 +43,7 @@ func printEvent(evt nostr.Event, nick *string) {
fromField = fmt.Sprintf("%s (%s)", *nick, shorten(evt.PubKey))
}

fmt.Printf("%s [%s] from %s %s\n ",
fmt.Printf("%s [%s] from %s %s\n",
kind,
shorten(evt.ID),
fromField,
Expand All @@ -60,10 +62,22 @@ func printEvent(evt nostr.Event, nick *string) {
y, _ := yaml.Marshal(metadata)
fmt.Print(string(y))
case nostr.KindTextNote:
if pows, ok := checkPow(evt); ok {
for alg, pow := range pows {
fmt.Printf(" ↑ %s %s\n", alg, shortPowHash(pow.hash))
}
}
fmt.Printf(" ")
fmt.Print(strings.ReplaceAll(evt.Content, "\n", "\n "))
case nostr.KindRecommendServer:
case nostr.KindContactList:
case nostr.KindEncryptedDirectMessage:
case nostr.KindPow:
if pows, ok := checkPowVote(evt); ok {
for alg, pow := range pows {
fmt.Printf(" ↑ %s %s", alg, shortPowHash(pow.hash))
}
}
default:
fmt.Print(evt.Content)
}
Expand All @@ -78,6 +92,22 @@ func shorten(id string) string {
return id[0:4] + "..." + id[len(id)-4:]
}

// A hash is an integer with a maximum value of MAX = 256^(len(hash))
// The probability that a single hash is less than some number x is:
// p = x / MAX
// The number of hashes, k, needed to generate a hash less than x,
// follows a geometric distribution (wikipedia.org/wiki/Geometric_distribution):
// Pr(X=k) = (1-p)^{k-1} p
// Thus, for a given x, the expected number of hashes is:
// E(X) = 1/p = MAX/x
func shortPowHash(hash []byte) string {
var x, MAX, r big.Int
x.SetBytes(hash)
MAX.Exp(big.NewInt(256), big.NewInt(int64(len(hash))), nil)
r.Div(&MAX, &x)
return fmt.Sprintf("+%s (%s...)", r.String(), hex.EncodeToString(hash)[0:12])
}

func printPublishStatus(event *nostr.Event, statuses chan nostr.PublishStatus) {
for status := range statuses {
switch status.Status {
Expand Down
22 changes: 19 additions & 3 deletions publish.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/hex"
"errors"
"log"
"time"
Expand Down Expand Up @@ -39,12 +40,27 @@ func publish(opts docopt.Opts) {
tags = append(tags, nostr.Tag([]interface{}{"p", profile}))
}

event, statuses, err := pool.PublishEvent(&nostr.Event{
evt := &nostr.Event{
CreatedAt: uint32(time.Now().Unix()),
Kind: nostr.KindTextNote,
Tags: tags,
Content: opts["<content>"].(string),
})
Tags: tags,
}

evt, err = pool.SignEvent(evt)
if err != nil {
log.Printf("Error signing: %s.\n", err.Error())
return
}

if n, err := opts.Int("--pow"); err == nil {
message, _ := hex.DecodeString(evt.ID)
if nonce, err := powScrypt(message, n); err == nil {
evt.Pow = [][]string{{"scrypt", hex.EncodeToString(nonce)}}
}
}

event, statuses, err := pool.PublishEvent(evt)
if err != nil {
log.Printf("Error publishing: %s.\n", err.Error())
return
Expand Down