tka: optimize common case of processing updates built from head
Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
parent
039def3b50
commit
472529af38
|
@ -88,7 +88,6 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||||
return errors.New("no netmap: are you logged into tailscale?")
|
return errors.New("no netmap: are you logged into tailscale?")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Generates a genesis AUM representing trust in the provided keys.
|
// Generates a genesis AUM representing trust in the provided keys.
|
||||||
// We use an in-memory tailchonk because we don't want to commit to
|
// We use an in-memory tailchonk because we don't want to commit to
|
||||||
// the filesystem until we've finished the initialization sequence,
|
// the filesystem until we've finished the initialization sequence,
|
||||||
|
@ -118,14 +117,14 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||||
// If we don't get these signatures ahead of time, everyone will loose
|
// If we don't get these signatures ahead of time, everyone will loose
|
||||||
// connectivity because control won't have any signatures to send which
|
// connectivity because control won't have any signatures to send which
|
||||||
// satisfy network-lock checks.
|
// satisfy network-lock checks.
|
||||||
var sigs []tkatype.MarshaledSignature
|
sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures))
|
||||||
for _, nkp := range initResp.NeedSignatures {
|
for _, nodeInfo := range initResp.NeedSignatures {
|
||||||
nks, err := signNodeKey(nkp, b.nlPrivKey)
|
nks, err := signNodeKey(nodeInfo, b.nlPrivKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generating signature: %v", err)
|
return fmt.Errorf("generating signature: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sigs = append(sigs, nks.Serialize())
|
sigs[nodeInfo.NodeID] = nks.Serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||||
|
@ -133,16 +132,17 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func signNodeKey(nk key.NodePublic, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||||
p, err := nk.MarshalBinary()
|
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := tka.NodeKeySignature{
|
sig := tka.NodeKeySignature{
|
||||||
SigKind: tka.SigDirect,
|
SigKind: tka.SigDirect,
|
||||||
KeyID: signer.KeyID(),
|
KeyID: signer.KeyID(),
|
||||||
Pubkey: p,
|
Pubkey: p,
|
||||||
|
RotationPubkey: nodeInfo.RotationPubkey,
|
||||||
}
|
}
|
||||||
sig.Signature, err = signer.SignNKS(sig.SigHash())
|
sig.Signature, err = signer.SignNKS(sig.SigHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -154,7 +154,7 @@ func signNodeKey(nk key.NodePublic, signer key.NLPrivate) (*tka.NodeKeySignature
|
||||||
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||||
var req bytes.Buffer
|
var req bytes.Buffer
|
||||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
||||||
NodeID: nm.SelfNode.ID,
|
NodeID: nm.SelfNode.ID,
|
||||||
GenesisAUM: aum.Serialize(),
|
GenesisAUM: aum.Serialize(),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("encoding request: %v", err)
|
return nil, fmt.Errorf("encoding request: %v", err)
|
||||||
|
@ -192,10 +192,10 @@ func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks []tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||||
var req bytes.Buffer
|
var req bytes.Buffer
|
||||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||||
NodeID: nm.SelfNode.ID,
|
NodeID: nm.SelfNode.ID,
|
||||||
Signatures: nks,
|
Signatures: nks,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("encoding request: %v", err)
|
return nil, fmt.Errorf("encoding request: %v", err)
|
||||||
|
|
|
@ -1829,25 +1829,31 @@ type PeerChange struct {
|
||||||
// TKAInitBeginRequest submits a genesis AUM to seed the creation of the
|
// TKAInitBeginRequest submits a genesis AUM to seed the creation of the
|
||||||
// tailnet's key authority.
|
// tailnet's key authority.
|
||||||
type TKAInitBeginRequest struct {
|
type TKAInitBeginRequest struct {
|
||||||
NodeID NodeID
|
NodeID NodeID // NodeID of the initiating client
|
||||||
|
|
||||||
GenesisAUM tkatype.MarshaledAUM
|
GenesisAUM tkatype.MarshaledAUM
|
||||||
}
|
}
|
||||||
|
|
||||||
// TKAInitBeginResponse describes a set of NodeKeys which must be signed to
|
// TKASignInfo describes information about an existing node that needs
|
||||||
|
// to be signed into a node-key signature.
|
||||||
|
type TKASignInfo struct {
|
||||||
|
NodeID NodeID // NodeID of the node-key being signed
|
||||||
|
NodePublic key.NodePublic
|
||||||
|
RotationPubkey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// TKAInitBeginResponse describes node information which must be signed to
|
||||||
// complete initialization of the tailnets' key authority.
|
// complete initialization of the tailnets' key authority.
|
||||||
type TKAInitBeginResponse struct {
|
type TKAInitBeginResponse struct {
|
||||||
NodeID NodeID
|
NeedSignatures []TKASignInfo
|
||||||
|
|
||||||
NeedSignatures []key.NodePublic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TKAInitFinishRequest finalizes initialization of the tailnet key authority
|
// TKAInitFinishRequest finalizes initialization of the tailnet key authority
|
||||||
// by submitting node-key signatures for all existing nodes.
|
// by submitting node-key signatures for all existing nodes.
|
||||||
type TKAInitFinishRequest struct {
|
type TKAInitFinishRequest struct {
|
||||||
NodeID NodeID
|
NodeID NodeID // NodeID of the initiating client
|
||||||
|
|
||||||
Signatures []tkatype.MarshaledSignature
|
Signatures map[NodeID]tkatype.MarshaledSignature
|
||||||
}
|
}
|
||||||
|
|
||||||
// TKAInitFinishResponse describes the successful enablement of the tailnet's
|
// TKAInitFinishResponse describes the successful enablement of the tailnet's
|
||||||
|
|
51
tka/tka.go
51
tka/tka.go
|
@ -553,13 +553,31 @@ func Bootstrap(storage Chonk, bootstrap AUM) (*Authority, error) {
|
||||||
// should be ordered oldest to newest. An error is returned if any
|
// should be ordered oldest to newest. An error is returned if any
|
||||||
// of the updates could not be processed.
|
// of the updates could not be processed.
|
||||||
func (a *Authority) Inform(updates []AUM) error {
|
func (a *Authority) Inform(updates []AUM) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return 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))
|
||||||
|
prevHash := a.Head()
|
||||||
|
|
||||||
|
// The state at HEAD is the current state of the authority. Its likely
|
||||||
|
// to be needed, so we prefill it rather than computing it.
|
||||||
|
stateAt[prevHash] = a.state
|
||||||
|
|
||||||
|
// Optimization: If the set of updates is a chain building from
|
||||||
|
// the current head, EG:
|
||||||
|
// <a.Head()> ==> updates[0] ==> updates[1] ...
|
||||||
|
// Then theres no need to recompute the resulting state from the
|
||||||
|
// stored ancestor, because the last state computed during iteration
|
||||||
|
// is the new state. This should be the common case.
|
||||||
|
// isHeadChain keeps track of this.
|
||||||
|
isHeadChain := true
|
||||||
|
|
||||||
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.
|
||||||
if _, err := a.storage.AUM(hash); err == nil {
|
if _, err := a.storage.AUM(hash); err == nil {
|
||||||
// Already have this AUM.
|
isHeadChain = false // Disable the head-chain optimization.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,6 +601,11 @@ func (a *Authority) Inform(updates []AUM) error {
|
||||||
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 fmt.Errorf("update %d cannot be applied: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isHeadChain && parent != prevHash {
|
||||||
|
isHeadChain = false
|
||||||
|
}
|
||||||
|
prevHash = hash
|
||||||
toCommit = append(toCommit, update)
|
toCommit = append(toCommit, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,18 +613,22 @@ func (a *Authority) Inform(updates []AUM) error {
|
||||||
return fmt.Errorf("commit: %v", err)
|
return fmt.Errorf("commit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(tom): Theres no need to recompute the state from scratch
|
if isHeadChain {
|
||||||
// in every case. We should detect when updates were
|
// Head-chain fastpath: We can use the state we computed
|
||||||
// a linear, non-forking series applied to head, and
|
// in the last iteration.
|
||||||
// just use the last State we computed.
|
a.head = updates[len(updates)-1]
|
||||||
oldestAncestor := a.oldestAncestor.Hash()
|
a.state = stateAt[prevHash]
|
||||||
c, err := computeActiveChain(a.storage, &oldestAncestor, 2000)
|
} else {
|
||||||
if err != nil {
|
oldestAncestor := a.oldestAncestor.Hash()
|
||||||
return fmt.Errorf("recomputing active chain: %v", err)
|
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
|
||||||
}
|
}
|
||||||
a.head = c.Head
|
|
||||||
a.oldestAncestor = c.Oldest
|
|
||||||
a.state = c.state
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -325,7 +325,7 @@ func TestCreateBootstrapAuthority(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthorityInform(t *testing.T) {
|
func TestAuthorityInformNonLinear(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}
|
||||||
|
|
||||||
|
@ -351,6 +351,8 @@ func TestAuthorityInform(t *testing.T) {
|
||||||
t.Fatalf("Bootstrap() failed: %v", err)
|
t.Fatalf("Bootstrap() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// L2 does not chain from L1, disabling the isHeadChain optimization
|
||||||
|
// 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(informAUMs); err != nil {
|
||||||
|
@ -371,3 +373,46 @@ func TestAuthorityInform(t *testing.T) {
|
||||||
t.Fatal("authority did not converge to correct AUM")
|
t.Fatal("authority did not converge to correct AUM")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthorityInformLinear(t *testing.T) {
|
||||||
|
pub, priv := testingKey25519(t, 1)
|
||||||
|
key := Key{Kind: Key25519, Public: pub, Votes: 2}
|
||||||
|
|
||||||
|
c := newTestchain(t, `
|
||||||
|
G1 -> L1 -> L2 -> L3
|
||||||
|
|
||||||
|
G1.template = genesis
|
||||||
|
`,
|
||||||
|
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
|
||||||
|
Keys: []Key{key},
|
||||||
|
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
|
||||||
|
}}),
|
||||||
|
optKey("key", key, priv),
|
||||||
|
optSignAllUsing("key"))
|
||||||
|
|
||||||
|
storage := &Mem{}
|
||||||
|
a, err := Bootstrap(storage, c.AUMs["G1"])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Bootstrap() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
informAUMs := []AUM{c.AUMs["L1"], c.AUMs["L2"], c.AUMs["L3"]}
|
||||||
|
|
||||||
|
if err := a.Inform(informAUMs); err != nil {
|
||||||
|
t.Fatalf("Inform() failed: %v", err)
|
||||||
|
}
|
||||||
|
for i, update := range informAUMs {
|
||||||
|
stored, err := storage.AUM(update.Hash())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading stored update %d: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(update, stored); diff != "" {
|
||||||
|
t.Errorf("update %d differs (-want, +got):\n%s", i, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Head() != c.AUMHashes["L3"] {
|
||||||
|
t.Fatal("authority did not converge to correct AUM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue