diff --git a/go.mod b/go.mod index df1f1b2..340dffa 100644 --- a/go.mod +++ b/go.mod @@ -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 ) @@ -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 ) diff --git a/main.go b/main.go index 4392849..6494c96 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,8 @@ Usage: noscl sign noscl verify noscl public - noscl publish [--reference=...] [--profile=...] + noscl publish [--reference=...] [--profile=...] [--pow=] + noscl pow noscl metadata --name= [--description=] [--image=] noscl profile noscl follow [--name=] @@ -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): diff --git a/pow.go b/pow.go new file mode 100644 index 0000000..db1869d --- /dev/null +++ b/pow.go @@ -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[""].(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("") + 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) +} diff --git a/printer.go b/printer.go index f44db63..ba64e19 100644 --- a/printer.go +++ b/printer.go @@ -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{ @@ -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) { @@ -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, @@ -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) } @@ -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 { diff --git a/publish.go b/publish.go index 059b795..744408a 100644 --- a/publish.go +++ b/publish.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "errors" "log" "time" @@ -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[""].(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