From 006861595d020dc34f31565ff07b4a2ce556c8d1 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 28 Mar 2024 10:23:49 -0400 Subject: [PATCH] Fetch and store Orchard subtree roots --- CHANGELOG.md | 3 + .../xcshareddata/swiftpm/Package.resolved | 4 +- Package.resolved | 4 +- Package.swift | 2 +- .../Actions/UpdateSubtreeRootsAction.swift | 39 +++++++++++-- .../Error/ZcashError.swift | 18 ++++++ .../Error/ZcashErrorCode.swift | 6 ++ .../Error/ZcashErrorCodeDefinition.swift | 10 ++++ .../Rust/ZcashRustBackend.swift | 58 +++++++++++++++++++ .../Rust/ZcashRustBackendWelding.swift | 2 + .../UpdateSubtreeRootsActionTests.swift | 36 +++++++++++- .../AutoMockable.generated.swift | 25 ++++++++ 12 files changed, 195 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7dc92c..ddbbcb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +## Fixed +- Orchard subtree roots are now fetched alongside Sapling subtree roots. + # 2.1.2 - 2024-03-27 ## Fixed diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a38445c3..2cc78dbb 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "8838b4f0ee4193349fed09f0248220d4ada271fc", - "version" : "0.7.3" + "revision" : "e2d8763f3a963fb0026b6160af2d211b527453cd", + "version" : "0.7.4" } } ], diff --git a/Package.resolved b/Package.resolved index cb45488b..30efaf66 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c7e5158edf5e62af15492d30237163b78af35ce9", - "version" : "0.7.1" + "revision" : "e2d8763f3a963fb0026b6160af2d211b527453cd", + "version" : "0.7.4" } } ], diff --git a/Package.swift b/Package.swift index 2b6c074b..58644946 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), - .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.7.3") + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", exact: "0.7.4") ], targets: [ .target( diff --git a/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift b/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift index 40b6ca68..24e980c8 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/UpdateSubtreeRootsAction.swift @@ -31,26 +31,55 @@ extension UpdateSubtreeRootsAction: Action { logger.debug("Attempt to get subtree roots, this may fail because lightwalletd may not support Spend before Sync.") let stream = service.getSubtreeRoots(request) - var roots: [SubtreeRoot] = [] + var saplingRoots: [SubtreeRoot] = [] do { for try await subtreeRoot in stream { - roots.append(subtreeRoot) + saplingRoots.append(subtreeRoot) } } catch ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) { throw ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) } - logger.debug("Sapling tree has \(roots.count) subtrees") + logger.debug("Sapling tree has \(saplingRoots.count) subtrees") do { - try await rustBackend.putSaplingSubtreeRoots(startIndex: UInt64(request.startIndex), roots: roots) + try await rustBackend.putSaplingSubtreeRoots(startIndex: UInt64(request.startIndex), roots: saplingRoots) await context.update(state: .updateChainTip) } catch { logger.debug("putSaplingSubtreeRoots failed with error \(error.localizedDescription)") throw ZcashError.compactBlockProcessorPutSaplingSubtreeRoots(error) } - + + if !saplingRoots.isEmpty { + logger.debug("Found Sapling subtree roots, SbS supported, fetching Orchard subtree roots") + + var orchardRequest = GetSubtreeRootsArg() + orchardRequest.shieldedProtocol = .orchard + + let stream = service.getSubtreeRoots(orchardRequest) + + var orchardRoots: [SubtreeRoot] = [] + + do { + for try await subtreeRoot in stream { + orchardRoots.append(subtreeRoot) + } + } catch ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) { + throw ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) + } + + logger.debug("Orchard tree has \(orchardRoots.count) subtrees") + do { + try await rustBackend.putOrchardSubtreeRoots(startIndex: UInt64(orchardRequest.startIndex), roots: orchardRoots) + + await context.update(state: .updateChainTip) + } catch { + logger.debug("putOrchardSubtreeRoots failed with error \(error.localizedDescription)") + throw ZcashError.compactBlockProcessorPutOrchardSubtreeRoots(error) + } + } + return context } diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index 40a48134..ebea735c 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -333,6 +333,15 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0059 case rustIsSeedRelevantToAnyDerivedAccount(_ rustError: String) + /// Unable to allocate memory required to write blocks when calling ZcashRustBackend.putOrchardSubtreeRoots + /// sourcery: code="ZRUST0060" + /// ZRUST0060 + case rustPutOrchardSubtreeRootsAllocationProblem + /// Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0061" + /// ZRUST0061 + case rustPutOrchardSubtreeRoots(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -578,6 +587,9 @@ public enum ZcashError: Equatable, Error { /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. /// ZCBPEO0021 case compactBlockProcessorSupportedSyncAlgorithm + /// Put Orchard subtree roots to the DB failed. + /// ZCBPEO0022 + case compactBlockProcessorPutOrchardSubtreeRoots(_ error: Error) /// The synchronizer is unprepared. /// ZSYNCO0001 case synchronizerNotPrepared @@ -691,6 +703,8 @@ public enum ZcashError: Equatable, Error { case .rustProposeTransferFromURI: return "Error from rust layer when calling ZcashRustBackend." case .rustListAccounts: return "Error from rust layer when calling ZcashRustBackend." case .rustIsSeedRelevantToAnyDerivedAccount: return "Error from rust layer when calling ZcashRustBackend.rustIsSeedRelevantToAnyDerivedAccount" + case .rustPutOrchardSubtreeRootsAllocationProblem: return "Unable to allocate memory required to write blocks when calling ZcashRustBackend.putOrchardSubtreeRoots" + case .rustPutOrchardSubtreeRoots: return "Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots" case .accountDAOGetAll: return "SQLite query failed when fetching all accounts from the database." case .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them." case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." @@ -769,6 +783,7 @@ public enum ZcashError: Equatable, Error { case .compactBlockProcessorPutSaplingSubtreeRoots: return "Put sapling subtree roots to the DB failed." case .compactBlockProcessorLastScannedHeight: return "Getting the `lastScannedHeight` failed but it's supposed to always provide some value." case .compactBlockProcessorSupportedSyncAlgorithm: return "Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value." + case .compactBlockProcessorPutOrchardSubtreeRoots: return "Put Orchard subtree roots to the DB failed." case .synchronizerNotPrepared: return "The synchronizer is unprepared." case .synchronizerSendMemoToTransparentAddress: return "Memos can't be sent to transparent addresses." case .synchronizerShieldFundsInsuficientTransparentFunds: return "There is not enough transparent funds to cover fee for the shielding." @@ -868,6 +883,8 @@ public enum ZcashError: Equatable, Error { case .rustProposeTransferFromURI: return .rustProposeTransferFromURI case .rustListAccounts: return .rustListAccounts case .rustIsSeedRelevantToAnyDerivedAccount: return .rustIsSeedRelevantToAnyDerivedAccount + case .rustPutOrchardSubtreeRootsAllocationProblem: return .rustPutOrchardSubtreeRootsAllocationProblem + case .rustPutOrchardSubtreeRoots: return .rustPutOrchardSubtreeRoots case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOFindBy: return .accountDAOFindBy @@ -946,6 +963,7 @@ public enum ZcashError: Equatable, Error { case .compactBlockProcessorPutSaplingSubtreeRoots: return .compactBlockProcessorPutSaplingSubtreeRoots case .compactBlockProcessorLastScannedHeight: return .compactBlockProcessorLastScannedHeight case .compactBlockProcessorSupportedSyncAlgorithm: return .compactBlockProcessorSupportedSyncAlgorithm + case .compactBlockProcessorPutOrchardSubtreeRoots: return .compactBlockProcessorPutOrchardSubtreeRoots case .synchronizerNotPrepared: return .synchronizerNotPrepared case .synchronizerSendMemoToTransparentAddress: return .synchronizerSendMemoToTransparentAddress case .synchronizerShieldFundsInsuficientTransparentFunds: return .synchronizerShieldFundsInsuficientTransparentFunds diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index 58203333..be9510df 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -181,6 +181,10 @@ public enum ZcashErrorCode: String { case rustListAccounts = "ZRUST0058" /// Error from rust layer when calling ZcashRustBackend.rustIsSeedRelevantToAnyDerivedAccount case rustIsSeedRelevantToAnyDerivedAccount = "ZRUST0059" + /// Unable to allocate memory required to write blocks when calling ZcashRustBackend.putOrchardSubtreeRoots + case rustPutOrchardSubtreeRootsAllocationProblem = "ZRUST0060" + /// Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots + case rustPutOrchardSubtreeRoots = "ZRUST0061" /// SQLite query failed when fetching all accounts from the database. case accountDAOGetAll = "ZADAO0001" /// Fetched accounts from SQLite but can't decode them. @@ -337,6 +341,8 @@ public enum ZcashErrorCode: String { case compactBlockProcessorLastScannedHeight = "ZCBPEO0020" /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. case compactBlockProcessorSupportedSyncAlgorithm = "ZCBPEO0021" + /// Put Orchard subtree roots to the DB failed. + case compactBlockProcessorPutOrchardSubtreeRoots = "ZCBPEO0022" /// The synchronizer is unprepared. case synchronizerNotPrepared = "ZSYNCO0001" /// Memos can't be sent to transparent addresses. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index a5fde898..c85c97b4 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -360,6 +360,13 @@ enum ZcashErrorDefinition { /// - `rustError` contains error generated by the rust layer. // sourcery: code="ZRUST0059" case rustIsSeedRelevantToAnyDerivedAccount(_ rustError: String) + /// Unable to allocate memory required to write blocks when calling ZcashRustBackend.putOrchardSubtreeRoots + /// sourcery: code="ZRUST0060" + case rustPutOrchardSubtreeRootsAllocationProblem + /// Error from rust layer when calling ZcashRustBackend.putOrchardSubtreeRoots + /// - `rustError` contains error generated by the rust layer. + /// sourcery: code="ZRUST0061" + case rustPutOrchardSubtreeRoots(_ rustError: String) // MARK: - Account DAO @@ -654,6 +661,9 @@ enum ZcashErrorDefinition { /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. // sourcery: code="ZCBPEO0021" case compactBlockProcessorSupportedSyncAlgorithm + /// Put Orchard subtree roots to the DB failed. + // sourcery: code="ZCBPEO0022" + case compactBlockProcessorPutOrchardSubtreeRoots(_ error: Error) // MARK: - SDKSynchronizer diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 5eb048ef..1d332584 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -571,6 +571,64 @@ actor ZcashRustBackend: ZcashRustBackendWelding { } } + func putOrchardSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws { + var ffiSubtreeRootsVec: [FfiSubtreeRoot] = [] + + for root in roots { + let hashPtr = UnsafeMutablePointer.allocate(capacity: root.rootHash.count) + + let contiguousHashBytes = ContiguousArray(root.rootHash.bytes) + + let result: Void? = contiguousHashBytes.withContiguousStorageIfAvailable { hashBytesPtr in + // swiftlint:disable:next force_unwrapping + hashPtr.initialize(from: hashBytesPtr.baseAddress!, count: hashBytesPtr.count) + } + + guard result != nil else { + defer { + hashPtr.deallocate() + ffiSubtreeRootsVec.deallocateElements() + } + throw ZcashError.rustPutOrchardSubtreeRootsAllocationProblem + } + + ffiSubtreeRootsVec.append( + FfiSubtreeRoot( + root_hash_ptr: hashPtr, + root_hash_ptr_len: UInt(contiguousHashBytes.count), + completing_block_height: UInt32(root.completingBlockHeight) + ) + ) + } + + var contiguousFfiRoots = ContiguousArray(ffiSubtreeRootsVec) + + let len = UInt(contiguousFfiRoots.count) + + let rootsPtr = UnsafeMutablePointer.allocate(capacity: 1) + + defer { + ffiSubtreeRootsVec.deallocateElements() + rootsPtr.deallocate() + } + + try contiguousFfiRoots.withContiguousMutableStorageIfAvailable { ptr in + var roots = FfiSubtreeRoots() + roots.ptr = ptr.baseAddress + roots.len = len + + rootsPtr.initialize(to: roots) + + globalDBLock.lock() + let res = zcashlc_put_orchard_subtree_roots(dbData.0, dbData.1, startIndex, rootsPtr, networkType.networkId) + globalDBLock.unlock() + + guard res else { + throw ZcashError.rustPutOrchardSubtreeRoots(lastErrorMessage(fallback: "`putOrchardSubtreeRoots` failed with unknown error")) + } + } + } + func updateChainTip(height: Int32) async throws { globalDBLock.lock() let result = zcashlc_update_chain_tip(dbData.0, dbData.1, height, networkType.networkId) diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 5f0fb115..5244d591 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -133,6 +133,8 @@ protocol ZcashRustBackendWelding { func putSaplingSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws + func putOrchardSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws + /// Updates the wallet's view of the blockchain. /// /// This method is used to provide the wallet with information about the state of the blockchain, diff --git a/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift index b7f58815..c6788da3 100644 --- a/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift +++ b/Tests/OfflineTests/CompactBlockProcessorActions/UpdateSubtreeRootsActionTests.swift @@ -70,6 +70,7 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase { } } await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsClosure({ _, _ in }) + await tupple.rustBackendMock.setPutOrchardSubtreeRootsStartIndexRootsClosure({ _, _ in }) do { let context = ActionContextMock.default() @@ -83,7 +84,7 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase { } } - func testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure() async throws { + func testUpdateSubtreeRootsAction_RootsAvailablePutSaplingRootsFailure() async throws { let loggerMock = LoggerMock() loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } @@ -98,6 +99,7 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase { } } await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsThrowableError("putSaplingFailed") + await tupple.rustBackendMock.setPutOrchardSubtreeRootsStartIndexRootsClosure({ _, _ in }) do { let context = ActionContextMock.default() @@ -111,7 +113,37 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase { XCTFail("testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure is not expected to fail. \(error)") } } - + + func testUpdateSubtreeRootsAction_RootsAvailablePutOrchardRootsFailure() async throws { + let loggerMock = LoggerMock() + + loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } + loggerMock.debugFileFunctionLineClosure = { _, _, _, _ in } + + let tupple = setupAction(loggerMock) + let updateSubtreeRootsActionAction = tupple.action + tupple.serviceMock.getSubtreeRootsClosure = { _ in + AsyncThrowingStream { continuation in + continuation.yield(SubtreeRoot()) + continuation.finish() + } + } + await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsClosure({ _, _ in }) + await tupple.rustBackendMock.setPutOrchardSubtreeRootsStartIndexRootsThrowableError("putOrchardFailed") + + do { + let context = ActionContextMock.default() + + _ = try await updateSubtreeRootsActionAction.run(with: context) { _ in } + + XCTFail("updateSubtreeRootsActionAction.run(with:) is excpected to fail but didn't.") + } catch ZcashError.compactBlockProcessorPutOrchardSubtreeRoots { + // this is expected result of this test + } catch { + XCTFail("testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure is not expected to fail. \(error)") + } + } + // swiftlint:disable large_tuple private func setupAction( _ loggerMock: LoggerMock = LoggerMock() diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index f99690a5..eec9e3b7 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -2722,6 +2722,31 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { try await putSaplingSubtreeRootsStartIndexRootsClosure!(startIndex, roots) } + // MARK: - putOrchardSubtreeRoots + + var putOrchardSubtreeRootsStartIndexRootsThrowableError: Error? + func setPutOrchardSubtreeRootsStartIndexRootsThrowableError(_ param: Error?) async { + putOrchardSubtreeRootsStartIndexRootsThrowableError = param + } + var putOrchardSubtreeRootsStartIndexRootsCallsCount = 0 + var putOrchardSubtreeRootsStartIndexRootsCalled: Bool { + return putOrchardSubtreeRootsStartIndexRootsCallsCount > 0 + } + var putOrchardSubtreeRootsStartIndexRootsReceivedArguments: (startIndex: UInt64, roots: [SubtreeRoot])? + var putOrchardSubtreeRootsStartIndexRootsClosure: ((UInt64, [SubtreeRoot]) async throws -> Void)? + func setPutOrchardSubtreeRootsStartIndexRootsClosure(_ param: ((UInt64, [SubtreeRoot]) async throws -> Void)?) async { + putOrchardSubtreeRootsStartIndexRootsClosure = param + } + + func putOrchardSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws { + if let error = putOrchardSubtreeRootsStartIndexRootsThrowableError { + throw error + } + putOrchardSubtreeRootsStartIndexRootsCallsCount += 1 + putOrchardSubtreeRootsStartIndexRootsReceivedArguments = (startIndex: startIndex, roots: roots) + try await putOrchardSubtreeRootsStartIndexRootsClosure!(startIndex, roots) + } + // MARK: - updateChainTip var updateChainTipHeightThrowableError: Error?