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-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-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
2021-10-04 04:18:37 -07:00
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
2022-07-12 05:40:09 -07:00
import cash.z.ecc.android.sdk.internal.isEmpty
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
2021-10-13 07:20:13 -07:00
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.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
2022-07-12 05:40:09 -07:00
import cash.z.ecc.android.sdk.model.BlockHeight
2022-07-07 05:52:07 -07:00
import cash.z.ecc.android.sdk.model.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
* /
2022-08-17 06:48:02 -07:00
@OptIn ( kotlinx . coroutines . ObsoleteCoroutinesApi :: class )
2019-06-14 16:24:52 -07:00
@OpenForTesting
2022-07-12 05:40:09 -07:00
class CompactBlockProcessor internal constructor (
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 ,
2022-07-12 05:40:09 -07:00
minimumHeight : BlockHeight = 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 .
* /
2022-07-12 05:40:09 -07:00
var onChainErrorListener : ( ( errorHeight : BlockHeight , rewindHeight : BlockHeight ) -> 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 )
2022-07-12 05:40:09 -07:00
private val lowerBoundHeight : BlockHeight = BlockHeight (
max (
rustBackend . network . saplingActivationHeight . value ,
minimumHeight . value - MAX _REORG _SIZE
)
)
2019-10-21 03:26:02 -07:00
2022-08-17 06:48:02 -07:00
// TODO [#288]: Remove Deprecated Usage of ConflatedBroadcastChannel
// TODO [#288]: https://github.com/zcash/zcash-android-wallet-sdk/issues/288
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 )
2022-07-12 05:40:09 -07:00
private val _processorInfo =
ConflatedBroadcastChannel ( ProcessorInfo ( null , null , null , null , null ) )
private val _networkHeight = MutableStateFlow < BlockHeight ? > ( null )
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 .
* /
2022-07-25 11:47:58 -07:00
// This accessed by the Dispatchers.IO thread, which means multiple threads are reading/writing
// concurrently.
@Volatile
2022-07-12 05:40:09 -07:00
internal var currentInfo = ProcessorInfo ( null , null , null , null , null )
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 .
* /
2022-08-17 06:48:02 -07:00
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress ( " DEPRECATION " )
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 .
* /
2022-08-17 06:48:02 -07:00
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress ( " DEPRECATION " )
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 .
* /
2022-08-17 06:48:02 -07:00
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress ( " DEPRECATION " )
2020-01-14 09:52:41 -08:00
val processorInfo = _processorInfo . asFlow ( )
2019-06-14 16:24:52 -07:00
2021-06-06 21:18:25 -07:00
/ * *
* The flow of network height . This value is updated at the same time that [ currentInfo ] is
* updated but this allows consumers to have the information pushed instead of polling .
* /
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
2022-07-12 05:40:09 -07:00
when ( result ) {
BlockProcessingResult . Reconnecting -> {
val napTime = calculatePollInterval ( true )
twig ( " Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime} ms " )
delay ( napTime )
}
BlockProcessingResult . NoBlocksToProcess , BlockProcessingResult . FailedEnhance -> {
val noWorkDone =
currentInfo . lastDownloadRange ?. isEmpty ( ) ?: true && currentInfo . lastScanRange ?. isEmpty ( ) ?: true
val summary = if ( noWorkDone ) {
" Nothing to process: no new blocks to download or scan "
} else {
" Done processing blocks "
}
consecutiveChainErrors . set ( 0 )
val napTime = calculatePollInterval ( )
twig ( " $summary ${if (result == BlockProcessingResult.FailedEnhance) " (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 (latest height: ${currentInfo.networkBlockHeight} ). " )
delay ( napTime )
}
is BlockProcessingResult . Error -> {
if ( consecutiveChainErrors . get ( ) >= RETRIES ) {
val errorMessage =
" ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts! "
fail ( CompactBlockProcessorException . FailedReorgRepair ( errorMessage ) )
} else {
handleChainError ( result . failedAtHeight )
}
consecutiveChainErrors . getAndIncrement ( )
}
is BlockProcessingResult . Success -> {
// Do nothing. We are done.
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
}
2022-07-12 05:40:09 -07:00
private suspend fun processNewBlocks ( ) : BlockProcessingResult = withContext ( IO ) {
2021-06-29 23:24:24 -07:00
twig ( " beginning to process new blocks (with lower bound: $lowerBoundHeight )... " , - 1 )
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 ( )
2022-07-12 05:40:09 -07:00
BlockProcessingResult . Reconnecting
2020-03-27 13:28:42 -07:00
} else if ( currentInfo . lastDownloadRange . isEmpty ( ) && currentInfo . lastScanRange . isEmpty ( ) ) {
2020-02-21 15:14:34 -08:00
setState ( Scanned ( currentInfo . lastScanRange ) )
2022-07-12 05:40:09 -07:00
BlockProcessingResult . NoBlocksToProcess
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 )
2022-07-12 05:40:09 -07:00
if ( error != BlockProcessingResult . Success ) {
error
} else {
currentInfo . lastScanRange ?. let { enhanceTransactionDetails ( it ) }
?: BlockProcessingResult . NoBlocksToProcess
2021-03-31 06:16:06 -07:00
}
2020-02-21 15:14:34 -08:00
}
}
2022-07-12 05:40:09 -07:00
sealed class BlockProcessingResult {
object NoBlocksToProcess : BlockProcessingResult ( )
object Success : BlockProcessingResult ( )
object Reconnecting : BlockProcessingResult ( )
object FailedEnhance : BlockProcessingResult ( )
data class Error ( val failedAtHeight : BlockHeight ) : BlockProcessingResult ( )
}
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 ( ) ,
2022-07-15 04:09:00 -07:00
lastDownloadedHeight = getLastDownloadedHeight ( ) ?. let {
BlockHeight . new (
network ,
max (
it . value ,
lowerBoundHeight . value - 1
)
2022-07-12 05:40:09 -07:00
)
2022-07-15 04:09:00 -07:00
} ,
2022-07-12 05:40:09 -07:00
lastDownloadRange = null ,
lastScanRange = null
2020-03-27 13:28:42 -07:00
) . let { initialInfo ->
updateProgress (
networkBlockHeight = initialInfo . networkBlockHeight ,
lastScannedHeight = initialInfo . lastScannedHeight ,
lastDownloadedHeight = initialInfo . lastDownloadedHeight ,
2022-07-12 05:40:09 -07:00
lastScanRange = if ( initialInfo . lastScannedHeight != null && initialInfo . networkBlockHeight != null ) {
initialInfo . lastScannedHeight + 1. . initialInfo . networkBlockHeight
} else {
null
} ,
2022-07-25 11:47:58 -07:00
lastDownloadRange = if ( initialInfo . networkBlockHeight != null ) {
2022-07-12 05:40:09 -07:00
BlockHeight . new (
network ,
2022-07-25 11:47:58 -07:00
buildList {
add ( network . saplingActivationHeight . value )
initialInfo . lastDownloadedHeight ?. let { add ( it . value + 1 ) }
initialInfo . lastScannedHeight ?. let { add ( it . value + 1 ) }
} . max ( )
2021-03-10 10:10:03 -08:00
) .. initialInfo . networkBlockHeight
2022-07-12 05:40:09 -07:00
} else {
null
}
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 .
2022-07-12 05:40:09 -07:00
* /
private suspend fun validateAndScanNewBlocks ( lastScanRange : ClosedRange < BlockHeight > ? ) : BlockProcessingResult =
withContext ( IO ) {
setState ( Validating )
val result = validateNewBlocks ( lastScanRange )
if ( result == BlockProcessingResult . Success ) {
// 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-02-21 15:14:34 -08:00
}
2022-07-12 05:40:09 -07:00
result
2020-01-14 09:52:41 -08:00
}
2019-11-22 23:18:20 -08:00
2022-07-12 05:40:09 -07:00
private suspend fun enhanceTransactionDetails ( lastScanRange : ClosedRange < BlockHeight > ) : BlockProcessingResult {
2020-03-25 14:58:08 -07:00
Twig . sprout ( " enhancing " )
twig ( " Enhancing transaction details for blocks $lastScanRange " )
setState ( Enhancing )
return try {
val newTxs = repository . findNewTransactions ( lastScanRange )
2022-07-12 05:40:09 -07:00
if ( newTxs . isEmpty ( ) ) {
2020-07-27 11:39:43 -07:00
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
}
2022-08-17 06:48:02 -07:00
newTxs . onEach { newTransaction ->
enhance ( newTransaction )
2020-03-25 14:58:08 -07:00
}
twig ( " Done enhancing transaction details " )
2022-07-12 05:40:09 -07:00
BlockProcessingResult . Success
2020-03-25 14:58:08 -07:00
} catch ( t : Throwable ) {
twig ( " Failed to enhance due to $t " )
t . printStackTrace ( )
2022-07-12 05:40:09 -07:00
BlockProcessingResult . FailedEnhance
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 (
2022-07-12 05:40:09 -07:00
if ( downloaded ) {
EnhanceTxDecryptError ( transaction . minedBlockHeight , t )
} else {
EnhanceTxDownloadError ( transaction . minedBlockHeight , t )
}
2020-06-09 19:00:41 -07:00
)
}
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 ->
2022-07-12 05:40:09 -07:00
val clientBranch =
" %x " . format ( rustBackend . getBranchIdForHeight ( BlockHeight ( info . blockHeight ) ) )
2021-05-05 11:26:13 -07:00
val network = rustBackend . network . networkName
when {
2022-07-12 05:40:09 -07:00
!in fo . matchingNetwork ( network ) -> MismatchedNetwork (
clientNetwork = network ,
serverNetwork = info . chainName
)
!in fo . matchingConsensusBranchId ( clientBranch ) -> MismatchedBranch (
clientBranch = clientBranch ,
serverBranch = info . consensusBranchId ,
networkName = network
)
2021-05-05 11:26:13 -07:00
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-06-29 23:25:40 -07:00
var failedUtxoFetches = 0
2022-07-12 05:40:09 -07:00
internal suspend fun refreshUtxos ( tAddress : String , startHeight : BlockHeight ) : Int ? =
withContext ( IO ) {
var count : Int ? = null
// todo: cleanup the way that we prevent this from running excessively
// For now, try for about 3 blocks per app launch. If the service fails it is
// probably disabled on ligthtwalletd, so then stop trying until the next app launch.
if ( failedUtxoFetches < 9 ) { // there are 3 attempts per block
try {
retryUpTo ( 3 ) {
val result = downloader . lightWalletService . fetchUtxos ( tAddress , startHeight )
count = processUtxoResult ( result , tAddress , startHeight )
}
} catch ( e : Throwable ) {
failedUtxoFetches ++
twig ( " Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session. " )
2021-02-17 13:07:57 -08:00
}
2022-07-12 05:40:09 -07:00
} else {
twig ( " Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd. " )
2021-06-29 23:25:40 -07:00
}
2022-07-12 05:40:09 -07:00
count
2021-06-29 23:25:40 -07:00
}
2022-07-12 05:40:09 -07:00
internal suspend fun processUtxoResult (
result : List < Service . GetAddressUtxosReply > ,
tAddress : String ,
startHeight : BlockHeight
) : Int = withContext ( IO ) {
2021-06-29 23:25:40 -07:00
var skipped = 0
2022-07-12 05:40:09 -07:00
val aboveHeight = startHeight
2021-06-29 23:25:40 -07:00
twig ( " Clearing utxos above height $aboveHeight " , - 1 )
rustBackend . clearUtxos ( tAddress , aboveHeight )
twig ( " Checking for UTXOs above height $aboveHeight " )
result . forEach { utxo : Service . GetAddressUtxosReply ->
twig ( " Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi " )
try {
rustBackend . putUtxo (
tAddress ,
utxo . txid . toByteArray ( ) ,
utxo . index ,
utxo . script . toByteArray ( ) ,
utxo . valueZat ,
2022-07-12 05:40:09 -07:00
BlockHeight ( utxo . height )
2021-06-29 23:25:40 -07:00
)
} 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 " )
2021-02-17 13:07:57 -08:00
}
}
2021-06-29 23:25:40 -07:00
// return the number of UTXOs that were downloaded
result . size - skipped
2021-02-17 13:07:57 -08:00
}
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
2022-07-12 05:40:09 -07:00
internal suspend fun downloadNewBlocks ( range : ClosedRange < BlockHeight > ? ) =
withContext < Unit > ( IO ) {
if ( null == range || range . isEmpty ( ) ) {
twig ( " no blocks to download " )
} else {
_state . send ( Downloading )
Twig . sprout ( " downloading " )
twig ( " downloading blocks in range $range " , - 1 )
var downloadedBlockHeight = range . start
val missingBlockCount = range . endInclusive . value - range . start . value + 1
val batches = (
missingBlockCount / DOWNLOAD _BATCH _SIZE +
( if ( missingBlockCount . rem ( DOWNLOAD _BATCH _SIZE ) == 0L ) 0 else 1 )
)
var progress : Int
twig ( " found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD _BATCH_SIZE... " )
for ( i in 1. . batches ) {
retryUpTo ( RETRIES , { CompactBlockProcessorException . FailedDownload ( it ) } ) {
2022-07-28 06:58:08 -07:00
val end = BlockHeight . new (
network ,
2022-07-12 05:40:09 -07:00
min (
( range . start . value + ( i * DOWNLOAD _BATCH _SIZE ) ) - 1 ,
range . endInclusive . value
)
) // 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 )
}
twig ( " downloaded $count blocks! " )
progress = ( i / batches . toFloat ( ) * 100 ) . roundToInt ( )
_progress . send ( progress )
val lastDownloadedHeight = downloader . getLastDownloadedHeight ( )
updateProgress ( lastDownloadedHeight = lastDownloadedHeight )
2022-07-28 06:58:08 -07:00
downloadedBlockHeight = end + 1
2019-06-19 14:52:15 -07:00
}
2019-06-14 16:24:52 -07:00
}
2022-07-12 05:40:09 -07:00
Twig . clip ( " downloading " )
2019-06-14 16:24:52 -07:00
}
2022-07-12 05:40:09 -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
2022-07-12 05:40:09 -07:00
* preceding block in the chain . Validation starts at the back of the chain and works toward the tip .
2020-02-27 00:25:07 -08:00
*
* @param range the range of blocks to validate .
2020-02-21 15:14:34 -08:00
* /
2022-07-12 05:40:09 -07:00
private suspend fun validateNewBlocks ( range : ClosedRange < BlockHeight > ? ) : BlockProcessingResult {
if ( null == range || range . isEmpty ( ) ) {
2019-06-14 16:24:52 -07:00
twig ( " no blocks to validate: $range " )
2022-07-12 05:40:09 -07:00
return BlockProcessingResult . NoBlocksToProcess
2019-06-14 16:24:52 -07:00
}
Twig . sprout ( " validating " )
2022-08-12 08:05:00 -07:00
twig ( " validating blocks in range $range in db: ${(rustBackend as RustBackend).cacheDbFile.absolutePath} " )
2019-09-26 09:58:37 -07:00
val result = rustBackend . validateCombinedChain ( )
2019-06-14 16:24:52 -07:00
Twig . clip ( " validating " )
2022-07-12 05:40:09 -07:00
return if ( null == result ) {
BlockProcessingResult . Success
} else {
BlockProcessingResult . Error ( result )
}
2019-06-14 16:24:52 -07:00
}
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
2022-07-12 05:40:09 -07:00
* wallet . Scanning starts at the back of the chain and works toward the tip .
2020-02-27 00:25:07 -08:00
*
* @param range the range of blocks to scan .
2020-02-21 15:14:34 -08:00
* /
2022-07-12 05:40:09 -07:00
private suspend fun scanNewBlocks ( range : ClosedRange < BlockHeight > ? ) : Boolean = withContext ( IO ) {
if ( null == range || range . isEmpty ( ) ) {
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 ( )
2022-07-25 11:47:58 -07:00
val lastScannedHeight =
BlockHeight . new ( network , range . start . value + metrics . cumulativeItems - 1 )
2022-07-12 05:40:09 -07:00
val percentValue =
( lastScannedHeight . value - range . start . value ) / ( range . endInclusive . value - range . start . value + 1 ) . toFloat ( ) * 100.0f
2021-04-09 18:19:33 -07:00
val percent = " %.0f " . format ( percentValue . coerceAtMost ( 100f ) . coerceAtLeast ( 0f ) )
2022-07-12 05:40:09 -07:00
twig ( " batch scanned ( $percent %): $lastScannedHeight / ${range.endInclusive} | ${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
2022-07-12 05:40:09 -07:00
} while ( result && scannedNewBlocks && lastScannedHeight < range . endInclusive )
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 (
2022-07-12 05:40:09 -07:00
networkBlockHeight : BlockHeight ? = currentInfo . networkBlockHeight ,
lastScannedHeight : BlockHeight ? = currentInfo . lastScannedHeight ,
lastDownloadedHeight : BlockHeight ? = currentInfo . lastDownloadedHeight ,
lastScanRange : ClosedRange < BlockHeight > ? = currentInfo . lastScanRange ,
lastDownloadRange : ClosedRange < BlockHeight > ? = currentInfo . lastDownloadRange
2022-07-25 11:47:58 -07:00
) {
2020-01-14 09:52:41 -08:00
currentInfo = currentInfo . copy (
networkBlockHeight = networkBlockHeight ,
lastScannedHeight = lastScannedHeight ,
lastDownloadedHeight = lastDownloadedHeight ,
lastScanRange = lastScanRange ,
lastDownloadRange = lastDownloadRange
)
2022-07-25 11:47:58 -07:00
withContext ( IO ) {
_networkHeight . value = networkBlockHeight
_processorInfo . send ( currentInfo )
}
2019-06-14 16:24:52 -07:00
}
2022-07-12 05:40:09 -07:00
private suspend fun handleChainError ( errorHeight : BlockHeight ) {
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 )
}
}
2022-07-12 05:40:09 -07:00
suspend fun getNearestRewindHeight ( height : BlockHeight ) : BlockHeight {
2021-04-29 00:34:35 -07:00
// TODO: add a concept of original checkpoint height to the processor. For now, derive it
2022-07-12 05:40:09 -07:00
val originalCheckpoint =
lowerBoundHeight + MAX _REORG _SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
2021-04-29 00:34:35 -07:00
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
2022-07-12 05:40:09 -07:00
// This could create an invalid height if if height was saplingActivationHeight
val rewindHeight = BlockHeight ( height . value - 1 )
rustBackend . getNearestRewindHeight ( rewindHeight )
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 .
* /
2022-01-19 10:39:07 -08:00
suspend fun quickRewind ( ) {
2021-05-03 19:53:23 -07:00
val height = max ( currentInfo . lastScannedHeight , repository . lastScannedHeight ( ) )
val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk . BLOCK_INTERVAL_MILLIS . toInt ( )
2022-07-12 05:40:09 -07:00
val twoWeeksBack = BlockHeight . new (
network ,
( height . value - blocksPerDay * 14 ) . coerceAtLeast ( lowerBoundHeight . value )
)
2021-05-03 19:53:23 -07:00
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 .
* /
2022-07-12 05:40:09 -07:00
suspend fun rewindToNearestHeight (
height : BlockHeight ,
alsoClearBlockCache : Boolean = false
) =
withContext ( IO ) {
processingMutex . withLockLogged ( " rewindToHeight " ) {
val lastScannedHeight = currentInfo . lastScannedHeight
val lastLocalBlock = repository . lastScannedHeight ( )
val targetHeight = getNearestRewindHeight ( height )
twig ( " Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock " )
if ( ( null == lastScannedHeight && targetHeight < lastLocalBlock ) || ( null != lastScannedHeight && targetHeight < lastScannedHeight ) ) {
rustBackend . rewindToHeight ( targetHeight )
} else {
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-05 15:37:13 -07:00
2022-07-12 05:40:09 -07:00
val currentNetworkBlockHeight = currentInfo . networkBlockHeight
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 )
if ( null == currentNetworkBlockHeight ) {
updateProgress (
lastScannedHeight = targetHeight ,
lastDownloadedHeight = targetHeight ,
lastScanRange = null ,
lastDownloadRange = null
)
} else {
updateProgress (
lastScannedHeight = targetHeight ,
lastDownloadedHeight = targetHeight ,
lastScanRange = ( targetHeight + 1 ) .. currentNetworkBlockHeight ,
lastDownloadRange = ( targetHeight + 1 ) .. currentNetworkBlockHeight
)
}
_progress . send ( 0 )
} else {
if ( null == currentNetworkBlockHeight ) {
updateProgress (
lastScannedHeight = targetHeight ,
lastScanRange = null
)
} else {
updateProgress (
lastScannedHeight = targetHeight ,
lastScanRange = ( targetHeight + 1 ) .. currentNetworkBlockHeight
)
}
_progress . send ( 0 )
if ( null != lastScannedHeight ) {
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.start} .. ${range.endInclusive} " )
if ( validateAndScanNewBlocks ( range ) == BlockProcessingResult . Success ) {
enhanceTransactionDetails ( range )
}
}
}
2021-04-09 18:25:21 -07:00
}
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 */
2022-07-12 05:40:09 -07:00
private suspend fun printValidationErrorInfo ( errorHeight : BlockHeight , count : Int = 11 ) {
2020-06-09 19:00:41 -07:00
// 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} " )
2022-07-12 05:40:09 -07:00
twig ( " =================== BLOCKS [ $errorHeight .. ${errorHeight.value + count - 1} ]: START ======== " )
2020-06-09 19:00:41 -07:00
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.
2022-07-12 05:40:09 -07:00
val hash = block ?. hash ?. toByteArray ( )
?: ( repository as PagedTransactionRepository ) . findBlockHash ( height )
twig (
" block: $height \t hash= ${hash?.toHexReversed()} \t prevHash= $ {
block ?. prevHash ?. toByteArray ( ) ?. toHexReversed ( )
} "
)
2020-06-09 19:00:41 -07:00
}
2022-07-12 05:40:09 -07:00
twig ( " =================== BLOCKS [ $errorHeight .. ${errorHeight.value + count - 1} ]: END ======== " )
2020-06-09 19:00:41 -07:00
}
2022-07-12 05:40:09 -07:00
private suspend fun fetchValidationErrorInfo ( errorHeight : BlockHeight ) : ValidationErrorInfo {
val hash = ( repository as PagedTransactionRepository ) . findBlockHash ( errorHeight + 1 )
?. toHexReversed ( )
2020-06-09 19:00:41 -07:00
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
}
2022-07-12 05:40:09 -07:00
private fun determineLowerBound ( errorHeight : BlockHeight ) : BlockHeight {
val offset = min ( MAX _REORG _SIZE , REWIND _DISTANCE * ( consecutiveChainErrors . get ( ) + 1 ) )
return BlockHeight ( max ( errorHeight . value - offset , lowerBoundHeight . value ) ) . also {
2020-01-15 04:10:22 -08:00
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
 * /
2022-08-17 06:48:02 -07:00
@Suppress ( " UNUSED_PARAMETER " )
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
}
2022-07-12 05:40:09 -07:00
suspend fun calculateBirthdayHeight ( ) : BlockHeight {
var oldestTransactionHeight : BlockHeight ? = null
2021-03-31 05:47:04 -07:00
try {
2022-07-12 05:40:09 -07:00
val tempOldestTransactionHeight = repository . receivedTransactions
. first ( )
. lastOrNull ( )
?. minedBlockHeight
?: lowerBoundHeight
2021-03-31 05:47:04 -07:00
// 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
2022-07-12 05:40:09 -07:00
oldestTransactionHeight = BlockHeight . new (
network ,
tempOldestTransactionHeight . value - tempOldestTransactionHeight . value . rem ( ZcashSdk . MAX _REORG _SIZE ) - ZcashSdk . MAX_REORG_SIZE . toLong ( )
)
2021-03-31 05:47:04 -07:00
} catch ( t : Throwable ) {
twig ( " failed to calculate birthday due to: $t " )
}
2022-07-12 05:40:09 -07:00
return buildList < BlockHeight > {
add ( lowerBoundHeight )
add ( rustBackend . network . saplingActivationHeight )
oldestTransactionHeight ?. let { add ( it ) }
} . maxOf { it }
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 .
* /
2022-01-19 10:39:07 -08:00
suspend fun getLastDownloadedHeight ( ) =
2019-06-14 16:24:52 -07:00
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 .
* /
2022-01-19 10:39:07 -08:00
suspend fun getLastScannedHeight ( ) =
2019-06-14 16:24:52 -07:00
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 .
* /
2022-01-19 10:39:07 -08:00
suspend fun getShieldedAddress ( accountId : Int = 0 ) =
2021-04-26 14:37:58 -07:00
repository . getAccount ( accountId ) ?. rawShieldedAddress
?: throw InitializerException . MissingAddressException ( " shielded " )
2021-04-05 15:37:13 -07:00
2022-01-19 10:39:07 -08:00
suspend fun getTransparentAddress ( accountId : Int = 0 ) =
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
* /
2022-01-19 10:39:07 -08:00
suspend fun getBalanceInfo ( accountIndex : Int = 0 ) : WalletBalance =
2021-06-29 23:24:24 -07:00
twigTask ( " checking balance info " , - 1 ) {
2019-11-01 13:25:28 -07:00
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 )
}
}
2022-07-12 05:40:09 -07:00
suspend fun getUtxoCacheBalance ( address : String ) : WalletBalance =
rustBackend . getDownloadedUtxoBalance ( address )
2021-02-17 13:07:57 -08:00
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 .
* /
2022-07-12 05:40:09 -07:00
class Scanned ( val scannedRange : ClosedRange < BlockHeight > ? ) : 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 (
2022-07-12 05:40:09 -07:00
val networkBlockHeight : BlockHeight ? ,
val lastScannedHeight : BlockHeight ? ,
val lastDownloadedHeight : BlockHeight ? ,
val lastDownloadRange : ClosedRange < BlockHeight > ? ,
val lastScanRange : ClosedRange < BlockHeight > ?
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
* /
2022-07-12 05:40:09 -07:00
val hasData
get ( ) = networkBlockHeight != null ||
lastScannedHeight != null ||
lastDownloadedHeight != null ||
lastDownloadRange != null ||
lastScanRange != null
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
* /
2022-07-12 05:40:09 -07:00
val isDownloading : Boolean
get ( ) =
lastDownloadedHeight != null &&
lastDownloadRange != null &&
! lastDownloadRange . isEmpty ( ) &&
lastDownloadedHeight < lastDownloadRange . endInclusive
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 .
* /
2022-07-12 05:40:09 -07:00
val isScanning : Boolean
get ( ) =
!is Downloading &&
lastScannedHeight != null &&
lastScanRange != null &&
! lastScanRange . isEmpty ( ) &&
lastScannedHeight < lastScanRange . endInclusive
2020-02-25 23:43:27 -08:00
/ * *
* The amount of scan progress from 0 to 100.
* /
2022-07-12 05:40:09 -07:00
val scanProgress
get ( ) = when {
lastScannedHeight == null -> 0
lastScanRange == null -> 100
lastScannedHeight >= lastScanRange . endInclusive -> 100
else -> {
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
val blocksScanned =
( lastScannedHeight . value - lastScanRange . start . value + 1 ) . coerceAtLeast ( 0 )
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
val numberOfBlocks =
lastScanRange . endInclusive . value - lastScanRange . start . value + 1
// take the percentage then convert and round
( ( blocksScanned . toFloat ( ) / numberOfBlocks ) * 100.0f ) . let { percent ->
percent . coerceAtMost ( 100.0f ) . roundToInt ( )
}
2020-02-25 23:43:27 -08:00
}
}
2020-06-09 19:00:41 -07:00
}
data class ValidationErrorInfo (
2022-07-12 05:40:09 -07:00
val errorHeight : BlockHeight ,
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 {
2022-08-17 06:48:02 -07:00
fun String . toId ( ) = lowercase ( 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 {
2021-06-29 23:24:24 -07:00
twig ( " $name MUTEX: acquiring lock... " , - 1 )
2021-04-09 18:25:21 -07:00
this . withLock {
2021-06-29 23:24:24 -07:00
twig ( " $name MUTEX: ...lock acquired! " , - 1 )
2021-04-09 18:25:21 -07:00
return block ( ) . also {
2021-06-29 23:24:24 -07:00
twig ( " $name MUTEX: releasing lock " , - 1 )
2021-04-09 18:25:21 -07:00
}
}
}
2022-07-12 05:40:09 -07:00
}
2021-04-09 18:25:21 -07:00
2022-07-12 05:40:09 -07:00
private fun max ( a : BlockHeight ? , b : BlockHeight ) = if ( null == a ) {
b
} else if ( a . value > b . value ) {
a
} else {
b
2020-03-26 04:00:04 -07:00
}