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:
Tom DNetto 2022-08-26 09:45:16 -07:00 committed by Tom
parent 7d1357162e
commit 79905a1162
9 changed files with 158 additions and 114 deletions

View File

@ -148,7 +148,7 @@ type LocalBackend struct {
inServerMode bool inServerMode bool
machinePrivKey key.MachinePrivate machinePrivKey key.MachinePrivate
nlPrivKey key.NLPrivate nlPrivKey key.NLPrivate
tka *tka.Authority tka *tkaState
state ipn.State state ipn.State
capFileSharing bool // whether netMap contains the file sharing capability capFileSharing bool // whether netMap contains the file sharing capability
// hostinfo is mutated in-place while mu is held. // 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. // used for locked tailnets.
// //
// It should only be called before the LocalBackend is used. // It should only be called before the LocalBackend is used.
func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority) { func (b *LocalBackend) SetTailnetKeyAuthority(a *tka.Authority, storage *tka.FS) {
b.tka = a b.tka = &tkaState{
authority: a,
storage: storage,
}
} }
// SetVarRoot sets the root directory of Tailscale's writable // SetVarRoot sets the root directory of Tailscale's writable

View File

@ -26,6 +26,11 @@
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK") 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 // CanSupportNetworkLock returns true if tailscaled is able to operate
// a local tailnet key authority (and hence enforce network lock). // a local tailnet key authority (and hence enforce network lock).
func (b *LocalBackend) CanSupportNetworkLock() bool { func (b *LocalBackend) CanSupportNetworkLock() bool {
@ -54,7 +59,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
} }
var head [32]byte var head [32]byte
h := b.tka.Head() h := b.tka.authority.Head()
copy(head[:], h[:]) copy(head[:], h[:])
return &ipnstate.NetworkLockStatus{ return &ipnstate.NetworkLockStatus{

View File

@ -775,15 +775,15 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
chonkDir := filepath.Join(root, "chonk") chonkDir := filepath.Join(root, "chonk")
if _, err := os.Stat(chonkDir); err == nil { if _, err := os.Stat(chonkDir); err == nil {
// The directory exists, which means network-lock has been initialized. // The directory exists, which means network-lock has been initialized.
chonk, err := tka.ChonkDir(chonkDir) storage, err := tka.ChonkDir(chonkDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("opening tailchonk: %v", err) return nil, fmt.Errorf("opening tailchonk: %v", err)
} }
authority, err := tka.Open(chonk) authority, err := tka.Open(storage)
if err != nil { if err != nil {
return nil, fmt.Errorf("initializing tka: %v", err) return nil, fmt.Errorf("initializing tka: %v", err)
} }
b.SetTailnetKeyAuthority(authority) b.SetTailnetKeyAuthority(authority, storage)
logf("tka initialized at head %x", authority.Head()) logf("tka initialized at head %x", authority.Head())
} }
} else { } else {

View File

@ -28,7 +28,8 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
pub, priv := testingKey25519(t, 1) pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2} key := Key{Kind: Key25519, Public: pub, Votes: 2}
a, _, err := Create(&Mem{}, State{ storage := &Mem{}
a, _, err := Create(storage, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, signer25519(priv)) }, signer25519(priv))
@ -50,7 +51,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
// See if the update is valid by applying it to the authority // See if the update is valid by applying it to the authority
// + checking if the new key is there. // + 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) t.Fatalf("could not apply generated updates: %v", err)
} }
if _, err := a.state.GetKey(key2.ID()); err != nil { if _, err := a.state.GetKey(key2.ID()); err != nil {
@ -64,7 +65,8 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
pub2, _ := testingKey25519(t, 2) pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1} key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
a, _, err := Create(&Mem{}, State{ storage := &Mem{}
a, _, err := Create(storage, State{
Keys: []Key{key, key2}, Keys: []Key{key, key2},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, signer25519(priv)) }, signer25519(priv))
@ -83,7 +85,7 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
// See if the update is valid by applying it to the authority // See if the update is valid by applying it to the authority
// + checking if the key has been removed. // + 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) t.Fatalf("could not apply generated updates: %v", err)
} }
if _, err := a.state.GetKey(key2.ID()); err != ErrNoSuchKey { if _, err := a.state.GetKey(key2.ID()); err != ErrNoSuchKey {
@ -95,7 +97,8 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
pub, priv := testingKey25519(t, 1) pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2} key := Key{Kind: Key25519, Public: pub, Votes: 2}
a, _, err := Create(&Mem{}, State{ storage := &Mem{}
a, _, err := Create(storage, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, signer25519(priv)) }, signer25519(priv))
@ -114,7 +117,7 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
// See if the update is valid by applying it to the authority // See if the update is valid by applying it to the authority
// + checking if the update is there. // + 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) t.Fatalf("could not apply generated updates: %v", err)
} }
k, err := a.state.GetKey(key.ID()) k, err := a.state.GetKey(key.ID())
@ -130,7 +133,8 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
pub, priv := testingKey25519(t, 1) pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2, Meta: map[string]string{"a": "b"}} 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}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, signer25519(priv)) }, signer25519(priv))
@ -149,7 +153,7 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
// See if the update is valid by applying it to the authority // See if the update is valid by applying it to the authority
// + checking if the update is there. // + 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) t.Fatalf("could not apply generated updates: %v", err)
} }
k, err := a.state.GetKey(key.ID()) k, err := a.state.GetKey(key.ID())
@ -165,7 +169,8 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
pub, priv := testingKey25519(t, 1) pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2} key := Key{Kind: Key25519, Public: pub, Votes: 2}
a, _, err := Create(&Mem{}, State{ storage := &Mem{}
a, _, err := Create(storage, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, signer25519(priv)) }, signer25519(priv))
@ -193,7 +198,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
// See if the update is valid by applying it to the authority // See if the update is valid by applying it to the authority
// + checking if the update is there. // + 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) t.Fatalf("could not apply generated updates: %v", err)
} }
k, err := a.state.GetKey(key2.ID()) k, err := a.state.GetKey(key2.ID())

