From 45fa30838f4702594f8fc65e56e767c3be7feea4 Mon Sep 17 00:00:00 2001 From: Francisco Gindre Date: Wed, 23 Dec 2020 20:01:09 -0300 Subject: [PATCH] WIP - shield funds --- .../DAO/UnspentTransactionOutputDao.swift | 12 +++++ .../UnspentTransactionOutputRepository.swift | 2 + .../Rust/ZcashRustBackend.swift | 18 ++++++- .../Rust/ZcashRustBackendWelding.swift | 14 +++++- ZcashLightClientKit/Synchronizer.swift | 16 ++++++ ZcashLightClientKit/Tool/DerivationTool.swift | 21 ++++++-- .../PersistentTransactionManager.swift | 47 +++++++++++++++++ .../Transaction/TransactionEncoder.swift | 33 ++++++++++++ .../Transaction/TransactionManager.swift | 2 + .../WalletTransactionEncoder.swift | 49 ++++++++++++++++++ .../UIKit/Synchronizer/SDKSynchronizer.swift | 50 ++++++++++++++++++- 11 files changed, 256 insertions(+), 8 deletions(-) diff --git a/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift b/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift index fe66a6a2..125e53c6 100644 --- a/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift +++ b/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift @@ -121,6 +121,18 @@ class UnspentTransactionOutputSQLDAO: UnspentTransactionOutputRepository { return allTxs } } + + func balance(address: String) throws -> Int { + + guard let sum = try dbProvider.connection().scalar( + table.select(TableColumns.valueZat.sum) + .filter(TableColumns.address == address) + ) else { + throw StorageError.operationFailed + } + return sum + + } } class UTXORepositoryBuilder { diff --git a/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift b/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift index e3104465..d8f74733 100644 --- a/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift +++ b/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift @@ -11,6 +11,8 @@ protocol UnspentTransactionOutputRepository { func getAll(address: String?) throws -> [UnspentTransactionOutputEntity] + func balance(address: String) throws -> Int + func store(utxos: [UnspentTransactionOutputEntity]) throws func clearAll(address: String?) throws diff --git a/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 45e61c1b..93d76ff5 100644 --- a/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -225,7 +225,6 @@ class ZcashRustBackend: ZcashRustBackendWelding { UInt(outputParamsPath.lengthOfBytes(using: .utf8))) } - static func shieldFunds(dbCache: URL, dbData: URL, account: Int32, tsk: String, extsk: String, memo: String?, spendParamsPath: String, outputParamsPath: String) -> Int64 { let dbData = dbData.osStr() let dbCache = dbCache.osStr() @@ -356,6 +355,22 @@ class ZcashRustBackend: ZcashRustBackendWelding { return sk } + static func deriveTransparentAddressFromSecretKey(_ tsk: String) throws -> String? { + + guard !tsk.containsCStringNullBytesBeforeStringEnding() else { + throw RustWeldingError.malformedStringInput + } + guard let tAddrCStr = zcashlc_derive_transparent_address_from_secret_key([CChar](tsk.utf8CString)) else { + if let error = lastError() { + throw error + } + return nil + } + let tAddr = String(validatingUTF8: tAddrCStr) + + return tAddr + } + static func consensusBranchIdFor(height: Int32) throws -> Int32 { let branchId = zcashlc_branch_id_for_height(height) @@ -365,6 +380,7 @@ class ZcashRustBackend: ZcashRustBackendWelding { return branchId } + } private extension ZcashRustBackend { diff --git a/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 886c0b62..8c810697 100644 --- a/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -255,9 +255,21 @@ public protocol ZcashRustBackendWelding { */ static func deriveTransparentAddressFromSeed(seed: [UInt8]) throws -> String? - + /** + Derives a transparent secret key from Seed + - Parameter seed: an array of bytes containing the seed + - Returns: an optional String containing the transparent secret (private) key + */ static func deriveTransparentPrivateKeyFromSeed(seed: [UInt8]) throws -> String? + /** + Derives a transparent address from a secret key + - Parameter tsk: a hex string containing the Secret Key + - Returns: an optional String containing the transparent address. + */ + + static func deriveTransparentAddressFromSecretKey(_ tsk: String) throws -> String? + /** Gets the consensus branch id for the given height - Parameter height: the height you what to know the branch id for diff --git a/ZcashLightClientKit/Synchronizer.swift b/ZcashLightClientKit/Synchronizer.swift index fe73fb90..3cbc6dd2 100644 --- a/ZcashLightClientKit/Synchronizer.swift +++ b/ZcashLightClientKit/Synchronizer.swift @@ -24,6 +24,12 @@ public enum SynchronizerError: Error { case parameterMissing(underlyingError: Error) } +public enum ShieldFundsError: Error { + case noUTXOFound + case insuficientTransparentFunds + case shieldingFailed(underlyingError: Error) +} + /** Primary interface for interacting with the SDK. Defines the contract that specific implementations like SdkSynchronizer fulfill. @@ -73,6 +79,16 @@ public protocol Synchronizer { */ func sendToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (_ result: Result) -> Void) + /** + Sends zatoshi. + - Parameter spendingKey: the key that allows spends to occur. + - Parameter transparentSecretKey: the key that allows to spend transaprent funds + - Parameter zatoshi: the amount of zatoshi to send. + - Parameter memo: the optional memo to include as part of the transaction. + - Parameter accountIndex: the optional account id to use. By default, the first account is used. + */ + func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (_ result: Result) -> Void) + /** Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only an option if the transaction has not yet been submitted to the server. diff --git a/ZcashLightClientKit/Tool/DerivationTool.swift b/ZcashLightClientKit/Tool/DerivationTool.swift index 60e139f1..19547d12 100644 --- a/ZcashLightClientKit/Tool/DerivationTool.swift +++ b/ZcashLightClientKit/Tool/DerivationTool.swift @@ -68,26 +68,27 @@ public protocol KeyDeriving { */ func deriveShieldedAddress(viewingKey: String) throws -> String - - /** Validates the given viewing key - Throws DerivationError when it's invalid */ func validateViewingKey(viewingKey: String) throws - // WIP probably shouldn't be used just yet. Why? // - because we need the private key associated with this seed and this function doesn't return it. // - the underlying implementation needs to be split out into a few lower-level calls func deriveTransparentAddress(seed: [UInt8]) throws -> String - /** Derives a SecretKey to spend transparent funds from the given seed */ func deriveTransparentPrivateKey(seed: [UInt8]) throws -> String + /** + Derives a transparent address from the given transparent Secret Key + */ + func deriveTransparentAddressFromPrivateKey(_ tsk: String) throws -> String + } public enum KeyDerivationErrors: Error { @@ -227,7 +228,7 @@ public class DerivationTool: KeyDeriving { public func validateViewingKey(viewingKey: String) throws { // TODO - throw KeyDerivationErrors.unableToDerive +// throw KeyDerivationErrors.unableToDerive } /** @@ -275,4 +276,14 @@ extension DerivationTool: KeyValidation { } + public func deriveTransparentAddressFromPrivateKey(_ tsk: String) throws -> String { + do { + guard let tAddr = try rustwelding.deriveTransparentAddressFromSecretKey(tsk) else { + throw KeyDerivationErrors.unableToDerive + } + return tAddr + } catch { + throw KeyDerivationErrors.derivationError(underlyingError: error) + } + } } diff --git a/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift b/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift index 57870b1a..78942706 100644 --- a/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift +++ b/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift @@ -15,6 +15,7 @@ enum TransactionManagerError: Error { case cancelled(tx: PendingTransactionEntity) case internalInconsistency(tx: PendingTransactionEntity) case submitFailed(tx: PendingTransactionEntity, errorCode: Int) + case shieldingEncodingFailed(tx: PendingTransactionEntity, reason: String) } class PersistentTransactionManager: OutboundTransactionManager { @@ -39,6 +40,45 @@ class PersistentTransactionManager: OutboundTransactionManager { return insertedTx } + func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result) -> Void) { + queue.async { [weak self] in + guard let self = self else { return } + + let derivationTool = DerivationTool() + guard let vk = try? derivationTool.deriveViewingKey(spendingKey: spendingKey), + let zAddr = try? derivationTool.deriveShieldedAddress(viewingKey: vk) else { + result(.failure(TransactionManagerError.shieldingEncodingFailed(tx: pendingTransaction, reason: "There was an error Deriving your keys"))) + return + } + + guard pendingTransaction.toAddress == zAddr else { + result(.failure(TransactionManagerError.shieldingEncodingFailed(tx: pendingTransaction, reason: "the recipient address does not match your derived shielded address. Shielding transactions addresses must match the ones derived from your keys. This is a serious error. We are not letting you encode this shielding transaction because it can lead to loss of funds"))) + return + } + do { + let encodedTransaction = try self.encoder.createShieldingTransaction(spendingKey: spendingKey, tSecretKey: tsk, memo: pendingTransaction.memo?.asZcashTransactionMemo(), from: pendingTransaction.accountIndex) + let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction) + + var pending = pendingTransaction + pending.encodeAttempts = pending.encodeAttempts + 1 + pending.raw = encodedTransaction.raw + pending.rawTransactionId = encodedTransaction.transactionId + pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty() + pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty() + try self.repository.update(pending) + result(.success(pending)) + } catch StorageError.updateFailed { + DispatchQueue.main.async { + result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction))) + } + } catch { + DispatchQueue.main.async { + result(.failure(error)) + } + } + } + } + func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result) -> Void) { queue.async { [weak self] in @@ -60,6 +100,13 @@ class PersistentTransactionManager: OutboundTransactionManager { result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction))) } } catch { + do { + try self.updateOnFailure(tx: pendingTransaction, error: error) + } catch { + DispatchQueue.main.async { + result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction))) + } + } DispatchQueue.main.async { result(.failure(error)) } diff --git a/ZcashLightClientKit/Transaction/TransactionEncoder.swift b/ZcashLightClientKit/Transaction/TransactionEncoder.swift index 276e0ea5..6038e255 100644 --- a/ZcashLightClientKit/Transaction/TransactionEncoder.swift +++ b/ZcashLightClientKit/Transaction/TransactionEncoder.swift @@ -52,6 +52,39 @@ protocol TransactionEncoder { */ func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) + /** + Creates a transaction that will attempt to shield transparent funds that are present on the cacheDB .throwing an exception whenever things are missing. When the provided wallet implementation + doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using + double-bangs for things). + Blocking + + - Parameters: + - Parameter spendingKey: a string containing the spending key + - Parameter tSecretKey: transparent secret key to spend the UTXOs + - Parameter memo: string containing the memo (optional) + - Parameter accountIndex: index of the account that will be used to send the funds + + - Throws: a TransactionEncoderError + */ + func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction + + /** + Creates a transaction that will attempt to shield transparent funds that are present on the cacheDB .throwing an exception whenever things are missing. When the provided wallet implementation + doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using + double-bangs for things). + Non-Blocking + + - Parameters: + - Parameter spendingKey: a string containing the spending key + - Parameter tSecretKey: transparent secret key to spend the UTXOs + - Parameter memo: string containing the memo (optional) + - Parameter accountIndex: index of the account that will be used to send the funds + + - Returns: a TransactionEncoderResultBlock + */ + + func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) + /** Fetch the Transaction Entity from the encoded representation - Parameter encodedTransaction: The encoded transaction to expand diff --git a/ZcashLightClientKit/Transaction/TransactionManager.swift b/ZcashLightClientKit/Transaction/TransactionManager.swift index e0c6a409..2e7368c3 100644 --- a/ZcashLightClientKit/Transaction/TransactionManager.swift +++ b/ZcashLightClientKit/Transaction/TransactionManager.swift @@ -15,6 +15,8 @@ import Foundation protocol OutboundTransactionManager { func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity + + func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result) -> Void) func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result) -> Void) diff --git a/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index 06082814..9ba7282a 100644 --- a/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -15,15 +15,18 @@ class WalletTransactionEncoder: TransactionEncoder { private var outputParamsURL: URL private var spendParamsURL: URL private var dataDbURL: URL + private var cacheDbURL: URL init(rust: ZcashRustBackendWelding.Type, dataDb: URL, + cacheDb: URL, repository: TransactionRepository, outputParams: URL, spendParams: URL) { self.rustBackend = rust self.dataDbURL = dataDb + self.cacheDbURL = cacheDb self.repository = repository self.outputParamsURL = outputParams self.spendParamsURL = spendParams @@ -34,6 +37,7 @@ class WalletTransactionEncoder: TransactionEncoder { convenience init(initializer: Initializer) { self.init(rust: initializer.rustBackend, dataDb: initializer.dataDbURL, + cacheDb: initializer.cacheDbURL, repository: initializer.transactionRepository, outputParams: initializer.outputParamsURL, spendParams: initializer.spendParamsURL) @@ -99,6 +103,51 @@ class WalletTransactionEncoder: TransactionEncoder { return Int(txId) } + func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction { + let txId = try createShieldingSpend(spendingKey: spendingKey, tsk: tSecretKey, memo: memo, accountIndex: accountIndex) + + do { + let transaction = try repository.findBy(id: txId) + + guard let tx = transaction else { + throw TransactionEncoderError.notFound(transactionId: txId) + } + + LoggerProxy.debug("sentTransaction id: \(txId)") + return EncodedTransaction(transactionId: tx.transactionId , raw: tx.raw) + } catch { + throw TransactionEncoderError.notFound(transactionId: txId) + } + } + + func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) { + queue.async { + result(.failure(RustWeldingError.genericError(message: "not implemented"))) + } + } + + func createShieldingSpend(spendingKey: String, tsk: String, memo: String?, accountIndex: Int) throws -> Int { + guard ensureParams(spend: self.spendParamsURL, output: self.spendParamsURL) else { + throw TransactionEncoderError.missingParams + } + + let txId = rustBackend.shieldFunds(dbCache: self.cacheDbURL, + dbData: self.dataDbURL, + account: Int32(accountIndex), + tsk: tsk, + extsk: spendingKey, + memo: memo, + spendParamsPath: self.spendParamsURL.path, + outputParamsPath: self.outputParamsURL.path) + + guard txId > 0 else { + throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed") + } + + return Int(txId) + + } + func ensureParams(spend: URL, output: URL) -> Bool { let readableSpend = FileManager.default.isReadableFile(atPath: spend.path) diff --git a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift index d581e704..444eccf7 100644 --- a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift +++ b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift @@ -70,7 +70,7 @@ public extension Notification.Name { Synchronizer implementation for UIKit and iOS 12+ */ public class SDKSynchronizer: Synchronizer { - + public struct NotificationKeys { public static let progress = "SDKSynchronizer.progress" public static let blockHeight = "SDKSynchronizer.blockHeight" @@ -79,6 +79,8 @@ public class SDKSynchronizer: Synchronizer { public static let error = "SDKSynchronizer.error" } + private static let shieldingThreshold: Int = 10000 + public private(set) var status: Status { didSet { notify(status: status) @@ -346,6 +348,52 @@ public class SDKSynchronizer: Synchronizer { } } + public func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result) -> Void) { + + // let's see if there are funds to shield + let derivationTool = DerivationTool.default + + do { + let tAddr = try derivationTool.deriveTransparentAddressFromPrivateKey(transparentSecretKey) + let tBalance = try utxoRepository.balance(address: tAddr) + + guard tBalance > Self.shieldingThreshold else { + resultBlock(.failure(ShieldFundsError.insuficientTransparentFunds)) + return + } + let vk = try derivationTool.deriveViewingKey(spendingKey: spendingKey) + let zAddr = try derivationTool.deriveShieldedAddress(viewingKey: vk) + + let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance, toAddress: zAddr, memo: memo, from: 0) + + transactionManager.encodeShieldingTransaction(spendingKey: spendingKey, tsk: transparentSecretKey, pendingTransaction: shieldingSpend) {[weak self] (result) in + guard let self = self else { return } + switch result { + + case .success(let tx): + self.transactionManager.submit(pendingTransaction: tx) { (submitResult) in + switch submitResult { + case .success(let submittedTx): + resultBlock(.success(submittedTx)) + case .failure(let submissionError): + DispatchQueue.main.async { + resultBlock(.failure(submissionError)) + } + } + } + + case .failure(let error): + resultBlock(.failure(error)) + } + } + + } catch { + resultBlock(.failure(error)) + return + } + + } + func createToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result) -> Void) { do {