diff --git a/cashu/nuts/nut10/nut10.go b/cashu/nuts/nut10/nut10.go index 7dcb5d8..b9528e9 100644 --- a/cashu/nuts/nut10/nut10.go +++ b/cashu/nuts/nut10/nut10.go @@ -2,10 +2,15 @@ package nut10 import ( "crypto/rand" + "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/elnosh/gonuts/cashu" ) type SecretKind int @@ -119,3 +124,27 @@ func NewSecretFromSpendingCondition(spendingCondition SpendingCondition) (string return secret, nil } +func SigAllMessageToSign(inputs cashu.Proofs, outputs cashu.BlindedMessages, isMelt bool, meltquote string) string { + message := "" + for _, proof := range inputs { + message = message + proof.Secret + } + for _, blindMessage := range outputs { + message = message + blindMessage.B_ + } + if isMelt { + message = message + meltquote + } + return message + +} +func SignMessage(message string, privateKey *btcec.PrivateKey) (*schnorr.Signature, error) { + hashMessage := sha256.Sum256([]byte(message)) + signature, err := schnorr.Sign(privateKey, hashMessage[:]) + if err != nil { + return nil, err + } + + return signature, nil + +} diff --git a/cashu/nuts/nut11/nut11.go b/cashu/nuts/nut11/nut11.go index 0b9ba03..f968940 100644 --- a/cashu/nuts/nut11/nut11.go +++ b/cashu/nuts/nut11/nut11.go @@ -18,11 +18,12 @@ import ( const ( // supported tags - SIGFLAG = "sigflag" - NSIGS = "n_sigs" - PUBKEYS = "pubkeys" - LOCKTIME = "locktime" - REFUND = "refund" + SIGFLAG = "sigflag" + NSIGS = "n_sigs" + PUBKEYS = "pubkeys" + LOCKTIME = "locktime" + REFUND = "refund" + NSIGREFUND = "n_sigs_refund" // SIGFLAG types SIGINPUTS = "SIG_INPUTS" @@ -54,11 +55,12 @@ type P2PKWitness struct { } type P2PKTags struct { - Sigflag string - NSigs int - Pubkeys []*btcec.PublicKey - Locktime int64 - Refund []*btcec.PublicKey + Sigflag string + NSigs int + Pubkeys []*btcec.PublicKey + Locktime int64 + Refund []*btcec.PublicKey + NSigsRefund int } func SerializeP2PKTags(p2pkTags P2PKTags) [][]string { @@ -95,7 +97,7 @@ func SerializeP2PKTags(p2pkTags P2PKTags) [][]string { } func ParseP2PKTags(tags [][]string) (*P2PKTags, error) { - if len(tags) > 5 { + if len(tags) > 6 { return nil, TooManyTagsErr } @@ -158,33 +160,51 @@ func ParseP2PKTags(tags [][]string) (*P2PKTags, error) { j++ } p2pkTags.Refund = refundKeys + case NSIGREFUND: + nstr := tag[1] + nsig, err := strconv.ParseInt(nstr, 10, 8) + if err != nil { + errmsg := fmt.Sprintf("invalig n_sigs_refund value: %v", err) + return nil, cashu.BuildCashuError(errmsg, NUT11ErrCode) + } + if nsig < 0 { + return nil, NSigsMustBePositiveErr + } + p2pkTags.NSigsRefund = int(nsig) } } 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)) - signature, err := schnorr.Sign(signingKey, hash[:]) - if err != nil { - return nil, err - } - signatureBytes := signature.Serialize() +func AddSignatureToInput(input cashu.Proof, signingKey *btcec.PrivateKey) (cashu.Proof, error) { + hash := sha256.Sum256([]byte(input.Secret)) + signature, err := schnorr.Sign(signingKey, hash[:]) + if err != nil { + return cashu.Proof{}, err + } + signatureBytes := signature.Serialize() - p2pkWitness := P2PKWitness{ - Signatures: []string{hex.EncodeToString(signatureBytes)}, - } + p2pkWitness := P2PKWitness{ + Signatures: []string{hex.EncodeToString(signatureBytes)}, + } - witness, err := json.Marshal(p2pkWitness) + witness, err := json.Marshal(p2pkWitness) + if err != nil { + return cashu.Proof{}, err + } + input.Witness = string(witness) + return input, nil +} + +func AddSignatureToInputs(inputs cashu.Proofs, signingKey *btcec.PrivateKey) (cashu.Proofs, error) { + for i, proof := range inputs { + signedProof, err := AddSignatureToInput(proof, signingKey) if err != nil { return nil, err } - proof.Witness = string(witness) - inputs[i] = proof + inputs[i] = signedProof } - return inputs, nil } @@ -402,3 +422,75 @@ func VerifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) } return nil } + +type SigflagValidation struct { + sigFlag string + signaturesRequired uint + pubkeys map[*btcec.PublicKey]bool +} + +func checkForSigAll(proofs cashu.Proofs) (SigflagValidation, error) { + sigFlagValidation := SigflagValidation{ + sigFlag: SIGINPUTS, + signaturesRequired: 0, + pubkeys: make(map[*btcec.PublicKey]bool), + } + for _, proof := range proofs { + wellknownSecret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + return sigFlagValidation, err + } + + p2pkTags, err := ParseP2PKTags(wellknownSecret.Data.Tags) + if err != nil { + return sigFlagValidation, err + } + + if p2pkTags != nil { + if p2pkTags.Sigflag == SIGALL { + sigFlagValidation.sigFlag = SIGALL + } + if sigFlagValidation.signaturesRequired < uint(p2pkTags.NSigs) { + sigFlagValidation.signaturesRequired = uint(p2pkTags.NSigs) + } + + for _, pubkey := range p2pkTags.Pubkeys { + sigFlagValidation.pubkeys[pubkey] = true + } + } + } + return sigFlagValidation, nil +} + +func verifySigAll(proofs cashu.Proofs) (SigflagValidation, error) { + sigFlagValidation := SigflagValidation{ + sigFlag: SIGINPUTS, + signaturesRequired: 0, + pubkeys: make(map[*btcec.PublicKey]bool), + } + for _, proof := range proofs { + wellknownSecret, err := nut10.DeserializeSecret(proof.Secret) + if err != nil { + return sigFlagValidation, err + } + + p2pkTags, err := ParseP2PKTags(wellknownSecret.Data.Tags) + if err != nil { + return sigFlagValidation, err + } + + if p2pkTags != nil { + if p2pkTags.Sigflag == SIGALL { + sigFlagValidation.sigFlag = SIGALL + } + if sigFlagValidation.signaturesRequired < uint(p2pkTags.NSigs) { + sigFlagValidation.signaturesRequired = uint(p2pkTags.NSigs) + } + + for _, pubkey := range p2pkTags.Pubkeys { + sigFlagValidation.pubkeys[pubkey] = true + } + } + } + return sigFlagValidation, nil +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 5753b4b..c1b5fae 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" "math" @@ -557,10 +558,7 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) if !nut11.CanSign(nut10Secret, w.privateKey) { return 0, fmt.Errorf("cannot sign locked proofs") } - proofsToSwap, err = nut11.AddSignatureToInputs(proofsToSwap, w.privateKey) - if err != nil { - return 0, fmt.Errorf("error signing inputs: %v", err) - } + } // if mint in token is already the default mint, do not swap to trusted @@ -594,12 +592,31 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) if err != nil { return 0, fmt.Errorf("could not create swap request: %v", err) } + nut10Secret, err := nut10.DeserializeSecret(proofsToSwap[0].Secret) + if err == nil && nut10Secret.Kind == nut10.P2PK && nut11.IsSigAll(nut10Secret) { + if nut11.IsSigAll(nut10Secret) { + msgToSign := nut10.SigAllMessageToSign(req.inputs, req.outputs, false, "") - //if P2PK locked ecash has `SIG_ALL` flag, sign outputs - if nut10Secret.Kind == nut10.P2PK && nut11.IsSigAll(nut10Secret) { - req.outputs, err = nut11.AddSignatureToOutputs(req.outputs, w.privateKey) - if err != nil { - return 0, fmt.Errorf("error signing outputs: %v", err) + sig, err := nut10.SignMessage(msgToSign, w.privateKey) + if err != nil { + return 0, fmt.Errorf("error signing sigall message: %v", err) + } + + sigAllWitness := nut11.P2PKWitness{ + Signatures: []string{hex.EncodeToString(sig.Serialize())}, + } + + witness, err := json.Marshal(sigAllWitness) + if err != nil { + return 0, err + } + // sign only the first input and keep the rest as unsigned + req.inputs[0].Witness = string(witness) + } else { + req.inputs, err = nut11.AddSignatureToInputs(req.inputs, w.privateKey) + if err != nil { + return 0, fmt.Errorf("error signing inputs: %v", err) + } } } @@ -754,9 +771,30 @@ func (w *Wallet) swapToTrusted(proofs cashu.Proofs, mint *walletMint) (uint64, e if err != nil { return 0, fmt.Errorf("could not create swap request: %v", err) } - req.outputs, err = nut11.AddSignatureToOutputs(req.outputs, w.privateKey) - if err != nil { - return 0, fmt.Errorf("error signing outputs: %v", err) + if nut11.IsSigAll(nut10Secret) { + msgToSign := nut10.SigAllMessageToSign(req.inputs, req.outputs, false, "") + + sig, err := nut10.SignMessage(msgToSign, w.privateKey) + if err != nil { + return 0, fmt.Errorf("error signing sigall message: %v", err) + } + + sigAllWitness := nut11.P2PKWitness{ + Signatures: []string{hex.EncodeToString(sig.Serialize())}, + } + + witness, err := json.Marshal(sigAllWitness) + if err != nil { + return 0, err + } + // sign only the first input and keep the rest as unsigned + req.inputs[0].Witness = string(witness) + + } else { + req.inputs, err = nut11.AddSignatureToInputs(req.inputs, w.privateKey) + if err != nil { + return 0, fmt.Errorf("error signing inputs: %v", err) + } } newProofs, err := swap(mint.mintURL, req)