Fetch and store Orchard subtree roots

This commit is contained in:
Jack Grigg 2024-03-28 10:23:49 -04:00
parent d8f389b8da
commit 006861595d
12 changed files with 195 additions and 12 deletions

View File

@ -6,6 +6,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased # Unreleased
## Fixed
- Orchard subtree roots are now fetched alongside Sapling subtree roots.
# 2.1.2 - 2024-03-27 # 2.1.2 - 2024-03-27
## Fixed ## Fixed

View File

@ -176,8 +176,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : { "state" : {
"revision" : "8838b4f0ee4193349fed09f0248220d4ada271fc", "revision" : "e2d8763f3a963fb0026b6160af2d211b527453cd",
"version" : "0.7.3" "version" : "0.7.4"
} }
} }
], ],

View File

@ -122,8 +122,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : { "state" : {
"revision" : "c7e5158edf5e62af15492d30237163b78af35ce9", "revision" : "e2d8763f3a963fb0026b6160af2d211b527453cd",
"version" : "0.7.1" "version" : "0.7.4"
} }
} }
], ],

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"), .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/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: [ targets: [
.target( .target(

View File

@ -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.") logger.debug("Attempt to get subtree roots, this may fail because lightwalletd may not support Spend before Sync.")
let stream = service.getSubtreeRoots(request) let stream = service.getSubtreeRoots(request)
var roots: [SubtreeRoot] = [] var saplingRoots: [SubtreeRoot] = []
do { do {
for try await subtreeRoot in stream { for try await subtreeRoot in stream {
roots.append(subtreeRoot) saplingRoots.append(subtreeRoot)
} }
} catch ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) { } catch ZcashError.serviceSubtreeRootsStreamFailed(LightWalletServiceError.timeOut) {
throw 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 { 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) await context.update(state: .updateChainTip)
} catch { } catch {
logger.debug("putSaplingSubtreeRoots failed with error \(error.localizedDescription)") logger.debug("putSaplingSubtreeRoots failed with error \(error.localizedDescription)")
throw ZcashError.compactBlockProcessorPutSaplingSubtreeRoots(error) 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 return context
} }

View File

@ -333,6 +333,15 @@ public enum ZcashError: Equatable, Error {
/// - `rustError` contains error generated by the rust layer. /// - `rustError` contains error generated by the rust layer.
/// ZRUST0059 /// ZRUST0059
case rustIsSeedRelevantToAnyDerivedAccount(_ rustError: String) 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. /// SQLite query failed when fetching all accounts from the database.
/// - `sqliteError` is error produced by SQLite library. /// - `sqliteError` is error produced by SQLite library.
/// ZADAO0001 /// ZADAO0001
@ -578,6 +587,9 @@ public enum ZcashError: Equatable, Error {
/// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value.
/// ZCBPEO0021 /// ZCBPEO0021
case compactBlockProcessorSupportedSyncAlgorithm case compactBlockProcessorSupportedSyncAlgorithm
/// Put Orchard subtree roots to the DB failed.
/// ZCBPEO0022
case compactBlockProcessorPutOrchardSubtreeRoots(_ error: Error)
/// The synchronizer is unprepared. /// The synchronizer is unprepared.
/// ZSYNCO0001 /// ZSYNCO0001
case synchronizerNotPrepared case synchronizerNotPrepared
@ -691,6 +703,8 @@ public enum ZcashError: Equatable, Error {
case .rustProposeTransferFromURI: return "Error from rust layer when calling ZcashRustBackend." case .rustProposeTransferFromURI: return "Error from rust layer when calling ZcashRustBackend."
case .rustListAccounts: 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 .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 .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 .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them."
case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." 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 .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 .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 .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 .synchronizerNotPrepared: return "The synchronizer is unprepared."
case .synchronizerSendMemoToTransparentAddress: return "Memos can't be sent to transparent addresses." 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." 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 .rustProposeTransferFromURI: return .rustProposeTransferFromURI
case .rustListAccounts: return .rustListAccounts case .rustListAccounts: return .rustListAccounts
case .rustIsSeedRelevantToAnyDerivedAccount: return .rustIsSeedRelevantToAnyDerivedAccount case .rustIsSeedRelevantToAnyDerivedAccount: return .rustIsSeedRelevantToAnyDerivedAccount
case .rustPutOrchardSubtreeRootsAllocationProblem: return .rustPutOrchardSubtreeRootsAllocationProblem
case .rustPutOrchardSubtreeRoots: return .rustPutOrchardSubtreeRoots
case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAll: return .accountDAOGetAll
case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode
case .accountDAOFindBy: return .accountDAOFindBy case .accountDAOFindBy: return .accountDAOFindBy
@ -946,6 +963,7 @@ public enum ZcashError: Equatable, Error {
case .compactBlockProcessorPutSaplingSubtreeRoots: return .compactBlockProcessorPutSaplingSubtreeRoots case .compactBlockProcessorPutSaplingSubtreeRoots: return .compactBlockProcessorPutSaplingSubtreeRoots
case .compactBlockProcessorLastScannedHeight: return .compactBlockProcessorLastScannedHeight case .compactBlockProcessorLastScannedHeight: return .compactBlockProcessorLastScannedHeight
case .compactBlockProcessorSupportedSyncAlgorithm: return .compactBlockProcessorSupportedSyncAlgorithm case .compactBlockProcessorSupportedSyncAlgorithm: return .compactBlockProcessorSupportedSyncAlgorithm
case .compactBlockProcessorPutOrchardSubtreeRoots: return .compactBlockProcessorPutOrchardSubtreeRoots
case .synchronizerNotPrepared: return .synchronizerNotPrepared case .synchronizerNotPrepared: return .synchronizerNotPrepared
case .synchronizerSendMemoToTransparentAddress: return .synchronizerSendMemoToTransparentAddress case .synchronizerSendMemoToTransparentAddress: return .synchronizerSendMemoToTransparentAddress
case .synchronizerShieldFundsInsuficientTransparentFunds: return .synchronizerShieldFundsInsuficientTransparentFunds case .synchronizerShieldFundsInsuficientTransparentFunds: return .synchronizerShieldFundsInsuficientTransparentFunds

View File

@ -181,6 +181,10 @@ public enum ZcashErrorCode: String {
case rustListAccounts = "ZRUST0058" case rustListAccounts = "ZRUST0058"
/// Error from rust layer when calling ZcashRustBackend.rustIsSeedRelevantToAnyDerivedAccount /// Error from rust layer when calling ZcashRustBackend.rustIsSeedRelevantToAnyDerivedAccount
case rustIsSeedRelevantToAnyDerivedAccount = "ZRUST0059" 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. /// SQLite query failed when fetching all accounts from the database.
case accountDAOGetAll = "ZADAO0001" case accountDAOGetAll = "ZADAO0001"
/// Fetched accounts from SQLite but can't decode them. /// Fetched accounts from SQLite but can't decode them.
@ -337,6 +341,8 @@ public enum ZcashErrorCode: String {
case compactBlockProcessorLastScannedHeight = "ZCBPEO0020" case compactBlockProcessorLastScannedHeight = "ZCBPEO0020"
/// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value.
case compactBlockProcessorSupportedSyncAlgorithm = "ZCBPEO0021" case compactBlockProcessorSupportedSyncAlgorithm = "ZCBPEO0021"
/// Put Orchard subtree roots to the DB failed.
case compactBlockProcessorPutOrchardSubtreeRoots = "ZCBPEO0022"
/// The synchronizer is unprepared. /// The synchronizer is unprepared.
case synchronizerNotPrepared = "ZSYNCO0001" case synchronizerNotPrepared = "ZSYNCO0001"
/// Memos can't be sent to transparent addresses. /// Memos can't be sent to transparent addresses.

View File

@ -360,6 +360,13 @@ enum ZcashErrorDefinition {
/// - `rustError` contains error generated by the rust layer. /// - `rustError` contains error generated by the rust layer.
// sourcery: code="ZRUST0059" // sourcery: code="ZRUST0059"
case rustIsSeedRelevantToAnyDerivedAccount(_ rustError: String) 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 // MARK: - Account DAO
@ -654,6 +661,9 @@ enum ZcashErrorDefinition {
/// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value. /// Getting the `supportedSyncAlgorithm` failed but it's supposed to always provide some value.
// sourcery: code="ZCBPEO0021" // sourcery: code="ZCBPEO0021"
case compactBlockProcessorSupportedSyncAlgorithm case compactBlockProcessorSupportedSyncAlgorithm
/// Put Orchard subtree roots to the DB failed.
// sourcery: code="ZCBPEO0022"
case compactBlockProcessorPutOrchardSubtreeRoots(_ error: Error)
// MARK: - SDKSynchronizer // MARK: - SDKSynchronizer

View File

@ -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<UInt8>.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<FfiSubtreeRoots>.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 { func updateChainTip(height: Int32) async throws {
globalDBLock.lock() globalDBLock.lock()
let result = zcashlc_update_chain_tip(dbData.0, dbData.1, height, networkType.networkId) let result = zcashlc_update_chain_tip(dbData.0, dbData.1, height, networkType.networkId)

View File

@ -133,6 +133,8 @@ protocol ZcashRustBackendWelding {
func putSaplingSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws func putSaplingSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws
func putOrchardSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws
/// Updates the wallet's view of the blockchain. /// Updates the wallet's view of the blockchain.
/// ///
/// This method is used to provide the wallet with information about the state of the blockchain, /// This method is used to provide the wallet with information about the state of the blockchain,

View File

@ -70,6 +70,7 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase {
} }
} }
await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsClosure({ _, _ in }) await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsClosure({ _, _ in })
await tupple.rustBackendMock.setPutOrchardSubtreeRootsStartIndexRootsClosure({ _, _ in })
do { do {
let context = ActionContextMock.default() 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() let loggerMock = LoggerMock()
loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in } loggerMock.infoFileFunctionLineClosure = { _, _, _, _ in }
@ -98,6 +99,7 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase {
} }
} }
await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsThrowableError("putSaplingFailed") await tupple.rustBackendMock.setPutSaplingSubtreeRootsStartIndexRootsThrowableError("putSaplingFailed")
await tupple.rustBackendMock.setPutOrchardSubtreeRootsStartIndexRootsClosure({ _, _ in })
do { do {
let context = ActionContextMock.default() let context = ActionContextMock.default()
@ -111,7 +113,37 @@ final class UpdateSubtreeRootsActionTests: ZcashTestCase {
XCTFail("testUpdateSubtreeRootsAction_RootsAvailablePutRootsFailure is not expected to fail. \(error)") 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 // swiftlint:disable large_tuple
private func setupAction( private func setupAction(
_ loggerMock: LoggerMock = LoggerMock() _ loggerMock: LoggerMock = LoggerMock()

View File

@ -2722,6 +2722,31 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding {
try await putSaplingSubtreeRootsStartIndexRootsClosure!(startIndex, roots) 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 // MARK: - updateChainTip
var updateChainTipHeightThrowableError: Error? var updateChainTipHeightThrowableError: Error?