diff --git a/README.md b/README.md index c1d86ae..fc7c512 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,12 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: • events stored: 4, subscriptions opened: 1 ``` +### make an event with a PoW target +```shell +~> nak event -c 'hello getwired.app and labour.fiatjaf.com' --pow 24 +{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"} +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. diff --git a/event.go b/event.go index df1dbb0..cedb0b8 100644 --- a/event.go +++ b/event.go @@ -11,11 +11,16 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip19" "golang.org/x/exp/slices" ) -const CATEGORY_EVENT_FIELDS = "EVENT FIELDS" +const ( + CATEGORY_EVENT_FIELDS = "EVENT FIELDS" + CATEGORY_SIGNER = "SIGNER OPTIONS" + CATEGORY_EXTRAS = "EXTRAS" +) var event = &cli.Command{ Name: "event", @@ -38,19 +43,23 @@ example: Usage: "secret key to sign the event, as nsec, ncryptsec or hex", DefaultText: "the key '1'", Value: "0000000000000000000000000000000000000000000000000000000000000001", + Category: CATEGORY_SIGNER, }, &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", + Name: "connect", + Usage: "sign event using NIP-46, expects a bunker://... URL", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ Name: "connect-as", Usage: "private key to when communicating with the bunker given on --connect", DefaultText: "a random key", + Category: CATEGORY_SIGNER, }, // ~ these args are only for the convoluted musig2 signing process // they will be generally copy-shared-pasted across some manual coordination method between participants @@ -59,6 +68,7 @@ example: Usage: "number of signers to use for musig2", Value: 1, DefaultText: "1 -- i.e. do not use musig2 at all", + Category: CATEGORY_SIGNER, }, &cli.StringSliceFlag{ Name: "musig-pubkey", @@ -77,17 +87,25 @@ example: Hidden: true, }, // ~~~ - &cli.BoolFlag{ - Name: "envelope", - Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", + &cli.UintFlag{ + Name: "pow", + Usage: "NIP-13 difficulty to target when doing hash work on the event id", + Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ - Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Name: "envelope", + Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", + Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ - Name: "nevent", - Usage: "print the nevent code (to stderr) after the event is published", + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Category: CATEGORY_EXTRAS, + }, + &cli.BoolFlag{ + Name: "nevent", + Usage: "print the nevent code (to stderr) after the event is published", + Category: CATEGORY_EXTRAS, }, &cli.UintFlag{ Name: "kind", @@ -193,8 +211,9 @@ example: mustRehashAndResign = true } - tags := make(nostr.Tags, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { + tagFlags := c.StringSlice("tag") + tags := make(nostr.Tags, 0, len(tagFlags)+2) + for _, tagFlag := range tagFlags { // tags are in the format key=value tagName, tagValue, found := strings.Cut(tagFlag, "=") tag := []string{tagName} @@ -203,20 +222,17 @@ example: tagValues := strings.Split(tagValue, ";") tag = append(tag, tagValues...) } - tags = tags.AppendUnique(tag) + tags = append(tags, tag) } for _, etag := range c.StringSlice("e") { tags = tags.AppendUnique([]string{"e", etag}) - mustRehashAndResign = true } for _, ptag := range c.StringSlice("p") { tags = tags.AppendUnique([]string{"p", ptag}) - mustRehashAndResign = true } for _, dtag := range c.StringSlice("d") { tags = tags.AppendUnique([]string{"d", dtag}) - mustRehashAndResign = true } if len(tags) > 0 { for _, tag := range tags { @@ -233,6 +249,31 @@ example: mustRehashAndResign = true } + if difficulty := c.Uint("pow"); difficulty > 0 { + // before doing pow we need the pubkey + if bunker != nil { + evt.PubKey, err = bunker.GetPublicKey(ctx) + if err != nil { + return fmt.Errorf("can't pow: failed to get public key from bunker: %w", err) + } + } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + pubkeys := c.StringSlice("musig-pubkey") + if int(numSigners) != len(pubkeys) { + return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront") + } + evt.PubKey, err = getMusigAggregatedKey(ctx, pubkeys) + if err != nil { + return err + } + } else { + evt.PubKey, _ = nostr.GetPublicKey(sec) + } + + // try to generate work with this difficulty -- essentially forever + nip13.Generate(&evt, int(difficulty), time.Hour*24*365) + mustRehashAndResign = true + } + if evt.Sig == "" || mustRehashAndResign { if bunker != nil { if err := bunker.SignEvent(ctx, &evt); err != nil { diff --git a/musig2.go b/musig2.go index 1775125..7e0a42e 100644 --- a/musig2.go +++ b/musig2.go @@ -15,6 +15,31 @@ import ( "github.com/nbd-wtf/go-nostr" ) +func getMusigAggregatedKey(_ context.Context, keys []string) (string, error) { + knownSigners := make([]*btcec.PublicKey, len(keys)) + for i, spk := range keys { + bpk, err := hex.DecodeString(spk) + if err != nil { + return "", fmt.Errorf("'%s' is invalid hex: %w", spk, err) + } + if len(bpk) == 32 { + return "", fmt.Errorf("'%s' is missing the leading parity byte", spk) + } + pk, err := btcec.ParsePubKey(bpk) + if err != nil { + return "", fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err) + } + knownSigners[i] = pk + } + + aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true) + if err != nil { + return "", fmt.Errorf("aggregation failed: %w", err) + } + + return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil +} + func performMusig( _ context.Context, sec string,