chaindata_test.go raw

   1  package chaindata
   2  
   3  import (
   4  	"encoding/binary"
   5  	"errors"
   6  	"strings"
   7  	"testing"
   8  	"time"
   9  
  10  	"git.smesh.lol/tiny-spawn"
  11  )
  12  
  13  // fakeSource is a chain that answers from memory and can be told to block until cancelled.
  14  type fakeSource struct {
  15  	position Position
  16  	heights  []uint32
  17  	filters  []Filter
  18  	failure  string
  19  	// block makes Sync wait until the request is cancelled, which is how the cancel path is
  20  	// exercised without a timer.
  21  	block bool
  22  	// seen records the scripts of the last match.
  23  	seen [][]byte
  24  	// from records the last sync height.
  25  	from uint32
  26  	// asked records the last filter range.
  27  	askedFrom, askedCount uint32
  28  	// rawBlocks is what Block answers, by height.
  29  	rawBlocks map[uint32][]byte
  30  	// askedBlock records the last block height.
  31  	askedBlock uint32
  32  	// broadcast records the last transaction the source was handed.
  33  	broadcast []byte
  34  }
  35  
  36  // Position implements Source.
  37  func (f *fakeSource) Position(Context) (Position, error) {
  38  	if f.failure != "" {
  39  		return Position{}, errors.New(f.failure)
  40  	}
  41  
  42  	return f.position, nil
  43  }
  44  
  45  // Sync implements Source.
  46  func (f *fakeSource) Sync(ctx Context, from uint32) (Position, error) {
  47  	f.from = from
  48  
  49  	if f.failure != "" {
  50  		return Position{}, errors.New(f.failure)
  51  	}
  52  
  53  	if f.block {
  54  		<-ctx.Done()
  55  
  56  		return Position{}, errors.New(ctx.Err())
  57  	}
  58  
  59  	return Position{Tip: from + 1, Filters: from + 1}, nil
  60  }
  61  
  62  // Match implements Source.
  63  func (f *fakeSource) Match(_ Context, scripts [][]byte) ([]uint32, error) {
  64  	f.seen = scripts
  65  
  66  	if f.failure != "" {
  67  		return nil, errors.New(f.failure)
  68  	}
  69  
  70  	return f.heights, nil
  71  }
  72  
  73  // Block implements Source.
  74  func (f *fakeSource) Block(_ Context, height uint32) ([]byte, error) {
  75  	f.askedBlock = height
  76  
  77  	if f.failure != "" {
  78  		return nil, errors.New(f.failure)
  79  	}
  80  
  81  	raw, ok := f.rawBlocks[height]
  82  
  83  	if !ok {
  84  		return nil, errors.New("no block at that height")
  85  	}
  86  
  87  	return raw, nil
  88  }
  89  
  90  // Broadcast implements Source.
  91  func (f *fakeSource) Broadcast(_ Context, raw []byte) error {
  92  	f.broadcast = append([]byte(nil), raw...)
  93  
  94  	if f.failure != "" {
  95  		return errors.New(f.failure)
  96  	}
  97  
  98  	return nil
  99  }
 100  
 101  // Filters implements Source.
 102  func (f *fakeSource) Filters(_ Context, from, count uint32) ([]Filter, error) {
 103  	f.askedFrom, f.askedCount = from, count
 104  
 105  	if f.failure != "" {
 106  		return nil, errors.New(f.failure)
 107  	}
 108  
 109  	return f.filters, nil
 110  }
 111  
 112  // harness is one domain and one client wired through a duplex.
 113  type harness struct {
 114  	client *Client
 115  	stop   tinyspawn.Breaker
 116  	done   <-chan struct{}
 117  	source *fakeSource
 118  }
 119  
 120  // start spawns a domain over a fake source and returns the client.
 121  func start(t *testing.T, source *fakeSource) *harness {
 122  	t.Helper()
 123  
 124  	server, wallet := tinyspawn.NewBufferedDuplex[[]byte](8)
 125  	serverStop := tinyspawn.NewBreaker()
 126  	clientStop := tinyspawn.NewBreaker()
 127  
 128  	done := tinyspawn.Spawn(Serve, Data{Source: source},
 129  		Chans{Link: server, Stop: serverStop})
 130  
 131  	client := NewClient("test", wallet, clientStop)
 132  
 133  	t.Cleanup(func() {
 134  		client.Close()
 135  
 136  		if !serverStop.Closed() {
 137  			serverStop.Close()
 138  		}
 139  
 140  		select {
 141  		case <-done:
 142  		case <-time.After(5 * time.Second):
 143  			t.Error("the domain did not stop")
 144  		}
 145  	})
 146  
 147  	return &harness{client: client, stop: serverStop, done: done, source: source}
 148  }
 149  
 150  // TestCodecRoundTrip: every kind and method survives encode and decode, payload included.
 151  func TestCodecRoundTrip(t *testing.T) {
 152  	for _, frame := range []Frame{
 153  		{Kind: KindRequest, ID: 1, Method: MethodStatus},
 154  		{Kind: KindRequest, ID: 2, Method: MethodSync,
 155  			Payload: binary.LittleEndian.AppendUint32(nil, 42)},
 156  		{Kind: KindResult, ID: 3, Method: MethodMatch, Status: StatusOK,
 157  			Payload: PutHeights(nil, []uint32{7, 9, 11})},
 158  		{Kind: KindCancel, ID: 4},
 159  		{Kind: KindClose, Status: StatusClosed, Payload: []byte("bye\x00")},
 160  	} {
 161  		raw, err := Encode(frame)
 162  
 163  		if err != nil {
 164  			t.Fatalf("%+v: %v", frame, err)
 165  		}
 166  
 167  		if len(raw) != HeaderLen+len(frame.Payload) {
 168  			t.Fatalf("encoded %d bytes for a %d-byte payload", len(raw),
 169  				len(frame.Payload))
 170  		}
 171  
 172  		decoded, err := Decode(raw)
 173  
 174  		if err != nil {
 175  			t.Fatalf("%+v: %v", frame, err)
 176  		}
 177  
 178  		if decoded.Kind != frame.Kind || decoded.ID != frame.ID ||
 179  			decoded.Method != frame.Method || decoded.Status != frame.Status {
 180  			t.Fatalf("decoded %+v from %+v", decoded, frame)
 181  		}
 182  
 183  		if string(decoded.Payload) != string(frame.Payload) {
 184  			t.Fatalf("payload %q from %q", decoded.Payload, frame.Payload)
 185  		}
 186  	}
 187  }
 188  
 189  // TestDecodeRefusesBadFrames: the shapes a decoder must not trust, each with its own
 190  // refusal, because a frame arrives from another instance.
 191  func TestDecodeRefusesBadFrames(t *testing.T) {
 192  	good, err := Encode(Frame{Kind: KindRequest, ID: 1, Method: MethodStatus})
 193  
 194  	if err != nil {
 195  		t.Fatal(err)
 196  	}
 197  
 198  	cases := map[string][]byte{
 199  		"short":          good[:HeaderLen-1],
 200  		"empty":          nil,
 201  		"wrong magic":    mutate(good, 0, 0xff),
 202  		"wrong version":  mutate(good, 4, 9),
 203  		"unknown kind":   mutate(good, 5, 9),
 204  		"trailing bytes": append(append([]byte{}, good...), 0x00),
 205  		"length lies":    mutate(good, 16, 200),
 206  		"huge length":    mutate(good, 19, 0xff),
 207  	}
 208  
 209  	for name, raw := range cases {
 210  		if _, err := Decode(raw); err == nil {
 211  			t.Errorf("%s was decoded", name)
 212  		} else if !errors.Is(err, ErrBadFrame) {
 213  			t.Errorf("%s failed with %v", name, err)
 214  		}
 215  	}
 216  
 217  	// A payload beyond the cap is refused before it is allocated.
 218  	large := Frame{Kind: KindRequest, Method: MethodMatch,
 219  		Payload: make([]byte, MaxPayload+1)}
 220  
 221  	if _, err := Encode(large); !errors.Is(err, ErrPayloadTooLarge) {
 222  		t.Fatalf("an oversized payload was encoded: %v", err)
 223  	}
 224  }
 225  
 226  // mutate copies raw and changes one byte.
 227  func mutate(raw []byte, at int, value byte) []byte {
 228  	out := append([]byte{}, raw...)
 229  
 230  	if at < len(out) {
 231  		out[at] = value
 232  	}
 233  
 234  	return out
 235  }
 236  
 237  // TestPayloadCodecsRefuseCounts: a count larger than the payload is refused before
 238  // anything is allocated from it.
 239  func TestPayloadCodecsRefuseCounts(t *testing.T) {
 240  	huge := binary.LittleEndian.AppendUint32(nil, 0xffffffff)
 241  
 242  	if _, err := ReadHeights(huge); err == nil {
 243  		t.Error("a height list with no heights behind its count was read")
 244  	}
 245  
 246  	if _, err := ReadScripts(huge); err == nil {
 247  		t.Error("a script list with no scripts behind its count was read")
 248  	}
 249  
 250  	if _, err := ReadFilters(huge); err == nil {
 251  		t.Error("a filter batch with nothing behind its count was read")
 252  	}
 253  
 254  	if _, _, err := ReadFilterRange(make([]byte, 3)); err == nil {
 255  		t.Error("a 3-byte filter range was read")
 256  	}
 257  
 258  	if _, _, err := ReadFilterRange(PutFilterRange(nil, 0, MaxFilters+1)); err == nil {
 259  		t.Error("an oversized filter range was read")
 260  	}
 261  
 262  	// Trailing bytes after a complete list are a malformed payload, not a second list.
 263  	trailing := append(PutHeights(nil, []uint32{1}), 0x00)
 264  
 265  	if _, err := ReadHeights(trailing); err == nil {
 266  		t.Error("trailing bytes were accepted")
 267  	}
 268  }
 269  
 270  // TestScriptsAndFiltersRoundTrip: the two payloads that carry byte strings.
 271  func TestScriptsAndFiltersRoundTrip(t *testing.T) {
 272  	scripts := [][]byte{{0x00, 0x14, 0x75, 0x1e}, {0x51}, {0x6a}}
 273  
 274  	payload, err := PutScripts(nil, scripts)
 275  
 276  	if err != nil {
 277  		t.Fatal(err)
 278  	}
 279  
 280  	decoded, err := ReadScripts(payload)
 281  
 282  	if err != nil {
 283  		t.Fatal(err)
 284  	}
 285  
 286  	if len(decoded) != len(scripts) {
 287  		t.Fatalf("%d scripts from %d", len(decoded), len(scripts))
 288  	}
 289  
 290  	for i := range scripts {
 291  		if string(decoded[i]) != string(scripts[i]) {
 292  			t.Fatalf("script %d is %x", i, decoded[i])
 293  		}
 294  	}
 295  
 296  	hash := make([]byte, 32)
 297  
 298  	for i := range hash {
 299  		hash[i] = byte(i)
 300  	}
 301  
 302  	filters := []Filter{
 303  		{Height: 100, BlockHash: hash, Filter: []byte{0x01, 0x02}},
 304  		{Height: 101, BlockHash: hash, Filter: []byte{0x03}},
 305  	}
 306  
 307  	batch, err := PutFilters(nil, filters)
 308  
 309  	if err != nil {
 310  		t.Fatal(err)
 311  	}
 312  
 313  	back, err := ReadFilters(batch)
 314  
 315  	if err != nil {
 316  		t.Fatal(err)
 317  	}
 318  
 319  	if len(back) != 2 || back[0].Height != 100 || back[1].Height != 101 {
 320  		t.Fatalf("filters = %+v", back)
 321  	}
 322  
 323  	if string(back[0].Filter) != string(filters[0].Filter) ||
 324  		string(back[1].BlockHash) != string(hash) {
 325  		t.Fatalf("the batch did not survive: %+v", back[0])
 326  	}
 327  
 328  	// A filter whose hash is not 32 bytes is refused rather than truncated.
 329  	if _, err := PutFilters(nil, []Filter{{Height: 1, BlockHash: []byte{0x01}}}); err == nil {
 330  		t.Fatal("a short block hash was encoded")
 331  	}
 332  }
 333  
 334  // TestStatusSyncMatchAndFilters: the four commands over the channel.
 335  func TestStatusSyncMatchAndFilters(t *testing.T) {
 336  	hash := make([]byte, 32)
 337  
 338  	source := &fakeSource{
 339  		position: Position{Tip: 90, Filters: 88, Running: false},
 340  		heights:  []uint32{91, 95},
 341  		filters:  []Filter{{Height: 91, BlockHash: hash, Filter: []byte{0x07}}},
 342  	}
 343  
 344  	run := start(t, source)
 345  
 346  	position, err := run.client.Position()
 347  
 348  	if err != nil {
 349  		t.Fatal(err)
 350  	}
 351  
 352  	if position.Tip != 90 || position.Filters != 88 || position.Running {
 353  		t.Fatalf("position = %+v", position)
 354  	}
 355  
 356  	synced, err := run.client.Sync(90)
 357  
 358  	if err != nil {
 359  		t.Fatal(err)
 360  	}
 361  
 362  	if source.from != 90 || synced.Tip != 91 {
 363  		t.Fatalf("sync from %d answered %+v", source.from, synced)
 364  	}
 365  
 366  	script := []byte{0x00, 0x14, 0x75, 0x1e}
 367  
 368  	heights, err := run.client.Match([][]byte{script})
 369  
 370  	if err != nil {
 371  		t.Fatal(err)
 372  	}
 373  
 374  	if len(heights) != 2 || heights[0] != 91 || heights[1] != 95 {
 375  		t.Fatalf("heights = %v", heights)
 376  	}
 377  
 378  	if len(source.seen) != 1 || string(source.seen[0]) != string(script) {
 379  		t.Fatalf("the source saw %x", source.seen)
 380  	}
 381  
 382  	filters, err := run.client.Filters(91, 2)
 383  
 384  	if err != nil {
 385  		t.Fatal(err)
 386  	}
 387  
 388  	if source.askedFrom != 91 || source.askedCount != 2 {
 389  		t.Fatalf("the source was asked %d/%d", source.askedFrom, source.askedCount)
 390  	}
 391  
 392  	if len(filters) != 1 || filters[0].Height != 91 || len(filters[0].Filter) != 1 {
 393  		t.Fatalf("filters = %+v", filters)
 394  	}
 395  }
 396  
 397  // TestFailureTravels: a chain that fails is a refusal the wallet reads, not a silence.
 398  func TestFailureTravels(t *testing.T) {
 399  	run := start(t, &fakeSource{failure: "the bridge is unreachable"})
 400  
 401  	if _, err := run.client.Position(); err == nil ||
 402  		!strings.Contains(err.Error(), "bridge is unreachable") {
 403  		t.Fatalf("position failed with %v", err)
 404  	}
 405  
 406  	if _, err := run.client.Sync(0); err == nil ||
 407  		!strings.Contains(err.Error(), "bridge is unreachable") {
 408  		t.Fatalf("sync failed with %v", err)
 409  	}
 410  
 411  	if _, err := run.client.Match([][]byte{{0x51}}); err == nil ||
 412  		!strings.Contains(err.Error(), "bridge is unreachable") {
 413  		t.Fatalf("match failed with %v", err)
 414  	}
 415  
 416  	if _, err := run.client.Filters(0, 1); err == nil ||
 417  		!strings.Contains(err.Error(), "bridge is unreachable") {
 418  		t.Fatalf("filters failed with %v", err)
 419  	}
 420  }
 421  
 422  // TestUnknownMethodAndBadPayload: a request the far end cannot serve is answered, because a
 423  // wallet waiting for an answer that never comes has to be reloaded.
 424  func TestUnknownMethodAndBadPayload(t *testing.T) {
 425  	run := start(t, &fakeSource{})
 426  
 427  	if _, err := run.client.Do(Frame{Method: Method(99)}); err == nil ||
 428  		!strings.Contains(err.Error(), "unknown chain-data method") {
 429  		t.Fatalf("an unknown method was answered with %v", err)
 430  	}
 431  
 432  	// A match with no scripts is refused by the client before it is sent.
 433  	if _, err := run.client.Match(nil); err == nil {
 434  		t.Fatal("a match with no scripts was sent")
 435  	}
 436  
 437  	// A sync without its height is a malformed payload, refused by the far end.
 438  	if _, err := run.client.Do(Frame{Method: MethodSync}); err == nil ||
 439  		!strings.Contains(err.Error(), "4-byte height") {
 440  		t.Fatalf("a sync with no height answered %v", err)
 441  	}
 442  }
 443  
 444  // TestCancelStopsARequest: a cancel frame reaches a request that is already running.
 445  func TestCancelStopsARequest(t *testing.T) {
 446  	run := start(t, &fakeSource{block: true})
 447  
 448  	answers := make(chan error, 1)
 449  
 450  	go func() {
 451  		_, err := run.client.Sync(1)
 452  		answers <- err
 453  	}()
 454  
 455  	// Wait until the request is in flight, then cancel it.
 456  	deadline := time.Now().Add(5 * time.Second)
 457  
 458  	for time.Now().Before(deadline) {
 459  		run.client.mu.Lock()
 460  		pending := len(run.client.pending)
 461  		run.client.mu.Unlock()
 462  
 463  		if pending > 0 {
 464  			break
 465  		}
 466  
 467  		time.Sleep(time.Millisecond)
 468  	}
 469  
 470  	run.client.mu.Lock()
 471  	ids := make([]uint32, 0, len(run.client.pending))
 472  
 473  	for id := range run.client.pending {
 474  		ids = append(ids, id)
 475  	}
 476  
 477  	run.client.mu.Unlock()
 478  
 479  	if len(ids) != 1 {
 480  		t.Fatalf("%d requests in flight", len(ids))
 481  	}
 482  
 483  	run.client.Cancel(ids[0])
 484  
 485  	select {
 486  	case err := <-answers:
 487  		if err == nil || !strings.Contains(err.Error(), "cancel") {
 488  			t.Fatalf("the cancelled sync answered %v", err)
 489  		}
 490  	case <-time.After(5 * time.Second):
 491  		t.Fatal("the cancel did not reach the request")
 492  	}
 493  }
 494  
 495  // TestCloseFailsWhatIsInFlight: a close frame means the far end is going away, so a waiter
 496  // is told rather than left waiting.
 497  func TestCloseFailsWhatIsInFlight(t *testing.T) {
 498  	server, wallet := tinyspawn.NewBufferedDuplex[[]byte](4)
 499  	clientStop := tinyspawn.NewBreaker()
 500  
 501  	client := NewClient("test", wallet, clientStop)
 502  
 503  	answers := make(chan error, 1)
 504  
 505  	go func() {
 506  		_, err := client.Position()
 507  		answers <- err
 508  	}()
 509  
 510  	// Read the request on the far end and answer it with a close instead of a result.
 511  	select {
 512  	case <-server.Recv():
 513  	case <-time.After(5 * time.Second):
 514  		t.Fatal("the request never arrived")
 515  	}
 516  
 517  	closeFrame, err := Encode(Frame{Kind: KindClose, Status: StatusClosed,
 518  		Payload: []byte("worker terminated\x00")})
 519  
 520  	if err != nil {
 521  		t.Fatal(err)
 522  	}
 523  
 524  	select {
 525  	case server.Tx <- closeFrame:
 526  	case <-time.After(5 * time.Second):
 527  		t.Fatal("the close frame could not be sent")
 528  	}
 529  
 530  	select {
 531  	case err := <-answers:
 532  		if err == nil || !strings.Contains(err.Error(), "worker terminated") {
 533  			t.Fatalf("the close answered %v", err)
 534  		}
 535  	case <-time.After(5 * time.Second):
 536  		t.Fatal("the close did not fail the request")
 537  	}
 538  
 539  	if !client.Closed() {
 540  		t.Fatal("the client does not report itself closed")
 541  	}
 542  
 543  	// A request after the close fails immediately rather than waiting.
 544  	if _, err := client.Position(); err == nil {
 545  		t.Fatal("a closed client accepted a request")
 546  	}
 547  
 548  	_ = server
 549  }
 550  
 551  // TestEndpointCloseFailsTheDomain: the channel closing under the domain is treated like a
 552  // close frame, and the loop ends.
 553  func TestEndpointCloseFailsTheDomain(t *testing.T) {
 554  	server, wallet := tinyspawn.NewBufferedDuplex[[]byte](4)
 555  	serverStop := tinyspawn.NewBreaker()
 556  
 557  	done := tinyspawn.Spawn(Serve, Data{Source: &fakeSource{}},
 558  		Chans{Link: server, Stop: serverStop})
 559  
 560  	// Closing the wallet's end is what a terminated worker looks like from inside. The
 561  	// domain reads that end's transmit side, so closing it ends the loop.
 562  	close(wallet.Tx)
 563  
 564  	select {
 565  	case <-done:
 566  	case <-time.After(5 * time.Second):
 567  		t.Fatal("the domain did not notice the channel closing")
 568  	}
 569  }
 570  
 571  // TestRandomBytesNeverPanic: the decoder and the payload readers are the surface another
 572  // instance's bytes reach, so no input may panic.
 573  func TestRandomBytesNeverPanic(t *testing.T) {
 574  	defer func() {
 575  		if recovered := recover(); recovered != nil {
 576  			t.Fatalf("a decoder panicked: %v", recovered)
 577  		}
 578  	}()
 579  
 580  	state := uint32(0x12345678)
 581  
 582  	next := func() byte {
 583  		state = state*1664525 + 1013904223
 584  
 585  		return byte(state >> 24)
 586  	}
 587  
 588  	for i := 0; i < 2000; i++ {
 589  		size := int(next()) + 1
 590  		raw := make([]byte, size)
 591  
 592  		for j := range raw {
 593  			raw[j] = next()
 594  		}
 595  
 596  		frame, err := Decode(raw)
 597  
 598  		if err != nil {
 599  			continue
 600  		}
 601  
 602  		_, _ = ReadPosition(frame.Payload)
 603  		_, _ = ReadHeights(frame.Payload)
 604  		_, _ = ReadScripts(frame.Payload)
 605  		_, _ = ReadFilters(frame.Payload)
 606  		_, _, _ = ReadFilterRange(frame.Payload)
 607  	}
 608  }
 609  
 610  // TestBlockRoundTrip: a block request carries a height and an answer carries the raw bytes.
 611  func TestBlockRoundTrip(t *testing.T) {
 612  	raw := make([]byte, 300)
 613  
 614  	for index := range raw {
 615  		raw[index] = byte(index)
 616  	}
 617  
 618  	source := &fakeSource{rawBlocks: map[uint32][]byte{120: raw}}
 619  	h := start(t, source)
 620  
 621  	got, err := h.client.Block(120)
 622  
 623  	if err != nil {
 624  		t.Fatal(err)
 625  	}
 626  
 627  	if string(got) != string(raw) {
 628  		t.Fatalf("the block is %d bytes, want %d", len(got), len(raw))
 629  	}
 630  
 631  	if source.askedBlock != 120 {
 632  		t.Fatalf("the source was asked for %d", source.askedBlock)
 633  	}
 634  }
 635  
 636  // TestABroadcastCarriesTheRawTransaction: the wallet signs locally, and the channel carries
 637  // the bytes to the chain unchanged. The answer is a status, not a second copy of the
 638  // transaction, because the peer never echoes it.
 639  func TestABroadcastCarriesTheRawTransaction(t *testing.T) {
 640  	raw := []byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9c, 0xff}
 641  
 642  	source := &fakeSource{}
 643  	h := start(t, source)
 644  
 645  	if err := h.client.Broadcast(raw); err != nil {
 646  		t.Fatal(err)
 647  	}
 648  
 649  	if string(source.broadcast) != string(raw) {
 650  		t.Fatalf("the source was handed %x, want %x", source.broadcast, raw)
 651  	}
 652  }
 653  
 654  // TestAnEmptyBroadcastIsRefused: there is nothing to send, and sending an empty tx frame would
 655  // be a protocol error at the peer rather than at the caller.
 656  func TestAnEmptyBroadcastIsRefused(t *testing.T) {
 657  	h := start(t, &fakeSource{})
 658  
 659  	if err := h.client.Broadcast(nil); err == nil {
 660  		t.Fatal("an empty transaction was broadcast")
 661  	}
 662  
 663  	answer, err := h.client.Do(Frame{Method: MethodBroadcast, Payload: nil})
 664  
 665  	if err == nil {
 666  		t.Fatal("an empty broadcast frame was answered")
 667  	}
 668  
 669  	if answer.Status != StatusMalformed {
 670  		t.Fatalf("the status is %v", answer.Status)
 671  	}
 672  }
 673  
 674  // TestAFailedBroadcastIsAnError: a chain that refuses the transaction says so, and the caller
 675  // does not mistake a refusal for a broadcast.
 676  func TestAFailedBroadcastIsAnError(t *testing.T) {
 677  	source := &fakeSource{failure: "the peer is gone"}
 678  	h := start(t, source)
 679  
 680  	if err := h.client.Broadcast([]byte{1, 2, 3}); err == nil {
 681  		t.Fatal("a refused broadcast was reported as sent")
 682  	}
 683  }
 684  
 685  // TestBlockHeightMustBeFourBytes: a malformed request is refused, not interpreted.
 686  func TestBlockHeightMustBeFourBytes(t *testing.T) {
 687  	source := &fakeSource{rawBlocks: map[uint32][]byte{}}
 688  	h := start(t, source)
 689  
 690  	answer, err := h.client.Do(Frame{Method: MethodBlock, Payload: []byte{1, 2}})
 691  
 692  	if err == nil {
 693  		t.Fatal("a two-byte height was accepted")
 694  	}
 695  
 696  	if answer.Status != StatusMalformed {
 697  		t.Fatalf("the status is %v", answer.Status)
 698  	}
 699  }
 700  
 701  // TestAnOversizedAnswerIsRefused: the transport will not carry a payload past its maximum, and
 702  // the request fails rather than returning a truncated block.
 703  func TestAnOversizedAnswerIsRefused(t *testing.T) {
 704  	source := &fakeSource{rawBlocks: map[uint32][]byte{1: make([]byte, MaxPayload+1)}}
 705  	h := start(t, source)
 706  
 707  	if _, err := h.client.Block(1); err == nil {
 708  		t.Fatal("an oversized block was answered")
 709  	}
 710  }
 711  
 712  // TestAFailedBlockIsAnError: a chain that cannot produce the block says so.
 713  func TestAFailedBlockIsAnError(t *testing.T) {
 714  	source := &fakeSource{failure: "the peer is gone"}
 715  	h := start(t, source)
 716  
 717  	if _, err := h.client.Block(3); err == nil {
 718  		t.Fatal("a failed block fetch reported success")
 719  	}
 720  }
 721  
 722  // TestAnUnansweredRequestFailsWithAReason: a chain channel that never answers must not wait
 723  // for ever. A browser whose light-client worker never comes up is exactly this case, and the
 724  // old behaviour was a page spinner and no error anywhere.
 725  func TestAnUnansweredRequestFailsWithAReason(t *testing.T) {
 726  	// One end of a duplex and no reader on the other: the frame is accepted and nothing
 727  	// ever answers it.
 728  	_, wallet := tinyspawn.NewBufferedDuplex[[]byte](1)
 729  	client := NewClient("btc", wallet, tinyspawn.NewBreaker())
 730  	client.SetTimeout(20 * time.Millisecond)
 731  
 732  	started := time.Now()
 733  	_, err := client.Do(Frame{Method: MethodStatus})
 734  
 735  	if !errors.Is(err, ErrNoAnswer) {
 736  		t.Fatalf("an unanswered request: %v, want ErrNoAnswer", err)
 737  	}
 738  
 739  	if elapsed := time.Since(started); elapsed > 2*time.Second {
 740  		t.Fatalf("the request waited %s, the bound was 20ms", elapsed)
 741  	}
 742  
 743  	if !strings.Contains(err.Error(), "btc") || !strings.Contains(err.Error(), "chain channel") {
 744  		t.Fatalf("the error does not name the chain and the cause: %v", err)
 745  	}
 746  }
 747  
 748  // TestTheAnswerDeadlineDependsOnTheMethod: a status that takes a minute is a dead channel; a
 749  // first sync that takes minutes is doing its job. The default deadline must therefore not apply
 750  // to Sync, while an explicit one applies to everything.
 751  func TestTheAnswerDeadlineDependsOnTheMethod(t *testing.T) {
 752  	_, wallet := tinyspawn.NewBufferedDuplex[[]byte](1)
 753  	client := NewClient("btc", wallet, tinyspawn.NewBreaker())
 754  
 755  	if got := client.requestTimeout(MethodSync); got != 0 {
 756  		t.Fatalf("the default deadline for Sync = %s, want none", got)
 757  	}
 758  
 759  	if got := client.requestTimeout(MethodStatus); got != DefaultRequestTimeout {
 760  		t.Fatalf("the default deadline for status = %s, want %s", got, DefaultRequestTimeout)
 761  	}
 762  
 763  	client.SetTimeout(20 * time.Millisecond)
 764  
 765  	if got := client.requestTimeout(MethodSync); got != 20*time.Millisecond {
 766  		t.Fatalf("Sync under an explicit deadline = %s, want 20ms", got)
 767  	}
 768  
 769  	if got := client.requestTimeout(MethodStatus); got != 20*time.Millisecond {
 770  		t.Fatalf("status under an explicit deadline = %s, want 20ms", got)
 771  	}
 772  
 773  	// Zero restores the per-method defaults, so Sync is unbounded again.
 774  	client.SetTimeout(0)
 775  
 776  	if got := client.requestTimeout(MethodSync); got != 0 {
 777  		t.Fatalf("Sync after SetTimeout(0) = %s, want none", got)
 778  	}
 779  
 780  	if got := client.requestTimeout(MethodStatus); got != DefaultRequestTimeout {
 781  		t.Fatalf("status after SetTimeout(0) = %s, want %s", got, DefaultRequestTimeout)
 782  	}
 783  }
 784  
 785  // TestAnExplicitDeadlineBoundsSync: the default leaves Sync alone, but a caller that sets a
 786  // deadline means it, so an unanswered sync does not wait for ever behind the exemption.
 787  func TestAnExplicitDeadlineBoundsSync(t *testing.T) {
 788  	_, wallet := tinyspawn.NewBufferedDuplex[[]byte](1)
 789  	client := NewClient("btc", wallet, tinyspawn.NewBreaker())
 790  	client.SetTimeout(20 * time.Millisecond)
 791  
 792  	started := time.Now()
 793  	_, err := client.Do(Frame{Method: MethodSync})
 794  
 795  	if !errors.Is(err, ErrNoAnswer) {
 796  		t.Fatalf("an unanswered sync under an explicit deadline: %v, want ErrNoAnswer", err)
 797  	}
 798  
 799  	if elapsed := time.Since(started); elapsed > 2*time.Second {
 800  		t.Fatalf("the sync waited %s, the explicit bound was 20ms", elapsed)
 801  	}
 802  }
 803