@@ -285,6 +285,16 @@ type CustomAnchorAssetOutputSummary struct {
285285 // AnchorValueSat is the BTC value assigned to the asset-bearing output.
286286 AnchorValueSat int64 `json:"anchor_value_sat"`
287287
288+ // TaprootAssetRoot is the root of the Taproot Asset commitment before
289+ // combining it with the host-supplied tapscript sibling. Hosts such as
290+ // SwapDK use this root to compose their policy tree and control blocks.
291+ TaprootAssetRoot Hash `json:"taproot_asset_root"`
292+
293+ // TaprootMerkleRoot is the final BIP341 root that combines the Taproot
294+ // Asset commitment with the optional host-supplied tapscript sibling.
295+ // It is the tweak committed to by the anchor output key.
296+ TaprootMerkleRoot Hash `json:"taproot_merkle_root"`
297+
288298 // ScriptKey is the asset script key for the new output.
289299 ScriptKey PubKey `json:"script_key"`
290300
@@ -1034,6 +1044,11 @@ func validatePackageOutputMapping(anchor *psbt.Packet, txID chainhash.Hash,
10341044 }
10351045 anchorOutpoint := Outpoint {Txid : [32 ]byte (txID ), Index : anchorIndex }
10361046 anchorValue := anchor .UnsignedTx .TxOut [anchorIndex ].Value
1047+ taprootAssetRoot , taprootMerkleRoot , err :=
1048+ deriveCustomAnchorOutputRoots (virtualOutput , commitments )
1049+ if err != nil {
1050+ return fmt .Errorf ("derive output commitment roots: %w" , err )
1051+ }
10371052 if ! customAnchorAssetRefMatchesIssuance (
10381053 summary .AssetRef , actualRef , issuanceID ,
10391054 ) ||
@@ -1045,6 +1060,16 @@ func validatePackageOutputMapping(anchor *psbt.Packet, txID chainhash.Hash,
10451060
10461061 return fmt .Errorf ("output summary does not match virtual output" )
10471062 }
1063+ if summary .TaprootAssetRoot != taprootAssetRoot ||
1064+ summary .TaprootMerkleRoot != taprootMerkleRoot {
1065+
1066+ return fmt .Errorf ("output commitment roots do not match virtual output" )
1067+ }
1068+ if err := validateCustomAnchorOutputRootHints (
1069+ anchor , anchorIndex , taprootAssetRoot , taprootMerkleRoot ,
1070+ ); err != nil {
1071+ return err
1072+ }
10481073 if customAnchorProofCourierString (
10491074 virtualOutput .ProofDeliveryAddress ,
10501075 ) != summary .ProofDelivery .CourierAddress {
@@ -1096,6 +1121,69 @@ func validatePackageOutputMapping(anchor *psbt.Packet, txID chainhash.Hash,
10961121 return nil
10971122}
10981123
1124+ func deriveCustomAnchorOutputRoots (output * tappsbt.VOutput ,
1125+ commitments tappsbt.OutputCommitments ) (Hash , Hash , error ) {
1126+
1127+ if output == nil {
1128+ return Hash {}, Hash {}, fmt .Errorf ("virtual output is missing" )
1129+ }
1130+ if output .AnchorOutputInternalKey == nil {
1131+ return Hash {}, Hash {}, fmt .Errorf ("anchor output internal key is missing" )
1132+ }
1133+ anchorCommitment , ok := commitments [output .AnchorOutputIndex ]
1134+ if ! ok || anchorCommitment == nil {
1135+ return Hash {}, Hash {}, fmt .Errorf (
1136+ "anchor output %d commitment is missing" ,
1137+ output .AnchorOutputIndex ,
1138+ )
1139+ }
1140+
1141+ _ , merkleRoot , assetRoot , err := tapsend .AnchorOutputScript (
1142+ output .AnchorOutputInternalKey ,
1143+ output .AnchorOutputTapscriptSibling , anchorCommitment ,
1144+ )
1145+ if err != nil {
1146+ return Hash {}, Hash {}, err
1147+ }
1148+
1149+ return Hash (assetRoot ), Hash (merkleRoot ), nil
1150+ }
1151+
1152+ func validateCustomAnchorOutputRootHints (anchor * psbt.Packet ,
1153+ anchorIndex uint32 , assetRoot , merkleRoot Hash ) error {
1154+
1155+ if anchorIndex >= uint32 (len (anchor .Outputs )) {
1156+ return fmt .Errorf ("anchor output metadata is out of range" )
1157+ }
1158+ unknowns := anchor .Outputs [anchorIndex ].Unknowns
1159+ assetRootHint := tappsbt .ExtractCustomField (
1160+ unknowns , tappsbt .PsbtKeyTypeOutputAssetRoot ,
1161+ )
1162+ if len (assetRootHint ) != len (assetRoot ) {
1163+ return fmt .Errorf ("anchor output %d Taproot Asset root hint must be %d " +
1164+ "bytes, was %d" , anchorIndex , len (assetRoot ), len (assetRootHint ))
1165+ }
1166+ if ! bytes .Equal (assetRootHint , assetRoot [:]) {
1167+ return fmt .Errorf ("anchor output %d Taproot Asset root hint does not " +
1168+ "match the committed assets" , anchorIndex )
1169+ }
1170+
1171+ merkleRootHint := tappsbt .ExtractCustomField (
1172+ unknowns , tappsbt .PsbtKeyTypeOutputTaprootMerkleRoot ,
1173+ )
1174+ if len (merkleRootHint ) != len (merkleRoot ) {
1175+ return fmt .Errorf ("anchor output %d Taproot merkle root hint must be %d " +
1176+ "bytes, was %d" , anchorIndex , len (merkleRoot ),
1177+ len (merkleRootHint ))
1178+ }
1179+ if ! bytes .Equal (merkleRootHint , merkleRoot [:]) {
1180+ return fmt .Errorf ("anchor output %d Taproot merkle root hint does not " +
1181+ "match the committed assets" , anchorIndex )
1182+ }
1183+
1184+ return nil
1185+ }
1186+
10991187// Clone returns a deep copy of the package so callers can snapshot a committed
11001188// state before external signing or broadcast mutates host-owned buffers.
11011189func (p * CustomAnchorTransferPackage ) Clone () * CustomAnchorTransferPackage {
0 commit comments