Skip to content

Commit

Permalink
Merge pull request #304 from ElementsProject/ct-discount
Browse files Browse the repository at this point in the history
liquid:  ct discount
  • Loading branch information
grubles authored Dec 3, 2024
2 parents 8ee6259 + f3c339c commit 5e40d7d
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 72 deletions.
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
# blockstream-electrs: init at 0.4.1 #299761
# https://github.com/NixOS/nixpkgs/pull/299761/commits/680d27ad847801af781e0a99e4b87ed73965c69a
nixpkgs2.url = "github:NixOS/nixpkgs/680d27ad847801af781e0a99e4b87ed73965c69a";
# lwk: init at wasm_0.6.3 #14bac28
# https://github.com/Blockstream/lwk/releases/tag/wasm_0.6.3
# lwk: init at 9ddd20a806625bb40cd063ad61d80d106809a9fd
# https://github.com/Blockstream/lwk/commit/9ddd20a806625bb40cd063ad61d80d106809a9fd
lwk-flake = {
url = "github:blockstream/lwk/14bac284fe712dd6fdbbbe82bda179a2a236b2fa";
url = "github:blockstream/lwk/9ddd20a806625bb40cd063ad61d80d106809a9fd";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
Expand Down
5 changes: 3 additions & 2 deletions lwk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ type unvalidatedAddressee struct {
type sendRequest struct {
Addressees []*unvalidatedAddressee `json:"addressees"`
// Optional fee rate in sat/vb
FeeRate *float64 `json:"fee_rate,omitempty"`
WalletName string `json:"name"`
FeeRate *float64 `json:"fee_rate,omitempty"`
WalletName string `json:"name"`
EnableCtDiscount bool `json:"enable_ct_discount"`
}

type sendResponse struct {
Expand Down
37 changes: 37 additions & 0 deletions lwk/error.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions lwk/error_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
16 changes: 13 additions & 3 deletions lwk/lwkwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
// Set up here because ctx is not inherited throughout the current codebase.
defaultContextTimeout = time.Second * 20
minimumFee SatPerVByte = 0.1
supportedCLIVersion = "0.5.1"
supportedCLIVersion = "0.8.0"
)

func SatPerVByteFromFeeBTCPerKb(feeBTCPerKb float64) SatPerVByte {
Expand Down Expand Up @@ -161,15 +161,17 @@ func (r *LWKRpcWallet) CreateAndBroadcastTransaction(swapParams *swap.OpeningPar
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
defer cancel()
feerate := r.getFeeSatPerVByte(ctx).getValue() * kb
// todo: There will be an option in the tx builder to enable the discount.
fundedTx, err := r.lwkClient.send(ctx, &sendRequest{
Addressees: []*unvalidatedAddressee{
{
Address: swapParams.OpeningAddress,
Satoshi: swapParams.Amount,
},
},
WalletName: r.c.GetWalletName(),
FeeRate: &feerate,
WalletName: r.c.GetWalletName(),
FeeRate: &feerate,
EnableCtDiscount: true,
})
if err != nil {
return "", "", 0, fmt.Errorf("failed to fund transaction: %w", err)
Expand Down Expand Up @@ -232,6 +234,7 @@ func (r *LWKRpcWallet) SendToAddress(address string, amount Satoshi) (string, er
Satoshi: amount,
},
},
EnableCtDiscount: true,
})
if err != nil {
return "", err
Expand Down Expand Up @@ -259,6 +262,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
Expand Down
100 changes: 76 additions & 24 deletions onchain/liquid.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,26 @@ 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)))
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
}
Expand Down Expand Up @@ -118,12 +130,21 @@ 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)))
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
}
Expand All @@ -140,11 +161,16 @@ 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(transactionKindCoop)))
if err != nil {
return "", "", "", err
log.Infof("error getting fee %v", err)
fee = feeAmountPlaceholder
}
refundFee, err := l.liquidWallet.GetFee(int64(l.getCoopClaimTxSize()))
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
}
Expand All @@ -156,7 +182,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
}
Expand Down Expand Up @@ -235,6 +261,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)
Expand Down Expand Up @@ -263,16 +292,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},
Expand Down Expand Up @@ -366,7 +386,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)
Expand All @@ -378,12 +398,44 @@ 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
// getEstimatedTxSize estimates the size of a transaction based on its kind and whether it's using Confidential Transactions (CT).
func getEstimatedTxSize(t transactionKind) int {
txsize := 0
switch t {
case transactionKindPreimageSpending:
// Preimage spending transactions have an estimated size of 1350 bytes.
txsize = 1350
case transactionKindCoop:
// Cooperative close transactions have an estimated size of 1360 bytes.
txsize = 1360
case transactionKindOpening:
// Opening transactions have a variable size, estimated in the constant EstimatedOpeningConfidentialTxSizeBytes.
txsize = EstimatedOpeningConfidentialTxSizeBytes
case transactionKindCSV:
// CSV transactions have an estimated size of 1350 bytes.
txsize = 1350
default:
// For unknown transaction types, assume a default size of 1360 bytes.
return 1360
}
// the transaction size is reduced by 75% for ct discount.
// TODO:
// This is a placeholder value, the actual discount should
// be calculated based on discount vsize.
// To do this, we would need to construct the transaction, decode it, and then get the discounted vsize.
// However, this would have a significant impact on the codebase.
// As a temporary measure, we're taking a conservative approach and applying a 75% discount.
// For the discount calculation, refer to https://github.com/ElementsProject/ELIPs/blob/main/elip-0200.mediawiki.
return txsize / 4
}

func (l *LiquidOnChain) TxIdFromHex(txHex string) (string, error) {
Expand Down Expand Up @@ -529,12 +581,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)))
}

// 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)))
}

func (l *LiquidOnChain) GetAsset() string {
Expand Down
2 changes: 1 addition & 1 deletion test/lwk_cln_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func Test_ClnCln_LWK_SwapIn(t *testing.T) {
bitcoind, liquidd, lightningds, scid, electrs, lwk := clnclnLWKSetup(t, uint64(math.Pow10(9)))
defer func() {
if t.Failed() {
filter := os.Getenv("PEERSWAP_TEST_FILTER")
filter := ""
pprintFail(
tailableProcess{
p: bitcoind.DaemonProcess,
Expand Down
2 changes: 1 addition & 1 deletion test/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/require"
)

const defaultLines = 30
const defaultLines = 1000

func IsIntegrationTest(t *testing.T) {
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
Expand Down
Loading

0 comments on commit 5e40d7d

Please sign in to comment.