Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

error codes #41

Merged
merged 1 commit into from
Jul 30, 2024
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
68 changes: 40 additions & 28 deletions cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,41 +213,53 @@ func (e Error) Error() string {

// Common error codes
const (
StandardErrCode CashuErrCode = 1000 + iota
KeysetErrCode
PaymentMethodErrCode
UnitErrCode
QuoteErrCode
InvoiceErrCode
ProofsErrCode
DBErrorCode
StandardErrCode CashuErrCode = 10000
// These will never be returned in a response.
// Using them to identify internally where
// the error originated and log appropriately
DBErrCode CashuErrCode = 1
LightningBackendErrCode CashuErrCode = 2

UnitErrCode CashuErrCode = 11005
PaymentMethodErrCode CashuErrCode = 11006

InvalidProofErrCode CashuErrCode = 10003
ProofAlreadyUsedErrCode CashuErrCode = 11001
InsufficientProofAmountErrCode CashuErrCode = 11002

UnknownKeysetErrCode CashuErrCode = 12001
InactiveKeysetErrCode CashuErrCode = 12002

MintQuoteRequestNotPaidErrCode CashuErrCode = 20001
MintQuoteAlreadyIssuedErrCode CashuErrCode = 20002

MeltQuotePendingErrCode CashuErrCode = 20005
MeltQuoteAlreadyPaidErrCode CashuErrCode = 20006

QuoteErrCode CashuErrCode = 20007
)

var (
StandardErr = Error{Detail: "unable to process request", Code: StandardErrCode}
EmptyBodyErr = Error{Detail: "request body cannot be emtpy", Code: StandardErrCode}
KeysetNotExistErr = Error{Detail: "keyset does not exist", Code: KeysetErrCode}
UnknownKeysetErr = Error{Detail: "unknown keyset", Code: UnknownKeysetErrCode}
PaymentMethodNotSupportedErr = Error{Detail: "payment method not supported", Code: PaymentMethodErrCode}
UnitNotSupportedErr = Error{Detail: "unit not supported", Code: UnitErrCode}
InvalidBlindedMessageAmount = Error{Detail: "invalid amount in blinded message", Code: KeysetErrCode}
QuoteIdNotSpecifiedErr = Error{Detail: "quote id not specified", Code: QuoteErrCode}
InvoiceNotExistErr = Error{Detail: "invoice does not exist", Code: InvoiceErrCode}
InvoiceNotPaidErr = Error{Detail: "invoice has not been paid", Code: InvoiceErrCode}
OutputsOverInvoiceErr = Error{
Detail: "sum of the output amounts is greater than amount of invoice paid",
Code: InvoiceErrCode}
InvoiceTokensIssuedErr = Error{Detail: "tokens already issued for invoice", Code: InvoiceErrCode}
ProofAlreadyUsedErr = Error{Detail: "proofs already used", Code: ProofsErrCode}
InvalidProofErr = Error{Detail: "invalid proof", Code: ProofsErrCode}
NoProofsProvided = Error{Detail: "no proofs provided", Code: ProofsErrCode}
DuplicateProofs = Error{Detail: "duplicate proofs", Code: ProofsErrCode}
InputsBelowOutputs = Error{Detail: "amount of input proofs is below amount of outputs", Code: ProofsErrCode}
EmptyInputsErr = Error{Detail: "inputs cannot be empty", Code: ProofsErrCode}
QuoteNotExistErr = Error{Detail: "quote does not exist", Code: QuoteErrCode}
QuoteAlreadyPaid = Error{Detail: "quote already paid", Code: QuoteErrCode}
InsufficientProofsAmount = Error{Detail: "amount of input proofs is below amount needed for transaction", Code: ProofsErrCode}
InvalidKeysetProof = Error{Detail: "proof from an invalid keyset", Code: ProofsErrCode}
InvalidSignatureRequest = Error{Detail: "requested signature from non-active keyset", Code: KeysetErrCode}
InvalidBlindedMessageAmount = Error{Detail: "invalid amount in blinded message", Code: StandardErrCode}
MintQuoteRequestNotPaid = Error{Detail: "quote request has not been paid", Code: MintQuoteRequestNotPaidErrCode}
MintQuoteAlreadyIssued = Error{Detail: "quote already issued", Code: MintQuoteAlreadyIssuedErrCode}
OutputsOverQuoteAmountErr = Error{Detail: "sum of the output amounts is greater than quote amount", Code: StandardErrCode}
ProofAlreadyUsedErr = Error{Detail: "proofs already used", Code: ProofAlreadyUsedErrCode}
InvalidProofErr = Error{Detail: "invalid proof", Code: InvalidProofErrCode}
NoProofsProvided = Error{Detail: "no proofs provided", Code: InvalidProofErrCode}
DuplicateProofs = Error{Detail: "duplicate proofs", Code: InvalidProofErrCode}
QuoteNotExistErr = Error{Detail: "quote does not exist", Code: QuoteErrCode}
MeltQuoteAlreadyPaid = Error{Detail: "quote already paid", Code: MeltQuoteAlreadyPaidErrCode}
InsufficientProofsAmount = Error{
Detail: "amount of input proofs is below amount needed for transaction",
Code: InsufficientProofAmountErrCode,
}
InactiveKeysetSignatureRequest = Error{Detail: "requested signature from non-active keyset", Code: InactiveKeysetErrCode}
)

// Given an amount, it returns list of amounts e.g 13 -> [1, 4, 8]
Expand Down
92 changes: 51 additions & 41 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (stor
invoice, err := m.requestInvoice(amount)
if err != nil {
msg := fmt.Sprintf("error generating payment request: %v", err)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.InvoiceErrCode)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode)
}

