Skip to content
Draft
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
30 changes: 22 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
"github.com/lightninglabs/loop/utils/chainhashutil"
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes"
Expand Down Expand Up @@ -272,14 +273,9 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
}

if len(cfg.SkippedTxns) != 0 {
skippedTxns := make(map[chainhash.Hash]struct{})
for _, txid := range cfg.SkippedTxns {
txid, err := chainhash.NewHashFromStr(txid)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse "+
"txid to skip %v: %w", txid, err)
}
skippedTxns[*txid] = struct{}{}
skippedTxns, err := parseSkippedTxns(cfg.SkippedTxns)
if err != nil {
return nil, nil, err
}
batcherOpts = append(batcherOpts, sweepbatcher.WithSkippedTxns(
skippedTxns,
Expand Down Expand Up @@ -323,6 +319,24 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
return client, cleanup, nil
}

// parseSkippedTxns parses the configured skipped transaction IDs and rejects
// any txid that is not fully specified.
func parseSkippedTxns(txids []string) (map[chainhash.Hash]struct{}, error) {
skippedTxns := make(map[chainhash.Hash]struct{}, len(txids))

for _, txid := range txids {
hash, err := chainhashutil.NewHashFromStrExact(txid)
if err != nil {
return nil, fmt.Errorf("failed to parse txid to skip %v: %w",
txid, err)
}

skippedTxns[hash] = struct{}{}
}

return skippedTxns, nil
}

// GetConn returns the gRPC connection to the server.
func (s *Client) GetConn() *grpc.ClientConn {
return s.clientConfig.Conn
Expand Down
45 changes: 45 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
"errors"
"strings"
"testing"

"github.com/btcsuite/btcd/btcutil"
Expand Down Expand Up @@ -46,6 +47,50 @@ var (
defaultConfirmations = int32(loopdb.DefaultLoopOutHtlcConfirmations)
)

// TestParseSkippedTxns verifies that skipped txids must be fully specified.
func TestParseSkippedTxns(t *testing.T) {
t.Parallel()

validTxid := strings.Repeat("01", 32)
validHash, err := chainhash.NewHashFromStr(validTxid)
require.NoError(t, err)

tests := []struct {
name string
txids []string
expected map[chainhash.Hash]struct{}
expectedErr string
}{
{
name: "valid",
txids: []string{validTxid},
expected: map[chainhash.Hash]struct{}{
*validHash: {},
},
},
{
name: "short",
txids: []string{"abcd"},
expectedErr: "failed to parse txid to skip abcd",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

skippedTxns, err := parseSkippedTxns(test.txids)
if test.expectedErr != "" {
require.ErrorContains(t, err, test.expectedErr)
return
}

require.NoError(t, err)
require.Equal(t, test.expected, skippedTxns)
})
}
}

var htlcKeys = func() loopdb.HtlcKeys {
var senderKey, receiverKey [33]byte

Expand Down
13 changes: 10 additions & 3 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1836,8 +1836,10 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
}, nil
}

// ListStaticAddressWithdrawals returns a list of all finalized withdrawal
// transactions.
// ListStaticAddressWithdrawals returns a list of all static address
// withdrawals, including pending withdrawals. Pending withdrawals expose
// default empty or zero values for fields that are only known after
// confirmation.
func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
_ *looprpc.ListStaticAddressWithdrawalRequest) (
*looprpc.ListStaticAddressWithdrawalResponse, error) {
Expand All @@ -1855,6 +1857,11 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
)
for _, w := range withdrawals {
txID := ""
if w.TxID != nil {
txID = w.TxID.String()
}

deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
for _, d := range w.Deposits {
deposits = append(deposits, &looprpc.Deposit{
Expand All @@ -1868,7 +1875,7 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
})
}
withdrawal := &looprpc.StaticAddressWithdrawal{
TxId: w.TxID.String(),
TxId: txID,
Deposits: deposits,
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
Expand Down
12 changes: 8 additions & 4 deletions loopdb/sql_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/utils/chainhashutil"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
Expand Down Expand Up @@ -754,12 +755,15 @@ func getSwapEvents(updates []sqlc.SwapUpdate) ([]*LoopEvent, error) {
}

if updates[i].HtlcTxhash != "" {
chainHash, err := chainhash.NewHashFromStr(updates[i].HtlcTxhash)
chainHash, err := chainhashutil.NewHashFromStrExact(
updates[i].HtlcTxhash,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("invalid htlc tx hash "+
"%q: %w", updates[i].HtlcTxhash, err)
}

events[i].HtlcTxHash = chainHash
events[i].HtlcTxHash = &chainHash
}
}

Expand Down
52 changes: 52 additions & 0 deletions loopdb/sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,58 @@ func TestBatchUpdateCost(t *testing.T) {
require.Equal(t, updateMap[hash2], swapsMap[hash2].State().Cost)
}

// TestSqliteRejectsTruncatedHtlcTxHash verifies that a persisted short HTLC
// txid is rejected instead of being accepted as a padded hash.
func TestSqliteRejectsTruncatedHtlcTxHash(t *testing.T) {
store := NewTestDB(t)

destAddr := test.GetDestAddr(t, 0)
pendingSwap := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
Preimage: testPreimage,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: testTime,
ProtocolVersion: ProtocolVersionMuSig2,
},
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
SweepConfTarget: 2,
HtlcConfirmations: 2,
}

