diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dac065ec1..e0ed49999 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,10 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '15.4' + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim - name: Build dependencies @@ -59,4 +63,4 @@ jobs: -scheme "DashSync-Example" \ -workspace "DashSync.xcworkspace" \ -destination "platform=$platform,name=iPhone 13" \ - CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED=NO \ No newline at end of file + CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED=NO diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6504c8e63..71a1fd2be 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -43,6 +43,10 @@ jobs: ${{ runner.os }}-pods- - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/.github/workflows/e2eTestsTestnet.yml b/.github/workflows/e2eTestsTestnet.yml index debb99192..85d59ffd8 100644 --- a/.github/workflows/e2eTestsTestnet.yml +++ b/.github/workflows/e2eTestsTestnet.yml @@ -45,6 +45,10 @@ jobs: ${{ runner.os }}-pods- - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2afc376d8..911ae0a85 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -43,6 +43,10 @@ jobs: key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} restore-keys: | ${{ runner.os }}-pods- + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim - name: Dependencies diff --git a/.github/workflows/network.yml b/.github/workflows/network.yml index 342edf1b3..18268c01a 100644 --- a/.github/workflows/network.yml +++ b/.github/workflows/network.yml @@ -44,6 +44,10 @@ jobs: run: cargo install cargo-lipo - name: Rustup add targets run: rustup target add x86_64-apple-darwin + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Build Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/.github/workflows/syncTestMainnet.yml b/.github/workflows/syncTestMainnet.yml index 54aa929e3..4f62f4c3f 100644 --- a/.github/workflows/syncTestMainnet.yml +++ b/.github/workflows/syncTestMainnet.yml @@ -44,6 +44,10 @@ jobs: ${{ runner.os }}-pods- - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/.github/workflows/syncTestTestnet.yml b/.github/workflows/syncTestTestnet.yml index d55703616..7deb6cab1 100644 --- a/.github/workflows/syncTestTestnet.yml +++ b/.github/workflows/syncTestTestnet.yml @@ -45,6 +45,10 @@ jobs: ${{ runner.os }}-pods- - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 - name: Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 205e35954..d8a98852b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,11 @@ jobs: - name: Rustup add targets run: rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim + - name: Setup CocoaPods + uses: maxim-lobanov/setup-cocoapods@v1 + with: + version: 1.15.2 + - name: Dependencies working-directory: ./dashsync/Example run: pod install --repo-update diff --git a/DashSync.podspec b/DashSync.podspec index f052a94ad..a3fc3e9eb 100644 --- a/DashSync.podspec +++ b/DashSync.podspec @@ -34,7 +34,7 @@ Pod::Spec.new do |s| s.ios.framework = 'UIKit' s.macos.framework = 'Cocoa' s.compiler_flags = '-Wno-comma' - s.dependency 'DashSharedCore', '0.4.19' + s.dependency 'DashSharedCore', '0.5.1' s.dependency 'CocoaLumberjack', '3.7.2' s.ios.dependency 'DWAlertController', '0.2.1' s.dependency 'DSDynamicOptions', '0.1.2' @@ -43,4 +43,3 @@ Pod::Spec.new do |s| s.prefix_header_contents = '#import "DSEnvironment.h"' end - diff --git a/DashSync/shared/DashSync.m b/DashSync/shared/DashSync.m index 25d8f807b..030364cc0 100644 --- a/DashSync/shared/DashSync.m +++ b/DashSync/shared/DashSync.m @@ -19,6 +19,7 @@ #import "DSPeerEntity+CoreDataClass.h" #import "DSPeerManager+Protected.h" #import "DSSyncState.h" +#import "DSCoinJoinManager.h" #import "DSQuorumEntryEntity+CoreDataClass.h" #import "DSQuorumSnapshotEntity+CoreDataClass.h" #import "DSSporkManager+Protected.h" @@ -122,13 +123,14 @@ - (void)startSyncForChain:(DSChain *)chain { - (void)stopSyncAllChains { NSArray *chains = [[DSChainsManager sharedInstance] chains]; for (DSChain *chain in chains) { + [[DSCoinJoinManager sharedInstanceForChain:chain] stop]; [[[DSChainsManager sharedInstance] chainManagerForChain:chain].peerManager disconnect: DSDisconnectReason_ChainWipe]; } } - (void)stopSyncForChain:(DSChain *)chain { NSParameterAssert(chain); - + [[DSCoinJoinManager sharedInstanceForChain:chain] stop]; [[[DSChainsManager sharedInstance] chainManagerForChain:chain] stopSync]; } diff --git a/DashSync/shared/Libraries/DSLogger.m b/DashSync/shared/Libraries/DSLogger.m index 5f59dc9c7..2d68e4ee0 100644 --- a/DashSync/shared/Libraries/DSLogger.m +++ b/DashSync/shared/Libraries/DSLogger.m @@ -16,6 +16,7 @@ // #import "DSLogger.h" +#import "CompressingLogFileManager.h" NS_ASSUME_NONNULL_BEGIN @@ -49,10 +50,13 @@ - (instancetype)init { if (self) { [DDLog addLogger:[DDOSLogger sharedInstance]]; // os_log - DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; - fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling - fileLogger.logFileManager.maximumNumberOfLogFiles = 3; // keep a 3 days worth of log files - //[fileLogger setLogFormatter:[[NoTimestampLogFormatter alloc] init]]; // Use the custom formatter + unsigned long long maxFileSize = 1024 * 1024 * 5; // 5 MB max. Then log files are ziped + CompressingLogFileManager *logFileManager = [[CompressingLogFileManager alloc] initWithFileSize:maxFileSize]; + DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:logFileManager]; + fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling + fileLogger.maximumFileSize = maxFileSize; + fileLogger.logFileManager.maximumNumberOfLogFiles = 10; + [DDLog addLogger:fileLogger]; _fileLogger = fileLogger; } @@ -60,22 +64,23 @@ - (instancetype)init { } - (NSArray *)logFiles { - NSArray *logFileInfos = [self.fileLogger.logFileManager unsortedLogFileInfos]; - NSMutableArray *logFiles = [NSMutableArray array]; - for (DDLogFileInfo *fileInfo in logFileInfos) { - NSURL *fileURL = [NSURL fileURLWithPath:fileInfo.filePath]; - if (fileURL) { - [logFiles addObject:fileURL]; - } - } - // add rust log file located at $CACHE/Logs/processor.log - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - NSString *cacheDirectory = [paths objectAtIndex:0]; - NSString *rustLogPath = [cacheDirectory stringByAppendingPathComponent:@"Logs/processor.log"]; + NSString *logsDirectory = [self.fileLogger.logFileManager logsDirectory]; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil]; + NSMutableArray *logFiles = [NSMutableArray arrayWithCapacity:[fileNames count]]; - if ([[NSFileManager defaultManager] fileExistsAtPath:rustLogPath]) { - [logFiles addObject:[NSURL fileURLWithPath:rustLogPath]]; + for (NSString *fileName in fileNames) { + BOOL hasProperSuffix = [fileName hasSuffix:@".log"] || [fileName hasSuffix:@".gz"]; + + if (hasProperSuffix) { + NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + + if (fileURL) { + [logFiles addObject:fileURL]; + } + } } + return [logFiles copy]; } diff --git a/DashSync/shared/Libraries/Logs/CompressingLogFileManager.h b/DashSync/shared/Libraries/Logs/CompressingLogFileManager.h new file mode 100644 index 000000000..8a34b52e2 --- /dev/null +++ b/DashSync/shared/Libraries/Logs/CompressingLogFileManager.h @@ -0,0 +1,20 @@ +// +// CompressingLogFileManager.h +// LogFileCompressor +// +// CocoaLumberjack Demos +// + +#import +#import + +@interface CompressingLogFileManager : DDLogFileManagerDefault +{ + BOOL upToDate; + BOOL isCompressing; +} + +@property (nonatomic, assign) unsigned long long maxFileSize; +- (instancetype)initWithFileSize:(unsigned long long)maxFileSize; + +@end diff --git a/DashSync/shared/Libraries/Logs/CompressingLogFileManager.m b/DashSync/shared/Libraries/Logs/CompressingLogFileManager.m new file mode 100644 index 000000000..e1aaa013c --- /dev/null +++ b/DashSync/shared/Libraries/Logs/CompressingLogFileManager.m @@ -0,0 +1,563 @@ +// +// CompressingLogFileManager.m +// LogFileCompressor +// +// CocoaLumberjack Demos +// + +#import "CompressingLogFileManager.h" +#import "dash_shared_core.h" +#import + +// We probably shouldn't be using DDLog() statements within the DDLog implementation. +// But we still want to leave our log statements for any future debugging, +// and to allow other developers to trace the implementation (which is a great learning tool). +// +// So we use primitive logging macros around NSLog. +// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. + +#define LOG_LEVEL 4 + +#define NSLogError(frmt, ...) do{ if(LOG_LEVEL >= 1) NSLog(frmt, ##__VA_ARGS__); } while(0) +#define NSLogWarn(frmt, ...) do{ if(LOG_LEVEL >= 2) NSLog(frmt, ##__VA_ARGS__); } while(0) +#define NSLogInfo(frmt, ...) do{ if(LOG_LEVEL >= 3) NSLog(frmt, ##__VA_ARGS__); } while(0) +#define NSLogVerbose(frmt, ...) do{ if(LOG_LEVEL >= 4) NSLog(frmt, ##__VA_ARGS__); } while(0) + +@interface CompressingLogFileManager (/* Must be nameless for properties */) + +@property (readwrite) BOOL isCompressing; + +@end + +@interface DDLogFileInfo (Compressor) + +@property (nonatomic, readonly) BOOL isCompressed; + +- (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt; +- (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation CompressingLogFileManager + +@synthesize isCompressing; + +- (instancetype)initWithFileSize:(unsigned long long)maxFileSize { + self = [self initWithLogsDirectory:nil]; + if (self) { + _maxFileSize = maxFileSize; + } + return self; +} + +- (id)init +{ + return [self initWithLogsDirectory:nil]; +} + +- (id)initWithLogsDirectory:(NSString *)aLogsDirectory +{ + if ((self = [super initWithLogsDirectory:aLogsDirectory])) + { + upToDate = NO; + + // Check for any files that need to be compressed. + // But don't start right away. + // Wait for the app startup process to finish. + + [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:5.0]; + } + return self; +} + +- (void)dealloc +{ + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(compressNextLogFile) object:nil]; +} + +- (void)compressLogFile:(DDLogFileInfo *)logFile +{ + self.isCompressing = YES; + + CompressingLogFileManager* __weak weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + [weakSelf backgroundThread_CompressLogFile:logFile]; + }); +} + +- (void)compressNextLogFile +{ + if (self.isCompressing) + { + // We're already compressing a file. + // Wait until it's done to move onto the next file. + return; + } + + NSLogVerbose(@"CompressingLogFileManager: compressNextLogFile"); + + upToDate = NO; + + NSMutableArray *sortedLogFileInfos = [[self sortedLogFileInfos] mutableCopy]; + [self handleProcessorLogRotationWithSortedLogs:sortedLogFileInfos]; + + NSUInteger count = [sortedLogFileInfos count]; + if (count == 0) + { + // Nothing to compress + upToDate = YES; + return; + } + + NSUInteger i = count; + while (i > 0) + { + DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:(i - 1)]; + + if ((logFileInfo.isArchived || [logFileInfo.fileName hasPrefix:@"processor"]) && !logFileInfo.isCompressed) + { + [self compressLogFile:logFileInfo]; + + break; + } + + i--; + } + + upToDate = YES; +} + +- (void)handleProcessorLogRotationWithSortedLogs:(NSMutableArray *)sortedLogFileInfos { + // Get all processor log files + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *files = [fileManager contentsOfDirectoryAtPath:self.logsDirectory error:nil]; + NSMutableArray *processorLogs = [NSMutableArray array]; + NSMutableArray *compressedLogs = [NSMutableArray array]; + + // Separate processor logs into compressed and uncompressed + for (NSString *file in files) { + if ([file hasPrefix:@"processor."]) { + NSArray *components = [file componentsSeparatedByString:@"."]; + if (components.count >= 3) { + if ([file hasSuffix:@".gz"]) { + [compressedLogs addObject:file]; + } else { + [processorLogs addObject:file]; + } + } + } + } + + // Remove compressed logs older than 5 days + NSDate *cutoffDate = [NSDate dateWithTimeIntervalSinceNow:-5 * 24 * 60 * 60]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd"]; + + for (NSString *file in compressedLogs) { + // Extract date from filename (assuming format processor.[date].log.gz) + NSArray *components = [file componentsSeparatedByString:@"."]; + if (components.count >= 2) { + NSString *dateString = components[1]; + NSDate *fileDate = [dateFormatter dateFromString:dateString]; + + if (fileDate && [fileDate compare:cutoffDate] == NSOrderedAscending) { + NSString *fullPath = [self.logsDirectory stringByAppendingPathComponent:file]; + [fileManager removeItemAtPath:fullPath error:nil]; + } + } + } + + // Handle uncompressed processor logs + if (processorLogs.count > 1) { + // Sort by date (oldest first) + [processorLogs sortUsingComparator:^NSComparisonResult(NSString *file1, NSString *file2) { + return [file1 compare:file2]; + }]; + + // Add older logs to sortedLogFileInfos for compression + for (NSInteger i = 0; i < processorLogs.count - 1; i++) { + NSString *logPath = [self.logsDirectory stringByAppendingPathComponent:processorLogs[i]]; + [sortedLogFileInfos addObject:[DDLogFileInfo logFileWithPath:logPath]]; + } + } +} + +- (void)compressionDidSucceed:(DDLogFileInfo *)logFile +{ + NSLogVerbose(@"CompressingLogFileManager: compressionDidSucceed: %@", logFile.fileName); + + self.isCompressing = NO; + + [self compressNextLogFile]; +} + +- (void)compressionDidFail:(DDLogFileInfo *)logFile +{ + NSLogWarn(@"CompressingLogFileManager: compressionDidFail: %@", logFile.fileName); + + self.isCompressing = NO; + + // We should try the compression again, but after a short delay. + // + // If the compression failed there is probably some filesystem issue, + // so flooding it with compression attempts is only going to make things worse. + + NSTimeInterval delay = (60 * 15); // 15 minutes + + [self performSelector:@selector(compressNextLogFile) withObject:nil afterDelay:delay]; +} + +- (void)didArchiveLogFile:(NSString *)logFilePath wasRolled:(BOOL)wasRolled { + NSLogVerbose(@"CompressingLogFileManager: didArchiveLogFile: %@ wasRolled: %@", + [logFilePath lastPathComponent], (wasRolled ? @"YES" : @"NO")); + + // If all other log files have been compressed, then we can get started right away. + // Otherwise we should just wait for the current compression process to finish. + + if (upToDate) + { + [self compressLogFile:[DDLogFileInfo logFileWithPath:logFilePath]]; + } +} + +- (void)backgroundThread_CompressLogFile:(DDLogFileInfo *)logFile +{ + @autoreleasepool { + + NSLogInfo(@"CompressingLogFileManager: Compressing log file: %@", logFile.fileName); + + // Steps: + // 1. Create a new file with the same fileName, but added "gzip" extension + // 2. Open the new file for writing (output file) + // 3. Open the given file for reading (input file) + // 4. Setup zlib for gzip compression + // 5. Read a chunk of the given file + // 6. Compress the chunk + // 7. Write the compressed chunk to the output file + // 8. Repeat steps 5 - 7 until the input file is exhausted + // 9. Close input and output file + // 10. Teardown zlib + + + // STEP 1 + + NSString *inputFilePath = logFile.filePath; + + NSString *tempOutputFilePath = [logFile tempFilePathByAppendingPathExtension:@"gz"]; + +#if TARGET_OS_IPHONE + // We use the same protection as the original file. This means that it has the same security characteristics. + // Also, if the app can run in the background, this means that it gets + // NSFileProtectionCompleteUntilFirstUserAuthentication so that we can do this compression even with the + // device locked. c.f. DDFileLogger.doesAppRunInBackground. + NSString* protection = logFile.fileAttributes[NSFileProtectionKey]; + NSDictionary* attributes = protection == nil ? nil : @{NSFileProtectionKey: protection}; + [[NSFileManager defaultManager] createFileAtPath:tempOutputFilePath contents:nil attributes:attributes]; +#endif + + // STEP 2 & 3 + + NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:inputFilePath]; + NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:tempOutputFilePath append:NO]; + + [inputStream open]; + [outputStream open]; + + // STEP 4 + + z_stream strm; + + // Zero out the structure before (to be safe) before we start using it + bzero(&strm, sizeof(strm)); + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.total_out = 0; + + // Compresssion Levels: + // Z_NO_COMPRESSION + // Z_BEST_SPEED + // Z_BEST_COMPRESSION + // Z_DEFAULT_COMPRESSION + + deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, (15+16), 9, Z_DEFAULT_STRATEGY); + + // Prepare our variables for steps 5-7 + // + // inputDataLength : Total length of buffer that we will read file data into + // outputDataLength : Total length of buffer that zlib will output compressed bytes into + // + // Note: The output buffer can be smaller than the input buffer because the + // compressed/output data is smaller than the file/input data (obviously). + // + // inputDataSize : The number of bytes in the input buffer that have valid data to be compressed. + // + // Imagine compressing a tiny file that is actually smaller than our inputDataLength. + // In this case only a portion of the input buffer would have valid file data. + // The inputDataSize helps represent the portion of the buffer that is valid. + // + // Imagine compressing a huge file, but consider what happens when we get to the very end of the file. + // The last read will likely only fill a portion of the input buffer. + // The inputDataSize helps represent the portion of the buffer that is valid. + + NSUInteger inputDataLength = (1024 * 64); // 64 KB + NSUInteger outputDataLength = (1024 * 32); // 32 KB + + NSMutableData *inputData = [NSMutableData dataWithLength:inputDataLength]; + NSMutableData *outputData = [NSMutableData dataWithLength:outputDataLength]; + + NSUInteger inputDataSize = 0; + + BOOL done = YES; + NSError* error = nil; + do + { + @autoreleasepool { + + // STEP 5 + // Read data from the input stream into our input buffer. + // + // inputBuffer : pointer to where we want the input stream to copy bytes into + // inputBufferLength : max number of bytes the input stream should read + // + // Recall that inputDataSize is the number of valid bytes that already exist in the + // input buffer that still need to be compressed. + // This value is usually zero, but may be larger if a previous iteration of the loop + // was unable to compress all the bytes in the input buffer. + // + // For example, imagine that we ready 2K worth of data from the file in the last loop iteration, + // but when we asked zlib to compress it all, zlib was only able to compress 1.5K of it. + // We would still have 0.5K leftover that still needs to be compressed. + // We want to make sure not to skip this important data. + // + // The [inputData mutableBytes] gives us a pointer to the beginning of the underlying buffer. + // When we add inputDataSize we get to the proper offset within the buffer + // at which our input stream can start copying bytes into without overwriting anything it shouldn't. + + const void *inputBuffer = [inputData mutableBytes] + inputDataSize; + NSUInteger inputBufferLength = inputDataLength - inputDataSize; + + NSInteger readLength = [inputStream read:(uint8_t *)inputBuffer maxLength:inputBufferLength]; + if (readLength < 0) { + error = [inputStream streamError]; + break; + } + + NSLogVerbose(@"CompressingLogFileManager: Read %li bytes from file", (long)readLength); + + inputDataSize += readLength; + + // STEP 6 + // Ask zlib to compress our input buffer. + // Tell it to put the compressed bytes into our output buffer. + + strm.next_in = (Bytef *)[inputData mutableBytes]; // Read from input buffer + strm.avail_in = (uInt)inputDataSize; // as much as was read from file (plus leftovers). + + strm.next_out = (Bytef *)[outputData mutableBytes]; // Write data to output buffer + strm.avail_out = (uInt)outputDataLength; // as much space as is available in the buffer. + + // When we tell zlib to compress our data, + // it won't directly tell us how much data was processed. + // Instead it keeps a running total of the number of bytes it has processed. + // In other words, every iteration from the loop it increments its total values. + // So to figure out how much data was processed in this iteration, + // we fetch the totals before we ask it to compress data, + // and then afterwards we subtract from the new totals. + + NSUInteger prevTotalIn = strm.total_in; + NSUInteger prevTotalOut = strm.total_out; + + int flush = [inputStream hasBytesAvailable] ? Z_SYNC_FLUSH : Z_FINISH; + deflate(&strm, flush); + + NSUInteger inputProcessed = strm.total_in - prevTotalIn; + NSUInteger outputProcessed = strm.total_out - prevTotalOut; + + NSLogVerbose(@"CompressingLogFileManager: Total bytes uncompressed: %lu", (unsigned long)strm.total_in); + NSLogVerbose(@"CompressingLogFileManager: Total bytes compressed: %lu", (unsigned long)strm.total_out); + NSLogVerbose(@"CompressingLogFileManager: Compression ratio: %.1f%%", + (double)(1.0F - (float)(strm.total_out) / (float)(strm.total_in)) * 100); + + // STEP 7 + // Now write all compressed bytes to our output stream. + // + // It is theoretically possible that the write operation doesn't write everything we ask it to. + // Although this is highly unlikely, we take precautions. + // Also, we watch out for any errors (maybe the disk is full). + + NSUInteger totalWriteLength = 0; + NSInteger writeLength = 0; + + do + { + const void *outputBuffer = [outputData mutableBytes] + totalWriteLength; + NSUInteger outputBufferLength = outputProcessed - totalWriteLength; + + writeLength = [outputStream write:(const uint8_t *)outputBuffer maxLength:outputBufferLength]; + + if (writeLength < 0) + { + error = [outputStream streamError]; + } + else + { + totalWriteLength += writeLength; + } + + } while((totalWriteLength < outputProcessed) && !error); + + // STEP 7.5 + // + // We now have data in our input buffer that has already been compressed. + // We want to remove all the processed data from the input buffer, + // and we want to move any unprocessed data to the beginning of the buffer. + // + // If the amount processed is less than the valid buffer size, we have leftovers. + + NSUInteger inputRemaining = inputDataSize - inputProcessed; + if (inputRemaining > 0) + { + void *inputDst = [inputData mutableBytes]; + void *inputSrc = [inputData mutableBytes] + inputProcessed; + + memmove(inputDst, inputSrc, inputRemaining); + } + + inputDataSize = inputRemaining; + + // Are we done yet? + + done = ((flush == Z_FINISH) && (inputDataSize == 0)); + + // STEP 8 + // Loop repeats until end of data (or unlikely error) + + } // end @autoreleasepool + + } while (!done && error == nil); + + // STEP 9 + + [inputStream close]; + [outputStream close]; + + // STEP 10 + + deflateEnd(&strm); + + // We're done! + // Report success or failure back to the logging thread/queue. + + if (error) + { + // Remove output file. + // Our compression attempt failed. + + NSLogError(@"Compression of %@ failed: %@", inputFilePath, error); + error = nil; + BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:tempOutputFilePath error:&error]; + if (!ok) + NSLogError(@"Failed to clean up %@ after failed compression: %@", tempOutputFilePath, error); + + // Report failure to class via logging thread/queue + + dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool { + [self compressionDidFail:logFile]; + }}); + } + else + { + // Remove original input file. + // It will be replaced with the new compressed version. + + error = nil; + BOOL ok = [[NSFileManager defaultManager] removeItemAtPath:inputFilePath error:&error]; + if (!ok) + NSLogWarn(@"Warning: failed to remove original file %@ after compression: %@", inputFilePath, error); + + // Mark the compressed file as archived, + // and then move it into its final destination. + // + // temp-log-ABC123.txt.gz -> log-ABC123.txt.gz + // + // The reason we were using the "temp-" prefix was so the file would not be + // considered a log file while it was only partially complete. + // Only files that begin with "log-" are considered log files. + + DDLogFileInfo *compressedLogFile = [DDLogFileInfo logFileWithPath:tempOutputFilePath]; + compressedLogFile.isArchived = YES; + + NSString *outputFileName = [logFile fileNameByAppendingPathExtension:@"gz"]; + [compressedLogFile renameFile:outputFileName]; + + // Report success to class via logging thread/queue + + dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool { + [self compressionDidSucceed:compressedLogFile]; + }}); + } + + } // end @autoreleasepool +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDLogFileInfo (Compressor) + +@dynamic isCompressed; + +- (BOOL)isCompressed +{ + return [[[self fileName] pathExtension] isEqualToString:@"gz"]; +} + +- (NSString *)tempFilePathByAppendingPathExtension:(NSString *)newExt +{ + // Example: + // + // Current File Name: "/full/path/to/log-ABC123.txt" + // + // newExt: "gzip" + // result: "/full/path/to/temp-log-ABC123.txt.gzip" + + NSString *tempFileName = [NSString stringWithFormat:@"temp-%@", [self fileName]]; + + NSString *newFileName = [tempFileName stringByAppendingPathExtension:newExt]; + + NSString *fileDir = [[self filePath] stringByDeletingLastPathComponent]; + + NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName]; + + return newFilePath; +} + +- (NSString *)fileNameByAppendingPathExtension:(NSString *)newExt +{ + // Example: + // + // Current File Name: "log-ABC123.txt" + // + // newExt: "gzip" + // result: "log-ABC123.txt.gzip" + + NSString *fileNameExtension = [[self fileName] pathExtension]; + + if ([fileNameExtension isEqualToString:newExt]) + { + return [self fileName]; + } + + return [[self fileName] stringByAppendingPathExtension:newExt]; +} + +@end diff --git a/DashSync/shared/Models/Chain/DSChain.m b/DashSync/shared/Models/Chain/DSChain.m index 834465ffc..cefb7a469 100644 --- a/DashSync/shared/Models/Chain/DSChain.m +++ b/DashSync/shared/Models/Chain/DSChain.m @@ -652,10 +652,12 @@ - (void)setDevnetNetworkName:(NSString *)networkName { - (NSArray *)standardDerivationPathsForAccountNumber:(uint32_t)accountNumber { if (accountNumber == 0) { - return @[[DSFundsDerivationPath bip32DerivationPathForAccountNumber:accountNumber onChain:self], [DSFundsDerivationPath bip44DerivationPathForAccountNumber:accountNumber onChain:self], [DSDerivationPath masterBlockchainIdentityContactsDerivationPathForAccountNumber:accountNumber onChain:self]]; + return @[[DSFundsDerivationPath bip32DerivationPathForAccountNumber:accountNumber onChain:self], [DSFundsDerivationPath bip44DerivationPathForAccountNumber:accountNumber onChain:self], [DSDerivationPath masterBlockchainIdentityContactsDerivationPathForAccountNumber:accountNumber onChain:self], + [DSFundsDerivationPath coinJoinDerivationPathForAccountNumber:accountNumber onChain:self]]; } else { //don't include BIP32 derivation path on higher accounts - return @[[DSFundsDerivationPath bip44DerivationPathForAccountNumber:accountNumber onChain:self], [DSDerivationPath masterBlockchainIdentityContactsDerivationPathForAccountNumber:accountNumber onChain:self]]; + return @[[DSFundsDerivationPath bip44DerivationPathForAccountNumber:accountNumber onChain:self], [DSDerivationPath masterBlockchainIdentityContactsDerivationPathForAccountNumber:accountNumber onChain:self], + [DSFundsDerivationPath coinJoinDerivationPathForAccountNumber:accountNumber onChain:self]]; } } @@ -1228,8 +1230,8 @@ - (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 registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INITIAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_INITIAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL internal:NO error:nil]; - [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INITIAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_INITIAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL internal:YES error:nil]; + [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INITIAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_INITIAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL coinJoinGapLimit:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN internal:NO error:nil]; + [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INITIAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_INITIAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INITIAL coinJoinGapLimit:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN internal:YES error:nil]; NSSet *addresses = [wallet.allReceiveAddresses setByAddingObjectsFromSet:wallet.allChangeAddresses]; [allAddresses addObjectsFromArray:[addresses allObjects]]; [allUTXOs addObjectsFromArray:wallet.unspentOutputs]; diff --git a/DashSync/shared/Models/Chain/DSChainConstants.h b/DashSync/shared/Models/Chain/DSChainConstants.h index 1a8b5f9b5..fc4a4f351 100644 --- a/DashSync/shared/Models/Chain/DSChainConstants.h +++ b/DashSync/shared/Models/Chain/DSChainConstants.h @@ -82,3 +82,5 @@ #define MAX_FEE_PER_B 1000 // slightly higher than a 1000bit fee on a 191byte tx #define HEADER_WINDOW_BUFFER_TIME (WEEK_TIME_INTERVAL / 2) //This is about the time if we consider a block every 10 mins (for 500 blocks) + +#define COINBASE_MATURITY 100 // Coinbase transaction outputs can only be spent after this number of new blocks (network rule) diff --git a/DashSync/shared/Models/CoinJoin/DSCoinControl.h b/DashSync/shared/Models/CoinJoin/DSCoinControl.h new file mode 100644 index 000000000..a2fbb0af5 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinControl.h @@ -0,0 +1,52 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "BigIntTypes.h" +#import "dash_shared_core.h" + +NS_ASSUME_NONNULL_BEGIN + +// CoinControl comes from Dash Core. Not all functions fields and functions are supported within the Wallet class +@interface DSCoinControl : NSObject + +@property (nonatomic, assign) BOOL allowOtherInputs; +@property (nonatomic, assign) BOOL requireAllInputs; +@property (nonatomic, assign) BOOL allowWatchOnly; +@property (nonatomic, assign) BOOL overrideFeeRate; +@property (nonatomic, assign) BOOL avoidPartialSpends; +@property (nonatomic, assign) BOOL avoidAddressReuse; +@property (nonatomic, assign) int32_t minDepth; +@property (nonatomic, assign) int32_t maxDepth; +@property (nonatomic, assign) uint64_t feeRate; +@property (nonatomic, assign) uint64_t discardFeeRate; +@property (nonatomic, strong) NSNumber *confirmTarget; +@property (nonatomic, assign) CoinType coinType; +@property (nonatomic, strong) NSMutableOrderedSet *setSelected; +@property (nonatomic, strong) NSString *destChange; + +- (instancetype)initWithFFICoinControl:(CoinControl *)coinControl chainType:(ChainType)chainType; + +- (BOOL)hasSelected; +- (BOOL)isSelected:(DSUTXO)utxo; +- (void)useCoinJoin:(BOOL)useCoinJoin; +- (BOOL)isUsingCoinJoin; +- (void)select:(DSUTXO)utxo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSCoinControl.m b/DashSync/shared/Models/CoinJoin/DSCoinControl.m new file mode 100644 index 000000000..f410679be --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinControl.m @@ -0,0 +1,104 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCoinControl.h" + +@implementation DSCoinControl + +- (instancetype)initWithFFICoinControl:(CoinControl *)coinControl chainType:(ChainType)chainType { + if (!(self = [super init])) return nil; + self.coinType = coinControl->coin_type; + self.minDepth = coinControl->min_depth; + self.maxDepth = coinControl->max_depth; + self.avoidAddressReuse = coinControl->avoid_address_reuse; + self.allowOtherInputs = coinControl->allow_other_inputs; + + if (coinControl->dest_change) { + char *c_string = address_with_script_pubkey(coinControl->dest_change->ptr, coinControl->dest_change->len, chainType); + self.destChange = [NSString stringWithUTF8String:c_string]; + } + + if (coinControl->set_selected && coinControl->set_selected_size > 0) { + self.setSelected = [[NSMutableOrderedSet alloc] init]; + + for (size_t i = 0; i < coinControl->set_selected_size; i++) { + TxOutPoint *outpoint = coinControl->set_selected[i]; + + if (outpoint) { + UInt256 hash; + memcpy(hash.u8, outpoint->hash, 32); + NSValue *value = dsutxo_obj(((DSUTXO){hash, outpoint->index})); + [self.setSelected addObject:value]; + } + } + } else { + self.setSelected = [[NSMutableOrderedSet alloc] init]; + } + + return self; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _setSelected = [[NSMutableOrderedSet alloc] init]; + _coinType = CoinType_AllCoins; + _allowOtherInputs = NO; + _requireAllInputs = NO; + _allowWatchOnly = NO; + _overrideFeeRate = NO; + _avoidPartialSpends = NO; + _avoidAddressReuse = NO; + _minDepth = 0; + _destChange = NULL; + } + return self; +} + +- (BOOL)hasSelected { + return self.setSelected.count > 0; +} + +- (BOOL)isSelected:(DSUTXO)utxo { + for (NSValue *selectedValue in self.setSelected) { + DSUTXO selectedUTXO; + [selectedValue getValue:&selectedUTXO]; + + if (dsutxo_eq(utxo, selectedUTXO)) { + return YES; + } + } + + return NO; +} + +- (void)useCoinJoin:(BOOL)useCoinJoin { + self.coinType = useCoinJoin ? CoinType_OnlyFullyMixed : CoinType_AllCoins; +} + +- (BOOL)isUsingCoinJoin { + return self.coinType == CoinType_OnlyFullyMixed; +} + +- (void)select:(DSUTXO)utxo { + NSValue *utxoValue = [NSValue valueWithBytes:&utxo objCType:@encode(DSUTXO)]; + if (![self.setSelected containsObject:utxoValue]) { + [self.setSelected addObject:utxoValue]; + } +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.h b/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.h new file mode 100644 index 000000000..2bff0c3fe --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.h @@ -0,0 +1,50 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "dash_shared_core.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DSCoinJoinBalance : NSObject + +@property (nonatomic, assign) uint64_t myTrusted; +@property (nonatomic, assign) uint64_t denominatedTrusted; +@property (nonatomic, assign) uint64_t anonymized; +@property (nonatomic, assign) uint64_t myImmature; +@property (nonatomic, assign) uint64_t myUntrustedPending; +@property (nonatomic, assign) uint64_t denominatedUntrustedPending; +@property (nonatomic, assign) uint64_t watchOnlyTrusted; +@property (nonatomic, assign) uint64_t watchOnlyUntrustedPending; +@property (nonatomic, assign) uint64_t watchOnlyImmature; + ++ (DSCoinJoinBalance *)balanceWithMyTrusted:(uint64_t)myTrusted + denominatedTrusted:(uint64_t)denominatedTrusted + anonymized:(uint64_t)anonymized + myImmature:(uint64_t)myImmature + myUntrustedPending:(uint64_t)myUntrustedPending + denominatedUntrustedPending:(uint64_t)denominatedUntrustedPending + watchOnlyTrusted:(uint64_t)watchOnlyTrusted + watchOnlyUntrustedPending:(uint64_t)watchOnlyUntrustedPending + watchOnlyImmature:(uint64_t)watchOnlyImmature; + ++ (void)ffi_free:(Balance *)balance; +- (Balance *)ffi_malloc; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.m b/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.m new file mode 100644 index 000000000..fbf2f12bf --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinBalance.m @@ -0,0 +1,66 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCoinJoinBalance.h" + +@implementation DSCoinJoinBalance + ++ (DSCoinJoinBalance *)balanceWithMyTrusted:(uint64_t)myTrusted + denominatedTrusted:(uint64_t)denominatedTrusted + anonymized:(uint64_t)anonymized + myImmature:(uint64_t)myImmature + myUntrustedPending:(uint64_t)myUntrustedPending + denominatedUntrustedPending:(uint64_t)denominatedUntrustedPending + watchOnlyTrusted:(uint64_t)watchOnlyTrusted + watchOnlyUntrustedPending:(uint64_t)watchOnlyUntrustedPending + watchOnlyImmature:(uint64_t)watchOnlyImmature { + + DSCoinJoinBalance *balance = [[DSCoinJoinBalance alloc] init]; + balance.myTrusted = myTrusted; + balance.denominatedTrusted = denominatedTrusted; + balance.anonymized = anonymized; + balance.myImmature = myImmature; + balance.myUntrustedPending = myUntrustedPending; + balance.denominatedUntrustedPending = denominatedUntrustedPending; + balance.watchOnlyTrusted = watchOnlyTrusted; + balance.watchOnlyUntrustedPending = watchOnlyUntrustedPending; + balance.watchOnlyImmature = watchOnlyImmature; + + return balance; +} + +- (Balance *)ffi_malloc { + Balance *balance = malloc(sizeof(Balance)); + balance->my_trusted = self.myTrusted; + balance->denominated_trusted = self.denominatedTrusted; + balance->anonymized = self.anonymized; + balance->my_immature = self.myImmature; + balance->my_untrusted_pending = self.myUntrustedPending; + balance->denominated_untrusted_pending = self.denominatedUntrustedPending; + balance->watch_only_trusted = self.watchOnlyTrusted; + balance->watch_only_untrusted_pending = self.watchOnlyUntrustedPending; + balance->watch_only_immature = self.watchOnlyImmature; + + return balance; +} + ++ (void)ffi_free:(Balance *)balance { + if (balance) { + free(balance); + } +} +@end diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h new file mode 100644 index 000000000..88b9275da --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.h @@ -0,0 +1,105 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSChain.h" +#import "DSTransactionOutput.h" +#import "DSCoinControl.h" +#import "DSCompactTallyItem.h" +#import "DSCoinJoinManager.h" +#import "DSCoinJoinWrapper.h" +#import "DSMasternodeGroup.h" +#import "DSCoinJoinBalance.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol DSCoinJoinManagerDelegate + +- (void)sessionStartedWithId:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined; +- (void)sessionCompleteWithId:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined; +- (void)mixingStarted; +- (void)mixingComplete:(BOOL)withError errorStatus:(PoolStatus)errorStatus isInterrupted:(BOOL)isInterrupted; +- (void)transactionProcessedWithId:(UInt256)txId type:(CoinJoinTransactionType)type; + +@end + +@interface DSCoinJoinManager : NSObject + +@property (nonatomic, assign, nullable) DSChain *chain; +@property (nonatomic, strong, nullable) DSMasternodeGroup *masternodeGroup; +@property (nonatomic, assign, nullable) CoinJoinClientOptions *options; +@property (nonatomic, nullable, weak) id managerDelegate; +@property (nonatomic, assign) BOOL anonymizableTallyCachedNonDenom; +@property (nonatomic, assign) BOOL anonymizableTallyCached; +@property (nonatomic, strong, nullable) DSCoinJoinWrapper *wrapper; +@property (nonatomic, readonly) BOOL isWaitingForNewBlock; +@property (atomic) BOOL isMixing; +@property (atomic) BOOL isShuttingDown; +@property (readonly) BOOL isChainSynced; + ++ (instancetype)sharedInstanceForChain:(DSChain *)chain; +- (instancetype)initWithChain:(DSChain *)chain; + +- (void)initMasternodeGroup; +- (BOOL)isMineInput:(UInt256)txHash index:(uint32_t)index; +- (NSArray *) availableCoins:(WalletEx *)walletEx onlySafe:(BOOL)onlySafe coinControl:(DSCoinControl *_Nullable)coinControl minimumAmount:(uint64_t)minimumAmount maximumAmount:(uint64_t)maximumAmount minimumSumAmount:(uint64_t)minimumSumAmount maximumCount:(uint64_t)maximumCount; +- (NSArray *)selectCoinsGroupedByAddresses:(WalletEx *)walletEx skipDenominated:(BOOL)skipDenominated anonymizable:(BOOL)anonymizable skipUnconfirmed:(BOOL)skipUnconfirmed maxOupointsPerAddress:(int32_t)maxOupointsPerAddress; +- (uint32_t)countInputsWithAmount:(uint64_t)inputAmount; +- (NSString *)freshAddress:(BOOL)internal; +- (NSArray *)getIssuedReceiveAddresses; +- (NSArray *)getUsedReceiveAddresses; +- (BOOL)commitTransactionForAmounts:(NSArray *)amounts outputs:(NSArray *)outputs coinControl:(DSCoinControl *)coinControl onPublished:(void (^)(UInt256 txId, NSError * _Nullable error))onPublished; +- (DSSimplifiedMasternodeEntry *)masternodeEntryByHash:(UInt256)hash; +- (uint64_t)validMNCount; +- (DSMasternodeList *)mnList; +- (BOOL)isMasternodeOrDisconnectRequested:(UInt128)ip port:(uint16_t)port; +- (BOOL)disconnectMasternode:(UInt128)ip port:(uint16_t)port; +- (BOOL)sendMessageOfType:(NSString *)messageType message:(NSData *)message withPeerIP:(UInt128)address port:(uint16_t)port warn:(BOOL)warn; +- (DSCoinJoinBalance *)getBalance; +- (void)configureMixingWithAmount:(uint64_t)amount rounds:(int32_t)rounds sessions:(int32_t)sessions withMultisession:(BOOL)multisession denominationGoal:(int32_t)denomGoal denominationHardCap:(int32_t)denomHardCap; +- (void)startAsync; +- (void)stopAsync; +- (void)start; +- (void)stop; +- (BOOL)addPendingMasternode:(UInt256)proTxHash clientSessionId:(UInt256)sessionId; +- (void)processMessageFrom:(DSPeer *)peer message:(NSData *)message type:(NSString *)type; +- (void)setStopOnNothingToDo:(BOOL)stop; +- (BOOL)startMixing; +- (void)doAutomaticDenominatingWithDryRun:(BOOL)dryRun completion:(void (^)(BOOL success))completion; +- (void)updateSuccessBlock; +- (void)refreshUnusedKeys; +- (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction; +- (double)getMixingProgress; +- (DSCoinControl *)selectCoinJoinUTXOs; +- (uint64_t)getSmallestDenomination; +- (void)hasCollateralInputsWithOnlyConfirmed:(BOOL)onlyConfirmed completion:(void (^)(BOOL balance))completion; +- (void)calculateAnonymizableBalanceWithSkipDenominated:(BOOL)skipDenominated skipUnconfirmed:(BOOL)skipUnconfirmed completion:(void (^)(uint64_t balance))completion; +- (void)minimumAnonymizableBalanceWithCompletion:(void (^)(uint64_t balance))completion; +- (void)updateOptionsWithAmount:(uint64_t)amount; +- (void)updateOptionsWithEnabled:(BOOL)isEnabled; +- (void)initiateShutdown; + +// Events +- (void)onSessionComplete:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined; +- (void)onSessionStarted:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined; +- (void)onMixingStarted:(nonnull NSArray *)statuses; +- (void)onMixingComplete:(nonnull NSArray *)statuses isInterrupted:(BOOL)isInterrupted; +- (void)onTransactionProcessed:(UInt256)txId type:(CoinJoinTransactionType)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.m b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.m new file mode 100644 index 000000000..69e5cb99f --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinManager.m @@ -0,0 +1,1045 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCoinJoinManager.h" +#import "DSTransaction.h" +#import "DSTransactionOutput.h" +#import "DSAccount.h" +#import "DSCoinControl.h" +#import "DSWallet.h" +#import "BigIntTypes.h" +#import "NSString+Bitcoin.h" +#import "DSChainManager.h" +#import "DSTransactionManager.h" +#import "DSMasternodeManager.h" +#import "DSCoinJoinAcceptMessage.h" +#import "DSCoinJoinEntryMessage.h" +#import "DSCoinJoinSignedInputs.h" +#import "DSPeerManager.h" +#import "DSSimplifiedMasternodeEntry.h" +#import "DSChain+Protected.h" +#import "DSBlock.h" +#import "DSKeyManager.h" +#import "DSDerivationPath+Protected.h" + +int32_t const DEFAULT_MIN_DEPTH = 0; +int32_t const DEFAULT_MAX_DEPTH = 9999999; +int32_t const MIN_BLOCKS_TO_WAIT = 1; + +@interface DSCoinJoinManager () + +@property (nonatomic, strong) dispatch_queue_t processingQueue; +@property (nonatomic, strong) dispatch_source_t coinjoinTimer; +@property (atomic) int32_t cachedLastSuccessBlock; +@property (atomic) int32_t cachedBlockHeight; // Keep track of current block height +@property (atomic) double lastReportedProgress; +@property (atomic) BOOL hasReportedSuccess; +@property (atomic) BOOL hasReportedFailure; + +@end + +@implementation DSCoinJoinManager + +static NSMutableDictionary *_managerChainDictionary = nil; +static dispatch_once_t managerChainToken = 0; + ++ (instancetype)sharedInstanceForChain:(DSChain *)chain { + NSParameterAssert(chain); + + dispatch_once(&managerChainToken, ^{ + _managerChainDictionary = [NSMutableDictionary dictionary]; + }); + DSCoinJoinManager *managerForChain = nil; + @synchronized(_managerChainDictionary) { + if (![_managerChainDictionary objectForKey:chain.uniqueID]) { + managerForChain = [[DSCoinJoinManager alloc] initWithChain:chain]; + _managerChainDictionary[chain.uniqueID] = managerForChain; + } else { + managerForChain = [_managerChainDictionary objectForKey:chain.uniqueID]; + } + } + return managerForChain; +} + +- (instancetype)initWithChain:(DSChain *)chain { + self = [super init]; + if (self) { + _chain = chain; + _wrapper = [[DSCoinJoinWrapper alloc] initWithManagers:self chainManager:chain.chainManager]; + _processingQueue = dispatch_queue_create([[NSString stringWithFormat:@"org.dashcore.dashsync.coinjoin.%@", self.chain.uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL); + _cachedBlockHeight = 0; + _cachedLastSuccessBlock = 0; + _lastReportedProgress = 0; + _options = [self createOptions]; + [self printUsedKeys]; + } + return self; +} + +- (void)initMasternodeGroup { + _masternodeGroup = [[DSMasternodeGroup alloc] initWithManager:self]; +} + +- (CoinJoinClientOptions *)createOptions { + CoinJoinClientOptions *options = malloc(sizeof(CoinJoinClientOptions)); + options->enable_coinjoin = YES; + options->coinjoin_rounds = 1; + options->coinjoin_sessions = 6; + options->coinjoin_amount = DUFFS / 8; + options->coinjoin_random_rounds = COINJOIN_RANDOM_ROUNDS; + options->coinjoin_denoms_goal = DEFAULT_COINJOIN_DENOMS_GOAL; + options->coinjoin_denoms_hardcap = DEFAULT_COINJOIN_DENOMS_HARDCAP; + options->coinjoin_multi_session = NO; + options->denom_only = NO; + options->chain_type = self.chain.chainType; + + return options; +} + +- (void)updateOptionsWithAmount:(uint64_t)amount { + if (self.options->coinjoin_amount != amount) { + self.options->coinjoin_amount = amount; + + if (self.wrapper.isRegistered) { + [self.wrapper updateOptions:self.options]; + } + } +} + +- (void)updateOptionsWithEnabled:(BOOL)isEnabled { + if (self.options->enable_coinjoin != isEnabled) { + self.options->enable_coinjoin = isEnabled; + + if (self.wrapper.isRegistered) { + [self.wrapper updateOptions:self.options]; + } + } +} + +- (void)updateOptionsWithSessions:(int32_t)sessions { + if (self.options->coinjoin_sessions != sessions) { + self.options->coinjoin_sessions = sessions; + + if (self.wrapper.isRegistered) { + [self.wrapper updateOptions:self.options]; + } + } +} + +- (void)configureMixingWithAmount:(uint64_t)amount rounds:(int32_t)rounds sessions:(int32_t)sessions withMultisession:(BOOL)multisession denominationGoal:(int32_t)denomGoal denominationHardCap:(int32_t)denomHardCap { + DSLog(@"[%@] CoinJoin: mixing configuration: { rounds: %d, sessions: %d, amount: %llu, multisession: %s, denomGoal: %d, denomHardCap: %d }", self.chain.name, rounds, sessions, amount, multisession ? "YES" : "NO", denomGoal, denomHardCap); + self.options->enable_coinjoin = true; + self.options->coinjoin_amount = amount; + self.options->coinjoin_rounds = rounds; + self.options->coinjoin_sessions = sessions; + self.options->coinjoin_multi_session = multisession; + self.options->coinjoin_denoms_goal = denomGoal; + self.options->coinjoin_denoms_hardcap = denomHardCap; + + if (self.wrapper.isRegistered) { + [self.wrapper updateOptions:self.options]; + } +} + +- (BOOL)isChainSynced { + return self.chain.chainManager.syncPhase == DSChainSyncPhase_Synced; +} + +- (void)startAsync { + if (!self.masternodeGroup.isRunning) { + [self.chain.chainManager.peerManager shouldSendDsq:true]; + [self.masternodeGroup startAsync]; + } +} + +- (void)start { + DSLog(@"[%@] CoinJoinManager starting", self.chain.name); + [self cancelCoinjoinTimer]; + uint32_t interval = 1; + uint32_t delay = 1; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleTransactionReceivedNotification) + name:DSTransactionManagerTransactionReceivedNotification + object:nil]; + @synchronized (self) { + self.cachedBlockHeight = self.chain.lastSyncBlock.height; + self.options->enable_coinjoin = YES; + + if ([self.wrapper isRegistered]) { + [self.wrapper updateOptions:self.options]; + } else { + [self.wrapper registerCoinJoin:self.options]; + } + + self.coinjoinTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.processingQueue); + if (self.coinjoinTimer) { + dispatch_source_set_timer(self.coinjoinTimer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), interval * NSEC_PER_SEC, 1ull * NSEC_PER_SEC); + dispatch_source_set_event_handler(self.coinjoinTimer, ^{ + [self doMaintenance]; + }); + dispatch_resume(self.coinjoinTimer); + } + } +} + +- (void)doMaintenance { + if ([self validMNCount] == 0) { + return; + } + + [self.wrapper doMaintenance]; +} + +- (BOOL)startMixing { + self.isMixing = true; + self.isShuttingDown = false; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSyncStateDidChangeNotification:) + name:DSChainManagerSyncStateDidChangeNotification + object:nil]; + return [self.wrapper startMixing]; +} + +- (void)initiateShutdown { + if (self.isMixing && !self.isShuttingDown) { + DSLog(@"[%@] CoinJoinManager initiated shutdown", self.chain.name); + self.isShuttingDown = true; + [self updateOptionsWithSessions:0]; + [self.wrapper initiateShutdown]; + + if (self.masternodeGroup != nil && self.masternodeGroup.isRunning) { + [self.chain.chainManager.peerManager shouldSendDsq:false]; + [self.masternodeGroup stopAsync]; + } + } +} + +- (void)stop { + if (self.isMixing) { + DSLog(@"[%@] CoinJoinManager stopping", self.chain.name); + self.isMixing = false; + [self cancelCoinjoinTimer]; + self.cachedLastSuccessBlock = 0; + [self updateOptionsWithEnabled:NO]; + [self.wrapper stopAndResetClientManager]; + [self stopAsync]; + self.isShuttingDown = false; + } +} + +- (void)stopAsync { + if (self.masternodeGroup != nil && self.masternodeGroup.isRunning) { + [self.chain.chainManager.peerManager shouldSendDsq:false]; + [self.masternodeGroup stopAsync]; + self.masternodeGroup = nil; + } + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + if (_options != NULL) { + free(_options); + } +} + +- (void)handleSyncStateDidChangeNotification:(NSNotification *)note { + if ([note.userInfo[DSChainManagerNotificationChainKey] isEqual:[self chain]] && self.chain.lastSyncBlock.height > self.cachedBlockHeight) { + self.cachedBlockHeight = self.chain.lastSyncBlock.height; + dispatch_async(self.processingQueue, ^{ + [self.wrapper notifyNewBestBlock:self.chain.lastSyncBlock]; + }); + } +} + +- (void)handleTransactionReceivedNotification { + DSWallet *wallet = self.chain.wallets.firstObject; + DSTransaction *lastTransaction = wallet.accounts.firstObject.recentTransactions.firstObject; + + if ([self.wrapper isMixingFeeTx:lastTransaction.txHash]) { +#if DEBUG + DSLogPrivate(@"[%@] CoinJoin tx: Mixing Fee: %@", self.chain.name, uint256_reverse_hex(lastTransaction.txHash)); +#else + DSLog(@"[%@] CoinJoin tx: Mixing Fee: %@", self.chain.name, @""); +#endif + [self onTransactionProcessed:lastTransaction.txHash type:CoinJoinTransactionType_MixingFee]; + } else if ([self coinJoinTxTypeForTransaction:lastTransaction] == CoinJoinTransactionType_Mixing) { +#if DEBUG + DSLogPrivate(@"[%@] CoinJoin tx: Mixing Transaction: %@", self.chain.name, uint256_reverse_hex(lastTransaction.txHash)); +#else + DSLog(@"[%@] CoinJoin tx: Mixing Transaction: %@", self.chain.name, @""); +#endif + [self.wrapper unlockOutputs:lastTransaction]; + [self onTransactionProcessed:lastTransaction.txHash type:CoinJoinTransactionType_Mixing]; + } +} + +- (void)doAutomaticDenominatingWithDryRun:(BOOL)dryRun completion:(void (^)(BOOL success))completion { + if (![self.wrapper isRegistered]) { + [self.wrapper registerCoinJoin:self.options]; + } + + if (!dryRun && [self validMNCount] == 0) { + completion(NO); + return; + } + + DSLog(@"[%@] CoinJoin: doAutomaticDenominatingWithDryRun: %@", self.chain.name, dryRun ? @"YES" : @"NO"); + + dispatch_async(self.processingQueue, ^{ + BOOL result = [self.wrapper doAutomaticDenominatingWithDryRun:dryRun]; + + if (!dryRun) { + if (result) { + if (!self.hasReportedSuccess) { + DSLog(@"[%@] CoinJoin: Mixing started successfully", self.chain.name); + self.hasReportedSuccess = YES; + self.hasReportedFailure = NO; + } + } else { + if (!self.hasReportedFailure) { + DSLog(@"[%@] CoinJoin: Mixing start failed, will retry", self.chain.name); + self.hasReportedFailure = YES; + self.hasReportedSuccess = NO; + } + } + } + + completion(result); + }); +} + +- (void)cancelCoinjoinTimer { + @synchronized (self) { + if (self.coinjoinTimer) { + dispatch_source_cancel(self.coinjoinTimer); + self.coinjoinTimer = nil; + } + } +} + +- (void)setStopOnNothingToDo:(BOOL)stop { + [self.wrapper setStopOnNothingToDo:stop]; +} + +- (void)refreshUnusedKeys { + [self.wrapper refreshUnusedKeys]; +} + +- (void)processMessageFrom:(DSPeer *)peer message:(NSData *)message type:(NSString *)type { + if (!self.isMixing) { + return; + } + + dispatch_async(self.processingQueue, ^{ + if ([type isEqualToString:MSG_COINJOIN_QUEUE]) { + if (!self.isShuttingDown) { + [self.wrapper processDSQueueFrom:peer message:message]; + } + } else { + [self.wrapper processMessageFrom:peer message:message type:type]; + } + }); +} + +- (BOOL)isMineInput:(UInt256)txHash index:(uint32_t)index { + DSTransaction *tx = [self.chain transactionForHash:txHash]; + DSAccount *account = [self.chain firstAccountThatCanContainTransaction:tx]; + + if (index < tx.outputs.count) { + DSTransactionOutput *output = tx.outputs[index]; + + if ([account containsAddress:output.address]) { + return YES; + } + } + + return NO; +} + +- (NSArray *)selectCoinsGroupedByAddresses:(WalletEx *)walletEx skipDenominated:(BOOL)skipDenominated anonymizable:(BOOL)anonymizable skipUnconfirmed:(BOOL)skipUnconfirmed maxOupointsPerAddress:(int32_t)maxOupointsPerAddress { + @synchronized(self) { + // Note: cache is checked in dash-shared-core. + + uint64_t smallestDenom = coinjoin_get_smallest_denomination(); + NSMutableDictionary *mapTally = [[NSMutableDictionary alloc] init]; + NSMutableSet *setWalletTxesCounted = [[NSMutableSet alloc] init]; + + DSUTXO outpoint; + NSArray *utxos = self.chain.wallets.firstObject.unspentOutputs; + for (NSValue *value in utxos) { + [value getValue:&outpoint]; + + if ([setWalletTxesCounted containsObject:uint256_data(outpoint.hash)]) { + continue; + } + + [setWalletTxesCounted addObject:uint256_data(outpoint.hash)]; + DSTransaction *wtx = [self.chain transactionForHash:outpoint.hash]; + + if (wtx == nil) { + continue; + } + + if (wtx.isCoinbaseClassicTransaction && [wtx getBlocksToMaturity] > 0) { + continue; + } + + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + if (!account.coinJoinDerivationPath.addressesLoaded) { + DSLog(@"[%@] CoinJoin selectCoinsGroupedByAddresses: CJDerivationPath addresses NOT loaded", self.chain.name); + } + + BOOL isTrusted = wtx.instantSendReceived || [account transactionIsVerified:wtx]; + + if (skipUnconfirmed && !isTrusted) { + continue; + } + + for (int32_t i = 0; i < wtx.outputs.count; i++) { + DSTransactionOutput *output = wtx.outputs[i]; + NSData *txDest = output.outScript; + NSString *address = [NSString bitcoinAddressWithScriptPubKey:txDest forChain:self.chain]; + + if (address == nil) { + continue; + } + + if (![account containsAddress:output.address]) { + continue; + } + + DSCompactTallyItem *tallyItem = mapTally[txDest]; + + if (maxOupointsPerAddress != -1 && tallyItem != nil && tallyItem.inputCoins.count >= maxOupointsPerAddress) { + continue; + } + + if ([account isSpent:dsutxo_obj(((DSUTXO){outpoint.hash, i}))]) { + continue; + } + + if (is_locked_coin(walletEx, (uint8_t (*)[32])(outpoint.hash.u8), (uint32_t)i)) { + continue; + } + + if (skipDenominated && is_denominated_amount(output.amount)) { + continue; + } + + if (anonymizable) { + // ignore collaterals + if (is_collateral_amount(output.amount)) { + continue; + } + + // ignore outputs that are 10 times smaller then the smallest denomination + // otherwise they will just lead to higher fee / lower priority + if (output.amount <= smallestDenom/10) { + continue; + } + + // ignore mixed + if (is_fully_mixed(walletEx, (uint8_t (*)[32])(outpoint.hash.u8), (uint32_t)i)) { + continue; + } + } + + if (tallyItem == nil) { + tallyItem = [[DSCompactTallyItem alloc] init]; + tallyItem.txDestination = txDest; + mapTally[txDest] = tallyItem; + } + + tallyItem.amount += output.amount; + DSInputCoin *coin = [[DSInputCoin alloc] initWithTx:wtx index:i]; + [tallyItem.inputCoins addObject:coin]; + } + } + + // construct resulting vector + // NOTE: vecTallyRet is "sorted" by txdest (i.e. address), just like mapTally + NSMutableArray *vecTallyRet = [NSMutableArray array]; + + for (DSCompactTallyItem *item in mapTally.allValues) { + // TODO: (dashj) ignore this to get this dust back in + if (anonymizable && item.amount < smallestDenom) { + continue; + } + + [vecTallyRet addObject:item]; + } + + // Note: cache is assigned in dash-shared-core + + return vecTallyRet; + } +} + +- (uint32_t)countInputsWithAmount:(uint64_t)inputAmount { + uint32_t total = 0; + + @synchronized(self) { + NSArray *unspent = self.chain.wallets.firstObject.unspentOutputs; + + DSUTXO outpoint; + for (uint32_t i = 0; i < unspent.count; i++) { + [unspent[i] getValue:&outpoint]; + DSTransaction *tx = [self.chain transactionForHash:outpoint.hash]; + + if (tx == NULL) { + continue; + } + + if (tx.outputs[outpoint.n].amount != inputAmount) { + continue; + } + + if (tx.confirmations < 0) { + continue; + } + + total++; + } + } + + return total; +} + +- (NSArray *)availableCoins:(WalletEx *)walletEx onlySafe:(BOOL)onlySafe coinControl:(DSCoinControl *_Nullable)coinControl minimumAmount:(uint64_t)minimumAmount maximumAmount:(uint64_t)maximumAmount minimumSumAmount:(uint64_t)minimumSumAmount maximumCount:(uint64_t)maximumCount { + NSMutableArray *vCoins = [NSMutableArray array]; + + @synchronized(self) { + CoinType coinType = coinControl != nil ? coinControl.coinType : CoinType_AllCoins; + + uint64_t total = 0; + // Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we always allow), or we default to avoiding, and only in the case where a coin control object is provided, and has the avoid address reuse flag set to false, do we allow already used addresses + BOOL allowUsedAddresses = /* !IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) || */ (coinControl != nil && !coinControl.avoidAddressReuse); + int32_t minDepth = coinControl != nil ? coinControl.minDepth : DEFAULT_MIN_DEPTH; + int32_t maxDepth = coinControl != nil ? coinControl.maxDepth : DEFAULT_MAX_DEPTH; + NSSet *spendables = [self getSpendableTXs]; + + for (DSTransaction *coin in spendables) { + UInt256 wtxid = coin.txHash; + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + if (!account.coinJoinDerivationPath.addressesLoaded) { + DSLog(@"[%@] CoinJoin availableCoins: CJDerivationPath addresses NOT loaded", self.chain.name); + } + + if ([account transactionIsPending:coin]) { + continue; + } + + if (coin.isImmatureCoinBase) { + continue; + } + + BOOL safeTx = coin.instantSendReceived || [account transactionIsVerified:coin]; + + if (onlySafe && !safeTx) { + continue; + } + + uint32_t depth = coin.confirmations; + + if (depth < minDepth || depth > maxDepth) { + continue; + } + + for (uint32_t i = 0; i < coin.outputs.count; i++) { + DSTransactionOutput *output = coin.outputs[i]; + uint64_t value = output.amount; + BOOL found = NO; + + if (coinType == CoinType_OnlyFullyMixed) { + if (!is_denominated_amount(value)) { + continue; + } + + found = is_fully_mixed(walletEx, (uint8_t (*)[32])(wtxid.u8), (uint32_t)i); + } else if (coinType == CoinType_OnlyReadyToMix) { + if (!is_denominated_amount(value)) { + continue; + } + + found = !is_fully_mixed(walletEx, (uint8_t (*)[32])(wtxid.u8), (uint32_t)i); + } else if (coinType == CoinType_OnlyNonDenominated) { + if (is_collateral_amount(value)) { + continue; // do not use collateral amounts + } + + found = !is_denominated_amount(value); + } else if (coinType == CoinType_OnlyMasternodeCollateral) { + found = value == 1000 * DUFFS; + } else if (coinType == CoinType_OnlyCoinJoinCollateral) { + found = is_collateral_amount(value); + } else { + found = YES; + } + + if (!found) { + continue; + } + + if (value < minimumAmount || value > maximumAmount) { + continue; + } + + DSUTXO utxo = ((DSUTXO){wtxid, i}); + + if (coinControl != nil && coinControl.hasSelected && !coinControl.allowOtherInputs && ![coinControl isSelected:utxo]) { + continue; + } + + if (is_locked_coin(walletEx, (uint8_t (*)[32])(wtxid.u8), (uint32_t)i) && coinType != CoinType_OnlyMasternodeCollateral) { + continue; + } + + if ([account isSpent:dsutxo_obj(utxo)]) { + continue; + } + + if (output.address == nil || ![account containsAddress:output.address]) { + continue; + } + + if (!allowUsedAddresses && [account transactionAddressAlreadySeenInOutputs:output.address]) { + continue; + } + + [vCoins addObject:[[DSInputCoin alloc] initWithTx:coin index:i]]; + + // Checks the sum amount of all UTXO's. + if (minimumSumAmount != MAX_MONEY) { + total += value; + + if (total >= minimumSumAmount) { + return vCoins; + } + } + + // Checks the maximum number of UTXO's. + if (maximumCount > 0 && vCoins.count >= maximumCount) { + return vCoins; + } + } + } + } + + return vCoins; +} + +- (double)getMixingProgress { + if (![self.wrapper isRegistered]) { + [self.wrapper registerCoinJoin:self.options]; + } + + double requiredRounds = self.options->coinjoin_rounds + 0.875; // 1 x 50% + 1 x 50%^2 + 1 x 50%^3 + __block int totalInputs = 0; + __block int totalRounds = 0; + + NSDictionary *> *outputs = [self getOutputs]; + uint64_t collateralAmount = [self.wrapper getCollateralAmount]; + NSArray *denominations = [self.wrapper getStandardDenominations]; + + [outputs enumerateKeysAndObjectsUsingBlock:^(NSNumber *denom, NSArray *outputs, BOOL *stop) { + [outputs enumerateObjectsUsingBlock:^(NSValue *output, NSUInteger idx, BOOL *stop) { + DSUTXO outpoint; + [output getValue:&outpoint]; + + if (denom.intValue >= 0) { + int rounds = [self.wrapper getRealOutpointCoinJoinRounds:outpoint]; + + if (rounds >= 0) { + totalInputs += 1; + totalRounds += rounds; + } + } else if (denom.intValue == -2) { + DSTransaction *tx = [self.chain transactionForHash:outpoint.hash]; + DSTransactionOutput *output = tx.outputs[outpoint.n]; + + __block int unmixedInputs = 0; + __block int64_t outputValue = output.amount - collateralAmount; + + [denominations enumerateObjectsUsingBlock:^(NSNumber *coin, NSUInteger idx, BOOL *stop) { + while (outputValue - coin.longLongValue > 0) { + unmixedInputs++; + outputValue -= coin.longLongValue; + } + }]; + + totalInputs += unmixedInputs; + } + }]; + }]; + + double progress = totalInputs != 0 ? (double)totalRounds / (requiredRounds * totalInputs) : 0.0; + + if (self.lastReportedProgress != progress) { + _lastReportedProgress = progress; + DSLog(@"[%@] CoinJoin: getMixingProgress: %f = %d / (%f * %d)", self.chain.name, progress, totalRounds, requiredRounds, totalInputs); + } + + return fmax(0.0, fmin(progress, 1.0)); +} + +- (NSDictionary *> *)getOutputs { + NSMutableDictionary *> *outputs = [NSMutableDictionary dictionary]; + + for (NSNumber *amount in [self.wrapper getStandardDenominations]) { + outputs[@([self.wrapper amountToDenomination:amount.unsignedLongLongValue])] = [NSMutableArray array]; + } + + outputs[@(-2)] = [NSMutableArray array]; + outputs[@(0)] = [NSMutableArray array]; + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + NSArray *utxos = account.unspentOutputs; + DSUTXO outpoint; + + for (NSValue *value in utxos) { + [value getValue:&outpoint]; + + DSTransaction *tx = [self.chain transactionForHash:outpoint.hash]; + DSTransactionOutput *output = tx.outputs[outpoint.n]; + NSString *address = [DSKeyManager addressWithScriptPubKey:output.outScript forChain:self.chain]; + + if ([account containsCoinJoinAddress:address]) { + int denom = [self.wrapper amountToDenomination:output.amount]; + NSMutableArray *listDenoms = outputs[@(denom)]; + [listDenoms addObject:value]; + } else { + // non-denominated and non-collateral coins + [outputs[@(-2)] addObject:value]; + } + } + + return outputs; +} + +- (BOOL)isCoinJoinOutput:(DSTransactionOutput *)output utxo:(DSUTXO)utxo { + if (![self.wrapper isDenominatedAmount:output.amount]) { + return false; + } + + if (![self.wrapper isFullyMixed:utxo]) { + return false; + } + + return [self.chain.wallets.firstObject.accounts.firstObject.coinJoinDerivationPath containsAddress:output.address]; +} + +- (DSCoinJoinBalance *)getBalance { + if (![self.wrapper isRegistered]) { + [self.wrapper registerCoinJoin:self.options]; + } + + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + uint64_t anonymizedBalance = 0; + uint64_t denominatedBalance = 0; + DSUTXO outpoint; + NSArray *utxos = account.unspentOutputs; + + for (NSValue *value in utxos) { + [value getValue:&outpoint]; + DSTransaction *tx = [account transactionForHash:outpoint.hash]; + DSTransactionOutput *output = tx.outputs[outpoint.n]; + + if ([self isCoinJoinOutput:output utxo:outpoint]) { + anonymizedBalance += output.amount; + } + + if ([self.wrapper isDenominatedAmount:output.amount]) { + denominatedBalance += output.amount; + } + } + + // TODO(DashJ): support more balance types? + DSCoinJoinBalance *balance = + [DSCoinJoinBalance balanceWithMyTrusted:self.chain.balance + denominatedTrusted:denominatedBalance + anonymized:anonymizedBalance + myImmature:0 + myUntrustedPending:0 + denominatedUntrustedPending:0 + watchOnlyTrusted:0 + watchOnlyUntrustedPending:0 + watchOnlyImmature:0]; + + return balance; +} + +- (NSSet *)getSpendableTXs { + NSMutableSet *ret = [[NSMutableSet alloc] init]; + NSArray *unspent = self.chain.wallets.firstObject.unspentOutputs; + + DSUTXO outpoint; + for (uint32_t i = 0; i < unspent.count; i++) { + [unspent[i] getValue:&outpoint]; + DSTransaction *tx = [self.chain transactionForHash:outpoint.hash]; + + if (tx) { + [ret addObject:tx]; + + // Skip entries until we encounter a new TX + DSUTXO nextOutpoint; + while (i + 1 < unspent.count) { + [unspent[i + 1] getValue:&nextOutpoint]; + + if (!uint256_eq(nextOutpoint.hash, outpoint.hash)) { + break; + } + i++; + } + } + } + + return ret; +} + +- (NSString *)freshAddress:(BOOL)internal { + NSString *address; + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + + if (internal) { + address = account.coinJoinChangeAddress; + } else { + address = account.coinJoinReceiveAddress; + } + + return address; +} + +- (NSArray *)getIssuedReceiveAddresses { + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + return account.allCoinJoinReceiveAddresses; +} + +- (NSArray *)getUsedReceiveAddresses { + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + return account.usedCoinJoinReceiveAddresses; +} + +- (BOOL)commitTransactionForAmounts:(NSArray *)amounts outputs:(NSArray *)outputs coinControl:(DSCoinControl *)coinControl onPublished:(void (^)(UInt256 txId, NSError * _Nullable error))onPublished { + DSAccount *account = self.chain.wallets.firstObject.accounts.firstObject; + DSTransaction *transaction = [account transactionForAmounts:amounts toOutputScripts:outputs withFee:YES coinControl:coinControl]; + + if (!transaction) { + return NO; + } + + BOOL signedTransaction = [account signTransaction:transaction]; + + if (!signedTransaction || !transaction.isSigned) { + DSLog(@"[%@] CoinJoin error: not signed", self.chain.name); + return NO; + } else { + [self.chain.chainManager.transactionManager publishTransaction:transaction completion:^(NSError *error) { + NSString *txDescription = @""; + #if DEBUG + txDescription = transaction.description; + #endif + + if (error) { + DSLog(@"[%@] CoinJoin publish error: %@ for tx: %@", self.chain.name, error.description, txDescription); + } else { + DSLog(@"[%@] CoinJoin publish success: %@", self.chain.name, txDescription); + } + + dispatch_async(self.processingQueue, ^{ + onPublished(transaction.txHash, error); + }); + }]; + } + + return YES; +} + +- (DSSimplifiedMasternodeEntry *)masternodeEntryByHash:(UInt256)hash { + return [self.chain.chainManager.masternodeManager.currentMasternodeList masternodeForRegistrationHash:uint256_reverse(hash)]; +} + +- (uint64_t)validMNCount { + return self.chain.chainManager.masternodeManager.currentMasternodeList.validMasternodeCount; +} + +- (DSMasternodeList *)mnList { + return self.chain.chainManager.masternodeManager.currentMasternodeList; +} + +- (BOOL)isMasternodeOrDisconnectRequested:(UInt128)ip port:(uint16_t)port { + return [self.masternodeGroup isMasternodeOrDisconnectRequested:ip port:port]; +} + +- (BOOL)disconnectMasternode:(UInt128)ip port:(uint16_t)port { + return [self.masternodeGroup disconnectMasternode:ip port:port]; +} + +- (BOOL)sendMessageOfType:(NSString *)messageType message:(NSData *)message withPeerIP:(UInt128)address port:(uint16_t)port warn:(BOOL)warn { + return [self.masternodeGroup forPeer:address port:port warn:warn withPredicate:^BOOL(DSPeer * _Nonnull peer) { + if ([messageType isEqualToString:DSCoinJoinAcceptMessage.type]) { + DSCoinJoinAcceptMessage *request = [DSCoinJoinAcceptMessage requestWithData:message]; + [peer sendRequest:request]; + } else if ([messageType isEqualToString:DSCoinJoinEntryMessage.type]) { + DSCoinJoinEntryMessage *request = [DSCoinJoinEntryMessage requestWithData:message]; + [peer sendRequest:request]; + } else if ([messageType isEqualToString:DSCoinJoinSignedInputs.type]) { + DSCoinJoinSignedInputs *request = [DSCoinJoinSignedInputs requestWithData:message]; + [peer sendRequest:request]; + } else { + DSLog(@"[%@] CoinJoin: unknown message type: %@", self.chain.name, messageType); + return NO; + } + + return YES; + }]; +} + +- (BOOL)addPendingMasternode:(UInt256)proTxHash clientSessionId:(UInt256)sessionId { + return [self.masternodeGroup addPendingMasternode:proTxHash clientSessionId:sessionId]; +} + +- (void)updateSuccessBlock { + self.cachedLastSuccessBlock = self.cachedBlockHeight; +} + +- (BOOL)isWaitingForNewBlock { + if (!self.isChainSynced) { + return true; + } + + if (self.options->coinjoin_multi_session == true) { + return false; + } + + return self.cachedBlockHeight - self.cachedLastSuccessBlock < MIN_BLOCKS_TO_WAIT; +} + +- (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction { + return [DSCoinJoinWrapper coinJoinTxTypeForTransaction:transaction]; +} + +- (void)calculateAnonymizableBalanceWithSkipDenominated:(BOOL)skipDenominated skipUnconfirmed:(BOOL)skipUnconfirmed completion:(void (^)(uint64_t balance))completion { + dispatch_async(self.processingQueue, ^{ + uint64_t balance = [self.wrapper getAnonymizableBalance:skipDenominated skipUnconfirmed:skipUnconfirmed]; + completion(balance); + }); +} + +- (void)minimumAnonymizableBalanceWithCompletion:(void (^)(uint64_t balance))completion { + dispatch_async(self.processingQueue, ^{ + uint64_t valueMin = [self.wrapper getSmallestDenomination]; + BOOL hasCollateralInputs = [self.wrapper hasCollateralInputs:YES]; + + if (hasCollateralInputs) { + valueMin += [self.wrapper getMaxCollateralAmount]; + } + + completion(valueMin); + }); +} + +- (uint64_t)getSmallestDenomination { + return [self.wrapper getSmallestDenomination]; +} + +- (DSCoinControl *)selectCoinJoinUTXOs { + DSCoinControl *coinControl = [[DSCoinControl alloc] init]; + [coinControl useCoinJoin:YES]; + NSArray *utxos = self.chain.wallets.firstObject.unspentOutputs; + + for (NSValue *value in utxos) { + DSUTXO utxo; + [value getValue:&utxo]; + + DSTransaction *tx = [self.chain transactionForHash:utxo.hash]; + if (!tx) continue; + + DSTransactionOutput *output = tx.outputs[utxo.n]; + if (!output) continue; + + if ([self isCoinJoinOutput:output utxo:utxo] && ![self.wrapper isLockedCoin:utxo]) { + [coinControl select:utxo]; + } + } + + return coinControl; +} + +- (void)printUsedKeys { + dispatch_async(self.processingQueue, ^{ + NSArray *issuedAddresses = [self getIssuedReceiveAddresses]; + NSArray *usedAddresses = [self getUsedReceiveAddresses]; + double percent = (double)usedAddresses.count * 100.0 / (double)issuedAddresses.count; + DSLog(@"[%@] CoinJoin init. Used addresses count %lu out of %lu (%.2f %%)", self.chain.name, (unsigned long)usedAddresses.count, (unsigned long)issuedAddresses.count, percent); + }); +} + +// Events + +- (void)onSessionStarted:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined { + DSLog(@"[%@] CoinJoin: onSessionStarted: baseId: %d, clientId: %@, denom: %d, state: %d, message: %d, address: %@, isJoined: %s", self.chain.name, baseId, [uint256_hex(clientId) substringToIndex:7], denom, state, message, [self.masternodeGroup hostFor:address], joined ? "yes" : "no"); + [self.managerDelegate sessionStartedWithId:baseId clientSessionId:clientId denomination:denom poolState:state poolMessage:message poolStatus:status ipAddress:address isJoined:joined]; +} + +- (void)onSessionComplete:(int32_t)baseId clientSessionId:(UInt256)clientId denomination:(uint32_t)denom poolState:(PoolState)state poolMessage:(PoolMessage)message poolStatus:(PoolStatus)status ipAddress:(UInt128)address isJoined:(BOOL)joined { + DSLog(@"[%@] CoinJoin: onSessionComplete: baseId: %d, clientId: %@, denom: %d, state: %d, status: %d, message: %d, address: %@, isJoined: %s", self.chain.name, baseId, [uint256_hex(clientId) substringToIndex:7], denom, state, status, message, [self.masternodeGroup hostFor:address], joined ? "yes" : "no"); + [self.managerDelegate sessionCompleteWithId:baseId clientSessionId:clientId denomination:denom poolState:state poolMessage:message poolStatus:status ipAddress:address isJoined:joined]; +} + +- (void)onMixingStarted:(nonnull NSArray *)statuses { + DSLog(@"[%@] CoinJoin: onMixingStarted, statuses: %@", self.chain.name, statuses.count > 0 ? [NSString stringWithFormat:@"%@", statuses] : @"empty"); + [self.managerDelegate mixingStarted]; +} + +- (void)onMixingComplete:(nonnull NSArray *)statuses isInterrupted:(BOOL)isInterrupted { + if (self.isShuttingDown) { + [self stop]; + } + + PoolStatus returnStatus = PoolStatus_ErrNotEnoughFunds; + BOOL isError = YES; + + for (NSNumber *statusNumber in statuses) { + PoolStatus status = [statusNumber intValue]; + if (![self isError:status]) { + returnStatus = status; + isError = NO; + break; + } + + if (status != PoolStatus_ErrNotEnoughFunds) { + returnStatus = status; + } + } + + [self.managerDelegate mixingComplete:isError errorStatus:returnStatus isInterrupted:isInterrupted]; +} + +- (void)onTransactionProcessed:(UInt256)txId type:(CoinJoinTransactionType)type { +#if DEBUG + DSLog(@"[%@] CoinJoin: onTransactionProcessed: %@, type: %d", self.chain.name, uint256_reverse_hex(txId), type); +#else + DSLog(@"[%@] CoinJoin: onTransactionProcessed: %@, type: %d", self.chain.name, @"", type); +#endif + [self.managerDelegate transactionProcessedWithId:txId type:type]; +} + +- (BOOL)isError:(PoolStatus)status { + return (status & 0x2000) != 0; +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.h b/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.h new file mode 100644 index 000000000..af83e12d3 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.h @@ -0,0 +1,66 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSMasternodeGroup.h" +#import "DSChainManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@class DSCoinJoinManager; + +@interface DSCoinJoinWrapper : NSObject + +@property (nonatomic, strong, nullable) DSChainManager *chainManager; +@property (nonatomic, strong) DSChain *chain; +@property (nonatomic, weak, nullable) DSCoinJoinManager *manager; +@property (nonatomic, assign, nullable) CoinJoinClientManager *clientManager; + +- (instancetype)initWithManagers:(DSCoinJoinManager *)manager chainManager:(DSChainManager *)chainManager; +- (void)processDSQueueFrom:(DSPeer *)peer message:(NSData *)message; +- (void)processMessageFrom:(DSPeer *)peer message:(NSData *)message type:(NSString *)type; +- (void)notifyNewBestBlock:(DSBlock *)block; +- (void)setStopOnNothingToDo:(BOOL)stop; +- (BOOL)startMixing; +- (BOOL)doAutomaticDenominatingWithDryRun:(BOOL)dryRun; +- (void)doMaintenance; +- (void)registerCoinJoin:(CoinJoinClientOptions *)options; +- (BOOL)isRegistered; +- (BOOL)isMixingFeeTx:(UInt256)txId; +- (void)refreshUnusedKeys; +- (BOOL)isDenominatedAmount:(uint64_t)amount; +- (BOOL)isFullyMixed:(DSUTXO)utxo; +- (void)initiateShutdown; ++ (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction; ++ (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction account:(DSAccount *)account; +- (void)unlockOutputs:(DSTransaction *)transaction; +- (uint64_t)getAnonymizableBalance:(BOOL)skipDenominated skipUnconfirmed:(BOOL)skipUnconfirmed; +- (uint64_t)getSmallestDenomination; +- (void)updateOptions:(CoinJoinClientOptions *)options; +- (NSArray *)getStandardDenominations; +- (uint64_t)getCollateralAmount; +- (uint64_t)getMaxCollateralAmount; +- (BOOL)hasCollateralInputs:(BOOL)onlyConfirmed; +- (uint32_t)amountToDenomination:(uint64_t)amount; +- (int32_t)getRealOutpointCoinJoinRounds:(DSUTXO)utxo; +- (BOOL)isLockedCoin:(DSUTXO)utxo; +- (void)stopAndResetClientManager; +- (NSArray *)getSessionStatuses; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.m b/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.m new file mode 100644 index 000000000..e3f29e99e --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCoinJoinWrapper.m @@ -0,0 +1,699 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCoinJoinManager.h" +#import "DSWallet.h" +#import "DSTransaction+CoinJoin.h" +#import "DSTransactionOutput+CoinJoin.h" +#import "DSSimplifiedMasternodeEntry+Mndiff.h" +#import "DSMasternodeList+Mndiff.h" +#import "DSAccount.h" +#import "DSChainManager.h" +#import "DSCoinJoinWrapper.h" +#import "DSBlock.h" + +#define AS_OBJC(context) ((__bridge DSCoinJoinWrapper *)(context)) +#define AS_RUST(context) ((__bridge void *)(context)) + +@implementation DSCoinJoinWrapper + +- (instancetype)initWithManagers:(DSCoinJoinManager *)manager chainManager:(DSChainManager *)chainManager { + self = [super init]; + if (self) { + _chainManager = chainManager; + _manager = manager; + } + return self; +} + +- (void)registerCoinJoin:(CoinJoinClientOptions *)options { + @synchronized (self) { + if (_clientManager == NULL) { + _clientManager = register_client_manager(AS_RUST(self), options, getMNList, destroyMNList, getInputValueByPrevoutHash, hasChainLock, destroyInputValue, updateSuccessBlock, isWaitingForNewBlock, getTransaction, signTransaction, destroyTransaction, isMineInput, commitTransaction, isBlockchainSynced, freshCoinJoinAddress, countInputsWithAmount, availableCoins, destroyGatheredOutputs, selectCoinsGroupedByAddresses, destroySelectedCoins, isMasternodeOrDisconnectRequested, disconnectMasternode, sendMessage, addPendingMasternode, startManagerAsync, sessionLifecycleListener, mixingLifecycleListener, getCoinJoinKeys, destroyCoinJoinKeys); + + add_client_queue_manager(_clientManager, masternodeByHash, destroyMasternodeEntry, validMNCount, AS_RUST(self)); + } + } +} + +- (BOOL)isRegistered { + return self.clientManager != NULL; +} + +- (void)updateOptions:(CoinJoinClientOptions *)options { + @synchronized (self) { + change_coinjoin_options(self.clientManager, options); + } +} + +- (void)setStopOnNothingToDo:(BOOL)stop { + @synchronized (self) { + set_stop_on_nothing_to_do(self.clientManager, stop); + } +} + +- (BOOL)startMixing { + @synchronized (self) { + return start_mixing(self.clientManager); + } +} + +- (void)refreshUnusedKeys { + @synchronized (self) { + refresh_unused_keys(self.clientManager); + } +} + +- (BOOL)doAutomaticDenominatingWithDryRun:(BOOL)dryRun { + @synchronized (self) { + Balance *balance = [[self.manager getBalance] ffi_malloc]; + BOOL result = do_automatic_denominating(_clientManager, *balance, dryRun); + [DSCoinJoinBalance ffi_free:balance]; + + return result; + } +} + +- (void)doMaintenance { + @synchronized (self) { + Balance *balance = [[self.manager getBalance] ffi_malloc]; + do_maintenance(self.clientManager, *balance); + [DSCoinJoinBalance ffi_free:balance]; + } +} + +- (void)initiateShutdown { + @synchronized (self) { + initiate_shutdown(self.clientManager); + } +} + +- (BOOL)isDenominatedAmount:(uint64_t)amount { + return is_denominated_amount(amount); +} + +- (BOOL)isFullyMixed:(DSUTXO)utxo { + @synchronized (self) { + return is_fully_mixed_with_manager(_clientManager, (uint8_t (*)[32])(utxo.hash.u8), (uint32_t)utxo.n); + } +} + +- (void)processDSQueueFrom:(DSPeer *)peer message:(NSData *)message { + @synchronized (self) { + ByteArray *array = malloc(sizeof(ByteArray)); + array->len = (uintptr_t)message.length; + array->ptr = data_malloc(message); + + process_ds_queue(_clientManager, peer.address.u8, peer.port, array); + + if (array) { + if (array->ptr) { + free((void *)array->ptr); + } + + free(array); + } + } +} + +- (void)processMessageFrom:(DSPeer *)peer message:(NSData *)message type:(NSString *)type { + @synchronized (self) { + ByteArray *array = malloc(sizeof(ByteArray)); + array->len = (uintptr_t)message.length; + array->ptr = data_malloc(message); + + process_coinjoin_message(_clientManager, peer.address.u8, peer.port, array, [type UTF8String]); + + if (array->ptr) { + free((void *)array->ptr); + } + + free(array); + } +} + +- (void)notifyNewBestBlock:(DSBlock *)block { + if (block) { + @synchronized (self) { + notify_new_best_block(self.clientManager, (uint8_t (*)[32])(block.blockHash.u8), block.height); + } + } +} + +- (BOOL)isMixingFeeTx:(UInt256)txId { + @synchronized (self) { + return is_mixing_fee_tx(self.clientManager, (uint8_t (*)[32])(txId.u8)); + } +} + ++ (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction { + DSAccount *account = [transaction.chain firstAccountThatCanContainTransaction:transaction]; + return [DSCoinJoinWrapper coinJoinTxTypeForTransaction:transaction account:account]; +} + ++ (CoinJoinTransactionType)coinJoinTxTypeForTransaction:(DSTransaction *)transaction account:(DSAccount *)account { + NSArray *amountsSent = [account amountsSentByTransaction:transaction]; + + Transaction *tx = [transaction ffi_malloc:transaction.chain.chainType]; + uint64_t *inputValues = malloc(amountsSent.count * sizeof(uint64_t)); + + for (uintptr_t i = 0; i < amountsSent.count; i++) { + inputValues[i] = [amountsSent[i] unsignedLongLongValue]; + } + + CoinJoinTransactionType type = get_coinjoin_tx_type(tx, inputValues, amountsSent.count); + [DSTransaction ffi_free:tx]; + free(inputValues); + + return type; +} + +- (void)unlockOutputs:(DSTransaction *)transaction { + @synchronized (self) { + Transaction *tx = [transaction ffi_malloc:transaction.chain.chainType]; + unlock_outputs(self.clientManager, tx); + [DSTransaction ffi_free:tx]; + } +} + +- (uint64_t)getAnonymizableBalance:(BOOL)skipDenominated skipUnconfirmed:(BOOL)skipUnconfirmed { + @synchronized (self) { + return get_anonymizable_balance(self.clientManager, skipDenominated, skipUnconfirmed); + } +} + +- (uint64_t)getSmallestDenomination { + return coinjoin_get_smallest_denomination(); +} + +- (NSArray *)getStandardDenominations { + @synchronized (self) { + CoinJoinDenominations *denominations = get_standard_denominations(); + NSMutableArray *result = [NSMutableArray arrayWithCapacity:denominations->length]; + + for (size_t i = 0; i < denominations->length; i++) { + [result addObject:@(denominations->denoms[i])]; + } + + destroy_coinjoin_denomination(denominations); + return result; + } +} + +- (uint64_t)getCollateralAmount { + return get_collateral_amount(); +} + +- (uint64_t)getMaxCollateralAmount { + return get_max_collateral_amount(); +} + +- (BOOL)hasCollateralInputs:(BOOL)onlyConfirmed { + @synchronized (self) { + return has_collateral_inputs(_clientManager, onlyConfirmed); + } +} + +- (uint32_t)amountToDenomination:(uint64_t)amount { + return amount_to_denomination(amount); +} + +- (int32_t)getRealOutpointCoinJoinRounds:(DSUTXO)utxo { + @synchronized (self) { + UInt256 hash = utxo.hash; + return get_real_outpoint_coinjoin_rounds(_clientManager, (uint8_t (*)[32])&hash, (uint32_t)utxo.n, 0); + } +} + +- (NSArray *)getSessionStatuses { + @synchronized (self) { + CoinJoinSessionStatuses* statuses = get_sessions_status(_clientManager); + + if (statuses) { + NSMutableArray *statusArray = [NSMutableArray arrayWithCapacity:statuses->length]; + + for (size_t i = 0; i < statuses->length; i++) { + PoolStatus status = statuses->statuses[i]; + [statusArray addObject:@(status)]; + } + + destroy_coinjoin_session_statuses(statuses); + + return statusArray; + } else { + return @[]; + } + } +} + +- (BOOL)isLockedCoin:(DSUTXO)utxo { + @synchronized (self) { + return is_locked_coin_with_manager(self.clientManager, (uint8_t (*)[32])(utxo.hash.u8), (uint32_t)utxo.n); + } +} + +- (void)stopAndResetClientManager { + @synchronized (self) { + stop_and_reset_coinjoin(self.clientManager); + } +} + +- (DSChain *)chain { + return self.chainManager.chain; +} + +- (void)dealloc { + @synchronized (self) { + unregister_client_manager(self.clientManager); + _clientManager = NULL; + } +} + +/// +/// MARK: Rust FFI callbacks +/// + +InputValue *getInputValueByPrevoutHash(uint8_t (*prevout_hash)[32], uint32_t index, const void *context) { + UInt256 txHash = *((UInt256 *)prevout_hash); + InputValue *inputValue = NULL; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + inputValue = malloc(sizeof(InputValue)); + DSWallet *wallet = wrapper.chain.wallets.firstObject; + int64_t value = [wallet inputValue:txHash inputIndex:index]; + + if (value != -1) { + inputValue->is_valid = TRUE; + inputValue->value = value; + } else { + inputValue->is_valid = FALSE; + } + } + + return inputValue; +} + + +bool hasChainLock(Block *block, const void *context) { + BOOL hasChainLock = NO; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + hasChainLock = [wrapper.chain blockHeightChainLocked:block->height]; + } + + return hasChainLock; +} + +Transaction *getTransaction(uint8_t (*tx_hash)[32], const void *context) { + UInt256 txHash = *((UInt256 *)tx_hash); + Transaction *tx = NULL; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + DSTransaction *transaction = [wrapper.chain transactionForHash:txHash]; + + if (transaction) { + tx = [transaction ffi_malloc:wrapper.chain.chainType]; + } + } + + return tx; +} + +bool isMineInput(uint8_t (*tx_hash)[32], uint32_t index, const void *context) { + UInt256 txHash = *((UInt256 *)tx_hash); + BOOL result = NO; + + @synchronized (context) { + result = [AS_OBJC(context).manager isMineInput:txHash index:index]; + } + + return result; +} + +GatheredOutputs* availableCoins(bool onlySafe, CoinControl *coinControl, WalletEx *walletEx, const void *context) { + GatheredOutputs *gatheredOutputs; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + ChainType chainType = wrapper.chain.chainType; + DSCoinControl *cc = [[DSCoinControl alloc] initWithFFICoinControl:coinControl chainType:wrapper.chain.chainType]; + NSArray *coins = [wrapper.manager availableCoins:walletEx onlySafe:onlySafe coinControl:cc minimumAmount:1 maximumAmount:MAX_MONEY minimumSumAmount:MAX_MONEY maximumCount:0]; + + gatheredOutputs = malloc(sizeof(GatheredOutputs)); + InputCoin **coinsArray = malloc(coins.count * sizeof(InputCoin *)); + + for (uintptr_t i = 0; i < coins.count; ++i) { + coinsArray[i] = [coins[i] ffi_malloc:chainType]; + } + + gatheredOutputs->items = coinsArray; + gatheredOutputs->item_count = (uintptr_t)coins.count; + } + + return gatheredOutputs; +} + +SelectedCoins* selectCoinsGroupedByAddresses(bool skipDenominated, bool anonymizable, bool skipUnconfirmed, int maxOupointsPerAddress, WalletEx* walletEx, const void *context) { + SelectedCoins *vecTallyRet; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + NSArray *tempVecTallyRet = [wrapper.manager selectCoinsGroupedByAddresses:walletEx skipDenominated:skipDenominated anonymizable:anonymizable skipUnconfirmed:skipUnconfirmed maxOupointsPerAddress:maxOupointsPerAddress]; + + vecTallyRet = malloc(sizeof(SelectedCoins)); + vecTallyRet->item_count = tempVecTallyRet.count; + vecTallyRet->items = malloc(tempVecTallyRet.count * sizeof(CompactTallyItem *)); + + for (uint32_t i = 0; i < tempVecTallyRet.count; i++) { + vecTallyRet->items[i] = [tempVecTallyRet[i] ffi_malloc:wrapper.chain.chainType]; + } + } + + return vecTallyRet; +} + +void destroyInputValue(InputValue *value) { + if (value) { + free(value); + } +} + +void destroyTransaction(Transaction *value) { + if (value) { + [DSTransaction ffi_free:value]; + } +} + +void destroySelectedCoins(SelectedCoins *selectedCoins) { + if (!selectedCoins) { + return; + } + + if (selectedCoins->items) { + for (int i = 0; i < selectedCoins->item_count; i++) { + [DSCompactTallyItem ffi_free:selectedCoins->items[i]]; + } + + free(selectedCoins->items); + } + + free(selectedCoins); +} + +void destroyGatheredOutputs(GatheredOutputs *gatheredOutputs) { + if (!gatheredOutputs) { + return; + } + + if (gatheredOutputs->items) { + for (int i = 0; i < gatheredOutputs->item_count; i++) { + [DSInputCoin ffi_free:gatheredOutputs->items[i]]; + } + + free(gatheredOutputs->items); + } + + free(gatheredOutputs); +} + +Transaction* signTransaction(Transaction *transaction, bool anyoneCanPay, const void *context) { + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + DSTransaction *tx = [[DSTransaction alloc] initWithTransaction:transaction onChain:wrapper.chain]; + BOOL isSigned = [wrapper.chain.wallets.firstObject.accounts.firstObject signTransaction:tx anyoneCanPay:anyoneCanPay]; + + if (isSigned) { + return [tx ffi_malloc:wrapper.chain.chainType]; + } + } + + return nil; +} + +unsigned int countInputsWithAmount(unsigned long long inputAmount, const void *context) { + @synchronized (context) { + return [AS_OBJC(context).manager countInputsWithAmount:inputAmount]; + } +} + +ByteArray freshCoinJoinAddress(bool internal, const void *context) { + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + NSString *address = [wrapper.manager freshAddress:internal]; + + return script_pubkey_for_address([address UTF8String], wrapper.chain.chainType); + } +} + +bool commitTransaction(struct Recipient **items, uintptr_t item_count, CoinControl *coinControl, bool is_denominating, uint8_t (*client_session_id)[32], const void *context) { + NSMutableArray *amounts = [NSMutableArray array]; + NSMutableArray *scripts = [NSMutableArray array]; + + for (uintptr_t i = 0; i < item_count; i++) { + Recipient *recipient = items[i]; + [amounts addObject:@(recipient->amount)]; + NSData *script = [NSData dataWithBytes:recipient->script_pub_key.ptr length:recipient->script_pub_key.len]; + [scripts addObject:script]; + } + + bool result = false; + + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + DSCoinControl *cc = [[DSCoinControl alloc] initWithFFICoinControl:coinControl chainType:wrapper.chain.chainType]; + result = [wrapper.manager commitTransactionForAmounts:amounts outputs:scripts coinControl:cc onPublished:^(UInt256 txId, NSError * _Nullable error) { + @synchronized (context) { + if (error) { + DSLog(@"[%@] CoinJoin: commit tx error: %@, tx type: %@", wrapper.chain.name, error, is_denominating ? @"denominations" : @"collateral"); + } else if (is_denominating) { + #if DEBUG + DSLog(@"[%@] CoinJoin tx: Denominations Created: %@", wrapper.chain.name, uint256_reverse_hex(txId)); + #else + DSLog(@"[%@] CoinJoin tx: Denominations Created: %@", wrapper.chain.name, @""); + #endif + bool isFinished = finish_automatic_denominating(wrapper.clientManager, client_session_id); + + if (!isFinished) { + DSLog(@"[%@] CoinJoin: auto_denom not finished", wrapper.chain.name); + } + + processor_destroy_block_hash(client_session_id); + [wrapper.manager onTransactionProcessed:txId type:CoinJoinTransactionType_CreateDenomination]; + } else { + #if DEBUG + DSLog(@"[%@] CoinJoin tx: Collateral Created: %@", wrapper.chain.name, uint256_reverse_hex(txId)); + #else + DSLog(@"[%@] CoinJoin tx: Collateral Created: %@", wrapper.chain.name, @""); + #endif + [wrapper.manager onTransactionProcessed:txId type:CoinJoinTransactionType_MakeCollateralInputs]; + } + } + }]; + } + + return result; +} + +MasternodeEntry* masternodeByHash(uint8_t (*hash)[32], const void *context) { + UInt256 mnHash = *((UInt256 *)hash); + MasternodeEntry *masternode; + + @synchronized (context) { + masternode = [[AS_OBJC(context).manager masternodeEntryByHash:mnHash] ffi_malloc]; + } + + return masternode; +} + +void destroyMasternodeEntry(MasternodeEntry *masternodeEntry) { + if (!masternodeEntry) { + return; + } + + [DSSimplifiedMasternodeEntry ffi_free:masternodeEntry]; +} + +uint64_t validMNCount(const void *context) { + @synchronized (context) { + return [AS_OBJC(context).manager validMNCount]; + } +} + +MasternodeList* getMNList(const void *context) { + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + DSMasternodeList *mnList = [wrapper.manager mnList]; + return [mnList ffi_malloc]; + } +} + +void destroyMNList(MasternodeList *masternodeList) { // TODO: check destroyMasternodeList + if (!masternodeList) { + return; + } + + [DSMasternodeList ffi_free:masternodeList]; +} + +bool isBlockchainSynced(const void *context) { + @synchronized (context) { + return AS_OBJC(context).manager.isChainSynced; + } +} + +bool isMasternodeOrDisconnectRequested(uint8_t (*ip_address)[16], uint16_t port, const void *context) { + UInt128 ipAddress = *((UInt128 *)ip_address); + + @synchronized (context) { + return [AS_OBJC(context).manager isMasternodeOrDisconnectRequested:ipAddress port:port]; + } +} + +bool disconnectMasternode(uint8_t (*ip_address)[16], uint16_t port, const void *context) { + UInt128 ipAddress = *((UInt128 *)ip_address); + + @synchronized (context) { + return [AS_OBJC(context).manager disconnectMasternode:ipAddress port:port]; + } +} + +bool sendMessage(char *message_type, ByteArray *byteArray, uint8_t (*ip_address)[16], uint16_t port, bool warn, const void *context) { + NSString *messageType = [NSString stringWithUTF8String:message_type]; + UInt128 ipAddress = *((UInt128 *)ip_address); + + @synchronized (context) { + NSData *message = [NSData dataWithBytes:byteArray->ptr length:byteArray->len]; + return [AS_OBJC(context).manager sendMessageOfType:messageType message:message withPeerIP:ipAddress port:port warn:warn]; + } +} + +bool addPendingMasternode(uint8_t (*pro_tx_hash)[32], uint8_t (*session_id)[32], const void *context) { + UInt256 sessionId = *((UInt256 *)session_id); + UInt256 proTxHash = *((UInt256 *)pro_tx_hash); + + @synchronized (context) { + return [AS_OBJC(context).manager addPendingMasternode:proTxHash clientSessionId:sessionId]; + } +} + +void startManagerAsync(const void *context) { + @synchronized (context) { + [AS_OBJC(context).manager startAsync]; + } +} + +void updateSuccessBlock(const void *context) { + @synchronized (context) { + [AS_OBJC(context).manager updateSuccessBlock]; + } +} + +bool isWaitingForNewBlock(const void *context) { + @synchronized (context) { + return [AS_OBJC(context).manager isWaitingForNewBlock]; + } +} + +void sessionLifecycleListener(bool is_complete, + int32_t base_session_id, + uint8_t (*client_session_id)[32], + uint32_t denomination, + enum PoolState state, + enum PoolMessage message, + enum PoolStatus status, + uint8_t (*ip_address)[16], + bool joined, + const void *context) { + @synchronized (context) { + UInt256 clientSessionId = *((UInt256 *)client_session_id); + UInt128 ipAddress = *((UInt128 *)ip_address); + + if (is_complete) { + [AS_OBJC(context).manager onSessionComplete:base_session_id clientSessionId:clientSessionId denomination:denomination poolState:state poolMessage:message poolStatus:status ipAddress:ipAddress isJoined:joined]; + } else { + [AS_OBJC(context).manager onSessionStarted:base_session_id clientSessionId:clientSessionId denomination:denomination poolState:state poolMessage:message poolStatus:status ipAddress:ipAddress isJoined:joined]; + } + } +} + +void mixingLifecycleListener(bool is_complete, + bool is_interrupted, + const enum PoolStatus *pool_statuses, + uintptr_t pool_statuses_len, + const void *context) { + @synchronized (context) { + NSMutableArray *statuses = [NSMutableArray array]; + + for (uintptr_t i = 0; i < pool_statuses_len; i++) { + [statuses addObject:@(pool_statuses[i])]; + } + + if (is_complete || is_interrupted) { + [AS_OBJC(context).manager onMixingComplete:statuses isInterrupted:is_interrupted]; + } else { + [AS_OBJC(context).manager onMixingStarted:statuses]; + } + } +} + +CoinJoinKeys* getCoinJoinKeys(bool used, const void *context) { + @synchronized (context) { + DSCoinJoinWrapper *wrapper = AS_OBJC(context); + NSArray *addresses; + + if (used) { + addresses = [wrapper.manager getIssuedReceiveAddresses]; + } else { + addresses = [wrapper.manager getUsedReceiveAddresses]; + } + + CoinJoinKeys *keys = malloc(sizeof(CoinJoinKeys)); + keys->item_count = addresses.count; + keys->items = malloc(sizeof(ByteArray *) * keys->item_count); + + for (NSUInteger i = 0; i < addresses.count; i++) { + NSString *address = addresses[i]; + ByteArray *byteArray = malloc(sizeof(ByteArray)); + byteArray->ptr = script_pubkey_for_address([address UTF8String], wrapper.chain.chainType).ptr; + byteArray->len = script_pubkey_for_address([address UTF8String], wrapper.chain.chainType).len; + keys->items[i] = byteArray; + } + + return keys; + } +} + +void destroyCoinJoinKeys(struct CoinJoinKeys *coinjoin_keys) { + if (coinjoin_keys == NULL) { + return; + } + + for (uintptr_t i = 0; i < coinjoin_keys->item_count; i++) { + if (coinjoin_keys->items[i] != NULL) { + free(coinjoin_keys->items[i]->ptr); + free(coinjoin_keys->items[i]); + } + } + + free(coinjoin_keys->items); + free(coinjoin_keys); +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.h b/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.h new file mode 100644 index 000000000..17c2289e8 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.h @@ -0,0 +1,34 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSInputCoin.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DSCompactTallyItem : NSObject + +@property (strong, nonatomic) NSData *txDestination; +@property (nonatomic, assign) uint64_t amount; +@property (strong, nonatomic) NSMutableArray *inputCoins; + +- (CompactTallyItem *)ffi_malloc:(ChainType)type; ++ (void)ffi_free:(CompactTallyItem *)item; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.m b/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.m new file mode 100644 index 000000000..59573e557 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSCompactTallyItem.m @@ -0,0 +1,69 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCompactTallyItem.h" + +@implementation DSCompactTallyItem + +- (instancetype)init { + self = [super init]; + if (self) { + _amount = 0; + _inputCoins = [[NSMutableArray alloc] init]; + } + return self; +} + +- (CompactTallyItem *)ffi_malloc:(ChainType)type { + CompactTallyItem *tallyItem = malloc(sizeof(CompactTallyItem)); + tallyItem->amount = self.amount; + + NSUInteger length = self.txDestination.length; + tallyItem->tx_destination_length = (uintptr_t)length; + NSData *scriptData = self.txDestination; + tallyItem->tx_destination = data_malloc(scriptData); + + uintptr_t inputCoinsCount = self.inputCoins.count; + tallyItem->input_coins_size = inputCoinsCount; + InputCoin **inputCoins = malloc(inputCoinsCount * sizeof(InputCoin *)); + + for (uintptr_t i = 0; i < inputCoinsCount; ++i) { + inputCoins[i] = [self.inputCoins[i] ffi_malloc:type]; + } + + tallyItem->input_coins = inputCoins; + + return tallyItem; +} + ++ (void)ffi_free:(CompactTallyItem *)item { + if (!item) return; + + free(item->tx_destination); + + if (item->input_coins) { + for (int i = 0; i < item->input_coins_size; i++) { + [DSInputCoin ffi_free:item->input_coins[i]]; + } + + free(item->input_coins); + } + + free(item); +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/DSInputCoin.h b/DashSync/shared/Models/CoinJoin/DSInputCoin.h new file mode 100644 index 000000000..e2cb7411e --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSInputCoin.h @@ -0,0 +1,38 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSTransactionOutput.h" +#import "BigIntTypes.h" +#import "DSTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DSInputCoin : NSObject + +@property (nonatomic, assign) UInt256 outpointHash; +@property (nonatomic, assign) uint32_t outpointIndex; +@property (strong, nonatomic) DSTransactionOutput *output; +@property (nonatomic, assign) uint64_t effectiveValue; + +- (instancetype)initWithTx:(DSTransaction *)tx index:(int32_t)i; +- (InputCoin *)ffi_malloc:(ChainType)type; ++ (void)ffi_free:(InputCoin *)inputCoin; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSInputCoin.m b/DashSync/shared/Models/CoinJoin/DSInputCoin.m new file mode 100644 index 000000000..10a95d6d7 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSInputCoin.m @@ -0,0 +1,58 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSInputCoin.h" +#import "DSTransactionOutput+CoinJoin.h" + +@implementation DSInputCoin + +- (instancetype)initWithTx:(DSTransaction *)tx index:(int32_t)i { + self = [super init]; + if (self) { + _outpointHash = tx.txHash; + _outpointIndex = i; + _output = tx.outputs[i]; + _effectiveValue = tx.outputs[i].amount; + } + return self; +} + +- (InputCoin *)ffi_malloc:(ChainType)type { + InputCoin *inputCoin = malloc(sizeof(InputCoin)); + inputCoin->outpoint_index = self.outpointIndex; + inputCoin->outpoint_hash = uint256_malloc(self.outpointHash); + inputCoin->output = [self.output ffi_malloc:type]; + inputCoin->effective_value = self.effectiveValue; + + return inputCoin; +} + ++ (void)ffi_free:(InputCoin *)inputCoin { + if (!inputCoin) return; + + if (inputCoin->outpoint_hash) { + free(inputCoin->outpoint_hash); + } + + if (inputCoin->output) { + [DSTransactionOutput ffi_free:inputCoin->output]; + } + + free(inputCoin); +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.h b/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.h new file mode 100644 index 000000000..80a21e05c --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.h @@ -0,0 +1,33 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSTransaction.h" +#import "dash_shared_core.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DSTransaction (CoinJoin) + +- (DSTransaction *)initWithTransaction:(Transaction *)transaction onChain:(DSChain *)chain; + +- (Transaction *)ffi_malloc:(ChainType)chainType; ++ (void)ffi_free:(Transaction *)tx; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.m b/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.m new file mode 100644 index 000000000..59ce8af24 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransaction+CoinJoin.m @@ -0,0 +1,134 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "BigIntTypes.h" +#import "DSTransaction.h" +#import "DSTransaction+CoinJoin.h" +#import "DSTransactionInput+CoinJoin.h" +#import "DSTransactionOutput+CoinJoin.h" +#import "NSData+Dash.h" +#import "DSKeyManager.h" + +@implementation DSTransaction (CoinJoin) + +- (DSTransaction *)initWithTransaction:(Transaction *)transaction onChain:(DSChain *)chain { + NSMutableArray *hashes = [NSMutableArray array]; + NSMutableArray *indexes = [NSMutableArray array]; + NSMutableArray *scripts = [NSMutableArray array]; + NSMutableArray *inputSequences = [NSMutableArray array]; + + for (uintptr_t i = 0; i < transaction->inputs_count; i++) { + TransactionInput *input = transaction->inputs[i]; + UInt256 hashValue; + memcpy(hashValue.u8, *input->input_hash, 32); + NSNumber *index = @(input->index); + NSData *script = [NSData data]; + + if (input->script && input->script_length != 0) { + script = [NSData dataWithBytes:input->script length:input->script_length]; + } else { + DSTransaction *inputTx = [chain transactionForHash:hashValue]; + + if (inputTx) { + script = inputTx.outputs[index.integerValue].outScript; + } + } + + NSNumber *sequence = @(input->sequence); + + [hashes addObject:uint256_obj(hashValue)]; + [indexes addObject:index]; + [scripts addObject:script]; + [inputSequences addObject:sequence]; + } + + NSMutableArray *addresses = [NSMutableArray array]; + NSMutableArray *amounts = [NSMutableArray array]; + + for (uintptr_t i = 0; i < transaction->outputs_count; i++) { + TransactionOutput *output = transaction->outputs[i]; + NSData *scriptPubKey = [NSData dataWithBytes:output->script length:output->script_length]; + NSString *address = [DSKeyManager addressWithScriptPubKey:scriptPubKey forChain:chain]; + NSNumber *amount = @(output->amount); + + [addresses addObject:address ?: [NSNull null]]; // Use NSNull turned into OP_RETURN script later + [amounts addObject:amount]; + } + + DSTransaction *tx = [[DSTransaction alloc] initWithInputHashes:hashes inputIndexes:indexes inputScripts:scripts inputSequences:inputSequences outputAddresses:addresses outputAmounts:amounts onChain:chain]; + tx.version = transaction->version; + + return tx; +} + +- (Transaction *)ffi_malloc:(ChainType)chainType { + Transaction *transaction = malloc(sizeof(Transaction)); + + transaction->tx_hash = uint256_malloc(self.txHash); + uintptr_t inputsCount = self.inputs.count; + uintptr_t outputsCount = self.outputs.count; + transaction->inputs_count = inputsCount; + transaction->outputs_count = outputsCount; + + TransactionInput **inputsArray = malloc(inputsCount * sizeof(TransactionInput *)); + TransactionOutput **outputsArray = malloc(outputsCount * sizeof(TransactionOutput *)); + + for (uintptr_t i = 0; i < inputsCount; ++i) { + inputsArray[i] = [self.inputs[i] ffi_malloc]; + } + + for (uintptr_t i = 0; i < outputsCount; ++i) { + outputsArray[i] = [self.outputs[i] ffi_malloc:chainType]; + } + + transaction->inputs = inputsArray; + transaction->outputs = outputsArray; + transaction->lock_time = self.lockTime; + transaction->version = self.version; + transaction->tx_type = (TransactionType)self.type; + transaction->payload_offset = self.payloadOffset; + transaction->block_height = self.blockHeight; + + return transaction; +} + ++ (void)ffi_free:(Transaction *)tx { + if (!tx) return; + + free(tx->tx_hash); + + if (tx->inputs) { + for (int i = 0; i < tx->inputs_count; i++) { + [DSTransactionInput ffi_free:tx->inputs[i]]; + } + + free(tx->inputs); + } + + if (tx->outputs) { + for (int i = 0; i < tx->outputs_count; i++) { + [DSTransactionOutput ffi_free:tx->outputs[i]]; + } + + free(tx->outputs); + } + + free(tx); +} + +@end + diff --git a/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.h b/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.h new file mode 100644 index 000000000..342a36709 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.h @@ -0,0 +1,31 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSTransactionInput.h" +#import "dash_shared_core.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DSTransactionInput (CoinJoin) + +- (TransactionInput *)ffi_malloc; ++ (void)ffi_free:(TransactionInput *)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.m b/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.m new file mode 100644 index 000000000..d7b822f6e --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransactionInput+CoinJoin.m @@ -0,0 +1,59 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "BigIntTypes.h" +#import "DSTransactionInput.h" +#import "DSTransactionInput+CoinJoin.h" +#import "NSData+Dash.h" + +@implementation DSTransactionInput (CoinJoin) + +- (TransactionInput *)ffi_malloc { + TransactionInput *transactionInput = malloc(sizeof(TransactionInput)); + transactionInput->input_hash = uint256_malloc(self.inputHash); + transactionInput->index = self.index; + transactionInput->sequence = self.sequence; + + NSData *scriptData = self.inScript; + transactionInput->script_length = scriptData.length; + transactionInput->script = data_malloc(scriptData); + + NSData *signatureData = self.signature; + transactionInput->signature_length = signatureData.length; + transactionInput->signature = data_malloc(signatureData); + + return transactionInput; +} + ++ (void)ffi_free:(TransactionInput *)input { + if (!input) return; + + free(input->input_hash); + + if (input->script) { + free(input->script); + } + + if (input->signature) { + free(input->signature); + } + + free(input); +} + +@end + diff --git a/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.h b/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.h new file mode 100644 index 000000000..39f706b67 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.h @@ -0,0 +1,31 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSTransactionOutput.h" +#import "dash_shared_core.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DSTransactionOutput (CoinJoin) + +- (TransactionOutput *)ffi_malloc:(ChainType) type; ++ (void)ffi_free:(TransactionOutput *)output; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.m b/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.m new file mode 100644 index 000000000..27a61cc02 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/DSTransactionOutput+CoinJoin.m @@ -0,0 +1,63 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "BigIntTypes.h" +#import "DSTransactionOutput.h" +#import "DSTransactionOutput+CoinJoin.h" +#import "NSData+Dash.h" + +@implementation DSTransactionOutput (CoinJoin) + +- (TransactionOutput *)ffi_malloc:(ChainType)type { + TransactionOutput *transactionOutput = malloc(sizeof(TransactionOutput)); + transactionOutput->amount = self.amount; + + NSUInteger length = self.outScript.length; + transactionOutput->script_length = (uintptr_t)length; + NSData *scriptData = self.outScript; + transactionOutput->script = data_malloc(scriptData); + + char *c_string = address_with_script_pubkey(self.outScript.bytes, self.outScript.length, type); + + if (c_string) { + size_t addressLength = strlen(c_string); + transactionOutput->address_length = (uintptr_t)addressLength; + transactionOutput->address = (uint8_t *)c_string; + } else { + transactionOutput->address_length = 0; + transactionOutput->address = NULL; + } + + return transactionOutput; +} + ++ (void)ffi_free:(TransactionOutput *)output { + if (!output) return; + + if (output->script) { + free(output->script); + } + + if (output->address) { + processor_destroy_string((char *)output->address); + } + + free(output); +} + +@end + diff --git a/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.h b/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.h new file mode 100644 index 000000000..4d023e9a2 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.h @@ -0,0 +1,33 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +@interface DSBackoff : NSObject + +@property (nonatomic, strong) NSDate *retryTime; +@property (nonatomic, assign) float_t backoff; +@property (nonatomic, readonly) float_t maxBackoff; +@property (nonatomic, readonly) float_t initialBackoff; +@property (nonatomic, readonly) float_t multiplier; + +- (instancetype)initInitialBackoff:(float_t)initial maxBackoff:(float_t)max multiplier:(float_t)multiplier; + +- (void)trackSuccess; +- (void)trackFailure; + +@end diff --git a/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.m b/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.m new file mode 100644 index 000000000..8954cef7b --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/Utils/DSBackoff.m @@ -0,0 +1,43 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSBackoff.h" + +@implementation DSBackoff + +- (instancetype)initInitialBackoff:(float_t)initial maxBackoff:(float_t)max multiplier:(float_t)multiplier { + self = [super init]; + if (self) { + _maxBackoff = max; + _initialBackoff = initial; + _multiplier = multiplier; + [self trackSuccess]; + } + return self; +} + +- (void)trackSuccess { + _backoff = _initialBackoff; + _retryTime = [NSDate date]; +} + +- (void)trackFailure { + _retryTime = [[NSDate date] dateByAddingTimeInterval:_backoff]; + _backoff = MIN(_backoff, _maxBackoff); +} + +@end diff --git a/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.h b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.h new file mode 100644 index 000000000..4aafa9ae2 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.h @@ -0,0 +1,43 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "DSChain.h" +#import "DSPeer.h" + +NS_ASSUME_NONNULL_BEGIN + +@class DSCoinJoinManager; + +@interface DSMasternodeGroup : NSObject + +@property (atomic, readonly) BOOL isRunning; + +- (instancetype)initWithManager:(DSCoinJoinManager *)manager; + +- (void)startAsync; +- (void)stopAsync; +- (void)triggerConnections; +- (BOOL)isMasternodeOrDisconnectRequested:(UInt128)ip port:(uint16_t)port; +- (BOOL)disconnectMasternode:(UInt128)ip port:(uint16_t)port; +- (BOOL)addPendingMasternode:(UInt256)proTxHash clientSessionId:(UInt256)sessionId; +- (BOOL)forPeer:(UInt128)ip port:(uint16_t)port warn:(BOOL)warn withPredicate:(BOOL (^)(DSPeer *peer))predicate; +- (NSString *)hostFor:(UInt128)address; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m new file mode 100644 index 000000000..b169c49f3 --- /dev/null +++ b/DashSync/shared/Models/CoinJoin/Utils/DSMasternodeGroup.m @@ -0,0 +1,584 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSMasternodeGroup.h" +#import "DSChainManager.h" +#import "DSChain+Protected.h" +#import "DSCoinJoinManager.h" +#import "DSSimplifiedMasternodeEntry.h" +#import "DSMasternodeManager.h" +#import "DSPeerManager.h" +#import "DSSendCoinJoinQueue.h" +#import "DSBackoff.h" +#import "DSBlock.h" +#import "DSPeerManager+Protected.h" +#import + +float_t const MIN_PEER_DISCOVERY_INTERVAL = 1; // One second +float_t const DEFAULT_INITIAL_BACKOFF = 1; // One second +float_t const DEFAULT_MAX_BACKOFF = 5; // Five seconds +float_t const GROUP_BACKOFF_MULTIPLIER = 1.5; +float_t const BACKOFF_MULTIPLIER = 1.001; + +@interface DSMasternodeGroup () + +@property (nonatomic, strong) DSChain *chain; +@property (nonatomic, weak, nullable) DSCoinJoinManager *coinJoinManager; +@property (nonatomic, strong) NSMutableSet *mutablePendingSessions; +@property (nonatomic, strong) NSMutableDictionary *masternodeMap; +@property (nonatomic, strong) NSMutableDictionary *sessionMap; +@property (nonatomic, strong) NSMutableDictionary *addressMap; +@property (atomic, readonly) NSUInteger maxConnections; +@property (nonatomic, strong) NSMutableArray *mutablePendingClosingMasternodes; +@property (nonatomic, strong) NSMutableSet *mutableConnectedPeers; +@property (nonatomic, strong) NSMutableSet *mutablePendingPeers; +@property (nonatomic, strong) NSObject *peersLock; +@property (nonatomic, readonly) BOOL shouldSendDsq; +@property (nullable, nonatomic, readwrite) DSPeer *downloadPeer; +@property (nonatomic, strong) DSBackoff *groupBackoff; +@property (nonatomic, strong) NSMutableDictionary *backoffMap; +@property (nonatomic) uint32_t lastSeenBlock; + +@end + +@implementation DSMasternodeGroup + +- (instancetype)initWithManager:(DSCoinJoinManager *)manager { + self = [super init]; + if (self) { + _coinJoinManager = manager; + _chain = manager.chain; + _mutablePendingSessions = [NSMutableSet set]; + _mutablePendingClosingMasternodes = [NSMutableArray array]; + _masternodeMap = [NSMutableDictionary dictionary]; + _sessionMap = [NSMutableDictionary dictionary]; + _addressMap = [NSMutableDictionary dictionary]; + _mutableConnectedPeers = [NSMutableSet set]; + _mutablePendingPeers = [NSMutableSet set]; + _peersLock = [[NSObject alloc] init]; + _downloadPeer = nil; + _maxConnections = 0; + _shouldSendDsq = true; + _groupBackoff = [[DSBackoff alloc] initInitialBackoff:DEFAULT_INITIAL_BACKOFF maxBackoff:DEFAULT_MAX_BACKOFF multiplier:GROUP_BACKOFF_MULTIPLIER]; + _backoffMap = [NSMutableDictionary dictionary]; + _lastSeenBlock = 0; + } + return self; +} + +- (void)startAsync { + _isRunning = true; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleSyncStateDidChangeNotification:) + name:DSChainManagerSyncStateDidChangeNotification + object:nil]; + [self triggerConnections]; +} + +- (dispatch_queue_t)networkingQueue { + return self.chain.networkingQueue; +} + +- (void)stopAsync { + _isRunning = false; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (BOOL)isMasternodeOrDisconnectRequested:(UInt128)ip port:(uint16_t)port { + BOOL found = [self forPeer:ip port:port warn:NO withPredicate:^BOOL(DSPeer *peer) { + return YES; + }]; + + if (!found) { + for (DSPeer *mn in self.pendingClosingMasternodes) { + if (uint128_eq(mn.address, ip) && mn.port == port) { + found = true; + } + } + } + + return found; +} + +- (BOOL)disconnectMasternode:(UInt128)ip port:(uint16_t)port { + return [self forPeer:ip port:port warn:YES withPredicate:^BOOL(DSPeer *peer) { + DSLog(@"[%@] CoinJoin: masternode[closing] %@", self.chain.name, [self hostFor:ip]); + + @synchronized (self.mutablePendingClosingMasternodes) { + [self.mutablePendingClosingMasternodes addObject:peer]; + } + + [self updateMaxConnections]; + [peer disconnect]; + + return true; + }]; +} + +- (BOOL)forPeer:(UInt128)ip port:(uint16_t)port warn:(BOOL)warn withPredicate:(BOOL (^)(DSPeer *peer))predicate { + NSMutableString *listOfPeers = [NSMutableString string]; + NSSet *peers = self.connectedPeers; + + for (DSPeer *peer in peers) { + [listOfPeers appendFormat:@"%@, ", peer.location]; + + if (uint128_eq(peer.address, ip) && peer.port == port) { + return predicate(peer); + } + } + + if (warn) { + if (![self isNodePending:ip port:port]) { + DSLog(@"[%@] CoinJoin: Cannot find %@ in the list of connected peers: %@", self.chain.name, [self hostFor:ip], listOfPeers); + NSAssert(NO, @"Cannot find %@", [self hostFor:ip]); + } else { + DSLog(@"[%@] CoinJoin: %@ in the list of pending peers", self.chain.name, [self hostFor:ip]); + } + } + + return NO; +} + +- (BOOL)isNodePending:(UInt128)ip port:(uint16_t)port { + for (DSPeer *peer in self.pendingPeers) { + if (uint128_eq(peer.address, ip) && peer.port == port) { + return true; + } + } + + return false; +} + +- (NSSet *)connectedPeers { + @synchronized(self.peersLock) { + return [self.mutableConnectedPeers copy]; + } +} + +- (NSSet *)pendingPeers { + @synchronized(self.peersLock) { + return [self.mutablePendingPeers copy]; + } +} + +- (NSArray *)pendingClosingMasternodes { + @synchronized(self.mutablePendingClosingMasternodes) { + return [self.mutablePendingClosingMasternodes copy]; + } +} + +- (NSSet *)pendingSessions { + @synchronized(self.mutablePendingSessions) { + return [self.mutablePendingSessions copy]; + } +} + +- (void)triggerConnections { + [self triggerConnectionsJobWithDelay:0]; +} + +- (void)triggerConnectionsJobWithDelay:(NSTimeInterval)delay { + dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); + dispatch_after(delayTime, self.networkingQueue, ^{ + if (!self.isRunning) { + return; + } + + if (!self.coinJoinManager.isChainSynced || self.coinJoinManager.isWaitingForNewBlock || !self.coinJoinManager.isMixing) { + return; + } + + NSUInteger numPeers = self.pendingPeers.count + self.connectedPeers.count; + + if (numPeers >= self.maxConnections) { + return; + } + + NSDate *now = [NSDate date]; + DSPeer *peerToTry = [self getNextPendingMasternode]; + + if (peerToTry) { + NSDate *retryTime = [self.backoffMap objectForKey:peerToTry.location].retryTime; + retryTime = [retryTime laterDate:self.groupBackoff.retryTime]; + NSTimeInterval delay = [retryTime timeIntervalSinceDate:now]; + + if (delay > 0.1) { + DSLog(@"[%@] CoinJoin: Waiting %fl s before next connect attempt to masternode to %@", self.chain.name, delay, peerToTry == NULL ? @"" : peerToTry.location); + [self triggerConnectionsJobWithDelay:delay]; + return; + } + + [self connectTo:peerToTry]; + } + + NSUInteger count = self.maxConnections; + + @synchronized (self.peersLock) { + if (peerToTry) { + [self.groupBackoff trackSuccess]; + } else { + [self.groupBackoff trackFailure]; + } + + count = self.mutablePendingPeers.count + self.mutableConnectedPeers.count; + } + + if (count < self.maxConnections) { + [self triggerConnectionsJobWithDelay:0]; // Try next peer immediately. + } + }); +} + +- (DSPeer *)getNextPendingMasternode { + NSArray *pendingClosingMasternodesCopy = self.pendingClosingMasternodes; + DSPeer *peerWithLeastBackoff = nil; + NSValue *sessionValueWithLeastBackoff = nil; + UInt256 sessionId = UINT256_ZERO; + NSDate *leastBackoffTime = [NSDate distantFuture]; + + for (NSValue *sessionValue in self.pendingSessions) { + [sessionValue getValue:&sessionId]; + DSSimplifiedMasternodeEntry *mixingMasternodeInfo = [self mixingMasternodeAddressFor:sessionId]; + + if (mixingMasternodeInfo) { + UInt128 ipAddress = mixingMasternodeInfo.address; + uint16_t port = mixingMasternodeInfo.port; + DSPeer *peer = [self peerForLocation:ipAddress port:port]; + + if (peer == nil) { + peer = [DSPeer peerWithAddress:ipAddress andPort:port onChain:self.chain]; + } + + if (![pendingClosingMasternodesCopy containsObject:peer] && ![self isNodeConnected:peer] && ![self isNodePending:peer]) { + DSBackoff *backoff = [self.backoffMap objectForKey:peer.location]; + + if (!backoff) { + backoff = [[DSBackoff alloc] initInitialBackoff:DEFAULT_INITIAL_BACKOFF maxBackoff:DEFAULT_MAX_BACKOFF multiplier:BACKOFF_MULTIPLIER]; + [self.backoffMap setObject:backoff forKey:peer.location]; + } + + if ([backoff.retryTime compare:leastBackoffTime] == NSOrderedAscending) { + leastBackoffTime = backoff.retryTime; + peerWithLeastBackoff = peer; + sessionValueWithLeastBackoff = sessionValue; + } + } + } + } + + if (peerWithLeastBackoff) { + @synchronized(self.addressMap) { + [self.addressMap setObject:sessionValueWithLeastBackoff forKey:peerWithLeastBackoff.location]; + DSLog(@"[%@] CoinJoin: discovery: %@ -> %@", self.chain.name, peerWithLeastBackoff.location, uint256_hex(sessionId)); + } + } + + return peerWithLeastBackoff; +} + +- (DSSimplifiedMasternodeEntry *)mixingMasternodeAddressFor:(UInt256)sessionId { + NSValue *sessionIdKey = [NSValue value:&sessionId withObjCType:@encode(UInt256)]; + NSValue *proTxHashValue = [self.sessionMap objectForKey:sessionIdKey]; + + if (proTxHashValue) { + UInt256 proTxHash = UINT256_ZERO; + [proTxHashValue getValue:&proTxHash]; + + return [self.coinJoinManager masternodeEntryByHash:proTxHash]; + } + + return nil; +} + +- (BOOL)addPendingMasternode:(UInt256)proTxHash clientSessionId:(UInt256)sessionId { + @synchronized (self.mutablePendingSessions) { + DSLog(@"[%@] CoinJoin: adding masternode for mixing. maxConnections = %lu, protx: %@, sessionId: %@", self.chain.name, (unsigned long)_maxConnections, uint256_hex(proTxHash), uint256_hex(sessionId)); + NSValue *sessionIdValue = [NSValue valueWithBytes:&sessionId objCType:@encode(UInt256)]; + [self.mutablePendingSessions addObject:sessionIdValue]; + + NSValue *proTxHashKey = [NSValue value:&proTxHash withObjCType:@encode(UInt256)]; + [self.masternodeMap setObject:sessionIdValue forKey:proTxHashKey]; + [self.sessionMap setObject:proTxHashKey forKey:sessionIdValue]; + } + + [self checkMasternodesWithoutSessions]; + [self updateMaxConnections]; + + return true; +} + +- (void)updateMaxConnections { + _maxConnections = self.mutablePendingSessions.count; + NSUInteger connections = MIN(self.maxConnections, self.coinJoinManager.options->coinjoin_sessions); + DSLog(@"[%@] CoinJoin: updating max connections to min(%lu, %lu)", self.chain.name, (unsigned long)_maxConnections, (unsigned long)self.coinJoinManager.options->coinjoin_sessions); + + [self updateMaxConnections:connections]; +} + +- (void)updateMaxConnections:(NSUInteger)connections { + _maxConnections = connections; + + if (!self.isRunning) { + return; + } + + // We may now have too many or too few open connections. Add more or drop some to get to the right amount. + NSInteger adjustment = 0; + + @synchronized (self.peersLock) { + NSUInteger pendingCount = self.mutablePendingPeers.count; + NSUInteger connectedCount = self.mutableConnectedPeers.count; + NSUInteger numPeers = pendingCount + connectedCount; + adjustment = self.maxConnections - numPeers; + DSLogPrivate(@"CoinJoin: updateMaxConnections adjustment %lu, pendingCount: %lu, connectedCount: %lu", adjustment, pendingCount, connectedCount); + } + + if (adjustment > 0) { + [self triggerConnections]; + } +} + +- (void)checkMasternodesWithoutSessions { + NSMutableArray *masternodesToDrop = [NSMutableArray array]; + NSArray *pendingSessions = self.pendingSessions; + + for (DSPeer *peer in self.connectedPeers) { + BOOL found = false; + + for (NSValue *value in pendingSessions) { + UInt256 sessionId; + [value getValue:&sessionId]; + DSSimplifiedMasternodeEntry *mixingMasternodeAddress = [self mixingMasternodeAddressFor:sessionId]; + + if (mixingMasternodeAddress) { + UInt128 ipAddress = mixingMasternodeAddress.address; + uint16_t port = mixingMasternodeAddress.port; + + if (uint128_eq(ipAddress, peer.address) && port == peer.port) { + found = YES; + } + } else { + // TODO(DashJ): we may not need this anymore + DSLog(@"[%@] CoinJoin: session is not connected to a masternode: %@", self.chain.name, uint256_hex(sessionId)); + } + } + + if (!found) { + DSLog(@"[%@] CoinJoin: masternode is not connected to a session: %@", self.chain.name, peer.location); + [masternodesToDrop addObject:peer]; + } + } + + DSLogPrivate(@"CoinJoin: need to drop %lu masternodes", (unsigned long)masternodesToDrop.count); + + for (DSPeer *peer in masternodesToDrop) { + DSSimplifiedMasternodeEntry *mn = [self.chain.chainManager.masternodeManager masternodeAtLocation:peer.address port:peer.port]; + DSLog(@"[%@] CoinJoin: masternode will be disconnected: %@: %@", self.chain.name, peer.location, uint256_hex(mn.providerRegistrationTransactionHash)); + + @synchronized (self.mutablePendingClosingMasternodes) { + [self.mutablePendingClosingMasternodes addObject:peer]; + } + + [peer disconnect]; + } +} + +- (NSString *)hostFor:(UInt128)address { + char s[INET6_ADDRSTRLEN]; + + if (address.u64[0] == 0 && address.u32[2] == CFSwapInt32HostToBig(0xffff)) { + return @(inet_ntop(AF_INET, &address.u32[3], s, sizeof(s))); + } else + return @(inet_ntop(AF_INET6, &address, s, sizeof(s))); +} + +- (BOOL)connectTo:(DSPeer *)peer { + DSLogPrivate(@"[%@] CoinJoin: connectTo: %@", self.chain.name, peer.location); + + if (![self isMasternodeSessionByPeer:peer]) { + DSLog(@"[%@] CoinJoin: %@ not a masternode session, exit", self.chain.name, peer.location); + return NO; + } + + if ([self isNodeConnected:peer] || [self isNodePending:peer]) { + DSLog(@"[%@] CoinJoin: attempting to connect to the same masternode again: %@", self.chain.name, peer.location); + return NO; // do not connect to the same peer again + } + + DSSimplifiedMasternodeEntry *mn = [_chain.chainManager.masternodeManager masternodeAtLocation:peer.address port:peer.port]; + UInt256 sessionId = UINT256_ZERO; + + @synchronized (self.masternodeMap) { + UInt256 proTxHash = mn.providerRegistrationTransactionHash; + NSValue *proTxHashKey = [NSValue value:&proTxHash withObjCType:@encode(UInt256)]; + NSValue *sessionObject = [self.masternodeMap objectForKey:proTxHashKey]; + + if (sessionObject) { + [sessionObject getValue:&sessionId]; + } + } + + if (uint256_is_zero(sessionId)) { + DSLog(@"[%@] CoinJoin: session is not connected to a masternode, proTxHashKey not found in masternodeMap", self.chain.name); + return NO; + } + + DSSimplifiedMasternodeEntry *mixingMasternodeAddress = [self mixingMasternodeAddressFor:sessionId]; + + if (!mixingMasternodeAddress) { + DSLog(@"[%@] CoinJoin: session is not connected to a masternode, sessionId: %@", self.chain.name, uint256_hex(sessionId)); + return NO; + } + + DSLog(@"[%@] CoinJoin: masternode[connecting] %@: %@; %@", self.chain.name, peer.location, uint256_hex(mn.providerRegistrationTransactionHash), uint256_hex(sessionId)); + + [peer setChainDelegate:self.chain.chainManager peerDelegate:self transactionDelegate:self.chain.chainManager.transactionManager governanceDelegate:self.chain.chainManager.governanceSyncManager sporkDelegate:self.chain.chainManager.sporkManager masternodeDelegate:self.chain.chainManager.masternodeManager queue:self.networkingQueue]; + peer.earliestKeyTime = self.chain.earliestWalletCreationTime;; + + @synchronized (self.peersLock) { + [self.mutablePendingPeers addObject:peer]; + } + + [peer connect]; + + return YES; +} + +- (BOOL)isMasternodeSessionByPeer:(DSPeer *)peer { + @synchronized (self.addressMap) { + return [self.addressMap objectForKey:peer.location] != nil; + } +} + +- (BOOL)isNodeConnected:(DSPeer *)node { + return [self forPeer:node.address port:node.port warn:NO withPredicate:^BOOL(DSPeer * _Nonnull peer) { + return YES; + }]; +} + +- (BOOL)isNodePending:(DSPeer *)node { + for (DSPeer *peer in self.pendingPeers) { + if (uint128_eq(node.address, peer.address) && node.port == peer.port) { + return YES; + } + } + + return NO; +} + +- (void)peerConnected:(nonnull DSPeer *)peer { + @synchronized (self.peersLock) { + [self.groupBackoff trackSuccess]; + [[self.backoffMap objectForKey:peer.location] trackSuccess]; + + DSLog(@"[%@] CoinJoin: New peer %@ ({%lu connected, %lu pending, %lu max)", self.chain.name, peer.location, self.mutableConnectedPeers.count, self.mutablePendingPeers.count, self.maxConnections); + + [self.mutablePendingPeers removeObject:peer]; + [self.mutableConnectedPeers addObject:peer]; + } + + if (self.shouldSendDsq) { + [peer sendRequest:[DSSendCoinJoinQueue requestWithShouldSend:true]]; + } +} + +- (void)peer:(nonnull DSPeer *)peer disconnectedWithError:(nonnull NSError *)error { + NSUInteger numPeers = self.maxConnections; + + @synchronized (self.peersLock) { + [self.mutablePendingPeers removeObject:peer]; + [self.mutableConnectedPeers removeObject:peer]; + [self.groupBackoff trackFailure]; + [[self.backoffMap objectForKey:peer.location] trackFailure]; + numPeers = self.mutablePendingPeers.count + self.mutableConnectedPeers.count; + } + + if (numPeers < self.maxConnections) { + [self triggerConnections]; + } + + DSPeer *masternode = NULL; + NSArray *pendingClosingMasternodes = self.pendingClosingMasternodes; + + for (DSPeer *mn in pendingClosingMasternodes) { + if ([peer.location isEqualToString:mn.location]) { + masternode = mn; + } + } + + DSLog(@"[%@] CoinJoin: handling this mn peer death: %@ -> %@", self.chain.name, peer.location, masternode != NULL ? masternode.location : @"not found in closing list"); + + if (masternode) { + NSString *address = peer.location; + + if ([pendingClosingMasternodes containsObject:masternode]) { + // if this is part of pendingClosingMasternodes, where we want to close the connection, + // we don't want to increase the backoff time + [[self.backoffMap objectForKey:address] trackSuccess]; + } + + @synchronized (self.mutablePendingClosingMasternodes) { + [self.mutablePendingClosingMasternodes removeObject:masternode]; + } + + UInt256 proTxHash = [self.chain.chainManager.masternodeManager masternodeAtLocation:masternode.address port:masternode.port].providerRegistrationTransactionHash; + NSValue *proTxHashKey = [NSValue valueWithBytes:&proTxHash objCType:@encode(UInt256)]; + NSValue *sessionIdObject = [self.masternodeMap objectForKey:proTxHashKey]; + + @synchronized (self.mutablePendingSessions) { + if (sessionIdObject) { + [self.mutablePendingSessions removeObject:sessionIdObject]; + [self.sessionMap removeObjectForKey:sessionIdObject]; + } + + [self.masternodeMap removeObjectForKey:proTxHashKey]; + [self.addressMap removeObjectForKey:masternode.location]; + } + + [self checkMasternodesWithoutSessions]; + } +} + +- (void)peer:(nonnull DSPeer *)peer relayedPeers:(nonnull NSArray *)peers { + // TODO ? +} + +- (DSPeer *)peerForLocation:(UInt128)ipAddress port:(uint16_t)port { + for (DSPeer *peer in self.connectedPeers) { + if (uint128_eq(peer.address, ipAddress) && peer.port == port) { + return peer; + } + } + + for (DSPeer *peer in self.pendingPeers) { + if (uint128_eq(peer.address, ipAddress) && peer.port == port) { + return peer; + } + } + + return [self.chain.chainManager.peerManager peerForLocation:ipAddress port:port]; +} + +- (void)handleSyncStateDidChangeNotification:(NSNotification *)note { + if ([note.userInfo[DSChainManagerNotificationChainKey] isEqual:[self chain]] && self.chain.lastSyncBlock.height > self.lastSeenBlock) { + self.lastSeenBlock = self.chain.lastSyncBlock.height; + DSLogPrivate(@"[%@] CoinJoin: new block found, restarting masternode connections job", self.chain.name); + [self triggerConnections]; + } +} + +@end diff --git a/DashSync/shared/Models/Derivation Paths/DSDerivationPath.h b/DashSync/shared/Models/Derivation Paths/DSDerivationPath.h index a0721d9d8..903a9040f 100644 --- a/DashSync/shared/Models/Derivation Paths/DSDerivationPath.h +++ b/DashSync/shared/Models/Derivation Paths/DSDerivationPath.h @@ -46,6 +46,7 @@ typedef void (^TransactionValidityCompletionBlock)(BOOL signedTransaction, BOOL #define FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_TOPUP 2 #define FEATURE_PURPOSE_IDENTITIES_SUBFEATURE_INVITATIONS 3 #define FEATURE_PURPOSE_DASHPAY 15 +#define FEATURE_PURPOSE_COINJOIN 4 @class DSTransaction, DSAccount, DSDerivationPath, DSKeyManager; @@ -82,6 +83,7 @@ typedef NS_ENUM(NSUInteger, DSDerivationPathReference) DSDerivationPathReference_BlockchainIdentityCreditTopupFunding = 12, DSDerivationPathReference_BlockchainIdentityCreditInvitationFunding = 13, DSDerivationPathReference_ProviderPlatformNodeKeys = 14, + DSDerivationPathReference_CoinJoin = 15, DSDerivationPathReference_Root = 255, }; diff --git a/DashSync/shared/Models/Derivation Paths/DSDerivationPath.m b/DashSync/shared/Models/Derivation Paths/DSDerivationPath.m index 4bf8267bf..d3a6caae8 100644 --- a/DashSync/shared/Models/Derivation Paths/DSDerivationPath.m +++ b/DashSync/shared/Models/Derivation Paths/DSDerivationPath.m @@ -358,7 +358,6 @@ - (NSSet *)allAddresses { return [self.mAllAddresses copy]; } - - (NSSet *)usedAddresses { return [self.mUsedAddresses copy]; } @@ -497,6 +496,9 @@ - (NSString *)referenceName { case DSDerivationPathReference_BlockchainIdentityCreditInvitationFunding: return @"BI Credit Invitation Funding"; break; + case DSDerivationPathReference_CoinJoin: + return @"CoinJoin"; + break; default: return @"Unknown"; break; diff --git a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.h b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.h index 6a134286b..a3eb354f3 100644 --- a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.h +++ b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.h @@ -10,6 +10,7 @@ #define SEQUENCE_GAP_LIMIT_EXTERNAL 10 #define SEQUENCE_GAP_LIMIT_INTERNAL 5 #define SEQUENCE_GAP_LIMIT_INITIAL 100 +#define SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN 400 #define SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL 10 #define SEQUENCE_UNUSED_GAP_LIMIT_INTERNAL 5 @@ -53,6 +54,8 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)bip44DerivationPathForAccountNumber:(uint32_t)accountNumber onChain:(DSChain *)chain; ++ (instancetype)coinJoinDerivationPathForAccountNumber:(uint32_t)accountNumber onChain:(DSChain *)chain; + // Derivation paths are composed of chains of addresses. Each chain is traversed until a gap of a certain number of addresses is // found that haven't been used in any transactions. This method returns an array of unused addresses // following the last used address in the chain. The internal chain is used for change addresses and the external chain diff --git a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m index 1e8493331..c3960a44c 100644 --- a/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m +++ b/DashSync/shared/Models/Derivation Paths/DSFundsDerivationPath.m @@ -37,6 +37,12 @@ + (instancetype _Nonnull)bip44DerivationPathForAccountNumber:(uint32_t)accountNu BOOL hardenedIndexes[] = {YES, YES, YES}; return [self derivationPathWithIndexes:indexes hardened:hardenedIndexes length:3 type:DSDerivationPathType_ClearFunds signingAlgorithm:KeyKind_ECDSA reference:DSDerivationPathReference_BIP44 onChain:chain]; } ++ (instancetype _Nonnull)coinJoinDerivationPathForAccountNumber:(uint32_t)accountNumber onChain:(DSChain *)chain { + UInt256 indexes[] = {uint256_from_long(FEATURE_PURPOSE), uint256_from_long((uint64_t) chain_coin_type(chain.chainType)), uint256_from_long(FEATURE_PURPOSE_COINJOIN), uint256_from_long(accountNumber)}; + BOOL hardenedIndexes[] = {YES, YES, YES, YES}; + return [self derivationPathWithIndexes:indexes hardened:hardenedIndexes length:4 type:DSDerivationPathType_AnonymousFunds signingAlgorithm:KeyKind_ECDSA reference:DSDerivationPathReference_CoinJoin onChain:chain]; +} + - (instancetype)initWithIndexes:(const UInt256[])indexes hardened:(const BOOL[])hardenedIndexes length:(NSUInteger)length type:(DSDerivationPathType)type signingAlgorithm:(KeyKind)signingAlgorithm reference:(DSDerivationPathReference)reference onChain:(DSChain *)chain { if (!(self = [super initWithIndexes:indexes hardened:hardenedIndexes length:length type:type signingAlgorithm:signingAlgorithm reference:reference onChain:chain])) return nil; @@ -107,8 +113,18 @@ - (void)loadAddresses { } }]; self.addressesLoaded = TRUE; - [self registerAddressesWithGapLimit:(self.shouldUseReducedGapLimit ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_INITIAL) internal:YES error:nil]; - [self registerAddressesWithGapLimit:(self.shouldUseReducedGapLimit ? SEQUENCE_UNUSED_GAP_LIMIT_INITIAL : SEQUENCE_GAP_LIMIT_INITIAL) internal:NO error:nil]; + NSUInteger gapLimit = 0; + + if (self.shouldUseReducedGapLimit) { + gapLimit = SEQUENCE_UNUSED_GAP_LIMIT_INITIAL; + } else if (self.type == DSDerivationPathType_AnonymousFunds) { + gapLimit = SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN; + } else { + gapLimit = SEQUENCE_GAP_LIMIT_INITIAL; + } + + [self registerAddressesWithGapLimit:gapLimit internal:YES error:nil]; + [self registerAddressesWithGapLimit:gapLimit internal:NO error:nil]; } } @@ -134,9 +150,10 @@ - (BOOL)registerTransactionAddress:(NSString *_Nonnull)address { // following the last used address in the chain. The internal chain is used for change addresses and the external chain // for receive addresses. - (NSArray *)registerAddressesWithGapLimit:(NSUInteger)gapLimit internal:(BOOL)internal error:(NSError **)error { - if (!self.account.wallet.isTransient) { - NSAssert(self.addressesLoaded, @"addresses must be loaded before calling this function"); + if (!self.account.wallet.isTransient && !self.addressesLoaded) { + return @[]; } + @synchronized(self) { NSMutableArray *a = [NSMutableArray arrayWithArray:(internal) ? self.internalAddresses : self.externalAddresses]; NSUInteger i = a.count; diff --git a/DashSync/shared/Models/Entities/DSFriendRequestEntity+CoreDataClass.m b/DashSync/shared/Models/Entities/DSFriendRequestEntity+CoreDataClass.m index 0975c5589..794c36d13 100644 --- a/DashSync/shared/Models/Entities/DSFriendRequestEntity+CoreDataClass.m +++ b/DashSync/shared/Models/Entities/DSFriendRequestEntity+CoreDataClass.m @@ -88,6 +88,7 @@ - (void)sendAmount:(uint64_t)amount fromAccount:(DSAccount *)account requestingA NSAssert([paymentRequest isValidAsNonDashpayPaymentRequest], @"Payment request must be valid"); + // TODO: MOCK_DASHPAY mixed only? [account.wallet.chain.chainManager.transactionManager confirmPaymentRequest:paymentRequest usingUserBlockchainIdentity:nil fromAccount:account diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m index de71bac40..982cd11ce 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSKeyManager.m @@ -192,10 +192,13 @@ + (UInt160)ecdsaKeyPublicKeyHashFromSecret:(NSString *)secret forChainType:(Chai } + (NSString *_Nullable)ecdsaKeyAddressFromPublicKeyData:(NSData *)data forChainType:(ChainType)chainType { + if (!data || data.length == 0) { + return nil; + } + return [DSKeyManager NSStringFrom:ecdsa_address_from_public_key_data(data.bytes, data.length, chainType)]; } - - (NSString *)ecdsaKeyPublicKeyUniqueIDFromDerivedKeyData:(UInt256)secret forChainType:(ChainType)chainType { uint64_t unque_id = ecdsa_public_key_unique_id_from_derived_key_data(secret.u8, 32, chainType); return [NSString stringWithFormat:@"%0llx", unque_id]; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h index 6259fb70b..c769d0da8 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.h @@ -68,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN - (DSSimplifiedMasternodeEntry *)masternodeHavingProviderRegistrationTransactionHash:(NSData *)providerRegistrationTransactionHash; +- (DSSimplifiedMasternodeEntry *)masternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port; - (BOOL)hasMasternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port; - (DSQuorumEntry *_Nullable)quorumEntryForInstantSendRequestID:(UInt256)requestID withBlockHeightOffset:(uint32_t)blockHeightOffset; diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m index 92cf5bd40..388bbf627 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSMasternodeManager.m @@ -162,12 +162,16 @@ - (NSUInteger)activeQuorumsCount { } - (BOOL)hasMasternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port { + return [self masternodeAtLocation:IPAddress port:port] != nil; +} + +- (DSSimplifiedMasternodeEntry *)masternodeAtLocation:(UInt128)IPAddress port:(uint32_t)port { for (DSSimplifiedMasternodeEntry *simplifiedMasternodeEntry in [self.currentMasternodeList.simplifiedMasternodeListDictionaryByReversedRegistrationTransactionHash allValues]) { if (uint128_eq(simplifiedMasternodeEntry.address, IPAddress) && simplifiedMasternodeEntry.port == port) { - return YES; + return simplifiedMasternodeEntry; } } - return NO; + return nil; } - (NSUInteger)masternodeListRetrievalQueueCount { diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.h index 174409623..9064eb275 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.h @@ -75,7 +75,9 @@ typedef NS_ENUM(uint16_t, DSDisconnectReason) @property (nonatomic, readonly) NSArray *registeredDevnetPeers; @property (nonatomic, readonly) NSArray *registeredDevnetPeerServices; @property (nullable, nonatomic, readonly) NSString *trustedPeerHost; +@property (nonatomic, readonly) BOOL shouldSendDsq; +- (DSPeer *)peerForLocation:(UInt128)IPAddress port:(uint16_t)port; - (DSPeerStatus)statusForLocation:(UInt128)IPAddress port:(uint32_t)port; - (DSPeerType)typeForLocation:(UInt128)IPAddress port:(uint32_t)port; - (void)setTrustedPeerHost:(NSString *_Nullable)host; @@ -91,6 +93,10 @@ typedef NS_ENUM(uint16_t, DSDisconnectReason) - (void)sendRequest:(DSMessageRequest *)request; +// MARK: CoinJoin + +- (void)shouldSendDsq:(BOOL)shouldSendDsq; + @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m index cdfb882d0..092fe6c85 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSPeerManager.m @@ -57,6 +57,7 @@ #import "NSError+Dash.h" #import "NSManagedObject+Sugar.h" #import "NSString+Bitcoin.h" +#import "DSSendCoinJoinQueue.h" #import #import @@ -696,7 +697,10 @@ - (void)connect { DSLog(@"[%@] [DSPeerManager] connect", self.chain.name); self.desiredState = DSPeerManagerDesiredState_Connected; dispatch_async(self.networkingQueue, ^{ - if ([self.chain syncsBlockchain] && ![self.chain canConstructAFilter]) return; // check to make sure the wallet has been created if only are a basic wallet with no dash features + 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 + } if (self.connectFailures >= MAX_CONNECT_FAILURES) self.connectFailures = 0; // this attempt is a manual retry @synchronized (self.chainManager) { @@ -880,6 +884,11 @@ - (void)peerConnected:(DSPeer *)peer { if (!self.masternodeList) { [peer sendGetaddrMessage]; // request a list of other dash peers } + + if (self.shouldSendDsq) { + [peer sendRequest:[DSSendCoinJoinQueue requestWithShouldSend:true]]; + } + dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification object:nil @@ -1060,5 +1069,14 @@ - (void)sendRequest:(DSMessageRequest *)request { [self.downloadPeer sendRequest:request]; } +// MARK: CoinJoin + +- (void)shouldSendDsq:(BOOL)shouldSendDsq { + for (DSPeer *peer in self.connectedPeers) { + DSSendCoinJoinQueue *request = [DSSendCoinJoinQueue requestWithShouldSend:shouldSendDsq]; + [peer sendRequest:request]; + } + _shouldSendDsq = shouldSendDsq; +} @end diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.h b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.h index 886cda483..8912d57fa 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.h +++ b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.h @@ -86,7 +86,7 @@ typedef void (^DSTransactionRequestRelayCompletionBlock)(DSTransaction *tx, DSPa publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock; -- (void)signAndPublishTransaction:(DSTransaction *)tx createdFromProtocolRequest:(DSPaymentProtocolRequest *)protocolRequest fromAccount:(DSAccount *)account toAddress:(NSString *)address requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingConfirmationPrompt promptMessage:(NSString *_Nullable)promptMessage forAmount:(uint64_t)amount keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge transactionCreationCompletion:(DSTransactionCreationCompletionBlock)transactionCreationCompletion signedCompletion:(DSTransactionSigningCompletionBlock)signedCompletion publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion requestRelayCompletion:(DSTransactionRequestRelayCompletionBlock _Nullable)requestRelayCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock; +- (void)signAndPublishTransaction:(DSTransaction *)tx createdFromProtocolRequest:(DSPaymentProtocolRequest *)protocolRequest fromAccount:(DSAccount *)account toAddress:(NSString *)address requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingConfirmationPrompt promptMessage:(NSString *_Nullable)promptMessage forAmount:(uint64_t)amount keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication mixedOnly:(BOOL)mixedOnly requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge transactionCreationCompletion:(DSTransactionCreationCompletionBlock)transactionCreationCompletion signedCompletion:(DSTransactionSigningCompletionBlock)signedCompletion publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion requestRelayCompletion:(DSTransactionRequestRelayCompletionBlock _Nullable)requestRelayCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock; - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(uint64_t)requestedAmount fromAccount:(DSAccount *)account addressIsFromPasteboard:(BOOL)addressIsFromPasteboard requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication @@ -98,7 +98,7 @@ typedef void (^DSTransactionRequestRelayCompletionBlock)(DSTransaction *tx, DSPa requestRelayCompletion:(DSTransactionRequestRelayCompletionBlock _Nullable)requestRelayCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock; -- (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(uint64_t)requestedAmount fromAccount:(DSAccount *)account acceptInternalAddress:(BOOL)acceptInternalAddress acceptReusingAddress:(BOOL)acceptReusingAddress addressIsFromPasteboard:(BOOL)addressIsFromPasteboard acceptUncertifiedPayee:(BOOL)acceptUncertifiedPayee requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt +- (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(uint64_t)requestedAmount fromAccount:(DSAccount *)account acceptInternalAddress:(BOOL)acceptInternalAddress acceptReusingAddress:(BOOL)acceptReusingAddress addressIsFromPasteboard:(BOOL)addressIsFromPasteboard acceptUncertifiedPayee:(BOOL)acceptUncertifiedPayee mixedOnly:(BOOL)mixedOnly requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge diff --git a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m index 9c2cd630a..da0e5a76d 100644 --- a/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m +++ b/DashSync/shared/Models/Managers/Chain Managers/DSTransactionManager.m @@ -53,6 +53,7 @@ #import "DSTransactionHashEntity+CoreDataClass.h" #import "DSTransactionInput.h" #import "DSTransition.h" +#import "DSCoinJoinManager.h" #import "DSWallet+Protected.h" #import "NSData+Dash.h" #import "NSDate+Utils.h" @@ -212,6 +213,13 @@ - (void)publishTransaction:(DSTransaction *)transaction completion:(void (^)(NSE dispatch_async(self.chainManager.chain.networkingQueue, ^{ [self performSelector:@selector(txTimeout:) withObject:hash afterDelay:PROTOCOL_TIMEOUT]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification + object:nil + userInfo:@{DSChainManagerNotificationChainKey: self.chain, + DSTransactionManagerNotificationTransactionKey: transaction}]; + }); for (DSPeer *p in peers) { if (p.status != DSPeerStatus_Connected) continue; @@ -279,7 +287,11 @@ - (void)removeUnrelayedTransactionsFromPeer:(DSPeer *)peer { for (DSTransaction *transaction in transactionsSet) { if (transaction.blockHeight != TX_UNCONFIRMED) continue; hash = uint256_obj(transaction.txHash); +#if DEBUG + DSLogPrivate(@"[%@] checking published callback %@ -> %@", self.chain.name, uint256_reverse_hex(transaction.txHash), self.publishedCallback[hash] ? @"OK" : @"no callback"); +#else DSLog(@"[%@] checking published callback -> %@", self.chain.name, self.publishedCallback[hash] ? @"OK" : @"no callback"); +#endif if (self.publishedCallback[hash] != NULL) continue; DSLog(@"[%@] transaction relays count %lu, transaction requests count %lu", self.chain.name, (unsigned long)[self.txRelays[hash] count], (unsigned long)[self.txRequests[hash] count]); DSAccount *account = [self.chain firstAccountThatCanContainTransaction:transaction]; @@ -291,6 +303,9 @@ - (void)removeUnrelayedTransactionsFromPeer:(DSPeer *)peer { NSAssert(FALSE, @"This probably needs more implementation work, if you are here now is the time to do it."); continue; } + + BOOL updateTransaction = NO; + if ([self.txRelays[hash] count] == 0 && [self.txRequests[hash] count] == 0) { // if this is for a transaction we sent, and it wasn't already known to be invalid, notify user of failure if (!rescan && account && [account amountSentByTransaction:transaction] > 0 && [account transactionIsValid:transaction]) { @@ -325,7 +340,7 @@ - (void)removeUnrelayedTransactionsFromPeer:(DSPeer *)peer { DSLog(@"[%@] removing transaction ", self.chain.name); #endif [transactionsToBeRemoved addObject:transaction]; - + updateTransaction = YES; } else if ([self.txRelays[hash] count] < self.peerManager.maxConnectCount) { // set timestamp 0 to mark as unverified #if DEBUG @@ -336,6 +351,17 @@ - (void)removeUnrelayedTransactionsFromPeer:(DSPeer *)peer { [self.chain setBlockHeight:TX_UNCONFIRMED andTimestamp:0 forTransactionHashes:@[hash]]; + updateTransaction = YES; + } + + if (updateTransaction) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification + object:nil + userInfo:@{DSChainManagerNotificationChainKey: self.chain, + DSTransactionManagerNotificationTransactionKey: transaction, + DSTransactionManagerNotificationTransactionChangesKey: @{DSTransactionManagerNotificationTransactionAcceptedStatusKey: @(NO)}}]; + }); } } @@ -447,6 +473,7 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u acceptReusingAddress:NO addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:NO + mixedOnly:NO requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest @@ -458,7 +485,7 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u errorNotificationBlock:errorNotificationBlock]; } -- (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(uint64_t)requestedAmount fromAccount:(DSAccount *)account acceptInternalAddress:(BOOL)acceptInternalAddress acceptReusingAddress:(BOOL)acceptReusingAddress addressIsFromPasteboard:(BOOL)addressIsFromPasteboard acceptUncertifiedPayee:(BOOL)acceptUncertifiedPayee +- (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(uint64_t)requestedAmount fromAccount:(DSAccount *)account acceptInternalAddress:(BOOL)acceptInternalAddress acceptReusingAddress:(BOOL)acceptReusingAddress addressIsFromPasteboard:(BOOL)addressIsFromPasteboard acceptUncertifiedPayee:(BOOL)acceptUncertifiedPayee mixedOnly:(BOOL)mixedOnly requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest @@ -506,7 +533,7 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u NSString *challengeAction = DSLocalizedString(@"Ignore", nil); challenge( challengeTitle, challengeMessage, challengeAction, ^{ - [self confirmProtocolRequest:protoReq forAmount:requestedAmount fromAccount:account acceptInternalAddress:YES acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:acceptUncertifiedPayee requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; + [self confirmProtocolRequest:protoReq forAmount:requestedAmount fromAccount:account acceptInternalAddress:YES acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:acceptUncertifiedPayee mixedOnly:mixedOnly requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; }, ^{ additionalInfoRequest(DSRequestingAdditionalInfo_CancelOrChangeAmount); @@ -528,6 +555,7 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u acceptReusingAddress:YES addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:acceptUncertifiedPayee + mixedOnly:mixedOnly requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest @@ -549,7 +577,7 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u NSString *challengeAction = DSLocalizedString(@"Ignore", nil); challenge( challengeTitle, challengeMessage, challengeAction, ^{ - [self confirmProtocolRequest:protoReq forAmount:requestedAmount fromAccount:account acceptInternalAddress:acceptInternalAddress acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:YES requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; + [self confirmProtocolRequest:protoReq forAmount:requestedAmount fromAccount:account acceptInternalAddress:acceptInternalAddress acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee:YES mixedOnly:mixedOnly requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; }, ^{ additionalInfoRequest(DSRequestingAdditionalInfo_CancelOrChangeAmount); @@ -582,15 +610,23 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u errorNotificationBlock(error, errorTitle, errorMessage, YES); return; } + + DSCoinControl *coinControl = nil; + + if (mixedOnly) { + coinControl = [[DSCoinJoinManager sharedInstanceForChain:self.chain] selectCoinJoinUTXOs]; + } if (requestedAmount == 0) { tx = [account transactionForAmounts:protoReq.details.outputAmounts toOutputScripts:protoReq.details.outputScripts - withFee:YES]; + withFee:YES + coinControl:coinControl]; } else if (amount <= account.balance) { tx = [account transactionForAmounts:@[@(requestedAmount)] toOutputScripts:@[protoReq.details.outputScripts.firstObject] - withFee:YES]; + withFee:YES + coinControl:coinControl]; } if (tx) { @@ -629,19 +665,19 @@ - (void)confirmProtocolRequest:(DSPaymentProtocolRequest *)protoReq forAmount:(u if (transactionCreationCompletion(tx, suggestedPrompt, amount, fee, address ? @[address] : @[], isSecure)) { CFRunLoopPerformBlock([[NSRunLoop mainRunLoop] getCFRunLoop], kCFRunLoopCommonModes, ^{ - [self signAndPublishTransaction:tx createdFromProtocolRequest:protoReq fromAccount:account toAddress:address requiresSpendingAuthenticationPrompt:YES promptMessage:suggestedPrompt forAmount:amount keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; + [self signAndPublishTransaction:tx createdFromProtocolRequest:protoReq fromAccount:account toAddress:address requiresSpendingAuthenticationPrompt:YES promptMessage:suggestedPrompt forAmount:amount keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication mixedOnly:mixedOnly requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; }); } } -- (void)signAndPublishTransaction:(DSTransaction *)tx createdFromProtocolRequest:(DSPaymentProtocolRequest *)protocolRequest fromAccount:(DSAccount *)account toAddress:(NSString *)address requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt promptMessage:(NSString *)promptMessage forAmount:(uint64_t)amount keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge transactionCreationCompletion:(DSTransactionCreationCompletionBlock)transactionCreationCompletion signedCompletion:(DSTransactionSigningCompletionBlock)signedCompletion publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion requestRelayCompletion:(DSTransactionRequestRelayCompletionBlock)requestRelayCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock { +- (void)signAndPublishTransaction:(DSTransaction *)tx createdFromProtocolRequest:(DSPaymentProtocolRequest *)protocolRequest fromAccount:(DSAccount *)account toAddress:(NSString *)address requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt promptMessage:(NSString *)promptMessage forAmount:(uint64_t)amount keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication mixedOnly:(BOOL)mixedOnly requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge transactionCreationCompletion:(DSTransactionCreationCompletionBlock)transactionCreationCompletion signedCompletion:(DSTransactionSigningCompletionBlock)signedCompletion publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion requestRelayCompletion:(DSTransactionRequestRelayCompletionBlock)requestRelayCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock { DSAuthenticationManager *authenticationManager = [DSAuthenticationManager sharedInstance]; __block BOOL previouslyWasAuthenticated = authenticationManager.didAuthenticate; if (!tx) { // tx is nil if there were insufficient wallet funds if (authenticationManager.didAuthenticate) { //the fee puts us over the limit - [self insufficientFundsForTransactionCreatedFromProtocolRequest:protocolRequest fromAccount:account forAmount:amount toAddress:address requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; + [self insufficientFundsForTransactionCreatedFromProtocolRequest:protocolRequest fromAccount:account forAmount:amount toAddress:address requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication mixedOnly:mixedOnly requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; } else { [authenticationManager seedWithPrompt:promptMessage forWallet:account.wallet @@ -650,7 +686,7 @@ - (void)signAndPublishTransaction:(DSTransaction *)tx createdFromProtocolRequest completion:^(NSData *_Nullable seed, BOOL cancelled) { if (seed) { //the fee puts us over the limit - [self insufficientFundsForTransactionCreatedFromProtocolRequest:protocolRequest fromAccount:account forAmount:amount toAddress:address requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; + [self insufficientFundsForTransactionCreatedFromProtocolRequest:protocolRequest fromAccount:account forAmount:amount toAddress:address requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication mixedOnly:mixedOnly requestingAdditionalInfo:additionalInfoRequest presentChallenge:challenge transactionCreationCompletion:transactionCreationCompletion signedCompletion:signedCompletion publishedCompletion:publishedCompletion requestRelayCompletion:requestRelayCompletion errorNotificationBlock:errorNotificationBlock]; } else { additionalInfoRequest(DSRequestingAdditionalInfo_CancelOrChangeAmount); } @@ -800,6 +836,7 @@ - (void)publishSignedTransaction:(DSTransaction *)tx createdFromProtocolRequest: - (void)insufficientFundsForTransactionCreatedFromProtocolRequest:(DSPaymentProtocolRequest *)protocolRequest fromAccount:(DSAccount *)account forAmount:(uint64_t)requestedSendAmount toAddress:(NSString *)address requiresSpendingAuthenticationPrompt:(BOOL)requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:(BOOL)keepAuthenticatedIfErrorAfterAuthentication + mixedOnly:(BOOL)mixedOnly requestingAdditionalInfo:(DSTransactionCreationRequestingAdditionalInfoBlock)additionalInfoRequest presentChallenge:(DSTransactionChallengeBlock)challenge transactionCreationCompletion:(DSTransactionCreationCompletionBlock)transactionCreationCompletion @@ -834,6 +871,7 @@ - (void)insufficientFundsForTransactionCreatedFromProtocolRequest:(DSPaymentProt acceptReusingAddress:YES addressIsFromPasteboard:NO acceptUncertifiedPayee:YES + mixedOnly:mixedOnly requiresSpendingAuthenticationPrompt:requiresSpendingAuthenticationPrompt keepAuthenticatedIfErrorAfterAuthentication:keepAuthenticatedIfErrorAfterAuthentication requestingAdditionalInfo:additionalInfoRequest @@ -873,7 +911,7 @@ - (void)confirmPaymentRequest:(DSPaymentRequest *)paymentRequest usingUserBlockc publishedCompletion:(DSTransactionPublishedCompletionBlock)publishedCompletion errorNotificationBlock:(DSTransactionErrorNotificationBlock)errorNotificationBlock { DSPaymentProtocolRequest *protocolRequest = [paymentRequest protocolRequestForBlockchainIdentity:blockchainIdentity onAccount:account inContext:[NSManagedObjectContext viewContext]]; - [self confirmProtocolRequest:protocolRequest forAmount:paymentRequest.amount fromAccount:account acceptInternalAddress:acceptInternalAddress acceptReusingAddress:acceptReusingAddress addressIsFromPasteboard:addressIsFromPasteboard acceptUncertifiedPayee: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 @@ -1000,8 +1038,8 @@ - (void)updateTransactionsBloomFilter { // 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 registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_EXTERNAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING internal:NO error:nil]; - [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INTERNAL unusedAccountGapLimit:SEQUENCE_GAP_LIMIT_INTERNAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING internal:YES error:nil]; + [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_EXTERNAL unusedAccountGapLimit:SEQUENCE_UNUSED_GAP_LIMIT_EXTERNAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING coinJoinGapLimit:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN internal:NO error:nil]; + [wallet registerAddressesWithGapLimit:SEQUENCE_GAP_LIMIT_INTERNAL unusedAccountGapLimit:SEQUENCE_GAP_LIMIT_INTERNAL dashpayGapLimit:SEQUENCE_DASHPAY_GAP_LIMIT_INCOMING coinJoinGapLimit:SEQUENCE_GAP_LIMIT_INITIAL_COINJOIN internal:YES error:nil]; NSSet *addresses = [wallet.allReceiveAddresses setByAddingObjectsFromSet:wallet.allChangeAddresses]; [allAddressesArray addObjectsFromArray:[addresses allObjects]]; @@ -1100,6 +1138,14 @@ - (DSTransaction *)peer:(DSPeer *)peer requestedTransaction:(UInt256)txHash { if (callback && !isTransactionValid) { [self.publishedTx removeObjectForKey:hash]; error = [NSError errorWithCode:401 localizedDescriptionKey:@"Double spend"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSTransactionManagerTransactionStatusDidChangeNotification + object:nil + userInfo:@{DSChainManagerNotificationChainKey: self.chain, + DSTransactionManagerNotificationTransactionKey: transaction, + DSTransactionManagerNotificationTransactionChangesKey: @{DSTransactionManagerNotificationTransactionAcceptedStatusKey: @(NO)}}]; + }); } else if (transaction) { for (DSAccount *account in accounts) { if (![account transactionForHash:txHash]) { @@ -1736,7 +1782,17 @@ - (void)checkChainLocksWaitingForQuorums { DSMerkleBlock *block = [self.chain blockForBlockHash:chainLock.blockHash]; [self.chainLocksWaitingForQuorums removeObjectForKey:chainLockHashData]; dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:DSChainBlockWasLockedNotification object:nil userInfo:@{DSChainManagerNotificationChainKey: self.chain, DSChainNotificationBlockKey: block}]; + if (self.chain && block) { + NSDictionary *userInfo = @{ + DSChainManagerNotificationChainKey: self.chain, + DSChainNotificationBlockKey: block + }; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:DSChainBlockWasLockedNotification object:nil userInfo:userInfo]; + }); + } else { + DSLog(@"Warning: Unable to post notification due to nil chain or block (%s : %s)", self.chain == nil ? "nil" : "valid", block == nil ? "nil" : "valid"); + } }); } else { #if DEBUG_CHAIN_LOCKS_WAITING_FOR_QUORUMS diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.h b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.h new file mode 100644 index 000000000..61ae08762 --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.h @@ -0,0 +1,32 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSMessageRequest.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DSCoinJoinAcceptMessage : DSMessageRequest + +@property (nonatomic, readonly) NSData *data; + ++ (instancetype)requestWithData:(NSData *)data; ++ (NSString *)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.m b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.m new file mode 100644 index 000000000..f58f2f30b --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinAcceptMessage.m @@ -0,0 +1,47 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +#import "DSCoinJoinAcceptMessage.h" +#import "DSPeer.h" + +@implementation DSCoinJoinAcceptMessage + ++ (instancetype)requestWithData:(NSData *)data { + return [[DSCoinJoinAcceptMessage alloc] initWithData:data]; +} + ++ (NSString *)type { + return MSG_COINJOIN_ACCEPT; +} + +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self) { + _data = data; + } + return self; +} + +- (NSString *)type { + return DSCoinJoinAcceptMessage.type; +} + +- (NSData *)toData { + return self.data; +} +@end diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.h b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.h new file mode 100644 index 000000000..883ef01c1 --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.h @@ -0,0 +1,32 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSMessageRequest.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DSCoinJoinEntryMessage : DSMessageRequest + +@property (nonatomic, readonly) NSData *data; + ++ (instancetype)requestWithData:(NSData *)data; ++ (NSString *)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.m b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.m new file mode 100644 index 000000000..a5394b25d --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinEntryMessage.m @@ -0,0 +1,46 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSCoinJoinEntryMessage.h" +#import "DSPeer.h" + +@implementation DSCoinJoinEntryMessage + ++ (instancetype)requestWithData:(NSData *)data { + return [[DSCoinJoinEntryMessage alloc] initWithData:data]; +} + ++ (NSString *)type { + return MSG_COINJOIN_ENTRY; +} + +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self) { + _data = data; + } + return self; +} + +- (NSString *)type { + return DSCoinJoinEntryMessage.type; +} + +- (NSData *)toData { + return self.data; +} +@end diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.h b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.h new file mode 100644 index 000000000..24b22b09b --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.h @@ -0,0 +1,32 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSMessageRequest.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DSCoinJoinSignedInputs : DSMessageRequest + +@property (nonatomic, readonly) NSData *data; + ++ (instancetype)requestWithData:(NSData *)data; ++ (NSString *)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.m b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.m new file mode 100644 index 000000000..2a6e2bb83 --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSCoinJoinSignedInputs.m @@ -0,0 +1,47 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +#import "DSCoinJoinSignedInputs.h" +#import "DSPeer.h" + +@implementation DSCoinJoinSignedInputs + ++ (instancetype)requestWithData:(NSData *)data { + return [[DSCoinJoinSignedInputs alloc] initWithData:data]; +} + ++ (NSString *)type { + return MSG_COINJOIN_SIGNED_INPUTS; +} + +- (instancetype)initWithData:(NSData *)data { + self = [super init]; + if (self) { + _data = data; + } + return self; +} + +- (NSString *)type { + return DSCoinJoinSignedInputs.type; +} + +- (NSData *)toData { + return self.data; +} +@end diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.h b/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.h new file mode 100644 index 000000000..af37f22e3 --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.h @@ -0,0 +1,30 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSMessageRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DSSendCoinJoinQueue : DSMessageRequest + +@property (nonatomic, readonly) BOOL send; + ++ (instancetype)requestWithShouldSend:(BOOL)shouldSend; + +@end + +NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.m b/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.m new file mode 100644 index 000000000..02a09fe32 --- /dev/null +++ b/DashSync/shared/Models/Messages/CoinJoin/DSSendCoinJoinQueue.m @@ -0,0 +1,39 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "DSSendCoinJoinQueue.h" +#import "DSPeer.h" +#import "NSData+Dash.h" + +@implementation DSSendCoinJoinQueue + ++ (instancetype)requestWithShouldSend:(BOOL)shouldSend { + return [[DSSendCoinJoinQueue alloc] initWithShouldSend:shouldSend]; +} + +- (instancetype)initWithShouldSend:(BOOL)shouldSend { + self = [super initWithType:MSG_SENDDSQ]; + if (self) { + _send = shouldSend; + } + return self; +} + +- (NSData *)toData { + return [NSData dataWithUInt8:(uint8_t)_send]; +} +@end diff --git a/DashSync/shared/Models/Network/DSPeer.h b/DashSync/shared/Models/Network/DSPeer.h index db8421e13..e487926d9 100644 --- a/DashSync/shared/Models/Network/DSPeer.h +++ b/DashSync/shared/Models/Network/DSPeer.h @@ -137,16 +137,16 @@ typedef NS_ENUM(uint32_t, DSInvType) #define MSG_GOVOBJVOTE @"govobjvote" #define MSG_GOVOBJSYNC @"govsync" -//Private send - -#define MSG_DARKSENDANNOUNCE @"dsa" -#define MSG_DARKSENDCONTROL @"dsc" -#define MSG_DARKSENDFINISH @"dsf" -#define MSG_DARKSENDINITIATE @"dsi" -#define MSG_DARKSENDQUORUM @"dsq" -#define MSG_DARKSENDSESSION @"dss" -#define MSG_DARKSENDSESSIONUPDATE @"dssu" -#define MSG_DARKSENDTX @"dstx" +// CoinJoin + +#define MSG_COINJOIN_ACCEPT @"dsa" +#define MSG_COINJOIN_ENTRY @"dsi" +#define MSG_COINJOIN_QUEUE @"dsq" +#define MSG_COINJOIN_BROADCAST_TX @"dstx" +#define MSG_COINJOIN_STATUS_UPDATE @"dssu" +#define MSG_COINJOIN_COMPLETE @"dsc" +#define MSG_COINJOIN_FINAL_TRANSACTION @"dsf" +#define MSG_COINJOIN_SIGNED_INPUTS @"dss" #define REJECT_INVALID 0x10 // transaction is invalid for some reason (invalid signature, output value > input, etc) #define REJECT_SPENT 0x12 // an input is already spent diff --git a/DashSync/shared/Models/Network/DSPeer.m b/DashSync/shared/Models/Network/DSPeer.m index 5a3cadb03..7db959267 100644 --- a/DashSync/shared/Models/Network/DSPeer.m +++ b/DashSync/shared/Models/Network/DSPeer.m @@ -64,6 +64,7 @@ #import "DSTransactionHashEntity+CoreDataClass.h" #import "DSTransactionInvRequest.h" #import "DSVersionRequest.h" +#import "DSCoinJoinManager.h" #import "NSData+DSHash.h" #import "NSData+Dash.h" #import "NSDate+Utils.h" @@ -377,7 +378,7 @@ - (void)receivedOrphanBlock { - (void)sendRequest:(DSMessageRequest *)request { NSString *type = [request type]; NSData *payload = [request toData]; - //DSLog(@"%@:%u sendRequest: [%@]: %@", self.host, self.port, type, [payload hexString]); +// DSLog(@"%@:%u sendRequest: [%@]: %@", self.host, self.port, type, [payload hexString]); [self sendMessage:payload type:type]; } @@ -843,23 +844,17 @@ - (void)acceptMessage:(NSData *)message type:(NSString *)type { [self acceptGovObjectMessage:message]; //else if ([MSG_GOVOBJSYNC isEqual:type]) [self acceptGovObjectSyncMessage:message]; - //private send - else if ([MSG_DARKSENDANNOUNCE isEqual:type]) - [self acceptDarksendAnnounceMessage:message]; - else if ([MSG_DARKSENDCONTROL isEqual:type]) - [self acceptDarksendControlMessage:message]; - else if ([MSG_DARKSENDFINISH isEqual:type]) - [self acceptDarksendFinishMessage:message]; - else if ([MSG_DARKSENDINITIATE isEqual:type]) - [self acceptDarksendInitiateMessage:message]; - else if ([MSG_DARKSENDQUORUM isEqual:type]) - [self acceptDarksendQuorumMessage:message]; - else if ([MSG_DARKSENDSESSION isEqual:type]) - [self acceptDarksendSessionMessage:message]; - else if ([MSG_DARKSENDSESSIONUPDATE isEqual:type]) - [self acceptDarksendSessionUpdateMessage:message]; - else if ([MSG_DARKSENDTX isEqual:type]) - [self acceptDarksendTransactionMessage:message]; + // CoinJoin + else if ([MSG_COINJOIN_COMPLETE isEqual:type]) + [self acceptCoinJoinCompleteMessage:message]; + else if ([MSG_COINJOIN_FINAL_TRANSACTION isEqual:type]) + [self acceptCoinJoinFinalTransaction:message]; + else if ([MSG_COINJOIN_QUEUE isEqual:type]) + [self acceptCoinJoinQueueMessage:message]; + else if ([MSG_COINJOIN_STATUS_UPDATE isEqual:type]) + [self acceptCoinJoinStatusUpdateMessage:message]; + else if ([MSG_COINJOIN_BROADCAST_TX isEqual:type]) + [self acceptCoinJoinBroadcastTxMessage:message]; #if DROP_MESSAGE_LOGGING else { DSLogWithLocation(self, @"dropping %@, len:%u, not implemented", type, message.length); @@ -1045,7 +1040,7 @@ - (void)acceptInvMessage:(NSData *)message { if (count == 0) { DSLogWithLocation(self, @"Got empty Inv message"); } - if (count > 0 && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodePing) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodePaymentVote) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodeVerify) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_GovernanceObjectVote) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_DSTx)) { + if (count > 0 && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodePing) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodePaymentVote) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_MasternodeVerify) && ([message UInt32AtOffset:l.unsignedIntegerValue] != DSInvType_GovernanceObjectVote)) { DSLogWithLocation(self, @"got inv with %u item%@ (first item %@ with hash %@/%@)", (int)count, count == 1 ? @"" : @"s", [self nameOfInvMessage:[message UInt32AtOffset:l.unsignedIntegerValue]], [NSData dataWithUInt256:[message UInt256AtOffset:l.unsignedIntegerValue + sizeof(uint32_t)]].hexString, [NSData dataWithUInt256:[message UInt256AtOffset:l.unsignedIntegerValue + sizeof(uint32_t)]].reverse.hexString); } #endif @@ -1068,7 +1063,7 @@ - (void)acceptInvMessage:(NSData *)message { switch (type) { case DSInvType_Tx: [txHashes addObject:uint256_obj(hash)]; break; case DSInvType_TxLockRequest: [txHashes addObject:uint256_obj(hash)]; break; - case DSInvType_DSTx: break; + case DSInvType_DSTx: [txHashes addObject:uint256_obj(hash)]; break; case DSInvType_TxLockVote: break; case DSInvType_InstantSendDeterministicLock: [instantSendLockDHashes addObject:uint256_obj(hash)]; break; case DSInvType_InstantSendLock: [instantSendLockHashes addObject:uint256_obj(hash)]; break; @@ -1824,61 +1819,26 @@ - (void)acceptGovObjectSyncMessage:(NSData *)message { DSLogWithLocation(self, @"Gov Object Sync"); } -// MARK: - Accept Dark send +// MARK: - Accept CoinJoin messages -- (void)acceptDarksendAnnounceMessage:(NSData *)message { +- (void)acceptCoinJoinCompleteMessage:(NSData *)message { + [[DSCoinJoinManager sharedInstanceForChain:self.chain] processMessageFrom:self message:message type:MSG_COINJOIN_COMPLETE]; } -- (void)acceptDarksendControlMessage:(NSData *)message { +- (void)acceptCoinJoinFinalTransaction:(NSData *)message { + [[DSCoinJoinManager sharedInstanceForChain:self.chain] processMessageFrom:self message:message type:MSG_COINJOIN_FINAL_TRANSACTION]; } -- (void)acceptDarksendFinishMessage:(NSData *)message { +- (void)acceptCoinJoinQueueMessage:(NSData *)message { + [[DSCoinJoinManager sharedInstanceForChain:self.chain] processMessageFrom:self message:message type:MSG_COINJOIN_QUEUE]; } -- (void)acceptDarksendInitiateMessage:(NSData *)message { +- (void)acceptCoinJoinStatusUpdateMessage:(NSData *)message { + [[DSCoinJoinManager sharedInstanceForChain:self.chain] processMessageFrom:self message:message type:MSG_COINJOIN_STATUS_UPDATE]; } -- (void)acceptDarksendQuorumMessage:(NSData *)message { -} - -- (void)acceptDarksendSessionMessage:(NSData *)message { -} - -- (void)acceptDarksendSessionUpdateMessage:(NSData *)message { -} - -- (void)acceptDarksendTransactionMessage:(NSData *)message { - // DSTransaction *tx = [DSTransaction transactionWithMessage:message]; - // - // if (! tx) { - // [self error:@"malformed tx message: %@", message]; - // return; - // } - // else if (! self.sentFilter && ! self.sentTxAndBlockGetdata) { - // [self error:@"got tx message before loading a filter"]; - // return; - // } - // - // DSLogPrivate(@"%@:%u got tx %@", self.host, self.port, uint256_obj(tx.txHash)); - // - // dispatch_async(self.delegateQueue, ^{ - // [self.delegate peer:self relayedTransaction:tx]; - // }); - // - // if (self.currentBlock) { // we're collecting tx messages for a merkleblock - // [self.currentBlockTxHashes removeObject:uint256_obj(tx.txHash)]; - // - // if (self.currentBlockTxHashes.count == 0) { // we received the entire block including all matched tx - // BRMerkleBlock *block = self.currentBlock; - // - // self.currentBlock = nil; - // self.currentBlockTxHashes = nil; - // - // dispatch_sync(self.delegateQueue, ^{ // syncronous dispatch so we don't get too many queued up tx - // [self.delegate peer:self relayedBlock:block]; - // }); - // } - // } +- (void)acceptCoinJoinBroadcastTxMessage:(NSData *)message { + [[DSCoinJoinManager sharedInstanceForChain:self.chain] processMessageFrom:self message:message type:MSG_COINJOIN_BROADCAST_TX]; } // MARK: - hash diff --git a/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityCloseTransition.m b/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityCloseTransition.m index 63612fedb..10fb8a1ee 100644 --- a/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityCloseTransition.m +++ b/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityCloseTransition.m @@ -87,9 +87,9 @@ @implementation DSBlockchainIdentityCloseTransition // return data; //} // -//- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex +//- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay //{ -// NSMutableData * data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; +// NSMutableData * data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; // NSData * payloadData = [self payloadData]; // [data appendVarInt:payloadData.length]; // [data appendData:payloadData]; diff --git a/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityUpdateTransition.m b/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityUpdateTransition.m index 4701614c4..5dd4ccdbc 100644 --- a/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityUpdateTransition.m +++ b/DashSync/shared/Models/Platform/Transitions/BlockchainIdentity/DSBlockchainIdentityUpdateTransition.m @@ -165,9 +165,9 @@ @implementation DSBlockchainIdentityUpdateTransition //} // // -//- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex +//- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay //{ -// NSMutableData * data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; +// NSMutableData * data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; // NSData * payloadData = [self payloadData]; // [data appendVarInt:payloadData.length]; // [data appendData:payloadData]; diff --git a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m index 1ce845e2a..e9aca1356 100644 --- a/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSAssetLockTransaction.m @@ -19,6 +19,7 @@ #import "DSChain.h" #import "DSTransactionFactory.h" #import "NSData+Dash.h" +#import "NSMutableData+Dash.h" @implementation DSAssetLockTransaction @@ -58,4 +59,35 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { return self; } + +- (NSData *)payloadData { + return [self basePayloadData]; +} + +- (NSData *)basePayloadData { + NSMutableData *data = [NSMutableData data]; + [data appendUInt8:self.specialTransactionVersion]; + NSUInteger creditOutputsCount = self.creditOutputs.count; + [data appendVarInt:creditOutputsCount]; + for (NSUInteger i = 0; i < creditOutputsCount; i++) { + DSTransactionOutput *output = self.creditOutputs[i]; + [data appendUInt64:output.amount]; + [data appendCountedData:output.outScript]; + } + return data; +} + +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { + @synchronized(self) { + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; + [data appendCountedData:[self payloadData]]; + if (subscriptIndex != NSNotFound) [data appendUInt32:SIGHASH_ALL]; + return data; + } +} + +- (size_t)size { + return [super size] + [self payloadData].length; +} + @end diff --git a/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m b/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m index 01824db34..f6d8ee8fc 100644 --- a/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSAssetUnlockTransaction.m @@ -80,9 +80,9 @@ - (NSData *)basePayloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; [data appendCountedData:[self payloadData]]; if (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 66fc56c01..67fca128b 100644 --- a/DashSync/shared/Models/Transactions/Base/DSTransaction.h +++ b/DashSync/shared/Models/Transactions/Base/DSTransaction.h @@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN #define TX_LOCKTIME 0x00000000u #define TXIN_SEQUENCE UINT32_MAX #define SIGHASH_ALL 0x00000001u +#define SIGHASH_ANYONECANPAY 0x80u #define MAX_ECDSA_SIGNATURE_SIZE 75 @@ -89,7 +90,8 @@ typedef NS_ENUM(NSInteger, DSTransactionSortType) @property (nonatomic, assign) uint32_t lockTime; @property (nonatomic, assign) uint64_t feeUsed; @property (nonatomic, assign) uint64_t roundedFeeCostPerByte; -@property (nonatomic, readonly) uint64_t amountSent; +@property (nonatomic, readonly) DSTransactionDirection direction; +@property (nonatomic, readonly) uint64_t dashAmount; @property (nonatomic, readonly) NSData *payloadData; @property (nonatomic, readonly) NSData *payloadDataForHash; @property (nonatomic, assign) uint32_t payloadOffset; @@ -104,6 +106,7 @@ typedef NS_ENUM(NSInteger, DSTransactionSortType) @property (nonatomic, readonly) NSString *longDescription; @property (nonatomic, readonly) BOOL isCoinbaseClassicTransaction; +@property (nonatomic, readonly) BOOL isImmatureCoinBase; @property (nonatomic, readonly) BOOL isCreditFundingTransaction; @property (nonatomic, readonly) UInt256 creditBurnIdentityIdentifier; @@ -143,6 +146,7 @@ typedef NS_ENUM(NSInteger, DSTransactionSortType) - (void)hasSetInputsAndOutputs; - (BOOL)signWithSerializedPrivateKeys:(NSArray *)privateKeys; - (BOOL)signWithPrivateKeys:(NSArray *)keys; +- (BOOL)signWithPrivateKeys:(NSArray *)keys anyoneCanPay:(BOOL)anyoneCanPay; - (BOOL)signWithPreorderedPrivateKeys:(NSArray *)keys; - (NSString *_Nullable)shapeshiftOutboundAddress; @@ -152,7 +156,7 @@ typedef NS_ENUM(NSInteger, DSTransactionSortType) // 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; +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay; - (BOOL)hasNonDustOutputInWallet:(DSWallet *)wallet; @@ -170,10 +174,8 @@ typedef NS_ENUM(NSInteger, DSTransactionSortType) - (void)loadBlockchainIdentitiesFromDerivationPaths:(NSArray *)derivationPaths; -@end +- (int32_t)getBlocksToMaturity; -@interface DSTransaction (Extensions) -- (DSTransactionDirection)direction; @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Transactions/Base/DSTransaction.m b/DashSync/shared/Models/Transactions/Base/DSTransaction.m index 428853fb9..23480fe63 100644 --- a/DashSync/shared/Models/Transactions/Base/DSTransaction.m +++ b/DashSync/shared/Models/Transactions/Base/DSTransaction.m @@ -58,6 +58,8 @@ @interface DSTransaction () @property (nonatomic, strong) NSSet *destinationBlockchainIdentities; @property (nonatomic, strong) NSMutableArray *mInputs; @property (nonatomic, strong) NSMutableArray *mOutputs; +@property (nonatomic, assign) DSTransactionDirection cachedDirection; +@property (nonatomic, assign) uint64_t cachedDashAmount; @end @@ -85,7 +87,7 @@ - (instancetype)init { - (instancetype)initOnChain:(DSChain *)chain { if (!(self = [super init])) return nil; - + _version = TX_VERSION; self.mInputs = [NSMutableArray array]; self.mOutputs = [NSMutableArray array]; @@ -96,17 +98,19 @@ - (instancetype)initOnChain:(DSChain *)chain { self.blockHeight = TX_UNCONFIRMED; self.sourceBlockchainIdentities = [NSSet set]; self.destinationBlockchainIdentities = [NSSet set]; + self.cachedDirection = DSTransactionDirection_NotAccountFunds; + self.cachedDashAmount = UINT64_MAX; return self; } - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { if (!(self = [self initOnChain:chain])) return nil; - + NSString *address = nil; NSNumber *l = 0; uint32_t off = 0; uint64_t count = 0; - + @autoreleasepool { self.chain = chain; _version = [message UInt16AtOffset:off]; // tx version @@ -118,7 +122,7 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { return nil; // at least one input is required } off += l.unsignedIntegerValue; - + for (NSUInteger i = 0; i < count; i++) { // inputs UInt256 hash = [message UInt256AtOffset:off]; // input hash off += sizeof(UInt256); @@ -132,10 +136,10 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { DSTransactionInput *transactionInput = [DSTransactionInput transactionInputWithHash:hash index:index inScript:inScript signature:signature sequence:sequence]; [self.mInputs addObject:transactionInput]; } - + count = (NSUInteger)[message varIntAtOffset:off length:&l]; // output count off += l.unsignedIntegerValue; - + for (NSUInteger i = 0; i < count; i++) { // outputs uint64_t amount = [message UInt64AtOffset:off]; // output amount off += sizeof(uint64_t); @@ -144,7 +148,7 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { DSTransactionOutput *transactionOutput = [DSTransactionOutput transactionOutputWithAmount:amount outScript:outScript onChain:self.chain]; [self.mOutputs addObject:transactionOutput]; } - + _lockTime = [message UInt32AtOffset:off]; // tx locktime off += 4; _payloadOffset = off; @@ -152,9 +156,9 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { _txHash = self.data.SHA256_2; } } - + if ([self type] != DSTransactionType_Classic) return self; //only classic transactions are shapeshifted - + NSString *outboundShapeshiftAddress = [self shapeshiftOutboundAddress]; if (!outboundShapeshiftAddress) return self; self.associatedShapeshift = [DSShapeshiftEntity shapeshiftHavingWithdrawalAddress:outboundShapeshiftAddress inContext:[NSManagedObjectContext chainContext]]; @@ -168,9 +172,9 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { self.associatedShapeshift.shapeshiftStatus = @(eShapeshiftAddressStatus_NoDeposits); } } - + if (self.associatedShapeshift || ![self.outputs count]) return self; - + NSString *mainOutputAddress = nil; NSMutableArray *allAddresses = [NSMutableArray array]; for (DSAddressEntity *e in [DSAddressEntity allObjectsInContext:chain.chainManagedObjectContext]) { @@ -186,7 +190,7 @@ - (instancetype)initWithMessage:(NSData *)message onChain:(DSChain *)chain { if (mainOutputAddress) { self.associatedShapeshift = [DSShapeshiftEntity registerShapeshiftWithInputAddress:mainOutputAddress andWithdrawalAddress:outboundShapeshiftAddress withStatus:eShapeshiftAddressStatus_NoDeposits inContext:[NSManagedObjectContext chainContext]]; } - + return self; } @@ -198,9 +202,9 @@ - (instancetype)initWithInputHashes:(NSArray *)hashes inputIndexes:(NSArray *)in if (hashes.count != indexes.count) return nil; if (scripts.count > 0 && hashes.count != scripts.count) return nil; if (addresses.count != amounts.count) return nil; - + if (!(self = [super init])) return nil; - + self.persistenceStatus = DSTransactionPersistenceStatus_NotSaved; self.chain = chain; _version = chain.transactionVersion; @@ -214,7 +218,7 @@ - (instancetype)initWithInputHashes:(NSArray *)hashes inputIndexes:(NSArray *)in NSData *inputScript = (scripts.count > 0) ? [scripts objectAtIndex:i] : nil; [self.mInputs addObject:[DSTransactionInput transactionInputWithHash:inputHash index:index inScript:inputScript signature:nil sequence:inputSequence]]; } - + self.mOutputs = [NSMutableArray array]; for (int i = 0; i < amounts.count; i++) { uint64_t amount = [[amounts objectAtIndex:i] unsignedLongValue]; @@ -227,7 +231,7 @@ - (instancetype)initWithInputHashes:(NSArray *)hashes inputIndexes:(NSArray *)in } [self.mOutputs addObject:[DSTransactionOutput transactionOutputWithAmount:amount outScript:outScript onChain:self.chain]]; } - + _lockTime = TX_LOCKTIME; self.blockHeight = TX_UNCONFIRMED; return self; @@ -314,28 +318,64 @@ - (NSString *)description { - (NSString *)longDescription { NSString *txid = [NSString hexWithData:[NSData dataWithBytes:self.txHash.u8 length:sizeof(UInt256)].reverse]; return [NSString stringWithFormat:@"%@(id=%@, inputs=%@, outputs=%@)", - [[self class] description], txid, - self.inputs, - self.outputs]; + [[self class] description], txid, + self.inputs, + self.outputs]; } -// retuns the amount sent from the wallet by the trasaction (total wallet outputs consumed, change and fee included) -- (uint64_t)amountSent { +- (uint64_t)dashAmount { + if (self.cachedDashAmount != UINT64_MAX) { + return self.cachedDashAmount; + } + uint64_t amount = 0; - for (DSTransactionInput *input in self.inputs) { - UInt256 hash = input.inputHash; - DSTransaction *tx = [self.chain transactionForHash:hash]; - DSAccount *account = [self.chain firstAccountThatCanContainTransaction:tx]; - uint32_t n = input.index; - if (n < tx.outputs.count) { - DSTransactionOutput *output = tx.outputs[n]; - if ([account containsAddress:output.address]) - amount += output.amount; + const uint64_t sent = [self.chain amountSentByTransaction:self]; + const uint64_t received = [self.chain amountReceivedFromTransaction:self]; + uint64_t fee = self.feeUsed; + + if (fee == UINT64_MAX) { + fee = 0; + } + + if (sent > 0 && (received + fee) == sent) { + // moved + amount = 0; + self.cachedDirection = DSTransactionDirection_Moved; + } else if (sent > 0) { + // sent + if (received > sent) { + // NOTE: During the sync we may get an incorrect amount + return UINT64_MAX; } + + self.cachedDirection = DSTransactionDirection_Sent; + amount = sent - received - fee; + } else if (received > 0) { + // received + self.cachedDirection = DSTransactionDirection_Received; + amount = received; + } else { + // no funds moved on this account + self.cachedDirection = DSTransactionDirection_NotAccountFunds; + amount = 0; } + + self.cachedDashAmount = amount; + return amount; } +- (DSTransactionDirection)direction { + if (self.cachedDirection != DSTransactionDirection_NotAccountFunds) { + return self.cachedDirection; + } + + DSTransactionDirection direction = [self.chain directionOfTransaction: self]; + self.cachedDirection = direction; + + return direction; +} + // size in bytes if signed, or estimated size assuming compact pubkey sigs - (size_t)size { @synchronized(self) { @@ -343,7 +383,7 @@ - (size_t)size { uint32_t inputCount = (uint32_t)self.mInputs.count; uint32_t outputCount = (uint32_t)self.mOutputs.count; return 8 + [NSMutableData sizeOfVarInt:inputCount] + [NSMutableData sizeOfVarInt:outputCount] + - TX_INPUT_SIZE * inputCount + TX_OUTPUT_SIZE * outputCount; + TX_INPUT_SIZE * inputCount + TX_OUTPUT_SIZE * outputCount; } } @@ -380,6 +420,19 @@ - (BOOL)isCoinbaseClassicTransaction { } } +- (BOOL)isImmatureCoinBase { + // note GetBlocksToMaturity is 0 for non-coinbase tx + return [self getBlocksToMaturity] > 0; +} + +- (int32_t)getBlocksToMaturity { + if (![self isCoinbaseClassicTransaction]) + return 0; + + uint32_t chainDepth = [self confirmations]; + return MAX(0, (COINBASE_MATURITY + 1) - chainDepth); +} + - (BOOL)isCreditFundingTransaction { for (DSTransactionOutput *output in self.outputs) { NSData *script = output.outScript; @@ -398,17 +451,27 @@ - (NSUInteger)hash { // MARK: - Wire Serialization - (NSData *)toData { - return [self toDataWithSubscriptIndex:NSNotFound]; + return [self toData:NO]; +} + +- (NSData *)toData:(BOOL)anyoneCanPay { + return [self toDataWithSubscriptIndex:NSNotFound anyoneCanPay: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 { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { NSArray *inputs = self.inputs; NSArray *outputs = self.outputs; NSUInteger inputsCount = inputs.count; NSUInteger outputsCount = outputs.count; + + if (anyoneCanPay && subscriptIndex < inputsCount) { + inputs = @[inputs[subscriptIndex]]; + inputsCount = 1; + } + BOOL forSigHash = ([self isMemberOfClass:[DSTransaction class]] || [self isMemberOfClass:[DSCreditFundingTransaction class]] || [self isMemberOfClass:[DSAssetUnlockTransaction class]]) && subscriptIndex != NSNotFound; NSUInteger dataSize = 8 + [NSMutableData sizeOfVarInt:inputsCount] + [NSMutableData sizeOfVarInt:outputsCount] + TX_INPUT_SIZE * inputsCount + TX_OUTPUT_SIZE * outputsCount + (forSigHash ? 4 : 0); @@ -424,7 +487,7 @@ - (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { if (subscriptIndex == NSNotFound && input.signature != nil) { [d appendCountedData:input.signature]; - } else if (subscriptIndex == i && input.inScript != nil) { + } else if (anyoneCanPay || (subscriptIndex == i && input.inScript != nil)) { // TODO: to fully match the reference implementation, OP_CODESEPARATOR related checksig logic should go here [d appendCountedData:input.inScript]; } else { @@ -442,7 +505,14 @@ - (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { } [d appendUInt32:self.lockTime]; - if (forSigHash) [d appendUInt32:SIGHASH_ALL]; + if (forSigHash) { + uint8_t sighashFlags = SIGHASH_ALL; + if (anyoneCanPay) { + sighashFlags |= SIGHASH_ANYONECANPAY; + } + + [d appendUInt32:sighashFlags]; + } return [d copy]; } } @@ -572,6 +642,10 @@ - (BOOL)signWithSerializedPrivateKeys:(NSArray *)privateKeys { } - (BOOL)signWithPrivateKeys:(NSArray *)keys { + return [self signWithPrivateKeys:keys anyoneCanPay:NO]; +} + +- (BOOL)signWithPrivateKeys:(NSArray *)keys anyoneCanPay:(BOOL)anyoneCanPay { NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:keys.count]; // TODO: avoid double looping: defer getting address into signWithPrivateKeys key <-> address @@ -580,41 +654,50 @@ - (BOOL)signWithPrivateKeys:(NSArray *)keys { } @synchronized (self) { for (NSUInteger i = 0; i < self.mInputs.count; i++) { - DSTransactionInput *transactionInput = self.mInputs[i]; - - NSString *addr = [DSKeyManager addressWithScriptPubKey:transactionInput.inScript forChain:self.chain]; - NSUInteger keyIdx = (addr) ? [addresses indexOfObject:addr] : NSNotFound; - if (keyIdx == NSNotFound) continue; - NSData *data = [self toDataWithSubscriptIndex:i]; - NSMutableData *sig = [NSMutableData data]; - NSValue *keyValue = keys[keyIdx]; - OpaqueKey *key = ((OpaqueKey *) keyValue.pointerValue); - UInt256 hash = data.SHA256_2; - NSData *signedData = [DSKeyManager NSDataFrom:key_ecdsa_sign(key->ecdsa, hash.u8, 32)]; - - NSMutableData *s = [NSMutableData dataWithData:signedData]; - [s appendUInt8:SIGHASH_ALL]; - [sig appendScriptPushData:s]; - NSArray *elem = [transactionInput.inScript scriptElements]; - if (elem.count >= 2 && [elem[elem.count - 2] intValue] == OP_EQUALVERIFY) { // pay-to-pubkey-hash scriptSig - [sig appendScriptPushData:[DSKeyManager publicKeyData:key]]; - } - - transactionInput.signature = sig; + DSTransactionInput *transactionInput = self.mInputs[i]; + NSString *addr = [DSKeyManager addressWithScriptPubKey:transactionInput.inScript forChain:self.chain]; + NSUInteger keyIdx = (addr) ? [addresses indexOfObject:addr] : NSNotFound; + if (keyIdx == NSNotFound) { + if (anyoneCanPay && !transactionInput.signature) { + transactionInput.signature = [NSData data]; + } + + continue; + } + NSData *data = [self toDataWithSubscriptIndex:i anyoneCanPay:anyoneCanPay]; + NSMutableData *sig = [NSMutableData data]; + NSValue *keyValue = keys[keyIdx]; + OpaqueKey *key = ((OpaqueKey *) keyValue.pointerValue); + UInt256 hash = data.SHA256_2; + NSData *signedData = [DSKeyManager NSDataFrom:key_ecdsa_sign(key->ecdsa, hash.u8, 32)]; + NSMutableData *s = [NSMutableData dataWithData:signedData]; + uint8_t sighashFlags = SIGHASH_ALL; + if (anyoneCanPay) { + sighashFlags |= SIGHASH_ANYONECANPAY; + } + [s appendUInt8:sighashFlags]; + [sig appendScriptPushData:s]; + NSArray *elem = [transactionInput.inScript scriptElements]; + if (elem.count >= 2 && [elem[elem.count - 2] intValue] == OP_EQUALVERIFY) { // pay-to-pubkey-hash scriptSig + [sig appendScriptPushData:[DSKeyManager publicKeyData:key]]; + } + + transactionInput.signature = sig; } if (!self.isSigned) return NO; - _txHash = self.data.SHA256_2; + _txHash = [self toData:anyoneCanPay].SHA256_2; return YES; } } - (BOOL)signWithPreorderedPrivateKeys:(NSArray *)keys { + // TODO: Function isn't used at all except commented out `testIdentityGrindingAttack` @synchronized (self) { for (NSUInteger i = 0; i < self.mInputs.count; i++) { DSTransactionInput *transactionInput = self.mInputs[i]; NSMutableData *sig = [NSMutableData data]; - NSData *data = [self toDataWithSubscriptIndex:i]; + NSData *data = [self toDataWithSubscriptIndex:i anyoneCanPay:NO]; UInt256 hash = data.SHA256_2; NSValue *keyValue = keys[i]; OpaqueKey *key = ((OpaqueKey *) keyValue.pointerValue); @@ -877,9 +960,3 @@ - (BOOL)saveInitialInContext:(NSManagedObjectContext *)context { } @end - -@implementation DSTransaction (Extensions) -- (DSTransactionDirection)direction { - return [_chain directionOfTransaction: self]; -} -@end diff --git a/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m b/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m index 3c2370ba6..fd17a7fb9 100644 --- a/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m +++ b/DashSync/shared/Models/Transactions/Coinbase/DSCoinbaseTransaction.m @@ -103,9 +103,10 @@ - (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 { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; return [data appendCountedData:[self payloadData]]; } } diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m index b49269d17..4a9c3526a 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderRegistrationTransaction.m @@ -229,9 +229,10 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; [data appendCountedData:[self payloadData]]; if (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 07a340f71..cd7210ae8 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRegistrarTransaction.m @@ -180,9 +180,10 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; NSData *payloadData = [self payloadData]; [data appendVarInt:payloadData.length]; [data appendData:payloadData]; diff --git a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m index 7de1b9d2c..c062365b6 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateRevocationTransaction.m @@ -148,9 +148,10 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; [data appendCountedData:[self payloadData]]; if (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 cbd2ee993..1fe7cb4aa 100644 --- a/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m +++ b/DashSync/shared/Models/Transactions/Provider/DSProviderUpdateServiceTransaction.m @@ -197,9 +197,10 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; [data appendCountedData:[self payloadData]]; if (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 7b4325e65..500e09d95 100644 --- a/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m +++ b/DashSync/shared/Models/Transactions/Quorums/DSQuorumCommitmentTransaction.m @@ -132,9 +132,10 @@ - (NSData *)payloadData { return data; } -- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex { +- (NSData *)toDataWithSubscriptIndex:(NSUInteger)subscriptIndex + anyoneCanPay:(BOOL)anyoneCanPay { @synchronized(self) { - NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex] mutableCopy]; + NSMutableData *data = [[super toDataWithSubscriptIndex:subscriptIndex anyoneCanPay:anyoneCanPay] mutableCopy]; [data appendCountedData:[self payloadData]]; if (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 3db72ad65..dc0ace3c8 100644 --- a/DashSync/shared/Models/Wallet/DSAccount.h +++ b/DashSync/shared/Models/Wallet/DSAccount.h @@ -26,6 +26,7 @@ #import "DSFundsDerivationPath.h" #import "DSIncomingFundsDerivationPath.h" #import "DSTransaction.h" +#import "DSCoinControl.h" #import "NSData+Dash.h" #import #import @@ -53,6 +54,8 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT @property (nullable, nonatomic, readonly) DSDerivationPath *masterContactsDerivationPath; +@property (nullable, nonatomic, readonly) DSFundsDerivationPath *coinJoinDerivationPath; + @property (nullable, nonatomic, weak) DSWallet *wallet; @property (nonatomic, readonly) NSString *uniqueID; @@ -94,13 +97,25 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // all previously generated internal addresses @property (nonatomic, readonly) NSArray *internalAddresses; +// returns the first unused coinjoin address +@property (nullable, nonatomic, readonly) NSString *coinJoinReceiveAddress; + +// returns the first unused coinjoin internal address +@property (nullable, nonatomic, readonly) NSString *coinJoinChangeAddress; + +// returns all issued CoinJoin receive addresses +@property (nullable, nonatomic, readonly) NSArray *usedCoinJoinReceiveAddresses; + +// returns all used CoinJoin receive addresses +@property (nullable, nonatomic, readonly) NSArray *allCoinJoinReceiveAddresses; + // all the contacts for an account @property (nonatomic, readonly) NSArray *_Nonnull contacts; // has an extended public key missing in one of the account derivation paths @property (nonatomic, readonly) BOOL hasAnExtendedPublicKeyMissing; -- (NSArray *_Nullable)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit internal:(BOOL)internal error:(NSError **)error; +- (NSArray *_Nullable)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit coinJoinGapLimit:(NSUInteger)coinJoinGapLimit internal:(BOOL)internal error:(NSError **)error; + (DSAccount *)accountWithAccountNumber:(uint32_t)accountNumber withDerivationPaths:(NSArray *)derivationPaths inContext:(NSManagedObjectContext *_Nullable)context; @@ -132,6 +147,9 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // true if the address is controlled by the wallet - (BOOL)containsAddress:(NSString *)address; +// true if the coinjoin address is controlled by the wallet +- (BOOL)containsCoinJoinAddress:(NSString *)coinJoinAddress; + // true if the address is internal and is controlled by the wallet - (BOOL)containsInternalAddress:(NSString *)address; @@ -163,6 +181,8 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee; +- (DSTransaction *)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee coinControl:(DSCoinControl *)coinControl; + // returns an unsigned transaction that sends the specified amounts from the wallet to the specified output scripts - (DSTransaction *_Nullable)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee toShapeshiftAddress:(NSString *_Nullable)shapeshiftAddress; @@ -175,11 +195,24 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT /// /// - Parameters: /// - transaction: Instance of `DSTransaction` you want to sign -/// - completion: Completion block that has type `TransactionValidityCompletionBlock` +/// +/// - Returns: boolean value indicating if the transaction was signed +/// +/// - Note: Using this method to sign a tx doesn't present pin controller, use this method carefully from UI +/// +- (BOOL)signTransaction:(DSTransaction *)transaction; + +/// Sign any inputs in the given transaction that can be signed using private keys from the wallet +/// +/// - Parameters: +/// - transaction: Instance of `DSTransaction` you want to sign +/// - anyoneCanPay: apply SIGHASH_ANYONECANPAY signature type +/// +/// - Returns: boolean value indicating if the transaction was signed /// /// - Note: Using this method to sign a tx doesn't present pin controller, use this method carefully from UI /// -- (void)signTransaction:(DSTransaction *)transaction completion:(_Nonnull TransactionValidityCompletionBlock)completion; +- (BOOL)signTransaction:(DSTransaction *)transaction anyoneCanPay:(BOOL)anyoneCanPay; /// Sign any inputs in the given transaction that can be signed using private keys from the wallet /// @@ -215,6 +248,11 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // true if no previous account transaction spends any of the given transaction's inputs, and no inputs are invalid - (BOOL)transactionIsValid:(DSTransaction *)transaction; +- (BOOL)isSpent:(NSValue *)output; + +// returns input value if no previous account transaction spends this input, and the input is valid, -1 otherwise. +- (int64_t)inputValue:(UInt256)txHash inputIndex:(uint32_t)index; + // received, sent or moved inside an account - (DSTransactionDirection)directionOfTransaction:(DSTransaction *)transaction; @@ -242,15 +280,15 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSAccountNewAccountShouldBeAddedFromT // retuns the amount sent from the account by the trasaction (total account outputs consumed, change and fee included) - (uint64_t)amountSentByTransaction:(DSTransaction *)transaction; +// Returns the amounts sent by the transaction +- (NSArray *)amountsSentByTransaction:(DSTransaction *)transaction; + // returns the external (receive) addresses of a transaction - (NSArray *)externalAddressesOfTransaction:(DSTransaction *)transaction; // returns the fee for the given transaction if all its inputs are from wallet transactions, UINT64_MAX otherwise - (uint64_t)feeForTransaction:(DSTransaction *)transaction; -// historical wallet balance after the given transaction, or current balance if transaction is not registered in wallet -- (uint64_t)balanceAfterTransaction:(DSTransaction *)transaction; - - (void)chainUpdatedBlockHeight:(int32_t)height; - (NSArray *)setBlockHeight:(int32_t)height andTimestamp:(NSTimeInterval)timestamp forTransactionHashes:(NSArray *)txHashes; diff --git a/DashSync/shared/Models/Wallet/DSAccount.m b/DashSync/shared/Models/Wallet/DSAccount.m index b0a9fe375..d994a0554 100644 --- a/DashSync/shared/Models/Wallet/DSAccount.m +++ b/DashSync/shared/Models/Wallet/DSAccount.m @@ -61,6 +61,7 @@ #import "DSTransactionFactory.h" #import "DSTransactionInput.h" #import "DSTransactionOutput.h" +#import "DSCoinControl.h" #import "NSData+Dash.h" #import "NSDate+Utils.h" #import "NSError+Dash.h" @@ -109,6 +110,8 @@ @interface DSAccount () @property (nonatomic, strong) DSDerivationPath *masterContactsDerivationPath; +@property (nonatomic, strong) DSFundsDerivationPath *coinJoinDerivationPath; + @property (nonatomic, assign) BOOL isViewOnlyAccount; @property (nonatomic, assign) UInt256 firstTransactionHash; @@ -160,7 +163,13 @@ - (void)verifyAndAssignAddedDerivationPaths:(NSArray *)deriv NSAssert(TRUE, @"There should only be one master contacts derivation path"); } self.masterContactsDerivationPath = derivationPath; + } else if (derivationPath.reference == DSDerivationPathReference_CoinJoin) { + if (self.coinJoinDerivationPath) { + NSAssert(TRUE, @"There should only be one CoinJoin derivation path"); + } + self.coinJoinDerivationPath = (DSFundsDerivationPath *)derivationPath; } + for (int j = i + 1; j < [derivationPaths count]; j++) { DSDerivationPath *derivationPath2 = [derivationPaths objectAtIndex:j]; NSAssert([derivationPath isDerivationPathEqual:derivationPath2] == NO, @"Derivation paths should all be different"); @@ -174,7 +183,7 @@ - (void)verifyAndAssignAddedDerivationPaths:(NSArray *)deriv - (instancetype)initWithAccountNumber:(uint32_t)accountNumber withDerivationPaths:(NSArray *)derivationPaths inContext:(NSManagedObjectContext *)context { NSParameterAssert(derivationPaths); - + if (!(self = [super init])) return nil; _accountNumber = accountNumber; [self verifyAndAssignAddedDerivationPaths:derivationPaths]; @@ -198,12 +207,12 @@ - (instancetype)initWithAccountNumber:(uint32_t)accountNumber withDerivationPath - (instancetype)initAsViewOnlyWithAccountNumber:(uint32_t)accountNumber withDerivationPaths:(NSArray *)derivationPaths inContext:(NSManagedObjectContext *)context { NSParameterAssert(derivationPaths); - + if (!(self = [self initWithAccountNumber:accountNumber withDerivationPaths:derivationPaths inContext:context])) return nil; self.isViewOnlyAccount = TRUE; self.transactionsToSave = [NSMutableArray array]; self.transactionsToSaveInBlockSave = [NSMutableDictionary dictionary]; - + return self; } @@ -228,17 +237,17 @@ - (void)loadTransactions { DSLogPrivate(@"[%@] Transaction %@", _wallet.chain.name, [transaction longDescription]); } #endif - + NSUInteger transactionCount = [DSTransactionEntity countObjectsInContext:self.managedObjectContext matching:@"transactionHash.chain == %@", [self.wallet.chain chainEntityInContext:self.managedObjectContext]]; if (transactionCount > self.allTx.count) { // pre-fetch transaction inputs and outputs @autoreleasepool { NSFetchRequest *fetchRequest = [DSTxOutputEntity fetchRequest]; - + //for some reason it is faster to search by the wallet unique id on the account, then it is by the account itself, this might change if there are more than 1 account; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"account.walletUniqueID = %@ && account.index = %@", self.wallet.uniqueIDString, @(self.accountNumber)]; [fetchRequest setRelationshipKeyPathsForPrefetching:@[@"transaction.inputs", @"transaction.outputs", @"transaction.transactionHash", @"spentInInput.transaction.inputs", @"spentInInput.transaction.outputs", @"spentInInput.transaction.transactionHash"]]; - + NSError *fetchRequestError = nil; //NSDate *transactionOutputsStartTime = [NSDate date]; NSArray *transactionOutputs = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchRequestError]; @@ -323,7 +332,7 @@ - (NSString *)uniqueID { - (uint32_t)blockHeight { static uint32_t height = 0; uint32_t h = self.wallet.chain.lastSyncBlockHeight; - + if (h > height) height = h; return height; } @@ -338,6 +347,32 @@ - (NSString *)changeAddress { return self.defaultDerivationPath.changeAddress; } +// returns the first unused coinjoin address +- (NSString *)coinJoinReceiveAddress { + NSString *address = self.coinJoinDerivationPath.receiveAddress; + dispatch_sync(self.wallet.chain.networkingQueue, ^{ + [self.coinJoinDerivationPath registerTransactionAddress:address]; + }); + return address; +} + +// returns the first unused coinjoin address +- (NSString *)coinJoinChangeAddress { + NSString *address = self.coinJoinDerivationPath.changeAddress; + dispatch_sync(self.wallet.chain.networkingQueue, ^{ + [self.coinJoinDerivationPath registerTransactionAddress:address]; + }); + return address; +} + +- (NSArray *)allCoinJoinReceiveAddresses { + return [self.coinJoinDerivationPath allReceiveAddresses]; +} + +- (NSArray *)usedCoinJoinReceiveAddresses { + return [self.coinJoinDerivationPath usedReceiveAddresses]; +} + // NSData objects containing serialized UTXOs - (NSArray *)unspentOutputs { return self.utxos.array; @@ -347,7 +382,7 @@ - (NSArray *)unspentOutputs { - (void)removeDerivationPath:(DSDerivationPath *)derivationPath { NSParameterAssert(derivationPath); - + if ([self.mFundDerivationPaths containsObject:derivationPath]) { [self.mFundDerivationPaths removeObject:derivationPath]; } @@ -371,7 +406,7 @@ - (DSIncomingFundsDerivationPath *)derivationPathForFriendshipWithIdentifier:(NS - (void)addDerivationPath:(DSDerivationPath *)derivationPath { NSParameterAssert(derivationPath); - + if (!_isViewOnlyAccount) { [self verifyAndAssignAddedDerivationPaths:@[derivationPath]]; } @@ -406,7 +441,7 @@ - (void)addOutgoingDerivationPath:(DSIncomingFundsDerivationPath *)derivationPat - (void)addDerivationPathsFromArray:(NSArray *)derivationPaths { NSParameterAssert(derivationPaths); - + if (!_isViewOnlyAccount) { [self verifyAndAssignAddedDerivationPaths:derivationPaths]; } @@ -446,12 +481,21 @@ - (BOOL)hasAnExtendedPublicKeyMissing { return NO; } -- (NSArray *)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit internal:(BOOL)internal error:(NSError **)error { +- (NSArray *)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit coinJoinGapLimit:(NSUInteger)coinJoinGapLimit internal:(BOOL)internal error:(NSError **)error { NSMutableArray *mArray = [NSMutableArray array]; for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { DSFundsDerivationPath *fundsDerivationPath = (DSFundsDerivationPath *)derivationPath; - NSUInteger registerGapLimit = [fundsDerivationPath shouldUseReducedGapLimit] ? unusedAccountGapLimit : gapLimit; + NSUInteger registerGapLimit = 0; + + if ([fundsDerivationPath shouldUseReducedGapLimit]) { + registerGapLimit = unusedAccountGapLimit; + } else if (fundsDerivationPath.type == DSDerivationPathType_AnonymousFunds) { + registerGapLimit = coinJoinGapLimit; + } else { + registerGapLimit = gapLimit; + } + [mArray addObjectsFromArray:[fundsDerivationPath registerAddressesWithGapLimit:registerGapLimit internal:internal error:error]]; } else if (!internal && [derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { [mArray addObjectsFromArray:[(DSIncomingFundsDerivationPath *)derivationPath registerAddressesWithGapLimit:dashpayGapLimit error:error]]; @@ -509,13 +553,26 @@ - (BOOL)containsAddress:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath containsAddress:address]) return TRUE; } return FALSE; } +// true if the coinjoin address is controlled by the wallet +- (BOOL)containsCoinJoinAddress:(NSString *)coinJoinAddress { + NSParameterAssert(coinJoinAddress); + if (![coinJoinAddress isKindOfClass:[NSString class]]) { + //in case address is of type [NSNull null] + return FALSE; + } + + if ([self.coinJoinDerivationPath containsAddress:coinJoinAddress]) return TRUE; + + return FALSE; +} + // true if the address is controlled by the wallet - (BOOL)containsInternalAddress:(NSString *)address { NSParameterAssert(address); @@ -523,7 +580,7 @@ - (BOOL)containsInternalAddress:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]] && [derivationPath containsChangeAddress:address]) { return TRUE; @@ -538,7 +595,7 @@ - (BOOL)baseDerivationPathsContainAddress:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]] && [derivationPath containsAddress:address]) { return TRUE; @@ -554,7 +611,7 @@ - (BOOL)containsExternalAddress:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { if ([(DSFundsDerivationPath *)derivationPath containsReceiveAddress:address]) return TRUE; @@ -571,7 +628,7 @@ - (DSIncomingFundsDerivationPath *)externalDerivationPathContainingAddress:(NSSt //in case address is of type [NSNull null] return nil; } - + for (DSIncomingFundsDerivationPath *derivationPath in self.mContactOutgoingFundDerivationPathsDictionary.allValues) { if ([derivationPath containsAddress:address]) return derivationPath; } @@ -585,7 +642,7 @@ - (BOOL)addressIsUsed:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath addressIsUsed:address]) return TRUE; } @@ -598,11 +655,11 @@ - (BOOL)transactionAddressAlreadySeenInOutputs:(NSString *)address { //in case address is of type [NSNull null] return FALSE; } - + for (DSTransaction *transaction in self.allTransactions) { if ([transaction.outputs indexOfObjectPassingTest:^BOOL(DSTransactionOutput *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - return [obj.address isEqual:address]; - }] != NSNotFound) return TRUE; + return [obj.address isEqual:address]; + }] != NSNotFound) return TRUE; } return FALSE; } @@ -614,13 +671,12 @@ - (void)updateBalance { NSMutableOrderedSet *utxos = [NSMutableOrderedSet orderedSet]; NSMutableSet *spentOutputs = [NSMutableSet set], *invalidTx = [NSMutableSet set], *pendingTransactionHashes = [NSMutableSet set]; NSMutableDictionary *pendingCoinbaseLockedTransactionHashes = [NSMutableDictionary dictionary]; - NSMutableArray *balanceHistory = [NSMutableArray array]; uint32_t now = [NSDate timeIntervalSince1970]; - + for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { derivationPath.balance = 0; } - + for (DSTransaction *tx in [self.transactions reverseObjectEnumerator]) { #if LOG_BALANCE_UPDATE DSLogPrivate(@"updating balance after transaction %@", [NSData dataWithUInt256:tx.txHash].reverse.hexString); @@ -630,7 +686,7 @@ - (void)updateBalance { NSSet *inputs; uint32_t n = 0; BOOL pending = NO; - + if (!tx.isCoinbaseClassicTransaction && ![tx isKindOfClass:[DSCoinbaseTransaction class]]) { NSMutableArray *rHashes = [NSMutableArray array]; @@ -644,25 +700,24 @@ - (void)updateBalance { if (tx.blockHeight == TX_UNCONFIRMED && ([spent intersectsSet:spentOutputs] || [inputs intersectsSet:invalidTx])) { [invalidTx addObject:uint256_obj(tx.txHash)]; - [balanceHistory insertObject:@(balance) atIndex:0]; continue; } } else { inputs = [NSSet set]; } - + [spentOutputs unionSet:spent]; // add inputs to spent output set n = 0; - + // check if any inputs are pending if (tx.blockHeight == TX_UNCONFIRMED) { if (tx.size > TX_MAX_SIZE) { pending = YES; // check transaction size is under TX_MAX_SIZE } - + for (DSTransactionInput *input in tx.inputs) { if (input.sequence == UINT32_MAX) continue; - + if (tx.lockTime < TX_MAX_LOCK_HEIGHT && tx.lockTime > self.wallet.chain.bestBlockHeight + 1) { pending = YES; // future lockTime @@ -682,7 +737,7 @@ - (void)updateBalance { #endif } } - + for (DSTransactionOutput *output in tx.outputs) { // check that no outputs are dust if (output.amount < TX_MIN_OUTPUT_AMOUNT) { pending = YES; @@ -694,24 +749,22 @@ - (void)updateBalance { } } } - + if (pending || [inputs intersectsSet:pendingTransactionHashes]) { [pendingTransactionHashes addObject:uint256_obj(tx.txHash)]; - [balanceHistory insertObject:@(balance) atIndex:0]; continue; } - + uint32_t lockedBlockHeight = [self transactionOutputsAreLockedTill:tx]; - + if (lockedBlockHeight) { if (![pendingCoinbaseLockedTransactionHashes objectForKey:@(lockedBlockHeight)]) { pendingCoinbaseLockedTransactionHashes[@(lockedBlockHeight)] = [NSMutableSet set]; } [((NSMutableSet *)pendingCoinbaseLockedTransactionHashes[@(lockedBlockHeight)]) addObject:uint256_obj(tx.txHash)]; - [balanceHistory insertObject:@(balance) atIndex:0]; continue; } - + //TODO: don't add outputs below TX_MIN_OUTPUT_AMOUNT //TODO: don't add coin generation outputs < 100 blocks deep //NOTE: balance/UTXOs will then need to be recalculated when last block changes @@ -729,15 +782,15 @@ - (void)updateBalance { } n++; } - + // transaction ordering is not guaranteed, so check the entire UTXO set against the entire spent output set [spent setSet:utxos.set]; [spent intersectSet:spentOutputs]; - + for (NSValue *output in spent) { // remove any spent outputs from UTXO set DSTransaction *transaction; DSUTXO o; - + [output getValue:&o]; transaction = self.allTx[uint256_obj(o.hash)]; [utxos removeObject:output]; @@ -751,10 +804,9 @@ - (void)updateBalance { } } } - + if (prevBalance < balance) totalReceived += balance - prevBalance; if (balance < prevBalance) totalSent += prevBalance - balance; - [balanceHistory insertObject:@(balance) atIndex:0]; prevBalance = balance; #if LOG_BALANCE_UPDATE DSLog(@"===UTXOS==="); @@ -772,19 +824,18 @@ - (void)updateBalance { #endif } } - + self.invalidTransactionHashes = invalidTx; self.pendingTransactionHashes = pendingTransactionHashes; self.pendingCoinbaseLockedTransactionHashes = pendingCoinbaseLockedTransactionHashes; self.spentOutputs = spentOutputs; self.utxos = utxos; - self.balanceHistory = balanceHistory; _totalSent = totalSent; _totalReceived = totalReceived; - + if (balance != _balance) { _balance = balance; - + dispatch_async(dispatch_get_main_queue(), ^{ [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(postBalanceDidChangeNotification) object:nil]; [self performSelector:@selector(postBalanceDidChangeNotification) withObject:nil afterDelay:0.1]; @@ -792,16 +843,6 @@ - (void)updateBalance { } } -// historical wallet balance after the given transaction, or current balance if transaction is not registered in wallet -- (uint64_t)balanceAfterTransaction:(DSTransaction *)transaction { - NSParameterAssert(transaction); - - NSUInteger i = [self.transactions indexOfObject:transaction]; - - return (i < self.balanceHistory.count) ? [self.balanceHistory[i] unsignedLongLongValue] : self.balance; -} - - - (void)postBalanceDidChangeNotification { [[NSNotificationCenter defaultCenter] postNotificationName:DSWalletBalanceDidChangeNotification object:nil]; } @@ -816,7 +857,7 @@ static NSUInteger transactionAddressIndex(DSTransaction *transaction, NSArray *a NSUInteger i = [addressChain indexOfObject:output.address]; if (i != NSNotFound) return i; } - + return NSNotFound; } @@ -831,31 +872,32 @@ __block __weak BOOL (^_isAscending)(id, id) = isAscending = ^BOOL(DSTransaction if (tx1.blockHeight < tx2.blockHeight) return NO; NSValue *hash1 = uint256_obj(tx1.txHash), *hash2 = uint256_obj(tx2.txHash); if ([tx1.inputs indexOfObjectPassingTest:^BOOL(DSTransactionInput *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - return uint256_eq(obj.inputHash, tx2.txHash); - }] != NSNotFound) return YES; + return uint256_eq(obj.inputHash, tx2.txHash); + }] != NSNotFound) return YES; if ([tx2.inputs indexOfObjectPassingTest:^BOOL(DSTransactionInput *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - return uint256_eq(obj.inputHash, tx1.txHash); - }] != NSNotFound) return NO; + return uint256_eq(obj.inputHash, tx1.txHash); + }] != NSNotFound) return NO; if ([self.invalidTransactionHashes containsObject:hash1] && ![self.invalidTransactionHashes containsObject:hash2]) return YES; if ([self.pendingTransactionHashes containsObject:hash1] && ![self.pendingTransactionHashes containsObject:hash2]) return YES; - for (DSTransactionInput *input in tx1.inputs) { - if (_isAscending(self.allTx[uint256_obj(input.inputHash)], tx2)) return YES; - } + return NO; }; - + + NSArray *externalAddresses = self.externalAddresses; + NSArray *internalAddresses = self.internalAddresses; + [self.transactions sortWithOptions:NSSortStable usingComparator:^NSComparisonResult(id tx1, id tx2) { - if (isAscending(tx1, tx2)) return NSOrderedAscending; - if (isAscending(tx2, tx1)) return NSOrderedDescending; - - NSUInteger i = transactionAddressIndex(tx1, self.internalAddresses); - NSUInteger j = transactionAddressIndex(tx2, (i == NSNotFound) ? self.externalAddresses : self.internalAddresses); - - if (i == NSNotFound && j != NSNotFound) i = transactionAddressIndex(tx1, self.externalAddresses); - if (i == NSNotFound || j == NSNotFound || i == j) return NSOrderedSame; - return (i > j) ? NSOrderedAscending : NSOrderedDescending; - }]; + if (isAscending(tx1, tx2)) return NSOrderedAscending; + if (isAscending(tx2, tx1)) return NSOrderedDescending; + + NSUInteger i = transactionAddressIndex(tx1, internalAddresses); + NSUInteger j = transactionAddressIndex(tx2, (i == NSNotFound) ? externalAddresses : internalAddresses); + + if (i == NSNotFound && j != NSNotFound) i = transactionAddressIndex(tx1, externalAddresses); + if (i == NSNotFound || j == NSNotFound || i == j) return NSOrderedSame; + return (i > j) ? NSOrderedAscending : NSOrderedDescending; + }]; } } @@ -873,7 +915,7 @@ - (DSTransaction *)transactionForHash:(UInt256)txHash { // last 100 transactions sorted by date, most recent first - (NSArray *)recentTransactions { return [self.transactions.array subarrayWithRange:NSMakeRange(0, (self.transactions.count > 100) ? 100 : - self.transactions.count)]; + self.transactions.count)]; } // last 100 transactions sorted by date, most recent first @@ -941,14 +983,14 @@ - (BOOL)canContainTransaction:(DSTransaction *)transaction { DSProviderUpdateRegistrarTransaction *providerUpdateRegistrarTransaction = (DSProviderUpdateRegistrarTransaction *)transaction; if ([self containsAddress:providerUpdateRegistrarTransaction.payoutAddress]) return YES; } - + return NO; } } - (BOOL)checkIsFirstTransaction:(DSTransaction *)transaction { NSParameterAssert(transaction); - + for (DSDerivationPath *derivationPath in self.fundDerivationPaths) { if ([derivationPath type] & DSDerivationPathType_IsForFunds) { NSString *firstAddress; @@ -958,8 +1000,8 @@ - (BOOL)checkIsFirstTransaction:(DSTransaction *)transaction { firstAddress = [(DSIncomingFundsDerivationPath *)derivationPath addressAtIndex:0]; } if ([transaction.outputs indexOfObjectPassingTest:^BOOL(DSTransactionOutput *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - return [obj.address isEqual:firstAddress]; - }] != NSNotFound) { + return [obj.address isEqual:firstAddress]; + }] != NSNotFound) { return TRUE; } } @@ -979,26 +1021,29 @@ - (DSTransaction *)transactionFor:(uint64_t)amount to:(NSString *)address withFe // returns an unsigned transaction that sends the specified amount from the wallet to the given address - (DSCreditFundingTransaction *)creditFundingTransactionFor:(uint64_t)amount to:(NSString *)address withFee:(BOOL)fee { NSParameterAssert(address); - + NSMutableData *script = [NSMutableData data]; - + [script appendCreditBurnScriptPubKeyForAddress:address forChain:self.wallet.chain]; - + DSCreditFundingTransaction *transaction = [[DSCreditFundingTransaction alloc] initOnChain:self.wallet.chain]; return (DSCreditFundingTransaction *)[self updateTransaction:transaction forAmounts:@[@(amount)] toOutputScripts:@[script] withFee:fee sortType:DSTransactionSortType_BIP69]; } - // returns an unsigned transaction that sends the specified amounts from the wallet to the specified output scripts - (DSTransaction *)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee { - return [self transactionForAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:nil]; + return [self transactionForAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:nil coinControl:nil]; } -- (DSTransaction *)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee toShapeshiftAddress:(NSString *)shapeshiftAddress { +- (DSTransaction *)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee coinControl:(DSCoinControl *)coinControl { + return [self transactionForAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:nil coinControl:coinControl]; +} + +- (DSTransaction *)transactionForAmounts:(NSArray *)amounts toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee toShapeshiftAddress:(NSString *)shapeshiftAddress coinControl:(DSCoinControl *)coinControl { NSParameterAssert(amounts); NSParameterAssert(scripts); DSTransaction *transaction = [[DSTransaction alloc] initOnChain:self.wallet.chain]; - return [self updateTransaction:transaction forAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:shapeshiftAddress sortType:DSTransactionSortType_BIP69]; + return [self updateTransaction:transaction forAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:shapeshiftAddress sortType:DSTransactionSortType_BIP69 coinControl:coinControl]; } // MARK: == Proposal Transaction Creation @@ -1025,7 +1070,7 @@ - (DSTransaction *)updateTransaction:(DSTransaction *)transaction toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee sortType:(DSTransactionSortType)sortType { - return [self updateTransaction:transaction forAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:nil sortType:sortType]; + return [self updateTransaction:transaction forAmounts:amounts toOutputScripts:scripts withFee:fee toShapeshiftAddress:nil sortType:sortType coinControl:nil]; } // returns an unsigned transaction that sends the specified amounts from the wallet to the specified output scripts @@ -1034,7 +1079,8 @@ - (DSTransaction *)updateTransaction:(DSTransaction *)transaction toOutputScripts:(NSArray *)scripts withFee:(BOOL)fee toShapeshiftAddress:(NSString *)shapeshiftAddress - sortType:(DSTransactionSortType)sortType { + sortType:(DSTransactionSortType)sortType + coinControl:(DSCoinControl *)coinControl { NSParameterAssert(transaction); NSParameterAssert(amounts); NSParameterAssert(scripts); @@ -1055,6 +1101,8 @@ - (DSTransaction *)updateTransaction:(DSTransaction *)transaction // attacker double spending and requesting a refund for (NSValue *output in self.utxos) { [output getValue:&o]; + + if (coinControl && ![coinControl isSelected:o]) continue; tx = self.allTx[uint256_obj(o.hash)]; if ([self transactionOutputsAreLocked:tx]) continue; if (!tx) continue; @@ -1129,9 +1177,18 @@ - (DSTransaction *)updateTransaction:(DSTransaction *)transaction if (followBIP69sorting) { [transaction sortInputsAccordingToBIP69]; } - + if (balance - (amount + feeAmount) >= self.wallet.chain.minOutputAmount) { - [transaction addOutputAddress:self.changeAddress amount:balance - (amount + feeAmount)]; + NSString *changeAddress; + + if (coinControl.destChange) { + changeAddress = coinControl.destChange; + } else { + changeAddress = self.changeAddress; + } + + [transaction addOutputAddress:changeAddress amount:balance - (amount + feeAmount)]; + if (followBIP69sorting) { [transaction sortOutputsAccordingToBIP69]; } else if (sortType == DSTransactionSortType_Shuffle) { @@ -1247,10 +1304,11 @@ - (NSArray *)usedDerivationPathsForTransaction:(DSTransaction *)transaction { NSMutableArray *usedDerivationPaths = [NSMutableArray array]; for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { + if (!(derivationPath.type == DSDerivationPathType_ClearFunds || derivationPath.type == DSDerivationPathType_AnonymousFunds)) continue; + NSMutableOrderedSet *externalIndexes = [NSMutableOrderedSet orderedSet], *internalIndexes = [NSMutableOrderedSet orderedSet]; for (NSString *addr in transaction.inputAddresses) { - if (!(derivationPath.type == DSDerivationPathType_ClearFunds || derivationPath.type == DSDerivationPathType_AnonymousFunds)) continue; if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { NSInteger index = [derivationPath.allChangeAddresses indexOfObject:addr]; if (index != NSNotFound) { @@ -1272,41 +1330,25 @@ - (NSArray *)usedDerivationPathsForTransaction:(DSTransaction *)transaction { return usedDerivationPaths; } -- (void)signTransaction:(DSTransaction *)transaction completion:(_Nonnull TransactionValidityCompletionBlock)completion { - NSParameterAssert(transaction); +- (BOOL)signTransaction:(DSTransaction *)transaction { + return [self signTransaction:transaction anyoneCanPay:NO]; +} - if (_isViewOnlyAccount) return; +- (BOOL)signTransaction:(DSTransaction *)transaction anyoneCanPay:(BOOL)anyoneCanPay { + NSParameterAssert(transaction); - //int64_t amount = [self amountSentByTransaction:transaction] - [self amountReceivedFromTransaction:transaction]; + if (_isViewOnlyAccount) return NO; NSArray *usedDerivationPaths = [self usedDerivationPathsForTransaction:transaction]; - + @autoreleasepool { // @autoreleasepool ensures sensitive data will be dealocated immediately - self.wallet.seedRequestBlock(^void(NSData *_Nullable seed, BOOL cancelled) { - if (!seed) { - if (completion) completion(NO, YES); - } else { - NSMutableArray *privkeys = [NSMutableArray array]; - for (NSDictionary *dictionary in usedDerivationPaths) { - DSDerivationPath *derivationPath = dictionary[@"derivationPath"]; - NSMutableOrderedSet *externalIndexes = dictionary[@"externalIndexes"], - *internalIndexes = dictionary[@"internalIndexes"]; - if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { - DSFundsDerivationPath *fundsDerivationPath = (DSFundsDerivationPath *)derivationPath; - [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:externalIndexes.array internal:NO fromSeed:seed]]; - [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:internalIndexes.array internal:YES fromSeed:seed]]; - } else if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { - DSIncomingFundsDerivationPath *incomingFundsDerivationPath = (DSIncomingFundsDerivationPath *)derivationPath; - [privkeys addObjectsFromArray:[incomingFundsDerivationPath privateKeys:externalIndexes.array fromSeed:seed]]; - } else { - NSAssert(FALSE, @"The derivation path must be a normal or incoming funds derivation path"); - } - } - - BOOL signedSuccessfully = [transaction signWithPrivateKeys:privkeys]; - if (completion) completion(signedSuccessfully, NO); - } - }); + NSData *seed = [self.wallet requestSeedNoAuth]; + + if (!seed) { + return NO; + } else { + return [self signTxWithDerivationPaths:usedDerivationPaths tx:transaction seed:seed anyoneCanPay:anyoneCanPay]; + } } } @@ -1324,30 +1366,38 @@ - (void)signTransaction:(DSTransaction *)transaction withPrompt:(NSString *_Null if (!seed) { if (completion) completion(NO, YES); } else { - NSMutableArray *privkeys = [NSMutableArray array]; - for (NSDictionary *dictionary in usedDerivationPaths) { - DSDerivationPath *derivationPath = dictionary[@"derivationPath"]; - NSMutableOrderedSet *externalIndexes = dictionary[@"externalIndexes"], - *internalIndexes = dictionary[@"internalIndexes"]; - if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { - DSFundsDerivationPath *fundsDerivationPath = (DSFundsDerivationPath *)derivationPath; - [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:externalIndexes.array internal:NO fromSeed:seed]]; - [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:internalIndexes.array internal:YES fromSeed:seed]]; - } else if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { - DSIncomingFundsDerivationPath *incomingFundsDerivationPath = (DSIncomingFundsDerivationPath *)derivationPath; - [privkeys addObjectsFromArray:[incomingFundsDerivationPath privateKeys:externalIndexes.array fromSeed:seed]]; - } else { - NSAssert(FALSE, @"The derivation path must be a normal or incoming funds derivation path"); - } - } - - BOOL signedSuccessfully = [transaction signWithPrivateKeys:privkeys]; + BOOL signedSuccessfully = [self signTxWithDerivationPaths:usedDerivationPaths tx:transaction seed:seed]; if (completion) completion(signedSuccessfully, NO); } }); } } +- (BOOL)signTxWithDerivationPaths:(NSArray *)usedDerivationPaths tx:(DSTransaction *)tx seed:(NSData *)seed { + return [self signTxWithDerivationPaths:usedDerivationPaths tx:tx seed:seed anyoneCanPay:NO]; +} + +- (BOOL)signTxWithDerivationPaths:(NSArray *)usedDerivationPaths tx:(DSTransaction *)tx seed:(NSData *)seed anyoneCanPay:(BOOL)anyoneCanPay { + NSMutableArray *privkeys = [NSMutableArray array]; + for (NSDictionary *dictionary in usedDerivationPaths) { + DSDerivationPath *derivationPath = dictionary[@"derivationPath"]; + NSMutableOrderedSet *externalIndexes = dictionary[@"externalIndexes"], + *internalIndexes = dictionary[@"internalIndexes"]; + if ([derivationPath isKindOfClass:[DSFundsDerivationPath class]]) { + DSFundsDerivationPath *fundsDerivationPath = (DSFundsDerivationPath *)derivationPath; + [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:externalIndexes.array internal:NO fromSeed:seed]]; + [privkeys addObjectsFromArray:[fundsDerivationPath privateKeys:internalIndexes.array internal:YES fromSeed:seed]]; + } else if ([derivationPath isKindOfClass:[DSIncomingFundsDerivationPath class]]) { + DSIncomingFundsDerivationPath *incomingFundsDerivationPath = (DSIncomingFundsDerivationPath *)derivationPath; + [privkeys addObjectsFromArray:[incomingFundsDerivationPath privateKeys:externalIndexes.array fromSeed:seed]]; + } else { + NSAssert(FALSE, @"The derivation path must be a normal or incoming funds derivation path"); + } + } + + return [tx signWithPrivateKeys:privkeys anyoneCanPay:anyoneCanPay]; +} + // sign any inputs in the given transaction that can be signed using private keys from the wallet - (void)signTransactions:(NSArray *)transactions withPrompt:(NSString *)authprompt completion:(TransactionValidityCompletionBlock)completion { if (_isViewOnlyAccount) return; @@ -1360,10 +1410,11 @@ - (void)signTransactions:(NSArray *)transactions withPrompt:(NS for (DSTransaction *transaction in transactions) { NSMutableArray *usedDerivationPaths = [NSMutableArray array]; for (DSFundsDerivationPath *derivationPath in self.fundDerivationPaths) { + if (!(derivationPath.type == DSDerivationPathType_ClearFunds || derivationPath.type == DSDerivationPathType_AnonymousFunds)) continue; + NSMutableOrderedSet *externalIndexes = [NSMutableOrderedSet orderedSet], *internalIndexes = [NSMutableOrderedSet orderedSet]; for (NSString *addr in transaction.inputAddresses) { - if (!(derivationPath.type == DSDerivationPathType_ClearFunds || derivationPath.type == DSDerivationPathType_AnonymousFunds)) continue; NSInteger index = [derivationPath.allChangeAddresses indexOfObject:addr]; if (index != NSNotFound) { [internalIndexes addObject:@(index)]; @@ -1486,7 +1537,8 @@ - (BOOL)transactionIsValid:(DSTransaction *)transaction { @synchronized (self) { if (transaction.blockHeight != TX_UNCONFIRMED) return YES; if (self.allTx[uint256_obj(transaction.txHash)] != nil) { - return ![self.invalidTransactionHashes containsObject:uint256_obj(transaction.txHash)]; + BOOL invalid = [self.invalidTransactionHashes containsObject:uint256_obj(transaction.txHash)]; + return !invalid; } for (DSTransactionInput *input in transaction.inputs) { UInt256 h = input.inputHash; @@ -1502,6 +1554,30 @@ - (BOOL)transactionIsValid:(DSTransaction *)transaction { } } +- (BOOL)isSpent:(NSValue *)output { + if (!output) { + return false; + } + + return [self.spentOutputs containsObject:output]; +} + +- (int64_t)inputValue:(UInt256)txHash inputIndex:(uint32_t)index { + NSValue *hash = uint256_obj(txHash); + DSTransaction *tx = self.allTx[hash]; + + if (tx == NULL) { + return -1; + } + + if (![self transactionIsValid:tx] || + [self.spentOutputs containsObject:dsutxo_obj(((DSUTXO){txHash, index}))]) { + return -1; + } + + return (int64_t)tx.outputs[index].amount; +} + // true if transaction cannot be immediately spent (i.e. if it or an input tx can be replaced-by-fee) - (BOOL)transactionIsPending:(DSTransaction *)transaction { NSParameterAssert(transaction); @@ -1635,6 +1711,24 @@ - (uint64_t)amountSentByTransaction:(DSTransaction *)transaction { return amount; } +// Returns the amounts sent by the transaction +- (NSArray *)amountsSentByTransaction:(DSTransaction *)transaction { + NSMutableArray *amounts = [NSMutableArray array]; + + for (DSTransactionInput *input in transaction.inputs) { + DSTransaction *tx = self.allTx[uint256_obj(input.inputHash)]; + uint64_t amount = tx.outputs[input.index].amount; + + if (amount > 0) { + [amounts addObject:@(amount)]; + } else { + [amounts addObject:@(0)]; + } + } + + return amounts; +} + // MARK: = Addresses - (NSArray *)externalAddressesOfTransaction:(DSTransaction *)transaction { diff --git a/DashSync/shared/Models/Wallet/DSWallet+Protected.h b/DashSync/shared/Models/Wallet/DSWallet+Protected.h index db2561f98..5b664e7e8 100644 --- a/DashSync/shared/Models/Wallet/DSWallet+Protected.h +++ b/DashSync/shared/Models/Wallet/DSWallet+Protected.h @@ -25,8 +25,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSString *creationTimeUniqueID; -@property (nonatomic, strong) SeedRequestBlock seedRequestBlock; - @property (nonatomic, strong) SecureSeedRequestBlock secureSeedRequestBlock; @property (nonatomic, readonly) BOOL hasAnExtendedPublicKeyMissing; @@ -48,6 +46,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)loadBlockchainIdentities; +- (NSData *_Nullable)requestSeedNoAuth; + @end NS_ASSUME_NONNULL_END diff --git a/DashSync/shared/Models/Wallet/DSWallet.h b/DashSync/shared/Models/Wallet/DSWallet.h index f6a3d6993..2416a1154 100644 --- a/DashSync/shared/Models/Wallet/DSWallet.h +++ b/DashSync/shared/Models/Wallet/DSWallet.h @@ -175,7 +175,7 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSWalletBalanceDidChangeNotification; // returns the transaction with the given hash if it's been registered in the wallet (might also return non-registered) - (DSTransaction *_Nullable)transactionForHash:(UInt256)txHash; -- (NSArray *)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit internal:(BOOL)internal error:(NSError *_Nullable *_Nullable)error; +- (NSArray *)registerAddressesWithGapLimit:(NSUInteger)gapLimit unusedAccountGapLimit:(NSUInteger)unusedAccountGapLimit dashpayGapLimit:(NSUInteger)dashpayGapLimit coinJoinGapLimit:(NSUInteger)coinJoinGapLimit internal:(BOOL)internal error:(NSError *_Nullable *_Nullable)error; // returns the amount received by the wallet from the transaction (total outputs to change and/or receive addresses) - (uint64_t)amountReceivedFromTransaction:(DSTransaction *)transaction; @@ -189,6 +189,9 @@ FOUNDATION_EXPORT NSString *_Nonnull const DSWalletBalanceDidChangeNotification; // true if no previous wallet transaction spends any of the given transaction's inputs, and no inputs are invalid - (BOOL)transactionIsValid:(DSTransaction *)transaction; +// returns input value if no previous wallet transaction spends this input, and the input is valid, -1 otherwise. +- (int64_t)inputValue:(UInt256)txHash inputIndex:(uint32_t)index; + // this is used to save transactions atomically with the block, needs to be called before switching threads to save the block - (void)prepareForIncomingTransactionPersistenceForBlockSaveWithNumber:(uint32_t)blockNumber; diff --git a/DashSync/shared/Models/Wallet/DSWallet.m b/DashSync/shared/Models/Wallet/DSWallet.m index 9eee22251..4a619cc4c 100644 --- a/DashSync/shared/Models/Wallet/DSWallet.m +++ b/DashSync/shared/Models/Wallet/DSWallet.m @@ -185,12 +185,6 @@ - (instancetype)initWithUniqueID:(NSString *)uniqueID andAccounts:(NSArray - + - + + @@ -20,14 +21,14 @@ - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + @@ -117,7 +141,7 @@ - + @@ -131,14 +155,14 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/DashSync/Base.lproj/Main.storyboard b/Example/DashSync/Base.lproj/Main.storyboard index d07e791d3..9b8c710de 100644 --- a/Example/DashSync/Base.lproj/Main.storyboard +++ b/Example/DashSync/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -81,7 +81,7 @@ - + @@ -121,10 +121,10 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/Example/Podfile.lock b/Example/Podfile.lock index c12111213..328f060fd 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -586,11 +586,11 @@ PODS: - "!ProtoCompiler-gRPCPlugin (~> 1.0)" - DAPI-GRPC/Messages - gRPC-ProtoRPC - - DashSharedCore (0.4.19) + - DashSharedCore (0.5.1) - DashSync (0.1.0): - CocoaLumberjack (= 3.7.2) - DAPI-GRPC (= 0.22.0-dev.8) - - DashSharedCore (= 0.4.19) + - DashSharedCore (= 0.5.1) - DSDynamicOptions (= 0.1.2) - DWAlertController (= 0.2.1) - TinyCborObjc (= 0.4.6) @@ -657,7 +657,7 @@ PODS: - gRPC/Interface-Legacy (1.49.0): - gRPC-RxLibrary/Interface (= 1.49.0) - KVO-MVVM (0.5.1) - - Protobuf (3.28.3) + - Protobuf (3.29.2) - SDWebImage (5.14.3): - SDWebImage/Core (= 5.14.3) - SDWebImage/Core (5.14.3) @@ -712,8 +712,8 @@ SPEC CHECKSUMS: CocoaImageHashing: 8656031d0899abe6c1c415827de43e9798189c53 CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da DAPI-GRPC: 138d62523bbfe7e88a39896f1053c0bc12390d9f - DashSharedCore: 009f29640756017406ee2b0ab5f1073f1856f85e - DashSync: f0ee76fe1409c9071bcee21202cc8002944c801d + DashSharedCore: b8481feb5f08acf162b548edbfc7a9b1ce491141 + DashSync: 1f5741aad267dcc0cfc04b6903490d304232ddf2 DSDynamicOptions: 347cc5d2c4e080eb3de6a86719ad3d861b82adfc DWAlertController: 5f4cd8adf90336331c054857f709f5f8d4b16a5b gRPC: 64f36d689b2ecd99c4351f74e6f91347cdc65d9f @@ -721,7 +721,7 @@ SPEC CHECKSUMS: gRPC-ProtoRPC: 1c223e0f1732bb8d0b9e9e0ea60cc0fe995b8e2d gRPC-RxLibrary: 92327f150e11cf3b1c0f52e083944fd9f5cb5d1e KVO-MVVM: 4df3afd1f7ebcb69735458b85db59c4271ada7c6 - Protobuf: 5a8a7781d8e1004302f108977ac2d5b99323146f + Protobuf: ba5d83b2201386fec27d484c099cac510ea5c169 SDWebImage: 9c36e66c8ce4620b41a7407698dda44211a96764 tinycbor: d4d71dddda1f8392fbb4249f63faf8552f327590 TinyCborObjc: 5204540fb90ff0c40fb22d408fa51bab79d78a80 diff --git a/Example/Tests/CoinJoinTests.xctestplan b/Example/Tests/CoinJoinTests.xctestplan new file mode 100644 index 000000000..5909fff17 --- /dev/null +++ b/Example/Tests/CoinJoinTests.xctestplan @@ -0,0 +1,60 @@ +{ + "configurations" : [ + { + "id" : "86CE06DB-48AE-4956-B3C5-C4AF38BA2644", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "skippedTests" : [ + "DSAttackTests", + "DSBIP32Tests", + "DSBIP39Tests", + "DSBigNumberTests", + "DSBloomFilterTests", + "DSChainLockTests", + "DSChainTests", + "DSChainedSigningTests", + "DSDIP14Tests", + "DSDataTests", + "DSDeterministicMasternodeListTests", + "DSGovernanceTests", + "DSHashTests", + "DSIESEncryptedDataTests", + "DSInstantSendLockTests", + "DSInvitationsTests", + "DSKeyTests", + "DSMainnetE2ETests", + "DSMainnetMetricSyncTests", + "DSMainnetSyncTests", + "DSMiningTests", + "DSPaymentProtocolTests", + "DSPaymentRequestTests", + "DSProviderTransactionsTests", + "DSSparseMerkleTreeTests", + "DSTestnetE2ETests", + "DSTestnetMetricSyncTests", + "DSTestnetSyncTests", + "DSTransactionTests", + "DSTransitionTests", + "DSUInt256IndexPathTests", + "DSWalletTests", + "MDCDamerauLevenshteinDistanceTests", + "MDCLevenshteinDistanceTests" + ], + "target" : { + "containerPath" : "container:DashSync.xcodeproj", + "identifier" : "6003F5AD195388D20070C39A", + "name" : "DashSync_Tests" + } + } + ], + "version" : 1 +} diff --git a/Example/Tests/CryptoTests.xctestplan b/Example/Tests/CryptoTests.xctestplan index 844bc4278..ddd1bf30a 100644 --- a/Example/Tests/CryptoTests.xctestplan +++ b/Example/Tests/CryptoTests.xctestplan @@ -34,6 +34,7 @@ "DSBloomFilterTests", "DSChainLockTests", "DSChainTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/DSCoinJoinSessionTest.m b/Example/Tests/DSCoinJoinSessionTest.m new file mode 100644 index 000000000..2d1fa6c0a --- /dev/null +++ b/Example/Tests/DSCoinJoinSessionTest.m @@ -0,0 +1,44 @@ +// +// Created by Andrei Ashikhmin +// Copyright © 2024 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +#import + +@interface DSCoinJoinSessionTest : XCTestCase + +@end + +@implementation DSCoinJoinSessionTest + +- (void)sessionTest { + CoinJoinClientOptions *options = malloc(sizeof(CoinJoinClientOptions)); + options->enable_coinjoin = YES; + options->coinjoin_rounds = 1; + options->coinjoin_sessions = 1; + options->coinjoin_amount = DUFFS / 4; // 0.25 DASH + options->coinjoin_random_rounds = COINJOIN_RANDOM_ROUNDS; // TODO: check + options->coinjoin_denoms_goal = DEFAULT_COINJOIN_DENOMS_GOAL; + options->coinjoin_denoms_hardcap = DEFAULT_COINJOIN_DENOMS_HARDCAP; + options->coinjoin_multi_session = NO; + + // TODO: session test + + free(options); +} + +@end diff --git a/Example/Tests/DerivationTests.xctestplan b/Example/Tests/DerivationTests.xctestplan index 24bbb3fa3..dc5494cc1 100644 --- a/Example/Tests/DerivationTests.xctestplan +++ b/Example/Tests/DerivationTests.xctestplan @@ -22,6 +22,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDataTests", "DSDeterministicMasternodeListTests", "DSGovernanceTests", diff --git a/Example/Tests/FullUnitTestPlan.xctestplan b/Example/Tests/FullUnitTestPlan.xctestplan index d33537564..d7bd388cc 100644 --- a/Example/Tests/FullUnitTestPlan.xctestplan +++ b/Example/Tests/FullUnitTestPlan.xctestplan @@ -15,6 +15,7 @@ { "skippedTests" : [ "DSAttackTests", + "DSCoinJoinSessionTest", "DSMainnetE2ETests", "DSMainnetMetricSyncTests", "DSMainnetSyncTests", diff --git a/Example/Tests/GovernanceTests.xctestplan b/Example/Tests/GovernanceTests.xctestplan index 474a6f8c2..e3219f0f3 100644 --- a/Example/Tests/GovernanceTests.xctestplan +++ b/Example/Tests/GovernanceTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/LibraryTests.xctestplan b/Example/Tests/LibraryTests.xctestplan index f0a38d13a..65cd295d7 100644 --- a/Example/Tests/LibraryTests.xctestplan +++ b/Example/Tests/LibraryTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDeterministicMasternodeListTests", diff --git a/Example/Tests/LockTests.xctestplan b/Example/Tests/LockTests.xctestplan index 02e33f8c8..bcf2495b2 100644 --- a/Example/Tests/LockTests.xctestplan +++ b/Example/Tests/LockTests.xctestplan @@ -22,6 +22,7 @@ "DSBloomFilterTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDataTests", "DSDeterministicMasternodeListTests", diff --git a/Example/Tests/MainnetSyncTests.xctestplan b/Example/Tests/MainnetSyncTests.xctestplan index 3c6f5f66c..338858f46 100644 --- a/Example/Tests/MainnetSyncTests.xctestplan +++ b/Example/Tests/MainnetSyncTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/MasternodeListTests.xctestplan b/Example/Tests/MasternodeListTests.xctestplan index e34fa0bca..6110205f0 100644 --- a/Example/Tests/MasternodeListTests.xctestplan +++ b/Example/Tests/MasternodeListTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/Metrics.xctestplan b/Example/Tests/Metrics.xctestplan index 9eb7df302..def05c053 100644 --- a/Example/Tests/Metrics.xctestplan +++ b/Example/Tests/Metrics.xctestplan @@ -24,6 +24,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/PaymentTests.xctestplan b/Example/Tests/PaymentTests.xctestplan index 0302c7fe8..09b32278d 100644 --- a/Example/Tests/PaymentTests.xctestplan +++ b/Example/Tests/PaymentTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/PlatformTransitionTests.xctestplan b/Example/Tests/PlatformTransitionTests.xctestplan index c28aac178..e0b432e03 100644 --- a/Example/Tests/PlatformTransitionTests.xctestplan +++ b/Example/Tests/PlatformTransitionTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/TestnetE2ETests.xctestplan b/Example/Tests/TestnetE2ETests.xctestplan index 5ebd633e2..4ef528e14 100644 --- a/Example/Tests/TestnetE2ETests.xctestplan +++ b/Example/Tests/TestnetE2ETests.xctestplan @@ -22,6 +22,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/TestnetSyncTests.xctestplan b/Example/Tests/TestnetSyncTests.xctestplan index 8cb7b4d7e..e9be7c98b 100644 --- a/Example/Tests/TestnetSyncTests.xctestplan +++ b/Example/Tests/TestnetSyncTests.xctestplan @@ -22,6 +22,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/TransactionTests.xctestplan b/Example/Tests/TransactionTests.xctestplan index 851737b7d..958d72896 100644 --- a/Example/Tests/TransactionTests.xctestplan +++ b/Example/Tests/TransactionTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests", diff --git a/Example/Tests/WalletTests.xctestplan b/Example/Tests/WalletTests.xctestplan index 75a82735d..a7cdee10e 100644 --- a/Example/Tests/WalletTests.xctestplan +++ b/Example/Tests/WalletTests.xctestplan @@ -23,6 +23,7 @@ "DSChainLockTests", "DSChainTests", "DSChainedSigningTests", + "DSCoinJoinSessionTest", "DSDIP13Tests", "DSDIP14Tests", "DSDataTests",