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.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
|
2022-10-19 13:52:54 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.exception.InitializeException
|
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
|
2022-12-13 05:25:09 -08:00
|
|
|
|
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
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
|
2022-12-13 05:25:09 -08:00
|
|
|
|
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
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
|
2022-10-19 13:52:54 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
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-10-06 10:44:34 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.model.Account
|
2022-07-12 05:40:09 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
2022-10-19 13:52:54 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
2022-09-27 06:01:53 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
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.delay
|
2021-03-31 06:07:37 -07:00
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2021-05-25 08:15:09 -07:00
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
|
import kotlin.time.Duration.Companion.days
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooManyFunctions", "LargeClass")
|
2022-07-12 05:40:09 -07:00
|
|
|
|
class CompactBlockProcessor internal constructor(
|
2019-12-23 11:50:52 -08:00
|
|
|
|
val downloader: CompactBlockDownloader,
|
2022-10-19 13:52:54 -07:00
|
|
|
|
private val repository: DerivedDataRepository,
|
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-27 05:25:54 -07:00
|
|
|
|
private val _state: MutableStateFlow<State> = MutableStateFlow(Initialized)
|
|
|
|
|
private val _progress = MutableStateFlow(0)
|
|
|
|
|
private val _processorInfo = MutableStateFlow(ProcessorInfo(null, null, null, null, null))
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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-27 05:25:54 -07:00
|
|
|
|
val state = _state.asStateFlow()
|
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-27 05:25:54 -07:00
|
|
|
|
val progress = _progress.asStateFlow()
|
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-27 05:25:54 -07:00
|
|
|
|
val processorInfo = _processorInfo.asStateFlow()
|
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
|
|
|
|
*/
|
2022-10-19 13:52:54 -07:00
|
|
|
|
@Suppress("LongMethod")
|
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)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"Unable to process new blocks because we are disconnected! Attempting to " +
|
|
|
|
|
"reconnect in ${napTime}ms"
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
delay(napTime)
|
|
|
|
|
}
|
|
|
|
|
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
val noWorkDone = currentInfo.lastDownloadRange?.isEmpty()
|
|
|
|
|
?: true && currentInfo.lastScanRange?.isEmpty() ?: true
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val summary = if (noWorkDone) {
|
|
|
|
|
"Nothing to process: no new blocks to download or scan"
|
|
|
|
|
} else {
|
|
|
|
|
"Done processing blocks"
|
|
|
|
|
}
|
|
|
|
|
consecutiveChainErrors.set(0)
|
|
|
|
|
val napTime = calculatePollInterval()
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
2022-10-19 13:52:54 -07:00
|
|
|
|
"$summary${
|
2022-12-20 00:25:04 -08:00
|
|
|
|
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 {
|
|
|
|
|
""
|
|
|
|
|
}
|
2022-10-19 13:52:54 -07:00
|
|
|
|
}! Sleeping" +
|
2022-08-23 06:49:00 -07:00
|
|
|
|
" for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight})."
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
delay(napTime)
|
|
|
|
|
}
|
|
|
|
|
is BlockProcessingResult.Error -> {
|
|
|
|
|
if (consecutiveChainErrors.get() >= RETRIES) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
val errorMessage = "ERROR: unable to resolve reorg at height $result after " +
|
|
|
|
|
"${consecutiveChainErrors.get()} correction attempts!"
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-27 05:25:54 -07:00
|
|
|
|
} while (isActive && _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 {
|
2022-12-13 05:25:09 -08:00
|
|
|
|
if (BenchmarkingExt.isBenchmarking()) {
|
|
|
|
|
// We inject a benchmark test blocks range at this point to process only a restricted range of blocks
|
|
|
|
|
// for a more reliable benchmark results.
|
|
|
|
|
val benchmarkBlockRange = BlockRangeFixture.new()
|
|
|
|
|
downloadNewBlocks(benchmarkBlockRange)
|
|
|
|
|
val error = validateAndScanNewBlocks(benchmarkBlockRange)
|
|
|
|
|
if (error != BlockProcessingResult.Success) {
|
|
|
|
|
error
|
|
|
|
|
} else {
|
|
|
|
|
enhanceTransactionDetails(benchmarkBlockRange)
|
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
2022-12-13 05:25:09 -08:00
|
|
|
|
downloadNewBlocks(currentInfo.lastDownloadRange)
|
|
|
|
|
val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
|
|
|
|
|
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 {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: 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?
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
2020-03-27 13:28:42 -07:00
|
|
|
|
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-08-23 06:49:00 -07:00
|
|
|
|
lastScanRange = if (
|
|
|
|
|
initialInfo.lastScannedHeight != null &&
|
|
|
|
|
initialInfo.networkBlockHeight != null
|
|
|
|
|
) {
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2020-03-25 14:58:08 -07:00
|
|
|
|
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
|
2022-10-19 13:52:54 -07:00
|
|
|
|
if (newTxs.size.toLong() == repository.getTransactionCount()) {
|
2021-03-31 05:47:04 -07:00
|
|
|
|
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) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig("Failed to enhance due to: ${t.message} caused by: ${t.cause}")
|
2022-07-12 05:40:09 -07:00
|
|
|
|
BlockProcessingResult.FailedEnhance
|
2020-03-25 14:58:08 -07:00
|
|
|
|
} finally {
|
|
|
|
|
Twig.clip("enhancing")
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: we still need a way to identify those transactions that failed to be enhanced
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
|
|
|
|
|
2022-10-19 13:52:54 -07:00
|
|
|
|
private suspend fun enhance(transaction: TransactionOverview) = withContext(Dispatchers.IO) {
|
|
|
|
|
enhanceHelper(transaction.id, transaction.rawId.byteArray, transaction.minedHeight)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) {
|
|
|
|
|
twig("START: enhancing transaction (id:$id block:$minedHeight)")
|
|
|
|
|
|
|
|
|
|
runCatching {
|
|
|
|
|
downloader.fetchTransaction(rawTransactionId)
|
|
|
|
|
}.onSuccess { tx ->
|
|
|
|
|
tx?.let {
|
|
|
|
|
runCatching {
|
|
|
|
|
twig("decrypting and storing transaction (id:$id block:$minedHeight)")
|
|
|
|
|
rustBackend.decryptAndStoreTransaction(it.data.toByteArray())
|
|
|
|
|
}.onSuccess {
|
|
|
|
|
twig("DONE: enhancing transaction (id:$id block:$minedHeight)")
|
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
onProcessorError(EnhanceTxDecryptError(minedHeight, error))
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2022-10-19 13:52:54 -07:00
|
|
|
|
} ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.")
|
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
onProcessorError(EnhanceTxDownloadError(minedHeight, error))
|
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
|
|
|
|
!info.matchingNetwork(network) -> MismatchedNetwork(
|
|
|
|
|
clientNetwork = network,
|
|
|
|
|
serverNetwork = info.chainName
|
|
|
|
|
)
|
|
|
|
|
!info.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 {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"Warning: An ${error::class.java.simpleName} was encountered while verifying setup but " +
|
|
|
|
|
"it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}"
|
|
|
|
|
)
|
2021-03-31 06:07:37 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-11 17:00:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-31 05:47:04 -07:00
|
|
|
|
private suspend fun updateBirthdayHeight() {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2021-03-31 05:47:04 -07:00
|
|
|
|
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-08-23 06:49:00 -07:00
|
|
|
|
|
|
|
|
|
@Suppress("MagicNumber")
|
2022-07-12 05:40:09 -07:00
|
|
|
|
internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? =
|
|
|
|
|
withContext(IO) {
|
|
|
|
|
var count: Int? = null
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [683]: cleanup the way that we prevent this from running excessively
|
2022-07-12 05:40:09 -07:00
|
|
|
|
// 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.
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
2022-07-12 05:40:09 -07:00
|
|
|
|
if (failedUtxoFetches < 9) { // there are 3 attempts per block
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2022-07-12 05:40:09 -07:00
|
|
|
|
try {
|
|
|
|
|
retryUpTo(3) {
|
|
|
|
|
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
|
|
|
|
|
count = processUtxoResult(result, tAddress, startHeight)
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
failedUtxoFetches++
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"Warning: Fetching UTXOs is repeatedly failing! We will only try about " +
|
|
|
|
|
"${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session. " +
|
|
|
|
|
"Exception message: ${e.message}, caused by: ${e.cause}."
|
|
|
|
|
)
|
2021-02-17 13:07:57 -08:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
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
|
2022-10-14 01:33:03 -07:00
|
|
|
|
// TODO(str4d): We no longer clear UTXOs here, as rustBackend.putUtxo now uses an upsert instead of an insert.
|
|
|
|
|
// This means that now-spent UTXOs would previously have been deleted, but now are left in the database (like
|
|
|
|
|
// shielded notes). Due to the fact that the lightwalletd query only returns _current_ UTXOs, we don't learn
|
|
|
|
|
// about recently-spent UTXOs here, so the transparent balance does not get updated here. Instead, when a
|
|
|
|
|
// received shielded note is "enhanced" by downloading the full transaction, we mark any UTXOs spent in that
|
|
|
|
|
// transaction as spent in the database. This relies on two current properties: UTXOs are only ever spent in
|
|
|
|
|
// shielding transactions, and at least one shielded note from each shielding transaction is always enhanced.
|
|
|
|
|
// However, for greater reliability, we may want to alter the Data Access API to support "inferring spentness"
|
|
|
|
|
// from what is _not_ returned as a UTXO, or alternatively fetch TXOs from lightwalletd instead of just UTXOs.
|
2021-06-29 23:25:40 -07:00
|
|
|
|
twig("Checking for UTXOs above height $aboveHeight")
|
|
|
|
|
result.forEach { utxo: Service.GetAddressUtxosReply ->
|
|
|
|
|
twig("Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi")
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2021-06-29 23:25:40 -07:00
|
|
|
|
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) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: more accurately track the utxos that were skipped (in theory, this could fail for other
|
|
|
|
|
// reasons)
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
2021-06-29 23:25:40 -07:00
|
|
|
|
skipped++
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because " +
|
|
|
|
|
"it already exists. Exception message: ${t.message}, caused by: ${t.cause}."
|
|
|
|
|
)
|
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
|
|
|
|
*/
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@VisibleForTesting
|
|
|
|
|
// allow mocks to verify how this is called, rather than the downloader, which is more complex
|
|
|
|
|
@Suppress("MagicNumber")
|
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 {
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_state.value = Downloading
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"found $missingBlockCount missing blocks, downloading in $batches batches of " +
|
|
|
|
|
"$DOWNLOAD_BATCH_SIZE..."
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"downloaded $downloadedBlockHeight..$end (batch $i of $batches) " +
|
|
|
|
|
"[${downloadedBlockHeight..end}]"
|
|
|
|
|
) {
|
2022-07-12 05:40:09 -07:00
|
|
|
|
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
|
|
|
|
|
}
|
|
|
|
|
twig("downloaded $count blocks!")
|
|
|
|
|
progress = (i / batches.toFloat() * 100).roundToInt()
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_progress.value = progress
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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-08-27 05:25:54 -07:00
|
|
|
|
_progress.value = 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-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("MagicNumber")
|
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 =
|
2022-08-23 06:49:00 -07:00
|
|
|
|
(lastScannedHeight.value - range.start.value) /
|
|
|
|
|
(range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f
|
|
|
|
|
val percent = "%.0f".format(
|
|
|
|
|
percentValue.coerceAtMost(100f)
|
|
|
|
|
.coerceAtLeast(0f)
|
|
|
|
|
)
|
|
|
|
|
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)
|
2022-08-23 06:49:00 -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
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_processorInfo.value = currentInfo
|
2022-07-25 11:47:58 -07:00
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
private suspend fun handleChainError(errorHeight: BlockHeight) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: consider an error object containing hash information
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
2020-06-09 19:00:41 -07:00
|
|
|
|
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 {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// TODO [#683]: add a concept of original checkpoint height to the processor. For now, derive it
|
|
|
|
|
// add one because we already have the checkpoint. Add one again because we delete ABOVE the block
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
|
|
|
|
val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2
|
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())
|
2022-08-23 06:49:00 -07:00
|
|
|
|
val blocksPer14Days = 14.days.inWholeMilliseconds / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val twoWeeksBack = BlockHeight.new(
|
|
|
|
|
network,
|
2022-08-23 06:49:00 -07:00
|
|
|
|
(height.value - blocksPer14Days).coerceAtLeast(lowerBoundHeight.value)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
)
|
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-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("LongMethod")
|
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)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
|
|
|
|
|
twig(
|
|
|
|
|
"Rewinding from $lastScannedHeight to requested height: $height using target height: " +
|
|
|
|
|
"$targetHeight with last local block: $lastLocalBlock"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (null == lastScannedHeight && targetHeight < lastLocalBlock) {
|
|
|
|
|
twig("Rewinding because targetHeight is less than lastLocalBlock.")
|
|
|
|
|
rustBackend.rewindToHeight(targetHeight)
|
|
|
|
|
} else if (null != lastScannedHeight && targetHeight < lastScannedHeight) {
|
|
|
|
|
twig("Rewinding because targetHeight is less than lastScannedHeight.")
|
2022-07-12 05:40:09 -07:00
|
|
|
|
rustBackend.rewindToHeight(targetHeight)
|
|
|
|
|
} else {
|
2022-08-23 06:49:00 -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"
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2021-04-05 15:37:13 -07:00
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val currentNetworkBlockHeight = currentInfo.networkBlockHeight
|
|
|
|
|
|
|
|
|
|
if (alsoClearBlockCache) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"Also clearing block cache back to $targetHeight. These rewound blocks will " +
|
|
|
|
|
"download in the next scheduled scan"
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
downloader.rewindToHeight(targetHeight)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// 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
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_progress.value = 0
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
|
|
|
|
if (null == currentNetworkBlockHeight) {
|
|
|
|
|
updateProgress(
|
|
|
|
|
lastScannedHeight = targetHeight,
|
|
|
|
|
lastScanRange = null
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
updateProgress(
|
|
|
|
|
lastScannedHeight = targetHeight,
|
|
|
|
|
lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_progress.value = 0
|
2022-07-12 05:40:09 -07:00
|
|
|
|
|
|
|
|
|
if (null != lastScannedHeight) {
|
|
|
|
|
val range = (targetHeight + 1)..lastScannedHeight
|
2022-08-23 06:49:00 -07:00
|
|
|
|
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}"
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
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) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're
|
|
|
|
|
// debugging something
|
2020-06-09 19:00:41 -07:00
|
|
|
|
if (!BuildConfig.DEBUG) return
|
|
|
|
|
|
|
|
|
|
var errorInfo = fetchValidationErrorInfo(errorHeight)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"validation failed at block ${errorInfo.errorHeight} which had hash " +
|
|
|
|
|
"${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}"
|
|
|
|
|
)
|
2020-06-09 19:00:41 -07:00
|
|
|
|
errorInfo = fetchValidationErrorInfo(errorHeight + 1)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but " +
|
|
|
|
|
"the expected hash was ${errorInfo.expectedPrevHash}"
|
|
|
|
|
)
|
2020-06-09 19:00:41 -07:00
|
|
|
|
|
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
|
2022-10-19 13:52:54 -07:00
|
|
|
|
val block = downloader.compactBlockRepository.findCompactBlock(height)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// 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()
|
2022-10-19 13:52:54 -07:00
|
|
|
|
?: repository.findBlockHash(height)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
twig(
|
|
|
|
|
"block: $height\thash=${hash?.toHexReversed()} \tprevHash=${
|
2022-12-20 00:25:04 -08:00
|
|
|
|
block?.prevHash?.toByteArray()?.toHexReversed()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}"
|
|
|
|
|
)
|
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 {
|
2022-10-19 13:52:54 -07:00
|
|
|
|
val hash = repository.findBlockHash(errorHeight + 1)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
?.toHexReversed()
|
2020-06-09 19:00:41 -07:00
|
|
|
|
val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed()
|
|
|
|
|
|
2022-10-19 13:52:54 -07:00
|
|
|
|
val compactBlock = downloader.compactBlockRepository.findCompactBlock(errorHeight + 1)
|
2020-06-09 19:00:41 -07:00
|
|
|
|
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 {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
twig(
|
|
|
|
|
"offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = " +
|
|
|
|
|
"$offset"
|
|
|
|
|
)
|
2020-01-15 04:10:22 -08:00
|
|
|
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2021-03-31 05:47:04 -07:00
|
|
|
|
try {
|
2022-10-19 13:52:54 -07:00
|
|
|
|
val tempOldestTransactionHeight = repository.getOldestTransaction()?.minedHeight
|
2022-07-12 05:40:09 -07:00
|
|
|
|
?: lowerBoundHeight
|
2021-03-31 05:47:04 -07:00
|
|
|
|
// to be safe adjust for reorgs (and generally a little cushion is good for privacy)
|
2022-08-23 06:49:00 -07:00
|
|
|
|
// 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,
|
2022-08-23 06:49:00 -07:00
|
|
|
|
tempOldestTransactionHeight.value -
|
|
|
|
|
tempOldestTransactionHeight.value.rem(MAX_REORG_SIZE) - MAX_REORG_SIZE.toLong()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
)
|
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
|
|
|
|
|
2022-10-26 18:37:40 -07:00
|
|
|
|
// CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
|
2022-09-27 06:01:53 -07:00
|
|
|
|
// access to the RustBackend live. This should be refactored.
|
|
|
|
|
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
|
|
|
|
|
rustBackend.createAccount(seed)
|
|
|
|
|
|
2020-02-27 00:25:07 -08:00
|
|
|
|
/**
|
2022-10-06 10:40:49 -07:00
|
|
|
|
* Get the current unified address for the given wallet account.
|
2020-02-27 00:25:07 -08:00
|
|
|
|
*
|
2022-10-06 10:40:49 -07:00
|
|
|
|
* @return the current unified address of this account.
|
2020-02-27 00:25:07 -08:00
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
|
suspend fun getCurrentAddress(account: Account) =
|
|
|
|
|
rustBackend.getCurrentAddress(account.value)
|
2021-04-05 15:37:13 -07:00
|
|
|
|
|
2022-10-06 10:40:49 -07:00
|
|
|
|
/**
|
|
|
|
|
* Get the legacy Sapling address corresponding to the current unified address for the given wallet account.
|
|
|
|
|
*
|
|
|
|
|
* @return a Sapling address.
|
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
|
suspend fun getLegacySaplingAddress(account: Account) =
|
2022-10-06 10:40:49 -07:00
|
|
|
|
rustBackend.getSaplingReceiver(
|
2022-10-06 10:44:34 -07:00
|
|
|
|
rustBackend.getCurrentAddress(account.value)
|
2022-10-06 10:40:49 -07:00
|
|
|
|
)
|
2022-10-19 13:52:54 -07:00
|
|
|
|
?: throw InitializeException.MissingAddressException("legacy Sapling")
|
2022-10-06 10:40:49 -07:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the legacy transparent address corresponding to the current unified address for the given wallet account.
|
|
|
|
|
*
|
|
|
|
|
* @return a transparent address.
|
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
|
suspend fun getTransparentAddress(account: Account) =
|
2022-10-06 10:40:49 -07:00
|
|
|
|
rustBackend.getTransparentReceiver(
|
2022-10-06 10:44:34 -07:00
|
|
|
|
rustBackend.getCurrentAddress(account.value)
|
2022-10-06 10:40:49 -07:00
|
|
|
|
)
|
2022-10-19 13:52:54 -07:00
|
|
|
|
?: throw InitializeException.MissingAddressException("legacy transparent")
|
2019-11-01 13:25:28 -07:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculates the latest balance info. Defaults to the first account.
|
|
|
|
|
*
|
2022-10-06 10:44:34 -07:00
|
|
|
|
* @param account 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-10-06 10:44:34 -07:00
|
|
|
|
suspend fun getBalanceInfo(account: Account): WalletBalance =
|
2021-06-29 23:24:24 -07:00
|
|
|
|
twigTask("checking balance info", -1) {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2019-11-01 13:25:28 -07:00
|
|
|
|
try {
|
2022-10-06 10:44:34 -07:00
|
|
|
|
val balanceTotal = rustBackend.getBalance(account.value)
|
2020-02-21 15:14:34 -08:00
|
|
|
|
twig("found total balance: $balanceTotal")
|
2022-10-06 10:44:34 -07:00
|
|
|
|
val balanceAvailable = rustBackend.getVerifiedBalance(account.value)
|
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) {
|
2022-08-27 05:25:54 -07:00
|
|
|
|
_state.value = newState
|
2019-10-21 03:26:02 -07:00
|
|
|
|
}
|
|
|
|
|
|
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() =
|
|
|
|
|
!isDownloading &&
|
|
|
|
|
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-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("MagicNumber")
|
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
|
|
|
|
}
|