From 618d0ba427507f2120b99072dcfe4d74d604e8b2 Mon Sep 17 00:00:00 2001 From: elnosh Date: Fri, 9 Aug 2024 15:05:04 -0500 Subject: [PATCH 1/2] mint - support NUT11 P2PK --- cashu/cashu.go | 44 ------- cashu/cashu_test.go | 38 ------ cashu/nuts/nut10/nut10.go | 42 ++++++- cashu/nuts/nut10/nut10_test.go | 40 +++++- cashu/nuts/nut11/nut11.go | 221 +++++++++++++++++++++++++++++++-- mint/mint.go | 175 +++++++++++++++++++++++++- wallet/wallet.go | 7 +- 7 files changed, 469 insertions(+), 98 deletions(-) diff --git a/cashu/cashu.go b/cashu/cashu.go index 87931ee..e656fd1 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -14,13 +14,6 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" ) -type SecretKind int - -const ( - Random SecretKind = iota - P2PK -) - // Cashu BlindedMessage. See https://github.com/cashubtc/nuts/blob/main/00.md#blindedmessage type BlindedMessage struct { Amount uint64 `json:"amount"` @@ -80,43 +73,6 @@ type Proof struct { Witness string `json:"witness,omitempty"` } -func (p Proof) IsSecretP2PK() bool { - return p.SecretType() == P2PK -} - -func (p Proof) SecretType() SecretKind { - var rawJsonSecret []json.RawMessage - // if not valid json, assume it is random secret - if err := json.Unmarshal([]byte(p.Secret), &rawJsonSecret); err != nil { - return Random - } - - // Well-known secret should have a length of at least 2 - if len(rawJsonSecret) < 2 { - return Random - } - - var kind string - if err := json.Unmarshal(rawJsonSecret[0], &kind); err != nil { - return Random - } - - if kind == "P2PK" { - return P2PK - } - - return Random -} - -func (kind SecretKind) String() string { - switch kind { - case P2PK: - return "P2PK" - default: - return "random" - } -} - type Proofs []Proof // Amount returns the total amount from diff --git a/cashu/cashu_test.go b/cashu/cashu_test.go index 902c329..03fb9ea 100644 --- a/cashu/cashu_test.go +++ b/cashu/cashu_test.go @@ -121,41 +121,3 @@ func TestTokenToString(t *testing.T) { } } } - -func TestSecretType(t *testing.T) { - tests := []struct { - proof Proof - expectedKind SecretKind - expectedIsP2PK bool - }{ - { - proof: Proof{Secret: `["P2PK", {"nonce":"da62796403af76c80cd6ce9153ed3746","data":"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e","tags":[["sigflag","SIG_ALL"]]}]`}, - expectedKind: P2PK, - expectedIsP2PK: true, - }, - - { - proof: Proof{Secret: `["DIFFERENT", {"nonce":"da62796403af76c80cd6ce9153ed3746","data":"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e","tags":[]}]`}, - expectedKind: Random, - expectedIsP2PK: false, - }, - - { - proof: Proof{Secret: `someranadomsecret`}, - expectedKind: Random, - expectedIsP2PK: false, - }, - } - - for _, test := range tests { - kind := test.proof.SecretType() - if kind != test.expectedKind { - t.Fatalf("expected '%v' but got '%v' instead", test.expectedKind.String(), kind.String()) - } - - isP2PK := test.proof.IsSecretP2PK() - if isP2PK != test.expectedIsP2PK { - t.Fatalf("expected '%v' but got '%v' instead", test.expectedIsP2PK, isP2PK) - } - } -} diff --git a/cashu/nuts/nut10/nut10.go b/cashu/nuts/nut10/nut10.go index 1e73b30..1cc9dde 100644 --- a/cashu/nuts/nut10/nut10.go +++ b/cashu/nuts/nut10/nut10.go @@ -8,6 +8,46 @@ import ( "github.com/elnosh/gonuts/cashu" ) +type SecretKind int + +const ( + AnyoneCanSpend SecretKind = iota + P2PK +) + +func SecretType(proof cashu.Proof) SecretKind { + var rawJsonSecret []json.RawMessage + // if not valid json, assume it is random secret + if err := json.Unmarshal([]byte(proof.Secret), &rawJsonSecret); err != nil { + return AnyoneCanSpend + } + + // Well-known secret should have a length of at least 2 + if len(rawJsonSecret) < 2 { + return AnyoneCanSpend + } + + var kind string + if err := json.Unmarshal(rawJsonSecret[0], &kind); err != nil { + return AnyoneCanSpend + } + + if kind == "P2PK" { + return P2PK + } + + return AnyoneCanSpend +} + +func (kind SecretKind) String() string { + switch kind { + case P2PK: + return "P2PK" + default: + return "anyonecanspend" + } +} + type WellKnownSecret struct { Nonce string `json:"nonce"` Data string `json:"data"` @@ -15,7 +55,7 @@ type WellKnownSecret struct { } // SerializeSecret returns the json string to be put in the secret field of a proof -func SerializeSecret(kind cashu.SecretKind, secretData WellKnownSecret) (string, error) { +func SerializeSecret(kind SecretKind, secretData WellKnownSecret) (string, error) { jsonSecret, err := json.Marshal(secretData) if err != nil { return "", err diff --git a/cashu/nuts/nut10/nut10_test.go b/cashu/nuts/nut10/nut10_test.go index 61e93e2..8078615 100644 --- a/cashu/nuts/nut10/nut10_test.go +++ b/cashu/nuts/nut10/nut10_test.go @@ -7,6 +7,44 @@ import ( "github.com/elnosh/gonuts/cashu" ) +func TestSecretType(t *testing.T) { + tests := []struct { + proof cashu.Proof + expectedKind SecretKind + expectedIsP2PK bool + }{ + { + proof: cashu.Proof{Secret: `["P2PK", {"nonce":"da62796403af76c80cd6ce9153ed3746","data":"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e","tags":[["sigflag","SIG_ALL"]]}]`}, + expectedKind: P2PK, + expectedIsP2PK: true, + }, + + { + proof: cashu.Proof{Secret: `["DIFFERENT", {"nonce":"da62796403af76c80cd6ce9153ed3746","data":"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e","tags":[]}]`}, + expectedKind: AnyoneCanSpend, + expectedIsP2PK: false, + }, + + { + proof: cashu.Proof{Secret: `someranadomsecret`}, + expectedKind: AnyoneCanSpend, + expectedIsP2PK: false, + }, + } + + for _, test := range tests { + kind := SecretType(test.proof) + if kind != test.expectedKind { + t.Fatalf("expected '%v' but got '%v' instead", test.expectedKind.String(), kind.String()) + } + + isP2PK := kind == P2PK + if isP2PK != test.expectedIsP2PK { + t.Fatalf("expected '%v' but got '%v' instead", test.expectedIsP2PK, isP2PK) + } + } +} + func TestSerializeSecret(t *testing.T) { secretData := WellKnownSecret{ Nonce: "da62796403af76c80cd6ce9153ed3746", @@ -16,7 +54,7 @@ func TestSerializeSecret(t *testing.T) { }, } - serialized, err := SerializeSecret(cashu.P2PK, secretData) + serialized, err := SerializeSecret(P2PK, secretData) if err != nil { t.Fatalf("got unexpected error: %v", err) } diff --git a/cashu/nuts/nut11/nut11.go b/cashu/nuts/nut11/nut11.go index 889ea87..e565faa 100644 --- a/cashu/nuts/nut11/nut11.go +++ b/cashu/nuts/nut11/nut11.go @@ -5,19 +5,67 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "fmt" "reflect" + "slices" + "strconv" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut10" ) +const ( + // supported tags + SIGFLAG = "sigflag" + NSIGS = "n_sigs" + PUBKEYS = "pubkeys" + LOCKTIME = "locktime" + REFUND = "refund" + + // SIGFLAG types + SIGINPUTS = "SIG_INPUTS" + SIGALL = "SIG_ALL" + + // Error code + NUT11ErrCode cashu.CashuErrCode = 30001 +) + +type SigFlag int + +const ( + SigInputs SigFlag = iota + SigAll + Unknown +) + +// errors +var ( + InvalidTagErr = cashu.Error{Detail: "invalid tag", Code: NUT11ErrCode} + TooManyTagsErr = cashu.Error{Detail: "too many tags", Code: NUT11ErrCode} + NSigsMustBePositiveErr = cashu.Error{Detail: "n_sigs must be a positive integer", Code: NUT11ErrCode} + EmptyPubkeysErr = cashu.Error{Detail: "pubkeys tag cannot be empty if n_sigs tag is present", Code: NUT11ErrCode} + EmptyWitnessErr = cashu.Error{Detail: "witness cannot be empty", Code: NUT11ErrCode} + NotEnoughSignaturesErr = cashu.Error{Detail: "not enough valid signatures provided", Code: NUT11ErrCode} + AllSigAllFlagsErr = cashu.Error{Detail: "all flags must be SIG_ALL", Code: NUT11ErrCode} + SigAllKeysMustBeEqualErr = cashu.Error{Detail: "all public keys must be the same for SIG_ALL", Code: NUT11ErrCode} + SigAllOnlySwap = cashu.Error{Detail: "SIG_ALL can only be used in /swap operation", Code: NUT11ErrCode} + NSigsMustBeEqualErr = cashu.Error{Detail: "all n_sigs must be the same for SIG_ALL", Code: NUT11ErrCode} +) + type P2PKWitness struct { Signatures []string `json:"signatures"` } +type P2PKTags struct { + Sigflag string + NSigs int + Pubkeys []*btcec.PublicKey + Locktime int64 + Refund []*btcec.PublicKey +} + // P2PKSecret returns a secret with a spending condition // that will lock ecash to a public key func P2PKSecret(pubkey string) (string, error) { @@ -34,7 +82,7 @@ func P2PKSecret(pubkey string) (string, error) { Data: pubkey, } - secret, err := nut10.SerializeSecret(cashu.P2PK, secretData) + secret, err := nut10.SerializeSecret(nut10.P2PK, secretData) if err != nil { return "", err } @@ -42,6 +90,76 @@ func P2PKSecret(pubkey string) (string, error) { return secret, nil } +func ParseP2PKTags(tags [][]string) (*P2PKTags, error) { + if len(tags) > 5 { + return nil, TooManyTagsErr + } + + p2pkTags := P2PKTags{} + + for _, tag := range tags { + if len(tag) < 2 { + return nil, InvalidTagErr + } + tagType := tag[0] + switch tagType { + case SIGFLAG: + sigflagType := tag[1] + if sigflagType == SIGINPUTS || sigflagType == SIGALL { + p2pkTags.Sigflag = sigflagType + } else { + errmsg := fmt.Sprintf("invalig sigflag: %v", sigflagType) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + case NSIGS: + nstr := tag[1] + nsig, err := strconv.ParseInt(nstr, 10, 8) + if err != nil { + errmsg := fmt.Sprintf("invalig n_sigs value: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + if nsig < 0 { + return nil, NSigsMustBePositiveErr + } + p2pkTags.NSigs = int(nsig) + case PUBKEYS: + pubkeys := make([]*btcec.PublicKey, len(tag)-1) + j := 0 + for i := 1; i < len(tag); i++ { + pubkey, err := ParsePublicKey(tag[i]) + if err != nil { + return nil, err + } + pubkeys[j] = pubkey + j++ + } + p2pkTags.Pubkeys = pubkeys + case LOCKTIME: + locktimestr := tag[1] + locktime, err := strconv.ParseInt(locktimestr, 10, 64) + if err != nil { + errmsg := fmt.Sprintf("invalid locktime: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + p2pkTags.Locktime = locktime + case REFUND: + refundKeys := make([]*btcec.PublicKey, len(tag)-1) + j := 0 + for i := 1; i < len(tag); i++ { + pubkey, err := ParsePublicKey(tag[i]) + if err != nil { + return nil, err + } + refundKeys[j] = pubkey + j++ + } + p2pkTags.Refund = refundKeys + } + } + + return &p2pkTags, nil +} + func AddSignatureToInputs(inputs cashu.Proofs, signingKey *btcec.PrivateKey) (cashu.Proofs, error) { for i, proof := range inputs { hash := sha256.Sum256([]byte(proof.Secret)) @@ -98,10 +216,46 @@ func AddSignatureToOutputs( return outputs, nil } +// PublicKeys returns a list of public keys that can sign +// a P2PK locked proof +func PublicKeys(secret nut10.WellKnownSecret) ([]*btcec.PublicKey, error) { + p2pkTags, err := ParseP2PKTags(secret.Tags) + if err != nil { + return nil, err + } + + pubkey, err := ParsePublicKey(secret.Data) + if err != nil { + return nil, err + } + pubkeys := append([]*btcec.PublicKey{pubkey}, p2pkTags.Pubkeys...) + return pubkeys, nil +} + +func IsSecretP2PK(proof cashu.Proof) bool { + return nut10.SecretType(proof) == nut10.P2PK +} + +// ProofsSigAll returns true if at least one of the proofs +// in the list has a SIG_ALL flag +func ProofsSigAll(proofs cashu.Proofs) bool { + for _, proof := range proofs { + secret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + return false + } + + if IsSigAll(secret) { + return true + } + } + return false +} + func IsSigAll(secret nut10.WellKnownSecret) bool { for _, tag := range secret.Tags { if len(tag) == 2 { - if tag[0] == "sigflag" && tag[1] == "SIG_ALL" { + if tag[0] == SIGFLAG && tag[1] == SIGALL { return true } } @@ -111,12 +265,7 @@ func IsSigAll(secret nut10.WellKnownSecret) bool { } func CanSign(secret nut10.WellKnownSecret, key *btcec.PrivateKey) bool { - secretData, err := hex.DecodeString(secret.Data) - if err != nil { - return false - } - - publicKey, err := secp256k1.ParsePubKey(secretData) + publicKey, err := ParsePublicKey(secret.Data) if err != nil { return false } @@ -127,3 +276,57 @@ func CanSign(secret nut10.WellKnownSecret, key *btcec.PrivateKey) bool { return false } + +func HasValidSignatures(hash []byte, witness P2PKWitness, Nsigs int, pubkeys []*btcec.PublicKey) bool { + pubkeysCopy := make([]*btcec.PublicKey, len(pubkeys)) + copy(pubkeysCopy, pubkeys) + + validSignatures := 0 + for _, signature := range witness.Signatures { + sig, err := ParseSignature(signature) + if err != nil { + continue + } + + for i, pubkey := range pubkeysCopy { + if sig.Verify(hash, pubkey) { + validSignatures++ + if len(pubkeysCopy) > 1 { + pubkeysCopy = slices.Delete(pubkeysCopy, i, i+1) + } + break + } + } + } + + return validSignatures >= Nsigs +} + +func ParsePublicKey(key string) (*btcec.PublicKey, error) { + hexPubkey, err := hex.DecodeString(key) + if err != nil { + errmsg := fmt.Sprintf("invalid public key: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + pubkey, err := btcec.ParsePubKey(hexPubkey) + if err != nil { + errmsg := fmt.Sprintf("invalid public key: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + return pubkey, nil +} + +func ParseSignature(signature string) (*schnorr.Signature, error) { + hexSig, err := hex.DecodeString(signature) + if err != nil { + errmsg := fmt.Sprintf("invalid signature: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + sig, err := schnorr.ParseSignature(hexSig) + if err != nil { + errmsg := fmt.Sprintf("invalid signature: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + + return sig, nil +} diff --git a/mint/mint.go b/mint/mint.go index 3cf50e9..5a62fa3 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -1,13 +1,16 @@ package mint import ( + "crypto/sha256" "database/sql" "encoding/hex" + "encoding/json" "errors" "fmt" "log" "os" "path/filepath" + "reflect" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -18,6 +21,8 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" "github.com/elnosh/gonuts/cashu/nuts/nut06" + "github.com/elnosh/gonuts/cashu/nuts/nut10" + "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint/lightning" "github.com/elnosh/gonuts/mint/storage" @@ -388,6 +393,13 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) return nil, cashu.BlindedMessageAlreadySigned } + // if sig all, verify signatures in blinded messages + if nut11.ProofsSigAll(proofs) { + if err := verifyP2PKBlindedMessages(proofs, blindedMessages); err != nil { + return nil, err + } + } + // if verification complete, sign blinded messages blindedSignatures, err := m.signBlindedMessages(blindedMessages) if err != nil { @@ -512,6 +524,10 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage. return storage.MeltQuote{}, cashu.InsufficientProofsAmount } + if nut11.ProofsSigAll(proofs) { + return storage.MeltQuote{}, nut11.SigAllOnlySwap + } + // if proofs are valid, ask the lightning backend // to make the payment preimage, err := m.lightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount) @@ -574,6 +590,13 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error { } } + // if P2PK locked proof, verify valid witness + if nut11.IsSecretP2PK(proof) { + if err := verifyP2PKLockedProof(proof); err != nil { + return err + } + } + Cbytes, err := hex.DecodeString(proof.C) if err != nil { return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) @@ -591,6 +614,154 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error { return nil } +func verifyP2PKLockedProof(proof cashu.Proof) error { + p2pkWellKnownSecret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) + } + + var p2pkWitness nut11.P2PKWitness + err = json.Unmarshal([]byte(proof.Witness), &p2pkWitness) + if err != nil { + errmsg := fmt.Sprintf("invalid witness: %v", err) + return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode) + } + if len(p2pkWitness.Signatures) < 1 { + return nut11.EmptyWitnessErr + } + + p2pkTags, err := nut11.ParseP2PKTags(p2pkWellKnownSecret.Tags) + if err != nil { + return err + } + + signaturesRequired := 1 + // if locktime is expired and there is no refund pubkey, treat as anyone can spend + // if refund pubkey present, check signature + if p2pkTags.Locktime > 0 && time.Now().Local().Unix() > p2pkTags.Locktime { + if len(p2pkTags.Refund) == 0 { + return nil + } else { + hash := sha256.Sum256([]byte(proof.Secret)) + if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, p2pkTags.Refund) { + return nut11.NotEnoughSignaturesErr + } + } + } else { + pubkey, err := nut11.ParsePublicKey(p2pkWellKnownSecret.Data) + if err != nil { + return err + } + keys := []*btcec.PublicKey{pubkey} + // message to sign + hash := sha256.Sum256([]byte(proof.Secret)) + + if p2pkTags.NSigs > 0 { + signaturesRequired = p2pkTags.NSigs + if len(p2pkTags.Pubkeys) == 0 { + return nut11.EmptyPubkeysErr + } + keys = append(keys, p2pkTags.Pubkeys...) + } + + if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, keys) { + return nut11.NotEnoughSignaturesErr + } + } + return nil +} + +func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error { + isSigAll := false + for _, proof := range proofs { + secret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + continue + } + + if nut11.IsSigAll(secret) { + isSigAll = true + break + } + } + + if isSigAll { + secret, err := nut10.DeserializeSecret(proofs[0].Secret) + if err != nil { + return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) + } + pubkeys, err := nut11.PublicKeys(secret) + if err != nil { + return err + } + + signaturesRequired := 1 + p2pkTags, err := nut11.ParseP2PKTags(secret.Tags) + if err != nil { + return err + } + if p2pkTags.NSigs > 0 { + signaturesRequired = p2pkTags.NSigs + } + + for _, proof := range proofs { + secret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) + } + // all flags need to be SIG_ALL + if !nut11.IsSigAll(secret) { + return nut11.AllSigAllFlagsErr + } + + currentSignaturesRequired := 1 + p2pkTags, err := nut11.ParseP2PKTags(secret.Tags) + if err != nil { + return err + } + if p2pkTags.NSigs > 0 { + currentSignaturesRequired = p2pkTags.NSigs + } + + currentKeys, err := nut11.PublicKeys(secret) + if err != nil { + return err + } + + // list of valid keys should be the same + // across all proofs + if !reflect.DeepEqual(pubkeys, currentKeys) { + return nut11.SigAllKeysMustBeEqualErr + } + + // all n_sigs must be same + if signaturesRequired != currentSignaturesRequired { + return nut11.NSigsMustBeEqualErr + } + } + + for _, bm := range blindedMessages { + hash := sha256.Sum256([]byte(bm.B_)) + + var witness nut11.P2PKWitness + err := json.Unmarshal([]byte(bm.Witness), &witness) + if err != nil { + errmsg := fmt.Sprintf("invalid witness: %v", err) + return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode) + } + if len(witness.Signatures) < 1 { + return nut11.EmptyWitnessErr + } + + if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) { + return nut11.NotEnoughSignaturesErr + } + } + } + + return nil +} + // signBlindedMessages will sign the blindedMessages and // return the blindedSignatures func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu.BlindedSignatures, error) { @@ -694,8 +865,8 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) error { 7: map[string]bool{"supported": false}, 8: map[string]bool{"supported": false}, 9: map[string]bool{"supported": false}, - 10: map[string]bool{"supported": false}, - 11: map[string]bool{"supported": false}, + 10: map[string]bool{"supported": true}, + 11: map[string]bool{"supported": true}, 12: map[string]bool{"supported": false}, } diff --git a/wallet/wallet.go b/wallet/wallet.go index 2877316..798132b 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -510,7 +510,7 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) func (w *Wallet) swap(proofsToSwap cashu.Proofs, mintURL string) (cashu.Proofs, error) { var nut10secret nut10.WellKnownSecret // if P2PK, add signature to Witness in the proofs - if proofsToSwap[0].IsSecretP2PK() { + if nut11.IsSecretP2PK(proofsToSwap[0]) { var err error nut10secret, err = nut10.DeserializeSecret(proofsToSwap[0].Secret) if err != nil { @@ -559,7 +559,7 @@ func (w *Wallet) swap(proofsToSwap cashu.Proofs, mintURL string) (cashu.Proofs, } // if P2PK locked ecash has `SIG_ALL` flag, sign outputs - if proofsToSwap[0].IsSecretP2PK() && nut11.IsSigAll(nut10secret) { + if nut11.IsSecretP2PK(proofsToSwap[0]) && nut11.IsSigAll(nut10secret) { outputs, err = nut11.AddSignatureToOutputs(outputs, w.privateKey) if err != nil { return nil, fmt.Errorf("error signing outputs: %v", err) @@ -615,7 +615,8 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { fees := uint64(w.fees(proofsToSwap, mint)) // if proofs are P2PK locked, sign appropriately - if proofsToSwap[0].IsSecretP2PK() { + //if proofsToSwap[0].IsSecretP2PK() { + if nut11.IsSecretP2PK(proofsToSwap[0]) { nut10secret, err := nut10.DeserializeSecret(proofsToSwap[0].Secret) if err != nil { return nil, err From 3ade5a8351a825efc9750093752b17a73bb7cfeb Mon Sep 17 00:00:00 2001 From: elnosh Date: Mon, 12 Aug 2024 16:19:19 -0500 Subject: [PATCH 2/2] mint - tests for NUT11 support --- cashu/nuts/nut11/nut11.go | 34 +++++++- mint/mint.go | 124 +++++++++++++--------------- mint/mint_integration_test.go | 142 ++++++++++++++++++++++++++++++++ testutils/utils.go | 148 ++++++++++++++++++++++++++++++++++ wallet/wallet.go | 3 +- 5 files changed, 379 insertions(+), 72 deletions(-) diff --git a/cashu/nuts/nut11/nut11.go b/cashu/nuts/nut11/nut11.go index e565faa..6aa4d80 100644 --- a/cashu/nuts/nut11/nut11.go +++ b/cashu/nuts/nut11/nut11.go @@ -46,7 +46,7 @@ var ( TooManyTagsErr = cashu.Error{Detail: "too many tags", Code: NUT11ErrCode} NSigsMustBePositiveErr = cashu.Error{Detail: "n_sigs must be a positive integer", Code: NUT11ErrCode} EmptyPubkeysErr = cashu.Error{Detail: "pubkeys tag cannot be empty if n_sigs tag is present", Code: NUT11ErrCode} - EmptyWitnessErr = cashu.Error{Detail: "witness cannot be empty", Code: NUT11ErrCode} + InvalidWitness = cashu.Error{Detail: "invalid witness", Code: NUT11ErrCode} NotEnoughSignaturesErr = cashu.Error{Detail: "not enough valid signatures provided", Code: NUT11ErrCode} AllSigAllFlagsErr = cashu.Error{Detail: "all flags must be SIG_ALL", Code: NUT11ErrCode} SigAllKeysMustBeEqualErr = cashu.Error{Detail: "all public keys must be the same for SIG_ALL", Code: NUT11ErrCode} @@ -68,7 +68,7 @@ type P2PKTags struct { // P2PKSecret returns a secret with a spending condition // that will lock ecash to a public key -func P2PKSecret(pubkey string) (string, error) { +func P2PKSecret(pubkey string, p2pkTags P2PKTags) (string, error) { // generate random nonce nonceBytes := make([]byte, 32) _, err := rand.Read(nonceBytes) @@ -77,9 +77,39 @@ func P2PKSecret(pubkey string) (string, error) { } nonce := hex.EncodeToString(nonceBytes) + var tags [][]string + if len(p2pkTags.Sigflag) > 0 { + tags = append(tags, []string{SIGFLAG, p2pkTags.Sigflag}) + } + if p2pkTags.NSigs > 0 { + numStr := strconv.Itoa(p2pkTags.NSigs) + tags = append(tags, []string{NSIGS, numStr}) + } + if len(p2pkTags.Pubkeys) > 0 { + pubkeys := []string{PUBKEYS} + for _, pubkey := range p2pkTags.Pubkeys { + key := hex.EncodeToString(pubkey.SerializeCompressed()) + pubkeys = append(pubkeys, key) + } + tags = append(tags, pubkeys) + } + if p2pkTags.Locktime > 0 { + locktime := strconv.Itoa(int(p2pkTags.Locktime)) + tags = append(tags, []string{LOCKTIME, locktime}) + } + if len(p2pkTags.Refund) > 0 { + refundKeys := []string{REFUND} + for _, pubkey := range p2pkTags.Refund { + key := hex.EncodeToString(pubkey.SerializeCompressed()) + refundKeys = append(refundKeys, key) + } + tags = append(tags, refundKeys) + } + secretData := nut10.WellKnownSecret{ Nonce: nonce, Data: pubkey, + Tags: tags, } secret, err := nut10.SerializeSecret(nut10.P2PK, secretData) diff --git a/mint/mint.go b/mint/mint.go index 5a62fa3..8d4bd44 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -623,11 +623,7 @@ func verifyP2PKLockedProof(proof cashu.Proof) error { var p2pkWitness nut11.P2PKWitness err = json.Unmarshal([]byte(proof.Witness), &p2pkWitness) if err != nil { - errmsg := fmt.Sprintf("invalid witness: %v", err) - return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode) - } - if len(p2pkWitness.Signatures) < 1 { - return nut11.EmptyWitnessErr + p2pkWitness.Signatures = []string{} } p2pkTags, err := nut11.ParseP2PKTags(p2pkWellKnownSecret.Tags) @@ -643,6 +639,9 @@ func verifyP2PKLockedProof(proof cashu.Proof) error { return nil } else { hash := sha256.Sum256([]byte(proof.Secret)) + if len(p2pkWitness.Signatures) < 1 { + return nut11.InvalidWitness + } if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, p2pkTags.Refund) { return nut11.NotEnoughSignaturesErr } @@ -664,6 +663,9 @@ func verifyP2PKLockedProof(proof cashu.Proof) error { keys = append(keys, p2pkTags.Pubkeys...) } + if len(p2pkWitness.Signatures) < 1 { + return nut11.InvalidWitness + } if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, keys) { return nut11.NotEnoughSignaturesErr } @@ -672,90 +674,76 @@ func verifyP2PKLockedProof(proof cashu.Proof) error { } func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error { - isSigAll := false - for _, proof := range proofs { - secret, err := nut10.DeserializeSecret(proof.Secret) - if err != nil { - continue - } + secret, err := nut10.DeserializeSecret(proofs[0].Secret) + if err != nil { + return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) + } + pubkeys, err := nut11.PublicKeys(secret) + if err != nil { + return err + } - if nut11.IsSigAll(secret) { - isSigAll = true - break - } + signaturesRequired := 1 + p2pkTags, err := nut11.ParseP2PKTags(secret.Tags) + if err != nil { + return err + } + if p2pkTags.NSigs > 0 { + signaturesRequired = p2pkTags.NSigs } - if isSigAll { - secret, err := nut10.DeserializeSecret(proofs[0].Secret) + // Check that the conditions across all proofs are the same + for _, proof := range proofs { + secret, err := nut10.DeserializeSecret(proof.Secret) if err != nil { return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) } - pubkeys, err := nut11.PublicKeys(secret) - if err != nil { - return err + // all flags need to be SIG_ALL + if !nut11.IsSigAll(secret) { + return nut11.AllSigAllFlagsErr } - signaturesRequired := 1 + currentSignaturesRequired := 1 p2pkTags, err := nut11.ParseP2PKTags(secret.Tags) if err != nil { return err } if p2pkTags.NSigs > 0 { - signaturesRequired = p2pkTags.NSigs + currentSignaturesRequired = p2pkTags.NSigs } - for _, proof := range proofs { - secret, err := nut10.DeserializeSecret(proof.Secret) - if err != nil { - return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) - } - // all flags need to be SIG_ALL - if !nut11.IsSigAll(secret) { - return nut11.AllSigAllFlagsErr - } - - currentSignaturesRequired := 1 - p2pkTags, err := nut11.ParseP2PKTags(secret.Tags) - if err != nil { - return err - } - if p2pkTags.NSigs > 0 { - currentSignaturesRequired = p2pkTags.NSigs - } - - currentKeys, err := nut11.PublicKeys(secret) - if err != nil { - return err - } + currentKeys, err := nut11.PublicKeys(secret) + if err != nil { + return err + } - // list of valid keys should be the same - // across all proofs - if !reflect.DeepEqual(pubkeys, currentKeys) { - return nut11.SigAllKeysMustBeEqualErr - } + // list of valid keys should be the same + // across all proofs + if !reflect.DeepEqual(pubkeys, currentKeys) { + return nut11.SigAllKeysMustBeEqualErr + } - // all n_sigs must be same - if signaturesRequired != currentSignaturesRequired { - return nut11.NSigsMustBeEqualErr - } + // all n_sigs must be same + if signaturesRequired != currentSignaturesRequired { + return nut11.NSigsMustBeEqualErr } + } - for _, bm := range blindedMessages { - hash := sha256.Sum256([]byte(bm.B_)) + for _, bm := range blindedMessages { + B_bytes, err := hex.DecodeString(bm.B_) + if err != nil { + return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) + } + hash := sha256.Sum256(B_bytes) - var witness nut11.P2PKWitness - err := json.Unmarshal([]byte(bm.Witness), &witness) - if err != nil { - errmsg := fmt.Sprintf("invalid witness: %v", err) - return cashu.BuildCashuError(errmsg, nut11.NUT11ErrCode) - } - if len(witness.Signatures) < 1 { - return nut11.EmptyWitnessErr - } + var witness nut11.P2PKWitness + err = json.Unmarshal([]byte(bm.Witness), &witness) + if err != nil || len(witness.Signatures) < 1 { + return nut11.InvalidWitness + } - if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) { - return nut11.NotEnoughSignaturesErr - } + if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) { + return nut11.NotEnoughSignaturesErr } } diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index 9b7d4b6..c75a8b8 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -11,11 +11,14 @@ import ( "os" "path/filepath" "testing" + "time" + "github.com/btcsuite/btcd/btcec/v2" btcdocker "github.com/elnosh/btc-docker-test" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" + "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" "github.com/elnosh/gonuts/testutils" @@ -658,5 +661,144 @@ func TestMintLimits(t *testing.T) { if err != nil { t.Fatalf("got unexpected error requesting mint quote: %v", err) } +} + +func TestNUT11P2PK(t *testing.T) { + lock, _ := btcec.NewPrivateKey() + + p2pkMintPath := filepath.Join(".", "p2pkmint") + p2pkMint, err := testutils.CreateTestMint(lnd1, p2pkMintPath, dbMigrationPath, 0, mint.MintLimits{}) + if err != nil { + t.Fatal(err) + } + defer func() { + os.RemoveAll(p2pkMintPath) + }() + + keyset := p2pkMint.GetActiveKeyset() + + var mintAmount uint64 = 1500 + lockedProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), nut11.P2PKTags{}, p2pkMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + blindedMessages, _, _, _ := testutils.CreateBlindedMessages(mintAmount, keyset) + + // swap with proofs that do not have valid witness + _, err = p2pkMint.Swap(lockedProofs, blindedMessages) + if !errors.Is(err, nut11.InvalidWitness) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err) + } + + // invalid proofs signed with another key + anotherKey, _ := btcec.NewPrivateKey() + invalidProofs, _ := nut11.AddSignatureToInputs(lockedProofs, anotherKey) + _, err = p2pkMint.Swap(invalidProofs, blindedMessages) + if !errors.Is(err, nut11.NotEnoughSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err) + } + + // valid signed proofs + signedProofs, _ := nut11.AddSignatureToInputs(lockedProofs, lock) + _, err = p2pkMint.Swap(signedProofs, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + // test multisig + key1, _ := btcec.NewPrivateKey() + key2, _ := btcec.NewPrivateKey() + multisigKeys := []*btcec.PublicKey{key1.PubKey(), key2.PubKey()} + tags := nut11.P2PKTags{ + Sigflag: nut11.SIGALL, + NSigs: 2, + Pubkeys: multisigKeys, + } + multisigProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + + // proofs with only 1 signature but require 2 + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + notEnoughSigsProofs, _ := nut11.AddSignatureToInputs(multisigProofs, lock) + _, err = p2pkMint.Swap(notEnoughSigsProofs, blindedMessages) + if !errors.Is(err, nut11.NotEnoughSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err) + } + + signingKeys := []*btcec.PrivateKey{key1, key2} + // enough signatures but blinded messages not signed + signedProofs, _ = testutils.AddSignaturesToInputs(multisigProofs, signingKeys) + _, err = p2pkMint.Swap(signedProofs, blindedMessages) + if !errors.Is(err, nut11.InvalidWitness) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err) + } + + // inputs and outputs with valid signatures + signedBlindedMessages, _ := testutils.AddSignaturesToOutputs(blindedMessages, signingKeys) + _, err = p2pkMint.Swap(signedProofs, signedBlindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + // test with locktime + tags = nut11.P2PKTags{ + Locktime: time.Now().Add(time.Minute * 1).Unix(), + } + locktimeProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + // unsigned proofs + _, err = p2pkMint.Swap(locktimeProofs, blindedMessages) + if !errors.Is(err, nut11.InvalidWitness) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err) + } + + signedProofs, _ = nut11.AddSignatureToInputs(locktimeProofs, lock) + _, err = p2pkMint.Swap(signedProofs, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + tags = nut11.P2PKTags{ + Locktime: time.Now().Add(-(time.Minute * 10)).Unix(), + } + locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + // locktime expired so spendable without signature + _, err = p2pkMint.Swap(locktimeProofs, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + // test locktime expired but with refund keys + tags = nut11.P2PKTags{ + Locktime: time.Now().Add(-(time.Minute * 10)).Unix(), + Refund: []*btcec.PublicKey{key1.PubKey()}, + } + locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + // unsigned proofs should fail because there were refund pubkeys in the tags + _, err = p2pkMint.Swap(locktimeProofs, blindedMessages) + if err == nil { + t.Fatal("expected error but got 'nil' instead") + } + + // sign with refund pubkey + signedProofs, _ = testutils.AddSignaturesToInputs(locktimeProofs, []*btcec.PrivateKey{key1}) + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + _, err = p2pkMint.Swap(signedProofs, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } } diff --git a/testutils/utils.go b/testutils/utils.go index 5a6b931..2e1c442 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -3,18 +3,23 @@ package testutils import ( "context" "crypto/rand" + "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" "os" "path/filepath" "time" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/dcrec/secp256k1/v4" btcdocker "github.com/elnosh/btc-docker-test" "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" "github.com/elnosh/gonuts/mint/lightning" @@ -329,6 +334,47 @@ func CreateBlindedMessages(amount uint64, keyset crypto.MintKeyset) (cashu.Blind return blindedMessages, secrets, rs, nil } +func CreateP2PKLockedBlindedMessages( + amount uint64, + keyset crypto.MintKeyset, + publicKey *btcec.PublicKey, + tags nut11.P2PKTags, +) (cashu.BlindedMessages, []string, []*secp256k1.PrivateKey, error) { + splitAmounts := cashu.AmountSplit(amount) + splitLen := len(splitAmounts) + + blindedMessages := make(cashu.BlindedMessages, splitLen) + secrets := make([]string, splitLen) + rs := make([]*secp256k1.PrivateKey, splitLen) + + pubkey := hex.EncodeToString(publicKey.SerializeCompressed()) + + for i, amt := range splitAmounts { + // generate new private key r + r, err := secp256k1.GeneratePrivateKey() + if err != nil { + return nil, nil, nil, err + } + + secret, err := nut11.P2PKSecret(pubkey, tags) + if err != nil { + return nil, nil, nil, err + } + + B_, r, err := crypto.BlindMessage(secret, r) + if err != nil { + return nil, nil, nil, err + } + + blindedMessage := newBlindedMessage(keyset.Id, amt, B_) + blindedMessages[i] = blindedMessage + secrets[i] = secret + rs[i] = r + } + + return blindedMessages, secrets, rs, nil +} + func ConstructProofs(blindedSignatures cashu.BlindedSignatures, secrets []string, rs []*secp256k1.PrivateKey, keyset *crypto.MintKeyset) (cashu.Proofs, error) { @@ -400,6 +446,108 @@ func GetValidProofsForAmount(amount uint64, mint *mint.Mint, payer *btcdocker.Ln return proofs, nil } +func GetProofsWithLock( + amount uint64, + publicKey *btcec.PublicKey, + tags nut11.P2PKTags, + mint *mint.Mint, + payer *btcdocker.Lnd, +) (cashu.Proofs, error) { + mintQuoteResponse, err := mint.RequestMintQuote(BOLT11_METHOD, amount, SAT_UNIT) + if err != nil { + return nil, fmt.Errorf("error requesting mint quote: %v", err) + } + + keyset := mint.GetActiveKeyset() + + blindedMessages, secrets, rs, err := CreateP2PKLockedBlindedMessages(amount, keyset, publicKey, tags) + if err != nil { + return nil, fmt.Errorf("error creating blinded message: %v", err) + } + + ctx := context.Background() + //pay invoice + sendPaymentRequest := lnrpc.SendRequest{ + PaymentRequest: mintQuoteResponse.PaymentRequest, + } + response, _ := payer.Client.SendPaymentSync(ctx, &sendPaymentRequest) + if len(response.PaymentError) > 0 { + return nil, fmt.Errorf("error paying invoice: %v", response.PaymentError) + } + + blindedSignatures, err := mint.MintTokens(BOLT11_METHOD, mintQuoteResponse.Id, blindedMessages) + if err != nil { + return nil, fmt.Errorf("got unexpected error minting tokens: %v", err) + } + + proofs, err := ConstructProofs(blindedSignatures, secrets, rs, &keyset) + if err != nil { + return nil, fmt.Errorf("error constructing proofs: %v", err) + } + + return proofs, nil +} + +func AddSignaturesToInputs(inputs cashu.Proofs, signingKeys []*btcec.PrivateKey) (cashu.Proofs, error) { + for i, proof := range inputs { + hash := sha256.Sum256([]byte(proof.Secret)) + signatures := make([]string, len(signingKeys)) + + for j, key := range signingKeys { + signature, err := schnorr.Sign(key, hash[:]) + if err != nil { + return nil, err + } + sig := hex.EncodeToString(signature.Serialize()) + signatures[j] = sig + } + + p2pkWitness := nut11.P2PKWitness{Signatures: signatures} + witness, err := json.Marshal(p2pkWitness) + if err != nil { + return nil, err + } + + proof.Witness = string(witness) + inputs[i] = proof + } + + return inputs, nil +} + +func AddSignaturesToOutputs( + outputs cashu.BlindedMessages, + signingKeys []*btcec.PrivateKey, +) (cashu.BlindedMessages, error) { + for i, output := range outputs { + msgToSign, err := hex.DecodeString(output.B_) + if err != nil { + return nil, err + } + hash := sha256.Sum256(msgToSign) + signatures := make([]string, len(signingKeys)) + + for j, key := range signingKeys { + signature, err := schnorr.Sign(key, hash[:]) + if err != nil { + return nil, err + } + sig := hex.EncodeToString(signature.Serialize()) + signatures[j] = sig + } + + p2pkWitness := nut11.P2PKWitness{Signatures: signatures} + witness, err := json.Marshal(p2pkWitness) + if err != nil { + return nil, err + } + output.Witness = string(witness) + outputs[i] = output + } + + return outputs, nil +} + func Fees(proofs cashu.Proofs, mint string) (uint, error) { keysetResponse, err := wallet.GetAllKeysets(mint) if err != nil { diff --git a/wallet/wallet.go b/wallet/wallet.go index 798132b..fa87c05 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -615,7 +615,6 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { fees := uint64(w.fees(proofsToSwap, mint)) // if proofs are P2PK locked, sign appropriately - //if proofsToSwap[0].IsSecretP2PK() { if nut11.IsSecretP2PK(proofsToSwap[0]) { nut10secret, err := nut10.DeserializeSecret(proofsToSwap[0].Secret) if err != nil { @@ -1202,7 +1201,7 @@ func blindedMessagesFromLock(splitAmounts []uint64, keysetId string, lockPubkey return nil, nil, nil, err } - secret, err := nut11.P2PKSecret(pubkey) + secret, err := nut11.P2PKSecret(pubkey, nut11.P2PKTags{}) if err != nil { return nil, nil, nil, err }