@@ -13,6 +13,7 @@ import (
1313 "github.com/btcsuite/btcd/btcutil/hdkeychain"
1414 "github.com/btcsuite/btcd/btcutil/psbt"
1515 "github.com/btcsuite/btcd/txscript"
16+ "github.com/btcsuite/btcd/wire"
1617 "github.com/btcsuite/btcwallet/waddrmgr"
1718 "github.com/davecgh/go-spew/spew"
1819 "github.com/lightninglabs/taproot-assets/address"
@@ -2511,6 +2512,175 @@ func testPsbtTrustlessSwap(t *harnessTest) {
25112512 require .Equal (t .t , bobScriptKeyBytes , bobAssets .Assets [0 ].ScriptKey )
25122513}
25132514
2515+ // testPsbtSTXOExclusionProofs tests that we can properly send normal assets
2516+ // back and forth, using partial amounts, between nodes with the use of PSBTs,
2517+ // and that we see the expected STXO exclusion proofs.
2518+ func testPsbtSTXOExclusionProofs (t * harnessTest ) {
2519+ // First, we'll make a normal asset with a bunch of units that we are
2520+ // going to send backand forth. We're also minting a passive asset that
2521+ // should remain where it is.
2522+ rpcAssets := MintAssetsConfirmBatch (
2523+ t .t , t .lndHarness .Miner ().Client , t .tapd ,
2524+ []* mintrpc.MintAssetRequest {
2525+ simpleAssets [0 ],
2526+ // Our "passive" asset.
2527+ {
2528+ Asset : & mintrpc.MintAsset {
2529+ AssetType : taprpc .AssetType_NORMAL ,
2530+ Name : "itestbuxx-passive" ,
2531+ AssetMeta : & taprpc.AssetMeta {
2532+ Data : []byte ("some metadata" ),
2533+ },
2534+ Amount : 123 ,
2535+ },
2536+ },
2537+ },
2538+ )
2539+
2540+ ctxb := context .Background ()
2541+ ctxt , cancel := context .WithTimeout (ctxb , defaultWaitTimeout )
2542+ defer cancel ()
2543+
2544+ mintedAsset := rpcAssets [0 ]
2545+ genInfo := rpcAssets [0 ].AssetGenesis
2546+ var assetId asset.ID
2547+ copy (assetId [:], genInfo .AssetId )
2548+
2549+ // Now that we have the asset created, we'll make a new node that'll
2550+ // serve as the node which'll receive the assets.
2551+ bobLnd := t .lndHarness .NewNodeWithCoins ("Bob" , nil )
2552+ bob := setupTapdHarness (t .t , t , bobLnd , t .universeServer )
2553+ defer func () {
2554+ require .NoError (t .t , bob .stop (! * noDelete ))
2555+ }()
2556+
2557+ alice := t .tapd
2558+
2559+ // We need to derive two keys, one for the new script key and
2560+ // one for the internal key.
2561+ bobScriptKey , bobAnchorIntKeyDesc := DeriveKeys (t .t , bob )
2562+
2563+ var id [32 ]byte
2564+ copy (id [:], genInfo .AssetId )
2565+ sendAmt := uint64 (2400 )
2566+
2567+ vPkt := tappsbt .ForInteractiveSend (
2568+ id , sendAmt , bobScriptKey , 0 , 0 , 0 ,
2569+ bobAnchorIntKeyDesc , asset .V0 , chainParams ,
2570+ )
2571+
2572+ // Next, we'll attempt to complete a transfer with PSBTs from
2573+ // alice to bob, using the partial amount.
2574+ fundResp := fundPacket (t , alice , vPkt )
2575+ signResp , err := alice .SignVirtualPsbt (
2576+ ctxt , & wrpc.SignVirtualPsbtRequest {
2577+ FundedPsbt : fundResp .FundedPsbt ,
2578+ },
2579+ )
2580+ require .NoError (t .t , err )
2581+
2582+ // Now we'll attempt to complete the transfer.
2583+ sendResp , err := alice .AnchorVirtualPsbts (
2584+ ctxt , & wrpc.AnchorVirtualPsbtsRequest {
2585+ VirtualPsbts : [][]byte {signResp .SignedPsbt },
2586+ },
2587+ )
2588+ require .NoError (t .t , err )
2589+
2590+ numOutputs := 2
2591+ changeAmt := mintedAsset .Amount - sendAmt
2592+ ConfirmAndAssertOutboundTransferWithOutputs (
2593+ t .t , t .lndHarness .Miner ().Client , alice , sendResp ,
2594+ genInfo .AssetId , []uint64 {changeAmt , sendAmt }, 0 , 1 , numOutputs ,
2595+ )
2596+
2597+ // We want the proof of the change asset since that is the root asset.
2598+ aliceScriptKeyBytes := sendResp .Transfer .Outputs [0 ].ScriptKey
2599+ proofResp := exportProof (
2600+ t , alice , sendResp , aliceScriptKeyBytes , genInfo ,
2601+ )
2602+ proofFile , err := proof .DecodeFile (proofResp .RawProofFile )
2603+ require .NoError (t .t , err )
2604+ require .Equal (t .t , proofFile .NumProofs (), 2 )
2605+ latestProof , err := proofFile .LastProof ()
2606+ require .NoError (t .t , err )
2607+
2608+ // This proof should contain the STXO exclusion proofs
2609+ stxoProofs := latestProof .ExclusionProofs [0 ].CommitmentProof .STXOProofs
2610+ require .NotNil (t .t , stxoProofs )
2611+
2612+ // We expect a single exclusion proof for the change output, which is
2613+ // the input asset that we spent which should not be committed to in the
2614+ // other anchor output.
2615+ outpoint , err := wire .NewOutPointFromString (
2616+ mintedAsset .ChainAnchor .AnchorOutpoint ,
2617+ )
2618+ require .NoError (t .t , err )
2619+
2620+ prevId := asset.PrevID {
2621+ OutPoint : * outpoint ,
2622+ ID : id ,
2623+ ScriptKey : asset .SerializedKey (mintedAsset .ScriptKey ),
2624+ }
2625+
2626+ prevIdKey := asset .DeriveBurnKey (prevId )
2627+ expectedScriptKey := asset .NewScriptKey (prevIdKey )
2628+
2629+ pubKey := expectedScriptKey .PubKey
2630+ identifier := asset .ToSerialized (pubKey )
2631+
2632+ require .Len (t .t , stxoProofs , 1 )
2633+
2634+ // If we derive the identifier from the script key we expect of the
2635+ // minimal asset, it should yield a proof when used as a key for the
2636+ // stxoProofs.
2637+ require .NotNil (t .t , stxoProofs [identifier ])
2638+
2639+ // Create the minimal asset for which we expect to see the STXO
2640+ // exclusion.
2641+ minAsset , err := asset .NewAltLeaf (expectedScriptKey , asset .ScriptV0 )
2642+ require .NoError (t .t , err )
2643+
2644+ // We need to copy the base exclusion proof for each STXO because we'll
2645+ // modify it with the specific asset and taproot proofs.
2646+ stxoProof := stxoProofs [identifier ]
2647+ stxoExclProof := proof .MakeSTXOProof (
2648+ latestProof .ExclusionProofs [0 ], & stxoProof ,
2649+ )
2650+
2651+ // Derive the possible taproot keys assuming the exclusion proof is
2652+ // correct.
2653+ derivedKeys , err := stxoExclProof .DeriveByAssetExclusion (
2654+ minAsset .AssetCommitmentKey (),
2655+ minAsset .TapCommitmentKey (),
2656+ )
2657+ require .NoError (t .t , err )
2658+
2659+ // Extract the actual taproot key from the anchor tx.
2660+ expectedTaprootKey , err := proof .ExtractTaprootKey (
2661+ & latestProof .AnchorTx , stxoExclProof .OutputIndex ,
2662+ )
2663+ require .NoError (t .t , err )
2664+ expectedKey := schnorr .SerializePubKey (expectedTaprootKey )
2665+
2666+ // Convert the derived (possible) keys into their schnorr serialized
2667+ // counterparts.
2668+ serializedKeys := make ([][]byte , 0 , len (derivedKeys ))
2669+ for derivedKey := range derivedKeys {
2670+ serializedKeys = append (
2671+ serializedKeys , derivedKey .SchnorrSerialized (),
2672+ )
2673+ }
2674+
2675+ // The derived keys should contain the expected key.
2676+ require .Contains (t .t , serializedKeys , expectedKey )
2677+
2678+ // This is an interactive transfer, so we do need to manually
2679+ // send the proof from the sender to the receiver.
2680+ bobScriptKeyBytes := bobScriptKey .PubKey .SerializeCompressed ()
2681+ sendProof (t , alice , bob , sendResp , bobScriptKeyBytes , genInfo )
2682+ }
2683+
25142684// testPsbtExternalCommit tests the ability to fully customize the BTC level of
25152685// an asset transfer using a PSBT. This exercises the CommitVirtualPsbts and
25162686// PublishAndLogTransfer RPCs. The test case moves some assets into an output
0 commit comments