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