Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions cashu/nuts/nut11/nut11.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"reflect"
"slices"
"strconv"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
Expand Down Expand Up @@ -345,3 +346,59 @@ func ParseSignature(signature string) (*schnorr.Signature, error) {

return sig, nil
}

func VerifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness P2PKWitness
json.Unmarshal([]byte(proof.Witness), &p2pkWitness)

p2pkTags, err := ParseP2PKTags(proofSecret.Data.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 len(p2pkWitness.Signatures) < 1 {
return InvalidWitness
}
if !HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) {
return NotEnoughSignaturesErr
}
}
} else {
pubkey, err := ParsePublicKey(proofSecret.Data.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 EmptyPubkeysErr
}
keys = append(keys, p2pkTags.Pubkeys...)
}

if len(p2pkWitness.Signatures) < 1 {
return InvalidWitness
}

if DuplicateSignatures(p2pkWitness.Signatures) {
return DuplicateSignaturesErr
}

if !HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) {
return NotEnoughSignaturesErr
}
}
return nil
}
62 changes: 62 additions & 0 deletions cashu/nuts/nut14/nut14.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"slices"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
Expand Down Expand Up @@ -116,3 +117,64 @@ func AddWitnessHTLCToOutputs(

return outputs, nil
}

func VerifyHTLCProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var htlcWitness HTLCWitness
json.Unmarshal([]byte(proof.Witness), &htlcWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
return err
}

// 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 len(htlcWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, 1, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

// verify valid preimage
preimageBytes, err := hex.DecodeString(htlcWitness.Preimage)
if err != nil {
return InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(proofSecret.Data.Data) != 64 {
return InvalidHashErr
}
if hash != proofSecret.Data.Data {
return InvalidPreimageErr
}

// if n_sigs flag present, verify signatures
if p2pkTags.NSigs > 0 {
if len(htlcWitness.Signatures) < 1 {
return nut11.NoSignaturesErr
}

hash := sha256.Sum256([]byte(proof.Secret))

if nut11.DuplicateSignatures(htlcWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, p2pkTags.NSigs, p2pkTags.Pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}

return nil
}
121 changes: 2 additions & 119 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1152,12 +1152,12 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
nut10Secret, err := nut10.DeserializeSecret(proof.Secret)
if err == nil {
if nut10Secret.Kind == nut10.P2PK {
if err := verifyP2PKLockedProof(proof, nut10Secret); err != nil {
if err := nut11.VerifyP2PKLockedProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified P2PK locked proof")
} else if nut10Secret.Kind == nut10.HTLC {
if err := verifyHTLCProof(proof, nut10Secret); err != nil {
if err := nut14.VerifyHTLCProof(proof, nut10Secret); err != nil {
return err
}
m.logDebugf("verified HTLC proof")
Expand All @@ -1182,123 +1182,6 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
return nil
}

func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var p2pkWitness nut11.P2PKWitness
json.Unmarshal([]byte(proof.Witness), &p2pkWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.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 len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
} else {
pubkey, err := nut11.ParsePublicKey(proofSecret.Data.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 len(p2pkWitness.Signatures) < 1 {
return nut11.InvalidWitness
}

if nut11.DuplicateSignatures(p2pkWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

func verifyHTLCProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) error {
var htlcWitness nut14.HTLCWitness
json.Unmarshal([]byte(proof.Witness), &htlcWitness)

p2pkTags, err := nut11.ParseP2PKTags(proofSecret.Data.Tags)
if err != nil {
return err
}

// 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 len(htlcWitness.Signatures) < 1 {
return nut11.InvalidWitness
}
if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, 1, p2pkTags.Refund) {
return nut11.NotEnoughSignaturesErr
}
}
return nil
}

// verify valid preimage
preimageBytes, err := hex.DecodeString(htlcWitness.Preimage)
if err != nil {
return nut14.InvalidPreimageErr
}
hashBytes := sha256.Sum256(preimageBytes)
hash := hex.EncodeToString(hashBytes[:])

if len(proofSecret.Data.Data) != 64 {
return nut14.InvalidHashErr
}
if hash != proofSecret.Data.Data {
return nut14.InvalidPreimageErr
}

// if n_sigs flag present, verify signatures
if p2pkTags.NSigs > 0 {
if len(htlcWitness.Signatures) < 1 {
return nut11.NoSignaturesErr
}

hash := sha256.Sum256([]byte(proof.Secret))

if nut11.DuplicateSignatures(htlcWitness.Signatures) {
return nut11.DuplicateSignaturesErr
}

if !nut11.HasValidSignatures(hash[:], htlcWitness.Signatures, p2pkTags.NSigs, p2pkTags.Pubkeys) {
return nut11.NotEnoughSignaturesErr
}
}

return nil
}

// verifyBlindedMessages used to verify blinded messages are signed when SIG_ALL flag
// is present in either a P2PK or HTLC locked proofs
func verifyBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error {
Expand Down