From 1167ea0cad6f5b8b62ad223914f689eda5687ce2 Mon Sep 17 00:00:00 2001 From: elnosh Date: Sat, 26 Oct 2024 10:47:46 -0500 Subject: [PATCH] mint - support for NUT-14 HTLCs --- cashu/nuts/nut11/nut11.go | 33 +++- cashu/nuts/nut14/nut14.go | 13 +- mint/mint.go | 133 ++++++++++++++-- mint/mint_integration_test.go | 285 ++++++++++++++++++++++++++++++++-- testutils/utils.go | 156 ++++++++++++------- 5 files changed, 530 insertions(+), 90 deletions(-) diff --git a/cashu/nuts/nut11/nut11.go b/cashu/nuts/nut11/nut11.go index 4c3bde9..fe8d46a 100644 --- a/cashu/nuts/nut11/nut11.go +++ b/cashu/nuts/nut11/nut11.go @@ -39,14 +39,17 @@ const ( Unknown ) -// errors +// NUT-11 specific 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} InvalidWitness = cashu.Error{Detail: "invalid witness", Code: NUT11ErrCode} + InvalidKindErr = cashu.Error{Detail: "invalid kind in secret", Code: NUT11ErrCode} + DuplicateSignaturesErr = cashu.Error{Detail: "witness has duplicate signatures", Code: NUT11ErrCode} NotEnoughSignaturesErr = cashu.Error{Detail: "not enough valid signatures provided", Code: NUT11ErrCode} + NoSignaturesErr = cashu.Error{Detail: "no signatures provided in witness", 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} @@ -231,11 +234,15 @@ func PublicKeys(secret nut10.WellKnownSecret) ([]*btcec.PublicKey, error) { return nil, err } - pubkey, err := ParsePublicKey(secret.Data.Data) - if err != nil { - return nil, err + pubkeys := p2pkTags.Pubkeys + // if P2PK proof, add key from data field + if secret.Kind == nut10.P2PK { + pubkey, err := ParsePublicKey(secret.Data.Data) + if err != nil { + return nil, err + } + pubkeys = append(pubkeys, pubkey) } - pubkeys := append([]*btcec.PublicKey{pubkey}, p2pkTags.Pubkeys...) return pubkeys, nil } @@ -280,12 +287,24 @@ func CanSign(secret nut10.WellKnownSecret, key *btcec.PrivateKey) bool { return false } -func HasValidSignatures(hash []byte, witness P2PKWitness, Nsigs int, pubkeys []*btcec.PublicKey) bool { +func DuplicateSignatures(signatures []string) bool { + sigs := make(map[string]bool) + for _, sig := range signatures { + if sigs[sig] { + return true + } else { + sigs[sig] = true + } + } + return false +} + +func HasValidSignatures(hash []byte, signatures []string, Nsigs int, pubkeys []*btcec.PublicKey) bool { pubkeysCopy := make([]*btcec.PublicKey, len(pubkeys)) copy(pubkeysCopy, pubkeys) validSignatures := 0 - for _, signature := range witness.Signatures { + for _, signature := range signatures { sig, err := ParseSignature(signature) if err != nil { continue diff --git a/cashu/nuts/nut14/nut14.go b/cashu/nuts/nut14/nut14.go index 5957f54..f40730c 100644 --- a/cashu/nuts/nut14/nut14.go +++ b/cashu/nuts/nut14/nut14.go @@ -14,6 +14,16 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut11" ) +const ( + NUT14ErrCode cashu.CashuErrCode = 30004 +) + +// NUT-14 specific errors +var ( + InvalidPreimageErr = cashu.Error{Detail: "Invalid preimage for HTLC", Code: NUT14ErrCode} + InvalidHashErr = cashu.Error{Detail: "Invalid hash in secret", Code: NUT14ErrCode} +) + type HTLCWitness struct { Preimage string `json:"preimage"` Signatures []string `json:"signatures"` @@ -34,14 +44,13 @@ func AddWitnessHTLC( } signatureNeeded := false - publicKey := signingKey.PubKey().SerializeCompressed() - if tags.NSigs > 0 { // return error if it requires more than 1 signature if tags.NSigs > 1 { return nil, errors.New("unable to provide enough signatures") } + publicKey := signingKey.PubKey().SerializeCompressed() canSign := false // read pubkeys and check signingKey can sign for _, pk := range tags.Pubkeys { diff --git a/mint/mint.go b/mint/mint.go index a1f81f1..84e1b7e 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -29,6 +29,7 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut07" "github.com/elnosh/gonuts/cashu/nuts/nut10" "github.com/elnosh/gonuts/cashu/nuts/nut11" + "github.com/elnosh/gonuts/cashu/nuts/nut14" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint/lightning" "github.com/elnosh/gonuts/mint/storage" @@ -492,8 +493,8 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) // if sig all, verify signatures in blinded messages if nut11.ProofsSigAll(proofs) { - m.logDebugf("P2PK locked proofs have SIG_ALL flag. Verifying blinded messages") - if err := verifyP2PKBlindedMessages(proofs, blindedMessages); err != nil { + m.logDebugf("locked proofs have SIG_ALL flag. Verifying blinded messages") + if err := verifyBlindedMessages(proofs, blindedMessages); err != nil { return nil, err } } @@ -1059,6 +1060,14 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error { m.logDebugf("verified P2PK locked proof") } + // verify if HTLC + if err == nil && nut10Secret.Kind == nut10.HTLC { + if err := verifyHTLCProof(proof, nut10Secret); err != nil { + return err + } + m.logDebugf("verified HTLC proof") + } + Cbytes, err := hex.DecodeString(proof.C) if err != nil { errmsg := fmt.Sprintf("invalid C: %v", err) @@ -1077,13 +1086,72 @@ 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 - err := json.Unmarshal([]byte(proof.Witness), &p2pkWitness) +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 { - p2pkWitness.Signatures = []string{} + 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 { + //signaturesRequired := p2pkTags.NSigs + 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 +} + +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 @@ -1100,7 +1168,7 @@ func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) if len(p2pkWitness.Signatures) < 1 { return nut11.InvalidWitness } - if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, p2pkTags.Refund) { + if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, p2pkTags.Refund) { return nut11.NotEnoughSignaturesErr } } @@ -1124,18 +1192,27 @@ func verifyP2PKLockedProof(proof cashu.Proof, proofSecret nut10.WellKnownSecret) if len(p2pkWitness.Signatures) < 1 { return nut11.InvalidWitness } - if !nut11.HasValidSignatures(hash[:], p2pkWitness, signaturesRequired, keys) { + + if nut11.DuplicateSignatures(p2pkWitness.Signatures) { + return nut11.DuplicateSignaturesErr + } + + if !nut11.HasValidSignatures(hash[:], p2pkWitness.Signatures, signaturesRequired, keys) { return nut11.NotEnoughSignaturesErr } } return nil } -func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) error { +// 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 { secret, err := nut10.DeserializeSecret(proofs[0].Secret) if err != nil { return cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) } + + // pubkeys will hold list of public keys that can sign pubkeys, err := nut11.PublicKeys(secret) if err != nil { return err @@ -1194,13 +1271,40 @@ func verifyP2PKBlindedMessages(proofs cashu.Proofs, blindedMessages cashu.Blinde } hash := sha256.Sum256(B_bytes) - var witness nut11.P2PKWitness - err = json.Unmarshal([]byte(bm.Witness), &witness) - if err != nil || len(witness.Signatures) < 1 { - return nut11.InvalidWitness + var signatures []string + switch secret.Kind { + case nut10.P2PK: + var witness nut11.P2PKWitness + if err := json.Unmarshal([]byte(bm.Witness), &witness); err != nil { + return nut11.InvalidWitness + } + signatures = witness.Signatures + case nut10.HTLC: + var witness nut14.HTLCWitness + if err := json.Unmarshal([]byte(bm.Witness), &witness); err != nil { + return nut11.InvalidWitness + } + + // verify valid preimage + preimageBytes, err := hex.DecodeString(witness.Preimage) + if err != nil { + return nut14.InvalidPreimageErr + } + hashBytes := sha256.Sum256(preimageBytes) + hash := hex.EncodeToString(hashBytes[:]) + + if len(secret.Data.Data) != 64 { + return nut14.InvalidHashErr + } + if hash != secret.Data.Data { + return nut14.InvalidPreimageErr + } + signatures = witness.Signatures + default: + return nut11.InvalidKindErr } - if !nut11.HasValidSignatures(hash[:], witness, signaturesRequired, pubkeys) { + if !nut11.HasValidSignatures(hash[:], signatures, signaturesRequired, pubkeys) { return nut11.NotEnoughSignaturesErr } } @@ -1325,6 +1429,7 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) { 10: map[string]bool{"supported": true}, 11: map[string]bool{"supported": true}, 12: map[string]bool{"supported": true}, + 14: map[string]bool{"supported": true}, } info := nut06.MintInfo{ diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index d648942..e7ba08d 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -21,8 +21,10 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" "github.com/elnosh/gonuts/cashu/nuts/nut07" + "github.com/elnosh/gonuts/cashu/nuts/nut10" "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/cashu/nuts/nut12" + "github.com/elnosh/gonuts/cashu/nuts/nut14" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" "github.com/elnosh/gonuts/testutils" @@ -974,7 +976,12 @@ func TestNUT11P2PK(t *testing.T) { keyset := p2pkMint.GetActiveKeyset() var mintAmount uint64 = 1500 - lockedProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), nut11.P2PKTags{}, p2pkMint, lnd2) + hexPubkey := hex.EncodeToString(lock.PubKey().SerializeCompressed()) + p2pkSpendingCondition := nut10.SpendingCondition{ + Kind: nut10.P2PK, + Data: hexPubkey, + } + lockedProofs, err := testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1010,7 +1017,8 @@ func TestNUT11P2PK(t *testing.T) { NSigs: 2, Pubkeys: multisigKeys, } - multisigProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = nut11.SerializeP2PKTags(tags) + multisigProofs, err := testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1025,14 +1033,14 @@ func TestNUT11P2PK(t *testing.T) { signingKeys := []*btcec.PrivateKey{key1, key2} // enough signatures but blinded messages not signed - signedProofs, _ = testutils.AddSignaturesToInputs(multisigProofs, signingKeys) + signedProofs, _ = testutils.AddP2PKWitnessToInputs(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) + signedBlindedMessages, _ := testutils.AddP2PKWitnessToOutputs(blindedMessages, signingKeys) _, err = p2pkMint.Swap(signedProofs, signedBlindedMessages) if err != nil { t.Fatalf("unexpected error in swap: %v", err) @@ -1042,7 +1050,8 @@ func TestNUT11P2PK(t *testing.T) { tags = nut11.P2PKTags{ Locktime: time.Now().Add(time.Minute * 1).Unix(), } - locktimeProofs, err := testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = nut11.SerializeP2PKTags(tags) + locktimeProofs, err := testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1062,7 +1071,8 @@ func TestNUT11P2PK(t *testing.T) { tags = nut11.P2PKTags{ Locktime: time.Now().Add(-(time.Minute * 10)).Unix(), } - locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = nut11.SerializeP2PKTags(tags) + locktimeProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1079,7 +1089,8 @@ func TestNUT11P2PK(t *testing.T) { Locktime: time.Now().Add(-(time.Minute * 10)).Unix(), Refund: []*btcec.PublicKey{key1.PubKey()}, } - locktimeProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = nut11.SerializeP2PKTags(tags) + locktimeProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1090,7 +1101,7 @@ func TestNUT11P2PK(t *testing.T) { } // sign with refund pubkey - signedProofs, _ = testutils.AddSignaturesToInputs(locktimeProofs, []*btcec.PrivateKey{key1}) + signedProofs, _ = testutils.AddP2PKWitnessToInputs(locktimeProofs, []*btcec.PrivateKey{key1}) blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) _, err = p2pkMint.Swap(signedProofs, blindedMessages) if err != nil { @@ -1098,7 +1109,8 @@ func TestNUT11P2PK(t *testing.T) { } // get locked proofs for melting - lockedProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), nut11.P2PKTags{}, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = [][]string{} + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } @@ -1118,8 +1130,8 @@ func TestNUT11P2PK(t *testing.T) { t.Fatalf("expected error '%v' but got '%v' instead", nut11.InvalidWitness, err) } - signedProofs, _ = testutils.AddSignaturesToInputs(lockedProofs, []*btcec.PrivateKey{lock}) - _, err = p2pkMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, lockedProofs) + signedProofs, _ = testutils.AddP2PKWitnessToInputs(lockedProofs, []*btcec.PrivateKey{lock}) + _, err = p2pkMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, signedProofs) if err != nil { t.Fatalf("unexpected error melting: %v", err) } @@ -1128,11 +1140,12 @@ func TestNUT11P2PK(t *testing.T) { tags = nut11.P2PKTags{ Sigflag: nut11.SIGALL, } - lockedProofs, err = testutils.GetProofsWithLock(mintAmount, lock.PubKey(), tags, p2pkMint, lnd2) + p2pkSpendingCondition.Tags = nut11.SerializeP2PKTags(tags) + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, p2pkSpendingCondition, p2pkMint, lnd2) if err != nil { t.Fatalf("error getting locked proofs: %v", err) } - signedProofs, _ = testutils.AddSignaturesToInputs(lockedProofs, []*btcec.PrivateKey{lock}) + signedProofs, _ = testutils.AddP2PKWitnessToInputs(lockedProofs, []*btcec.PrivateKey{lock}) invoice = lnrpc.Invoice{Value: 500} addInvoiceResponse, err = lnd2.Client.AddInvoice(ctx, &invoice) @@ -1143,7 +1156,7 @@ func TestNUT11P2PK(t *testing.T) { if err != nil { t.Fatalf("got unexpected error in melt request: %v", err) } - _, err = p2pkMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, lockedProofs) + _, err = p2pkMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, signedProofs) if !errors.Is(err, nut11.SigAllOnlySwap) { t.Fatalf("expected error '%v' but got '%v' instead", nut11.SigAllOnlySwap, err) } @@ -1189,3 +1202,247 @@ func TestDLEQProofs(t *testing.T) { } } } + +func TestHTLC(t *testing.T) { + var mintAmount uint64 = 1500 + preimage := "111111" + preimageBytes, _ := hex.DecodeString(preimage) + hashBytes := sha256.Sum256(preimageBytes) + hash := hex.EncodeToString(hashBytes[:]) + htlcSpendingCondition := nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + } + lockedProofs, err := testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + keyset := testMint.GetActiveKeyset() + blindedMessages, _, _, _ := testutils.CreateBlindedMessages(mintAmount, keyset) + + // test with proofs that do not have valid witness + _, err = testMint.Swap(lockedProofs, blindedMessages) + if !errors.Is(err, nut14.InvalidPreimageErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut14.InvalidPreimageErr, err) + } + + // test with invalid preimage to hash + proofs, _ := testutils.AddHTLCWitnessToInputs(lockedProofs, "000000", nil) + _, err = testMint.Swap(proofs, blindedMessages) + if !errors.Is(err, nut14.InvalidPreimageErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut14.InvalidPreimageErr, err) + } + + // test with valid preimage + proofs, _ = testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, nil) + _, err = testMint.Swap(proofs, blindedMessages) + if err != nil { + t.Fatalf("got unexpected error swapping HTLC proofs: %v", err) + } + + // test 1-of-1 + signingKey, _ := btcec.NewPrivateKey() + tags := nut11.P2PKTags{ + //Sigflag: nut11.SIGALL, + NSigs: 1, + Pubkeys: []*btcec.PublicKey{signingKey.PubKey()}, + } + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: nut11.SerializeP2PKTags(tags), + } + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + + // test requiring signature but witness only has preimage + lockedProofs, _ = testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, nil) + _, err = testMint.Swap(lockedProofs, blindedMessages) + if !errors.Is(err, nut11.NoSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NoSignaturesErr, err) + } + + // test valid preimage but with invalid signature + anotherKey, _ := btcec.NewPrivateKey() + invalidProofs, _ := testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, anotherKey) + _, err = testMint.Swap(invalidProofs, blindedMessages) + if !errors.Is(err, nut11.NotEnoughSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err) + } + + // test with valid preimage and valid signatures + validProofs, _ := testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, signingKey) + _, err = testMint.Swap(validProofs, blindedMessages) + if err != nil { + t.Fatalf("got unexpected error swapping HTLC proofs: %v", err) + } + + // test multisig + multisigKeys := []*btcec.PublicKey{signingKey.PubKey(), anotherKey.PubKey()} + tags = nut11.P2PKTags{ + NSigs: 2, + Pubkeys: multisigKeys, + } + serializedTags := nut11.SerializeP2PKTags(tags) + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: serializedTags, + } + multisigHTLC, err := testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + + // test with valid preimage and 1 signature but require 2 + notEnoughSigsProofs, _ := testutils.AddHTLCWitnessToInputs(multisigHTLC, preimage, signingKey) + _, err = testMint.Swap(notEnoughSigsProofs, blindedMessages) + if !errors.Is(err, nut11.NotEnoughSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err) + } + + // test SIG_ALL flag + tags = nut11.P2PKTags{ + Sigflag: nut11.SIGALL, + NSigs: 1, + Pubkeys: []*btcec.PublicKey{signingKey.PubKey()}, + } + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: nut11.SerializeP2PKTags(tags), + } + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + + // test only inputs signed + proofs, _ = testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, signingKey) + blindedMessages, _ = testutils.AddHTLCWitnessToOutputs(blindedMessages, preimage, nil) + _, err = testMint.Swap(proofs, blindedMessages) + if !errors.Is(err, nut11.NotEnoughSignaturesErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.NotEnoughSignaturesErr, err) + } + + // add signatures to outputs for SIG_ALL + blindedMessages, err = testutils.AddHTLCWitnessToOutputs(blindedMessages, preimage, signingKey) + _, err = testMint.Swap(proofs, blindedMessages) + if err != nil { + t.Fatalf("got unexpected error swapping HTLC proofs: %v", err) + } + + // test with locktime + tags = nut11.P2PKTags{ + Locktime: time.Now().Add(-(time.Minute * 10)).Unix(), + } + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: nut11.SerializeP2PKTags(tags), + } + locktimeProofs, err := testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + // locktime expired so spendable without signature + _, err = testMint.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{signingKey.PubKey()}, + } + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: nut11.SerializeP2PKTags(tags), + } + locktimeProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, 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 = testMint.Swap(locktimeProofs, blindedMessages) + if err == nil { + t.Fatal("expected error but got 'nil' instead") + } + + // sign with refund pubkey + signedProofs, _ := testutils.AddHTLCWitnessToInputs(locktimeProofs, "", signingKey) + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + _, err = testMint.Swap(signedProofs, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + // get locked proofs for melting + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: [][]string{}, + } + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + + invoice := lnrpc.Invoice{Value: 500} + addInvoiceResponse, err := lnd2.Client.AddInvoice(ctx, &invoice) + if err != nil { + t.Fatalf("error creating invoice: %v", err) + } + meltQuote, err := testMint.RequestMeltQuote(testutils.BOLT11_METHOD, addInvoiceResponse.PaymentRequest, testutils.SAT_UNIT) + if err != nil { + t.Fatalf("got unexpected error in melt request: %v", err) + } + _, err = testMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, lockedProofs) + if !errors.Is(err, nut14.InvalidPreimageErr) { + t.Fatalf("expected error '%v' but got '%v' instead", nut14.InvalidPreimageErr, err) + } + + validProofs, _ = testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, nil) + _, err = testMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, validProofs) + if err != nil { + t.Fatalf("unexpected error melting: %v", err) + } + + // test melt with SIG_ALL fails + tags = nut11.P2PKTags{ + Sigflag: nut11.SIGALL, + } + htlcSpendingCondition = nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hash, + Tags: nut11.SerializeP2PKTags(tags), + } + lockedProofs, err = testutils.GetProofsWithSpendingCondition(mintAmount, htlcSpendingCondition, testMint, lnd2) + if err != nil { + t.Fatalf("error getting locked proofs: %v", err) + } + lockedProofs, _ = testutils.AddHTLCWitnessToInputs(lockedProofs, preimage, signingKey) + + invoice = lnrpc.Invoice{Value: 500} + addInvoiceResponse, err = lnd2.Client.AddInvoice(ctx, &invoice) + if err != nil { + t.Fatalf("error creating invoice: %v", err) + } + meltQuote, err = testMint.RequestMeltQuote(testutils.BOLT11_METHOD, addInvoiceResponse.PaymentRequest, testutils.SAT_UNIT) + if err != nil { + t.Fatalf("got unexpected error in melt request: %v", err) + } + _, err = testMint.MeltTokens(ctx, testutils.BOLT11_METHOD, meltQuote.Id, lockedProofs) + if !errors.Is(err, nut11.SigAllOnlySwap) { + t.Fatalf("expected error '%v' but got '%v' instead", nut11.SigAllOnlySwap, err) + } +} diff --git a/testutils/utils.go b/testutils/utils.go index 3e210cd..9ce49e8 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -22,6 +22,7 @@ import ( "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut10" "github.com/elnosh/gonuts/cashu/nuts/nut11" + "github.com/elnosh/gonuts/cashu/nuts/nut14" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" "github.com/elnosh/gonuts/mint/lightning" @@ -339,53 +340,6 @@ 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()) - - p2pkSpendingCondition := nut10.SpendingCondition{ - Kind: nut10.P2PK, - Data: pubkey, - Tags: nut11.SerializeP2PKTags(tags), - } - - for i, amt := range splitAmounts { - // generate new private key r - r, err := secp256k1.GeneratePrivateKey() - if err != nil { - return nil, nil, nil, err - } - - secret, err := nut10.NewSecretFromSpendingCondition(p2pkSpendingCondition) - 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) { @@ -482,10 +436,47 @@ func GetValidProofsForAmount(amount uint64, mint *mint.Mint, payer *btcdocker.Ln return proofs, nil } -func GetProofsWithLock( +func BlindedMessagesFromSpendingCondition( + splitAmounts []uint64, + keysetId string, + spendingCondition nut10.SpendingCondition, +) ( + cashu.BlindedMessages, + []string, + []*secp256k1.PrivateKey, + error, +) { + splitLen := len(splitAmounts) + blindedMessages := make(cashu.BlindedMessages, splitLen) + secrets := make([]string, splitLen) + rs := make([]*secp256k1.PrivateKey, splitLen) + for i, amt := range splitAmounts { + r, err := secp256k1.GeneratePrivateKey() + if err != nil { + return nil, nil, nil, err + } + + secret, err := nut10.NewSecretFromSpendingCondition(spendingCondition) + if err != nil { + return nil, nil, nil, err + } + + B_, r, err := crypto.BlindMessage(secret, r) + if err != nil { + return nil, nil, nil, err + } + + blindedMessages[i] = cashu.NewBlindedMessage(keysetId, amt, B_) + secrets[i] = secret + rs[i] = r + } + + return blindedMessages, secrets, rs, nil +} + +func GetProofsWithSpendingCondition( amount uint64, - publicKey *btcec.PublicKey, - tags nut11.P2PKTags, + spendingCondition nut10.SpendingCondition, mint *mint.Mint, payer *btcdocker.Lnd, ) (cashu.Proofs, error) { @@ -496,7 +487,8 @@ func GetProofsWithLock( keyset := mint.GetActiveKeyset() - blindedMessages, secrets, rs, err := CreateP2PKLockedBlindedMessages(amount, keyset, publicKey, tags) + split := cashu.AmountSplit(amount) + blindedMessages, secrets, rs, err := BlindedMessagesFromSpendingCondition(split, keyset.Id, spendingCondition) if err != nil { return nil, fmt.Errorf("error creating blinded message: %v", err) } @@ -524,7 +516,7 @@ func GetProofsWithLock( return proofs, nil } -func AddSignaturesToInputs(inputs cashu.Proofs, signingKeys []*btcec.PrivateKey) (cashu.Proofs, error) { +func AddP2PKWitnessToInputs(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)) @@ -551,7 +543,7 @@ func AddSignaturesToInputs(inputs cashu.Proofs, signingKeys []*btcec.PrivateKey) return inputs, nil } -func AddSignaturesToOutputs( +func AddP2PKWitnessToOutputs( outputs cashu.BlindedMessages, signingKeys []*btcec.PrivateKey, ) (cashu.BlindedMessages, error) { @@ -584,6 +576,64 @@ func AddSignaturesToOutputs( return outputs, nil } +// it will add signatures if signingKey is not nil +func AddHTLCWitnessToInputs(inputs cashu.Proofs, preimage string, signingKey *btcec.PrivateKey) (cashu.Proofs, error) { + for i, proof := range inputs { + htlcWitness := nut14.HTLCWitness{Preimage: preimage} + + if signingKey != nil { + hash := sha256.Sum256([]byte(proof.Secret)) + signature, err := schnorr.Sign(signingKey, hash[:]) + if err != nil { + return nil, err + } + sig := hex.EncodeToString(signature.Serialize()) + htlcWitness.Signatures = []string{sig} + } + + witness, err := json.Marshal(htlcWitness) + if err != nil { + return nil, err + } + + proof.Witness = string(witness) + inputs[i] = proof + } + + return inputs, nil +} + +// it will add signatures if signingKey is not nil +func AddHTLCWitnessToOutputs(outputs cashu.BlindedMessages, preimage string, signingKey *btcec.PrivateKey) (cashu.BlindedMessages, error) { + for i, output := range outputs { + htlcWitness := nut14.HTLCWitness{Preimage: preimage} + + if signingKey != nil { + msgToSign, err := hex.DecodeString(output.B_) + if err != nil { + return nil, err + } + hash := sha256.Sum256(msgToSign) + signature, err := schnorr.Sign(signingKey, hash[:]) + if err != nil { + return nil, err + } + sig := hex.EncodeToString(signature.Serialize()) + htlcWitness.Signatures = []string{sig} + } + + witness, err := json.Marshal(htlcWitness) + 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 {