You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/tstest/chonktest/chonktest.go

257 lines
7.9 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package chonktest contains a shared set of tests for the Chonk
// interface used to store AUM messages in Tailnet Lock, which we can
// share between different implementations.
package chonktest
import (
"bytes"
"encoding/binary"
"math/rand"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/crypto/blake2s"
"tailscale.com/tka"
"tailscale.com/util/must"
)
// returns a random source based on the test name + extraSeed.
func testingRand(t *testing.T, extraSeed int64) *rand.Rand {
var seed int64
if err := binary.Read(bytes.NewBuffer([]byte(t.Name())), binary.LittleEndian, &seed); err != nil {
panic(err)
}
return rand.New(rand.NewSource(seed + extraSeed))
}
// randHash derives a fake blake2s hash from the test name
// and the given seed.
func randHash(t *testing.T, seed int64) [blake2s.Size]byte {
var out [blake2s.Size]byte
testingRand(t, seed).Read(out[:])
return out
}
func hashesLess(x, y tka.AUMHash) bool {
return bytes.Compare(x[:], y[:]) < 0
}
func aumHashesLess(x, y tka.AUM) bool {
return hashesLess(x.Hash(), y.Hash())
}
// RunChonkTests is a set of tests for the behaviour of a Chonk.
//
// Any implementation of Chonk should pass these tests, so we know all
// Chonks behave in the same way. If you want to test behaviour that's
// specific to one implementation, write a separate test.
func RunChonkTests(t *testing.T, newChonk func(*testing.T) tka.Chonk) {
t.Run("ChildAUMs", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
parentHash := randHash(t, 1)
data := []tka.AUM{
{
MessageKind: tka.AUMRemoveKey,
KeyID: []byte{1, 2},
PrevAUMHash: parentHash[:],
},
{
MessageKind: tka.AUMRemoveKey,
KeyID: []byte{3, 4},
PrevAUMHash: parentHash[:],
},
}
if err := chonk.CommitVerifiedAUMs(data); err != nil {
t.Fatalf("CommitVerifiedAUMs failed: %v", err)
}
stored, err := chonk.ChildAUMs(parentHash)
if err != nil {
t.Fatalf("ChildAUMs failed: %v", err)
}
if diff := cmp.Diff(data, stored, cmpopts.SortSlices(aumHashesLess)); diff != "" {
t.Errorf("stored AUM differs (-want, +got):\n%s", diff)
}
})
t.Run("AUMMissing", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
var notExists tka.AUMHash
notExists[:][0] = 42
if _, err := chonk.AUM(notExists); err != os.ErrNotExist {
t.Errorf("chonk.AUM(notExists).err = %v, want %v", err, os.ErrNotExist)
}
})
t.Run("ReadChainFromHead", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
genesis := tka.AUM{MessageKind: tka.AUMRemoveKey, KeyID: []byte{1, 2}}
gHash := genesis.Hash()
intermediate := tka.AUM{PrevAUMHash: gHash[:]}
iHash := intermediate.Hash()
leaf := tka.AUM{PrevAUMHash: iHash[:]}
commitSet := []tka.AUM{
genesis,
intermediate,
leaf,
}
if err := chonk.CommitVerifiedAUMs(commitSet); err != nil {
t.Fatalf("CommitVerifiedAUMs failed: %v", err)
}
t.Logf("genesis hash = %X", genesis.Hash())
t.Logf("intermediate hash = %X", intermediate.Hash())
t.Logf("leaf hash = %X", leaf.Hash())
// Read the chain from the leaf backwards.
gotLeafs, err := chonk.Heads()
if err != nil {
t.Fatalf("Heads failed: %v", err)
}
if diff := cmp.Diff([]tka.AUM{leaf}, gotLeafs); diff != "" {
t.Fatalf("leaf AUM differs (-want, +got):\n%s", diff)
}
parent, _ := gotLeafs[0].Parent()
gotIntermediate, err := chonk.AUM(parent)
if err != nil {
t.Fatalf("AUM(<intermediate>) failed: %v", err)
}
if diff := cmp.Diff(intermediate, gotIntermediate); diff != "" {
t.Errorf("intermediate AUM differs (-want, +got):\n%s", diff)
}
parent, _ = gotIntermediate.Parent()
gotGenesis, err := chonk.AUM(parent)
if err != nil {
t.Fatalf("AUM(<genesis>) failed: %v", err)
}
if diff := cmp.Diff(genesis, gotGenesis); diff != "" {
t.Errorf("genesis AUM differs (-want, +got):\n%s", diff)
}
})
t.Run("LastActiveAncestor", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
aum := tka.AUM{MessageKind: tka.AUMRemoveKey, KeyID: []byte{1, 2}}
hash := aum.Hash()
if err := chonk.SetLastActiveAncestor(hash); err != nil {
t.Fatal(err)
}
got, err := chonk.LastActiveAncestor()
if err != nil {
t.Fatal(err)
}
if got == nil || hash.String() != got.String() {
t.Errorf("LastActiveAncestor=%s, want %s", got, hash)
}
})
}
// RunCompactableChonkTests is a set of tests for the behaviour of a
// CompactableChonk.
//
// Any implementation of CompactableChonk should pass these tests, so we
// know all CompactableChonk behave in the same way. If you want to test
// behaviour that's specific to one implementation, write a separate test.
func RunCompactableChonkTests(t *testing.T, newChonk func(t *testing.T) tka.CompactableChonk) {
t.Run("PurgeAUMs", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
parentHash := randHash(t, 1)
aum := tka.AUM{MessageKind: tka.AUMNoOp, PrevAUMHash: parentHash[:]}
if err := chonk.CommitVerifiedAUMs([]tka.AUM{aum}); err != nil {
t.Fatal(err)
}
if err := chonk.PurgeAUMs([]tka.AUMHash{aum.Hash()}); err != nil {
t.Fatal(err)
}
if _, err := chonk.AUM(aum.Hash()); err != os.ErrNotExist {
t.Errorf("AUM() on purged AUM returned err = %v, want ErrNotExist", err)
}
})
t.Run("AllAUMs", func(t *testing.T) {
chonk := newChonk(t)
genesis := tka.AUM{MessageKind: tka.AUMRemoveKey, KeyID: []byte{1, 2}}
gHash := genesis.Hash()
intermediate := tka.AUM{PrevAUMHash: gHash[:]}
iHash := intermediate.Hash()
leaf := tka.AUM{PrevAUMHash: iHash[:]}
commitSet := []tka.AUM{
genesis,
intermediate,
leaf,
}
if err := chonk.CommitVerifiedAUMs(commitSet); err != nil {
t.Fatalf("CommitVerifiedAUMs failed: %v", err)
}
hashes, err := chonk.AllAUMs()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff([]tka.AUMHash{genesis.Hash(), intermediate.Hash(), leaf.Hash()}, hashes, cmpopts.SortSlices(hashesLess)); diff != "" {
t.Fatalf("AllAUMs() output differs (-want, +got):\n%s", diff)
}
})
t.Run("ChildAUMsOfPurgedAUM", func(t *testing.T) {
t.Parallel()
chonk := newChonk(t)
parent := tka.AUM{MessageKind: tka.AUMRemoveKey, KeyID: []byte{0, 0}}
parentHash := parent.Hash()
child1 := tka.AUM{MessageKind: tka.AUMAddKey, KeyID: []byte{1, 1}, PrevAUMHash: parentHash[:]}
child2 := tka.AUM{MessageKind: tka.AUMAddKey, KeyID: []byte{2, 2}, PrevAUMHash: parentHash[:]}
child3 := tka.AUM{MessageKind: tka.AUMAddKey, KeyID: []byte{3, 3}, PrevAUMHash: parentHash[:]}
child2Hash := child2.Hash()
grandchild2A := tka.AUM{MessageKind: tka.AUMAddKey, KeyID: []byte{2, 2, 2, 2}, PrevAUMHash: child2Hash[:]}
grandchild2B := tka.AUM{MessageKind: tka.AUMAddKey, KeyID: []byte{2, 2, 2, 2, 2}, PrevAUMHash: child2Hash[:]}
commitSet := []tka.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
childHashes := must.Get(chonk.ChildAUMs(parentHash))
if diff := cmp.Diff([]tka.AUM{child1, child2, child3}, childHashes, 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([]tka.AUMHash{parent.Hash()})
childHashes = must.Get(chonk.ChildAUMs(parentHash))
if diff := cmp.Diff([]tka.AUM{child1, child2, child3}, childHashes, 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([]tka.AUMHash{child3.Hash()})
childHashes = must.Get(chonk.ChildAUMs(parentHash))
if diff := cmp.Diff([]tka.AUM{child1, child2}, childHashes, cmpopts.SortSlices(aumHashesLess)); diff != "" {
t.Fatalf("ChildAUMs() output differs (-want, +got):\n%s", diff)
}
})
}