ZcashLightClientKit/Tests/DarksideTests/ShieldFundsTests.swift

348 lines
14 KiB
Swift

//
// ShieldFundsTests.swift
// ZcashLightClientSample
//
// Created by Francisco Gindre on 4/12/22.
// Copyright © 2022 Electric Coin Company. All rights reserved.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
// FIXME: [#587] disabled until https://github.com/zcash/ZcashLightClientKit/issues/587 fixed
class ShieldFundsTests: ZcashTestCase {
let sendAmount = Zatoshi(1000)
var birthday: BlockHeight = 1631000
var coordinator: TestCoordinator!
var syncedExpectation = XCTestExpectation(description: "synced")
var sentTransactionExpectation = XCTestExpectation(description: "sent")
let branchID = "e9ff75a6"
let chainName = "main"
let network = DarksideWalletDNetwork()
override func setUp() async throws {
try await super.setUp()
self.coordinator = try await TestCoordinator(
container: mockContainer,
walletBirthday: birthday,
network: network
)
try coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName)
try coordinator.service.clearAddedUTXOs()
}
override func tearDown() async throws {
try await super.tearDown()
let coordinator = self.coordinator!
self.coordinator = nil
try await coordinator.stop()
try? FileManager.default.removeItem(at: coordinator.databases.fsCacheDbRoot)
try? FileManager.default.removeItem(at: coordinator.databases.dataDB)
}
/// Tests shielding funds from a UTXO
///
/// This test uses the dataset `shield-funds` on the repo `darksidewalletd-test-data`
/// (see: https://github.com/zcash-hackworks/darksidewalletd-test-data)
/// The dataset consists on a wallet that has no shielded funds and suddenly encounters a UTXO
/// at `utxoHeight` with 10000 zatoshi that will attempt to shield them.
///
/// Steps:
/// 1. load the dataset
/// 2. applyStaged to `utxoHeight - 1`
/// 3. sync up to that height
/// at this point the balance should be all zeroes for transparent and shielded funds
/// 4. Add the UTXO to darksidewalletd fake chain
/// 5. advance chain to the `utxoHeight`
/// 6. Sync and find the UXTO on chain.
/// at this point the balance should be zero for shielded, then zero verified transparent funds
/// and 10000 zatoshi of total (not verified) transparent funds.
/// 7. stage ten blocks and confirm the transparent funds at `utxoHeight + 10`
/// 8. sync up to chain tip.
/// the transparent funds should be 10000 zatoshis both total and verified
/// 9. shield the funds
/// when funds are shielded the UTXOs should be marked as spend and not shown on the balance.
/// now balance should be zero shielded, zero transaparent.
/// 10. clear the UTXO from darksidewalletd's cache
/// 11. stage the pending shielding transaction in darksidewalletd ad `utxoHeight + 12`
/// 12. advance the chain tip to sync the now mined shielding transaction
/// 13. sync up to chain tip
/// Now it should verify that the balance has been shielded. The resulting balance should be zero
/// transparent funds and `10000 - fee` total shielded funds, zero verified shielded funds.
/// Fees at the time of writing the tests are 1000 zatoshi as defined on ZIP-313
/// 14. proceed confirm the shielded funds by staging ten more blocks
/// 15. sync up to the new chain tip
/// verify that the shielded transactions are confirmed
///
func testShieldFunds() async throws {
// 1. load the dataset
try coordinator.service.useDataset(from: "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/shield-funds/1631000.txt")
sleep(1)
try coordinator.stageBlockCreate(height: birthday + 1, count: 200, nonce: 0)
sleep(1)
let utxoHeight = BlockHeight(1631177)
var shouldContinue = false
var initialTotalBalance = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-1)
var initialTransparentBalance: WalletBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
let utxo = try GetAddressUtxosReply(jsonString:
"""
{
"txid": "3md9M0OOpPBsF02Rp2b7CJZMpv093bjLuSCIG1RPioU=",
"script": "dqkU1mkF+eETNMCYyJs0OZcygn0KDi+IrA==",
"valueZat": "10000",
"height": "1631177",
"address": "t1dRJRY7GmyeykJnMH38mdQoaZtFhn1QmGz"
}
""")
// 2. applyStaged to `utxoHeight - 1`
try coordinator.service.applyStaged(nextLatestHeight: utxoHeight - 1)
sleep(2)
let preTxExpectation = XCTestExpectation(description: "pre receive")
// 3. sync up to that height
do {
try await coordinator.sync(
completion: { synchronizer in
initialVerifiedBalance = try await synchronizer.getShieldedVerifiedBalance()
initialTotalBalance = try await synchronizer.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
}
// at this point the balance should be all zeroes for transparent and shielded funds
XCTAssertEqual(initialTotalBalance, Zatoshi.zero)
XCTAssertEqual(initialVerifiedBalance, Zatoshi.zero)
initialTransparentBalance = (try? await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)) ?? .zero
XCTAssertEqual(initialTransparentBalance.total, .zero)
XCTAssertEqual(initialTransparentBalance.verified, .zero)
// 4. Add the UTXO to darksidewalletd fake chain
try coordinator.service.addUTXO(utxo)
sleep(1)
// 5. advance chain to the `utxoHeight`
try coordinator.service.applyStaged(nextLatestHeight: utxoHeight)
sleep(1)
let tFundsDetectionExpectation = XCTestExpectation(description: "t funds detection expectation")
shouldContinue = false
// 6. Sync and find the UXTO on chain.
do {
try await coordinator.sync(
completion: { _ in
shouldContinue = true
tFundsDetectionExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [tFundsDetectionExpectation], timeout: 2)
// at this point the balance should be zero for shielded, then zero verified transparent funds
// and 10000 zatoshi of total (not verified) transparent funds.
let tFundsDetectedBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
XCTAssertEqual(tFundsDetectedBalance.total, Zatoshi(10000))
XCTAssertEqual(tFundsDetectedBalance.verified, .zero)
let tFundsConfirmationSyncExpectation = XCTestExpectation(description: "t funds confirmation")
shouldContinue = false
// 7. stage ten blocks and confirm the transparent funds at `utxoHeight + 10`
try coordinator.applyStaged(blockheight: utxoHeight + 10)
sleep(2)
// 8. sync up to chain tip.
do {
try await coordinator.sync(
completion: { _ in
shouldContinue = true
tFundsConfirmationSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
}
await fulfillment(of: [tFundsConfirmationSyncExpectation], timeout: 5)
// the transparent funds should be 10000 zatoshis both total and verified
let confirmedTFundsBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
XCTAssertEqual(confirmedTFundsBalance.total, Zatoshi(10000))
XCTAssertEqual(confirmedTFundsBalance.verified, Zatoshi(10000))
// 9. shield the funds
let shieldFundsExpectation = XCTestExpectation(description: "shield funds")
shouldContinue = false
var shieldingPendingTx: ZcashTransaction.Overview?
// shield the funds
do {
let pendingTx = try await coordinator.synchronizer.shieldFunds(
spendingKey: coordinator.spendingKey,
memo: try Memo(string: "shield funds"),
shieldingThreshold: Zatoshi(10000)
)
shouldContinue = true
XCTAssertEqual(pendingTx.value, Zatoshi(10000) - pendingTx.fee!)
shieldingPendingTx = pendingTx
shieldFundsExpectation.fulfill()
} catch {
shieldFundsExpectation.fulfill()
XCTFail("Failed With error: \(error)")
}
await fulfillment(of: [shieldFundsExpectation], timeout: 30)
guard shouldContinue else { return }
let postShieldingBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
// when funds are shielded the UTXOs should be marked as spend and not shown on the balance.
// now balance should be zero shielded, zero transaparent.
// verify that the balance has been marked as spent regardless of confirmation
// FIXME: [#720] this should be zero, https://github.com/zcash/ZcashLightClientKit/issues/720
XCTAssertEqual(postShieldingBalance.verified, Zatoshi(10000))
// FIXME: [#720] this should be zero, https://github.com/zcash/ZcashLightClientKit/issues/720
XCTAssertEqual(postShieldingBalance.total, Zatoshi(10000))
var expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedBalance, .zero)
// 10. clear the UTXO from darksidewalletd's cache
try coordinator.service.clearAddedUTXOs()
guard let rawTxData = shieldingPendingTx?.raw else {
XCTFail("Pending transaction has no raw data")
return
}
let rawTx = RawTransaction.with({ raw in
raw.data = rawTxData
})
// 11. stage the pending shielding transaction in darksidewalletd ad `utxoHeight + 1`
try coordinator.service.stageTransaction(rawTx, at: utxoHeight + 10 + 1)
sleep(1)
// 12. advance the chain tip to sync the now mined shielding transaction
try coordinator.service.applyStaged(nextLatestHeight: utxoHeight + 10 + 1)
sleep(1)
// 13. sync up to chain tip
let postShieldSyncExpectation = XCTestExpectation(description: "sync Post shield")
shouldContinue = false
do {
try await coordinator.sync(
completion: { _ in
shouldContinue = true
postShieldSyncExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
postShieldSyncExpectation.fulfill()
}
await fulfillment(of: [postShieldSyncExpectation], timeout: 3)
guard shouldContinue else { return }
// Now it should verify that the balance has been shielded. The resulting balance should be zero
// transparent funds and `10000 - fee` total shielded funds, zero verified shielded funds.
// Fees at the time of writing the tests are 1000 zatoshi as defined on ZIP-313
let postShieldingShieldedBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
XCTAssertEqual(postShieldingShieldedBalance.total, .zero)
XCTAssertEqual(postShieldingShieldedBalance.verified, .zero)
expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedBalance, Zatoshi(9000))
// 14. proceed confirm the shielded funds by staging ten more blocks
try coordinator.service.applyStaged(nextLatestHeight: utxoHeight + 10 + 1 + 10)
sleep(2)
let confirmationExpectation = XCTestExpectation(description: "confirmation expectation")
shouldContinue = false
// 15. sync up to the new chain tip
do {
try await coordinator.sync(
completion: { _ in
shouldContinue = true
confirmationExpectation.fulfill()
},
error: self.handleError
)
} catch {
await handleError(error)
confirmationExpectation.fulfill()
}
await fulfillment(of: [confirmationExpectation], timeout: 5)
guard shouldContinue else { return }
// verify that there's a confirmed transaction that's the shielding transaction
let clearedTransaction = await coordinator.synchronizer.transactions.first(
where: { $0.rawID == shieldingPendingTx?.rawID }
)
XCTAssertNotNil(clearedTransaction)
expectedBalance = try await coordinator.synchronizer.getShieldedBalance()
XCTAssertEqual(expectedBalance, Zatoshi(9000))
let postShieldingConfirmationShieldedBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
XCTAssertEqual(postShieldingConfirmationShieldedBalance.total, .zero)
XCTAssertEqual(postShieldingConfirmationShieldedBalance.verified, .zero)
}
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)")
}
}