Skip to content

Commit a0e0cbe

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 a0e0cbe

2 files changed

Lines changed: 310 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: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,3 +3383,289 @@ func TestUpsertAssetsWithSplitCommitments(t *testing.T) {
33833383
})
33843384
}
33853385
}
3386+
3387+
// TestFetchOrphanUTXOs tests that FetchOrphanUTXOs correctly:
3388+
// 1. Filters out UTXOs with missing signing info (KeyFamily=0 AND KeyIndex=0)
3389+
// 2. Limits the number of returned UTXOs to MaxOrphanUTXOs
3390+
func TestFetchOrphanUTXOs(t *testing.T) {
3391+
t.Parallel()
3392+
3393+
_, assetsStore, db := newAssetStore(t)
3394+
ctx := context.Background()
3395+
3396+
// Helper to create a tombstone asset (zero value with NUMS key).
3397+
createTombstoneAsset := func(gen asset.Genesis) *asset.Asset {
3398+
return asset.NewAssetNoErr(
3399+
t, gen, 0, 0, 0, asset.NUMSScriptKey, nil,
3400+
)
3401+
}
3402+
3403+
// Helper to insert a managed UTXO with an internal key that has
3404+
// specific KeyFamily and KeyIndex values.
3405+
insertOrphanUTXO := func(
3406+
keyFamily, keyIndex int32,
3407+
) (wire.OutPoint, *btcec.PublicKey) {
3408+
3409+
internalKey := test.RandPubKey(t)
3410+
3411+
// Insert the internal key with specific KeyFamily and KeyIndex.
3412+
_, err := db.UpsertInternalKey(ctx, sqlc.UpsertInternalKeyParams{
3413+
RawKey: internalKey.SerializeCompressed(),
3414+
KeyFamily: keyFamily,
3415+
KeyIndex: keyIndex,
3416+
})
3417+
require.NoError(t, err)
3418+
3419+
// Create a chain transaction.
3420+
anchorTx := wire.NewMsgTx(2)
3421+
anchorTx.AddTxIn(&wire.TxIn{
3422+
PreviousOutPoint: test.RandOp(t),
3423+
})
3424+
anchorTx.AddTxOut(&wire.TxOut{
3425+
PkScript: bytes.Repeat([]byte{0x01}, 34),
3426+
Value: 1000,
3427+
})
3428+
3429+
txBytes, err := fn.Serialize(anchorTx)
3430+
require.NoError(t, err)
3431+
3432+
txHash := anchorTx.TxHash()
3433+
chainTxID, err := db.UpsertChainTx(ctx, sqlc.UpsertChainTxParams{
3434+
Txid: txHash[:],
3435+
RawTx: txBytes,
3436+
BlockHeight: sql.NullInt32{Int32: 100, Valid: true},
3437+
})
3438+
require.NoError(t, err)
3439+
3440+
outpoint := wire.OutPoint{
3441+
Hash: txHash,
3442+
Index: 0,
3443+
}
3444+
outpointBytes, err := encodeOutpoint(outpoint)
3445+
require.NoError(t, err)
3446+
3447+
// Insert the managed UTXO.
3448+
_, err = db.UpsertManagedUTXO(ctx, sqlc.UpsertManagedUTXOParams{
3449+
RawKey: internalKey.SerializeCompressed(),
3450+
Outpoint: outpointBytes,
3451+
AmtSats: 1000,
3452+
TaprootAssetRoot: test.RandBytes(32),
3453+
MerkleRoot: test.RandBytes(32),
3454+
TxnID: chainTxID,
3455+
})
3456+
require.NoError(t, err)
3457+
3458+
// Create and insert a tombstone asset for this UTXO.
3459+
gen := asset.RandGenesis(t, asset.Normal)
3460+
gen.FirstPrevOut = outpoint
3461+
tombstone := createTombstoneAsset(gen)
3462+
3463+
assetCommitment, err := commitment.NewAssetCommitment(tombstone)
3464+
require.NoError(t, err)
3465+
3466+
tapCommitment, err := commitment.NewTapCommitment(
3467+
nil, assetCommitment,
3468+
)
3469+
require.NoError(t, err)
3470+
3471+
txMerkleProof, err := proof.NewTxMerkleProof(
3472+
[]*wire.MsgTx{anchorTx}, 0,
3473+
)
3474+
require.NoError(t, err)
3475+
3476+
assetProof := proof.Proof{
3477+
AnchorTx: *anchorTx,
3478+
BlockHeight: 100,
3479+
TxMerkleProof: *txMerkleProof,
3480+
Asset: *tombstone,
3481+
InclusionProof: proof.TaprootProof{
3482+
OutputIndex: 0,
3483+
InternalKey: internalKey,
3484+
},
3485+
}
3486+
3487+
proofBlob, err := proof.EncodeAsProofFile(&assetProof)
3488+
require.NoError(t, err)
3489+
3490+
err = assetsStore.ImportProofs(
3491+
ctx, proof.MockVerifierCtx, false,
3492+
&proof.AnnotatedProof{
3493+
AssetSnapshot: &proof.AssetSnapshot{
3494+
AnchorTx: anchorTx,
3495+
InternalKey: internalKey,
3496+
Asset: tombstone,
3497+
ScriptRoot: tapCommitment,
3498+
AnchorBlockHeight: 100,
3499+
},
3500+
Blob: proofBlob,
3501+
},
3502+
)
3503+
require.NoError(t, err)
3504+
3505+
return outpoint, internalKey
3506+
}
3507+
3508+
// Test 1: Filter out UTXOs with missing signing info.
3509+
t.Run("filters missing signing info", func(t *testing.T) {
3510+
// Insert a UTXO with valid signing info.
3511+
validOutpoint, _ := insertOrphanUTXO(212, 5)
3512+
3513+
// Insert a UTXO with missing signing info (KeyFamily=0,
3514+
// KeyIndex=0).
3515+
_, _ = insertOrphanUTXO(0, 0)
3516+
3517+
// Fetch orphan UTXOs - should only return the valid one.
3518+
orphans, err := assetsStore.FetchOrphanUTXOs(ctx)
3519+
require.NoError(t, err)
3520+
3521+
// Should only have 1 orphan (the one with valid signing info).
3522+
require.Len(t, orphans, 1)
3523+
require.Equal(t, validOutpoint, orphans[0].OutPoint)
3524+
3525+
// Verify the signing info is correct.
3526+
require.Equal(
3527+
t, keychain.KeyFamily(212),
3528+
orphans[0].InternalKey.Family,
3529+
)
3530+
require.Equal(t, uint32(5), orphans[0].InternalKey.Index)
3531+
})
3532+
}
3533+
3534+
// TestFetchOrphanUTXOsLimit tests that FetchOrphanUTXOs respects the
3535+
// MaxOrphanUTXOs limit.
3536+
func TestFetchOrphanUTXOsLimit(t *testing.T) {
3537+
t.Parallel()
3538+
3539+
_, assetsStore, db := newAssetStore(t)
3540+
ctx := context.Background()
3541+
3542+
// Helper to create a tombstone asset (zero value with NUMS key).
3543+
createTombstoneAsset := func(gen asset.Genesis) *asset.Asset {
3544+
return asset.NewAssetNoErr(
3545+
t, gen, 0, 0, 0, asset.NUMSScriptKey, nil,
3546+
)
3547+
}
3548+
3549+
// Helper to insert a managed UTXO with valid signing info.
3550+
insertOrphanUTXO := func(keyFamily, keyIndex int32) wire.OutPoint {
3551+
internalKey := test.RandPubKey(t)
3552+
3553+
// Insert the internal key with valid KeyFamily and KeyIndex.
3554+
_, err := db.UpsertInternalKey(ctx, sqlc.UpsertInternalKeyParams{
3555+
RawKey: internalKey.SerializeCompressed(),
3556+
KeyFamily: keyFamily,
3557+
KeyIndex: keyIndex,
3558+
})
3559+
require.NoError(t, err)
3560+
3561+
// Create a chain transaction.
3562+
anchorTx := wire.NewMsgTx(2)
3563+
anchorTx.AddTxIn(&wire.TxIn{
3564+
PreviousOutPoint: test.RandOp(t),
3565+
})
3566+
anchorTx.AddTxOut(&wire.TxOut{
3567+
PkScript: bytes.Repeat([]byte{0x01}, 34),
3568+
Value: 1000,
3569+
})
3570+
3571+
txBytes, err := fn.Serialize(anchorTx)
3572+
require.NoError(t, err)
3573+
3574+
txHash := anchorTx.TxHash()
3575+
chainTxID, err := db.UpsertChainTx(ctx, sqlc.UpsertChainTxParams{
3576+
Txid: txHash[:],
3577+
RawTx: txBytes,
3578+
BlockHeight: sql.NullInt32{Int32: 100, Valid: true},
3579+
})
3580+
require.NoError(t, err)
3581+
3582+
outpoint := wire.OutPoint{
3583+
Hash: txHash,
3584+
Index: 0,
3585+
}
3586+
outpointBytes, err := encodeOutpoint(outpoint)
3587+
require.NoError(t, err)
3588+
3589+
// Insert the managed UTXO.
3590+
_, err = db.UpsertManagedUTXO(ctx, sqlc.UpsertManagedUTXOParams{
3591+
RawKey: internalKey.SerializeCompressed(),
3592+
Outpoint: outpointBytes,
3593+
AmtSats: 1000,
3594+
TaprootAssetRoot: test.RandBytes(32),
3595+
MerkleRoot: test.RandBytes(32),
3596+
TxnID: chainTxID,
3597+
})
3598+
require.NoError(t, err)
3599+
3600+
// Create and insert a tombstone asset for this UTXO.
3601+
gen := asset.RandGenesis(t, asset.Normal)
3602+
gen.FirstPrevOut = outpoint
3603+
tombstone := createTombstoneAsset(gen)
3604+
3605+
assetCommitment, err := commitment.NewAssetCommitment(tombstone)
3606+
require.NoError(t, err)
3607+
3608+
tapCommitment, err := commitment.NewTapCommitment(
3609+
nil, assetCommitment,
3610+
)
3611+
require.NoError(t, err)
3612+
3613+
txMerkleProof, err := proof.NewTxMerkleProof(
3614+
[]*wire.MsgTx{anchorTx}, 0,
3615+
)
3616+
require.NoError(t, err)
3617+
3618+
assetProof := proof.Proof{
3619+
AnchorTx: *anchorTx,
3620+
BlockHeight: 100,
3621+
TxMerkleProof: *txMerkleProof,
3622+
Asset: *tombstone,
3623+
InclusionProof: proof.TaprootProof{
3624+
OutputIndex: 0,
3625+
InternalKey: internalKey,
3626+
},
3627+
}
3628+
3629+
proofBlob, err := proof.EncodeAsProofFile(&assetProof)
3630+
require.NoError(t, err)
3631+
3632+
err = assetsStore.ImportProofs(
3633+
ctx, proof.MockVerifierCtx, false,
3634+
&proof.AnnotatedProof{
3635+
AssetSnapshot: &proof.AssetSnapshot{
3636+
AnchorTx: anchorTx,
3637+
InternalKey: internalKey,
3638+
Asset: tombstone,
3639+
ScriptRoot: tapCommitment,
3640+
AnchorBlockHeight: 100,
3641+
},
3642+
Blob: proofBlob,
3643+
},
3644+
)
3645+
require.NoError(t, err)
3646+
3647+
return outpoint
3648+
}
3649+
3650+
// Insert more than MaxOrphanUTXOs orphan UTXOs.
3651+
numToInsert := MaxOrphanUTXOs + 10
3652+
for i := 0; i < numToInsert; i++ {
3653+
// Use varying KeyFamily and KeyIndex values (all valid, i.e.,
3654+
// not both 0).
3655+
insertOrphanUTXO(int32(i+1), int32(i+1))
3656+
}
3657+
3658+
// Fetch orphan UTXOs - should be limited to MaxOrphanUTXOs.
3659+
orphans, err := assetsStore.FetchOrphanUTXOs(ctx)
3660+
require.NoError(t, err)
3661+
3662+
// Should be capped at MaxOrphanUTXOs.
3663+
require.Len(t, orphans, MaxOrphanUTXOs)
3664+
3665+
// Verify all returned UTXOs have valid signing info.
3666+
for _, orphan := range orphans {
3667+
// Neither should be 0 (our test inserts with i+1 values).
3668+
require.NotZero(t, orphan.InternalKey.Family)
3669+
require.NotZero(t, orphan.InternalKey.Index)
3670+
}
3671+
}

0 commit comments

Comments
 (0)