quoteId, err := cashu.GenerateRandomQuoteId()
Expand All @@ -202,7 +202,7 @@ func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (stor
err = m.db.SaveMintQuote(mintQuote)
if err != nil {
msg := fmt.Sprintf("error saving mint quote to db: %v", err)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode)
}

return mintQuote, nil
Expand All @@ -224,15 +224,15 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (storage.MintQuote, err
status, err := m.LightningClient.InvoiceStatus(mintQuote.PaymentHash)
if err != nil {
msg := fmt.Sprintf("error getting status of payment request: %v", err)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.InvoiceErrCode)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode)
}

if status.Settled && mintQuote.State == nut04.Unpaid {
mintQuote.State = nut04.Paid
err := m.db.UpdateMintQuoteState(mintQuote.Id, mintQuote.State)
if err != nil {
msg := fmt.Sprintf("error getting quote state: %v", err)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode)
}
}

Expand All @@ -255,11 +255,11 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag
status, err := m.LightningClient.InvoiceStatus(mintQuote.PaymentHash)
if err != nil {
msg := fmt.Sprintf("error getting status of payment request: %v", err)
return nil, cashu.BuildCashuError(msg, cashu.InvoiceErrCode)
return nil, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode)
}
if status.Settled {
if mintQuote.State == nut04.Issued {
return nil, cashu.InvoiceTokensIssuedErr
return nil, cashu.MintQuoteAlreadyIssued
}

blindedMessagesAmount := blindedMessages.Amount()
Expand All @@ -275,7 +275,7 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag
// verify that amount from blinded messages is less
// than quote amount
if blindedMessagesAmount > mintQuote.Amount {
return nil, cashu.OutputsOverInvoiceErr
return nil, cashu.OutputsOverQuoteAmountErr
}

var err error
Expand All @@ -288,10 +288,10 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag
err = m.db.UpdateMintQuoteState(mintQuote.Id, nut04.Issued)
if err != nil {
msg := fmt.Sprintf("error getting quote state: %v", err)
return nil, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return nil, cashu.BuildCashuError(msg, cashu.DBErrCode)
}
} else {
return nil, cashu.InvoiceNotPaidErr
return nil, cashu.MintQuoteRequestNotPaid
}

return blindedSignatures, nil
Expand All @@ -303,13 +303,8 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag
// the proofs that were used as input.
// It returns the BlindedSignatures.
func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) (cashu.BlindedSignatures, error) {
proofsLen := len(proofs)
if proofsLen == 0 {
return nil, cashu.NoProofsProvided
}

var proofsAmount uint64
Ys := make([]string, proofsLen)
Ys := make([]string, len(proofs))
for i, proof := range proofs {
proofsAmount += proof.Amount

Expand All @@ -335,19 +330,7 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages)
return nil, cashu.InsufficientProofsAmount
}