View File

@ -16,6 +16,8 @@ type scenarioNode struct {
Name string Name string
A *Authority A *Authority
AUMs map[string]AUM AUMs map[string]AUM
storage Chonk
} }
type scenarioTest struct { type scenarioTest struct {
@ -30,7 +32,8 @@ type scenarioTest struct {
} }
func (s *scenarioTest) mkNode(name string) *scenarioNode { func (s *scenarioTest) mkNode(name string) *scenarioNode {
authority, err := Open(s.initial.Chonk()) storage := s.initial.Chonk()
authority, err := Open(storage)
if err != nil { if err != nil {
s.t.Fatal(err) s.t.Fatal(err)
} }
@ -44,6 +47,7 @@ func (s *scenarioTest) mkNode(name string) *scenarioNode {
A: authority, A: authority,
AUMs: aums, AUMs: aums,
Name: name, Name: name,
storage: storage,
} }
s.nodes[name] = n s.nodes[name] = n
@ -89,7 +93,7 @@ func (s *scenarioTest) mkNodeWithForks(name string, signWithDefault bool, chains
} }
return false return false
}) })
if err := n.A.Inform(aums); err != nil { if err := n.A.Inform(n.storage, aums); err != nil {
panic(err) panic(err)
} }
} }
@ -114,27 +118,27 @@ func aumsToNames(n *scenarioNode, aums []AUM) string {
} }
func (s *scenarioTest) syncBetween(n1, n2 *scenarioNode) error { func (s *scenarioTest) syncBetween(n1, n2 *scenarioNode) error {
o1, err := n1.A.SyncOffer() o1, err := n1.A.SyncOffer(n1.storage)
if err != nil { if err != nil {
return err return err
} }
o2, err := n2.A.SyncOffer() o2, err := n2.A.SyncOffer(n2.storage)
if err != nil { if err != nil {
return err return err
} }
aumsFrom1, err := n1.A.MissingAUMs(o2) aumsFrom1, err := n1.A.MissingAUMs(n1.storage, o2)
if err != nil { if err != nil {
return err return err
} }
aumsFrom2, err := n2.A.MissingAUMs(o1) aumsFrom2, err := n2.A.MissingAUMs(n2.storage, o1)
if err != nil { if err != nil {
return err return err
} }
if err := n2.A.Inform(aumsFrom1); err != nil { if err := n2.A.Inform(n2.storage, aumsFrom1); err != nil {
return err return err
} }
if err := n1.A.Inform(aumsFrom2); err != nil { if err := n1.A.Inform(n1.storage, aumsFrom2); err != nil {
return err return err
} }
return nil return nil
@ -303,7 +307,7 @@ func TestInvalidAUMPropergationRejected(t *testing.T) {
l4 := AUM{MessageKind: AUMAddKey, PrevAUMHash: l3H[:]} l4 := AUM{MessageKind: AUMAddKey, PrevAUMHash: l3H[:]}
l4.sign25519(s.defaultPriv) l4.sign25519(s.defaultPriv)
l4H := l4.Hash() l4H := l4.Hash()
n1.A.storage.CommitVerifiedAUMs([]AUM{l4}) n1.storage.CommitVerifiedAUMs([]AUM{l4})
n1.A.state.LastAUMHash = &l4H n1.A.state.LastAUMHash = &l4H
// Does control nope out with syncing? // Does control nope out with syncing?
@ -336,7 +340,7 @@ func TestUnsignedAUMPropergationRejected(t *testing.T) {
l3H := l3.Hash() l3H := l3.Hash()
l4 := AUM{MessageKind: AUMNoOp, PrevAUMHash: l3H[:]} l4 := AUM{MessageKind: AUMNoOp, PrevAUMHash: l3H[:]}
l4H := l4.Hash() l4H := l4.Hash()
n1.A.storage.CommitVerifiedAUMs([]AUM{l4}) n1.storage.CommitVerifiedAUMs([]AUM{l4})
n1.A.state.LastAUMHash = &l4H n1.A.state.LastAUMHash = &l4H
// Does control nope out with syncing? // Does control nope out with syncing?
@ -370,7 +374,7 @@ func TestBadSigAUMPropergationRejected(t *testing.T) {
l4.sign25519(s.defaultPriv) l4.sign25519(s.defaultPriv)
l4.Signatures[0].Signature[3] = 42 l4.Signatures[0].Signature[3] = 42
l4H := l4.Hash() l4H := l4.Hash()
n1.A.storage.CommitVerifiedAUMs([]AUM{l4}) n1.storage.CommitVerifiedAUMs([]AUM{l4})
n1.A.state.LastAUMHash = &l4H n1.A.state.LastAUMHash = &l4H
// Does control nope out with syncing? // Does control nope out with syncing?

View File

@ -43,7 +43,19 @@ type SyncOffer struct {
ancestorsSkipShift = 2 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() oldest := a.oldestAncestor.Hash()
out := SyncOffer{ out := SyncOffer{
@ -65,7 +77,7 @@ func (a *Authority) syncOffer() (SyncOffer, error) {
skipAmount = skipAmount << ancestorsSkipShift skipAmount = skipAmount << ancestorsSkipShift
} }
parent, err := a.storage.AUM(curs) parent, err := storage.AUM(curs)
if err != nil { if err != nil {
if err != os.ErrNotExist { if err != os.ErrNotExist {
return SyncOffer{}, err return SyncOffer{}, err
@ -84,22 +96,6 @@ func (a *Authority) syncOffer() (SyncOffer, error) {
return out, nil 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 // intersection describes how to synchronize AUMs with a remote
// authority. // authority.
type intersection struct { type intersection struct {
@ -119,7 +115,7 @@ type intersection struct {
// computeSyncIntersection determines the common AUMs between a local and // computeSyncIntersection determines the common AUMs between a local and
// remote SyncOffer. This intersection can be used to synchronize both // remote SyncOffer. This intersection can be used to synchronize both
// sides. // sides.
func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncOffer) (*intersection, error) { func computeSyncIntersection(storage Chonk, localOffer, remoteOffer SyncOffer) (*intersection, error) {
// Simple case: up to date. // Simple case: up to date.
if remoteOffer.Head == localOffer.Head { if remoteOffer.Head == localOffer.Head {
return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil
@ -136,7 +132,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
// <Them> A -> B // <Them> A -> B
// ∴ their head intersects with our chain, we need to send C // ∴ their head intersects with our chain, we need to send C
var hasRemoteHead bool var hasRemoteHead bool
_, err := authority.storage.AUM(remoteOffer.Head) _, err := storage.AUM(remoteOffer.Head)
if err != nil { if err != nil {
if err != os.ErrNotExist { if err != os.ErrNotExist {
return nil, err return nil, err
@ -148,7 +144,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
if hasRemoteHead { if hasRemoteHead {
curs := localOffer.Head curs := localOffer.Head
for i := 0; i < maxSyncHeadIntersectionIter; i++ { for i := 0; i < maxSyncHeadIntersectionIter; i++ {
parent, err := authority.storage.AUM(curs) parent, err := storage.AUM(curs)
if err != nil { if err != nil {
if err != os.ErrNotExist { if err != os.ErrNotExist {
return nil, err 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 / // a bit of luck we can use an earlier one and hence do less work /
// transmit fewer AUMs. // transmit fewer AUMs.
for _, a := range remoteOffer.Ancestors { for _, a := range remoteOffer.Ancestors {
state, err := computeStateAt(authority.storage, maxSyncIter, a) state, err := computeStateAt(storage, maxSyncIter, a)
if err != nil { if err != nil {
if err != os.ErrNotExist { if err != os.ErrNotExist {
return nil, fmt.Errorf("computeStateAt: %v", err) return nil, fmt.Errorf("computeStateAt: %v", err)
@ -184,7 +180,7 @@ func computeSyncIntersection(authority *Authority, localOffer, remoteOffer SyncO
continue 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 return curs.Hash() == localOffer.Head
}) })
if err != nil { 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 // MissingAUMs returns AUMs a remote may be missing based on the
// remotes' SyncOffer. // remotes' SyncOffer.
func (a *Authority) MissingAUMs(remoteOffer SyncOffer) ([]AUM, error) { func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, error) {
localOffer, err := a.syncOffer() localOffer, err := a.SyncOffer(storage)
if err != nil { if err != nil {
return nil, fmt.Errorf("local syncOffer: %v", err) return nil, fmt.Errorf("local syncOffer: %v", err)
} }
intersection, err := computeSyncIntersection(a, localOffer, remoteOffer) intersection, err := computeSyncIntersection(storage, localOffer, remoteOffer)
if err != nil { if err != nil {
return nil, fmt.Errorf("intersection: %v", err) 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. out := make([]AUM, 0, 12) // 12 chosen arbitrarily.
if intersection.headIntersection != nil { if intersection.headIntersection != nil {
state, err := computeStateAt(a.storage, maxSyncIter, *intersection.headIntersection) state, err := computeStateAt(storage, maxSyncIter, *intersection.headIntersection)
if err != nil { if err != nil {
return nil, err 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 { if curs.Hash() != *intersection.headIntersection {
out = append(out, curs) out = append(out, curs)
} }
@ -233,12 +229,12 @@ func (a *Authority) MissingAUMs(remoteOffer SyncOffer) ([]AUM, error) {
} }
if intersection.tailIntersection != nil { if intersection.tailIntersection != nil {
state, err := computeStateAt(a.storage, maxSyncIter, *intersection.tailIntersection) state, err := computeStateAt(storage, maxSyncIter, *intersection.tailIntersection)
if err != nil { if err != nil {
return nil, err 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 { if curs.Hash() != *intersection.tailIntersection {
out = append(out, curs) out = append(out, curs)
} }

View File

@ -18,11 +18,12 @@ func TestSyncOffer(t *testing.T) {
A10 -> A11 -> A12 -> A13 -> A14 -> A15 -> A16 -> A17 -> A18 A10 -> A11 -> A12 -> A13 -> A14 -> A15 -> A16 -> A17 -> A18
A18 -> A19 -> A20 -> A21 -> A22 -> A23 -> A24 -> A25 A18 -> A19 -> A20 -> A21 -> A22 -> A23 -> A24 -> A25
`) `)
a, err := Open(c.Chonk()) storage := c.Chonk()
a, err := Open(storage)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
got, err := a.SyncOffer() got, err := a.SyncOffer(storage)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -56,7 +57,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer1, err := n1.SyncOffer() offer1, err := n1.SyncOffer(chonk1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -66,7 +67,7 @@ func TestComputeSyncIntersection_FastForward(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer2, err := n2.SyncOffer() offer2, err := n2.SyncOffer(chonk2)
if err != nil { if err != nil {
t.Fatal(err) 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 // Node 1 only knows about the first two nodes, so the head of n2 is
// alien to it. // alien to it.
t.Run("n1", func(t *testing.T) { t.Run("n1", func(t *testing.T) {
got, err := computeSyncIntersection(n1, offer1, offer2) got, err := computeSyncIntersection(chonk1, offer1, offer2)
if err != nil { if err != nil {
t.Fatalf("computeSyncIntersection() failed: %v", err) 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 // 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). // intersects with a subset of its chain (a Head Intersection).
t.Run("n2", func(t *testing.T) { t.Run("n2", func(t *testing.T) {
got, err := computeSyncIntersection(n2, offer2, offer1) got, err := computeSyncIntersection(chonk2, offer2, offer1)
if err != nil { if err != nil {
t.Fatalf("computeSyncIntersection() failed: %v", err) 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") 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer1, err := n1.SyncOffer() offer1, err := n1.SyncOffer(chonk1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -140,11 +142,12 @@ func TestComputeSyncIntersection_ForkSmallDiff(t *testing.T) {
t.Errorf("offer1 diff (-want, +got):\n%s", diff) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer2, err := n2.SyncOffer() offer2, err := n2.SyncOffer(chonk2)
if err != nil { if err != nil {
t.Fatal(err) 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 // n2 has 10 nodes, so the first common ancestor should be 10-ancestorsSkipStart
wantIntersection := c.AUMHashes["A"+strconv.Itoa(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 { if err != nil {
t.Fatalf("computeSyncIntersection() failed: %v", err) 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 // n1 has 9 nodes, so the first common ancestor should be 9-ancestorsSkipStart
wantIntersection := c.AUMHashes["A"+strconv.Itoa(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 { if err != nil {
t.Fatalf("computeSyncIntersection() failed: %v", err) t.Fatalf("computeSyncIntersection() failed: %v", err)
} }
@ -210,7 +213,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer1, err := n1.SyncOffer() offer1, err := n1.SyncOffer(chonk1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -220,7 +223,7 @@ func TestMissingAUMs_FastForward(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer2, err := n2.SyncOffer() offer2, err := n2.SyncOffer(chonk2)
if err != nil { if err != nil {
t.Fatal(err) 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, // 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). // A1 (if the chain was longer there would be one in the middle).
t.Run("n1", func(t *testing.T) { t.Run("n1", func(t *testing.T) {
got, err := n1.MissingAUMs(offer2) got, err := n1.MissingAUMs(chonk1, offer2)
if err != nil { if err != nil {
t.Fatalf("MissingAUMs() failed: %v", err) 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 // 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). // intersects with a subset of its chain (a Head Intersection).
t.Run("n2", func(t *testing.T) { t.Run("n2", func(t *testing.T) {
got, err := n2.MissingAUMs(offer1) got, err := n2.MissingAUMs(chonk2, offer1)
if err != nil { if err != nil {
t.Fatalf("MissingAUMs() failed: %v", err) t.Fatalf("MissingAUMs() failed: %v", err)
} }
@ -277,7 +280,7 @@ func TestMissingAUMs_Fork(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer1, err := n1.SyncOffer() offer1, err := n1.SyncOffer(chonk1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -287,13 +290,13 @@ func TestMissingAUMs_Fork(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
offer2, err := n2.SyncOffer() offer2, err := n2.SyncOffer(chonk2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Run("n1", func(t *testing.T) { t.Run("n1", func(t *testing.T) {
got, err := n1.MissingAUMs(offer2) got, err := n1.MissingAUMs(chonk1, offer2)
if err != nil { if err != nil {
t.Fatalf("MissingAUMs() failed: %v", err) t.Fatalf("MissingAUMs() failed: %v", err)
} }
@ -311,7 +314,7 @@ func TestMissingAUMs_Fork(t *testing.T) {
}) })
t.Run("n2", func(t *testing.T) { t.Run("n2", func(t *testing.T) {
got, err := n2.MissingAUMs(offer1) got, err := n2.MissingAUMs(chonk2, offer1)
if err != nil { if err != nil {
t.Fatalf("MissingAUMs() failed: %v", err) t.Fatalf("MissingAUMs() failed: %v", err)
} }
@ -344,26 +347,28 @@ func TestSyncSimpleE2E(t *testing.T) {
optKey("key", key, priv), optKey("key", key, priv),
optSignAllUsing("key")) optSignAllUsing("key"))
node, err := Bootstrap(&Mem{}, c.AUMs["G1"]) nodeStorage := &Mem{}
node, err := Bootstrap(nodeStorage, c.AUMs["G1"])
if err != nil { if err != nil {
t.Fatalf("node Bootstrap() failed: %v", err) t.Fatalf("node Bootstrap() failed: %v", err)
} }
control, err := Open(c.Chonk()) controlStorage := c.Chonk()
control, err := Open(controlStorage)
if err != nil { if err != nil {
t.Fatalf("control Open() failed: %v", err) t.Fatalf("control Open() failed: %v", err)
} }
// Control knows the full chain, node only knows the genesis. Lets see // Control knows the full chain, node only knows the genesis. Lets see
// if they can sync. // if they can sync.
nodeOffer, err := node.SyncOffer() nodeOffer, err := node.SyncOffer(nodeStorage)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
controlAUMs, err := control.MissingAUMs(nodeOffer) controlAUMs, err := control.MissingAUMs(controlStorage, nodeOffer)
if err != nil { if err != nil {
t.Fatalf("control.MissingAUMs(%v) failed: %v", nodeOffer, err) 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) t.Fatalf("node.Inform(%v) failed: %v", controlAUMs, err)
} }

View File

@ -39,8 +39,15 @@ type Authority struct {
head AUM head AUM
oldestAncestor AUM oldestAncestor AUM
state State 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, // A chain describes a linear sequence of updates from Oldest to Head,
@ -477,7 +484,6 @@ func Open(storage Chonk) (*Authority, error) {
return &Authority{ return &Authority{
head: c.Head, head: c.Head,
oldestAncestor: c.Oldest, oldestAncestor: c.Oldest,
storage: storage,
state: c.state, state: c.state,
}, nil }, nil
} }
@ -557,12 +563,18 @@ func (a *Authority) ValidDisablement(secret []byte) bool {
return a.state.checkDisablement(secret) return a.state.checkDisablement(secret)
} }
// Inform is called to tell the authority about new updates. Updates // InformIdempotent returns a new Authority based on applying the given
// should be ordered oldest to newest. An error is returned if any // updates, with the given updates committed to storage.
// of the updates could not be processed. //
func (a *Authority) Inform(updates []AUM) error { // 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 { 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) stateAt := make(map[AUMHash]State, len(updates)+1)
toCommit := make([]AUM, 0, len(updates)) toCommit := make([]AUM, 0, len(updates))
@ -584,30 +596,30 @@ func (a *Authority) Inform(updates []AUM) error {
for i, update := range updates { for i, update := range updates {
hash := update.Hash() hash := update.Hash()
// Check if we already have this AUM thus don't need to process it. // 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. isHeadChain = false // Disable the head-chain optimization.
continue continue
} }
parent, hasParent := update.Parent() parent, hasParent := update.Parent()
if !hasParent { if !hasParent {
return fmt.Errorf("update %d: missing parent", i) return Authority{}, fmt.Errorf("update %d: missing parent", i)
} }
state, hasState := stateAt[parent] state, hasState := stateAt[parent]
var err error var err error
if !hasState { if !hasState {
if state, err = computeStateAt(a.storage, 2000, parent); err != nil { if state, err = computeStateAt(storage, 2000, parent); err != nil {
return fmt.Errorf("update %d computing state: %v", i, err) return Authority{}, fmt.Errorf("update %d computing state: %v", i, err)
} }
stateAt[parent] = state stateAt[parent] = state
} }
if err := aumVerify(update, state, false); err != nil { 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 { 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 { if isHeadChain && parent != prevHash {
@ -617,26 +629,40 @@ func (a *Authority) Inform(updates []AUM) error {
toCommit = append(toCommit, update) toCommit = append(toCommit, update)
} }
if err := a.storage.CommitVerifiedAUMs(toCommit); err != nil { if err := storage.CommitVerifiedAUMs(toCommit); err != nil {
return fmt.Errorf("commit: %v", err) return Authority{}, fmt.Errorf("commit: %v", err)
} }
if isHeadChain { if isHeadChain {
// Head-chain fastpath: We can use the state we computed // Head-chain fastpath: We can use the state we computed
// in the last iteration. // in the last iteration.
a.head = updates[len(updates)-1] return Authority{
a.state = stateAt[prevHash] head: updates[len(updates)-1],
} else { oldestAncestor: a.oldestAncestor,
oldestAncestor := a.oldestAncestor.Hash() state: stateAt[prevHash],
c, err := computeActiveChain(a.storage, &oldestAncestor, 2000) }, nil
if err != nil {
return fmt.Errorf("recomputing active chain: %v", err)
}
a.head = c.Head
a.oldestAncestor = c.Oldest
a.state = c.state
} }
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 return nil
} }

View File

@ -376,7 +376,7 @@ func TestAuthorityInformNonLinear(t *testing.T) {
// and forcing Inform() to take the slow path. // 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"]} 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) t.Fatalf("Inform() failed: %v", err)
} }
for i, update := range informAUMs { 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"]} 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) t.Fatalf("Inform() failed: %v", err)
} }
for i, update := range informAUMs { for i, update := range informAUMs {