Skip to content

Commit

Permalink
Merge pull request #36 from elnosh/nut02-fees
Browse files Browse the repository at this point in the history
NUT-02: fees
  • Loading branch information
elnosh committed Jul 16, 2024
2 parents d961a2d + 27a4cae commit d793565
Show file tree
Hide file tree
Showing 12 changed files with 732 additions and 162 deletions.
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

0 comments on commit d793565

Please sign in to comment.