From f96f03de73f69253be7f7fd227a1b6f1159531b6 Mon Sep 17 00:00:00 2001 From: pankcuf Date: Tue, 8 Apr 2025 17:47:15 +0800 Subject: [PATCH 1/4] chore: fix masternode ip addr, add modified pipelines for masternode lists --- .../shared/Categories/NSData/NSData+Dash.m | 2 +- DashSync/shared/DSDashSharedCore.m | 3 +- .../Others/NSError+Platform.h | 4 + .../Others/NSError+Platform.m | 2 +- ...IdentityKeyPathEntity+CoreDataProperties.h | 4 +- DashSync/shared/Models/Identity/DSIdentity.m | 12 +- .../Managers/Chain Managers/DSChainManager.h | 2 +- .../Managers/Chain Managers/DSChainManager.m | 5 +- .../Managers/Chain Managers/DSChainsManager.m | 2 +- .../Chain Managers/DSIdentitiesManager.m | 8 +- .../DSMasternodeManager+Protected.h | 2 + .../Chain Managers/DSMasternodeManager.h | 14 --- .../Chain Managers/DSMasternodeManager.m | 97 ++++++++++++---- .../Managers/Chain Managers/DSPeerManager.m | 4 +- .../Masternode/DSMasternodeListDiffService.h | 2 - .../Masternode/DSMasternodeListDiffService.m | 38 +++--- .../DSMasternodeListService+Protected.h | 6 +- .../Masternode/DSMasternodeListService.h | 18 +-- .../Masternode/DSMasternodeListService.m | 109 ++---------------- .../Masternode/DSQuorumRotationService.h | 1 - .../Masternode/DSQuorumRotationService.m | 23 +++- DashSync/shared/Models/Network/DSPeer.m | 8 +- .../shared/Models/Notifications/DSSyncState.h | 9 -- Example/DashSync.xcodeproj/project.pbxproj | 16 ++- Example/Podfile | 6 +- Example/Podfile.lock | 2 +- 26 files changed, 171 insertions(+), 228 deletions(-) diff --git a/DashSync/shared/Categories/NSData/NSData+Dash.m b/DashSync/shared/Categories/NSData/NSData+Dash.m index 3e9294b6..697ff7d5 100644 --- a/DashSync/shared/Categories/NSData/NSData+Dash.m +++ b/DashSync/shared/Categories/NSData/NSData+Dash.m @@ -883,7 +883,7 @@ uint16_t compactBitsLE(UInt256 number) { for (int pos = 7; pos >= 0; pos--) { if (number.u32[pos]) { for (int bits = 31; bits > 0; bits--) { - if (number.u32[pos] & 1 << bits) + if (number.u32[pos] & 1U << bits) return 32 * pos + bits + 1; } return 32 * pos + 1; diff --git a/DashSync/shared/DSDashSharedCore.m b/DashSync/shared/DSDashSharedCore.m index 6fe83b75..10565672 100644 --- a/DashSync/shared/DSDashSharedCore.m +++ b/DashSync/shared/DSDashSharedCore.m @@ -248,7 +248,8 @@ uint32_t get_block_height_by_hash_caller(const void *context, u256 *block_hash) height = [[core.chain insightVerifiedBlocksByHashDictionary] objectForKey:NSDataFromPtr(block_hash)].height; } u256_dtor(block_hash); - DSLog(@"[SDK] get_block_height_by_hash_caller: %@ = %u", uint256_hex(blockHash), height); + if (height == UINT32_MAX) + DSLog(@"[SDK] get_block_height_by_hash_caller: %@ = %u", uint256_hex(blockHash), height); return height; } void get_block_height_by_hash_dtor(uint32_t result) {} diff --git a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.h b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.h index 2547afe1..9e740be1 100644 --- a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.h +++ b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.h @@ -41,4 +41,8 @@ NS_ASSUME_NONNULL_BEGIN + (NSError *)ffi_from_sml_error:(dashcore_sml_error_SmlError *)ffi_ref; @end +@interface NSError (dashcore_sml_quorum_validation_error_QuorumValidationError) ++ (NSError *)ffi_from_quorum_validation_error:(dashcore_sml_quorum_validation_error_QuorumValidationError *)ffi_ref; +@end + NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m index 1215cef0..c2594c57 100644 --- a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m +++ b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m @@ -92,7 +92,7 @@ @implementation NSError (dashcore_sml_quorum_validation_error_QuorumValidationEr + (NSError *)ffi_from_quorum_validation_error:(dashcore_sml_quorum_validation_error_QuorumValidationError *)ffi_ref { switch (ffi_ref->tag) { case dashcore_sml_quorum_validation_error_QuorumValidationError_RequiredBlockNotPresent: { - u256 *block_hash = dashcore_hash_types_BlockHash_inner(ffi_ref->required_block_not_present); + u256 *block_hash = dashcore_hash_types_BlockHash_inner(ffi_ref->required_block_not_present._0); return [NSError errorWithCode:0 descriptionKey:DSLocalizedFormat(@"QuorumValidationError::RequiredBlockNotPresent: %@ (%@)", nil, u256_hex(block_hash), u256_reversed_hex(block_hash))]; } case dashcore_sml_quorum_validation_error_QuorumValidationError_RequiredBlockHeightNotPresent: diff --git a/DashSync/shared/Models/Entities/DSBlockchainIdentityKeyPathEntity+CoreDataProperties.h b/DashSync/shared/Models/Entities/DSBlockchainIdentityKeyPathEntity+CoreDataProperties.h index bb6497e7..9cd4ff71 100644 --- a/DashSync/shared/Models/Entities/DSBlockchainIdentityKeyPathEntity+CoreDataProperties.h +++ b/DashSync/shared/Models/Entities/DSBlockchainIdentityKeyPathEntity+CoreDataProperties.h @@ -21,8 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) uint16_t keyType; @property (nonatomic, assign) uint16_t keyStatus; @property (nonatomic, assign) uint32_t keyID; -@property (nonatomic, assign) uint8_t securityLevel; -@property (nonatomic, assign) uint8_t purpose; +@property (nonatomic, assign) uint16_t securityLevel; +@property (nonatomic, assign) uint16_t purpose; @property (nullable, nonatomic, retain) NSData *publicKeyData; @end diff --git a/DashSync/shared/Models/Identity/DSIdentity.m b/DashSync/shared/Models/Identity/DSIdentity.m index a4b40f01..ceeb25bd 100644 --- a/DashSync/shared/Models/Identity/DSIdentity.m +++ b/DashSync/shared/Models/Identity/DSIdentity.m @@ -302,7 +302,7 @@ - (void)applyIdentityEntity:(DSBlockchainIdentityEntity *)identityEntity { DSAssetLockTransactionEntity *assetLockEntity = [DSAssetLockTransactionEntity anyObjectInContext:identityEntity.managedObjectContext matching:@"transactionHash.txHash == %@", transactionHashData]; if (assetLockEntity) { self.registrationAssetLockTransactionHash = assetLockEntity.transactionHash.txHash.UInt256; - DSLog(@"%@: AssetLockTX: Found: txHash: %@: entity: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash), assetLockEntity); + DSLog(@"%@: AssetLockTX: Entity Found for txHash: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash)); DSAssetLockTransaction *registrationAssetLockTransaction = (DSAssetLockTransaction *)[assetLockEntity transactionForChain:self.chain]; BOOL correctIndex = self.isOutgoingInvitation ? [registrationAssetLockTransaction checkInvitationDerivationPathIndexForWallet:self.wallet isIndex:self.index] : @@ -1911,20 +1911,20 @@ - (BOOL)processStateTransitionResult:(DMaybeStateTransitionProofResult *)result break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedPartialIdentity: { - NSData *identifier = NSDataFromPtr(proof_result->verified_partial_identity>id->_0->_0); + NSData *identifier = NSDataFromPtr(proof_result->verified_partial_identity->id->_0->_0); DSLog(@"%@: VerifiedPartialIdentity: %@", self.logPrefix, identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedBalanceTransfer: { - dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedBalanceTransfer_Body *transfer = proof_result->verified_balance_transfer; - NSData *from_identifier = NSDataFromPtr(transfer->_0->id->_0->_0); - NSData *to_identifier = NSDataFromPtr(transfer->_1->id->_0->_0); + dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedBalanceTransfer_Body transfer = proof_result->verified_balance_transfer; + NSData *from_identifier = NSDataFromPtr(transfer._0->id->_0->_0); + NSData *to_identifier = NSDataFromPtr(transfer._1->id->_0->_0); DSLog(@"%@: VerifiedBalanceTransfer: %@ --> %@", self.logPrefix, from_identifier.hexString, to_identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedDocuments: { std_collections_Map_keys_platform_value_types_identifier_Identifier_values_Option_dpp_document_Document *verified_documents = proof_result->verified_documents; - DSLog(@"%@: VerifiedDocuments: %u", self.logPrefix, verified_documents->count); + DSLog(@"%@: VerifiedDocuments: %lu", self.logPrefix, verified_documents->count); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedMasternodeVote: { diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h index 421212f7..92e168c3 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h @@ -40,7 +40,7 @@ typedef NS_ENUM(uint32_t, DSSyncCountInfo) DSSyncCountInfo_GovernanceObjectVote = 11, }; -#define PROTOCOL_TIMEOUT 20.0 +#define PROTOCOL_TIMEOUT 40.0 FOUNDATION_EXPORT NSString *const DSChainManagerNotificationChainKey; FOUNDATION_EXPORT NSString *const DSChainManagerNotificationWalletKey; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m index a95640b8..308f90d9 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m @@ -110,7 +110,7 @@ - (instancetype)initWithChain:(DSChain *)chain { } - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [Chain Manager] ", self.chain.name]; + return [NSString stringWithFormat:@"[%@] [Chain Manager]", self.chain.name]; } - (BOOL)isSynced { @@ -157,7 +157,7 @@ - (void)disconnectedMasternodeListAndBlocksRescan { [self removeNonMainnetTrustedPeer]; [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; - DSLog(@"[%@] Disconnected (MasternodeListAndBlocksRescan) -> peerManager::connect", self.logPrefix); + DSLog(@"%@ Disconnected (MasternodeListAndBlocksRescan) -> peerManager::connect", self.logPrefix); [self.peerManager connect]; } @@ -359,6 +359,7 @@ - (void)chain:(DSChain *)chain receivedOrphanBlock:(DSBlock *)block fromPeer:(DS - (void)chain:(DSChain *)chain wasExtendedWithBlock:(DSBlock *)merkleBlock fromPeer:(DSPeer *)peer { if (([[DSOptionsManager sharedInstance] syncType] & DSSyncType_MasternodeList)) { // make sure we care about masternode lists + DSLog(@"%@ [%@:%d] chain extended with %@ -> re-request masternode lists", self.logPrefix, peer.host, peer.port, uint256_hex(merkleBlock.blockHash)); [self.masternodeManager getCurrentMasternodeListWithSafetyDelay:3]; } } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainsManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSChainsManager.m index bb691628..d400f222 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainsManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainsManager.m @@ -73,7 +73,7 @@ - (instancetype)init { self.knownChains = [NSMutableArray array]; self.knownDevnetChains = [NSMutableArray array]; self.reachability = [DSReachabilityManager sharedManager]; - register_rust_logger(); + //register_rust_logger(); } return self; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m index b5dd2176..65047cbe 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m @@ -59,7 +59,7 @@ @interface DSIdentitiesManager () @implementation DSIdentitiesManager - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [Identity Manager] ", self.chain.name]; + return [NSString stringWithFormat:@"[%@] [Identity Manager]", self.chain.name]; } - (instancetype)initWithChain:(DSChain *)chain { @@ -128,11 +128,11 @@ - (NSArray *)unsyncedIdentities { NSMutableArray *unsyncedIdentities = [NSMutableArray array]; for (DSIdentity *identity in [self.chain localIdentities]) { if (!identity.registrationAssetLockTransaction || (identity.registrationAssetLockTransaction.blockHeight == BLOCK_UNKNOWN_HEIGHT)) { - DSLog(@"%@: unsynced identity (asset lock tx unknown or has unknown height) %@ %@", self.logPrefix, uint256_hex(identity.registrationAssetLockTransactionHash), identity.registrationAssetLockTransaction); + DSLog(@"%@ Unsynced identity (asset lock tx unknown or has unknown height) %@ %@", self.logPrefix, uint256_hex(identity.registrationAssetLockTransactionHash), identity.registrationAssetLockTransaction); [unsyncedIdentities addObject:identity]; } else if (self.chain.lastSyncBlockHeight > identity.dashpaySyncronizationBlockHeight) { - DSLog(@"%@: unsynced identity (lastSyncBlockHeight (%u) > dashpaySyncronizationBlockHeight %u)", self.logPrefix, self.chain.lastSyncBlockHeight, identity.dashpaySyncronizationBlockHeight); + DSLog(@"%@: Unsynced identity (lastSyncBlockHeight (%u) > dashpaySyncronizationBlockHeight %u)", self.logPrefix, self.chain.lastSyncBlockHeight, identity.dashpaySyncronizationBlockHeight); //If they are equal then the blockchain identity is synced //This is because the dashpaySyncronizationBlock represents the last block for the bloom filter used in L1 should be considered valid //That's because it is set at the time with the hash of the last @@ -151,7 +151,7 @@ - (NSArray *)unsyncedIdentities { //TODO: if we get an error or identity not found, better stop the process and start syncing chain - (void)syncIdentitiesWithCompletion:(IdentitiesSuccessCompletionBlock)completion { - DSLog(@"%@: Sync Identities", self.logPrefix); + DSLog(@"%@ Sync Identities", self.logPrefix); if (!self.chain.isEvolutionEnabled) { if (completion) dispatch_async(self.chain.networkingQueue, ^{ completion(@[]); }); return; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h index 2f6240a2..ac26c452 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h @@ -56,6 +56,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)processRequestFromFileForBlockHash:(UInt256)blockHash; - (void)issueWithMasternodeListFromPeer:(DSPeer *)peer; +- (void)printEngineStatus; + @end diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h index 1a08e6e4..0d498a8b 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h @@ -47,14 +47,12 @@ FOUNDATION_EXPORT NSString *const DSQuorumListDidChangeNotification; @property (nonatomic, readonly) NSUInteger simplifiedMasternodeEntryCount; @property (nonatomic, readonly) NSUInteger activeQuorumsCount; @property (nonatomic, readonly) NSUInteger knownMasternodeListsCount; -//@property (nonatomic, readonly) uint32_t earliestMasternodeListBlockHeight; @property (nonatomic, readonly) uint32_t lastMasternodeListBlockHeight; @property (nonatomic, readonly) DMasternodeList *currentMasternodeList; @property (nonatomic, readonly) NSUInteger masternodeListRetrievalQueueCount; @property (nonatomic, readonly) NSUInteger masternodeListRetrievalQueueMaxAmount; @property (nonatomic, readonly) BOOL currentMasternodeListIsInLast24Hours; -//@property (nonatomic, readonly) DSMasternodeListStore *store; @property (nonatomic, readonly) DSMasternodeListDiffService *masternodeListDiffService; @property (nonatomic, readonly) DSQuorumRotationService *quorumRotationService; @property (nonatomic, assign, readonly) uint32_t rotatedQuorumsActivationHeight; @@ -69,27 +67,15 @@ FOUNDATION_EXPORT NSString *const DSQuorumListDidChangeNotification; - (DMasternodeEntry *)masternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port; - (BOOL)hasMasternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port; -//- (DLLMQEntry *_Nullable)quorumEntryForInstantSendRequestID:(UInt256)requestID -// withBlockHeightOffset:(uint32_t)blockHeightOffset; -//- (DLLMQEntry *_Nullable)quorumEntryForChainLockRequestID:(UInt256)requestID -// withBlockHeightOffset:(uint32_t)blockHeightOffset; -//- (DLLMQEntry *_Nullable)quorumEntryForChainLockRequestID:(UInt256)requestID -// forBlockHeight:(uint32_t)blockHeight; -//- (DLLMQEntry *_Nullable)quorumEntryForPlatformHavingQuorumHash:(UInt256)quorumHash -// forBlockHeight:(uint32_t)blockHeight; - - (DMasternodeList *_Nullable)masternodeListForBlockHash:(UInt256)blockHash withBlockHeightLookup:(uint32_t (^_Nullable)(UInt256 blockHash))blockHeightLookup; - (DMasternodeList *_Nullable)masternodeListForBlockHash:(UInt256)blockHash; - (void)startSync; - (void)stopSync; -//- (BOOL)requestMasternodeListForBlockHeight:(uint32_t)blockHeight -// error:(NSError *_Nullable *_Nullable)error; /// Returns current masternode list - (void)reloadMasternodeLists; -//- (DMasternodeList *_Nullable)reloadMasternodeListsWithBlockHeightLookup:(BlockHeightFinder)blockHeightLookup; - (void)checkPingTimesForCurrentMasternodeListInContext:(NSManagedObjectContext *)context withCompletion:(void (^)(NSMutableDictionary *pingTimes, NSMutableDictionary *errors))completion; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m index 0105b8fb..be17dadd 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m @@ -63,7 +63,8 @@ @interface DSMasternodeManager () @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; @property (nonatomic) BOOL isSyncing; @property (nonatomic) BOOL isRestored; -@property (nonatomic, strong) NSData* baseBlockHashWaitingForDiffs; +@property (nonatomic) BOOL hasHandledQrInfoPipeline; +@property (nonatomic) BOOL isPendingValidation; @end @@ -88,7 +89,7 @@ - (MasternodeProcessor *)processor { } - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [MasternodeManager] ", self.chain.name]; + return [NSString stringWithFormat:@"[%@] [MasternodeManager]", self.chain.name]; } - (BOOL)hasCurrentMasternodeListInLast30Days { @@ -104,9 +105,7 @@ - (BOOL)hasCurrentMasternodeListInLast30Days { - (void)masternodeListServiceEmptiedRetrievalQueue:(DSMasternodeListService *)service { - if (self.baseBlockHashWaitingForDiffs) - [self getRecentMasternodeList]; - else + if (!self.isPendingValidation) [self.chain.chainManager chainFinishedSyncingMasternodeListsAndQuorums:self.chain]; } @@ -127,7 +126,11 @@ - (uint32_t)heightForBlockHash:(UInt256)blockhash { - (BOOL)isMasternodeListOutdated { uint32_t lastHeight = self.lastMasternodeListBlockHeight; - return lastHeight == UINT32_MAX || lastHeight < self.chain.lastTerminalBlockHeight - 8; + return lastHeight == UINT32_MAX || lastHeight < self.chain.lastTerminalBlockHeight; +} + +- (BOOL)isQRInfoOutdated { + return dash_spv_masternode_processor_processing_processor_MasternodeProcessor_is_qr_info_outdated(self.processor, self.chain.lastTerminalBlockHeight); } - (DMasternodeEntry *)masternodeHavingProviderRegistrationTransactionHash:(NSData *)providerRegistrationTransactionHash { @@ -349,7 +352,12 @@ - (void)getRecentMasternodeList { DSLog(@"%@ getRecentMasternodeList: (no block exist) for tip", self.logPrefix); return; } - [self.quorumRotationService getRecent:merkleBlock.blockHash]; + NSData *blockHash = uint256_data(merkleBlock.blockHash); + if (!self.isPendingValidation && (!self.hasHandledQrInfoPipeline || [self isQRInfoOutdated])) + [self.quorumRotationService getRecent:blockHash]; + + if (!self.isPendingValidation && self.hasHandledQrInfoPipeline && [self isMasternodeListOutdated]) + [self.masternodeListDiffService getRecent:blockHash]; } @@ -363,7 +371,7 @@ - (void)getCurrentMasternodeListWithSafetyDelay:(uint32_t)safetyDelay { dispatch_source_set_timer(self.masternodeListTimer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(safetyDelay * NSEC_PER_SEC)), DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC); dispatch_source_set_event_handler(self.masternodeListTimer, ^{ NSTimeInterval timeElapsed = [[NSDate date] timeIntervalSince1970] - self.timeIntervalForMasternodeRetrievalSafetyDelay; - if (timeElapsed > safetyDelay) { + if (timeElapsed > safetyDelay && !self.isPendingValidation) { [self getRecentMasternodeList]; } }); @@ -445,16 +453,38 @@ - (void)issueWithMasternodeListFromPeer:(DSPeer *)peer { [self.chain.chainManager notify:DSMasternodeListDiffValidationErrorNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; } +- (void)printEngineStatus { + dash_spv_masternode_processor_processing_processor_MasternodeProcessor_print_engine_status(self.processor); +} + +- (NSError *_Nullable)verifyQuorums { +#if defined(DASHCORE_QUORUM_VALIDATION) + DSLog(@"%@: Quorums Verification: Started", self.logPrefix); + Result_ok_bool_err_dashcore_sml_quorum_validation_error_QuorumValidationError *result = dash_spv_masternode_processor_processing_processor_MasternodeProcessor_verify_current_masternode_list_quorums(self.processor); + if (result->error) { + NSError *quorumValidationError = [NSError ffi_from_quorum_validation_error:result->error]; + DSLog(@"%@ Quorums Verification: Error: %@", self.logPrefix, quorumValidationError.description); + Result_ok_bool_err_dashcore_sml_quorum_validation_error_QuorumValidationError_destroy(result); + return quorumValidationError; + } + DSLog(@"%@: Quorums Verification: Ok", self.logPrefix); + Result_ok_bool_err_dashcore_sml_quorum_validation_error_QuorumValidationError_destroy(result); +#else + DSLog(@"%@: Quorums Verification: Disabled", self.logPrefix); +#endif + return nil; +} - (void)peer:(DSPeer *)peer relayedMasternodeDiffMessage:(NSData *)message { - DSLog(@"%@ [%@:%d] mnlistdiff: received: %@", self.logPrefix, peer.host, peer.port, uint256_hex(message.SHA256)); + //DSLog(@"%@ [%@:%d] mnlistdiff: received: %@", self.logPrefix, peer.host, peer.port, uint256_hex(message.SHA256)); @synchronized (self.masternodeListDiffService) { self.masternodeListDiffService.timedOutAttempt = 0; } dispatch_async(self.processingQueue, ^{ dispatch_group_enter(self.processingGroup); Slice_u8 *message_slice = slice_ctor(message); - DMnDiffResult *result = DMnDiffFromMessage(self.processor, message_slice, nil, false); + DMnDiffResult *result = DMnDiffFromMessage(self.processor, message_slice, nil, self.hasHandledQrInfoPipeline); + if (result->error) { NSError *error = [NSError ffi_from_processing_error:result->error]; DSLog(@"%@ mnlistdiff: Error: %@", self.logPrefix, error.description); @@ -470,7 +500,7 @@ - (void)peer:(DSPeer *)peer relayedMasternodeDiffMessage:(NSData *)message { break; } #if SAVE_MASTERNODE_DIFF_TO_FILE - [self writeToDisk:[NSString stringWithFormat:@"MNL_ERR__%d_%lu.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; + [self writeToDisk:[NSString stringWithFormat:@"MNL_ERR__%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; #endif DMnDiffResultDtor(result); dispatch_group_leave(self.processingGroup); @@ -480,14 +510,20 @@ - (void)peer:(DSPeer *)peer relayedMasternodeDiffMessage:(NSData *)message { if (self.isSyncing) { u256 *block_hash = dashcore_hash_types_BlockHash_inner(result->ok->o_1); NSData *blockHashData = NSDataFromPtr(block_hash); + DSLog(@"%@ MNLISTDIFF: OK: %@", self.logPrefix, blockHashData.hexString); u256_dtor(block_hash); [self.masternodeListDiffService removeFromRetrievalQueue:blockHashData]; - if ([self.masternodeListDiffService retrievalQueueCount]) { + if ([self.masternodeListDiffService hasActiveQueue]) { [self.masternodeListDiffService dequeueMasternodeListRequest]; - } else { - DSLog(@"%@: All diffs are here -> Re-request QRINFO for", self.logPrefix, self.baseBlockHashWaitingForDiffs.hexString); - [self getRecentMasternodeList]; + } else if (self.isPendingValidation) { + NSError *quorumValidationError = [self verifyQuorums]; + if (quorumValidationError) { + DMnDiffResultDtor(result); + dispatch_group_leave(self.processingGroup); + return; + } + [self finishIntitialQrInfoPipeline]; } } DMnDiffResultDtor(result); @@ -533,7 +569,7 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin break; } #if SAVE_MASTERNODE_DIFF_TO_FILE - [self writeToDisk:[NSString stringWithFormat:@"QRINFO_ERR_%d_%lu.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; + [self writeToDisk:[NSString stringWithFormat:@"QRINFO_ERR_%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; #endif #if SAVE_ERROR_STATE Result_ok_Vec_u8_err_dash_spv_masternode_processor_processing_processor_processing_error_ProcessingError *bincode = dash_spv_masternode_processor_processing_processor_MasternodeProcessor_serialize_engine(self.processor); @@ -553,18 +589,25 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin [[NSUserDefaults standardUserDefaults] removeObjectForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; std_collections_BTreeSet_dashcore_hash_types_BlockHash *missed_hashes = result->ok; - DSLog(@"%@ qrinfo: OK: %ul", self.logPrefix, (uint16_t) missed_hashes->count); + DSLog(@"%@ QRINFO: OK: %u", self.logPrefix, (uint32_t) missed_hashes->count); +// #if SAVE_MASTERNODE_DIFF_TO_FILE +// [self writeToDisk:[NSString stringWithFormat:@"QRINFO_MISSED_%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; +// #endif + if (missed_hashes->count > 0) { - self.baseBlockHashWaitingForDiffs = [self.quorumRotationService retrievalBlockHash]; + self.isPendingValidation = YES; [self.quorumRotationService cleanAllLists]; NSArray *missedHashes = [NSArray ffi_from_block_hash_btree_set:missed_hashes]; [self.masternodeListDiffService addToRetrievalQueueArray:missedHashes]; [self.masternodeListDiffService dequeueMasternodeListRequest]; } else { - self.baseBlockHashWaitingForDiffs = nil; - [self.quorumRotationService cleanAllLists]; - [self.quorumRotationService dequeueMasternodeListRequest]; - [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + NSError *quorumValidationError = [self verifyQuorums]; + if (quorumValidationError) { + DQRInfoResultDtor(result); + dispatch_group_leave(self.processingGroup); + return; + } + [self finishIntitialQrInfoPipeline]; } DQRInfoResultDtor(result); @@ -572,8 +615,16 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin }); } +- (void)finishIntitialQrInfoPipeline { + self.hasHandledQrInfoPipeline = YES; + self.isPendingValidation = NO; + [self.quorumRotationService cleanAllLists]; + [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + [self.quorumRotationService dequeueMasternodeListRequest]; +} + - (void)peer:(DSPeer *)peer relayedQuorumRotationInfoMessage:(NSData *)message { - DSLog(@"%@ [%@:%d] qrinfo: received: %@", self.logPrefix, peer.host, peer.port, uint256_hex(message.SHA256)); + //DSLog(@"%@ [%@:%d] qrinfo: received: %@", self.logPrefix, peer.host, peer.port, uint256_hex(message.SHA256)); @synchronized (self.quorumRotationService) { self.quorumRotationService.timedOutAttempt = 0; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m index 4e7c1dfc..ef07942e 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m @@ -261,7 +261,9 @@ - (void)clearPeers:(DSDisconnectReason)reason { NSMutableArray *mArray = [NSMutableArray array]; for (int i = 0; i < vec->count; i++) { Tuple_Arr_u8_16_u16 *pair = vec->values[i]; - [mArray addObject:[[DSPeer alloc] initWithAddress:u128_cast(pair->o_0) andPort:pair->o_1 onChain:self.chain]]; + UInt128 ip = u128_cast(pair->o_0); + uint16_t port = pair->o_1; + [mArray addObject:[[DSPeer alloc] initWithAddress:ip andPort:port onChain:self.chain]]; } Vec_Tuple_Arr_u8_16_u16_destroy(vec); return mArray; diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.h b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.h index 0b0167d5..05f5710e 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.h +++ b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.h @@ -24,12 +24,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSOrderedSet *retrievalQueue; @property (nonatomic, readonly) NSUInteger retrievalQueueCount; -//@property (nonatomic, readonly) NSUInteger retrievalQueueMaxAmount; - (NSUInteger)addToRetrievalQueue:(NSData *)masternodeBlockHashData; - (NSUInteger)removeFromRetrievalQueue:(NSData *)masternodeBlockHashData; - (NSUInteger)addToRetrievalQueueArray:(NSArray *_Nonnull)masternodeBlockHashDataArray; -//- (void)cleanListsRetrievalQueue; - (void)fetchMasternodeListsToRetrieve:(void (^)(NSOrderedSet *listsToRetrieve))completion; diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m index a772e300..5a4b0b0b 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m +++ b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m @@ -44,22 +44,19 @@ - (NSString *)logPrefix { return [NSString stringWithFormat:@"[%@] [MasternodeManager::DiffService] ", self.chain.name]; } +- (void)getRecent:(NSData *)blockHash { + [self addToRetrievalQueue:blockHash]; + [self dequeueMasternodeListRequest]; +} + - (void)composeMasternodeListRequest:(NSOrderedSet *)list { NSMutableString *debugString = [NSMutableString stringWithString:@"Needed:\n"]; for (NSData *data in list) { uint32_t h = [self.chain heightForBlockHash:data.UInt256]; [debugString appendFormat:@"%u: %@\n", h, data.hexString]; } - [debugString appendFormat:@"KnownLists:\n"]; - DKnownMasternodeLists *lists = dash_spv_masternode_processor_processing_processor_MasternodeProcessor_masternode_lists(self.chain.sharedProcessorObj); - for (int i = 0; i < lists->count; i++) { - dashcore_prelude_CoreBlockHeight *core_block_height = lists->keys[i]; - DMasternodeList *list = lists->values[i]; - u256 *block_hash = dashcore_hash_types_BlockHash_inner(list->block_hash); - [debugString appendFormat:@"%u: %@\n", core_block_height->_0, u256_hex(block_hash)]; - } - DKnownMasternodeListsDtor(lists); DSLog(@"%@ composeMasternodeListRequest: \n%@", self.logPrefix, debugString); +// [self.chain.masternodeManager printEngineStatus]; for (NSData *blockHashData in list) { // we should check the associated block still exists if ([self.chain.masternodeManager hasBlockForBlockHash:blockHashData]) { @@ -69,7 +66,7 @@ - (void)composeMasternodeListRequest:(NSOrderedSet *)list { BOOL success = [self.chain.masternodeManager processRequestFromFileForBlockHash:blockHash]; if (success) { [self removeFromRetrievalQueue:blockHashData]; - if (![self retrievalQueueCount]) + if (![self hasActiveQueue]) [self.chain.chainManager.transactionManager checkWaitingForQuorums]; } else { // we need to go get it @@ -84,7 +81,6 @@ - (void)composeMasternodeListRequest:(NSOrderedSet *)list { // request at: every new block // NSAssert(([self.store heightForBlockHash:previousBlockHash] != UINT32_MAX) || uint256_is_zero(previousBlockHash), @"This block height should be known"); [self requestMasternodeListDiff:previousBlockHash forBlockHash:blockHash]; -// [self requestMasternodeListDiff:@"00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6".hexToData.reverse.UInt256 forBlockHash:@"c21ff900433ace7e6b7841bdfec8c449ca06414b237167e30b00000000000000".hexToData.UInt256]; } } else { DSLog(@"%@ Missing block (%@)", self.logPrefix, blockHashData.hexString); @@ -94,7 +90,8 @@ - (void)composeMasternodeListRequest:(NSOrderedSet *)list { } - (void)fetchMasternodeListsToRetrieve:(void (^)(NSOrderedSet *listsToRetrieve))completion { - if (![self retrievalQueueCount]) { + //DSLog(@"%@ fetchMasternodeListToRetrieve...: %u", self.logPrefix, [self hasActiveQueue]); + if (![self hasActiveQueue]) { DSLog(@"%@ No masternode lists in retrieval", self.logPrefix); [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; return; @@ -187,8 +184,13 @@ - (void)cleanListsRetrievalQueue { @synchronized (_retrievalQueue) { [_retrievalQueue removeAllObjects]; } + self.retrievalQueueMaxAmount = 0; + [self notifyQueueChange:0 maxAmount:0]; } +- (BOOL)hasActiveQueue { + return [self.retrievalQueue count]; +} - (void)requestMasternodeListDiff:(UInt256)previousBlockHash forBlockHash:(UInt256)blockHash { DSGetMNListDiffRequest *request = [DSGetMNListDiffRequest requestWithBaseBlockHash:previousBlockHash blockHash:blockHash]; @@ -199,27 +201,17 @@ - (void)requestMasternodeListDiff:(UInt256)previousBlockHash forBlockHash:(UInt2 } uint32_t prev_h = [self.chain heightForBlockHash:previousBlockHash]; uint32_t h = [self.chain heightForBlockHash:blockHash]; - -// uint32_t prev_h = DHeightForBlockHash(self.chain.sharedProcessorObj, u256_ctor_u(previousBlockHash)); -// uint32_t h = DHeightForBlockHash(self.chain.sharedProcessorObj, u256_ctor_u(blockHash)); DSLog(@"%@ Request: %u..%u %@ .. %@", self.logPrefix, prev_h, h, uint256_hex(previousBlockHash), uint256_hex(blockHash)); - if (prev_h == 0) { - DSLog(@"%@ Zero height", self.logPrefix); - } - if (prev_h == 530000) { - DSLog(@"start from checkpoint"); - } [self sendMasternodeListRequest:request]; } - (void)notifyQueueChange:(NSUInteger)newCount maxAmount:(NSUInteger)maxAmount { - DSLog(@"%@ Queue Changed: %u/%u ", self.logPrefix, (uint32_t)newCount, (uint32_t)maxAmount); + // DSLog(@"%@Queue Changed: %u/%u ", self.logPrefix, (uint32_t)newCount, (uint32_t)maxAmount); @synchronized (self.chain.chainManager.syncState) { self.chain.chainManager.syncState.masternodeListSyncInfo.retrievalQueueCount = (uint32_t) newCount; self.chain.chainManager.syncState.masternodeListSyncInfo.retrievalQueueMaxAmount = (uint32_t) maxAmount; [self.chain.chainManager notifySyncStateChanged]; } - } /// test-only diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h b/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h index fa4f81aa..c62d21c3 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h +++ b/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h @@ -23,16 +23,12 @@ NS_ASSUME_NONNULL_BEGIN @interface DSMasternodeListService (Protected) -//@property (nonatomic, assign) NSMutableOrderedSet *retrievalQueue; - - (NSString *)logPrefix; +- (BOOL)hasActiveQueue; -//- (void)updateAfterProcessingMasternodeListWithBlockHash:(NSData *)blockHashData fromPeer:(DSPeer *)peer; - (BOOL)shouldProcessDiffResult:(u256 *)block_hash isValid:(BOOL)isValid skipPresenceInRetrieval:(BOOL)skipPresenceInRetrieval; -//- (BOOL)shouldProcessDiffResult:(DSMnDiffProcessingResult *)diffResult skipPresenceInRetrieval:(BOOL)skipPresenceInRetrieval; -//- (DSMasternodeListRequest*__nullable)requestInRetrievalFor:(UInt256)baseBlockHash blockHash:(UInt256)blockHash; - (UInt256)closestKnownBlockHashForBlockHeight:(uint32_t)blockHeight; - (void)startTimeOutObserver; diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListService.h b/DashSync/shared/Models/Masternode/DSMasternodeListService.h index b82346ee..acb2ae41 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListService.h +++ b/DashSync/shared/Models/Masternode/DSMasternodeListService.h @@ -34,37 +34,21 @@ FOUNDATION_EXPORT NSString *const DSMasternodeListDiffValidationErrorNotificatio @property (nonatomic, readonly, nonnull) DSChain *chain; @property (nonatomic, readonly) NSMutableSet *requestsInRetrieval; -//@property (nonatomic, readonly) NSOrderedSet *retrievalQueue; -//@property (nonatomic, readonly) NSUInteger retrievalQueueCount; @property (nonatomic, readwrite) NSUInteger retrievalQueueMaxAmount; - @property (nonatomic, assign) uint16_t timedOutAttempt; @property (nonatomic, assign) uint16_t timeOutObserverTry; - (instancetype)initWithChain:(DSChain *)chain; - - (void)dequeueMasternodeListRequest; +- (void)getRecent:(NSData *)blockHash; - (void)stop; - - (BOOL)peerIsDisconnected; - - (void)cleanAllLists; - (void)cleanRequestsInRetrieval; -//- (void)composeMasternodeListRequest:(NSOrderedSet *)list; - -//- (void)fetchMasternodeListsToRetrieve:(void (^)(NSOrderedSet *listsToRetrieve))completion; - -//- (NSUInteger)addToRetrievalQueue:(NSData *)masternodeBlockHashData; -//- (void)removeFromRetrievalQueue:(NSData *)masternodeBlockHashData; - (void)cleanListsRetrievalQueue; - (BOOL)removeRequestInRetrievalForBaseBlockHash:(UInt256)baseBlockHash blockHash:(UInt256)blockHash; - - (void)disconnectFromDownloadPeer; -//- (void)issueWithMasternodeListFromPeer:(DSPeer *)peer; - - (void)sendMasternodeListRequest:(DSMasternodeListRequest *)request; - -//- (void)checkWaitingForQuorums; - (DSMasternodeListRequest*__nullable)requestInRetrievalFor:(UInt256)baseBlockHash blockHash:(UInt256)blockHash; @end diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListService.m b/DashSync/shared/Models/Masternode/DSMasternodeListService.m index c37d48f6..ca38e666 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListService.m +++ b/DashSync/shared/Models/Masternode/DSMasternodeListService.m @@ -52,11 +52,14 @@ - (UInt256)closestKnownBlockHashForBlockHeight:(uint32_t)blockHeight { return known; } +- (BOOL)hasActiveQueue { + return NO; +} + - (void)startTimeOutObserver { [self cancelTimeOutObserver]; @synchronized (self) { NSSet *requestsInRetrieval = [self.requestsInRetrieval copy]; -// uintptr_t masternodeListCount = DKnownMasternodeListsCount(self.chain.sharedCacheObj); self.timeOutObserverTry++; uint16_t timeOutObserverTry = self.timeOutObserverTry; dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(40 * (self.timedOutAttempt + 1) * NSEC_PER_SEC)); @@ -65,15 +68,14 @@ - (void)startTimeOutObserver { dispatch_source_set_timer(self.timeoutTimer, timeout, DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC); dispatch_source_set_event_handler(self.timeoutTimer, ^{ @synchronized (self) { - if (!self.retrievalQueueMaxAmount || self.timeOutObserverTry != timeOutObserverTry) { + if (![self hasActiveQueue] || self.timeOutObserverTry != timeOutObserverTry) return; - } + NSSet *requestsInRetrieval2 = [self.requestsInRetrieval copy]; NSMutableSet *leftToGet = [requestsInRetrieval mutableCopy]; [leftToGet intersectSet:requestsInRetrieval2]; -// uintptr_t count = DKnownMasternodeListsCount(self.chain.sharedCacheObj); - - if (/*(masternodeListCount == count) &&*/ [requestsInRetrieval isEqualToSet:leftToGet]) { + + if ([requestsInRetrieval isEqualToSet:leftToGet]) { DSLog(@"%@ TimedOut -> dequeueMasternodeListRequest", self.logPrefix); self.timedOutAttempt++; [self disconnectFromDownloadPeer]; @@ -98,21 +100,10 @@ - (void)cancelTimeOutObserver { } } -//- (void)checkWaitingForQuorums { -// if (![self retrievalQueueCount]) { -// [self.chain.chainManager.transactionManager checkWaitingForQuorums]; -// } -//} +- (void)getRecent:(NSData *)blockHash { +} -//- (void)composeMasternodeListRequest:(NSOrderedSet *)list { -// /* Should be overriden */ -//} -// - (void)dequeueMasternodeListRequest { -// [self fetchMasternodeListsToRetrieve:^(NSOrderedSet *list) { -// [self composeMasternodeListRequest:list]; -// [self startTimeOutObserver]; -// }]; } - (void)stop { @@ -120,36 +111,6 @@ - (void)stop { [self cleanAllLists]; } -//- (void)updateAfterProcessingMasternodeListWithBlockHash:(NSData *)blockHashData fromPeer:(DSPeer *)peer { -// -// [self removeFromRetrievalQueue:blockHashData]; -// DSLog(@"%@ updateAfterProcessingMasternodeListWithBlockHash %@ -> dequeueMasternodeListRequest (mn)", self.logPrefix, blockHashData.hexString); -// [self dequeueMasternodeListRequest]; -// [self checkWaitingForQuorums]; -// [[NSUserDefaults standardUserDefaults] removeObjectForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; -//} - -//- (NSUInteger)addToRetrievalQueue:(NSData *)masternodeBlockHashData { -// @synchronized (_retrievalQueue) { -// [_retrievalQueue addObject:uint256_data(masternodeBlockHashData.UInt256)]; -// NSUInteger newCount = [_retrievalQueue count]; -// _retrievalQueueMaxAmount = MAX(self.retrievalQueueMaxAmount, newCount); -// [_retrievalQueue sortUsingComparator:^NSComparisonResult(NSData *obj1, NSData *obj2) { -// if ([self.chain heightForBlockHash:obj1.UInt256] < [self.chain heightForBlockHash:obj2.UInt256]) { -// return NSOrderedAscending; -// } else { -// return NSOrderedDescending; -// } -// }]; -// return newCount; -// } -//} -// -//- (void)removeFromRetrievalQueue:(NSData *)masternodeBlockHashData { -// [_retrievalQueue removeObject:masternodeBlockHashData]; -//// [self.retrievalQueue ] -//// DMnDiffQueueRemove(self.chain.sharedProcessorObj, u256_ctor(masternodeBlockHashData)); -//} - (void)cleanRequestsInRetrieval { [self.requestsInRetrieval removeAllObjects]; @@ -175,30 +136,6 @@ - (DSPeerManager *)peerManager { return self.chain.chainManager.peerManager; } -//- (NSOrderedSet *)retrievalQueue { -// @synchronized (_retrievalQueue) { -// return [_retrievalQueue copy]; -// } -// -//// indexmap_IndexSet_u8_32 *queue = dash_spv_masternode_processor_processing_processor_cache_MasternodeProcessorCache_mn_list_retrieval_queue(self.chain.sharedCacheObj); -//// NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSetWithCapacity:queue->count]; -//// for (int i = 0; i < queue->count; i++) { -//// [set addObject:NSDataFromPtr(queue->values[i])]; -//// } -//// indexmap_IndexSet_u8_32_destroy(queue); -//// return [_retrievalQueue copy]; -//} -// -//- (NSUInteger)retrievalQueueCount { -// @synchronized (_retrievalQueue) { -// return [_retrievalQueue count]; -// } -//// return DMnDiffQueueCount(self.chain.sharedCacheObj); -//} -//- (NSUInteger)retrievalQueueMaxAmount { -// return DMnDiffQueueMaxAmount(self.chain.sharedCacheObj); -//} - - (BOOL)peerIsDisconnected { BOOL peerIsDisconnected; @synchronized (self.peerManager.downloadPeer) { @@ -207,8 +144,6 @@ - (BOOL)peerIsDisconnected { return peerIsDisconnected; } - - - (DSMasternodeListRequest*__nullable)requestInRetrievalFor:(UInt256)baseBlockHash blockHash:(UInt256)blockHash { DSMasternodeListRequest *matchedRequest = nil; for (DSMasternodeListRequest *request in [self.requestsInRetrieval copy]) { @@ -233,30 +168,6 @@ - (void)disconnectFromDownloadPeer { [self.peerManager.downloadPeer disconnect]; } -//- (void)issueWithMasternodeListFromPeer:(DSPeer *)peer { -// [self.peerManager peerMisbehaving:peer errorMessage:@"Issue with Deterministic Masternode list"]; -// NSArray *faultyPeers = [[NSUserDefaults standardUserDefaults] arrayForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; -// if (faultyPeers.count >= MAX_FAULTY_DML_PEERS) { -// DSLog(@"%@ Exceeded max failures for masternode list, starting from scratch", self.logPrefix); -// //no need to remove local masternodes -// [self cleanListsRetrievalQueue]; -//// [self.store deleteAllOnChain]; -//// [self.store removeOldMasternodeLists]; -// [[NSUserDefaults standardUserDefaults] removeObjectForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; -// [self.chain.masternodeManager getRecentMasternodeList]; -// } else { -// if (!faultyPeers) { -// faultyPeers = @[peer.location]; -// } else if (![faultyPeers containsObject:peer.location]) { -// faultyPeers = [faultyPeers arrayByAddingObject:peer.location]; -// } -// [[NSUserDefaults standardUserDefaults] setObject:faultyPeers forKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; -// DSLog(@"%@ Failure %lu for masternode list from peer: %@", self.logPrefix, (unsigned long)faultyPeers.count, peer); -// [self dequeueMasternodeListRequest]; -// } -// [self.chain.chainManager notify:DSMasternodeListDiffValidationErrorNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; -//} - - (void)sendMasternodeListRequest:(DSMasternodeListRequest *)request { // DSLog(@"•••• sendMasternodeListRequest: %@", [request toData].hexString); [self.peerManager sendRequest:request]; diff --git a/DashSync/shared/Models/Masternode/DSQuorumRotationService.h b/DashSync/shared/Models/Masternode/DSQuorumRotationService.h index 2a6bc546..c655b91e 100644 --- a/DashSync/shared/Models/Masternode/DSQuorumRotationService.h +++ b/DashSync/shared/Models/Masternode/DSQuorumRotationService.h @@ -25,7 +25,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) NSTimeInterval lastSyncedTimestamp; @property (nonatomic, strong, readonly) NSData *retrievalBlockHash; -- (void)getRecent:(UInt256)blockHash; - (void)fetchMasternodeListToRetrieve:(void (^)(NSData *listsToRetrieve))completion; - (void)requestQuorumRotationInfo:(UInt256)previousBlockHash forBlockHash:(UInt256)blockHash; diff --git a/DashSync/shared/Models/Masternode/DSQuorumRotationService.m b/DashSync/shared/Models/Masternode/DSQuorumRotationService.m index 0bceb27e..69b15e94 100644 --- a/DashSync/shared/Models/Masternode/DSQuorumRotationService.m +++ b/DashSync/shared/Models/Masternode/DSQuorumRotationService.m @@ -55,6 +55,7 @@ - (void)composeMasternodeListRequest:(NSData *)blockHashData { } else { DSLog(@"%@ Missing block: %@ (%@)", self.logPrefix, blockHashData.hexString, blockHashData.reverse.hexString); self.retrievalBlockHash = nil; + self.retrievalQueueMaxAmount = 0; } } @@ -65,9 +66,13 @@ - (void)dequeueMasternodeListRequest { }]; } -- (NSUInteger)retrievalQueueMaxAmount { return 1; } - - (void)fetchMasternodeListToRetrieve:(void (^)(NSData *listsToRetrieve))completion { + //DSLog(@"%@ fetchMasternodeListToRetrieve...: %u", self.logPrefix, [self hasActiveQueue]); + if (![self hasActiveQueue]) { + DSLog(@"%@ No masternode lists in retrieval", self.logPrefix); + [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; + return; + } if ([self.requestsInRetrieval count]) { DSLog(@"%@ A masternode list is already in retrieval", self.logPrefix); return; @@ -83,15 +88,19 @@ - (void)fetchMasternodeListToRetrieve:(void (^)(NSData *listsToRetrieve))complet completion([self.retrievalBlockHash copy]); } -- (void)getRecent:(UInt256)blockHash { - self.retrievalBlockHash = uint256_data(blockHash); +- (void)getRecent:(NSData *)blockHash { + self.retrievalQueueMaxAmount = 1; + self.retrievalBlockHash = blockHash; [self dequeueMasternodeListRequest]; } - (void)cleanListsRetrievalQueue { + self.retrievalQueueMaxAmount = 0; self.retrievalBlockHash = nil; } - +- (BOOL)hasActiveQueue { + return self.retrievalBlockHash != nil; +} - (void)requestQuorumRotationInfo:(UInt256)previousBlockHash forBlockHash:(UInt256)blockHash { // TODO: optimize qrinfo request queue (up to 4 blocks simultaneously, so we'd make masternodeListsToRetrieve.count%4) // blockHeight % dkgInterval == activeSigningQuorumsCount + 11 + 8 @@ -100,7 +109,9 @@ - (void)requestQuorumRotationInfo:(UInt256)previousBlockHash forBlockHash:(UInt2 DSLog(@"%@ Request: already in retrieval: %@ .. %@", self.logPrefix, uint256_hex(previousBlockHash), uint256_hex(blockHash)); return; } - NSArray *baseBlockHashes = @[[NSData dataWithUInt256:previousBlockHash]]; + + NSArray *baseBlockHashes = self.chain.isMainnet ? @[@"989ba7a808cd8dda1755658a235b366c1496122485cdfd990800000000000000".hexToData, [NSData dataWithUInt256:previousBlockHash]] : @[[NSData dataWithUInt256:previousBlockHash]]; + DSGetQRInfoRequest *request = [DSGetQRInfoRequest requestWithBaseBlockHashes:baseBlockHashes blockHash:blockHash extraShare:YES]; uint32_t prev_h = [self.chain heightForBlockHash:previousBlockHash]; uint32_t h = [self.chain heightForBlockHash:blockHash]; diff --git a/DashSync/shared/Models/Network/DSPeer.m b/DashSync/shared/Models/Network/DSPeer.m index 49c06ebb..98a61c3b 100644 --- a/DashSync/shared/Models/Network/DSPeer.m +++ b/DashSync/shared/Models/Network/DSPeer.m @@ -936,9 +936,11 @@ - (void)acceptAddrMessage:(NSData *)message { NSTimeInterval timestamp = [message UInt32AtOffset:off]; uint64_t services = [message UInt64AtOffset:off + sizeof(uint32_t)]; UInt128 address = *(UInt128 *)((const uint8_t *)message.bytes + off + sizeof(uint32_t) + sizeof(uint64_t)); - uint16_t port = CFSwapInt16BigToHost(*(const uint16_t *)((const uint8_t *)message.bytes + off + - sizeof(uint32_t) + sizeof(uint64_t) + - sizeof(UInt128))); + + uint16_t rawPort; + memcpy(&rawPort, (const uint8_t *)message.bytes + off + sizeof(uint32_t) + sizeof(uint64_t) + sizeof(UInt128), sizeof(rawPort)); + uint16_t port = CFSwapInt16BigToHost(rawPort); + if (!(services & SERVICES_NODE_NETWORK)) continue; // skip peers that don't carry full blocks if (address.u64[0] != 0 || address.u32[2] != CFSwapInt32HostToBig(0xffff)) continue; // ignore IPv6 for now diff --git a/DashSync/shared/Models/Notifications/DSSyncState.h b/DashSync/shared/Models/Notifications/DSSyncState.h index 89882c36..083c18c6 100644 --- a/DashSync/shared/Models/Notifications/DSSyncState.h +++ b/DashSync/shared/Models/Notifications/DSSyncState.h @@ -39,15 +39,6 @@ typedef NS_ENUM(uint16_t, DSSyncStateKind) { - (void)updateWithSyncState:(DMNSyncState *)state; @end -//@interface DSPlatformSyncState : NSObject -// -//@property (nonatomic, assign) uint32_t retrievalQueueCount; -//@property (nonatomic, assign) uint32_t retrievalQueueMaxAmount; -//@property (nonatomic, assign) double storedCount; -//@property (nonatomic, assign) double stubCount; -// -//@end - @interface DSSyncState : NSObject diff --git a/Example/DashSync.xcodeproj/project.pbxproj b/Example/DashSync.xcodeproj/project.pbxproj index e6c62fe4..dded0e64 100644 --- a/Example/DashSync.xcodeproj/project.pbxproj +++ b/Example/DashSync.xcodeproj/project.pbxproj @@ -3268,13 +3268,18 @@ "$(inherited)", "COCOAPODS=1", "$(inherited)", - "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + "DASHCORE_QUORUM_VALIDATION=1", "DPP_STATE_TRANSITIONS=1", "PLATFORM_VALUE_STD=1", ); INFOPLIST_FILE = "DashSync/DashSync-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MODULE_NAME = ExampleApp; + OTHER_CFLAGS = ( + "-DDASHCORE_QUORUM_VALIDATION=1", + "-DDPP_STATE_TRANSITIONS=1", + "-DPLATFORM_VALUE_STD=1", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3305,7 +3310,6 @@ "-framework", "\"UIKit\"", "-ld64", - "-DPP_STATE_TRANSITIONS", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3329,13 +3333,18 @@ "$(inherited)", "COCOAPODS=1", "$(inherited)", - "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + "DASHCORE_QUORUM_VALIDATION=1", "DPP_STATE_TRANSITIONS=1", "PLATFORM_VALUE_STD=1", ); INFOPLIST_FILE = "DashSync/DashSync-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MODULE_NAME = ExampleApp; + OTHER_CFLAGS = ( + "-DDASHCORE_QUORUM_VALIDATION=1", + "-DDPP_STATE_TRANSITIONS=1", + "-DPLATFORM_VALUE_STD=1", + ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3366,7 +3375,6 @@ "-framework", "\"UIKit\"", "-ld64", - "-DPP_STATE_TRANSITIONS", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Example/Podfile b/Example/Podfile index 35938fd4..072cc0bd 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,6 +1,6 @@ def common_pods # pod 'DashSharedCore', :git => 'https://github.com/dashpay/dash-shared-core.git', :branch => 'fix/core-20-test-additional' - pod 'DashSharedCore', :git => 'https://github.com/dashpay/dash-shared-core.git', :commit => 'a506c8560642b50dbf4e37528b8885e41cedf0db' + pod 'DashSharedCore', :git => 'https://github.com/dashpay/dash-shared-core.git', :commit => '3cfd5b41a408eb75a7b77f9d757146b7dec3931f' # pod 'DashSharedCore', :path => '../../dash-shared-core-ferment/' pod 'DashSync', :path => '../' pod 'SDWebImage', '5.21.0' @@ -30,6 +30,10 @@ post_install do |installer| # fixes warnings about unsupported Deployment Target in Xcode 10 target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'DASHCORE_QUORUM_VALIDATION=1' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'DPP_STATE_TRANSITIONS=1' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PLATFORM_VALUE_STD=1' end end end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 87637c2b..c917da49 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -55,6 +55,6 @@ SPEC CHECKSUMS: KVO-MVVM: 4df3afd1f7ebcb69735458b85db59c4271ada7c6 SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 -PODFILE CHECKSUM: cd434cceb1ac899847b4f9d7693c7deb9f0af2ae +PODFILE CHECKSUM: 45f72cc47868b7f63c3c149723d1e03b70c88dec COCOAPODS: 1.15.2 From 6b4fa73168b158d6c11d9ddcac96af0429022d8f Mon Sep 17 00:00:00 2001 From: pankcuf Date: Tue, 22 Apr 2025 14:30:13 +0800 Subject: [PATCH 2/4] chore: update checkpoints, improve sync state notifications, create additional ui for sync progress indication, include coinjoin-related addresses for bloom filtering, add address registration test, refactor transaction data serialization --- DashSync/shared/Categories/BigIntTypes.h | 1 + .../shared/Categories/NSData/NSData+Dash.m | 2 +- DashSync/shared/DSDashSharedCore.m | 95 +---- DashSync/shared/DashSync.m | 12 +- .../Others/NSError+Platform.m | 2 + .../MasternodeLists/MNL_TESTNET_0_1220040.dat | Bin 0 -> 124601 bytes ...2227096.bin => mn_list_diff_0_2227096.dat} | Bin .../shared/Models/Chain/DSChain+Checkpoint.h | 1 + .../shared/Models/Chain/DSChain+Checkpoint.m | 19 + .../shared/Models/Chain/DSChain+Transaction.m | 4 +- DashSync/shared/Models/Chain/DSChain.m | 16 +- .../shared/Models/Chain/DSChainCheckpoints.h | 5 +- .../Models/CoinJoin/DSCoinJoinManager.h | 2 +- .../Models/CoinJoin/Utils/DSMasternodeGroup.m | 3 +- .../DSDerivationPathFactory.m | 2 + .../Derivation Paths/DSFundsDerivationPath.m | 6 +- .../Models/Derivation Paths/DSGapLimit.h | 5 + .../Identity/DSIdentity+ContactRequest.m | 8 +- .../Models/Identity/DSIdentity+Profile.m | 2 +- .../Models/Identity/DSIdentity+Username.h | 4 +- .../Models/Identity/DSIdentity+Username.m | 16 +- DashSync/shared/Models/Identity/DSIdentity.h | 3 - DashSync/shared/Models/Identity/DSIdentity.m | 72 ++-- .../Chain Managers/DSChainManager+Protected.h | 2 +- .../Managers/Chain Managers/DSChainManager.h | 1 - .../Managers/Chain Managers/DSChainManager.m | 136 +++---- .../Chain Managers/DSGovernanceSyncManager.m | 11 +- .../Chain Managers/DSIdentitiesManager.h | 2 - .../Chain Managers/DSIdentitiesManager.m | 160 +++++--- .../Managers/Chain Managers/DSKeyManager.m | 1 - .../DSMasternodeManager+Protected.h | 9 +- .../Chain Managers/DSMasternodeManager.h | 8 +- .../Chain Managers/DSMasternodeManager.m | 284 +++++++++---- .../Managers/Chain Managers/DSPeerManager.m | 65 +-- .../Managers/Chain Managers/DSSporkManager.m | 9 +- .../Chain Managers/DSTransactionManager.m | 79 ++-- .../Service Managers/DSInsightManager.m | 6 +- .../Masternode/DSMasternodeListDiffService.m | 80 ++-- .../DSMasternodeListService+Protected.h | 1 + .../Masternode/DSMasternodeListService.m | 16 + .../Masternode/DSQuorumRotationService.m | 24 +- .../Masternodes/DSGetMNListDiffRequest.m | 4 +- DashSync/shared/Models/Network/DSPeer.h | 3 +- DashSync/shared/Models/Network/DSPeer.m | 12 +- .../shared/Models/Notifications/DSSyncState.h | 93 ++++- .../shared/Models/Notifications/DSSyncState.m | 354 +++++++++++++---- .../Base/DSAssetLockTransaction.h | 1 + .../Base/DSAssetLockTransaction.m | 8 +- .../Base/DSAssetUnlockTransaction.m | 6 +- .../Models/Transactions/Base/DSTransaction.h | 13 +- .../Models/Transactions/Base/DSTransaction.m | 81 ++-- .../Coinbase/DSCoinbaseTransaction.m | 5 +- .../Transactions/DSTransactionFactory.m | 4 +- .../DSProviderRegistrationTransaction.m | 7 +- .../DSProviderUpdateRegistrarTransaction.m | 7 +- .../DSProviderUpdateRevocationTransaction.m | 7 +- .../DSProviderUpdateServiceTransaction.m | 7 +- .../Quorums/DSQuorumCommitmentTransaction.m | 7 +- DashSync/shared/Models/Wallet/DSAccount.h | 5 +- DashSync/shared/Models/Wallet/DSAccount.m | 133 +++++-- .../shared/Models/Wallet/DSWallet+Identity.m | 1 + .../shared/Models/Wallet/DSWallet+Tests.m | 2 + DashSync/shared/Models/Wallet/DSWallet.h | 4 +- DashSync/shared/Models/Wallet/DSWallet.m | 18 +- Example/DashSync/Base.lproj/Main.storyboard | 170 +++++--- .../DSAccountsDerivationPathsViewController.m | 76 ++-- Example/DashSync/DSAccountsViewController.m | 7 +- .../DSMasternodeListsViewController.m | 33 +- Example/DashSync/DSQuorumListViewController.m | 2 +- Example/DashSync/DSSyncViewController.m | 372 +++++++----------- Example/DashSync/DSWalletViewController.m | 4 +- Example/DashSync/Masternodes.storyboard | 149 ++++--- Example/Tests/DSBIP32Tests.m | 36 +- 73 files changed, 1730 insertions(+), 1075 deletions(-) create mode 100644 DashSync/shared/MasternodeLists/MNL_TESTNET_0_1220040.dat rename DashSync/shared/MasternodeLists/{mn_list_diff_0_2227096.bin => mn_list_diff_0_2227096.dat} (100%) diff --git a/DashSync/shared/Categories/BigIntTypes.h b/DashSync/shared/Categories/BigIntTypes.h index e49e4094..6f9179a7 100644 --- a/DashSync/shared/Categories/BigIntTypes.h +++ b/DashSync/shared/Categories/BigIntTypes.h @@ -34,6 +34,7 @@ #define NoTimeLog(format, ...) CFShow([NSString stringWithFormat:format, ##__VA_ARGS__]); #endif +#define FLAG_IS_SET(value, flag) ((value & flag) == flag) typedef union _UInt768 { uint8_t u8[768 / 8]; diff --git a/DashSync/shared/Categories/NSData/NSData+Dash.m b/DashSync/shared/Categories/NSData/NSData+Dash.m index 697ff7d5..00c5ec7e 100644 --- a/DashSync/shared/Categories/NSData/NSData+Dash.m +++ b/DashSync/shared/Categories/NSData/NSData+Dash.m @@ -993,7 +993,7 @@ UInt256 uInt256DivideLE(UInt256 a, UInt256 b) { while (shift >= 0) { if (uint256_supeq(num, div)) { num = uInt256SubtractLE(num, div); - r.u32[shift / 32] |= (1 << (shift & 31)); // set a bit of the result. + r.u32[shift / 32] |= (1U << (shift & 31)); // set a bit of the result. } div = uInt256ShiftRightLE(div, 1); // shift back. shift--; diff --git a/DashSync/shared/DSDashSharedCore.m b/DashSync/shared/DSDashSharedCore.m index 10565672..d19ab369 100644 --- a/DashSync/shared/DSDashSharedCore.m +++ b/DashSync/shared/DSDashSharedCore.m @@ -16,6 +16,7 @@ // #import "DSBlock.h" +#import "DSChain+Checkpoint.h" #import "DSChain+Params.h" #import "DSChain+Protected.h" #import "DSChain+Identity.h" @@ -46,31 +47,13 @@ #define GetBlockHeightByHash Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_u32 #define GetBlockHashByHeight Fn_ARGS_std_os_raw_c_void_u32_RTRN_Option_u8_32 -#define MerkleBlockByBlockHash Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_Result_ok_dash_spv_masternode_processor_common_block_MBlock_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError -#define LastMerkleBlockByBlockHashForPeer Fn_ARGS_std_os_raw_c_void_Arr_u8_32_std_os_raw_c_void_RTRN_Result_ok_dash_spv_masternode_processor_common_block_MBlock_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError - - -#define AddInsight Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_ - -#define HasPersistInRetrieval Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_bool -#define GetBlockHeightOrLastTerminal Fn_ARGS_std_os_raw_c_void_u32_RTRN_Result_ok_dash_spv_masternode_processor_common_block_Block_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError - -#define FnMaybeCLSignature Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_Result_ok_dashcore_bls_sig_utils_BLSSignature_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError #define DMaybeCLSignature Result_ok_dashcore_bls_sig_utils_BLSSignature_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError #define DMaybeCLSignatureCtor(ok, err) Result_ok_dashcore_bls_sig_utils_BLSSignature_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError_ctor(ok, err) -#define LoadMasternodeList Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_Result_ok_dashcore_sml_masternode_list_MasternodeList_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError -#define SaveMasternodeList Fn_ARGS_std_os_raw_c_void_Arr_u8_32_std_collections_Map_keys_u8_arr_32_values_dashcore_sml_masternode_list_entry_qualified_masternode_list_entry_QualifiedMasternodeListEntry_RTRN_Result_ok_bool_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError -#define LoadLLMQSnapshot Fn_ARGS_std_os_raw_c_void_Arr_u8_32_RTRN_Result_ok_dashcore_network_message_qrinfo_QuorumSnapshot_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError -#define SaveLLMQSnapshot Fn_ARGS_std_os_raw_c_void_Arr_u8_32_dashcore_network_message_qrinfo_QuorumSnapshot_RTRN_Result_ok_bool_err_dash_spv_masternode_processor_processing_core_provider_CoreProviderError - #define UpdateMasternodesAddressUsage Fn_ARGS_std_os_raw_c_void_Vec_dashcore_sml_masternode_list_entry_qualified_masternode_list_entry_QualifiedMasternodeListEntry_RTRN_ -#define IssueWithMasternodeList Fn_ARGS_std_os_raw_c_void_bool_std_os_raw_c_void_RTRN_ -#define NotifySyncState Fn_ARGS_std_os_raw_c_void_dash_spv_masternode_processor_models_sync_state_CacheState_RTRN_ - @interface DSDashSharedCore () -@property (nonatomic) DSChain *chain; +@property (nonatomic, strong) DSChain *chain; @property (nonatomic, assign) DashSPVCore *core; @property (nonatomic, strong) NSMutableDictionary *devnetSharedCoreDictionary; @@ -149,16 +132,6 @@ - (instancetype)initOnChain:(DSChain *)chain { UpdateMasternodesAddressUsage update_address_usage_of_masternodes = { .caller = &update_address_usage_of_masternodes_caller }; - IssueWithMasternodeList issue_with_masternode_list_from_peer = { - .caller = &issue_with_masternode_list_from_peer_caller - }; - FnMaybeCLSignature get_cl_signature_by_block_hash = { - .caller = &get_cl_signature_by_block_hash_caller, - .destructor = &get_cl_signature_by_block_hash_dtor - }; - NotifySyncState notify_sync_state = { - .caller = ¬ify_sync_state_caller, - }; NSArray *addresses = @[@"127.0.0.1"]; switch (chain.chainType->tag) { @@ -178,17 +151,8 @@ - (instancetype)initOnChain:(DSChain *)chain { break; } Vec_ *address_list = [NSArray ffi_to_vec:addresses]; - dash_spv_apple_bindings_DiffConfig *diff_config = NULL; - if ([chain isMainnet]) { - NSString *bundlePath = [[NSBundle bundleForClass:self.class] pathForResource:@"DashSync" ofType:@"bundle"]; - NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; - NSString *filePath = [bundle pathForResource:@"mn_list_diff_0_2227096" ofType:@"bin"]; - NSData *data = [NSData dataWithContentsOfFile:filePath]; - - diff_config = dash_spv_apple_bindings_DiffConfig_ctor(bytes_ctor(data), 2227096); - } - - self.core = dash_spv_apple_bindings_DashSPVCore_with_callbacks(chain.chainType, diff_config, address_list, get_data_contract, get_platform_activation_height, callback_signer, callback_can_sign, get_block_height_by_hash, get_block_hash_by_height, get_cl_signature_by_block_hash, update_address_usage_of_masternodes, issue_with_masternode_list_from_peer, notify_sync_state, context); + dash_spv_masternode_processor_processing_processor_DiffConfig *diff_config = [chain createDiffConfig]; + self.core = dash_spv_apple_bindings_DashSPVCore_with_callbacks(chain.chainType, diff_config, address_list, get_data_contract, get_platform_activation_height, callback_signer, callback_can_sign, get_block_height_by_hash, get_block_hash_by_height, update_address_usage_of_masternodes, context); return self; } @@ -269,61 +233,10 @@ void get_block_height_by_hash_dtor(uint32_t result) {} void get_block_hash_by_height_dtor(u256 *result) {} -DMaybeCLSignature *get_cl_signature_by_block_hash_caller(const void *context, u256 *block_hash) { - DSDashSharedCore *core = AS_OBJC(context); - UInt256 blockHash = u256_cast(block_hash); - UInt256 blockHashRev = uint256_reverse(blockHash); - u256_dtor(block_hash); - DSChainLock *chainLock = [core.chain.chainManager chainLockForBlockHash:blockHash]; - DSChainLock *chainLockRev = [core.chain.chainManager chainLockForBlockHash:blockHashRev]; - if (chainLock) { - DBLSSignature *bls_sig = DChainLockSignature(chainLock.lock); - DSLog(@"[SDK] get_cl_signature_by_block_hash_caller: %@ = %@", uint256_hex(blockHash), u768_hex(bls_sig->_0)); - return DMaybeCLSignatureCtor(bls_sig, NULL); - } else if (chainLockRev) { - DBLSSignature *bls_sig = DChainLockSignature(chainLockRev.lock); - DSLog(@"[SDK] get_cl_signature_by_block_hash_caller: %@ = %@", uint256_hex(blockHashRev), u768_hex(bls_sig->_0)); - return DMaybeCLSignatureCtor(bls_sig, NULL); - } else { - DSLog(@"[SDK] get_cl_signature_by_block_hash_caller: %@ = None", uint256_hex(blockHash)); - return DMaybeCLSignatureCtor(NULL, DCoreProviderErrorNullResultCtor(DSLocalizedChar(@"No clsig for block hash %@", nil, uint256_hex(blockHash)))); - } -} -void get_cl_signature_by_block_hash_dtor(DMaybeCLSignature *result) {} - - void update_address_usage_of_masternodes_caller(const void *context, DMasternodeEntryList *masternodes) { DSDashSharedCore *core = AS_OBJC(context); [core.chain updateAddressUsageOfSimplifiedMasternodeEntries:masternodes]; DMasternodeEntryListDtor(masternodes); } -void issue_with_masternode_list_from_peer_caller(const void *context, bool is_dip24, const void *peer_context) { - DSDashSharedCore *core = AS_OBJC(context); - DSPeer *peer = ((__bridge DSPeer *)(peer_context)); - [core.chain.masternodeManager issueWithMasternodeListFromPeer:peer]; -} - -void notify_sync_state_caller(const void *context, DMNSyncState *state) { - DSDashSharedCore *core = AS_OBJC(context); - DSMasternodeListSyncState *syncInfo = core.chain.chainManager.syncState.masternodeListSyncInfo; - @synchronized (syncInfo) { - [syncInfo updateWithSyncState:state]; - switch (state->tag) { - case DMNSyncStateQueueChanged: - DSLog(@"[%@] Masternode list queue updated: %lu/%lu", core.chain.name, state->queue_changed.count, state->queue_changed.max_amount); - break; - case DMNSyncStateStoreChanged: - DSLog(@"[%@] Masternode list store updated: %lu/%u", core.chain.name, state->store_changed.count, state->store_changed.last_block_height); - break; - case DMNSyncStateStubCount: - DSLog(@"[%@] Masternode list DB updated: %lu", core.chain.name, state->stub_count.count); - default: - break; - } - DMNSyncStateDtor(state); - [core.chain.chainManager notifySyncStateChanged]; - } -} - @end diff --git a/DashSync/shared/DashSync.m b/DashSync/shared/DashSync.m index 2521d2a1..ce64f21c 100644 --- a/DashSync/shared/DashSync.m +++ b/DashSync/shared/DashSync.m @@ -162,9 +162,11 @@ - (void)wipeBlockchainDataForChain:(DSChain *)chain inContext:(NSManagedObjectCo [DSDerivationPathEntity deleteDerivationPathsOnChainEntity:chainEntity]; [DSFriendRequestEntity deleteFriendRequestsOnChainEntity:chainEntity]; [chain wipeBlockchainInfoInContext:context]; - [chain.chainManager restartChainSyncStartHeight]; - [chain.chainManager restartTerminalSyncStartHeight]; - chain.chainManager.syncPhase = DSChainSyncPhase_InitialTerminalBlocks; + dispatch_async(chain.networkingQueue, ^{ + [chain.chainManager restartChainSyncStartHeight]; + [chain.chainManager restartTerminalSyncStartHeight]; + chain.chainManager.syncPhase = DSChainSyncPhase_InitialTerminalBlocks; + }); [DSBlockchainIdentityEntity deleteBlockchainIdentitiesOnChainEntity:chainEntity]; [DSDashpayUserEntity deleteContactsOnChainEntity:chainEntity]; // this must move after wipeBlockchainInfo where blockchain identities are removed [context ds_save]; @@ -192,7 +194,9 @@ - (void)wipeBlockchainNonTerminalDataForChain:(DSChain *)chain inContext:(NSMana [DSDerivationPathEntity deleteDerivationPathsOnChainEntity:chainEntity]; [DSFriendRequestEntity deleteFriendRequestsOnChainEntity:chainEntity]; [chain wipeBlockchainNonTerminalInfoInContext:context]; - [chain.chainManager restartChainSyncStartHeight]; + dispatch_async(chain.networkingQueue, ^{ + [chain.chainManager restartChainSyncStartHeight]; + }); [DSBlockchainIdentityEntity deleteBlockchainIdentitiesOnChainEntity:chainEntity]; [DSDashpayUserEntity deleteContactsOnChainEntity:chainEntity]; // this must move after wipeBlockchainInfo where blockchain identities are removed [context ds_save]; diff --git a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m index c2594c57..ffefde31 100644 --- a/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m +++ b/DashSync/shared/Libraries/AdvancedOperations/Others/NSError+Platform.m @@ -206,6 +206,8 @@ + (NSError *)ffi_from_processing_error:(DProcessingError *)ffi_ref { return [NSError errorWithCode:0 descriptionKey:DSLocalizedFormat(@"Decode Error: %@", nil, NSStringFromPtr(ffi_ref->decode_error))]; case dash_spv_masternode_processor_processing_processor_processing_error_ProcessingError_QuorumValidationError: return [NSError ffi_from_quorum_validation_error:ffi_ref->quorum_validation_error]; + case dash_spv_masternode_processor_processing_processor_processing_error_ProcessingError_SML: + return [NSError ffi_from_sml_error:ffi_ref->sml]; } } @end diff --git a/DashSync/shared/MasternodeLists/MNL_TESTNET_0_1220040.dat b/DashSync/shared/MasternodeLists/MNL_TESTNET_0_1220040.dat new file mode 100644 index 0000000000000000000000000000000000000000..8bea035897b8243cb85f1ee7c9c0670fa608259d GIT binary patch literal 124601 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 literal 0 HcmV?d00001 diff --git a/DashSync/shared/MasternodeLists/mn_list_diff_0_2227096.bin b/DashSync/shared/MasternodeLists/mn_list_diff_0_2227096.dat similarity index 100% rename from DashSync/shared/MasternodeLists/mn_list_diff_0_2227096.bin rename to DashSync/shared/MasternodeLists/mn_list_diff_0_2227096.dat diff --git a/DashSync/shared/Models/Chain/DSChain+Checkpoint.h b/DashSync/shared/Models/Chain/DSChain+Checkpoint.h index 095fddb5..e7b3c187 100644 --- a/DashSync/shared/Models/Chain/DSChain+Checkpoint.h +++ b/DashSync/shared/Models/Chain/DSChain+Checkpoint.h @@ -64,6 +64,7 @@ NS_ASSUME_NONNULL_BEGIN // MARK: Protected + (NSMutableArray *)createCheckpointsArrayFromCheckpoints:(checkpoint *)checkpoints count:(NSUInteger)checkpointCount; +- (dash_spv_masternode_processor_processing_processor_DiffConfig *_Nullable)createDiffConfig; @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Chain/DSChain+Checkpoint.m b/DashSync/shared/Models/Chain/DSChain+Checkpoint.m index 0a6b8c1d..e6e8ccb1 100644 --- a/DashSync/shared/Models/Chain/DSChain+Checkpoint.m +++ b/DashSync/shared/Models/Chain/DSChain+Checkpoint.m @@ -151,4 +151,23 @@ + (NSMutableArray *)createCheckpointsArrayFromCheckpoints:(checkpoint *)checkpoi return [checkpointMutableArray copy]; } +- (dash_spv_masternode_processor_processing_processor_DiffConfig *_Nullable)createDiffConfig { + dash_spv_masternode_processor_processing_processor_DiffConfig *diff_config = NULL; + if ([self isMainnet]) { + NSString *bundlePath = [[NSBundle bundleForClass:self.class] pathForResource:@"DashSync" ofType:@"bundle"]; + NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; + NSString *filePath = [bundle pathForResource:@"mn_list_diff_0_2227096" ofType:@"dat"]; + NSData *data = [NSData dataWithContentsOfFile:filePath]; + + diff_config = dash_spv_masternode_processor_processing_processor_DiffConfig_ctor(bytes_ctor(data), 2227096); + } else if ([self isTestnet]) { + NSString *bundlePath = [[NSBundle bundleForClass:self.class] pathForResource:@"DashSync" ofType:@"bundle"]; + NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; + NSString *filePath = [bundle pathForResource:@"MNL_TESTNET_0_1220040" ofType:@"dat"]; + NSData *data = [NSData dataWithContentsOfFile:filePath]; + diff_config = dash_spv_masternode_processor_processing_processor_DiffConfig_ctor(bytes_ctor(data), 1220040); + } + return diff_config; +} + @end diff --git a/DashSync/shared/Models/Chain/DSChain+Transaction.m b/DashSync/shared/Models/Chain/DSChain+Transaction.m index 00a7cb87..2db8df6d 100644 --- a/DashSync/shared/Models/Chain/DSChain+Transaction.m +++ b/DashSync/shared/Models/Chain/DSChain+Transaction.m @@ -92,8 +92,6 @@ - (uint64_t)amountSentByTransaction:(DSTransaction *)transaction { //Does the chain mat - (BOOL)transactionHasLocalReferences:(DSTransaction *)transaction { - if ([self firstAccountThatCanContainTransaction:transaction]) return TRUE; - //PROVIDERS if ([transaction isKindOfClass:[DSProviderRegistrationTransaction class]]) { DSProviderRegistrationTransaction *tx = (DSProviderRegistrationTransaction *)transaction; @@ -230,6 +228,7 @@ - (BOOL)registerProviderUpdateRevocationTransaction:(DSProviderUpdateRevocationT return NO; } } +// always from chain.networkingQueue - (BOOL)registerAssetLockTransaction:(DSAssetLockTransaction *)transaction saveImmediately:(BOOL)saveImmediately { DSAssetLockTransaction *assetLockTransaction = (DSAssetLockTransaction *)transaction; UInt160 creditBurnPublicKeyHash = assetLockTransaction.creditBurnPublicKeyHash; @@ -272,6 +271,7 @@ - (BOOL)registerAssetLockTransaction:(DSAssetLockTransaction *)transaction saveI return isNewIdentity; } +// always from chain.networkingQueue - (BOOL)registerSpecialTransaction:(DSTransaction *)transaction saveImmediately:(BOOL)saveImmediately { if ([transaction isKindOfClass:[DSProviderRegistrationTransaction class]]) { return [self registerProviderRegistrationTransaction:(DSProviderRegistrationTransaction *)transaction saveImmediately:saveImmediately]; diff --git a/DashSync/shared/Models/Chain/DSChain.m b/DashSync/shared/Models/Chain/DSChain.m index de416103..1f741e76 100644 --- a/DashSync/shared/Models/Chain/DSChain.m +++ b/DashSync/shared/Models/Chain/DSChain.m @@ -634,7 +634,7 @@ - (NSArray *)standaloneDerivationPaths { // every time a new wallet address is added, the bloom filter has to be rebuilt, and each address is only used for // one transaction, so here we generate some spare addresses to avoid rebuilding the filter each time a wallet // transaction is encountered during the blockchain download - [wallet registerAddressesWithProlongGapLimit]; + [wallet registerAddressesAtStage:DSGapLimitStage_Prolong]; [allAddressesArray addObjectsFromArray:[wallet allAddresses]]; } @@ -654,7 +654,7 @@ - (DSBloomFilter *)bloomFilterWithFalsePositiveRate:(double)falsePositiveRate wi // every time a new wallet address is added, the bloom filter has to be rebuilt, and each address is only used for // one transaction, so here we generate some spare addresses to avoid rebuilding the filter each time a wallet // transaction is encountered during the blockchain download - [wallet registerAddressesWithInitialGapLimit]; + [wallet registerAddressesAtStage:DSGapLimitStage_Initial]; [allUTXOs addObjectsFromArray:wallet.unspentOutputs]; [allAddresses addObjectsFromArray:[wallet allAddresses]]; } @@ -1063,7 +1063,8 @@ - (BOOL)addMinedFullBlock:(DSFullBlock *)block { return TRUE; } -//TRUE if it was added to the end of the chain +// always from chain.networkingQueue +// TRUE if it was added to the end of the chain - (BOOL)addBlock:(DSBlock *)block receivedAsHeader:(BOOL)isHeaderOnly fromPeer:(DSPeer *)peer { NSString *prefix = [NSString stringWithFormat:@"[%@: %@:%d]", self.name, peer.host ? peer.host : @"TEST", peer.port]; if (peer && !self.chainManager.syncPhase) { @@ -1256,7 +1257,6 @@ - (BOOL)addBlock:(DSBlock *)block receivedAsHeader:(BOOL)isHeaderOnly fromPeer:( self.lastTerminalBlock = block; self.chainManager.syncState.estimatedBlockHeight = self.estimatedBlockHeight; self.chainManager.syncState.lastTerminalBlockHeight = block.height; - @synchronized(peer) { if (peer) { peer.currentBlockHeight = h; //might be download peer instead @@ -1454,7 +1454,7 @@ - (BOOL)addBlock:(DSBlock *)block receivedAsHeader:(BOOL)isHeaderOnly fromPeer:( if (((blockPosition & DSBlockPosition_Terminal) && block.height > self.estimatedBlockHeight) || ((blockPosition & DSBlockPosition_Sync) && block.height >= self.lastTerminalBlockHeight)) { @synchronized (self) { _bestEstimatedBlockHeight = block.height; - self.chainManager.syncState.estimatedBlockHeight = _bestEstimatedBlockHeight; + self.chainManager.syncState.estimatedBlockHeight = self->_bestEstimatedBlockHeight; } if (peer && (blockPosition & DSBlockPosition_Sync) && !savedBlockLocators) { [self saveBlockLocators]; @@ -1579,6 +1579,7 @@ - (void)clearOrphans { // MARK: Chain Locks +// always from chain.networkingQueue - (BOOL)addChainLock:(DSChainLock *)chainLock { DSBlock *terminalBlock = self.mTerminalBlocks[uint256_obj(chainLock.blockHashData.UInt256)]; [terminalBlock setChainLockedWithChainLock:chainLock]; @@ -1730,6 +1731,7 @@ - (NSTimeInterval)lastSyncBlockTimestamp { return _lastSyncBlock ? _lastSyncBlock.timestamp : (self.lastPersistedChainSyncBlockTimestamp ? self.lastPersistedChainSyncBlockTimestamp : self.lastSyncBlock.timestamp); } +// this is thread-unsafe, it's preferable to use NSNotificationCenter's DSChainManagerSyncStateDidChangeNotification to get progress in main thread - (uint32_t)lastSyncBlockHeight { @synchronized (_lastSyncBlock) { if (_lastSyncBlock) { @@ -1750,6 +1752,7 @@ - (UInt256)lastSyncBlockChainWork { return _lastSyncBlock ? _lastSyncBlock.chainWork : (uint256_is_not_zero(self.lastPersistedChainSyncBlockChainWork) ? self.lastPersistedChainSyncBlockChainWork : self.lastSyncBlock.chainWork); } +// this is thread-unsafe, it's preferable to use NSNotificationCenter's DSChainManagerSyncStateDidChangeNotification to get progress in main thread - (uint32_t)lastTerminalBlockHeight { return self.lastTerminalBlock.height; } @@ -1934,6 +1937,7 @@ - (NSUInteger)countEstimatedBlockHeightAnnouncers { return [announcers count]; } +// always from chain.networkingQueue - (void)setEstimatedBlockHeight:(uint32_t)estimatedBlockHeight fromPeer:(DSPeer *)peer thresholdPeerCount:(uint32_t)thresholdPeerCount { uint32_t oldEstimatedBlockHeight = self.estimatedBlockHeight; @@ -1968,6 +1972,7 @@ - (void)setEstimatedBlockHeight:(uint32_t)estimatedBlockHeight fromPeer:(DSPeer } } +// always from chain.networkingQueue - (void)removeEstimatedBlockHeightOfPeer:(DSPeer *)peer { for (NSNumber *height in [self.estimatedBlockHeights copy]) { NSMutableArray *announcers = self.estimatedBlockHeights[height]; @@ -2144,7 +2149,6 @@ - (void)saveBlockLocators { for (DSTransactionHashEntity *e in [DSTransactionHashEntity objectsInContext:self.chainManagedObjectContext matching:@"txHash in %@", [self.transactionHashHeights allKeys]]) { e.blockHeight = [self.transactionHashHeights[e.txHash] intValue]; e.timestamp = [self.transactionHashTimestamps[e.txHash] intValue]; - ; [entities addObject:e]; } for (DSTransactionHashEntity *e in entities) { diff --git a/DashSync/shared/Models/Chain/DSChainCheckpoints.h b/DashSync/shared/Models/Chain/DSChainCheckpoints.h index a2f49d45..ad09286c 100644 --- a/DashSync/shared/Models/Chain/DSChainCheckpoints.h +++ b/DashSync/shared/Models/Chain/DSChainCheckpoints.h @@ -24,7 +24,7 @@ typedef const struct checkpoint { const char *merkleRoot; const char *chainWork; } checkpoint; - +// height | hash | time | bits | path to MNLdiff | merkle root | chainwork static checkpoint testnet_checkpoint_array[] = { {0, "00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c", 1390666206, 0x1e0ffff0u, "", "", "0000000000000000000000000000000000000000000000000000000000100010"}, {1500, "000002d7a07979a4d6b24efdda0bbf6e3c03a59c22765a0128a5c53b3888aa28", 1423460945, 0x1e03ffffu, "", "", "000000000000000000000000000000000000000000000000000000009f403ba7"}, @@ -45,8 +45,10 @@ static checkpoint testnet_checkpoint_array[] = { {480000, "000001210c081f763d18db332b38ec1ac14fac62170a0d1a2028cabe8cecc799", 1618235036, 0x1e01eec7u, "", "d9fae96cce9bf0edcf9ece1b7894e0356165c0a5dcdc6f2e0784461c4168cbec", "000000000000000000000000000000000000000000000000022f14bf215f8016"}, {530000, "0000060db4b6bdb17f0617d15637bdf0f18ad738ccb438ee2cd000fef11c7130", 1625277934, 0x1e0fffffu, "MNT530000__70228", "7a6a78a22df2d9dc8c44afd48dfe4a60f75428f5e6004cf4cdf82e4f81a0a68b", "000000000000000000000000000000000000000000000000022f1524ad0dacd3"}, {760000, "000000b80d3010bb62b309aec9a7dd748777cc5e2640a26b1981cb3c61c66211", 1657592023, 0x1e01f865u, "", "8302c05bdca60e7dfcac26cebea8d797bda1b87111cf3a3dc33050c43f2abfbe", "000000000000000000000000000000000000000000000000027baba1fe003e84"}, + {1220040, "0000004d2801fdc42eb915df814bf9718bdb61819a9c47cb31fa3678ecf54818", 1742907841, 0x1e00eb3bu, "MNL_TESTNET_0_1220040", "a4d9cee58d0554bfa522dd6122141c0fe828c3e4741c86ca59a81678101fed60", "000000000000000000000000000000000000000000000000033ccd994d0f0a4e"}, }; + // blockchain checkpoints - these are also used as starting points for partial chain downloads, so they need to be at // difficulty transition boundaries in order to verify the block difficulty at the immediately following transition static checkpoint mainnet_checkpoint_array[] = { @@ -116,4 +118,5 @@ static checkpoint mainnet_checkpoint_array[] = { {1700000, "000000000000001d7579a371e782fd9c4480f626a62b916fa4eb97e16a49043a", 1657142113, 0x1927e30eu, "", "dafe57cefc3bc265dfe8416e2f2e3a22af268fd587a48f36affd404bec738305", "000000000000000000000000000000000000000000007562df93a26b81386288"}, {1720000, "000000000000001ef1f8a3d33bbe304c1d12f59f2c8aa989099dc215fd10903e", 1660295895, 0x19362176u, "", "67c6348c35bc42aa4cabd25e29560f5d22c6a9fba274bf0c52fe73021d0e8d5e", "000000000000000000000000000000000000000000007715a9ae4dd7ff1d3902"}, {1720000, "000000000000001ef1f8a3d33bbe304c1d12f59f2c8aa989099dc215fd10903e", 1660295895, 0x19362176u, "ML1720000__70218", "67c6348c35bc42aa4cabd25e29560f5d22c6a9fba274bf0c52fe73021d0e8d5e", "000000000000000000000000000000000000000000007715a9ae4dd7ff1d3902"}, + {2227096, "000000000000000899fdcd85241296146c365b238a655517da8dcd08a8a79b98", 1740273743, 0x19287724u, "mn_list_diff_0_2227096", "298585a781111ad060e5e99669893a3999b52b1d8125be0297e7efc6e62ff231", "00000000000000000000000000000000000000000000a491879d910661346c7b"}, }; diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h index 84363324..e328b171 100644 --- a/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h @@ -58,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN @interface DSCoinJoinManager : NSObject -@property (nonatomic, assign, nullable) DSChain *chain; +@property (nonatomic, strong, nullable) DSChain *chain; @property (nonatomic, strong, nullable) DSMasternodeGroup *masternodeGroup; @property (nonatomic, assign, nullable) DCoinJoinClientOptions *options; @property (nonatomic, nullable, weak) id managerDelegate; diff --git a/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m index c431ee2f..7b8d0f00 100644 --- a/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m +++ b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m @@ -456,8 +456,7 @@ - (BOOL)connectTo:(DSPeer *)peer { transactionDelegate:(id) chainManager.transactionManager governanceDelegate:(id) chainManager.governanceSyncManager sporkDelegate:(id) chainManager.sporkManager - masternodeDelegate:chainManager.masternodeManager - queue:self.networkingQueue]; + masternodeDelegate:chainManager.masternodeManager]; peer.earliestKeyTime = self.chain.earliestWalletCreationTime;; @synchronized (self.peersLock) { diff --git a/DashSync/shared/Models/Derivation Paths/DSDerivationPathFactory.m b/DashSync/shared/Models/Derivation Paths/DSDerivationPathFactory.m index 233933a7..dcb0864c 100644 --- a/DashSync/shared/Models/Derivation Paths/DSDerivationPathFactory.m +++ b/DashSync/shared/Models/Derivation Paths/DSDerivationPathFactory.m @@ -279,6 +279,8 @@ - (DSAuthenticationKeysDerivationPath *)identityECDSAKeysDerivationPathForWallet [mArray addObject:fundsDerivationPath]; } } + if (account.coinJoinDerivationPath && ![account.coinJoinDerivationPath hasExtendedPublicKey]) + [mArray addObject:account.coinJoinDerivationPath]; } return [mArray copy]; diff --git a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m index 5e26fd91..b6c074cc 100644 --- a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m +++ b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m @@ -135,7 +135,8 @@ - (void)loadAddresses { // MARK: - Derivation Path Addresses - (BOOL)registerTransactionAddress:(NSString *_Nonnull)address { - if ([self containsAddress:address]) { + BOOL contains = [self containsAddress:address]; + if (contains) { if (![self.mUsedAddresses containsObject:address]) { [self.mUsedAddresses addObject:address]; DSGapLimit *gapLimit = [self.allChangeAddresses containsObject:address] @@ -143,9 +144,8 @@ - (BOOL)registerTransactionAddress:(NSString *_Nonnull)address { : [DSGapLimitFunds external:SEQUENCE_GAP_LIMIT_EXTERNAL]; [self registerAddressesWithSettings:gapLimit]; } - return TRUE; } - return FALSE; + return contains; } // Wallets are composed of chains of addresses. Each chain is traversed until a gap of a certain number of addresses is diff --git a/DashSync/shared/Models/Derivation Paths/DSGapLimit.h b/DashSync/shared/Models/Derivation Paths/DSGapLimit.h index b8773800..feb391a3 100644 --- a/DashSync/shared/Models/Derivation Paths/DSGapLimit.h +++ b/DashSync/shared/Models/Derivation Paths/DSGapLimit.h @@ -25,6 +25,11 @@ typedef NS_ENUM(NSUInteger, DSGapLimitFundsDirection) { DSGapLimitFundsDirection_Both = DSGapLimitFundsDirection_Internal | DSGapLimitFundsDirection_External, }; +typedef NS_ENUM(NSUInteger, DSGapLimitStage) { + DSGapLimitStage_Initial = 1, + DSGapLimitStage_Prolong = 2, +}; + @interface DSGapLimit : NSObject @property (readwrite, nonatomic, assign) uintptr_t gapLimit; diff --git a/DashSync/shared/Models/Identity/DSIdentity+ContactRequest.m b/DashSync/shared/Models/Identity/DSIdentity+ContactRequest.m index 6934e219..05125f73 100644 --- a/DashSync/shared/Models/Identity/DSIdentity+ContactRequest.m +++ b/DashSync/shared/Models/Identity/DSIdentity+ContactRequest.m @@ -92,7 +92,7 @@ - (void)fetchIncomingContactRequestsInContext:(NSManagedObjectContext *)context withCompletion:(void (^)(BOOL success, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: fetch incoming contact requests: (startAfter: %@)", self.logPrefix, startAfter ? startAfter.hexString : @"NULL"]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Fetch Incoming Contact Requests: (after: %@)", self.logPrefix, startAfter ? startAfter.hexString : @"NULL"]; DPContract *dashpayContract = [DSDashPlatform sharedInstanceForChain:self.chain].dashPayContract; if (dashpayContract.contractState != DPContractState_Registered) { [debugInfo appendFormat:@" : ERROR: DashPay Contract State: %lu", dashpayContract.contractState]; @@ -207,7 +207,7 @@ - (void)fetchOutgoingContactRequestsInContext:(NSManagedObjectContext *)context startAfter:(NSData*_Nullable)startAfter withCompletion:(void (^)(BOOL success, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: fetch outgoing contact requests: (startAfter: %@)", self.logPrefix, startAfter ? startAfter.hexString : @"NULL"]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Fetch Outgoing Contact Requests: (after: %@)", self.logPrefix, startAfter ? startAfter.hexString : @"NULL"]; DPContract *dashpayContract = [DSDashPlatform sharedInstanceForChain:self.chain].dashPayContract; if (dashpayContract.contractState != DPContractState_Registered) { [debugInfo appendFormat:@" : ERROR: DashPay Contract State: %lu", dashpayContract.contractState]; @@ -291,7 +291,7 @@ - (void)fetchOutgoingContactRequestsInContext:(NSManagedObjectContext *)context [self.platformContext performBlockAndWait:^{ self.lastCheckedOutgoingContactsTimestamp = [[NSDate date] timeIntervalSince1970]; }]; - [debugInfo appendFormat:@" : OK: %u: %@", succeeded, rErrors]; + [debugInfo appendFormat:@" : %@: %@", succeeded ? @"OK" : @"No", [rErrors count] ? rErrors.description : @""]; DSLog(@"%@", debugInfo); __block NSData * hasMoreStartAfter = nil; if (documents->count > 0) { @@ -530,7 +530,7 @@ - (void)handleOutgoingRequests:(NSArray *)outgoingRequests atTimestamp:request->created_at inContext:context]; } else { - succeeded = FALSE; + succeeded = NO; [errors addObjectsFromArray:networkErrors]; } dispatch_group_leave(dispatchGroup); diff --git a/DashSync/shared/Models/Identity/DSIdentity+Profile.m b/DashSync/shared/Models/Identity/DSIdentity+Profile.m index a5a18cef..adc9eee8 100644 --- a/DashSync/shared/Models/Identity/DSIdentity+Profile.m +++ b/DashSync/shared/Models/Identity/DSIdentity+Profile.m @@ -384,7 +384,7 @@ - (void)updateDashpayProfileWithAvatarURLString:(NSString *)avatarURLString } - (void)signAndPublishProfileWithCompletion:(void (^)(BOOL success, BOOL cancelled, NSError *error))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: Sign and publish profile", self.logPrefix]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Sign & Publish Profile", self.logPrefix]; DSLog(@"%@", debugInfo); NSManagedObjectContext *context = self.platformContext; __block uint32_t profileDocumentRevision; diff --git a/DashSync/shared/Models/Identity/DSIdentity+Username.h b/DashSync/shared/Models/Identity/DSIdentity+Username.h index b295efe4..590fab4b 100644 --- a/DashSync/shared/Models/Identity/DSIdentity+Username.h +++ b/DashSync/shared/Models/Identity/DSIdentity+Username.h @@ -43,9 +43,9 @@ NS_ASSUME_NONNULL_BEGIN status:(DUsernameStatus *)status save:(BOOL)save registerOnNetwork:(BOOL)registerOnNetwork; -- (DUsernameStatus *)statusOfUsername:(NSString *)username +- (DUsernameStatus *_Nullable)statusOfUsername:(NSString *)username inDomain:(NSString *)domain; -- (DUsernameStatus *)statusOfDashpayUsername:(NSString *)username; +- (DUsernameStatus *_Nullable)statusOfDashpayUsername:(NSString *)username; - (void)registerUsernamesWithCompletion:(void (^_Nullable)(BOOL success, NSArray *errors))completion; - (void)fetchUsernamesInContext:(NSManagedObjectContext *)context diff --git a/DashSync/shared/Models/Identity/DSIdentity+Username.m b/DashSync/shared/Models/Identity/DSIdentity+Username.m index 58849b0c..df01ee8d 100644 --- a/DashSync/shared/Models/Identity/DSIdentity+Username.m +++ b/DashSync/shared/Models/Identity/DSIdentity+Username.m @@ -130,13 +130,13 @@ - (void)addUsername:(NSString *)username }); } -- (DUsernameStatus *)statusOfUsername:(NSString *)username - inDomain:(NSString *)domain { - return dash_spv_platform_identity_model_IdentityModel_status_of_username(self.identity_model, DChar(username), DChar(domain)); +- (DUsernameStatus *_Nullable)statusOfUsername:(NSString *)username + inDomain:(NSString *)domain { + return username ? dash_spv_platform_identity_model_IdentityModel_status_of_username(self.identity_model, DChar(username), DChar(domain)) : nil; } -- (DUsernameStatus *)statusOfDashpayUsername:(NSString *)username { - return dash_spv_platform_identity_model_IdentityModel_status_of_dashpay_username(self.identity_model, DChar(username)); +- (DUsernameStatus *_Nullable)statusOfDashpayUsername:(NSString *)username { + return username ? dash_spv_platform_identity_model_IdentityModel_status_of_dashpay_username(self.identity_model, DChar(username)) : nil; } - (DUsernameStatus *)statusOfUsernameFullPath:(NSString *)usernameFullPath { @@ -339,7 +339,7 @@ - (void)saveUsername:(NSString *)username - (void)fetchUsernamesInContext:(NSManagedObjectContext *)context withCompletion:(void (^)(BOOL success, NSError *error))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: fetchUsernamesInContext", self.logPrefix]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Fetch Usernames", self.logPrefix]; DSLog(@"%@", debugInfo); DPContract *contract = [DSDashPlatform sharedInstanceForChain:self.chain].dpnsContract; if (contract.contractState != DPContractState_Registered) { @@ -412,7 +412,7 @@ - (NSError *_Nullable)registerUsernameWithSaltedDomainHash:(NSData *)saltedDomai andEntropyData:(NSData *)entropyData withIdentityPublicKey:(DIdentityPublicKey *)identity_public_key withPrivateKey:(DMaybeOpaqueKey *)maybe_private_key { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"[%@]: registerUsernameWithSaltedDomainHash [%@]", self.logPrefix, saltedDomainHashData.hexString]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"[%@] Register Username With SaltedDomainHash [%@]", self.logPrefix, saltedDomainHashData.hexString]; DDocumentResult *result = dash_spv_platform_PlatformSDK_register_preordered_salted_domain_hash_for_username_full_path(self.chain.sharedRuntime, self.chain.sharedPlatformObj, contract, u256_ctor_u(self.uniqueID), identity_public_key, bytes_ctor(saltedDomainHashData), u256_ctor(entropyData)); if (result->error) { NSError *error = [NSError ffi_from_platform_error:result->error]; @@ -440,7 +440,7 @@ - (void)registerUsernamesAtStage:(DUsernameStatus *)status inContext:(NSManagedObjectContext *)context completion:(void (^_Nullable)(BOOL success, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"[%@]: registerUsernamesAtStage [%lu]", self.logPrefix, (unsigned long) status]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Register Usernames At Stage [%hhu]", self.logPrefix, DUsernameStatusIndex(status)]; DSLog(@"%@", debugInfo); Vec_String *result = dash_spv_platform_identity_model_IdentityModel_username_full_paths_with_status(self.identity_model, status); NSArray *usernameFullPaths = [NSArray ffi_from_vec_of_string:result]; diff --git a/DashSync/shared/Models/Identity/DSIdentity.h b/DashSync/shared/Models/Identity/DSIdentity.h index 7977e18e..5bbdddd6 100644 --- a/DashSync/shared/Models/Identity/DSIdentity.h +++ b/DashSync/shared/Models/Identity/DSIdentity.h @@ -12,9 +12,6 @@ NS_ASSUME_NONNULL_BEGIN -#define FLAG_IS_SET(value, flag) ((value & flag) == flag) - - @class DSWallet, DSAccount, DSChain, DSDashpayUserEntity, DSPotentialOneWayFriendship, DSTransaction, DSFriendRequestEntity, DSPotentialContact, DSAssetLockTransaction, DSTransientDashpayUser, DSInvitation, DSAuthenticationKeysDerivationPath, UIImage; typedef NS_ENUM(NSUInteger, DSIdentityRegistrationStep) diff --git a/DashSync/shared/Models/Identity/DSIdentity.m b/DashSync/shared/Models/Identity/DSIdentity.m index ceeb25bd..8a98eb50 100644 --- a/DashSync/shared/Models/Identity/DSIdentity.m +++ b/DashSync/shared/Models/Identity/DSIdentity.m @@ -295,20 +295,20 @@ - (void)applyIdentityEntity:(DSBlockchainIdentityEntity *)identityEntity { if (self.isLocal || self.isOutgoingInvitation) { if (identityEntity.registrationFundingTransaction) { self.registrationAssetLockTransactionHash = identityEntity.registrationFundingTransaction.transactionHash.txHash.UInt256; - DSLog(@"%@: AssetLockTX: Entity Attached: txHash: %@: entity: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash), identityEntity.registrationFundingTransaction); + DSLog(@"%@ AssetLockTX: Entity Attached: txHash: %@: entity: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash), identityEntity.registrationFundingTransaction); } else { NSData *transactionHashData = uint256_data(uint256_reverse(self.lockedOutpoint.hash)); - DSLog(@"%@: AssetLockTX: Load: lockedOutpoint: %@: %lu %@", self.logPrefix, uint256_hex(self.lockedOutpoint.hash), self.lockedOutpoint.n, transactionHashData.hexString); + DSLog(@"%@ AssetLockTX: Load: lockedOutpoint: %@: %lu %@", self.logPrefix, uint256_hex(self.lockedOutpoint.hash), self.lockedOutpoint.n, transactionHashData.hexString); DSAssetLockTransactionEntity *assetLockEntity = [DSAssetLockTransactionEntity anyObjectInContext:identityEntity.managedObjectContext matching:@"transactionHash.txHash == %@", transactionHashData]; if (assetLockEntity) { self.registrationAssetLockTransactionHash = assetLockEntity.transactionHash.txHash.UInt256; - DSLog(@"%@: AssetLockTX: Entity Found for txHash: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash)); + DSLog(@"%@ AssetLockTX: Entity Found for txHash: %@", self.logPrefix, uint256_hex(self.registrationAssetLockTransactionHash)); DSAssetLockTransaction *registrationAssetLockTransaction = (DSAssetLockTransaction *)[assetLockEntity transactionForChain:self.chain]; BOOL correctIndex = self.isOutgoingInvitation ? [registrationAssetLockTransaction checkInvitationDerivationPathIndexForWallet:self.wallet isIndex:self.index] : [registrationAssetLockTransaction checkDerivationPathIndexForWallet:self.wallet isIndex:self.index]; if (!correctIndex) { - DSLog(@"%@: AssetLockTX: IncorrectIndex %u (%@)", self.logPrefix, self.index, registrationAssetLockTransaction.toData.hexString); + DSLog(@"%@ AssetLockTX: IncorrectIndex %u (%@)", self.logPrefix, self.index, registrationAssetLockTransaction.toData.hexString); //NSAssert(FALSE, @"We should implement this"); } } @@ -348,7 +348,7 @@ - (instancetype)initAtIndex:(uint32_t)index self.lockedOutpoint = lockedOutpoint; self.uniqueID = [dsutxo_data(lockedOutpoint) SHA256_2]; - DSLog(@"%@: initAtIndex: %u lockedOutpoint: %@: %lu", self.logPrefix, index, uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); + DSLog(@"%@ initAtIndex: %u lockedOutpoint: %@: %lu", self.logPrefix, index, uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); return self; } @@ -583,7 +583,7 @@ - (void)registerOnNetwork:(DSIdentityRegistrationStep)steps pinPrompt:(NSString *)prompt stepCompletion:(void (^_Nullable)(DSIdentityRegistrationStep stepCompleted))stepCompletion completion:(void (^_Nullable)(DSIdentityRegistrationStep stepsCompleted, NSArray *errors))completion { - DSLog(@"%@: registerOnNetwork: %@", self.logPrefix, DSRegistrationStepsDescription(steps)); + DSLog(@"%@ Register On Network: %@", self.logPrefix, DSRegistrationStepsDescription(steps)); __block DSIdentityRegistrationStep stepsCompleted = DSIdentityRegistrationStep_None; if (![self hasIdentityExtendedPublicKeys]) { if (completion) dispatch_async(dispatch_get_main_queue(), ^{ completion(stepsCompleted, @[ERROR_REGISTER_KEYS_BEFORE_IDENTITY]); }); @@ -724,7 +724,7 @@ - (void)registerInWalletForAssetLockTransaction:(DSAssetLockTransaction *)transa self.registrationAssetLockTransactionHash = transaction.txHash; DSUTXO lockedOutpoint = transaction.lockedOutpoint; UInt256 creditBurnIdentityIdentifier = transaction.creditBurnIdentityIdentifier; - DSLog(@"%@: registerInWalletForAssetLockTransaction: txHash: %@: creditBurnIdentityID: %@, creditBurnPublicKeyHash: %@, lockedOutpoint: %@: %lu", self.logPrefix, uint256_hex(transaction.txHash), uint256_hex(creditBurnIdentityIdentifier), uint160_hex(transaction.creditBurnPublicKeyHash), uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); + DSLog(@"%@ Register In Wallet (AssetLockTx Register): txHash: %@: creditBurnIdentityID: %@, creditBurnPublicKeyHash: %@, lockedOutpoint: %@: %lu", self.logPrefix, uint256_hex(transaction.txHash), uint256_hex(creditBurnIdentityIdentifier), uint160_hex(transaction.creditBurnPublicKeyHash), uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); self.lockedOutpoint = lockedOutpoint; [self registerInWalletForIdentityUniqueId:creditBurnIdentityIdentifier]; //we need to also set the address of the funding transaction to being used so future identities past the initial gap limit are found @@ -738,7 +738,7 @@ - (void)registerInWalletForAssetLockTopupTransaction:(DSAssetLockTransaction *)t DSUTXO lockedOutpoint = transaction.lockedOutpoint; UInt256 creditBurnIdentityIdentifier = transaction.creditBurnIdentityIdentifier; - DSLog(@"%@: registerInWalletForAssetLockTopupTransaction: txHash: %@: creditBurnIdentityID: %@, creditBurnPublicKeyHash: %@, lockedOutpoint: %@: %lu", self.logPrefix, uint256_hex(transaction.txHash), uint256_hex(creditBurnIdentityIdentifier), uint160_hex(transaction.creditBurnPublicKeyHash), uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); + DSLog(@"%@ Register In Wallet (AssetLockTx TopUp): txHash: %@: creditBurnIdentityID: %@, creditBurnPublicKeyHash: %@, lockedOutpoint: %@: %lu", self.logPrefix, uint256_hex(transaction.txHash), uint256_hex(creditBurnIdentityIdentifier), uint160_hex(transaction.creditBurnPublicKeyHash), uint256_hex(lockedOutpoint.hash), lockedOutpoint.n); // self.lockedOutpoint = lockedOutpoint; [self registerInWalletForIdentityUniqueId:creditBurnIdentityIdentifier]; //we need to also set the address of the funding transaction to being used so future identities past the initial gap limit are found @@ -1350,7 +1350,7 @@ - (void)registerIdentityWithProof:(DAssetLockProof *)proof public_key:(DIdentityPublicKey *)public_key atIndex:(uint32_t)index completion:(void (^)(BOOL, NSError *))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: Register Identity using public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalRegistrationFundingPrivateKey->ok]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Register Identity using public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalRegistrationFundingPrivateKey->ok]; DSLog(@"%@", debugInfo); DMaybeStateTransitionProofResult *state_transition_result = dash_spv_platform_PlatformSDK_identity_register_using_public_key_at_index(self.chain.sharedRuntime, self.chain.sharedPlatformObj, public_key, index, proof, self.internalRegistrationFundingPrivateKey->ok); if (state_transition_result->error) { @@ -1405,7 +1405,7 @@ - (void)registerIdentityWithProof2:(DAssetLockProof *)proof public_key:(DIdentityPublicKey *)public_key atIndex:(uint32_t)index completion:(void (^)(BOOL, NSError *_Nullable))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: Register Identity using public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalRegistrationFundingPrivateKey->ok]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ Register Identity (public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalRegistrationFundingPrivateKey->ok]; DSLog(@"%@", debugInfo); Result_ok_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error *state_transition_result = dash_spv_platform_PlatformSDK_identity_register_using_public_key_at_index2(self.chain.sharedRuntime, self.chain.sharedPlatformObj, public_key, index, proof, self.internalRegistrationFundingPrivateKey->ok); if (state_transition_result->error) { @@ -1438,7 +1438,7 @@ - (void)topupIdentityWithProof:(DAssetLockProof *)proof public_key:(DIdentityPublicKey *)public_key atIndex:(uint32_t)index completion:(void (^)(BOOL, NSError *_Nullable))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: TopUp Identity using public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalTopupFundingPrivateKey->ok]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ TopUp Identity using public key (%u: %p) at %u with private key: %p", self.logPrefix, public_key->tag, public_key, index, self.internalTopupFundingPrivateKey->ok]; DSLog(@"%@", debugInfo); u256 *identity_id = u256_ctor_u(self.uniqueID); DMaybeStateTransitionProofResult *state_transition_result = dash_spv_platform_PlatformSDK_identity_topup(self.chain.sharedRuntime, self.chain.sharedPlatformObj, identity_id, proof, self.internalTopupFundingPrivateKey->ok); @@ -1494,7 +1494,7 @@ - (void)createAndPublishTopUpTransitionForAmount:(uint64_t)amount fundedByAccount:(DSAccount *)fundingAccount pinPrompt:(NSString *)prompt withCompletion:(void (^)(BOOL, NSError *_Nullable))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: CREATE AND PUBLISH IDENTITY TOPUP TRANSITION", self.logPrefix]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ CREATE AND PUBLISH IDENTITY TOPUP TRANSITION", self.logPrefix]; DSLog(@"%@", debugInfo); DSAssetLockDerivationPath *path = [[DSDerivationPathFactory sharedInstance] identityTopupFundingDerivationPathForWallet:self.wallet]; NSString *topupAddress = [path addressAtIndexPath:[NSIndexPath indexPathWithIndex:self.index]]; @@ -1547,7 +1547,7 @@ - (void)createAndPublishTopUpTransitionForAmount:(uint64_t)amount } - (void)createAndPublishRegistrationTransitionWithCompletion:(void (^)(BOOL, NSError *))completion { - NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@: CREATE AND PUBLISH IDENTITY REGISTRATION TRANSITION", self.logPrefix]; + NSMutableString *debugInfo = [NSMutableString stringWithFormat:@"%@ CREATE AND PUBLISH IDENTITY REGISTRATION TRANSITION", self.logPrefix]; DSLog(@"%@", debugInfo); if (!self.internalRegistrationFundingPrivateKey) { DSLog(@"%@: ERROR: No Funding Private Key", debugInfo); @@ -1576,7 +1576,7 @@ - (void)createAndPublishRegistrationTransitionWithCompletion:(void (^)(BOOL, NSE // MARK: Retrieval - (void)fetchIdentityNetworkStateInformationWithCompletion:(void (^)(BOOL success, BOOL found, NSError *error))completion { - NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@: Fetch Identity State", self.logPrefix]; + NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@ Fetch Identity State", self.logPrefix]; DSLog(@"%@", debugString); dispatch_async(self.identityQueue, ^{ DMaybeIdentity *result = dash_spv_platform_identity_manager_IdentitiesManager_monitor_for_id_bytes(self.chain.sharedRuntime, self.chain.sharedIdentitiesObj, u256_ctor(self.uniqueIDData), DRetryDown50(DEFAULT_FETCH_IDENTITY_RETRY_COUNT), self.isLocal ? DAcceptIdentityNotFound() : DRaiseIdentityNotFound()); @@ -1671,7 +1671,7 @@ - (void)fetchL3NetworkStateInformation:(DSIdentityQueryStep)queryStep inContext:(NSManagedObjectContext *)context withCompletion:(void (^)(DSIdentityQueryStep failureStep, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - DSLog(@"%@: Fetch L3 State (%@)", self.logPrefix, DSIdentityQueryStepsDescription(queryStep)); + DSLog(@"%@ Fetch L3 State (%@)", self.logPrefix, DSIdentityQueryStepsDescription(queryStep)); if (!(queryStep & DSIdentityQueryStep_Identity) && (!self.activeKeyCount)) { // We need to fetch keys if we want to query other information if (completion) completion(DSIdentityQueryStep_BadQuery, @[ERROR_ATTEMPT_QUERY_WITHOUT_KEYS]); @@ -1771,7 +1771,7 @@ - (void)fetchNetworkStateInformation:(DSIdentityQueryStep)querySteps inContext:(NSManagedObjectContext *)context withCompletion:(void (^)(DSIdentityQueryStep failureStep, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@: fetchNetworkStateInformation (%@)", self.logPrefix, DSIdentityQueryStepsDescription(querySteps)]; + NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@ fetchNetworkStateInformation (%@)", self.logPrefix, DSIdentityQueryStepsDescription(querySteps)]; DSLog(@"%@", debugString); if (querySteps & DSIdentityQueryStep_Identity) { [self fetchIdentityNetworkStateInformationWithCompletion:^(BOOL success, BOOL found, NSError *error) { @@ -1851,7 +1851,7 @@ - (void)fetchIfNeededNetworkStateInformation:(DSIdentityQueryStep)querySteps - (void)fetchNeededNetworkStateInformationInContext:(NSManagedObjectContext *)context withCompletion:(void (^)(DSIdentityQueryStep failureStep, NSArray *errors))completion onCompletionQueue:(dispatch_queue_t)completionQueue { - NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@: fetchNeededNetworkStateInformationInContext (local: %u, active keys: %lu) ", self.logPrefix, self.isLocal, self.activeKeyCount]; + NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@ Fetch Needed Network State Info (local: %u, active keys: %lu) ", self.logPrefix, self.isLocal, self.activeKeyCount]; DSLog(@"%@", debugString); dispatch_async(self.identityQueue, ^{ if (!self.activeKeyCount) { @@ -1902,34 +1902,34 @@ - (BOOL)processStateTransitionResult:(DMaybeStateTransitionProofResult *)result switch (proof_result->tag) { case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedDataContract: { NSData *identifier = NSDataFromPtr(proof_result->verified_data_contract->v0->id->_0->_0); - DSLog(@"%@: VerifiedDataContract: %@", self.logPrefix, identifier.hexString); + DSLog(@"%@ VerifiedDataContract: %@", self.logPrefix, identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedIdentity: { NSData *identifier = NSDataFromPtr(proof_result->verified_identity->v0->id->_0->_0); - DSLog(@"%@: VerifiedIdentity: %@", self.logPrefix, identifier.hexString); + DSLog(@"%@ VerifiedIdentity: %@", self.logPrefix, identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedPartialIdentity: { NSData *identifier = NSDataFromPtr(proof_result->verified_partial_identity->id->_0->_0); - DSLog(@"%@: VerifiedPartialIdentity: %@", self.logPrefix, identifier.hexString); + DSLog(@"%@ VerifiedPartialIdentity: %@", self.logPrefix, identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedBalanceTransfer: { dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedBalanceTransfer_Body transfer = proof_result->verified_balance_transfer; NSData *from_identifier = NSDataFromPtr(transfer._0->id->_0->_0); NSData *to_identifier = NSDataFromPtr(transfer._1->id->_0->_0); - DSLog(@"%@: VerifiedBalanceTransfer: %@ --> %@", self.logPrefix, from_identifier.hexString, to_identifier.hexString); + DSLog(@"%@ VerifiedBalanceTransfer: %@ --> %@", self.logPrefix, from_identifier.hexString, to_identifier.hexString); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedDocuments: { std_collections_Map_keys_platform_value_types_identifier_Identifier_values_Option_dpp_document_Document *verified_documents = proof_result->verified_documents; - DSLog(@"%@: VerifiedDocuments: %lu", self.logPrefix, verified_documents->count); + DSLog(@"%@ VerifiedDocuments: %lu", self.logPrefix, verified_documents->count); break; } case dpp_state_transition_proof_result_StateTransitionProofResult_VerifiedMasternodeVote: { dpp_voting_votes_Vote *verified_masternode_vote = proof_result->verified_masternode_vote; - DSLog(@"%@: VerifiedMasternodeVote: %u", self.logPrefix, verified_masternode_vote->tag); + DSLog(@"%@ VerifiedMasternodeVote: %u", self.logPrefix, verified_masternode_vote->tag); break; } default: @@ -1942,7 +1942,7 @@ - (BOOL)processStateTransitionResult:(DMaybeStateTransitionProofResult *)result // MARK: - Contracts - (void)fetchAndUpdateContract:(DPContract *)contract { - NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@: fetchAndUpdateContract (%lu) ", self.logPrefix, (unsigned long) contract.contractState]; + NSMutableString *debugString = [NSMutableString stringWithFormat:@"%@ Fetch & Update Contract (%lu) ", self.logPrefix, (unsigned long) contract.contractState]; DSLog(@"%@", debugString); NSManagedObjectContext *context = [NSManagedObjectContext platformContext]; __weak typeof(contract) weakContract = contract; @@ -1967,11 +1967,11 @@ - (void)fetchAndUpdateContract:(DPContract *)contract { DMaybeStateTransitionProofResult *state_transition_result = dash_spv_platform_PlatformSDK_data_contract_create2(self.chain.sharedRuntime, self.chain.sharedPlatformObj, data_contracts_SystemDataContract_DPNS_ctor(), u256_ctor_u(self.uniqueID), 0, privateKey->ok); if (state_transition_result->error) { - DSLog(@"%@: ERROR: %@", debugString, [NSError ffi_from_platform_error:state_transition_result->error]); + DSLog(@"%@ ERROR: %@", debugString, [NSError ffi_from_platform_error:state_transition_result->error]); DMaybeStateTransitionProofResultDtor(state_transition_result); return; } - DSLog(@"%@: OK", debugString); + DSLog(@"%@ OK", debugString); if ([self processStateTransitionResult:state_transition_result]) { contract.contractState = DPContractState_Registering; } else { @@ -1983,36 +1983,36 @@ - (void)fetchAndUpdateContract:(DPContract *)contract { if (monitor_result->error) { DMaybeContractDtor(monitor_result); - DSLog(@"%@: Contract Monitoring Error: %@", self.logPrefix, [NSError ffi_from_platform_error:monitor_result->error]); + DSLog(@"%@ Contract Monitoring Error: %@", self.logPrefix, [NSError ffi_from_platform_error:monitor_result->error]); return; } if (monitor_result->ok) { NSData *identifier = NSDataFromPtr(monitor_result->ok->v0->id->_0->_0); if ([identifier isEqualToData:uint256_data(contract.contractId)]) { - DSLog(@"%@: Contract Monitoring OK", self.logPrefix); + DSLog(@"%@ Contract Monitoring OK", self.logPrefix); contract.contractState = DPContractState_Registered; [contract saveAndWaitInContext:context]; } else { - DSLog(@"%@: Contract Monitoring Error: Ids dont match", self.logPrefix); + DSLog(@"%@ Contract Monitoring Error: Ids dont match", self.logPrefix); } } - DSLog(@"%@: Contract Monitoring Error", self.logPrefix); + DSLog(@"%@ Contract Monitoring Error", self.logPrefix); } else if (contract.contractState == DPContractState_Registered || contract.contractState == DPContractState_Registering) { - DSLog(@"%@: Fetching contract for verification %@", self.logPrefix, contract.base58ContractId); + DSLog(@"%@ Fetching contract for verification %@", self.logPrefix, contract.base58ContractId); DMaybeContract *contract_result = dash_spv_platform_contract_manager_ContractsManager_fetch_contract_by_id_bytes(self.chain.sharedRuntime, self.chain.sharedContractsObj, u256_ctor_u(contract.contractId)); dispatch_async(self.identityQueue, ^{ __strong typeof(weakContract) strongContract = weakContract; if (!weakContract || !contract_result) return; if (!contract_result->ok) { - DSLog(@"%@: Contract Monitoring ERROR: NotRegistered ", self.logPrefix); + DSLog(@"%@ Contract Monitoring ERROR: NotRegistered ", self.logPrefix); strongContract.contractState = DPContractState_NotRegistered; [strongContract saveAndWaitInContext:context]; DMaybeContractDtor(contract_result); return; } - DSLog(@"%@: Contract Monitoring OK: %@ ", self.logPrefix, strongContract); + DSLog(@"%@ Contract Monitoring OK: %@ ", self.logPrefix, strongContract); if (strongContract.contractState == DPContractState_Registered && !dash_spv_platform_contract_manager_has_equal_document_type_keys(contract_result->ok, strongContract.raw_contract)) { strongContract.contractState = DPContractState_NotRegistered; [strongContract saveAndWaitInContext:context]; @@ -2034,13 +2034,13 @@ - (void)updateCreditBalance { if (!strongSelf) return; DMaybeIdentityBalance *result = dash_spv_platform_identity_manager_IdentitiesManager_fetch_balance_by_id_bytes(strongSelf.chain.sharedRuntime, strongSelf.chain.sharedIdentitiesObj, u256_ctor(self.uniqueIDData)); if (!result->ok) { - DSLog(@"%@: updateCreditBalance: ERROR RESULT: %u", self.logPrefix, result->error->tag); + DSLog(@"%@ Update Credit Balance: ERROR RESULT: %u", self.logPrefix, result->error->tag); DMaybeIdentityBalanceDtor(result); return; } uint64_t balance = result->ok[0]; DMaybeIdentityBalanceDtor(result); - DSLog(@"%@: updateCreditBalance: OK: %llu", self.logPrefix, balance); + DSLog(@"%@ Update Credit Balance: OK: %llu", self.logPrefix, balance); dispatch_async(self.identityQueue, ^{ strongSelf.creditBalance = balance; }); @@ -2445,7 +2445,7 @@ - (NSString *)debugDescription { } - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [Identity: %@] ", self.chain.name, uint256_hex(self.uniqueID)]; + return [NSString stringWithFormat:@"[%@] [Identity: %@]", self.chain.name, uint256_hex(self.uniqueID)]; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager+Protected.h b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager+Protected.h index 3ae80d6a..11d5b782 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager+Protected.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager+Protected.h @@ -34,7 +34,7 @@ typedef NS_ENUM(uint16_t, DSChainNotificationType) { - (void)wipeMasternodeInfo; - (void)notifySyncStateChanged; -- (void)notifyMasternodeSyncStateChange:(uint32_t)lastBlockHeihgt storedCount:(uintptr_t)storedCount; +- (void)notifyMasternodeSyncStateChange:(uint32_t)lastBlockHeihgt storedCount:(uint32_t)storedCount; @end diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h index 92e168c3..4b421806 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.h @@ -82,7 +82,6 @@ typedef void (^MultipleBlockMiningCompletionBlock)(NSArray *block - (void)stopSync; - (void)syncBlocksRescan; - (void)masternodeListAndBlocksRescan; -- (void)masternodeListRescan; - (DSChainLock * _Nullable)chainLockForBlockHash:(UInt256)blockHash; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m index 308f90d9..7d3edcb7 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSChainManager.m @@ -98,7 +98,10 @@ - (instancetype)initWithChain:(DSChain *)chain { self.lastNotifiedBlockDidChange = 0; if ([self.masternodeManager hasCurrentMasternodeListInLast30Days]) { - [self.peerManager useMasternodeList:self.masternodeManager.currentMasternodeList withConnectivityNonce:self.sessionConnectivityNonce]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.peerManager useMasternodeList:self.masternodeManager.currentMasternodeList + withConnectivityNonce:self.sessionConnectivityNonce]; + }); } //[self loadMaxTransactionInfo]; @@ -114,10 +117,11 @@ - (NSString *)logPrefix { } - (BOOL)isSynced { - return self.syncState.combinedSyncProgress == 1.0; + return self.syncState.progress == 1.0; } +// this is thread-unsafe, it's preferable to use NSNotificationCenter's DSChainManagerSyncStateDidChangeNotification to get progress in main thread - (double)combinedSyncProgress { - return self.syncState.combinedSyncProgress; + return self.syncState.progress; } @@ -137,10 +141,14 @@ - (void)startSync { - (void)stopSync { DSLog(@"%@ stop (chain switch)", self.logPrefix); - [self.masternodeManager stopSync]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.masternodeManager stopSync]; + }); [self.peerManager disconnect:DSDisconnectReason_ChainSwitch]; - self.syncState.syncPhase = DSChainSyncPhase_Offline; - [self notifySyncStateChanged]; + dispatch_async(self.chain.networkingQueue, ^{ + self.syncState.syncPhase = DSChainSyncPhase_Offline; + [self notifySyncStateChanged]; + }); } - (void)removeNonMainnetTrustedPeer { @@ -150,81 +158,43 @@ - (void)removeNonMainnetTrustedPeer { } } -- (void)disconnectedMasternodeListAndBlocksRescan { - NSManagedObjectContext *chainContext = [NSManagedObjectContext chainContext]; - [[DashSync sharedSyncController] wipeMasternodeDataForChain:self.chain inContext:chainContext]; - [[DashSync sharedSyncController] wipeBlockchainDataForChain:self.chain inContext:chainContext]; - - [self removeNonMainnetTrustedPeer]; - [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; - DSLog(@"%@ Disconnected (MasternodeListAndBlocksRescan) -> peerManager::connect", self.logPrefix); - [self.peerManager connect]; -} - -- (void)disconnectedMasternodeListRescan { - NSManagedObjectContext *chainContext = [NSManagedObjectContext chainContext]; - [[DashSync sharedSyncController] wipeMasternodeDataForChain:self.chain inContext:chainContext]; - - [self removeNonMainnetTrustedPeer]; - [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; - DSLog(@"%@ Disconnected (MasternodeListRescan) -> peerManager::connect", self.logPrefix); - [self.peerManager connect]; -} - -- (void)disconnectedSyncBlocksRescan { - NSManagedObjectContext *chainContext = [NSManagedObjectContext chainContext]; - [[DashSync sharedSyncController] wipeBlockchainNonTerminalDataForChain:self.chain inContext:chainContext]; - - [self removeNonMainnetTrustedPeer]; - [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; - DSLog(@"%@ Disconnected (SyncBlocksRescan) -> peerManager::connect", self.logPrefix); - [self.peerManager connect]; -} - // rescans blocks and transactions after earliestKeyTime, a new random download peer is also selected due to the // possibility that a malicious node might lie by omitting transactions that match the bloom filter - (void)syncBlocksRescan { - if (!self.peerManager.connected) { - [self disconnectedSyncBlocksRescan]; - } else { - [self.peerManager disconnectDownloadPeerForError:nil - withCompletion:^(BOOL success) { - [self disconnectedSyncBlocksRescan]; - }]; - } + [self.peerManager disconnectDownloadPeerForError:nil + withCompletion:^(BOOL success) { + NSManagedObjectContext *chainContext = [NSManagedObjectContext chainContext]; + [[DashSync sharedSyncController] wipeBlockchainNonTerminalDataForChain:self.chain inContext:chainContext]; + [self removeNonMainnetTrustedPeer]; + [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; + DSLog(@"%@ Disconnected (SyncBlocksRescan) -> peerManager::connect", self.logPrefix); + [self.peerManager connect]; + }]; } - (void)masternodeListAndBlocksRescan { - if (!self.peerManager.connected) { - [self disconnectedMasternodeListAndBlocksRescan]; - } else { - [self.peerManager disconnectDownloadPeerForError:nil - withCompletion:^(BOOL success) { - [self disconnectedMasternodeListAndBlocksRescan]; - }]; - } -} - -- (void)masternodeListRescan { - if (!self.peerManager.connected) { - [self disconnectedMasternodeListRescan]; - } else { - [self.peerManager disconnectDownloadPeerForError:nil - withCompletion:^(BOOL success) { - [self disconnectedMasternodeListRescan]; - }]; - } + [self.peerManager disconnectDownloadPeerForError:nil + withCompletion:^(BOOL success) { + NSManagedObjectContext *chainContext = [NSManagedObjectContext chainContext]; + [[DashSync sharedSyncController] wipeMasternodeDataForChain:self.chain inContext:chainContext]; + [[DashSync sharedSyncController] wipeBlockchainDataForChain:self.chain inContext:chainContext]; + [self removeNonMainnetTrustedPeer]; + [self notify:DSChainManagerSyncWillStartNotification userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; + DSLog(@"%@ Disconnected (MasternodeListAndBlocksRescan) -> peerManager::connect", self.logPrefix); + [self.peerManager connect]; + }]; } - // MARK: - DSChainDelegate - (void)chain:(DSChain *)chain didSetBlockHeight:(int32_t)height andTimestamp:(NSTimeInterval)timestamp forTransactionHashes:(NSArray *)txHashes updatedTransactions:(NSArray *)updatedTransactions { [self.transactionManager chain:chain didSetBlockHeight:height andTimestamp:timestamp forTransactionHashes:txHashes updatedTransactions:updatedTransactions]; } +// always from chain.networkingQueue - (void)chain:(DSChain *)chain didFinishInChainSyncPhaseFetchingIdentityDAPInformation:(DSIdentity *)identity { dispatch_async(chain.networkingQueue, ^{ + [self.syncState removeSyncKind:DSSyncStateExtKind_Platform]; [self.peerManager resumeBlockchainSynchronizationOnPeers]; }); } @@ -250,19 +220,28 @@ - (void)chainShouldStartSyncingBlockchain:(DSChain *)chain onPeer:(DSPeer *)peer DSPeerManagerNotificationPeerKey: peer ? peer : [NSNull null]}]; dispatch_async(self.chain.networkingQueue, ^{ if ((self.syncPhase != DSChainSyncPhase_ChainSync && self.syncPhase != DSChainSyncPhase_Synced) && self.chain.needsInitialTerminalHeadersSync) { + DSLog(@"%@ syncBlockchainStarted [phase:%d] -> get headers (terminal block locators)", self.logPrefix, self.syncPhase); //masternode list should be synced first and the masternode list is old + [self.syncState addSyncKind:DSSyncStateExtKind_Headers]; self.syncState.syncPhase = DSChainSyncPhase_InitialTerminalBlocks; [peer sendGetheadersMessageWithLocators:[self.chain terminalBlocksLocatorArray] andHashStop:UINT256_ZERO]; } else if (([[DSOptionsManager sharedInstance] syncType] & DSSyncType_MasternodeList) && [self.masternodeManager isMasternodeListOutdated]) { + DSLog(@"%@ syncBlockchainStarted [phase:%d] -> get masternodes", self.logPrefix, self.syncPhase); + [self.syncState addSyncKind:DSSyncStateExtKind_Masternodes]; self.syncState.syncPhase = DSChainSyncPhase_InitialTerminalBlocks; [self.masternodeManager startSync]; } else { - self.syncState.syncPhase = DSChainSyncPhase_ChainSync; BOOL startingDevnetSync = [self.chain isDevnetAny] && self.chain.lastSyncBlockHeight < 5; NSTimeInterval cutoffTime = self.chain.earliestWalletCreationTime - HEADER_WINDOW_BUFFER_TIME; if (startingDevnetSync || (self.chain.lastSyncBlockTimestamp >= cutoffTime && [self shouldRequestMerkleBlocksForZoneAfterHeight:[self.chain lastSyncBlockHeight]])) { + DSLog(@"%@ syncBlockchainStarted [phase:%d] -> get blocks", self.logPrefix, self.syncPhase); + self.syncState.syncPhase = DSChainSyncPhase_ChainSync; + [self.syncState addSyncKind:DSSyncStateExtKind_Transactions]; [peer sendGetblocksMessageWithLocators:[self.chain chainSyncBlockLocatorArray] andHashStop:UINT256_ZERO]; } else { + DSLog(@"%@ syncBlockchainStarted [phase:%d] -> get headers (sync block locators)", self.logPrefix, self.syncPhase); + self.syncState.syncPhase = DSChainSyncPhase_ChainSync; + [self.syncState addSyncKind:DSSyncStateExtKind_Headers]; [peer sendGetheadersMessageWithLocators:[self.chain chainSyncBlockLocatorArray] andHashStop:UINT256_ZERO]; } } @@ -270,9 +249,11 @@ - (void)chainShouldStartSyncingBlockchain:(DSChain *)chain onPeer:(DSPeer *)peer }); } +// always from chain.networkingQueue - (void)chainFinishedSyncingInitialHeaders:(DSChain *)chain fromPeer:(DSPeer *)peer onMainChain:(BOOL)onMainChain { if (onMainChain && peer && (peer == self.peerManager.downloadPeer)) [self relayedNewItem]; DSLog(@"%@ Sync Status: initial headers: OK -> sync masternode lists & quorums", self.logPrefix); + [self.syncState removeSyncKind:DSSyncStateExtKind_Headers]; [self.peerManager chainSyncStopped]; if (([[DSOptionsManager sharedInstance] syncType] & DSSyncType_MasternodeList)) { // make sure we care about masternode lists @@ -280,10 +261,11 @@ - (void)chainFinishedSyncingInitialHeaders:(DSChain *)chain fromPeer:(DSPeer *)p } } +// always from chain.networkingQueue - (void)chainFinishedSyncingTransactionsAndBlocks:(DSChain *)chain fromPeer:(DSPeer *)peer onMainChain:(BOOL)onMainChain { if (onMainChain && peer && (peer == self.peerManager.downloadPeer)) [self relayedNewItem]; DSLog(@"%@ Sync Status: transactions and blocks: OK -> sync mempool, sporks & governance", self.logPrefix); - + [self.syncState removeSyncKind:DSSyncStateExtKind_Transactions]; self.syncState.chainSyncStartHeight = 0; self.syncState.syncPhase = DSChainSyncPhase_Synced; [self.transactionManager fetchMempoolFromNetwork]; @@ -304,28 +286,32 @@ - (void)setSyncPhase:(DSChainSyncPhase)syncPhase { self.syncState.syncPhase = syncPhase; } +// always from chain.networkingQueue - (void)syncBlockchain { - DSLog(@"%@ syncBlockchain connected peers: %lu phase: %d", self.logPrefix, self.peerManager.connectedPeerCount, self.syncPhase); if (self.peerManager.connectedPeerCount == 0) { + DSLog(@"%@ syncBlockchain [phase:%d] (no connected peers -> connect)", self.logPrefix, self.syncPhase); if (self.syncPhase == DSChainSyncPhase_InitialTerminalBlocks) { self.syncState.syncPhase = DSChainSyncPhase_ChainSync; [self notifySyncStateChanged]; } - DSLog(@"%@ syncBlockchain -> peerManager::connect", self.logPrefix); [self.peerManager connect]; } else if (!self.peerManager.masternodeList && self.masternodeManager.currentMasternodeList) { + DSLog(@"%@ syncBlockchain [phase:%d] (use masternode list peers)", self.logPrefix, self.syncPhase); [self.peerManager useMasternodeList:self.masternodeManager.currentMasternodeList withConnectivityNonce:self.sessionConnectivityNonce]; } else if (self.syncPhase == DSChainSyncPhase_InitialTerminalBlocks) { + DSLog(@"%@ syncBlockchain [phase:%d] (should start)", self.logPrefix, self.syncPhase); self.syncState.syncPhase = DSChainSyncPhase_ChainSync; [self notifySyncStateChanged]; [self chainShouldStartSyncingBlockchain:self.chain onPeer:self.peerManager.downloadPeer]; } } +// always from chain.networkingQueue - (void)chainFinishedSyncingMasternodeListsAndQuorums:(DSChain *)chain { - if (chain.isEvolutionEnabled) { + if (chain.isEvolutionEnabled && ![self.syncState.platformSyncInfo hasRecentIdentitiesSync]) { DSLog(@"%@ Sync Status: masternode list and quorums: OK -> sync identities", self.logPrefix); [self.identitiesManager syncIdentitiesWithCompletion:^(NSArray *_Nullable identities) { + // always from chain.networkingQueue DSLog(@"%@ Sync Status: identities: OK -> sync chain", self.logPrefix); [self syncBlockchain]; }]; @@ -437,7 +423,7 @@ - (NSString *)terminalSyncStartHeightKey { return [NSString stringWithFormat:@"%@_%@", TERMINAL_SYNC_STARTHEIGHT_KEY, [self.chain uniqueID]]; } - +// always from chain.networkingQueue - (void)resetChainSyncStartHeight { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; BOOL changed = NO; @@ -454,6 +440,7 @@ - (void)resetChainSyncStartHeight { [self notifySyncStateChanged]; } +// always from chain.networkingQueue - (void)restartChainSyncStartHeight { self.syncState.chainSyncStartHeight = 0; [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:self.chainSyncStartHeightKey]; @@ -462,17 +449,18 @@ - (void)restartChainSyncStartHeight { } +// always from chain.networkingQueue - (void)resetTerminalSyncStartHeight { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; if (self.syncState.terminalSyncStartHeight == 0) self.syncState.terminalSyncStartHeight = (uint32_t)[userDefaults integerForKey:self.terminalSyncStartHeightKey]; - if (self.syncState.terminalSyncStartHeight == 0) { self.syncState.terminalSyncStartHeight = self.chain.lastTerminalBlockHeight; [[NSUserDefaults standardUserDefaults] setInteger:self.syncState.terminalSyncStartHeight forKey:self.terminalSyncStartHeightKey]; } } +// always from chain.networkingQueue - (void)restartTerminalSyncStartHeight { self.syncState.terminalSyncStartHeight = 0; [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:self.terminalSyncStartHeightKey]; @@ -499,9 +487,9 @@ - (void)setupNotificationTimer:(void (^ __nullable)(void))completion { } } -- (void)notifyMasternodeSyncStateChange:(uint32_t)lastBlockHeihgt storedCount:(uintptr_t)storedCount { +- (void)notifyMasternodeSyncStateChange:(uint32_t)lastBlockHeihgt storedCount:(uint32_t)storedCount { @synchronized (self.syncState) { - self.syncState.masternodeListSyncInfo.lastBlockHeight = lastBlockHeihgt; + self.syncState.masternodeListSyncInfo.lastListHeight = lastBlockHeihgt; self.syncState.masternodeListSyncInfo.storedCount = storedCount; [self notifySyncStateChanged]; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSGovernanceSyncManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSGovernanceSyncManager.m index eec67cfa..63d1dd1a 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSGovernanceSyncManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSGovernanceSyncManager.m @@ -131,7 +131,9 @@ - (void)continueGovernanceSync { - (void)startGovernanceSync { //Do we want to sync? if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_Governance)) return; // make sure we care about Governance objects - + dispatch_async(dispatch_get_main_queue(), ^{ + [self.chain.chainManager.syncState addSyncKind:DSSyncStateExtKind_Governance]; + }); //Do we need to sync? if ([[NSUserDefaults standardUserDefaults] objectForKey:[NSString stringWithFormat:@"%@_%@", self.chain.uniqueID, LAST_SYNCED_GOVERANCE_OBJECTS]]) { //no need to do a governance sync if we already completed one recently NSTimeInterval lastSyncedGovernance = [[NSUserDefaults standardUserDefaults] integerForKey:[NSString stringWithFormat:@"%@_%@", self.chain.uniqueID, LAST_SYNCED_GOVERANCE_OBJECTS]]; @@ -139,6 +141,7 @@ - (void)startGovernanceSync { NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; if (lastSyncedGovernance + interval > now) { [self continueGovernanceSync]; + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Governance]; return; }; } @@ -196,12 +199,16 @@ - (void)finishedGovernanceObjectSyncWithPeer:(DSPeer *)peer { [[NSUserDefaults standardUserDefaults] setInteger:[[NSDate date] timeIntervalSince1970] forKey:[NSString stringWithFormat:@"%@_%@", self.chain.uniqueID, LAST_SYNCED_GOVERANCE_OBJECTS]]; //Do we want to request votes now? - if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_GovernanceVotes)) return; + if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_GovernanceVotes)) { + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Governance]; + return; + } self.needVoteSyncGovernanceObjects = [self.governanceObjects mutableCopy]; [self startNextGoveranceVoteSyncWithPeer:peer]; } - (void)finishedGovernanceVoteSyncWithPeer:(DSPeer *)peer { + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Governance]; if (peer.governanceRequestState != DSGovernanceRequestState_GovernanceObjectVotes) return; if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_GovernanceVotes)) return; peer.governanceRequestState = DSGovernanceRequestState_None; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.h index 52e82a6c..54138d09 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.h @@ -37,8 +37,6 @@ typedef void (^DashpayUserInfoCompletionBlock)(BOOL success, DSTransientDashpayU /*! @brief Returns the timestamp of the last time identities were synced. */ @property (nonatomic, readonly) NSTimeInterval lastSyncedIndentitiesTimestamp; -/*! @brief Returns if we synced identities in the last 30 seconds. */ -@property (nonatomic, readonly) BOOL hasRecentIdentitiesSync; - (instancetype)initWithChain:(DSChain *)chain; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m index 65047cbe..7424fcac 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSIdentitiesManager.m @@ -27,7 +27,7 @@ #import "DSChain+Params.h" #import "DSChain+Protected.h" #import "DSChain+Wallet.h" -#import "DSChainManager.h" +#import "DSChainManager+Protected.h" #import "DSDashPlatform.h" #import "DSDerivationPathFactory.h" #import "DSMerkleBlock.h" @@ -52,7 +52,6 @@ @interface DSIdentitiesManager () @property (nonatomic, strong) dispatch_queue_t identityQueue; @property (nonatomic, strong) NSMutableDictionary *foreignIdentities; @property (nonatomic, assign) NSTimeInterval lastSyncedIndentitiesTimestamp; -@property (nonatomic, assign) BOOL hasRecentIdentitiesSync; @end @@ -68,7 +67,10 @@ - (instancetype)initWithChain:(DSChain *)chain { if (!(self = [super init])) return nil; self.chain = chain; - _identityQueue = dispatch_queue_create([@"org.dashcore.dashsync.identity" UTF8String], DISPATCH_QUEUE_SERIAL); + + dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); + _identityQueue = dispatch_queue_create("org.dashcore.dashsync.identity", attr); + [self setup]; // self.foreignIdentities = [NSMutableDictionary dictionary]; // [self loadExternalIdentities]; @@ -76,12 +78,6 @@ - (instancetype)initWithChain:(DSChain *)chain { return self; } -// MARK: - Loading - - -- (BOOL)hasRecentIdentitiesSync { - return ([[NSDate date] timeIntervalSince1970] - self.lastSyncedIndentitiesTimestamp < 30); -} // MARK: - Wiping @@ -132,7 +128,7 @@ - (NSArray *)unsyncedIdentities { [unsyncedIdentities addObject:identity]; } else if (self.chain.lastSyncBlockHeight > identity.dashpaySyncronizationBlockHeight) { - DSLog(@"%@: Unsynced identity (lastSyncBlockHeight (%u) > dashpaySyncronizationBlockHeight %u)", self.logPrefix, self.chain.lastSyncBlockHeight, identity.dashpaySyncronizationBlockHeight); + DSLog(@"%@ Unsynced identity (lastSyncBlockHeight (%u) > dashpaySyncronizationBlockHeight %u)", self.logPrefix, self.chain.lastSyncBlockHeight, identity.dashpaySyncronizationBlockHeight); //If they are equal then the blockchain identity is synced //This is because the dashpaySyncronizationBlock represents the last block for the bloom filter used in L1 should be considered valid //That's because it is set at the time with the hash of the last @@ -142,31 +138,32 @@ - (NSArray *)unsyncedIdentities { return unsyncedIdentities; } -//- (void)syncPlatformWithCompletion:(IdentitiesSuccessCompletionBlock)completion { -// [self syncIdentitiesWithCompletion:^(NSArray *_Nullable identities) { -// [self retrieveAllIdentitiesChainStates:completion]; -// }]; -// -//} //TODO: if we get an error or identity not found, better stop the process and start syncing chain - (void)syncIdentitiesWithCompletion:(IdentitiesSuccessCompletionBlock)completion { - DSLog(@"%@ Sync Identities", self.logPrefix); if (!self.chain.isEvolutionEnabled) { if (completion) dispatch_async(self.chain.networkingQueue, ^{ completion(@[]); }); return; } + DSLog(@"%@ Sync Identities", self.logPrefix); dispatch_async(self.identityQueue, ^{ NSArray *wallets = self.chain.wallets; __block dispatch_group_t keyHashesDispatchGroup = dispatch_group_create(); __block NSMutableArray *errors = [NSMutableArray array]; __block NSMutableArray *allIdentities = [NSMutableArray array]; - + const int keysToCheck = 5; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState addSyncKind:DSSyncStateExtKind_Platform]; + [self.chain.chainManager.syncState.platformSyncInfo addSyncKind:DSPlatformSyncStateKind_KeyHashes]; + self.chain.chainManager.syncState.platformSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.platformSyncInfo.queueMaxAmount = keysToCheck * (uint32_t) wallets.count; + [self.chain.chainManager notifySyncStateChanged]; + }); + for (DSWallet *wallet in wallets) { uint32_t unusedIndex = [wallet unusedIdentityIndex]; DSAuthenticationKeysDerivationPath *derivationPath = [[DSDerivationPathFactory sharedInstance] identityECDSAKeysDerivationPathForWallet:wallet]; - const int keysToCheck = 5; NSMutableDictionary *keyIndexes = [NSMutableDictionary dictionaryWithCapacity:keysToCheck]; u160 **key_hashes = malloc(keysToCheck * sizeof(u160 *)); for (int i = 0; i < keysToCheck; i++) { @@ -179,64 +176,89 @@ - (void)syncIdentitiesWithCompletion:(IdentitiesSuccessCompletionBlock)completio dispatch_group_enter(keyHashesDispatchGroup); DRetry *stragegy = DRetryLinear(5); dash_spv_platform_identity_manager_IdentityValidator *options = DAcceptIdentityNotFound(); - Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error *result = dash_spv_platform_identity_manager_IdentitiesManager_monitor_for_key_hashes(self.chain.sharedRuntime, self.chain.sharedIdentitiesObj, Vec_u8_20_ctor(keysToCheck, key_hashes), stragegy, options); - - if (result->error) { - NSError *error = [NSError ffi_from_platform_error:result->error]; - DSLog(@"%@: Sync Identities: ERROR %@", self.logPrefix, error); - Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error_destroy(result); - [errors addObject:error]; - dispatch_group_leave(keyHashesDispatchGroup); - return; - } - std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity *ok = result->ok; - NSMutableArray *identities = [NSMutableArray array]; - for (int j = 0; j < ok->count; j++) { - DIdentity *identity = ok->values[j]; - switch (identity->tag) { - case dpp_identity_identity_Identity_V0: { - dpp_identity_v0_IdentityV0 *identity_v0 = identity->v0; - DMaybeOpaqueKey *maybe_opaque_key = DOpaqueKeyFromIdentityPubKey(identity_v0->public_keys->values[0]); - NSData *publicKeyData = [DSKeyManager publicKeyData:maybe_opaque_key->ok]; - NSNumber *index = [keyIndexes objectForKey:publicKeyData]; - DSIdentity *identityModel = [[DSIdentity alloc] initAtIndex:index.intValue uniqueId:u256_cast(identity_v0->id->_0->_0) inWallet:wallet]; - [identityModel applyIdentity:identity save:NO inContext:nil]; - [identities addObject:identityModel]; - break; - } - - default: - break; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error *result = dash_spv_platform_identity_manager_IdentitiesManager_monitor_for_key_hashes(self.chain.sharedRuntime, self.chain.sharedIdentitiesObj, Vec_u8_20_ctor(keysToCheck, key_hashes), stragegy, options); + + if (result->error) { + NSError *error = [NSError ffi_from_platform_error:result->error]; + DSLog(@"%@: Sync Identities: ERROR %@", self.logPrefix, error); + Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error_destroy(result); + [errors addObject:error]; + dispatch_group_leave(keyHashesDispatchGroup); + return; } - } - Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error_destroy(result); - BOOL success = [wallet registerIdentities:identities verify:YES]; - DSLog(@"%@: Sync Identities: %@", self.logPrefix, DSLocalizedFormat(success ? @"OK (%lu)" : @"Retrieved (%lu) but can't register in wallet", nil, identities.count)); - if (success) { - [allIdentities addObjectsFromArray:identities]; - NSManagedObjectContext *platformContext = [NSManagedObjectContext platformContext]; - [platformContext performBlockAndWait:^{ - for (DSIdentity *identity in identities) { - [identity saveInitialInContext:platformContext]; + std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity *ok = result->ok; + NSMutableArray *identities = [NSMutableArray array]; + + for (int j = 0; j < ok->count; j++) { + DIdentity *identity = ok->values[j]; + switch (identity->tag) { + case dpp_identity_identity_Identity_V0: { + dpp_identity_v0_IdentityV0 *identity_v0 = identity->v0; + DMaybeOpaqueKey *maybe_opaque_key = DOpaqueKeyFromIdentityPubKey(identity_v0->public_keys->values[0]); + NSData *publicKeyData = [DSKeyManager publicKeyData:maybe_opaque_key->ok]; + NSNumber *index = [keyIndexes objectForKey:publicKeyData]; + DSIdentity *identityModel = [[DSIdentity alloc] initAtIndex:index.intValue uniqueId:u256_cast(identity_v0->id->_0->_0) inWallet:wallet]; + [identityModel applyIdentity:identity save:NO inContext:nil]; + [identities addObject:identityModel]; + break; + } + + default: + break; } - }]; - } else { - [errors addObject:ERROR_UNKNOWN_KEYS]; - } - dispatch_group_leave(keyHashesDispatchGroup); + } + + Result_ok_std_collections_Map_keys_u8_arr_20_values_dpp_identity_identity_Identity_err_dash_spv_platform_error_Error_destroy(result); + BOOL success = [wallet registerIdentities:identities verify:YES]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.platformSyncInfo.queueCount = keysToCheck; + }); + + DSLog(@"%@: Sync Identities: %@", self.logPrefix, DSLocalizedFormat(success ? @"OK (%lu)" : @"Retrieved (%lu) but can't register in wallet", nil, identities.count)); + if (success) { + [allIdentities addObjectsFromArray:identities]; + NSManagedObjectContext *platformContext = [NSManagedObjectContext platformContext]; + [platformContext performBlockAndWait:^{ + for (DSIdentity *identity in identities) { + [identity saveInitialInContext:platformContext]; + } + }]; + } else { + [errors addObject:ERROR_UNKNOWN_KEYS]; + } + dispatch_group_leave(keyHashesDispatchGroup); + }); + } dispatch_group_notify(keyHashesDispatchGroup, self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.platformSyncInfo removeSyncKind:DSPlatformSyncStateKind_KeyHashes]; NSArray *identities = [self unsyncedIdentities]; - DSLog(@"%@ Sync Identities: unsynced: %@", self.logPrefix, identities); + NSMutableString *deb_id = [NSMutableString stringWithFormat:@"%@ Sync Identities: unsynced: ", self.logPrefix]; + for (DSIdentity *identitity in identities) { + [deb_id appendFormat:@"%@,", uint256_hex(identitity.uniqueID)]; + } + DSLog(@"%@", deb_id); + NSUInteger identitiesCount = [identities count]; + if (identitiesCount) { + self.chain.chainManager.syncState.platformSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.platformSyncInfo.queueMaxAmount = (uint32_t) identitiesCount; + [self.chain.chainManager.syncState.platformSyncInfo addSyncKind:DSPlatformSyncStateKind_Unsynced]; + } + [self.chain.chainManager notifySyncStateChanged]; + dispatch_group_t dispatchGroup = dispatch_group_create(); __block NSMutableArray *errors = [NSMutableArray array]; for (DSIdentity *identity in identities) { dispatch_group_enter(dispatchGroup); - [self fetchNeededNetworkStateInformationForIdentity:identity - withCompletion:^(BOOL success, DSIdentity *_Nullable identity, NSError *_Nullable error) { + [self fetchNeededNetworkStateInformationForIdentity:identity withCompletion:^(BOOL success, DSIdentity *_Nullable identity, NSError *_Nullable error) { + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.platformSyncInfo.queueCount++; + [self.chain.chainManager notifySyncStateChanged]; + }); if (success && identity != nil) { dispatch_group_leave(dispatchGroup); } else { @@ -247,6 +269,12 @@ - (void)syncIdentitiesWithCompletion:(IdentitiesSuccessCompletionBlock)completio } dispatch_group_notify(dispatchGroup, self.chain.networkingQueue, ^{ self.lastSyncedIndentitiesTimestamp = [[NSDate date] timeIntervalSince1970]; + self.chain.chainManager.syncState.platformSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.platformSyncInfo.queueMaxAmount = 0; + self.chain.chainManager.syncState.platformSyncInfo.lastSyncedIndentitiesTimestamp = self.lastSyncedIndentitiesTimestamp; + [self.chain.chainManager.syncState.platformSyncInfo resetSyncKind]; + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Platform]; + [self.chain.chainManager notifySyncStateChanged]; if (!errors.count && completion) completion(identities); }); @@ -507,6 +535,7 @@ - (void)searchIdentitiesByDPNSRegisteredIdentityUniqueID:(NSData *)userID if (completion) dispatch_async(dispatch_get_main_queue(), ^{ completion(YES, [rIdentities copy], @[]); }); } +// always from chain.networkingQueue - (void)checkAssetLockTransactionForPossibleNewIdentity:(DSAssetLockTransaction *)transaction { uint32_t index; DSWallet *wallet = [self.chain walletHavingIdentityAssetLockRegistrationHash:transaction.creditBurnPublicKeyHash foundAtIndex:&index]; @@ -544,6 +573,7 @@ - (void)fetchNeededNetworkStateInformationForIdentity:(DSIdentity *)identity // MARK: - DSChainIdentitiesDelegate +// always from chain.networkingQueue - (void)chain:(DSChain *)chain didFinishInChainSyncPhaseFetchingIdentityDAPInformation:(DSIdentity *)identity { [self.chain.chainManager chain:chain didFinishInChainSyncPhaseFetchingIdentityDAPInformation:identity]; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m index 0f8c75ce..92c0fee7 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m @@ -36,7 +36,6 @@ @implementation DSKeyManager - (instancetype)initWithChain:(DSChain *)chain { NSParameterAssert(chain); if (!(self = [super init])) return nil; - DSLog(@"[%@] DSKeyManager.initWithChain: %@: ", chain.name, chain); return self; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h index ac26c452..fc9446d5 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager+Protected.h @@ -30,11 +30,12 @@ NS_ASSUME_NONNULL_BEGIN @interface DSMasternodeManager (Protected) +@property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext; + - (instancetype)initWithChain:(DSChain *_Nonnull)chain; - (void)setUp; - (BOOL)restoreState; -//- (void)loadFileDistributedMasternodeLists; - (void)wipeMasternodeInfo; - (void)getRecentMasternodeList; - (void)getCurrentMasternodeListWithSafetyDelay:(uint32_t)safetyDelay; @@ -47,14 +48,8 @@ NS_ASSUME_NONNULL_BEGIN ownerKeyIndex:(uint32_t)ownerKeyIndex onChain:(DSChain *)chain; -//+ (nullable NSError *)saveMasternodeList:(DMasternodeList *)masternodeList -// toChain:(DSChain *)chain -// havingModifiedMasternodes:(DMasternodeEntryMap *)modifiedMasternodes -// createUnknownBlocks:(BOOL)createUnknownBlocks -// inContext:(NSManagedObjectContext *)context; - (BOOL)isMasternodeListOutdated; - (BOOL)processRequestFromFileForBlockHash:(UInt256)blockHash; -- (void)issueWithMasternodeListFromPeer:(DSPeer *)peer; - (void)printEngineStatus; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h index 0d498a8b..07a6311d 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h @@ -25,7 +25,6 @@ #import "DSChain.h" #import "DSKeyManager.h" -//#import "DSMasternodeListStore.h" #import "DSPeer.h" #import @@ -68,7 +67,7 @@ FOUNDATION_EXPORT NSString *const DSQuorumListDidChangeNotification; - (BOOL)hasMasternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port; - (DMasternodeList *_Nullable)masternodeListForBlockHash:(UInt256)blockHash - withBlockHeightLookup:(uint32_t (^_Nullable)(UInt256 blockHash))blockHeightLookup; + withBlockHeightLookup:(uint32_t (^_Nullable)(UInt256 blockHash))blockHeightLookup; - (DMasternodeList *_Nullable)masternodeListForBlockHash:(UInt256)blockHash; - (void)startSync; @@ -83,12 +82,15 @@ FOUNDATION_EXPORT NSString *const DSQuorumListDidChangeNotification; - (void)masternodeListServiceEmptiedRetrievalQueue:(DSMasternodeListService *)service; -- (BOOL)hasBlockForBlockHash:(NSData *)blockHashData; - (NSSet *)blockHashesUsedByMasternodeLists; - (uintptr_t)currentQuorumsOfType:(DLLMQType)type; - (uintptr_t)currentValidQuorumsOfType:(DLLMQType)type; + +- (NSError *_Nullable)requestMasternodeListForBlockHeight:(uint32_t)blockHeight; +- (void)requestMasternodeListForBlockHash:(NSData *)blockHash; +- (void)requestMasternodeListForBaseBlockHash:(NSData *)baseBlockHash blockHash:(NSData *)blockHash; @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m index be17dadd..e1204246 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m @@ -46,8 +46,10 @@ #define ENGINE_STORAGE_LOCATION(chain) [NSString stringWithFormat:@"MNL_ENGINE_%@.dat", chain.name] -#define SAVE_MASTERNODE_DIFF_TO_FILE (1 && DEBUG) -#define SAVE_ERROR_STATE (1 && DEBUG) +#define SAVE_MASTERNODE_DIFF_TO_FILE (0 && DEBUG) +#define SAVE_MASTERNODE_DIFF_ERROR_TO_FILE (1 && DEBUG) +#define SAVE_ERROR_STATE (0 && DEBUG) +#define RESTORE_FROM_CHECKPOINT (0 && DEBUG) @interface DSMasternodeManager () @@ -104,9 +106,12 @@ - (BOOL)hasCurrentMasternodeListInLast30Days { #pragma mark - DSMasternodeListServiceDelegate +// always from chain.networkingQueue - (void)masternodeListServiceEmptiedRetrievalQueue:(DSMasternodeListService *)service { - if (!self.isPendingValidation) + if (!self.isPendingValidation) { + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Masternodes]; [self.chain.chainManager chainFinishedSyncingMasternodeListsAndQuorums:self.chain]; + } } @@ -211,6 +216,7 @@ - (BOOL)restoreEngine { } } +#if RESTORE_FROM_CHECKPOINT - (BOOL)restoreFromCheckpoint { DSCheckpoint *checkpoint = [self.chain lastCheckpointHavingMasternodeList]; if (!checkpoint || !checkpoint.masternodeListName || [checkpoint.masternodeListName isEqualToString:@""]) @@ -230,15 +236,20 @@ - (BOOL)restoreFromCheckpoint { DMnDiffResultDtor(result); return YES; } +#endif +// always from chain.networkingQueue - (BOOL)restoreState { + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Checkpoints]; BOOL restored = [self restoreEngine]; if (!restored) { DSLog(@"%@ No Engine Stored", self.logPrefix); + #if RESTORE_FROM_CHECKPOINT // TODO: checkpoints don't work anymore, since old protocol version support was dropped -// restored = [self restoreFromCheckpoint]; -// if (!restored) -// DSLog(@"%@ No Checkpoint Stored", self.logPrefix); + restored = [self restoreFromCheckpoint]; + if (!restored) + DSLog(@"%@ No Checkpoint Stored", self.logPrefix); + #endif } if (restored) { DMasternodeList *current_list = [self currentMasternodeList]; @@ -249,7 +260,12 @@ - (BOOL)restoreState { DSMasternodeManagerNotificationMasternodeListKey: current_list ? [NSValue valueWithPointer:current_list] : [NSNull null] }]; self.isRestored = YES; + } + self.chain.chainManager.syncState.masternodeListSyncInfo.lastListHeight = DCurrentMasternodeListBlockHeight(self.processor); + self.chain.chainManager.syncState.masternodeListSyncInfo.storedCount = (uint32_t) DKnownMasternodeListsCount(self.processor); + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Checkpoints]; + [self.chain.chainManager notifySyncStateChanged]; return restored; } @@ -264,28 +280,22 @@ - (BOOL)restoreState { } - (void)setUp { - [self restoreState]; + dispatch_async(self.chain.networkingQueue, ^{ + [self restoreState]; + }); [DSLocalMasternodeEntity loadLocalMasternodesInContext:self.managedObjectContext onChainEntity:[self.chain chainEntityInContext:self.managedObjectContext]]; } - (void)reloadMasternodeLists { DProcessorClear(self.processor); - [self.chain.chainManager notifyMasternodeSyncStateChange:UINT32_MAX storedCount:0]; - [self restoreState]; -} - -- (BOOL)hasBlockForBlockHash:(NSData *)blockHashData { - UInt256 blockHash = blockHashData.UInt256; - BOOL hasBlock = [self.chain blockForBlockHash:blockHash] != nil; - if (!hasBlock) { - hasBlock = [DSMerkleBlockEntity hasBlocksWithHash:blockHash inContext:self.managedObjectContext]; - } - if (!hasBlock && self.chain.isTestnet) { - //We can trust insight if on testnet - [self.chain blockUntilGetInsightForBlockHash:blockHash]; - hasBlock = !![[self.chain insightVerifiedBlocksByHashDictionary] objectForKey:blockHashData]; - } - return hasBlock; + [self notify:DSCurrentMasternodeListDidChangeNotification userInfo:@{ + DSChainManagerNotificationChainKey: self.chain, + DSMasternodeManagerNotificationMasternodeListKey: [NSNull null] + }]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager notifyMasternodeSyncStateChange:UINT32_MAX storedCount:0]; + [self restoreState]; + }); } - (DMasternodeList *)currentMasternodeList { @@ -297,18 +307,19 @@ - (void)wipeMasternodeInfo { DProcessorClear(self.processor); [self.masternodeListDiffService cleanAllLists]; [self.quorumRotationService cleanAllLists]; - [self.chain.chainManager notifyMasternodeSyncStateChange:UINT32_MAX storedCount:0]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:DSMasternodeListDidChangeNotification - object:nil - userInfo:@{ - DSChainManagerNotificationChainKey: self.chain - }]; - [[NSNotificationCenter defaultCenter] postNotificationName:DSQuorumListDidChangeNotification - object:nil - userInfo:@{ - DSChainManagerNotificationChainKey: self.chain - }]; + dash_spv_masternode_processor_processing_processor_MasternodeProcessor_reinit_engine(self.processor, self.chain.chainType, [self.chain createDiffConfig]); + uint32_t lastListHeight = DCurrentMasternodeListBlockHeight(self.processor); + uint32_t storedCount = (uint32_t) DKnownMasternodeListsCount(self.processor); + DMasternodeList *current_list = [self currentMasternodeList]; + dispatch_async(self.chain.networkingQueue, ^{ +// [self.chain.chainManager.syncState.masternodeListSyncInfo resetSyncKind]; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = 0; + self.chain.chainManager.syncState.masternodeListSyncInfo.lastListHeight = lastListHeight; + self.chain.chainManager.syncState.masternodeListSyncInfo.storedCount = storedCount; + [self.chain.chainManager notifySyncStateChanged]; + + [self notifyCurrentListChanged:current_list]; }); } @@ -327,14 +338,17 @@ - (DMasternodeList *)masternodeListForBlockHash:(UInt256)blockHash // MARK: - Requesting Masternode List +// always from chain.networkingQueue - (void)startSync { DSLog(@"%@ [Start]", self.logPrefix); self.isSyncing = YES; if (!self.isRestored) [self restoreState]; [self getRecentMasternodeList]; + [self.chain.chainManager.syncState addSyncKind:DSSyncStateExtKind_Masternodes]; } +// always from chain.networkingQueue - (void)stopSync { DSLog(@"%@ [Stop]", self.logPrefix); self.isSyncing = NO; @@ -342,6 +356,12 @@ - (void)stopSync { if (self.chain.isRotatedQuorumsPresented) [self.quorumRotationService stop]; [self.masternodeListDiffService stop]; + + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = 0; + [self.chain.chainManager.syncState.masternodeListSyncInfo resetSyncKind]; + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Masternodes]; + [self.chain.chainManager notifySyncStateChanged]; } - (void)getRecentMasternodeList { @@ -353,11 +373,22 @@ - (void)getRecentMasternodeList { return; } NSData *blockHash = uint256_data(merkleBlock.blockHash); - if (!self.isPendingValidation && (!self.hasHandledQrInfoPipeline || [self isQRInfoOutdated])) + if (!self.isPendingValidation && (!self.hasHandledQrInfoPipeline || [self isQRInfoOutdated])) { [self.quorumRotationService getRecent:blockHash]; + } + if (!self.isPendingValidation && self.hasHandledQrInfoPipeline && [self isMasternodeListOutdated]) { + + NSUInteger newCount = [self.masternodeListDiffService addToRetrievalQueue:blockHash]; + NSUInteger maxAmount = self.masternodeListDiffService.retrievalQueueMaxAmount; + [self.masternodeListDiffService dequeueMasternodeListRequest]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = (uint32_t) maxAmount; + [self.chain.chainManager notifySyncStateChanged]; + }); - if (!self.isPendingValidation && self.hasHandledQrInfoPipeline && [self isMasternodeListOutdated]) - [self.masternodeListDiffService getRecent:blockHash]; + + } } @@ -408,28 +439,6 @@ - (BOOL)processRequestFromFileForBlockHash:(UInt256)blockHash { return YES; } - -// MARK: - Deterministic Masternode List Sync - -- (DSBlock *)lastBlockForBlockHash:(UInt256)blockHash fromPeer:(DSPeer *)peer { - DSBlock *lastBlock = nil; - if ([self.chain heightForBlockHash:blockHash]) { - lastBlock = [[peer.chain terminalBlocks] objectForKey:uint256_obj(blockHash)]; - if (!lastBlock && [peer.chain allowInsightBlocksForVerification]) { - NSData *blockHashData = uint256_data(blockHash); - lastBlock = [[peer.chain insightVerifiedBlocksByHashDictionary] objectForKey:blockHashData]; - if (!lastBlock && peer.chain.isTestnet) { - //We can trust insight if on testnet - [self.chain blockUntilGetInsightForBlockHash:blockHash]; - lastBlock = [[peer.chain insightVerifiedBlocksByHashDictionary] objectForKey:blockHashData]; - } - } - } else { - lastBlock = [peer.chain recentTerminalBlockForBlockHash:blockHash]; - } - return lastBlock; -} - - (void)issueWithMasternodeListFromPeer:(DSPeer *)peer { [self.chain.chainManager chain:self.chain badMasternodeListReceivedFromPeer:peer]; NSArray *faultyPeers = [[NSUserDefaults standardUserDefaults] arrayForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; @@ -439,6 +448,12 @@ - (void)issueWithMasternodeListFromPeer:(DSPeer *)peer { [self.masternodeListDiffService cleanListsRetrievalQueue]; [self.quorumRotationService cleanListsRetrievalQueue]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = 0; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = 0; + [self.chain.chainManager notifySyncStateChanged]; + }); + [self.chain.masternodeManager getRecentMasternodeList]; } else { if (!faultyPeers) { @@ -499,31 +514,74 @@ - (void)peer:(DSPeer *)peer relayedMasternodeDiffMessage:(NSData *)message { [self issueWithMasternodeListFromPeer:peer]; break; } - #if SAVE_MASTERNODE_DIFF_TO_FILE + #if SAVE_MASTERNODE_DIFF_ERROR_TO_FILE [self writeToDisk:[NSString stringWithFormat:@"MNL_ERR__%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; #endif DMnDiffResultDtor(result); dispatch_group_leave(self.processingGroup); return; } + [[NSUserDefaults standardUserDefaults] removeObjectForKey:CHAIN_FAULTY_DML_MASTERNODE_PEERS]; if (self.isSyncing) { + u256 *base_block_hash = dashcore_hash_types_BlockHash_inner(result->ok->o_0); u256 *block_hash = dashcore_hash_types_BlockHash_inner(result->ok->o_1); - NSData *blockHashData = NSDataFromPtr(block_hash); - DSLog(@"%@ MNLISTDIFF: OK: %@", self.logPrefix, blockHashData.hexString); + UInt256 baseBlockHash = u256_cast(base_block_hash); + UInt256 blockHash = u256_cast(block_hash); + DSLog(@"%@ MNLISTDIFF: OK: %@", self.logPrefix, uint256_hex(blockHash)); + #if SAVE_MASTERNODE_DIFF_TO_FILE + [self writeToDisk:[NSString stringWithFormat:@"MNL_%@_%@__%d.dat", uint256_hex(baseBlockHash), uint256_hex(blockHash), peer.version] data:message]; + #endif + u256_dtor(base_block_hash); u256_dtor(block_hash); - [self.masternodeListDiffService removeFromRetrievalQueue:blockHashData]; + uint32_t newCount = (uint32_t) [self.masternodeListDiffService removeFromRetrievalQueue:uint256_data(blockHash)]; + uint32_t maxQueueCount = (uint32_t) [self.masternodeListDiffService retrievalQueueMaxAmount]; + [self.masternodeListDiffService removeRequestInRetrievalForBaseBlockHash:baseBlockHash blockHash:blockHash]; if ([self.masternodeListDiffService hasActiveQueue]) { + uint32_t lastListHeight = DCurrentMasternodeListBlockHeight(self.processor); + uint32_t storedCount = (uint32_t) DKnownMasternodeListsCount(self.processor); + + DMasternodeList *current_list = [self currentMasternodeList]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = maxQueueCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.lastListHeight = lastListHeight; + self.chain.chainManager.syncState.masternodeListSyncInfo.storedCount = storedCount; + [self.chain.chainManager notifySyncStateChanged]; + [self notifyCurrentListChanged:current_list]; + }); [self.masternodeListDiffService dequeueMasternodeListRequest]; } else if (self.isPendingValidation) { + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = maxQueueCount; + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_QrInfo]; + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Diffs]; + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Quorums]; + [self.chain.chainManager notifySyncStateChanged]; + }); NSError *quorumValidationError = [self verifyQuorums]; if (quorumValidationError) { - DMnDiffResultDtor(result); - dispatch_group_leave(self.processingGroup); - return; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Quorums]; + }); + } else { + [self finishIntitialQrInfoPipeline]; } - [self finishIntitialQrInfoPipeline]; + } else { + uint32_t lastListHeight = DCurrentMasternodeListBlockHeight(self.processor); + uint32_t storedCount = (uint32_t) DKnownMasternodeListsCount(self.processor); + DMasternodeList *current_list = [self currentMasternodeList]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = maxQueueCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.lastListHeight = lastListHeight; + self.chain.chainManager.syncState.masternodeListSyncInfo.storedCount = storedCount; + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Diffs]; + [self.chain.chainManager notifySyncStateChanged]; + [self notifyCurrentListChanged:current_list]; + }); } } DMnDiffResultDtor(result); @@ -568,7 +626,7 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin default: break; } - #if SAVE_MASTERNODE_DIFF_TO_FILE + #if SAVE_MASTERNODE_DIFF_ERROR_TO_FILE [self writeToDisk:[NSString stringWithFormat:@"QRINFO_ERR_%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; #endif #if SAVE_ERROR_STATE @@ -590,24 +648,35 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin std_collections_BTreeSet_dashcore_hash_types_BlockHash *missed_hashes = result->ok; DSLog(@"%@ QRINFO: OK: %u", self.logPrefix, (uint32_t) missed_hashes->count); -// #if SAVE_MASTERNODE_DIFF_TO_FILE -// [self writeToDisk:[NSString stringWithFormat:@"QRINFO_MISSED_%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; -// #endif + #if SAVE_MASTERNODE_DIFF_TO_FILE + [self writeToDisk:[NSString stringWithFormat:@"QRINFO_MISSED_%d_%f.dat", peer.version, [NSDate timeIntervalSinceReferenceDate]] data:message]; + #endif if (missed_hashes->count > 0) { self.isPendingValidation = YES; [self.quorumRotationService cleanAllLists]; NSArray *missedHashes = [NSArray ffi_from_block_hash_btree_set:missed_hashes]; - [self.masternodeListDiffService addToRetrievalQueueArray:missedHashes]; + NSUInteger newCount = [self.masternodeListDiffService addToRetrievalQueueArray:missedHashes]; + NSUInteger maxAmount = self.masternodeListDiffService.retrievalQueueMaxAmount; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Diffs]; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = (uint32_t) maxAmount; + [self.chain.chainManager notifySyncStateChanged]; + }); [self.masternodeListDiffService dequeueMasternodeListRequest]; } else { + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Quorums]; + }); NSError *quorumValidationError = [self verifyQuorums]; if (quorumValidationError) { - DQRInfoResultDtor(result); - dispatch_group_leave(self.processingGroup); - return; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Quorums]; + }); + } else { + [self finishIntitialQrInfoPipeline]; } - [self finishIntitialQrInfoPipeline]; } DQRInfoResultDtor(result); @@ -618,8 +687,19 @@ - (void)tryToProcessQrInfo:(DSPeer *)peer message:(NSData *)message attempt:(uin - (void)finishIntitialQrInfoPipeline { self.hasHandledQrInfoPipeline = YES; self.isPendingValidation = NO; + DMasternodeList *current_list = [self currentMasternodeList]; + uint32_t lastListHeight = DCurrentMasternodeListBlockHeight(self.processor); + uint32_t storedCount = (uint32_t) DKnownMasternodeListsCount(self.processor); [self.quorumRotationService cleanAllLists]; - [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Quorums]; + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_QrInfo]; + self.chain.chainManager.syncState.masternodeListSyncInfo.lastListHeight = lastListHeight; + self.chain.chainManager.syncState.masternodeListSyncInfo.storedCount = storedCount; + [self.chain.chainManager notifySyncStateChanged]; + [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + [self notifyCurrentListChanged:current_list]; + }); [self.quorumRotationService dequeueMasternodeListRequest]; } @@ -657,4 +737,52 @@ - (uintptr_t)currentValidQuorumsOfType:(DLLMQType)type { return dash_spv_masternode_processor_processing_processor_MasternodeProcessor_current_valid_quorums_of_type_count(self.processor, &type); } + +- (NSError *_Nullable)requestMasternodeListForBlockHeight:(uint32_t)blockHeight { + DSMerkleBlock *merkleBlock = [self.chain blockAtHeight:blockHeight]; + if (!merkleBlock) + return [NSError errorWithDomain:@"DashSync" code:600 userInfo:@{NSLocalizedDescriptionKey: @"Unknown block"}]; + [self requestMasternodeListForBlockHash:uint256_data(merkleBlock.blockHash)]; + return nil; +} +- (void)requestMasternodeListForBlockHash:(NSData *)blockHash { + NSUInteger newCount = [self.masternodeListDiffService addToRetrievalQueue:blockHash]; + NSUInteger maxAmount = self.masternodeListDiffService.retrievalQueueMaxAmount; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Diffs]; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = (uint32_t) maxAmount; + [self.chain.chainManager notifySyncStateChanged]; + }); + [self.masternodeListDiffService dequeueMasternodeListRequest]; + +} +- (void)requestMasternodeListForBaseBlockHash:(NSData *)baseBlockHash blockHash:(NSData *)blockHash { + NSUInteger maxAmount = self.masternodeListDiffService.retrievalQueueMaxAmount; + NSUInteger newCount = [self.masternodeListDiffService addToRetrievalQueueArray:@[baseBlockHash, blockHash]]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Diffs]; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = (uint32_t) maxAmount; + [self.chain.chainManager notifySyncStateChanged]; + }); + [self.masternodeListDiffService dequeueMasternodeListRequest]; +} + +- (void)notifyCurrentListChanged:(DMasternodeList *_Nullable)list { + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSCurrentMasternodeListDidChangeNotification object:nil userInfo:@{ + DSChainManagerNotificationChainKey: self.chain, + DSMasternodeManagerNotificationMasternodeListKey: list ? [NSValue valueWithPointer:list] : [NSNull null] + }]; + [[NSNotificationCenter defaultCenter] postNotificationName:DSMasternodeListDidChangeNotification object:nil userInfo:@{ + DSChainManagerNotificationChainKey: self.chain, + }]; + [[NSNotificationCenter defaultCenter] postNotificationName:DSQuorumListDidChangeNotification object:nil userInfo:@{ + DSChainManagerNotificationChainKey: self.chain, + }]; + }); + +} + @end diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m index ef07942e..d54e94b4 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m @@ -668,6 +668,7 @@ - (NSArray *)registeredDevnetPeerServices { // MARK: - Using Masternode List for connectivitity +// always from chain.networkingQueue - (void)useMasternodeList:(DMasternodeList *)masternodeList withConnectivityNonce:(uint64_t)connectivityNonce { self.masternodeList = masternodeList; @@ -677,7 +678,7 @@ - (void)useMasternodeList:(DMasternodeList *)masternodeList NSArray *peers = [self peers:500 withConnectivityNonce:connectivityNonce]; - + [self.chain.chainManager.syncState.peersSyncInfo addSyncKind:DSPeersSyncStateKind_Selection]; @synchronized(self) { if (!_peers) { _peers = [NSMutableOrderedSet orderedSetWithArray:peers]; @@ -690,7 +691,7 @@ - (void)useMasternodeList:(DMasternodeList *)masternodeList } if (peers.count > 1 && peers.count < 1000) [self savePeers]; // peer relaying is complete when we receive <1000 - + [self.chain.chainManager.syncState.peersSyncInfo removeSyncKind:DSPeersSyncStateKind_Selection]; if (connected) { DSLog(@"[%@] [DSPeerManager] useMasternodeList -> connect", self.chain.name); [self connect]; @@ -724,7 +725,9 @@ - (void)startBackgroundMode:(BOOL)performDisconnects { - (void)connect { DSLog(@"[%@] [DSPeerManager] connect", self.chain.name); self.desiredState = DSPeerManagerDesiredState_Connected; + dispatch_async(self.networkingQueue, ^{ + [self.chain.chainManager.syncState.peersSyncInfo addSyncKind:DSPeersSyncStateKind_Connecting]; if ([self.chain syncsBlockchain] && ![self.chain canConstructAFilter]) { DSLog(@"[%@] [DSPeerManager] failed to connect: check that wallet is created", self.chain.name); return; // check to make sure the wallet has been created if only are a basic wallet with no dash features @@ -775,7 +778,12 @@ - (void)connect { DSPeer *peer = peers[(NSUInteger)(pow(arc4random_uniform((uint32_t)peers.count), 2) / peers.count)]; if (peer && ![self.connectedPeers containsObject:peer]) { - [peer setChainDelegate:self.chainManager peerDelegate:self transactionDelegate:self.transactionManager governanceDelegate:self.governanceSyncManager sporkDelegate:self.sporkManager masternodeDelegate:self.masternodeManager queue:self.networkingQueue]; + [peer setChainDelegate:self.chainManager + peerDelegate:self + transactionDelegate:self.transactionManager + governanceDelegate:self.governanceSyncManager + sporkDelegate:self.sporkManager + masternodeDelegate:self.masternodeManager]; peer.earliestKeyTime = earliestWalletCreationTime; [self.mutableConnectedPeers addObject:peer]; @@ -793,6 +801,7 @@ - (void)connect { if (peers.count == 0) { [self chainSyncStopped]; DSLog(@"[%@] [DSPeerManager] No peers found -> SyncFailed", self.chain.name); + [self.chain.chainManager.syncState.peersSyncInfo removeSyncKind:DSPeersSyncStateKind_Connecting]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:DSChainManagerSyncFailedNotification object:nil @@ -814,6 +823,12 @@ - (void)disconnect:(DSDisconnectReason)reason { } - (void)disconnectDownloadPeerForError:(NSError *)error withCompletion:(void (^_Nullable)(BOOL success))completion { + if (!self.connected) { + dispatch_async(self.networkingQueue, ^{ + if (completion) completion(YES); + }); + return; + } [self.downloadPeer disconnectWithError:error]; dispatch_async(self.networkingQueue, ^{ if (self.downloadPeer) { // disconnect the current download peer so a new random one will be selected @@ -853,6 +868,7 @@ - (void)chainSyncStopped { // MARK: - DSPeerDelegate +// always from chain.networkingQueue - (void)peerConnected:(DSPeer *)peer { NSTimeInterval now = [NSDate timeIntervalSince1970]; @@ -872,7 +888,7 @@ - (void)peerConnected:(DSPeer *)peer { [peer disconnectWithError:ERROR_NO_BLOOM(peer.host)]; return; } - + [self.chain.chainManager.syncState.peersSyncInfo removeSyncKind:DSPeersSyncStateKind_Connecting]; if (self.connected) { if (![self.chain syncsBlockchain]) return; if ([self.chain canConstructAFilter]) { @@ -968,32 +984,36 @@ - (void)peerConnected:(DSPeer *)peer { uint32_t lastTerminalBlockHeight = self.chain.lastTerminalBlockHeight; uint32_t bestPeerLastBlockHeight = bestPeer.lastBlockHeight; bestPeer.currentBlockHeight = lastSyncBlockHeight; - - dispatch_async(dispatch_get_main_queue(), ^{ // setup a timer to detect if the sync stalls - self.chainManager.syncState.hasDownloadPeer = bestPeer; - self.chainManager.syncState.peerManagerConnected = YES; - self.chainManager.syncState.estimatedBlockHeight = estimatedBlockHeight; - self.chainManager.syncState.lastTerminalBlockHeight = lastTerminalBlockHeight; - self.chainManager.syncState.lastSyncBlockHeight = lastSyncBlockHeight; - if ([self.chain syncsBlockchain] && ((lastSyncBlockHeight != lastTerminalBlockHeight) || (lastSyncBlockHeight < bestPeerLastBlockHeight))) { - // start blockchain sync - [self restartSyncTimeout:PROTOCOL_TIMEOUT]; - [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification + self.chainManager.syncState.peersSyncInfo.hasDownloadPeer = bestPeer; + self.chainManager.syncState.peersSyncInfo.peerManagerConnected = YES; + self.chainManager.syncState.estimatedBlockHeight = estimatedBlockHeight; + self.chainManager.syncState.lastTerminalBlockHeight = lastTerminalBlockHeight; + self.chainManager.syncState.lastSyncBlockHeight = lastSyncBlockHeight; + if ([self.chain syncsBlockchain] && ((lastSyncBlockHeight != lastTerminalBlockHeight) || (lastSyncBlockHeight < bestPeerLastBlockHeight))) { + // start blockchain sync + [self restartSyncTimeout:PROTOCOL_TIMEOUT]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; - [self.chainManager chainWillStartSyncingBlockchain:self.chain]; - [self.chainManager chainShouldStartSyncingBlockchain:self.chain onPeer:bestPeer]; - } else { // we're already synced - [self.chainManager chainFinishedSyncingTransactionsAndBlocks:self.chain fromPeer:nil onMainChain:TRUE]; - } + }); + [self.chainManager chainWillStartSyncingBlockchain:self.chain]; + [self.chainManager chainShouldStartSyncingBlockchain:self.chain onPeer:bestPeer]; + } else { // we're already synced + [self.chainManager chainFinishedSyncingTransactionsAndBlocks:self.chain fromPeer:nil onMainChain:TRUE]; + } + dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:DSPeerManagerConnectedPeersDidChangeNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; [[NSNotificationCenter defaultCenter] postNotificationName:DSPeerManagerDownloadPeerDidChangeNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; + + }); } +// always from chain.networkingQueue - (void)peer:(DSPeer *)peer disconnectedWithError:(NSError *)error { DSLog(@"[%@: %@:%d] [DSPeerManager] disconnected %@%@", self.chain.name, peer.host, peer.port, (error ? @", " : @""), (error ? error : @"")); @@ -1012,10 +1032,9 @@ - (void)peer:(DSPeer *)peer disconnectedWithError:(NSError *)error { _connected = NO; [self.chain removeEstimatedBlockHeightOfPeer:peer]; self.downloadPeer = nil; - - self.chainManager.syncState.hasDownloadPeer = NO; + self.chainManager.syncState.peersSyncInfo.hasDownloadPeer = NO; [self.chainManager notifySyncStateChanged]; - + if (self.connectFailures > MAX_CONNECT_FAILURES) self.connectFailures = MAX_CONNECT_FAILURES; } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSSporkManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSSporkManager.m index f92c01ff..96f8aa48 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSSporkManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSSporkManager.m @@ -145,16 +145,15 @@ - (void)performSporkRequest { - (void)getSporks { if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_Sporks)) return; // make sure we care about sporks - if (!self.sporkTimer) { [self performSporkRequest]; self.sporkTimer = [NSTimer scheduledTimerWithTimeInterval:600 repeats:TRUE block:^(NSTimer *_Nonnull timer) { - if (self.lastSyncedSporks < [NSDate timeIntervalSince1970] - 60 * 10) { //wait 10 minutes between requests - [self performSporkRequest]; - } - }]; + if (self.lastSyncedSporks < [NSDate timeIntervalSince1970] - 60 * 10) { //wait 10 minutes between requests + [self performSporkRequest]; + } + }]; } } diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m index 349286cb..0113c2d8 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m @@ -370,9 +370,13 @@ - (void)removeUnrelayedTransactionsFromPeer:(DSPeer *)peer { dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification object:nil - userInfo:@{DSChainManagerNotificationChainKey: self.chain, - DSTransactionManagerNotificationTransactionKey: transaction, - DSTransactionManagerNotificationTransactionChangesKey: @{DSTransactionManagerNotificationTransactionAcceptedStatusKey: @(NO)}}]; + userInfo:@{ + DSChainManagerNotificationChainKey: self.chain, + DSTransactionManagerNotificationTransactionKey: transaction, + DSTransactionManagerNotificationTransactionChangesKey: @{ + DSTransactionManagerNotificationTransactionAcceptedStatusKey: @(NO) + } + }]; }); } } @@ -948,11 +952,28 @@ - (void)confirmPaymentRequest:(DSPaymentRequest *)paymentRequest publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock { DSPaymentProtocolRequest *protocolRequest = [paymentRequest protocolRequestForIdentity:identity onAccount:account inContext:[NSManagedObjectContext viewContext]]; - [self confirmProtocolRequest:protocolRequest forAmount:paymentRequest.amount fromAccount:account acceptInternalAddress:acceptInternalAddress acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:NO mixedOnly:NO requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:nil errorNotificationBlock:errorNotificationBlock]; + [self confirmProtocolRequest:protocolRequest + forAmount:paymentRequest.amount + fromAccount:account + acceptInternalAddress:acceptInternalAddress + acceptReusingAddress:acceptReusingAddress + addressIsFromPasteboard:addressIsFromPasteboard + acceptUncertifiedPayee:NO + mixedOnly:NO +requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt +keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication + requestingAdditionalInfo:additionalInfoRequest + presentChallenge:challenge + transactionCreationCompletion:transactionCreationCompletion + signedCompletion:signedCompletion + publishedCompletion:publishedCompletion + requestRelayCompletion:nil + errorNotificationBlock:errorNotificationBlock]; } // MARK: - Mempools Sync +// always from chain.networkingQueue - (void)fetchMempoolFromPeer:(DSPeer *)peer { DSLog(@"[%@: %@:%d] [DSTransactionManager] fetching mempool from peer", self.chain.name, peer.host, peer.port); if (peer.status != DSPeerStatus_Connected) return; @@ -988,6 +1009,7 @@ - (void)fetchMempoolFromPeer:(DSPeer *)peer { } } if (peer == self.peerManager.downloadPeer) { + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Mempool]; [self.peerManager chainSyncStopped]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:DSChainManagerSyncFinishedNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain}]; @@ -996,6 +1018,7 @@ - (void)fetchMempoolFromPeer:(DSPeer *)peer { }]; } else if (peer == self.peerManager.downloadPeer) { DSLog(@"[%@: %@:%d] [DSTransactionManager] fetching mempool ping failure on download peer", self.chain.name, peer.host, peer.port); + [self.chain.chainManager.syncState removeSyncKind:DSSyncStateExtKind_Mempool]; [self.peerManager chainSyncStopped]; dispatch_async(dispatch_get_main_queue(), ^{ @@ -1010,10 +1033,13 @@ - (void)fetchMempoolFromPeer:(DSPeer *)peer { }]; } +// always from chain.networkingQueue - (void)fetchMempoolFromNetwork { // this can come from any queue if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_Mempools)) return; // make sure we care about mempool - for (DSPeer *peer in self.peerManager.connectedPeers) { // after syncing, load filters and get mempools from other peers + [self.chain.chainManager.syncState addSyncKind:DSSyncStateExtKind_Mempool]; + for (DSPeer *peer in self.peerManager.connectedPeers) { + // after syncing, load filters and get mempools from other peers [self fetchMempoolFromPeer:peer]; } } @@ -1244,21 +1270,23 @@ - (void)peer:(DSPeer *)peer hasTransactionWithHash:(UInt256)txHash { [self.txRequests[hash] removeObject:peer]; } -//The peer has sent us a transaction we are interested in and that we did not send ourselves +// always from chain.networkingQueue +// The peer has sent us a transaction we are interested in and that we did not send ourselves - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBlock:(DSBlock *)block { - NSValue *hash = uint256_obj(transaction.txHash); + UInt256 txHash = transaction.txHash; + NSValue *hash = uint256_obj(txHash); BOOL syncing = (self.chain.lastSyncBlockHeight < self.chain.estimatedBlockHeight); void (^callback)(NSError *error) = self.publishedCallback[hash]; if (peer) { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] relayed transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] relayed transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] relayed transaction %@", self.chain.name, peer.host, peer.port, @""); #endif } else { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] accepting local transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] accepting local transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] accepting local transaction %@", self.chain.name, peer.host, peer.port, @""); #endif @@ -1273,13 +1301,13 @@ - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBl if (![self.chain transactionHasLocalReferences:transaction]) { if (peer) { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, @""); #endif } else { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] no account or local references for transaction %@", self.chain.name, peer.host, peer.port, @""); #endif @@ -1288,13 +1316,13 @@ - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBl } else { if (peer) { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, @""); #endif } else { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] no account for transaction with local references %@", self.chain.name, peer.host, peer.port, @""); #endif @@ -1313,13 +1341,13 @@ - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBl } else { if (peer) { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, @""); #endif } else { #if DEBUG - DSLogPrivate(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, hash); + DSLogPrivate(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, uint256_hex(txHash)); #else DSLog(@"[%@: %@:%d] could not register transaction %@", self.chain.name, peer.host, peer.port, @""); #endif @@ -1364,10 +1392,10 @@ - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBl } } - DSInstantSendTransactionLock *transactionLockReceivedEarlier = [self.instantSendLocksWaitingForTransactions objectForKey:uint256_data(transaction.txHash)]; + DSInstantSendTransactionLock *transactionLockReceivedEarlier = [self.instantSendLocksWaitingForTransactions objectForKey:uint256_data(txHash)]; if (transactionLockReceivedEarlier) { - [self.instantSendLocksWaitingForTransactions removeObjectForKey:uint256_data(transaction.txHash)]; + [self.instantSendLocksWaitingForTransactions removeObjectForKey:uint256_data(txHash)]; [transaction setInstantSendReceivedWithInstantSendLock:transactionLockReceivedEarlier]; [transactionLockReceivedEarlier saveInitial]; } @@ -1414,12 +1442,12 @@ - (void)peer:(DSPeer *)peer relayedTransaction:(DSTransaction *)transaction inBl if (callback) [self.publishedCallback removeObjectForKey:hash]; for (DSAccount *account in accountsAcceptingTransaction) { - if (account && [self.txRelays[hash] count] >= self.peerManager.maxConnectCount && - [account transactionForHash:transaction.txHash].blockHeight == TX_UNCONFIRMED && - [account transactionForHash:transaction.txHash].timestamp == 0) { - [account setBlockHeight:TX_UNCONFIRMED - andTimestamp:[NSDate timeIntervalSince1970] - forTransactionHashes:@[hash]]; // set timestamp when tx is verified + if (account && [self.txRelays[hash] count] >= self.peerManager.maxConnectCount) { + DSTransaction *tx = [account transactionForHash:txHash]; + if (tx.blockHeight == TX_UNCONFIRMED && tx.timestamp == 0) + [account setBlockHeight:TX_UNCONFIRMED + andTimestamp:[NSDate timeIntervalSince1970] + forTransactionHashes:@[hash]]; // set timestamp when tx is verified } } @@ -1645,6 +1673,7 @@ - (void)checkInstantSendLocksWaitingForQuorums { // MARK: Blocks +// always from chain.networkingQueue - (void)peer:(DSPeer *)peer relayedHeader:(DSMerkleBlock *)block { //DSLogPrivate(@"relayed block %@ total transactions %d %u",uint256_hex(block.blockHash), block.totalTransactions,block.timestamp); // ignore block headers that are newer than 2 days before earliestKeyTime (headers have 0 totalTransactions) @@ -1660,6 +1689,7 @@ - (void)peer:(DSPeer *)peer relayedHeader:(DSMerkleBlock *)block { [self.chain addBlock:block receivedAsHeader:YES fromPeer:peer]; } +// always from chain.networkingQueue - (void)peer:(DSPeer *)peer relayedBlock:(DSMerkleBlock *)block { if (!self.chainManager.syncPhase) { DSLog(@"[%@: %@:%d] Block was received after reset, ignoring it", self.chain.name, peer.host, peer.port); @@ -1752,6 +1782,7 @@ - (void)peer:(DSPeer *)peer relayedTooManyOrphanBlocks:(NSUInteger)orphanBlockCo [self.peerManager peerMisbehaving:peer errorMessage:@"Too many orphan blocks"]; } +// always from chain.networkingQueue - (void)peer:(DSPeer *)peer relayedChainLock:(DSChainLock *)chainLock { BOOL verified = [chainLock verifySignature]; UInt256 clBlockHash = uint256_reverse(chainLock.blockHash); @@ -1766,7 +1797,7 @@ - (void)peer:(DSPeer *)peer relayedChainLock:(DSChainLock *)chainLock { [[NSNotificationCenter defaultCenter] postNotificationName:DSChainBlockWasLockedNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain, DSChainNotificationBlockKey: block}]; }); } else { - DSLog(@"[%@: %@:%d] no block for chain lock %@", self.chain.name, peer.host, peer.port, uint256_hex(clBlockHash)); + DSLog(@"[%@: %@:%d] no block %@ for chain lock", self.chain.name, peer.host, peer.port, uint256_hex(clBlockHash)); [self.chainLocksWaitingForMerkleBlocks setObject:chainLock forKey:uint256_data(clBlockHash)]; } diff --git a/DashSync/shared/Models/Managers/Service Managers/DSInsightManager.m b/DashSync/shared/Models/Managers/Service Managers/DSInsightManager.m index a219f699..a1b5f6d3 100644 --- a/DashSync/shared/Models/Managers/Service Managers/DSInsightManager.m +++ b/DashSync/shared/Models/Managers/Service Managers/DSInsightManager.m @@ -23,7 +23,8 @@ #define INSIGHT_URL @"https://insight.dash.org/insight-api-dash" #define INSIGHT_FAILOVER_URL @"https://insight.dash.show/api" -#define TESTNET_INSIGHT_URL @"https://testnet-insight.dashevo.org/insight-api-dash" +#define TESTNET_INSIGHT_URL @"https://insight.testnet.networks.dash.org/insight-api" +//#define TESTNET_INSIGHT_URL @"https://testnet-insight.dashevo.org/insight-api-dash" @implementation DSInsightManager @@ -117,7 +118,7 @@ - (void)blockForBlockHeight:(uint32_t)blockHeight onChain:(DSChain *)chain compl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0]; req.HTTPMethod = @"GET"; - DSLogPrivate(@"%@ GET: %@", req.URL.absoluteString, + DSLogPrivate(@"[%@] %@ GET: %@", chain.name, req.URL.absoluteString, [[NSString alloc] initWithData:req.HTTPBody encoding:NSUTF8StringEncoding]); @@ -144,6 +145,7 @@ - (void)blockForBlockHeight:(uint32_t)blockHeight onChain:(DSChain *)chain compl NSData *chainWork = [json[@"chainwork"] hexToData]; NSNumber *height = json[@"height"]; DSBlock *block = [[DSBlock alloc] initWithVersion:[version unsignedIntValue] blockHash:blockHash.reverse.UInt256 prevBlock:previousBlockHash.reverse.UInt256 timestamp:timestamp.unsignedIntValue merkleRoot:merkleRoot.reverse.UInt256 target:[targetString.hexToData UInt32AtOffset:0] chainWork:chainWork.reverse.UInt256 height:height.unsignedIntValue onChain:chain]; + DSLogPrivate(@"[%@] BLOCK FROM INSIGHT: %@", chain.name, block); completion(block, nil); }] resume]; diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m index 5a4b0b0b..03b2e2b9 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m +++ b/DashSync/shared/Models/Masternode/DSMasternodeListDiffService.m @@ -41,7 +41,7 @@ - (instancetype)initWithChain:(DSChain *)chain { } - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [MasternodeManager::DiffService] ", self.chain.name]; + return [NSString stringWithFormat:@"[%@] [MasternodeManager::DiffService]", self.chain.name]; } - (void)getRecent:(NSData *)blockHash { @@ -50,24 +50,21 @@ - (void)getRecent:(NSData *)blockHash { } - (void)composeMasternodeListRequest:(NSOrderedSet *)list { - NSMutableString *debugString = [NSMutableString stringWithString:@"Needed:\n"]; - for (NSData *data in list) { - uint32_t h = [self.chain heightForBlockHash:data.UInt256]; - [debugString appendFormat:@"%u: %@\n", h, data.hexString]; - } - DSLog(@"%@ composeMasternodeListRequest: \n%@", self.logPrefix, debugString); -// [self.chain.masternodeManager printEngineStatus]; for (NSData *blockHashData in list) { // we should check the associated block still exists - if ([self.chain.masternodeManager hasBlockForBlockHash:blockHashData]) { + if ([self hasBlockForBlockHash:blockHashData]) { //there is the rare possibility we have the masternode list as a checkpoint, so lets first try that NSUInteger pos = [list indexOfObject:blockHashData]; UInt256 blockHash = blockHashData.UInt256; BOOL success = [self.chain.masternodeManager processRequestFromFileForBlockHash:blockHash]; if (success) { - [self removeFromRetrievalQueue:blockHashData]; - if (![self hasActiveQueue]) - [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + NSUInteger newCount = [self removeFromRetrievalQueue:blockHashData]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + [self.chain.chainManager notifySyncStateChanged]; + if (!newCount) + [self.chain.chainManager.transactionManager checkWaitingForQuorums]; + }); } else { // we need to go get it uint32_t blockHeight = [self.chain heightForBlockHash:blockHash]; @@ -79,12 +76,24 @@ - (void)composeMasternodeListRequest:(NSOrderedSet *)list { uint32_t prevInQueueBlockHeight = [self.chain heightForBlockHash:u256_cast(prev_in_queue_block_hash)]; UInt256 previousBlockHash = pos ? (prevKnownHeight > prevInQueueBlockHeight ? prevKnownBlockHash : prevInQueueBlockHash) : prevKnownBlockHash; // request at: every new block -// NSAssert(([self.store heightForBlockHash:previousBlockHash] != UINT32_MAX) || uint256_is_zero(previousBlockHash), @"This block height should be known"); - [self requestMasternodeListDiff:previousBlockHash forBlockHash:blockHash]; + // NSAssert(([self.store heightForBlockHash:previousBlockHash] != UINT32_MAX) || uint256_is_zero(previousBlockHash), @"This block height should be known"); + if (uint256_eq(previousBlockHash, blockHash)) { + NSUInteger newCount = [self removeFromRetrievalQueue:blockHashData]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + [self.chain.chainManager notifySyncStateChanged]; + }); + } else { + [self requestMasternodeListDiff:previousBlockHash forBlockHash:blockHash]; + } } } else { DSLog(@"%@ Missing block (%@)", self.logPrefix, blockHashData.hexString); - [self removeFromRetrievalQueue:blockHashData]; + NSUInteger newCount = [self removeFromRetrievalQueue:blockHashData]; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + [self.chain.chainManager notifySyncStateChanged]; + }); } } } @@ -93,11 +102,14 @@ - (void)fetchMasternodeListsToRetrieve:(void (^)(NSOrderedSet *listsTo //DSLog(@"%@ fetchMasternodeListToRetrieve...: %u", self.logPrefix, [self hasActiveQueue]); if (![self hasActiveQueue]) { DSLog(@"%@ No masternode lists in retrieval", self.logPrefix); - [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_Diffs]; + [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; + }); return; } if ([self.requestsInRetrieval count]) { - //DSLog(@"%@ Already in retrieval", self.logPrefix); + DSLog(@"%@ Already in retrieval", self.logPrefix); return; } if ([self peerIsDisconnected]) { @@ -141,7 +153,6 @@ - (NSUInteger)addToRetrievalQueue:(NSData *)masternodeBlockHashData { return ([self.chain heightForBlockHash:obj1.UInt256] < [self.chain heightForBlockHash:obj2.UInt256]) ? NSOrderedAscending : NSOrderedDescending; }]; } - [self notifyQueueChange:newCount maxAmount:maxAmount]; return newCount; } @@ -150,10 +161,12 @@ - (NSUInteger)addToRetrievalQueueArray:(NSArray *_Nonnull)masternodeBl NSMutableArray *nonEmptyBlockHashes = [NSMutableArray array]; NSUInteger newCount = 0, maxAmount = 0; @synchronized (_retrievalQueue) { + NSMutableString *debugString = [NSMutableString string]; for (NSData *blockHashData in masternodeBlockHashDataArray) { NSAssert(uint256_is_not_zero(blockHashData.UInt256), @"We should not be adding an empty block hash"); if (uint256_is_not_zero(blockHashData.UInt256)) { [nonEmptyBlockHashes addObject:blockHashData]; + [debugString appendFormat:@"\t%@,\n", blockHashData.hexString]; } } [_retrievalQueue addObjectsFromArray:nonEmptyBlockHashes]; @@ -164,7 +177,6 @@ - (NSUInteger)addToRetrievalQueueArray:(NSArray *_Nonnull)masternodeBl return ([self.chain heightForBlockHash:obj1.UInt256] < [self.chain heightForBlockHash:obj2.UInt256]) ? NSOrderedAscending : NSOrderedDescending; }]; } - [self notifyQueueChange:newCount maxAmount:maxAmount]; return newCount; } @@ -174,9 +186,7 @@ - (NSUInteger)removeFromRetrievalQueue:(NSData *)masternodeBlockHashData { [_retrievalQueue removeObject:masternodeBlockHashData]; newCount = [_retrievalQueue count]; maxAmount = MAX(self.retrievalQueueMaxAmount, newCount); - } - [self notifyQueueChange:newCount maxAmount:maxAmount]; return newCount; } @@ -185,7 +195,6 @@ - (void)cleanListsRetrievalQueue { [_retrievalQueue removeAllObjects]; } self.retrievalQueueMaxAmount = 0; - [self notifyQueueChange:0 maxAmount:0]; } - (BOOL)hasActiveQueue { @@ -196,34 +205,27 @@ - (void)requestMasternodeListDiff:(UInt256)previousBlockHash forBlockHash:(UInt2 DSGetMNListDiffRequest *request = [DSGetMNListDiffRequest requestWithBaseBlockHash:previousBlockHash blockHash:blockHash]; DSMasternodeListRequest *matchedRequest = [self requestInRetrievalFor:previousBlockHash blockHash:blockHash]; if (matchedRequest) { -// DSLog(@"[%@] •••• mnlistdiff request with such a range already in retrieval: %u..%u %@ .. %@", self.chain.name, [self.store heightForBlockHash:previousBlockHash], [self.store heightForBlockHash:blockHash], uint256_hex(previousBlockHash), uint256_hex(blockHash)); +// DSLog(@"[%@] •••• mnlistdiff request with such a range already in retrieval: %@ .. %@", self.chain.name, uint256_hex(previousBlockHash), uint256_hex(blockHash)); return; } uint32_t prev_h = [self.chain heightForBlockHash:previousBlockHash]; uint32_t h = [self.chain heightForBlockHash:blockHash]; + + DSLog(@"%@ Request: %u..%u %@ .. %@", self.logPrefix, prev_h, h, uint256_hex(previousBlockHash), uint256_hex(blockHash)); + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_Diffs]; + }); [self sendMasternodeListRequest:request]; } - (void)notifyQueueChange:(NSUInteger)newCount maxAmount:(NSUInteger)maxAmount { // DSLog(@"%@Queue Changed: %u/%u ", self.logPrefix, (uint32_t)newCount, (uint32_t)maxAmount); - @synchronized (self.chain.chainManager.syncState) { - self.chain.chainManager.syncState.masternodeListSyncInfo.retrievalQueueCount = (uint32_t) newCount; - self.chain.chainManager.syncState.masternodeListSyncInfo.retrievalQueueMaxAmount = (uint32_t) maxAmount; + dispatch_async(self.chain.networkingQueue, ^{ + self.chain.chainManager.syncState.masternodeListSyncInfo.queueCount = (uint32_t) newCount; + self.chain.chainManager.syncState.masternodeListSyncInfo.queueMaxAmount = (uint32_t) maxAmount; [self.chain.chainManager notifySyncStateChanged]; - } + }); } -/// test-only -/// Used for fast obtaining list diff chain for specific block hashes like this: -/// //DSMasternodeListDiffService *service = self.masternodeListDiffService; -// [service sendReversedHashes:@"00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c" blockHash:@"000000e6b51b9aba9754e6b4ef996ef1d142d6cfcc032c1fd7fc78ca6663ee0a"]; -// [service sendReversedHashes:@"000000e6b51b9aba9754e6b4ef996ef1d142d6cfcc032c1fd7fc78ca6663ee0a" blockHash:@"00000009d7c0bcb59acf741f25239f45820eea178b74597d463ca80e104f753b"]; - -//-(void)sendReversedHashes:(NSString *)baseBlockHash blockHash:(NSString *)blockHash { -// DSGetMNListDiffRequest *request = [DSGetMNListDiffRequest requestWithBaseBlockHash:baseBlockHash.hexToData.reverse.UInt256 -// blockHash:blockHash.hexToData.reverse.UInt256]; -// [self sendMasternodeListRequest:request]; -//} - @end diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h b/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h index c62d21c3..83e93fd2 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h +++ b/DashSync/shared/Models/Masternode/DSMasternodeListService+Protected.h @@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN skipPresenceInRetrieval:(BOOL)skipPresenceInRetrieval; - (UInt256)closestKnownBlockHashForBlockHeight:(uint32_t)blockHeight; - (void)startTimeOutObserver; +- (BOOL)hasBlockForBlockHash:(NSData *)blockHashData; @end diff --git a/DashSync/shared/Models/Masternode/DSMasternodeListService.m b/DashSync/shared/Models/Masternode/DSMasternodeListService.m index ca38e666..36b5ba38 100644 --- a/DashSync/shared/Models/Masternode/DSMasternodeListService.m +++ b/DashSync/shared/Models/Masternode/DSMasternodeListService.m @@ -23,6 +23,7 @@ #import "DSGetQRInfoRequest.h" #import "DSMasternodeManager+Protected.h" #import "DSMerkleBlock.h" +#import "DSMerkleBlockEntity+CoreDataClass.h" #import "DSPeerManager+Protected.h" #import "NSData+Dash.h" @@ -109,6 +110,7 @@ - (void)dequeueMasternodeListRequest { - (void)stop { [self cancelTimeOutObserver]; [self cleanAllLists]; + } @@ -176,4 +178,18 @@ - (void)sendMasternodeListRequest:(DSMasternodeListRequest *)request { } } +- (BOOL)hasBlockForBlockHash:(NSData *)blockHashData { + UInt256 blockHash = blockHashData.UInt256; + BOOL hasBlock = [self.chain blockForBlockHash:blockHash] != nil; + if (!hasBlock) { + hasBlock = [DSMerkleBlockEntity hasBlocksWithHash:blockHash inContext:self.chain.masternodeManager.managedObjectContext]; + } + if (!hasBlock && self.chain.isTestnet) { + //We can trust insight if on testnet + [self.chain blockUntilGetInsightForBlockHash:blockHash]; + hasBlock = !![[self.chain insightVerifiedBlocksByHashDictionary] objectForKey:blockHashData]; + } + return hasBlock; +} + @end diff --git a/DashSync/shared/Models/Masternode/DSQuorumRotationService.m b/DashSync/shared/Models/Masternode/DSQuorumRotationService.m index 69b15e94..18b7906c 100644 --- a/DashSync/shared/Models/Masternode/DSQuorumRotationService.m +++ b/DashSync/shared/Models/Masternode/DSQuorumRotationService.m @@ -33,7 +33,7 @@ @interface DSQuorumRotationService () @implementation DSQuorumRotationService - (NSString *)logPrefix { - return [NSString stringWithFormat:@"[%@] [MasternodeManager::QRInfoService] ", self.chain.name]; + return [NSString stringWithFormat:@"[%@] [MasternodeManager::QRInfoService]", self.chain.name]; } - (BOOL)hasRecentQrInfoSync { @@ -45,13 +45,17 @@ - (void)composeMasternodeListRequest:(NSData *)blockHashData { if (!blockHashData) { return; } - if ([self.chain.masternodeManager hasBlockForBlockHash:blockHashData]) { + if ([self hasBlockForBlockHash:blockHashData]) { UInt256 blockHash = blockHashData.UInt256; uint32_t blockHeight = [self.chain heightForBlockHash:blockHash]; UInt256 previousBlockHash = [self closestKnownBlockHashForBlockHeight:blockHeight]; -// @"000000000000000899fdcd85241296146c365b238a655517da8dcd08a8a79b98".hexToData; // NSAssert(([self.store heightForBlockHash:previousBlockHash] != UINT32_MAX) || uint256_is_zero(previousBlockHash), @"This block height should be known"); - [self requestQuorumRotationInfo:previousBlockHash forBlockHash:blockHash]; + if (uint256_eq(previousBlockHash, blockHash)) { + self.retrievalBlockHash = nil; + self.retrievalQueueMaxAmount = 0; + } else { + [self requestQuorumRotationInfo:previousBlockHash forBlockHash:blockHash]; + } } else { DSLog(@"%@ Missing block: %@ (%@)", self.logPrefix, blockHashData.hexString, blockHashData.reverse.hexString); self.retrievalBlockHash = nil; @@ -69,12 +73,15 @@ - (void)dequeueMasternodeListRequest { - (void)fetchMasternodeListToRetrieve:(void (^)(NSData *listsToRetrieve))completion { //DSLog(@"%@ fetchMasternodeListToRetrieve...: %u", self.logPrefix, [self hasActiveQueue]); if (![self hasActiveQueue]) { - DSLog(@"%@ No masternode lists in retrieval", self.logPrefix); - [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; + //DSLog(@"%@ No masternode lists in retrieval", self.logPrefix); + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo removeSyncKind:DSMasternodeListSyncStateKind_QrInfo]; + [self.chain.masternodeManager masternodeListServiceEmptiedRetrievalQueue:self]; + }); return; } if ([self.requestsInRetrieval count]) { - DSLog(@"%@ A masternode list is already in retrieval", self.logPrefix); + //DSLog(@"%@ A masternode list is already in retrieval", self.logPrefix); return; } if ([self peerIsDisconnected]) { @@ -116,6 +123,9 @@ - (void)requestQuorumRotationInfo:(UInt256)previousBlockHash forBlockHash:(UInt2 uint32_t prev_h = [self.chain heightForBlockHash:previousBlockHash]; uint32_t h = [self.chain heightForBlockHash:blockHash]; DSLog(@"%@ Request: %u..%u %@ .. %@", self.logPrefix, prev_h, h, uint256_hex(previousBlockHash), uint256_hex(blockHash)); + dispatch_async(self.chain.networkingQueue, ^{ + [self.chain.chainManager.syncState.masternodeListSyncInfo addSyncKind:DSMasternodeListSyncStateKind_QrInfo]; + }); [self sendMasternodeListRequest:request]; } diff --git a/DashSync/shared/Models/Messages/Masternodes/DSGetMNListDiffRequest.m b/DashSync/shared/Models/Messages/Masternodes/DSGetMNListDiffRequest.m index f31634fc..bc6ab666 100644 --- a/DashSync/shared/Models/Messages/Masternodes/DSGetMNListDiffRequest.m +++ b/DashSync/shared/Models/Messages/Masternodes/DSGetMNListDiffRequest.m @@ -79,5 +79,7 @@ -(BOOL)matchesInRangeWithBaseBlockHash:(UInt256)baseBlockHash blockHash:(UInt256 - (NSString *)logWithBlockHeightLookup:(BlockHeightFinder)blockHeightLookup { return [NSString stringWithFormat:@"%u: %@ .. %u: %@", blockHeightLookup(self.baseBlockHash), uint256_hex(self.baseBlockHash), blockHeightLookup(self.blockHash), uint256_hex(self.blockHash)]; } - +- (BOOL)logging { + return YES; +} @end diff --git a/DashSync/shared/Models/Network/DSPeer.h b/DashSync/shared/Models/Network/DSPeer.h index 9b6fa7a6..0a96dffc 100644 --- a/DashSync/shared/Models/Network/DSPeer.h +++ b/DashSync/shared/Models/Network/DSPeer.h @@ -271,6 +271,7 @@ typedef NS_ENUM(NSInteger, DSPeerType) @property (nonatomic, readonly, weak) id sporkDelegate; @property (nonatomic, readonly, weak) id masternodeDelegate; @property (nonatomic, readonly, weak) id peerChainDelegate; +// it's actually the chain networking queue @property (nonatomic, readonly) dispatch_queue_t delegateQueue; // set this to the timestamp when the wallet was created to improve initial sync time (interval since reference date) @@ -313,7 +314,7 @@ typedef NS_ENUM(NSInteger, DSPeerType) - (instancetype)initWithAddress:(UInt128)address port:(uint16_t)port onChain:(DSChain *)chain timestamp:(NSTimeInterval)timestamp services:(uint64_t)services; - (instancetype)initWithHost:(NSString *)host onChain:(DSChain *)chain; -- (void)setChainDelegate:(id)chainDelegate peerDelegate:(id)peerDelegate transactionDelegate:(id)transactionDelegate governanceDelegate:(id)governanceDelegate sporkDelegate:(id)sporkDelegate masternodeDelegate:(id)masternodeDelegate queue:(dispatch_queue_t)delegateQueue; +- (void)setChainDelegate:(id)chainDelegate peerDelegate:(id)peerDelegate transactionDelegate:(id)transactionDelegate governanceDelegate:(id)governanceDelegate sporkDelegate:(id)sporkDelegate masternodeDelegate:(id)masternodeDelegate; - (void)connect; - (void)disconnect; - (void)disconnectWithError:(NSError *_Nullable)error; diff --git a/DashSync/shared/Models/Network/DSPeer.m b/DashSync/shared/Models/Network/DSPeer.m index 98a61c3b..631c549e 100644 --- a/DashSync/shared/Models/Network/DSPeer.m +++ b/DashSync/shared/Models/Network/DSPeer.m @@ -195,7 +195,12 @@ - (void)dealloc { [NSObject cancelPreviousPerformRequestsWithTarget:self]; } -- (void)setChainDelegate:(id)chainDelegate peerDelegate:(id)peerDelegate transactionDelegate:(id)transactionDelegate governanceDelegate:(id)governanceDelegate sporkDelegate:(id)sporkDelegate masternodeDelegate:(id)masternodeDelegate queue:(dispatch_queue_t)delegateQueue { +- (void)setChainDelegate:(id)chainDelegate + peerDelegate:(id)peerDelegate + transactionDelegate:(id)transactionDelegate + governanceDelegate:(id)governanceDelegate + sporkDelegate:(id)sporkDelegate + masternodeDelegate:(id)masternodeDelegate { _peerChainDelegate = chainDelegate; _peerDelegate = peerDelegate; _transactionDelegate = transactionDelegate; @@ -203,7 +208,7 @@ - (void)setChainDelegate:(id)chainDelegate peerDelegate:(id _sporkDelegate = sporkDelegate; _masternodeDelegate = masternodeDelegate; - _delegateQueue = (delegateQueue) ? delegateQueue : dispatch_get_main_queue(); + _delegateQueue = self.chain.networkingQueue; } - (NSString *)location { @@ -562,7 +567,8 @@ - (void)mempoolTimeout { // are generated and local peer sends filterload with an updated bloom filter // - after filterload is sent, getdata is sent to re-request recent blocks that may contain new tx matching the filter -- (void)sendGetheadersMessageWithLocators:(NSArray *)locators andHashStop:(UInt256)hashStop { +- (void)sendGetheadersMessageWithLocators:(NSArray *)locators + andHashStop:(UInt256)hashStop { DSGetHeadersRequest *request = [DSGetHeadersRequest requestWithLocators:locators andHashStop:hashStop protocolVersion:self.chain.protocolVersion]; if (self.relayStartTime == 0) self.relayStartTime = [NSDate timeIntervalSince1970]; diff --git a/DashSync/shared/Models/Notifications/DSSyncState.h b/DashSync/shared/Models/Notifications/DSSyncState.h index 083c18c6..ee34a17e 100644 --- a/DashSync/shared/Models/Notifications/DSSyncState.h +++ b/DashSync/shared/Models/Notifications/DSSyncState.h @@ -24,27 +24,89 @@ typedef NS_ENUM(uint16_t, DSSyncStateKind) { DSSyncStateKind_Chain = 0, DSSyncStateKind_Headers = 1, DSSyncStateKind_Masternodes = 2, -// DSSyncStateKind_Platofrm = 3, + DSSyncStateKind_Platform = 3, }; +typedef NS_ENUM(uint32_t, DSSyncStateExtKind) { + DSSyncStateExtKind_None = 1 << 0, + DSSyncStateExtKind_Peers = 1 << 1, + DSSyncStateExtKind_Governance = 1 << 2, + DSSyncStateExtKind_Mempool = 1 << 3, + DSSyncStateExtKind_Headers = 1 << 4, + DSSyncStateExtKind_Masternodes = 1 << 5, + DSSyncStateExtKind_Transactions = 1 << 6, + DSSyncStateExtKind_CoinJoin = 1 << 7, + DSSyncStateExtKind_Platform = 1 << 8, +}; + +typedef NS_ENUM(uint16_t, DSPlatformSyncStateKind) { + DSPlatformSyncStateKind_None = 1 << 0, + DSPlatformSyncStateKind_KeyHashes = 1 << 1, + DSPlatformSyncStateKind_Unsynced = 1 << 2, +}; + +typedef NS_ENUM(uint16_t, DSPeersSyncStateKind) { + DSPeersSyncStateKind_None = 1 << 0, + DSPeersSyncStateKind_Selection = 1 << 1, + DSPeersSyncStateKind_Connecting = 1 << 2, +}; + +typedef NS_ENUM(uint16_t, DSMasternodeListSyncStateKind) { + DSMasternodeListSyncStateKind_None = 1 << 0, + DSMasternodeListSyncStateKind_Checkpoints = 1 << 1, + DSMasternodeListSyncStateKind_Diffs = 1 << 2, + DSMasternodeListSyncStateKind_QrInfo = 1 << 3, + DSMasternodeListSyncStateKind_Quorums = 1 << 4, +}; + +NSString * DSSyncStateExtKindDescription(DSSyncStateExtKind kind); +NSString * DSPeersSyncStateKindDescription(DSPeersSyncStateKind kind); +NSString * DSPlatformSyncStateKindDescription(DSPlatformSyncStateKind kind); +NSString * DSMasternodeListSyncStateKindDescription(DSMasternodeListSyncStateKind kind); @interface DSMasternodeListSyncState : NSObject -@property (nonatomic, assign) uint32_t retrievalQueueCount; -@property (nonatomic, assign) uint32_t retrievalQueueMaxAmount; -@property (nonatomic, assign) double storedCount; -@property (nonatomic, assign) double stubCount; -@property (nonatomic, assign) uint32_t lastBlockHeight; +@property (nonatomic, assign) uint32_t queueCount; +@property (nonatomic, assign) uint32_t queueMaxAmount; +@property (nonatomic, assign) uint32_t storedCount; +@property (nonatomic, assign) uint32_t lastListHeight; +@property (nonatomic, assign) uint32_t estimatedBlockHeight; +@property (nonatomic, readonly) DSMasternodeListSyncStateKind kind; + +- (void)addSyncKind:(DSMasternodeListSyncStateKind)kind; +- (void)removeSyncKind:(DSMasternodeListSyncStateKind)kind; +- (void)resetSyncKind; - (void)updateWithSyncState:(DMNSyncState *)state; @end +@interface DSPlatformSyncState : NSObject +@property (nonatomic, assign) uint32_t queueCount; +@property (nonatomic, assign) uint32_t queueMaxAmount; +@property (nonatomic, assign) NSTimeInterval lastSyncedIndentitiesTimestamp; +@property (nonatomic, readonly) DSPlatformSyncStateKind kind; +/*! @brief Returns if we synced identities in the last 30 seconds. */ +@property (nonatomic, readonly) BOOL hasRecentIdentitiesSync; -@interface DSSyncState : NSObject +- (void)addSyncKind:(DSPlatformSyncStateKind)kind; +- (void)removeSyncKind:(DSPlatformSyncStateKind)kind; +- (void)resetSyncKind; -@property (nonatomic, assign) DSChainSyncPhase syncPhase; +@end + +@interface DSPeersSyncState : NSObject +@property (nonatomic, readonly) DSPeersSyncStateKind kind; @property (nonatomic, assign) BOOL hasDownloadPeer; @property (nonatomic, assign) BOOL peerManagerConnected; +- (void)addSyncKind:(DSPeersSyncStateKind)kind; +- (void)removeSyncKind:(DSPeersSyncStateKind)kind; +- (void)resetSyncKind; +@end + +@interface DSSyncState : NSObject + +@property (nonatomic, assign) DSChainSyncPhase syncPhase; +@property (nonatomic, readonly) DSSyncStateExtKind extKind; @property (nonatomic, assign) uint32_t estimatedBlockHeight; @@ -54,19 +116,32 @@ typedef NS_ENUM(uint16_t, DSSyncStateKind) { @property (nonatomic, assign) uint32_t lastTerminalBlockHeight; @property (nonatomic, assign) uint32_t terminalSyncStartHeight; @property (nonatomic, strong) DSMasternodeListSyncState *masternodeListSyncInfo; +@property (nonatomic, strong) DSPlatformSyncState *platformSyncInfo; +@property (nonatomic, strong) DSPeersSyncState *peersSyncInfo; // MARK: Read-only @property (nonatomic, readonly) double masternodeListProgress; @property (nonatomic, readonly) double chainSyncProgress; @property (nonatomic, readonly) double terminalHeaderSyncProgress; -@property (nonatomic, readonly) double combinedSyncProgress; +@property (nonatomic, readonly) double progress; @property (nonatomic, readonly) DSSyncStateKind kind; // MARK: Constructor - (instancetype)initWithSyncPhase:(DSChainSyncPhase)phase; +// MARK: Description + +- (NSString *)peersDescription; +- (NSString *)chainDescription; +- (NSString *)headersDescription; +- (NSString *)masternodesDescription; +- (NSString *)platformDescription; + +- (void)addSyncKind:(DSSyncStateExtKind)kind; +- (void)removeSyncKind:(DSSyncStateExtKind)kind; +- (void)resetSyncKind; @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Notifications/DSSyncState.m b/DashSync/shared/Models/Notifications/DSSyncState.m index a1aa09c8..0e635c4b 100644 --- a/DashSync/shared/Models/Notifications/DSSyncState.m +++ b/DashSync/shared/Models/Notifications/DSSyncState.m @@ -18,37 +18,144 @@ #import "DSOptionsManager.h" #import "DSSyncState.h" +NSString * DSSyncStateExtKindDescription(DSSyncStateExtKind kind) { + NSMutableArray *components = [NSMutableArray array]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Peers)) + [components addObject:@"Peers"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Governance)) + [components addObject:@"Governance"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Mempool)) + [components addObject:@"Mempool"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Headers)) + [components addObject:@"Headers"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Masternodes)) + [components addObject:@"Masternodes"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Transactions)) + [components addObject:@"Transactions"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_CoinJoin)) + [components addObject:@"CoinJoin"]; + if (FLAG_IS_SET(kind, DSSyncStateExtKind_Platform)) + [components addObject:@"Platform"]; + return [components count] ? [components componentsJoinedByString:@" | "] : @"None"; +} + +NSString * DSPlatformSyncStateKindDescription(DSPlatformSyncStateKind kind) { + NSMutableArray *components = [NSMutableArray array]; + if (FLAG_IS_SET(kind, DSPlatformSyncStateKind_KeyHashes)) + [components addObject:@"KeyHashes"]; + if (FLAG_IS_SET(kind, DSPlatformSyncStateKind_Unsynced)) + [components addObject:@"Unsynced"]; + return [components count] ? [components componentsJoinedByString:@" | "] : @"None"; +} + +NSString * DSPeersSyncStateKindDescription(DSPeersSyncStateKind kind) { + NSMutableArray *components = [NSMutableArray array]; + if (FLAG_IS_SET(kind, DSPeersSyncStateKind_Selection)) + [components addObject:@"Selection"]; + if (FLAG_IS_SET(kind, DSPeersSyncStateKind_Connecting)) + [components addObject:@"Connecting"]; + return [components count] ? [components componentsJoinedByString:@" | "] : @"None"; +} + +NSString * DSMasternodeListSyncStateKindDescription(DSMasternodeListSyncStateKind kind) { + NSMutableArray *components = [NSMutableArray array]; + if (FLAG_IS_SET(kind, DSMasternodeListSyncStateKind_Checkpoints)) + [components addObject:@"Checkpoints"]; + if (FLAG_IS_SET(kind, DSMasternodeListSyncStateKind_Diffs)) + [components addObject:@"Diffs"]; + if (FLAG_IS_SET(kind, DSMasternodeListSyncStateKind_QrInfo)) + [components addObject:@"QrInfo"]; + if (FLAG_IS_SET(kind, DSMasternodeListSyncStateKind_Quorums)) + [components addObject:@"Quorums"]; + + return [components count] ? [components componentsJoinedByString:@" | "] : @"None"; +} + +@interface DSMasternodeListSyncState () +@property (nonatomic, assign) DSMasternodeListSyncStateKind kind; +@end @implementation DSMasternodeListSyncState - (id)copyWithZone:(NSZone *)zone { DSMasternodeListSyncState *copy = [[[self class] alloc] init]; - copy.retrievalQueueCount = self.retrievalQueueCount; - copy.retrievalQueueMaxAmount = self.retrievalQueueMaxAmount; + copy.queueCount = self.queueCount; + copy.queueMaxAmount = self.queueMaxAmount; copy.storedCount = self.storedCount; - copy.lastBlockHeight = self.lastBlockHeight; - copy.stubCount = self.stubCount; + copy.lastListHeight = self.lastListHeight; + copy.estimatedBlockHeight = self.estimatedBlockHeight; + copy.kind = self.kind; return copy; } - (NSString *)description { - return [NSString stringWithFormat:@"%u/%u/%f/%f/%u", - self.retrievalQueueCount, - self.retrievalQueueMaxAmount, + return [NSString stringWithFormat:@"[%@: %u %u/%u %u/%u = %.2f/%.2f]", + DSMasternodeListSyncStateKindDescription(self.kind), + self.lastListHeight, + self.queueCount, + self.queueMaxAmount, self.storedCount, - self.stubCount, - self.lastBlockHeight]; + [self listsToSync], + [self progress], [self weight]]; +} +- (uint32_t)listsToSync { + uint32_t estimatedBlockHeight = self.estimatedBlockHeight; + uint32_t amountLeft = self.queueCount; + uint32_t lastMasternodeListHeight = self.lastListHeight; + uint32_t maxAmount = self.queueMaxAmount; + uint32_t storedCount = self.storedCount; + uint32_t masternodeListsToSync; + if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_MasternodeList)) + masternodeListsToSync = 0; + else if (!maxAmount || storedCount <= 1) // 1 because there might be a default + masternodeListsToSync = (lastMasternodeListHeight == UINT32_MAX || estimatedBlockHeight < lastMasternodeListHeight) + ? 24 + : MIN(24, (uint32_t)ceil((estimatedBlockHeight - lastMasternodeListHeight) / 24.0f)); + else + masternodeListsToSync = amountLeft; + + return masternodeListsToSync; } + +- (double)progress { + uint32_t amountLeft = self.queueCount; + uint32_t maxAmount = self.queueMaxAmount; + uint32_t lastBlockHeight = self.lastListHeight; + uint32_t estimatedBlockHeight = self.estimatedBlockHeight; + return amountLeft ? MAX(MIN((maxAmount - amountLeft) / maxAmount, 1), 0) : lastBlockHeight != UINT32_MAX && estimatedBlockHeight != 0 && lastBlockHeight + 16 >= estimatedBlockHeight; +} + +- (double)weight { + uint32_t listsToSync = [self listsToSync]; + return listsToSync ? (200 + 20 * (listsToSync - 1)) : 0; +} + +- (void)addSyncKind:(DSMasternodeListSyncStateKind)kind { + if (!FLAG_IS_SET(_kind, kind)) { + _kind |= kind; + } +} + +- (void)removeSyncKind:(DSMasternodeListSyncStateKind)kind { + if (FLAG_IS_SET(_kind, kind)) { + _kind &= ~kind; + } +} + +- (void)resetSyncKind { + _kind = DSMasternodeListSyncStateKind_None; +} + - (void)updateWithSyncState:(DMNSyncState *)state { switch (state->tag) { case DMNSyncStateQueueChanged: - self.retrievalQueueCount = (uint32_t) state->queue_changed.count; - self.retrievalQueueMaxAmount = (uint32_t) state->queue_changed.max_amount; + self.queueCount = (uint32_t) state->queue_changed.count; + self.queueMaxAmount = (uint32_t) state->queue_changed.max_amount; break; case DMNSyncStateStoreChanged: self.storedCount = (uint32_t) state->store_changed.count; - self.lastBlockHeight = state->store_changed.last_block_height; + self.lastListHeight = state->store_changed.last_block_height; break; - case DMNSyncStateStubCount: - self.stubCount = state->stub_count.count; +// case DMNSyncStateStubCount: +// self.stubCount = state->stub_count.count; default: break; } @@ -56,61 +163,187 @@ - (void)updateWithSyncState:(DMNSyncState *)state { @end +@interface DSPlatformSyncState () +@property (nonatomic, assign) DSPlatformSyncStateKind kind; +@end + +@implementation DSPlatformSyncState +- (id)copyWithZone:(NSZone *)zone { + DSPlatformSyncState *copy = [[[self class] alloc] init]; + copy.kind = self.kind; + copy.queueCount = self.queueCount; + copy.queueMaxAmount = self.queueMaxAmount; + copy.lastSyncedIndentitiesTimestamp = self.lastSyncedIndentitiesTimestamp; + return copy; +} +- (void)addSyncKind:(DSPlatformSyncStateKind)kind { + if (!FLAG_IS_SET(self.kind, kind)) + _kind |= kind; +} + +- (void)removeSyncKind:(DSPlatformSyncStateKind)kind { + if (FLAG_IS_SET(self.kind, kind)) + _kind &= ~kind; +} +- (void)resetSyncKind { + _kind = DSPlatformSyncStateKind_None; +} + +- (BOOL)hasRecentIdentitiesSync { + return [[NSDate date] timeIntervalSince1970] - self.lastSyncedIndentitiesTimestamp < 30; +} + +- (double)progress { + uint32_t amountLeft = self.queueCount; + uint32_t maxAmount = self.queueMaxAmount; + return amountLeft && maxAmount ? MAX(MIN((maxAmount - amountLeft) / maxAmount, 1), 0) : [self hasRecentIdentitiesSync]; +} + +- (double)weight { + uint32_t identitiesToSync = self.queueMaxAmount; + BOOL outdated = ![self hasRecentIdentitiesSync]; + return identitiesToSync ? (20000 + 2000 * (identitiesToSync - 1)) : outdated; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"[%@: %f %u/%u = %.2f/%.2f]", DSPlatformSyncStateKindDescription(self.kind), self.lastSyncedIndentitiesTimestamp, self.queueCount, self.queueMaxAmount, [self progress], [self weight]]; +} + +@end +@interface DSPeersSyncState () +@property (nonatomic, assign) DSPeersSyncStateKind kind; +@end + +@implementation DSPeersSyncState +- (id)copyWithZone:(NSZone *)zone { + DSPeersSyncState *copy = [[[self class] alloc] init]; + copy.kind = self.kind; + copy.hasDownloadPeer = self.hasDownloadPeer; + copy.peerManagerConnected = self.peerManagerConnected; + return copy; +} +- (double)progress { + return self.peerManagerConnected && self.hasDownloadPeer ? 1 : 0; +} + +- (void)addSyncKind:(DSPeersSyncStateKind)kind { + if (!FLAG_IS_SET(self.kind, kind)) + _kind |= kind; +} + +- (void)removeSyncKind:(DSPeersSyncStateKind)kind { + if (FLAG_IS_SET(self.kind, kind)) + _kind &= ~kind; +} +- (void)resetSyncKind { + _kind = DSPeersSyncStateKind_None; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"[%@: %u/%u]", + DSPeersSyncStateKindDescription(self.kind), + self.hasDownloadPeer, + self.peerManagerConnected]; +} + +@end + +@interface DSSyncState () +@property (nonatomic, assign) DSSyncStateExtKind extKind; +@end + @implementation DSSyncState - (instancetype)initWithSyncPhase:(DSChainSyncPhase)phase { if (!(self = [super init])) return nil; self.syncPhase = phase; + self.extKind = DSSyncStateExtKind_None; self.masternodeListSyncInfo = [[DSMasternodeListSyncState alloc] init]; + self.platformSyncInfo = [[DSPlatformSyncState alloc] init]; + self.peersSyncInfo = [[DSPeersSyncState alloc] init]; return self; } - (id)copyWithZone:(NSZone *)zone { DSSyncState *copy = [[[self class] alloc] init]; copy.syncPhase = self.syncPhase; - copy.hasDownloadPeer = self.hasDownloadPeer; - copy.peerManagerConnected = self.peerManagerConnected; + copy.extKind = self.extKind; copy.estimatedBlockHeight = self.estimatedBlockHeight; copy.chainSyncStartHeight = self.chainSyncStartHeight; copy.lastSyncBlockHeight = self.lastSyncBlockHeight; copy.terminalSyncStartHeight = self.terminalSyncStartHeight; copy.lastTerminalBlockHeight = self.lastTerminalBlockHeight; copy.masternodeListSyncInfo = [self.masternodeListSyncInfo copy]; + copy.platformSyncInfo = [self.platformSyncInfo copy]; + copy.peersSyncInfo = [self.peersSyncInfo copy]; return copy; } +- (void)addSyncKind:(DSSyncStateExtKind)kind { + if (!FLAG_IS_SET(self.extKind, kind)) + _extKind |= kind; +} + +- (void)removeSyncKind:(DSSyncStateExtKind)kind { + if (FLAG_IS_SET(self.extKind, kind)) + _extKind &= ~kind; +} +- (void)resetSyncKind { + _extKind = DSSyncStateExtKind_None; +} + - (NSString *)description { - return [NSString stringWithFormat:@"SyncState: { phase: %u, peer: %u, connected: %u, estimated: %u, chain: [%u/%u/%f] headers: [%u/%u/%f], mn: [%@/%u/%f] == %f", + return [NSString stringWithFormat:@"{ phase: %u, kind: %u, %@, estimated: %u, %@, %@, %@, %@ == %f }", self.syncPhase, - self.hasDownloadPeer, - self.peerManagerConnected, + self.extKind, + self.peersDescription, self.estimatedBlockHeight, - self.chainSyncStartHeight, - self.lastSyncBlockHeight, - self.chainSyncProgress, - self.terminalSyncStartHeight, - self.lastTerminalBlockHeight, - self.terminalHeaderSyncProgress, - self.masternodeListSyncInfo, - self.masternodeListsToSync, - self.masternodeListProgress, - self.combinedSyncProgress + self.chainDescription, + self.headersDescription, + self.masternodesDescription, + self.platformDescription, + [self progress] ]; } +- (NSString *)chainDescription { + return [NSString stringWithFormat:@"chain: [%u/%u = %.2f/%.2f]", self.chainSyncStartHeight, self.lastSyncBlockHeight, self.chainSyncProgress, self.chainSyncWeight]; +} + +- (NSString *)headersDescription { + return [NSString stringWithFormat:@"headers: [%u/%u = %.2f/%.2f]", self.terminalSyncStartHeight, self.lastTerminalBlockHeight, self.terminalHeaderSyncProgress, self.headersSyncWeight]; +} + +- (NSString *)peersDescription { + return [NSString stringWithFormat:@"peers: %@", self.peersSyncInfo]; +} + +- (NSString *)masternodesDescription { + return [NSString stringWithFormat:@"mn: %@", self.masternodeListSyncInfo]; +} + +- (NSString *)platformDescription { + return [NSString stringWithFormat:@"evo: %@", self.platformSyncInfo]; +} + - (double)masternodeListProgress { - uint32_t amountLeft = self.masternodeListSyncInfo.retrievalQueueCount; - uint32_t maxAmount = self.masternodeListSyncInfo.retrievalQueueMaxAmount; - uint32_t lastBlockHeight = self.masternodeListSyncInfo.lastBlockHeight; - uint32_t estimatedBlockHeight = self.estimatedBlockHeight; - return amountLeft ? MAX(MIN((maxAmount - amountLeft) / maxAmount, 1), 0) : lastBlockHeight != UINT32_MAX && estimatedBlockHeight != 0 && lastBlockHeight + 16 >= estimatedBlockHeight; + return [self.masternodeListSyncInfo progress]; +} + +- (double)platformProgress { + return [self.platformSyncInfo progress]; +} + +- (void)setEstimatedBlockHeight:(uint32_t)estimatedBlockHeight { + _estimatedBlockHeight = estimatedBlockHeight; + self.masternodeListSyncInfo.estimatedBlockHeight = estimatedBlockHeight; } - (double)chainSyncProgress { uint32_t chainSyncStartHeight = self.chainSyncStartHeight; uint32_t lastSyncBlockHeight = self.lastSyncBlockHeight; uint32_t estimatedBlockHeight = self.estimatedBlockHeight; - if (!self.hasDownloadPeer && chainSyncStartHeight == 0) + if (!self.peersSyncInfo.hasDownloadPeer && chainSyncStartHeight == 0) return 0.0; else if (lastSyncBlockHeight >= estimatedBlockHeight) return 1.0; @@ -126,7 +359,7 @@ - (double)terminalHeaderSyncProgress { uint32_t terminalSyncStartHeight = self.terminalSyncStartHeight; uint32_t lastTerminalBlockHeight = self.lastTerminalBlockHeight; uint32_t estimatedBlockHeight = self.estimatedBlockHeight; - if (!self.hasDownloadPeer && terminalSyncStartHeight == 0) + if (!self.peersSyncInfo.hasDownloadPeer && terminalSyncStartHeight == 0) return 0.0; else if (lastTerminalBlockHeight >= estimatedBlockHeight) return 1.0; @@ -134,46 +367,35 @@ - (double)terminalHeaderSyncProgress { return MIN(1.0, MAX(0.0, 0.1 + 0.9 * (terminalSyncStartHeight > lastTerminalBlockHeight ? lastTerminalBlockHeight / estimatedBlockHeight : (lastTerminalBlockHeight - terminalSyncStartHeight) / (estimatedBlockHeight - terminalSyncStartHeight)))); } -- (uint32_t)masternodeListsToSync { - uint32_t estimatedBlockHeight = self.estimatedBlockHeight; - uint32_t amountLeft = self.masternodeListSyncInfo.retrievalQueueCount; - uint32_t lastMasternodeListHeight = self.masternodeListSyncInfo.lastBlockHeight; - uint32_t maxAmount = self.masternodeListSyncInfo.retrievalQueueMaxAmount; - uint32_t storedCount = self.masternodeListSyncInfo.storedCount; - uint32_t masternodeListsToSync; - if (!([[DSOptionsManager sharedInstance] syncType] & DSSyncType_MasternodeList)) - masternodeListsToSync = 0; - else if (!maxAmount || storedCount <= 1) // 1 because there might be a default - masternodeListsToSync = (lastMasternodeListHeight == UINT32_MAX || estimatedBlockHeight < lastMasternodeListHeight) - ? 32 - : MIN(32, (uint32_t)ceil((estimatedBlockHeight - lastMasternodeListHeight) / 24.0f)); - else - masternodeListsToSync = amountLeft; - - return masternodeListsToSync; +- (double)chainSyncWeight { + double weight = self.lastSyncBlockHeight >= self.estimatedBlockHeight ? 0 : self.estimatedBlockHeight - self.lastSyncBlockHeight; + return weight; } +- (double)headersSyncWeight { + double weight = self.lastTerminalBlockHeight >= self.estimatedBlockHeight ? 0 : (self.estimatedBlockHeight - self.lastTerminalBlockHeight) / 4; + return weight; +} + /** * A unit of weight is the time it would take to sync 1000 blocks; * terminal headers are 4 times faster the blocks * the first masternode list is worth 20000 blocks * each masternode list after that is worth 2000 blocks */ -- (double)combinedSyncProgress { - uint32_t estimatedBlockHeight = self.estimatedBlockHeight; - uint32_t lastTerminalBlockHeight = self.lastTerminalBlockHeight; - uint32_t lastSyncBlockHeight = self.lastSyncBlockHeight; - double chainWeight = lastSyncBlockHeight >= estimatedBlockHeight ? 0 : estimatedBlockHeight - lastSyncBlockHeight; - double terminalWeight = lastTerminalBlockHeight >= estimatedBlockHeight ? 0 : (estimatedBlockHeight - lastTerminalBlockHeight) / 4; - uint32_t listsToSync = [self masternodeListsToSync]; - double masternodeWeight = listsToSync ? (20000 + 2000 * (listsToSync - 1)) : 0; - double totalWeight = chainWeight + terminalWeight + masternodeWeight; +- (double)progress { + double chainWeight = [self chainSyncWeight]; + double terminalWeight = [self headersSyncWeight]; + double platformWeight = [self.platformSyncInfo weight]; + double masternodeWeight = [self.masternodeListSyncInfo weight]; + double totalWeight = chainWeight + terminalWeight + masternodeWeight + platformWeight; if (totalWeight == 0) { - return self.peerManagerConnected && self.hasDownloadPeer ? 1 : 0; + return [self.peersSyncInfo progress]; } else { double terminalProgress = self.terminalHeaderSyncProgress * (terminalWeight / totalWeight); double chainProgress = self.chainSyncProgress * (chainWeight / totalWeight); double masternodesProgress = self.masternodeListProgress * (masternodeWeight / totalWeight); - double progress = terminalProgress + masternodesProgress + chainProgress; + double platformProgress = self.platformProgress * (platformWeight / totalWeight); + double progress = terminalProgress + masternodesProgress + chainProgress + platformProgress; if (progress < 0.99995) { return progress; } else { @@ -196,14 +418,14 @@ - (DSSyncStateKind)kind { - (BOOL)atTheEndOfSyncBlocksAndSyncingMasternodeList { // We give a 6 block window, just in case a new block comes in return self.lastSyncBlockHeight + 6 >= self.estimatedBlockHeight - && self.masternodeListSyncInfo.retrievalQueueCount > 0 + && self.masternodeListSyncInfo.queueCount > 0 && self.syncPhase == DSChainSyncPhase_Synced; } - (BOOL)atTheEndOfInitialTerminalBlocksAndSyncingMasternodeList { // We give a 6 block window, just in case a new block comes in return self.lastTerminalBlockHeight + 6 >= self.estimatedBlockHeight - && self.masternodeListSyncInfo.retrievalQueueCount > 0 + && self.masternodeListSyncInfo.queueCount > 0 && self.syncPhase == DSChainSyncPhase_InitialTerminalBlocks; } diff --git a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.h b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.h index de2946e6..7616fde5 100644 --- a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.h +++ b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.h @@ -25,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) uint8_t specialTransactionVersion; @property (nonatomic, strong) NSMutableArray *creditOutputs; +@property (nonatomic, readonly) UInt256 creditBurnIdentityIdentifier; @property (nonatomic, readonly) UInt160 creditBurnPublicKeyHash; @property (nonatomic, readonly) DSUTXO lockedOutpoint; diff --git a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m index dcda82a0..e52eccdf 100644 --- a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m @@ -119,15 +119,15 @@ - (NSData *)basePayloadData { return data; } - -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } + - (size_t)size { @synchronized(self) { if (uint256_is_not_zero(self.txHash)) return self.data.length; diff --git a/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m b/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m index ad2d582b..8ec89818 100644 --- a/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m @@ -90,11 +90,11 @@ - (NSData *)basePayloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Transactions/Base/DSTransaction.h b/DashSync/shared/Models/Transactions/Base/DSTransaction.h index 595a8f9e..c2fd4cce 100644 --- a/DashSync/shared/Models/Transactions/Base/DSTransaction.h +++ b/DashSync/shared/Models/Transactions/Base/DSTransaction.h @@ -77,6 +77,16 @@ typedef NS_ENUM(NSUInteger, DSTransactionDirection) DSTransactionDirection_NotAccountFunds, }; +@interface DSTransactionOptions : NSObject +@property (nonatomic, assign) BOOL anyoneCanPay; +@property (nonatomic, assign) NSUInteger subscriptIndex; ++ (instancetype)default; ++ (instancetype)withAnyoneCanPay:(BOOL)anyoneCanPay; ++ (instancetype)withSubscriptIndex:(NSUInteger)subscriptIndex; ++ (instancetype)withSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay; +@end + @interface DSTransaction : NSObject @property (nonatomic, readonly) NSArray *inputs; @@ -118,7 +128,6 @@ typedef NS_ENUM(NSUInteger, DSTransactionDirection) @property (nonatomic, readonly) NSString *longDescription; @property (nonatomic, readonly) BOOL isCoinbaseClassicTransaction; @property (nonatomic, readonly) BOOL isImmatureCoinBase; -@property (nonatomic, readonly) UInt256 creditBurnIdentityIdentifier; @property (nonatomic, strong) DSShapeshiftEntity *associatedShapeshift; @property (nonatomic, readonly) DSChain *chain; @@ -167,7 +176,7 @@ typedef NS_ENUM(NSUInteger, DSTransactionDirection) // priority = sum(input_amount_in_satoshis*input_age_in_blocks)/tx_size_in_bytes - (uint64_t)priorityForAmounts:(NSArray *)amounts withAges:(NSArray *)ages; -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay; +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options; - (BOOL)hasNonDustOutputInWallet:(DSWallet *)wallet; diff --git a/DashSync/shared/Models/Transactions/Base/DSTransaction.m b/DashSync/shared/Models/Transactions/Base/DSTransaction.m index 037676c5..5cb8dcd5 100644 --- a/DashSync/shared/Models/Transactions/Base/DSTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSTransaction.m @@ -52,6 +52,31 @@ #import "NSString+Bitcoin.h" #import "NSString+Dash.h" +@implementation DSTransactionOptions : NSObject ++ (nonnull instancetype)default { + return [DSTransactionOptions withSubscriptIndex:NSNotFound anyoneCanPay:NO]; +} ++ (nonnull instancetype)withAnyoneCanPay:(BOOL)anyoneCanPay { + return [DSTransactionOptions withSubscriptIndex:NSNotFound anyoneCanPay:anyoneCanPay]; +} ++ (nonnull instancetype)withSubscriptIndex:(NSUInteger)subscriptIndex { + return [DSTransactionOptions withSubscriptIndex:subscriptIndex anyoneCanPay:NO]; +} ++ (instancetype)withSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { + DSTransactionOptions *options = [[self alloc] init]; + if (options) { + options.subscriptIndex = subscriptIndex; + options.anyoneCanPay = anyoneCanPay; + } + return options; +} + + + +@end + + @interface DSTransaction () @property (nonatomic, strong) DSChain *chain; @@ -437,16 +462,18 @@ - (NSUInteger)hash { // MARK: - Wire Serialization - (NSData *)toData { - return [self toData:NO]; + return [self toDataWithOptions:[DSTransactionOptions default]]; } - (NSData *)toData:(BOOL)anyoneCanPay { - return [self toDataWithSubscriptIndex:NSNotFound anyoneCanPay:anyoneCanPay]; + return [self toDataWithOptions:[DSTransactionOptions withAnyoneCanPay:anyoneCanPay]]; } // Returns the binary transaction data that needs to be hashed and signed with the private key for the tx input at // subscriptIndex. A subscriptIndex of NSNotFound will return the entire signed transaction. -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { + BOOL anyoneCanPay = options.anyoneCanPay; + NSUInteger subscriptIndex = options.subscriptIndex; @synchronized(self) { NSArray *inputs = self.inputs; NSArray *outputs = self.outputs; @@ -503,6 +530,7 @@ - (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BO } } + // MARK: - Construction - (void)addInputHash:(UInt256)hash index:(NSUInteger)index script:(NSData *)script { @@ -649,7 +677,8 @@ - (BOOL)signWithPrivateKeys:(NSArray *)keys anyoneCanPay:(BOOL)anyoneCanPay { transactionInput.signature = [NSData data]; continue; } - NSData *data = [self toDataWithSubscriptIndex:i anyoneCanPay:anyoneCanPay]; + DSTransactionOptions *options = [DSTransactionOptions withSubscriptIndex:i anyoneCanPay:anyoneCanPay]; + NSData *data = [self toDataWithOptions:options]; uint8_t sighashFlags = SIGHASH_ALL; if (anyoneCanPay) { sighashFlags |= SIGHASH_ANYONECANPAY; @@ -660,7 +689,9 @@ - (BOOL)signWithPrivateKeys:(NSArray *)keys anyoneCanPay:(BOOL)anyoneCanPay { transactionInput.signature = sig; } if (!self.isSigned) return NO; - _txHash = self.data.SHA256_2; + // TODO: check if this is wrong +// _txHash = self.data.SHA256_2; + _txHash = [self toData:anyoneCanPay].SHA256_2; return YES; } } @@ -675,7 +706,8 @@ - (BOOL)signWithMaybePrivateKeySets:(NSArray *)keysSets anyoneCanPay:(BOOL)anyon if (maybe_opaque_keys->ok) { DOpaqueKey *opaque_key = DOpaqueKeyUsedInTxInputScript(bytes_ctor(inScript), maybe_opaque_keys->ok, self.chain.chainType); if (opaque_key) { - NSData *data = [self toDataWithSubscriptIndex:i anyoneCanPay:anyoneCanPay]; + DSTransactionOptions *options = [DSTransactionOptions withSubscriptIndex:i anyoneCanPay:anyoneCanPay]; + NSData *data = [self toDataWithOptions:options]; NSData *sig = [DSTransaction signInput:data flags:SIGHASH_ALL inputScript:inScript withOpaqueKey:opaque_key]; DOpaqueKeyDtor(opaque_key); transactionInput.signature = sig; @@ -694,7 +726,9 @@ - (BOOL)signWithPreorderedPrivateKeys:(NSArray *)keys { @synchronized (self) { for (NSUInteger i = 0; i < self.mInputs.count; i++) { DSTransactionInput *transactionInput = self.mInputs[i]; - NSData *sig = [DSTransaction signInput:[self toDataWithSubscriptIndex:i anyoneCanPay:NO] flags:SIGHASH_ALL inputScript:transactionInput.inScript withOpaqueKeyValue:keys[i]]; + DSTransactionOptions *options = [DSTransactionOptions withSubscriptIndex:i]; + NSData *data = [self toDataWithOptions:options]; + NSData *sig = [DSTransaction signInput:data flags:SIGHASH_ALL inputScript:transactionInput.inScript withOpaqueKeyValue:keys[i]]; transactionInput.signature = sig; } if (!self.isSigned) return NO; @@ -713,7 +747,7 @@ + (NSData *)signInput:(NSData *)data + (NSData *)signInput:(NSData *)data flags:(uint8_t)flags inputScript:(NSData *)inputScript - withOpaqueKey:(DOpaqueKey *)key { + withOpaqueKey:(DOpaqueKey *)key { Slice_u8 *input = slice_ctor(data); Vec_u8 *tx_input_script = bytes_ctor(inputScript); Vec_u8 *tx_sig = DOpaqueKeyCreateTxSig(key, input, flags, tx_input_script); @@ -766,15 +800,11 @@ - (BOOL)hasNonDustOutputInWallet:(DSWallet *)wallet { - (void)setInstantSendReceivedWithInstantSendLock:(DSInstantSendTransactionLock *)instantSendLock { self.instantSendReceived = instantSendLock.signatureVerified; - self.hasUnverifiedInstantSendLock = (instantSendLock && !instantSendLock.signatureVerified); - if (self.hasUnverifiedInstantSendLock) { - self.instantSendLockAwaitingProcessing = instantSendLock; - } else { - self.instantSendLockAwaitingProcessing = nil; - } - if (!instantSendLock.saved) { + BOOL hasUnverifiedISLock = instantSendLock && !instantSendLock.signatureVerified; + self.hasUnverifiedInstantSendLock = hasUnverifiedISLock; + self.instantSendLockAwaitingProcessing = hasUnverifiedISLock ? instantSendLock : nil; + if (!instantSendLock.saved) [instantSendLock saveInitial]; - } } - (uint32_t)confirmations { @@ -805,22 +835,13 @@ - (void)loadIdentitiesFromDerivationPaths:(NSArray *)derivat for (DSFundsDerivationPath *derivationPath in derivationPaths) { if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]] && [derivationPath containsAddress:output.address]) { - DSIncomingFundsDerivationPath *incomingFundsDerivationPath = ((DSIncomingFundsDerivationPath *)derivationPath); - DSIdentity *destinationIdentity = [self.chain identityForUniqueId:incomingFundsDerivationPath.contactDestinationIdentityUniqueId - foundInWallet:nil - includeForeignIdentities:YES]; - - DSIdentity *sourceIdentity = [self.chain identityForUniqueId:incomingFundsDerivationPath.contactSourceIdentityUniqueId - foundInWallet:nil - includeForeignIdentities:YES]; - - - if (sourceIdentity) { + DSIncomingFundsDerivationPath *path = ((DSIncomingFundsDerivationPath *)derivationPath); + DSIdentity *destinationIdentity = [self.chain identityForUniqueId:path.contactDestinationIdentityUniqueId foundInWallet:nil includeForeignIdentities:YES]; + DSIdentity *sourceIdentity = [self.chain identityForUniqueId:path.contactSourceIdentityUniqueId foundInWallet:nil includeForeignIdentities:YES]; + if (sourceIdentity) [destinationIdentities addObject:sourceIdentity]; //these need to be inverted since the derivation path is incoming - } - if (destinationIdentity) { + if (destinationIdentity) [sourceIdentities addObject:destinationIdentity]; //these need to be inverted since the derivation path is incoming - } } } } diff --git a/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m b/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m index 40910729..b18ace69 100644 --- a/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m +++ b/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m @@ -105,10 +105,9 @@ - (NSData *)payloadData { // Returns the binary transaction data that needs to be hashed and signed with the private key for the tx input at // subscriptIndex. A subscriptIndex of NSNotFound will return the entire signed transaction. -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; return [data appendCountedData:[self payloadData]]; } } diff --git a/DashSync/shared/Models/Transactions/DSTransactionFactory.m b/DashSync/shared/Models/Transactions/DSTransactionFactory.m index 1153a3cb..9c37cdb2 100644 --- a/DashSync/shared/Models/Transactions/DSTransactionFactory.m +++ b/DashSync/shared/Models/Transactions/DSTransactionFactory.m @@ -21,14 +21,14 @@ @implementation DSTransactionFactory + (DSTransactionType)transactionTypeOfMessage:(NSData *)message { uint16_t version = [message UInt16AtOffset:0]; - if (version < 3) return DSTransactionType_Classic; + if (version < SPECIAL_TX_VERSION) return DSTransactionType_Classic; return [message UInt16AtOffset:2]; } + (DSTransaction *)transactionWithMessage:(NSData *)message onChain:(DSChain *)chain { uint16_t version = [message UInt16AtOffset:0]; uint16_t type; - if (version < 3) { + if (version < SPECIAL_TX_VERSION) { type = DSTransactionType_Classic; } else { type = [message UInt16AtOffset:2]; diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m index 6d499f77..fab6a26b 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m @@ -235,12 +235,11 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m index 604062d1..7602199e 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m @@ -196,14 +196,13 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; NSData *payloadData = [self payloadData]; [data appendVarInt:payloadData.length]; [data appendData:payloadData]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m index eee818fe..6b3b8ebc 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m @@ -147,12 +147,11 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m index c3d39b1f..afdf0341 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m @@ -196,12 +196,11 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m b/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m index 500e09d9..b9f7fefc 100644 --- a/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m +++ b/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m @@ -132,12 +132,11 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex - anyoneCanPay:(BOOL)anyoneCanPay { +- (NSData *)toDataWithOptions:(DSTransactionOptions *)options { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + NSMutableData *data = [[super toDataWithOptions:options] mutableCopy]; [data appendCountedData:[self payloadData]]; - if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + if (options.subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; return data; } } diff --git a/DashSync/shared/Models/Wallet/DSAccount.h b/DashSync/shared/Models/Wallet/DSAccount.h index 3bf196bc..5ff1c5a7 100644 --- a/DashSync/shared/Models/Wallet/DSAccount.h +++ b/DashSync/shared/Models/Wallet/DSAccount.h @@ -25,6 +25,7 @@ #import "DSAssetLockTransaction.h" #import "DSFundsDerivationPath.h" +#import "DSGapLimit.h" #import "DSIncomingFundsDerivationPath.h" #import "DSTransaction.h" #import "NSData+Dash.h" @@ -87,8 +88,7 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // has an extended public key missing in one of the account derivation paths @property (nonatomic, readonly) BOOL hasAnExtendedPublicKeyMissing; -- (NSArray *_Nullable)registerAddressesWithInitialGapLimit; -- (NSArray *_Nullable)registerAddressesWithProlongGapLimit; +- (NSArray *)registerAddressesAtStage:(DSGapLimitStage)stage; + (DSAccount *)accountWithAccountNumber:(uint32_t)accountNumber withDerivationPaths:(NSArray *)derivationPaths @@ -212,6 +212,7 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // true if the given transaction is associated with the account (even if it hasn't been registered), false otherwise - (BOOL)canContainTransaction:(DSTransaction *)transaction; //- (BOOL)canContainRustTransaction:(Result_ok_dashcore_blockdata_transaction_Transaction_err_dash_spv_platform_error_Error *)transaction; +- (BOOL)canContainTransactionIncludingCoinjoin:(DSTransaction *)transaction; // adds a transaction to the account, or returns false if it isn't associated with the account - (BOOL)registerTransaction:(DSTransaction *)transaction diff --git a/DashSync/shared/Models/Wallet/DSAccount.m b/DashSync/shared/Models/Wallet/DSAccount.m index a98ab42f..622a97a5 100644 --- a/DashSync/shared/Models/Wallet/DSAccount.m +++ b/DashSync/shared/Models/Wallet/DSAccount.m @@ -28,7 +28,6 @@ #import "DSChain+Params.h" #import "DSChain+Protected.h" #import "DSFundsDerivationPath.h" -#import "DSGapLimit.h" #import "DSWallet+Protected.h" #import "DSProviderRegistrationTransaction.h" @@ -304,8 +303,10 @@ - (void)loadDerivationPaths { [derivationPath loadAddresses]; } } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath hasExtendedPublicKey]) + [self.coinJoinDerivationPath loadAddresses]; } else { - for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { [derivationPath registerAddressesWithSettings:[DSGapLimit withLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL]]; } else { @@ -313,6 +314,10 @@ - (void)loadDerivationPaths { [derivationPath registerAddressesWithSettings:[DSGapLimitFunds external:SEQUENCE_GAP_LIMIT_INITIAL]]; } } + if (self.coinJoinDerivationPath) { + [self.coinJoinDerivationPath registerAddressesWithSettings:[DSGapLimitFunds internal:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN]]; + [self.coinJoinDerivationPath registerAddressesWithSettings:[DSGapLimitFunds external:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN]]; + } } if (!self.isViewOnlyAccount) { if (self.bip44DerivationPath && [self.bip44DerivationPath hasExtendedPublicKey]) { @@ -329,6 +334,7 @@ - (void)loadDerivationPaths { - (void)wipeBlockchainInfo { [self.mFundDerivationPaths removeObjectsInArray:[self.mContactIncomingFundDerivationPathsDictionary allValues]]; + self.coinJoinDerivationPath = nil; [self.mContactIncomingFundDerivationPathsDictionary removeAllObjects]; [self.mContactOutgoingFundDerivationPathsDictionary removeAllObjects]; [self.transactions removeAllObjects]; @@ -465,6 +471,8 @@ - (DSDerivationPath *)derivationPathContainingAddress:(NSString *)address { for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath containsAddress:address]) return derivationPath; } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsAddress:address]) + return self.coinJoinDerivationPath; return nil; } @@ -474,45 +482,40 @@ - (BOOL)hasAnExtendedPublicKeyMissing { for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if (![derivationPath hasExtendedPublicKey]) return YES; } + if (self.coinJoinDerivationPath && ![self.coinJoinDerivationPath hasExtendedPublicKey]) return YES; return NO; } - -- (NSArray *)registerAddressesWithInitialGapLimit { +- (NSArray *)registerAddressesAtStage:(DSGapLimitStage)stage { NSMutableArray *mArray = [NSMutableArray array]; for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { DSFundsDerivationPath *path = (DSFundsDerivationPath *)derivationPath; BOOL useReduced = [path shouldUseReducedGapLimit]; BOOL isAnonymous = path.type == DSDerivationPathType_AnonymousFunds; - NSUInteger limit = useReduced ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : (isAnonymous ? SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN : SEQUENCE_GAP_LIMIT_INITIAL); - [mArray addObjectsFromArray:[path registerAddressesWithSettings:[DSGapLimitFunds external:limit]]]; - [mArray addObjectsFromArray:[path registerAddressesWithSettings:[DSGapLimitFunds internal:limit]]]; - } else if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { - NSArray *addresses = [derivationPath registerAddressesWithSettings:[DSGapLimit withLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL]]; - [mArray addObjectsFromArray:addresses]; - } - } - return [mArray copy]; -} + BOOL isInitial = stage == DSGapLimitStage_Initial; + NSUInteger externalLimit = useReduced ? (isInitial ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL) : (isAnonymous ? SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN : (isInitial ? SEQUENCE_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_EXTERNAL)); + NSUInteger internalLimit = useReduced ? (isInitial ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_INTERNAL) : (isAnonymous ? SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN : (isInitial ? SEQUENCE_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_INTERNAL)); -- (NSArray *)registerAddressesWithProlongGapLimit { - NSMutableArray *mArray = [NSMutableArray array]; - for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { - if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { - DSFundsDerivationPath *path = (DSFundsDerivationPath *)derivationPath; - BOOL useReduced = [path shouldUseReducedGapLimit]; - BOOL isAnonymous = path.type == DSDerivationPathType_AnonymousFunds; - NSUInteger externalLimit = useReduced ? SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL : (isAnonymous ? SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN : SEQUENCE_GAP_LIMIT_EXTERNAL); - NSUInteger internalLimit = useReduced ? SEQUENCE_GAP_LIMIT_INTERNAL : (isAnonymous ? SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN : SEQUENCE_GAP_LIMIT_INTERNAL); [mArray addObjectsFromArray:[path registerAddressesWithSettings:[DSGapLimitFunds external:externalLimit]]]; [mArray addObjectsFromArray:[path registerAddressesWithSettings:[DSGapLimitFunds internal:internalLimit]]]; } else if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { - NSArray *addresses = [derivationPath registerAddressesWithSettings:[DSGapLimit withLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING]]; + BOOL isInitial = stage == DSGapLimitStage_Initial; + NSUInteger limit = isInitial ? SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL : SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING; + + NSArray *addresses = [derivationPath registerAddressesWithSettings:[DSGapLimit withLimit:limit]]; [mArray addObjectsFromArray:addresses]; } } - return mArray; - + if (self.coinJoinDerivationPath) { + BOOL useReduced = [self.coinJoinDerivationPath shouldUseReducedGapLimit]; + BOOL isInitial = stage == DSGapLimitStage_Initial; + NSUInteger externalLimit = useReduced ? (isInitial ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL) : SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN; + NSUInteger internalLimit = useReduced ? (isInitial ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_INTERNAL) : SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN; + + [mArray addObjectsFromArray:[self.coinJoinDerivationPath registerAddressesWithSettings:[DSGapLimitFunds external:externalLimit]]]; + [mArray addObjectsFromArray:[self.coinJoinDerivationPath registerAddressesWithSettings:[DSGapLimitFunds internal:internalLimit]]]; + } + return [mArray copy]; } // all previously generated external addresses @@ -521,6 +524,9 @@ - (NSArray *)externalAddresses { for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { [mSet addObjectsFromArray:[(id)derivationPath allReceiveAddresses]]; } + if (self.coinJoinDerivationPath) + [mSet addObjectsFromArray:[self.coinJoinDerivationPath allReceiveAddresses]]; + if ([mSet containsObject:[NSNull null]]) { [mSet removeObject:[NSNull null]]; } @@ -535,6 +541,8 @@ - (NSArray *)internalAddresses { [mSet addObjectsFromArray:[(DSFundsDerivationPath *)derivationPath allChangeAddresses]]; } } + if (self.coinJoinDerivationPath) + [mSet addObjectsFromArray:[self.coinJoinDerivationPath allChangeAddresses]]; if ([mSet containsObject:[NSNull null]]) { [mSet removeObject:[NSNull null]]; } @@ -546,6 +554,8 @@ - (NSSet *)allAddresses { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { [mSet unionSet:[derivationPath allAddresses]]; } + if (self.coinJoinDerivationPath) + [mSet unionSet:[self.coinJoinDerivationPath allAddresses]]; return mSet; } @@ -554,6 +564,8 @@ - (NSSet *)usedAddresses { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { [mSet unionSet:[derivationPath usedAddresses]]; } + if (self.coinJoinDerivationPath) + [mSet unionSet:[self.coinJoinDerivationPath usedAddresses]]; return mSet; } @@ -568,6 +580,7 @@ - (BOOL)containsAddress:(NSString *)address { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath containsAddress:address]) return TRUE; } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsAddress:address]) return YES; return FALSE; } @@ -597,6 +610,7 @@ - (BOOL)containsInternalAddress:(NSString *)address { return TRUE; } } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsChangeAddress:address]) return YES; return FALSE; } @@ -612,6 +626,7 @@ - (BOOL)baseDerivationPathsContainAddress:(NSString *)address { return TRUE; } } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsAddress:address]) return YES; return FALSE; } @@ -630,6 +645,7 @@ - (BOOL)containsExternalAddress:(NSString *)address { if ([(DSIncomingFundsDerivationPath *)derivationPath containsAddress:address]) return TRUE; //!OCLINT } } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsReceiveAddress:address]) return TRUE; return FALSE; } @@ -657,6 +673,7 @@ - (BOOL)addressIsUsed:(NSString *)address { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath addressIsUsed:address]) return TRUE; } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath addressIsUsed:address]) return YES; return FALSE; } @@ -687,6 +704,8 @@ - (void)updateBalance { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { derivationPath.balance = 0; } + if (self.coinJoinDerivationPath) + self.coinJoinDerivationPath.balance = 0; for (DSTransaction *tx in [self.transactions reverseObjectEnumerator]) { #if LOG_BALANCE_UPDATE @@ -791,6 +810,16 @@ - (void)updateBalance { balance += amount; } } + if (self.coinJoinDerivationPath) { + if ([self.coinJoinDerivationPath containsAddress:output.address]) { + [self.coinJoinDerivationPath setHasKnownBalance]; + uint64_t amount = output.amount; + self.coinJoinDerivationPath.balance += amount; + [utxos addObject:dsutxo_obj(((DSUTXO){tx.txHash, n}))]; + balance += amount; + } + } + n++; } @@ -814,6 +843,9 @@ - (void)updateBalance { break; } } + if (self.coinJoinDerivationPath && [self.coinJoinDerivationPath containsAddress:output.address]) { + self.coinJoinDerivationPath.balance -= amount; + } } if (prevBalance < balance) totalReceived += balance - prevBalance; @@ -975,7 +1007,7 @@ - (BOOL)hasCoinbaseTransaction { - (BOOL)canContainTransaction:(DSTransaction *)transaction { NSParameterAssert(transaction); @synchronized (self) { - if ([[NSSet setWithArray:transaction.outputAddresses] intersectsSet:self.allAddresses]) return YES; + if ([self canContainTransactionOutputAddresses:transaction]) return YES; for (DSTransactionInput *input in transaction.inputs) { DSTransaction *tx = self.allTx[uint256_obj(input.inputHash)]; uint32_t n = input.index; @@ -1000,6 +1032,44 @@ - (BOOL)canContainTransaction:(DSTransaction *)transaction { } } +- (BOOL)canContainTransactionOutputAddresses:(DSTransaction *)transaction { + return [[NSSet setWithArray:transaction.outputAddresses] intersectsSet:self.allAddresses]; +} +- (BOOL)canContainTransactionOutputAddressesIncludingCoinjoin:(DSTransaction *)transaction { + for (NSString *address in transaction.outputAddresses) { + if ([self containsCoinJoinAddress:address]) return YES; + } + return NO; +} + +- (BOOL)canContainTransactionIncludingCoinjoin:(DSTransaction *)transaction { + NSParameterAssert(transaction); + @synchronized (self) { + if ([self canContainTransactionOutputAddresses:transaction] || [self canContainTransactionOutputAddressesIncludingCoinjoin:transaction]) return YES; + for (DSTransactionInput *input in transaction.inputs) { + DSTransaction *tx = self.allTx[uint256_obj(input.inputHash)]; + uint32_t n = input.index; + if (n < tx.outputs.count && ([self containsAddress:tx.outputs[n].address] || [self containsCoinJoinAddress:tx.outputs[n].address])) + return YES; + } + if ([transaction isKindOfClass:[DSProviderRegistrationTransaction class]]) { + DSProviderRegistrationTransaction *tx = (DSProviderRegistrationTransaction *)transaction; + if ([self containsAddress:tx.payoutAddress]) return YES; + } else if ([transaction isKindOfClass:[DSProviderUpdateServiceTransaction class]]) { + DSProviderUpdateServiceTransaction *tx = (DSProviderUpdateServiceTransaction *)transaction; + NSString *payoutAddress = tx.payoutAddress; + if (payoutAddress && [self containsAddress:payoutAddress]) return YES; + } else if ([transaction isKindOfClass:[DSProviderUpdateRegistrarTransaction class]]) { + DSProviderUpdateRegistrarTransaction *tx = (DSProviderUpdateRegistrarTransaction *)transaction; + if ([self containsAddress:tx.payoutAddress]) return YES; + } + // TODO: asset locks/unlocks/transitions? +// else if ([transaction isKindOfClass:[DSAs]]) + + return NO; + } +} + - (BOOL)checkIsFirstTransaction:(DSTransaction *)transaction { NSParameterAssert(transaction); for (DSDerivationPath *path in self.fundDerivationPaths) { @@ -1381,7 +1451,7 @@ - (NSArray *)usedDerivationPathsForTransaction:(DSTransaction *)transaction { @"internalIndexes": internalIndexes }]; } - + // TODO: why coinjoin doesn't matter? return usedDerivationPaths; } @@ -1615,14 +1685,21 @@ - (BOOL)registerTransaction:(DSTransaction *)transaction for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { [derivationPath registerTransactionAddress:address]; //only will register if derivation path contains address } + if (self.coinJoinDerivationPath) + [self.coinJoinDerivationPath registerTransactionAddress:address]; } for (DSTransactionOutput *output in transaction.outputs) { for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { [derivationPath registerTransactionAddress:output.address]; //only will register if derivation path contains address } + if (self.coinJoinDerivationPath) + [self.coinJoinDerivationPath registerTransactionAddress:output.address]; //only will register if derivation path contains address } + [transaction loadIdentitiesFromDerivationPaths:self.fundDerivationPaths]; [transaction loadIdentitiesFromDerivationPaths:self.outgoingFundDerivationPaths]; + if (self.coinJoinDerivationPath) + [transaction loadIdentitiesFromDerivationPaths:@[self.coinJoinDerivationPath]]; [self updateBalance]; if (saveImmediately) { if (!self.wallet.isTransient) { diff --git a/DashSync/shared/Models/Wallet/DSWallet+Identity.m b/DashSync/shared/Models/Wallet/DSWallet+Identity.m index 80282954..ffb0cd6b 100644 --- a/DashSync/shared/Models/Wallet/DSWallet+Identity.m +++ b/DashSync/shared/Models/Wallet/DSWallet+Identity.m @@ -131,6 +131,7 @@ - (void)loadIdentities { for (DSTransaction *transaction in account.allTransactions) { [transaction loadIdentitiesFromDerivationPaths:account.fundDerivationPaths]; [transaction loadIdentitiesFromDerivationPaths:account.outgoingFundDerivationPaths]; + [transaction loadIdentitiesFromDerivationPaths:@[account.coinJoinDerivationPath]]; } } }]; diff --git a/DashSync/shared/Models/Wallet/DSWallet+Tests.m b/DashSync/shared/Models/Wallet/DSWallet+Tests.m index f8e3060f..0b7c696d 100644 --- a/DashSync/shared/Models/Wallet/DSWallet+Tests.m +++ b/DashSync/shared/Models/Wallet/DSWallet+Tests.m @@ -64,6 +64,8 @@ + (NSString *)setTransientDerivedKeyData:(NSData *)derivedKeyData withAccounts:( for (DSDerivationPath *derivationPath in account.fundDerivationPaths) { [derivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:nil]; } + if (account.coinJoinDerivationPath) + [account.coinJoinDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:nil]; if ([chain isEvolutionEnabled]) { [account.masterContactsDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:nil]; } diff --git a/DashSync/shared/Models/Wallet/DSWallet.h b/DashSync/shared/Models/Wallet/DSWallet.h index 03ccbf3b..24be8ac2 100644 --- a/DashSync/shared/Models/Wallet/DSWallet.h +++ b/DashSync/shared/Models/Wallet/DSWallet.h @@ -25,6 +25,7 @@ #import "BigIntTypes.h" #import "DSBIP39Mnemonic.h" #import "DSIdentity.h" +#import "DSGapLimit.h" #import NS_ASSUME_NONNULL_BEGIN @@ -154,8 +155,7 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSWalletBalanceDidChangeNotification; - (DSTransaction *_Nullable)transactionForHash:(UInt256)txHash; - (NSArray *)allAddresses; -- (NSArray *_Nullable)registerAddressesWithInitialGapLimit; -- (NSArray *_Nullable)registerAddressesWithProlongGapLimit; +- (NSArray *)registerAddressesAtStage:(DSGapLimitStage)stage; // returns the amount received by the wallet from the transaction (total outputs to change and/or receive addresses) - (uint64_t)amountReceivedFromTransaction:(DSTransaction *)transaction; diff --git a/DashSync/shared/Models/Wallet/DSWallet.m b/DashSync/shared/Models/Wallet/DSWallet.m index 5d71896e..325e97a7 100644 --- a/DashSync/shared/Models/Wallet/DSWallet.m +++ b/DashSync/shared/Models/Wallet/DSWallet.m @@ -303,6 +303,9 @@ - (DSAccount *)addAnotherAccountIfAuthenticated { [derivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:self.uniqueIDString]; } + if (addAccount.coinJoinDerivationPath) + [addAccount.coinJoinDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:self.uniqueIDString]; + if ([self.chain isEvolutionEnabled]) { [addAccount.masterContactsDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:self.uniqueIDString]; @@ -618,6 +621,8 @@ + (NSString *)setSeedPhrase:(NSString *)seedPhrase createdAt:(NSTimeInterval)cre for (DSDerivationPath *derivationPath in account.fundDerivationPaths) { [derivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:storeOnUniqueId]; } + if (account.coinJoinDerivationPath) + [account.coinJoinDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:storeOnUniqueId]; if ([chain isEvolutionEnabled]) { [account.masterContactsDerivationPath generateExtendedPublicKeyFromSeed:derivedKeyData storeUnderWalletUniqueId:storeOnUniqueId]; } @@ -720,20 +725,13 @@ - (uint64_t)balance { return allAddressesArray; } -- (NSArray *)registerAddressesWithInitialGapLimit { +- (NSArray *)registerAddressesAtStage:(DSGapLimitStage)stage { NSMutableArray *mArray = [NSMutableArray array]; for (DSAccount *account in self.accounts) { - [mArray addObjectsFromArray:[account registerAddressesWithInitialGapLimit]]; + [mArray addObjectsFromArray:[account registerAddressesAtStage:stage]]; } return [mArray copy]; -} -- (NSArray *)registerAddressesWithProlongGapLimit { - NSMutableArray *mArray = [NSMutableArray array]; - for (DSAccount *account in self.accounts) { - [mArray addObjectsFromArray:[account registerAddressesWithProlongGapLimit]]; - } - return [mArray copy]; } - (DSAccount *)firstAccountThatCanContainTransaction:(DSTransaction *)transaction { @@ -1003,6 +1001,8 @@ - (void)reloadDerivationPaths { for (DSDerivationPath *derivationPath in account.fundDerivationPaths) { [derivationPath reloadAddresses]; } + if (account.coinJoinDerivationPath) + [account.coinJoinDerivationPath reloadAddresses]; } for (DSDerivationPath *derivationPath in self.specializedDerivationPaths) { [derivationPath reloadAddresses]; diff --git a/Example/DashSync/Base.lproj/Main.storyboard b/Example/DashSync/Base.lproj/Main.storyboard index 51812501..0bd27a2c 100644 --- a/Example/DashSync/Base.lproj/Main.storyboard +++ b/Example/DashSync/Base.lproj/Main.storyboard @@ -21,13 +21,13 @@