ZcashLightClientKit/Tests/DarksideTests/ShieldFundsTests.swift

339 lines
15 KiB
Swift
Raw Normal View History

//
// 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()
2022-10-31 05:57:10 -07:00
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
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
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)")
}
}