tailchonk_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package tka
  4. import (
  5. "bytes"
  6. "os"
  7. "path/filepath"
  8. "slices"
  9. "sync"
  10. "testing"
  11. "time"
  12. "github.com/google/go-cmp/cmp"
  13. "github.com/google/go-cmp/cmp/cmpopts"
  14. "golang.org/x/crypto/blake2s"
  15. "tailscale.com/types/key"
  16. "tailscale.com/util/must"
  17. )
  18. // This package has implementation-specific tests for Mem and FS.
  19. //
  20. // We also have tests for the Chonk interface in `chonktest`, which exercises
  21. // both Mem and FS. Those tests are in a separate package so they can be shared
  22. // with other repos; we don't call the shared test helpers from this package
  23. // to avoid creating a circular dependency.
  24. // randHash derives a fake blake2s hash from the test name
  25. // and the given seed.
  26. func randHash(t *testing.T, seed int64) [blake2s.Size]byte {
  27. var out [blake2s.Size]byte
  28. testingRand(t, seed).Read(out[:])
  29. return out
  30. }
  31. func TestImplementsChonk(t *testing.T) {
  32. impls := []Chonk{ChonkMem(), &FS{}}
  33. t.Logf("chonks: %v", impls)
  34. }
  35. func TestTailchonkFS_Commit(t *testing.T) {
  36. chonk := must.Get(ChonkDir(t.TempDir()))
  37. parentHash := randHash(t, 1)
  38. aum := AUM{MessageKind: AUMNoOp, PrevAUMHash: parentHash[:]}
  39. if err := chonk.CommitVerifiedAUMs([]AUM{aum}); err != nil {
  40. t.Fatal(err)
  41. }
  42. dir, base := chonk.aumDir(aum.Hash())
  43. if got, want := dir, filepath.Join(chonk.base, "PD"); got != want {
  44. t.Errorf("aum dir=%s, want %s", got, want)
  45. }
  46. if want := "PD57DVP6GKC76OOZMXFFZUSOEFQXOLAVT7N2ZM5KB3HDIMCANF4A"; base != want {
  47. t.Errorf("aum base=%s, want %s", base, want)
  48. }
  49. if _, err := os.Stat(filepath.Join(dir, base)); err != nil {
  50. t.Errorf("stat of AUM file failed: %v", err)
  51. }
  52. info, err := chonk.get(aum.Hash())
  53. if err != nil {
  54. t.Fatal(err)
  55. }
  56. if info.PurgedUnix > 0 {
  57. t.Errorf("recently-created AUM PurgedUnix = %d, want 0", info.PurgedUnix)
  58. }
  59. }
  60. func TestTailchonkFS_CommitTime(t *testing.T) {
  61. chonk := must.Get(ChonkDir(t.TempDir()))
  62. parentHash := randHash(t, 1)
  63. aum := AUM{MessageKind: AUMNoOp, PrevAUMHash: parentHash[:]}
  64. if err := chonk.CommitVerifiedAUMs([]AUM{aum}); err != nil {
  65. t.Fatal(err)
  66. }
  67. ct, err := chonk.CommitTime(aum.Hash())
  68. if err != nil {
  69. t.Fatalf("CommitTime() failed: %v", err)
  70. }
  71. if ct.Before(time.Now().Add(-time.Minute)) || ct.After(time.Now().Add(time.Minute)) {
  72. t.Errorf("commit time was wrong: %v more than a minute off from now (%v)", ct, time.Now())
  73. }
  74. }
  75. // If we were interrupted while writing a temporary file, AllAUMs()
  76. // should ignore it when scanning the AUM directory.
  77. func TestTailchonkFS_IgnoreTempFile(t *testing.T) {
  78. base := t.TempDir()
  79. chonk := must.Get(ChonkDir(base))
  80. parentHash := randHash(t, 1)
  81. aum := AUM{MessageKind: AUMNoOp, PrevAUMHash: parentHash[:]}
  82. must.Do(chonk.CommitVerifiedAUMs([]AUM{aum}))
  83. writeAUMFile := func(filename, contents string) {
  84. t.Helper()
  85. if err := os.MkdirAll(filepath.Join(base, filename[0:2]), os.ModePerm); err != nil {
  86. t.Fatal(err)
  87. }
  88. if err := os.WriteFile(filepath.Join(base, filename[0:2], filename), []byte(contents), 0600); err != nil {
  89. t.Fatal(err)
  90. }
  91. }
  92. // Check that calling AllAUMs() returns the single committed AUM
  93. got, err := chonk.AllAUMs()
  94. if err != nil {
  95. t.Fatalf("AllAUMs() failed: %v", err)
  96. }
  97. want := []AUMHash{aum.Hash()}
  98. if !slices.Equal(got, want) {
  99. t.Fatalf("AllAUMs() is wrong: got %v, want %v", got, want)
  100. }
  101. // Write some temporary files which are named like partially-committed AUMs,
  102. // then check that AllAUMs() only returns the single committed AUM.
  103. writeAUMFile("AUM1234.tmp", "incomplete AUM\n")
  104. writeAUMFile("AUM1234.tmp_123", "second incomplete AUM\n")
  105. got, err = chonk.AllAUMs()
  106. if err != nil {
  107. t.Fatalf("AllAUMs() failed: %v", err)
  108. }
  109. if !slices.Equal(got, want) {
  110. t.Fatalf("AllAUMs() is wrong: got %v, want %v", got, want)
  111. }
  112. }
  113. // If we use a non-existent directory with filesystem Chonk storage,
  114. // it's automatically created.
  115. func TestTailchonkFS_CreateChonkDir(t *testing.T) {
  116. base := filepath.Join(t.TempDir(), "a", "b", "c")
  117. chonk, err := ChonkDir(base)
  118. if err != nil {
  119. t.Fatalf("ChonkDir: %v", err)
  120. }
  121. aum := AUM{MessageKind: AUMNoOp}
  122. must.Do(chonk.CommitVerifiedAUMs([]AUM{aum}))
  123. got, err := chonk.AUM(aum.Hash())
  124. if err != nil {
  125. t.Errorf("Chonk.AUM: %v", err)
  126. }
  127. if diff := cmp.Diff(got, aum); diff != "" {
  128. t.Errorf("wrong AUM; (-got+want):%v", diff)
  129. }
  130. if _, err := os.Stat(base); err != nil {
  131. t.Errorf("os.Stat: %v", err)
  132. }
  133. }
  134. // You can't use a file as the root of your filesystem Chonk storage.
  135. func TestTailchonkFS_CannotUseFile(t *testing.T) {
  136. base := filepath.Join(t.TempDir(), "tka_storage.txt")
  137. must.Do(os.WriteFile(base, []byte("this won't work"), 0644))
  138. _, err := ChonkDir(base)
  139. if err == nil {
  140. t.Fatal("ChonkDir succeeded; expected an error")
  141. }
  142. }
  143. func TestMarkActiveChain(t *testing.T) {
  144. type aumTemplate struct {
  145. AUM AUM
  146. }
  147. tcs := []struct {
  148. name string
  149. minChain int
  150. chain []aumTemplate
  151. expectLastActiveIdx int // expected lastActiveAncestor, corresponds to an index on chain.
  152. }{
  153. {
  154. name: "genesis",
  155. minChain: 2,
  156. chain: []aumTemplate{
  157. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  158. },
  159. expectLastActiveIdx: 0,
  160. },
  161. {
  162. name: "simple truncate",
  163. minChain: 2,
  164. chain: []aumTemplate{
  165. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  166. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  167. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  168. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  169. },
  170. expectLastActiveIdx: 1,
  171. },
  172. {
  173. name: "long truncate",
  174. minChain: 5,
  175. chain: []aumTemplate{
  176. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  177. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  178. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  179. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  180. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  181. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  182. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  183. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  184. },
  185. expectLastActiveIdx: 2,
  186. },
  187. {
  188. name: "truncate finding checkpoint",
  189. minChain: 2,
  190. chain: []aumTemplate{
  191. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  192. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  193. {AUM: AUM{MessageKind: AUMAddKey, Key: &Key{}}}, // Should keep searching upwards for a checkpoint
  194. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  195. {AUM: AUM{MessageKind: AUMCheckpoint, State: &State{}}},
  196. },
  197. expectLastActiveIdx: 1,
  198. },
  199. }
  200. for _, tc := range tcs {
  201. t.Run(tc.name, func(t *testing.T) {
  202. verdict := make(map[AUMHash]retainState, len(tc.chain))
  203. // Build the state of the tailchonk for tests.
  204. storage := ChonkMem()
  205. var prev AUMHash
  206. for i := range tc.chain {
  207. if !prev.IsZero() {
  208. tc.chain[i].AUM.PrevAUMHash = make([]byte, len(prev[:]))
  209. copy(tc.chain[i].AUM.PrevAUMHash, prev[:])
  210. }
  211. if err := storage.CommitVerifiedAUMs([]AUM{tc.chain[i].AUM}); err != nil {
  212. t.Fatal(err)
  213. }
  214. h := tc.chain[i].AUM.Hash()
  215. prev = h
  216. verdict[h] = 0
  217. }
  218. got, err := markActiveChain(storage, verdict, tc.minChain, prev)
  219. if err != nil {
  220. t.Logf("state = %+v", verdict)
  221. t.Fatalf("markActiveChain() failed: %v", err)
  222. }
  223. want := tc.chain[tc.expectLastActiveIdx].AUM.Hash()
  224. if got != want {
  225. t.Logf("state = %+v", verdict)
  226. t.Errorf("lastActiveAncestor = %v, want %v", got, want)
  227. }
  228. // Make sure the verdict array was marked correctly.
  229. for i := range tc.chain {
  230. h := tc.chain[i].AUM.Hash()
  231. if i >= tc.expectLastActiveIdx {
  232. if (verdict[h] & retainStateActive) == 0 {
  233. t.Errorf("verdict[%v] = %v, want %v set", h, verdict[h], retainStateActive)
  234. }
  235. } else {
  236. if (verdict[h] & retainStateCandidate) == 0 {
  237. t.Errorf("verdict[%v] = %v, want %v set", h, verdict[h], retainStateCandidate)
  238. }
  239. }
  240. }
  241. })
  242. }
  243. }
  244. func TestMarkDescendantAUMs(t *testing.T) {
  245. c := newTestchain(t, `
  246. genesis -> B -> C -> C2
  247. | -> D
  248. | -> E -> F -> G -> H
  249. | -> E2
  250. // tweak seeds so hashes arent identical
  251. C.hashSeed = 1
  252. D.hashSeed = 2
  253. E.hashSeed = 3
  254. E2.hashSeed = 4
  255. `)
  256. verdict := make(map[AUMHash]retainState, len(c.AUMs))
  257. for _, a := range c.AUMs {
  258. verdict[a.Hash()] = 0
  259. }
  260. // Mark E & C.
  261. verdict[c.AUMHashes["C"]] = retainStateActive
  262. verdict[c.AUMHashes["E"]] = retainStateActive
  263. if err := markDescendantAUMs(c.Chonk(), verdict); err != nil {
  264. t.Errorf("markDescendantAUMs() failed: %v", err)
  265. }
  266. // Make sure the descendants got marked.
  267. hs := c.AUMHashes
  268. for _, h := range []AUMHash{hs["C2"], hs["F"], hs["G"], hs["H"], hs["E2"]} {
  269. if (verdict[h] & retainStateLeaf) == 0 {
  270. t.Errorf("%v was not marked as a descendant", h)
  271. }
  272. }
  273. for _, h := range []AUMHash{hs["genesis"], hs["B"], hs["D"]} {
  274. if (verdict[h] & retainStateLeaf) != 0 {
  275. t.Errorf("%v was marked as a descendant and shouldnt be", h)
  276. }
  277. }
  278. }
  279. func TestMarkAncestorIntersectionAUMs(t *testing.T) {
  280. fakeState := &State{
  281. Keys: []Key{{Kind: Key25519, Votes: 1}},
  282. DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)},
  283. }
  284. tcs := []struct {
  285. name string
  286. chain *testChain
  287. verdicts map[string]retainState
  288. initialAncestor string
  289. wantAncestor string
  290. wantRetained []string
  291. wantDeleted []string
  292. }{
  293. {
  294. name: "genesis",
  295. chain: newTestchain(t, `
  296. A
  297. A.template = checkpoint`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  298. initialAncestor: "A",
  299. wantAncestor: "A",
  300. verdicts: map[string]retainState{
  301. "A": retainStateActive,
  302. },
  303. wantRetained: []string{"A"},
  304. },
  305. {
  306. name: "no adjustment",
  307. chain: newTestchain(t, `
  308. DEAD -> A -> B -> C
  309. A.template = checkpoint
  310. B.template = checkpoint`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  311. initialAncestor: "A",
  312. wantAncestor: "A",
  313. verdicts: map[string]retainState{
  314. "A": retainStateActive,
  315. "B": retainStateActive,
  316. "C": retainStateActive,
  317. "DEAD": retainStateCandidate,
  318. },
  319. wantRetained: []string{"A", "B", "C"},
  320. wantDeleted: []string{"DEAD"},
  321. },
  322. {
  323. name: "fork",
  324. chain: newTestchain(t, `
  325. A -> B -> C -> D
  326. | -> FORK
  327. A.template = checkpoint
  328. C.template = checkpoint
  329. D.template = checkpoint
  330. FORK.hashSeed = 2`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  331. initialAncestor: "D",
  332. wantAncestor: "C",
  333. verdicts: map[string]retainState{
  334. "A": retainStateCandidate,
  335. "B": retainStateCandidate,
  336. "C": retainStateCandidate,
  337. "D": retainStateActive,
  338. "FORK": retainStateYoung,
  339. },
  340. wantRetained: []string{"C", "D", "FORK"},
  341. wantDeleted: []string{"A", "B"},
  342. },
  343. {
  344. name: "fork finding earlier checkpoint",
  345. chain: newTestchain(t, `
  346. A -> B -> C -> D -> E -> F
  347. | -> FORK
  348. A.template = checkpoint
  349. B.template = checkpoint
  350. E.template = checkpoint
  351. FORK.hashSeed = 2`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  352. initialAncestor: "E",
  353. wantAncestor: "B",
  354. verdicts: map[string]retainState{
  355. "A": retainStateCandidate,
  356. "B": retainStateCandidate,
  357. "C": retainStateCandidate,
  358. "D": retainStateCandidate,
  359. "E": retainStateActive,
  360. "F": retainStateActive,
  361. "FORK": retainStateYoung,
  362. },
  363. wantRetained: []string{"B", "C", "D", "E", "F", "FORK"},
  364. wantDeleted: []string{"A"},
  365. },
  366. {
  367. name: "fork multi",
  368. chain: newTestchain(t, `
  369. A -> B -> C -> D -> E
  370. | -> DEADFORK
  371. C -> FORK
  372. A.template = checkpoint
  373. C.template = checkpoint
  374. D.template = checkpoint
  375. E.template = checkpoint
  376. FORK.hashSeed = 2
  377. DEADFORK.hashSeed = 3`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  378. initialAncestor: "D",
  379. wantAncestor: "C",
  380. verdicts: map[string]retainState{
  381. "A": retainStateCandidate,
  382. "B": retainStateCandidate,
  383. "C": retainStateCandidate,
  384. "D": retainStateActive,
  385. "E": retainStateActive,
  386. "FORK": retainStateYoung,
  387. "DEADFORK": 0,
  388. },
  389. wantRetained: []string{"C", "D", "E", "FORK"},
  390. wantDeleted: []string{"A", "B", "DEADFORK"},
  391. },
  392. {
  393. name: "fork multi 2",
  394. chain: newTestchain(t, `
  395. A -> B -> C -> D -> E -> F -> G
  396. F -> F1
  397. D -> F2
  398. B -> F3
  399. A.template = checkpoint
  400. B.template = checkpoint
  401. D.template = checkpoint
  402. F.template = checkpoint
  403. F1.hashSeed = 2
  404. F2.hashSeed = 3
  405. F3.hashSeed = 4`, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState})),
  406. initialAncestor: "F",
  407. wantAncestor: "B",
  408. verdicts: map[string]retainState{
  409. "A": retainStateCandidate,
  410. "B": retainStateCandidate,
  411. "C": retainStateCandidate,
  412. "D": retainStateCandidate,
  413. "E": retainStateCandidate,
  414. "F": retainStateActive,
  415. "G": retainStateActive,
  416. "F1": retainStateYoung,
  417. "F2": retainStateYoung,
  418. "F3": retainStateYoung,
  419. },
  420. wantRetained: []string{"B", "C", "D", "E", "F", "G", "F1", "F2", "F3"},
  421. },
  422. }
  423. for _, tc := range tcs {
  424. t.Run(tc.name, func(t *testing.T) {
  425. verdict := make(map[AUMHash]retainState, len(tc.verdicts))
  426. for name, v := range tc.verdicts {
  427. verdict[tc.chain.AUMHashes[name]] = v
  428. }
  429. got, err := markAncestorIntersectionAUMs(tc.chain.Chonk(), verdict, tc.chain.AUMHashes[tc.initialAncestor])
  430. if err != nil {
  431. t.Logf("state = %+v", verdict)
  432. t.Fatalf("markAncestorIntersectionAUMs() failed: %v", err)
  433. }
  434. if want := tc.chain.AUMHashes[tc.wantAncestor]; got != want {
  435. t.Logf("state = %+v", verdict)
  436. t.Errorf("lastActiveAncestor = %v, want %v", got, want)
  437. }
  438. for _, name := range tc.wantRetained {
  439. h := tc.chain.AUMHashes[name]
  440. if v := verdict[h]; v&retainAUMMask == 0 {
  441. t.Errorf("AUM %q was not retained: verdict = %v", name, v)
  442. }
  443. }
  444. for _, name := range tc.wantDeleted {
  445. h := tc.chain.AUMHashes[name]
  446. if v := verdict[h]; v&retainAUMMask != 0 {
  447. t.Errorf("AUM %q was retained: verdict = %v", name, v)
  448. }
  449. }
  450. if t.Failed() {
  451. for name, hash := range tc.chain.AUMHashes {
  452. t.Logf("AUM[%q] = %v", name, hash)
  453. }
  454. }
  455. })
  456. }
  457. }
  458. type compactingChonkFake struct {
  459. Mem
  460. aumAge map[AUMHash]time.Time
  461. t *testing.T
  462. wantDelete []AUMHash
  463. }
  464. func (c *compactingChonkFake) AllAUMs() ([]AUMHash, error) {
  465. out := make([]AUMHash, 0, len(c.Mem.aums))
  466. for h := range c.Mem.aums {
  467. out = append(out, h)
  468. }
  469. return out, nil
  470. }
  471. func (c *compactingChonkFake) CommitTime(hash AUMHash) (time.Time, error) {
  472. return c.aumAge[hash], nil
  473. }
  474. func hashesLess(x, y AUMHash) bool {
  475. return bytes.Compare(x[:], y[:]) < 0
  476. }
  477. func (c *compactingChonkFake) PurgeAUMs(hashes []AUMHash) error {
  478. if diff := cmp.Diff(c.wantDelete, hashes, cmpopts.SortSlices(hashesLess)); diff != "" {
  479. c.t.Errorf("deletion set differs (-want, +got):\n%s", diff)
  480. }
  481. return nil
  482. }
  483. // Avoid go vet complaining about copying a lock value
  484. func cloneMem(src, dst *Mem) {
  485. dst.mu = sync.RWMutex{}
  486. dst.aums = src.aums
  487. dst.parentIndex = src.parentIndex
  488. dst.lastActiveAncestor = src.lastActiveAncestor
  489. }
  490. func TestCompact(t *testing.T) {
  491. fakeState := &State{
  492. Keys: []Key{{Kind: Key25519, Votes: 1}},
  493. DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)},
  494. }
  495. // A & B are deleted because the new lastActiveAncestor advances beyond them.
  496. // OLD is deleted because it does not match retention criteria, and
  497. // though it is a descendant of the new lastActiveAncestor (C), it is not a
  498. // descendant of a retained AUM.
  499. // G, & H are retained as recent (MinChain=2) ancestors of HEAD.
  500. // E & F are retained because they are between retained AUMs (G+) and
  501. // their newest checkpoint ancestor.
  502. // D is retained because it is the newest checkpoint ancestor from
  503. // MinChain-retained AUMs.
  504. // G2 is retained because it is a descendant of a retained AUM (G).
  505. // F1 is retained because it is new enough by wall-clock time.
  506. // F2 is retained because it is a descendant of a retained AUM (F1).
  507. // C2 is retained because it is between an ancestor checkpoint and
  508. // a retained AUM (F1).
  509. // C is retained because it is the new lastActiveAncestor. It is the
  510. // new lastActiveAncestor because it is the newest common checkpoint
  511. // of all retained AUMs.
  512. c := newTestchain(t, `
  513. A -> B -> C -> C2 -> D -> E -> F -> G -> H
  514. | -> F1 -> F2 | -> G2
  515. | -> OLD
  516. // make {A,B,C,D} compaction candidates
  517. A.template = checkpoint
  518. B.template = checkpoint
  519. C.template = checkpoint
  520. D.template = checkpoint
  521. // tweak seeds of forks so hashes arent identical
  522. F1.hashSeed = 1
  523. OLD.hashSeed = 2
  524. G2.hashSeed = 3
  525. `, optTemplate("checkpoint", AUM{MessageKind: AUMCheckpoint, State: fakeState}))
  526. storage := &compactingChonkFake{
  527. aumAge: map[AUMHash]time.Time{(c.AUMHashes["F1"]): time.Now()},
  528. t: t,
  529. wantDelete: []AUMHash{c.AUMHashes["A"], c.AUMHashes["B"], c.AUMHashes["OLD"]},
  530. }
  531. cloneMem(c.Chonk().(*Mem), &storage.Mem)
  532. lastActiveAncestor, err := Compact(storage, c.AUMHashes["H"], CompactionOptions{MinChain: 2, MinAge: time.Hour})
  533. if err != nil {
  534. t.Errorf("Compact() failed: %v", err)
  535. }
  536. if lastActiveAncestor != c.AUMHashes["C"] {
  537. t.Errorf("last active ancestor = %v, want %v", lastActiveAncestor, c.AUMHashes["C"])
  538. }
  539. if t.Failed() {
  540. for name, hash := range c.AUMHashes {
  541. t.Logf("AUM[%q] = %v", name, hash)
  542. }
  543. }
  544. }
  545. func TestCompactLongButYoung(t *testing.T) {
  546. ourPriv := key.NewNLPrivate()
  547. ourKey := Key{Kind: Key25519, Public: ourPriv.Public().Verifier(), Votes: 1}
  548. someOtherKey := Key{Kind: Key25519, Public: key.NewNLPrivate().Public().Verifier(), Votes: 1}
  549. storage := ChonkMem()
  550. auth, _, err := Create(storage, State{
  551. Keys: []Key{ourKey, someOtherKey},
  552. DisablementSecrets: [][]byte{DisablementKDF(bytes.Repeat([]byte{0xa5}, 32))},
  553. }, ourPriv)
  554. if err != nil {
  555. t.Fatalf("tka.Create() failed: %v", err)
  556. }
  557. genesis := auth.Head()
  558. for range 100 {
  559. upd := auth.NewUpdater(ourPriv)
  560. must.Do(upd.RemoveKey(someOtherKey.MustID()))
  561. must.Do(upd.AddKey(someOtherKey))
  562. aums := must.Get(upd.Finalize(storage))
  563. must.Do(auth.Inform(storage, aums))
  564. }
  565. lastActiveAncestor := must.Get(Compact(storage, auth.Head(), CompactionOptions{MinChain: 5, MinAge: time.Hour}))
  566. if lastActiveAncestor != genesis {
  567. t.Errorf("last active ancestor = %v, want %v", lastActiveAncestor, genesis)
  568. }
  569. }