diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 1b59bdd4c..c69bb54e9 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -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 diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 89164828c..22ddea105 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -26,6 +26,11 @@ import ( 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{ diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 42fe8c28e..0f23d56d5 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -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 { diff --git a/tka/builder_test.go b/tka/builder_test.go index b3c599700..10ea71d19 100644 --- a/tka/builder_test.go +++ b/tka/builder_test.go @@ -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()) diff --git a/tka/scenario_test.go b/tka/scenario_test.go index eb96f1ec5..7aa7a960c 100644 --- a/tka/scenario_test.go +++ b/tka/scenario_test.go @@ -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 @@ outer: } 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? diff --git a/tka/sync.go b/tka/sync.go index 7d4443016..2594059e0 100644 --- a/tka/sync.go +++ b/tka/sync.go @@ -43,7 +43,19 @@ const ( 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 // 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) } diff --git a/tka/sync_test.go b/tka/sync_test.go index 65cba0ab8..47e0d7a05 100644 --- a/tka/sync_test.go +++ b/tka/sync_test.go @@ -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) } diff --git a/tka/tka.go b/tka/tka.go index 61434592c..17159d2e2 100644 --- a/tka/tka.go +++ b/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 } diff --git a/tka/tka_test.go b/tka/tka_test.go index 2215b5d33..f975a6b7b 100644 --- a/tka/tka_test.go +++ b/tka/tka_test.go @@ -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 {