mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
tka: make storage a parameter rather than an Authority struct member
Updates #5435 Based on the discussion in #5435, we can better support transactional data models by making the underlying storage layer a parameter (which can be specialized for the request) rather than a long-lived member of Authority. Now that Authority is just an instantaneous snapshot of state, we can do things like provide idempotent methods and make it cloneable, too. Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
7d1357162e
commit
79905a1162
@ -148,7 +148,7 @@ type LocalBackend struct {
|
||||
inServerMode bool
|
||||
machinePrivKey key.MachinePrivate
|
||||
nlPrivKey key.NLPrivate
|
||||
tka *tka.Authority
|
||||
tka *tkaState
|
||||
state ipn.State
|
||||
capFileSharing bool // whether netMap contains the file sharing capability
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
@ -2507,8 +2507,11 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
// used for locked tailnets.
|
||||
//
|
||||
// It should only be called before the LocalBackend is used.
|
||||
func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority) {
|
||||
b.tka = a
|
||||
func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority, storage *tka.FS) {
|
||||
b.tka = &tkaState{
|
||||
authority: a,
|
||||
storage: storage,
|
||||
}
|
||||
}
|
||||
|
||||
// SetVarRoot sets the root directory of Tailscale's writable
|
||||
|
@ -26,6 +26,11 @@
|
||||
|
||||
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
|
||||
|
||||
type tkaState struct {
|
||||
authority *tka.Authority
|
||||
storage *tka.FS
|
||||
}
|
||||
|
||||
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() bool {
|
||||
@ -54,7 +59,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
}
|
||||
|
||||
var head [32]byte
|
||||
h := b.tka.Head()
|
||||
h := b.tka.authority.Head()
|
||||
copy(head[:], h[:])
|
||||
|
||||
return &ipnstate.NetworkLockStatus{
|
||||
|
@ -775,15 +775,15 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
chonkDir := filepath.Join(root, "chonk")
|
||||
if _, err := os.Stat(chonkDir); err == nil {
|
||||
// The directory exists, which means network-lock has been initialized.
|
||||
chonk, err := tka.ChonkDir(chonkDir)
|
||||
storage, err := tka.ChonkDir(chonkDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening tailchonk: %v", err)
|
||||
}
|
||||
authority, err := tka.Open(chonk)
|
||||
authority, err := tka.Open(storage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing tka: %v", err)
|
||||
}
|
||||
b.SetTailnetKeyAuthority(authority)
|
||||
b.SetTailnetKeyAuthority(authority, storage)
|
||||
logf("tka initialized at head %x", authority.Head())
|
||||
}
|
||||
} else {
|
||||
|
@ -28,7 +28,8 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||
|
||||
a, _, err := Create(&Mem{}, State{
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
@ -50,7 +51,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
|
||||
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the new key is there.
|
||||
if err := a.Inform(updates); err != nil {
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
if _, err := a.state.GetKey(key2.ID()); err != nil {
|
||||
@ -64,7 +65,8 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
|
||||
pub2, _ := testingKey25519(t, 2)
|
||||
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
|
||||
|
||||
a, _, err := Create(&Mem{}, State{
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key, key2},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
@ -83,7 +85,7 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
|
||||
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the key has been removed.
|
||||
if err := a.Inform(updates); err != nil {
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
if _, err := a.state.GetKey(key2.ID()); err != ErrNoSuchKey {
|
||||
@ -95,7 +97,8 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||
|
||||
a, _, err := Create(&Mem{}, State{
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
@ -114,7 +117,7 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
|
||||
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the update is there.
|
||||
if err := a.Inform(updates); err != nil {
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
k, err := a.state.GetKey(key.ID())
|
||||
@ -130,7 +133,8 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2, Meta: map[string]string{"a": "b"}}
|
||||
|
||||
a, _, err := Create(&Mem{}, State{
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
@ -149,7 +153,7 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
|
||||
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the update is there.
|
||||
if err := a.Inform(updates); err != nil {
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
k, err := a.state.GetKey(key.ID())
|
||||
@ -165,7 +169,8 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
pub, priv := testingKey25519(t, 1)
|
||||
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||
|
||||
a, _, err := Create(&Mem{}, State{
|
||||
storage := &Mem{}
|
||||
a, _, err := Create(storage, State{
|
||||
Keys: []Key{key},
|
||||
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||
}, signer25519(priv))
|
||||
@ -193,7 +198,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
|
||||
|
||||
// See if the update is valid by applying it to the authority
|
||||
// + checking if the update is there.
|
||||
if err := a.Inform(updates); err != nil {
|
||||
if err := a.Inform(storage, updates); err != nil {
|
||||
t.Fatalf("could not apply generated updates: %v", err)
|
||||
}
|
||||
k, err := a.state.GetKey(key2.ID())
|
||||
|
@ -16,6 +16,8 @@ type scenarioNode struct {
|
||||
Name string
|
||||
A *Authority
|
||||
AUMs map[string]AUM
|
||||
|
||||
storage Chonk
|
||||
}
|
||||
|
||||
type scenarioTest struct {
|
||||
@ -30,7 +32,8 @@ type scenarioTest struct {
|
||||
}
|
||||
|
||||
func (s *scenarioTest) mkNode(name string) *scenarioNode {
|
||||
authority, err := Open(s.initial.Chonk())
|
||||
storage := s.initial.Chonk()
|
||||
authority, err := Open(storage)
|
||||
if err != nil {
|
||||
s.t.Fatal(err)
|
||||
}
|
||||
@ -41,9 +44,10 @@ func (s *scenarioTest) mkNode(name string) *scenarioNode {
|
||||
}
|
||||
|
||||
n := &scenarioNode{
|
||||
A: authority,
|
||||
AUMs: aums,
|
||||
Name: name,
|
||||
A: authority,
|
||||
AUMs: aums,
|
||||
Name: name,
|
||||
storage: storage,
|
||||
}
|
||||
|
||||
s.nodes[name] = n
|
||||
@ -89,7 +93,7 @@ func (s *scenarioTest) mkNodeWithForks(name string, signWithDefault bool, chains
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err := n.A.Inform(aums); err != nil {
|
||||
if err := n.A.Inform(n.storage, aums); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@ -114,27 +118,27 @@ func aumsToNames(n *scenarioNode, aums []AUM) string {
|
||||
}
|
||||
|
||||
func (s *scenarioTest) syncBetween(n1, n2 *scenarioNode) error {
|
||||
o1, err := n1.A.SyncOffer()
|
||||
o1, err := n1.A.SyncOffer(n1.storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o2, err := n2.A.SyncOffer()
|
||||
o2, err := n2.A.SyncOffer(n2.storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aumsFrom1, err := n1.A.MissingAUMs(o2)
|
||||
aumsFrom1, err := n1.A.MissingAUMs(n1.storage, o2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aumsFrom2, err := n2.A.MissingAUMs(o1)
|
||||
aumsFrom2, err := n2.A.MissingAUMs(n2.storage, o1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := n2.A.Inform(aumsFrom1); err != nil {
|
||||
if err := n2.A.Inform(n2.storage, aumsFrom1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := n1.A.Inform(aumsFrom2); err != nil {
|
||||
if err := n1.A.Inform(n1.storage, aumsFrom2); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -303,7 +307,7 @@ func TestInvalidAUMPropergationRejected(t *testing.T) {
|
||||
l4 := AUM{MessageKind: AUMAddKey, PrevAUMHash: l3H[:]}
|
||||
l4.sign25519(s.defaultPriv)
|
||||
l4H := l4.Hash()
|
||||
n1.A.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.A.state.LastAUMHash = &l4H
|
||||
|
||||
// Does control nope out with syncing?
|
||||
@ -336,7 +340,7 @@ func TestUnsignedAUMPropergationRejected(t *testing.T) {
|
||||
l3H := l3.Hash()
|
||||
l4 := AUM{MessageKind: AUMNoOp, PrevAUMHash: l3H[:]}
|
||||
l4H := l4.Hash()
|
||||
n1.A.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.A.state.LastAUMHash = &l4H
|
||||
|
||||
// Does control nope out with syncing?
|
||||
@ -370,7 +374,7 @@ func TestBadSigAUMPropergationRejected(t *testing.T) {
|
||||
l4.sign25519(s.defaultPriv)
|
||||
l4.Signatures[0].Signature[3] = 42
|
||||
l4H := l4.Hash()
|
||||
n1.A.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.storage.CommitVerifiedAUMs([]AUM{l4})
|
||||
n1.A.state.LastAUMHash = &l4H
|
||||
|
||||
// Does control nope out with syncing?
|
||||
|
56
tka/sync.go
56
tka/sync.go
@ -43,7 +43,19 @@ type SyncOffer struct {
|
||||
ancestorsSkipShift = 2
|
||||
)
|
||||
|
||||
func (a *Authority) syncOffer() (SyncOffer, error) {
|
||||
// SyncOffer returns an abbreviated description of the current AUM
|
||||
// chain, which can be used to synchronize with another (untrusted)
|
||||
// Authority instance.
|
||||
//
|
||||
// The returned SyncOffer structure should be transmitted to the remote
|
||||
// Authority, which should call MissingAUMs() using it to determine
|
||||
// AUMs which need to be transmitted. This list of AUMs from the remote
|
||||
// can then be applied locally with Inform().
|
||||
//
|
||||
// This SyncOffer + AUM exchange should be performed by both ends,
|
||||
// because its possible that either end has AUMs that the other needs
|
||||
// to find out about.
|
||||
func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
|
||||
oldest := a.oldestAncestor.Hash()
|
||||
|
||||
out := SyncOffer{
|
||||
@ -65,7 +77,7 @@ func (a *Authority) syncOffer() (SyncOffer, error) {
|
||||
skipAmount = skipAmount << ancestorsSkipShift
|
||||
}
|
||||
|
||||
parent, err := a.storage.AUM(curs)
|
||||
parent, err := storage.AUM(curs)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
return SyncOffer{}, err
|
||||
@ -84,22 +96,6 @@ func (a *Authority) syncOffer() (SyncOffer, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SyncOffer returns an abbreviated description of the current AUM
|
||||
// chain, which can be used to synchronize with another (untrusted)
|
||||
// Authority instance.
|
||||
//
|
||||
// The returned SyncOffer structure should be transmitted to the remote
|
||||
// Authority, which should call MissingAUMs() using it to determine
|
||||
// AUMs which need to be transmitted. This list of AUMs from the remote
|
||||
// can then be applied locally with Inform().
|
||||
//
|
||||
// This SyncOffer + AUM exchange should be performed by both ends,
|
||||
// because its possible that either end has AUMs that the other needs
|
||||
// to find out about.
|
||||
func (a *Authority) SyncOffer() (SyncOffer, error) {
|
||||
return a.syncOffer()
|
||||
}
|
||||
|
||||
// intersection describes how to synchronize AUMs with a remote
|
||||
// authority.
|
||||
type intersection struct {
|
||||
@ -119,7 +115,7 @@ type intersection struct {
|
||||
// computeSyncIntersection determines the common AUMs between a local and
|
||||
// remote SyncOffer. This intersection can be used to synchronize both
|
||||
// sides.
|
||||
func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncOffer) (*intersection, error) {
|
||||
func computeSyncIntersection(storage Chonk, localOffer, remoteOffer SyncOffer) (*intersection, error) {
|
||||
// Simple case: up to date.
|
||||
if remoteOffer.Head == localOffer.Head {
|
||||
return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil
|
||||
@ -136,7 +132,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
|
||||
// <Them> A -> B
|
||||
// ∴ their head intersects with our chain, we need to send C
|
||||
var hasRemoteHead bool
|
||||
_, err := authority.storage.AUM(remoteOffer.Head)
|
||||
_, err := storage.AUM(remoteOffer.Head)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
return nil, err
|
||||
@ -148,7 +144,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
|
||||
if hasRemoteHead {
|
||||
curs := localOffer.Head
|
||||
for i := 0; i < maxSyncHeadIntersectionIter; i++ {
|
||||
parent, err := authority.storage.AUM(curs)
|
||||
parent, err := storage.AUM(curs)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
return nil, err
|
||||
@ -176,7 +172,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
|
||||
// a bit of luck we can use an earlier one and hence do less work /
|
||||
// transmit fewer AUMs.
|
||||
for _, a := range remoteOffer.Ancestors {
|
||||
state, err := computeStateAt(authority.storage, maxSyncIter, a)
|
||||
state, err := computeStateAt(storage, maxSyncIter, a)
|
||||
if err != nil {
|
||||
if err != os.ErrNotExist {
|
||||
return nil, fmt.Errorf("computeStateAt: %v", err)
|
||||
@ -184,7 +180,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
|
||||
continue
|
||||
}
|
||||
|
||||
end, _, err := fastForward(authority.storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
end, _, err := fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
return curs.Hash() == localOffer.Head
|
||||
})
|
||||
if err != nil {
|
||||
@ -203,12 +199,12 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
|
||||
|
||||
// MissingAUMs returns AUMs a remote may be missing based on the
|
||||
// remotes' SyncOffer.
|
||||
func (a *Authority) MissingAUMs(remoteOffer SyncOffer) ([]AUM, error) {
|
||||
localOffer, err := a.syncOffer()
|
||||
func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, error) {
|
||||
localOffer, err := a.SyncOffer(storage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("local syncOffer: %v", err)
|
||||
}
|
||||
intersection, err := computeSyncIntersection(a, localOffer, remoteOffer)
|
||||
intersection, err := computeSyncIntersection(storage, localOffer, remoteOffer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("intersection: %v", err)
|
||||
}
|
||||
@ -218,12 +214,12 @@ func (a *Authority) MissingAUMs(remoteOffer SyncOffer) ([]AUM, error) {
|
||||
out := make([]AUM, 0, 12) // 12 chosen arbitrarily.
|
||||
|
||||
if intersection.headIntersection != nil {
|
||||
state, err := computeStateAt(a.storage, maxSyncIter, *intersection.headIntersection)
|
||||
state, err := computeStateAt(storage, maxSyncIter, *intersection.headIntersection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, err = fastForward(a.storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
_, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
if curs.Hash() != *intersection.headIntersection {
|
||||
out = append(out, curs)
|
||||
}
|
||||
@ -233,12 +229,12 @@ func (a *Authority) MissingAUMs(remoteOffer SyncOffer) ([]AUM, error) {
|
||||
}
|
||||
|
||||
if intersection.tailIntersection != nil {
|
||||
state, err := computeStateAt(a.storage, maxSyncIter, *intersection.tailIntersection)
|
||||
state, err := computeStateAt(storage, maxSyncIter, *intersection.tailIntersection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, err = fastForward(a.storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
_, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
|
||||
if curs.Hash() != *intersection.tailIntersection {
|
||||
out = append(out, curs)
|
||||
}
|
||||
|
@ -18,11 +18,12 @@ func TestSyncOffer(t *testing.T) {
|
||||
A10 -> A11 -> A12 -> A13 -> A14 -> A15 -> A16 -> A17 -> A18
|
||||
A18 -> A19 -> A20 -> A21 -> A22 -> A23 -> A24 -> A25
|
||||
`)
|
||||
a, err := Open(c.Chonk())
|
||||
storage := c.Chonk()
|
||||
a, err := Open(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := a.SyncOffer()
|
||||
got, err := a.SyncOffer(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -56,7 +57,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer1, err := n1.SyncOffer()
|
||||
offer1, err := n1.SyncOffer(chonk1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -66,7 +67,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer2, err := n2.SyncOffer()
|
||||
offer2, err := n2.SyncOffer(chonk2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -74,7 +75,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
|
||||
// Node 1 only knows about the first two nodes, so the head of n2 is
|
||||
// alien to it.
|
||||
t.Run("n1", func(t *testing.T) {
|
||||
got, err := computeSyncIntersection(n1, offer1, offer2)
|
||||
got, err := computeSyncIntersection(chonk1, offer1, offer2)
|
||||
if err != nil {
|
||||
t.Fatalf("computeSyncIntersection() failed: %v", err)
|
||||
}
|
||||
@ -89,7 +90,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
|
||||
// Node 2 knows about the full chain, so it can see that the head of n1
|
||||
// intersects with a subset of its chain (a Head Intersection).
|
||||
t.Run("n2", func(t *testing.T) {
|
||||
got, err := computeSyncIntersection(n2, offer2, offer1)
|
||||
got, err := computeSyncIntersection(chonk2, offer2, offer1)
|
||||
if err != nil {
|
||||
t.Fatalf("computeSyncIntersection() failed: %v", err)
|
||||
}
|
||||
@ -122,11 +123,12 @@ func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
|
||||
t.Fatal("failed assert: h(a9) > h(f1H)\nTweak hashSeed till this passes")
|
||||
}
|
||||
|
||||
n1, err := Open(c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "F1"))
|
||||
chonk1 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "F1")
|
||||
n1, err := Open(chonk1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer1, err := n1.SyncOffer()
|
||||
offer1, err := n1.SyncOffer(chonk1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -140,11 +142,12 @@ func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
|
||||
t.Errorf("offer1 diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
n2, err := Open(c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10"))
|
||||
chonk2 := c.ChonkWith("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10")
|
||||
n2, err := Open(chonk2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer2, err := n2.SyncOffer()
|
||||
offer2, err := n2.SyncOffer(chonk2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -164,7 +167,7 @@ func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
|
||||
// n2 has 10 nodes, so the first common ancestor should be 10-ancestorsSkipStart
|
||||
wantIntersection := c.AUMHashes["A"+strconv.Itoa(10-ancestorsSkipStart)]
|
||||
|
||||
got, err := computeSyncIntersection(n1, offer1, offer2)
|
||||
got, err := computeSyncIntersection(chonk1, offer1, offer2)
|
||||
if err != nil {
|
||||
t.Fatalf("computeSyncIntersection() failed: %v", err)
|
||||
}
|
||||
@ -181,7 +184,7 @@ func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
|
||||
// n1 has 9 nodes, so the first common ancestor should be 9-ancestorsSkipStart
|
||||
wantIntersection := c.AUMHashes["A"+strconv.Itoa(9-ancestorsSkipStart)]
|
||||
|
||||
got, err := computeSyncIntersection(n2, offer2, offer1)
|
||||
got, err := computeSyncIntersection(chonk2, offer2, offer1)
|
||||
if err != nil {
|
||||
t.Fatalf("computeSyncIntersection() failed: %v", err)
|
||||
}
|
||||
@ -210,7 +213,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer1, err := n1.SyncOffer()
|
||||
offer1, err := n1.SyncOffer(chonk1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -220,7 +223,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer2, err := n2.SyncOffer()
|
||||
offer2, err := n2.SyncOffer(chonk2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -229,7 +232,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
|
||||
// alien to it. As such, it should send history from the newest ancestor,
|
||||
// A1 (if the chain was longer there would be one in the middle).
|
||||
t.Run("n1", func(t *testing.T) {
|
||||
got, err := n1.MissingAUMs(offer2)
|
||||
got, err := n1.MissingAUMs(chonk1, offer2)
|
||||
if err != nil {
|
||||
t.Fatalf("MissingAUMs() failed: %v", err)
|
||||
}
|
||||
@ -245,7 +248,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
|
||||
// Node 2 knows about the full chain, so it can see that the head of n1
|
||||
// intersects with a subset of its chain (a Head Intersection).
|
||||
t.Run("n2", func(t *testing.T) {
|
||||
got, err := n2.MissingAUMs(offer1)
|
||||
got, err := n2.MissingAUMs(chonk2, offer1)
|
||||
if err != nil {
|
||||
t.Fatalf("MissingAUMs() failed: %v", err)
|
||||
}
|
||||
@ -277,7 +280,7 @@ func TestMissingAUMs_Fork(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer1, err := n1.SyncOffer()
|
||||
offer1, err := n1.SyncOffer(chonk1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -287,13 +290,13 @@ func TestMissingAUMs_Fork(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
offer2, err := n2.SyncOffer()
|
||||
offer2, err := n2.SyncOffer(chonk2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("n1", func(t *testing.T) {
|
||||
got, err := n1.MissingAUMs(offer2)
|
||||
got, err := n1.MissingAUMs(chonk1, offer2)
|
||||
if err != nil {
|
||||
t.Fatalf("MissingAUMs() failed: %v", err)
|
||||
}
|
||||
@ -311,7 +314,7 @@ func TestMissingAUMs_Fork(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("n2", func(t *testing.T) {
|
||||
got, err := n2.MissingAUMs(offer1)
|
||||
got, err := n2.MissingAUMs(chonk2, offer1)
|
||||
if err != nil {
|
||||
t.Fatalf("MissingAUMs() failed: %v", err)
|
||||
}
|
||||
@ -344,26 +347,28 @@ func TestSyncSimpleE2E(t *testing.T) {
|
||||
optKey("key", key, priv),
|
||||
optSignAllUsing("key"))
|
||||
|
||||
node, err := Bootstrap(&Mem{}, c.AUMs["G1"])
|
||||
nodeStorage := &Mem{}
|
||||
node, err := Bootstrap(nodeStorage, c.AUMs["G1"])
|
||||
if err != nil {
|
||||
t.Fatalf("node Bootstrap() failed: %v", err)
|
||||
}
|
||||
control, err := Open(c.Chonk())
|
||||
controlStorage := c.Chonk()
|
||||
control, err := Open(controlStorage)
|
||||
if err != nil {
|
||||
t.Fatalf("control Open() failed: %v", err)
|
||||
}
|
||||
|
||||
// Control knows the full chain, node only knows the genesis. Lets see
|
||||
// if they can sync.
|
||||
nodeOffer, err := node.SyncOffer()
|
||||
nodeOffer, err := node.SyncOffer(nodeStorage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controlAUMs, err := control.MissingAUMs(nodeOffer)
|
||||
controlAUMs, err := control.MissingAUMs(controlStorage, nodeOffer)
|
||||
if err != nil {
|
||||
t.Fatalf("control.MissingAUMs(%v) failed: %v", nodeOffer, err)
|
||||
}
|
||||
if err := node.Inform(controlAUMs); err != nil {
|
||||
if err := node.Inform(nodeStorage, controlAUMs); err != nil {
|
||||
t.Fatalf("node.Inform(%v) failed: %v", controlAUMs, err)
|
||||
}
|
||||
|
||||
|
78
tka/tka.go
78
tka/tka.go
@ -39,8 +39,15 @@ type Authority struct {
|
||||
head AUM
|
||||
oldestAncestor AUM
|
||||
state State
|
||||
}
|
||||
|
||||
storage Chonk
|
||||
// Clone duplicates the Authority structure.
|
||||
func (a *Authority) Clone() *Authority {
|
||||
return &Authority{
|
||||
head: a.head,
|
||||
oldestAncestor: a.oldestAncestor,
|
||||
state: a.state.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// A chain describes a linear sequence of updates from Oldest to Head,
|
||||
@ -477,7 +484,6 @@ func Open(storage Chonk) (*Authority, error) {
|
||||
return &Authority{
|
||||
head: c.Head,
|
||||
oldestAncestor: c.Oldest,
|
||||
storage: storage,
|
||||
state: c.state,
|
||||
}, nil
|
||||
}
|
||||
@ -557,12 +563,18 @@ func (a *Authority) ValidDisablement(secret []byte) bool {
|
||||
return a.state.checkDisablement(secret)
|
||||
}
|
||||
|
||||
// Inform is called to tell the authority about new updates. Updates
|
||||
// should be ordered oldest to newest. An error is returned if any
|
||||
// of the updates could not be processed.
|
||||
func (a *Authority) Inform(updates []AUM) error {
|
||||
// InformIdempotent returns a new Authority based on applying the given
|
||||
// updates, with the given updates committed to storage.
|
||||
//
|
||||
// If any of the updates could not be applied:
|
||||
// - An error is returned
|
||||
// - No changes to storage are made.
|
||||
//
|
||||
// MissingAUMs() should be used to get a list of updates appropriate for
|
||||
// this function. In any case, updates should be ordered oldest to newest.
|
||||
func (a *Authority) InformIdempotent(storage Chonk, updates []AUM) (Authority, error) {
|
||||
if len(updates) == 0 {
|
||||
return errors.New("inform called with empty slice")
|
||||
return Authority{}, errors.New("inform called with empty slice")
|
||||
}
|
||||
stateAt := make(map[AUMHash]State, len(updates)+1)
|
||||
toCommit := make([]AUM, 0, len(updates))
|
||||
@ -584,30 +596,30 @@ func (a *Authority) Inform(updates []AUM) error {
|
||||
for i, update := range updates {
|
||||
hash := update.Hash()
|
||||
// Check if we already have this AUM thus don't need to process it.
|
||||
if _, err := a.storage.AUM(hash); err == nil {
|
||||
if _, err := storage.AUM(hash); err == nil {
|
||||
isHeadChain = false // Disable the head-chain optimization.
|
||||
continue
|
||||
}
|
||||
|
||||
parent, hasParent := update.Parent()
|
||||
if !hasParent {
|
||||
return fmt.Errorf("update %d: missing parent", i)
|
||||
return Authority{}, fmt.Errorf("update %d: missing parent", i)
|
||||
}
|
||||
|
||||
state, hasState := stateAt[parent]
|
||||
var err error
|
||||
if !hasState {
|
||||
if state, err = computeStateAt(a.storage, 2000, parent); err != nil {
|
||||
return fmt.Errorf("update %d computing state: %v", i, err)
|
||||
if state, err = computeStateAt(storage, 2000, parent); err != nil {
|
||||
return Authority{}, fmt.Errorf("update %d computing state: %v", i, err)
|
||||
}
|
||||
stateAt[parent] = state
|
||||
}
|
||||
|
||||
if err := aumVerify(update, state, false); err != nil {
|
||||
return fmt.Errorf("update %d invalid: %v", i, err)
|
||||
return Authority{}, fmt.Errorf("update %d invalid: %v", i, err)
|
||||
}
|
||||
if stateAt[hash], err = state.applyVerifiedAUM(update); err != nil {
|
||||
return fmt.Errorf("update %d cannot be applied: %v", i, err)
|
||||
return Authority{}, fmt.Errorf("update %d cannot be applied: %v", i, err)
|
||||
}
|
||||
|
||||
if isHeadChain && parent != prevHash {
|
||||
@ -617,26 +629,40 @@ func (a *Authority) Inform(updates []AUM) error {
|
||||
toCommit = append(toCommit, update)
|
||||
}
|
||||
|
||||
if err := a.storage.CommitVerifiedAUMs(toCommit); err != nil {
|
||||
return fmt.Errorf("commit: %v", err)
|
||||
if err := storage.CommitVerifiedAUMs(toCommit); err != nil {
|
||||
return Authority{}, fmt.Errorf("commit: %v", err)
|
||||
}
|
||||
|
||||
if isHeadChain {
|
||||
// Head-chain fastpath: We can use the state we computed
|
||||
// in the last iteration.
|
||||
a.head = updates[len(updates)-1]
|
||||
a.state = stateAt[prevHash]
|
||||
} else {
|
||||
oldestAncestor := a.oldestAncestor.Hash()
|
||||
c, err := computeActiveChain(a.storage, &oldestAncestor, 2000)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recomputing active chain: %v", err)
|
||||
}
|
||||
a.head = c.Head
|
||||
a.oldestAncestor = c.Oldest
|
||||
a.state = c.state
|
||||
return Authority{
|
||||
head: updates[len(updates)-1],
|
||||
oldestAncestor: a.oldestAncestor,
|
||||
state: stateAt[prevHash],
|
||||
}, nil
|
||||
}
|
||||
|
||||
oldestAncestor := a.oldestAncestor.Hash()
|
||||
c, err := computeActiveChain(storage, &oldestAncestor, 2000)
|
||||
if err != nil {
|
||||
return Authority{}, fmt.Errorf("recomputing active chain: %v", err)
|
||||
}
|
||||
return Authority{
|
||||
head: c.Head,
|
||||
oldestAncestor: c.Oldest,
|
||||
state: c.state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Inform is the same as InformIdempotent, except the state of the Authority
|
||||
// is updated in-place.
|
||||
func (a *Authority) Inform(storage Chonk, updates []AUM) error {
|
||||
newAuthority, err := a.InformIdempotent(storage, updates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*a = newAuthority
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -376,7 +376,7 @@ func TestAuthorityInformNonLinear(t *testing.T) {
|
||||
// and forcing Inform() to take the slow path.
|
||||
informAUMs := []AUM{c.AUMs["L1"], c.AUMs["L2"], c.AUMs["L3"], c.AUMs["L4"], c.AUMs["L5"]}
|
||||
|
||||
if err := a.Inform(informAUMs); err != nil {
|
||||
if err := a.Inform(storage, informAUMs); err != nil {
|
||||
t.Fatalf("Inform() failed: %v", err)
|
||||
}
|
||||
for i, update := range informAUMs {
|
||||
@ -419,7 +419,7 @@ func TestAuthorityInformLinear(t *testing.T) {
|
||||
|
||||
informAUMs := []AUM{c.AUMs["L1"], c.AUMs["L2"], c.AUMs["L3"]}
|
||||
|
||||
if err := a.Inform(informAUMs); err != nil {
|
||||
if err := a.Inform(storage, informAUMs); err != nil {
|
||||
t.Fatalf("Inform() failed: %v", err)
|
||||
}
|
||||
for i, update := range informAUMs {
|
||||
|
Loading…
Reference in New Issue
Block a user