// check if proofs were alredy used
usedProofs, err := m.db.GetProofsUsed(Ys)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
msg := fmt.Sprintf("could not get used proofs from db: %v", err)
return nil, cashu.BuildCashuError(msg, cashu.DBErrorCode)
}
}
if len(usedProofs) != 0 {
return nil, cashu.ProofAlreadyUsedErr
}

err = m.verifyProofs(proofs)
err := m.verifyProofs(proofs, Ys)
if err != nil {
return nil, err
}
Expand All @@ -362,7 +345,7 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages)
err = m.db.SaveProofs(proofs)
if err != nil {
msg := fmt.Sprintf("error invalidating proofs. Could not save proofs to db: %v", err)
return nil, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return nil, cashu.BuildCashuError(msg, cashu.DBErrCode)
}

return blindedSignatures, nil
Expand All @@ -382,7 +365,7 @@ func (m *Mint) MeltRequest(method, request, unit string) (storage.MeltQuote, err
bolt11, err := decodepay.Decodepay(request)
if err != nil {
msg := fmt.Sprintf("invalid invoice: %v", err)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.InvoiceErrCode)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.StandardErrCode)
}

quoteId, err := cashu.GenerateRandomQuoteId()
Expand All @@ -406,7 +389,7 @@ func (m *Mint) MeltRequest(method, request, unit string) (storage.MeltQuote, err
}
if err := m.db.SaveMeltQuote(meltQuote); err != nil {
msg := fmt.Sprintf("error saving melt quote to db: %v", err)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode)
}

return meltQuote, nil
Expand All @@ -430,6 +413,19 @@ func (m *Mint) GetMeltQuoteState(method, quoteId string) (storage.MeltQuote, err
// MeltTokens verifies whether proofs provided are valid
// and proceeds to attempt payment.
func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage.MeltQuote, error) {
var proofsAmount uint64
Ys := make([]string, len(proofs))
for i, proof := range proofs {
proofsAmount += proof.Amount

Y, err := crypto.HashToCurve([]byte(proof.Secret))
if err != nil {
return storage.MeltQuote{}, cashu.InvalidProofErr
}
Yhex := hex.EncodeToString(Y.SerializeCompressed())
Ys[i] = Yhex
}

if method != BOLT11_METHOD {
return storage.MeltQuote{}, cashu.PaymentMethodNotSupportedErr
}
Expand All @@ -439,15 +435,14 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage.
return storage.MeltQuote{}, cashu.QuoteNotExistErr
}
if meltQuote.State == nut05.Paid {
return storage.MeltQuote{}, cashu.QuoteAlreadyPaid
return storage.MeltQuote{}, cashu.MeltQuoteAlreadyPaid
}

err = m.verifyProofs(proofs)
err = m.verifyProofs(proofs, Ys)
if err != nil {
return storage.MeltQuote{}, err
}

proofsAmount := proofs.Amount()
fees := m.TransactionFees(proofs)
// checks if amount in proofs is enough
if proofsAmount < meltQuote.Amount+meltQuote.FeeReserve+uint64(fees) {
Expand All @@ -458,7 +453,7 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage.
// to make the payment
preimage, err := m.LightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount)
if err != nil {
return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.InvoiceErrCode)
return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.LightningBackendErrCode)
}

// if payment succeeded, mark melt quote as paid
Expand All @@ -468,21 +463,33 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage.
err = m.db.UpdateMeltQuote(meltQuote.Id, meltQuote.Preimage, meltQuote.State)
if err != nil {
msg := fmt.Sprintf("error getting quote state: %v", err)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode)
}

err = m.db.SaveProofs(proofs)
if err != nil {
msg := fmt.Sprintf("error invalidating proofs. Could not save proofs to db: %v", err)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrorCode)
return storage.MeltQuote{}, cashu.BuildCashuError(msg, cashu.DBErrCode)
}

return *meltQuote, nil
}

func (m *Mint) verifyProofs(proofs cashu.Proofs) error {
func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error {
if len(proofs) == 0 {
return cashu.EmptyInputsErr
return cashu.NoProofsProvided
}

// check if proofs were alredy used
usedProofs, err := m.db.GetProofsUsed(Ys)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
msg := fmt.Sprintf("could not get used proofs from db: %v", err)
return cashu.BuildCashuError(msg, cashu.DBErrCode)
}
}
if len(usedProofs) != 0 {
return cashu.ProofAlreadyUsedErr
}

