2020-06-10 00:08:19 -07:00
package cash.z.ecc.android.sdk.block
2019-06-14 16:24:52 -07:00
import androidx.annotation.VisibleForTesting
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.BuildConfig
import cash.z.ecc.android.sdk.annotation.OpenForTesting
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Disconnected
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Downloading
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Enhancing
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Initialized
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError
2021-03-31 06:07:37 -07:00
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedBranch
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork
2021-04-26 14:37:58 -07:00
import cash.z.ecc.android.sdk.exception.InitializerException
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.exception.RustLayerException
2021-03-31 05:51:53 -07:00
import cash.z.ecc.android.sdk.ext.BatchMetrics
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.ext.Twig
2021-03-31 06:07:37 -07:00
import cash.z.ecc.android.sdk.ext.ZcashSdk
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.ext.ZcashSdk.DOWNLOAD_BATCH_SIZE
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_REORG_SIZE
import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL
import cash.z.ecc.android.sdk.ext.ZcashSdk.RETRIES
import cash.z.ecc.android.sdk.ext.ZcashSdk.REWIND_DISTANCE
import cash.z.ecc.android.sdk.ext.ZcashSdk.SCAN_BATCH_SIZE
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.ext.retryUpTo
import cash.z.ecc.android.sdk.ext.retryWithBackoff
import cash.z.ecc.android.sdk.ext.toHexReversed
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.ext.twigTask
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.transaction.TransactionRepository
2021-03-31 23:14:57 -07:00
import cash.z.ecc.android.sdk.type.WalletBalance
2021-02-17 13:07:57 -08:00
import cash.z.wallet.sdk.rpc.Service
2020-03-27 13:28:42 -07:00
import io.grpc.StatusRuntimeException
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
2021-03-31 06:07:37 -07:00
import kotlinx.coroutines.flow.MutableStateFlow
2019-10-21 03:26:02 -07:00
import kotlinx.coroutines.flow.asFlow
2021-05-25 08:15:09 -07:00
import kotlinx.coroutines.flow.asStateFlow
2021-03-31 06:07:37 -07:00
import kotlinx.coroutines.flow.first
2019-06-14 16:24:52 -07:00
import kotlinx.coroutines.isActive
2021-04-09 18:25:21 -07:00
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
2019-06-14 16:24:52 -07:00
import kotlinx.coroutines.withContext
2021-04-09 18:19:33 -07:00
import java.util.Locale
2019-06-14 16:24:52 -07:00
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 ,
2021-04-09 18:20:09 -07:00
minimumHeight : Int = rustBackend . network . saplingActivationHeight
2019-06-14 16:24:52 -07:00
) {
2020-02-27 00:25:07 -08:00
/ * *
2020-06-09 19:00:41 -07:00
* Callback for any non - trivial errors that occur while processing compact blocks .
*
* @return true when processing should continue . Return false when the error is unrecoverable
* and all processing should halt and stop retrying .
2020-02-27 00:25:07 -08:00
* /
2020-02-21 15:14:34 -08:00
var onProcessorErrorListener : ( ( Throwable ) -> Boolean ) ? = null
2020-02-27 00:25:07 -08:00
/ * *
2020-10-30 06:20:32 -07:00
* Callback for reorgs . This callback is invoked when validation fails with the height at which
2020-02-27 00:25:07 -08:00
* an error was found and the lower bound to which the data will rewind , at most .
* /
2020-06-09 19:00:41 -07:00
var onChainErrorListener : ( ( errorHeight : Int , rewindHeight : Int ) -> Any ) ? = null
2019-10-21 03:26:02 -07:00
2021-03-31 06:07:37 -07:00
/ * *
* Callback for setup errors that occur prior to processing compact blocks . Can be used to
* override any errors from [ verifySetup ] . When this listener is missing then all setup errors
* will result in the processor not starting . This is particularly useful for wallets to receive
* a callback right before the SDK will reject a lightwalletd server because it appears not to
* match .
*
* @return true when the setup error should be ignored and processing should be allowed to
* start . Otherwise , processing will not begin .
* /
var onSetupErrorListener : ( ( Throwable ) -> Boolean ) ? = null
2021-03-31 05:51:53 -07:00
/ * *
* Callback for apps to report scan times . As blocks are scanned in batches , this listener is
* invoked at the end of every batch and the second parameter is only true when all batches are
* complete . The first parameter contains useful information on the blocks scanned per second .
2021-04-09 18:19:33 -07:00
*
* The Boolean param ( isComplete ) is true when this event represents the completion of a scan
2021-03-31 05:51:53 -07:00
* /
var onScanMetricCompleteListener : ( ( BatchMetrics , Boolean ) -> Unit ) ? = null
2019-06-17 02:01:29 -07:00
private val consecutiveChainErrors = AtomicInteger ( 0 )
2021-04-09 18:20:09 -07:00
private val lowerBoundHeight : Int = max ( rustBackend . network . saplingActivationHeight , minimumHeight - MAX _REORG _SIZE )
2019-10-21 03:26:02 -07:00
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 ( ) )
2021-05-25 08:15:09 -07:00
private val _networkHeight = MutableStateFlow ( - 1 )
2021-04-09 18:25:21 -07:00
private val processingMutex = Mutex ( )
2020-01-14 09:52:41 -08:00
2021-03-31 05:47:04 -07:00
/ * *
* Flow of birthday heights . The birthday is essentially the first block that the wallet cares
* about . Any prior block can be ignored . This is not a fixed value because the height is
* influenced by the first transaction , which isn ' t always known . So we start with an estimation
* and improve it as the wallet progresses . Once the first transaction occurs , this value is
* effectively fixed .
* /
private val _birthdayHeight = MutableStateFlow ( lowerBoundHeight )
2020-01-14 09:52:41 -08:00
/ * *
* 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 .
* /
2020-06-09 19:00:41 -07:00
internal var currentInfo = ProcessorInfo ( )
2019-06-14 16:24:52 -07:00
2021-04-09 18:20:09 -07:00
/ * *
* The zcash network that is being processed . Either Testnet or Mainnet .
* /
val network = rustBackend . network
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
2021-05-25 08:15:09 -07:00
val networkHeight = _networkHeight . asStateFlow ( )
2021-03-31 05:47:04 -07:00
/ * *
* The first block this wallet cares about anything prior can be ignored . If a wallet has no
* transactions , this value will later update to 100 blocks before the first transaction ,
* rounded down to the nearest 100. So in some cases , this is a dynamic value .
* /
val birthdayHeight = _birthdayHeight . value
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 ) {
2021-03-31 06:07:37 -07:00
verifySetup ( )
2021-03-31 05:47:04 -07:00
updateBirthdayHeight ( )
2021-03-31 06:07:37 -07:00
twig ( " setup verified. processor starting " )
2019-06-14 16:24:52 -07:00
// 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 ) {
2021-04-09 18:25:21 -07:00
val result = processingMutex . withLockLogged ( " processNewBlocks " ) {
processNewBlocks ( )
}
2019-06-14 16:24:52 -07:00
// immediately process again after failures in order to download new blocks right away
2020-06-09 18:43:23 -07:00
if ( result == ERROR _CODE _RECONNECT ) {
val napTime = calculatePollInterval ( true )
twig ( " Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime} ms " )
delay ( napTime )
} else if ( result == ERROR _CODE _NONE || result == ERROR _CODE _FAILED _ENHANCE ) {
consecutiveChainErrors . set ( 0 )
val napTime = calculatePollInterval ( )
twig ( " Successfully processed new blocks ${if (result == ERROR_CODE_FAILED_ENHANCE) " (but there were enhancement errors! We ignore those, for now. Memos in this block range are probably missing! This will be improved in a future release.)" else ""} . Sleeping for ${napTime} ms " )
delay ( napTime )
2019-06-14 16:24:52 -07:00
} else {
2021-03-10 10:10:03 -08: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-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
2020-03-27 13:28:42 -07:00
if ( ! updateRanges ( ) ) {
twig ( " Disconnection detected! Attempting to reconnect! " )
setState ( Disconnected )
2020-09-23 08:11:45 -07:00
downloader . lightWalletService . reconnect ( )
2020-06-09 19:00:41 -07:00
ERROR _CODE _RECONNECT
2020-03-27 13:28:42 -07:00
} else if ( currentInfo . lastDownloadRange . isEmpty ( ) && currentInfo . lastScanRange . isEmpty ( ) ) {
2021-03-31 06:16:06 -07:00
twig ( " Nothing to process: no new blocks to download or scan, right now (latest: ${currentInfo.networkBlockHeight} ). " )
2020-02-21 15:14:34 -08:00
setState ( Scanned ( currentInfo . lastScanRange ) )
2020-06-09 19:00:41 -07:00
ERROR _CODE _NONE
2020-02-21 15:14:34 -08:00
} else {
downloadNewBlocks ( currentInfo . lastDownloadRange )
2020-06-09 19:00:41 -07:00
val error = validateAndScanNewBlocks ( currentInfo . lastScanRange )
2021-03-31 06:16:06 -07:00
if ( error != ERROR _CODE _NONE ) error else {
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 .
2020-03-27 13:28:42 -07:00
*
* @return true when the update succeeds .
2020-02-21 15:14:34 -08:00
* /
2021-03-10 10:10:03 -08:00
private suspend fun updateRanges ( ) : Boolean = withContext ( IO ) {
2020-03-27 13:28:42 -07:00
try {
// 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?
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 ,
2021-03-10 10:10:03 -08:00
lastDownloadRange = (
max (
initialInfo . lastDownloadedHeight ,
initialInfo . lastScannedHeight
) + 1
) .. initialInfo . networkBlockHeight
2020-03-27 13:28:42 -07:00
)
}
true
} catch ( t : StatusRuntimeException ) {
twig ( " Warning: failed to update ranges due to $t caused by ${t.cause} " )
false
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-06-09 19:00:41 -07:00
* @return error code or [ ERROR _CODE _NONE ] when there is no error .
2020-02-21 15:14:34 -08:00
* /
private suspend fun validateAndScanNewBlocks ( lastScanRange : IntRange ) : Int = withContext ( IO ) {
setState ( Validating )
var error = validateNewBlocks ( lastScanRange )
2020-06-09 19:00:41 -07:00
if ( error == ERROR _CODE _NONE ) {
2020-02-21 15:14:34 -08:00
// 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 ) )
}
2020-06-09 19:00:41 -07:00
ERROR _CODE _NONE
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 )
2020-07-27 11:39:43 -07:00
if ( newTxs == null ) {
twig ( " no new transactions found in $lastScanRange " )
} else {
twig ( " enhancing ${newTxs.size} transaction(s)! " )
2021-03-31 05:47:04 -07:00
// if the first transaction has been added
if ( newTxs . size == repository . count ( ) ) {
twig ( " Encountered the first transaction. This changes the birthday height! " )
updateBirthdayHeight ( )
}
2020-07-27 11:39:43 -07:00
}
2020-03-25 14:58:08 -07:00
newTxs ?. onEach { newTransaction ->
if ( newTransaction == null ) twig ( " somehow, new transaction was null!!! " )
else enhance ( newTransaction )
}
twig ( " Done enhancing transaction details " )
2020-06-09 19:00:41 -07:00
ERROR _CODE _NONE
2020-03-25 14:58:08 -07:00
} catch ( t : Throwable ) {
twig ( " Failed to enhance due to $t " )
t . printStackTrace ( )
2020-06-09 19:00:41 -07:00
ERROR _CODE _FAILED _ENHANCE
2020-03-25 14:58:08 -07:00
} finally {
Twig . clip ( " enhancing " )
}
}
2020-06-09 19:00:41 -07:00
// TODO: we still need a way to identify those transactions that failed to be enhanced
2020-03-25 14:58:08 -07:00
private suspend fun enhance ( transaction : ConfirmedTransaction ) = withContext ( Dispatchers . IO ) {
2020-06-09 19:00:41 -07:00
var downloaded = false
try {
twig ( " START: enhancing transaction (id: ${transaction.id} block: ${transaction.minedHeight} ) " )
downloader . fetchTransaction ( transaction . rawTransactionId ) ?. let { tx ->
downloaded = true
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} ) " )
} catch ( t : Throwable ) {
twig ( " Warning: failure on transaction: error: $t \t transaction: $transaction " )
onProcessorError (
if ( downloaded ) EnhanceTxDecryptError ( transaction . minedHeight , t )
else EnhanceTxDownloadError ( transaction . minedHeight , t )
)
}
2020-03-25 14:58:08 -07:00
}
2020-02-21 15:14:34 -08:00
/ * *
* Confirm that the wallet data is properly setup for use .
* /
2021-03-31 06:07:37 -07:00
private suspend fun verifySetup ( ) {
// verify that the data is initialized
2021-05-05 11:26:13 -07:00
var error = when {
! repository . isInitialized ( ) -> CompactBlockProcessorException . Uninitialized
repository . getAccountCount ( ) == 0 -> CompactBlockProcessorException . NoAccount
else -> {
// verify that the server is correct
downloader . getServerInfo ( ) . let { info ->
val clientBranch = " %x " . format ( rustBackend . getBranchIdForHeight ( info . blockHeight . toInt ( ) ) )
val network = rustBackend . network . networkName
when {
!in fo . matchingNetwork ( network ) -> MismatchedNetwork ( clientNetwork = network , serverNetwork = info . chainName )
!in fo . matchingConsensusBranchId ( clientBranch ) -> MismatchedBranch ( clientBranch = clientBranch , serverBranch = info . consensusBranchId , networkName = network )
else -> null
}
2021-03-31 06:07:37 -07:00
}
}
}
if ( error != null ) {
2021-05-05 11:26:13 -07:00
twig ( " Validating setup prior to scanning . . . ISSUE FOUND! - ${error.javaClass.simpleName} " )
2021-03-31 06:07:37 -07:00
// give listener a chance to override
if ( onSetupErrorListener ?. invoke ( error ) != true ) {
throw error
} else {
twig ( " Warning: An ${error::class.java.simpleName} was encountered while verifying setup but it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message} " )
}
}
2020-02-11 17:00:35 -08:00
}
2021-03-31 05:47:04 -07:00
private suspend fun updateBirthdayHeight ( ) {
try {
val betterBirthday = calculateBirthdayHeight ( )
if ( betterBirthday > birthdayHeight ) {
twig ( " Better birthday found! Birthday height updated from $birthdayHeight to $betterBirthday " )
_birthdayHeight . value = betterBirthday
}
} catch ( e : Throwable ) {
twig ( " Warning: updating the birthday height failed due to $e " )
}
}
2021-04-05 15:37:13 -07:00
internal suspend fun refreshUtxos ( tAddress : String , startHeight : Int ) : Int = withContext ( IO ) {
2021-02-17 13:07:57 -08:00
var skipped = 0
2021-04-05 15:37:13 -07:00
// todo test what happens when this call fails
2021-02-17 13:07:57 -08:00
downloader . lightWalletService . fetchUtxos ( tAddress , startHeight ) . let { result ->
2021-04-05 15:37:13 -07:00
twig ( " Clearing utxos above height ${startHeight - 1} " )
rustBackend . clearUtxos ( tAddress , startHeight - 1 )
twig ( " Downloading utxos starting at height $startHeight " )
2021-02-17 13:07:57 -08:00
result . forEach { utxo : Service . GetAddressUtxosReply ->
twig ( " Found UTXO at height ${utxo.height.toInt()} " )
try {
rustBackend . putUtxo (
tAddress ,
utxo . txid . toByteArray ( ) ,
utxo . index ,
utxo . script . toByteArray ( ) ,
utxo . valueZat ,
utxo . height . toInt ( )
)
} catch ( t : Throwable ) {
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)
skipped ++
twig ( " Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists " )
}
}
// return the number of UTXOs that were downloaded
result . size - skipped
}
}
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
* /
2021-03-10 10:10:03 -08: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
2021-03-10 10:10:03 -08: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
2021-03-10 10:10:03 -08: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 )
2021-04-09 18:20:09 -07:00
val lastDownloadedHeight = downloader . getLastDownloadedHeight ( ) . takeUnless { it < network . saplingActivationHeight } ?: - 1
updateProgress ( lastDownloadedHeight = lastDownloadedHeight )
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 .
*
2020-06-09 19:00:41 -07:00
* @return [ ERROR _CODE _NONE ] when there is no problem . Otherwise , return the lowest height where an error was
2020-02-27 00:25:07 -08:00
* 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 " )
2020-06-09 19:00:41 -07:00
return ERROR _CODE _NONE
2019-06-14 16:24:52 -07:00
}
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 .
*
2020-06-09 19:00:41 -07:00
* @return [ ERROR _CODE _NONE ] when there is no problem . Otherwise , return the lowest height where an error was
2020-02-27 00:25:07 -08:00
* 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
2021-03-31 05:51:53 -07:00
var metrics = BatchMetrics ( range , SCAN _BATCH _SIZE , onScanMetricCompleteListener )
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
2021-03-31 05:51:53 -07:00
metrics . beginBatch ( )
2020-01-15 04:10:22 -08:00
result = rustBackend . scanBlocks ( SCAN _BATCH _SIZE )
2021-03-31 05:51:53 -07:00
metrics . endBatch ( )
val lastScannedHeight = range . start + metrics . cumulativeItems - 1
2021-04-09 18:19:33 -07:00
val percentValue = ( lastScannedHeight - range . first ) / ( range . last - range . first + 1 ) . toFloat ( ) * 100.0f
val percent = " %.0f " . format ( percentValue . coerceAtMost ( 100f ) . coerceAtLeast ( 0f ) )
2021-03-31 05:51:53 -07:00
twig ( " batch scanned ( $percent %): $lastScannedHeight / ${range.last} | ${metrics.batchTime} ms, ${metrics.batchItems} blks, ${metrics.batchIps.format()} bps " )
2020-01-14 09:52:41 -08:00
if ( currentInfo . lastScannedHeight != lastScannedHeight ) {
scannedNewBlocks = true
updateProgress ( lastScannedHeight = lastScannedHeight )
}
2021-03-10 10:10:03 -08:00
// if we made progress toward our scan, then keep trying
} while ( result && scannedNewBlocks && lastScannedHeight < range . last )
2021-03-31 05:51:53 -07:00
twig ( " batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()} " )
2020-01-14 09:52:41 -08:00
}
Twig . clip ( " scanning " )
result
2019-06-14 16:24:52 -07:00
}
2020-01-14 09:52:41 -08:00
}
2021-03-31 05:51:53 -07:00
private fun Float . format ( places : Int = 0 ) = " %. ${places} f " . format ( this )
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
)
2021-05-25 08:15:09 -07:00
_networkHeight . value = networkBlockHeight
2020-01-14 09:52:41 -08:00
_processorInfo . send ( currentInfo )
2019-06-14 16:24:52 -07:00
}
2021-03-31 06:16:06 -07:00
private suspend fun handleChainError ( errorHeight : Int ) {
2020-06-09 19:00:41 -07:00
// TODO consider an error object containing hash information
printValidationErrorInfo ( errorHeight )
determineLowerBound ( errorHeight ) . let { lowerBound ->
twig ( " handling chain error at $errorHeight by rewinding to block $lowerBound " )
onChainErrorListener ?. invoke ( errorHeight , lowerBound )
2021-04-14 15:44:17 -07:00
rewindToNearestHeight ( lowerBound , true )
}
}
suspend fun getNearestRewindHeight ( height : Int ) : Int {
2021-04-29 00:34:35 -07:00
// TODO: add a concept of original checkpoint height to the processor. For now, derive it
val originalCheckpoint = lowerBoundHeight + MAX _REORG _SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
return if ( height < originalCheckpoint ) {
originalCheckpoint
2021-04-14 15:44:17 -07:00
} else {
2021-04-22 15:47:58 -07:00
// tricky: subtract one because we delete ABOVE this block
rustBackend . getNearestRewindHeight ( height ) - 1
2021-03-31 06:16:06 -07:00
}
}
2021-05-03 19:53:23 -07:00
/ * *
* Rewind back at least two weeks worth of blocks .
* /
suspend fun quickRewind ( ) = withContext ( IO ) {
val height = max ( currentInfo . lastScannedHeight , repository . lastScannedHeight ( ) )
val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk . BLOCK_INTERVAL_MILLIS . toInt ( )
val twoWeeksBack = ( height - blocksPerDay * 14 ) . coerceAtLeast ( lowerBoundHeight )
rewindToNearestHeight ( twoWeeksBack , false )
}
2021-03-31 06:16:06 -07:00
/ * *
* @param alsoClearBlockCache when true , also clear the block cache which forces a redownload of
* blocks . Otherwise , the cached blocks will be used in the rescan , which in most cases , is fine .
* /
2021-04-14 15:44:17 -07:00
suspend fun rewindToNearestHeight ( height : Int , alsoClearBlockCache : Boolean = false ) = withContext ( IO ) {
2021-04-09 18:25:21 -07:00
processingMutex . withLockLogged ( " rewindToHeight " ) {
val lastScannedHeight = currentInfo . lastScannedHeight
2021-05-03 19:53:23 -07:00
val lastLocalBlock = repository . lastScannedHeight ( )
2021-04-14 15:44:17 -07:00
val targetHeight = getNearestRewindHeight ( height )
2021-05-03 19:53:23 -07:00
twig ( " Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock " )
if ( targetHeight < lastScannedHeight || ( lastScannedHeight == - 1 && ( targetHeight < lastLocalBlock ) ) ) {
2021-04-09 18:25:21 -07:00
rustBackend . rewindToHeight ( targetHeight )
} else {
2021-05-03 19:53:23 -07:00
twig ( " not rewinding dataDb because the last scanned height is $lastScannedHeight and the last local block is $lastLocalBlock both of which are less than the target height of $targetHeight " )
2021-04-09 18:25:21 -07:00
}
2021-04-05 15:37:13 -07:00
2021-04-09 18:25:21 -07:00
if ( alsoClearBlockCache ) {
twig ( " Also clearing block cache back to $targetHeight . These rewound blocks will download in the next scheduled scan " )
downloader . rewindToHeight ( targetHeight )
// communicate that the wallet is no longer synced because it might remain this way for 20+ seconds because we only download on 20s time boundaries so we can't trigger any immediate action
setState ( Downloading )
updateProgress (
lastScannedHeight = targetHeight ,
lastDownloadedHeight = targetHeight ,
lastScanRange = ( targetHeight + 1 ) .. currentInfo . networkBlockHeight ,
lastDownloadRange = ( targetHeight + 1 ) .. currentInfo . networkBlockHeight
)
_progress . send ( 0 )
} else {
updateProgress (
lastScannedHeight = targetHeight ,
lastScanRange = ( targetHeight + 1 ) .. currentInfo . networkBlockHeight ,
)
_progress . send ( 0 )
val range = ( targetHeight + 1 ) .. lastScannedHeight
twig ( " We kept the cache blocks in place so we don't need to wait for the next scheduled download to rescan. Instead we will rescan and validate blocks ${range.first} .. ${range.last} " )
if ( validateAndScanNewBlocks ( range ) == ERROR _CODE _NONE ) enhanceTransactionDetails ( range )
}
2020-06-09 19:00:41 -07:00
}
2019-06-14 16:24:52 -07:00
}
2020-06-09 19:00:41 -07:00
/** insightful function for debugging these critical errors */
private suspend fun printValidationErrorInfo ( errorHeight : Int , count : Int = 11 ) {
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something
if ( ! BuildConfig . DEBUG ) return
var errorInfo = fetchValidationErrorInfo ( errorHeight )
twig ( " validation failed at block ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash} " )
errorInfo = fetchValidationErrorInfo ( errorHeight + 1 )
twig ( " The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash} " )
twig ( " =================== BLOCKS [ $errorHeight .. ${errorHeight + count - 1} ]: START ======== " )
repeat ( count ) { i ->
val height = errorHeight + i
val block = downloader . compactBlockStore . findCompactBlock ( height )
// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get the hash another way but prevHash is correctly null.
val hash = block ?. hash ?. toByteArray ( ) ?: ( repository as PagedTransactionRepository ) . findBlockHash ( height )
twig ( " block: $height \t hash= ${hash?.toHexReversed()} \t prevHash= ${block?.prevHash?.toByteArray()?.toHexReversed()} " )
}
twig ( " =================== BLOCKS [ $errorHeight .. ${errorHeight + count - 1} ]: END ======== " )
}
private suspend fun fetchValidationErrorInfo ( errorHeight : Int ) : ValidationErrorInfo {
val hash = ( repository as PagedTransactionRepository ) . findBlockHash ( errorHeight + 1 ) ?. toHexReversed ( )
val prevHash = repository . findBlockHash ( errorHeight ) ?. toHexReversed ( )
val compactBlock = downloader . compactBlockStore . findCompactBlock ( errorHeight + 1 )
val expectedPrevHash = compactBlock ?. prevHash ?. toByteArray ( ) ?. toHexReversed ( )
return ValidationErrorInfo ( errorHeight , hash , expectedPrevHash , prevHash )
}
/ * *
* Called for every noteworthy error .
*
* @return true when processing should continue . Return false when the error is unrecoverable
* and all processing should halt and stop retrying .
* /
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-06-09 18:43:23 -07:00
/ * *
2021-03-10 10:10:03 -08:00
 * Poll on time boundaries . Per Issue # 95 , we want to avoid exposing computation time to a
