diff --git a/cashu/nuts/nut07/nut07.go b/cashu/nuts/nut07/nut07.go index 7ad85d1..8c19086 100644 --- a/cashu/nuts/nut07/nut07.go +++ b/cashu/nuts/nut07/nut07.go @@ -61,8 +61,9 @@ type TempProofState struct { func (state *ProofState) MarshalJSON() ([]byte, error) { tempProof := TempProofState{ - Y: state.Y, - State: state.State.String(), + Y: state.Y, + State: state.State.String(), + Witness: state.Witness, } return json.Marshal(tempProof) } diff --git a/mint/mint.go b/mint/mint.go index e46b1bf..da4ae00 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -640,10 +640,11 @@ func (m *Mint) removePendingProofsForQuote(quoteId string) (cashu.Proofs, error) Ys[i] = dbproof.Y proof := cashu.Proof{ - Amount: dbproof.Amount, - Id: dbproof.Id, - Secret: dbproof.Secret, - C: dbproof.C, + Amount: dbproof.Amount, + Id: dbproof.Id, + Secret: dbproof.Secret, + C: dbproof.C, + Witness: dbproof.Witness, } proofs[i] = proof } @@ -930,19 +931,23 @@ func (m *Mint) ProofsStateCheck(Ys []string) ([]nut07.ProofState, error) { for i, y := range Ys { state := nut07.Unspent - YSpent := slices.ContainsFunc(usedProofs, func(proof storage.DBProof) bool { + YSpentIdx := slices.IndexFunc(usedProofs, func(proof storage.DBProof) bool { return proof.Y == y }) - YPending := slices.ContainsFunc(pendingProofs, func(proof storage.DBProof) bool { + YPendingIdx := slices.IndexFunc(pendingProofs, func(proof storage.DBProof) bool { return proof.Y == y }) - if YSpent { + + var witness string + if YSpentIdx >= 0 { state = nut07.Spent - } else if YPending { + witness = usedProofs[YSpentIdx].Witness + } else if YPendingIdx >= 0 { state = nut07.Pending + witness = pendingProofs[YPendingIdx].Witness } - proofStates[i] = nut07.ProofState{Y: y, State: state} + proofStates[i] = nut07.ProofState{Y: y, State: state, Witness: witness} } return proofStates, nil diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index 525988a..2b7b6f3 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -783,55 +783,97 @@ func TestPendingProofs(t *testing.T) { } func TestProofsStateCheck(t *testing.T) { - validProofs, err := testutils.GetValidProofsForAmount(5000, testMint, lnd2) + proofs, err := testutils.GetValidProofsForAmount(5000, testMint, lnd2) if err != nil { t.Fatalf("error generating valid proofs: %v", err) } - Ys := make([]string, len(validProofs)) - for i, proof := range validProofs { - Y, _ := crypto.HashToCurve([]byte(proof.Secret)) - Yhex := hex.EncodeToString(Y.SerializeCompressed()) - Ys[i] = Yhex + // proofs with P2PK witness + lock, _ := btcec.NewPrivateKey() + p2pkSpendingCondition := nut10.SpendingCondition{ + Kind: nut10.P2PK, + Data: hex.EncodeToString(lock.PubKey().SerializeCompressed()), } - - proofStates, err := testMint.ProofsStateCheck(Ys) + p2pkProofs, err := testutils.GetProofsWithSpendingCondition(2100, p2pkSpendingCondition, testMint, lnd2) if err != nil { - t.Fatalf("unexpected error checking proof states: %v", err) - } - - // proofs should be unspent here - for _, proofState := range proofStates { - if proofState.State != nut07.Unspent { - t.Fatalf("expected proof state '%s' but got '%s'", nut07.Unspent, proofState.State) - } + t.Fatalf("error getting locked proofs: %v", err) } + p2pkProofs, _ = testutils.AddP2PKWitnessToInputs(p2pkProofs, []*btcec.PrivateKey{lock}) - // spend proofs and check spent state in response from mint - proofsToSpend := cashu.Proofs{} - numProofs := len(validProofs) / 2 - Ys = make([]string, numProofs) - for i := 0; i < numProofs; i++ { - proofsToSpend = append(proofsToSpend, validProofs[i]) - Y, _ := crypto.HashToCurve([]byte(validProofs[i].Secret)) - Yhex := hex.EncodeToString(Y.SerializeCompressed()) - Ys[i] = Yhex + // proofs with HTLC witness + preimage := "111111" + preimageBytes, _ := hex.DecodeString(preimage) + hashBytes := sha256.Sum256(preimageBytes) + htlcSpendingCondition := nut10.SpendingCondition{ + Kind: nut10.HTLC, + Data: hex.EncodeToString(hashBytes[:]), } - - blindedMessages, _, _, _ := testutils.CreateBlindedMessages(proofsToSpend.Amount(), testMint.GetActiveKeyset()) - _, err = testMint.Swap(proofsToSpend, blindedMessages) + htlcProofs, err := testutils.GetProofsWithSpendingCondition(2100, htlcSpendingCondition, testMint, lnd2) if err != nil { - t.Fatalf("unexpected error in swap: %v", err) + t.Fatalf("error getting locked proofs: %v", err) } + htlcProofs, _ = testutils.AddHTLCWitnessToInputs(htlcProofs, preimage, nil) - proofStates, err = testMint.ProofsStateCheck(Ys) - if err != nil { - t.Fatalf("unexpected error checking proof states: %v", err) + tests := []struct { + proofs cashu.Proofs + }{ + {proofs}, + {p2pkProofs}, + {htlcProofs}, } - for _, proofState := range proofStates { - if proofState.State != nut07.Spent { - t.Fatalf("expected proof state '%s' but got '%s'", nut07.Spent, proofState.State) + for _, test := range tests { + Ys := make([]string, len(test.proofs)) + for i, proof := range test.proofs { + Y, _ := crypto.HashToCurve([]byte(proof.Secret)) + Yhex := hex.EncodeToString(Y.SerializeCompressed()) + Ys[i] = Yhex + } + + proofStates, err := testMint.ProofsStateCheck(Ys) + if err != nil { + t.Fatalf("unexpected error checking proof states: %v", err) + } + + // proofs should be unspent here + for _, proofState := range proofStates { + if proofState.State != nut07.Unspent { + t.Fatalf("expected proof state '%s' but got '%s'", nut07.Unspent, proofState.State) + } + } + + // spend proofs and check spent state in response from mint + proofsToSpend := cashu.Proofs{} + numProofs := len(test.proofs) / 2 + Ys = make([]string, numProofs) + for i := 0; i < numProofs; i++ { + proofsToSpend = append(proofsToSpend, test.proofs[i]) + Y, _ := crypto.HashToCurve([]byte(test.proofs[i].Secret)) + Yhex := hex.EncodeToString(Y.SerializeCompressed()) + Ys[i] = Yhex + } + + blindedMessages, _, _, _ := testutils.CreateBlindedMessages(proofsToSpend.Amount(), testMint.GetActiveKeyset()) + _, err = testMint.Swap(proofsToSpend, blindedMessages) + if err != nil { + t.Fatalf("unexpected error in swap: %v", err) + } + + proofStates, err = testMint.ProofsStateCheck(Ys) + if err != nil { + t.Fatalf("unexpected error checking proof states: %v", err) + } + + for i, proofState := range proofStates { + if proofState.State != nut07.Spent { + t.Fatalf("expected proof state '%s' but got '%s'", nut07.Spent, proofState.State) + } + + if len(proofsToSpend[i].Witness) > 0 { + if proofState.Witness != proofsToSpend[i].Witness { + t.Fatalf("expected state witness '%s' but got '%s'", proofsToSpend[i].Witness, proofState.Witness) + } + } } } } diff --git a/mint/storage/sqlite/migrations/000007_add_witness_proofs.down.sql b/mint/storage/sqlite/migrations/000007_add_witness_proofs.down.sql new file mode 100644 index 0000000..646b81e --- /dev/null +++ b/mint/storage/sqlite/migrations/000007_add_witness_proofs.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE proofs DROP COLUMN witness; +ALTER TABLE pending_proofs DROP COLUMN witness; diff --git a/mint/storage/sqlite/migrations/000007_add_witness_proofs.up.sql b/mint/storage/sqlite/migrations/000007_add_witness_proofs.up.sql new file mode 100644 index 0000000..a81af7a --- /dev/null +++ b/mint/storage/sqlite/migrations/000007_add_witness_proofs.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE proofs ADD COLUMN witness TEXT; +ALTER TABLE pending_proofs ADD COLUMN witness TEXT; \ No newline at end of file diff --git a/mint/storage/sqlite/sqlite.go b/mint/storage/sqlite/sqlite.go index d51b362..b93fcdd 100644 --- a/mint/storage/sqlite/sqlite.go +++ b/mint/storage/sqlite/sqlite.go @@ -145,7 +145,7 @@ func (sqlite *SQLiteDB) SaveProofs(proofs cashu.Proofs) error { return err } - stmt, err := tx.Prepare("INSERT INTO proofs (y, amount, keyset_id, secret, c) VALUES (?, ?, ?, ?, ?)") + stmt, err := tx.Prepare("INSERT INTO proofs (y, amount, keyset_id, secret, c, witness) VALUES (?, ?, ?, ?, ?, ?)") if err != nil { return err } @@ -158,7 +158,7 @@ func (sqlite *SQLiteDB) SaveProofs(proofs cashu.Proofs) error { } Yhex := hex.EncodeToString(Y.SerializeCompressed()) - if _, err := stmt.Exec(Yhex, proof.Amount, proof.Id, proof.Secret, proof.C); err != nil { + if _, err := stmt.Exec(Yhex, proof.Amount, proof.Id, proof.Secret, proof.C, proof.Witness); err != nil { tx.Rollback() return err } @@ -188,16 +188,22 @@ func (sqlite *SQLiteDB) GetProofsUsed(Ys []string) ([]storage.DBProof, error) { for rows.Next() { var proof storage.DBProof + var witness sql.NullString + err := rows.Scan( &proof.Y, &proof.Amount, &proof.Id, &proof.Secret, &proof.C, + &witness, ) if err != nil { return nil, err } + if witness.Valid { + proof.Witness = witness.String + } proofs = append(proofs, proof) } @@ -211,7 +217,7 @@ func (sqlite *SQLiteDB) AddPendingProofs(proofs cashu.Proofs, quoteId string) er return err } - stmt, err := tx.Prepare("INSERT INTO pending_proofs (y, amount, keyset_id, secret, c, melt_quote_id) VALUES (?, ?, ?, ?, ?, ?)") + stmt, err := tx.Prepare("INSERT INTO pending_proofs (y, amount, keyset_id, secret, c, witness, melt_quote_id) VALUES (?, ?, ?, ?, ?, ?, ?)") if err != nil { return err } @@ -224,7 +230,7 @@ func (sqlite *SQLiteDB) AddPendingProofs(proofs cashu.Proofs, quoteId string) er } Yhex := hex.EncodeToString(Y.SerializeCompressed()) - if _, err := stmt.Exec(Yhex, proof.Amount, proof.Id, proof.Secret, proof.C, quoteId); err != nil { + if _, err := stmt.Exec(Yhex, proof.Amount, proof.Id, proof.Secret, proof.C, proof.Witness, quoteId); err != nil { tx.Rollback() return err } @@ -254,6 +260,8 @@ func (sqlite *SQLiteDB) GetPendingProofs(Ys []string) ([]storage.DBProof, error) for rows.Next() { var proof storage.DBProof + var witness sql.NullString + err := rows.Scan( &proof.Y, &proof.Amount, @@ -261,11 +269,16 @@ func (sqlite *SQLiteDB) GetPendingProofs(Ys []string) ([]storage.DBProof, error) &proof.Secret, &proof.C, &proof.MeltQuoteId, + &witness, ) if err != nil { return nil, err } + if witness.Valid { + proof.Witness = witness.String + } + proofs = append(proofs, proof) } @@ -274,7 +287,7 @@ func (sqlite *SQLiteDB) GetPendingProofs(Ys []string) ([]storage.DBProof, error) func (sqlite *SQLiteDB) GetPendingProofsByQuote(quoteId string) ([]storage.DBProof, error) { proofs := []storage.DBProof{} - query := `SELECT y, amount, keyset_id, secret, c FROM pending_proofs WHERE melt_quote_id = ?` + query := `SELECT y, amount, keyset_id, secret, c, witness FROM pending_proofs WHERE melt_quote_id = ?` rows, err := sqlite.db.Query(query, quoteId) if err != nil { @@ -284,17 +297,24 @@ func (sqlite *SQLiteDB) GetPendingProofsByQuote(quoteId string) ([]storage.DBPro for rows.Next() { var proof storage.DBProof + var witness sql.NullString + err := rows.Scan( &proof.Y, &proof.Amount, &proof.Id, &proof.Secret, &proof.C, + &witness, ) if err != nil { return nil, err } + if witness.Valid { + proof.Witness = witness.String + } + proofs = append(proofs, proof) } diff --git a/mint/storage/storage.go b/mint/storage/storage.go index 2b28bda..7c32d93 100644 --- a/mint/storage/storage.go +++ b/mint/storage/storage.go @@ -51,11 +51,12 @@ type DBKeyset struct { } type DBProof struct { - Amount uint64 - Id string - Secret string - Y string - C string + Amount uint64 + Id string + Secret string + Y string + C string + Witness string // for proofs in pending table MeltQuoteId string }