Implement the logic for changing servers.

This commit is contained in:
Kevin Gorham 2020-09-23 11:12:49 -04:00
parent 24f7433f1c
commit ab5b34cbd8
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
4 changed files with 153 additions and 47 deletions

View File

@ -1,8 +1,15 @@
package cash.z.ecc.android.sdk.block
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.tryCatchWith
import cash.z.ecc.android.sdk.ext.tryWarn
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
@ -11,13 +18,20 @@ import kotlinx.coroutines.withContext
* these dependencies, the downloader remains agnostic to the particular implementation of how to retrieve and store
* data; although, by default the SDK uses gRPC and SQL.
*
* @property lightwalletService the service used for requesting compact blocks
* @property lightWalletService the service used for requesting compact blocks
* @property compactBlockStore responsible for persisting the compact blocks that are received
*/
open class CompactBlockDownloader(
val lightwalletService: LightWalletService,
val compactBlockStore: CompactBlockStore
) {
open class CompactBlockDownloader private constructor(val compactBlockStore: CompactBlockStore) {
lateinit var lightWalletService: LightWalletService
private set
constructor(
lightWalletService: LightWalletService,
compactBlockStore: CompactBlockStore
) : this(compactBlockStore) {
this.lightWalletService = lightWalletService
}
/**
* Requests the given range of blocks from the lightwalletService and then persists them to the
@ -29,7 +43,7 @@ open class CompactBlockDownloader(
* @return the number of blocks that were returned in the results from the lightwalletService.
*/
suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) {
val result = lightwalletService.getBlockRange(heightRange)
val result = lightWalletService.getBlockRange(heightRange)
compactBlockStore.write(result)
result.size
}
@ -50,7 +64,7 @@ open class CompactBlockDownloader(
* @return the latest block height.
*/
suspend fun getLatestBlockHeight() = withContext(IO) {
lightwalletService.getLatestBlockHeight()
lightWalletService.getLatestBlockHeight()
}
/**
@ -63,14 +77,42 @@ open class CompactBlockDownloader(
}
suspend fun getServerInfo(): Service.LightdInfo = withContext(IO) {
lightwalletService.getServerInfo()
lightWalletService.getServerInfo()
}
suspend fun changeService(
newService: LightWalletService,
errorHandler: (Throwable) -> Unit = { throw it }
) = withContext(IO) {
try {
val existing = lightWalletService.getServerInfo()
val new = newService.getServerInfo()
val nonMatching = existing.essentialPropertyDiff(new)
if (nonMatching.size > 0) {
errorHandler(
LightWalletException.ChangeServerException.ChainInfoNotMatching(
nonMatching.joinToString(),
existing,
new
)
)
}
gracefullyShutdown(lightWalletService)
lightWalletService = newService
} catch (s: StatusRuntimeException) {
errorHandler(LightWalletException.ChangeServerException.StatusException(s.status))
} catch (t: Throwable) {
errorHandler(t)
}
}
/**
* Stop this downloader and cleanup any resources being used.
*/
fun stop() {
lightwalletService.shutdown()
lightWalletService.shutdown()
compactBlockStore.close()
}
@ -79,7 +121,35 @@ open class CompactBlockDownloader(
*
* @return the full transaction info.
*/
fun fetchTransaction(txId: ByteArray) = lightwalletService.fetchTransaction(txId)
fun fetchTransaction(txId: ByteArray) = lightWalletService.fetchTransaction(txId)
//
// Convenience functions
//
private suspend fun CoroutineScope.gracefullyShutdown(service: LightWalletService) = launch {
delay(2_000L)
tryWarn("Warning: error while shutting down service") {
service.shutdown()
}
}
/**
* Return a list of critical properties that do not match.
*/
private fun Service.LightdInfo.essentialPropertyDiff(other: Service.LightdInfo) =
mutableListOf<String>().also {
if (!consensusBranchId.equals(other.consensusBranchId, true)) {
it.add("consensusBranchId")
}
if (saplingActivationHeight != other.saplingActivationHeight) {
it.add("saplingActivationHeight")
}
if (!chainName.equals(other.chainName, true)) {
it.add("chainName")
}
}
}

View File

@ -1,5 +1,9 @@
package cash.z.ecc.android.sdk.exception
import cash.z.wallet.sdk.rpc.Service
import io.grpc.Status
import io.grpc.Status.Code.UNAVAILABLE
/**
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
@ -113,21 +117,40 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
/**
* Exceptions thrown while interacting with lightwalletd.
*/
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object InsecureConnection : LightwalletException("Error: attempted to connect to lightwalletd" +
sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object InsecureConnection : LightWalletException("Error: attempted to connect to lightwalletd" +
" with an insecure connection! Plaintext connections are only allowed when the" +
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
" because this choice should be explicit.")
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
LightwalletException(
LightWalletException(
"Error: the lightwalletd server is using a consensus branch" +
" (branch: $lwdBranch) that does not match the transactions being created" +
" (branch: $sdkBranch). This probably means the SDK and Server are on two" +
" different chains, most likely because of a recent network upgrade (NU). Either" +
" update the SDK to match lightwalletd or use a lightwalletd that matches the SDK."
)
open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class ChainInfoNotMatching(val propertyNames: String, val expectedInfo: Service.LightdInfo, val actualInfo: Service.LightdInfo) : ChangeServerException(
"Server change error: the $propertyNames values did not match."
)
class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) {
companion object {
private fun Status.toMessage(): String {
return when(this.code) {
UNAVAILABLE -> {
"Error: the new server is unavailable. Verify that the host and port are correct. Failed with $this"
}
else -> "Changing servers failed with status $this"
}
}
}
}
}
}
/**
* Potentially user-facing exceptions thrown while encoding transactions.
*/

View File

@ -2,7 +2,8 @@ package cash.z.ecc.android.sdk.service
import android.content.Context
import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.exception.LightwalletException
import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.ext.ZcashSdk.DEFAULT_LIGHTWALLETD_PORT
import cash.z.ecc.android.sdk.ext.twig
import cash.z.wallet.sdk.rpc.CompactFormats
@ -18,18 +19,19 @@ import java.util.concurrent.TimeUnit
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
*
* @property channel the channel to use for communicating with the lightwalletd server.
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub is
* created, it will use a deadline that is after the given duration from now.
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub is
* created for streaming requests, it will use a deadline that is after the given duration from now.
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub
* is created, it will use a deadline that is after the given duration from now.
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub
* is created for streaming requests, it will use a deadline that is after the given duration from
* now.
*/
@OpenForTesting
class LightWalletGrpcService private constructor(
var channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L,
private val streamingRequestTimeoutSec: Long = 90L
) : LightWalletService {
//TODO: find a better way to do this, maybe change the constructor to keep the properties
lateinit var connectionInfo: ConnectionInfo
/**
@ -46,7 +48,8 @@ class LightWalletGrpcService private constructor(
appContext: Context,
host: String,
port: Int = DEFAULT_LIGHTWALLETD_PORT,
usePlaintext: Boolean = appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
usePlaintext: Boolean =
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
) : this(createDefaultChannel(appContext, host, port, usePlaintext)) {
connectionInfo = ConnectionInfo(appContext.applicationContext, host, port, usePlaintext)
}
@ -57,17 +60,20 @@ class LightWalletGrpcService private constructor(
if (heightRange.isEmpty()) return listOf()
channel.resetConnectBackoff()
return channel.createStub(streamingRequestTimeoutSec).getBlockRange(heightRange.toBlockRange()).toList()
return channel.createStub(streamingRequestTimeoutSec)
.getBlockRange(heightRange.toBlockRange()).toList()
}
override fun getLatestBlockHeight(): Int {
channel.resetConnectBackoff()
return channel.createStub(singleRequestTimeoutSec).getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
return channel.createStub(singleRequestTimeoutSec)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
}
override fun getServerInfo(): Service.LightdInfo {
channel.resetConnectBackoff()
return channel.createStub(singleRequestTimeoutSec).getLightdInfo(Service.Empty.newBuilder().build())
return channel.createStub(singleRequestTimeoutSec)
.getLightdInfo(Service.Empty.newBuilder().build())
}
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
@ -87,6 +93,7 @@ class LightWalletGrpcService private constructor(
}
override fun shutdown() {
twig("Shutting down channel")
channel.shutdown()
}
@ -94,7 +101,9 @@ class LightWalletGrpcService private constructor(
if (txId.isEmpty()) return null
channel.resetConnectBackoff()
return channel.createStub().getTransaction(Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build())
return channel.createStub().getTransaction(
Service.TxFilter.newBuilder().setHash(ByteString.copyFrom(txId)).build()
)
}
override fun getTAddressTransactions(
@ -112,8 +121,9 @@ class LightWalletGrpcService private constructor(
}
override fun reconnect() {
twig("closing existing channel and then reconnecting to" +
" ${connectionInfo.host}:${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}")
twig("closing existing channel and then reconnecting to ${connectionInfo.host}:" +
"${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}"
)
channel.shutdown()
channel = createDefaultChannel(
connectionInfo.appContext,
@ -128,12 +138,12 @@ class LightWalletGrpcService private constructor(
// Utilities
//
private fun Channel.createStub(timeoutSec: Long = 60L): CompactTxStreamerGrpc.CompactTxStreamerBlockingStub =
CompactTxStreamerGrpc
.newBlockingStub(this)
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
private fun Channel.createStub(timeoutSec: Long = 60L) = CompactTxStreamerGrpc
.newBlockingStub(this)
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
private inline fun Int.toBlockHeight(): Service.BlockID = Service.BlockID.newBuilder().setHeight(this.toLong()).build()
private inline fun Int.toBlockHeight(): Service.BlockID =
Service.BlockID.newBuilder().setHeight(this.toLong()).build()
private inline fun IntRange.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
@ -177,7 +187,9 @@ class LightWalletGrpcService private constructor(
.context(appContext)
.apply {
if (usePlaintext) {
if (!appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)) throw LightwalletException.InsecureConnection
if (!appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)) throw LightWalletException.InsecureConnection
usePlaintext()
} else {
useTransportSecurity()

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.sdk.service
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.Service
@ -9,6 +8,14 @@ import cash.z.wallet.sdk.rpc.Service
* calls because async concerns are handled at a higher level.
*/
interface LightWalletService {
/**
* Fetch the details of a known transaction.
*
* @return the full transaction info.
*/
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
/**
* Return the given range of blocks.
*
@ -46,13 +53,6 @@ interface LightWalletService {
*/
fun getServerInfo(): Service.LightdInfo
/**
* Submit a raw transaction.
*
* @return the response from the server.
*/
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
/**
* Gets all the transactions for a given t-address over the given range. In practice, this is
* effectively the same as an RPC call to a node that's running an insight server. The data is
@ -63,11 +63,10 @@ interface LightWalletService {
fun getTAddressTransactions(tAddress: String, blockHeightRange: IntRange): List<Service.RawTransaction>
/**
* Fetch the details of a known transaction.
*
* @return the full transaction info.
* Reconnect to the same or a different server. This is useful when the connection is
* unrecoverable. That might be time to switch to a mirror or just reconnect.
*/
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
fun reconnect()
/**
* Cleanup any connections when the service is shutting down and not going to be used again.
@ -75,8 +74,10 @@ interface LightWalletService {
fun shutdown()
/**
* Reconnect to the same or a different server. This is useful when the connection is
* unrecoverable. That might be time to switch to a mirror or just reconnect.
* Submit a raw transaction.
*
* @return the response from the server.
*/
fun reconnect()
fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse
}