Skip to content

Commit 4539224

Browse files
committed
tapdb: filter orphan utxos with missing signing information
Added filtering to exclude orphan UTXOs with missing signing information (KeyFamily=0 and KeyIndex=0). These UTXOs were created in prior versions that didn't store this information, causing LND to fail when signing.
1 parent fd7d3e5 commit 4539224

2 files changed

Lines changed: 216 additions & 0 deletions

File tree

tapdb/assets_store.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ type (
188188
QueryBurnsFilters = sqlc.QueryBurnsParams
189189
)
190190

191+
const (
192+
// MaxOrphanUTXOs is the maximum number of orphan UTXOs that can be
193+
// fetched at once. This limit prevents transactions from becoming too
194+
// large when sweeping orphan UTXOs.
195+
MaxOrphanUTXOs = 20
196+
)
197+
191198
// ActiveAssetsStore is a sub-set of the main sqlc.Querier interface that
192199
// contains methods related to querying the set of confirmed assets.
193200
type ActiveAssetsStore interface {
@@ -1340,6 +1347,12 @@ func (a *AssetStore) FetchOrphanUTXOs(ctx context.Context) (
13401347
}
13411348

13421349
for _, u := range utxos {
1350+
if len(results) >= MaxOrphanUTXOs {
1351+
log.DebugS(ctx, "reached max orphan UTXOs "+
1352+
"limit", "limit", MaxOrphanUTXOs)
1353+
break
1354+
}
1355+
13431356
if len(u.LeaseOwner) > 0 &&
13441357
u.LeaseExpiry.Valid &&
13451358
u.LeaseExpiry.Time.UTC().After(now) {
@@ -1351,6 +1364,17 @@ func (a *AssetStore) FetchOrphanUTXOs(ctx context.Context) (
13511364
continue
13521365
}
13531366

1367+
// Skip UTXOs with missing signing information. In prior
1368+
// versions, we didn't store KeyFamily and KeyIndex for
1369+
// orphan UTXOs. If both are 0, LND will fail to sign
1370+
// the transaction.
1371+
if u.KeyFamily == 0 && u.KeyIndex == 0 {
1372+
log.DebugS(ctx, "skipping orphan utxo with "+
1373+
"missing signing info",
1374+
"raw_key", fmt.Sprintf("%x", u.RawKey))
1375+
continue
1376+
}
1377+
13541378
var anchorPoint wire.OutPoint
13551379
err := readOutPoint(
13561380
bytes.NewReader(u.Outpoint), 0, 0, &anchorPoint,

tapdb/assets_store_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,3 +3383,195 @@ func TestUpsertAssetsWithSplitCommitments(t *testing.T) {
33833383
})
33843384
}
33853385
}
3386+
3387+
// TestFetchOrphanUTXOs tests that FetchOrphanUTXOs:
3388+
// 1. Filters out UTXOs with missing signing info (KeyFamily=0 AND KeyIndex=0)
3389+
// 2. Respects the MaxOrphanUTXOs limit.
3390+
func TestFetchOrphanUTXOs(t *testing.T) {
3391+
t.Parallel()
3392+
3393+
testCases := []struct {
3394+
name string
3395+
utxosToInsert [][2]int32
3396+
expectedCount int
3397+
checkFunc func(t *testing.T,
3398+
orphans []*tapfreighter.ZeroValueInput)
3399+
}{
3400+
{
3401+
name: "filters missing signing info",
3402+
utxosToInsert: [][2]int32{
3403+
{212, 5}, // Valid signing info.
3404+
{0, 0}, // Missing signing info.
3405+
},
3406+
expectedCount: 1,
3407+
checkFunc: func(t *testing.T,
3408+
orphans []*tapfreighter.ZeroValueInput) {
3409+
3410+
require.Equal(
3411+
t, keychain.KeyFamily(212),
3412+
orphans[0].InternalKey.Family,
3413+
)
3414+
require.Equal(
3415+
t, uint32(5),
3416+
orphans[0].InternalKey.Index,
3417+
)
3418+
},
3419+
},
3420+
{
3421+
name: "respects max limit",
3422+
// Insert MaxOrphanUTXOs + 10 UTXOs with valid signing
3423+
// info.
3424+
utxosToInsert: func() [][2]int32 {
3425+
utxos := make([][2]int32, MaxOrphanUTXOs+10)
3426+
for i := range utxos {
3427+
utxos[i] = [2]int32{
3428+
int32(i + 1), int32(i + 1),
3429+
}
3430+
}
3431+
return utxos
3432+
}(),
3433+
expectedCount: MaxOrphanUTXOs,
3434+
checkFunc: func(t *testing.T,
3435+
orphans []*tapfreighter.ZeroValueInput) {
3436+
3437+
// Verify all returned UTXOs have valid signing
3438+
// info.
3439+
for _, orphan := range orphans {
3440+
require.NotZero(
3441+
t, orphan.InternalKey.Family,
3442+
)
3443+
require.NotZero(
3444+
t, orphan.InternalKey.Index,
3445+
)
3446+
}
3447+
},
3448+
},
3449+
}
3450+
3451+
for _, tc := range testCases {
3452+
t.Run(tc.name, func(t *testing.T) {
3453+
_, assetsStore, db := newAssetStore(t)
3454+
ctx := context.Background()
3455+
3456+
// Insert all UTXOs for this test case.
3457+
for _, keyInfo := range tc.utxosToInsert {
3458+
insertOrphanUTXO(
3459+
t, ctx, db, assetsStore,
3460+
keyInfo[0], keyInfo[1],
3461+
)
3462+
}
3463+
3464+
// Fetch orphan UTXOs.
3465+
orphans, err := assetsStore.FetchOrphanUTXOs(ctx)
3466+
require.NoError(t, err)
3467+
3468+
require.Len(t, orphans, tc.expectedCount)
3469+
3470+
if tc.checkFunc != nil {
3471+
tc.checkFunc(t, orphans)
3472+
}
3473+
})
3474+
}
3475+
}
3476+
3477+
// insertOrphanUTXO inserts a managed UTXO with a tombstone asset and the
3478+
// specified KeyFamily and KeyIndex values for the internal key.
3479+
func insertOrphanUTXO(t *testing.T, ctx context.Context, db sqlc.Querier,
3480+
assetsStore *AssetStore, keyFamily, keyIndex int32) wire.OutPoint {
3481+
3482+
internalKey := test.RandPubKey(t)
3483+
3484+
// Insert the internal key with specific KeyFamily and KeyIndex.
3485+
_, err := db.UpsertInternalKey(ctx, sqlc.UpsertInternalKeyParams{
3486+
RawKey: internalKey.SerializeCompressed(),
3487+
KeyFamily: keyFamily,
3488+
KeyIndex: keyIndex,
3489+
})
3490+
require.NoError(t, err)
3491+
3492+
// Create a chain transaction.
3493+
anchorTx := wire.NewMsgTx(2)
3494+
anchorTx.AddTxIn(&wire.TxIn{
3495+
PreviousOutPoint: test.RandOp(t),
3496+
})
3497+
anchorTx.AddTxOut(&wire.TxOut{
3498+
PkScript: bytes.Repeat([]byte{0x01}, 34),
3499+
Value: 1000,
3500+
})
3501+
3502+
txBytes, err := fn.Serialize(anchorTx)
3503+
require.NoError(t, err)
3504+
3505+
txHash := anchorTx.TxHash()
3506+
chainTxID, err := db.UpsertChainTx(ctx, sqlc.UpsertChainTxParams{
3507+
Txid: txHash[:],
3508+
RawTx: txBytes,
3509+
BlockHeight: sqlInt32(100),
3510+
})
3511+
require.NoError(t, err)
3512+
3513+
outpoint := wire.OutPoint{
3514+
Hash: txHash,
3515+
Index: 0,
3516+
}
3517+
outpointBytes, err := encodeOutpoint(outpoint)
3518+
require.NoError(t, err)
3519+
3520+
// Insert the managed UTXO.
3521+
_, err = db.UpsertManagedUTXO(ctx, sqlc.UpsertManagedUTXOParams{
3522+
RawKey: internalKey.SerializeCompressed(),
3523+
Outpoint: outpointBytes,
3524+
AmtSats: 1000,
3525+
TaprootAssetRoot: test.RandBytes(32),
3526+
MerkleRoot: test.RandBytes(32),
3527+
TxnID: chainTxID,
3528+
})
3529+
require.NoError(t, err)
3530+
3531+
// Create and insert a tombstone asset for this UTXO.
3532+
gen := asset.RandGenesis(t, asset.Normal)
3533+
gen.FirstPrevOut = outpoint
3534+
tombstone := asset.NewAssetNoErr(
3535+
t, gen, 0, 0, 0, asset.NUMSScriptKey, nil,
3536+
)
3537+
3538+
assetCommitment, err := commitment.NewAssetCommitment(tombstone)
3539+
require.NoError(t, err)
3540+
3541+
tapCommitment, err := commitment.NewTapCommitment(nil, assetCommitment)
3542+
require.NoError(t, err)
3543+
3544+
txMerkleProof, err := proof.NewTxMerkleProof([]*wire.MsgTx{anchorTx}, 0)
3545+
require.NoError(t, err)
3546+
3547+
assetProof := proof.Proof{
3548+
AnchorTx: *anchorTx,
3549+
BlockHeight: 100,
3550+
TxMerkleProof: *txMerkleProof,
3551+
Asset: *tombstone,
3552+
InclusionProof: proof.TaprootProof{
3553+
OutputIndex: 0,
3554+
InternalKey: internalKey,
3555+
},
3556+
}
3557+
3558+
proofBlob, err := proof.EncodeAsProofFile(&assetProof)
3559+
require.NoError(t, err)
3560+
3561+
err = assetsStore.ImportProofs(
3562+
ctx, proof.MockVerifierCtx, false,
3563+
&proof.AnnotatedProof{
3564+
AssetSnapshot: &proof.AssetSnapshot{
3565+
AnchorTx: anchorTx,
3566+
InternalKey: internalKey,
3567+
Asset: tombstone,
3568+
ScriptRoot: tapCommitment,
3569+
AnchorBlockHeight: 100,
3570+
},
3571+
Blob: proofBlob,
3572+
},
3573+
)
3574+
require.NoError(t, err)
3575+
3576+
return outpoint
3577+
}

0 commit comments

Comments
 (0)