sync.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package tka
  4. import (
  5. "errors"
  6. "fmt"
  7. "os"
  8. )
  9. const (
  10. // Max iterations searching for any intersection.
  11. maxSyncIter = 2000
  12. // Max iterations searching for a head intersection.
  13. maxSyncHeadIntersectionIter = 400
  14. )
  15. // ErrNoIntersection is returned when a shared AUM could
  16. // not be determined when evaluating a remote sync offer.
  17. var ErrNoIntersection = errors.New("no intersection")
  18. // SyncOffer conveys information about the current head & ancestor AUMs,
  19. // for the purpose of synchronization with some remote end.
  20. //
  21. // Ancestors should contain a subset of the ancestors of the chain.
  22. // The last entry in that slice is the oldest-known AUM in the chain.
  23. type SyncOffer struct {
  24. Head AUMHash
  25. Ancestors []AUMHash
  26. }
  27. const (
  28. // The starting number of AUMs to skip when listing
  29. // ancestors in a SyncOffer.
  30. ancestorsSkipStart = 4
  31. // How many bits to advance the skip count when listing
  32. // ancestors in a SyncOffer.
  33. //
  34. // 2 bits, so (4<<2), so after skipping 4 it skips 16.
  35. ancestorsSkipShift = 2
  36. )
  37. // SyncOffer returns an abbreviated description of the current AUM
  38. // chain, which can be used to synchronize with another (untrusted)
  39. // Authority instance.
  40. //
  41. // The returned SyncOffer structure should be transmitted to the remote
  42. // Authority, which should call MissingAUMs() using it to determine
  43. // AUMs which need to be transmitted. This list of AUMs from the remote
  44. // can then be applied locally with Inform().
  45. //
  46. // This SyncOffer + AUM exchange should be performed by both ends,
  47. // because its possible that either end has AUMs that the other needs
  48. // to find out about.
  49. func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
  50. oldest := a.oldestAncestor.Hash()
  51. out := SyncOffer{
  52. Head: a.Head(),
  53. Ancestors: make([]AUMHash, 0, 6), // 6 chosen arbitrarily.
  54. }
  55. // We send some subset of our ancestors to help the remote
  56. // find a more-recent 'head intersection'.
  57. // The number of AUMs between each ancestor entry gets
  58. // exponentially larger.
  59. var (
  60. skipAmount uint64 = ancestorsSkipStart
  61. curs AUMHash = a.Head()
  62. )
  63. for i := uint64(0); i < maxSyncHeadIntersectionIter; i++ {
  64. if i > 0 && (i%skipAmount) == 0 {
  65. out.Ancestors = append(out.Ancestors, curs)
  66. skipAmount = skipAmount << ancestorsSkipShift
  67. }
  68. parent, err := storage.AUM(curs)
  69. if err != nil {
  70. if err != os.ErrNotExist {
  71. return SyncOffer{}, err
  72. }
  73. break
  74. }
  75. // We add the oldest later on, so don't duplicate.
  76. if parent.Hash() == oldest {
  77. break
  78. }
  79. copy(curs[:], parent.PrevAUMHash)
  80. }
  81. out.Ancestors = append(out.Ancestors, oldest)
  82. return out, nil
  83. }
  84. // intersection describes how to synchronize AUMs with a remote
  85. // authority.
  86. type intersection struct {
  87. // if true, no exchange of AUMs is needed.
  88. upToDate bool
  89. // headIntersection is the latest common AUM on the remote. In other
  90. // words, we need to send all AUMs since this one.
  91. headIntersection *AUMHash
  92. // tailIntersection is the oldest common AUM on the remote. In other
  93. // words, we diverge with the remote after this AUM, so we both need
  94. // to transmit our AUM chain starting here.
  95. tailIntersection *AUMHash
  96. }
  97. // computeSyncIntersection determines the common AUMs between a local and
  98. // remote SyncOffer. This intersection can be used to synchronize both
  99. // sides.
  100. func computeSyncIntersection(storage Chonk, localOffer, remoteOffer SyncOffer) (*intersection, error) {
  101. // Simple case: up to date.
  102. if remoteOffer.Head == localOffer.Head {
  103. return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil
  104. }
  105. // Case: 'head intersection'
  106. // If we have the remote's head, its more likely than not that
  107. // we have updates that build on that head. To confirm this,
  108. // we iterate backwards through our chain to see if the given
  109. // head is an ancestor of our current chain.
  110. //
  111. // In other words:
  112. // <Us> A -> B -> C
  113. // <Them> A -> B
  114. // ∴ their head intersects with our chain, we need to send C
  115. var hasRemoteHead bool
  116. _, err := storage.AUM(remoteOffer.Head)
  117. if err != nil {
  118. if err != os.ErrNotExist {
  119. return nil, err
  120. }
  121. } else {
  122. hasRemoteHead = true
  123. }
  124. if hasRemoteHead {
  125. curs := localOffer.Head
  126. for i := 0; i < maxSyncHeadIntersectionIter; i++ {
  127. parent, err := storage.AUM(curs)
  128. if err != nil {
  129. if err != os.ErrNotExist {
  130. return nil, err
  131. }
  132. break
  133. }
  134. if parent.Hash() == remoteOffer.Head {
  135. h := parent.Hash()
  136. return &intersection{headIntersection: &h}, nil
  137. }
  138. copy(curs[:], parent.PrevAUMHash)
  139. }
  140. }
  141. // Case: 'tail intersection'
  142. // So we don't have a clue what the remote's head is, but
  143. // if one of the ancestors they gave us is part of our chain,
  144. // then theres an intersection, which is a starting point for
  145. // the remote to send us AUMs from.
  146. //
  147. // We iterate the list of ancestors in order because the remote
  148. // ordered them such that the newer ones are earlier, so with
  149. // a bit of luck we can use an earlier one and hence do less work /
  150. // transmit fewer AUMs.
  151. for _, a := range remoteOffer.Ancestors {
  152. state, err := computeStateAt(storage, maxSyncIter, a)
  153. if err != nil {
  154. if err != os.ErrNotExist {
  155. return nil, fmt.Errorf("computeStateAt: %v", err)
  156. }
  157. continue
  158. }
  159. end, _, err := fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  160. return curs.Hash() == localOffer.Head
  161. })
  162. if err != nil {
  163. return nil, err
  164. }
  165. // fastForward can terminate before the done condition if there are
  166. // no more children left, so we check again before considering this
  167. // an intersection.
  168. if end.Hash() == localOffer.Head {
  169. return &intersection{tailIntersection: &a}, nil
  170. }
  171. }
  172. return nil, ErrNoIntersection
  173. }
  174. // MissingAUMs returns AUMs a remote may be missing based on the
  175. // remotes' SyncOffer.
  176. func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, error) {
  177. localOffer, err := a.SyncOffer(storage)
  178. if err != nil {
  179. return nil, fmt.Errorf("local syncOffer: %v", err)
  180. }
  181. intersection, err := computeSyncIntersection(storage, localOffer, remoteOffer)
  182. if err != nil {
  183. return nil, fmt.Errorf("intersection: %v", err)
  184. }
  185. if intersection.upToDate {
  186. return nil, nil
  187. }
  188. out := make([]AUM, 0, 12) // 12 chosen arbitrarily.
  189. if intersection.headIntersection != nil {
  190. state, err := computeStateAt(storage, maxSyncIter, *intersection.headIntersection)
  191. if err != nil {
  192. return nil, err
  193. }
  194. _, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  195. if curs.Hash() != *intersection.headIntersection {
  196. out = append(out, curs)
  197. }
  198. return false
  199. })
  200. return out, err
  201. }
  202. if intersection.tailIntersection != nil {
  203. state, err := computeStateAt(storage, maxSyncIter, *intersection.tailIntersection)
  204. if err != nil {
  205. return nil, err
  206. }
  207. _, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
  208. if curs.Hash() != *intersection.tailIntersection {
  209. out = append(out, curs)
  210. }
  211. return false
  212. })
  213. return out, err
  214. }
  215. panic("unreachable")
  216. }