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

NUT-02: fees #36

Merged
merged 4 commits into from
Jul 16, 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
2 changes: 2 additions & 0 deletions .env.mint.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
MINT_PRIVATE_KEY="mykey"
# use only if setting up mint with one unit (defaults to sat)
MINT_DERIVATION_PATH="0/0/0"
# fee to charge per input (in parts per thousand)
INPUT_FEE_PPK=100

# mint info
MINT_NAME="a cashu mint"
Expand Down
19 changes: 18 additions & 1 deletion cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ var (
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: "insufficient amount in proofs", Code: ProofsErrCode}
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}
)
Expand All @@ -258,3 +258,20 @@ func AmountSplit(amount uint64) []uint64 {
}
return rv
}

func Max(x, y uint64) uint64 {
if x > y {
return x
}
return y
}

func Count(amounts []uint64, amount uint64) uint {
var count uint = 0
for _, amt := range amounts {
if amt == amount {
count++
}
}
return count
}
7 changes: 4 additions & 3 deletions cashu/nuts/nut02/nut02.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ type GetKeysetsResponse struct {
}

type Keyset struct {
Id string `json:"id"`
Unit string `json:"unit"`
Active bool `json:"active"`
Id string `json:"id"`
Unit string `json:"unit"`
Active bool `json:"active"`
InputFeePpk uint `json:"input_fee_ppk"`
}
78 changes: 46 additions & 32 deletions crypto/keyset.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,41 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

const maxOrder = 64
const MAX_ORDER = 64

type Keyset struct {
Id string
Unit string
Active bool
Keys map[uint64]KeyPair
Id string
Unit string
Active bool
Keys map[uint64]KeyPair
InputFeePpk uint
}

type KeyPair struct {
PrivateKey *secp256k1.PrivateKey
PublicKey *secp256k1.PublicKey
}

// KeysetsMap maps a mint url to map of string keyset id to keyset
type KeysetsMap map[string]map[string]WalletKeyset

type WalletKeyset struct {
Id string
MintURL string
Unit string
Active bool
PublicKeys map[uint64]*secp256k1.PublicKey
Counter uint32
}

func GenerateKeyset(seed, derivationPath string) *Keyset {
keys := make(map[uint64]KeyPair, maxOrder)
func GenerateKeyset(seed, derivationPath string, inputFeePpk uint) *Keyset {
keys := make(map[uint64]KeyPair, MAX_ORDER)

pks := make(map[uint64]*secp256k1.PublicKey)
for i := 0; i < maxOrder; i++ {
for i := 0; i < MAX_ORDER; i++ {
amount := uint64(math.Pow(2, float64(i)))
hash := sha256.Sum256([]byte(seed + derivationPath + strconv.FormatUint(amount, 10)))
privKey, pubKey := btcec.PrivKeyFromBytes(hash[:])
keys[amount] = KeyPair{PrivateKey: privKey, PublicKey: pubKey}
pks[amount] = pubKey
}
keysetId := DeriveKeysetId(pks)
return &Keyset{Id: keysetId, Unit: "sat", Active: true, Keys: keys}

return &Keyset{
Id: keysetId,
Unit: "sat",
Active: true,
Keys: keys,
InputFeePpk: inputFeePpk,
}
}

// DeriveKeysetId returns the string ID derived from the map keyset
Expand Down Expand Up @@ -97,10 +93,11 @@ func (ks *Keyset) DerivePublic() map[uint64]string {
}

type KeysetTemp struct {
Id string
Unit string
Active bool
Keys map[uint64]json.RawMessage
Id string
Unit string
Active bool
Keys map[uint64]json.RawMessage
InputFeePpk uint
}

func (ks *Keyset) MarshalJSON() ([]byte, error) {
Expand All @@ -116,6 +113,7 @@ func (ks *Keyset) MarshalJSON() ([]byte, error) {
}
return m
}(),
InputFeePpk: ks.InputFeePpk,
}

return json.Marshal(temp)
Expand Down Expand Up @@ -180,13 +178,27 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error {
return nil
}

// KeysetsMap maps a mint url to map of string keyset id to keyset
type KeysetsMap map[string]map[string]WalletKeyset

type WalletKeyset struct {
Id string
MintURL string
Unit string
Active bool
PublicKeys map[uint64]*secp256k1.PublicKey
Counter uint32
InputFeePpk uint
}

type WalletKeysetTemp struct {
Id string
MintURL string
Unit string
Active bool
PublicKeys map[uint64][]byte
Counter uint32
Id string
MintURL string
Unit string
Active bool
PublicKeys map[uint64][]byte
Counter uint32
InputFeePpk uint
}

func (wk *WalletKeyset) MarshalJSON() ([]byte, error) {
Expand All @@ -202,7 +214,8 @@ func (wk *WalletKeyset) MarshalJSON() ([]byte, error) {
}
return m
}(),
Counter: wk.Counter,
Counter: wk.Counter,
InputFeePpk: wk.InputFeePpk,
}

