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
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

View File

@ -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{

View File

@ -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 {

View File

@ -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())

View File

@ -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?

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {