tka/sync: add regression test for compacted nodes on forked chains

We previously identified sync failures that occur when a node falls behind
the remote, and compacts away most its local state. We fixed the underlying
issue in #19444, but that PR only tested the basic scenario where the
local chain is a direct ancestor of the remote chain.

This patch adds an explicit regression test for the case where a node is on
a fork (that is, its HEAD is not part of the remote's active chain).

Although #19444 happened to cover this case, other proposed patches did not
handle the forked state. Adding this test locks in the behaviour and prevents
future sync regressions in this area.

Also, add a shared helper for writing this sort of TKA sync test.

Updates tailscale/corp#40404

Change-Id: I78fdc6beaf71392edf11806197f126db48886f93
Signed-off-by: Alex Chan <alexc@tailscale.com>
This commit is contained in:
Alex Chan
2026-07-24 16:42:42 +01:00
committed by Alex Chan
parent 8c98d2a417
commit b5fb042501
2 changed files with 154 additions and 25 deletions
+58
View File
@@ -9,6 +9,8 @@ import (
"errors"
"fmt"
"os"
"tailscale.com/util/testenv"
)
// ErrNoIntersection is returned when a shared AUM could
@@ -257,3 +259,59 @@ func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, er
panic("unreachable")
}
// seedNode is an authority-chonk pair that can be seeded by [SeedAUMs].
type seedNode struct {
authority *Authority
storage Chonk
}
// CreateSeedNode creates a node for use with [SeedAUMs].
func CreateSeedNode(t testenv.TB, authority *Authority, storage Chonk) seedNode {
t.Helper()
return seedNode{authority, storage}
}
// SeedAUMs generates many AUMs by repeatedly adding and removing keys
// from the TKA.
//
// The AUMs are written to all the supplied nodes, so if you pass more
// than one, you can build up a long sync history.
//
// This is only for use in testing.
func SeedAUMs(t testenv.TB, count int, signer Signer, nodes ...seedNode) {
t.Helper()
if len(nodes) == 0 {
panic("called SeedAUMs without any nodes")
}
primaryNode := nodes[0]
// The key that we'll repeatedly add/remove in the TKA.
key := Key{Kind: Key25519, Public: []byte{1, 1, 1}, Votes: 1}
for i := 0; i < count/2; i++ {
for _, action := range []string{"add", "remove"} {
updater := primaryNode.authority.NewUpdater(signer)
if action == "add" {
if err := updater.AddKey(key); err != nil {
t.Fatalf("error from updater.AddKey: %v")
}
} else {
if err := updater.RemoveKey(key.MustID()); err != nil {
t.Fatalf("error from updater.RemoveKey: %v")
}
}
aum, err := updater.Finalize(primaryNode.storage)
if err != nil {
t.Fatalf("error from authority.Finalize: %v", err)
}
for _, n := range nodes {
if err := n.authority.Inform(n.storage, aum); err != nil {
t.Fatalf("error from authority.Inform: %v", err)
}
}
}
}
}