From 9fb41430dc3e7d4fd93f8a8562e8491e9e010822 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 19 Feb 2025 13:00:08 -0500 Subject: [PATCH] mint: issued and redeemed ecash for db --- mint/mint.go | 35 +++++++- .../000010_keyset_balance_view.down.sql | 2 + .../000010_keyset_balance_view.up.sql | 19 ++++ mint/storage/sqlite/sqlite.go | 52 ++++++++--- mint/storage/sqlite/sqlite_test.go | 88 ++++++++++++++++++- mint/storage/storage.go | 6 +- 6 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 mint/storage/sqlite/migrations/000010_keyset_balance_view.down.sql create mode 100644 mint/storage/sqlite/migrations/000010_keyset_balance_view.up.sql diff --git a/mint/mint.go b/mint/mint.go index 61d532a..0ced693 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -294,7 +294,7 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques } } if m.limits.MaxBalance > 0 { - balance, err := m.db.GetBalance() + balance, err := m.TotalBalance() if err != nil { errmsg := fmt.Sprintf("could not get mint balance from db: %v", err) return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.DBErrCode) @@ -1499,6 +1499,37 @@ func (m *Mint) GetActiveKeyset() crypto.MintKeyset { return keyset } +func (m *Mint) EcashIssued() (map[string]uint64, error) { + return m.db.GetEcashIssued() +} + +func (m *Mint) EcashRedeemed() (map[string]uint64, error) { + return m.db.GetEcashRedeemed() +} + +func (m *Mint) TotalBalance() (uint64, error) { + ecashIssued, err := m.db.GetEcashIssued() + if err != nil { + return 0, err + } + var totalIssued uint64 + for _, issuedForKeyset := range ecashIssued { + totalIssued += issuedForKeyset + } + + ecashRedeemed, err := m.db.GetEcashRedeemed() + if err != nil { + return 0, err + } + + var totalRedeemed uint64 + for _, redeemedForKeyset := range ecashRedeemed { + totalRedeemed += redeemedForKeyset + } + + return totalIssued - totalRedeemed, nil +} + func (m *Mint) SetMintInfo(mintInfo MintInfo) { nuts := nut06.Nuts{ Nut04: nut06.NutSetting{ @@ -1582,7 +1613,7 @@ func (m Mint) RetrieveMintInfo() (nut06.MintInfo, error) { } mintingDisabled := false - mintBalance, err := m.db.GetBalance() + mintBalance, err := m.TotalBalance() if err != nil { errmsg := fmt.Sprintf("error getting mint balance: %v", err) return nut06.MintInfo{}, cashu.BuildCashuError(errmsg, cashu.DBErrCode) diff --git a/mint/storage/sqlite/migrations/000010_keyset_balance_view.down.sql b/mint/storage/sqlite/migrations/000010_keyset_balance_view.down.sql new file mode 100644 index 0000000..1c9e4db --- /dev/null +++ b/mint/storage/sqlite/migrations/000010_keyset_balance_view.down.sql @@ -0,0 +1,2 @@ +DROP VIEW total_issued; +DROP VIEW total_redeemed; diff --git a/mint/storage/sqlite/migrations/000010_keyset_balance_view.up.sql b/mint/storage/sqlite/migrations/000010_keyset_balance_view.up.sql new file mode 100644 index 0000000..2bbd945 --- /dev/null +++ b/mint/storage/sqlite/migrations/000010_keyset_balance_view.up.sql @@ -0,0 +1,19 @@ +-- drop previous balance views +DROP VIEW minted_ecash; +DROP VIEW melted_ecash; +DROP VIEW balance; + +-- create new balance views by keyset +CREATE VIEW IF NOT EXISTS total_issued AS +SELECT keyset_id, COALESCE(amount, 0) AS balance FROM ( + SELECT keyset_id, SUM(amount) AS amount + FROM blind_signatures + GROUP BY keyset_id +); + +CREATE VIEW IF NOT EXISTS total_redeemed AS +SELECT keyset_id, COALESCE(amount, 0) AS balance FROM ( + SELECT keyset_id, SUM(amount) AS amount + FROM proofs + GROUP BY keyset_id +); diff --git a/mint/storage/sqlite/sqlite.go b/mint/storage/sqlite/sqlite.go index 28283aa..4875b53 100644 --- a/mint/storage/sqlite/sqlite.go +++ b/mint/storage/sqlite/sqlite.go @@ -104,16 +104,6 @@ func (sqlite *SQLiteDB) Close() error { return sqlite.db.Close() } -func (sqlite *SQLiteDB) GetBalance() (uint64, error) { - var balance uint64 - row := sqlite.db.QueryRow("SELECT balance FROM balance") - err := row.Scan(&balance) - if err != nil { - return 0, err - } - return balance, nil -} - func (sqlite *SQLiteDB) SaveSeed(seed []byte) error { hexSeed := hex.EncodeToString(seed) @@ -725,3 +715,45 @@ func (sqlite *SQLiteDB) GetBlindSignatures(B_s []string) (cashu.BlindedSignature return signatures, nil } + +func (sqlite *SQLiteDB) GetEcashIssued() (map[string]uint64, error) { + ecashIssued := make(map[string]uint64) + + rows, err := sqlite.db.Query("SELECT * FROM total_issued") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var keysetId string + var amount uint64 + if err := rows.Scan(&keysetId, &amount); err != nil { + return nil, err + } + ecashIssued[keysetId] = amount + } + + return ecashIssued, nil +} + +func (sqlite *SQLiteDB) GetEcashRedeemed() (map[string]uint64, error) { + ecashRedeemed := make(map[string]uint64) + + rows, err := sqlite.db.Query("SELECT * FROM total_redeemed") + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var keysetId string + var amount uint64 + if err := rows.Scan(&keysetId, &amount); err != nil { + return nil, err + } + ecashRedeemed[keysetId] = amount + } + + return ecashRedeemed, nil +} diff --git a/mint/storage/sqlite/sqlite_test.go b/mint/storage/sqlite/sqlite_test.go index 51fde7f..a0ed422 100644 --- a/mint/storage/sqlite/sqlite_test.go +++ b/mint/storage/sqlite/sqlite_test.go @@ -366,6 +366,88 @@ func TestBlindSignatures(t *testing.T) { } +func TestBalanceViews(t *testing.T) { + dbpath := "./balanceviewsdb" + if err := os.MkdirAll(dbpath, 0750); err != nil { + t.Fatalf("could not create directory test db: %v", err) + } + + db, err := InitSQLite(dbpath) + if err != nil { + t.Fatalf("unexpected error creating sqlite db: %v", err) + } + defer os.RemoveAll(dbpath) + + count := 210 + B_s := generateRandomB_s(count) + blindSignatures := generateBlindSignatures(count) + if err := db.SaveBlindSignatures(B_s, blindSignatures); err != nil { + t.Fatalf("unexpected error saving blind signatures: %v", err) + } + keysetId := blindSignatures[0].Id + + // 2nd batch of blind signatures + B_s = generateRandomB_s(count) + blindSignatures2 := generateBlindSignatures(count) + if err := db.SaveBlindSignatures(B_s, blindSignatures2); err != nil { + t.Fatalf("unexpected error saving blind signatures: %v", err) + } + keysetId2 := blindSignatures[0].Id + + proofs := generateRandomProofs(21) + if err := db.SaveProofs(proofs); err != nil { + t.Fatalf("unexpected error saving proofs: %v", err) + } + proofKeysetId := proofs[0].Id + + // 2nd batch of proofs + proofs2 := generateRandomProofs(21) + if err := db.SaveProofs(proofs2); err != nil { + t.Fatalf("unexpected error saving proofs: %v", err) + } + proofKeysetId2 := proofs[0].Id + + totalIssued := blindSignatures.Amount() + blindSignatures2.Amount() + ecashIssued, err := db.GetEcashIssued() + if err != nil { + t.Fatalf("unexpected error getting issued ecash: %v", err) + } + if len(ecashIssued) != 2 { + t.Fatalf("expected map of length 2 but got '%v'", len(ecashIssued)) + } + // check map hash both keyset ids + if _, ok := ecashIssued[keysetId]; !ok { + t.Fatalf("expected ecash issued with keyset id '%v' but it was not present", keysetId) + } + if _, ok := ecashIssued[keysetId2]; !ok { + t.Fatalf("expected ecash issued with keyset id '%v' but it was not present", keysetId2) + } + issuedFromDB := ecashIssued[keysetId] + ecashIssued[keysetId2] + if totalIssued != issuedFromDB { + t.Fatalf("expected total issued of '%v' but got '%v'", totalIssued, issuedFromDB) + } + + totalRedeemed := proofs.Amount() + proofs2.Amount() + ecashRedeemed, err := db.GetEcashRedeemed() + if err != nil { + t.Fatalf("unexpected error getting redeemed ecash: %v", err) + } + if len(ecashRedeemed) != 2 { + t.Fatalf("expected map of length 2 but got '%v'", len(ecashRedeemed)) + } + // check map hash both keyset ids + if _, ok := ecashRedeemed[proofKeysetId]; !ok { + t.Fatalf("expected ecash redeemed with keyset id '%v' but it was not present", proofKeysetId) + } + if _, ok := ecashRedeemed[proofKeysetId2]; !ok { + t.Fatalf("expected ecash redeemed with keyset id '%v' but it was not present", proofKeysetId2) + } + redeemedFromDB := ecashRedeemed[proofKeysetId] + ecashRedeemed[proofKeysetId2] + if totalRedeemed != redeemedFromDB { + t.Fatalf("expected total redeemed of '%v' but got '%v'", totalRedeemed, redeemedFromDB) + } +} + func generateRandomString(length int) string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, length) @@ -377,11 +459,12 @@ func generateRandomString(length int) string { func generateRandomProofs(num int) cashu.Proofs { proofs := make(cashu.Proofs, num) + keysetId := generateRandomString(32) for i := 0; i < num; i++ { proof := cashu.Proof{ Amount: 21, - Id: generateRandomString(32), + Id: keysetId, Secret: generateRandomString(64), C: generateRandomString(64), } @@ -456,10 +539,11 @@ func generateRandomB_s(num int) []string { func generateBlindSignatures(num int) cashu.BlindedSignatures { blindSigs := make(cashu.BlindedSignatures, num) + keysetId := generateRandomString(32) for i := 0; i < num; i++ { sig := cashu.BlindedSignature{ C_: generateRandomString(33), - Id: generateRandomString(32), + Id: keysetId, Amount: 21, DLEQ: &cashu.DLEQProof{ E: generateRandomString(33), diff --git a/mint/storage/storage.go b/mint/storage/storage.go index 17ec455..7fb8d67 100644 --- a/mint/storage/storage.go +++ b/mint/storage/storage.go @@ -8,8 +8,6 @@ import ( ) type MintDB interface { - GetBalance() (uint64, error) - SaveSeed([]byte) error GetSeed() ([]byte, error) @@ -39,6 +37,10 @@ type MintDB interface { GetBlindSignature(B_ string) (cashu.BlindedSignature, error) GetBlindSignatures(B_s []string) (cashu.BlindedSignatures, error) + // these return a map of keyset id and amount + GetEcashIssued() (map[string]uint64, error) + GetEcashRedeemed() (map[string]uint64, error) + Close() error }