From b884044c83f297623b94c388f5fb72b9723ad144 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Thu, 7 Mar 2024 16:11:48 +0100 Subject: [PATCH 1/6] extract fee logic into its own file --- server.go | 6 +- swapper/fee.go | 137 ++++++++++++++++++++++++++++++++++++++++++++++ swapper/redeem.go | 126 ++---------------------------------------- 3 files changed, 146 insertions(+), 123 deletions(-) create mode 100644 swapper/fee.go diff --git a/server.go b/server.go index b0f7373..acf1cd5 100644 --- a/server.go +++ b/server.go @@ -435,9 +435,11 @@ func main() { go registerPastBoltzReverseSwapTxNotifications() ctx, cancel := context.WithCancel(context.Background()) defer cancel() + swapFeeService := swapper.NewFeeService() + swapFeeService.Start(ctx) redeemer := swapper.NewRedeemer(ssClient, ssRouterClient, subswapClient, - updateSubswapTxid, updateSubswapPreimage, getInProgressRedeems, - setSubswapConfirmed) + swapFeeService, updateSubswapTxid, updateSubswapPreimage, + getInProgressRedeems, setSubswapConfirmed) redeemer.Start(ctx) lsp.InitLSP() diff --git a/swapper/fee.go b/swapper/fee.go new file mode 100644 index 0000000..d924284 --- /dev/null +++ b/swapper/fee.go @@ -0,0 +1,137 @@ +package swapper + +import ( + "context" + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "strconv" + "sync" + "time" +) + +type FeeService struct { + feesLastUpdated time.Time + currentFees *whatthefeeBody + mtx sync.RWMutex +} + +type whatthefeeBody struct { + Index []int32 `json:"index"` + Columns []string `json:"columns"` + Data [][]int32 `json:"data"` +} + +func NewFeeService() *FeeService { + return &FeeService{} +} + +func (f *FeeService) Start(ctx context.Context) { + go f.watchFeeRate(ctx) +} + +func (f *FeeService) GetFeeRate(blocks int32) (float64, error) { + f.mtx.RLock() + defer f.mtx.RUnlock() + + if f.currentFees == nil { + return 0, fmt.Errorf("still no fees") + } + + if len(f.currentFees.Index) < 1 { + return 0, fmt.Errorf("empty row index") + } + + // get the block between 0 and SwapLockTime + b := math.Min(math.Max(0, float64(blocks)), SwapLockTime) + + // certainty is linear between 0.5 and 1 based on the amount of blocks left + certainty := 0.5 + (((SwapLockTime - b) / SwapLockTime) / 2) + + // Get the row closest to the amount of blocks left + rowIndex := 0 + prevRow := f.currentFees.Index[rowIndex] + for i := 1; i < len(f.currentFees.Index); i++ { + current := f.currentFees.Index[i] + if math.Abs(float64(current)-b) < math.Abs(float64(prevRow)-b) { + rowIndex = i + prevRow = current + } + } + + if len(f.currentFees.Columns) < 1 { + return 0, fmt.Errorf("empty column index") + } + + // Get the column closest to the certainty + columnIndex := 0 + prevColumn, err := strconv.ParseFloat(f.currentFees.Columns[columnIndex], 64) + if err != nil { + return 0, fmt.Errorf("invalid column content '%s'", f.currentFees.Columns[columnIndex]) + } + for i := 1; i < len(f.currentFees.Columns); i++ { + current, err := strconv.ParseFloat(f.currentFees.Columns[i], 64) + if err != nil { + return 0, fmt.Errorf("invalid column content '%s'", f.currentFees.Columns[i]) + } + if math.Abs(current-certainty) < math.Abs(prevColumn-certainty) { + columnIndex = i + prevColumn = current + } + } + + if rowIndex >= len(f.currentFees.Data) { + return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response") + } + row := f.currentFees.Data[rowIndex] + if columnIndex >= len(row) { + return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response") + } + + rate := row[columnIndex] + satPerVByte := math.Exp(float64(rate) / 100) + return satPerVByte, nil +} + +func (f *FeeService) watchFeeRate(ctx context.Context) { + for { + now := time.Now() + fees, err := f.getFees() + if err != nil { + log.Printf("failed to get current chain fee rates: %v", err) + } else { + f.mtx.Lock() + f.currentFees = fees + f.feesLastUpdated = now + f.mtx.Unlock() + } + + select { + case <-time.After(time.Minute * 5): + case <-ctx.Done(): + return + } + } +} + +func (r *FeeService) getFees() (*whatthefeeBody, error) { + now := time.Now().Unix() + cacheBust := (now / 300) * 300 + resp, err := http.Get( + fmt.Sprintf("https://whatthefee.io/data.json?c=%d", cacheBust), + ) + if err != nil { + return nil, fmt.Errorf("failed to call whatthefee.io: %v", err) + } + defer resp.Body.Close() + + var body whatthefeeBody + err = json.NewDecoder(resp.Body).Decode(&body) + if err != nil { + return nil, fmt.Errorf("failed to decode whatthefee.io response: %w", err) + } + + return &body, nil +} diff --git a/swapper/redeem.go b/swapper/redeem.go index 75bfd00..eee5077 100644 --- a/swapper/redeem.go +++ b/swapper/redeem.go @@ -5,14 +5,9 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "log" - "math" - "net/http" "os" - "strconv" - "sync" "time" "github.com/btcsuite/btcd/blockchain" @@ -40,19 +35,18 @@ type Redeemer struct { ssClient lnrpc.LightningClient ssRouterClient routerrpc.RouterClient subswapClient submarineswaprpc.SubmarineSwapperClient + feeService *FeeService updateSubswapTxid func(paymentHash, txid string) error updateSubswapPreimage func(paymentHash, paymentPreimage string) error getInProgressRedeems func(blockheight int32) ([]*InProgressRedeem, error) setSubswapConfirmed func(paymentHash string) error - feesLastUpdated time.Time - currentFees *whatthefeeBody - mtx sync.RWMutex } func NewRedeemer( ssClient lnrpc.LightningClient, ssRouterClient routerrpc.RouterClient, subswapClient submarineswaprpc.SubmarineSwapperClient, + feeService *FeeService, updateSubswapTxid func(paymentHash, txid string) error, updateSubswapPreimage func(paymentHash, paymentPreimage string) error, getInProgressRedeems func(blockheight int32) ([]*InProgressRedeem, error), @@ -62,6 +56,7 @@ func NewRedeemer( ssClient: ssClient, ssRouterClient: ssRouterClient, subswapClient: subswapClient, + feeService: feeService, updateSubswapTxid: updateSubswapTxid, updateSubswapPreimage: updateSubswapPreimage, getInProgressRedeems: getInProgressRedeems, @@ -72,28 +67,6 @@ func NewRedeemer( func (r *Redeemer) Start(ctx context.Context) { log.Printf("REDEEM - before r.watchRedeemTxns()") go r.watchRedeemTxns(ctx) - go r.watchFeeRate(ctx) -} - -func (r *Redeemer) watchFeeRate(ctx context.Context) { - for { - now := time.Now() - fees, err := r.getFees() - if err != nil { - log.Printf("failed to get current chain fee rates: %v", err) - } else { - r.mtx.Lock() - r.currentFees = fees - r.feesLastUpdated = now - r.mtx.Unlock() - } - - select { - case <-time.After(time.Minute * 5): - case <-ctx.Done(): - return - } - } } func (r *Redeemer) watchRedeemTxns(ctx context.Context) { @@ -109,95 +82,6 @@ func (r *Redeemer) watchRedeemTxns(ctx context.Context) { } } -type whatthefeeBody struct { - Index []int32 `json:"index"` - Columns []string `json:"columns"` - Data [][]int32 `json:"data"` -} - -func (r *Redeemer) getFees() (*whatthefeeBody, error) { - now := time.Now().Unix() - cacheBust := (now / 300) * 300 - resp, err := http.Get( - fmt.Sprintf("https://whatthefee.io/data.json?c=%d", cacheBust), - ) - if err != nil { - return nil, fmt.Errorf("failed to call whatthefee.io: %v", err) - } - defer resp.Body.Close() - - var body whatthefeeBody - err = json.NewDecoder(resp.Body).Decode(&body) - if err != nil { - return nil, fmt.Errorf("failed to decode whatthefee.io response: %w", err) - } - - return &body, nil -} - -func (r *Redeemer) getFeeRate(blocks int32) (float64, error) { - r.mtx.RLock() - defer r.mtx.RUnlock() - - if r.currentFees == nil { - return 0, fmt.Errorf("still no fees") - } - - if len(r.currentFees.Index) < 1 { - return 0, fmt.Errorf("empty row index") - } - - // get the block between 0 and SwapLockTime - b := math.Min(math.Max(0, float64(blocks)), SwapLockTime) - - // certainty is linear between 0.5 and 1 based on the amount of blocks left - certainty := 0.5 + (((SwapLockTime - b) / SwapLockTime) / 2) - - // Get the row closest to the amount of blocks left - rowIndex := 0 - prevRow := r.currentFees.Index[rowIndex] - for i := 1; i < len(r.currentFees.Index); i++ { - current := r.currentFees.Index[i] - if math.Abs(float64(current)-b) < math.Abs(float64(prevRow)-b) { - rowIndex = i - prevRow = current - } - } - - if len(r.currentFees.Columns) < 1 { - return 0, fmt.Errorf("empty column index") - } - - // Get the column closest to the certainty - columnIndex := 0 - prevColumn, err := strconv.ParseFloat(r.currentFees.Columns[columnIndex], 64) - if err != nil { - return 0, fmt.Errorf("invalid column content '%s'", r.currentFees.Columns[columnIndex]) - } - for i := 1; i < len(r.currentFees.Columns); i++ { - current, err := strconv.ParseFloat(r.currentFees.Columns[i], 64) - if err != nil { - return 0, fmt.Errorf("invalid column content '%s'", r.currentFees.Columns[i]) - } - if math.Abs(current-certainty) < math.Abs(prevColumn-certainty) { - columnIndex = i - prevColumn = current - } - } - - if rowIndex >= len(r.currentFees.Data) { - return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response") - } - row := r.currentFees.Data[rowIndex] - if columnIndex >= len(row) { - return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response") - } - - rate := row[columnIndex] - satPerVByte := math.Exp(float64(rate) / 100) - return satPerVByte, nil -} - func (r *Redeemer) checkRedeems() { log.Printf("REDEEM - checkRedeems() begin") @@ -308,7 +192,7 @@ func (r *Redeemer) checkRedeem(blockHeight int32, inProgressRedeem *InProgressRe } } - satPerVbyte, err := r.getFeeRate(blocksLeft) + satPerVbyte, err := r.feeService.GetFeeRate(blocksLeft) if err != nil { log.Printf("failed to get redeem fee rate: %v", err) // If there is a problem getting the fees, try to bump the tx on best effort. @@ -348,7 +232,7 @@ func getWeight(tx *lnrpc.Transaction) (int64, error) { } func (r *Redeemer) RedeemWithinBlocks(preimage []byte, blocks int32) (string, error) { - rate, err := r.getFeeRate(blocks) + rate, err := r.feeService.GetFeeRate(blocks) if err != nil { log.Printf("RedeemWithinBlocks(%x, %d) - getFeeRate error: %v", preimage, blocks, err) } From acb4b7b6f36cc8cb7f4d4c3f293fed5cda7be7e0 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 11 Mar 2024 10:22:27 +0100 Subject: [PATCH 2/6] error on stale fees --- swapper/fee.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/swapper/fee.go b/swapper/fee.go index d924284..9ae5a28 100644 --- a/swapper/fee.go +++ b/swapper/fee.go @@ -40,6 +40,10 @@ func (f *FeeService) GetFeeRate(blocks int32) (float64, error) { return 0, fmt.Errorf("still no fees") } + if f.feesLastUpdated.Before(time.Now().Add(-time.Hour)) { + return 0, fmt.Errorf("fees are stale") + } + if len(f.currentFees.Index) < 1 { return 0, fmt.Errorf("empty row index") } From 5e41043e32b913965f48a1654ed355536ad43604 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 11 Mar 2024 14:28:34 +0100 Subject: [PATCH 3/6] pass lockheight to GetFeeRate --- swapper/fee.go | 6 +++--- swapper/redeem.go | 10 +++++----- swapper/swapper.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/swapper/fee.go b/swapper/fee.go index 9ae5a28..4bdefeb 100644 --- a/swapper/fee.go +++ b/swapper/fee.go @@ -32,7 +32,7 @@ func (f *FeeService) Start(ctx context.Context) { go f.watchFeeRate(ctx) } -func (f *FeeService) GetFeeRate(blocks int32) (float64, error) { +func (f *FeeService) GetFeeRate(blocks int32, locktime int32) (float64, error) { f.mtx.RLock() defer f.mtx.RUnlock() @@ -49,10 +49,10 @@ func (f *FeeService) GetFeeRate(blocks int32) (float64, error) { } // get the block between 0 and SwapLockTime - b := math.Min(math.Max(0, float64(blocks)), SwapLockTime) + b := math.Min(math.Max(0, float64(blocks)), float64(locktime)) // certainty is linear between 0.5 and 1 based on the amount of blocks left - certainty := 0.5 + (((SwapLockTime - b) / SwapLockTime) / 2) + certainty := 0.5 + (((float64(locktime) - b) / float64(locktime)) / 2) // Get the row closest to the amount of blocks left rowIndex := 0 diff --git a/swapper/redeem.go b/swapper/redeem.go index eee5077..c16dbbb 100644 --- a/swapper/redeem.go +++ b/swapper/redeem.go @@ -158,7 +158,7 @@ func (r *Redeemer) checkRedeem(blockHeight int32, inProgressRedeem *InProgressRe if len(txns) == 0 { log.Printf("RedeemWithinBlocks - preimage: %x, blocksLeft: %v", preimage, blocksLeft) return nil - // _, err := r.RedeemWithinBlocks(preimage, blocksLeft) + // _, err := r.RedeemWithinBlocks(preimage, blocksLeft, inProgressRedeem.LockHeight) // return err } @@ -192,13 +192,13 @@ func (r *Redeemer) checkRedeem(blockHeight int32, inProgressRedeem *InProgressRe } } - satPerVbyte, err := r.feeService.GetFeeRate(blocksLeft) + satPerVbyte, err := r.feeService.GetFeeRate(blocksLeft, inProgressRedeem.LockHeight) if err != nil { log.Printf("failed to get redeem fee rate: %v", err) // If there is a problem getting the fees, try to bump the tx on best effort. log.Printf("RedeemWithinBlocks - preimage: %x, blocksLeft: %v", preimage, blocksLeft) return nil - // _, err = r.RedeemWithinBlocks(preimage, blocksLeft) + // _, err = r.RedeemWithinBlocks(preimage, blocksLeft, inProgressRedeem.LockHeight) // return err } @@ -231,8 +231,8 @@ func getWeight(tx *lnrpc.Transaction) (int64, error) { return weight, nil } -func (r *Redeemer) RedeemWithinBlocks(preimage []byte, blocks int32) (string, error) { - rate, err := r.feeService.GetFeeRate(blocks) +func (r *Redeemer) RedeemWithinBlocks(preimage []byte, blocks int32, locktime int32) (string, error) { + rate, err := r.feeService.GetFeeRate(blocks, locktime) if err != nil { log.Printf("RedeemWithinBlocks(%x, %d) - getFeeRate error: %v", preimage, blocks, err) } diff --git a/swapper/swapper.go b/swapper/swapper.go index c403511..a4d068c 100644 --- a/swapper/swapper.go +++ b/swapper/swapper.go @@ -325,7 +325,7 @@ func (s *Server) getSwapPayment(ctx context.Context, in *breez.GetSwapPaymentReq log.Printf("Failed to update subswap preimage '%x' for payment hash '%x', error: %v", sendResponse.PaymentPreimage, sendResponse.PaymentHash, err) } - _, err = s.redeemer.RedeemWithinBlocks(sendResponse.PaymentPreimage, int32(chainInfo.BlockHeight)-minHeight) + _, err = s.redeemer.RedeemWithinBlocks(sendResponse.PaymentPreimage, int32(chainInfo.BlockHeight)-minHeight, utxos.LockHeight) if err != nil { log.Printf("RedeemWithinBlocks - couldn't redeem transaction for preimage: %x, error: %v", sendResponse.PaymentPreimage, err) } From a137bb7f9311435d03607f096444c04dbf95d7b5 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 11 Mar 2024 14:40:06 +0100 Subject: [PATCH 4/6] precompute redeem fees based on current fee estimate --- server.go | 7 +++--- swapper/redeem.go | 61 ++++++++++++++++++++++++++++++++++++++++++++-- swapper/swapper.go | 21 ++++++++++------ 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/server.go b/server.go index acf1cd5..584cd24 100644 --- a/server.go +++ b/server.go @@ -437,7 +437,7 @@ func main() { defer cancel() swapFeeService := swapper.NewFeeService() swapFeeService.Start(ctx) - redeemer := swapper.NewRedeemer(ssClient, ssRouterClient, subswapClient, + redeemer := swapper.NewRedeemer(network, ssClient, ssRouterClient, subswapClient, swapFeeService, updateSubswapTxid, updateSubswapPreimage, getInProgressRedeems, setSubswapConfirmed) redeemer.Start(ctx) @@ -518,8 +518,9 @@ func main() { supportServer := support.NewServer(sendPaymentFailureNotification, breezStatus, lspList) breez.RegisterSupportServer(s, supportServer) - swapperServer = swapper.NewServer(network, redisPool, client, ssClient, subswapClient, redeemer, ssWalletKitClient, ssRouterClient, - insertSubswapPayment, updateSubswapPreimage, hasFilteredAddress) + swapperServer = swapper.NewServer(network, redisPool, client, ssClient, subswapClient, redeemer, + swapFeeService, ssWalletKitClient, ssRouterClient, insertSubswapPayment, + updateSubswapPreimage, hasFilteredAddress) breez.RegisterSwapperServer(s, swapperServer) lspServer := &lsp.Server{ diff --git a/swapper/redeem.go b/swapper/redeem.go index c16dbbb..fe7f66f 100644 --- a/swapper/redeem.go +++ b/swapper/redeem.go @@ -5,13 +5,18 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "fmt" "log" "os" "time" "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" @@ -19,8 +24,10 @@ import ( "google.golang.org/grpc/metadata" ) -const SwapLockTime = 288 -const MinConfirmations = 6 +const ( + MinConfirmations = 6 + RedeemWitnessInputSize int32 = 1 + 1 + 73 + 1 + 32 + 1 + 100 +) type InProgressRedeem struct { PaymentHash string @@ -32,6 +39,7 @@ type InProgressRedeem struct { } type Redeemer struct { + network *chaincfg.Params ssClient lnrpc.LightningClient ssRouterClient routerrpc.RouterClient subswapClient submarineswaprpc.SubmarineSwapperClient @@ -43,6 +51,7 @@ type Redeemer struct { } func NewRedeemer( + network *chaincfg.Params, ssClient lnrpc.LightningClient, ssRouterClient routerrpc.RouterClient, subswapClient submarineswaprpc.SubmarineSwapperClient, @@ -53,6 +62,7 @@ func NewRedeemer( setSubswapConfirmed func(paymentHash string) error, ) *Redeemer { return &Redeemer{ + network: network, ssClient: ssClient, ssRouterClient: ssRouterClient, subswapClient: subswapClient, @@ -270,3 +280,50 @@ func (r *Redeemer) doRedeem(preimage []byte, targetConf int32, satPerByte int64) return redeem.Txid, err } + +func (r *Redeemer) RedeemWeight(utxos []*submarineswaprpc.UnspentAmountResponse_Utxo) (int32, error) { + if len(utxos) == 0 { + return 0, errors.New("no utxo") + } + + redeemTx := wire.NewMsgTx(1) + + // Add the inputs without the witness and calculate the amount to redeem + var amount btcutil.Amount + for _, utxo := range utxos { + txid, err := chainhash.NewHashFromStr(utxo.Txid) + if err != nil { + return 0, fmt.Errorf("failed to parse txid: %w", err) + } + outpoint := &wire.OutPoint{ + Hash: *txid, + Index: utxo.Index, + } + amount += btcutil.Amount(utxo.Amount) + txIn := wire.NewTxIn(outpoint, nil, nil) + txIn.Sequence = 0 + redeemTx.AddTxIn(txIn) + } + + //Generate a random address + privateKey, err := btcec.NewPrivateKey() + if err != nil { + return 0, err + } + redeemAddress, err := btcutil.NewAddressPubKey(privateKey.PubKey().SerializeCompressed(), r.network) + if err != nil { + return 0, err + } + // Add the single output + redeemScript, err := txscript.PayToAddrScript(redeemAddress) + if err != nil { + return 0, err + } + txOut := wire.TxOut{PkScript: redeemScript} + redeemTx.AddTxOut(&txOut) + redeemTx.LockTime = uint32(833000) // fake block height + + // Calcluate the weight and the fee + weight := 4*int32(redeemTx.SerializeSizeStripped()) + RedeemWitnessInputSize*int32(len(redeemTx.TxIn)) + return weight, nil +} diff --git a/swapper/swapper.go b/swapper/swapper.go index a4d068c..a326b7b 100644 --- a/swapper/swapper.go +++ b/swapper/swapper.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "log" + "math" "os" "strconv" "strings" @@ -40,6 +41,7 @@ type Server struct { ssClient lnrpc.LightningClient subswapClient submarineswaprpc.SubmarineSwapperClient redeemer *Redeemer + feeService *FeeService walletKitClient walletrpc.WalletKitClient ssRouterClient routerrpc.RouterClient insertSubswapPayment func(paymentHash, paymentRequest string, lockheight, confirmationheight int32, utxos []string) error @@ -53,6 +55,7 @@ func NewServer( client, ssClient lnrpc.LightningClient, subswapClient submarineswaprpc.SubmarineSwapperClient, redeemer *Redeemer, + feeService *FeeService, walletKitClient walletrpc.WalletKitClient, ssRouterClient routerrpc.RouterClient, insertSubswapPayment func(paymentHash, paymentRequest string, lockheight, confirmationheight int32, utxos []string) error, @@ -66,6 +69,7 @@ func NewServer( ssClient: ssClient, subswapClient: subswapClient, redeemer: redeemer, + feeService: feeService, walletKitClient: walletKitClient, ssRouterClient: ssRouterClient, insertSubswapPayment: insertSubswapPayment, @@ -230,16 +234,19 @@ func (s *Server) getSwapPayment(ctx context.Context, in *breez.GetSwapPaymentReq return nil, status.Errorf(codes.Internal, "there are no UTXOs related to payment request") } - fees, err := s.subswapClient.SubSwapServiceRedeemFees(subswapClientCtx, &submarineswaprpc.SubSwapServiceRedeemFeesRequest{ - Hash: decodedPayReq.PaymentHash[:], - TargetConf: 30, - }) + satPerVbyte, err := s.feeService.GetFeeRate(3, utxos.LockHeight) + if err != nil { + log.Printf("GetSwapPayment - GetFeeRate error: %v", err) + return nil, status.Errorf(codes.Internal, "couldn't determine the redeem transaction fees") + } + weight, err := s.redeemer.RedeemWeight(utxos.Utxos) if err != nil { - log.Printf("GetSwapPayment - SubSwapServiceRedeemFees error: %v", err) + log.Printf("GetSwapPayment - RedeemWeight error: %v", err) return nil, status.Errorf(codes.Internal, "couldn't determine the redeem transaction fees") } - log.Printf("GetSwapPayment - SubSwapServiceRedeemFees: %v for amount in utxos: %v amount in payment request: %v", fees.Amount, utxos.Amount, decodedAmt) - if 2*utxos.Amount < 3*fees.Amount { + fees := int64(math.Ceil((satPerVbyte * float64(weight)) / 4)) + log.Printf("GetSwapPayment - SubSwapServiceRedeemFees: %v for amount in utxos: %v amount in payment request: %v", fees, utxos.Amount, decodedAmt) + if 2*utxos.Amount < 3*fees { log.Println("GetSwapPayment - utxo amount less than 1.5 fees. Cannot proceed") return &breez.GetSwapPaymentReply{ FundsExceededLimit: true, From 600a4750d206be94e58e724326f92bb2067a56a4 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 11 Mar 2024 14:45:19 +0100 Subject: [PATCH 5/6] use local fee estimate for minAllowedDeposit --- swapper/swapper.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/swapper/swapper.go b/swapper/swapper.go index a326b7b..5dc288c 100644 --- a/swapper/swapper.go +++ b/swapper/swapper.go @@ -115,14 +115,13 @@ func (s *Server) addFundInit(ctx context.Context, in *breez.AddFundInitRequest, } var minAllowedDeposit int64 - ct := 12 - fees, err := s.walletKitClient.EstimateFee(clientCtx, &walletrpc.EstimateFeeRequest{ConfTarget: int32(ct)}) + fees, err := s.feeService.GetFeeRate(3, 288) if err != nil { - log.Printf("walletKitClient.EstimateFee(%v) error: %v", ct, err) + log.Printf("feeService.GetFeeRate(3, 288) error: %v", err) } else { - log.Printf("walletKitClient.EstimateFee(%v): %v", ct, fees.SatPerKw) + log.Printf("feeService.GetFeeRate(3, 288): %v sat/vbyte", fees) // Assume a weight of 1K for the transaction. - minAllowedDeposit = fees.SatPerKw * 3 / 2 + minAllowedDeposit = int64(fees * 250 * 3 / 2) } address := subSwapServiceInitResponse.Address From 582237d2a3fc6a0744d97f570d534ec3bb472551 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 11 Mar 2024 16:00:33 +0100 Subject: [PATCH 6/6] watch unpersisted preimages --- bitcoind/bitcoind.go | 25 +++++++++ db.go | 34 ++++++++++++ server.go | 2 +- swapper/redeem.go | 122 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 164 insertions(+), 19 deletions(-) diff --git a/bitcoind/bitcoind.go b/bitcoind/bitcoind.go index dae3dee..bc3983d 100644 --- a/bitcoind/bitcoind.go +++ b/bitcoind/bitcoind.go @@ -5,6 +5,7 @@ import ( "log" "os" "strconv" + "time" gbitcoind "github.com/toorop/go-bitcoind" ) @@ -68,3 +69,27 @@ func GetTransaction(txid string) (*gbitcoind.RawTransaction, error) { return &rawtx, nil } + +func GetDateForHeight(height int32) (*time.Time, error) { + bitcoindPort, err := strconv.Atoi(os.Getenv("BITCOIND_PORT")) + if err != nil { + return nil, fmt.Errorf("no valid port for bitcoind: %v", os.Getenv("BITCOIND_PORT")) + } + bc, err := gbitcoind.New(os.Getenv("BITCOIND_HOST"), bitcoindPort, os.Getenv("BITCOIND_USER"), os.Getenv("BITCOIND_PASSWORD"), false) + if err != nil { + return nil, fmt.Errorf("cannot create a bitcoind client: %w", err) + } + + hash, err := bc.GetBlockHash(uint64(height)) + if err != nil { + return nil, fmt.Errorf("error getting block hash: %w", err) + } + + header, err := bc.GetBlockheader(hash) + if err != nil { + return nil, fmt.Errorf("error getting block header: %w", err) + } + + t := time.Unix(header.Time, 0) + return &t, nil +} diff --git a/db.go b/db.go index 142ad4c..e935264 100644 --- a/db.go +++ b/db.go @@ -67,6 +67,40 @@ func insertSubswapPayment(paymentHash, paymentRequest string, lockheight, confir return nil } +func getSwapsWithoutPreimage() ([]*swapper.SwapWithoutPreimage, error) { + rows, err := pgxPool.Query(context.Background(), + `SELECT payment_hash + FROM swap_payments + WHERE redeem_confirmed = false + AND payment_preimage is null + ORDER BY confirmation_height + `) + if err != nil { + return nil, fmt.Errorf("failed to query swap_payments: %w", err) + } + defer rows.Close() + + var result []*swapper.SwapWithoutPreimage + for rows.Next() { + var payment_hash string + var confirmation_height int32 + err = rows.Scan( + &payment_hash, + &confirmation_height, + ) + if err != nil { + return nil, fmt.Errorf("rows.Scan() error: %w", err) + } + + result = append(result, &swapper.SwapWithoutPreimage{ + PaymentHash: payment_hash, + ConfirmationHeight: confirmation_height, + }) + } + + return result, nil +} + func getInProgressRedeems(blockheight int32) ([]*swapper.InProgressRedeem, error) { ignoreBefore := blockheight - (288 * 14) rows, err := pgxPool.Query(context.Background(), diff --git a/server.go b/server.go index 584cd24..eee4a19 100644 --- a/server.go +++ b/server.go @@ -439,7 +439,7 @@ func main() { swapFeeService.Start(ctx) redeemer := swapper.NewRedeemer(network, ssClient, ssRouterClient, subswapClient, swapFeeService, updateSubswapTxid, updateSubswapPreimage, - getInProgressRedeems, setSubswapConfirmed) + getInProgressRedeems, getSwapsWithoutPreimage, setSubswapConfirmed) redeemer.Start(ctx) lsp.InitLSP() diff --git a/swapper/redeem.go b/swapper/redeem.go index fe7f66f..121d4c2 100644 --- a/swapper/redeem.go +++ b/swapper/redeem.go @@ -11,6 +11,7 @@ import ( "os" "time" + "github.com/breez/server/bitcoind" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" @@ -29,6 +30,11 @@ const ( RedeemWitnessInputSize int32 = 1 + 1 + 73 + 1 + 32 + 1 + 100 ) +type SwapWithoutPreimage struct { + PaymentHash string + ConfirmationHeight int32 +} + type InProgressRedeem struct { PaymentHash string Preimage *string @@ -39,15 +45,16 @@ type InProgressRedeem struct { } type Redeemer struct { - network *chaincfg.Params - ssClient lnrpc.LightningClient - ssRouterClient routerrpc.RouterClient - subswapClient submarineswaprpc.SubmarineSwapperClient - feeService *FeeService - updateSubswapTxid func(paymentHash, txid string) error - updateSubswapPreimage func(paymentHash, paymentPreimage string) error - getInProgressRedeems func(blockheight int32) ([]*InProgressRedeem, error) - setSubswapConfirmed func(paymentHash string) error + network *chaincfg.Params + ssClient lnrpc.LightningClient + ssRouterClient routerrpc.RouterClient + subswapClient submarineswaprpc.SubmarineSwapperClient + feeService *FeeService + updateSubswapTxid func(paymentHash, txid string) error + updateSubswapPreimage func(paymentHash, paymentPreimage string) error + getInProgressRedeems func(blockheight int32) ([]*InProgressRedeem, error) + getSwapsWithoutPreimage func() ([]*SwapWithoutPreimage, error) + setSubswapConfirmed func(paymentHash string) error } func NewRedeemer( @@ -59,24 +66,103 @@ func NewRedeemer( updateSubswapTxid func(paymentHash, txid string) error, updateSubswapPreimage func(paymentHash, paymentPreimage string) error, getInProgressRedeems func(blockheight int32) ([]*InProgressRedeem, error), + getSwapsWithoutPreimage func() ([]*SwapWithoutPreimage, error), setSubswapConfirmed func(paymentHash string) error, ) *Redeemer { return &Redeemer{ - network: network, - ssClient: ssClient, - ssRouterClient: ssRouterClient, - subswapClient: subswapClient, - feeService: feeService, - updateSubswapTxid: updateSubswapTxid, - updateSubswapPreimage: updateSubswapPreimage, - getInProgressRedeems: getInProgressRedeems, - setSubswapConfirmed: setSubswapConfirmed, + network: network, + ssClient: ssClient, + ssRouterClient: ssRouterClient, + subswapClient: subswapClient, + feeService: feeService, + updateSubswapTxid: updateSubswapTxid, + updateSubswapPreimage: updateSubswapPreimage, + getInProgressRedeems: getInProgressRedeems, + getSwapsWithoutPreimage: getSwapsWithoutPreimage, + setSubswapConfirmed: setSubswapConfirmed, } } func (r *Redeemer) Start(ctx context.Context) { log.Printf("REDEEM - before r.watchRedeemTxns()") go r.watchRedeemTxns(ctx) + go r.watchPreimages(ctx) +} + +func (r *Redeemer) watchPreimages(ctx context.Context) { + for { + log.Printf("REDEEM - before checkPreimages()") + r.checkPreimages() + + select { + case <-time.After(time.Minute * 30): + case <-ctx.Done(): + return + } + } +} + +func (r *Redeemer) checkPreimages() { + swaps, err := r.getSwapsWithoutPreimage() + if err != nil { + log.Printf("checkPreimages - Failed to getSwapsWithoutPreimage: %v", err) + return + } + + if len(swaps) == 0 { + return + } + + earliestHeight := swaps[0].ConfirmationHeight + t, err := bitcoind.GetDateForHeight(earliestHeight) + if err != nil { + log.Printf("checkPreimages - Failed to date for height %d: %v", earliestHeight, err) + return + } + + // substract a day for leeway + time := t.Add(-time.Hour * 24) + log.Printf("checkPreimages - getting payments after %s", time.String()) + subswapClientCtx := metadata.AppendToOutgoingContext(context.Background(), "macaroon", os.Getenv("SUBSWAPPER_LND_MACAROON_HEX")) + payments, err := r.ssClient.ListPayments(subswapClientCtx, &lnrpc.ListPaymentsRequest{ + CreationDateStart: uint64(time.Unix()), + }) + if err != nil { + log.Printf("checkPreimages - Failed to ListPayments: %v", err) + return + } + log.Printf("checkPreimages - getting payments after %s yielded %d payments", time.String(), len(payments.Payments)) + + swapLookup := make(map[string]bool, 0) + for _, swap := range swaps { + swapLookup[swap.PaymentHash] = true + } + + newPreimages := make(map[string]string, 0) + for _, payment := range payments.Payments { + if payment.PaymentPreimage == "" { + continue + } + + _, ok := swapLookup[payment.PaymentHash] + if !ok { + continue + } + + newPreimages[payment.PaymentHash] = payment.PaymentPreimage + } + + if len(newPreimages) == 0 { + log.Printf("checkPreimages - no new preimages") + return + } + + for paymentHash, preimage := range newPreimages { + err = r.updateSubswapPreimage(paymentHash, preimage) + if err != nil { + log.Printf("checkPreimages - failed to update preimage %s for payment hash %s", preimage, paymentHash) + } + } } func (r *Redeemer) watchRedeemTxns(ctx context.Context) {