mirror of
https://github.com/fiatjaf/nak.git
synced 2024-10-30 09:09:08 -04:00
nak key combine
and nak event --musig2
This commit is contained in:
parent
f198a46c19
commit
81968f6c0c
40
event.go
40
event.go
|
@ -53,6 +53,31 @@ example:
|
||||||
Usage: "private key to when communicating with the bunker given on --connect",
|
Usage: "private key to when communicating with the bunker given on --connect",
|
||||||
DefaultText: "a random key",
|
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: "musig2",
|
||||||
|
Usage: "number of signers to use for musig2",
|
||||||
|
Value: 1,
|
||||||
|
DefaultText: "1 -- i.e. do not use musig2 at all",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig2-pubkey",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "musig2-nonce-secret",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig2-nonce",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "musig2-partial",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
// ~~~
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "envelope",
|
Name: "envelope",
|
||||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||||
|
@ -226,6 +251,21 @@ example:
|
||||||
if err := bunker.SignEvent(c.Context, &evt); err != nil {
|
if err := bunker.SignEvent(c.Context, &evt); err != nil {
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
return fmt.Errorf("failed to sign with bunker: %w", err)
|
||||||
}
|
}
|
||||||
|
} else if numSigners := c.Uint("musig2"); numSigners > 1 && sec != "" {
|
||||||
|
pubkeys := c.StringSlice("musig2-pubkey")
|
||||||
|
secNonce := c.String("musig2-nonce-secret")
|
||||||
|
pubNonces := c.StringSlice("musig2-nonce")
|
||||||
|
partialSigs := c.StringSlice("musig2-partial")
|
||||||
|
signed, err := performMusig(c.Context,
|
||||||
|
sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("musig2 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 {
|
} else if err := evt.Sign(sec); err != nil {
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -5,17 +5,17 @@ go 1.21
|
||||||
toolchain go1.21.0
|
toolchain go1.21.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/nbd-wtf/go-nostr v0.30.0
|
github.com/nbd-wtf/go-nostr v0.30.2
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5
|
github.com/nbd-wtf/nostr-sdk v0.0.5
|
||||||
github.com/urfave/cli/v2 v2.25.7
|
github.com/urfave/cli/v2 v2.25.7
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -79,8 +79,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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/nbd-wtf/go-nostr v0.29.0 h1:kpAZ9oQPFeB9aJPloCsGS+UCNDPyN0jkt7sxlxxZock=
|
github.com/nbd-wtf/go-nostr v0.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI=
|
||||||
github.com/nbd-wtf/go-nostr v0.29.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
|
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 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
@ -92,6 +92,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.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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
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/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 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
@ -99,6 +100,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/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/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.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/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 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
@ -160,3 +163,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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/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.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=
|
||||||
|
|
43
key.go
43
key.go
|
@ -1,9 +1,12 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"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"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
|
@ -19,6 +22,7 @@ var key = &cli.Command{
|
||||||
public,
|
public,
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
|
combine,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +43,7 @@ var public = &cli.Command{
|
||||||
Description: ``,
|
Description: ``,
|
||||||
ArgsUsage: "[secret]",
|
ArgsUsage: "[secret]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
for sec := range getSecretKeyFromStdinLinesOrSlice(c, c.Args().Slice()) {
|
for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) {
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
pubkey, err := nostr.GetPublicKey(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to derive public key: %s", err)
|
lineProcessingError(c, "failed to derive public key: %s", err)
|
||||||
|
@ -78,7 +82,7 @@ var encrypt = &cli.Command{
|
||||||
if password == "" {
|
if password == "" {
|
||||||
return fmt.Errorf("no password given")
|
return fmt.Errorf("no password given")
|
||||||
}
|
}
|
||||||
for sec := range getSecretKeyFromStdinLinesOrSlice(c, []string{content}) {
|
for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) {
|
||||||
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
|
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lineProcessingError(c, "failed to encrypt: %s", err)
|
lineProcessingError(c, "failed to encrypt: %s", err)
|
||||||
|
@ -122,7 +126,38 @@ var decrypt = &cli.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSecretKeyFromStdinLinesOrSlice(c *cli.Context, keys []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.X().Bytes()))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string {
|
||||||
ch := make(chan string)
|
ch := make(chan string)
|
||||||
go func() {
|
go func() {
|
||||||
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
||||||
|
@ -138,7 +173,7 @@ func getSecretKeyFromStdinLinesOrSlice(c *cli.Context, keys []string) chan strin
|
||||||
sec = data.(string)
|
sec = data.(string)
|
||||||
}
|
}
|
||||||
if !nostr.IsValid32ByteHex(sec) {
|
if !nostr.IsValid32ByteHex(sec) {
|
||||||
lineProcessingError(c, "invalid hex secret key")
|
lineProcessingError(c, "invalid hex key")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ch <- sec
|
ch <- sec
|
||||||
|
|
324
musig2.go
Normal file
324
musig2.go
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
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 --musig2-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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} 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 --musig2-nonce-secret value")
|
||||||
|
}
|
||||||
|
secNonceB, err := base64.StdEncoding.DecodeString(secNonce)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid --musig2-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 {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
fmt.Println("signing over", hex.EncodeToString(msg32[:]))
|
||||||
|
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 = " --musig2-nonce-secret '<their-nonce-secret>'"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <their-key> --musig2 %d %s%s%s%s%s",
|
||||||
|
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) * (17 + 66))
|
||||||
|
|
||||||
|
for _, signerPub := range knownSigners {
|
||||||
|
b.WriteString(" --musig2-pubkey ")
|
||||||
|
b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func noncesToCliArgs(knownNonces [][66]byte) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(len(knownNonces) * (16 + 132))
|
||||||
|
|
||||||
|
for _, nonce := range knownNonces {
|
||||||
|
b.WriteString(" --musig2-nonce ")
|
||||||
|
b.WriteString(hex.EncodeToString(nonce[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string {
|
||||||
|
b := strings.Builder{}
|
||||||
|
b.Grow(len(knownPartialSigs) * (18 + 64))
|
||||||
|
|
||||||
|
for _, partialSig := range knownPartialSigs {
|
||||||
|
b.WriteString(" --musig2-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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user