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