@@ -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