2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
29 #import "TPPeerPermanentInfo.h"
30 #import "TPPeerStableInfo.h"
31 #import "TPPeerDynamicInfo.h"
33 #import "TPPolicyDocument.h"
37 @property (nonatomic, strong) NSMutableDictionary<NSString*, TPPeer*>* peersByID;
38 @property (nonatomic, strong) NSMutableDictionary<NSString*, TPCircle*>* circlesByID;
39 @property (nonatomic, strong) NSMutableDictionary<NSNumber*, TPPolicyDocument*>* policiesByVersion;
40 @property (nonatomic, strong) NSMutableSet<TPVoucher*>* vouchers;
41 @property (nonatomic, strong) id<TPDecrypter> decrypter;
44 @implementation TPModel
46 - (instancetype)initWithDecrypter:(id<TPDecrypter>)decrypter
50 _peersByID = [[NSMutableDictionary alloc] init];
51 _circlesByID = [[NSMutableDictionary alloc] init];
52 _policiesByVersion = [[NSMutableDictionary alloc] init];
53 _vouchers = [[NSMutableSet alloc] init];
54 _decrypter = decrypter;
59 - (TPCounter)latestEpochAmongPeerIDs:(NSSet<NSString*> *)peerIDs
61 TPCounter latestEpoch = 0;
62 for (NSString *peerID in peerIDs) {
63 TPPeer *peer = self.peersByID[peerID];
67 latestEpoch = MAX(latestEpoch, peer.permanentInfo.epoch);
72 - (void)registerPolicyDocument:(TPPolicyDocument *)policyDoc
74 NSAssert(policyDoc, @"policyDoc must not be nil");
75 self.policiesByVersion[@(policyDoc.policyVersion)] = policyDoc;
78 - (void)registerPeerWithPermanentInfo:(TPPeerPermanentInfo *)permanentInfo
80 NSAssert(permanentInfo, @"permanentInfo must not be nil");
81 if (nil == [self.peersByID objectForKey:permanentInfo.peerID]) {
82 TPPeer *peer = [[TPPeer alloc] initWithPermanentInfo:permanentInfo];
83 [self.peersByID setObject:peer forKey:peer.peerID];
85 // Do nothing, to avoid overwriting the existing peer object which might have accumulated state.
89 - (void)deletePeerWithID:(NSString *)peerID
91 [self.peersByID removeObjectForKey:peerID];
94 - (BOOL)hasPeerWithID:(NSString *)peerID
96 return nil != self.peersByID[peerID];
99 - (nonnull TPPeer *)peerWithID:(NSString *)peerID
101 TPPeer *peer = [self.peersByID objectForKey:peerID];
102 NSAssert(nil != peer, @"peerID is not registered: %@", peerID);
106 - (TPPeerStatus)statusOfPeerWithID:(NSString *)peerID
108 TPPeer *peer = [self peerWithID:peerID];
109 TPPeerStatus status = 0;
110 if (0 < [peer.circle.includedPeerIDs count]) {
111 status |= TPPeerStatusFullyReciprocated;
112 // This flag might get cleared again, below.
114 for (NSString *otherID in peer.circle.includedPeerIDs) {
115 if ([peerID isEqualToString:otherID]) {
118 TPPeer *otherPeer = self.peersByID[otherID];
119 if (nil == otherPeer) {
122 if ([otherPeer.circle.includedPeerIDs containsObject:peerID]) {
123 status |= TPPeerStatusPartiallyReciprocated;
125 status &= ~TPPeerStatusFullyReciprocated;
127 if ([otherPeer.circle.excludedPeerIDs containsObject:peerID]) {
128 status |= TPPeerStatusExcluded;
130 if (otherPeer.permanentInfo.epoch > peer.permanentInfo.epoch) {
131 status |= TPPeerStatusOutdatedEpoch;
133 if (otherPeer.permanentInfo.epoch > peer.permanentInfo.epoch + 1) {
134 status |= TPPeerStatusAncientEpoch;
137 if ([peer.circle.excludedPeerIDs containsObject:peerID]) {
138 status |= TPPeerStatusExcluded;
144 - (TPPeerPermanentInfo *)getPermanentInfoForPeerWithID:(NSString *)peerID
146 return [self peerWithID:peerID].permanentInfo;
149 - (TPPeerStableInfo *)getStableInfoForPeerWithID:(NSString *)peerID
151 return [self peerWithID:peerID].stableInfo;
154 - (NSData *)getWrappedPrivateKeysForPeerWithID:(NSString *)peerID
156 return [self peerWithID:peerID].wrappedPrivateKeys;
159 - (void)setWrappedPrivateKeys:(nullable NSData *)wrappedPrivateKeys
160 forPeerWithID:(NSString *)peerID
162 [self peerWithID:peerID].wrappedPrivateKeys = wrappedPrivateKeys;
165 - (TPPeerDynamicInfo *)getDynamicInfoForPeerWithID:(NSString *)peerID
167 return [self peerWithID:peerID].dynamicInfo;
170 - (TPCircle *)getCircleForPeerWithID:(NSString *)peerID
172 return [self peerWithID:peerID].circle;
175 - (void)registerCircle:(TPCircle *)circle
177 NSAssert(circle, @"circle must not be nil");
178 [self.circlesByID setObject:circle forKey:circle.circleID];
180 // A dynamicInfo might have been set on a peer before we had the circle identified by dynamicInfo.circleID.
181 // Check if this circle is referenced by any dynamicInfo.circleID.
182 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
183 if (nil == peer.circle && [peer.dynamicInfo.circleID isEqualToString:circle.circleID]) {
184 peer.circle = circle;
189 - (void)deleteCircleWithID:(NSString *)circleID
191 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
192 NSAssert(![circleID isEqualToString:peer.dynamicInfo.circleID],
193 @"circle being deleted is in use by peer %@, circle %@", peerID, circleID);
195 [self.circlesByID removeObjectForKey:circleID];
198 - (TPCircle *)circleWithID:(NSString *)circleID
200 return [self.circlesByID objectForKey:circleID];
203 - (TPResult)updateStableInfo:(TPPeerStableInfo *)stableInfo
204 forPeerWithID:(NSString *)peerID
206 TPPeer *peer = [self peerWithID:peerID];
207 return [peer updateStableInfo:stableInfo];
210 - (TPPeerStableInfo *)createStableInfoWithDictionary:(NSDictionary *)dict
211 policyVersion:(TPCounter)policyVersion
212 policyHash:(NSString *)policyHash
213 policySecrets:(nullable NSDictionary *)policySecrets
214 forPeerWithID:(NSString *)peerID
215 error:(NSError **)error
217 TPPeer *peer = [self peerWithID:peerID];
218 TPCounter clock = [self maxClock] + 1;
219 return [TPPeerStableInfo stableInfoWithDict:dict
221 policyVersion:policyVersion
222 policyHash:policyHash
223 policySecrets:policySecrets
224 trustSigningKey:peer.permanentInfo.trustSigningKey
228 - (TPResult)updateDynamicInfo:(TPPeerDynamicInfo *)dynamicInfo
229 forPeerWithID:(NSString *)peerID
231 TPPeer *peer = [self peerWithID:peerID];
232 TPResult result = [peer updateDynamicInfo:dynamicInfo];
233 if (result != TPResultOk) {
236 TPCircle *circle = [self.circlesByID objectForKey:dynamicInfo.circleID];
238 peer.circle = circle;
240 // When the corresponding circleID is eventually registered,
241 // a call to registerCircle: will set peer.circle.
246 - (TPCounter)maxClock
248 __block TPCounter maxClock = 0;
249 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
250 if (nil != peer.stableInfo) {
251 maxClock = MAX(maxClock, peer.stableInfo.clock);
253 if (nil != peer.dynamicInfo) {
254 maxClock = MAX(maxClock, peer.dynamicInfo.clock);
260 - (TPCounter)maxRemovals
262 __block TPCounter maxRemovals = 0;
263 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
264 if (nil != peer.dynamicInfo) {
265 maxRemovals = MAX(maxRemovals, peer.dynamicInfo.removals);
271 - (TPPeerDynamicInfo *)createDynamicInfoForPeerWithID:(NSString *)peerID
272 circle:(TPCircle *)circle
273 clique:(NSString *)clique
274 newRemovals:(TPCounter)newRemovals
275 error:(NSError **)error
277 TPPeer *peer = self.peersByID[peerID];
279 TPCounter clock = [self maxClock] + 1;
280 TPCounter removals = [self maxRemovals] + newRemovals;
282 return [TPPeerDynamicInfo dynamicInfoWithCircleID:circle.circleID
286 trustSigningKey:peer.permanentInfo.trustSigningKey
290 - (BOOL)canTrustCandidate:(TPPeerPermanentInfo *)candidate inEpoch:(TPCounter)epoch
292 return candidate.epoch + 1 >= epoch;
295 - (BOOL)canIntroduceCandidate:(TPPeerPermanentInfo *)candidate
296 withSponsor:(TPPeerPermanentInfo *)sponsor
297 toEpoch:(TPCounter)epoch
298 underPolicy:(id<TPPolicy>)policy
300 if (![self canTrustCandidate:candidate inEpoch:sponsor.epoch]) {
303 if (![self canTrustCandidate:candidate inEpoch:epoch]) {
307 NSString *sponsorCategory = [policy categoryForModel:sponsor.modelID];
308 NSString *candidateCategory = [policy categoryForModel:candidate.modelID];
310 return [policy trustedPeerInCategory:sponsorCategory canIntroduceCategory:candidateCategory];
313 - (nullable TPVoucher *)createVoucherForCandidate:(TPPeerPermanentInfo *)candidate
314 withSponsorID:(NSString *)sponsorID
315 error:(NSError **)error
317 TPPeer *sponsor = [self peerWithID:sponsorID];
319 NSSet<NSString*> *peerIDs = [sponsor.trustedPeerIDs setByAddingObject:candidate.peerID];
320 id<TPPolicy> policy = [self policyForPeerIDs:peerIDs error:error];
325 if (![self canIntroduceCandidate:candidate
326 withSponsor:sponsor.permanentInfo
327 toEpoch:sponsor.permanentInfo.epoch
336 // clock is correctly zero if sponsor does not yet have dynamicInfo
337 TPCounter clock = sponsor.dynamicInfo.clock;
338 return [TPVoucher voucherWithBeneficiaryID:candidate.peerID
341 trustSigningKey:sponsor.permanentInfo.trustSigningKey
345 - (TPResult)registerVoucher:(TPVoucher *)voucher
347 NSAssert(voucher, @"voucher must not be nil");
348 TPPeer *sponsor = [self peerWithID:voucher.sponsorID];
349 if (![sponsor.permanentInfo.trustSigningKey checkSignature:voucher.voucherInfoSig matchesData:voucher.voucherInfoPList]) {
350 return TPResultSignatureMismatch;
352 [self.vouchers addObject:voucher];
356 - (NSSet<NSString*> *)calculateUnusedCircleIDs
358 NSMutableSet<NSString *>* circleIDs = [NSMutableSet setWithArray:[self.circlesByID allKeys]];
360 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
361 if (nil != peer.dynamicInfo) {
362 [circleIDs removeObject:peer.dynamicInfo.circleID];
368 - (nullable NSError *)considerCandidateID:(NSString *)candidateID
369 withSponsor:(TPPeer *)sponsor
370 toExpandIncludedPeerIDs:(NSMutableSet<NSString *>*)includedPeerIDs
371 andExcludedPeerIDs:(NSMutableSet<NSString *>*)excludedPeerIDs
372 forEpoch:(TPCounter)epoch
374 if ([includedPeerIDs containsObject:candidateID]) {
375 // Already included, nothing to do.
378 if ([excludedPeerIDs containsObject:candidateID]) {
383 TPPeer *candidate = self.peersByID[candidateID];
384 if (nil == candidate) {
387 NSMutableSet<NSString*> *peerIDs = [NSMutableSet setWithSet:includedPeerIDs];
388 [peerIDs minusSet:excludedPeerIDs];
389 [peerIDs addObject:candidateID];
390 NSError *error = nil;
391 id<TPPolicy> policy = [self policyForPeerIDs:peerIDs error:&error];
396 if ([self canIntroduceCandidate:candidate.permanentInfo
397 withSponsor:sponsor.permanentInfo
401 [includedPeerIDs addObject:candidateID];
402 [excludedPeerIDs unionSet:candidate.circle.excludedPeerIDs];
404 // The accepted candidate can now be a sponsor.
405 error = [self recursivelyExpandIncludedPeerIDs:includedPeerIDs
406 andExcludedPeerIDs:excludedPeerIDs
407 withPeersTrustedBySponsorID:candidateID
416 - (nullable NSError *)considerVouchersSponsoredByPeer:(TPPeer *)sponsor
417 toReecursivelyExpandIncludedPeerIDs:(NSMutableSet<NSString *>*)includedPeerIDs
418 andExcludedPeerIDs:(NSMutableSet<NSString *>*)excludedPeerIDs
419 forEpoch:(TPCounter)epoch
421 for (TPVoucher *voucher in self.vouchers) {
422 if ([voucher.sponsorID isEqualToString:sponsor.peerID]
423 && voucher.clock == sponsor.dynamicInfo.clock)
425 NSError *error = [self considerCandidateID:voucher.beneficiaryID
427 toExpandIncludedPeerIDs:includedPeerIDs
428 andExcludedPeerIDs:excludedPeerIDs
438 - (nullable NSError *)recursivelyExpandIncludedPeerIDs:(NSMutableSet<NSString *>*)includedPeerIDs
439 andExcludedPeerIDs:(NSMutableSet<NSString *>*)excludedPeerIDs
440 withPeersTrustedBySponsorID:(NSString *)sponsorID
441 forEpoch:(TPCounter)epoch
443 TPPeer *sponsor = self.peersByID[sponsorID];
444 if (nil == sponsor) {
445 // It is possible that we might receive a voucher sponsored
446 // by a peer that has not yet been registered or has been deleted,
447 // or that a peer will have a circle that includes a peer that
448 // has not yet been registered or has been deleted.
451 [excludedPeerIDs unionSet:sponsor.circle.excludedPeerIDs];
452 for (NSString *candidateID in sponsor.circle.includedPeerIDs) {
453 NSError *error = [self considerCandidateID:candidateID
455 toExpandIncludedPeerIDs:includedPeerIDs
456 andExcludedPeerIDs:excludedPeerIDs
462 return [self considerVouchersSponsoredByPeer:sponsor
463 toReecursivelyExpandIncludedPeerIDs:includedPeerIDs
464 andExcludedPeerIDs:excludedPeerIDs
468 - (TPPeerDynamicInfo *)calculateDynamicInfoForPeerWithID:(NSString *)peerID
469 addingPeerIDs:(NSArray<NSString*> *)addingPeerIDs
470 removingPeerIDs:(NSArray<NSString*> *)removingPeerIDs
471 createClique:(NSString* (^)())createClique
472 updatedCircle:(TPCircle **)updatedCircle
473 error:(NSError **)error
475 TPPeer *peer = [self peerWithID:peerID];
476 TPCounter epoch = peer.permanentInfo.epoch;
478 // If we have dynamicInfo then we must know the corresponding circle.
479 NSAssert(nil != peer.circle || nil == peer.dynamicInfo, @"dynamicInfo without corresponding circle");
481 // If I am excluded by myself then make no changes. I am no longer playing the game.
482 // This is useful in the case where I have replaced myself with a new peer.
483 if ([peer.circle.excludedPeerIDs containsObject:peerID]) {
485 *updatedCircle = peer.circle;
487 return peer.dynamicInfo;
490 NSMutableSet<NSString *> *includedPeerIDs = [NSMutableSet setWithSet:peer.circle.includedPeerIDs];
491 NSMutableSet<NSString *> *excludedPeerIDs = [NSMutableSet setWithSet:peer.circle.excludedPeerIDs];
493 // I trust myself by default, though this might be overridden by excludedPeerIDs
494 [includedPeerIDs addObject:peerID];
496 // The user has explictly told us to trust addingPeerIDs.
497 // This implies that the peers included in the circles of addingPeerIDs should also be trusted,
498 // as long epoch tests pass. This is regardless of whether trust policy says a member of addingPeerIDs
499 // can *introduce* a peer in its circle, because it isn't introducing it, the user already trusts it.
500 [includedPeerIDs addObjectsFromArray:addingPeerIDs];
501 for (NSString *addingPeerID in addingPeerIDs) {
502 TPPeer *addingPeer = self.peersByID[addingPeerID];
503 for (NSString *candidateID in addingPeer.circle.includedPeerIDs) {
504 TPPeer *candidate = self.peersByID[candidateID];
505 if (candidate && [self canTrustCandidate:candidate.permanentInfo inEpoch:epoch]) {
506 [includedPeerIDs addObject:candidateID];
511 [excludedPeerIDs addObjectsFromArray:removingPeerIDs];
512 [includedPeerIDs minusSet:excludedPeerIDs];
514 // We iterate over a copy because the loop will mutate includedPeerIDs
515 NSSet<NSString *>* sponsorIDs = [includedPeerIDs copy];
517 for (NSString *sponsorID in sponsorIDs) {
518 NSError *err = [self recursivelyExpandIncludedPeerIDs:includedPeerIDs
519 andExcludedPeerIDs:excludedPeerIDs
520 withPeersTrustedBySponsorID:sponsorID
529 NSError *err = [self considerVouchersSponsoredByPeer:peer
530 toReecursivelyExpandIncludedPeerIDs:includedPeerIDs
531 andExcludedPeerIDs:excludedPeerIDs
540 [includedPeerIDs minusSet:excludedPeerIDs];
542 NSString *clique = [self bestCliqueAmongPeerIDs:includedPeerIDs];
544 clique = peer.dynamicInfo.clique;
546 if (nil == clique && nil != createClique) {
547 clique = createClique();
550 // Either nil == createClique or createClique returned nil.
551 // We would create a clique but caller has said not to.
552 // Not an error, it's just what they asked for.
560 if ([excludedPeerIDs containsObject:peerID]) {
561 // I have been kicked out, and anybody who trusts me should now exclude me.
562 newCircle = [TPCircle circleWithIncludedPeerIDs:addingPeerIDs excludedPeerIDs:@[peerID]];
564 // Drop items from excludedPeerIDs that predate epoch - 1
565 NSSet<NSString*> *filteredExcluded = [excludedPeerIDs objectsPassingTest:^BOOL(NSString *exPeerID, BOOL *stop) {
566 TPPeer *exPeer = self.peersByID[exPeerID];
570 // If we could trust it then we have to keep it in the exclude list.
571 return [self canTrustCandidate:exPeer.permanentInfo inEpoch:epoch];
573 newCircle = [TPCircle circleWithIncludedPeerIDs:[includedPeerIDs allObjects]
574 excludedPeerIDs:[filteredExcluded allObjects]];
577 *updatedCircle = newCircle;
579 return [self createDynamicInfoForPeerWithID:peerID
582 newRemovals:[removingPeerIDs count]
586 - (NSString *)bestCliqueAmongPeerIDs:(NSSet<NSString*>*)peerIDs
588 // The "best" clique is considered the one that is last in lexical ordering.
589 NSString *bestClique = nil;
590 for (NSString *peerID in peerIDs) {
591 NSString *clique = self.peersByID[peerID].dynamicInfo.clique;
593 if (bestClique && NSOrderedAscending != [bestClique compare:clique]) {
602 - (TPCircle *)advancePeerWithID:(NSString *)peerID
603 addingPeerIDs:(NSArray<NSString*> *)addingPeerIDs
604 removingPeerIDs:(NSArray<NSString*> *)removingPeerIDs
605 createClique:(NSString* (^)())createClique
607 TPCircle *circle = nil;
608 TPPeerDynamicInfo *dyn;
609 dyn = [self calculateDynamicInfoForPeerWithID:peerID
610 addingPeerIDs:addingPeerIDs
611 removingPeerIDs:removingPeerIDs
612 createClique:createClique
613 updatedCircle:&circle
616 [self registerCircle:circle];
617 [self updateDynamicInfo:dyn forPeerWithID:peerID];
624 NSString *TPErrorDomain = @"com.apple.security.trustedpeers";
627 TPErrorUnknownPolicyVersion = 1,
628 TPErrorPolicyHashMismatch = 2,
629 TPErrorMissingStableInfo = 3,
633 - (nullable id<TPPolicy>)policyForPeerIDs:(NSSet<NSString*> *)peerIDs
634 error:(NSError **)error
636 NSAssert(peerIDs.count > 0, @"policyForPeerIDs does not accept empty set");
638 TPPolicyDocument *newestPolicyDoc = nil;
640 // This will become the union of policySecrets across the members of peerIDs
641 NSMutableDictionary<NSString*,NSData*> *secrets = [NSMutableDictionary dictionary];
643 for (NSString *peerID in peerIDs) {
644 TPPeerStableInfo *stableInfo = [self peerWithID:peerID].stableInfo;
645 if (nil == stableInfo) {
646 // Allowing missing stableInfo here might be useful if we are writing a voucher
647 // for a peer for which we got permanentInfo over some channel that does not
648 // also convey stableInfo.
651 for (NSString *name in stableInfo.policySecrets) {
652 secrets[name] = stableInfo.policySecrets[name];
654 if (newestPolicyDoc && newestPolicyDoc.policyVersion > stableInfo.policyVersion) {
657 TPPolicyDocument *policyDoc = self.policiesByVersion[@(stableInfo.policyVersion)];
658 if (nil == policyDoc) {
660 *error = [NSError errorWithDomain:TPErrorDomain
661 code:TPErrorUnknownPolicyVersion
664 @"policyVersion": @(stableInfo.policyVersion)
669 if (![policyDoc.policyHash isEqualToString:stableInfo.policyHash]) {
671 *error = [NSError errorWithDomain:TPErrorDomain
672 code:TPErrorPolicyHashMismatch
675 @"policyVersion": @(stableInfo.policyVersion),
676 @"policyDocHash": policyDoc.policyHash,
677 @"peerExpectsHash": stableInfo.policyHash
682 newestPolicyDoc = policyDoc;
684 if (nil == newestPolicyDoc) {
685 // Can happen if no members of peerIDs have stableInfo
687 *error = [NSError errorWithDomain:TPErrorDomain
688 code:TPErrorMissingStableInfo
693 return [newestPolicyDoc policyWithSecrets:secrets decrypter:self.decrypter error:error];
696 - (NSSet<NSString*> *)getPeerIDsTrustedByPeerWithID:(NSString *)peerID
697 toAccessView:(NSString *)view
698 error:(NSError **)error
700 TPCircle *circle = [self peerWithID:peerID].circle;
701 NSMutableSet<NSString*> *peerIDs = [NSMutableSet set];
703 id<TPPolicy> policy = [self policyForPeerIDs:circle.includedPeerIDs error:error];
705 for (NSString *candidateID in circle.includedPeerIDs) {
706 TPPeer *candidate = self.peersByID[candidateID];
707 if (candidate != nil) {
708 NSString *category = [policy categoryForModel:candidate.permanentInfo.modelID];
709 if ([policy peerInCategory:category canAccessView:view]) {
710 [peerIDs addObject:candidateID];
717 - (NSDictionary<NSString*,NSNumber*> *)vectorClock
719 NSMutableDictionary<NSString*,NSNumber*> *dict = [NSMutableDictionary dictionary];
721 [self.peersByID enumerateKeysAndObjectsUsingBlock:^(NSString *peerID, TPPeer *peer, BOOL *stop) {
722 if (peer.stableInfo || peer.dynamicInfo) {
723 TPCounter clock = MAX(peer.stableInfo.clock, peer.dynamicInfo.clock);
724 dict[peerID] = @(clock);