362 lines
16 KiB
Swift
362 lines
16 KiB
Swift
//
|
|
// XCTRewindRescanTests.swift
|
|
// ZcashLightClientKit-Unit-Tests
|
|
//
|
|
// Created by Francisco Gindre on 3/25/21.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import ZcashLightClientKit
|
|
class RewindRescanTests: XCTestCase {
|
|
var seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" //TODO: Parameterize this from environment?
|
|
|
|
let testRecipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a" //TODO: Parameterize this from environment
|
|
|
|
let sendAmount: Int64 = 1000
|
|
var birthday: BlockHeight = 663150
|
|
let defaultLatestHeight: BlockHeight = 663175
|
|
var coordinator: TestCoordinator!
|
|
var syncedExpectation = XCTestExpectation(description: "synced")
|
|
var sentTransactionExpectation = XCTestExpectation(description: "sent")
|
|
var expectedReorgHeight: BlockHeight = 665188
|
|
var expectedRewindHeight: BlockHeight = 665188
|
|
var reorgExpectation: XCTestExpectation = XCTestExpectation(description: "reorg")
|
|
let branchID = "2bb40e60"
|
|
let chainName = "main"
|
|
override func setUpWithError() throws {
|
|
|
|
coordinator = try TestCoordinator(
|
|
seed: seedPhrase,
|
|
walletBirthday: birthday,
|
|
channelProvider: ChannelProvider()
|
|
)
|
|
try coordinator.reset(saplingActivation: 663150, branchID: "e9ff75a6", chainName: "main")
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
NotificationCenter.default.removeObserver(self)
|
|
try coordinator.stop()
|
|
try? FileManager.default.removeItem(at: coordinator.databases.cacheDB)
|
|
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
|
|
try? FileManager.default.removeItem(at: coordinator.databases.pendingDB)
|
|
}
|
|
|
|
|
|
func handleError(_ error: Error?) {
|
|
guard let testError = error else {
|
|
XCTFail("failed with nil error")
|
|
return
|
|
}
|
|
XCTFail("Failed with error: \(testError)")
|
|
}
|
|
|
|
func testBirthdayRescan() throws {
|
|
// 1 sync and get spendable funds
|
|
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
|
|
|
|
try coordinator.applyStaged(blockheight: defaultLatestHeight + 50)
|
|
let initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
let initialTotalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
sleep(1)
|
|
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
|
|
|
|
try coordinator.sync(completion: { (synchronizer) in
|
|
firstSyncExpectation.fulfill()
|
|
}, error: handleError)
|
|
|
|
wait(for: [firstSyncExpectation], timeout: 12)
|
|
let verifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
let totalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
// 2 check that there are no unconfirmed funds
|
|
XCTAssertTrue(verifiedBalance > ZcashSDK.defaultFee())
|
|
XCTAssertEqual(verifiedBalance, totalBalance)
|
|
|
|
// rewind to birthday
|
|
try coordinator.synchronizer.rewind(.birthday)
|
|
|
|
// assert that after the new height is
|
|
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 coordinator.sync(completion: { (synchronizer) 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())
|
|
|
|
}
|
|
|
|
|
|
func testRescanToHeight() throws {
|
|
// 1 sync and get spendable funds
|
|
try FakeChainBuilder.buildChainWithTxsFarFromEachOther(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: 10000)
|
|
let newChaintTip = defaultLatestHeight + 10000
|
|
try coordinator.applyStaged(blockheight: newChaintTip)
|
|
sleep(3)
|
|
let initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
// let initialTotalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
|
|
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
|
|
|
|
try coordinator.sync(completion: { (synchronizer) in
|
|
firstSyncExpectation.fulfill()
|
|
}, error: handleError)
|
|
|
|
wait(for: [firstSyncExpectation], timeout: 20)
|
|
let verifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
let totalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
// 2 check that there are no unconfirmed funds
|
|
XCTAssertTrue(verifiedBalance > ZcashSDK.defaultFee())
|
|
XCTAssertEqual(verifiedBalance, totalBalance)
|
|
|
|
// rewind to birthday
|
|
let targetHeight: BlockHeight = newChaintTip - 8000
|
|
let rewindHeight = ZcashRustBackend.getNearestRewindHeight(dbData: coordinator.databases.dataDB, height: Int32(targetHeight))
|
|
try coordinator.synchronizer.rewind(.height(blockheight: targetHeight))
|
|
|
|
|
|
guard rewindHeight > 0 else {
|
|
XCTFail("get nearest height failed error: \(ZcashRustBackend.getLastError() ?? "null")")
|
|
return
|
|
}
|
|
// assert that after the new height is
|
|
// XCTAssertEqual(try coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight(), BlockHeight(rewindHeight))
|
|
|
|
// check that the balance is cleared
|
|
XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
|
|
|
|
let secondScanExpectation = XCTestExpectation(description: "rescan")
|
|
|
|
try coordinator.sync(completion: { (synchronizer) in
|
|
secondScanExpectation.fulfill()
|
|
}, error: handleError)
|
|
|
|
wait(for: [secondScanExpectation], timeout: 20)
|
|
|
|
// verify that the balance still adds up
|
|
XCTAssertEqual(verifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
|
|
XCTAssertEqual(totalBalance, coordinator.synchronizer.initializer.getBalance())
|
|
|
|
// try to spend the funds
|
|
let sendExpectation = XCTestExpectation(description: "after rewind expectation")
|
|
coordinator.synchronizer.sendToAddress(spendingKey: coordinator.spendingKey, zatoshi: 1000, toAddress: testRecipientAddress, memo: nil, from: 0) { result in
|
|
sendExpectation.fulfill()
|
|
switch result {
|
|
case .success(let pendingTx):
|
|
XCTAssertEqual(1000, pendingTx.value)
|
|
case .failure(let error):
|
|
XCTFail("sending fail: \(error)")
|
|
}
|
|
}
|
|
wait(for: [sendExpectation], timeout: 15)
|
|
|
|
|
|
|
|
}
|
|
|
|
func testRescanToTransaction() throws {
|
|
// 1 sync and get spendable funds
|
|
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: { (synchronizer) in
|
|
firstSyncExpectation.fulfill()
|
|
}, error: handleError)
|
|
|
|
wait(for: [firstSyncExpectation], timeout: 12)
|
|
let verifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
let totalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
// 2 check that there are no unconfirmed funds
|
|
XCTAssertTrue(verifiedBalance > ZcashSDK.defaultFee())
|
|
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
|
|
}
|
|
|
|
try coordinator.synchronizer.rewind(.transaction(transaction.transactionEntity))
|
|
|
|
|
|
// assert that after the new height is
|
|
XCTAssertEqual(try coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight(),transaction.transactionEntity.anchor)
|
|
|
|
let secondScanExpectation = XCTestExpectation(description: "rescan")
|
|
|
|
try coordinator.sync(completion: { (synchronizer) 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())
|
|
|
|
}
|
|
|
|
func testRewindAfterSendingTransaction() 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
|
|
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 coordinator.sync(completion: { (synchronizer) in
|
|
firstSyncExpectation.fulfill()
|
|
}, error: handleError)
|
|
|
|
wait(for: [firstSyncExpectation], timeout: 12)
|
|
// 2 check that there are no unconfirmed funds
|
|
|
|
let verifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
|
|
let totalBalance = coordinator.synchronizer.initializer.getBalance()
|
|
XCTAssertTrue(verifiedBalance > ZcashSDK.defaultFee())
|
|
XCTAssertEqual(verifiedBalance, totalBalance)
|
|
|
|
let maxBalance = verifiedBalance - Int64(ZcashSDK.defaultFee())
|
|
|
|
// 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?
|
|
coordinator.synchronizer.sendToAddress(spendingKey: spendingKey,
|
|
zatoshi: maxBalance,
|
|
toAddress: testRecipientAddress,
|
|
memo: "test send \(self.description) \(Date().description)",
|
|
from: 0) { result in
|
|
switch result {
|
|
case .failure(let error):
|
|
XCTFail("sendToAddress failed: \(error)")
|
|
case .success(let transaction):
|
|
pendingTx = transaction
|
|
}
|
|
self.sentTransactionExpectation.fulfill()
|
|
}
|
|
wait(for: [sentTransactionExpectation], timeout: 20)
|
|
guard let pendingTx = pendingTx else {
|
|
XCTFail("transaction creation failed")
|
|
return
|
|
}
|
|
|
|
notificationHandler.synchronizerMinedTransaction = { tx in
|
|
XCTAssertNotNil(tx.rawTransactionId)
|
|
XCTAssertNotNil(pendingTx.rawTransactionId)
|
|
XCTAssertEqual(tx.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.rawTransactionId == 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 coordinator.sync(completion: { (synchronizer) in
|
|
let p = synchronizer.pendingTransactions.first(where: {$0.rawTransactionId == pendingTx.rawTransactionId})
|
|
XCTAssertNotNil(p, "pending transaction should have been mined by now")
|
|
XCTAssertTrue(p?.isMined ?? false)
|
|
XCTAssertEqual(p?.minedHeight, sentTxHeight)
|
|
mineExpectation.fulfill()
|
|
|
|
}, error: { (error) in
|
|
guard let e = error else {
|
|
XCTFail("unknown error syncing after sending transaction")
|
|
return
|
|
}
|
|
|
|
XCTFail("Error: \(e)")
|
|
})
|
|
|
|
wait(for: [mineExpectation, transactionMinedExpectation, foundTransactionsExpectation], timeout: 5)
|
|
|
|
// 7 advance to confirmation
|
|
|
|
try coordinator.applyStaged(blockheight: sentTxHeight + 10)
|
|
|
|
sleep(2)
|
|
|
|
// rewind 5 blocks prior to sending
|
|
|
|
try coordinator.synchronizer.rewind(.height(blockheight: sentTxHeight - 5))
|
|
|
|
guard let np = try coordinator.synchronizer.allPendingTransactions().first(where: { $0.rawTransactionId == pendingTx.rawTransactionId}) else {
|
|
XCTFail("sent pending transaction not found after rewind")
|
|
return
|
|
}
|
|
|
|
XCTAssertFalse(np.isMined)
|
|
let confirmExpectation = XCTestExpectation(description: "confirm expectation")
|
|
notificationHandler.transactionsFound = { txs in
|
|
XCTAssertEqual(txs.count, 1)
|
|
guard let t = txs.first else {
|
|
XCTFail("should have found sent transaction but didn't")
|
|
return
|
|
}
|
|
XCTAssertEqual(t.rawTransactionId, pendingTx.rawTransactionId,"should have mined sent transaction but didn't")
|
|
}
|
|
notificationHandler.synchronizerMinedTransaction = { tx in
|
|
XCTFail("We shouldn't find any mined transactions at this point but found \(tx)")
|
|
}
|
|
try coordinator.sync(completion: { synchronizer in
|
|
confirmExpectation.fulfill()
|
|
}, error: { e in
|
|
self.handleError(e)
|
|
})
|
|
|
|
wait(for: [confirmExpectation], timeout: 10)
|
|
|
|
let confirmedPending = try coordinator.synchronizer.allPendingTransactions().first(where: { $0.rawTransactionId == pendingTx.rawTransactionId})
|
|
|
|
XCTAssertNil(confirmedPending, "pending, now confirmed transaction found")
|
|
|
|
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), 0)
|
|
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), 0)
|
|
}
|
|
}
|