Skip to content

Commit 77866ef

Browse files
committed
multi: turn VInput's proof into actual struct
Now that we've resolved the circular package dependency between the proof and tappsbt packages, we can finally use the *proof.Proof type directly for the packet's input proof (and later the output's proof suffix).
1 parent d7ff486 commit 77866ef

File tree

9 files changed

+273
-43
lines changed

9 files changed

+273
-43
lines changed

tapfreighter/wallet.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ func (f *AssetWallet) passiveAssetVPacket(passiveAsset *asset.Asset,
514514

515515
// Set the input asset. The input asset proof is not provided as it is
516516
// not needed for the re-anchoring process.
517-
vPacket.SetInputAsset(0, inputAsset, nil)
517+
vPacket.SetInputAsset(0, inputAsset)
518518

519519
return vPacket
520520
}
@@ -999,7 +999,7 @@ func (f *AssetWallet) setVPacketInputs(ctx context.Context,
999999
return nil, fmt.Errorf("cannot decode proof for input "+
10001000
"asset: %w", err)
10011001
}
1002-
inputProof, err := inputProofFile.RawLastProof()
1002+
inputProof, err := inputProofFile.LastProof()
10031003
if err != nil {
10041004
return nil, fmt.Errorf("cannot get last proof for "+
10051005
"input asset: %w", err)
@@ -1037,11 +1037,12 @@ func (f *AssetWallet) setVPacketInputs(ctx context.Context,
10371037
inTrBip32Derivation,
10381038
},
10391039
},
1040+
Proof: inputProof,
10401041
PInput: psbt.PInput{
10411042
SighashType: txscript.SigHashDefault,
10421043
},
10431044
}
1044-
vPkt.SetInputAsset(idx, assetInput.Asset, inputProof)
1045+
vPkt.SetInputAsset(idx, assetInput.Asset)
10451046

10461047
inputCommitments[idx] = assetInput.Commitment
10471048
}
@@ -1127,10 +1128,10 @@ func (f *AssetWallet) SignVirtualPacket(vPkt *tappsbt.VPacket,
11271128
// verifyInclusionProof verifies that the given virtual input's asset is
11281129
// actually committed in the anchor transaction.
11291130
func verifyInclusionProof(vIn *tappsbt.VInput) error {
1130-
proofReader := bytes.NewReader(vIn.Proof())
1131-
assetProof := &proof.Proof{}
1132-
if err := assetProof.Decode(proofReader); err != nil {
1133-
return fmt.Errorf("unable to decode asset proof: %w", err)
1131+
assetProof := vIn.Proof
1132+
1133+
if assetProof == nil {
1134+
return fmt.Errorf("input proof is nil")
11341135
}
11351136

11361137
// Before we look at the inclusion proof, we'll make sure that the input
@@ -1143,8 +1144,7 @@ func verifyInclusionProof(vIn *tappsbt.VInput) error {
11431144

11441145
if op.Hash != anchorTxHash {
11451146
return fmt.Errorf("proof anchor tx hash %v doesn't match "+
1146-
"input anchor outpoint %v in proof %x", anchorTxHash,
1147-
op.Hash, vIn.Proof())
1147+
"input anchor outpoint %v", anchorTxHash, op.Hash)
11481148
}
11491149
if op.Index >= uint32(len(assetProof.AnchorTx.TxOut)) {
11501150
return fmt.Errorf("input anchor outpoint index out of range")
@@ -1153,8 +1153,8 @@ func verifyInclusionProof(vIn *tappsbt.VInput) error {
11531153
anchorTxOut := assetProof.AnchorTx.TxOut[op.Index]
11541154
if !bytes.Equal(anchorTxOut.PkScript, vIn.Anchor.PkScript) {
11551155
return fmt.Errorf("proof anchor tx pk script %x doesn't "+
1156-
"match input anchor script %x in proof %x",
1157-
anchorTxOut.PkScript, vIn.Anchor.PkScript, vIn.Proof())
1156+
"match input anchor script %x", anchorTxOut.PkScript,
1157+
vIn.Anchor.PkScript)
11581158
}
11591159

11601160
anchorKey, err := proof.ExtractTaprootKeyFromScript(vIn.Anchor.PkScript)

tappsbt/address.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func OwnershipProofPacket(ownedAsset *asset.Asset,
150150
}},
151151
ChainParams: chainParams,
152152
}
153-
vPkt.SetInputAsset(0, ownedAsset, nil)
153+
vPkt.SetInputAsset(0, ownedAsset)
154154

155155
return vPkt
156156
}

tappsbt/decode.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/lightninglabs/taproot-assets/address"
1616
"github.com/lightninglabs/taproot-assets/asset"
1717
"github.com/lightninglabs/taproot-assets/commitment"
18+
"github.com/lightninglabs/taproot-assets/proof"
1819
"github.com/lightningnetwork/lnd/tlv"
1920
)
2021

