sync_test.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package tka
  4. import (
  5. "bytes"
  6. "strconv"
  7. "testing"
  8. "github.com/google/go-cmp/cmp"
  9. )
  10. func TestSyncOffer(t *testing.T) {
  11. c := newTestchain(t, `
  12. A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
  13. A10 -> A11 -> A12 -> A13 -> A14 -> A15 -> A16 -> A17 -> A18
  14. A18 -> A19 -> A20 -> A21 -> A22 -> A23 -> A24 -> A25
  15. `)
  16. storage := c.Chonk()
  17. a, err := Open(storage)
  18. if err != nil {
  19. t.Fatal(err)
  20. }
  21. got, err := a.SyncOffer(storage)
  22. if err != nil {
  23. t.Fatal(err)
  24. }
  25. // A SyncOffer includes a selection of AUMs going backwards in the tree,
  26. // progressively skipping more and more each iteration.
  27. want := SyncOffer{
  28. Head: c.AUMHashes["A25"],
  29. Ancestors: []AUMHash{
  30. c.AUMHashes["A"+strconv.Itoa(25-ancestorsSkipStart)],
  31. c.AUMHashes["A"+strconv.Itoa(25-ancestorsSkipStart<<ancestorsSkipShift)],
  32. c.AUMHashes["A1"],
  33. },
  34. }
  35. if diff := cmp.Diff(want, got); diff != "" {
  36. t.Errorf("SyncOffer diff (-want, +got):\n%s", diff)
  37. }
  38. }
  39. func TestComputeSyncIntersection_FastForward(t *testing.T) {
  40. // Node 1 has: A1 -> A2
  41. // Node 2 has: A1 -> A2 -> A3 -> A4
  42. c := newTestchain(t, `
  43. A1 -> A2 -> A3 -> A4
  44. `)
  45. a1H, a2H := c.AUMHashes["A1"], c.AUMHashes["A2"]
  46. chonk1 := c.ChonkWith("A1", "A2")
  47. n1, err := Open(chonk1)
  48. if err != nil {
  49. t.Fatal(err)
  50. }
  51. offer1, err := n1.SyncOffer(chonk1)
  52. if err != nil {
  53. t.Fatal(err)
  54. }
  55. chonk2 := c.Chonk() // All AUMs
  56. n2, err := Open(chonk2)
  57. if err != nil {
  58. t.Fatal(err)
  59. }
  60. offer2, err := n2.SyncOffer(chonk2)
  61. if err != nil {
  62. t.Fatal(err)
  63. }
  64. // Node 1 only knows about the first two nodes, so the head of n2 is
  65. // alien to it.
  66. t.Run("n1", func(t *testing.T) {
  67. got, err := computeSyncIntersection(chonk1, offer1, offer2)
  68. if err != nil {
  69. t.Fatalf("computeSyncIntersection() failed: %v", err)
  70. }
  71. want := &intersection{
  72. tailIntersection: &a1H,
  73. }
  74. if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
  75. t.Errorf("intersection diff (-want, +got):\n%s", diff)
  76. }
  77. })
  78. // Node 2 knows about the full chain, so it can see that the head of n1
  79. // intersects with a subset of its chain (a Head Intersection).
  80. t.Run("n2", func(t *testing.T) {
  81. got, err := computeSyncIntersection(chonk2, offer2, offer1)
  82. if err != nil {
  83. t.Fatalf("computeSyncIntersection() failed: %v", err)
  84. }
  85. want := &intersection{
  86. headIntersection: &a2H,
  87. }
  88. if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
  89. t.Errorf("intersection diff (-want, +got):\n%s", diff)
  90. }
  91. })
  92. }
  93. func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
  94. // The number of nodes in the chain is longer than ancestorSkipStart,
  95. // so that during sync both nodes are able to find a common ancestor
  96. // which was later than A1.
  97. c := newTestchain(t, `
  98. A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
  99. | -> F1
  100. // Make F1 different to A9.
  101. // hashSeed is chosen such that the hash is higher than A9.
  102. F1.hashSeed = 7
  103. `)
  104. // Node 1 has: A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> F1
  105. // Node 2 has: A1 -> A2 -> A3 -> A4 -> A5 -> A6 -> A7 -> A8 -> A9 -> A10
  106. f1H, a9H := c.AUMHashes["F1"], c.AUMHashes["A9"]
  107. if bytes.Compare(f1H[:], a9H[:]) < 0 {
  108. t.Fatal("failed assert: h(a9) > h(f1H)\nTweak hashSeed till this passes")
  109. }
  110. chonk1 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "F1")
  111. n1, err := Open(chonk1)
  112. if err != nil {
  113. t.Fatal(err)
  114. }
  115. offer1, err := n1.SyncOffer(chonk1)
  116. if err != nil {
  117. t.Fatal(err)
  118. }
  119. if diff := cmp.Diff(SyncOffer{
  120. Head: c.AUMHashes["F1"],
  121. Ancestors: []AUMHash{
  122. c.AUMHashes["A"+strconv.Itoa(9-ancestorsSkipStart)],
  123. c.AUMHashes["A1"],
  124. },
  125. }, offer1); diff != "" {
  126. t.Errorf("offer1 diff (-want, +got):\n%s", diff)
  127. }
  128. chonk2 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10")
  129. n2, err := Open(chonk2)
  130. if err != nil {
  131. t.Fatal(err)
  132. }
  133. offer2, err := n2.SyncOffer(chonk2)
  134. if err != nil {
  135. t.Fatal(err)
  136. }
  137. if diff := cmp.Diff(SyncOffer{
  138. Head: c.AUMHashes["A10"],
  139. Ancestors: []AUMHash{
  140. c.AUMHashes["A"+strconv.Itoa(10-ancestorsSkipStart)],
  141. c.AUMHashes["A1"],
  142. },
  143. }, offer2); diff != "" {
  144. t.Errorf("offer2 diff (-want, +got):\n%s", diff)
  145. }
  146. // Node 1 only knows about the first eight nodes, so the head of n2 is
  147. // alien to it.
  148. t.Run("n1", func(t *testing.T) {
  149. // n2 has 10 nodes, so the first common ancestor should be 10-ancestorsSkipStart
  150. wantIntersection := c.AUMHashes["A"+strconv.Itoa(10-ancestorsSkipStart)]
  151. got, err := computeSyncIntersection(chonk1, offer1, offer2)
  152. if err != nil {
  153. t.Fatalf("computeSyncIntersection() failed: %v", err)
  154. }
  155. want := &intersection{
  156. tailIntersection: &wantIntersection,
  157. }
  158. if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
  159. t.Errorf("intersection diff (-want, +got):\n%s", diff)
  160. }
  161. })
  162. // Node 2 knows about the full chain but doesn't recognize the head.
  163. t.Run("n2", func(t *testing.T) {
  164. // n1 has 9 nodes, so the first common ancestor should be 9-ancestorsSkipStart
  165. wantIntersection := c.AUMHashes["A"+strconv.Itoa(9-ancestorsSkipStart)]
  166. got, err := computeSyncIntersection(chonk2, offer2, offer1)
  167. if err != nil {
  168. t.Fatalf("computeSyncIntersection() failed: %v", err)
  169. }
  170. want := &intersection{
  171. tailIntersection: &wantIntersection,
  172. }
  173. if diff := cmp.Diff(want, got, cmp.AllowUnexported(intersection{})); diff != "" {
  174. t.Errorf("intersection diff (-want, +got):\n%s", diff)
  175. }
  176. })
  177. }
  178. func TestMissingAUMs_FastForward(t *testing.T) {
  179. // Node 1 has: A1 -> A2
  180. // Node 2 has: A1 -> A2 -> A3 -> A4
  181. c := newTestchain(t, `
  182. A1 -> A2 -> A3 -> A4
  183. A1.hashSeed = 1
  184. A2.hashSeed = 2
  185. A3.hashSeed = 3
  186. A4.hashSeed = 4
  187. `)
  188. chonk1 := c.ChonkWith("A1", "A2")
  189. n1, err := Open(chonk1)
  190. if err != nil {
  191. t.Fatal(err)
  192. }
  193. offer1, err := n1.SyncOffer(chonk1)
  194. if err != nil {
  195. t.Fatal(err)
  196. }
  197. chonk2 := c.Chonk() // All AUMs
  198. n2, err := Open(chonk2)
  199. if err != nil {
  200. t.Fatal(err)
  201. }
  202. offer2, err := n2.SyncOffer(chonk2)
  203. if err != nil {
  204. t.Fatal(err)
  205. }
  206. // Node 1 only knows about the first two nodes, so the head of n2 is
  207. // alien to it. As such, it should send history from the newest ancestor,
  208. // A1 (if the chain was longer there would be one in the middle).
  209. t.Run("n1", func(t *testing.T) {
  210. got, err := n1.MissingAUMs(chonk1, offer2)
  211. if err != nil {
  212. t.Fatalf("MissingAUMs() failed: %v", err)
  213. }
  214. // Both sides have A1, so the only AUM that n2 might not have is
  215. // A2.
  216. want := []AUM{c.AUMs["A2"]}
  217. if diff := cmp.Diff(want, got); diff != "" {
  218. t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
  219. }
  220. })
  221. // Node 2 knows about the full chain, so it can see that the head of n1
  222. // intersects with a subset of its chain (a Head Intersection).
  223. t.Run("n2", func(t *testing.T) {
  224. got, err := n2.MissingAUMs(chonk2, offer1)
  225. if err != nil {
  226. t.Fatalf("MissingAUMs() failed: %v", err)
  227. }
  228. want := []AUM{
  229. c.AUMs["A3"],
  230. c.AUMs["A4"],
  231. }
  232. if diff := cmp.Diff(want, got); diff != "" {
  233. t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
  234. }
  235. })
  236. }
  237. func TestMissingAUMs_Fork(t *testing.T) {
  238. // Node 1 has: A1 -> A2 -> A3 -> F1
  239. // Node 2 has: A1 -> A2 -> A3 -> A4
  240. c := newTestchain(t, `
  241. A1 -> A2 -> A3 -> A4
  242. | -> F1
  243. A1.hashSeed = 1
  244. A2.hashSeed = 2
  245. A3.hashSeed = 3
  246. A4.hashSeed = 4
  247. `)
  248. chonk1 := c.ChonkWith("A1", "A2", "A3", "F1")
  249. n1, err := Open(chonk1)
  250. if err != nil {
  251. t.Fatal(err)
  252. }
  253. offer1, err := n1.SyncOffer(chonk1)
  254. if err != nil {
  255. t.Fatal(err)
  256. }
  257. chonk2 := c.ChonkWith("A1", "A2", "A3", "A4")
  258. n2, err := Open(chonk2)
  259. if err != nil {
  260. t.Fatal(err)
  261. }
  262. offer2, err := n2.SyncOffer(chonk2)
  263. if err != nil {
  264. t.Fatal(err)
  265. }
  266. t.Run("n1", func(t *testing.T) {
  267. got, err := n1.MissingAUMs(chonk1, offer2)
  268. if err != nil {
  269. t.Fatalf("MissingAUMs() failed: %v", err)
  270. }
  271. // Both sides have A1, so n1 will send everything it knows from
  272. // there to head.
  273. want := []AUM{
  274. c.AUMs["A2"],
  275. c.AUMs["A3"],
  276. c.AUMs["F1"],
  277. }
  278. if diff := cmp.Diff(want, got); diff != "" {
  279. t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
  280. }
  281. })
  282. t.Run("n2", func(t *testing.T) {
  283. got, err := n2.MissingAUMs(chonk2, offer1)
  284. if err != nil {
  285. t.Fatalf("MissingAUMs() failed: %v", err)
  286. }
  287. // Both sides have A1, so n2 will send everything it knows from
  288. // there to head.
  289. want := []AUM{
  290. c.AUMs["A2"],
  291. c.AUMs["A3"],
  292. c.AUMs["A4"],
  293. }
  294. if diff := cmp.Diff(want, got); diff != "" {
  295. t.Errorf("MissingAUMs diff (-want, +got):\n%s", diff)
  296. }
  297. })
  298. }
  299. func TestSyncSimpleE2E(t *testing.T) {
  300. pub, priv := testingKey25519(t, 1)
  301. key := Key{Kind: Key25519, Public: pub, Votes: 2}
  302. c := newTestchain(t, `
  303. G1 -> L1 -> L2 -> L3
  304. G1.template = genesis
  305. `,
  306. optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
  307. Keys: []Key{key},
  308. DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
  309. }}),
  310. optKey("key", key, priv),
  311. optSignAllUsing("key"))
  312. nodeStorage := ChonkMem()
  313. node, err := Bootstrap(nodeStorage, c.AUMs["G1"])
  314. if err != nil {
  315. t.Fatalf("node Bootstrap() failed: %v", err)
  316. }
  317. controlStorage := c.Chonk()
  318. control, err := Open(controlStorage)
  319. if err != nil {
  320. t.Fatalf("control Open() failed: %v", err)
  321. }
  322. // Control knows the full chain, node only knows the genesis. Let's see
  323. // if they can sync.
  324. nodeOffer, err := node.SyncOffer(nodeStorage)
  325. if err != nil {
  326. t.Fatal(err)
  327. }
  328. controlAUMs, err := control.MissingAUMs(controlStorage, nodeOffer)
  329. if err != nil {
  330. t.Fatalf("control.MissingAUMs(%v) failed: %v", nodeOffer, err)
  331. }
  332. if err := node.Inform(nodeStorage, controlAUMs); err != nil {
  333. t.Fatalf("node.Inform(%v) failed: %v", controlAUMs, err)
  334. }
  335. if cHash, nHash := control.Head(), node.Head(); cHash != nHash {
  336. t.Errorf("node & control are not synced: c=%x, n=%x", cHash, nHash)
  337. }
  338. }