2020-06-09 18:43:23 -07:00
* network observer . Instead , we poll at regular time intervals that are large enough for all
* computation to complete so no intervals are skipped . See 95 for more details .
*
* @param fastIntervalDesired currently not used but sometimes we want to poll quickly , such as
* when we unexpectedly lose server connection or are waiting for an event to happen on the
* chain . We can pass this desire along now and later figure out how to handle it , privately .
2021-03-10 10:10:03 -08:00
 * /
2020-06-09 18:43:23 -07:00
private fun calculatePollInterval ( fastIntervalDesired : Boolean = false ) : Long {
val interval = POLL _INTERVAL
val now = System . currentTimeMillis ( )
val deltaToNextInteral = interval - ( now + interval ) . rem ( interval )
2021-03-31 06:07:37 -07:00
// twig("sleeping for ${deltaToNextInteral}ms from $now in order to wake at ${now + deltaToNextInteral}")
2020-06-09 18:43:23 -07:00
return deltaToNextInteral
}
2021-03-31 05:47:04 -07:00
suspend fun calculateBirthdayHeight ( ) : Int {
var oldestTransactionHeight = 0
try {
oldestTransactionHeight = repository . receivedTransactions . first ( ) . last ( ) ?. minedHeight ?: lowerBoundHeight
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away
oldestTransactionHeight = ZcashSdk . MAX_REORG_SIZE . let { boundary ->
oldestTransactionHeight . let { it - it . rem ( boundary ) - boundary }
}
} catch ( t : Throwable ) {
twig ( " failed to calculate birthday due to: $t " )
}
2021-04-09 18:20:09 -07:00
return maxOf ( lowerBoundHeight , oldestTransactionHeight , rustBackend . network . saplingActivationHeight )
2021-03-31 05:47:04 -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 .
* /
2021-04-05 15:37:13 -07:00
suspend fun getShieldedAddress ( accountId : Int = 0 ) = withContext ( IO ) {
2021-04-26 14:37:58 -07:00
repository . getAccount ( accountId ) ?. rawShieldedAddress
?: throw InitializerException . MissingAddressException ( " shielded " )
2021-04-05 15:37:13 -07:00
}
suspend fun getTransparentAddress ( accountId : Int = 0 ) = withContext ( IO ) {
2021-04-26 14:37:58 -07:00
repository . getAccount ( accountId ) ?. rawTransparentAddress
?: throw InitializerException . MissingAddressException ( " transparent " )
2019-11-01 13:25:28 -07:00
}
/ * *
* 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 )
}
}
}
2021-02-17 13:07:57 -08:00
suspend fun getUtxoCacheBalance ( address : String ) : WalletBalance = withContext ( IO ) {
rustBackend . getDownloadedUtxoBalance ( address )
}
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 .
* /
2021-03-10 10:10:03 -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
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
2021-03-10 10:10:03 -08:00
val lastScanRange : IntRange = 0. . - 1 // empty range
2020-01-14 09:52:41 -08:00
) {
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
* /
2021-03-10 10:10:03 -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
* /
2021-03-10 10:10:03 -08:00
val isDownloading : Boolean get ( ) = ! lastDownloadRange . isEmpty ( ) &&
lastDownloadedHeight < lastDownloadRange . last
2020-02-25 23:43:27 -08:00
/ * *
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 .
* /
2021-03-10 10:10:03 -08:00
val isScanning : Boolean get ( ) = !is Downloading &&
! lastScanRange . isEmpty ( ) &&
lastScannedHeight < lastScanRange . last
2020-02-25 23:43:27 -08:00
/ * *
* 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-06-09 19:00:41 -07:00
}
data class ValidationErrorInfo (
val errorHeight : Int ,
2021-03-10 10:10:03 -08:00
val hash : String ? ,
2020-06-09 19:00:41 -07:00
val expectedPrevHash : String ? ,
val actualPrevHash : String ?
)
2021-03-31 06:07:37 -07:00
//
// Helper Extensions
//
private fun Service . LightdInfo . matchingConsensusBranchId ( clientBranch : String ) : Boolean {
return consensusBranchId . equals ( clientBranch , true )
}
private fun Service . LightdInfo . matchingNetwork ( network : String ) : Boolean {
2021-04-09 18:19:33 -07:00
fun String . toId ( ) = toLowerCase ( Locale . US ) . run {
2021-03-31 06:07:37 -07:00
when {
contains ( " main " ) -> " mainnet "
contains ( " test " ) -> " testnet "
else -> this
}
}
return chainName . toId ( ) == network . toId ( )
}
2021-04-09 18:25:21 -07:00
/ * *
* Log the mutex in great detail just in case we need it for troubleshooting deadlock .
* /
private suspend inline fun < T > Mutex . withLockLogged ( name : String , block : ( ) -> T ) : T {
twig ( " $name MUTEX: acquiring lock... " )
this . withLock {
twig ( " $name MUTEX: ...lock acquired! " )
return block ( ) . also {
twig ( " $name MUTEX: releasing lock " )
}
}
twig ( " $name MUTEX: withLock complete " )
}
2020-06-09 19:00:41 -07:00
companion object {
const val ERROR _CODE _NONE = - 1
const val ERROR _CODE _RECONNECT = 20
const val ERROR _CODE _FAILED _ENHANCE = 40
2020-01-14 09:52:41 -08:00
}
2020-03-26 04:00:04 -07:00
}