return json.Marshal(temp)
Expand All @@ -220,6 +233,7 @@ func (wk *WalletKeyset) UnmarshalJSON(data []byte) error {
wk.Unit = temp.Unit
wk.Active = temp.Active
wk.Counter = temp.Counter
wk.InputFeePpk = temp.InputFeePpk

wk.PublicKeys = make(map[uint64]*secp256k1.PublicKey)
for k, v := range temp.PublicKeys {
Expand Down
13 changes: 13 additions & 0 deletions mint/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
"strconv"

"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/elnosh/gonuts/cashu/nuts/nut06"
Expand All @@ -15,14 +17,25 @@ type Config struct {
DerivationPath string
Port string
DBPath string
InputFeePpk uint
}

func GetConfig() Config {
var inputFeePpk uint = 0
if len(os.Getenv("INPUT_FEE_PPK")) > 0 {
fee, err := strconv.ParseUint(os.Getenv("INPUT_FEE_PPK"), 10, 16)
if err != nil {
log.Fatalf("unable to parse INPUT_FEE_PPK: %v", err)
}
inputFeePpk = uint(fee)
}

return Config{
PrivateKey: os.Getenv("MINT_PRIVATE_KEY"),
DerivationPath: os.Getenv("MINT_DERIVATION_PATH"),
Port: os.Getenv("MINT_PORT"),
DBPath: os.Getenv("MINT_DB_PATH"),
InputFeePpk: inputFeePpk,
}
}

Expand Down
32 changes: 21 additions & 11 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func LoadMint(config Config) (*Mint, error) {
log.Fatalf("error starting mint: %v", err)
}

activeKeyset := crypto.GenerateKeyset(config.PrivateKey, config.DerivationPath)
activeKeyset := crypto.GenerateKeyset(config.PrivateKey, config.DerivationPath, config.InputFeePpk)
mint := &Mint{db: db, ActiveKeysets: map[string]crypto.Keyset{activeKeyset.Id: *activeKeyset}}

mint.db.SaveKeyset(activeKeyset)
Expand Down Expand Up @@ -240,9 +240,9 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages)
}
}
}

if proofsAmount < blindedMessagesAmount {
return nil, cashu.InputsBelowOutputs
fees := m.TransactionFees(proofs)
if proofsAmount-uint64(fees) < blindedMessagesAmount {
return nil, cashu.InsufficientProofsAmount
}

err := m.verifyProofs(proofs)
Expand Down Expand Up @@ -351,18 +351,18 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuot
return MeltQuote{}, cashu.QuoteAlreadyPaid
}

proofsAmount := proofs.Amount()

// checks if amount in proofs is enough
if proofsAmount < meltQuote.Amount+meltQuote.FeeReserve {
return MeltQuote{}, cashu.InsufficientProofsAmount
}

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

proofsAmount := proofs.Amount()
fees := m.TransactionFees(proofs)
// checks if amount in proofs is enough
if proofsAmount < meltQuote.Amount+meltQuote.FeeReserve+uint64(fees) {
return MeltQuote{}, cashu.InsufficientProofsAmount
}

// if proofs are valid, ask the lightning backend
// to make the payment
preimage, err := m.LightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount)
Expand Down Expand Up @@ -482,3 +482,13 @@ func (m *Mint) requestInvoice(amount uint64) (*lightning.Invoice, error) {

return &invoice, nil
}

func (m *Mint) TransactionFees(inputs cashu.Proofs) uint {
var fees uint = 0
for _, proof := range inputs {
// note: not checking that proof id is from valid keyset
// because already doing that in call to verifyProofs
fees += m.Keysets[proof.Id].InputFeePpk
}
return (fees + 999) / 1000
}
Loading
Loading