package neutrino import ( "math/big" "testing" "git.smesh.lol/nostrino/pkg/btcwire" "git.smesh.lol/nostrino/pkg/chain" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) func mineAt(t *testing.T, prev chainhash.Hash, timestamp, bits uint32, tag byte) btcwire.BlockHeader { t.Helper() h := btcwire.BlockHeader{ Version: 1, PrevBlock: prev, MerkleRoot: chainhash.DoubleHashH([]byte{tag}), Timestamp: timestamp, Bits: bits, } for nonce := uint32(0); nonce < 1_000_000; nonce++ { h.Nonce = nonce if h.CheckProofOfWork() { return h } } t.Fatalf("could not mine at %d", timestamp) return btcwire.BlockHeader{} } func testClient(t *testing.T, profile *chain.Profile, store Store) *Client { t.Helper() c, err := New(Config{Profile: profile, Store: store, Address: "127.0.0.1:1"}) if err != nil { t.Fatal(err) } return c } func seedHeaders(t *testing.T, profile *chain.Profile, store Store, count int, step uint32) chainhash.Hash { t.Helper() if err := store.SetAnchor(Anchor{Height: 0, BlockHash: profile.GenesisHash, FilterHeight: -1}); err != nil { t.Fatal(err) } genesis := profile.GenesisHeader if err := store.PutHeaders(0, []btcwire.BlockHeader{genesis}); err != nil { t.Fatal(err) } prev := genesis.BlockHash() for i := 1; i <= count; i++ { h := mineAt(t, prev, genesis.Timestamp+step*uint32(i), profile.PowLimitBits, byte(i)) if err := store.PutHeaders(uint32(i), []btcwire.BlockHeader{h}); err != nil { t.Fatal(err) } prev = h.BlockHash() } return prev } func TestBigToCompactRoundTrip(t *testing.T) { for _, bits := range []uint32{0x1d00ffff, 0x207fffff, 0x1b0404cb, 0x1e0377ae} { target, err := btcwire.CompactToBig(bits) if err != nil { t.Fatalf("%08x: %v", bits, err) } if got := BigToCompact(target); got != bits { t.Fatalf("%08x round trips to %08x", bits, got) } } if BigToCompact(nil) != 0 { t.Fatal("nil target is not zero") } } func TestMedianTimePast(t *testing.T) { profile := chain.Regtest store := NewMemory() tip := seedHeaders(t, &profile, store, 11, 600) c := testClient(t, &profile, store) tipHeader, _ := store.Header(11) old := mineAt(t, tip, profile.GenesisHeader.Timestamp+600, profile.PowLimitBits, 99) if err := c.validateHeader(12, &old, tip); err == nil { t.Fatal("accepted a header at or below the median time") } good := mineAt(t, tip, tipHeader.Timestamp+600, profile.PowLimitBits, 100) if err := c.validateHeader(12, &good, tip); err != nil { t.Fatalf("good header rejected: %v", err) } } func TestDifficultyBits(t *testing.T) { profile := chain.Regtest profile.NoRetarget = false profile.RetargetInterval = 4 profile.TargetTimespan = 2400 store := NewMemory() tip := seedHeaders(t, &profile, store, 5, 600) c := testClient(t, &profile, store) // 0x2000ffff is a valid, easy target but not the regtest pow limit the // previous header uses, so only the expected-bits rule can reject it. h := mineAt(t, tip, profile.GenesisHeader.Timestamp+6*600, 0x2000ffff, 50) // The header satisfies its own target, so only the expected-bits rule can // reject it. if err := c.validateHeader(6, &h, tip); err == nil { t.Fatal("accepted the wrong difficulty bits") } } func bigInt(v int64) *big.Int { return big.NewInt(v) } func TestRetargetExpectedBits(t *testing.T) { profile := chain.Regtest profile.NoRetarget = false profile.RetargetInterval = 4 profile.TargetTimespan = 2400 profile.TargetSpacing = 600 store := NewMemory() seedHeaders(t, &profile, store, 4, 600) c := testClient(t, &profile, store) bits3, ok := c.expectedBits(3, nil) if !ok || bits3 != profile.PowLimitBits { t.Fatalf("height 3 bits %08x ok=%v", bits3, ok) } bits4, ok := c.expectedBits(4, nil) if !ok { t.Fatal("height 4 did not determine the retarget") } first, _ := store.Header(0) last, _ := store.Header(3) actual := int64(last.Timestamp) - int64(first.Timestamp) oldTarget, _ := btcwire.CompactToBig(last.Bits) want := oldTarget.Mul(oldTarget, bigInt(actual)) want.Div(want, bigInt(profile.TargetTimespan)) limit, _ := btcwire.CompactToBig(profile.PowLimitBits) if want.Cmp(limit) > 0 { want = limit } if bits4 != BigToCompact(want) { t.Fatalf("height 4 bits %08x, want %08x", bits4, BigToCompact(want)) } }