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
|
|
|
|
|
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.MismatchedNetwork
|
2022-10-19 13:52:54 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.exception.InitializeException
|
2023-04-14 03:55:51 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.exception.LightWalletException
|
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.MAX_BACKOFF_INTERVAL
|
|
|
|
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL
|
2023-05-18 04:36:15 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.Backend
|
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
|
2023-05-18 04:36:15 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.createAccountAndGetSpendingKey
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.ext.isNullOrEmpty
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.ext.length
|
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
|
2023-05-18 04:36:15 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.getBalance
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.getBranchIdForHeight
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.getCurrentAddress
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.getDownloadedUtxoBalance
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.getNearestRewindHeight
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.getVerifiedBalance
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.listTransparentReceivers
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.model.BlockBatch
|
2023-05-05 14:46:07 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
2023-04-13 04:36:24 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.model.ext.from
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
|
2023-05-18 04:36:15 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.network
|
2022-10-19 13:52:54 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
2023-05-18 04:36:15 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.internal.rewindToHeight
|
|
|
|
|
import cash.z.ecc.android.sdk.internal.validateCombinedChainOrErrorBlockHeight
|
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
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
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
|
2023-02-01 02:14:55 -08:00
|
|
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
|
|
|
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
|
2023-03-08 07:04:04 -08:00
|
|
|
|
import co.electriccoin.lightwallet.client.fixture.BenchmarkingBlockRangeFixture
|
2023-02-01 02:14:55 -08:00
|
|
|
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
2023-04-14 03:55:51 -07:00
|
|
|
|
import co.electriccoin.lightwallet.client.model.GetAddressUtxosReplyUnsafe
|
2023-02-01 02:14:55 -08:00
|
|
|
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
|
|
|
|
import co.electriccoin.lightwallet.client.model.Response
|
2019-06-14 16:24:52 -07:00
|
|
|
|
import kotlinx.coroutines.delay
|
2023-05-01 04:12:38 -07:00
|
|
|
|
import kotlinx.coroutines.flow.Flow
|
2021-03-31 06:07:37 -07:00
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import kotlinx.coroutines.flow.asFlow
|
2021-05-25 08:15:09 -07:00
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import kotlinx.coroutines.flow.buffer
|
|
|
|
|
import kotlinx.coroutines.flow.collect
|
2023-04-14 03:55:51 -07:00
|
|
|
|
import kotlinx.coroutines.flow.filterIsInstance
|
2023-05-01 04:12:38 -07:00
|
|
|
|
import kotlinx.coroutines.flow.flow
|
2023-04-14 03:55:51 -07:00
|
|
|
|
import kotlinx.coroutines.flow.map
|
2023-04-13 04:36:24 -07:00
|
|
|
|
import kotlinx.coroutines.flow.onCompletion
|
2023-04-14 03:55:51 -07:00
|
|
|
|
import kotlinx.coroutines.flow.onEach
|
2023-05-10 03:45:23 -07:00
|
|
|
|
import kotlinx.coroutines.flow.takeWhile
|
2021-04-09 18:25:21 -07:00
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
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.
|
2023-05-18 04:36:15 -07:00
|
|
|
|
* @property backend 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,
|
2023-05-18 04:36:15 -07:00
|
|
|
|
private val backend: Backend,
|
|
|
|
|
minimumHeight: BlockHeight
|
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)
|
2023-05-18 04:36:15 -07:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The zcash network that is being processed. Either Testnet or Mainnet.
|
|
|
|
|
*/
|
|
|
|
|
val network = backend.network
|
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
private val lowerBoundHeight: BlockHeight = BlockHeight(
|
|
|
|
|
max(
|
2023-05-18 04:36:15 -07:00
|
|
|
|
network.saplingActivationHeight.value,
|
2022-07-12 05:40:09 -07:00
|
|
|
|
minimumHeight.value - MAX_REORG_SIZE
|
|
|
|
|
)
|
|
|
|
|
)
|
2019-10-21 03:26:02 -07:00
|
|
|
|
|
2023-03-09 14:07:59 -08:00
|
|
|
|
private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initialized)
|
2023-05-10 03:45:23 -07:00
|
|
|
|
private val _progress = MutableStateFlow(PercentDecimal.ZERO_PERCENT)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
private val _processorInfo = MutableStateFlow(ProcessorInfo(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-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")
|
2023-05-01 04:12:38 -07:00
|
|
|
|
suspend fun start() {
|
2021-03-31 06:07:37 -07:00
|
|
|
|
verifySetup()
|
2023-05-10 03:45:23 -07:00
|
|
|
|
|
2021-03-31 05:47:04 -07:00
|
|
|
|
updateBirthdayHeight()
|
2023-05-10 03:45:23 -07:00
|
|
|
|
|
|
|
|
|
// Clear any undeleted left over block files from previous sync attempts
|
|
|
|
|
deleteAllBlockFiles(
|
|
|
|
|
downloader = downloader,
|
|
|
|
|
lastKnownHeight = getLastScannedHeight(repository)
|
|
|
|
|
)
|
|
|
|
|
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "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)
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
"Unable to process new blocks because we are disconnected! Attempting to " +
|
|
|
|
|
"reconnect in ${napTime}ms"
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
delay(napTime)
|
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val noWorkDone = _processorInfo.value.lastSyncRange?.isEmpty() ?: true
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val summary = if (noWorkDone) {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
"Nothing to process: no new blocks to sync"
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
|
|
|
|
"Done processing blocks"
|
|
|
|
|
}
|
|
|
|
|
consecutiveChainErrors.set(0)
|
|
|
|
|
val napTime = calculatePollInterval()
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
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" +
|
2023-05-01 04:12:38 -07:00
|
|
|
|
" for ${napTime}ms (latest height: ${_processorInfo.value.networkBlockHeight})."
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
delay(napTime)
|
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-04-06 23:12:25 -07:00
|
|
|
|
is BlockProcessingResult.FailedDeleteBlocks -> {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
Twig.error {
|
2023-04-06 23:12:25 -07:00
|
|
|
|
"Failed to delete temporary blocks files from the device disk. It will be retried on the" +
|
|
|
|
|
" next time, while downloading new blocks."
|
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
checkErrorResult(result.failedAtHeight)
|
2023-04-06 23:12:25 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
is BlockProcessingResult.FailedDownloadBlocks -> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.error { "Failed while downloading blocks at height: ${result.failedAtHeight}" }
|
|
|
|
|
checkErrorResult(result.failedAtHeight)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
is BlockProcessingResult.FailedValidateBlocks -> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.error { "Failed while validating blocks at height: ${result.failedAtHeight}" }
|
|
|
|
|
checkErrorResult(result.failedAtHeight)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
is BlockProcessingResult.FailedScanBlocks -> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.error { "Failed while scanning blocks at height: ${result.failedAtHeight}" }
|
|
|
|
|
checkErrorResult(result.failedAtHeight)
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
is BlockProcessingResult.Success -> {
|
|
|
|
|
// Do nothing. We are done.
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
|
|
|
|
|
is BlockProcessingResult.DownloadSuccess -> {
|
|
|
|
|
// Do nothing. Syncing of blocks is in progress.
|
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} while (_state.value !is State.Stopped)
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "processor complete" }
|
2019-06-14 16:24:52 -07:00
|
|
|
|
stop()
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
suspend fun checkErrorResult(failedHeight: BlockHeight) {
|
|
|
|
|
if (consecutiveChainErrors.get() >= RETRIES) {
|
|
|
|
|
val errorMessage = "ERROR: unable to resolve reorg at height $failedHeight after " +
|
|
|
|
|
"${consecutiveChainErrors.get()} correction attempts!"
|
|
|
|
|
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
|
|
|
|
|
} else {
|
|
|
|
|
handleChainError(failedHeight)
|
|
|
|
|
}
|
|
|
|
|
consecutiveChainErrors.getAndIncrement()
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 15:14:34 -08:00
|
|
|
|
/**
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* Sets the state to [State.Stopped], which causes the processor loop to exit.
|
2020-02-21 15:14:34 -08:00
|
|
|
|
*/
|
2019-10-21 03:26:02 -07:00
|
|
|
|
suspend fun stop() {
|
2020-02-21 15:14:34 -08:00
|
|
|
|
runCatching {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
setState(State.Stopped)
|
2020-02-21 15:14:34 -08:00
|
|
|
|
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()
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "${error.message}" }
|
2019-06-14 16:24:52 -07:00
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
private suspend fun processNewBlocks(): BlockProcessingResult {
|
|
|
|
|
Twig.debug { "Beginning to process new blocks (with lower bound: $lowerBoundHeight)..." }
|
2020-02-21 15:14:34 -08:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return if (!updateRanges()) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "Disconnection detected! Attempting to reconnect!" }
|
2023-03-09 14:07:59 -08:00
|
|
|
|
setState(State.Disconnected)
|
2023-02-01 02:14:55 -08:00
|
|
|
|
downloader.lightWalletClient.reconnect()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
BlockProcessingResult.Reconnecting
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else if (_processorInfo.value.lastSyncRange.isNullOrEmpty()) {
|
|
|
|
|
setState(State.Synced(_processorInfo.value.lastSyncRange))
|
2022-07-12 05:40:09 -07:00
|
|
|
|
BlockProcessingResult.NoBlocksToProcess
|
2020-02-21 15:14:34 -08:00
|
|
|
|
} else {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val syncRange = 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 = BenchmarkingBlockRangeFixture.new().let {
|
|
|
|
|
// Convert range of Longs to range of BlockHeights
|
|
|
|
|
BlockHeight.new(ZcashNetwork.Mainnet, it.start)..(
|
|
|
|
|
BlockHeight.new(ZcashNetwork.Mainnet, it.endInclusive)
|
|
|
|
|
)
|
2022-12-13 05:25:09 -08:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
benchmarkBlockRange
|
|
|
|
|
} else {
|
|
|
|
|
_processorInfo.value.lastSyncRange!!
|
2021-03-31 06:16:06 -07:00
|
|
|
|
}
|
2023-04-06 23:12:25 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
syncBlocksAndEnhanceTransactions(
|
|
|
|
|
syncRange = syncRange,
|
|
|
|
|
withDownload = true
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-06 23:12:25 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
@Suppress("ReturnCount")
|
|
|
|
|
private suspend fun syncBlocksAndEnhanceTransactions(
|
|
|
|
|
syncRange: ClosedRange<BlockHeight>,
|
|
|
|
|
withDownload: Boolean
|
|
|
|
|
): BlockProcessingResult {
|
|
|
|
|
_state.value = State.Syncing
|
|
|
|
|
|
|
|
|
|
// Sync
|
|
|
|
|
var syncResult: BlockProcessingResult = BlockProcessingResult.Success
|
|
|
|
|
syncNewBlocks(
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend = backend,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
downloader = downloader,
|
|
|
|
|
repository = repository,
|
|
|
|
|
network = network,
|
|
|
|
|
syncRange = syncRange,
|
|
|
|
|
withDownload = withDownload
|
|
|
|
|
).collect { syncProgress ->
|
|
|
|
|
_progress.value = syncProgress.percentage
|
|
|
|
|
updateProgress(lastSyncedHeight = syncProgress.lastSyncedHeight)
|
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
// Cancel collecting in case of any unwanted state comes
|
|
|
|
|
if (syncProgress.result != BlockProcessingResult.Success) {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
syncResult = syncProgress.result
|
|
|
|
|
return@collect
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (syncResult != BlockProcessingResult.Success) {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
// Remove persisted but not validated and scanned blocks in case of any failure
|
|
|
|
|
val lastScannedHeight = getLastScannedHeight(repository)
|
|
|
|
|
downloader.rewindToHeight(lastScannedHeight)
|
|
|
|
|
deleteAllBlockFiles(
|
|
|
|
|
downloader = downloader,
|
|
|
|
|
lastKnownHeight = lastScannedHeight
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return syncResult
|
2023-04-06 23:12:25 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
// Enhance
|
|
|
|
|
val enhanceResult = enhanceTransactionDetails(syncRange)
|
|
|
|
|
|
|
|
|
|
if (enhanceResult != BlockProcessingResult.Success ||
|
|
|
|
|
enhanceResult != BlockProcessingResult.NoBlocksToProcess
|
|
|
|
|
) {
|
|
|
|
|
return enhanceResult
|
2020-02-21 15:14:34 -08:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return BlockProcessingResult.Success
|
|
|
|
|
}
|
2023-04-06 23:12:25 -07:00
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
sealed class BlockProcessingResult {
|
|
|
|
|
object NoBlocksToProcess : BlockProcessingResult()
|
|
|
|
|
object Success : BlockProcessingResult()
|
2023-05-10 03:45:23 -07:00
|
|
|
|
data class DownloadSuccess(val downloadedBlocks: List<JniBlockMeta>?) : BlockProcessingResult()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
object Reconnecting : BlockProcessingResult()
|
2023-05-10 03:45:23 -07:00
|
|
|
|
data class FailedDownloadBlocks(val failedAtHeight: BlockHeight) : BlockProcessingResult()
|
|
|
|
|
data class FailedScanBlocks(val failedAtHeight: BlockHeight) : BlockProcessingResult()
|
|
|
|
|
data class FailedValidateBlocks(val failedAtHeight: BlockHeight) : BlockProcessingResult()
|
|
|
|
|
data class FailedDeleteBlocks(val failedAtHeight: BlockHeight) : BlockProcessingResult()
|
2023-05-01 04:12:38 -07:00
|
|
|
|
object FailedEnhance : BlockProcessingResult()
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
private suspend fun updateRanges(): Boolean {
|
2023-02-01 02:14:55 -08:00
|
|
|
|
// This fetches the latest height each time this method is called, which can be very inefficient
|
|
|
|
|
// when downloading all of the blocks from the server
|
|
|
|
|
val networkBlockHeight = run {
|
|
|
|
|
val networkBlockHeightUnsafe =
|
|
|
|
|
when (val response = downloader.getLatestBlockHeight()) {
|
|
|
|
|
is Response.Success -> response.result
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runCatching { networkBlockHeightUnsafe?.toBlockHeight(network) }.getOrNull()
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} ?: return false
|
|
|
|
|
|
|
|
|
|
// If we find out that we previously downloaded, but not validated and scanned persisted blocks, we need
|
|
|
|
|
// to rewind the blocks above the last scanned height first.
|
|
|
|
|
val lastScannedHeight = getLastScannedHeight(repository)
|
|
|
|
|
val lastDownloadedHeight = getLastDownloadedHeight(downloader).let {
|
|
|
|
|
BlockHeight.new(
|
|
|
|
|
network,
|
|
|
|
|
max(
|
|
|
|
|
it?.value ?: 0,
|
|
|
|
|
lowerBoundHeight.value - 1
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
val lastSyncedHeight = if (lastDownloadedHeight.value - lastScannedHeight.value > 0) {
|
|
|
|
|
Twig.verbose {
|
|
|
|
|
"Clearing blocks of last persisted batch within the last scanned height " +
|
|
|
|
|
"$lastScannedHeight and last download height $lastDownloadedHeight, as all these blocks " +
|
|
|
|
|
"possibly haven't been validated and scanned in the previous blocks sync attempt."
|
|
|
|
|
}
|
|
|
|
|
downloader.rewindToHeight(lastScannedHeight)
|
|
|
|
|
lastScannedHeight
|
|
|
|
|
} else {
|
|
|
|
|
lastDownloadedHeight
|
|
|
|
|
}
|
2023-02-01 02:14:55 -08:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
updateProgress(
|
2023-02-01 02:14:55 -08:00
|
|
|
|
networkBlockHeight = networkBlockHeight,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight = lastSyncedHeight,
|
2023-05-10 03:45:23 -07:00
|
|
|
|
lastSyncRange = lastSyncedHeight + 1..networkBlockHeight
|
|
|
|
|
)
|
2023-02-01 02:14:55 -08:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return true
|
2020-02-21 15:14:34 -08:00
|
|
|
|
}
|
2019-10-21 03:26:02 -07:00
|
|
|
|
|
2023-04-06 23:12:25 -07:00
|
|
|
|
private suspend fun enhanceTransactionDetails(lastScanRange: ClosedRange<BlockHeight>?): BlockProcessingResult {
|
|
|
|
|
if (lastScanRange == null) {
|
|
|
|
|
return BlockProcessingResult.NoBlocksToProcess
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "Enhancing transaction details for blocks $lastScanRange" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
setState(State.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()) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "no new transactions found in $lastScanRange" }
|
2020-07-27 11:39:43 -07:00
|
|
|
|
} else {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "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()) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "Encountered the first transaction. This changes the birthday height!" }
|
2021-03-31 05:47:04 -07:00
|
|
|
|
updateBirthdayHeight()
|
|
|
|
|
}
|
2020-07-27 11:39:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 08:32:43 -08:00
|
|
|
|
newTxs.filter { it.minedHeight != null }.onEach { newTransaction ->
|
2022-08-17 06:48:02 -07:00
|
|
|
|
enhance(newTransaction)
|
2020-03-25 14:58:08 -07:00
|
|
|
|
}
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "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) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "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
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-06 23:12:25 -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-08-23 06:49:00 -07:00
|
|
|
|
|
2023-05-05 14:46:07 -07:00
|
|
|
|
private suspend fun enhance(transaction: DbTransactionOverview) {
|
2023-01-12 08:32:43 -08:00
|
|
|
|
transaction.minedHeight?.let { minedHeight ->
|
|
|
|
|
enhanceHelper(transaction.id, transaction.rawId.byteArray, minedHeight)
|
|
|
|
|
}
|
2022-10-19 13:52:54 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "START: enhancing transaction (id:$id block:$minedHeight)" }
|
2022-10-19 13:52:54 -07:00
|
|
|
|
|
2023-02-01 02:14:55 -08:00
|
|
|
|
when (val response = downloader.fetchTransaction(rawTransactionId)) {
|
|
|
|
|
is Response.Success -> {
|
2022-10-19 13:52:54 -07:00
|
|
|
|
runCatching {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "decrypting and storing transaction (id:$id block:$minedHeight)" }
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.decryptAndStoreTransaction(response.result.data)
|
2022-10-19 13:52:54 -07:00
|
|
|
|
}.onSuccess {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "DONE: enhancing transaction (id:$id block:$minedHeight)" }
|
2022-10-19 13:52:54 -07:00
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
onProcessorError(EnhanceTxDecryptError(minedHeight, error))
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2023-02-01 02:14:55 -08:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-02-01 02:14:55 -08:00
|
|
|
|
is Response.Failure -> {
|
|
|
|
|
onProcessorError(EnhanceTxDownloadError(minedHeight, response.toThrowable()))
|
|
|
|
|
}
|
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.
|
|
|
|
|
*/
|
2023-04-06 23:12:25 -07:00
|
|
|
|
// Need to refactor this to be less ugly and more testable
|
2023-02-01 02:14:55 -08:00
|
|
|
|
@Suppress("NestedBlockDepth")
|
2021-03-31 06:07:37 -07:00
|
|
|
|
private suspend fun verifySetup() {
|
|
|
|
|
// verify that the data is initialized
|
2023-02-01 02:14:55 -08:00
|
|
|
|
val error = if (!repository.isInitialized()) {
|
|
|
|
|
CompactBlockProcessorException.Uninitialized
|
|
|
|
|
} else if (repository.getAccountCount() == 0) {
|
|
|
|
|
CompactBlockProcessorException.NoAccount
|
|
|
|
|
} else {
|
|
|
|
|
// verify that the server is correct
|
|
|
|
|
|
|
|
|
|
// How do we handle network connection issues?
|
|
|
|
|
|
|
|
|
|
downloader.getServerInfo()?.let { info ->
|
|
|
|
|
val serverBlockHeight =
|
|
|
|
|
runCatching { info.blockHeightUnsafe.toBlockHeight(network) }.getOrNull()
|
|
|
|
|
|
|
|
|
|
if (null == serverBlockHeight) {
|
|
|
|
|
// TODO Better signal network connection issue
|
|
|
|
|
CompactBlockProcessorException.BadBlockHeight(info.blockHeightUnsafe)
|
|
|
|
|
} else {
|
|
|
|
|
val clientBranch = "%x".format(
|
|
|
|
|
Locale.ROOT,
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.getBranchIdForHeight(serverBlockHeight)
|
2023-02-01 02:14:55 -08:00
|
|
|
|
)
|
2023-05-18 04:36:15 -07:00
|
|
|
|
val network = backend.network.networkName
|
2023-02-01 02:14:55 -08:00
|
|
|
|
|
|
|
|
|
if (!clientBranch.equals(info.consensusBranchId, true)) {
|
|
|
|
|
MismatchedNetwork(
|
2022-07-12 05:40:09 -07:00
|
|
|
|
clientNetwork = network,
|
|
|
|
|
serverNetwork = info.chainName
|
|
|
|
|
)
|
2023-02-01 02:14:55 -08:00
|
|
|
|
} else if (!info.matchingNetwork(network)) {
|
|
|
|
|
MismatchedNetwork(
|
|
|
|
|
clientNetwork = network,
|
|
|
|
|
serverNetwork = info.chainName
|
2022-07-12 05:40:09 -07:00
|
|
|
|
)
|
2023-02-01 02:14:55 -08:00
|
|
|
|
} else {
|
|
|
|
|
null
|
2021-05-05 11:26:13 -07:00
|
|
|
|
}
|
2021-03-31 06:07:37 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error != null) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "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 {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
"Warning: An ${error::class.java.simpleName} was encountered while verifying setup but " +
|
|
|
|
|
"it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}"
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
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) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "Better birthday found! Birthday height updated from $birthdayHeight to $betterBirthday" }
|
2021-03-31 05:47:04 -07:00
|
|
|
|
_birthdayHeight.value = betterBirthday
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Throwable) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug(e) { "Warning: updating the birthday height failed" }
|
2021-03-31 05:47:04 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 23:25:40 -07:00
|
|
|
|
var failedUtxoFetches = 0
|
2022-08-23 06:49:00 -07:00
|
|
|
|
|
2023-04-14 03:55:51 -07:00
|
|
|
|
@Suppress("MagicNumber", "LongMethod")
|
2023-05-01 04:12:38 -07:00
|
|
|
|
internal suspend fun refreshUtxos(account: Account, startHeight: BlockHeight): Int {
|
|
|
|
|
Twig.debug { "Checking for UTXOs above height $startHeight" }
|
|
|
|
|
var count = 0
|
|
|
|
|
// TODO [683]: 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.
|
|
|
|
|
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
|
|
|
|
if (failedUtxoFetches < 9) { // there are 3 attempts per block
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
|
try {
|
|
|
|
|
retryUpTo(3) {
|
2023-05-18 04:36:15 -07:00
|
|
|
|
val tAddresses = backend.listTransparentReceivers(account)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
|
|
|
|
downloader.lightWalletClient.fetchUtxos(
|
|
|
|
|
tAddresses,
|
|
|
|
|
BlockHeightUnsafe.from(startHeight)
|
|
|
|
|
).onEach { response ->
|
|
|
|
|
when (response) {
|
|
|
|
|
is Response.Success -> {
|
|
|
|
|
Twig.verbose { "Downloading UTXO at height: ${response.result.height} succeeded." }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is Response.Failure -> {
|
|
|
|
|
Twig.warn {
|
|
|
|
|
"Downloading UTXO from height:" +
|
|
|
|
|
" $startHeight failed with: ${response.description}."
|
2023-04-14 03:55:51 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
throw LightWalletException.FetchUtxosException(
|
|
|
|
|
response.code,
|
|
|
|
|
response.description,
|
|
|
|
|
response.toThrowable()
|
|
|
|
|
)
|
2023-04-13 04:36:24 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
|
|
|
|
.filterIsInstance<Response.Success<GetAddressUtxosReplyUnsafe>>()
|
|
|
|
|
.map { response ->
|
|
|
|
|
response.result
|
|
|
|
|
}
|
|
|
|
|
.onCompletion {
|
|
|
|
|
if (it != null) {
|
|
|
|
|
Twig.debug { "UTXOs from height $startHeight failed to download with: $it" }
|
|
|
|
|
} else {
|
|
|
|
|
Twig.debug { "All UTXOs from height $startHeight fetched successfully" }
|
2023-04-14 03:55:51 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}.collect { utxo ->
|
2023-05-11 05:55:58 -07:00
|
|
|
|
Twig.verbose { "Fetched UTXO at height: ${utxo.height}" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val processResult = processUtxoResult(utxo)
|
|
|
|
|
if (processResult) {
|
|
|
|
|
count++
|
2023-04-14 03:55:51 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2021-02-17 13:07:57 -08:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
failedUtxoFetches++
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
"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}."
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2021-06-29 23:25:40 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else {
|
|
|
|
|
Twig.debug {
|
|
|
|
|
"Warning: gave up on fetching UTXOs for this session. It seems to unavailable on " +
|
|
|
|
|
"lightwalletd."
|
|
|
|
|
}
|
2021-06-29 23:25:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-13 04:36:24 -07:00
|
|
|
|
/**
|
|
|
|
|
* @return True in case of the UTXO processed successfully, false otherwise
|
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
internal suspend fun processUtxoResult(utxo: GetAddressUtxosReplyUnsafe): Boolean {
|
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.
|
2023-04-13 04:36:24 -07:00
|
|
|
|
Twig.debug { "Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi" }
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return try {
|
2023-04-13 04:36:24 -07:00
|
|
|
|
// TODO [#920]: Tweak RustBackend public APIs to have void return values.
|
|
|
|
|
// TODO [#920]: Thus, we don't need to check the boolean result of this call until fixed.
|
|
|
|
|
// TODO [#920]: https://github.com/zcash/zcash-android-wallet-sdk/issues/920
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.putUtxo(
|
2023-04-13 04:36:24 -07:00
|
|
|
|
utxo.address,
|
2023-04-14 03:55:51 -07:00
|
|
|
|
utxo.txid,
|
2023-04-13 04:36:24 -07:00
|
|
|
|
utxo.index,
|
2023-04-14 03:55:51 -07:00
|
|
|
|
utxo.script,
|
2023-04-13 04:36:24 -07:00
|
|
|
|
utxo.valueZat,
|
2023-05-18 04:36:15 -07:00
|
|
|
|
utxo.height
|
2023-04-13 04:36:24 -07:00
|
|
|
|
)
|
|
|
|
|
true
|
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
|
Twig.debug {
|
|
|
|
|
"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
|
|
|
|
}
|
2023-04-13 04:36:24 -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
|
|
|
|
|
false
|
2021-02-17 13:07:57 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
companion object {
|
|
|
|
|
/**
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* Default attempts at retrying.
|
|
|
|
|
*/
|
|
|
|
|
internal const val RETRIES = 5
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.
|
|
|
|
|
*/
|
|
|
|
|
internal const val MAX_REORG_SIZE = 100
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default size of batches of blocks to request from the compact block service. Then it's also used as a default
|
|
|
|
|
* size of batches of blocks to scan via librustzcash. The smaller this number the more granular information can
|
|
|
|
|
* be provided about scan state. Unfortunately, it may also lead to a lot of overhead during scanning.
|
|
|
|
|
*/
|
|
|
|
|
internal const val SYNC_BATCH_SIZE = 10
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover
|
|
|
|
|
* from the reorg but smaller than the theoretical max reorg size of 100.
|
|
|
|
|
*/
|
|
|
|
|
internal const val REWIND_DISTANCE = 10
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Requests, processes and persists all blocks from the given range.
|
2023-05-01 04:12:38 -07:00
|
|
|
|
*
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* @param backend the Rust backend component
|
|
|
|
|
* @param downloader the compact block downloader component
|
|
|
|
|
* @param repository the derived data repository component
|
|
|
|
|
* @param network the network in which the sync mechanism operates
|
|
|
|
|
* @param syncRange the range of blocks to download
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* @param withDownload the flag indicating whether the blocks should also be downloaded and processed, or
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* processed existing blocks
|
|
|
|
|
|
|
|
|
|
* @return Flow of BatchSyncProgress sync results
|
2023-05-01 04:12:38 -07:00
|
|
|
|
*/
|
|
|
|
|
@VisibleForTesting
|
2023-05-10 03:45:23 -07:00
|
|
|
|
@Suppress("LongParameterList", "LongMethod")
|
2023-05-01 04:12:38 -07:00
|
|
|
|
internal suspend fun syncNewBlocks(
|
|
|
|
|
backend: Backend,
|
|
|
|
|
downloader: CompactBlockDownloader,
|
|
|
|
|
repository: DerivedDataRepository,
|
|
|
|
|
network: ZcashNetwork,
|
|
|
|
|
syncRange: ClosedRange<BlockHeight>,
|
|
|
|
|
withDownload: Boolean
|
|
|
|
|
): Flow<BatchSyncProgress> = flow {
|
|
|
|
|
if (syncRange.isEmpty()) {
|
|
|
|
|
Twig.debug { "No blocks to sync" }
|
2023-05-10 03:45:23 -07:00
|
|
|
|
emit(
|
|
|
|
|
BatchSyncProgress(
|
|
|
|
|
percentage = PercentDecimal.ONE_HUNDRED_PERCENT,
|
|
|
|
|
lastSyncedHeight = getLastScannedHeight(repository),
|
|
|
|
|
result = BlockProcessingResult.Success
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.debug { "Syncing blocks in range $syncRange" }
|
|
|
|
|
|
|
|
|
|
val batches = getBatchedBlockList(syncRange, network)
|
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
batches.asFlow().map {
|
|
|
|
|
Twig.debug { "Syncing process starts for batch: $it" }
|
|
|
|
|
|
|
|
|
|
// Run downloading stage
|
|
|
|
|
SyncStageResult(
|
|
|
|
|
batch = it,
|
|
|
|
|
stageResult = if (withDownload) {
|
|
|
|
|
downloadBatchOfBlocks(
|
|
|
|
|
downloader = downloader,
|
|
|
|
|
batch = it
|
2023-05-01 04:12:38 -07:00
|
|
|
|
)
|
2023-05-10 03:45:23 -07:00
|
|
|
|
} else {
|
|
|
|
|
BlockProcessingResult.DownloadSuccess(null)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
)
|
|
|
|
|
}.buffer(1).map { downloadStageResult ->
|
|
|
|
|
Twig.debug { "Download stage done with result: $downloadStageResult" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
if (downloadStageResult.stageResult !is BlockProcessingResult.DownloadSuccess) {
|
|
|
|
|
// In case of any failure, we just propagate the result
|
|
|
|
|
downloadStageResult
|
|
|
|
|
} else {
|
|
|
|
|
// Enrich batch model with fetched blocks. It's useful for later blocks deletion
|
|
|
|
|
downloadStageResult.batch.blocks = downloadStageResult.stageResult.downloadedBlocks
|
|
|
|
|
|
|
|
|
|
// Run validation stage
|
|
|
|
|
SyncStageResult(
|
|
|
|
|
downloadStageResult.batch,
|
|
|
|
|
validateBatchOfBlocks(
|
|
|
|
|
backend = backend,
|
|
|
|
|
batch = downloadStageResult.batch
|
2023-05-01 04:12:38 -07:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
}.map { validateResult ->
|
|
|
|
|
Twig.debug { "Validation stage done with result: $validateResult" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
if (validateResult.stageResult != BlockProcessingResult.Success) {
|
|
|
|
|
validateResult
|
|
|
|
|
} else {
|
|
|
|
|
// Run scanning stage
|
|
|
|
|
SyncStageResult(
|
|
|
|
|
validateResult.batch,
|
|
|
|
|
scanBatchOfBlocks(
|
|
|
|
|
backend = backend,
|
|
|
|
|
batch = validateResult.batch
|
2023-05-01 04:12:38 -07:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
}.map { scanResult ->
|
|
|
|
|
Twig.debug { "Scan stage done with result: $scanResult" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
if (scanResult.stageResult != BlockProcessingResult.Success) {
|
|
|
|
|
scanResult
|
|
|
|
|
} else {
|
|
|
|
|
// Run deletion stage
|
|
|
|
|
SyncStageResult(
|
|
|
|
|
scanResult.batch,
|
|
|
|
|
deleteFilesOfBatchOfBlocks(
|
|
|
|
|
downloader = downloader,
|
|
|
|
|
batch = scanResult.batch
|
2023-05-01 04:12:38 -07:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
}.onEach { deleteResult ->
|
|
|
|
|
Twig.debug { "Deletion stage done with result: $deleteResult" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
BatchSyncProgress(
|
2023-05-10 03:45:23 -07:00
|
|
|
|
percentage = PercentDecimal(deleteResult.batch.index / batches.size.toFloat()),
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight = getLastScannedHeight(repository),
|
2023-05-10 03:45:23 -07:00
|
|
|
|
result = deleteResult.stageResult
|
2023-05-01 04:12:38 -07:00
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
)
|
2023-04-13 04:36:24 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
Twig.debug { "All sync stages done for the batch: ${deleteResult.batch}" }
|
|
|
|
|
}.takeWhile { continuousResult ->
|
|
|
|
|
continuousResult.stageResult == BlockProcessingResult.Success
|
|
|
|
|
}.collect()
|
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2023-04-13 04:36:24 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
private fun getBatchedBlockList(
|
|
|
|
|
syncRange: ClosedRange<BlockHeight>,
|
|
|
|
|
network: ZcashNetwork
|
2023-05-10 03:45:23 -07:00
|
|
|
|
): List<BlockBatch> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val missingBlockCount = syncRange.endInclusive.value - syncRange.start.value + 1
|
|
|
|
|
val batchCount = (
|
|
|
|
|
missingBlockCount / SYNC_BATCH_SIZE +
|
|
|
|
|
(if (missingBlockCount.rem(SYNC_BATCH_SIZE) == 0L) 0 else 1)
|
|
|
|
|
)
|
2023-04-13 04:36:24 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.debug {
|
|
|
|
|
"Found $missingBlockCount missing blocks, syncing in $batchCount batches of $SYNC_BATCH_SIZE..."
|
|
|
|
|
}
|
2023-02-06 14:36:28 -08:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
var start = syncRange.start
|
2023-05-10 03:45:23 -07:00
|
|
|
|
return buildList {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
for (index in 1..batchCount) {
|
|
|
|
|
val end = BlockHeight.new(
|
|
|
|
|
network,
|
|
|
|
|
min(
|
|
|
|
|
(syncRange.start.value + (index * SYNC_BATCH_SIZE)) - 1,
|
|
|
|
|
syncRange.endInclusive.value
|
|
|
|
|
)
|
|
|
|
|
) // subtract 1 on the first value because the range is inclusive
|
2023-02-06 14:36:28 -08:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
add(BlockBatch(index, start..end))
|
2023-05-01 04:12:38 -07:00
|
|
|
|
start = end + 1
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
/**
|
|
|
|
|
* Request and download all blocks in the given range and persist them locally for processing, later.
|
|
|
|
|
*
|
|
|
|
|
* @param batch the batch of blocks to download.
|
|
|
|
|
*/
|
|
|
|
|
@VisibleForTesting
|
|
|
|
|
@Throws(CompactBlockProcessorException.FailedDownload::class)
|
|
|
|
|
@Suppress("MagicNumber")
|
|
|
|
|
internal suspend fun downloadBatchOfBlocks(
|
|
|
|
|
downloader: CompactBlockDownloader,
|
2023-05-10 03:45:23 -07:00
|
|
|
|
batch: BlockBatch
|
2023-05-01 04:12:38 -07:00
|
|
|
|
): BlockProcessingResult {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
var downloadedBlocks = listOf<JniBlockMeta>()
|
2023-05-01 04:12:38 -07:00
|
|
|
|
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) { failedAttempts ->
|
|
|
|
|
if (failedAttempts == 0) {
|
|
|
|
|
Twig.verbose { "Starting to download batch $batch" }
|
|
|
|
|
} else {
|
|
|
|
|
Twig.verbose { "Retrying to download batch $batch after $failedAttempts failure(s)..." }
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
downloadedBlocks = downloader.downloadBlockRange(batch.range)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
Twig.verbose { "Successfully downloaded batch: $batch of $downloadedBlocks blocks" }
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
return if (downloadedBlocks.isNotEmpty()) {
|
|
|
|
|
BlockProcessingResult.DownloadSuccess(downloadedBlocks)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
BlockProcessingResult.FailedDownloadBlocks(batch.range.start)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
|
|
|
|
@VisibleForTesting
|
2023-05-10 03:45:23 -07:00
|
|
|
|
internal suspend fun validateBatchOfBlocks(batch: BlockBatch, backend: Backend): BlockProcessingResult {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.verbose { "Starting to validate batch $batch" }
|
|
|
|
|
|
2023-05-10 02:49:42 -07:00
|
|
|
|
val result = backend.validateCombinedChainOrErrorBlockHeight(batch.range.length())
|
2023-05-01 04:12:38 -07:00
|
|
|
|
|
|
|
|
|
return if (null == result) {
|
|
|
|
|
Twig.verbose { "Successfully validated batch $batch" }
|
|
|
|
|
BlockProcessingResult.Success
|
|
|
|
|
} else {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
BlockProcessingResult.FailedValidateBlocks(result)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
@VisibleForTesting
|
2023-05-10 03:45:23 -07:00
|
|
|
|
internal suspend fun scanBatchOfBlocks(batch: BlockBatch, backend: Backend): BlockProcessingResult {
|
2023-05-10 02:49:42 -07:00
|
|
|
|
val scanResult = backend.scanBlocks(batch.range.length())
|
2023-05-01 04:12:38 -07:00
|
|
|
|
return if (scanResult) {
|
|
|
|
|
Twig.verbose { "Successfully scanned batch $batch" }
|
|
|
|
|
BlockProcessingResult.Success
|
|
|
|
|
} else {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
BlockProcessingResult.FailedScanBlocks(batch.range.start)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
@VisibleForTesting
|
2023-05-10 03:45:23 -07:00
|
|
|
|
internal suspend fun deleteAllBlockFiles(
|
|
|
|
|
downloader: CompactBlockDownloader,
|
|
|
|
|
lastKnownHeight: BlockHeight
|
|
|
|
|
): BlockProcessingResult {
|
|
|
|
|
Twig.verbose { "Starting to delete all temporary block files" }
|
|
|
|
|
return if (downloader.compactBlockRepository.deleteAllCompactBlockFiles()) {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
Twig.verbose { "Successfully deleted all temporary block files" }
|
|
|
|
|
BlockProcessingResult.Success
|
|
|
|
|
} else {
|
2023-05-10 03:45:23 -07:00
|
|
|
|
BlockProcessingResult.FailedDeleteBlocks(lastKnownHeight)
|
2020-01-14 09:52:41 -08:00
|
|
|
|
}
|
2019-06-14 16:24:52 -07:00
|
|
|
|
}
|
2020-01-14 09:52:41 -08:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
@VisibleForTesting
|
|
|
|
|
internal suspend fun deleteFilesOfBatchOfBlocks(
|
|
|
|
|
batch: BlockBatch,
|
|
|
|
|
downloader: CompactBlockDownloader
|
|
|
|
|
): BlockProcessingResult {
|
|
|
|
|
Twig.verbose { "Starting to delete temporary block files from batch: $batch" }
|
|
|
|
|
|
|
|
|
|
return batch.blocks?.let { blocks ->
|
|
|
|
|
val deleted = downloader.compactBlockRepository.deleteCompactBlockFiles(blocks)
|
|
|
|
|
if (deleted) {
|
|
|
|
|
Twig.verbose { "Successfully deleted all temporary batched block files" }
|
|
|
|
|
BlockProcessingResult.Success
|
|
|
|
|
} else {
|
|
|
|
|
BlockProcessingResult.FailedDeleteBlocks(batch.range.start)
|
|
|
|
|
}
|
|
|
|
|
} ?: BlockProcessingResult.Success
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
/**
|
|
|
|
|
* Get the height of the last block that was scanned by this processor.
|
|
|
|
|
*
|
|
|
|
|
* @return the last scanned height reported by the repository.
|
|
|
|
|
*/
|
|
|
|
|
@VisibleForTesting
|
|
|
|
|
internal suspend fun getLastScannedHeight(repository: DerivedDataRepository) =
|
|
|
|
|
repository.lastScannedHeight()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the height of the last block that was downloaded by this processor.
|
|
|
|
|
*
|
|
|
|
|
* @return the last downloaded height reported by the downloader.
|
|
|
|
|
*/
|
|
|
|
|
internal suspend fun getLastDownloadedHeight(downloader: CompactBlockDownloader) =
|
|
|
|
|
downloader.getLastDownloadedHeight()
|
|
|
|
|
|
|
|
|
|
// CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
|
|
|
|
|
// access to the RustBackend live. This should be refactored.
|
|
|
|
|
internal suspend fun createAccount(rustBackend: Backend, seed: ByteArray): UnifiedSpendingKey =
|
|
|
|
|
rustBackend.createAccountAndGetSpendingKey(seed)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the current unified address for the given wallet account.
|
|
|
|
|
*
|
|
|
|
|
* @return the current unified address of this account.
|
|
|
|
|
*/
|
|
|
|
|
internal suspend fun getCurrentAddress(rustBackend: Backend, account: Account) =
|
|
|
|
|
rustBackend.getCurrentAddress(account)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the legacy Sapling address corresponding to the current unified address for the given wallet account.
|
|
|
|
|
*
|
|
|
|
|
* @return a Sapling address.
|
|
|
|
|
*/
|
|
|
|
|
internal suspend fun getLegacySaplingAddress(rustBackend: Backend, account: Account) =
|
|
|
|
|
rustBackend.getSaplingReceiver(
|
|
|
|
|
rustBackend.getCurrentAddress(account)
|
|
|
|
|
)
|
|
|
|
|
?: throw InitializeException.MissingAddressException("legacy Sapling")
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the legacy transparent address corresponding to the current unified address for the given wallet account.
|
|
|
|
|
*
|
|
|
|
|
* @return a transparent address.
|
|
|
|
|
*/
|
|
|
|
|
internal suspend fun getTransparentAddress(rustBackend: Backend, account: Account) =
|
|
|
|
|
rustBackend.getTransparentReceiver(
|
|
|
|
|
rustBackend.getCurrentAddress(account)
|
|
|
|
|
)
|
|
|
|
|
?: throw InitializeException.MissingAddressException("legacy transparent")
|
|
|
|
|
}
|
2021-03-31 05:51:53 -07:00
|
|
|
|
|
2020-01-14 09:52:41 -08:00
|
|
|
|
/**
|
2020-02-27 00:25:07 -08:00
|
|
|
|
* Emit an instance of processorInfo, corresponding to the provided data.
|
|
|
|
|
*
|
|
|
|
|
* @param networkBlockHeight the latest block available to lightwalletd that may or may not be
|
|
|
|
|
* downloaded by this wallet yet.
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* @param lastSyncedHeight the height up to which the wallet last synced. This determines
|
|
|
|
|
* where the next sync will begin.
|
|
|
|
|
* @param lastSyncRange the inclusive range to sync. This represents what we most recently
|
|
|
|
|
* wanted to sync. In most cases, it will be an invalid range because we'd like to sync blocks
|
2020-01-14 09:52:41 -08:00
|
|
|
|
* that we don't yet have.
|
|
|
|
|
*/
|
2023-05-10 03:45:23 -07:00
|
|
|
|
private fun updateProgress(
|
2023-05-01 04:12:38 -07:00
|
|
|
|
networkBlockHeight: BlockHeight? = _processorInfo.value.networkBlockHeight,
|
|
|
|
|
lastSyncedHeight: BlockHeight? = _processorInfo.value.lastSyncedHeight,
|
|
|
|
|
lastSyncRange: ClosedRange<BlockHeight>? = _processorInfo.value.lastSyncRange,
|
2022-07-25 11:47:58 -07:00
|
|
|
|
) {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
_networkHeight.value = networkBlockHeight
|
|
|
|
|
_processorInfo.value = ProcessorInfo(
|
2020-01-14 09:52:41 -08:00
|
|
|
|
networkBlockHeight = networkBlockHeight,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight = lastSyncedHeight,
|
|
|
|
|
lastSyncRange = lastSyncRange
|
2020-01-14 09:52:41 -08: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 ->
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "handling chain error at $errorHeight by rewinding to block $lowerBound" }
|
2020-06-09 19:00:41 -07:00
|
|
|
|
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)
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.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() {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val height = max(_processorInfo.value.lastSyncedHeight, 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
|
2023-05-01 04:12:38 -07:00
|
|
|
|
) {
|
|
|
|
|
processingMutex.withLockLogged("rewindToHeight") {
|
|
|
|
|
val lastSyncedHeight = _processorInfo.value.lastSyncedHeight
|
|
|
|
|
val lastLocalBlock = repository.lastScannedHeight()
|
|
|
|
|
val targetHeight = getNearestRewindHeight(height)
|
|
|
|
|
|
|
|
|
|
Twig.debug {
|
|
|
|
|
"Rewinding from $lastSyncedHeight to requested height: $height using target height: " +
|
|
|
|
|
"$targetHeight with last local block: $lastLocalBlock"
|
|
|
|
|
}
|
2022-08-23 06:49:00 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
if (null == lastSyncedHeight && targetHeight < lastLocalBlock) {
|
|
|
|
|
Twig.debug { "Rewinding because targetHeight is less than lastLocalBlock." }
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.rewindToHeight(targetHeight)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else if (null != lastSyncedHeight && targetHeight < lastSyncedHeight) {
|
|
|
|
|
Twig.debug { "Rewinding because targetHeight is less than lastSyncedHeight." }
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.rewindToHeight(targetHeight)
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
"Not rewinding dataDb because the last synced height is $lastSyncedHeight and the" +
|
|
|
|
|
" last local block is $lastLocalBlock both of which are less than the target height of " +
|
|
|
|
|
"$targetHeight"
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2023-05-01 04:12:38 -07:00
|
|
|
|
}
|
2022-08-23 06:49:00 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val currentNetworkBlockHeight = _processorInfo.value.networkBlockHeight
|
|
|
|
|
|
|
|
|
|
if (alsoClearBlockCache) {
|
|
|
|
|
Twig.debug {
|
|
|
|
|
"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+ second
|
|
|
|
|
// because we only download on 20s time boundaries so we can't trigger any immediate action
|
|
|
|
|
setState(State.Syncing)
|
|
|
|
|
if (null == currentNetworkBlockHeight) {
|
|
|
|
|
updateProgress(
|
|
|
|
|
lastSyncedHeight = targetHeight,
|
|
|
|
|
lastSyncRange = null
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
} else {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
updateProgress(
|
|
|
|
|
lastSyncedHeight = targetHeight,
|
|
|
|
|
lastSyncRange = (targetHeight + 1)..currentNetworkBlockHeight
|
|
|
|
|
)
|
|
|
|
|
}
|
2023-05-10 03:45:23 -07:00
|
|
|
|
_progress.value = PercentDecimal.ZERO_PERCENT
|
2023-05-01 04:12:38 -07:00
|
|
|
|
} else {
|
|
|
|
|
if (null == currentNetworkBlockHeight) {
|
|
|
|
|
updateProgress(
|
|
|
|
|
lastSyncedHeight = targetHeight,
|
|
|
|
|
lastSyncRange = null
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
updateProgress(
|
|
|
|
|
lastSyncedHeight = targetHeight,
|
|
|
|
|
lastSyncRange = (targetHeight + 1)..currentNetworkBlockHeight
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2021-04-05 15:37:13 -07:00
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
_progress.value = PercentDecimal.ZERO_PERCENT
|
2022-07-12 05:40:09 -07:00
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
if (null != lastSyncedHeight) {
|
|
|
|
|
val range = (targetHeight + 1)..lastSyncedHeight
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 04:12:38 -07:00
|
|
|
|
syncBlocksAndEnhanceTransactions(
|
|
|
|
|
syncRange = range,
|
|
|
|
|
withDownload = false
|
|
|
|
|
)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2021-04-09 18:25:21 -07:00
|
|
|
|
}
|
2020-06-09 19:00:41 -07:00
|
|
|
|
}
|
2023-05-01 04:12:38 -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
|
2023-03-08 07:04:04 -08:00
|
|
|
|
if (!BuildConfig.DEBUG) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2020-06-09 19:00:41 -07:00
|
|
|
|
|
|
|
|
|
var errorInfo = fetchValidationErrorInfo(errorHeight)
|
2023-03-08 07:04:04 -08:00
|
|
|
|
Twig.debug { "validation failed at block ${errorInfo.errorHeight} with hash: ${errorInfo.hash}" }
|
|
|
|
|
|
2020-06-09 19:00:41 -07:00
|
|
|
|
errorInfo = fetchValidationErrorInfo(errorHeight + 1)
|
2023-03-08 07:04:04 -08:00
|
|
|
|
Twig.debug { "the next block is ${errorInfo.errorHeight} with hash: ${errorInfo.hash}" }
|
2020-06-09 19:00:41 -07:00
|
|
|
|
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "=================== 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
|
2023-03-08 07:04:04 -08:00
|
|
|
|
// the hash another way.
|
|
|
|
|
val checkedHash = block?.hash ?: repository.findBlockHash(height)
|
|
|
|
|
Twig.debug { "block: $height\thash=${checkedHash?.toHexReversed()}" }
|
2020-06-09 19:00:41 -07:00
|
|
|
|
}
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "=================== 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 {
|
2023-03-08 07:04:04 -08:00
|
|
|
|
val hash = repository.findBlockHash(errorHeight + 1)?.toHexReversed()
|
2020-06-09 19:00:41 -07:00
|
|
|
|
|
2023-03-08 07:04:04 -08:00
|
|
|
|
return ValidationErrorInfo(errorHeight, hash)
|
2020-06-09 19:00:41 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug {
|
2022-08-23 06:49:00 -07:00
|
|
|
|
"offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = " +
|
|
|
|
|
"$offset"
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
|
|
|
|
Twig.debug { "lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it" }
|
2020-01-15 04:10:22 -08:00
|
|
|
|
}
|
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)
|
|
|
|
|
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) {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug(t) { "failed to calculate birthday" }
|
2021-03-31 05:47:04 -07:00
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
return buildList<BlockHeight> {
|
|
|
|
|
add(lowerBoundHeight)
|
2023-05-18 04:36:15 -07:00
|
|
|
|
add(backend.network.saplingActivationHeight)
|
2022-07-12 05:40:09 -07:00
|
|
|
|
oldestTransactionHeight?.let { add(it) }
|
|
|
|
|
}.maxOf { it }
|
2021-03-31 05:47:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-01 13:25:28 -07:00
|
|
|
|
/**
|
2023-01-09 07:42:38 -08:00
|
|
|
|
* Calculates the latest balance info.
|
2019-11-01 13:25:28 -07:00
|
|
|
|
*
|
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
|
|
|
|
*/
|
2023-02-06 14:36:28 -08:00
|
|
|
|
suspend fun getBalanceInfo(account: Account): WalletBalance {
|
|
|
|
|
@Suppress("TooGenericExceptionCaught")
|
|
|
|
|
return try {
|
2023-05-18 04:36:15 -07:00
|
|
|
|
val balanceTotal = backend.getBalance(account)
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "found total balance: $balanceTotal" }
|
2023-05-18 04:36:15 -07:00
|
|
|
|
val balanceAvailable = backend.getVerifiedBalance(account)
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "found available balance: $balanceAvailable" }
|
|
|
|
|
WalletBalance(balanceTotal, balanceAvailable)
|
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
|
Twig.debug { "failed to get balance due to $t" }
|
|
|
|
|
throw RustLayerException.BalanceException(t)
|
2019-11-01 13:25:28 -07:00
|
|
|
|
}
|
2023-02-06 14:36:28 -08:00
|
|
|
|
}
|
2019-11-01 13:25:28 -07:00
|
|
|
|
|
2022-07-12 05:40:09 -07:00
|
|
|
|
suspend fun getUtxoCacheBalance(address: String): WalletBalance =
|
2023-05-18 04:36:15 -07:00
|
|
|
|
backend.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.
|
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
interface IConnected
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Marker interface for [State] instances that represent when the wallet is syncing.
|
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
interface ISyncing
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
|
|
/**
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* [State] for common syncing stage. It starts with downloading new blocks, then validating these blocks
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* and scanning them at the end.
|
|
|
|
|
*
|
|
|
|
|
* **Downloading** is when the wallet is actively downloading compact blocks because the latest
|
2020-02-27 00:25:07 -08:00
|
|
|
|
* 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.
|
2023-05-01 04:12:38 -07:00
|
|
|
|
*
|
|
|
|
|
* **Validating** is when the blocks that have been downloaded are actively being validated to
|
2020-02-27 00:25:07 -08:00
|
|
|
|
* 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.
|
2023-05-01 04:12:38 -07:00
|
|
|
|
*
|
|
|
|
|
* **Scanning** is when the blocks that have been downloaded are actively being decrypted.
|
2020-02-27 00:25:07 -08:00
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
object Syncing : IConnected, ISyncing, State()
|
2020-02-27 00:25:07 -08:00
|
|
|
|
|
|
|
|
|
/**
|
2023-05-10 03:45:23 -07:00
|
|
|
|
* [State] for when we are done with syncing the blocks, for now, i.e. all necessary stages done (download,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* validate, and scan).
|
2020-02-27 00:25:07 -08:00
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
class Synced(val syncedRange: ClosedRange<BlockHeight>?) : IConnected, ISyncing, 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.
|
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
object Enhancing : IConnected, ISyncing, State()
|
2020-03-25 14:58:08 -07:00
|
|
|
|
|
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
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
/**
|
|
|
|
|
* Progress model class for sharing the whole batch sync progress out of the sync process.
|
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
internal data class BatchSyncProgress(
|
2023-05-10 03:45:23 -07:00
|
|
|
|
val percentage: PercentDecimal,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val lastSyncedHeight: BlockHeight?,
|
|
|
|
|
val result: BlockProcessingResult
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-10 03:45:23 -07:00
|
|
|
|
/**
|
|
|
|
|
* Progress model class for sharing particular sync stage result internally in the sync process.
|
|
|
|
|
*/
|
|
|
|
|
private data class SyncStageResult(
|
|
|
|
|
val batch: BlockBatch,
|
|
|
|
|
val stageResult: BlockProcessingResult
|
|
|
|
|
)
|
|
|
|
|
|
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.
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* @param lastSyncedHeight the height up to which the wallet last synced. This determines
|
|
|
|
|
* where the next sync will begin.
|
|
|
|
|
* @param lastSyncRange inclusive range to sync. Meaning, if the range is 10..10,
|
2020-01-14 09:52:41 -08:00
|
|
|
|
* then we will download exactly block 10. If the range is 11..10, then we want to download
|
|
|
|
|
* block 11 but can't.
|
|
|
|
|
*/
|
|
|
|
|
data class ProcessorInfo(
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val networkBlockHeight: BlockHeight?,
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val lastSyncedHeight: BlockHeight?,
|
|
|
|
|
val lastSyncRange: 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 ||
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight != null ||
|
|
|
|
|
lastSyncRange != null
|
2020-02-25 23:43:27 -08:00
|
|
|
|
|
|
|
|
|
/**
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* Determines whether this instance is actively syncing compact blocks.
|
2020-02-27 00:25:07 -08:00
|
|
|
|
*
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* @return true when there are more than zero blocks remaining to sync.
|
2020-02-25 23:43:27 -08:00
|
|
|
|
*/
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val isSyncing: Boolean
|
2022-07-12 05:40:09 -07:00
|
|
|
|
get() =
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight != null &&
|
|
|
|
|
lastSyncRange != null &&
|
|
|
|
|
!lastSyncRange.isEmpty() &&
|
|
|
|
|
lastSyncedHeight < lastSyncRange.endInclusive
|
2020-02-25 23:43:27 -08:00
|
|
|
|
|
|
|
|
|
/**
|
2023-05-01 04:12:38 -07:00
|
|
|
|
* The amount of sync progress from 0 to 100.
|
2020-02-25 23:43:27 -08:00
|
|
|
|
*/
|
2022-08-23 06:49:00 -07:00
|
|
|
|
@Suppress("MagicNumber")
|
2023-05-01 04:12:38 -07:00
|
|
|
|
val syncProgress
|
2022-07-12 05:40:09 -07:00
|
|
|
|
get() = when {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncedHeight == null -> 0
|
|
|
|
|
lastSyncRange == null -> 100
|
|
|
|
|
lastSyncedHeight >= lastSyncRange.endInclusive -> 100
|
2022-07-12 05:40:09 -07:00
|
|
|
|
else -> {
|
2023-05-01 04:12:38 -07:00
|
|
|
|
// when lastSyncedHeight == lastSyncedRange.first, we have synced one block, thus the offsets
|
|
|
|
|
val blocksSynced =
|
|
|
|
|
(lastSyncedHeight.value - lastSyncRange.start.value + 1).coerceAtLeast(0)
|
|
|
|
|
// we sync the range inclusively so 100..100 is one block to sync, thus the offset
|
2022-07-12 05:40:09 -07:00
|
|
|
|
val numberOfBlocks =
|
2023-05-01 04:12:38 -07:00
|
|
|
|
lastSyncRange.endInclusive.value - lastSyncRange.start.value + 1
|
2022-07-12 05:40:09 -07:00
|
|
|
|
// take the percentage then convert and round
|
2023-05-01 04:12:38 -07:00
|
|
|
|
((blocksSynced.toFloat() / numberOfBlocks) * 100.0f).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,
|
2023-03-08 07:04:04 -08:00
|
|
|
|
val hash: String?
|
2020-06-09 19:00:41 -07:00
|
|
|
|
)
|
|
|
|
|
|
2023-04-06 23:12:25 -07:00
|
|
|
|
//
|
|
|
|
|
// Helper Extensions
|
|
|
|
|
//
|
2021-03-31 06:07:37 -07:00
|
|
|
|
|
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 {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "$name MUTEX: acquiring lock..." }
|
2021-04-09 18:25:21 -07:00
|
|
|
|
this.withLock {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "$name MUTEX: ...lock acquired!" }
|
2021-04-09 18:25:21 -07:00
|
|
|
|
return block().also {
|
2023-02-06 14:36:28 -08:00
|
|
|
|
Twig.debug { "$name MUTEX: releasing lock" }
|
2021-04-09 18:25:21 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-07-12 05:40:09 -07:00
|
|
|
|
}
|
2021-04-09 18:25:21 -07:00
|
|
|
|
|
2023-02-01 02:14:55 -08:00
|
|
|
|
private fun LightWalletEndpointInfoUnsafe.matchingNetwork(network: String): Boolean {
|
|
|
|
|
fun String.toId() = lowercase(Locale.ROOT).run {
|
|
|
|
|
when {
|
|
|
|
|
contains("main") -> "mainnet"
|
|
|
|
|
contains("test") -> "testnet"
|
|
|
|
|
else -> this
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return chainName.toId() == network.toId()
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|