// check duplicte proofs
Expand All @@ -495,7 +502,7 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs) error {
// of the mint's keyset
var k *secp256k1.PrivateKey
if keyset, ok := m.Keysets[proof.Id]; !ok {
return cashu.InvalidKeysetProof
return cashu.UnknownKeysetErr
} else {
if key, ok := keyset.Keys[proof.Amount]; ok {
k = key.PrivateKey
Expand Down Expand Up @@ -527,10 +534,13 @@ func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu
blindedSignatures := make(cashu.BlindedSignatures, len(blindedMessages))

for i, msg := range blindedMessages {
if _, ok := m.Keysets[msg.Id]; !ok {
return nil, cashu.UnknownKeysetErr
}
var k *secp256k1.PrivateKey
keyset, ok := m.ActiveKeysets[msg.Id]
if !ok {
return nil, cashu.InvalidSignatureRequest
return nil, cashu.InactiveKeysetSignatureRequest
} else {
if key, ok := keyset.Keys[msg.Amount]; ok {
k = key.PrivateKey
Expand Down
32 changes: 21 additions & 11 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ func TestMintTokens(t *testing.T) {

// test without paying invoice
_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, blindedMessages)
if !errors.Is(err, cashu.InvoiceNotPaidErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.InvoiceNotPaidErr, err)
if !errors.Is(err, cashu.MintQuoteRequestNotPaid) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.MintQuoteRequestNotPaid, err)
}

// test invalid quote
Expand All @@ -224,16 +224,16 @@ func TestMintTokens(t *testing.T) {
// test with blinded messages over request mint amount
overBlindedMessages, _, _, err := testutils.CreateBlindedMessages(mintAmount+100, keyset)
_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, overBlindedMessages)
if !errors.Is(err, cashu.OutputsOverInvoiceErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.OutputsOverInvoiceErr, err)
if !errors.Is(err, cashu.OutputsOverQuoteAmountErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.OutputsOverQuoteAmountErr, err)
}

// test with invalid keyset in blinded messages
invalidKeyset := crypto.MintKeyset{Id: "0192384aa"}
invalidKeysetMessages, _, _, err := testutils.CreateBlindedMessages(mintAmount, invalidKeyset)
_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, invalidKeysetMessages)
if !errors.Is(err, cashu.InvalidSignatureRequest) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.InvalidSignatureRequest, err)
if !errors.Is(err, cashu.UnknownKeysetErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.UnknownKeysetErr, err)
}

_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, blindedMessages)
Expand All @@ -243,8 +243,8 @@ func TestMintTokens(t *testing.T) {

// test already minted tokens
_, err = testMint.MintTokens(testutils.BOLT11_METHOD, mintQuoteResponse.Id, blindedMessages)
if !errors.Is(err, cashu.InvoiceTokensIssuedErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.InvoiceTokensIssuedErr, err)
if !errors.Is(err, cashu.MintQuoteAlreadyIssued) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.MintQuoteAlreadyIssued, err)
}
}

Expand Down Expand Up @@ -459,10 +459,20 @@ func TestMelt(t *testing.T) {
t.Fatal("got unexpected unpaid melt quote")
}

// test already used proofs
// test quote already paid
_, err = testMint.MeltTokens(testutils.BOLT11_METHOD, meltQuote.Id, validProofs)
if !errors.Is(err, cashu.QuoteAlreadyPaid) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.QuoteAlreadyPaid, err)
if !errors.Is(err, cashu.MeltQuoteAlreadyPaid) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.MeltQuoteAlreadyPaid, err)
}

// test already used proofs
newQuote, err := testMint.MeltRequest(testutils.BOLT11_METHOD, addInvoiceResponse.PaymentRequest, testutils.SAT_UNIT)
if err != nil {
t.Fatalf("got unexpected error in melt request: %v", err)
}
_, err = testMint.MeltTokens(testutils.BOLT11_METHOD, newQuote.Id, validProofs)
if !errors.Is(err, cashu.ProofAlreadyUsedErr) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.ProofAlreadyUsedErr, err)
}

// mint with fees
Expand Down
Loading
Loading