WIP - shield funds

This commit is contained in:
Francisco Gindre 2020-12-23 20:01:09 -03:00
parent 6ec126d471
commit 45fa30838f
11 changed files with 256 additions and 8 deletions

View File

@ -121,6 +121,18 @@ class UnspentTransactionOutputSQLDAO: UnspentTransactionOutputRepository {
return allTxs 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 { class UTXORepositoryBuilder {

View File

@ -11,6 +11,8 @@ protocol UnspentTransactionOutputRepository {
func getAll(address: String?) throws -> [UnspentTransactionOutputEntity] func getAll(address: String?) throws -> [UnspentTransactionOutputEntity]
func balance(address: String) throws -> Int
func store(utxos: [UnspentTransactionOutputEntity]) throws func store(utxos: [UnspentTransactionOutputEntity]) throws
func clearAll(address: String?) throws func clearAll(address: String?) throws

View File

@ -225,7 +225,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
UInt(outputParamsPath.lengthOfBytes(using: .utf8))) 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 { 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 dbData = dbData.osStr()
let dbCache = dbCache.osStr() let dbCache = dbCache.osStr()
@ -356,6 +355,22 @@ class ZcashRustBackend: ZcashRustBackendWelding {
return sk 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 { static func consensusBranchIdFor(height: Int32) throws -> Int32 {
let branchId = zcashlc_branch_id_for_height(height) let branchId = zcashlc_branch_id_for_height(height)
@ -365,6 +380,7 @@ class ZcashRustBackend: ZcashRustBackendWelding {
return branchId return branchId
} }
} }
private extension ZcashRustBackend { private extension ZcashRustBackend {

View File

@ -255,9 +255,21 @@ public protocol ZcashRustBackendWelding {
*/ */
static func deriveTransparentAddressFromSeed(seed: [UInt8]) throws -> String? 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? 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 Gets the consensus branch id for the given height
- Parameter height: the height you what to know the branch id for - Parameter height: the height you what to know the branch id for

View File

@ -24,6 +24,12 @@ public enum SynchronizerError: Error {
case parameterMissing(underlyingError: 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 Primary interface for interacting with the SDK. Defines the contract that specific
implementations like SdkSynchronizer fulfill. 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<PendingTransactionEntity, Error>) -> Void) func sendToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> Void)
/** /**
Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only 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. an option if the transaction has not yet been submitted to the server.

View File

@ -68,26 +68,27 @@ public protocol KeyDeriving {
*/ */
func deriveShieldedAddress(viewingKey: String) throws -> String func deriveShieldedAddress(viewingKey: String) throws -> String
/** /**
Validates the given viewing key Validates the given viewing key
- Throws DerivationError when it's invalid - Throws DerivationError when it's invalid
*/ */
func validateViewingKey(viewingKey: String) throws func validateViewingKey(viewingKey: String) throws
// WIP probably shouldn't be used just yet. Why? // 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. // - 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 // - the underlying implementation needs to be split out into a few lower-level calls
func deriveTransparentAddress(seed: [UInt8]) throws -> String func deriveTransparentAddress(seed: [UInt8]) throws -> String
/** /**
Derives a SecretKey to spend transparent funds from the given seed Derives a SecretKey to spend transparent funds from the given seed
*/ */
func deriveTransparentPrivateKey(seed: [UInt8]) throws -> String 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 { public enum KeyDerivationErrors: Error {
@ -227,7 +228,7 @@ public class DerivationTool: KeyDeriving {
public func validateViewingKey(viewingKey: String) throws { public func validateViewingKey(viewingKey: String) throws {
// TODO // 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)
}
}
} }

View File

@ -15,6 +15,7 @@ enum TransactionManagerError: Error {
case cancelled(tx: PendingTransactionEntity) case cancelled(tx: PendingTransactionEntity)
case internalInconsistency(tx: PendingTransactionEntity) case internalInconsistency(tx: PendingTransactionEntity)
case submitFailed(tx: PendingTransactionEntity, errorCode: Int) case submitFailed(tx: PendingTransactionEntity, errorCode: Int)
case shieldingEncodingFailed(tx: PendingTransactionEntity, reason: String)
} }
class PersistentTransactionManager: OutboundTransactionManager { class PersistentTransactionManager: OutboundTransactionManager {
@ -39,6 +40,45 @@ class PersistentTransactionManager: OutboundTransactionManager {
return insertedTx return insertedTx
} }
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> Void) { func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
queue.async { [weak self] in queue.async { [weak self] in
@ -60,6 +100,13 @@ class PersistentTransactionManager: OutboundTransactionManager {
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction))) result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
} }
} catch { } catch {
do {
try self.updateOnFailure(tx: pendingTransaction, error: error)
} catch {
DispatchQueue.main.async {
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
}
}
DispatchQueue.main.async { DispatchQueue.main.async {
result(.failure(error)) result(.failure(error))
} }

View File

@ -52,6 +52,39 @@ protocol TransactionEncoder {
*/ */
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) 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 Fetch the Transaction Entity from the encoded representation
- Parameter encodedTransaction: The encoded transaction to expand - Parameter encodedTransaction: The encoded transaction to expand

View File

@ -15,6 +15,8 @@ import Foundation
protocol OutboundTransactionManager { protocol OutboundTransactionManager {
func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)

View File

@ -15,15 +15,18 @@ class WalletTransactionEncoder: TransactionEncoder {
private var outputParamsURL: URL private var outputParamsURL: URL
private var spendParamsURL: URL private var spendParamsURL: URL
private var dataDbURL: URL private var dataDbURL: URL
private var cacheDbURL: URL
init(rust: ZcashRustBackendWelding.Type, init(rust: ZcashRustBackendWelding.Type,
dataDb: URL, dataDb: URL,
cacheDb: URL,
repository: TransactionRepository, repository: TransactionRepository,
outputParams: URL, outputParams: URL,
spendParams: URL) { spendParams: URL) {
self.rustBackend = rust self.rustBackend = rust
self.dataDbURL = dataDb self.dataDbURL = dataDb
self.cacheDbURL = cacheDb
self.repository = repository self.repository = repository
self.outputParamsURL = outputParams self.outputParamsURL = outputParams
self.spendParamsURL = spendParams self.spendParamsURL = spendParams
@ -34,6 +37,7 @@ class WalletTransactionEncoder: TransactionEncoder {
convenience init(initializer: Initializer) { convenience init(initializer: Initializer) {
self.init(rust: initializer.rustBackend, self.init(rust: initializer.rustBackend,
dataDb: initializer.dataDbURL, dataDb: initializer.dataDbURL,
cacheDb: initializer.cacheDbURL,
repository: initializer.transactionRepository, repository: initializer.transactionRepository,
outputParams: initializer.outputParamsURL, outputParams: initializer.outputParamsURL,
spendParams: initializer.spendParamsURL) spendParams: initializer.spendParamsURL)
@ -99,6 +103,51 @@ class WalletTransactionEncoder: TransactionEncoder {
return Int(txId) 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 { func ensureParams(spend: URL, output: URL) -> Bool {
let readableSpend = FileManager.default.isReadableFile(atPath: spend.path) let readableSpend = FileManager.default.isReadableFile(atPath: spend.path)

View File

@ -70,7 +70,7 @@ public extension Notification.Name {
Synchronizer implementation for UIKit and iOS 12+ Synchronizer implementation for UIKit and iOS 12+
*/ */
public class SDKSynchronizer: Synchronizer { public class SDKSynchronizer: Synchronizer {
public struct NotificationKeys { public struct NotificationKeys {
public static let progress = "SDKSynchronizer.progress" public static let progress = "SDKSynchronizer.progress"
public static let blockHeight = "SDKSynchronizer.blockHeight" public static let blockHeight = "SDKSynchronizer.blockHeight"
@ -79,6 +79,8 @@ public class SDKSynchronizer: Synchronizer {
public static let error = "SDKSynchronizer.error" public static let error = "SDKSynchronizer.error"
} }
private static let shieldingThreshold: Int = 10000
public private(set) var status: Status { public private(set) var status: Status {
didSet { didSet {
notify(status: status) 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<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> Void) { func createToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
do { do {