@@ -168,7 +169,7 @@ func (i *VInput) decode(pIn psbt.PInput) error {
168169
decoder: assetDecoder(&i.asset),
169170
}, {
170171
key: PsbtKeyTypeInputTapAssetProof,
171-
decoder: tlvDecoder(&i.proof, tlv.DVarBytes),
172+
decoder: proofDecoder(&i.Proof),
172173
}}
173174

174175
for idx := range mapping {
@@ -309,7 +310,7 @@ func (o *VOutput) decode(pOut psbt.POutput, txOut *wire.TxOut) error {
309310
return nil
310311
}
311312

312-
// tlvDecoder returns a function that encodes the given byte slice using the
313+
// tlvDecoder returns a function that decodes the given byte slice using the
313314
// given TLV tlvDecoder.
314315
func tlvDecoder(val any, dec tlv.Decoder) decoderFunc {
315316
return func(_, byteVal []byte) error {
@@ -326,6 +327,20 @@ func tlvDecoder(val any, dec tlv.Decoder) decoderFunc {
326327
}
327328
}
328329

330+
// proofDecoder returns a decoder function that can handle nil proofs.
331+
func proofDecoder(p **proof.Proof) decoderFunc {
332+
return func(key, byteVal []byte) error {
333+
if len(byteVal) == 0 {
334+
return nil
335+
}
336+
337+
if *p == nil {
338+
*p = &proof.Proof{}
339+
}
340+
return (*p).Decode(bytes.NewReader(byteVal))
341+
}
342+
}
343+
329344
// assetDecoder returns a decoder function that can handle nil assets.
330345
func assetDecoder(a **asset.Asset) decoderFunc {
331346
return func(key, byteVal []byte) error {

tappsbt/encode.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/lightninglabs/taproot-assets/asset"
1616
"github.com/lightninglabs/taproot-assets/commitment"
1717
"github.com/lightninglabs/taproot-assets/fn"
18+
"github.com/lightninglabs/taproot-assets/proof"
1819
"github.com/lightningnetwork/lnd/tlv"
1920
)
2021

@@ -181,7 +182,7 @@ func (i *VInput) encode() (psbt.PInput, error) {
181182
},
182183
{
183184
key: PsbtKeyTypeInputTapAssetProof,
184-
encoder: tlvEncoder(&i.proof, tlv.EVarBytes),
185+
encoder: proofEncoder(i.Proof),
185186
},
186187
}
187188

@@ -336,6 +337,28 @@ func pubKeyEncoder(pubKey *btcec.PublicKey) encoderFunc {
336337
return tlvEncoder(&pubKey, tlv.EPubKey)
337338
}
338339

340+
// proofEncoder is an encoder that does nothing if the given proof is nil.
341+
func proofEncoder(p *proof.Proof) encoderFunc {
342+
return func(key []byte) ([]*customPsbtField, error) {
343+
if p == nil {
344+
return nil, nil
345+
}
346+
347+
var buf bytes.Buffer
348+
err := p.Encode(&buf)
349+
if err != nil {
350+
return nil, err
351+
}
352+
353+
return []*customPsbtField{
354+
{
355+
Key: fn.CopySlice(key),
356+
Value: buf.Bytes(),
357+
},
358+
}, nil
359+
}
360+
}
361+
339362
// assetEncoder is an encoder that does nothing if the given asset is nil.
340363
func assetEncoder(a *asset.Asset) encoderFunc {
341364
if a == nil {

tappsbt/interface.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/lightninglabs/taproot-assets/asset"
1414
"github.com/lightninglabs/taproot-assets/commitment"
1515
"github.com/lightninglabs/taproot-assets/fn"
16+
"github.com/lightninglabs/taproot-assets/proof"
1617
"github.com/lightningnetwork/lnd/keychain"
1718
)
1819

@@ -156,12 +157,11 @@ type VPacket struct {
156157
}
157158

158159
// SetInputAsset sets the input asset that is being spent.
159-
func (p *VPacket) SetInputAsset(index int, a *asset.Asset, proof []byte) {
160+
func (p *VPacket) SetInputAsset(index int, a *asset.Asset) {
160161
if index >= len(p.Inputs) {
161162
p.Inputs = append(p.Inputs, &VInput{})
162163
}
163164
p.Inputs[index].asset = a.Copy()
164-
p.Inputs[index].proof = proof
165165
p.Inputs[index].serializeScriptKey(
166166
a.ScriptKey, p.ChainParams.HDCoinType,
167167
)
@@ -309,23 +309,16 @@ type VInput struct {
309309
// input struct for the signing to work correctly.
310310
asset *asset.Asset
311311

312-
// proof is the proof blob that proves the asset being spent was
313-
// committed to in the anchor transaction above. This cannot be of type
314-
// proof.Proof directly because that would cause a circular dependency.
315-
proof []byte
312+
// Proof is a transition proof that proves the asset being spent was
313+
// committed to in the anchor transaction above.
314+
Proof *proof.Proof
316315
}
317316

318317
// Asset returns the input's asset that's being spent.
319318
func (i *VInput) Asset() *asset.Asset {
320319
return i.asset
321320
}
322321

323-
// Proof returns the proof blob that the asset being spent was committed to in
324-
// the anchor transaction.
325-
func (i *VInput) Proof() []byte {
326-
return i.proof
327-
}
328-
329322
// serializeScriptKey serializes the input asset's script key as the PSBT
330323
// derivation information on the virtual input.
331324
func (i *VInput) serializeScriptKey(key asset.ScriptKey, coinType uint32) {

tappsbt/mock.go

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
11
package tappsbt
22

33
import (
4+
"bytes"
45
"encoding/hex"
6+
"os"
7+
"path/filepath"
8+
"strings"
59
"testing"
610

711
"github.com/btcsuite/btcd/btcec/v2"
812
"github.com/btcsuite/btcd/btcutil"
913
"github.com/btcsuite/btcd/btcutil/psbt"
1014
"github.com/btcsuite/btcd/txscript"
15+
"github.com/btcsuite/btcd/wire"
1116
"github.com/lightninglabs/taproot-assets/address"
1217
"github.com/lightninglabs/taproot-assets/asset"
1318
"github.com/lightninglabs/taproot-assets/commitment"
1419
"github.com/lightninglabs/taproot-assets/internal/test"
20+
"github.com/lightninglabs/taproot-assets/proof"
1521
"github.com/lightningnetwork/lnd/keychain"
1622
"github.com/stretchr/testify/require"
1723
)
1824

25+
const (
26+
// testDataFileName is the name of the directory with the test data.
27+
testDataFileName = "testdata"
28+
)
29+
1930
var (
2031
testParams = &address.MainNetTap
32+
33+
// Block 100002 with 9 transactions on bitcoin mainnet.
34+
oddTxBlockHexFileName = filepath.Join(
35+
testDataFileName, "odd-block.hex",
36+
)
2137
)
2238

2339
// RandPacket generates a random virtual packet for testing purposes.
@@ -68,6 +84,22 @@ func RandPacket(t testing.TB) *VPacket {
6884
txscript.NewTapBranch(leaf1, leaf1),
6985
)
7086

87+
oddTxBlockHex, err := os.ReadFile(oddTxBlockHexFileName)
88+
require.NoError(t, err)
89+
90+
oddTxBlockBytes, err := hex.DecodeString(
91+
strings.Trim(string(oddTxBlockHex), "\n"),
92+
)
93+
require.NoError(t, err)
94+
95+
var oddTxBlock wire.MsgBlock
96+
err = oddTxBlock.Deserialize(bytes.NewReader(oddTxBlockBytes))
97+
require.NoError(t, err)
98+
99+
inputProof := proof.RandProof(
100+
t, testAsset.Genesis, inputScriptKey.PubKey, oddTxBlock, 1, 0,
101+
)
102+
71103
vPacket := &VPacket{
72104
Inputs: []*VInput{{
73105
PrevID: asset.PrevID{
@@ -85,12 +117,15 @@ func RandPacket(t testing.TB) *VPacket {
85117
Bip32Derivation: bip32Derivations,
86118
TrBip32Derivation: trBip32Derivations,
87119
},
120+
Proof: &inputProof,
88121
}, {
89122
// Empty input.
90123
}},
91124
Outputs: []*VOutput{{
92-
Amount: 123,
93-
AssetVersion: asset.Version(test.RandIntn(2)),
125+
Amount: 123,
126+
AssetVersion: asset.Version(
127+
test.RandIntn(2),
128+
),
94129
Type: TypeSplitRoot,
95130
Interactive: true,
96131
AnchorOutputIndex: 0,
@@ -102,8 +137,10 @@ func RandPacket(t testing.TB) *VPacket {
102137
SplitAsset: testOutputAsset,
103138
AnchorOutputTapscriptSibling: testPreimage1,
104139
}, {
105-
Amount: 345,
106-
AssetVersion: asset.Version(test.RandIntn(2)),
140+
Amount: 345,
141+
AssetVersion: asset.Version(
142+
test.RandIntn(2),
143+
),
107144
Type: TypeSplitRoot,
108145
Interactive: false,
109146
AnchorOutputIndex: 1,
@@ -116,7 +153,7 @@ func RandPacket(t testing.TB) *VPacket {
116153
}},
117154
ChainParams: testParams,
118155
}
119-
vPacket.SetInputAsset(0, testAsset, []byte("this is a proof"))
156+
vPacket.SetInputAsset(0, testAsset)
120157

121158
return vPacket
122159
}
@@ -207,7 +244,6 @@ func NewTestFromVInput(t testing.TB, i *VInput) *TestVInput {
207244
TrMerkleRoot: hex.EncodeToString(i.TaprootMerkleRoot),
208245
PrevID: asset.NewTestFromPrevID(&i.PrevID),
209246
Anchor: NewTestFromAnchor(&i.Anchor),
210-
Proof: hex.EncodeToString(i.proof),
211247
}
212248

213249
for idx := range i.Bip32Derivation {
@@ -230,6 +266,10 @@ func NewTestFromVInput(t testing.TB, i *VInput) *TestVInput {
230266
ti.Asset = asset.NewTestFromAsset(t, i.asset)
231267
}
232268

269+
if i.Proof != nil {
270+
ti.Proof = proof.NewTestFromProof(t, i.Proof)
271+
}
272+
233273
return ti
234274
}
235275

@@ -241,7 +281,7 @@ type TestVInput struct {
241281
PrevID *asset.TestPrevID `json:"prev_id"`
242282
Anchor *TestAnchor `json:"anchor"`
243283
Asset *asset.TestAsset `json:"asset"`
244-
Proof string `json:"proof"`
284+
Proof *proof.TestProof `json:"proof"`
245285
}
246286

247287
func (ti *TestVInput) ToVInput(t testing.TB) *VInput {
@@ -254,7 +294,6 @@ func (ti *TestVInput) ToVInput(t testing.TB) *VInput {
254294
},
255295
PrevID: *ti.PrevID.ToPrevID(t),
256296
Anchor: *ti.Anchor.ToAnchor(t),
257-
proof: test.ParseHex(t, ti.Proof),
258297
}
259298

260299
for idx := range ti.Bip32Derivation {
@@ -281,6 +320,10 @@ func (ti *TestVInput) ToVInput(t testing.TB) *VInput {
281320
require.NoError(t, err)
282321
}
283322

323+
if ti.Proof != nil {
324+
vi.Proof = ti.Proof.ToProof(t)
325+
}
326+
284327
return vi
285328
}
286329

tappsbt/testdata/odd-block.hex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0100000090f0a9f110702f808219ebea1173056042a714bad51b916cb6800000000000005275289558f51c9966699404ae2294730c3c9f9bda53523ce50e9b95e558da2fdb261b4d4c86041b1ab1bf930901000000010000000000000000000000000000000000000000000000000000000000000000ffffffff07044c86041b0146ffffffff0100f2052a01000000434104e18f7afbe4721580e81e8414fc8c24d7cfacf254bb5c7b949450c3e997c2dc1242487a8169507b631eb3771f2b425483fb13102c4eb5d858eef260fe70fbfae0ac00000000010000000196608ccbafa16abada902780da4dc35dafd7af05fa0da08cf833575f8cf9e836000000004a493046022100dab24889213caf43ae6adc41cf1c9396c08240c199f5225acf45416330fd7dbd022100fe37900e0644bf574493a07fc5edba06dbc07c311b947520c2d514bc5725dcb401ffffffff0100f2052a010000001976a914f15d1921f52e4007b146dfa60f369ed2fc393ce288ac000000000100000001fb766c1288458c2bafcfec81e48b24d98ec706de6b8af7c4e3c29419bfacb56d000000008c493046022100f268ba165ce0ad2e6d93f089cfcd3785de5c963bb5ea6b8c1b23f1ce3e517b9f022100da7c0f21adc6c401887f2bfd1922f11d76159cbc597fbd756a23dcbb00f4d7290141042b4e8625a96127826915a5b109852636ad0da753c9e1d5606a50480cd0c40f1f8b8d898235e571fe9357d9ec842bc4bba1827daaf4de06d71844d0057707966affffffff0280969800000000001976a9146963907531db72d0ed1a0cfb471ccb63923446f388ac80d6e34c000000001976a914f0688ba1c0d1ce182c7af6741e02658c7d4dfcd388ac000000000100000002c40297f730dd7b5a99567eb8d27b78758f607507c52292d02d4031895b52f2ff010000008b483045022100f7edfd4b0aac404e5bab4fd3889e0c6c41aa8d0e6fa122316f68eddd0a65013902205b09cc8b2d56e1cd1f7f2fafd60a129ed94504c4ac7bdc67b56fe67512658b3e014104732012cb962afa90d31b25d8fb0e32c94e513ab7a17805c14ca4c3423e18b4fb5d0e676841733cb83abaf975845c9f6f2a8097b7d04f4908b18368d6fc2d68ecffffffffca5065ff9617cbcba45eb23726df6498a9b9cafed4f54cbab9d227b0035ddefb000000008a473044022068010362a13c7f9919fa832b2dee4e788f61f6f5d344a7c2a0da6ae740605658022006d1af525b9a14a35c003b78b72bd59738cd676f845d1ff3fc25049e01003614014104732012cb962afa90d31b25d8fb0e32c94e513ab7a17805c14ca4c3423e18b4fb5d0e676841733cb83abaf975845c9f6f2a8097b7d04f4908b18368d6fc2d68ecffffffff01001ec4110200000043410469ab4181eceb28985b9b4e895c13fa5e68d85761b7eee311db5addef76fa8621865134a221bd01f28ec9999ee3e021e60766e9d1f3458c115fb28650605f11c9ac000000000100000001cdaf2f758e91c514655e2dc50633d1e4c84989f8aa90a0dbc883f0d23ed5c2fa010000008b48304502207ab51be6f12a1962ba0aaaf24a20e0b69b27a94fac5adf45aa7d2d18ffd9236102210086ae728b370e5329eead9accd880d0cb070aea0c96255fae6c4f1ddcce1fd56e014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffff02404b4c00000000001976a9142b6ba7c9d796b75eef7942fc9288edd37c32f5c388ac002d3101000000001976a9141befba0cdc1ad56529371864d9f6cb042faa06b588ac000000000100000001b4a47603e71b61bc3326efd90111bf02d2f549b067f4c4a8fa183b57a0f800cb010000008a4730440220177c37f9a505c3f1a1f0ce2da777c339bd8339ffa02c7cb41f0a5804f473c9230220585b25a2ee80eb59292e52b987dad92acb0c64eced92ed9ee105ad153cdb12d001410443bd44f683467e549dae7d20d1d79cbdb6df985c6e9c029c8d0c6cb46cc1a4d3cf7923c5021b27f7a0b562ada113bc85d5fda5a1b41e87fe6e8802817cf69996ffffffff0280651406000000001976a9145505614859643ab7b547cd7f1f5e7e2a12322d3788ac00aa0271000000001976a914ea4720a7a52fc166c55ff2298e07baf70ae67e1b88ac00000000010000000586c62cd602d219bb60edb14a3e204de0705176f9022fe49a538054fb14abb49e010000008c493046022100f2bc2aba2534becbdf062eb993853a42bbbc282083d0daf9b4b585bd401aa8c9022100b1d7fd7ee0b95600db8535bbf331b19eed8d961f7a8e54159c53675d5f69df8c014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffff03ad0e58ccdac3df9dc28a218bcf6f1997b0a93306faaa4b3a28ae83447b2179010000008b483045022100be12b2937179da88599e27bb31c3525097a07cdb52422d165b3ca2f2020ffcf702200971b51f853a53d644ebae9ec8f3512e442b1bcb6c315a5b491d119d10624c83014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffff2acfcab629bbc8685792603762c921580030ba144af553d271716a95089e107b010000008b483045022100fa579a840ac258871365dd48cd7552f96c8eea69bd00d84f05b283a0dab311e102207e3c0ee9234814cfbb1b659b83671618f45abc1326b9edcc77d552a4f2a805c0014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffffdcdc6023bbc9944a658ddc588e61eacb737ddf0a3cd24f113b5a8634c517fcd2000000008b4830450221008d6df731df5d32267954bd7d2dda2302b74c6c2a6aa5c0ca64ecbabc1af03c75022010e55c571d65da7701ae2da1956c442df81bbf076cdbac25133f99d98a9ed34c014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffffe15557cd5ce258f479dfd6dc6514edf6d7ed5b21fcfa4a038fd69f06b83ac76e010000008b483045022023b3e0ab071eb11de2eb1cc3a67261b866f86bf6867d4558165f7c8c8aca2d86022100dc6e1f53a91de3efe8f63512850811f26284b62f850c70ca73ed5de8771fb451014104462e76fd4067b3a0aa42070082dcb0bf2f388b6495cf33d789904f07d0f55c40fbd4b82963c69b3dc31895d0c772c812b1d5fbcade15312ef1c0e8ebbb12dcd4ffffffff01404b4c00000000001976a9142b6ba7c9d796b75eef7942fc9288edd37c32f5c388ac00000000010000000166d7577163c932b4f9690ca6a80b6e4eb001f0a2fa9023df5595602aae96ed8d000000008a4730440220262b42546302dfb654a229cefc86432b89628ff259dc87edd1154535b16a67e102207b4634c020a97c3e7bbd0d4d19da6aa2269ad9dded4026e896b213d73ca4b63f014104979b82d02226b3a4597523845754d44f13639e3bf2df5e82c6aab2bdc79687368b01b1ab8b19875ae3c90d661a3d0a33161dab29934edeb36aa01976be3baf8affffffff02404b4c00000000001976a9144854e695a02af0aeacb823ccbc272134561e0a1688ac40420f00000000001976a914abee93376d6b37b5c2940655a6fcaf1c8e74237988ac0000000001000000014e3f8ef2e91349a9059cb4f01e54ab2597c1387161d3da89919f7ea6acdbb371010000008c49304602210081f3183471a5ca22307c0800226f3ef9c353069e0773ac76bb580654d56aa523022100d4c56465bdc069060846f4fbf2f6b20520b2a80b08b168b31e66ddb9c694e240014104976c79848e18251612f8940875b2b08d06e6dc73b9840e8860c066b7e87432c477e9a59a453e71e6d76d5fe34058b800a098fc1740ce3012e8fc8a00c96af966ffffffff02c0e1e400000000001976a9144134e75a6fcb6042034aab5e18570cf1f844f54788ac404b4c00000000001976a9142b6ba7c9d796b75eef7942fc9288edd37c32f5c388ac00000000

0 commit comments

Comments
 (0)