Compare commits

...

9 Commits

Author SHA1 Message Date
fiatjaf
71dfe583ed rename flags from --musig2-... to --musig-..., add id to event and other small tweaks. 2024-05-14 23:52:56 -03:00
fiatjaf
84bde7dacd musig2 works now. 2024-05-14 23:41:12 -03:00
fiatjaf
81968f6c0c nak key combine and nak event --musig2 2024-05-14 15:23:08 -03:00
fiatjaf
f198a46c19 remove wss:// from relay urls in readme. 2024-04-29 08:36:59 -03:00
fiatjaf
c3ea9c15f6 LimitZero when -l 0 and when --stream 2024-03-29 08:16:28 -03:00
Yasuhiro Matsumoto
8ddb9ce021 fix color output on Windows 2024-03-28 15:52:30 -03:00
fiatjaf
569d38a137 accept multiple arguments in many commands, add a lot of more tests. 2024-03-19 11:34:59 -03:00
fiatjaf
34c189af28 bunker improvements. 2024-03-02 08:18:40 -03:00
fiatjaf
ffe2db7f96 event: accept tags with a single item. 2024-02-21 09:47:34 -03:00
15 changed files with 612 additions and 74 deletions

View File

@@ -16,7 +16,7 @@ take a look at the help text that comes in it to learn all possibilities, but he
### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays
```shell
~> nak event --sec 02 -c 'good morning' --tag t=gm wss://nostr-pub.wellorder.net wss://relay.damus.io
~> nak event --sec 02 -c 'good morning' --tag t=gm nostr-pub.wellorder.net relay.damus.io
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
publishing to wss://nostr-pub.wellorder.net... success.
publishing to wss://relay.damus.io... success.
@@ -24,7 +24,7 @@ publishing to wss://relay.damus.io... success.
### query a bunch of relays for a tag with a limit of 2 for each, print their content
```shell
~> nak req -k 1 -t t=gm -l 2 wss://nostr.mom wss://nostr.wine wss://nostr-pub.wellorder.net | jq .content
~> nak req -k 1 -t t=gm -l 2 nostr.mom nostr.wine nostr-pub.wellorder.net | jq .content
"#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. "
"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg "
"good morning"
@@ -34,13 +34,13 @@ publishing to wss://relay.damus.io... success.
### decode a nip19 note1 code, add a relay hint, encode it back to nevent1
```shell
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r wss://nostr.zbd.gg
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r nostr.zbd.gg
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
{
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
"relays": [
"wss://nostr.zbd.gg"
"nostr.zbd.gg"
]
}
```
@@ -61,7 +61,7 @@ nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue
### republish an event from one relay to multiple others
```shell
~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a wss://nostr.wine/ wss://nostr-pub.wellorder.net | nak event wss://nostr.wine wss://offchain.pub wss://public.relaying.io wss://eden.nostr.land wss://atlas.nostr.land wss://relayable.org
~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a nostr.wine/ nostr-pub.wellorder.net | nak event nostr.wine offchain.pub public.relaying.io eden.nostr.land atlas.nostr.land relayable.org
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
publishing to wss://offchain.pub... success.
@@ -79,7 +79,7 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c
### fetch all quoted events by a given pubkey in their last 100 notes
```shell
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io
```
## Contributing to this repository

View File

@@ -101,11 +101,16 @@ var bunker = &cli.Command{
_, cancel := context.WithCancel(c.Context)
cancelPreviousBunkerInfoPrint = cancel
// asking user for authorization
signer.AuthorizeRequest = func(harmless bool, from string) bool {
return alwaysYes || harmless || askProceed(from)
}
for ie := range events {
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
// handle the NIP-46 request event
req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event)
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
if err != nil {
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
continue
@@ -116,25 +121,23 @@ var bunker = &cli.Command{
jresp, _ := json.MarshalIndent(resp, " ", " ")
log("~ responding with %s\n", string(jresp))
if alwaysYes || harmless || askProceed(ie.Event.PubKey) {
handlerWg.Add(len(relayURLs))
for _, relayURL := range relayURLs {
go func(relayURL string) {
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(c.Context, eventResponse)
printLock.Lock()
if err == nil {
log("* sent response through %s\n", relay.URL)
} else {
log("* failed to send response: %s\n", err)
}
printLock.Unlock()
handlerWg.Done()
handlerWg.Add(len(relayURLs))
for _, relayURL := range relayURLs {
go func(relayURL string) {
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(c.Context, eventResponse)
printLock.Lock()
if err == nil {
log("* sent response through %s\n", relay.URL)
} else {
log("* failed to send response: %s\n", err)
}
}(relayURL)
}
handlerWg.Wait()
printLock.Unlock()
handlerWg.Done()
}
}(relayURL)
}
handlerWg.Wait()
// just after handling one request we trigger this
go func() {

View File

@@ -33,7 +33,7 @@ var decode = &cli.Command{
},
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
Action: func(c *cli.Context) error {
for input := range getStdinLinesOrFirstArgument(c.Args().First()) {
for input := range getStdinLinesOrArguments(c.Args()) {
if strings.HasPrefix(input, "nostr:") {
input = input[6:]
}

View File

@@ -29,7 +29,7 @@ var encode = &cli.Command{
Name: "npub",
Usage: "encode a hex public key into bech32 'npub' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValidPublicKey(target); !ok {
lineProcessingError(c, "invalid public key: %s", target)
continue
@@ -50,7 +50,7 @@ var encode = &cli.Command{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid private key: %s", target)
continue
@@ -78,7 +78,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid public key: %s", target)
continue
@@ -115,7 +115,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target)
continue
@@ -212,7 +212,7 @@ var encode = &cli.Command{
Name: "note",
Usage: "generate note1 event codes (not recommended)",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c.Args().First()) {
for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target)
continue

View File

@@ -53,6 +53,31 @@ example:
Usage: "private key to when communicating with the bunker given on --connect",
DefaultText: "a random key",
},
// ~ these args are only for the convoluted musig2 signing process
// they will be generally copy-shared-pasted across some manual coordination method between participants
&cli.UintFlag{
Name: "musig",
Usage: "number of signers to use for musig2",
Value: 1,
DefaultText: "1 -- i.e. do not use musig2 at all",
},
&cli.StringSliceFlag{
Name: "musig-pubkey",
Hidden: true,
},
&cli.StringFlag{
Name: "musig-nonce-secret",
Hidden: true,
},
&cli.StringSliceFlag{
Name: "musig-nonce",
Hidden: true,
},
&cli.StringSliceFlag{
Name: "musig-partial",
Hidden: true,
},
// ~~~
&cli.BoolFlag{
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
@@ -182,9 +207,8 @@ example:
// tags may also contain extra elements separated with a ";"
tagValues := strings.Split(tagValue, ";")
tag = append(tag, tagValues...)
// ~
tags = tags.AppendUnique(tag)
}
tags = tags.AppendUnique(tag)
}
for _, etag := range c.StringSlice("e") {
@@ -227,6 +251,21 @@ example:
if err := bunker.SignEvent(c.Context, &evt); err != nil {
return fmt.Errorf("failed to sign with bunker: %w", err)
}
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
pubkeys := c.StringSlice("musig-pubkey")
secNonce := c.String("musig-nonce-secret")
pubNonces := c.StringSlice("musig-nonce")
partialSigs := c.StringSlice("musig-partial")
signed, err := performMusig(c.Context,
sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs)
if err != nil {
return fmt.Errorf("musig error: %w", err)
}
if !signed {
// we haven't finished signing the event, so the users still have to do more steps
// instructions for what to do should have been printed by the performMusig() function
return nil
}
} else if err := evt.Sign(sec); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}

View File

@@ -1,31 +1,135 @@
package main
import "os"
func ExampleEventBasic() {
app.Run([]string{"nak", "event", "--ts", "1699485669"})
// Output:
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
}
// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
func ExampleEventParsingFromStdin() {
prevStdin := os.Stdin
defer func() { os.Stdin = prevStdin }()
r, w, _ := os.Pipe()
os.Stdin = r
w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n")
app.Run([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
// Output:
// {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"}
// {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"}
}
func ExampleEventComplex() {
app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"})
// Output:
// {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
}
func ExampleEncode() {
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"})
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a
}
func ExampleDecode() {
app.Run([]string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
// Output:
// {
// "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
// "kind": 31923,
// "identifier": "4cd6cfe7",
// "relays": [
// "wss://nos.lol"
// ]
// }
// {
// "id": "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5",
// "relays": [
// "wss://pyramid.fiatjaf.com/",
// "wss://relay.westernbtc.com/",
// "wss://relay.snort.social/",
// "wss://atlas.nostr.land/"
// ]
// }
}
func ExampleReq() {
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
// Output:
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
}
func ExampleEncodeNpub() {
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
func ExampleReqIdFromRelay() {
app.Run([]string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"})
// Output:
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
}
func ExampleEncodeNprofile() {
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
func ExampleMultipleFetch() {
app.Run([]string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
// Output:
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"}
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
}
func ExampleKeyPublic() {
app.Run([]string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"})
// Output:
// 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
}
func ExampleKeyDecrypt() {
app.Run([]string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"})
// Output:
// nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0
}
func ExampleRelay() {
app.Run([]string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"})
// Output:
// {
// "name": "nos.social strfry relay",
// "description": "This is a strfry instance handled by nos.social",
// "pubkey": "89ef92b9ebe6dc1e4ea398f6477f227e95429627b0a33dc89b640e137b256be5",
// "contact": "https://nos.social",
// "supported_nips": [
// 1,
// 2,
// 4,
// 9,
// 11,
// 12,
// 16,
// 20,
// 22,
// 28,
// 33,
// 40
// ],
// "software": "git+https://github.com/hoytech/strfry.git",
// "version": "0.9.4",
// "icon": ""
// }
// {
// "name": "the fiatjaf pyramid",
// "description": "a relay just for the coolest of the coolest",
// "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
// "contact": "",
// "supported_nips": [],
// "software": "https://github.com/fiatjaf/khatru",
// "version": "n/a",
// "limitation": {
// "auth_required": false,
// "payment_required": false,
// "restricted_writes": true
// },
// "icon": "https://clipart-library.com/images_k/pyramid-transparent/pyramid-transparent-19.png"
// }
}

View File

@@ -31,7 +31,7 @@ var fetch = &cli.Command{
})
}()
for code := range getStdinLinesOrFirstArgument(c.Args().First()) {
for code := range getStdinLinesOrArguments(c.Args()) {
filter := nostr.Filter{}
prefix, value, err := nip19.Decode(code)

7
go.mod
View File

@@ -5,24 +5,24 @@ go 1.21
toolchain go1.21.0
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/fatih/color v1.16.0
github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.28.6
github.com/nbd-wtf/go-nostr v0.30.2
github.com/nbd-wtf/nostr-sdk v0.0.5
github.com/urfave/cli/v2 v2.25.7
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
)
require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fiatjaf/eventstore v0.2.16 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -38,4 +38,5 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.8.0 // indirect
)

15
go.sum
View File

@@ -6,6 +6,8 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v5
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
@@ -41,6 +43,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPc
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
@@ -79,8 +83,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nbd-wtf/go-nostr v0.28.6 h1:iOyzk+6ReG0lvyRAar7w7omFmUk5mnXDyFYkJ+zEjiw=
github.com/nbd-wtf/go-nostr v0.28.6/go.mod h1:aFcp8NO3erHg+glzBfh4wpaMrV1/ahcUPAgITdptxwA=
github.com/nbd-wtf/go-nostr v0.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI=
github.com/nbd-wtf/go-nostr v0.30.2/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -92,6 +96,7 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
@@ -99,6 +104,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -142,6 +149,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -158,3 +167,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -23,7 +23,7 @@ const (
)
var log = func(msg string, args ...any) {
fmt.Fprintf(os.Stderr, msg, args...)
fmt.Fprintf(color.Error, msg, args...)
}
var stdout = fmt.Println
@@ -45,13 +45,21 @@ func getStdinLinesOrBlank() chan string {
}
}
func getStdinLinesOrFirstArgument(arg string) chan string {
func getStdinLinesOrArguments(args cli.Args) chan string {
return getStdinLinesOrArgumentsFromSlice(args.Slice())
}
func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
// try the first argument
if arg != "" {
single := make(chan string, 1)
single <- arg
close(single)
return single
if len(args) > 0 {
argsCh := make(chan string, 1)
go func() {
for _, arg := range args {
argsCh <- arg
}
close(argsCh)
}()
return argsCh
}
// try the stdin
@@ -142,7 +150,9 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
} else {
clientKey = nostr.GeneratePrivateKey()
}
bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil)
bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) {
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
})
return "", bunker, err
}
sec := c.String("sec")
@@ -195,7 +205,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) {
func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) {
return _ask(&readline.Config{
Stdout: os.Stderr,
Stdout: color.Error,
Prompt: color.YellowString(msg),
InterruptPrompt: "^C",
DisableAutoSaveHistory: true,
@@ -204,7 +214,7 @@ func ask(msg string, defaultValue string, shouldAskAgain func(answer string) boo
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
config := &readline.Config{
Stdout: os.Stderr,
Stdout: color.Error,
Prompt: color.YellowString(msg),
InterruptPrompt: "^C",
DisableAutoSaveHistory: true,

47
key.go
View File

@@ -1,9 +1,12 @@
package main
import (
"encoding/hex"
"fmt"
"strings"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip49"
@@ -19,6 +22,7 @@ var key = &cli.Command{
public,
encrypt,
decrypt,
combine,
},
}
@@ -39,7 +43,7 @@ var public = &cli.Command{
Description: ``,
ArgsUsage: "[secret]",
Action: func(c *cli.Context) error {
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, c.Args().First()) {
for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) {
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
lineProcessingError(c, "failed to derive public key: %s", err)
@@ -78,7 +82,7 @@ var encrypt = &cli.Command{
if password == "" {
return fmt.Errorf("no password given")
}
for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, content) {
for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) {
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
if err != nil {
lineProcessingError(c, "failed to encrypt: %s", err)
@@ -109,7 +113,7 @@ var decrypt = &cli.Command{
if password == "" {
return fmt.Errorf("no password given")
}
for ncryptsec := range getStdinLinesOrFirstArgument(content) {
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{content}) {
sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil {
lineProcessingError(c, "failed to decrypt: %s", err)
@@ -122,10 +126,41 @@ var decrypt = &cli.Command{
},
}
func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) chan string {
var combine = &cli.Command{
Name: "combine",
Usage: "combines two or more pubkeys using musig2",
Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.`,
ArgsUsage: "[pubkey...]",
Action: func(c *cli.Context) error {
keys := make([]*btcec.PublicKey, 0, 5)
for _, pub := range c.Args().Slice() {
keyb, err := hex.DecodeString(pub)
if err != nil {
return fmt.Errorf("error parsing key %s: %w", pub, err)
}
pubk, err := btcec.ParsePubKey(keyb)
if err != nil {
return fmt.Errorf("error parsing key %s: %w", pub, err)
}
keys = append(keys, pubk)
}
agg, _, _, err := musig2.AggregateKeys(keys, true)
if err != nil {
return err
}
fmt.Println(hex.EncodeToString(agg.FinalKey.SerializeCompressed()))
return nil
},
}
func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string {
ch := make(chan string)
go func() {
for sec := range getStdinLinesOrFirstArgument(content) {
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
if sec == "" {
continue
}
@@ -138,7 +173,7 @@ func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) c
sec = data.(string)
}
if !nostr.IsValid32ByteHex(sec) {
lineProcessingError(c, "invalid hex secret key")
lineProcessingError(c, "invalid hex key")
continue
}
ch <- sec

331
musig2.go Normal file
View File

@@ -0,0 +1,331 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/nbd-wtf/go-nostr"
)
func performMusig(
ctx context.Context,
sec string,
evt *nostr.Event,
numSigners int,
keys []string,
nonces []string,
secNonce string,
partialSigs []string,
) (signed bool, err error) {
// preprocess data received
secb, err := hex.DecodeString(sec)
if err != nil {
return false, err
}
seck, pubk := btcec.PrivKeyFromBytes(secb)
knownSigners := make([]*btcec.PublicKey, 0, numSigners)
includesUs := false
for _, hexpub := range keys {
bpub, err := hex.DecodeString(hexpub)
if err != nil {
return false, err
}
spub, err := btcec.ParsePubKey(bpub)
if err != nil {
return false, err
}
knownSigners = append(knownSigners, spub)
if spub.IsEqual(pubk) {
includesUs = true
}
}
if !includesUs {
knownSigners = append(knownSigners, pubk)
}
knownNonces := make([][66]byte, 0, numSigners)
for _, hexnonce := range nonces {
bnonce, err := hex.DecodeString(hexnonce)
if err != nil {
return false, err
}
if len(bnonce) != 66 {
return false, fmt.Errorf("nonce is not 66 bytes: %s", hexnonce)
}
var b66nonce [66]byte
copy(b66nonce[:], bnonce)
knownNonces = append(knownNonces, b66nonce)
}
knownPartialSigs := make([]*musig2.PartialSignature, 0, numSigners)
for _, hexps := range partialSigs {
bps, err := hex.DecodeString(hexps)
if err != nil {
return false, err
}
var ps musig2.PartialSignature
if err := ps.Decode(bytes.NewBuffer(bps)); err != nil {
return false, fmt.Errorf("invalid partial signature %s: %w", hexps, err)
}
knownPartialSigs = append(knownPartialSigs, &ps)
}
// create the context
var mctx *musig2.Context
if len(knownSigners) < numSigners {
// we don't know all the signers yet
mctx, err = musig2.NewContext(seck, true,
musig2.WithNumSigners(numSigners),
musig2.WithEarlyNonceGen(),
)
if err != nil {
return false, fmt.Errorf("failed to create signing context with %d unknown signers: %w",
numSigners, err)
}
} else {
// we know all the signers
mctx, err = musig2.NewContext(seck, true,
musig2.WithKnownSigners(knownSigners),
)
if err != nil {
return false, fmt.Errorf("failed to create signing context with %d known signers: %w",
len(knownSigners), err)
}
}
// nonce generation phase -- for sharing
if len(knownSigners) < numSigners {
// if we don't have all the signers we just generate a nonce and yield it to the next people
nonce, err := mctx.EarlySessionNonce()
if err != nil {
return false, err
}
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
knownNonces = append(knownNonces, nonce.PubNonce)
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
return false, nil
}
// if we got here we have all the pubkeys, so we can print the combined key
if comb, err := mctx.CombinedKey(); err != nil {
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
} else {
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
evt.ID = evt.GetID()
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
}
// we have all the signers, which means we must also have all the nonces
var session *musig2.Session
if len(keys) == numSigners-1 {
// if we were the last to include our key, that means we have to include our nonce here to
// i.e. we didn't input our own pub nonce in the parameters
session, err = mctx.NewSession()
if err != nil {
return false, fmt.Errorf("failed to create session as the last peer to include our key: %w", err)
}
knownNonces = append(knownNonces, session.PublicNonce())
} else {
// otherwise we have included our own nonce in the parameters (from copypasting) but must
// also include the secret nonce that wasn't shared with peers
if secNonce == "" {
return false, fmt.Errorf("missing --musig-nonce-secret value")
}
secNonceB, err := base64.StdEncoding.DecodeString(secNonce)
if err != nil {
return false, fmt.Errorf("invalid --musig-nonce-secret: %w", err)
}
var secNonce97 [97]byte
copy(secNonce97[:], secNonceB)
session, err = mctx.NewSession(musig2.WithPreGeneratedNonce(&musig2.Nonces{
SecNonce: secNonce97,
PubNonce: secNonceToPubNonce(secNonce97),
}))
if err != nil {
return false, fmt.Errorf("failed to create signing session with secret nonce: %w", err)
}
}
var noncesOk bool
for _, b66nonce := range knownNonces {
if b66nonce == session.PublicNonce() {
// don't add our own nonce
continue
}
noncesOk, err = session.RegisterPubNonce(b66nonce)
if err != nil {
return false, fmt.Errorf("failed to register nonce: %w", err)
}
}
if !noncesOk {
return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing, this shouldn't happen")
}
// signing phase
// we always have to sign, so let's do this
id := evt.GetID()
hash, _ := hex.DecodeString(id)
var msg32 [32]byte
copy(msg32[:], hash)
partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle
if err != nil {
return false, fmt.Errorf("failed to produce partial signature: %w", err)
}
if len(knownPartialSigs)+1 < len(knownSigners) {
// still missing some signatures
knownPartialSigs = append(knownPartialSigs, partialSig) // we include ours here just so it's printed
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, knownPartialSigs, true)
return false, nil
} else {
// we have all signatures
for _, ps := range knownPartialSigs {
_, err = session.CombineSig(ps)
if err != nil {
return false, fmt.Errorf("failed to combine partial signature: %w", err)
}
}
}
// we have the signature
evt.Sig = hex.EncodeToString(session.FinalSig().Serialize())
return true, nil
}
func printPublicCommandForNextPeer(
evt *nostr.Event,
numSigners int,
knownSigners []*btcec.PublicKey,
knownNonces [][66]byte,
knownPartialSigs []*musig2.PartialSignature,
includeNonceSecret bool,
) {
maybeNonceSecret := ""
if includeNonceSecret {
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
}
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
numSigners,
eventToCliArgs(evt),
signersToCliArgs(knownSigners),
noncesToCliArgs(knownNonces),
partialSigsToCliArgs(knownPartialSigs),
maybeNonceSecret,
)
}
func eventToCliArgs(evt *nostr.Event) string {
b := strings.Builder{}
b.Grow(100)
b.WriteString("-k ")
b.WriteString(strconv.Itoa(evt.Kind))
b.WriteString(" -ts ")
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
b.WriteString(" -c '")
b.WriteString(evt.Content)
b.WriteString("'")
for _, tag := range evt.Tags {
b.WriteString(" -t '")
b.WriteString(tag.Key())
if len(tag) > 1 {
b.WriteString("=")
b.WriteString(tag[1])
if len(tag) > 2 {
for _, item := range tag[2:] {
b.WriteString(",")
b.WriteString(item)
}
}
}
b.WriteString("'")
}
return b.String()
}
func signersToCliArgs(knownSigners []*btcec.PublicKey) string {
b := strings.Builder{}
b.Grow(len(knownSigners) * (16 + 66))
for _, signerPub := range knownSigners {
b.WriteString(" --musig-pubkey ")
b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed()))
}
return b.String()
}
func noncesToCliArgs(knownNonces [][66]byte) string {
b := strings.Builder{}
b.Grow(len(knownNonces) * (15 + 132))
for _, nonce := range knownNonces {
b.WriteString(" --musig-nonce ")
b.WriteString(hex.EncodeToString(nonce[:]))
}
return b.String()
}
func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string {
b := strings.Builder{}
b.Grow(len(knownPartialSigs) * (17 + 64))
for _, partialSig := range knownPartialSigs {
b.WriteString(" --musig-partial ")
w := &bytes.Buffer{}
partialSig.Encode(w)
b.Write([]byte(hex.EncodeToString(w.Bytes())))
}
return b.String()
}
// this function is copied from btcec because it's not exported for some reason
func secNonceToPubNonce(secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte {
var k1Mod, k2Mod btcec.ModNScalar
k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen])
k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:])
var r1, r2 btcec.JacobianPoint
btcec.ScalarBaseMultNonConst(&k1Mod, &r1)
btcec.ScalarBaseMultNonConst(&k2Mod, &r2)
// Next, we'll convert the key in jacobian format to a normal public
// key expressed in affine coordinates.
r1.ToAffine()
r2.ToAffine()
r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y)
r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y)
var pubNonce [musig2.PubNonceSize]byte
// The public nonces are serialized as: R1 || R2, where both keys are
// serialized in compressed format.
copy(pubNonce[:], r1Pub.SerializeCompressed())
copy(
pubNonce[btcec.PubKeyBytesLenCompressed:],
r2Pub.SerializeCompressed(),
)
return pubNonce
}

