Reduce configurability per Security Finding 1.

These changes largely reduce the amount of configuration that can be tweaked in order to prevent fragmentation of the anonymity set of users. If wallet makers change certain properties than it can become easy to detect which network requests are coming from which client. The goal is for clients to be as anonymous as possible.
This commit is contained in:
Kevin Gorham 2019-09-26 12:58:37 -04:00
parent 1391fe897a
commit 42f29f534c
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
27 changed files with 761 additions and 376 deletions

View File

@ -50,7 +50,7 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
apply plugin: 'org.owasp.dependencycheck'
group = 'cash.z.android.wallet'
version = '1.0.0-alpha01'
version = '1.0.0-alpha02'
repositories {
google()
@ -65,7 +65,7 @@ android {
defaultConfig {
minSdkVersion buildConfig.minSdkVersion
targetSdkVersion buildConfig.targetSdkVersion
versionCode = 1_00_00_001 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionCode = 1_00_00_002 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionName = "$version"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'

View File

@ -13,7 +13,7 @@ import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.service.LightWalletGrpcService
object Injection {
private const val host: String = "lightwalletd.z.cash"
private const val host: String = "34.68.177.238"
private const val port: Int = 9067
private const val cacheDbName = "memos-cache.db"
private const val dataDbName = "memos-data.db"

View File

@ -5,7 +5,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.test.core.app.ApplicationProvider
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.entity.CompactBlockEntity
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.jni.RustBackendWelding
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
@ -47,19 +47,19 @@ class GlueIntegrationTest {
)
while (result.hasNext()) {
val compactBlock = result.next()
dao.insert(CompactBlock(compactBlock.height.toInt(), compactBlock.toByteArray()))
dao.insert(CompactBlockEntity(compactBlock.height.toInt(), compactBlock.toByteArray()))
System.err.println("stored block at height: ${compactBlock.height} with time ${compactBlock.time}")
}
}
private fun scanData() {
val dbFileName = "/data/user/0/cash.z.wallet.sdk.test/databases/new-data-glue.db"
rustBackend.initDataDb(dbFileName)
rustBackend.initAccountsTable(dbFileName, "dummyseed".toByteArray(), 1)
rustBackend.initDataDb()
rustBackend.initAccountsTable("dummyseed".toByteArray(), 1)
Log.e("tezt", "scanning blocks...")
val result = rustBackend.scanBlocks(cacheDbPath, dbFileName)
val result = rustBackend.scanBlocks()
System.err.println("done.")
}
@ -69,7 +69,7 @@ class GlueIntegrationTest {
companion object {
// jni
val rustBackend: RustBackendWelding = RustBackend()
val rustBackend: RustBackendWelding = RustBackend
// db
private lateinit var dao: CompactBlockDao
@ -83,7 +83,7 @@ class GlueIntegrationTest {
@BeforeClass
@JvmStatic
fun setup() {
rustBackend.initLogs()
rustBackend.create(ApplicationProvider.getApplicationContext())
val channel = ManagedChannelBuilder.forAddress("10.0.2.2", 9067).usePlaintext().build()
blockingStub = CompactTxStreamerGrpc.newBlockingStub(channel)

View File

@ -5,7 +5,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.test.core.app.ApplicationProvider
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.entity.CompactBlockEntity
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.jni.RustBackendWelding
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
@ -49,14 +49,14 @@ class GlueSetupIntegrationTest {
)
while (result.hasNext()) {
val compactBlock = result.next()
dao.insert(CompactBlock(compactBlock.height.toInt(), compactBlock.toByteArray()))
dao.insert(CompactBlockEntity(compactBlock.height.toInt(), compactBlock.toByteArray()))
System.err.println("stored block at height: ${compactBlock.height}")
}
}
private fun scanData() {
Log.e("tezt", "scanning blocks...")
val result = rustBackend.scanBlocks(cacheDbPath, "/data/user/0/cash.z.wallet.sdk.test/databases/data-glue.db")
val result = rustBackend.scanBlocks()
System.err.println("done.")
}
@ -66,13 +66,13 @@ class GlueSetupIntegrationTest {
companion object {
// jni
val rustBackend: RustBackendWelding = RustBackend()
val rustBackend: RustBackendWelding = RustBackend
// db
private lateinit var dao: CompactBlockDao
private lateinit var db: CompactBlockDb
private const val cacheDbName = "dummy-cache-glue.db"
private const val cacheDbPath = "/data/user/0/cash.z.wallet.sdk.test/databases/$cacheDbName"
private const val cacheDbName = "cache-glue.db"
private const val dataDbName = "data-glue.db"
// grpc
lateinit var blockingStub: CompactTxStreamerGrpc.CompactTxStreamerBlockingStub
@ -80,7 +80,7 @@ class GlueSetupIntegrationTest {
@BeforeClass
@JvmStatic
fun setup() {
rustBackend.initLogs()
rustBackend.create(ApplicationProvider.getApplicationContext(), cacheDbName, dataDbName)
val channel = ManagedChannelBuilder.forAddress("10.0.2.2", 9067).usePlaintext().build()
blockingStub = CompactTxStreamerGrpc.newBlockingStub(channel)

View File

@ -4,8 +4,10 @@ import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.block.CompactBlockDbStore
import cash.z.wallet.sdk.block.CompactBlockDownloader
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.block.ProcessorConfig
import cash.z.wallet.sdk.data.*
import cash.z.wallet.sdk.data.PollingTransactionRepository
import cash.z.wallet.sdk.data.Synchronizer
import cash.z.wallet.sdk.data.TroubleshootingTwig
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.ext.SampleSpendingKeyProvider
import cash.z.wallet.sdk.jni.RustBackend
@ -47,27 +49,19 @@ class IntegrationTest {
@Test(timeout = 120_000L)
fun testSync() = runBlocking<Unit> {
val rustBackend = RustBackend()
rustBackend.initLogs()
val config = ProcessorConfig(
cacheDbPath = context.getDatabasePath(cacheDdName).absolutePath,
dataDbPath = context.getDatabasePath(dataDbName).absolutePath,
downloadBatchSize = 2000,
blockPollFrequencyMillis = 10_000L
)
val rustBackend = RustBackend.create(context)
val lightwalletService = LightWalletGrpcService(context,"192.168.1.134")
val compactBlockStore = CompactBlockDbStore(context, config.cacheDbPath)
val compactBlockStore = CompactBlockDbStore(context)
downloader = CompactBlockDownloader(lightwalletService, compactBlockStore)
processor = CompactBlockProcessor(config, downloader, repository, rustBackend)
processor = CompactBlockProcessor(downloader, repository, rustBackend)
repository = PollingTransactionRepository(context, dataDbName, 10_000L)
wallet = Wallet(
context = context,
rustBackend = rustBackend,
dataDbName = dataDbName,
seedProvider = SampleSeedProvider("dummyseed"),
spendingKeyProvider = SampleSpendingKeyProvider("dummyseed")
context,
rustBackend,
SampleSeedProvider("dummyseed"),
SampleSpendingKeyProvider("dummyseed")
)
// repository.start(this)

View File

@ -1,5 +1,6 @@
package cash.z.wallet.sdk.jni
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.BeforeClass
@ -7,30 +8,25 @@ import org.junit.Test
class RustBackendTest {
private val dbDataFile = "/data/user/0/cash.z.wallet.sdk.test/databases/data2.db"
@Test
fun testGetAddress_exists() {
assertNotNull(rustBackend.getAddress(dbDataFile, 0))
assertNotNull(rustBackend.getAddress(0))
}
@Test
fun testGetAddress_valid() {
val address = rustBackend.getAddress(dbDataFile, 0)
val address = rustBackend.getAddress(0)
val expectedAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t"
assertEquals("Invalid address", expectedAddress, address)
}
@Test
fun testScanBlocks() {
rustBackend.initDataDb(dbDataFile)
rustBackend.initAccountsTable(dbDataFile, "dummyseed".toByteArray(), 1)
rustBackend.initDataDb()
rustBackend.initAccountsTable("dummyseed".toByteArray(), 1)
// note: for this to work, the db file below must be uploaded to the device. Eventually, this test will be self-contained and remove that requirement.
val result = rustBackend.scanBlocks(
"/data/user/0/cash.z.wallet.sdk.test/databases/dummy-cache.db",
dbDataFile
)
val result = rustBackend.scanBlocks()
// Thread.sleep(15 * DateUtils.MINUTE_IN_MILLIS)
assertEquals("Invalid number of results", 3, 3)
}
@ -38,19 +34,16 @@ class RustBackendTest {
@Test
fun testSend() {
rustBackend.createToAddress(
"/data/user/0/cash.z.wallet.sdk.test/databases/data2.db",
0,
"dummykey",
"ztestsapling1fg82ar8y8whjfd52l0xcq0w3n7nn7cask2scp9rp27njeurr72ychvud57s9tu90fdqgwdt07lg",
210_000,
"",
"/data/user/0/cash.z.wallet.sdk.test/databases/sapling-spend.params",
"/data/user/0/cash.z.wallet.sdk.test/databases/sapling-output.params"
""
)
}
companion object {
val rustBackend: RustBackendWelding = RustBackend()
val rustBackend: RustBackendWelding = RustBackend.create(ApplicationProvider.getApplicationContext(), "rustTestCache.db", "rustTestData.db")
}
}

View File

@ -1,4 +1,4 @@
package cash.z.wallet.sdk.db
package cash.z.wallet.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.data.TroubleshootingTwig
@ -23,14 +23,13 @@ class AddressGeneratorUtil {
private val dataDbName = "AddressUtilData.db"
private val context = InstrumentationRegistry.getInstrumentation().context
private val rustBackend = RustBackend()
private val rustBackend = RustBackend.create(context)
private lateinit var wallet: Wallet
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
rustBackend.initLogs()
}
private fun deleteDb() {
@ -64,13 +63,7 @@ class AddressGeneratorUtil {
private fun initWallet(seed: String): ReadWriteProperty<Any?, String> {
deleteDb()
val spendingKeyProvider = Delegates.notNull<String>()
wallet = Wallet(
context = context,
rustBackend = rustBackend,
dataDbName = dataDbName,
seedProvider = SampleSeedProvider(seed),
spendingKeyProvider = spendingKeyProvider
)
wallet = Wallet(context, rustBackend, SampleSeedProvider(seed), spendingKeyProvider)
wallet.initialize()
return spendingKeyProvider
}

View File

@ -36,19 +36,16 @@ class BalancePrinterUtil {
private val context = InstrumentationRegistry.getInstrumentation().context
private val cacheDbName = "BalanceUtilCache.db"
private val dataDbName = "BalanceUtilData.db"
private val cacheDbPath = context.getDatabasePath("BalanceUtilCache.db").absolutePath
private val dataDbPath = context.getDatabasePath("BalanceUtilData.db").absolutePath
private val rustBackend = RustBackend()
private val rustBackend = RustBackend.create(context, cacheDbName, dataDbName)
private val downloader = CompactBlockDownloader(
LightWalletGrpcService(context, host),
CompactBlockDbStore(context, cacheDbName)
CompactBlockDbStore(context)
)
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
rustBackend.initLogs()
cacheBlocks()
}
@ -73,11 +70,11 @@ class BalancePrinterUtil {
deleteDb(dataDbName)
initWallet(seed)
twig("scanning blocks for seed <$seed>")
rustBackend.scanBlocks(cacheDbPath, dataDbPath)
rustBackend.scanBlocks()
twig("done scanning blocks for seed $seed")
val total = rustBackend.getBalance(dataDbPath, 0)
val total = rustBackend.getBalance(0)
twig("found total: $total")
val available = rustBackend.getVerifiedBalance(dataDbPath, 0)
val available = rustBackend.getVerifiedBalance(0)
twig("found available: $available")
twig("xrxrx2\t$seed\t$total\t$available")
println("xrxrx2\t$seed\t$total\t$available")
@ -102,12 +99,11 @@ class BalancePrinterUtil {
private fun initWallet(seed: String): Wallet {
val spendingKeyProvider = Delegates.notNull<String>()
return Wallet(
context = context,
birthday = Wallet.loadBirthdayFromAssets(context, birthday),
rustBackend = rustBackend,
dataDbName = dataDbName,
seedProvider = SampleSeedProvider(seed),
spendingKeyProvider = spendingKeyProvider
context,
rustBackend,
SampleSeedProvider(seed),
spendingKeyProvider,
Wallet.loadBirthdayFromAssets(context, birthday)
).apply {
runCatching {
initialize()
@ -139,7 +135,7 @@ class BalancePrinterUtil {
val dummyWallet = initWallet("dummySeed")
Twig.sprout("validating")
twig("validating blocks in range $range")
val result = rustBackend.validateCombinedChain(cacheDbPath, dataDbPath)
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
return result
}

View File

@ -5,26 +5,27 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.db.CompactBlockDao
import cash.z.wallet.sdk.db.CompactBlockDb
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.ext.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.entity.CompactBlockEntity
import cash.z.wallet.sdk.ext.ZcashSdk.DB_CACHE_NAME
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.rpc.CompactFormats
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
class CompactBlockDbStore(
applicationContext: Context,
cacheDbName: String
applicationContext: Context
) : CompactBlockStore {
private val cacheDao: CompactBlockDao
private val cacheDb: CompactBlockDb
init {
cacheDb = createCompactBlockCacheDb(applicationContext, cacheDbName)
cacheDb = createCompactBlockCacheDb(applicationContext)
cacheDao = cacheDb.complactBlockDao()
}
private fun createCompactBlockCacheDb(applicationContext: Context, cacheDbName: String): CompactBlockDb {
return Room.databaseBuilder(applicationContext, CompactBlockDb::class.java, cacheDbName)
private fun createCompactBlockCacheDb(applicationContext: Context): CompactBlockDb {
return Room.databaseBuilder(applicationContext, CompactBlockDb::class.java, DB_CACHE_NAME)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
// this is a simple cache of blocks. destroying the db should be benign
.fallbackToDestructiveMigration()
@ -36,8 +37,8 @@ class CompactBlockDbStore(
if (lastBlock < SAPLING_ACTIVATION_HEIGHT) -1 else lastBlock
}
override suspend fun write(result: List<CompactBlock>) = withContext(IO) {
cacheDao.insert(result)
override suspend fun write(result: List<CompactFormats.CompactBlock>) = withContext(IO) {
cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) })
}
override suspend fun rewindTo(height: Int) = withContext(IO) {

View File

@ -1,6 +1,5 @@
package cash.z.wallet.sdk.block
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext

View File

@ -6,7 +6,15 @@ import cash.z.wallet.sdk.data.TransactionRepository
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.exception.CompactBlockProcessorException
import cash.z.wallet.sdk.ext.*
import cash.z.wallet.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE
import cash.z.wallet.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
import cash.z.wallet.sdk.ext.ZcashSdk.MAX_REORG_SIZE
import cash.z.wallet.sdk.ext.ZcashSdk.POLL_INTERVAL
import cash.z.wallet.sdk.ext.ZcashSdk.RETRIES
import cash.z.wallet.sdk.ext.ZcashSdk.REWIND_DISTANCE
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.ext.retryUpTo
import cash.z.wallet.sdk.ext.retryWithBackoff
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.jni.RustBackendWelding
import kotlinx.coroutines.Dispatchers.IO
@ -15,8 +23,10 @@ import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Responsible for processing the compact blocks that are received from the lightwallet server. This class encapsulates
@ -25,10 +35,9 @@ import java.util.concurrent.atomic.AtomicInteger
*/
@OpenForTesting
class CompactBlockProcessor(
internal val config: ProcessorConfig,
internal val downloader: CompactBlockDownloader,
private val repository: TransactionRepository,
private val rustBackend: RustBackendWelding = RustBackend()
private val rustBackend: RustBackendWelding
) {
var onErrorListener: ((Throwable) -> Boolean)? = null
var isConnected: Boolean = false
@ -45,22 +54,21 @@ class CompactBlockProcessor(
*/
suspend fun start() = withContext(IO) {
twig("processor starting")
validateConfig()
// using do/while makes it easier to execute exactly one loop which helps with testing this processor quickly
// (because you can start and then immediately set isStopped=true to always get precisely one loop)
do {
retryWithBackoff(::onConnectionError, maxDelayMillis = config.maxBackoffInterval) {
retryWithBackoff(::onConnectionError, maxDelayMillis = MAX_BACKOFF_INTERVAL) {
val result = processNewBlocks()
// immediately process again after failures in order to download new blocks right away
if (result < 0) {
isSyncing = false
isScanning = false
consecutiveChainErrors.set(0)
twig("Successfully processed new blocks. Sleeping for ${config.blockPollFrequencyMillis}ms")
delay(config.blockPollFrequencyMillis)
twig("Successfully processed new blocks. Sleeping for ${POLL_INTERVAL}ms")
delay(POLL_INTERVAL)
} else {
if(consecutiveChainErrors.get() >= config.retries) {
if(consecutiveChainErrors.get() >= RETRIES) {
val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
} else {
@ -74,16 +82,6 @@ class CompactBlockProcessor(
stop()
}
/**
* Validate the config to expose a common pitfall.
*/
private fun validateConfig() {
if(!config.cacheDbPath.contains(File.separator))
throw CompactBlockProcessorException.FileInsteadOfPath(config.cacheDbPath)
if(!config.dataDbPath.contains(File.separator))
throw CompactBlockProcessorException.FileInsteadOfPath(config.dataDbPath)
}
fun stop() {
isStopped = true
}
@ -107,7 +105,7 @@ class CompactBlockProcessor(
val latestBlockHeight = downloader.getLatestBlockHeight()
isConnected = true // no exception on downloader call
isSyncing = true
val lastDownloadedHeight = Math.max(getLastDownloadedHeight(), SAPLING_ACTIVATION_HEIGHT - 1)
val lastDownloadedHeight = max(getLastDownloadedHeight(), SAPLING_ACTIVATION_HEIGHT - 1)
val lastScannedHeight = getLastScannedHeight()
twig("latestBlockHeight: $latestBlockHeight\tlastDownloadedHeight: $lastDownloadedHeight" +
@ -115,14 +113,18 @@ class CompactBlockProcessor(
// as long as the database has the sapling tree (like when it's initialized from a checkpoint) we can avoid
// downloading earlier blocks so take the larger of these two numbers
val rangeToDownload = (Math.max(lastDownloadedHeight, lastScannedHeight) + 1)..latestBlockHeight
val rangeToDownload = (max(lastDownloadedHeight, lastScannedHeight) + 1)..latestBlockHeight
val rangeToScan = (lastScannedHeight + 1)..latestBlockHeight
downloadNewBlocks(rangeToDownload)
val error = validateNewBlocks(rangeToScan)
if (error < 0) {
scanNewBlocks(rangeToScan)
-1 // TODO: in theory scan should not fail when validate succeeds but maybe consider returning the failed block height whenever scan does fail
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
val success = scanNewBlocks(rangeToScan)
if (!success) throw CompactBlockProcessorException.FailedScan
-1
} else {
error
}
@ -137,20 +139,19 @@ class CompactBlockProcessor(
Twig.sprout("downloading")
twig("downloading blocks in range $range")
var downloadedBlockHeight = range.start
var downloadedBlockHeight = range.first
val missingBlockCount = range.last - range.first + 1
val batches = (missingBlockCount / config.downloadBatchSize
+ (if (missingBlockCount.rem(config.downloadBatchSize) == 0) 0 else 1))
val batches = (missingBlockCount / DOWNLOAD_BATCH_SIZE
+ (if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0) 0 else 1))
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of ${config.downloadBatchSize}...")
twig("found $missingBlockCount missing blocks, downloading in $batches batches of ${DOWNLOAD_BATCH_SIZE}...")
for (i in 1..batches) {
retryUpTo(config.retries) {
val end = Math.min(range.first + (i * config.downloadBatchSize), range.last + 1)
val batchRange = downloadedBlockHeight..(end - 1)
twig("downloaded $batchRange (batch $i of $batches)") {
downloader.downloadBlockRange(batchRange)
retryUpTo(RETRIES) {
val end = min(range.first + (i * DOWNLOAD_BATCH_SIZE), range.last + 1)
twig("downloaded $downloadedBlockHeight..${(end - 1)} (batch $i of $batches)") {
downloader.downloadBlockRange(downloadedBlockHeight until end)
}
progress = Math.round(i / batches.toFloat() * 100)
progress = (i / batches.toFloat() * 100).roundToInt()
// only report during large downloads. TODO: allow for configuration of "large"
progressChannel.send(progress)
downloadedBlockHeight = end
@ -168,7 +169,7 @@ class CompactBlockProcessor(
}
Twig.sprout("validating")
twig("validating blocks in range $range")
val result = rustBackend.validateCombinedChain(config.cacheDbPath, config.dataDbPath)
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
return result
}
@ -181,7 +182,7 @@ class CompactBlockProcessor(
Twig.sprout("scanning")
twig("scanning blocks in range $range")
isScanning = true
val result = rustBackend.scanBlocks(config.cacheDbPath, config.dataDbPath)
val result = rustBackend.scanBlocks()
isScanning = false
Twig.clip("scanning")
return result
@ -190,7 +191,7 @@ class CompactBlockProcessor(
private suspend fun handleChainError(errorHeight: Int) = withContext(IO) {
val lowerBound = determineLowerBound(errorHeight)
twig("handling chain error at $errorHeight by rewinding to block $lowerBound")
rustBackend.rewindToHeight(config.dataDbPath, lowerBound)
rustBackend.rewindToHeight(lowerBound)
downloader.rewindTo(lowerBound)
}
@ -202,7 +203,7 @@ class CompactBlockProcessor(
}
private fun determineLowerBound(errorHeight: Int): Int {
val offset = Math.min(MAX_REORG_SIZE, config.rewindDistance * (consecutiveChainErrors.get() + 1))
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return Math.max(errorHeight - offset, SAPLING_ACTIVATION_HEIGHT)
}
@ -213,18 +214,4 @@ class CompactBlockProcessor(
suspend fun getLastScannedHeight() = withContext(IO) {
repository.lastScannedHeight()
}
}
/**
* @property cacheDbPath absolute file path of the DB where raw, unprocessed compact blocks are stored.
* @property dataDbPath absolute file path of the DB where all information derived from the cache DB is stored.
*/
data class ProcessorConfig(
val cacheDbPath: String = "",
val dataDbPath: String = "",
val downloadBatchSize: Int = DEFAULT_BATCH_SIZE,
val blockPollFrequencyMillis: Long = DEFAULT_POLL_INTERVAL,
val retries: Int = DEFAULT_RETRIES,
val maxBackoffInterval: Long = DEFAULT_MAX_BACKOFF_INTERVAL,
val rewindDistance: Int = DEFAULT_REWIND_DISTANCE
)
}

View File

@ -1,6 +1,6 @@
package cash.z.wallet.sdk.block
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.rpc.CompactFormats
/**
* Interface for storing compact blocks.
@ -14,7 +14,7 @@ interface CompactBlockStore {
/**
* Write the given blocks to this store, which may be anything from an in-memory cache to a DB.
*/
suspend fun write(result: List<CompactBlock>)
suspend fun write(result: List<CompactFormats.CompactBlock>)
/**
* Remove every block above and including the given height.

View File

@ -7,7 +7,7 @@ import cash.z.wallet.sdk.db.PendingTransactionDao
import cash.z.wallet.sdk.db.PendingTransactionDb
import cash.z.wallet.sdk.entity.PendingTransaction
import cash.z.wallet.sdk.entity.Transaction
import cash.z.wallet.sdk.ext.EXPIRY_OFFSET
import cash.z.wallet.sdk.ext.ZcashSdk.EXPIRY_OFFSET
import cash.z.wallet.sdk.service.LightWalletService
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext

View File

@ -200,8 +200,8 @@ class SdkSynchronizer (
val progressUpdates = progress()
for (progress in progressUpdates) {
if (progress == 100) {
twig("triggering a balance update because progress is complete")
refreshBalance()
twig("triggering a balance update because progress is complete (j/k)")
//refreshBalance()
}
}
twig("done monitoring for progress changes")
@ -217,8 +217,8 @@ class SdkSynchronizer (
val channel = pendingChannel.openSubscription()
for (pending in channel) {
if(balanceChannel.isClosedForSend) break
twig("triggering a balance update because pending transactions have changed")
refreshBalance()
twig("triggering a balance update because pending transactions have changed (j/kk)")
// refreshBalance()
}
twig("done monitoring for pending changes and balance changes")
}

View File

@ -0,0 +1,305 @@
// This has been replaced by "StableSynchronizer" We keep it around for the docs
//package cash.z.wallet.sdk.data
//
//import cash.z.wallet.sdk.block.CompactBlockProcessor
//import cash.z.wallet.sdk.entity.ClearedTransaction
//import cash.z.wallet.sdk.exception.SynchronizerException
//import cash.z.wallet.sdk.exception.WalletException
//import cash.z.wallet.sdk.secure.Wallet
//import kotlinx.coroutines.*
//import kotlinx.coroutines.Dispatchers.IO
//import kotlinx.coroutines.channels.ConflatedBroadcastChannel
//import kotlinx.coroutines.channels.ReceiveChannel
//import kotlinx.coroutines.channels.distinct
//import kotlin.coroutines.CoroutineContext
//
///**
// * The glue. Downloads compact blocks to the database and then scans them for transactions. In order to serve that
// * purpose, this class glues together a variety of key components. Each component contributes to the team effort of
// * providing a simple source of truth to interact with.
// *
// * Another way of thinking about this class is the reference that demonstrates how all the pieces can be tied
// * together.
// *
// * @param processor the component that saves the downloaded compact blocks to the cache and then scans those blocks for
// * data related to this wallet.
// * @param repository the component that exposes streams of wallet transaction information.
// * @param activeTransactionManager the component that manages the lifecycle of active transactions. This includes sent
// * transactions that have not been mined.
// * @param wallet the component that wraps the JNI layer that interacts with librustzcash and manages wallet config.
// * @param batchSize the number of compact blocks to download at a time.
// * @param staleTolerance the number of blocks to allow before considering our data to be stale
// * @param blockPollFrequency how often to poll for compact blocks. Once all missing blocks have been downloaded, this
// * number represents the number of milliseconds the synchronizer will wait before checking for newly mined blocks.
// */
//class SdkSynchronizer(
// private val processor: CompactBlockProcessor,
// private val repository: TransactionRepository,
// private val activeTransactionManager: ActiveTransactionManager,
// private val wallet: Wallet,
// private val staleTolerance: Int = 10
//) : Synchronizer {
//
// /**
// * The primary job for this Synchronizer. It leverages structured concurrency to cancel all work when the
// * `parentScope` provided to the [start] method ends.
// */
// private lateinit var blockJob: Job
//
// /**
// * The state this Synchronizer was in when it started. This is helpful because the conditions that lead to FirstRun
// * or isStale being detected can change quickly so retaining the initial state is useful for walkthroughs or other
// * elements of an app that need to rely on this information later, rather than in realtime.
// */
// private lateinit var initialState: SyncState
//
// /**
// * Returns true when `start` has been called on this synchronizer.
// */
// private val wasPreviouslyStarted
// get() = ::blockJob.isInitialized
//
// /**
// * Retains the error that caused this synchronizer to fail for future error handling or reporting.
// */
// private var failure: Throwable? = null
//
// /**
// * The default exception handler for the block job. Calls [onException].
// */
// private val exceptionHandler: (c: CoroutineContext, t: Throwable) -> Unit = { _, t -> onException(t) }
//
// /**
// * Sets a listener to be notified of uncaught Synchronizer errors. When null, errors will only be logged.
// */
// override var onSynchronizerErrorListener: ((Throwable?) -> Boolean)? = null
// set(value) {
// field = value
// if (failure != null) value?.invoke(failure)
// }
//
// /**
// * Channel of transactions from the repository.
// */
// private val transactionChannel = ConflatedBroadcastChannel<List<ClearedTransaction>>()
//
// /**
// * Channel of balance information.
// */
// private val balanceChannel = ConflatedBroadcastChannel<Wallet.WalletBalance>()
//
// //
// // Public API
// //
//
// /* Lifecycle */
//
// /**
// * Starts this synchronizer within the given scope. For simplicity, attempting to start an instance that has already
// * been started will throw a [SynchronizerException.FalseStart] exception. This reduces the complexity of managing
// * resources that must be recycled. Instead, each synchronizer is designed to have a long lifespan and should be
// * started from an activity, application or session.
// *
// * @param parentScope the scope to use for this synchronizer, typically something with a lifecycle such as an
// * Activity for single-activity apps or a logged in user session. This scope is only used for launching this
// * synchronzer's job as a child.
// */
// override fun start(parentScope: CoroutineScope): Synchronizer {
// // prevent restarts so the behavior of this class is easier to reason about
// if (wasPreviouslyStarted) throw SynchronizerException.FalseStart
// twig("starting")
// failure = null
// blockJob = parentScope.launch(CoroutineExceptionHandler(exceptionHandler)) {
// supervisorScope {
// try {
// wallet.initialize()
// } catch (e: WalletException.AlreadyInitializedException) {
// twig("Warning: wallet already initialized but this is safe to ignore " +
// "because the SDK now automatically detects where to start downloading.")
// }
// onReady()
// }
// }
// return this
// }
//
// /**
// * Stops this synchronizer by stopping the downloader, repository, and activeTransactionManager, then cancelling the
// * parent job. Note that we do not cancel the parent scope that was passed into [start] because the synchronizer
// * does not own that scope, it just uses it for launching children.
// */
// override fun stop() {
// twig("stopping")
// (repository as? PollingTransactionRepository)?.stop().also { twig("repository stopped") }
// activeTransactionManager.stop().also { twig("activeTransactionManager stopped") }
// // TODO: investigate whether this is necessary and remove or improve, accordingly
// Thread.sleep(5000L)
// blockJob.cancel().also { twig("blockJob cancelled") }
// }
//
//
// /* Channels */
//
// /**
// * A stream of all the wallet transactions, delegated to the [activeTransactionManager].
// */
// override fun activeTransactions() = activeTransactionManager.subscribe()
//
// /**
// * A stream of all the wallet transactions, delegated to the [repository].
// */
// override fun allTransactions(): ReceiveChannel<List<ClearedTransaction>> {
// return transactionChannel.openSubscription()
// }
//
// /**
// * A stream of progress values, corresponding to this Synchronizer downloading blocks, delegated to the
// * [downloader]. Any non-zero value below 100 indicates that progress indicators can be shown and a value of 100
// * signals that progress is complete and any progress indicators can be hidden. At that point, the synchronizer
// * switches from catching up on missed blocks to periodically monitoring for newly mined blocks.
// */
// override fun progress(): ReceiveChannel<Int> {
// return processor.progress()
// }
//
// /**
// * A stream of balance values, delegated to the [wallet].
// */
// override fun balances(): ReceiveChannel<Wallet.WalletBalance> {
// return balanceChannel.openSubscription()
// }
//
//
// /* Status */
//
// /**
// * A flag to indicate that this Synchronizer is significantly out of sync with it's server. This is determined by
// * the delta between the current block height reported by the server and the latest block we have stored in cache.
// * Whenever this delta is greater than the [staleTolerance], this function returns true. This is intended for
// * showing progress indicators when the user returns to the app after having not used it for a long period.
// * Typically, this means the user may have to wait for downloading to occur and the current balance and transaction
// * information cannot be trusted as 100% accurate.
// *
// * @return true when the local data is significantly out of sync with the remote server and the app data is stale.
// */
// override suspend fun isStale(): Boolean = withContext(IO) {
// val latestBlockHeight = processor.downloader.getLatestBlockHeight()
// val ourHeight = processor.downloader.getLastDownloadedHeight()
// val tolerance = staleTolerance
// val delta = latestBlockHeight - ourHeight
// twig("checking whether out of sync. " +
// "LatestHeight: $latestBlockHeight ourHeight: $ourHeight Delta: $delta tolerance: $tolerance")
// delta > tolerance
// }
//
// /* Operations */
//
// /**
// * Gets the address for the given account.
// *
// * @param accountId the optional accountId whose address of interest. Typically, this value is zero.
// */
// override fun getAddress(accountId: Int): String = wallet.getAddress()
//
// override suspend fun getBalance(accountId: Int): Wallet.WalletBalance = wallet.getBalanceInfo(accountId)
//
// /**
// * Sends zatoshi.
// *
// * @param zatoshi the amount of zatoshi to send.
// * @param toAddress the recipient's address.
// * @param memo the optional memo to include as part of the transaction.
// * @param fromAccountId the optional account id to use. By default, the first account is used.
// */
// override suspend fun sendToAddress(zatoshi: Long, toAddress: String, memo: String, fromAccountId: Int) =
// activeTransactionManager.sendToAddress(zatoshi, toAddress, memo, fromAccountId)
//
// /**
// * Attempts to cancel a previously sent transaction. Transactions can only be cancelled during the calculation phase
// * before they've been submitted to the server. This method will return false when it is too late to cancel. This
// * logic is delegated to the activeTransactionManager, which knows the state of the given transaction.
// *
// * @param transaction the transaction to cancel.
// * @return true when the cancellation request was successful. False when it is too late to cancel.
// */
// override fun cancelSend(transaction: ActiveSendTransaction): Boolean = activeTransactionManager.cancel(transaction)
//
//
// //
// // Private API
// //
//
//
// /**
// * Logic for starting the Synchronizer once it is ready for processing. All starts eventually end with this method.
// */
// private fun CoroutineScope.onReady() = launch {
// twig("synchronization is ready to begin!")
// launch { monitorTransactions(transactionChannel.openSubscription().distinct()) }
//
// activeTransactionManager.start()
// repository.poll(transactionChannel)
// processor.start()
// }
//
// /**
// * Monitors transactions and recalculates the balance any time transactions have changed.
// */
// private suspend fun monitorTransactions(transactionChannel: ReceiveChannel<List<ClearedTransaction>>) =
// withContext(IO) {
// twig("beginning to monitor transactions in order to update the balance")
// launch {
// for (i in transactionChannel) {
// twig("triggering a balance update because transactions have changed")
// balanceChannel.send(wallet.getBalanceInfo())
// twig("done triggering balance check!")
// }
// }
// twig("done monitoring transactions in order to update the balance")
// }
//
// /**
// * Wraps exceptions, logs them and then invokes the [onSynchronizerErrorListener], if it exists.
// */
// private fun onException(throwable: Throwable) {
// twig("********")
// twig("******** ERROR: $throwable")
// if (throwable.cause != null) twig("******** caused by ${throwable.cause}")
// if (throwable.cause?.cause != null) twig("******** caused by ${throwable.cause?.cause}")
// twig("********")
//
// val hasRecovered = onSynchronizerErrorListener?.invoke(throwable)
// if (hasRecovered != true) stop().also { failure = throwable }
// }
//
// /**
// * Represents the initial state of the Synchronizer.
// */
// sealed class SyncState {
// /**
// * State for the first run of the Synchronizer, when the database has not been initialized.
// */
// object FirstRun : SyncState()
//
// /**
// * State for when compact blocks have been downloaded but not scanned. This state is typically achieved when the
// * app was previously started but killed before the first scan took place. In this case, we do not need to
// * download compact blocks that we already have.
// *
// * @param startingBlockHeight the last block that has been downloaded into the cache. We do not need to download
// * any blocks before this height because we already have them.
// */
// class CacheOnly(val startingBlockHeight: Int = Int.MAX_VALUE) : SyncState()
//
// /**
// * The final state of the Synchronizer, when all initialization is complete and the starting block is known.
// *
// * @param startingBlockHeight the height that will be fed to the downloader. In most cases, it will represent
// * either the wallet birthday or the last block that was processed in the previous session.
// */
// class ReadyToProcess(val startingBlockHeight: Int = Int.MAX_VALUE) : SyncState()
// }
//
//}

View File

@ -1,7 +1,7 @@
package cash.z.wallet.sdk.db
import androidx.room.*
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.entity.CompactBlockEntity
//
@ -9,8 +9,7 @@ import cash.z.wallet.sdk.entity.CompactBlock
//
@Database(
entities = [
CompactBlock::class],
entities = [CompactBlockEntity::class],
version = 1,
exportSchema = false
)
@ -26,10 +25,10 @@ abstract class CompactBlockDb : RoomDatabase() {
@Dao
interface CompactBlockDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(block: CompactBlock)
fun insert(block: CompactBlockEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(block: List<CompactBlock>)
fun insert(block: List<CompactBlockEntity>)
@Query("DELETE FROM compactblocks WHERE height >= :height")
fun rewindTo(height: Int)

View File

@ -4,14 +4,14 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(primaryKeys = ["height"], tableName = "compactblocks")
data class CompactBlock(
data class CompactBlockEntity(
val height: Int,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val data: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CompactBlock) return false
if (other !is CompactBlockEntity) return false
if (height != other.height) return false
if (!data.contentEquals(other.data)) return false

View File

@ -30,6 +30,8 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
" than just the database filename because Rust does not access the app Context." +
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.", null)
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
object FailedScan : CompactBlockProcessorException("Error while scanning blocks. This most " +
"likely means a block is missing or a reorg was mishandled. See Rust logs for details.")
}
sealed class CompactBlockStreamException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) {

View File

@ -2,6 +2,7 @@ package cash.z.wallet.sdk.ext
import cash.z.wallet.sdk.ext.Conversions.USD_FORMATTER
import cash.z.wallet.sdk.ext.Conversions.ZEC_FORMATTER
import cash.z.wallet.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode

View File

@ -2,6 +2,7 @@ package cash.z.wallet.sdk.ext
import android.content.Context
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
import kotlinx.coroutines.delay
import java.io.File
import kotlin.random.Random
@ -22,7 +23,7 @@ suspend inline fun retryUpTo(retries: Int, initialDelay: Int = 10, block: () ->
}
}
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = DEFAULT_MAX_BACKOFF_INTERVAL, block: () -> Unit) {
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, block: () -> Unit) {
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
while (true) {
try {

View File

@ -1,75 +1,84 @@
package cash.z.wallet.sdk.ext
//
// Constants
//
/**
* Miner's fee in zatoshi.
* Wrapper for all the constant values in the SDK. It is important that these values stay fixed for
* all users of the SDK. Otherwise, if individual wallet makers are using different values, it
* becomes easier to reduce privacy by segmenting the anonymity set of users, particularly as it
* relates to network requests.
*/
const val MINERS_FEE_ZATOSHI = 10_000L
object ZcashSdk {
/**
* The number of zatoshi that equal 1 ZEC.
*/
const val ZATOSHI_PER_ZEC = 100_000_000L
/**
* Miner's fee in zatoshi.
*/
const val MINERS_FEE_ZATOSHI = 10_000L
/**
* The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks
* prior to this height, at all.
*/
const val SAPLING_ACTIVATION_HEIGHT = 280_000
/**
* The number of zatoshi that equal 1 ZEC.
*/
const val ZATOSHI_PER_ZEC = 100_000_000L
/**
* The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.
*/
const val MAX_REORG_SIZE = 100
/**
* The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks
* prior to this height, at all.
*/
const val SAPLING_ACTIVATION_HEIGHT = 280_000
/**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
* by the rust backend but it is helpful to know what it is set to and shdould be kept in sync.
*/
const val EXPIRY_OFFSET = 20
/**
* The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.
*/
const val MAX_REORG_SIZE = 100
//
// Defaults
//
/**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
* by the rust backend but it is helpful to know what it is set to and shdould be kept in sync.
*/
const val EXPIRY_OFFSET = 20
/**
* Default size of batches of blocks to request from the compact block service.
*/
const val DEFAULT_BATCH_SIZE = 100
/**
* Default size of batches of blocks to request from the compact block service.
*/
const val DOWNLOAD_BATCH_SIZE = 100
/**
* Default amount of time, in milliseconds, to poll for new blocks. Typically, this should be about half the average
* block time.
*/
const val DEFAULT_POLL_INTERVAL = 75_000L
/**
* Default amount of time, in milliseconds, to poll for new blocks. Typically, this should be about half the average
* block time.
*/
const val POLL_INTERVAL = 75_000L
/**
* Default attempts at retrying.
*/
const val DEFAULT_RETRIES = 5
/**
* Default attempts at retrying.
*/
const val RETRIES = 5
/**
* The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than
* this before retyring.
*/
const val DEFAULT_MAX_BACKOFF_INTERVAL = 600_000L
/**
* The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than
* this before retyring.
*/
const val MAX_BACKOFF_INTERVAL = 600_000L
/**
* Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the
* reorg but smaller than the theoretical max reorg size of 100.
*/
const val DEFAULT_REWIND_DISTANCE = 10
/**
* Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the
* reorg but smaller than the theoretical max reorg size of 100.
*/
const val REWIND_DISTANCE = 10
/**
* The number of blocks to allow before considering our data to be stale. This usually helps with what to do when
* returning from the background and is exposed via the Synchronizer's isStale function.
*/
const val DEFAULT_STALE_TOLERANCE = 10
/**
* The default port to use for connecting to lightwalletd instances.
*/
const val LIGHTWALLETD_PORT = 9067
/**
* The default port to use for connecting to lightwalletd instances.
*/
const val DEFAULT_LIGHTWALLETD_PORT = 9067
const val DB_DATA_NAME = "transactionData.db"
const val DB_CACHE_NAME = "compactBlockCache.db"
/**
* File name for the sappling spend params
*/
const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
* File name for the sapling output params
*/
const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
}

View File

@ -1,49 +1,142 @@
package cash.z.wallet.sdk.jni
import android.content.Context
import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.wallet.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
/**
* Serves as the JNI boundary between the Kotlin and Rust layers. Functions in this class should not be called directly
* by code outside of the SDK. Instead, one of the higher-level components should be used such as Wallet.kt or
* CompactBlockProcessor.kt.
* Serves as the JNI boundary between the Kotlin and Rust layers. Functions in this class should
* not be called directly by code outside of the SDK. Instead, one of the higher-level components
* should be used such as Wallet.kt or CompactBlockProcessor.kt.
*/
class RustBackend : RustBackendWelding {
internal object RustBackend : RustBackendWelding {
private var loaded = false
private lateinit var dbDataPath: String
private lateinit var dbCachePath: String
lateinit var paramDestinationDir: String
external override fun initDataDb(dbDataPath: String): Boolean
override fun create(appContext: Context, dbCacheName: String, dbDataName: String): RustBackend {
twig("Creating RustBackend") {
// It is safe to call these things twice but not efficient. So we add a loose check and
// ignore the fact that it's not thread-safe.
if (!loaded) {
initLogs()
loadRustLibrary()
}
dbCachePath = appContext.getDatabasePath(dbCacheName).absolutePath
dbDataPath = appContext.getDatabasePath(dbDataName).absolutePath
paramDestinationDir = "${appContext.cacheDir.absolutePath}/params"
}
return this
}
external override fun initAccountsTable(
/**
* The first call made to this object in order to load the Rust backend library. All other calls
* will fail if this function has not been invoked.
*/
private fun loadRustLibrary() {
try {
System.loadLibrary("zcashwalletsdk")
loaded = true
} catch (e: Throwable) {
twig("Error while loading native library: ${e.message}")
}
}
//
// Wrapper Functions
//
override fun initDataDb(): Boolean = initDataDb(dbDataPath)
override fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String> =
initAccountsTable(dbDataPath, seed, numberOfAccounts)
override fun initBlocksTable(
height: Int,
hash: String,
time: Long,
saplingTree: String
): Boolean = initBlocksTable(dbDataPath, height, hash, time, saplingTree)
override fun getAddress(account: Int): String = getAddress(dbDataPath, account)
override fun getBalance(account: Int): Long = getBalance(dbDataPath, account)
override fun getVerifiedBalance(account: Int): Long = getVerifiedBalance(dbDataPath, account)
override fun getReceivedMemoAsUtf8(idNote: Long): String =
getReceivedMemoAsUtf8(dbDataPath, idNote)
override fun getSentMemoAsUtf8(idNote: Long): String = getSentMemoAsUtf8(dbDataPath, idNote)
override fun validateCombinedChain(): Int = validateCombinedChain(dbCachePath, dbDataPath)
override fun rewindToHeight(height: Int): Boolean = rewindToHeight(dbDataPath, height)
override fun scanBlocks(): Boolean = scanBlocks(dbCachePath, dbDataPath)
override fun createToAddress(
account: Int,
extsk: String,
to: String,
value: Long,
memo: String
): Long = createToAddress(
dbDataPath,
account,
extsk,
to,
value,
memo,
"${paramDestinationDir}/$SPEND_PARAM_FILE_NAME",
"${paramDestinationDir}/$OUTPUT_PARAM_FILE_NAME"
)
//
// External Functions
//
private external fun initDataDb(dbDataPath: String): Boolean
private external fun initAccountsTable(
dbDataPath: String,
seed: ByteArray,
accounts: Int): Array<String>
accounts: Int
): Array<String>
external override fun initBlocksTable(
private external fun initBlocksTable(
dbDataPath: String,
height: Int,
hash: String,
time: Long,
saplingTree: String): Boolean
saplingTree: String
): Boolean
external override fun getAddress(dbDataPath: String, account: Int): String
private external fun getAddress(dbDataPath: String, account: Int): String
external override fun isValidShieldedAddress(addr: String): Boolean
external override fun isValidTransparentAddress(addr: String): Boolean
external override fun getBalance(dbDataPath: String, account: Int): Long
private external fun getBalance(dbDataPath: String, account: Int): Long
external override fun getVerifiedBalance(dbDataPath: String, account: Int): Long
private external fun getVerifiedBalance(dbDataPath: String, account: Int): Long
external override fun getReceivedMemoAsUtf8(dbDataPath: String, idNote: Long): String
private external fun getReceivedMemoAsUtf8(dbDataPath: String, idNote: Long): String
external override fun getSentMemoAsUtf8(dbDataPath: String, idNote: Long): String
private external fun getSentMemoAsUtf8(dbDataPath: String, idNote: Long): String
external override fun validateCombinedChain(dbCachePath: String, dbDataPath: String): Int
private external fun validateCombinedChain(dbCachePath: String, dbDataPath: String): Int
external override fun rewindToHeight(dbDataPath: String, height: Int): Boolean
private external fun rewindToHeight(dbDataPath: String, height: Int): Boolean
external override fun scanBlocks(dbCachePath: String, dbDataPath: String): Boolean
private external fun scanBlocks(dbCachePath: String, dbDataPath: String): Boolean
external override fun createToAddress(
private external fun createToAddress(
dbDataPath: String,
account: Int,
extsk: String,
@ -54,16 +147,6 @@ class RustBackend : RustBackendWelding {
outputParamsPath: String
): Long
external override fun initLogs()
companion object {
init {
try {
System.loadLibrary("zcashwalletsdk")
} catch (e: Throwable) {
twig("Error while loading native library: ${e.message}")
}
}
}
private external fun initLogs()
}

View File

@ -1,56 +1,52 @@
package cash.z.wallet.sdk.jni
import android.content.Context
import cash.z.wallet.sdk.ext.ZcashSdk
/**
* Contract defining the exposed capabilitiies of the Rust backend.
* Contract defining the exposed capabilities of the Rust backend.
* This is what welds the SDK to the Rust layer.
*/
interface RustBackendWelding {
fun initDataDb(dbDataPath: String): Boolean
fun create(
appContext: Context,
dbCacheName: String = ZcashSdk.DB_CACHE_NAME,
dbDataName: String = ZcashSdk.DB_DATA_NAME
): RustBackendWelding
fun initAccountsTable(
dbDataPath: String,
seed: ByteArray,
accounts: Int): Array<String>
fun initDataDb(): Boolean
fun initBlocksTable(
dbDataPath: String,
height: Int,
hash: String,
time: Long,
saplingTree: String): Boolean
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
fun getAddress(dbDataPath: String, account: Int): String
fun initBlocksTable(height: Int, hash: String, time: Long, saplingTree: String): Boolean
fun getAddress(account: Int = 0): String
fun isValidShieldedAddress(addr: String): Boolean
fun isValidTransparentAddress(addr: String): Boolean
fun getBalance(dbDataPath: String, account: Int): Long
fun getBalance(account: Int = 0): Long
fun getVerifiedBalance(dbDataPath: String, account: Int): Long
fun getVerifiedBalance(account: Int = 0): Long
fun getReceivedMemoAsUtf8(dbDataPath: String, idNote: Long): String
fun getReceivedMemoAsUtf8(idNote: Long): String
fun getSentMemoAsUtf8(dbDataPath: String, idNote: Long): String
fun getSentMemoAsUtf8(idNote: Long): String
fun validateCombinedChain(dbCachePath: String, dbDataPath: String): Int
fun validateCombinedChain(): Int
fun rewindToHeight(dbDataPath: String, height: Int): Boolean
fun rewindToHeight(height: Int): Boolean
fun scanBlocks(dbCachePath: String, dbDataPath: String): Boolean
fun scanBlocks(): Boolean
fun createToAddress(
dbDataPath: String,
account: Int,
extsk: String,
to: String,
value: Long,
memo: String,
spendParamsPath: String,
outputParamsPath: String
memo: String
): Long
fun initLogs()
}

View File

@ -6,10 +6,14 @@ import cash.z.wallet.sdk.data.twig
import cash.z.wallet.sdk.data.twigTask
import cash.z.wallet.sdk.exception.RustLayerException
import cash.z.wallet.sdk.exception.WalletException
import cash.z.wallet.sdk.ext.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.exception.WalletException.MalformattedBirthdayFilesException
import cash.z.wallet.sdk.exception.WalletException.MissingBirthdayFilesException
import cash.z.wallet.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.wallet.sdk.ext.masked
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.jni.RustBackendWelding
import cash.z.wallet.sdk.secure.Wallet.WalletBirthday
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import com.squareup.okhttp.OkHttpClient
@ -24,76 +28,96 @@ import kotlin.properties.ReadWriteProperty
/**
* Wrapper for the Rust backend. This class basically represents all the Rust-wallet
* capabilities and the supporting data required to exercise those abilities.
*
* @param birthday the birthday of this wallet. See [WalletBirthday] for more info.
* Wrapper for all the Rust backend functionality that does not involve processing blocks. This
* class initializes the Rust backend and the supporting data required to exercise those abilities.
* The [cash.z.wallet.sdk.block.CompactBlockProcessor] handles all the remaining Rust backend
* functionality, related to processing blocks.
*/
class Wallet(
class Wallet private constructor(
private val birthday: WalletBirthday,
private val rustBackend: RustBackendWelding,
private val dataDbPath: String,
private val paramDestinationDir: String,
/** indexes of accounts ids. In the reference wallet, we only work with account 0 */
private val accountIds: Array<Int> = arrayOf(0),
private val seedProvider: ReadOnlyProperty<Any?, ByteArray>,
spendingKeyProvider: ReadWriteProperty<Any?, String>
spendingKeyProvider: ReadWriteProperty<Any?, String>,
/** indexes of accounts ids. In the reference wallet, we only work with account 0 */
private val accountIds: Array<Int> = arrayOf(0)
) {
/**
* Construct a wallet from the given birthday. It will take care of downloading params, on first
* use, to a private cache directory as well as initializing the RustBackend. The file paths for
* these components are not configurable
* per github issue https://github.com/zcash/zcash-android-wallet-sdk/issues/30
*
* @param appContext the application context. This is used for loading a checkpoint
* corresponding to the wallet's birthday from the SDK assets directory and also for setting
* the path for storing the sapling params.
* @param birthday the date this wallet's seed was created.
* @param rustBackend the RustBackend that the rest of the SDK will use. It is initialized by
* by this class.
* @param seedProvider a read-only property that provides the seed.
* @param spendingKeyProvider a read-only property that provides the spending key.
* @param accountIds the ids of the accounts to use. Defaults to a array of 0.
*/
constructor(
context: Context,
appContext: Context,
rustBackend: RustBackendWelding,
dataDbName: String,
seedProvider: ReadOnlyProperty<Any?, ByteArray>,
spendingKeyProvider: ReadWriteProperty<Any?, String>,
birthday: WalletBirthday = loadBirthdayFromAssets(context),
paramDestinationDir: String = "${context.cacheDir.absolutePath}/params",
birthday: WalletBirthday = loadBirthdayFromAssets(appContext),
accountIds: Array<Int> = arrayOf(0)
) : this(
birthday = birthday,
rustBackend = rustBackend,
dataDbPath = context.getDatabasePath(dataDbName).absolutePath,
paramDestinationDir = paramDestinationDir,
accountIds = accountIds,
paramDestinationDir = (rustBackend as RustBackend).paramDestinationDir,
seedProvider = seedProvider,
spendingKeyProvider = spendingKeyProvider
spendingKeyProvider = spendingKeyProvider,
accountIds = accountIds
)
/**
* Delegate for storing spending keys. This will be used when the spending keys are created during initialization.
* Delegate for storing spending keys. This will be used when the spending keys are created
* during initialization.
*/
private var spendingKeyStore by spendingKeyProvider
/**
* Initializes the wallet by creating the DataDb and pre-populating it with data corresponding to the birthday for
* this wallet.
* Initializes the wallet by creating the DataDb and pre-populating it with data corresponding
* to the birthday for this wallet.
*/
fun initialize(
firstRunStartHeight: Int = SAPLING_ACTIVATION_HEIGHT
): Int {
// TODO: find a better way to map these exceptions from the Rust side. For now, match error text :(
try {
rustBackend.initDataDb(dataDbPath)
twig("Initialized wallet for first run into file $dataDbPath")
rustBackend.initDataDb()
twig("Initialized wallet for first run")
} catch (e: Throwable) {
throw WalletException.FalseStart(e)
}
try {
rustBackend.initBlocksTable(dataDbPath, birthday.height, birthday.hash, birthday.time, birthday.tree)
twig("seeded the database with sapling tree at height ${birthday.height} into file $dataDbPath")
rustBackend.initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
} catch (t: Throwable) {
if (t.message?.contains("is not empty") == true) throw WalletException.AlreadyInitializedException(t)
else throw WalletException.FalseStart(t)
if (t.message?.contains("is not empty") == true) {
throw WalletException.AlreadyInitializedException(t)
} else {
throw WalletException.FalseStart(t)
}
}
try {
// store the spendingkey by leveraging the utilities provided during construction
val seed by seedProvider
val accountSpendingKeys = rustBackend.initAccountsTable(dataDbPath, seed, 1)
val accountSpendingKeys =
rustBackend.initAccountsTable(seed, 1)
spendingKeyStore = accountSpendingKeys[0]
twig("Initialized the accounts table into file $dataDbPath")
twig("Initialized the accounts table")
return Math.max(firstRunStartHeight, birthday.height)
} catch (e: Throwable) {
throw WalletException.FalseStart(e)
@ -104,7 +128,7 @@ class Wallet(
* Gets the address for this wallet, defaulting to the first account.
*/
fun getAddress(accountId: Int = accountIds[0]): String {
return rustBackend.getAddress(dataDbPath, accountId)
return rustBackend.getAddress(accountId)
}
/**
@ -114,20 +138,21 @@ class Wallet(
* @param accountId the account to check for balance info. Defaults to zero.
*/
fun availableBalanceSnapshot(accountId: Int = accountIds[0]): Long {
return rustBackend.getVerifiedBalance(dataDbPath, accountId)
return rustBackend.getVerifiedBalance(accountId)
}
/**
* Calculates the latest balance info and emits it into the balance channel. Defaults to the first account.
* Calculates the latest balance info and emits it into the balance channel. Defaults to the
* first account.
*
* @param accountId the account to check for balance info.
*/
suspend fun getBalanceInfo(accountId: Int = accountIds[0]): WalletBalance = withContext(IO) {
twigTask("checking balance info") {
try {
val balanceTotal = rustBackend.getBalance(dataDbPath, accountId)
val balanceTotal = rustBackend.getBalance(accountId)
twig("found total balance of: $balanceTotal")
val balanceAvailable = rustBackend.getVerifiedBalance(dataDbPath, accountId)
val balanceAvailable = rustBackend.getVerifiedBalance(accountId)
twig("found available balance of: $balanceAvailable")
WalletBalance(balanceTotal, balanceAvailable)
} catch (t: Throwable) {
@ -154,20 +179,17 @@ class Wallet(
memo: String = "",
fromAccountId: Int = accountIds[0]
): Long = withContext(IO) {
twigTask("creating transaction to spend $value zatoshi to ${toAddress.masked()} with memo $memo") {
twigTask("creating transaction to spend $value zatoshi to" +
" ${toAddress.masked()} with memo $memo") {
try {
ensureParams(paramDestinationDir)
twig("params exist at $paramDestinationDir! attempting to send...")
rustBackend.createToAddress(
dataDbPath,
fromAccountId,
spendingKeyStore,
toAddress,
value,
memo,
// using names here so it's easier to avoid transposing them, if the function signature changes
spendParamsPath = SPEND_PARAM_FILE_NAME.toPath(),
outputParamsPath = OUTPUT_PARAM_FILE_NAME.toPath()
memo
)
} catch (t: Throwable) {
twig("${t.message}")
@ -181,9 +203,10 @@ class Wallet(
/**
* Download and store the params into the given directory.
*
* @param destinationDir the directory where the params will be stored. It's assumed that we have write access to
* this directory. Typically, this should be the app's cache directory because it is not harmful if these files are
* cleared by the user since they are downloaded on-demand.
* @param destinationDir the directory where the params will be stored. It's assumed that we
* have write access to this directory. Typically, this should be the app's cache directory
* because it is not harmful if these files are cleared by the user since they are downloaded
* on-demand.
*/
suspend fun fetchParams(destinationDir: String) = withContext(IO) {
val client = createHttpClient()
@ -215,7 +238,8 @@ class Wallet(
}
/**
* Checks the given directory for the output and spending params and calls [fetchParams] if they're missing.
* Checks the given directory for the output and spending params and calls [fetchParams] if
* they're missing.
*
* @param destinationDir the directory where the params should be stored.
*/
@ -245,8 +269,8 @@ class Wallet(
//
/**
* Http client is only used for downloading sapling spend and output params data, which are necessary for the wallet to
* scan blocks.
* Http client is only used for downloading sapling spend and output params data, which are
* necessary for the wallet to scan blocks.
*/
private fun createHttpClient(): OkHttpClient {
//TODO: add logging and timeouts
@ -259,77 +283,74 @@ class Wallet(
companion object {
/**
* The Url that is used by default in zcashd.
* We'll want to make this externally configurable, rather than baking it into the SDK but this will do for now,
* since we're using a cloudfront URL that already redirects.
* We'll want to make this externally configurable, rather than baking it into the SDK but
* this will do for now, since we're using a cloudfront URL that already redirects.
*/
const val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
/**
* File name for the sappling spend params
*/
const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
* File name for the sapling output params
*/
const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
/**
* Directory within the assets folder where birthday data (i.e. sapling trees for a given height) can be found.
* Directory within the assets folder where birthday data
* (i.e. sapling trees for a given height) can be found.
*/
const val BIRTHDAY_DIRECTORY = "zcash/saplingtree"
/**
* Load the given birthday file from the assets of the given context. When no height is specified, we default to
* the file with the greatest name.
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when parsing fails.
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
fun loadBirthdayFromAssets(context: Context, birthdayHeight: Int? = null): WalletBirthday {
val treeFiles = context.assets.list(Wallet.BIRTHDAY_DIRECTORY)?.apply { sortDescending() }
if (treeFiles.isNullOrEmpty()) throw WalletException.MissingBirthdayFilesException(BIRTHDAY_DIRECTORY)
val treeFiles =
context.assets.list(Wallet.BIRTHDAY_DIRECTORY)?.apply { sortDescending() }
if (treeFiles.isNullOrEmpty()) throw MissingBirthdayFilesException(BIRTHDAY_DIRECTORY)
try {
val file = treeFiles.first {
if (birthdayHeight == null) true
else it.contains(birthdayHeight.toString())
}
val reader =
JsonReader(InputStreamReader(context.assets.open("$BIRTHDAY_DIRECTORY/$file")))
val reader = JsonReader(
InputStreamReader(context.assets.open("$BIRTHDAY_DIRECTORY/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw WalletException.MalformattedBirthdayFilesException(BIRTHDAY_DIRECTORY, treeFiles[0])
throw MalformattedBirthdayFilesException(BIRTHDAY_DIRECTORY, treeFiles[0])
}
}
}
/**
* Represents the wallet's birthday which can be thought of as a checkpoint at the earliest moment in history where
* transactions related to this wallet could exist. Ideally, this would correspond to the latest block height at the
* time the wallet key was created. Worst case, the height of Sapling activation could be used (280000).
* Represents the wallet's birthday which can be thought of as a checkpoint at the earliest
* moment in history where transactions related to this wallet could exist. Ideally, this would
* correspond to the latest block height at the time the wallet key was created. Worst case, the
* height of Sapling activation could be used (280000).
*
* Knowing a wallet's birthday can significantly reduce the amount of data that it needs to download because none of
* the data before that height needs to be scanned for transactions. However, we do need the Sapling tree data in
* order to construct valid transactions from that point forward. This birthday contains that tree data, allowing us
* to avoid downloading all the compact blocks required in order to generate it.
* Knowing a wallet's birthday can significantly reduce the amount of data that it needs to
* download because none of the data before that height needs to be scanned for transactions.
* However, we do need the Sapling tree data in order to construct valid transactions from that
* point forward. This birthday contains that tree data, allowing us to avoid downloading all
* the compact blocks required in order to generate it.
*
* Currently, the data for this is generated by running `cargo run --release --features=updater` with the SDK and
* saving the resulting JSON to the `src/main/assets/zcash` folder. That script simply builds a Sapling tree from
* the start of Sapling activation up to the latest block height. In the future, this data could be exposed as a
* service on the lightwalletd server because every zcashd node already maintains the sapling tree for each block.
* For now, we just include the latest checkpoint in each SDK release.
* Currently, the data for this is generated by running `cargo run --release --features=updater`
* with the SDK and saving the resulting JSON to the `src/main/assets/zcash` folder. That script
* simply builds a Sapling tree from the start of Sapling activation up to the latest block
* height. In the future, this data could be exposed as a service on the lightwalletd server
* because every zcashd node already maintains the sapling tree for each block. For now, we just
* include the latest checkpoint in each SDK release.
*
* New wallets can ignore any blocks created before their birthday.
*
* @param height the height at the time the wallet was born
* @param hash the block hash corresponding to the given height
* @param time the time the wallet was born, in seconds
* @param tree the sapling tree corresponding to the given height. This takes around 15 minutes of processing to
* generate from scratch because all blocks since activation need to be considered. So when it is calculated in
* advance it can save the user a lot of time.
* @param tree the sapling tree corresponding to the given height. This takes around 15 minutes
* of processing to generate from scratch because all blocks since activation need to be
* considered. So when it is calculated in advance it can save the user a lot of time.
*/
data class WalletBirthday(
val height: Int = -1,
@ -339,14 +360,14 @@ class Wallet(
)
/**
* Data structure to hold the total and available balance of the wallet. This is what is received on the balance
* channel.
* Data structure to hold the total and available balance of the wallet. This is what is
* received on the balance channel.
*
* @param total the total balance, ignoring funds that cannot be used.
* @param available the amount of funds that are available for use. Typical reasons that funds may be unavailable
* include fairly new transactions that do not have enough confirmations or notes that are tied up because we are
* awaiting change from a transaction. When a note has been spent, its change cannot be used until there are enough
* confirmations.
* @param available the amount of funds that are available for use. Typical reasons that funds
* may be unavailable include fairly new transactions that do not have enough confirmations or
* notes that are tied up because we are awaiting change from a transaction. When a note has
* been spent, its change cannot be used until there are enough confirmations.
*/
data class WalletBalance(
val total: Long = -1,

View File

@ -2,9 +2,8 @@ package cash.z.wallet.sdk.service
import android.content.Context
import cash.z.wallet.sdk.R
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.exception.LightwalletException
import cash.z.wallet.sdk.ext.DEFAULT_LIGHTWALLETD_PORT
import cash.z.wallet.sdk.ext.ZcashSdk.LIGHTWALLETD_PORT
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
import cash.z.wallet.sdk.rpc.Service
@ -32,13 +31,19 @@ class LightWalletGrpcService private constructor(
constructor(
appContext: Context,
host: String,
port: Int = DEFAULT_LIGHTWALLETD_PORT,
port: Int = LIGHTWALLETD_PORT,
usePlaintext: Boolean = !appContext.resources.getBoolean(R.bool.is_mainnet)
) : this(createDefaultChannel(appContext, host, port, usePlaintext))
/* LightWalletService implementation */
override fun getBlockRange(heightRange: IntRange): List<CompactBlock> {
/**
* Blocking call to download all blocks in the given range.
*
* @param heightRange the inclusive range of block heights to download.
* @return a list of compact blocks for the given range
*/
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
channel.resetConnectBackoff()
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
}
@ -48,9 +53,9 @@ class LightWalletGrpcService private constructor(
return channel.createStub(singleRequestTimeoutSec).getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
}
override fun submitTransaction(raw: ByteArray): Service.SendResponse {
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
channel.resetConnectBackoff()
val request = Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(raw)).build()
val request = Service.RawTransaction.newBuilder().setData(ByteString.copyFrom(spendTransaction)).build()
return channel.createStub().sendTransaction(request)
}
@ -68,15 +73,14 @@ class LightWalletGrpcService private constructor(
private inline fun IntRange.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
.setStart(this.first.toBlockHeight())
.setEnd(this.last.toBlockHeight())
.setStart(first.toBlockHeight())
.setEnd(last.toBlockHeight())
.build()
private fun Iterator<CompactFormats.CompactBlock>.toList(): List<CompactBlock> =
mutableListOf<CompactBlock>().apply {
private fun Iterator<CompactFormats.CompactBlock>.toList(): List<CompactFormats.CompactBlock> =
mutableListOf<CompactFormats.CompactBlock>().apply {
while (hasNext()) {
val compactBlock = next()
this@apply += CompactBlock(compactBlock.height.toInt(), compactBlock.toByteArray())
this@apply += next()
}
}
@ -101,5 +105,4 @@ class LightWalletGrpcService private constructor(
.build()
}
}
}
}

View File

@ -1,6 +1,6 @@
package cash.z.wallet.sdk.service
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.Service
/**
@ -14,7 +14,7 @@ interface LightWalletService {
* @param heightRange the inclusive range to fetch. For instance if 1..5 is given, then every block in that range
* will be fetched, including 1 and 5.
*/
fun getBlockRange(heightRange: IntRange): List<CompactBlock>
fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock>
/**
* Return the latest block height known to the service.
@ -24,5 +24,5 @@ interface LightWalletService {
/**
* Submit a raw transaction.
*/
fun submitTransaction(transactionRaw: ByteArray): Service.SendResponse
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
}

View File

@ -4,7 +4,9 @@ import cash.z.wallet.sdk.data.TransactionRepository
import cash.z.wallet.sdk.data.TroubleshootingTwig
import cash.z.wallet.sdk.data.Twig
import cash.z.wallet.sdk.entity.CompactBlock
import cash.z.wallet.sdk.entity.CompactBlockEntity
import cash.z.wallet.sdk.ext.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT
import cash.z.wallet.sdk.jni.RustBackendWelding
import cash.z.wallet.sdk.service.LightWalletService
import com.nhaarman.mockitokotlin2.*
@ -12,7 +14,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ -51,7 +53,7 @@ internal class CompactBlockProcessorTest {
getBlockRange(any())
}.thenAnswer { invocation ->
val range = invocation.arguments[0] as IntRange
range.map { CompactBlock(it, ByteArray(0)) }
range.map { CompactBlockEntity(it, ByteArray(0)) }
}
}
lightwalletService.stub {
@ -64,7 +66,7 @@ internal class CompactBlockProcessorTest {
onBlocking {
write(any())
}.thenAnswer { invocation ->
val lastBlockHeight = (invocation.arguments[0] as List<CompactBlock>).last().height
val lastBlockHeight = (invocation.arguments[0] as List<CompactBlockEntity>).last().height
lastDownloadedHeight = lastBlockHeight
Unit
}