Skip to content

Commit

Permalink
Merge pull request #41 from elnosh/error-codes
Browse files Browse the repository at this point in the history
error codes
  • Loading branch information
elnosh authored Jul 30, 2024
2 parents cc9fbf2 + 33265f1 commit 49b0c1b
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 89 deletions.
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

0 comments on commit 49b0c1b

Please sign in to comment.