ZcashLightClientKit/Tests/DarksideTests/RewindRescanTests.swift

521 lines
21 KiB
Swift
Raw Normal View History

//
// XCTRewindRescanTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 3/25/21.
//
import Combine
import XCTest
2022-02-28 09:03:20 -08:00
@testable import TestUtils
@testable import ZcashLightClientKit
2021-09-23 06:26:41 -07:00
// FIXME: [#586] disabled until this is resolved https://github.com/zcash/ZcashLightClientKit/issues/586
class RewindRescanTests: XCTestCase {
let sendAmount: Int64 = 1000
let defaultLatestHeight: BlockHeight = 663175
2021-09-23 06:26:41 -07:00
let branchID = "2bb40e60"
let chainName = "main"
var cancellables: [AnyCancellable] = []
2021-09-23 06:26:41 -07:00
var birthday: BlockHeight = 663150
var coordinator: TestCoordinator!
var syncedExpectation = XCTestExpectation(description: "synced")
var sentTransactionExpectation = XCTestExpectation(description: "sent")
var expectedReorgHeight: BlockHeight = 665188
var expectedRewindHeight: BlockHeight = 665188
2021-09-23 06:26:41 -07:00
var reorgExpectation = XCTestExpectation(description: "reorg")
2021-07-28 15:25:47 -07:00
var network = ZcashNetworkBuilder.network(for: .mainnet)
2021-09-23 06:26:41 -07:00
override func setUpWithError() throws {
2021-09-23 06:26:41 -07:00
try super.setUpWithError()
self.coordinator = try TestCoordinator(
seed: Environment.seedPhrase,
walletBirthday: self.birthday,
network: self.network
)
try self.coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
}
override func tearDownWithError() throws {
2021-09-23 06:26:41 -07:00
try super.tearDownWithError()
NotificationCenter.default.removeObserver(self)
try coordinator.stop()
- [#679] Implementation of the File-system based block cache (#679) Closes https://github.com/zcash/ZcashLightClientKit/issues/697 Closes https://github.com/zcash/ZcashLightClientKit/issues/720 Closes https://github.com/zcash/ZcashLightClientKit/issues/587 Closes https://github.com/zcash/ZcashLightClientKit/issues/667 Closes https://github.com/zcash/ZcashLightClientKit/issues/443 Closes https://github.com/zcash/ZcashLightClientKit/issues/754 - [#790] Fix ShieldFundsTests Closes #790 Removes comments on `ShieldFundsTests` since those issues have been fixed Depends on zcash-light-client-ffi changes that adopt newer versions of librustzcash crates `zcash_primitives 0.10`, `zcash_client_backend 0.7`, `zcash_proofs 0.10`, `zcash_client_sqlite 0.5.0`. Also allows wallets to define a shielding_threshold and will set foundations to customize minimum confirmations for balances, spends and shielding operations. **Test Bootstrapping** - `ZcashCompactBlockDescriptor`: struct that holds functions to describe blocks as filenames and compare those filenames `ZcashCompactBlockDescriptor.live` has the actual implementation but it can be replaced by mocks if needed on Tests main implementations are held under `FSCompactBlockRepository.filenameDescription` and `FSCompactBlockRepository.filenameComparison` on a separate extention `DirectoryListingProviders` provide two default implementations of listing a directory deterministically. `FileManager` does not define a sorting and needs to be done in-memory by calling `.sorted()` on the resulting collection. If this is a big toll on performance it can be changed to a POSIX implementation but this is good for now. `ZcashCompactBlockDescriptor` adds a `height` helper function to turn a filename into the height of the block stored. Implemented `func latestHeight() throws -> BlockHeight ` that returns the blockheight by querying the cache directory in a sorted fashion and getting the last value and turning the filename into a `BlockHeight` Added `Meta` struct to ZcashCompactBlock. Tests implemented: - `filterBlockFiles` - `testClearTheCache` - `testLatestHeightEmptyCacheThrows` - `testLatestHeightEmptyCacheThrowsAsync` - `testRewindEmptyCacheDoesNothing` - `testRewindEmptyCacheDoesNothingAsync` - `testWhenBlockIsStoredItFollowsTheDescribedFormat` - `testWhenBlockIsStoredItFollowsTheFilenameConvention` - `testGetLatestHeight` - `testRewindDeletesTheRightBlocks` test - `testPerformanceExample` test. This isn't a real performance test because the API doesn't work with async/await yet adopts `shield_funds` shielding threshold parameter Implements `initBlockMetadataDb` and fix tests Renames dbCache parameter to `fsBlockDbRoot`. Builds but tests don't pass. Removes cacheDb uses from code. Testing utilities still persist. Added needed information in MIGRATING and CHANGELOG. Added helper to perform deletion of legacy db and creation a the new file system backed cache. Renames parameters and changes code where needed. Network Constants turned into `enum` with static methods. DeletelastDownloadedBlock helper from initializer Removes CompactBlockStorage and CompactBlockEntity. Implements `latestCachedBlockHeight` on rustbackend. *Replaces dependencies on ZcashRustWelding with `FSMetadataStore`* This allows the tests to not depend in a particular implementation of either the MockRustBackend of or ZcashRustBackend. Also provides a way to test errors properly and switch implementations of critical areas like `writeBlocks`.
2023-02-02 08:58:12 -08:00
try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
try? FileManager.default.removeItem(at: coordinator.databases.pendingDB)
2023-02-16 08:14:31 -08:00
coordinator = nil
cancellables = []
}
func handleError(_ error: Error?) {
guard let testError = error else {
XCTFail("failed with nil error")
return
}
2021-09-23 06:26:41 -07:00
XCTFail("Failed with error: \(testError)")
}
func testBirthdayRescan() async throws {
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 50)
let initialVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let initialTotalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
continuation.resume()
},
error: self.handleError
)
} catch {
continuation.resume(with: .failure(error))
}
}
wait(for: [firstSyncExpectation], timeout: 12)
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
// 2 check that there are no unconfirmed funds
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
let rewindExpectation = XCTestExpectation(description: "RewindExpectation")
try await withCheckedThrowingContinuation { continuation in
// rewind to birthday
coordinator.synchronizer.rewind(.birthday)
.sink(
receiveCompletion: { result in
rewindExpectation.fulfill()
switch result {
case .finished:
continuation.resume()
case let .failure(error):
XCTFail("Rewind failed with error: \(error)")
continuation.resume(with: .failure(error))
}
},
receiveValue: { _ in }
)
.store(in: &cancellables)
}
wait(for: [rewindExpectation], timeout: 2)
// assert that after the new height is
2021-09-23 06:26:41 -07:00
XCTAssertEqual(try coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight(), self.birthday)
// check that the balance is cleared
XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(initialTotalBalance, coordinator.synchronizer.initializer.getBalance())
let secondScanExpectation = XCTestExpectation(description: "rescan")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(
completion: { _ in
secondScanExpectation.fulfill()
continuation.resume()
},
error: self.handleError
)
} catch {
continuation.resume(with: .failure(error))
}
}
wait(for: [secondScanExpectation], timeout: 12)
// verify that the balance still adds up
XCTAssertEqual(verifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance())
}
// FIXME [#789]: Fix test
func testRescanToHeight() async throws {
// 1 sync and get spendable funds
2021-09-23 06:26:41 -07:00
try FakeChainBuilder.buildChainWithTxsFarFromEachOther(
darksideWallet: coordinator.service,
branchID: branchID,
chainName: chainName,
length: 10000
)
2021-04-19 10:16:24 -07:00
let newChaintTip = defaultLatestHeight + 10000
try coordinator.applyStaged(blockheight: newChaintTip)
sleep(3)
let initialVerifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
2021-04-19 10:16:24 -07:00
wait(for: [firstSyncExpectation], timeout: 20)
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
// 2 check that there are no unconfirmed funds
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
// rewind to birthday
2021-04-19 10:16:24 -07:00
let targetHeight: BlockHeight = newChaintTip - 8000
2021-09-23 06:26:41 -07:00
let rewindHeight = ZcashRustBackend.getNearestRewindHeight(
dbData: coordinator.databases.dataDB,
height: Int32(targetHeight),
networkType: network.networkType
)
let rewindExpectation = XCTestExpectation(description: "RewindExpectation")
try await withCheckedThrowingContinuation { continuation in
coordinator.synchronizer.rewind(.height(blockheight: targetHeight))
.sink(
receiveCompletion: { result in
rewindExpectation.fulfill()
switch result {
case .finished:
continuation.resume()
case let .failure(error):
XCTFail("Rewind failed with error: \(error)")
continuation.resume(with: .failure(error))
}
},
receiveValue: { _ in }
)
.store(in: &cancellables)
}
wait(for: [rewindExpectation], timeout: 2)
2021-04-19 10:16:24 -07:00
guard rewindHeight > 0 else {
XCTFail("get nearest height failed error: \(ZcashRustBackend.getLastError() ?? "null")")
return
}
2021-09-23 06:26:41 -07:00
// check that the balance is cleared
XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
2021-04-19 10:16:24 -07:00
let secondScanExpectation = XCTestExpectation(description: "rescan")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { _ in
secondScanExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
2021-04-19 10:16:24 -07:00
wait(for: [secondScanExpectation], timeout: 20)
// verify that the balance still adds up
XCTAssertEqual(verifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance())
2021-04-19 10:16:24 -07:00
// try to spend the funds
let sendExpectation = XCTestExpectation(description: "after rewind expectation")
do {
let pendingTx = try await coordinator.synchronizer.sendToAddress(
spendingKey: coordinator.spendingKey,
zatoshi: Zatoshi(1000),
toAddress: try! Recipient(Environment.testRecipientAddress, network: .mainnet),
memo: .empty
)
XCTAssertEqual(Zatoshi(1000), pendingTx.value)
sendExpectation.fulfill()
} catch {
XCTFail("sending fail: \(error)")
2021-04-19 10:16:24 -07:00
}
wait(for: [sendExpectation], timeout: 15)
}
// FIX [#790]: Fix tests
@MainActor func testRescanToTransaction() async throws {
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 50)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
try coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: handleError
)
wait(for: [firstSyncExpectation], timeout: 12)
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
// 2 check that there are no unconfirmed funds
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
// rewind to transaction
guard let transaction = try coordinator.synchronizer.allClearedTransactions().first else {
XCTFail("failed to get a transaction to rewind to")
return
}
let rewindExpectation = XCTestExpectation(description: "RewindExpectation")
try await withCheckedThrowingContinuation { continuation in
coordinator.synchronizer.rewind(.transaction(transaction))
.sink(
receiveCompletion: { result in
rewindExpectation.fulfill()
switch result {
case .finished:
continuation.resume()
case let .failure(error):
XCTFail("Rewind failed with error: \(error)")
continuation.resume(with: .failure(error))
}
},
receiveValue: { _ in }
)
.store(in: &cancellables)
}
wait(for: [rewindExpectation], timeout: 2)
// assert that after the new height is lower or same as transaction, rewind doesn't have to be make exactly to transaction height, it can
// be done to nearest height provided by rust
XCTAssertLessThanOrEqual(
2021-09-23 06:26:41 -07:00
try coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight(),
transaction.anchor(network: network) ?? -1
2021-09-23 06:26:41 -07:00
)
let secondScanExpectation = XCTestExpectation(description: "rescan")
try coordinator.sync(
completion: { _ in
secondScanExpectation.fulfill()
},
error: handleError
)
wait(for: [secondScanExpectation], timeout: 12)
// verify that the balance still adds up
XCTAssertEqual(verifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance())
}
// FIXME [#791]: Fix test
func disabled_testRewindAfterSendingTransaction() async throws {
let notificationHandler = SDKSynchonizerListener()
let foundTransactionsExpectation = XCTestExpectation(description: "found transactions expectation")
let transactionMinedExpectation = XCTestExpectation(description: "transaction mined expectation")
// 0 subscribe to updated transactions events
notificationHandler.subscribeToSynchronizer(coordinator.synchronizer)
// 1 sync and get spendable funds
2021-05-18 14:22:29 -07:00
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: defaultLatestHeight + 10)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [firstSyncExpectation], timeout: 12)
// 2 check that there are no unconfirmed funds
let verifiedBalance: Zatoshi = coordinator.synchronizer.initializer.getVerifiedBalance()
let totalBalance: Zatoshi = coordinator.synchronizer.initializer.getBalance()
2021-07-28 09:59:10 -07:00
XCTAssertTrue(verifiedBalance > network.constants.defaultFee(for: defaultLatestHeight))
XCTAssertEqual(verifiedBalance, totalBalance)
let maxBalance = verifiedBalance - network.constants.defaultFee(for: defaultLatestHeight)
// 3 create a transaction for the max amount possible
// 4 send the transaction
guard let spendingKey = coordinator.spendingKeys?.first else {
XCTFail("failed to create spending keys")
return
}
var pendingTx: PendingTransactionEntity?
do {
let transaction = try await coordinator.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: maxBalance,
toAddress: try! Recipient(Environment.testRecipientAddress, network: .mainnet),
memo: try Memo(string: "test send \(self.description) \(Date().description)")
)
pendingTx = transaction
self.sentTransactionExpectation.fulfill()
} catch {
XCTFail("sendToAddress failed: \(error)")
}
wait(for: [sentTransactionExpectation], timeout: 20)
guard let pendingTx = pendingTx else {
XCTFail("transaction creation failed")
return
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTAssertNotNil(transaction.rawTransactionId)
XCTAssertNotNil(pendingTx.rawTransactionId)
2021-09-23 06:26:41 -07:00
XCTAssertEqual(transaction.rawTransactionId, pendingTx.rawTransactionId)
transactionMinedExpectation.fulfill()
}
// 5 apply to height
// 6 mine the block
guard let rawTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction after")
return
}
let latestHeight = try coordinator.latestHeight()
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
let foundTx = txs.first(where: { $0.rawID == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
foundTransactionsExpectation.fulfill()
}
try coordinator.stageBlockCreate(height: sentTxHeight, count: 100)
sleep(1)
try coordinator.stageTransaction(rawTx, at: sentTxHeight)
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2)
let mineExpectation = XCTestExpectation(description: "mineTxExpectation")
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(
completion: { synchronizer in
let pendingTransaction = synchronizer.pendingTransactions
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNotNil(pendingTransaction, "pending transaction should have been mined by now")
XCTAssertTrue(pendingTransaction?.isMined ?? false)
XCTAssertEqual(pendingTransaction?.minedHeight, sentTxHeight)
mineExpectation.fulfill()
continuation.resume()
}, error: { error in
guard let error else {
XCTFail("unknown error syncing after sending transaction")
return
}
XCTFail("Error: \(error)")
}
)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5)
// 7 advance to confirmation
try coordinator.applyStaged(blockheight: sentTxHeight + 10)
sleep(2)
let rewindExpectation = XCTestExpectation(description: "RewindExpectation")
try await withCheckedThrowingContinuation { continuation in
// rewind 5 blocks prior to sending
coordinator.synchronizer.rewind(.height(blockheight: sentTxHeight - 5))
.sink(
receiveCompletion: { result in
rewindExpectation.fulfill()
switch result {
case .finished:
continuation.resume()
case let .failure(error):
XCTFail("Rewind failed with error: \(error)")
continuation.resume(with: .failure(error))
}
},
receiveValue: { _ in }
)
.store(in: &cancellables)
}
wait(for: [rewindExpectation], timeout: 2)
2021-09-23 06:26:41 -07:00
guard
let pendingEntity = try coordinator.synchronizer.allPendingTransactions()
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
else {
XCTFail("sent pending transaction not found after rewind")
return
}
2021-09-23 06:26:41 -07:00
XCTAssertFalse(pendingEntity.isMined)
let confirmExpectation = XCTestExpectation(description: "confirm expectation")
notificationHandler.transactionsFound = { txs in
XCTAssertEqual(txs.count, 1)
2021-09-23 06:26:41 -07:00
guard let transaction = txs.first else {
XCTFail("should have found sent transaction but didn't")
return
}
XCTAssertEqual(transaction.rawID, pendingTx.rawTransactionId, "should have mined sent transaction but didn't")
}
2021-09-23 06:26:41 -07:00
notificationHandler.synchronizerMinedTransaction = { transaction in
XCTFail("We shouldn't find any mined transactions at this point but found \(transaction)")
}
2021-09-23 06:26:41 -07:00
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { _ in
confirmExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
2021-09-23 06:26:41 -07:00
}
}
wait(for: [confirmExpectation], timeout: 10)
2021-09-23 06:26:41 -07:00
let confirmedPending = try coordinator.synchronizer.allPendingTransactions()
.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
XCTAssertNil(confirmedPending, "pending, now confirmed transaction found")
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), .zero)
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), .zero)
}
}