ZcashLightClientKit/Tests/DarksideTests/AdvancedReOrgTests.swift

1391 lines
50 KiB
Swift

//
// AdvancedReOrgTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 5/14/20.
//
import Combine
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
class AdvancedReOrgTests: ZcashTestCase {
let sendAmount = Zatoshi(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()
var cancellables: [AnyCancellable] = []
override func setUp() async throws {
try await super.setUp()
// don't use an exact birthday, users never do.
self.coordinator = try await TestCoordinator(
container: mockContainer,
walletBirthday: birthday + 50,
network: network
)
try coordinator.reset(saplingActivation: 663150, branchID: self.branchID, chainName: self.chainName)
}
override func tearDown() async throws {
try await super.tearDown()
let coordinator = self.coordinator!
self.coordinator = nil
cancellables = []
try await coordinator.stop()
try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
}
func handleReorg(event: CompactBlockProcessor.Event) {
guard case let .handledReorg(reorgHeight, rewindHeight) = event else { return XCTFail("empty reorg event") }
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() async throws {
await hookToReOrgNotification()
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
var shouldContinue = false
let receivedTxHeight: BlockHeight = 663188
var initialTotalBalance = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-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?
do {
try await coordinator.sync(
completion: { synchro in
synchronizer = synchro
initialVerifiedBalance = try await synchro.getShieldedVerifiedBalance()
initialTotalBalance = try await synchro.getShieldedBalance()
preTxExpectation.fulfill()
shouldContinue = true
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [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 = Zatoshi(-1)
var receivedTxVerifiedBalance = Zatoshi(-1)
do {
try await coordinator.sync(
completion: { synchro in
synchronizer = synchro
receivedTxVerifiedBalance = try await synchro.getShieldedVerifiedBalance()
receivedTxTotalBalance = try await synchro.getShieldedBalance()
receivedTxExpectation.fulfill()
}, error: self.handleError
)
} catch {
await handleError(error)
}
sleep(2)
await fulfillment(of: [receivedTxExpectation], timeout: 10)
guard let syncedSynchronizer = synchronizer else {
XCTFail("nil synchronizer")
return
}
sleep(5)
guard let receivedTx = await 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 + receivedTx.value)
XCTAssertEqual(receivedTxVerifiedBalance, initialVerifiedBalance)
/*
4. get that transaction hex encoded data
*/
let receivedTxData = receivedTx.raw ?? Data()
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 = Zatoshi(-1)
var afterReorgTxVerifiedBalance = Zatoshi(-1)
do {
try await coordinator.sync(
completion: { synchronizer in
afterReorgTxTotalBalance = try await synchronizer.getShieldedBalance()
afterReorgTxVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
reorgSyncexpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
/*
8. assert that reorg happened at received_Tx_height
*/
sleep(2)
await fulfillment(of: [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 = Zatoshi(-1)
var finalReorgTxVerifiedBalance = Zatoshi(-1)
try coordinator.applyStaged(blockheight: reorgedTxheight + 1)
sleep(3)
do {
try await coordinator.sync(
completion: { synchronizer in
finalReorgTxTotalBalance = try await synchronizer.getShieldedBalance()
finalReorgTxVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
finalsyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [finalsyncExpectation], timeout: 5)
sleep(3)
guard let reorgedTx = await coordinator.synchronizer.receivedTransactions.first else {
XCTFail("no transactions found")
return
}
XCTAssertEqual(reorgedTx.minedHeight, reorgedTxheight)
XCTAssertEqual(initialVerifiedBalance, finalReorgTxVerifiedBalance)
XCTAssertEqual(initialTotalBalance + 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
/// a. getIncomingTx
/// b. stageTransaction(sentTx, sentTxHeight)
/// c. 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() async throws {
try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName)
let receivedTxHeight: BlockHeight = 663188
var initialTotalBalance = Zatoshi(-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
*/
do {
try await coordinator.sync(
completion: { synchronizer in
initialTotalBalance = try await synchronizer.getShieldedBalance()
preTxExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [preTxExpectation], timeout: 5)
let sendExpectation = XCTestExpectation(description: "sendToAddress")
var pendingEntity: ZcashTransaction.Overview?
var testError: Error?
let sendAmount = Zatoshi(10000)
/*
4. create transaction
*/
do {
let pendingTx = try await coordinator.synchronizer.sendToAddress(
spendingKey: coordinator.spendingKey,
zatoshi: sendAmount,
toAddress: try Recipient(Environment.testRecipientAddress, network: self.network.networkType),
memo: try Memo(string: "test transaction")
)
pendingEntity = pendingTx
sendExpectation.fulfill()
} catch {
testError = error
XCTFail("error sending to address. Error: \(String(describing: error))")
}
await fulfillment(of: [sendExpectation], timeout: 2)
guard let pendingTx = pendingEntity else {
XCTFail("error sending to address. Error: \(String(describing: testError))")
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")
do {
try await coordinator.sync(
completion: { synchronizer in
let pMinedHeight = await synchronizer.pendingTransactions.first?.minedHeight
XCTAssertEqual(pMinedHeight, sentTxHeight)
sentTxSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [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)
print("Starting after reorg sync")
let afterReOrgExpectation = XCTestExpectation(description: "after ReOrg Expectation")
do {
try await coordinator.sync(
completion: { synchronizer in
/*
11. verify that the sent tx is mined and balance is correct
*/
let pMinedHeight = await synchronizer.pendingTransactions.first?.minedHeight
XCTAssertEqual(pMinedHeight, sentTxHeight)
// fee change on this branch
let expectedBalance = try await synchronizer.getShieldedBalance()
XCTAssertEqual(initialTotalBalance - sendAmount - Zatoshi(1000), expectedBalance)
afterReOrgExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [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")
do {
try await coordinator.sync(
completion: { _ in
lastSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [lastSyncExpectation], timeout: 5)
let expectedVerifiedBalance = initialTotalBalance + pendingTx.value
let currentVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedPendingTransactionsCount = await coordinator.synchronizer.pendingTransactions.count
XCTAssertEqual(expectedPendingTransactionsCount, 0)
XCTAssertEqual(expectedVerifiedBalance, currentVerifiedBalance)
let resultingBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(resultingBalance, currentVerifiedBalance)
}
func testIncomingTransactionIndexChange() async throws {
await 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 = Zatoshi.zero
var preReorgVerifiedBalance = Zatoshi.zero
try await coordinator.sync(
completion: { synchronizer in
preReorgTotalBalance = try await synchronizer.getShieldedBalance()
preReorgVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
firstSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [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 = Zatoshi.zero
var postReorgVerifiedBalance = Zatoshi.zero
try await coordinator.sync(
completion: { synchronizer in
postReorgTotalBalance = try await synchronizer.getShieldedBalance()
postReorgVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
afterReorgSync.fulfill()
},
error: self.handleError
)
await fulfillment(of: [reorgExpectation, afterReorgSync], timeout: 30)
XCTAssertEqual(postReorgVerifiedBalance, preReorgVerifiedBalance)
XCTAssertEqual(postReorgTotalBalance, preReorgTotalBalance)
}
func testReOrgExpiresInboundTransaction() async 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 = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-1)
try await coordinator.sync(
completion: { synchronizer in
initialBalance = try await synchronizer.getShieldedBalance()
initialVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
expectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [expectation], timeout: 5)
let afterTxHeight = receivedTxHeight + 1
try coordinator.applyStaged(blockheight: afterTxHeight)
sleep(2)
let afterTxSyncExpectation = XCTestExpectation(description: "sync to \(afterTxHeight) expectation")
var afterTxBalance = Zatoshi(-1)
var afterTxVerifiedBalance = Zatoshi(-1)
try await coordinator.sync(
completion: { synchronizer in
afterTxBalance = try await synchronizer.getShieldedBalance()
afterTxVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
let receivedTransactions = await synchronizer.receivedTransactions
XCTAssertNotNil(
receivedTransactions.first { $0.minedHeight == receivedTxHeight },
"Transaction not found at \(receivedTxHeight)"
)
afterTxSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [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 = Zatoshi(-1)
var afterReOrgVerifiedBalance = Zatoshi(-1)
try await coordinator.sync(
completion: { synchronizer in
afterReOrgBalance = try await synchronizer.getShieldedBalance()
afterReOrgVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
let receivedTransactions = await synchronizer.receivedTransactions
XCTAssertNil(
receivedTransactions.first { $0.minedHeight == receivedTxHeight },
"Transaction found at \(receivedTxHeight) after reorg"
)
afterReorgExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [afterReorgExpectation], timeout: 5)
XCTAssertEqual(afterReOrgBalance, initialBalance)
XCTAssertEqual(afterReOrgVerifiedBalance, initialVerifiedBalance)
guard
let receivedTransaction = try await coordinator.synchronizer.allTransactions().first
else {
XCTFail("expected to have a received transaction, but found none")
return
}
let transactionOutputs = await coordinator.synchronizer.getTransactionOutputs(
for: receivedTransaction
)
guard transactionOutputs.count == 1 else {
XCTFail("expected output count to be 1")
return
}
let output = transactionOutputs[0]
XCTAssertEqual(output.recipient, .internalAccount(0))
XCTAssertEqual(output.value, Zatoshi(100000))
}
/// 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() async throws {
try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName)
let incomingTxHeight = BlockHeight(663188)
try coordinator.applyStaged(blockheight: incomingTxHeight + 1)
sleep(1)
/*
1. sync up to an incoming transaction (incomingTxHeight + 1)
*/
let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
var initialBalance = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-1)
var incomingTx: ZcashTransaction.Overview!
try await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError
)
await fulfillment(of: [firstSyncExpectation], timeout: 5)
/*
1a. save balances
*/
initialBalance = try await coordinator.synchronizer.getShieldedBalance()
initialVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
incomingTx = await coordinator.synchronizer.receivedTransactions.first(where: { $0.minedHeight == incomingTxHeight })
let txRawData = incomingTx.raw ?? Data()
let rawTransaction = RawTransaction.with({ rawTx in
rawTx.data = txRawData
})
/*
2. stage 10 blocks from incomingTxHeight - 1 with different nonce
*/
let blockCount = 10
try coordinator.stageBlockCreate(height: incomingTxHeight - 1, count: blockCount, nonce: 1)
/*
3. stage otherTx at incomingTxHeight
*/
try coordinator.stageTransaction(url: FakeChainBuilder.someOtherTxUrl, at: incomingTxHeight)
/*
4. stage incomingTx at incomingTxHeight
*/
try coordinator.stageTransaction(rawTransaction, at: incomingTxHeight)
/*
5. applyHeight(incomingHeight + 3)
*/
try coordinator.applyStaged(blockheight: incomingTxHeight + 3)
sleep(1)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
/*
6. sync to latest height
*/
try await coordinator.sync(
completion: { _ in
lastSyncExpectation.fulfill()
}, error: self.handleError
)
await fulfillment(of: [lastSyncExpectation], timeout: 5)
/*
7. check that balances still match
*/
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedVerifiedBalance, initialVerifiedBalance)
XCTAssertEqual(expectedBalance, initialBalance)
}
func testTxIndexReorg() async throws {
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeBefore))
let txReorgHeight = BlockHeight(663195)
let finalHeight = BlockHeight(663200)
try coordinator.applyStaged(blockheight: txReorgHeight)
sleep(1)
let firstSyncExpectation = XCTestExpectation(description: "first sync test expectation")
var initialBalance = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-1)
try await coordinator.sync(
completion: { synchronizer in
initialBalance = try await synchronizer.getShieldedBalance()
initialVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
firstSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [firstSyncExpectation], timeout: 5)
try coordinator.resetBlocks(dataset: .predefined(dataset: .txIndexChangeAfter))
try coordinator.applyStaged(blockheight: finalHeight)
sleep(1)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
try await coordinator.sync(
completion: { _ in
lastSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [lastSyncExpectation], timeout: 5)
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedBalance, initialBalance)
XCTAssertEqual(expectedVerifiedBalance, 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() async throws {
await 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
*/
do {
try await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [firstSyncExpectation], timeout: 5)
sleep(1)
let initialTotalBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
let sendExpectation = XCTestExpectation(description: "send expectation")
var pendingEntity: ZcashTransaction.Overview?
/*
2. send transaction to recipient address
*/
let recipient = try Recipient(Environment.testRecipientAddress, network: self.network.networkType)
do {
let pendingTx = try await coordinator.synchronizer.sendToAddress(
spendingKey: self.coordinator.spendingKey,
zatoshi: Zatoshi(20000),
toAddress: recipient,
memo: try Memo(string: "this is a test")
)
pendingEntity = pendingTx
sendExpectation.fulfill()
} catch {
await handleError(error)
}
await fulfillment(of: [sendExpectation], timeout: 11)
guard pendingEntity != nil else {
XCTFail("no pending transaction after sending")
try await coordinator.stop()
return
}
/**
3. getIncomingTransaction
*/
guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction")
try await 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")
do {
try await coordinator.sync(
completion: { _ in
secondSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [secondSyncExpectation], timeout: 5)
var pendingTransactionsCount = await coordinator.synchronizer.pendingTransactions.count
XCTAssertEqual(pendingTransactionsCount, 1)
guard let afterStagePendingTx = await 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")
do {
try await coordinator.sync(
completion: { _ in
afterReorgExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [reorgExpectation, afterReorgExpectation], timeout: 5)
/*
10. verify that there's a pending transaction with -1 mined height
*/
guard let newPendingTx = await coordinator.synchronizer.pendingTransactions.first else {
XCTFail("No pending transaction")
try await coordinator.stop()
return
}
XCTAssertNil(newPendingTx.minedHeight)
/*
11. applyHeight(sentTxHeight + 2)
*/
try coordinator.applyStaged(blockheight: sentTxHeight + 2)
sleep(2)
let yetAnotherExpectation = XCTestExpectation(description: "after staging expectation")
/*
11a. sync to latest height
*/
do {
try await coordinator.sync(
completion: { _ in
yetAnotherExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [yetAnotherExpectation], timeout: 5)
/*
12. verify that there's a pending transaction with a mined height of sentTxHeight + 2
*/
pendingTransactionsCount = await coordinator.synchronizer.pendingTransactions.count
XCTAssertEqual(pendingTransactionsCount, 1)
guard let newlyPendingTx = try await coordinator.synchronizer.allPendingTransactions().first(where: { $0.isSentTransaction }) else {
XCTFail("no pending transaction")
try await 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
*/
do {
try await coordinator.sync(
completion: { _ in
thisIsTheLastExpectationIPromess.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [thisIsTheLastExpectationIPromess], timeout: 5)
/*
15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
*/
let pendingTranscationsCount = await coordinator.synchronizer.pendingTransactions.count
XCTAssertEqual(pendingTranscationsCount, 0)
let sentTransactions = await coordinator.synchronizer.sentTransactions
.first(
where: { transaction in
return transaction.rawID == newlyPendingTx.rawID
}
)
XCTAssertNotNil(
sentTransactions,
"Sent Tx is not on sent transactions"
)
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(
initialTotalBalance + newlyPendingTx.value, // Note: sent transactions have negative values
expectedBalance
)
XCTAssertEqual(
initialTotalBalance + newlyPendingTx.value, // Note: sent transactions have negative values
expectedVerifiedBalance
)
let txRecipients = await coordinator.synchronizer.getRecipients(for: newPendingTx)
XCTAssertEqual(txRecipients.count, 2)
XCTAssertNotNil(txRecipients.first(where: { $0 == .internalAccount(0) }))
XCTAssertNotNil(txRecipients.first(where: { $0 == .address(recipient) }))
}
/// 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() async 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 await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [firstSyncExpectation], timeout: 5)
let initialBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
let initialVerifiedBalance: Zatoshi = try await coordinator.synchronizer.getShieldedVerifiedBalance()
guard let initialTxHeight = try await 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 await coordinator.sync(
completion: { _ in
afterReOrgExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [afterReOrgExpectation], timeout: 5)
guard let afterReOrgTxHeight = await coordinator.synchronizer.receivedTransactions.first?.minedHeight else {
XCTFail("no incoming transaction found after re org!")
return
}
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(initialVerifiedBalance, expectedVerifiedBalance)
XCTAssertEqual(initialBalance, expectedBalance)
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
// FIXME [#644]: Test works with lightwalletd v0.4.13 but is broken when using newer lightwalletd. More info is in #644.
func testReOrgRemovesIncomingTxForever() async throws {
await 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 await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [firstSyncExpectation], timeout: 5)
let initialTotalBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
let initialVerifiedBalance: Zatoshi = try await coordinator.synchronizer.getShieldedVerifiedBalance()
try coordinator.applyStaged(blockheight: reorgHeight - 1)
sleep(1)
let secondSyncExpectation = XCTestExpectation(description: "second sync expectation")
/**
2. sync to latest height
*/
try await coordinator.sync(
completion: { _ in
secondSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [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 await coordinator.sync(
completion: { _ in
afterReorgSyncExpectation.fulfill()
},
error: self.handleError
)
await fulfillment(of: [afterReorgSyncExpectation], timeout: 5)
let expectedVerifiedBalance = try await coordinator.synchronizer.getShieldedVerifiedBalance()
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(initialVerifiedBalance, expectedVerifiedBalance)
XCTAssertEqual(initialTotalBalance, expectedBalance)
}
/// 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() async throws {
await 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
*/
do {
try await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [firstSyncExpectation], timeout: 10)
sleep(1)
let initialTotalBalance: Zatoshi = try await coordinator.synchronizer.getShieldedBalance()
let sendExpectation = XCTestExpectation(description: "send expectation")
var pendingEntity: ZcashTransaction.Overview?
/*
2. send transaction to recipient address
*/
do {
let pendingTx = try await coordinator.synchronizer.sendToAddress(
spendingKey: self.coordinator.spendingKey,
zatoshi: Zatoshi(20000),
toAddress: try Recipient(Environment.testRecipientAddress, network: self.network.networkType),
memo: try! Memo(string: "this is a test")
)
pendingEntity = pendingTx
sendExpectation.fulfill()
} catch {
await handleError(error)
}
await fulfillment(of: [sendExpectation], timeout: 11)
guard pendingEntity != nil else {
XCTFail("no pending transaction after sending")
try await coordinator.stop()
return
}
/**
3. getIncomingTransaction
*/
guard let incomingTx = try coordinator.getIncomingTransactions()?.first else {
XCTFail("no incoming transaction")
try await 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")
do {
try await coordinator.sync(
completion: { _ in
secondSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [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")
do {
try await coordinator.sync(
completion: { _ in
reorgSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [reorgExpectation, reorgSyncExpectation], timeout: 5)
guard let pendingTx = await coordinator.synchronizer.pendingTransactions.first else {
XCTFail("no pending transaction after reorg sync")
return
}
XCTAssertNil(pendingTx.minedHeight)
LoggerProxy.info("applyStaged(blockheight: \(sentTxHeight + extraBlocks - 1))")
try coordinator.applyStaged(blockheight: sentTxHeight + extraBlocks - 1)
sleep(2)
let lastSyncExpectation = XCTestExpectation(description: "last sync expectation")
do {
try await coordinator.sync(
completion: { _ in
lastSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [lastSyncExpectation], timeout: 5)
let expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedBalance, initialTotalBalance)
}
func testLongSync() async throws {
await 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(20)
let firstSyncExpectation = XCTestExpectation(description: "first sync")
/*
sync to latest height
*/
do {
try await coordinator.sync(
completion: { _ in
firstSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [firstSyncExpectation], timeout: 600)
let latestScannedHeight = await coordinator.synchronizer.latestBlocksDataProvider.latestScannedHeight
XCTAssertEqual(latestScannedHeight, birthday + fullSyncLength)
}
func handleError(_ error: Error?) async {
_ = try? await coordinator.stop()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
func hookToReOrgNotification() async {
let eventClosure: CompactBlockProcessor.EventClosure = { [weak self] event in
switch event {
case .handledReorg: self?.handleReorg(event: event)
default: break
}
}
await coordinator.synchronizer.blockProcessor.updateEventClosure(identifier: "tests", closure: eventClosure)
}
}