diff --git a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj index 8927e911..79eb2fe5 100644 --- a/TangemSdk/TangemSdk.xcodeproj/project.pbxproj +++ b/TangemSdk/TangemSdk.xcodeproj/project.pbxproj @@ -288,6 +288,10 @@ DC70AD652A80FC9F00928836 /* CommonFirmwareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC70AD642A80FC9F00928836 /* CommonFirmwareTests.swift */; }; DC70AD672A8115BB00928836 /* FWTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC70AD662A8115BB00928836 /* FWTestCase.swift */; }; DC7254902A03E20A0003FE1B /* DerivedKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC72548F2A03E20A0003FE1B /* DerivedKeys.swift */; }; + DC77F24D2CD42610001B2929 /* ChunkHashesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77F24C2CD4260A001B2929 /* ChunkHashesTests.swift */; }; + DC77F24F2CD426B6001B2929 /* ChunkedHashesContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77F24E2CD426B6001B2929 /* ChunkedHashesContainer.swift */; }; + DC77F2512CD426E3001B2929 /* SignDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77F2502CD426E3001B2929 /* SignDTO.swift */; }; + DC77F2532CD430A3001B2929 /* ChunkHashesUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77F2522CD43099001B2929 /* ChunkHashesUtil.swift */; }; DC8B0E3F286F221D009D64F7 /* BiometricsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8B0E3E286F221D009D64F7 /* BiometricsUtil.swift */; }; DCA9706628E35EAD0046E62E /* GenerateOTPCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9706528E35EAD0046E62E /* GenerateOTPCommand.swift */; }; DCACA0402CB51FF400A3DD51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCACA03F2CB51FF400A3DD51 /* Assets.xcassets */; }; @@ -679,6 +683,10 @@ DC70AD642A80FC9F00928836 /* CommonFirmwareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonFirmwareTests.swift; sourceTree = ""; }; DC70AD662A8115BB00928836 /* FWTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FWTestCase.swift; sourceTree = ""; }; DC72548F2A03E20A0003FE1B /* DerivedKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DerivedKeys.swift; sourceTree = ""; }; + DC77F24C2CD4260A001B2929 /* ChunkHashesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkHashesTests.swift; sourceTree = ""; }; + DC77F24E2CD426B6001B2929 /* ChunkedHashesContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkedHashesContainer.swift; sourceTree = ""; }; + DC77F2502CD426E3001B2929 /* SignDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignDTO.swift; sourceTree = ""; }; + DC77F2522CD43099001B2929 /* ChunkHashesUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChunkHashesUtil.swift; sourceTree = ""; }; DC8B0E3E286F221D009D64F7 /* BiometricsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsUtil.swift; sourceTree = ""; }; DCA9706528E35EAD0046E62E /* GenerateOTPCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateOTPCommand.swift; sourceTree = ""; }; DCACA03F2CB51FF400A3DD51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -986,7 +994,10 @@ 5D46F156268105A500DC6447 /* Sign */ = { isa = PBXGroup; children = ( + DC77F2522CD43099001B2929 /* ChunkHashesUtil.swift */, + DC77F24E2CD426B6001B2929 /* ChunkedHashesContainer.swift */, 5D6A92E42345F2B200158457 /* SignCommand.swift */, + DC77F2502CD426E3001B2929 /* SignDTO.swift */, 5D46F1542681032B00DC6447 /* SignHashCommand.swift */, 5D46F157268105BF00DC6447 /* SignHashesCommand.swift */, ); @@ -1290,6 +1301,7 @@ DC0665562A7AC8F500CFFCC6 /* Ed25519Slip0010Tests.swift */, DC70AD642A80FC9F00928836 /* CommonFirmwareTests.swift */, DC70AD662A8115BB00928836 /* FWTestCase.swift */, + DC77F24C2CD4260A001B2929 /* ChunkHashesTests.swift */, ); path = TangemSdkTests; sourceTree = ""; @@ -2052,6 +2064,7 @@ 5D6A92E72345F2D600158457 /* AttestWalletKeyCommand.swift in Sources */, 5D73FC2926B8140200DF1BB4 /* DerivationPath.swift in Sources */, B06EBBC12534794100B0FEEA /* ChangeFileSettingsCommand.swift in Sources */, + DC77F24F2CD426B6001B2929 /* ChunkedHashesContainer.swift in Sources */, 5DDD6C6C25D30B0D00E48D7B /* SuccessResponse.swift in Sources */, DC59CB0429AF597900EC14E1 /* Wordlist.swift in Sources */, 5D8666622731687A0095CC82 /* ResetCodesViewModel.swift in Sources */, @@ -2108,6 +2121,7 @@ 5DA5B618233E124A0058C720 /* StatusWord.swift in Sources */, 5D866658273163BB0095CC82 /* BaseViewDelegate.swift in Sources */, B0A9447B256546DE00A7958E /* Card.swift in Sources */, + DC77F2512CD426E3001B2929 /* SignDTO.swift in Sources */, DCFCA17728F5629F0037586C /* FocusableTextField.swift in Sources */, B06EBBC525347B7800B0FEEA /* ChangeFileSettingsTask.swift in Sources */, 5D0F8D0226C6A80F002E84A4 /* UserCodeHeaderView.swift in Sources */, @@ -2168,6 +2182,7 @@ 5D705B5B23DAF2BB002CCD7A /* Config.swift in Sources */, DC612D722AFD60C2005A547F /* SessionFilter.swift in Sources */, 5D6A92EC2346069700158457 /* TangemSdk.swift in Sources */, + DC77F2532CD430A3001B2929 /* ChunkHashesUtil.swift in Sources */, DC1244B329B60B6F0037BC05 /* BIP39.swift in Sources */, 5DFFC49F233B9D69004964E8 /* NFCReader.swift in Sources */, 5DE43A6626D515B100ECA36A /* FinalizePrimaryCardTask.swift in Sources */, @@ -2209,6 +2224,7 @@ DC1244C929B778750037BC05 /* BIP32Tests.swift in Sources */, DC70AD652A80FC9F00928836 /* CommonFirmwareTests.swift in Sources */, DC3D980A2A792804001EEE7A /* KeysImportTests.swift in Sources */, + DC77F24D2CD42610001B2929 /* ChunkHashesTests.swift in Sources */, DC1244E429BB806E0037BC05 /* WIFTests.swift in Sources */, DC1244B929B610550037BC05 /* BIP39Tests.swift in Sources */, 5DD127A224F3D1A0009ACA29 /* JsonTests.swift in Sources */, diff --git a/TangemSdk/TangemSdk/Common/Core/TangemSdkError.swift b/TangemSdk/TangemSdk/Common/Core/TangemSdkError.swift index 672d8394..df29b80e 100644 --- a/TangemSdk/TangemSdk/Common/Core/TangemSdkError.swift +++ b/TangemSdk/TangemSdk/Common/Core/TangemSdkError.swift @@ -125,10 +125,7 @@ public enum TangemSdkError: Error, LocalizedError, Encodable { /// This error is returned when a `SignCommand` receives only empty hashes for signature. case emptyHashes - - /// This error is returned when a `SignCommand` receives hashes of different lengths for signature. - case hashSizeMustBeEqual - + case signHashesNotAvailable // Write Extra Issuer Data Errors @@ -370,7 +367,6 @@ public enum TangemSdkError: Error, LocalizedError, Encodable { case .noRemainingSignatures: return 40901 case .emptyHashes: return 40902 - case .hashSizeMustBeEqual: return 40903 case .signHashesNotAvailable: return 40905 case .oldCard: return 40907 diff --git a/TangemSdk/TangemSdk/Operations/Sign/ChunkHashesUtil.swift b/TangemSdk/TangemSdk/Operations/Sign/ChunkHashesUtil.swift new file mode 100644 index 00000000..91c420cf --- /dev/null +++ b/TangemSdk/TangemSdk/Operations/Sign/ChunkHashesUtil.swift @@ -0,0 +1,56 @@ +// +// ChunkHashesUtil.swift +// TangemSdk +// +// Created by Alexander Osokin on 31.10.2024. +// Copyright © 2024 Tangem AG. All rights reserved. +// + +struct ChunkHashesUtil { + func chunkHashes(_ hashes: [Data]) -> [Chunk] { + let hashes = hashes.enumerated().map { Hash(index: $0.offset, data: $0.element) } + let hashesBySize = Dictionary(grouping: hashes, by: { $0.data.count }) + + let chunks = hashesBySize.flatMap { hashesGroup in + let hashSize = hashesGroup.key + let chunkSize = getChunkSize(for: hashSize) + + let chunkedHashes = hashesGroup.value.chunked(into: chunkSize) + let chunks = chunkedHashes.map { Chunk(hashSize: hashSize, hashes: $0) } + + return chunks + } + + return chunks + } + + func getChunkSize(for hashSize: Int) -> Int { + /// These devices are not able to sign long hashes. + if NFCUtils.isPoorNfcQualityDevice { + return Constants.maxChunkSizePoorNfcQualityDevice + } + + guard hashSize > 0 else { + return Constants.maxChunkSize + } + + let estimatedChunkSize = Constants.packageSize / hashSize + let chunkSize = max(1, min(estimatedChunkSize, Constants.maxChunkSize)) + return chunkSize + } +} + +// MARK: - Constants + +private extension ChunkHashesUtil { + enum Constants { + /// The max answer is 1152 bytes (unencrypted) and 1120 (encrypted). The worst case is 8 hashes * 64 bytes for ed + 512 bytes of signatures + cardId, SignedHashes + TLV + SW is ok. + static let packageSize = 512 + + /// Card limitation + static let maxChunkSize = 10 + + /// Empirical value + static let maxChunkSizePoorNfcQualityDevice = 2 + } +} diff --git a/TangemSdk/TangemSdk/Operations/Sign/ChunkedHashesContainer.swift b/TangemSdk/TangemSdk/Operations/Sign/ChunkedHashesContainer.swift new file mode 100644 index 00000000..a6117b45 --- /dev/null +++ b/TangemSdk/TangemSdk/Operations/Sign/ChunkedHashesContainer.swift @@ -0,0 +1,51 @@ +// +// ChunkedHashesContainer.swift +// TangemSdk +// +// Created by Alexander Osokin on 31.10.2024. +// Copyright © 2024 Tangem AG. All rights reserved. +// + + +import Foundation + +struct ChunkedHashesContainer { + var isEmpty: Bool { chunks.isEmpty } + let chunksCount: Int + + private(set) var currentChunkIndex: Int = 0 + + private let chunks: [Chunk] + private var signedChunks: [SignedChunk] = [] + + init(hashes: [Data]) { + self.chunks = ChunkHashesUtil().chunkHashes(hashes) + self.chunksCount = chunks.count + } + + func getCurrentChunk() throws -> Chunk { + guard currentChunkIndex < chunks.count else { + throw ChunkedHashesContainerError.processingError + } + + return chunks[currentChunkIndex] + } + + mutating func addSignedChunk(_ signedChunk: SignedChunk) { + signedChunks.append(signedChunk) + currentChunkIndex += 1 + } + + func getSignatures() -> [Data] { + let signedHashes = signedChunks.flatMap { $0.signedHashes }.sorted() + let signatures = signedHashes.map { $0.signature } + return signatures + } +} + +// MARK: - ChunkedHashesContainerError + +enum ChunkedHashesContainerError: Error { + case processingError +} + diff --git a/TangemSdk/TangemSdk/Operations/Sign/SignCommand.swift b/TangemSdk/TangemSdk/Operations/Sign/SignCommand.swift index 2f215c99..117d142b 100644 --- a/TangemSdk/TangemSdk/Operations/Sign/SignCommand.swift +++ b/TangemSdk/TangemSdk/Operations/Sign/SignCommand.swift @@ -20,45 +20,25 @@ public struct SignResponse: JSONStringConvertible { /// Signs transaction hashes using a wallet private key, stored on the card. class SignCommand: Command { + typealias Response = SignResponse + typealias CommandResponse = PartialSignResponse + var requiresPasscode: Bool { return true } private let walletPublicKey: Data private let derivationPath: DerivationPath? - private let hashes: [Data] - private var signatures: [Data] = [] - - private var currentChunkNumber: Int { - signatures.count / chunkSize - } - - private lazy var chunkSize: Int = { - /// These devices are not able to sign long hashes. - if NFCUtils.isPoorNfcQualityDevice { - return Constants.maxChunkSizePoorNfcQualityDevice - } - if let hashSize = hashes.first?.count, hashSize > 0 { - let estimatedChunkSize = Constants.packageSize / hashSize - let chunkSize = max(1, min(estimatedChunkSize, Constants.maxChunkSize)) - return chunkSize - } + private var chunkHashesHelper: ChunkedHashesContainer - return Constants.maxChunkSize - }() - - private lazy var numberOfChunks: Int = { - return stride(from: 0, to: hashes.count, by: chunkSize).underestimatedCount - }() - /// Command initializer /// - Parameters: /// - hashes: Array of transaction hashes. /// - walletPublicKey: Public key of the wallet, using for sign. /// - derivationPath: Derivation path of the wallet. Optional. COS v. 4.28 and higher, init(hashes: [Data], walletPublicKey: Data, derivationPath: DerivationPath? = nil) { - self.hashes = hashes self.walletPublicKey = walletPublicKey self.derivationPath = derivationPath + self.chunkHashesHelper = ChunkedHashesContainer(hashes: hashes) } deinit { @@ -104,16 +84,11 @@ class SignCommand: Command { } func run(in session: CardSession, completion: @escaping CompletionResult) { - if hashes.isEmpty { + if chunkHashesHelper.isEmpty { completion(.failure(.emptyHashes)) return } - if hashes.contains(where: { $0.count != hashes.first!.count }) { - completion(.failure(.hashSizeMustBeEqual)) - return - } - sign(in: session, completion: completion) } @@ -126,23 +101,25 @@ class SignCommand: Command { } func sign(in session: CardSession, completion: @escaping CompletionResult) { - if numberOfChunks > 1 { - session.viewDelegate.showAlertMessage("sign_multiple_chunks_part".localized([currentChunkNumber + 1, numberOfChunks])) + if chunkHashesHelper.chunksCount > 1 { + session.viewDelegate.showAlertMessage("sign_multiple_chunks_part".localized([chunkHashesHelper.currentChunkIndex + 1, chunkHashesHelper.chunksCount])) } transceive(in: session) { result in switch result { case .success(let response): - self.signatures.append(contentsOf: response.signatures) - if self.signatures.count == self.hashes.count { + self.chunkHashesHelper.addSignedChunk(response.signedChunk) + + if self.chunkHashesHelper.currentChunkIndex >= self.chunkHashesHelper.chunksCount { session.environment.card?.wallets[self.walletPublicKey]?.totalSignedHashes = response.totalSignedHashes - - if let remainingSignatures = session.environment.card?.wallets[self.walletPublicKey]?.remainingSignatures { - session.environment.card?.wallets[self.walletPublicKey]?.remainingSignatures = remainingSignatures - self.signatures.count - } - + do { let signatures = try self.processSignatures(with: session.environment) + + if let remainingSignatures = session.environment.card?.wallets[self.walletPublicKey]?.remainingSignatures { + session.environment.card?.wallets[self.walletPublicKey]?.remainingSignatures = remainingSignatures - signatures.count + } + completion(.success(SignResponse(cardId: response.cardId, signatures: signatures, totalSignedHashes: response.totalSignedHashes))) @@ -165,16 +142,17 @@ class SignCommand: Command { } } - func serialize(with environment: SessionEnvironment) throws -> CommandApdu { guard let walletIndex = environment.card?.wallets[walletPublicKey]?.index else { throw TangemSdkError.walletNotFound } + + let chunk = try chunkHashesHelper.getCurrentChunk() - let hashSize = hashes.first!.count + let hashSize = chunk.hashSize let hashSizeData = hashSize > 255 ? hashSize.bytes2 : hashSize.byte - - let flattenHashes = Data(hashes[getChunk()].joined()) + + let flattenHashes = Data(chunk.hashes.flatMap { $0.data }) let tlvBuilder = try createTlvBuilder(legacyMode: environment.legacyMode) .append(.pin, value: environment.accessCode.value) .append(.pin2, value: environment.passcode.value) @@ -211,24 +189,43 @@ class SignCommand: Command { return CommandApdu(.sign, tlv: tlvBuilder.serialize()) } - func deserialize(with environment: SessionEnvironment, from apdu: ResponseApdu) throws -> SignResponse { + func deserialize(with environment: SessionEnvironment, from apdu: ResponseApdu) throws -> PartialSignResponse { guard let tlv = apdu.getTlvData(encryptionKey: environment.encryptionKey) else { throw TangemSdkError.deserializeApduFailed } let decoder = TlvDecoder(tlv: tlv) - let splittedSignatures = splitSignedSignature(try decoder.decode(.walletSignature), numberOfSignatures: getChunk().underestimatedCount) - let resp = SignResponse(cardId: try decoder.decode(.cardId), - signatures: splittedSignatures, - totalSignedHashes: try decoder.decode(.walletSignedHashes)) - return resp + let chunk = try chunkHashesHelper.getCurrentChunk() + + let signatureBLOB: Data = try decoder.decode(.walletSignature) + let signatures = splitSignatureBLOB(signatureBLOB, numberOfSignatures: chunk.hashes.count) + + let signedHashes = zip(chunk.hashes, signatures).map { (hash, signature) in + SignedHash( + index: hash.index, + data: hash.data, + signature: signature + ) + } + + let signedChunk = SignedChunk(signedHashes: signedHashes) + + let response = PartialSignResponse( + cardId: try decoder.decode(.cardId), + signedChunk: signedChunk, + totalSignedHashes: try decoder.decode(.walletSignedHashes) + ) + + return response } private func processSignatures(with environment: SessionEnvironment) throws -> [Data] { + let signatures = chunkHashesHelper.getSignatures() + if environment.card?.wallets[self.walletPublicKey]?.curve == .secp256k1, environment.config.canonizeSecp256k1Signatures { let secp256k1 = Secp256k1Utils() - let normalizedSignatures = try self.signatures.map { try secp256k1.normalizeSignature($0) } + let normalizedSignatures = try signatures.map { try secp256k1.normalizeSignature($0) } if normalizedSignatures.count != signatures.count { throw TangemSdkError.cryptoUtilsError("Normalization error") } @@ -236,16 +233,10 @@ class SignCommand: Command { return normalizedSignatures } - return self.signatures - } - - private func getChunk() -> Range { - let from = currentChunkNumber * chunkSize - let to = min(from + chunkSize, hashes.count) - return from.. [Data] { + private func splitSignatureBLOB(_ signature: Data, numberOfSignatures: Int) -> [Data] { var signatures = [Data]() let signatureSize = signature.count / numberOfSignatures for index in 0.. Bool { + lhs.index < rhs.index + } +} + +// MARK: - Chunk + +struct Chunk: Equatable { + let hashSize: Int + let hashes: [Hash] +} + +// MARK: - SignedChunk + +struct SignedChunk { + let signedHashes: [SignedHash] +} diff --git a/TangemSdk/TangemSdkTests/ChunkHashesTests.swift b/TangemSdk/TangemSdkTests/ChunkHashesTests.swift new file mode 100644 index 00000000..8f590f95 --- /dev/null +++ b/TangemSdk/TangemSdkTests/ChunkHashesTests.swift @@ -0,0 +1,152 @@ +// +// ChunkHashesTests.swift +// TangemSdk +// +// Created by Alexander Osokin on 31.10.2024. +// Copyright © 2024 Tangem AG. All rights reserved. +// + + +import Foundation +import XCTest +@testable import TangemSdk + +class ChunkHashesTests: XCTestCase { + func testSingleHashChunk() { + let testData = ["f1642bb080e1f320924dde7238c1c5f8"] + + let hashes = testData.map { Data(hexString: $0) } + let util = ChunkHashesUtil() + + let chunks = util.chunkHashes(hashes) + XCTAssertEqual(chunks.count, 1) + + let expectedChunk = Chunk(hashSize: 16, hashes: [Hash(index: 0, data: hashes[0])]) + XCTAssertEqual(chunks, [expectedChunk]) + } + + func testMultipleHashesChunk() { + let testData = [ + "f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f0", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f1", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f2", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f3", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f4", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f5", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f6", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f7", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f9", + "f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8aa", + "f1642bb080e1f320924dde7238c1c5f8ab", + ] + + let hashes = testData.map { Data(hexString: $0) } + let util = ChunkHashesUtil() + + let chunks = util.chunkHashes(hashes) + XCTAssertEqual(chunks.count, 4) + + let expectedChunks = [ + Chunk( + hashSize: 16, + hashes: [ + Hash(index: 0, data: hashes[0]), + Hash(index: 12, data: hashes[12]) + ] + ), + Chunk( + hashSize: 17, + hashes: [ + Hash(index: 13, data: hashes[13]), + Hash(index: 14, data: hashes[14]) + ] + ), + Chunk( + hashSize: 32, + hashes: [ + Hash(index: 1, data: hashes[1]), + Hash(index: 2, data: hashes[2]), + Hash(index: 3, data: hashes[3]), + Hash(index: 4, data: hashes[4]), + Hash(index: 5, data: hashes[5]), + Hash(index: 6, data: hashes[6]), + Hash(index: 7, data: hashes[7]), + Hash(index: 8, data: hashes[8]), + Hash(index: 9, data: hashes[9]), + Hash(index: 10, data: hashes[10]), + ] + ), + Chunk( + hashSize: 32, + hashes: [ + Hash(index: 11, data: hashes[11]) + ] + ) + ] + + XCTAssertEqual(chunks.sorted(by: { $0.hashSize < $1.hashSize }), expectedChunks.sorted(by: { $0.hashSize < $1.hashSize })) + } + + func testStrictSignaturesOrder() throws { + let testHashesData = [ + "f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f0", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f1", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f2", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f3", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f4", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f5", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f6", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f7", + "f1642bb080e1f320924dde7238c1c5f8f1642bb080e1f320924dde7238c1c5f9", + "f1642bb080e1f320924dde7238c1c5f8", + "f1642bb080e1f320924dde7238c1c5f8aa", + "f1642bb080e1f320924dde7238c1c5f8ab", + ] + + let testSignaturesData = [ + "0001", + "0002", + "0003", + "0004", + "0005", + "0006", + "0007", + "0008", + "0009", + "0010", + "0011", + "0012", + "0013", + "0014", + "0015", + ] + + let hashes = testHashesData.map { Data(hexString: $0) } + let expectedSignatures = testSignaturesData.map { Data(hexString: $0) } + + var container = ChunkedHashesContainer(hashes: hashes) + + for _ in 0..