ctxb := t.Context()
hash := pendingSwap.Preimage.Hash()

err := store.CreateLoopOut(ctxb, hash, &pendingSwap)
require.NoError(t, err)

err = store.Queries.InsertSwapUpdate(ctxb, sqlc.InsertSwapUpdateParams{
SwapHash: hash[:],
UpdateTimestamp: testTime,
UpdateState: int32(StatePreimageRevealed),
HtlcTxhash: "abcd",
})
require.NoError(t, err)

_, err = store.FetchLoopOutSwap(ctxb, hash)
require.ErrorContains(t, err, "invalid htlc tx hash")
}

// TestMigrationTracker tests the migration tracker functionality.
func TestMigrationTracker(t *testing.T) {
ctxb := context.Background()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
-- withdrawals stores finalized static address withdrawals.
-- withdrawals stores pending and finalized static address withdrawals.
CREATE TABLE IF NOT EXISTS withdrawals (
-- id is the auto-incrementing primary key for a withdrawal.
id INTEGER PRIMARY KEY,

-- withdrawal_id is the unique identifier for the withdrawal.
withdrawal_id BLOB NOT NULL UNIQUE,

-- withdrawal_tx_id is the transaction tx id of the withdrawal.
-- withdrawal_tx_id is the confirmed transaction txid of the withdrawal.
-- It remains NULL while the withdrawal is still pending.
withdrawal_tx_id TEXT UNIQUE,

-- total_deposit_amount is the total amount of the deposits in satoshis.
Expand Down
11 changes: 7 additions & 4 deletions looprpc/client.pb.go

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

14 changes: 9 additions & 5 deletions looprpc/client.proto
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ service SwapClient {
returns (ListStaticAddressDepositsResponse);

/* loop:`listwithdrawals`
ListStaticAddressWithdrawals returns a list of static address withdrawals.
ListStaticAddressWithdrawals returns a list of static address withdrawals,
including pending withdrawals that have not yet been confirmed.
*/
rpc ListStaticAddressWithdrawals (ListStaticAddressWithdrawalRequest)
returns (ListStaticAddressWithdrawalResponse);
Expand Down Expand Up @@ -2047,7 +2048,8 @@ message Deposit {

message StaticAddressWithdrawal {
/*
The transaction id of the withdrawal transaction.
The transaction id of the withdrawal transaction. It is empty until the
confirmed transaction is persisted.
*/
string tx_id = 1;

Expand All @@ -2064,17 +2066,19 @@ message StaticAddressWithdrawal {
/*
The actual amount that was withdrawn from the selected deposits. This value
represents the sum of selected deposit values minus tx fees minus optional
change output.
change output. It is zero until the confirmed transaction is persisted.
*/
int64 withdrawn_amount_satoshis = 4;

/*
An optional change.
An optional change. It is zero until the confirmed transaction is
persisted.
*/
int64 change_amount_satoshis = 5;

/*
The confirmation block height of the withdrawal transaction.
The confirmation block height of the withdrawal transaction. It is zero
until the withdrawal is confirmed.
*/
uint32 confirmation_height = 6;
}
Expand Down
10 changes: 5 additions & 5 deletions looprpc/client.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@
},
"/v1/staticaddr/withdrawals": {
"get": {
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals.",
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals,\nincluding pending withdrawals that have not yet been confirmed.",
"operationId": "SwapClient_ListStaticAddressWithdrawals",
"responses": {
"200": {
Expand Down Expand Up @@ -2872,7 +2872,7 @@
"properties": {
"tx_id": {
"type": "string",
"description": "The transaction id of the withdrawal transaction."
"description": "The transaction id of the withdrawal transaction. It is empty until the\nconfirmed transaction is persisted."
},
"deposits": {
"type": "array",
Expand All @@ -2890,17 +2890,17 @@
"withdrawn_amount_satoshis": {
"type": "string",
"format": "int64",
"description": "The actual amount that was withdrawn from the selected deposits. This value\nrepresents the sum of selected deposit values minus tx fees minus optional\nchange output."
"description": "The actual amount that was withdrawn from the selected deposits. This value\nrepresents the sum of selected deposit values minus tx fees minus optional\nchange output. It is zero until the confirmed transaction is persisted."
},
"change_amount_satoshis": {
"type": "string",
"format": "int64",
"description": "An optional change."
"description": "An optional change. It is zero until the confirmed transaction is\npersisted."
},
"confirmation_height": {
"type": "integer",
"format": "int64",
"description": "The confirmation block height of the withdrawal transaction."
"description": "The confirmation block height of the withdrawal transaction. It is zero\nuntil the withdrawal is confirmed."
}
}
},
Expand Down
6 changes: 4 additions & 2 deletions looprpc/client_grpc.pb.go

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

Loading
Loading