2019-06-14 16:24:52 -07:00
|
|
|
package cash.z.wallet.sdk.block
|
|
|
|
|
|
|
|
import androidx.annotation.VisibleForTesting
|
|
|
|
import cash.z.wallet.sdk.annotation.OpenForTesting
|
2019-10-21 03:26:02 -07:00
|
|
|
import cash.z.wallet.sdk.block.CompactBlockProcessor.State.*
|
2020-03-25 14:58:08 -07:00
|
|
|
import cash.z.wallet.sdk.entity.ConfirmedTransaction
|
2019-06-14 16:24:52 -07:00
|
|
|
import cash.z.wallet.sdk.exception.CompactBlockProcessorException
|
2019-11-01 13:25:28 -07:00
|
|
|
import cash.z.wallet.sdk.exception.RustLayerException
|
|
|
|
import cash.z.wallet.sdk.ext.*
|
2019-09-26 09:58:37 -07:00
|
|
|
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
|
2020-01-15 04:10:22 -08:00
|
|
|
import cash.z.wallet.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE
|
2019-11-01 13:25:28 -07:00
|
|
|
import cash.z.wallet.sdk.jni.RustBackend
|
2019-06-14 16:24:52 -07:00
|
|
|
import cash.z.wallet.sdk.jni.RustBackendWelding
|
2019-11-01 13:25:28 -07:00
|
|
|
import cash.z.wallet.sdk.transaction.TransactionRepository
|
2020-03-25 14:58:08 -07:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2019-06-14 16:24:52 -07:00
|
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
|
|
|
import kotlinx.coroutines.delay
|
2019-10-21 03:26:02 -07:00
|
|
|
import kotlinx.coroutines.flow.asFlow
|
2020-03-25 14:58:08 -07:00
|
|
|
import kotlinx.coroutines.flow.collect
|
|
|
|
import kotlinx.coroutines.flow.onEach
|
2019-06-14 16:24:52 -07:00
|
|
|
import kotlinx.coroutines.isActive
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger
|
2019-09-26 09:58:37 -07:00
|
|
|
import kotlin.math.max
|
|
|
|
import kotlin.math.min
|
|
|
|
import kotlin.math.roundToInt
|
2019-06-14 16:24:52 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Responsible for processing the compact blocks that are received from the lightwallet server. This class encapsulates
|
|
|
|
* all the business logic required to validate and scan the blockchain and is therefore tightly coupled with
|
|
|
|
* librustzcash.
|
2019-12-23 12:02:58 -08:00
|
|
|
*
|
2020-02-27 00:25:07 -08:00
|
|
|
* @property downloader the component responsible for downloading compact blocks and persisting them
|
|
|
|
* locally for processing.
|
|
|
|
* @property repository the repository holding transaction information.
|
|
|
|
* @property rustBackend the librustzcash functionality available and exposed to the SDK.
|
2019-12-23 12:02:58 -08:00
|
|
|
* @param minimumHeight the lowest height that we could care about. This is mostly used during
|
|
|
|
* reorgs as a backstop to make sure we do not rewind beyond sapling activation. It also is factored
|
|
|
|
* in when considering initial range to download. In most cases, this should be the birthday height
|
|
|
|
* of the current wallet--the height before which we do not need to scan for transactions.
|
2019-06-14 16:24:52 -07:00
|
|
|
*/
|
|
|
|
@OpenForTesting
|
|
|
|
class CompactBlockProcessor(
|
2019-12-23 11:50:52 -08:00
|
|
|
val downloader: CompactBlockDownloader,
|
2019-06-14 16:24:52 -07:00
|
|
|
private val repository: TransactionRepository,
|
2019-10-21 03:26:02 -07:00
|
|
|
private val rustBackend: RustBackendWelding,
|
|
|
|
minimumHeight: Int = SAPLING_ACTIVATION_HEIGHT
|
2019-06-14 16:24:52 -07:00
|
|
|
) {
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Callback for any critical errors that occur while processing compact blocks.
|
|
|
|
*/
|
2020-02-21 15:14:34 -08:00
|
|
|
var onProcessorErrorListener: ((Throwable) -> Boolean)? = null
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Callbaqck for reorgs. This callback is invoked when validation fails with the height at which
|
|
|
|
* an error was found and the lower bound to which the data will rewind, at most.
|
|
|
|
*/
|
2020-02-21 15:14:34 -08:00
|
|
|
var onChainErrorListener: ((Int, Int) -> Any)? = null
|
2019-10-21 03:26:02 -07:00
|
|
|
|
2019-06-17 02:01:29 -07:00
|
|
|
private val consecutiveChainErrors = AtomicInteger(0)
|
2019-10-21 03:26:02 -07:00
|
|
|
private val lowerBoundHeight: Int = max(SAPLING_ACTIVATION_HEIGHT, minimumHeight - MAX_REORG_SIZE)
|
|
|
|
|
|
|
|
private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized)
|
2019-11-01 13:25:28 -07:00
|
|
|
private val _progress = ConflatedBroadcastChannel(0)
|
2020-01-14 09:52:41 -08:00
|
|
|
private val _processorInfo = ConflatedBroadcastChannel(ProcessorInfo())
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The root source of truth for the processor's progress. All processing must be done
|
|
|
|
* sequentially, due to the way sqlite works so it is okay for this not to be threadsafe or
|
|
|
|
* coroutine safe because processing cannot be concurrent.
|
|
|
|
*/
|
|
|
|
private var currentInfo = ProcessorInfo()
|
2019-06-14 16:24:52 -07:00
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* The flow of state values so that a wallet can monitor the state of this class without needing
|
|
|
|
* to poll.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
val state = _state.asFlow()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The flow of progress values so that a wallet can monitor how much downloading remains
|
|
|
|
* without needing to poll.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
val progress = _progress.asFlow()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The flow of detailed processorInfo like the range of blocks that shall be downloaded and
|
|
|
|
* scanned. This gives the wallet a lot of insight into the work of this processor.
|
|
|
|
*/
|
2020-01-14 09:52:41 -08:00
|
|
|
val processorInfo = _processorInfo.asFlow()
|
2019-06-14 16:24:52 -07:00
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Download compact blocks, verify and scan them until [stop] is called.
|
2019-06-14 16:24:52 -07:00
|
|
|
*/
|
|
|
|
suspend fun start() = withContext(IO) {
|
|
|
|
twig("processor starting")
|
|
|
|
|
|
|
|
// using do/while makes it easier to execute exactly one loop which helps with testing this processor quickly
|
2019-06-17 02:01:29 -07:00
|
|
|
// (because you can start and then immediately set isStopped=true to always get precisely one loop)
|
2019-06-14 16:24:52 -07:00
|
|
|
do {
|
2020-02-21 15:14:34 -08:00
|
|
|
retryWithBackoff(::onProcessorError, maxDelayMillis = MAX_BACKOFF_INTERVAL) {
|
2019-06-14 16:24:52 -07:00
|
|
|
val result = processNewBlocks()
|
|
|
|
// immediately process again after failures in order to download new blocks right away
|
|
|
|
if (result < 0) {
|
2019-06-17 02:01:29 -07:00
|
|
|
consecutiveChainErrors.set(0)
|
2019-09-26 09:58:37 -07:00
|
|
|
twig("Successfully processed new blocks. Sleeping for ${POLL_INTERVAL}ms")
|
|
|
|
delay(POLL_INTERVAL)
|
2019-06-14 16:24:52 -07:00
|
|
|
} else {
|
2019-09-26 09:58:37 -07:00
|
|
|
if(consecutiveChainErrors.get() >= RETRIES) {
|
2019-06-17 02:01:29 -07:00
|
|
|
val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"
|
2019-06-14 16:24:52 -07:00
|
|
|
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
|
|
|
|
} else {
|
|
|
|
handleChainError(result)
|
|
|
|
}
|
2019-06-17 02:01:29 -07:00
|
|
|
consecutiveChainErrors.getAndIncrement()
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
}
|
2019-12-23 12:02:58 -08:00
|
|
|
} while (isActive && !_state.isClosedForSend && _state.value !is Stopped)
|
2019-06-14 16:24:52 -07:00
|
|
|
twig("processor complete")
|
|
|
|
stop()
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
|
|
|
* Sets the state to [Stopped], which causes the processor loop to exit.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
suspend fun stop() {
|
2020-02-21 15:14:34 -08:00
|
|
|
runCatching {
|
|
|
|
setState(Stopped)
|
|
|
|
downloader.stop()
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
|
|
|
* Stop processing and throw an error.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
private suspend fun fail(error: Throwable) {
|
2019-06-14 16:24:52 -07:00
|
|
|
stop()
|
|
|
|
twig("${error.message}")
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process new blocks returning false whenever an error was found.
|
|
|
|
*
|
|
|
|
* @return -1 when processing was successful and did not encounter errors during validation or scanning. Otherwise
|
|
|
|
* return the block height where an error was found.
|
|
|
|
*/
|
2019-06-19 14:52:15 -07:00
|
|
|
private suspend fun processNewBlocks(): Int = withContext(IO) {
|
2020-02-11 17:00:35 -08:00
|
|
|
verifySetup()
|
2020-01-14 09:52:41 -08:00
|
|
|
twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...")
|
2020-02-21 15:14:34 -08:00
|
|
|
|
|
|
|
updateRanges()
|
|
|
|
if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
|
|
|
|
twig("Nothing to process: no new blocks to download or scan, right now.")
|
|
|
|
setState(Scanned(currentInfo.lastScanRange))
|
|
|
|
-1
|
|
|
|
} else {
|
|
|
|
downloadNewBlocks(currentInfo.lastDownloadRange)
|
2020-03-25 14:58:08 -07:00
|
|
|
validateAndScanNewBlocks(currentInfo.lastScanRange).also {
|
|
|
|
enhanceTransactionDetails(currentInfo.lastScanRange)
|
|
|
|
}
|
2020-02-21 15:14:34 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the latest range info and then uses that initialInfo to update (and transmit)
|
|
|
|
* the scan/download ranges that require processing.
|
|
|
|
*/
|
|
|
|
private suspend fun updateRanges() = withContext(IO) {
|
2020-03-25 14:58:08 -07:00
|
|
|
// TODO: rethink this and make it easier to understand what's happening. Can we reduce this
|
|
|
|
// so that we only work with actual changing info rather than periodic snapshots? Do we need
|
|
|
|
// to calculate these derived values every time?
|
2020-01-14 09:52:41 -08:00
|
|
|
ProcessorInfo(
|
|
|
|
networkBlockHeight = downloader.getLatestBlockHeight(),
|
|
|
|
lastScannedHeight = getLastScannedHeight(),
|
|
|
|
lastDownloadedHeight = max(getLastDownloadedHeight(), lowerBoundHeight - 1)
|
|
|
|
).let { initialInfo ->
|
|
|
|
updateProgress(
|
|
|
|
networkBlockHeight = initialInfo.networkBlockHeight,
|
|
|
|
lastScannedHeight = initialInfo.lastScannedHeight,
|
|
|
|
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
|
|
|
|
lastScanRange = (initialInfo.lastScannedHeight + 1)..initialInfo.networkBlockHeight,
|
2020-02-21 15:14:34 -08:00
|
|
|
lastDownloadRange = (max(
|
|
|
|
initialInfo.lastDownloadedHeight,
|
|
|
|
initialInfo.lastScannedHeight
|
|
|
|
) + 1)..initialInfo.networkBlockHeight
|
2020-01-14 09:52:41 -08:00
|
|
|
)
|
|
|
|
}
|
2020-02-21 15:14:34 -08:00
|
|
|
}
|
2019-10-21 03:26:02 -07:00
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
|
|
|
* Given a range, validate and then scan all blocks. Validation is ensuring that the blocks are
|
|
|
|
* in ascending order, with no gaps and are also chain-sequential. This means every block's
|
|
|
|
* prevHash value matches the preceding block in the chain.
|
|
|
|
*
|
2020-02-27 00:25:07 -08:00
|
|
|
* @param lastScanRange the range to be validated and scanned.
|
|
|
|
*
|
2020-02-21 15:14:34 -08:00
|
|
|
* @return error code or -1 when there is no error.
|
|
|
|
*/
|
|
|
|
private suspend fun validateAndScanNewBlocks(lastScanRange: IntRange): Int = withContext(IO) {
|
|
|
|
setState(Validating)
|
|
|
|
var error = validateNewBlocks(lastScanRange)
|
|
|
|
if (error < 0) {
|
|
|
|
// 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
|
|
|
|
setState(Scanning)
|
|
|
|
val success = scanNewBlocks(lastScanRange)
|
|
|
|
if (!success) throw CompactBlockProcessorException.FailedScan()
|
|
|
|
else {
|
|
|
|
setState(Scanned(lastScanRange))
|
|
|
|
}
|
2019-09-26 09:58:37 -07:00
|
|
|
-1
|
2019-06-14 16:24:52 -07:00
|
|
|
} else {
|
2020-02-21 15:14:34 -08:00
|
|
|
error
|
2020-01-14 09:52:41 -08:00
|
|
|
}
|
|
|
|
}
|
2019-11-22 23:18:20 -08:00
|
|
|
|
2020-03-25 14:58:08 -07:00
|
|
|
private suspend fun enhanceTransactionDetails(lastScanRange: IntRange): Int {
|
|
|
|
Twig.sprout("enhancing")
|
|
|
|
twig("Enhancing transaction details for blocks $lastScanRange")
|
|
|
|
setState(Enhancing)
|
|
|
|
return try {
|
|
|
|
val newTxs = repository.findNewTransactions(lastScanRange)
|
|
|
|
if (newTxs == null) twig("no new transactions found in $lastScanRange")
|
|
|
|
newTxs?.onEach { newTransaction ->
|
|
|
|
if (newTransaction == null) twig("somehow, new transaction was null!!!")
|
|
|
|
else enhance(newTransaction)
|
|
|
|
}
|
|
|
|
twig("Done enhancing transaction details")
|
|
|
|
1
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
twig("Failed to enhance due to $t")
|
|
|
|
t.printStackTrace()
|
|
|
|
-1
|
|
|
|
} finally {
|
|
|
|
Twig.clip("enhancing")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun enhance(transaction: ConfirmedTransaction) = withContext(Dispatchers.IO) {
|
|
|
|
twig("START: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
|
|
|
downloader.fetchTransaction(transaction.rawTransactionId)?.let { tx ->
|
|
|
|
twig("decrypting and storing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
|
|
|
rustBackend.decryptAndStoreTransaction(tx.data.toByteArray())
|
|
|
|
} ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.")
|
|
|
|
twig("DONE: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})")
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
|
|
|
* Confirm that the wallet data is properly setup for use.
|
|
|
|
*/
|
2020-02-11 17:00:35 -08:00
|
|
|
private fun verifySetup() {
|
|
|
|
if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Request all blocks in the given range and persist them locally for processing, later.
|
|
|
|
*
|
|
|
|
* @param range the range of blocks to download.
|
2020-02-21 15:14:34 -08:00
|
|
|
*/
|
2019-06-14 16:24:52 -07:00
|
|
|
@VisibleForTesting //allow mocks to verify how this is called, rather than the downloader, which is more complex
|
2019-06-19 14:52:15 -07:00
|
|
|
internal suspend fun downloadNewBlocks(range: IntRange) = withContext<Unit>(IO) {
|
2019-06-14 16:24:52 -07:00
|
|
|
if (range.isEmpty()) {
|
|
|
|
twig("no blocks to download")
|
2019-06-19 14:52:15 -07:00
|
|
|
} else {
|
2019-10-21 03:26:02 -07:00
|
|
|
_state.send(Downloading)
|
2019-06-19 14:52:15 -07:00
|
|
|
Twig.sprout("downloading")
|
|
|
|
twig("downloading blocks in range $range")
|
|
|
|
|
2019-09-26 09:58:37 -07:00
|
|
|
var downloadedBlockHeight = range.first
|
2019-06-19 14:52:15 -07:00
|
|
|
val missingBlockCount = range.last - range.first + 1
|
2019-09-26 09:58:37 -07:00
|
|
|
val batches = (missingBlockCount / DOWNLOAD_BATCH_SIZE
|
|
|
|
+ (if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0) 0 else 1))
|
2019-06-19 14:52:15 -07:00
|
|
|
var progress: Int
|
2019-09-26 09:58:37 -07:00
|
|
|
twig("found $missingBlockCount missing blocks, downloading in $batches batches of ${DOWNLOAD_BATCH_SIZE}...")
|
2019-06-19 14:52:15 -07:00
|
|
|
for (i in 1..batches) {
|
2020-02-21 15:14:34 -08:00
|
|
|
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
|
2020-01-14 09:57:39 -08:00
|
|
|
val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive
|
|
|
|
var count = 0
|
|
|
|
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
|
|
|
|
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
|
2019-06-19 14:52:15 -07:00
|
|
|
}
|
2020-01-14 09:57:39 -08:00
|
|
|
twig("downloaded $count blocks!")
|
2019-09-26 09:58:37 -07:00
|
|
|
progress = (i / batches.toFloat() * 100).roundToInt()
|
2019-11-01 13:25:28 -07:00
|
|
|
_progress.send(progress)
|
2020-02-11 16:56:31 -08:00
|
|
|
updateProgress(lastDownloadedHeight = downloader.getLastDownloadedHeight())
|
2019-06-19 14:52:15 -07:00
|
|
|
downloadedBlockHeight = end
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
}
|
2019-06-19 14:52:15 -07:00
|
|
|
Twig.clip("downloading")
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
2019-11-01 13:25:28 -07:00
|
|
|
_progress.send(100)
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
|
|
|
* Validate all blocks in the given range, ensuring that the blocks are in ascending order, with
|
|
|
|
* no gaps and are also chain-sequential. This means every block's prevHash value matches the
|
|
|
|
* preceding block in the chain.
|
2020-02-27 00:25:07 -08:00
|
|
|
*
|
|
|
|
* @param range the range of blocks to validate.
|
|
|
|
*
|
|
|
|
* @return -1 when there is not problem. Otherwise, return the lowest height where an error was
|
|
|
|
* found. In other words, validation starts at the back of the chain and works toward the tip.
|
2020-02-21 15:14:34 -08:00
|
|
|
*/
|
2019-06-14 16:24:52 -07:00
|
|
|
private fun validateNewBlocks(range: IntRange?): Int {
|
|
|
|
if (range?.isEmpty() != false) {
|
|
|
|
twig("no blocks to validate: $range")
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
Twig.sprout("validating")
|
2020-02-11 16:56:31 -08:00
|
|
|
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
|
2019-09-26 09:58:37 -07:00
|
|
|
val result = rustBackend.validateCombinedChain()
|
2019-06-14 16:24:52 -07:00
|
|
|
Twig.clip("validating")
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Scan all blocks in the given range, decrypting and persisting anything that matches our
|
|
|
|
* wallet.
|
|
|
|
*
|
|
|
|
* @param range the range of blocks to scan.
|
|
|
|
*
|
|
|
|
* @return -1 when there is not problem. Otherwise, return the lowest height where an error was
|
|
|
|
* found. In other words, scanning starts at the back of the chain and works toward the tip.
|
2020-02-21 15:14:34 -08:00
|
|
|
*/
|
2020-01-14 09:52:41 -08:00
|
|
|
private suspend fun scanNewBlocks(range: IntRange?): Boolean = withContext(IO) {
|
2019-06-14 16:24:52 -07:00
|
|
|
if (range?.isEmpty() != false) {
|
2020-01-14 09:52:41 -08:00
|
|
|
twig("no blocks to scan for range $range")
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
Twig.sprout("scanning")
|
|
|
|
twig("scanning blocks for range $range in batches")
|
|
|
|
var result = false
|
2020-02-21 15:14:34 -08:00
|
|
|
// Attempt to scan a few times to work around any concurrent modification errors, then
|
|
|
|
// rethrow as an official processorError which is handled by [start.retryWithBackoff]
|
|
|
|
retryUpTo(3, { CompactBlockProcessorException.FailedScan(it) }) { failedAttempts ->
|
2020-01-14 09:52:41 -08:00
|
|
|
if (failedAttempts > 0) twig("retrying the scan after $failedAttempts failure(s)...")
|
|
|
|
do {
|
|
|
|
var scannedNewBlocks = false
|
2020-01-15 04:10:22 -08:00
|
|
|
result = rustBackend.scanBlocks(SCAN_BATCH_SIZE)
|
2020-01-14 09:52:41 -08:00
|
|
|
val lastScannedHeight = getLastScannedHeight()
|
2020-02-21 15:14:34 -08:00
|
|
|
twig("batch scanned: $lastScannedHeight/${range.last}")
|
2020-01-14 09:52:41 -08:00
|
|
|
if (currentInfo.lastScannedHeight != lastScannedHeight) {
|
|
|
|
scannedNewBlocks = true
|
|
|
|
updateProgress(lastScannedHeight = lastScannedHeight)
|
|
|
|
}
|
|
|
|
// if we made progress toward our scan, then keep trying
|
|
|
|
} while(result && scannedNewBlocks && lastScannedHeight < range.last)
|
|
|
|
twig("batch scan complete!")
|
|
|
|
}
|
|
|
|
Twig.clip("scanning")
|
|
|
|
result
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
2020-01-14 09:52:41 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Emit an instance of processorInfo, corresponding to the provided data.
|
|
|
|
*
|
|
|
|
* @param networkBlockHeight the latest block available to lightwalletd that may or may not be
|
|
|
|
* downloaded by this wallet yet.
|
|
|
|
* @param lastScannedHeight the height up to which the wallet last scanned. This determines
|
|
|
|
* where the next scan will begin.
|
|
|
|
* @param lastDownloadedHeight the last compact block that was successfully downloaded.
|
2020-01-14 09:52:41 -08:00
|
|
|
* @param lastScanRange the inclusive range to scan. This represents what we most recently
|
|
|
|
* wanted to scan. In most cases, it will be an invalid range because we'd like to scan blocks
|
|
|
|
* that we don't yet have.
|
|
|
|
* @param lastDownloadRange the inclusive range to download. This represents what we most
|
|
|
|
* recently wanted to scan. In most cases, it will be an invalid range because we'd like to scan
|
|
|
|
* blocks that we don't yet have.
|
|
|
|
*/
|
|
|
|
private suspend fun updateProgress(
|
|
|
|
networkBlockHeight: Int = currentInfo.networkBlockHeight,
|
|
|
|
lastScannedHeight: Int = currentInfo.lastScannedHeight,
|
|
|
|
lastDownloadedHeight: Int = currentInfo.lastDownloadedHeight,
|
|
|
|
lastScanRange: IntRange = currentInfo.lastScanRange,
|
|
|
|
lastDownloadRange: IntRange = currentInfo.lastDownloadRange
|
|
|
|
): Unit = withContext(IO) {
|
|
|
|
currentInfo = currentInfo.copy(
|
|
|
|
networkBlockHeight = networkBlockHeight,
|
|
|
|
lastScannedHeight = lastScannedHeight,
|
|
|
|
lastDownloadedHeight = lastDownloadedHeight,
|
|
|
|
lastScanRange = lastScanRange,
|
|
|
|
lastDownloadRange = lastDownloadRange
|
|
|
|
)
|
|
|
|
_processorInfo.send(currentInfo)
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun handleChainError(errorHeight: Int) = withContext(IO) {
|
|
|
|
val lowerBound = determineLowerBound(errorHeight)
|
|
|
|
twig("handling chain error at $errorHeight by rewinding to block $lowerBound")
|
2020-02-21 15:14:34 -08:00
|
|
|
onChainErrorListener?.invoke(errorHeight, lowerBound)
|
2019-09-26 09:58:37 -07:00
|
|
|
rustBackend.rewindToHeight(lowerBound)
|
2019-11-01 13:25:28 -07:00
|
|
|
downloader.rewindToHeight(lowerBound)
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
private fun onProcessorError(throwable: Throwable): Boolean {
|
|
|
|
return onProcessorErrorListener?.invoke(throwable) ?: true
|
2019-06-17 02:01:29 -07:00
|
|
|
}
|
|
|
|
|
2019-06-14 16:24:52 -07:00
|
|
|
private fun determineLowerBound(errorHeight: Int): Int {
|
2019-09-26 09:58:37 -07:00
|
|
|
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
|
2020-01-15 04:10:22 -08:00
|
|
|
return Math.max(errorHeight - offset, lowerBoundHeight).also {
|
|
|
|
twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset")
|
|
|
|
twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it")
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
}
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Get the height of the last block that was downloaded by this processor.
|
|
|
|
*
|
|
|
|
* @return the last downloaded height reported by the downloader.
|
|
|
|
*/
|
2019-06-14 16:24:52 -07:00
|
|
|
suspend fun getLastDownloadedHeight() = withContext(IO) {
|
|
|
|
downloader.getLastDownloadedHeight()
|
|
|
|
}
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Get the height of the last block that was scanned by this processor.
|
|
|
|
*
|
|
|
|
* @return the last scanned height reported by the repository.
|
|
|
|
*/
|
2019-06-14 16:24:52 -07:00
|
|
|
suspend fun getLastScannedHeight() = withContext(IO) {
|
|
|
|
repository.lastScannedHeight()
|
|
|
|
}
|
2019-10-21 03:26:02 -07:00
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Get address corresponding to the given account for this wallet.
|
|
|
|
*
|
|
|
|
* @return the address of this wallet.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
suspend fun getAddress(accountId: Int) = withContext(IO) {
|
|
|
|
rustBackend.getAddress(accountId)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates the latest balance info. Defaults to the first account.
|
|
|
|
*
|
|
|
|
* @param accountIndex the account to check for balance info.
|
2020-02-27 00:25:07 -08:00
|
|
|
*
|
|
|
|
* @return an instance of WalletBalance containing information about available and total funds.
|
2019-11-01 13:25:28 -07:00
|
|
|
*/
|
|
|
|
suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance = withContext(IO) {
|
|
|
|
twigTask("checking balance info") {
|
|
|
|
try {
|
|
|
|
val balanceTotal = rustBackend.getBalance(accountIndex)
|
2020-02-21 15:14:34 -08:00
|
|
|
twig("found total balance: $balanceTotal")
|
2019-11-01 13:25:28 -07:00
|
|
|
val balanceAvailable = rustBackend.getVerifiedBalance(accountIndex)
|
2020-02-21 15:14:34 -08:00
|
|
|
twig("found available balance: $balanceAvailable")
|
2019-11-01 13:25:28 -07:00
|
|
|
WalletBalance(balanceTotal, balanceAvailable)
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
twig("failed to get balance due to $t")
|
|
|
|
throw RustLayerException.BalanceException(t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Transmits the given state for this processor.
|
|
|
|
*/
|
|
|
|
private suspend fun setState(newState: State) {
|
2019-10-21 03:26:02 -07:00
|
|
|
_state.send(newState)
|
|
|
|
}
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Sealed class representing the various states of this processor.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
sealed class State {
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* Marker interface for [State] instances that represent when the wallet is connected.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
interface Connected
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Marker interface for [State] instances that represent when the wallet is syncing.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
interface Syncing
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] for when the wallet is actively downloading compact blocks because the latest
|
|
|
|
* block height available from the server is greater than what we have locally. We move out
|
|
|
|
* of this state once our local height matches the server.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
object Downloading : Connected, Syncing, State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] for when the blocks that have been downloaded are actively being validated to
|
|
|
|
* ensure that there are no gaps and that every block is chain-sequential to the previous
|
|
|
|
* block, which determines whether a reorg has happened on our watch.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
object Validating : Connected, Syncing, State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] for when the blocks that have been downloaded are actively being decrypted.
|
|
|
|
*/
|
2019-11-01 13:25:28 -07:00
|
|
|
object Scanning : Connected, Syncing, State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] for when we are done decrypting blocks, for now.
|
|
|
|
*/
|
2019-11-22 23:18:20 -08:00
|
|
|
class Scanned(val scannedRange:IntRange) : Connected, Syncing, State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
2020-03-25 14:58:08 -07:00
|
|
|
/**
|
|
|
|
* [State] for when transaction details are being retrieved. This typically means the wallet
|
|
|
|
* has downloaded and scanned blocks and is now processing any transactions that were
|
|
|
|
* discovered. Once a transaction is discovered, followup network requests are needed in
|
|
|
|
* order to retrieve memos or outbound transaction information, like the recipient address.
|
|
|
|
* The existing information we have about transactions is enhanced by the new information.
|
|
|
|
*/
|
|
|
|
object Enhancing : Connected, Syncing, State()
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
/**
|
|
|
|
* [State] for when we have no connection to lightwalletd.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
object Disconnected : State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] for when [stop] has been called. For simplicity, processors should not be
|
|
|
|
* restarted but they are not prevented from this behavior.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
object Stopped : State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* [State] the initial state of the processor, once it is constructed.
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
object Initialized : State()
|
|
|
|
}
|
2019-11-01 13:25:28 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Data structure to hold the total and available balance of the wallet. This is what is
|
|
|
|
* received on the balance channel.
|
|
|
|
*
|
2020-01-14 09:52:41 -08:00
|
|
|
* @param totalZatoshi the total balance, ignoring funds that cannot be used.
|
|
|
|
* @param availableZatoshi the amount of funds that are available for use. Typical reasons that funds
|
2019-11-01 13:25:28 -07:00
|
|
|
* 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(
|
2020-01-14 09:52:41 -08:00
|
|
|
val totalZatoshi: Long = -1,
|
|
|
|
val availableZatoshi: Long = -1
|
2019-11-01 13:25:28 -07:00
|
|
|
)
|
2020-01-14 09:52:41 -08:00
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Data class for holding detailed information about the processor.
|
|
|
|
*
|
|
|
|
* @param networkBlockHeight the latest block available to lightwalletd that may or may not be
|
|
|
|
* downloaded by this wallet yet.
|
|
|
|
* @param lastScannedHeight the height up to which the wallet last scanned. This determines
|
|
|
|
* where the next scan will begin.
|
|
|
|
* @param lastDownloadedHeight the last compact block that was successfully downloaded.
|
|
|
|
*
|
2020-01-14 09:52:41 -08:00
|
|
|
* @param lastDownloadRange inclusive range to download. Meaning, if the range is 10..10,
|
|
|
|
* then we will download exactly block 10. If the range is 11..10, then we want to download
|
|
|
|
* block 11 but can't.
|
|
|
|
* @param lastScanRange inclusive range to scan.
|
|
|
|
*/
|
|
|
|
data class ProcessorInfo(
|
|
|
|
val networkBlockHeight: Int = -1,
|
|
|
|
val lastScannedHeight: Int = -1,
|
|
|
|
val lastDownloadedHeight: Int = -1,
|
|
|
|
val lastDownloadRange: IntRange = 0..-1, // empty range
|
|
|
|
val lastScanRange: IntRange = 0..-1 // empty range
|
|
|
|
) {
|
2020-02-25 23:43:27 -08:00
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Determines whether this instance has data.
|
|
|
|
*
|
|
|
|
* @return false when all values match their defaults.
|
2020-02-25 23:43:27 -08:00
|
|
|
*/
|
2020-01-14 09:52:41 -08:00
|
|
|
val hasData get() = networkBlockHeight != -1
|
|
|
|
|| lastScannedHeight != -1
|
|
|
|
|| lastDownloadedHeight != -1
|
|
|
|
|| lastDownloadRange != 0..-1
|
|
|
|
|| lastScanRange != 0..-1
|
2020-02-25 23:43:27 -08:00
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Determines whether this instance is actively downloading compact blocks.
|
|
|
|
*
|
|
|
|
* @return true when there are more than zero blocks remaining to download.
|
2020-02-25 23:43:27 -08:00
|
|
|
*/
|
|
|
|
val isDownloading: Boolean get() = !lastDownloadRange.isEmpty()
|
|
|
|
&& lastDownloadedHeight < lastDownloadRange.last
|
|
|
|
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
* Determines whether this instance is actively scanning or validating compact blocks.
|
|
|
|
*
|
|
|
|
* @return true when downloading has completed and there are more than zero blocks remaining
|
2020-02-25 23:43:27 -08:00
|
|
|
* to be scanned.
|
|
|
|
*/
|
|
|
|
val isScanning: Boolean get() = !isDownloading
|
|
|
|
&& !lastScanRange.isEmpty()
|
|
|
|
&& lastScannedHeight < lastScanRange.last
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The amount of scan progress from 0 to 100.
|
|
|
|
*/
|
|
|
|
val scanProgress get() = when {
|
|
|
|
lastScannedHeight <= -1 -> 0
|
|
|
|
lastScanRange.isEmpty() -> 100
|
|
|
|
lastScannedHeight >= lastScanRange.last -> 100
|
|
|
|
else -> {
|
|
|
|
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
|
|
|
|
val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0)
|
|
|
|
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
|
|
|
|
val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1
|
|
|
|
// take the percentage then convert and round
|
|
|
|
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
|
|
|
|
percent.coerceAtMost(100.0f).roundToInt()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-14 09:52:41 -08:00
|
|
|
}
|
2019-09-26 09:58:37 -07:00
|
|
|
}
|