View File

@@ -16,22 +16,24 @@ var relay = &cli.Command{
nak relay nostr.wine`,
ArgsUsage: "<relay-url>",
Action: func(c *cli.Context) error {
url := c.Args().First()
if url == "" {
return fmt.Errorf("specify the <relay-url>")
}
for url := range getStdinLinesOrArguments(c.Args()) {
if url == "" {
return fmt.Errorf("specify the <relay-url>")
}
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
url = "wss://" + url
}
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
url = "wss://" + url
}
info, err := nip11.Fetch(c.Context, url)
if err != nil {
return fmt.Errorf("failed to fetch '%s' information document: %w", url, err)
}
info, err := nip11.Fetch(c.Context, url)
if err != nil {
lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err)
continue
}
pretty, _ := json.MarshalIndent(info, "", " ")
stdout(string(pretty))
pretty, _ := json.MarshalIndent(info, "", " ")
stdout(string(pretty))
}
return nil
},
}

2
req.go
View File

@@ -246,6 +246,8 @@ example:
}
if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit
} else if c.IsSet("limit") || c.Bool("stream") {
filter.LimitZero = true
}
if len(relayUrls) > 0 {

View File

@@ -15,7 +15,7 @@ var verify = &cli.Command{
it outputs nothing if the verification is successful.`,
Action: func(c *cli.Context) error {
for stdinEvent := range getStdinLinesOrFirstArgument(c.Args().First()) {
for stdinEvent := range getStdinLinesOrArguments(c.Args()) {
evt := nostr.Event{}
if stdinEvent != "" {
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {