pow_test.go raw
1 package neutrino
2
3 import (
4 "math/big"
5 "testing"
6
7 "git.smesh.lol/nostrino/pkg/btcwire"
8 "git.smesh.lol/nostrino/pkg/chain"
9 "git.smesh.lol/nostrino/pkg/crypto/chainhash"
10 )
11
12 func mineAt(t *testing.T, prev chainhash.Hash, timestamp, bits uint32, tag byte) btcwire.BlockHeader {
13 t.Helper()
14 h := btcwire.BlockHeader{
15 Version: 1,
16 PrevBlock: prev,
17 MerkleRoot: chainhash.DoubleHashH([]byte{tag}),
18 Timestamp: timestamp,
19 Bits: bits,
20 }
21 for nonce := uint32(0); nonce < 1_000_000; nonce++ {
22 h.Nonce = nonce
23 if h.CheckProofOfWork() {
24 return h
25 }
26 }
27 t.Fatalf("could not mine at %d", timestamp)
28 return btcwire.BlockHeader{}
29 }
30
31 func testClient(t *testing.T, profile *chain.Profile, store Store) *Client {
32 t.Helper()
33 c, err := New(Config{Profile: profile, Store: store, Address: "127.0.0.1:1"})
34 if err != nil {
35 t.Fatal(err)
36 }
37 return c
38 }
39
40 func seedHeaders(t *testing.T, profile *chain.Profile, store Store, count int, step uint32) chainhash.Hash {
41 t.Helper()
42 if err := store.SetAnchor(Anchor{Height: 0, BlockHash: profile.GenesisHash, FilterHeight: -1}); err != nil {
43 t.Fatal(err)
44 }
45 genesis := profile.GenesisHeader
46 if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil {
47 t.Fatal(err)
48 }
49 prev := genesis.BlockHash()
50 for i := 1; i <= count; i++ {
51 h := mineAt(t, prev, genesis.Timestamp+step*uint32(i), profile.PowLimitBits, byte(i))
52 if err := store.PutHeaders(uint32(i), []btcwire.BlockHeader{h}); err != nil {
53 t.Fatal(err)
54 }
55 prev = h.BlockHash()
56 }
57 return prev
58 }
59
60 func TestBigToCompactRoundTrip(t *testing.T) {
61 for _, bits := range []uint32{0x1d00ffff, 0x207fffff, 0x1b0404cb, 0x1e0377ae} {
62 target, err := btcwire.CompactToBig(bits)
63 if err != nil {
64 t.Fatalf("%08x: %v", bits, err)
65 }
66 if got := BigToCompact(target); got != bits {
67 t.Fatalf("%08x round trips to %08x", bits, got)
68 }
69 }
70 if BigToCompact(nil) != 0 {
71 t.Fatal("nil target is not zero")
72 }
73 }
74
75 func TestMedianTimePast(t *testing.T) {
76 profile := chain.Regtest
77 store := NewMemory()
78 tip := seedHeaders(t, &profile, store, 11, 600)
79 c := testClient(t, &profile, store)
80 tipHeader, _ := store.Header(11)
81
82 old := mineAt(t, tip, profile.GenesisHeader.Timestamp+600, profile.PowLimitBits, 99)
83 if err := c.validateHeader(12, &old, tip); err == nil {
84 t.Fatal("accepted a header at or below the median time")
85 }
86 good := mineAt(t, tip, tipHeader.Timestamp+600, profile.PowLimitBits, 100)
87 if err := c.validateHeader(12, &good, tip); err != nil {
88 t.Fatalf("good header rejected: %v", err)
89 }
90 }
91
92 func TestDifficultyBits(t *testing.T) {
93 profile := chain.Regtest
94 profile.NoRetarget = false
95 profile.RetargetInterval = 4
96 profile.TargetTimespan = 2400
97 store := NewMemory()
98 tip := seedHeaders(t, &profile, store, 5, 600)
99 c := testClient(t, &profile, store)
100 // 0x2000ffff is a valid, easy target but not the regtest pow limit the
101 // previous header uses, so only the expected-bits rule can reject it.
102 h := mineAt(t, tip, profile.GenesisHeader.Timestamp+6*600, 0x2000ffff, 50)
103 // The header satisfies its own target, so only the expected-bits rule can
104 // reject it.
105 if err := c.validateHeader(6, &h, tip); err == nil {
106 t.Fatal("accepted the wrong difficulty bits")
107 }
108 }
109
110 func bigInt(v int64) *big.Int { return big.NewInt(v) }
111
112 func TestRetargetExpectedBits(t *testing.T) {
113 profile := chain.Regtest
114 profile.NoRetarget = false
115 profile.RetargetInterval = 4
116 profile.TargetTimespan = 2400
117 profile.TargetSpacing = 600
118 store := NewMemory()
119 seedHeaders(t, &profile, store, 4, 600)
120 c := testClient(t, &profile, store)
121
122 bits3, ok := c.expectedBits(3, nil)
123 if !ok || bits3 != profile.PowLimitBits {
124 t.Fatalf("height 3 bits %08x ok=%v", bits3, ok)
125 }
126 bits4, ok := c.expectedBits(4, nil)
127 if !ok {
128 t.Fatal("height 4 did not determine the retarget")
129 }
130 first, _ := store.Header(0)
131 last, _ := store.Header(3)
132 actual := int64(last.Timestamp) - int64(first.Timestamp)
133 oldTarget, _ := btcwire.CompactToBig(last.Bits)
134 want := oldTarget.Mul(oldTarget, bigInt(actual))
135 want.Div(want, bigInt(profile.TargetTimespan))
136 limit, _ := btcwire.CompactToBig(profile.PowLimitBits)
137 if want.Cmp(limit) > 0 {
138 want = limit
139 }
140 if bits4 != BigToCompact(want) {
141 t.Fatalf("height 4 bits %08x, want %08x", bits4, BigToCompact(want))
142 }
143 }
144