diff --git a/address/mock.go b/address/mock.go index 7b4db7758..f718457da 100644 --- a/address/mock.go +++ b/address/mock.go @@ -111,6 +111,10 @@ type TestVectors struct { func NewTestFromAddress(t testing.TB, a *Tap) *TestAddress { t.Helper() + if a == nil { + return nil + } + ta := &TestAddress{ ChainParamsHRP: a.ChainParams.TapHRP, AssetVersion: uint8(a.AssetVersion), diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index 8de74ff81..a128d66d4 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -201,6 +202,9 @@ type TransferOutput struct { // includes all the proof information other than the final chain // information. ProofSuffix []byte + + // The Tap address that should be used to satisfy the transfer. + Addr address.Tap } // OutboundParcel represents the database level delta of an outbound Taproot diff --git a/tappsbt/address.go b/tappsbt/address.go index f29862884..4ec36003f 100644 --- a/tappsbt/address.go +++ b/tappsbt/address.go @@ -61,6 +61,7 @@ func FromAddresses(receiverAddrs []*address.Tap, ), AnchorOutputInternalKey: &addr.InternalKey, AnchorOutputTapscriptSibling: addr.TapscriptSibling, + Addr: addr, }) } diff --git a/tappsbt/decode.go b/tappsbt/decode.go index 4dc2c47ab..e9bd507a1 100644 --- a/tappsbt/decode.go +++ b/tappsbt/decode.go @@ -262,6 +262,9 @@ func (o *VOutput) decode(pOut psbt.POutput, txOut *wire.TxOut) error { &o.AnchorOutputTapscriptSibling, commitment.TapscriptPreimageDecoder, ), + }, { + key: PsbtKeyTypeOutputTapAddr, + decoder: addrDecoder(&o.Addr), }} for idx := range mapping { @@ -319,6 +322,34 @@ func assetDecoder(a **asset.Asset) decoderFunc { } } +// addrDecoder returns a decoder function that can handle a Tap address. +func addrDecoder(a **address.Tap) decoderFunc { + return func(key, byteVal []byte) error { + if len(byteVal) == 0 { + return nil + } + + // Decode variable length byte slice into address bytes. + var addrBytes []byte + err := tlvDecoder(&addrBytes, tlv.DVarBytes)(key, byteVal) + if err != nil { + return err + } + + // Decode address bytes into address. + if *a == nil { + *a = &address.Tap{} + } + + buf := bytes.NewBuffer(addrBytes) + if err := (*a).Decode(buf); err != nil { + return err + } + + return nil + } +} + // booleanDecoder returns a function that decodes the given byte slice as a // boolean. func booleanDecoder(target *bool) decoderFunc { diff --git a/tappsbt/decode_test.go b/tappsbt/decode_test.go index 6ac934312..1c432e0a8 100644 --- a/tappsbt/decode_test.go +++ b/tappsbt/decode_test.go @@ -85,12 +85,28 @@ func TestEncodingDecoding(t *testing.T) { decoded, err := NewFromRawBytes(&buf, false) require.NoError(t, err) + // Repopulate chain params in the VOutput address fields since they are + // not encoded/decoded. + for i := range pkg.Outputs { + if decoded.Outputs[i].Addr != nil { + decoded.Outputs[i].Addr.ChainParams = pkg.ChainParams + } + } + assertEqualPackets(t, pkg, decoded) // Also make sure we can decode the packet from the base PSBT. decoded, err = NewFromPsbt(packet) require.NoError(t, err) + // Repopulate chain params in the VOutput address fields since they are + // not encoded/decoded. + for i := range pkg.Outputs { + if decoded.Outputs[i].Addr != nil { + decoded.Outputs[i].Addr.ChainParams = pkg.ChainParams + } + } + assertEqualPackets(t, pkg, decoded) } @@ -109,6 +125,7 @@ func TestEncodingDecoding(t *testing.T) { require.NoError(t, err) pkg.Outputs = append(pkg.Outputs, &VOutput{ ScriptKey: asset.RandScriptKey(t), + Addr: addr.Tap, }) return pkg diff --git a/tappsbt/encode.go b/tappsbt/encode.go index b96970827..691cd8247 100644 --- a/tappsbt/encode.go +++ b/tappsbt/encode.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -213,6 +214,11 @@ func (o *VOutput) encode(coinType uint32) (psbt.POutput, *wire.TxOut, error) { } anchorOutputIndex := uint64(o.AnchorOutputIndex) + addrEncoderFunc, err := addrEncoder(o.Addr) + if err != nil { + return pOut, nil, fmt.Errorf("error encoding address: %w", err) + } + mapping := []encoderMapping{{ key: PsbtKeyTypeOutputTapType, encoder: tlvEncoder(&o.Type, vOutputTypeEncoder), @@ -244,6 +250,9 @@ func (o *VOutput) encode(coinType uint32) (psbt.POutput, *wire.TxOut, error) { encoder: tapscriptPreimageEncoder( o.AnchorOutputTapscriptSibling, ), + }, { + key: PsbtKeyTypeOutputTapAddr, + encoder: addrEncoderFunc, }} for idx := range mapping { @@ -309,6 +318,30 @@ func assetEncoder(a *asset.Asset) encoderFunc { return tlvEncoder(a, asset.LeafEncoder) } +// addrEncoder returns an encoder function for encoding a Tap address. +func addrEncoder(addr *address.Tap) (encoderFunc, error) { + // Initially set the encoder function to a no-op. If the address is nil, + // or we encounter an error, we will return this function as the + // encoder. + encoder := func([]byte) ([]*customPsbtField, error) { + return nil, nil + } + + if addr == nil { + return encoder, nil + } + + // Encode address as a variable length byte slice. + var buf bytes.Buffer + if err := addr.Encode(&buf); err != nil { + return encoder, err + } + addrBytes := buf.Bytes() + + encoder = tlvEncoder(&addrBytes, tlv.EVarBytes) + return encoder, nil +} + // booleanEncoder returns a function that encodes the given boolean value as a // byte slice. func booleanEncoder(val bool) encoderFunc { diff --git a/tappsbt/interface.go b/tappsbt/interface.go index a773122ca..8be797a65 100644 --- a/tappsbt/interface.go +++ b/tappsbt/interface.go @@ -50,6 +50,7 @@ var ( PsbtKeyTypeOutputTapAsset = []byte{0x76} PsbtKeyTypeOutputTapSplitAsset = []byte{0x77} PsbtKeyTypeOutputTapAnchorTapscriptSibling = []byte{0x78} + PsbtKeyTypeOutputTapAddr = []byte{0x79} ) // The following keys are used as custom fields on the BTC level anchor @@ -493,6 +494,11 @@ type VOutput struct { // serialized, this will be stored in the TaprootInternalKey and // TaprootDerivationPath fields of the PSBT output. ScriptKey asset.ScriptKey + + // The Tap address that should be used to satisfy the transfer. Some + // outputs may not have a tap address, in which case this field will be + // nil. + Addr *address.Tap } // SplitLocator creates a split locator from the output. The asset ID is passed diff --git a/tappsbt/mock.go b/tappsbt/mock.go index 42a8cb5d4..89cb64bb0 100644 --- a/tappsbt/mock.go +++ b/tappsbt/mock.go @@ -66,6 +66,13 @@ func RandPacket(t testing.TB) *VPacket { txscript.NewTapBranch(leaf1, leaf1), ) + // Create two random Tap addresses, one for each virtual output. + proofCourierAddr1 := address.RandProofCourierAddr(t) + addr1, _, _ := address.RandAddr(t, testParams, proofCourierAddr1) + + proofCourierAddr2 := address.RandProofCourierAddr(t) + addr2, _, _ := address.RandAddr(t, testParams, proofCourierAddr2) + vPacket := &VPacket{ Inputs: []*VInput{{ PrevID: asset.PrevID{ @@ -98,6 +105,7 @@ func RandPacket(t testing.TB) *VPacket { ScriptKey: testOutputAsset.ScriptKey, SplitAsset: testOutputAsset, AnchorOutputTapscriptSibling: testPreimage1, + Addr: addr1.Tap, }, { Amount: 345, Type: TypeSplitRoot, @@ -110,6 +118,7 @@ func RandPacket(t testing.TB) *VPacket { Asset: testOutputAsset, ScriptKey: testOutputAsset.ScriptKey, AnchorOutputTapscriptSibling: testPreimage2, + Addr: addr2.Tap, }}, ChainParams: testParams, } @@ -440,6 +449,7 @@ func NewTestFromVOutput(t testing.TB, v *VOutput, PkScript: hex.EncodeToString(test.ComputeTaprootScript( t, v.ScriptKey.PubKey, )), + Addr: address.NewTestFromAddress(t, v.Addr), } if v.Asset != nil { @@ -524,6 +534,7 @@ type TestVOutput struct { TrBip32Derivation []*TestTrBip32Derivation `json:"tr_bip32_derivation"` TrInternalKey string `json:"tr_internal_key"` TrMerkleRoot string `json:"tr_merkle_root"` + Addr *address.TestAddress `json:"address"` } func (to *TestVOutput) ToVOutput(t testing.TB) *VOutput { diff --git a/tappsbt/testdata/psbt_encoding_generated.json b/tappsbt/testdata/psbt_encoding_generated.json index fe556e95b..4a69962c8 100644 --- a/tappsbt/testdata/psbt_encoding_generated.json +++ b/tappsbt/testdata/psbt_encoding_generated.json @@ -43,7 +43,8 @@ "bip32_derivation": null, "tr_bip32_derivation": null, "tr_internal_key": "", - "tr_merkle_root": "" + "tr_merkle_root": "", + "address": null }, { "amount": 5263531936693774911, @@ -60,7 +61,18 @@ "bip32_derivation": null, "tr_bip32_derivation": null, "tr_internal_key": "", - "tr_merkle_root": "" + "tr_merkle_root": "", + "address": { + "chain_params_hrp": "tapbc", + "asset_version": 0, + "asset_id": "6abedfb7cc21821085134aec85f1ec2335aae36608efb88578042e2d29a137f2", + "group_key": "", + "script_key": "02458a92cc12d01b8e8892da58ed511d398f2ce42b4d6295cf0c66346249f4d306", + "internal_key": "034a821d5ec008712983929de448b8afb6c24e5a1b97367b9a65b6220d7f083fe3", + "tapscript_sibling": "", + "amount": 5263531936693774911, + "proof_courier_addr": "hashmail://rand.hashmail.proof.courier:443" + } }, { "amount": 0, @@ -77,13 +89,24 @@ "bip32_derivation": null, "tr_bip32_derivation": null, "tr_internal_key": "", - "tr_merkle_root": "" + "tr_merkle_root": "", + "address": { + "chain_params_hrp": "tapbc", + "asset_version": 0, + "asset_id": "6abedfb7cc21821085134aec85f1ec2335aae36608efb88578042e2d29a137f2", + "group_key": "", + "script_key": "02458a92cc12d01b8e8892da58ed511d398f2ce42b4d6295cf0c66346249f4d306", + "internal_key": "034a821d5ec008712983929de448b8afb6c24e5a1b97367b9a65b6220d7f083fe3", + "tapscript_sibling": "", + "amount": 5263531936693774911, + "proof_courier_addr": "hashmail://rand.hashmail.proof.courier:443" + } } ], "version": 0, "chain_params_hrp": "tapbc" }, - "expected": "cHNidP8BALQCAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAIlEgfHm5sm5GOJXu9WedhViULIbErSIzre8BvD5tVAs2U/4/ao62aNILSSJRIEWKkswS0BuOiJLaWO1RHTmPLOQrTWKVzwxmNGJJ9NMGAAAAAAAAAAAiUSCb/fBsMyKz8/3kPZwT22tcWIL2n7umqVjBMS9I4ccbEQAAAAABcAEBAXEFdGFwYmMBcgEAAAFwZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGq+37fMIYIQhRNK7IXx7CM1quNmCO+4hXgELi0poTfyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXEIAAAAAAAAAAABcgABcwgAAAAAAAAAAAF1AAF4AAF6AAABcAEBAXEBAAFyCAAAAAAAAAAAAAFwAQABcQEAAXIIAAAAAAAAAAEBcyEDSoIdXsAIcSmDkp3kSLivtsJOWhuXNnuaZbYiDX8IP+MAAXABAAFxAQABcggAAAAAAAAAAAA=", + "expected": "cHNidP8BALQCAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAIlEgfHm5sm5GOJXu9WedhViULIbErSIzre8BvD5tVAs2U/4/ao62aNILSSJRIEWKkswS0BuOiJLaWO1RHTmPLOQrTWKVzwxmNGJJ9NMGAAAAAAAAAAAiUSCb/fBsMyKz8/3kPZwT22tcWIL2n7umqVjBMS9I4ccbEQAAAAABcAEBAXEFdGFwYmMBcgEAAAFwZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGq+37fMIYIQhRNK7IXx7CM1quNmCO+4hXgELi0poTfyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXEIAAAAAAAAAAABcgABcwgAAAAAAAAAAAF1AAF4AAF6AAABcAEBAXEBAAFyCAAAAAAAAAAAAAFwAQABcQEAAXIIAAAAAAAAAAEBcyEDSoIdXsAIcSmDkp3kSLivtsJOWhuXNnuaZbYiDX8IP+MBef0uAQABAAIgar7ft8whghCFE0rshfHsIzWq42YI77iFeAQuLSmhN/IEIQJFipLMEtAbjoiS2ljtUR05jyzkK01ilc8MZjRiSfTTBgYhA0qCHV7ACHEpg5Kd5Ei4r7bCTloblzZ7mmW2Ig1/CD/jCAn/SQvSaLaOaj8KKmhhc2htYWlsOi8vcmFuZC5oYXNobWFpbC5wcm9vZi5jb3VyaWVyOjQ0Mw2KIbZoC058i3Y6Gx1J1JVchIYhYyUlP+xzjdep4ov5IRGkJlXZQGViOWQxOGE0NDc4NDI3NDZlOTk1YWY1YTI1MzY3OTUxYmFhMmZmNmNkNDcxYzQ4M2YxNWZiOTBiYWRiMzdjNTjrnRikR4QnRumVr1olNnlRuqL/bNRxxIPxX7kLrbN8WLsVhkQAAAFwAQABcQEAAXIIAAAAAAAAAAABef0uAQABAAIgar7ft8whghCFE0rshfHsIzWq42YI77iFeAQuLSmhN/IEIQJFipLMEtAbjoiS2ljtUR05jyzkK01ilc8MZjRiSfTTBgYhA0qCHV7ACHEpg5Kd5Ei4r7bCTloblzZ7mmW2Ig1/CD/jCAn/SQvSaLaOaj8KKmhhc2htYWlsOi8vcmFuZC5oYXNobWFpbC5wcm9vZi5jb3VyaWVyOjQ0Mw2KIbZoC058i3Y6Gx1J1JVchIYhYyUlP+xzjdep4ov5IRGkJlXZQGViOWQxOGE0NDc4NDI3NDZlOTk1YWY1YTI1MzY3OTUxYmFhMmZmNmNkNDcxYzQ4M2YxNWZiOTBiYWRiMzdjNTjrnRikR4QnRumVr1olNnlRuqL/bNRxxIPxX7kLrbN8WLsVhkQAAA==", "comment": "minimal packet" }, { @@ -121,8 +144,8 @@ "tr_merkle_root": "6d65726b6c6520726f6f74", "prev_id": { "out_point": "42c29291018d7c2d968d3f71f8cb984b92f67403049442fd5ad145f422b02936:4104515131", - "asset_id": "9d17346865fcf92b0c3a17c9028be9914eb7649c6c9347800979d1830356f2a5", - "script_key": "035f01644ac40614b96363ba9baf1a7381a7bac742d267909edac6eebfc3eba5e7" + "asset_id": "b57c7da41ab0408e3969c2e2cdcf233438bf1774ace7709a4f091e9a83fdeae0", + "script_key": "0279219cae03062dc76cd8c409b9f1a77b178bc261891464b9db57bc9c6c2828e4" }, "anchor": { "value": 777, @@ -311,7 +334,18 @@ "bip32_derivation": null, "tr_bip32_derivation": null, "tr_internal_key": "", - "tr_merkle_root": "" + "tr_merkle_root": "", + "address": { + "chain_params_hrp": "tapbc", + "asset_version": 0, + "asset_id": "da70685456a015f437c9c9da74d36446c663552f7309a34619378d3b9d802652", + "group_key": "", + "script_key": "026b869c0f65479a16ed80806fcb0b3b92c659c3462a9696af35884cbcc02c7e82", + "internal_key": "035f01644ac40614b96363ba9baf1a7381a7bac742d267909edac6eebfc3eba5e7", + "tapscript_sibling": "", + "amount": 4739111663495868, + "proof_courier_addr": "hashmail://rand.hashmail.proof.courier:443" + } }, { "amount": 345, @@ -381,13 +415,24 @@ "bip32_derivation": null, "tr_bip32_derivation": null, "tr_internal_key": "", - "tr_merkle_root": "" + "tr_merkle_root": "", + "address": { + "chain_params_hrp": "tapbc", + "asset_version": 0, + "asset_id": "480f0850ea67ca59b5578334f7b122e3d3a07e3c5f13e4a0d030772705af8b27", + "group_key": "02d0384efd303a30c37caa4bfcb200381725553e9d503f3d82d210d756bb99849d", + "script_key": "021d49c9351a4754adfda80b535e028cc05d7c7f2f7d5de6fcdb9335f34c26585a", + "internal_key": "03a971b56fd3170902ad55940bbc0e15554bd75d7ce31e439b4948dc03145cb378", + "tapscript_sibling": "00c0126e6f7420612076616c696420736372697074", + "amount": 6924566981437551529, + "proof_courier_addr": "hashmail://rand.hashmail.proof.courier:443" + } } ], "version": 0, "chain_params_hrp": "tapbc" }, - "expected": "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", + "expected": "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", "comment": "random packet" } ],