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