mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loopdb, staticaddr: harden SQL txid loading
Use strict txid parsing when reading swap updates and static address loop-ins from SQL rows. Empty swap update txids still mean "absent", but any non-empty truncated value now fails loudly. Add store-level tests that inject truncated txids through the query layer and assert that the fetch paths reject them.
This commit is contained in:
parent
99d87422de
commit
7afb925d25
4 changed files with 221 additions and 7 deletions
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/version"
|
||||
"github.com/lightninglabs/loop/utils/chainhashutil"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
|
|
@ -498,13 +500,20 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
|
|||
}
|
||||
|
||||
var htlcTimeoutSweepTxHash *chainhash.Hash
|
||||
if swap.HtlcTimeoutSweepTxID.Valid {
|
||||
htlcTimeoutSweepTxHash, err = chainhash.NewHashFromStr(
|
||||
// Loop never writes empty timeout sweep txids, but tolerate them on read
|
||||
// so a malformed row does not prevent swap recovery.
|
||||
if swap.HtlcTimeoutSweepTxID.Valid &&
|
||||
swap.HtlcTimeoutSweepTxID.String != "" {
|
||||
|
||||
hash, err := chainhashutil.NewHashFromStrExact(
|
||||
swap.HtlcTimeoutSweepTxID.String,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("invalid htlc timeout sweep txid %q: %w",
|
||||
swap.HtlcTimeoutSweepTxID.String, err)
|
||||
}
|
||||
|
||||
htlcTimeoutSweepTxHash = &hash
|
||||
}
|
||||
|
||||
depositOutpoints := strings.Split(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package loopin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
|
|
@ -271,3 +273,150 @@ func TestCreateLoopIn(t *testing.T) {
|
|||
require.Equal(t, d2.Value, swap.Deposits[1].Value)
|
||||
require.Equal(t, deposit.LoopingIn, swap.Deposits[1].GetState())
|
||||
}
|
||||
|
||||
// TestGetLoopInByHashRejectsTruncatedTimeoutSweepTxID verifies that a
|
||||
// persisted short timeout sweep txid is rejected during swap loading.
|
||||
func TestGetLoopInByHashRejectsTruncatedTimeoutSweepTxID(t *testing.T) {
|
||||
ctxb := t.Context()
|
||||
testDb := loopdb.NewTestDB(t)
|
||||
testClock := clock.NewTestClock(time.Now())
|
||||
defer testDb.Close()
|
||||
|
||||
depositStore := deposit.NewSqlStore(testDb.BaseDB)
|
||||
swapStore := NewSqlStore(
|
||||
loopdb.NewTypedStore[Querier](testDb), testClock,
|
||||
&chaincfg.RegressionNetParams,
|
||||
)
|
||||
|
||||
depositID, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
depositRecord := &deposit.Deposit{
|
||||
ID: depositID,
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
|
||||
Index: 0,
|
||||
},
|
||||
Value: btcutil.Amount(100_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
|
||||
},
|
||||
}
|
||||
|
||||
err = depositStore.CreateDeposit(ctxb, depositRecord)
|
||||
require.NoError(t, err)
|
||||
|
||||
depositRecord.SetState(deposit.LoopingIn)
|
||||
err = depositStore.UpdateDeposit(ctxb, depositRecord)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, clientPubKey := test.CreateKey(1)
|
||||
_, serverPubKey := test.CreateKey(2)
|
||||
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
|
||||
loopIn := StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
|
||||
DepositOutpoints: []string{
|
||||
depositRecord.OutPoint.String(),
|
||||
},
|
||||
Deposits: []*deposit.Deposit{depositRecord},
|
||||
ClientPubkey: clientPubKey,
|
||||
ServerPubkey: serverPubKey,
|
||||
HtlcTimeoutSweepAddress: addr,
|
||||
}
|
||||
loopIn.SetState(SignHtlcTx)
|
||||
|
||||
err = swapStore.CreateLoopIn(ctxb, &loopIn)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testDb.Queries.UpdateStaticAddressLoopIn(
|
||||
ctxb, sqlc.UpdateStaticAddressLoopInParams{
|
||||
SwapHash: swapHash[:],
|
||||
HtlcTimeoutSweepTxID: sql.NullString{
|
||||
String: "abcd",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = swapStore.GetLoopInByHash(ctxb, swapHash)
|
||||
require.ErrorContains(t, err, "invalid htlc timeout sweep txid")
|
||||
}
|
||||
|
||||
// TestGetLoopInByHashAllowsEmptyTimeoutSweepTxID verifies that empty persisted
|
||||
// timeout sweep txids are tolerated for recovery robustness.
|
||||
func TestGetLoopInByHashAllowsEmptyTimeoutSweepTxID(t *testing.T) {
|
||||
ctxb := t.Context()
|
||||
testDb := loopdb.NewTestDB(t)
|
||||
testClock := clock.NewTestClock(time.Now())
|
||||
defer testDb.Close()
|
||||
|
||||
depositStore := deposit.NewSqlStore(testDb.BaseDB)
|
||||
swapStore := NewSqlStore(
|
||||
loopdb.NewTypedStore[Querier](testDb), testClock,
|
||||
&chaincfg.RegressionNetParams,
|
||||
)
|
||||
|
||||
depositID, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
depositRecord := &deposit.Deposit{
|
||||
ID: depositID,
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
|
||||
Index: 0,
|
||||
},
|
||||
Value: btcutil.Amount(100_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
|
||||
},
|
||||
}
|
||||
|
||||
err = depositStore.CreateDeposit(ctxb, depositRecord)
|
||||
require.NoError(t, err)
|
||||
|
||||
depositRecord.SetState(deposit.LoopingIn)
|
||||
err = depositStore.UpdateDeposit(ctxb, depositRecord)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, clientPubKey := test.CreateKey(1)
|
||||
_, serverPubKey := test.CreateKey(2)
|
||||
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
|
||||
loopIn := StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
|
||||
DepositOutpoints: []string{
|
||||
depositRecord.OutPoint.String(),
|
||||
},
|
||||
Deposits: []*deposit.Deposit{depositRecord},
|
||||
ClientPubkey: clientPubKey,
|
||||
ServerPubkey: serverPubKey,
|
||||
HtlcTimeoutSweepAddress: addr,
|
||||
}
|
||||
loopIn.SetState(SignHtlcTx)
|
||||
|
||||
err = swapStore.CreateLoopIn(ctxb, &loopIn)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testDb.Queries.UpdateStaticAddressLoopIn(
|
||||
ctxb, sqlc.UpdateStaticAddressLoopInParams{
|
||||
SwapHash: swapHash[:],
|
||||
HtlcTimeoutSweepTxID: sql.NullString{
|
||||
String: "",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
swap, err := swapStore.GetLoopInByHash(ctxb, swapHash)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, swap.HtlcTimeoutSweepTxHash)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue