From 3aa552d790ff75e24d5409a235b45cc99c4eece5 Mon Sep 17 00:00:00 2001 From: bruwbird Date: Tue, 20 Aug 2024 08:40:41 +0000 Subject: [PATCH] liquid: drop the flat fee for mempool policy patch elements releases have the mempool policy patch. To apply ct discount on peerswap, we uses An "Ask forgiveness not permission" approach, where we attempt to broadcast with the discounted fee, and if that fails, we retry with the non-discounted fee. The discount is achieved by calculating the fee based on a discounted vsize (equivalent to 1/4 of the original tx size). --- go.mod | 2 +- go.sum | 2 + lwk/error.go | 37 +++++++++++ lwk/error_test.go | 54 ++++++++++++++++ lwk/lwkwallet.go | 8 +++ onchain/liquid.go | 123 +++++++++++++++++++++++++++++------- wallet/elementsrpcwallet.go | 80 +++++++++++++---------- wallet/wallet.go | 3 +- 8 files changed, 251 insertions(+), 58 deletions(-) create mode 100644 lwk/error.go create mode 100644 lwk/error_test.go diff --git a/go.mod b/go.mod index 90540f96..b343ab69 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.2 github.com/btcsuite/btcd/btcutil/psbt v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 - github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a + github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 github.com/jessevdk/go-flags v1.5.0 diff --git a/go.sum b/go.sum index f8289c28..71f3c6e2 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 h1:sE4tvxWw01v7K3M github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a h1:xnVQmVqGmSs3m8zPQF4iYEYiUAmJx8MlT9vJ3lAaOjc= github.com/elementsproject/glightning v0.0.0-20240224063423-55240d61b52a/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= +github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4 h1:7CXEOi0uTeMrwLfFmHsbBS5yRfpSAHALwa9k9Rtl1Vw= +github.com/elementsproject/glightning v0.0.0-20240802020216-b4e19b004ca4/go.mod h1:YAdIeSyx8VEhDCtEaGOJLmWNpPaQ3x4vYSAj9Vrppdo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/lwk/error.go b/lwk/error.go new file mode 100644 index 00000000..f947a1d8 --- /dev/null +++ b/lwk/error.go @@ -0,0 +1,37 @@ +package lwk + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" +) + +// electrumRPCError represents the structure of an RPC error response +type electrumRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Regular expression to match RPC error messages with any prefix +var re = regexp.MustCompile(`^(.*) RPC error: (.*)$`) + +// parseRPCError parses an error and extracts the RPC error code and message if present +func parseRPCError(err error) (*electrumRPCError, error) { + var rpcErr electrumRPCError + errStr := err.Error() + + matches := re.FindStringSubmatch(errStr) + + if len(matches) == 3 { // Prefix and JSON payload extracted successfully + errJSON := matches[2] + if jerr := json.Unmarshal([]byte(errJSON), &rpcErr); jerr != nil { + return nil, fmt.Errorf("error parsing rpc error: %v", jerr) + } + } else { + // If no RPC error pattern is found, return the original error + return nil, errors.New(errStr) + } + + return &rpcErr, nil +} diff --git a/lwk/error_test.go b/lwk/error_test.go new file mode 100644 index 00000000..ad0fcef7 --- /dev/null +++ b/lwk/error_test.go @@ -0,0 +1,54 @@ +package lwk + +import ( + "errors" + "testing" +) + +func TestParseRPCError(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + err error + expectedCode int + expectedMsg string + wantErr bool + }{ + "Valid RPC error": { + err: errors.New("sendrawtransaction RPC error: {\"code\":-26,\"message\":\"min relay fee not met\"}"), + expectedCode: -26, + expectedMsg: "min relay fee not met", + wantErr: false, + }, + "Invalid JSON payload": { + + err: errors.New("RPC error: {invalid json}"), + expectedCode: 0, + expectedMsg: "", + wantErr: true, + }, + "No RPC error pattern": { + err: errors.New("Some other error"), + expectedCode: 0, + expectedMsg: "", + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + rpcErr, err := parseRPCError(tc.err) + if (err != nil) != tc.wantErr { + t.Errorf("wantErr: %v, got error: %v", tc.wantErr, err) + } + if err == nil { + if rpcErr.Code != tc.expectedCode { + t.Errorf("expected code: %d, got: %d", tc.expectedCode, rpcErr.Code) + } + if rpcErr.Message != tc.expectedMsg { + t.Errorf("expected message: %s, got: %s", tc.expectedMsg, rpcErr.Message) + } + } + }) + } +} diff --git a/lwk/lwkwallet.go b/lwk/lwkwallet.go index 00a9a6cc..419645fd 100644 --- a/lwk/lwkwallet.go +++ b/lwk/lwkwallet.go @@ -3,6 +3,7 @@ package lwk import ( "context" "errors" + "fmt" "math" "strings" @@ -258,6 +259,13 @@ func (r *LWKRpcWallet) SendRawTx(txHex string) (string, error) { defer cancel() res, err := r.electrumClient.BroadcastTransaction(ctx, txHex) if err != nil { + rpcErr, pErr := parseRPCError(err) + if pErr != nil { + return "", fmt.Errorf("error parsing rpc error: %v", pErr) + } + if rpcErr.Code == -26 { + return "", wallet.MinRelayFeeNotMetError + } return "", err } return res, nil diff --git a/onchain/liquid.go b/onchain/liquid.go index c7ae7de5..17e60160 100644 --- a/onchain/liquid.go +++ b/onchain/liquid.go @@ -81,14 +81,38 @@ func (l *LiquidOnChain) CreateOpeningTransaction(swapParams *swap.OpeningParams) return txHex, blindedScriptAddr, txId, fee, vout, nil } +// feeAmountPlaceholder is a placeholder for the fee amount +const feeAmountPlaceholder = uint64(500) + func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (string, string, string, error) { + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, true))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createPreimageSpendingTransaction(swapParams, claimParams, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { + return "", "", "", err + } + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createPreimageSpendingTransaction(swapParams, claimParams, fee) +} + +func (l *LiquidOnChain) createPreimageSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, fee uint64) (string, string, string, error) { newAddr, err := l.liquidWallet.GetAddress() if err != nil { return "", "", "", err } l.AddBlindingRandomFactors(claimParams) - tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, 0, 0) + tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, 0, fee) if err != nil { return "", "", "", err } @@ -118,12 +142,33 @@ func (l *LiquidOnChain) CreatePreimageSpendingTransaction(swapParams *swap.Openi } func (l *LiquidOnChain) CreateCsvSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams) (txId, txHex, address string, error error) { + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, true))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createCsvSpendingTransaction(swapParams, claimParams, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { + return "", "", "", err + } + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createCsvSpendingTransaction(swapParams, claimParams, fee) +} + +func (l *LiquidOnChain) createCsvSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, fee uint64) (txId, txHex, address string, error error) { newAddr, err := l.liquidWallet.GetAddress() if err != nil { return "", "", "", err } l.AddBlindingRandomFactors(claimParams) - tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, LiquidCsv, 0) + tx, sigBytes, redeemScript, err := l.prepareSpendingTransaction(swapParams, claimParams, newAddr, LiquidCsv, fee) if err != nil { return "", "", "", err } @@ -140,11 +185,28 @@ func (l *LiquidOnChain) CreateCsvSpendingTransaction(swapParams *swap.OpeningPar } func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, takerSigner swap.Signer) (txId, txHex, address string, error error) { - refundAddr, err := l.NewAddress() + fee, err := l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, true))) if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + txId, txHex, newAddr, err := l.createCoopSpendingTransaction(swapParams, claimParams, takerSigner, fee) + if err == nil { + return txId, txHex, newAddr, nil + } + if !errors.Is(err, wallet.MinRelayFeeNotMetError) { return "", "", "", err } - refundFee, err := l.liquidWallet.GetFee(int64(l.getCoopClaimTxSize())) + fee, err = l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindPreimageSpending, false))) + if err != nil { + log.Infof("error getting fee %v", err) + fee = feeAmountPlaceholder + } + return l.createCoopSpendingTransaction(swapParams, claimParams, takerSigner, fee) +} + +func (l *LiquidOnChain) createCoopSpendingTransaction(swapParams *swap.OpeningParams, claimParams *swap.ClaimParams, takerSigner swap.Signer, fee uint64) (txId, txHex, address string, error error) { + refundAddr, err := l.NewAddress() if err != nil { return "", "", "", err } @@ -156,7 +218,7 @@ func (l *LiquidOnChain) CreateCoopSpendingTransaction(swapParams *swap.OpeningPa if err != nil { return "", "", "", err } - spendingTx, sigHash, err := l.createSpendingTransaction(claimParams.OpeningTxHex, swapParams.Amount, 0, l.asset, redeemScript, refundAddr, refundFee, swapParams.BlindingKey, claimParams.EphemeralKey, claimParams.OutputAssetBlindingFactor, claimParams.BlindingSeed) + spendingTx, sigHash, err := l.createSpendingTransaction(claimParams.OpeningTxHex, swapParams.Amount, 0, l.asset, redeemScript, refundAddr, fee, swapParams.BlindingKey, claimParams.EphemeralKey, claimParams.OutputAssetBlindingFactor, claimParams.BlindingSeed) if err != nil { return "", "", "", err } @@ -235,6 +297,9 @@ func (l *LiquidOnChain) prepareSpendingTransaction(swapParams *swap.OpeningParam // CreateSpendingTransaction returns the spendningTransaction for the swap func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmount uint64, csv uint32, asset, redeemScript []byte, redeemAddr string, preparedFee uint64, blindingKey, ephemeralPrivKey *btcec.PrivateKey, outputAbf, seed []byte) (tx *transaction.Transaction, sigHash [32]byte, err error) { + if preparedFee == 0 { + return nil, [32]byte{}, errors.New("fee must be set other than 0") + } firstTx, err := transaction.NewTxFromHex(openingTxHex) if err != nil { log.Infof("error creating first tx %s, %v", openingTxHex, err) @@ -263,16 +328,7 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun return nil, [32]byte{}, errors.New(fmt.Sprintf("Tx value is not equal to the swap contract expected: %v, tx: %v", swapAmount, ubRes.Value)) } - feeAmountPlaceholder := uint64(500) - fee := preparedFee - if preparedFee == 0 { - fee, err = l.liquidWallet.GetFee(int64(l.getClaimTxSize())) - if err != nil { - fee = feeAmountPlaceholder - } - } - - outputValue := ubRes.Value - fee + outputValue := ubRes.Value - preparedFee finalVbfArgs := confidential.FinalValueBlindingFactorArgs{ InValues: []uint64{ubRes.Value}, @@ -366,7 +422,7 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun spendingTx.Outputs = append(spendingTx.Outputs, receiverOutput) // add feeoutput - feeValue, _ := elementsutil.ValueToBytes(fee) + feeValue, _ := elementsutil.ValueToBytes(preparedFee) feeScript := []byte{} feeOutput := transaction.NewTxOutput(asset, feeValue, feeScript) spendingTx.Outputs = append(spendingTx.Outputs, feeOutput) @@ -378,12 +434,33 @@ func (l *LiquidOnChain) createSpendingTransaction(openingTxHex string, swapAmoun return spendingTx, sigHash, nil } -func (l *LiquidOnChain) getClaimTxSize() int { - return 1350 -} +type transactionKind string + +const ( + transactionKindPreimageSpending transactionKind = "preimage" + transactionKindCoop transactionKind = "coop" + transactionKindOpening transactionKind = "open" + transactionKindCSV transactionKind = "csv" +) -func (l *LiquidOnChain) getCoopClaimTxSize() int { - return 1360 +func getEstimatedTxSize(t transactionKind, ctDiscount bool) int { + txsize := 0 + switch t { + case transactionKindPreimageSpending: + txsize = 1350 + case transactionKindCoop: + txsize = 1360 + case transactionKindOpening: + txsize = EstimatedOpeningConfidentialTxSizeBytes + case transactionKindCSV: + txsize = 1350 + default: + return 1360 + } + if ctDiscount { + return txsize / 4 + } + return txsize } func (l *LiquidOnChain) TxIdFromHex(txHex string) (string, error) { @@ -529,12 +606,12 @@ func (l *LiquidOnChain) Blech32ToScript(blech32Addr string) ([]byte, error) { } func (l *LiquidOnChain) GetRefundFee() (uint64, error) { - return l.liquidWallet.GetFee(int64(l.getClaimTxSize())) + return l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindCoop, false))) } // GetFlatOpeningTXFee returns an estimate of the fee for the opening transaction. func (l *LiquidOnChain) GetFlatOpeningTXFee() (uint64, error) { - return l.liquidWallet.GetFee(EstimatedOpeningConfidentialTxSizeBytes) + return l.liquidWallet.GetFee(int64(getEstimatedTxSize(transactionKindOpening, true))) } func (l *LiquidOnChain) GetAsset() string { diff --git a/wallet/elementsrpcwallet.go b/wallet/elementsrpcwallet.go index 624d6313..a4476fbb 100644 --- a/wallet/elementsrpcwallet.go +++ b/wallet/elementsrpcwallet.go @@ -3,11 +3,12 @@ package wallet import ( "errors" "fmt" - "math" + "strings" "github.com/elementsproject/glightning/gelements" + "github.com/elementsproject/glightning/jrpc2" "github.com/elementsproject/peerswap/log" "github.com/elementsproject/peerswap/swap" "github.com/vulpemventures/go-elements/address" @@ -20,6 +21,11 @@ var ( AlreadyLoadedError = errors.New("wallet is already loaded") ) +const ( + // https://github.com/ElementsProject/elements/releases/tag/elements-23.2.2 + elementsdFeeDiscountedVersion = 230202 +) + type RpcClient interface { GetNewAddress(addrType int) (string, error) SendToAddress(address string, amount string) (string, error) @@ -35,6 +41,7 @@ type RpcClient interface { EstimateFee(blocks uint32, mode string) (*gelements.FeeResponse, error) SetLabel(address, label string) error Ping() (bool, error) + GetNetworkInfo() (*gelements.NetworkInfo, error) } // ElementsRpcWallet uses the elementsd rpc wallet @@ -92,8 +99,12 @@ func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.Openi if err != nil { return "", "", 0, err } + feerate, err := r.getFeeRate() + if err != nil { + return "", "", 0, err + } fundedTx, err := r.rpcClient.FundRawWithOptions(txHex, &gelements.FundRawOptions{ - FeeRate: fmt.Sprintf("%f", r.getFeeRate()), + FeeRate: fmt.Sprintf("%f", feerate), }, nil) if err != nil { @@ -110,27 +121,6 @@ func (r *ElementsRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.Openi return txid, finalized, gelements.ConvertBtc(fundedTx.Fee), nil } -const ( - // minFeeRateBTCPerKb defines the minimum fee rate in BTC/kB. - // This value is equivalent to 0.1 sat/byte. - minFeeRateBTCPerKb = 0.000001 -) - -// getFeeRate retrieves the optimal fee rate based on the current Liquid network conditions. -// Returns the recommended fee rate in BTC/kB -func (r *ElementsRpcWallet) getFeeRate() float64 { - feeRes, err := r.rpcClient.EstimateFee(LiquidTargetBlocks, "ECONOMICAL") - if err != nil || len(feeRes.Errors) > 0 { - log.Debugf("Error estimating fee: %v", err) - if len(feeRes.Errors) > 0 { - log.Debugf(" Errors encountered during fee estimation process: %v", feeRes.Errors) - } - // Return the minimum fee rate in case of an error - return minFeeRateBTCPerKb - } - return math.Max(feeRes.FeeRate, minFeeRateBTCPerKb) -} - // setupWallet checks if the swap wallet is already loaded in elementsd, if not it loads/creates it func (r *ElementsRpcWallet) setupWallet() error { loadedWallets, err := r.rpcClient.ListWallets() @@ -188,25 +178,49 @@ func (r *ElementsRpcWallet) SendToAddress(address string, amount uint64) (string } func (r *ElementsRpcWallet) SendRawTx(txHex string) (string, error) { - return r.rpcClient.SendRawTx(txHex) + raw, err := r.rpcClient.SendRawTx(txHex) + if err != nil { + errWithCode, ok := err.(*jrpc2.RpcError) + if ok && errWithCode.Code == -26 { + return "", MinRelayFeeNotMetError + } + } + return raw, err } -func (r *ElementsRpcWallet) GetFee(txSize int64) (uint64, error) { +const ( + // minFeeRateBTCPerKb defines the minimum fee rate in BTC/kB. + // This value is equivalent to 0.1 sat/byte. + minFeeRateBTCPerKb = 0.000001 +) + +// getFeeRate retrieves the optimal fee rate based on the current Liquid network conditions. +// Returns the recommended fee rate in BTC/kB +func (r *ElementsRpcWallet) getFeeRate() (float64, error) { feeRes, err := r.rpcClient.EstimateFee(LiquidTargetBlocks, "ECONOMICAL") if err != nil { return 0, err } - satPerByte := float64(feeRes.SatPerKb()) / float64(1000) - if satPerByte < 0.1 { - satPerByte = 0.1 - } if len(feeRes.Errors) > 0 { - //todo sane default sat per byte - satPerByte = 0.1 + log.Debugf(" Errors encountered during fee estimation process: %v", feeRes.Errors) + return minFeeRateBTCPerKb, nil } - // assume largest witness - fee := satPerByte * float64(txSize) + return math.Max(minFeeRateBTCPerKb, feeRes.FeeRate), nil +} + +const ( + // 1 kb = 1000 bytes + kb = 1000 + btcToSatoshiExp = 8 +) +func (r *ElementsRpcWallet) GetFee(txSize int64) (uint64, error) { + feeRate, err := r.getFeeRate() + if err != nil { + return 0, fmt.Errorf("error getting fee rate: %v", err) + } + satPerByte := feeRate * math.Pow10(btcToSatoshiExp) / kb + fee := satPerByte * float64(txSize) return uint64(fee), nil } diff --git a/wallet/wallet.go b/wallet/wallet.go index 9c2ed177..bf9bfa4c 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -7,7 +7,8 @@ import ( ) var ( - NotEnoughBalanceError = errors.New("Not enough balance on utxos") + NotEnoughBalanceError = errors.New("Not enough balance on utxos") + MinRelayFeeNotMetError = errors.New("MinRelayFee not met") ) const (