ZcashLightClientKit/Tests/DarksideTests/ShieldFundsTests.swift

339 lines
15 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: disabled until https://github.com/zcash/ZcashLightClientKit/issues/587 fixed
class ShieldFundsTests: 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 = 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 setUpWithError() throws {
try super.setUpWithError()
Task { @MainActor [self] in
self.coordinator = try await TestCoordinator(
seed: seedPhrase,
walletBirthday: birthday,
channelProvider: ChannelProvider(),
network: network
)
}
try coordinator.reset(saplingActivation: birthday, branchID: self.branchID, chainName: self.chainName)
try coordinator.service.clearAddedUTXOs()
}
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)
}
/// 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://github.com/zcash-hackworks/darksidewalletd-test-data/blob/master/shield-funds/1631000.txt")
try coordinator.stageBlockCreate(height: birthday + 1, count: 200, nonce: 0)
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
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
initialVerifiedBalance = synchronizer.initializer.getVerifiedBalance()
initialTotalBalance = synchronizer.initializer.getBalance()
preTxExpectation.fulfill()
shouldContinue = true
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [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)
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.
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
shouldContinue = true
tFundsDetectionExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [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, Zatoshi(10000)) //FIXME: this should be 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 + 20) // FIXME: funds are confirmed at 20 blocks
sleep(2)
// 8. sync up to chain tip.
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
shouldContinue = true
tFundsConfirmationSyncExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [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: PendingTransactionEntity?
// shield the funds
do {
let pendingTx = try await coordinator.synchronizer.shieldFunds(
spendingKey: coordinator.spendingKey,
memo: try Memo(string: "shield funds")
)
shouldContinue = true
XCTAssertEqual(pendingTx.value, Zatoshi(10000))
shieldingPendingTx = pendingTx
shieldFundsExpectation.fulfill()
} catch {
XCTFail("Failed With error: \(error.localizedDescription)")
}
wait(for: [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
XCTAssertEqual(postShieldingBalance.verified, Zatoshi(10000)) //FIXME: this should be zero
XCTAssertEqual(postShieldingBalance.total, Zatoshi(10000)) //FIXME: this should be zero
XCTAssertEqual(coordinator.synchronizer.getShieldedBalance(), .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
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
shouldContinue = true
postShieldSyncExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [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, Zatoshi(10000)) //FIXME: this should be zero
XCTAssertEqual(postShieldingShieldedBalance.verified, Zatoshi(10000)) //FIXME: this should be zero
XCTAssertEqual(coordinator.synchronizer.getShieldedBalance(), .zero) //FIXME: this should be 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
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
shouldContinue = true
confirmationExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
} catch {
continuation.resume(throwing: error)
}
}
wait(for: [confirmationExpectation], timeout: 5)
guard shouldContinue else { return }
// verify that there's a confirmed transaction that's the shielding transaction
let clearedTransaction = coordinator.synchronizer.clearedTransactions.first(where: { $0.rawTransactionId == shieldingPendingTx?.rawTransactionId })
XCTAssertNotNil(clearedTransaction)
XCTAssertEqual(coordinator.synchronizer.getShieldedBalance(), Zatoshi(9000))
let postShieldingConfirmationShieldedBalance = try await coordinator.synchronizer.getTransparentBalance(accountIndex: 0)
XCTAssertEqual(postShieldingConfirmationShieldedBalance.total, .zero)
XCTAssertEqual(postShieldingConfirmationShieldedBalance.verified, .zero)
}
func handleError(_ error: Error?) {
_ = try? coordinator.stop()
guard let testError = error else {
XCTFail("failed with nil error")
return
}
XCTFail("Failed with error: \(testError)")
}
}