block_test.go raw
1 package block
2
3 import (
4 "encoding/hex"
5 "os"
6 "strings"
7 "testing"
8 )
9
10 // The fixture is regtest block 1 from the rig node: one coinbase transaction, so a single
11 // transaction id and the merkle root are the same value — which makes a mistake in either the
12 // transaction scan or the root collapse into one mismatch rather than two.
13 const (
14 fixtureHash = "772aaf3a73102e9c5826524c776ede9499bcb82740f2c7e14913c70d611f271f"
15 fixtureMerkle = "1d89b965ec54a83d571087f604495c46e937574a8483ba089dbc9920b6589b36"
16 )
17
18 // loadFixture reads the block.
19 func loadFixture(t *testing.T) []byte {
20 t.Helper()
21
22 text, err := os.ReadFile("testdata/block1.hex")
23
24 if err != nil {
25 t.Fatal(err)
26 }
27
28 raw, err := hex.DecodeString(strings.TrimSpace(string(text)))
29
30 if err != nil {
31 t.Fatal(err)
32 }
33
34 return raw
35 }
36
37 // display reverses a hash into the order a person reads it in.
38 func display(hash [32]byte) string {
39 out := make([]byte, len(hash))
40
41 for index := range hash {
42 out[len(hash)-1-index] = hash[index]
43 }
44
45 return hex.EncodeToString(out)
46 }
47
48 // TestTheBlockParses: header hash, merkle root and transaction ids come out right.
49 func TestTheBlockParses(t *testing.T) {
50 parsed, err := Parse(loadFixture(t))
51
52 if err != nil {
53 t.Fatal(err)
54 }
55
56 if got := display(parsed.Hash); got != fixtureHash {
57 t.Fatalf("the block hash is %s, want %s", got, fixtureHash)
58 }
59
60 if got := display(parsed.MerkleRoot); got != fixtureMerkle {
61 t.Fatalf("the header's root is %s, want %s", got, fixtureMerkle)
62 }
63
64 if len(parsed.TxIDs) != 1 {
65 t.Fatalf("%d transactions", len(parsed.TxIDs))
66 }
67
68 if got := display(parsed.TxIDs[0]); got != fixtureMerkle {
69 t.Fatalf("the coinbase id is %s, and a one-transaction root must equal it", got)
70 }
71 }
72
73 // TestTheMerkleRootVerifies: the check a fetched block has to pass.
74 func TestTheMerkleRootVerifies(t *testing.T) {
75 parsed, err := Parse(loadFixture(t))
76
77 if err != nil {
78 t.Fatal(err)
79 }
80
81 if err := parsed.VerifyMerkle(); err != nil {
82 t.Fatal(err)
83 }
84 }
85
86 // TestAFabricatedBlockIsRefused: a block whose transactions do not hash to the header's root is
87 // exactly what a lying gateway would send.
88 func TestAFabricatedBlockIsRefused(t *testing.T) {
89 raw := loadFixture(t)
90
91 // Flip a byte inside the coinbase's scriptSig, which changes the transaction id.
92 raw[HeaderLen+1+4+40] ^= 0xff
93
94 parsed, err := Parse(raw)
95
96 if err != nil {
97 t.Fatal(err)
98 }
99
100 if err := parsed.VerifyMerkle(); err == nil {
101 t.Fatal("a block with a changed transaction passed the merkle check")
102 }
103 }
104
105 // TestTruncationIsRefused: every cut is an error, not a shorter block.
106 func TestTruncationIsRefused(t *testing.T) {
107 raw := loadFixture(t)
108
109 for _, cut := range []int{0, 40, HeaderLen, HeaderLen + 5, len(raw) / 2, len(raw) - 1} {
110 if _, err := Parse(raw[:cut]); err == nil {
111 t.Fatalf("a %d-byte block parsed", cut)
112 }
113 }
114 }
115
116 // TestTheCoinbaseOutputIsFound: the point of parsing a block — which output pays this script,
117 // in which transaction, for how much.
118 func TestTheCoinbaseOutputIsFound(t *testing.T) {
119 parsed, err := Parse(loadFixture(t))
120
121 if err != nil {
122 t.Fatal(err)
123 }
124
125 if len(parsed.Outputs) != 1 || len(parsed.Outputs[0]) != 2 {
126 t.Fatalf("%d transactions with %d and %d outputs", len(parsed.Outputs),
127 len(parsed.Outputs[0]), len(parsed.Outputs[0]))
128 }
129
130 // The first output pays 50 BTC to a P2WPKH script; match it by its own bytes.
131 paid := parsed.Outputs[0][0]
132
133 if paid.Value != 5000000000 {
134 t.Fatalf("the coinbase pays %d satoshis", paid.Value)
135 }
136
137 matches := parsed.Match([][]byte{paid.Script, []byte{0x00, 0x14}})
138
139 if len(matches) != 1 {
140 t.Fatalf("%d matches", len(matches))
141 }
142
143 if matches[0].Vout != 0 || matches[0].Value != paid.Value {
144 t.Fatalf("the match is %+v", matches[0])
145 }
146
147 if matches[0].TxID != parsed.TxIDs[0] {
148 t.Fatal("the match names a different transaction")
149 }
150
151 // A script nobody pays finds nothing.
152 if got := parsed.Match([][]byte{{0x00, 0x14, 0x99}}); len(got) != 0 {
153 t.Fatalf("%d matches for an unpayed script", len(got))
154 }
155 }
156