| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package tka
- import (
- "bytes"
- "strconv"
- "testing"
- "github.com/google/go-cmp/cmp"
- )
- func TestSyncOffer(t *testing.T) {
- c := newTestchain(t, `
- A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
- A10 -> A11 -> A12 -> A13 -> A14 -> A15 -> A16 -> A17 -> A18
- A18 -> A19 -> A20 -> A21 -> A22 -> A23 -> A24 -> A25
- `)
- storage := c.Chonk()
- a, err := Open(storage)
- if err != nil {
- t.Fatal(err)
- }
- got, err := a.SyncOffer(storage)
- if err != nil {
- t.Fatal(err)
- }
- // A SyncOffer includes a selection of AUMs going backwards in the tree,
- // progressively skipping more and more each iteration.
- want := SyncOffer{
- Head: c.AUMHashes["A25"],
- Ancestors: []AUMHash{
- c.AUMHashes["A"+strconv.Itoa(25-ancestorsSkipStart)],
- c.AUMHashes["A"+strconv.Itoa(25-ancestorsSkipStart<<ancestorsSkipShift)],
- c.AUMHashes["A1"],
- },
- }
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("SyncOffer diff (-want, +got):\n%s", diff)
- }
- }
- func TestComputeSyncIntersection_FastForward(t *testing.T) {
- // Node 1 has: A1 -> A2
- // Node 2 has: A1 -> A2 -> A3 -> A4
- c := newTestchain(t, `
- A1 -> A2 -> A3 -> A4
- `)
- a1H, a2H := c.AUMHashes["A1"], c.AUMHashes["A2"]
- chonk1 := c.ChonkWith("A1", "A2")
- n1, err := Open(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- offer1, err := n1.SyncOffer(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- chonk2 := c.Chonk() // All AUMs
- n2, err := Open(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- offer2, err := n2.SyncOffer(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- // Node 1 only knows about the first two nodes, so the head of n2 is
- // alien to it.
- t.Run("n1", func(t *testing.T) {
- got, err := computeSyncIntersection(chonk1, offer1, offer2)
- if err != nil {
- t.Fatalf("computeSyncIntersection() failed: %v", err)
- }
- want := &intersection{
- tailIntersection: &a1H,
- }
- if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
- t.Errorf("intersection diff (-want, +got):\n%s", diff)
- }
- })
- // Node 2 knows about the full chain, so it can see that the head of n1
- // intersects with a subset of its chain (a Head Intersection).
- t.Run("n2", func(t *testing.T) {
- got, err := computeSyncIntersection(chonk2, offer2, offer1)
- if err != nil {
- t.Fatalf("computeSyncIntersection() failed: %v", err)
- }
- want := &intersection{
- headIntersection: &a2H,
- }
- if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
- t.Errorf("intersection diff (-want, +got):\n%s", diff)
- }
- })
- }
- func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
- // The number of nodes in the chain is longer than ancestorSkipStart,
- // so that during sync both nodes are able to find a common ancestor
- // which was later than A1.
- c := newTestchain(t, `
- A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
- | -> F1
- // Make F1 different to A9.
- // hashSeed is chosen such that the hash is higher than A9.
- F1.hashSeed = 7
- `)
- // Node 1 has: A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> F1
- // Node 2 has: A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
- f1H, a9H := c.AUMHashes["F1"], c.AUMHashes["A9"]
- if bytes.Compare(f1H[:], a9H[:]) < 0 {
- t.Fatal("failed assert: h(a9) > h(f1H)\nTweak hashSeed till this passes")
- }
- chonk1 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "F1")
- n1, err := Open(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- offer1, err := n1.SyncOffer(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- if diff := cmp.Diff(SyncOffer{
- Head: c.AUMHashes["F1"],
- Ancestors: []AUMHash{
- c.AUMHashes["A"+strconv.Itoa(9-ancestorsSkipStart)],
- c.AUMHashes["A1"],
- },
- }, offer1); diff != "" {
- t.Errorf("offer1 diff (-want, +got):\n%s", diff)
- }
- chonk2 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10")
- n2, err := Open(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- offer2, err := n2.SyncOffer(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- if diff := cmp.Diff(SyncOffer{
- Head: c.AUMHashes["A10"],
- Ancestors: []AUMHash{
- c.AUMHashes["A"+strconv.Itoa(10-ancestorsSkipStart)],
- c.AUMHashes["A1"],
- },
- }, offer2); diff != "" {
- t.Errorf("offer2 diff (-want, +got):\n%s", diff)
- }
- // Node 1 only knows about the first eight nodes, so the head of n2 is
- // alien to it.
- t.Run("n1", func(t *testing.T) {
- // n2 has 10 nodes, so the first common ancestor should be 10-ancestorsSkipStart
- wantIntersection := c.AUMHashes["A"+strconv.Itoa(10-ancestorsSkipStart)]
- got, err := computeSyncIntersection(chonk1, offer1, offer2)
- if err != nil {
- t.Fatalf("computeSyncIntersection() failed: %v", err)
- }
- want := &intersection{
- tailIntersection: &wantIntersection,
- }
- if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
- t.Errorf("intersection diff (-want, +got):\n%s", diff)
- }
- })
- // Node 2 knows about the full chain but doesn't recognize the head.
- t.Run("n2", func(t *testing.T) {
- // n1 has 9 nodes, so the first common ancestor should be 9-ancestorsSkipStart
- wantIntersection := c.AUMHashes["A"+strconv.Itoa(9-ancestorsSkipStart)]
- got, err := computeSyncIntersection(chonk2, offer2, offer1)
- if err != nil {
- t.Fatalf("computeSyncIntersection() failed: %v", err)
- }
- want := &intersection{
- tailIntersection: &wantIntersection,
- }
- if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
- t.Errorf("intersection diff (-want, +got):\n%s", diff)
- }
- })
- }
- func TestMissingAUMs_FastForward(t *testing.T) {
- // Node 1 has: A1 -> A2
- // Node 2 has: A1 -> A2 -> A3 -> A4
- c := newTestchain(t, `
- A1 -> A2 -> A3 -> A4
- A1.hashSeed = 1
- A2.hashSeed = 2
- A3.hashSeed = 3
- A4.hashSeed = 4
- `)
- chonk1 := c.ChonkWith("A1", "A2")
- n1, err := Open(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- offer1, err := n1.SyncOffer(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- chonk2 := c.Chonk() // All AUMs
- n2, err := Open(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- offer2, err := n2.SyncOffer(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- // Node 1 only knows about the first two nodes, so the head of n2 is
- // alien to it. As such, it should send history from the newest ancestor,
- // A1 (if the chain was longer there would be one in the middle).
- t.Run("n1", func(t *testing.T) {
- got, err := n1.MissingAUMs(chonk1, offer2)
- if err != nil {
- t.Fatalf("MissingAUMs() failed: %v", err)
- }
- // Both sides have A1, so the only AUM that n2 might not have is
- // A2.
- want := []AUM{c.AUMs["A2"]}
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
- }
- })
- // Node 2 knows about the full chain, so it can see that the head of n1
- // intersects with a subset of its chain (a Head Intersection).
- t.Run("n2", func(t *testing.T) {
- got, err := n2.MissingAUMs(chonk2, offer1)
- if err != nil {
- t.Fatalf("MissingAUMs() failed: %v", err)
- }
- want := []AUM{
- c.AUMs["A3"],
- c.AUMs["A4"],
- }
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
- }
- })
- }
- func TestMissingAUMs_Fork(t *testing.T) {
- // Node 1 has: A1 -> A2 -> A3 -> F1
- // Node 2 has: A1 -> A2 -> A3 -> A4
- c := newTestchain(t, `
- A1 -> A2 -> A3 -> A4
- | -> F1
- A1.hashSeed = 1
- A2.hashSeed = 2
- A3.hashSeed = 3
- A4.hashSeed = 4
- `)
- chonk1 := c.ChonkWith("A1", "A2", "A3", "F1")
- n1, err := Open(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- offer1, err := n1.SyncOffer(chonk1)
- if err != nil {
- t.Fatal(err)
- }
- chonk2 := c.ChonkWith("A1", "A2", "A3", "A4")
- n2, err := Open(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- offer2, err := n2.SyncOffer(chonk2)
- if err != nil {
- t.Fatal(err)
- }
- t.Run("n1", func(t *testing.T) {
- got, err := n1.MissingAUMs(chonk1, offer2)
- if err != nil {
- t.Fatalf("MissingAUMs() failed: %v", err)
- }
- // Both sides have A1, so n1 will send everything it knows from
- // there to head.
- want := []AUM{
- c.AUMs["A2"],
- c.AUMs["A3"],
- c.AUMs["F1"],
- }
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
- }
- })
- t.Run("n2", func(t *testing.T) {
- got, err := n2.MissingAUMs(chonk2, offer1)
- if err != nil {
- t.Fatalf("MissingAUMs() failed: %v", err)
- }
- // Both sides have A1, so n2 will send everything it knows from
- // there to head.
- want := []AUM{
- c.AUMs["A2"],
- c.AUMs["A3"],
- c.AUMs["A4"],
- }
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
- }
- })
- }
- func TestSyncSimpleE2E(t *testing.T) {
- pub, priv := testingKey25519(t, 1)
- key := Key{Kind: Key25519, Public: pub, Votes: 2}
- c := newTestchain(t, `
- G1 -> L1 -> L2 -> L3
- G1.template = genesis
- `,
- optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
- Keys: []Key{key},
- DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
- }}),
- optKey("key", key, priv),
- optSignAllUsing("key"))
- nodeStorage := ChonkMem()
- node, err := Bootstrap(nodeStorage, c.AUMs["G1"])
- if err != nil {
- t.Fatalf("node Bootstrap() failed: %v", err)
- }
- controlStorage := c.Chonk()
- control, err := Open(controlStorage)
- if err != nil {
- t.Fatalf("control Open() failed: %v", err)
- }
- // Control knows the full chain, node only knows the genesis. Let's see
- // if they can sync.
- nodeOffer, err := node.SyncOffer(nodeStorage)
- if err != nil {
- t.Fatal(err)
- }
- controlAUMs, err := control.MissingAUMs(controlStorage, nodeOffer)
- if err != nil {
- t.Fatalf("control.MissingAUMs(%v) failed: %v", nodeOffer, err)
- }
- if err := node.Inform(nodeStorage, controlAUMs); err != nil {
- t.Fatalf("node.Inform(%v) failed: %v", controlAUMs, err)
- }
- if cHash, nHash := control.Head(), node.Head(); cHash != nHash {
- t.Errorf("node & control are not synced: c=%x, n=%x", cHash, nHash)
- }
- }
|