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
}
}
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 {

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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<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
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
/**
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)
}
}
}

View File

@ -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<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) {
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))
}

View File

@ -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

View File

@ -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<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 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)

View File

@ -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<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) {
do {