ZcashLightClientKit/ZcashLightClientKitTests/AdvancedReOrgTests.swift

1212 lines
44 KiB
Swift

//
// AdvancedReOrgTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 5/14/20.
//
import XCTest
@testable import ZcashLightClientKit
// swiftlint:disable implicitly_unwrapped_optional force_unwrapping type_body_length
class AdvancedReOrgTests: XCTestCase {
// TODO: Parameterize this from environment?
// swiftlint:disable:next line_length
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"
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(description: "reorg")
let branchID = "2bb40e60"
let chainName = "main"
let network = DarksideWalletDNetwork()
override func setUpWithError() throws {
try super.setUpWithError()
coordinator = try TestCoordinator(
seed: seedPhrase,
walletBirthday: birthday,
channelProvider: ChannelProvider(),
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
}
override func tearDownWithError() throws {
try super.tearDownWithError()
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)
}
@objc func handleReorg(_ notification: Notification) {
guard
let reorgHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.reorgHeight] as? BlockHeight,
let rewindHeight = notification.userInfo?[CompactBlockProcessorNotificationKey.rewindHeight] as? BlockHeight
else {
XCTFail("empty reorg notification")
return
}
logger!.debug("--- REORG DETECTED \(reorgHeight)--- RewindHeight: \(rewindHeight)", file: #file, function: #function, line: #line)
XCTAssertEqual(reorgHeight, expectedReorgHeight)
reorgExpectation.fulfill()
}
/*
pre-condition: know balances before tx at received_Tx_height arrives
1. Setup w/ default dataset
2. applyStaged(received_Tx_height)
3. sync up to received_Tx_height
3a. verify that balance is previous balance + tx amount
4. get that transaction hex encoded data
5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 3
6. stage tx at received_Tx_height + 3
6a. applyheight(received_Tx_height + 1)
7. sync to received_Tx_height + 1
8. assert that reorg happened at received_Tx_height
9. verify that balance equals initial balance
10. sync up to received_Tx_height + 3
11. verify that balance equals initial balance + tx amount
*/
func testReOrgChangesInboundTxMinedHeight() throws {
hookToReOrgNotification()
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
var shouldContinue = false
let receivedTxHeight: BlockHeight = 663188
var initialTotalBalance: Int64 = -1
var initialVerifiedBalance: Int64 = -1
self.expectedReorgHeight = receivedTxHeight + 1
/*
precondition:know balances before tx at received_Tx_height arrives
*/
try coordinator.applyStaged(blockheight: receivedTxHeight - 1)
sleep(3)
let preTxExpectation = XCTestExpectation(description: "pre receive")
var synchronizer: SDKSynchronizer?
try coordinator.sync(
completion: { synchro in
synchronizer = synchro
initialVerifiedBalance = synchro.initializer.getVerifiedBalance()
initialTotalBalance = synchro.initializer.getBalance()
preTxExpectation.fulfill()
shouldContinue = true
},
error: self.handleError
)
wait(for: [preTxExpectation], timeout: 10)
guard shouldContinue else {
XCTFail("pre receive sync failed")
return
}
/*
2. applyStaged(received_Tx_height)
*/
try coordinator.applyStaged(blockheight: receivedTxHeight)
sleep(2)
/*
3. sync up to received_Tx_height
*/
let receivedTxExpectation = XCTestExpectation(description: "received tx")
var receivedTxTotalBalance = Int64(-1)
var receivedTxVerifiedBalance = Int64(-1)
try coordinator.sync(completion: { synchro in
synchronizer = synchro
receivedTxVerifiedBalance = synchro.initializer.getVerifiedBalance()
receivedTxTotalBalance = synchro.initializer.getBalance()
receivedTxExpectation.fulfill()
}, error: self.handleError)
sleep(2)
wait(for: [receivedTxExpectation], timeout: 10)
guard let syncedSynchronizer = synchronizer else {
XCTFail("nil synchronizer")
return
}
sleep(5)
guard let receivedTx = syncedSynchronizer.receivedTransactions.first, receivedTx.minedHeight == receivedTxHeight else {
XCTFail("did not receive transaction")
return
}
/*
3a. verify that balance is previous balance + tx amount
*/
XCTAssertEqual(receivedTxTotalBalance, initialTotalBalance + Int64(receivedTx.value))
XCTAssertEqual(receivedTxVerifiedBalance, initialVerifiedBalance)
/*
4. get that transaction hex encoded data
*/
guard let receivedTxData = receivedTx.raw else {
XCTFail("received tx has no raw data!")
return
}
let receivedRawTx = RawTransaction.with { rawTx in
rawTx.height = UInt64(receivedTxHeight)
rawTx.data = receivedTxData
}
/*
5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 4
*/
try coordinator.stageBlockCreate(height: receivedTxHeight, count: 5)
/*
6. stage tx at received_Tx_height + 3
*/
let reorgedTxheight = receivedTxHeight + 2
try coordinator.stageTransaction(receivedRawTx, at: reorgedTxheight)
/*
6a. applyheight(received_Tx_height + 1)
*/
try coordinator.applyStaged(blockheight: receivedTxHeight + 1)
sleep(2)
/*
7. sync to received_Tx_height + 1
*/
let reorgSyncexpectation = XCTestExpectation(description: "reorg expectation")
var afterReorgTxTotalBalance = Int64(-1)
var afterReorgTxVerifiedBalance = Int64(-1)
try coordinator.sync(completion: { synchronizer in
afterReorgTxTotalBalance = synchronizer.initializer.getBalance()
afterReorgTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
reorgSyncexpectation.fulfill()
}, error: self.handleError(_:))
/*
8. assert that reorg happened at received_Tx_height
*/
sleep(2)
wait(for: [reorgExpectation, reorgSyncexpectation], timeout: 5, enforceOrder: false)
/*
9. verify that balance equals initial balance
*/
XCTAssertEqual(afterReorgTxVerifiedBalance, initialVerifiedBalance)
XCTAssertEqual(afterReorgTxTotalBalance, initialTotalBalance)
/*
10. sync up to received_Tx_height + 3
*/
let finalsyncExpectation = XCTestExpectation(description: "final sync")
var finalReorgTxTotalBalance = Int64(-1)
var finalReorgTxVerifiedBalance = Int64(-1)
try coordinator.applyStaged(blockheight: reorgedTxheight + 1)
sleep(3)
try coordinator.sync(completion: { synchronizer in
finalReorgTxTotalBalance = synchronizer.initializer.getBalance()
finalReorgTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
finalsyncExpectation.fulfill()
}, error: self.handleError(_:))
wait(for: [finalsyncExpectation], timeout: 5)
sleep(3)
guard let reorgedTx = coordinator.synchronizer.receivedTransactions.first else {
XCTFail("no transactions found")
return
}
XCTAssertEqual(reorgedTx.minedHeight, reorgedTxheight)
XCTAssertEqual(initialVerifiedBalance, finalReorgTxVerifiedBalance)
XCTAssertEqual(initialTotalBalance + Int64(receivedTx.value), finalReorgTxTotalBalance)
}
/**
An outbound, unconfirmed transaction in a specific block changes height in the event of a reorg
The wallet handles this change, reflects it appropriately in local storage, and funds remain spendable post confirmation.
Pre-conditions:
- Wallet has spendable funds
1. Setup w/ default dataset
2. applyStaged(received_Tx_height)
3. sync up to received_Tx_height
4. create transaction
5. stage 10 empty blocks
6. submit tx at sentTxHeight
6a. getIncomingTx
6b. stageTransaction(sentTx, sentTxHeight)
6c. applyheight(sentTxHeight + 1 )
7. sync to sentTxHeight + 2
8. stage sentTx and otherTx at sentTxheight
9. applyStaged(sentTx + 2)
10. sync up to received_Tx_height + 2
11. verify that the sent tx is mined and balance is correct
12. applyStaged(sentTx + 10)
13. verify that there's no more pending transaction
*/
func testReorgChangesOutboundTxIndex() throws {
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
let receivedTxHeight: BlockHeight = 663188
var initialTotalBalance: Int64 = -1
/*
2. applyStaged(received_Tx_height)
*/
try coordinator.applyStaged(blockheight: receivedTxHeight)
sleep(2)
let preTxExpectation = XCTestExpectation(description: "pre receive")
/*
3. sync up to received_Tx_height
*/
try coordinator.sync(completion: { synchronizer in
initialTotalBalance = synchronizer.initializer.getBalance()
preTxExpectation.fulfill()
}, error: self.handleError)
wait(for: [preTxExpectation], timeout: 5)
let sendExpectation = XCTestExpectation(description: "sendToAddress")
var pendingEntity: PendingTransactionEntity?
var error: Error?
let sendAmount: Int64 = 10000
/*
4. create transaction
*/
coordinator.synchronizer.sendToAddress(
spendingKey: coordinator.spendingKeys!.first!,
zatoshi: sendAmount,
toAddress: testRecipientAddress,
memo: "test transaction",
from: 0
) { result in
switch result {
case .success(let pending):
pendingEntity = pending
case .failure(let e):
error = e
}
sendExpectation.fulfill()
}
wait(for: [sendExpectation], timeout: 12)
guard let pendingTx = pendingEntity else {
XCTFail("error sending to address. Error: \(String(describing: error))")
return
}
/*
5. stage 10 empty blocks
*/
try coordinator.stageBlockCreate(height: receivedTxHeight + 1, count: 10)
let sentTxHeight = receivedTxHeight + 1
/*
6. stage sent tx at sentTxHeight
*/
guard let sentTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("sent transaction not present on Darksidewalletd")
return
}
try coordinator.stageTransaction(sentTx, at: sentTxHeight)
/*
6a. applyheight(sentTxHeight + 1 )
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 1)
sleep(2)
/*
7. sync to sentTxHeight + 1
*/
let sentTxSyncExpectation = XCTestExpectation(description: "sent tx sync expectation")
try coordinator.sync(
completion: { synchronizer in
let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight
XCTAssertEqual(pMinedHeight, sentTxHeight)
sentTxSyncExpectation.fulfill()
},
error: self.handleError
)
wait(for: [sentTxSyncExpectation], timeout: 5)
/*
8. stage sentTx and otherTx at sentTxheight
*/
try coordinator.stageBlockCreate(height: sentTxHeight, count: 20, nonce: 5)
try coordinator.stageTransaction(url: FakeChainBuilder.someOtherTxUrl, at: sentTxHeight)
try coordinator.stageTransaction(sentTx, at: sentTxHeight)
/*
9. applyStaged(sentTx + 1)
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 1)
sleep(2)
let afterReOrgExpectation = XCTestExpectation(description: "after ReOrg Expectation")
try coordinator.sync(
completion: { synchronizer in
/*
11. verify that the sent tx is mined and balance is correct
*/
let pMinedHeight = synchronizer.pendingTransactions.first?.minedHeight
XCTAssertEqual(pMinedHeight, sentTxHeight)
XCTAssertEqual(initialTotalBalance - sendAmount - Int64(1000), synchronizer.initializer.getBalance()) // fee change on this branch
afterReOrgExpectation.fulfill()
},
error: self.handleError
)
wait(for: [afterReOrgExpectation], timeout: 5)
/*
12. applyStaged(sentTx + 10)
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 12)
sleep(2)
/*
13. verify that there's no more pending transaction
*/
let lastSyncExpectation = XCTestExpectation(description: "sync to confirmation")
try coordinator.sync(completion: { _ in
lastSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [lastSyncExpectation], timeout: 5)
XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 0)
XCTAssertEqual(initialTotalBalance - Int64(pendingTx.value) - Int64(1000), coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), coordinator.synchronizer.initializer.getVerifiedBalance())
}
func testIncomingTransactionIndexChange() throws {
hookToReOrgNotification()
self.expectedReorgHeight = 663196
self.expectedRewindHeight = 663175
try coordinator.reset(saplingActivation: birthday, branchID: "2bb40e60", chainName: "main")
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
try coordinator.applyStaged(blockheight: 663195)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync expectation")
var preReorgTotalBalance = Int64(0)
var preReorgVerifiedBalance = Int64(0)
try coordinator.sync(completion: { synchronizer in
preReorgTotalBalance = synchronizer.initializer.getBalance()
preReorgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 10)
/*
trigger reorg
*/
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeAfter))
try coordinator.applyStaged(blockheight: 663200)
sleep(1)
let afterReorgSync = XCTestExpectation(description: "after reorg sync")
var postReorgTotalBalance = Int64(0)
var postReorgVerifiedBalance = Int64(0)
try coordinator.sync(completion: { synchronizer in
postReorgTotalBalance = synchronizer.initializer.getBalance()
postReorgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
afterReorgSync.fulfill()
}, error: self.handleError)
wait(for: [reorgExpectation, afterReorgSync], timeout: 15)
XCTAssertEqual(postReorgVerifiedBalance, preReorgVerifiedBalance)
XCTAssertEqual(postReorgTotalBalance, preReorgTotalBalance)
}
func testReOrgExpiresInboundTransaction() throws {
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
let receivedTxHeight = BlockHeight(663188)
try coordinator.applyStaged(blockheight: receivedTxHeight - 1)
sleep(2)
let expectation = XCTestExpectation(description: "sync to \(receivedTxHeight - 1) expectation")
var initialBalance: Int64 = -1
var initialVerifiedBalance: Int64 = -1
try coordinator.sync(completion: { synchronizer in
initialBalance = synchronizer.initializer.getBalance()
initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
expectation.fulfill()
}, error: self.handleError)
wait(for: [expectation], timeout: 5)
let afterTxHeight = receivedTxHeight + 1
try coordinator.applyStaged(blockheight: afterTxHeight)
sleep(2)
let afterTxSyncExpectation = XCTestExpectation(description: "sync to \(afterTxHeight) expectation")
var afterTxBalance: Int64 = -1
var afterTxVerifiedBalance: Int64 = -1
try coordinator.sync(completion: { synchronizer in
afterTxBalance = synchronizer.initializer.getBalance()
afterTxVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
XCTAssertNotNil(
synchronizer.receivedTransactions.first { $0.minedHeight == receivedTxHeight },
"Transaction not found at \(receivedTxHeight)"
)
afterTxSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [afterTxSyncExpectation], timeout: 10.0)
XCTAssertEqual(initialVerifiedBalance, afterTxVerifiedBalance)
XCTAssertNotEqual(initialBalance, afterTxBalance)
let reorgSize: Int = 3
let newBlocksCount: Int = 11 + reorgSize
try coordinator.stageBlockCreate(height: receivedTxHeight - reorgSize, count: newBlocksCount + reorgSize)
try coordinator.applyStaged(blockheight: receivedTxHeight + newBlocksCount - 1)
sleep(2)
let afterReorgExpectation = XCTestExpectation(description: "after reorg expectation")
var afterReOrgBalance: Int64 = -1
var afterReOrgVerifiedBalance: Int64 = -1
try coordinator.sync(completion: { synchronizer in
afterReOrgBalance = synchronizer.initializer.getBalance()
afterReOrgVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
XCTAssertNil(
synchronizer.receivedTransactions.first { $0.minedHeight == receivedTxHeight },
"Transaction found at \(receivedTxHeight) after reorg"
)
afterReorgExpectation.fulfill()
}, error: self.handleError)
wait(for: [afterReorgExpectation], timeout: 5)
XCTAssertEqual(afterReOrgBalance, initialBalance)
XCTAssertEqual(afterReOrgVerifiedBalance, initialVerifiedBalance)
}
/**
Steps:
1. sync up to an incoming transaction (incomingTxHeight + 1)
1a. save balances
2. stage 4 blocks from incomingTxHeight - 1 with different nonce
3. stage otherTx at incomingTxHeight
4. stage incomingTx at incomingTxHeight
5. applyHeight(incomingHeight + 3)
6. sync to latest height
7. check that balances still match
*/
func testReOrgChangesInboundTxIndexInBlock() throws {
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
let incomingTxHeight = BlockHeight(663188)
try coordinator.applyStaged(blockheight: incomingTxHeight + 1)
/*
1. sync up to an incoming transaction (incomingTxHeight + 1)
*/
let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
var initialBalance: Int64 = -1
var initialVerifiedBalance: Int64 = -1
var incomingTx: ConfirmedTransactionEntity?
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
/*
1a. save balances
*/
initialBalance = coordinator.synchronizer.initializer.getBalance()
initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
incomingTx = coordinator.synchronizer.receivedTransactions.first(where: { $0.minedHeight == incomingTxHeight })
guard let transaction = incomingTx else {
XCTFail("no tx found")
return
}
guard let txRawData = transaction.raw else {
XCTFail("transaction has no raw data")
return
}
let rawTransaction = RawTransaction.with({ rawTx in
rawTx.data = txRawData
})
/*
2. stage 4 blocks from incomingTxHeight - 1 with different nonce
*/
let blockCount = 4
try coordinator.stageBlockCreate(height: incomingTxHeight - 1, count: blockCount, nonce: Int.random(in: 0 ... Int.max))
/*
3. stage otherTx at incomingTxHeight
*/
try coordinator.stageTransaction(url: FakeChainBuilder.someOtherTxUrl, at: incomingTxHeight)
/*
4. stage incomingTx at incomingTxHeight
5. applyHeight(incomingHeight + 3)
6. sync to latest height
7. check that balances still match
*/
try coordinator.stageTransaction(rawTransaction, at: incomingTxHeight)
/*
5. applyHeight(incomingHeight + 2)
*/
try coordinator.applyStaged(blockheight: incomingTxHeight + 2)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
/*
6. sync to latest height
*/
try coordinator.sync(completion: { _ in
lastSyncExpectation.fulfill()
}, error: self.handleError)
/*
7. check that balances still match
*/
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), initialVerifiedBalance)
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialBalance)
wait(for: [lastSyncExpectation], timeout: 5)
}
func testTxIndexReorg() throws {
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
let txReorgHeight = BlockHeight(663195)
let finalHeight = BlockHeight(663200)
try coordinator.applyStaged(blockheight: txReorgHeight)
let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
var initialBalance: Int64 = -1
var initialVerifiedBalance: Int64 = -1
try coordinator.sync(completion: { synchronizer in
initialBalance = synchronizer.initializer.getBalance()
initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeAfter))
try coordinator.applyStaged(blockheight: finalHeight)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
try coordinator.sync(completion: { _ in
lastSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [lastSyncExpectation], timeout: 5)
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialBalance)
XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), initialVerifiedBalance)
}
/**
A Re Org occurs and changes the height of an outbound transaction
Pre-condition: Wallet has funds
Steps:
1. create fake chain
1a. sync to latest height
2. send transaction to recipient address
3. getIncomingTransaction
4. stage transaction at sentTxHeight
5. applyHeight(sentTxHeight)
6. sync to latest height
6a. verify that there's a pending transaction with a mined height of sentTxHeight
7. stage 15 blocks from sentTxHeight
7. a stage sent tx to sentTxHeight + 2
8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg
9. sync to latest height
10. verify that there's a pending transaction with -1 mined height
11. applyHeight(sentTxHeight + 2)
11a. sync to latest height
12. verify that there's a pending transaction with a mined height of sentTxHeight + 2
13. apply height(sentTxHeight + 15)
14. sync to latest height
15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
*/
func testReOrgChangesOutboundTxMinedHeight() throws {
hookToReOrgNotification()
/*
1. create fake chain
*/
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
try coordinator.applyStaged(blockheight: 663188)
sleep(2)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/*
1a. sync to latest height
*/
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
sleep(1)
let initialTotalBalance = coordinator.synchronizer.initializer.getBalance()
let sendExpectation = XCTestExpectation(description: "send expectation")
var pendingEntity: PendingTransactionEntity?
/*
2. send transaction to recipient address
*/
coordinator.synchronizer.sendToAddress(
spendingKey: self.coordinator.spendingKeys!.first!,
zatoshi: 20000,
toAddress: self.testRecipientAddress,
memo: "this is a test",
from: 0,
resultBlock: { result in
switch result {
case .failure(let e):
self.handleError(e)
case .success(let pendingTx):
pendingEntity = pendingTx
}
sendExpectation.fulfill()
}
)
wait(for: [sendExpectation], timeout: 11)
guard pendingEntity != nil else {
XCTFail("no pending transaction after sending")
try coordinator.stop()
return
}
/**
3. getIncomingTransaction
*/
guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction")
try coordinator.stop()
return
}
let sentTxHeight: BlockHeight = 663189
/*
4. stage transaction at sentTxHeight
*/
try coordinator.stageBlockCreate(height: sentTxHeight)
try coordinator.stageTransaction(incomingTx, at: sentTxHeight)
/*
5. applyHeight(sentTxHeight)
*/
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2)
/*
6. sync to latest height
*/
let secondSyncExpectation = XCTestExpectation(description: "after send expectation")
try coordinator.sync(completion: { _ in
secondSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [secondSyncExpectation], timeout: 5)
XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1)
guard let afterStagePendingTx = coordinator.synchronizer.pendingTransactions.first else {
return
}
/*
6a. verify that there's a pending transaction with a mined height of sentTxHeight
*/
XCTAssertEqual(afterStagePendingTx.minedHeight, sentTxHeight)
/*
7. stage 20 blocks from sentTxHeight
*/
try coordinator.stageBlockCreate(height: sentTxHeight, count: 25)
/*
7a. stage sent tx to sentTxHeight + 2
*/
try coordinator.stageTransaction(incomingTx, at: sentTxHeight + 2)
/*
8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 1)
sleep(2)
/*
9. sync to latest height
*/
self.expectedReorgHeight = sentTxHeight + 1
let afterReorgExpectation = XCTestExpectation(description: "after reorg sync")
try coordinator.sync(completion: { _ in
afterReorgExpectation.fulfill()
}, error: self.handleError)
wait(for: [reorgExpectation, afterReorgExpectation], timeout: 5)
/*
10. verify that there's a pending transaction with -1 mined height
*/
guard let newPendingTx = coordinator.synchronizer.pendingTransactions.first else {
XCTFail("No pending transaction")
try coordinator.stop()
return
}
XCTAssertEqual(newPendingTx.minedHeight, BlockHeight.empty())
/*
11. applyHeight(sentTxHeight + 2)
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 2)
sleep(2)
let yetAnotherExpectation = XCTestExpectation(description: "after staging expectation")
/*
11a. sync to latest height
*/
try coordinator.sync(completion: { _ in
yetAnotherExpectation.fulfill()
}, error: self.handleError)
wait(for: [yetAnotherExpectation], timeout: 5)
/*
12. verify that there's a pending transaction with a mined height of sentTxHeight + 2
*/
XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 1)
guard let newlyPendingTx = try coordinator.synchronizer.allPendingTransactions().first else {
XCTFail("no pending transaction")
try coordinator.stop()
return
}
XCTAssertEqual(newlyPendingTx.minedHeight, sentTxHeight + 2)
/*
13. apply height(sentTxHeight + 25)
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 25)
sleep(2)
let thisIsTheLastExpectationIPromess = XCTestExpectation(description: "last sync")
/*
14. sync to latest height
*/
try coordinator.sync(completion: { _ in
thisIsTheLastExpectationIPromess.fulfill()
}, error: self.handleError)
wait(for: [thisIsTheLastExpectationIPromess], timeout: 5)
/*
15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
*/
XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 0)
XCTAssertNotNil(
coordinator.synchronizer.sentTransactions
.first(
where: { transaction in
guard let txId = transaction.rawTransactionId else { return false }
return txId == newlyPendingTx.rawTransactionId
}
),
"Sent Tx is not on sent transactions"
)
XCTAssertEqual(
initialTotalBalance - Int64(newlyPendingTx.value) - Int64(1000),
coordinator.synchronizer.initializer.getBalance()
)
XCTAssertEqual(
initialTotalBalance - Int64(newlyPendingTx.value) - Int64(1000),
coordinator.synchronizer.initializer.getVerifiedBalance()
)
}
/**
Uses the zcash-hackworks data set.
A Re Org occurs at 663195, and sweeps an Inbound Tx that appears later on the chain.
Steps:
1. reset dlwd
2. load blocks from txHeightReOrgBefore
3. applyStaged(663195)
4. sync to latest height
5. get balances
6. load blocks from dataset txHeightReOrgBefore
7. apply stage 663200
8. sync to latest height
9. verify that the balance is equal to the one before the reorg
*/
func testReOrgChangesInboundMinedHeight() throws {
try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
sleep(2)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgBefore))
sleep(2)
try coordinator.applyStaged(blockheight: 663195)
sleep(2)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
let initialBalance = coordinator.synchronizer.initializer.getBalance()
let initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
guard let initialTxHeight = try coordinator.synchronizer.allReceivedTransactions().first?.minedHeight else {
XCTFail("no incoming transaction found!")
return
}
try coordinator.resetBlocks(dataset: .predefined(dataset: .txHeightReOrgAfter))
sleep(5)
try coordinator.applyStaged(blockheight: 663200)
sleep(6)
let afterReOrgExpectation = XCTestExpectation(description: "after reorg")
try coordinator.sync(completion: { _ in
afterReOrgExpectation.fulfill()
}, error: self.handleError)
wait(for: [afterReOrgExpectation], timeout: 5)
guard let afterReOrgTxHeight = coordinator.synchronizer.receivedTransactions.first?.minedHeight else {
XCTFail("no incoming transaction found after re org!")
return
}
XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(initialBalance, coordinator.synchronizer.initializer.getBalance())
XCTAssert(afterReOrgTxHeight > initialTxHeight)
}
/**
Re Org removes incoming transaction and is never mined
Steps:
1. sync prior to incomingTxHeight - 1 to get balances there
2. sync to latest height
3. cause reorg
4. sync to latest height
5. verify that reorg Happened at reorgHeight
6. verify that balances match initial balances
*/
func testReOrgRemovesIncomingTxForever() throws {
hookToReOrgNotification()
try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxBefore))
let reorgHeight: BlockHeight = 663195
self.expectedReorgHeight = reorgHeight
self.expectedRewindHeight = reorgHeight - 10
try coordinator.applyStaged(blockheight: reorgHeight - 1)
sleep(2)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/**
1. sync prior to incomingTxHeight - 1 to get balances there
*/
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
let initialTotalBalance = coordinator.synchronizer.initializer.getBalance()
let initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
try coordinator.applyStaged(blockheight: reorgHeight)
let secondSyncExpectation = XCTestExpectation(description: "second sync expectation")
/**
2. sync to latest height
*/
try coordinator.sync(completion: { _ in
secondSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [secondSyncExpectation], timeout: 10)
/**
3. cause reorg
*/
try coordinator.resetBlocks(dataset: .predefined(dataset: .txReOrgRemovesInboundTxAfter))
try coordinator.applyStaged(blockheight: 663200)
sleep(2)
let afterReorgSyncExpectation = XCTestExpectation(description: "after reorg expectation")
try coordinator.sync(completion: { _ in
afterReorgSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [afterReorgSyncExpectation], timeout: 5)
XCTAssertEqual(initialVerifiedBalance, coordinator.synchronizer.initializer.getVerifiedBalance())
XCTAssertEqual(initialTotalBalance, coordinator.synchronizer.initializer.getBalance())
}
/**
Transaction was included in a block, and then is not included in a block after a reorg, and expires.
Steps:
1. create fake chain
1a. sync to latest height
2. send transaction to recipient address
3. getIncomingTransaction
4. stage transaction at sentTxHeight
5. applyHeight(sentTxHeight)
6. sync to latest height
6a. verify that there's a pending transaction with a mined height of sentTxHeight
7. stage 15 blocks from sentTxHeigth to cause a reorg
8. sync to latest height
9. verify that there's an expired transaction as a pending transaction
*/
func testReOrgRemovesOutboundTxAndIsNeverMined() throws {
hookToReOrgNotification()
/*
1. create fake chain
*/
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
let sentTxHeight: BlockHeight = 663195
try coordinator.applyStaged(blockheight: sentTxHeight - 1)
sleep(2)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/*
1a. sync to latest height
*/
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [firstSyncExpectation], timeout: 5)
sleep(1)
let initialTotalBalance = coordinator.synchronizer.initializer.getBalance()
let sendExpectation = XCTestExpectation(description: "send expectation")
var pendingEntity: PendingTransactionEntity?
/*
2. send transaction to recipient address
*/
coordinator.synchronizer.sendToAddress(
spendingKey: self.coordinator.spendingKeys!.first!,
zatoshi: 20000,
toAddress: self.testRecipientAddress,
memo: "this is a test",
from: 0,
resultBlock: { result in
switch result {
case .failure(let e):
self.handleError(e)
case .success(let pendingTx):
pendingEntity = pendingTx
}
sendExpectation.fulfill()
}
)
wait(for: [sendExpectation], timeout: 11)
guard pendingEntity != nil else {
XCTFail("no pending transaction after sending")
try coordinator.stop()
return
}
/**
3. getIncomingTransaction
*/
guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction")
try coordinator.stop()
return
}
self.expectedReorgHeight = sentTxHeight + 1
/*
4. stage transaction at sentTxHeight
*/
try coordinator.stageBlockCreate(height: sentTxHeight)
try coordinator.stageTransaction(incomingTx, at: sentTxHeight)
/*
5. applyHeight(sentTxHeight)
*/
try coordinator.applyStaged(blockheight: sentTxHeight)
sleep(2)
/*
6. sync to latest height
*/
let secondSyncExpectation = XCTestExpectation(description: "after send expectation")
try coordinator.sync(completion: { _ in
secondSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [secondSyncExpectation], timeout: 5)
let extraBlocks = 25
try coordinator.stageBlockCreate(height: sentTxHeight, count: extraBlocks, nonce: 5)
try coordinator.applyStaged(blockheight: sentTxHeight + 5)
sleep(2)
let reorgSyncExpectation = XCTestExpectation(description: "reorg sync expectation")
try coordinator.sync(completion: { _ in
reorgSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [reorgExpectation, reorgSyncExpectation], timeout: 5)
guard let pendingTx = coordinator.synchronizer.pendingTransactions.first else {
XCTFail("no pending transaction after reorg sync")
return
}
XCTAssertFalse(pendingTx.isMined)
LoggerProxy.info("applyStaged(blockheight: \(sentTxHeight + extraBlocks - 1))")
try coordinator.applyStaged(blockheight: sentTxHeight + extraBlocks - 1)
sleep(2)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
try coordinator.sync(completion: { _ in
lastSyncExpectation.fulfill()
}, error: self.handleError)
wait(for: [lastSyncExpectation], timeout: 5)
XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialTotalBalance)
}
func testLongSync() throws {
hookToReOrgNotification()
/*
1. create fake chain
*/
let fullSyncLength = 100_000
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName, length: fullSyncLength)
try coordinator.applyStaged(blockheight: birthday + fullSyncLength)
sleep(10)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/*
sync to latest height
*/
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: { error in
_ = try? self.coordinator.stop()
firstSyncExpectation.fulfill()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
})
wait(for: [firstSyncExpectation], timeout: 500)
XCTAssertEqual(try coordinator.synchronizer.latestDownloadedHeight(), birthday + fullSyncLength)
}
func handleError(_ error: Error?) {
_ = try? coordinator.stop()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
func hookToReOrgNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(handleReorg(_:)), name: .blockProcessorHandledReOrg, object: nil)
}
}