Skip to content

Commit 3571e1a

Browse files
committed
BIP158: add test vectors and generation code
In this commit, we add test vectors for filter and header construction and the code to generate them. The included test vectors are for testnet with a value of 20 for P. The code generates filters and headers for values of 1 through 32 for P using testnet blocks. Currently, to run the code, the `Roasbeef` fork of `btcd` (at https://github.com/roasbeef/btcd) is required to be running locally in testnet mode; this will be changed in a future commit after the code is merged into the `btcsuite` mainline.
1 parent d1874d5 commit 3571e1a

File tree

3 files changed

+376
-1
lines changed

3 files changed

+376
-1
lines changed

bip-0158.mediawiki

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ gcs_match_any(key: [16]byte, compressed_set: []byte, targets: [][]byte, P: uint,
420420

421421
== Appendix C: Test Vectors ==
422422

423-
TODO: To be generated.
423+
Test vectors for a P value of 20 on five testnet blocks, including the filters and filter headers, can be found [[bip-0158/testnet-20.csv|here]]. The code to generate these vectors for P values of 1 through 32 can be found [[bip-0158/gentestvectors.go|here]].
424424

425425
== References ==
426426

bip-0158/gentestvectors.go

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
// This program connects to your local btcd and generates test vectors for
2+
// 5 blocks and collision space sizes of 1-32 bits. Change the RPC cert path
3+
// and credentials to run on your system. The program assumes you're running
4+
// a btcd with cfilter support, which mainline btcd doesn't have; in order to
5+
// circumvent this assumption, comment out the if block that checks for
6+
// filter size of DefaultP.
7+
8+
package main
9+
10+
import (
11+
"bytes"
12+
"encoding/hex"
13+
"fmt"
14+
"io/ioutil"
15+
"os"
16+
"path"
17+
18+
"github.com/roasbeef/btcd/chaincfg"
19+
"github.com/roasbeef/btcd/chaincfg/chainhash"
20+
"github.com/roasbeef/btcd/rpcclient"
21+
"github.com/roasbeef/btcd/wire"
22+
"github.com/roasbeef/btcutil/gcs"
23+
"github.com/roasbeef/btcutil/gcs/builder"
24+
)
25+
26+
func main() {
27+
err := os.Mkdir("gcstestvectors", os.ModeDir|0755)
28+
if err != nil { // Don't overwrite existing output if any
29+
fmt.Println("Couldn't create directory: ", err)
30+
return
31+
}
32+
files := make([]*os.File, 33)
33+
prevBasicHeaders := make([]chainhash.Hash, 33)
34+
prevExtHeaders := make([]chainhash.Hash, 33)
35+
for i := 1; i <= 32; i++ { // Min 1 bit of collision space, max 32
36+
var blockBuf bytes.Buffer
37+
fName := fmt.Sprintf("gcstestvectors/testnet-%02d.csv", i)
38+
file, err := os.Create(fName)
39+
if err != nil {
40+
fmt.Println("Error creating CSV file: ", err.Error())
41+
return
42+
}
43+
_, err = file.WriteString("Block Height,Block Hash,Block,Previous Basic Header,Previous Ext Header,Basic Filter,Ext Filter,Basic Header,Ext Header\n")
44+
if err != nil {
45+
fmt.Println("Error writing to CSV file: ", err.Error())
46+
return
47+
}
48+
files[i] = file
49+
basicFilter, err := buildBasicFilter(
50+
chaincfg.TestNet3Params.GenesisBlock, uint8(i))
51+
if err != nil {
52+
fmt.Println("Error generating basic filter: ", err.Error())
53+
return
54+
}
55+
prevBasicHeaders[i], err = builder.MakeHeaderForFilter(basicFilter,
56+
chaincfg.TestNet3Params.GenesisBlock.Header.PrevBlock)
57+
if err != nil {
58+
fmt.Println("Error generating header for filter: ", err.Error())
59+
return
60+
}
61+
if basicFilter == nil {
62+
basicFilter = &gcs.Filter{}
63+
}
64+
extFilter, err := buildExtFilter(
65+
chaincfg.TestNet3Params.GenesisBlock, uint8(i))
66+
if err != nil {
67+
fmt.Println("Error generating ext filter: ", err.Error())
68+
return
69+
}
70+
prevExtHeaders[i], err = builder.MakeHeaderForFilter(extFilter,
71+
chaincfg.TestNet3Params.GenesisBlock.Header.PrevBlock)
72+
if err != nil {
73+
fmt.Println("Error generating header for filter: ", err.Error())
74+
return
75+
}
76+
if extFilter == nil {
77+
extFilter = &gcs.Filter{}
78+
}
79+
err = chaincfg.TestNet3Params.GenesisBlock.Serialize(&blockBuf)
80+
if err != nil {
81+
fmt.Println("Error serializing block to buffer: ", err.Error())
82+
return
83+
}
84+
bfBytes, err := basicFilter.NBytes()
85+
if err != nil {
86+
fmt.Println("Couldn't get NBytes(): ", err)
87+
return
88+
}
89+
efBytes, err := extFilter.NBytes()
90+
if err != nil {
91+
fmt.Println("Couldn't get NBytes(): ", err)
92+
return
93+
}
94+
err = writeCSVRow(
95+
file,
96+
0, // Height
97+
*chaincfg.TestNet3Params.GenesisHash,
98+
blockBuf.Bytes(),
99+
chaincfg.TestNet3Params.GenesisBlock.Header.PrevBlock,
100+
chaincfg.TestNet3Params.GenesisBlock.Header.PrevBlock,
101+
bfBytes,
102+
efBytes,
103+
prevBasicHeaders[i],
104+
prevExtHeaders[i],
105+
)
106+
if err != nil {
107+
fmt.Println("Error writing to CSV file: ", err.Error())
108+
return
109+
}
110+
}
111+
cert, err := ioutil.ReadFile(
112+
path.Join(os.Getenv("HOME"), "/.btcd/rpc.cert"))
113+
if err != nil {
114+
fmt.Println("Couldn't read RPC cert: ", err.Error())
115+
return
116+
}
117+
conf := rpcclient.ConnConfig{
118+
Host: "127.0.0.1:18334",
119+
Endpoint: "ws",
120+
User: "kek",
121+
Pass: "kek",
122+
Certificates: cert,
123+
}
124+
client, err := rpcclient.New(&conf, nil)
125+
if err != nil {
126+
fmt.Println("Couldn't create a new client: ", err.Error())
127+
return
128+
}
129+
for height := 1; height < 988000; height++ {
130+
fmt.Printf("Height: %d\n", height)
131+
blockHash, err := client.GetBlockHash(int64(height))
132+
if err != nil {
133+
fmt.Println("Couldn't get block hash: ", err.Error())
134+
return
135+
}
136+
block, err := client.GetBlock(blockHash)
137+
if err != nil {
138+
fmt.Println("Couldn't get block hash: ", err.Error())
139+
return
140+
}
141+
var blockBuf bytes.Buffer
142+
err = block.Serialize(&blockBuf)
143+
if err != nil {
144+
fmt.Println("Error serializing block to buffer: ", err.Error())
145+
return
146+
}
147+
blockBytes := blockBuf.Bytes()
148+
for i := 1; i <= 32; i++ {
149+
basicFilter, err := buildBasicFilter(block, uint8(i))
150+
if err != nil {
151+
fmt.Println("Error generating basic filter: ", err.Error())
152+
return
153+
}
154+
basicHeader, err := builder.MakeHeaderForFilter(basicFilter,
155+
prevBasicHeaders[i])
156+
if err != nil {
157+
fmt.Println("Error generating header for filter: ", err.Error())
158+
return
159+
}
160+
if basicFilter == nil {
161+
basicFilter = &gcs.Filter{}
162+
}
163+
extFilter, err := buildExtFilter(block, uint8(i))
164+
if err != nil {
165+
fmt.Println("Error generating ext filter: ", err.Error())
166+
return
167+
}
168+
extHeader, err := builder.MakeHeaderForFilter(extFilter,
169+
prevExtHeaders[i])
170+
if err != nil {
171+
fmt.Println("Error generating header for filter: ", err.Error())
172+
return
173+
}
174+
if extFilter == nil {
175+
extFilter = &gcs.Filter{}
176+
}
177+
if i == builder.DefaultP { // This is the default filter size so we can check against the server's info
178+
filter, err := client.GetCFilter(blockHash, wire.GCSFilterRegular)
179+
if err != nil {
180+
fmt.Println("Error getting basic filter: ", err.Error())
181+
return
182+
}
183+
nBytes, err := basicFilter.NBytes()
184+
if err != nil {
185+
fmt.Println("Couldn't get NBytes(): ", err)
186+
return
187+
}
188+
if !bytes.Equal(filter.Data, nBytes) {
189+
// Don't error on empty filters
190+
fmt.Println("Basic filter doesn't match!\n", filter.Data, "\n", nBytes)
191+
return
192+
}
193+
filter, err = client.GetCFilter(blockHash, wire.GCSFilterExtended)
194+
if err != nil {
195+
fmt.Println("Error getting extended filter: ", err.Error())
196+
return
197+
}
198+
nBytes, err = extFilter.NBytes()
199+
if err != nil {
200+
fmt.Println("Couldn't get NBytes(): ", err)
201+
return
202+
}
203+
if !bytes.Equal(filter.Data, nBytes) {
204+
fmt.Println("Extended filter doesn't match!")
205+
return
206+
}
207+
header, err := client.GetCFilterHeader(blockHash, wire.GCSFilterRegular)
208+
if err != nil {
209+
fmt.Println("Error getting basic header: ", err.Error())
210+
return
211+
}
212+
if !bytes.Equal(header.PrevFilterHeader[:], basicHeader[:]) {
213+
fmt.Println("Basic header doesn't match!")
214+
return
215+
}
216+
header, err = client.GetCFilterHeader(blockHash, wire.GCSFilterExtended)
217+
if err != nil {
218+
fmt.Println("Error getting extended header: ", err.Error())
219+
return
220+
}
221+
if !bytes.Equal(header.PrevFilterHeader[:], extHeader[:]) {
222+
fmt.Println("Extended header doesn't match!")
223+
return
224+
}
225+
fmt.Println("Verified against server")
226+
}
227+
switch height {
228+
case 1, 2, 3, 926485, 987876: // Blocks for test cases
229+
var bfBytes []byte
230+
var efBytes []byte
231+
if basicFilter.N() > 0 {
232+
bfBytes, err = basicFilter.NBytes()
233+
if err != nil {
234+
fmt.Println("Couldn't get NBytes(): ", err)
235+
return
236+
}
237+
}
238+
if extFilter.N() > 0 { // Exclude special case for block 987876
239+
efBytes, err = extFilter.NBytes()
240+
if err != nil {
241+
fmt.Println("Couldn't get NBytes(): ", err)
242+
return
243+
}
244+
}
245+
writeCSVRow(
246+
files[i],
247+
height,
248+
*blockHash,
249+
blockBytes,
250+
prevBasicHeaders[i],
251+
prevExtHeaders[i],
252+
bfBytes,
253+
efBytes,
254+
basicHeader,
255+
extHeader)
256+
}
257+
prevBasicHeaders[i] = basicHeader
258+
prevExtHeaders[i] = extHeader
259+
}
260+
}
261+
}
262+
263+
// writeCSVRow writes a test vector to a CSV file.
264+
func writeCSVRow(file *os.File, height int, blockHash chainhash.Hash,
265+
blockBytes []byte, prevBasicHeader, prevExtHeader chainhash.Hash,
266+
basicFilter, extFilter []byte, basicHeader, extHeader chainhash.Hash) error {
267+
row := fmt.Sprintf("%d,%s,%s,%s,%s,%s,%s,%s,%s\n",
268+
height,
269+
blockHash.String(),
270+
hex.EncodeToString(blockBytes),
271+
prevBasicHeader.String(),
272+
prevExtHeader.String(),
273+
hex.EncodeToString(basicFilter),
274+
hex.EncodeToString(extFilter),
275+
basicHeader.String(),
276+
extHeader.String(),
277+
)
278+
_, err := file.WriteString(row)
279+
if err != nil {
280+
return err
281+
}
282+
return nil
283+
}
284+
285+
// buildBasicFilter builds a basic GCS filter from a block. A basic GCS filter
286+
// will contain all the previous outpoints spent within a block, as well as the
287+
// data pushes within all the outputs created within a block. p is specified as
288+
// an argument in order to create test vectors with various values for p.
289+
func buildBasicFilter(block *wire.MsgBlock, p uint8) (*gcs.Filter, error) {
290+
blockHash := block.BlockHash()
291+
b := builder.WithKeyHashP(&blockHash, p)
292+
293+
// If the filter had an issue with the specified key, then we force it
294+
// to bubble up here by calling the Key() function.
295+
_, err := b.Key()
296+
if err != nil {
297+
return nil, err
298+
}
299+
300+
// In order to build a basic filter, we'll range over the entire block,
301+
// adding the outpoint data as well as the data pushes within the
302+
// pkScript.
303+
for i, tx := range block.Transactions {
304+
// First we'll compute the bash of the transaction and add that
305+
// directly to the filter.
306+
txHash := tx.TxHash()
307+
b.AddHash(&txHash)
308+
309+
// Skip the inputs for the coinbase transaction
310+
if i != 0 {
311+
// Each each txin, we'll add a serialized version of
312+
// the txid:index to the filters data slices.
313+
for _, txIn := range tx.TxIn {
314+
b.AddOutPoint(txIn.PreviousOutPoint)
315+
}
316+
}
317+
318+
// For each output in a transaction, we'll add each of the
319+
// individual data pushes within the script.
320+
for _, txOut := range tx.TxOut {
321+
b.AddEntry(txOut.PkScript)
322+
}
323+
}
324+
325+
return b.Build()
326+
}
327+
328+
// buildExtFilter builds an extended GCS filter from a block. An extended
329+
// filter supplements a regular basic filter by include all the _witness_ data
330+
// found within a block. This includes all the data pushes within any signature
331+
// scripts as well as each element of an input's witness stack. Additionally,
332+
// the _hashes_ of each transaction are also inserted into the filter. p is
333+
// specified as an argument in order to create test vectors with various values
334+
// for p.
335+
func buildExtFilter(block *wire.MsgBlock, p uint8) (*gcs.Filter, error) {
336+
blockHash := block.BlockHash()
337+
b := builder.WithKeyHashP(&blockHash, p)
338+
339+
// If the filter had an issue with the specified key, then we force it
340+
// to bubble up here by calling the Key() function.
341+
_, err := b.Key()
342+
if err != nil {
343+
return nil, err
344+
}
345+
346+
// In order to build an extended filter, we add the hash of each
347+
// transaction as well as each piece of witness data included in both
348+
// the sigScript and the witness stack of an input.
349+
for i, tx := range block.Transactions {
350+
// Skip the inputs for the coinbase transaction
351+
if i != 0 {
352+
// Next, for each input, we'll add the sigScript (if
353+
// it's present), and also the witness stack (if it's
354+
// present)
355+
for _, txIn := range tx.TxIn {
356+
if txIn.SignatureScript != nil {
357+
b.AddScript(txIn.SignatureScript)
358+
}
359+
360+
if len(txIn.Witness) != 0 {
361+
b.AddWitness(txIn.Witness)
362+
}
363+
}
364+
}
365+
}
366+
367+
return b.Build()
368+
}

bip-0158/testnet-20.csv

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Block Height,Block Hash,Block,Previous Basic Header,Previous Ext Header,Basic Filter,Ext Filter,Basic Header,Ext Header
2+
0,000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943,0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,0285c7cdbe33a0,00,c0589c7f567cffaf7bc0c9f6ad61710b78d3c1afef5d65a2a08e8a753173aa54,753e0d1c28585269ab770b166ca2cd1b32f9bc918750547941ed4849d5a80ba8
3+
1,00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206,0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000,c0589c7f567cffaf7bc0c9f6ad61710b78d3c1afef5d65a2a08e8a753173aa54,753e0d1c28585269ab770b166ca2cd1b32f9bc918750547941ed4849d5a80ba8,026929d09bee00,,81e4f3e934488be62758f0b88037aa558262da3190ca018329997a319a0f8b5b,31b674ab635e074717329dabdb25d3cb0e14cb2526000cc2cedac7b5f2595110
4+
2,000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820,0100000006128e87be8b1b4dea47a7247d5528d2702c96826c7a648497e773b800000000e241352e3bec0a95a6217e10c3abb54adfa05abb12c126695595580fb92e222032e7494dffff001d00d235340101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000,81e4f3e934488be62758f0b88037aa558262da3190ca018329997a319a0f8b5b,31b674ab635e074717329dabdb25d3cb0e14cb2526000cc2cedac7b5f2595110,0278fc41168ec0,,ec48f9f8a625bd8adb2d2684867a05baafebf935553e0b78b386da98179dcf49,0dd53b407c3f242f1838e39e2fc0cfb89cdca27de07ec230568a08d1872f9e01
5+
3,000000008b896e272758da5297bcd98fdc6d97c9b765ecec401e286dc1fdbe10,0100000020782a005255b657696ea057d5b98f34defcf75196f64f6eeac8026c0000000041ba5afc532aae03151b8aa87b65e1594f97504a768e010c98c0add79216247186e7494dffff001d058dc2b60101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0486e7494d0151062f503253482fffffffff0100f2052a01000000232103f6d9ff4c12959445ca5549c811683bf9c88e637b222dd2e0311154c4c85cf423ac00000000,ec48f9f8a625bd8adb2d2684867a05baafebf935553e0b78b386da98179dcf49,0dd53b407c3f242f1838e39e2fc0cfb89cdca27de07ec230568a08d1872f9e01,022ce4b3256540,,060b7e1be150cc3cabd9e96b00af217132b387f31fd2bd9adfb0c7f5a09a3356,5b2a59bc476d52b45de1398ee34ed10d0cccd2a4b19ba502a456c7356b35be0d
6+
926485,000000000000015d6077a411a8f5cc95caf775ccf11c54e27df75ce58d187313,0000002060bbab0edbf3ef8a49608ee326f8fd75c473b7e3982095e2d100000000000000c30134f8c9b6d2470488d7a67a888f6fa12f8692e0c3411fbfb92f0f68f67eedae03ca57ef13021acc22dc4105010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff2f0315230e0004ae03ca57043e3d1e1d0c8796bf579aef0c0000000000122f4e696e6a61506f6f6c2f5345475749542fffffffff038427a112000000001976a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac0000000000000000266a24aa21a9ed5c748e121c0fe146d973a4ac26fa4a68b0549d46ee22d25f50a5e46fe1b377ee00000000000000002952534b424c4f434b3acd16772ad61a3c5f00287480b720f6035d5e54c9efc71be94bb5e3727f10909001200000000000000000000000000000000000000000000000000000000000000000000000000100000000010145310e878941a1b2bc2d33797ee4d89d95eaaf2e13488063a2aa9a74490f510a0100000023220020b6744de4f6ec63cc92f7c220cdefeeb1b1bed2b66c8e5706d80ec247d37e65a1ffffffff01002d3101000000001976a9143ebc40e411ed3c76f86711507ab952300890397288ac0400473044022001dd489a5d4e2fbd8a3ade27177f6b49296ba7695c40dbbe650ea83f106415fd02200b23a0602d8ff1bdf79dee118205fc7e9b40672bf31563e5741feb53fb86388501483045022100f88f040e90cc5dc6c6189d04718376ac19ed996bf9e4a3c29c3718d90ffd27180220761711f16c9e3a44f71aab55cbc0634907a1fa8bb635d971a9a01d368727bea10169522103b3623117e988b76aaabe3d63f56a4fc88b228a71e64c4cc551d1204822fe85cb2103dd823066e096f72ed617a41d3ca56717db335b1ea47a1b4c5c9dbdd0963acba621033d7c89bd9da29fa8d44db7906a9778b53121f72191184a9fee785c39180e4be153ae00000000010000000120925534261de4dcebb1ed5ab1b62bfe7a3ef968fb111dc2c910adfebc6e3bdf010000006b483045022100f50198f5ae66211a4f485190abe4dc7accdabe3bc214ebc9ea7069b97097d46e0220316a70a03014887086e335fc1b48358d46cd6bdc9af3b57c109c94af76fc915101210316cff587a01a2736d5e12e53551b18d73780b83c3bfb4fcf209c869b11b6415effffffff0220a10700000000001976a91450333046115eaa0ac9e0216565f945070e44573988ac2e7cd01a000000001976a914c01a7ca16b47be50cbdbc60724f701d52d75156688ac00000000010000000203a25f58630d7a1ea52550365fd2156683f56daf6ca73a4b4bbd097e66516322010000006a47304402204efc3d70e4ca3049c2a425025edf22d5ca355f9ec899dbfbbeeb2268533a0f2b02204780d3739653035af4814ea52e1396d021953f948c29754edd0ee537364603dc012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffff03a25f58630d7a1ea52550365fd2156683f56daf6ca73a4b4bbd097e66516322000000006a47304402202d96defdc5b4af71d6ba28c9a6042c2d5ee7bc6de565d4db84ef517445626e03022022da80320e9e489c8f41b74833dfb6a54a4eb5087cdb46eb663eef0b25caa526012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffff0200e1f5050000000017a914b7e6f7ff8658b2d1fb107e3d7be7af4742e6b1b3876f88fc00000000001976a914913bcc2be49cb534c20474c4dee1e9c4c317e7eb88ac0000000001000000043ffd60d3818431c495b89be84afac205d5d1ed663009291c560758bbd0a66df5010000006b483045022100f344607de9df42049688dcae8ff1db34c0c7cd25ec05516e30d2bc8f12ac9b2f022060b648f6a21745ea6d9782e17bcc4277b5808326488a1f40d41e125879723d3a012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffffa5379401cce30f84731ef1ba65ce27edf2cc7ce57704507ebe8714aa16a96b92010000006a473044022020c37a63bf4d7f564c2192528709b6a38ab8271bd96898c6c2e335e5208661580220435c6f1ad4d9305d2c0a818b2feb5e45d443f2f162c0f61953a14d097fd07064012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffff70e731e193235ff12c3184510895731a099112ffca4b00246c60003c40f843ce000000006a473044022053760f74c29a879e30a17b5f03a5bb057a5751a39f86fa6ecdedc36a1b7db04c022041d41c9b95f00d2d10a0373322a9025dba66c942196bc9d8adeb0e12d3024728012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffff66b7a71b3e50379c8e85fc18fe3f1a408fc985f257036c34702ba205cef09f6f000000006a4730440220499bf9e2db3db6e930228d0661395f65431acae466634d098612fd80b08459ee022040e069fc9e3c60009f521cef54c38aadbd1251aee37940e6018aadb10f194d6a012103f7a897e4dbecab2264b21917f90664ea8256189ea725d28740cf7ba5d85b5763ffffffff0200e1f5050000000017a9148fc37ad460fdfbd2b44fe446f6e3071a4f64faa6878f447f0b000000001976a914913bcc2be49cb534c20474c4dee1e9c4c317e7eb88ac00000000,5c169b91332e661a4ebf684d6dffcf64aa139e1e736096ce462609b2e0783c55,5f3cd111e10ed28b7c2bc138fe4bc62e5df1cdc292c010b78c84e5111a5daaa6,16040c63f7ddea293f2d9c13690c0ba7b2910228c38b0fe542ce525021e49b598ada05f83bb9c37c711a02b1850265991c34c4fea6261d22a4b84596c0,0e6651beff00ee7a3be424a90e98450727b304558434c8d53781d469131ad21d399376c151ca28,c8f83ffbc9781c2bd4b7e3c055e888b00d3e2fea14e93b3c4f3adee86b063374,61f8ee615258981089be9f98337f53f44b14cc1532aa469a348fdee547117b80
7+
987876,0000000000000c00901f2049055e2a437c819d79a3d54fd63e6af796cd7b8a79,000000202694f74969fdb542090e95a56bc8aa2d646e27033850e32f1c5f000000000000f7e53676b3f12d5beb524ed617f2d25f5a93b5f4f52c1ba2678260d72712f8dd0a6dfe5740257e1a4b1768960101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1603e4120ff9c30a1c216900002f424d4920546573742fffffff0001205fa012000000001e76a914c486de584a735ec2f22da7cd9681614681f92173d83d0aa68688ac00000000,37bf9e681888b3cd204ca4e0c995aad68cd0ecb86bdf19dd0fa2e72dbabcda28,c4c5051dd741c11840ef3f11fb4f372a16bb5aac1dc66576e89e8e6835d667e0,021016dc7a6a20,,156e9bf3ec5be367f0a829858e9ee182cc3a6531bedced491a52fcfed841c6cb,cab50aab93410cb150fd761f2067a909d35c8a9c0114578efd9590e2d381ee02

0 commit comments

Comments
 (0)