summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--tka/tailchonk.go97
-rw-r--r--tka/tailchonk_test.go63
2 files changed, 108 insertions, 52 deletions
diff --git a/tka/tailchonk.go b/tka/tailchonk.go
index 6c441669a..50e03e339 100644
--- a/tka/tailchonk.go
+++ b/tka/tailchonk.go
@@ -11,6 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "slices"
"sync"
"time"
@@ -219,10 +220,14 @@ func ChonkDir(dir string) (*FS, error) {
// CBOR was chosen because we are already using it and it serializes
// much smaller than JSON for AUMs. The 'keyasint' thing isn't essential
// but again it saves a bunch of bytes.
+//
+// We have removed the following fields from fsHashInfo, but they may be
+// present in data stored in existing deployments. Do not reuse these values,
+// to avoid getting unexpected values from legacy data:
+// - cbor:1, Children
type fsHashInfo struct {
- Children []AUMHash `cbor:"1,keyasint"`
- AUM *AUM `cbor:"2,keyasint"`
- CreatedUnix int64 `cbor:"3,keyasint,omitempty"`
+ AUM *AUM `cbor:"2,keyasint"`
+ CreatedUnix int64 `cbor:"3,keyasint,omitempty"`
// PurgedUnix is set when the AUM is deleted. The value is
// the unix epoch at the time it was deleted.
@@ -298,29 +303,23 @@ func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
- info, err := c.get(prevAUMHash)
+ hashes, err := c.AllAUMs()
if err != nil {
- if os.IsNotExist(err) {
- // not knowing about this hash is not an error
- return nil, nil
- }
return nil, err
}
- // NOTE(tom): We don't check PurgedUnix here because 'purged'
- // only applies to that specific AUM (i.e. info.AUM) and not to
- // any information about children stored against that hash.
- out := make([]AUM, len(info.Children))
- for i, h := range info.Children {
- c, err := c.get(h)
+ var out []AUM
+
+ for _, h := range hashes {
+ aum, err := c.AUM(h)
+
if err != nil {
- // We expect any AUM recorded as a child on its parent to exist.
- return nil, fmt.Errorf("reading child %d of %x: %v", i, h, err)
+ return nil, err
}
- if c.AUM == nil || c.PurgedUnix > 0 {
- return nil, fmt.Errorf("child %d of %x: AUM not stored", i, h)
+
+ if bytes.Equal(aum.PrevAUMHash, prevAUMHash[:]) {
+ out = append(out, aum)
}
- out[i] = *c.AUM
}
return out, nil
@@ -359,13 +358,45 @@ func (c *FS) Heads() ([]AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
+ // Scan the complete list of AUMs, and build a list of all parent hashes.
+ // This tells us which AUMs have children.
+ var parentHashes []AUMHash
+
+ allAUMs, err := c.AllAUMs()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, h := range allAUMs {
+ aum, err := c.AUM(h)
+ if err != nil {
+ return nil, err
+ }
+ parent, hasParent := aum.Parent()
+ if !hasParent {
+ continue
+ }
+ if !slices.Contains(parentHashes, parent) {
+ parentHashes = append(parentHashes, parent)
+ }
+ }
+
+ // Now scan a second time, and only include AUMs which weren't marked as
+ // the parent of any other AUM.
out := make([]AUM, 0, 6) // 6 is arbitrary.
- err := c.scanHashes(func(info *fsHashInfo) {
- if len(info.Children) == 0 && info.AUM != nil && info.PurgedUnix == 0 {
- out = append(out, *info.AUM)
+
+ for _, h := range allAUMs {
+ if slices.Contains(parentHashes, h) {
+ continue
}
- })
- return out, err
+ aum, err := c.AUM(h)
+ if err != nil {
+ return nil, err
+ }
+ out = append(out, aum)
+ }
+
+ return out, nil
}
// AllAUMs returns all AUMs stored in the chonk.
@@ -458,24 +489,6 @@ func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
for i, aum := range updates {
h := aum.Hash()
- // We keep track of children against their parent so that
- // ChildAUMs() do not need to scan all AUMs.
- parent, hasParent := aum.Parent()
- if hasParent {
- err := c.commit(parent, func(info *fsHashInfo) {
- // Only add it if its not already there.
- for i := range info.Children {
- if info.Children[i] == h {
- return
- }
- }
- info.Children = append(info.Children, h)
- })
- if err != nil {
- return fmt.Errorf("committing update[%d] to parent %x: %v", i, parent, err)
- }
- }
-
err := c.commit(h, func(info *fsHashInfo) {
info.PurgedUnix = 0 // just in-case it was set for some reason
info.AUM = &aum
diff --git a/tka/tailchonk_test.go b/tka/tailchonk_test.go
index 86d5642a3..48b069971 100644
--- a/tka/tailchonk_test.go
+++ b/tka/tailchonk_test.go
@@ -15,6 +15,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/crypto/blake2s"
+ "tailscale.com/util/must"
)
// randHash derives a fake blake2s hash from the test name
@@ -171,9 +172,6 @@ func TestTailchonkFS_Commit(t *testing.T) {
if _, err := os.Stat(filepath.Join(dir, base)); err != nil {
t.Errorf("stat of AUM file failed: %v", err)
}
- if _, err := os.Stat(filepath.Join(chonk.base, "M7", "M7LL2NDB4NKCZIUPVS6RDM2GUOIMW6EEAFVBWMVCPUANQJPHT3SQ")); err != nil {
- t.Errorf("stat of AUM parent failed: %v", err)
- }
info, err := chonk.get(aum.Hash())
if err != nil {
@@ -226,6 +224,14 @@ func TestTailchonkFS_PurgeAUMs(t *testing.T) {
}
}
+func hashesLess(x, y AUMHash) bool {
+ return bytes.Compare(x[:], y[:]) < 0
+}
+
+func aumHashesLess(x, y AUM) bool {
+ return hashesLess(x.Hash(), y.Hash())
+}
+
func TestTailchonkFS_AllAUMs(t *testing.T) {
chonk := &FS{base: t.TempDir()}
genesis := AUM{MessageKind: AUMRemoveKey, KeyID: []byte{1, 2}}
@@ -247,14 +253,54 @@ func TestTailchonkFS_AllAUMs(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- hashesLess := func(a, b AUMHash) bool {
- return bytes.Compare(a[:], b[:]) < 0
- }
if diff := cmp.Diff([]AUMHash{genesis.Hash(), intermediate.Hash(), leaf.Hash()}, hashes, cmpopts.SortSlices(hashesLess)); diff != "" {
t.Fatalf("AllAUMs() output differs (-want, +got):\n%s", diff)
}
}
+func TestTailchonkFS_ChildAUMsOfPurgedAUM(t *testing.T) {
+ chonk := &FS{base: t.TempDir()}
+ parent := AUM{MessageKind: AUMRemoveKey, KeyID: []byte{0, 0}}
+
+ parentHash := parent.Hash()
+
+ child1 := AUM{MessageKind: AUMAddKey, KeyID: []byte{1, 1}, PrevAUMHash: parentHash[:]}
+ child2 := AUM{MessageKind: AUMAddKey, KeyID: []byte{2, 2}, PrevAUMHash: parentHash[:]}
+ child3 := AUM{MessageKind: AUMAddKey, KeyID: []byte{3, 3}, PrevAUMHash: parentHash[:]}
+
+ child2Hash := child2.Hash()
+ grandchild2A := AUM{MessageKind: AUMAddKey, KeyID: []byte{2, 2, 2, 2}, PrevAUMHash: child2Hash[:]}
+ grandchild2B := AUM{MessageKind: AUMAddKey, KeyID: []byte{2, 2, 2, 2, 2}, PrevAUMHash: child2Hash[:]}
+
+ commitSet := []AUM{parent, child1, child2, child3, grandchild2A, grandchild2B}
+
+ if err := chonk.CommitVerifiedAUMs(commitSet); err != nil {
+ t.Fatalf("CommitVerifiedAUMs failed: %v", err)
+ }
+
+ // Check the set of hashes is correct
+ got := must.Get(chonk.ChildAUMs(parentHash))
+ if diff := cmp.Diff([]AUM{child1, child2, child3}, got, cmpopts.SortSlices(aumHashesLess)); diff != "" {
+ t.Fatalf("ChildAUMs() output differs (-want, +got):\n%s", diff)
+ }
+
+ // Purge the parent AUM, and check the set of child AUMs is unchanged
+ chonk.PurgeAUMs([]AUMHash{parent.Hash()})
+
+ got = must.Get(chonk.ChildAUMs(parentHash))
+ if diff := cmp.Diff([]AUM{child1, child2, child3}, got, cmpopts.SortSlices(aumHashesLess)); diff != "" {
+ t.Fatalf("ChildAUMs() output differs (-want, +got):\n%s", diff)
+ }
+
+ // Now purge one of the child AUMs, and check it no longer appears as a child of the parent
+ chonk.PurgeAUMs([]AUMHash{child3.Hash()})
+
+ got = must.Get(chonk.ChildAUMs(parentHash))
+ if diff := cmp.Diff([]AUM{child1, child2}, got, cmpopts.SortSlices(aumHashesLess)); diff != "" {
+ t.Fatalf("ChildAUMs() output differs (-want, +got):\n%s", diff)
+ }
+}
+
func TestMarkActiveChain(t *testing.T) {
type aumTemplate struct {
AUM AUM
@@ -612,10 +658,7 @@ func (c *compactingChonkFake) CommitTime(hash AUMHash) (time.Time, error) {
}
func (c *compactingChonkFake) PurgeAUMs(hashes []AUMHash) error {
- lessHashes := func(a, b AUMHash) bool {
- return bytes.Compare(a[:], b[:]) < 0
- }
- if diff := cmp.Diff(c.wantDelete, hashes, cmpopts.SortSlices(lessHashes)); diff != "" {
+ if diff := cmp.Diff(c.wantDelete, hashes, cmpopts.SortSlices(hashesLess)); diff != "" {
c.t.Errorf("deletion set differs (-want, +got):\n%s", diff)
}
return nil