New: Added darksidewalletd testing support.

Also modified tests so that they can compile again.
This commit is contained in:
Kevin Gorham 2020-06-09 22:11:07 -04:00
parent 5cc8a38a5f
commit c5ad25a5ce
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
22 changed files with 1577 additions and 149 deletions

View File

@ -150,12 +150,5 @@ class SampleCodeTest {
}
fun log(message: String?) = twig(message ?: "null")
private fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2)
for (b in this)
sb.append(String.format("%02x", b))
return sb.toString()
}
}
}

View File

@ -0,0 +1,89 @@
package cash.z.wallet.sdk.ext
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.ImportedWalletBirthdayStore
import cash.z.wallet.sdk.util.SimpleMnemonics
import kotlinx.coroutines.*
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
fun Initializer.import(
seedPhrase: String,
birthdayHeight: Int,
alias: String = ZcashSdk.DEFAULT_DB_NAME_PREFIX
) {
SimpleMnemonics().toSeed(seedPhrase.toCharArray()).let { seed ->
ImportedWalletBirthdayStore(context, birthdayHeight, alias).getBirthday().let {
import(seed, it, true, true)
}
}
}
open class ScopedTest(val defaultTimeout: Long = 2000L) {
protected lateinit var testScope: CoroutineScope
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(5, this.javaClass.simpleName)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit>{
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}

View File

@ -0,0 +1,96 @@
//package cash.z.wallet.sdk.integration
//
//import cash.z.wallet.sdk.ext.ScopedTest
//import cash.z.wallet.sdk.util.DarksideTestCoordinator
//import org.junit.Before
//import org.junit.BeforeClass
//import org.junit.Test
//
//class OutboundTransactionsTest : ScopedTest() {
//
// @Before
// fun beforeEachTest() {
// testCoordinator.clearUnminedTransactions()
// }
//
// @Test
// fun testSendIncrementsTransaction() {
// validator.validateTransactionCount(initialTxCount)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validatTransactionCount(initialTxCount + 1)
// }
//
// @Test
// fun testSendReducesBalance() {
// validator.validateBalance(initialBalance)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateBalanceLessThan(initialBalance)
// }
//
// @Test
// fun testTransactionPending() {
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateTransactionPending(testCoordinator.lastTransactionId)
// }
//
// @Test
// fun testTransactionConfirmations_1() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 1)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_9() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(8).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 9)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_10() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(9).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 10)
// validator.validateBalance(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionExpiration() {
// validator.validateBalance(initialBalance)
//
// // pending initially
// testCoordinator.sendTransaction(txAmount).awaitSync()
// val id = testCoordinator.lastTransactionId
// validator.validateTransactionPending(id)
//
// // still pending after 9 blocks
// testCoordinator.advanceBlocksBy(9).awaitSync()
// validator.validateTransactionPending(id)
// validator.validateBalanceLessThan(initialBalance)
//
// // expired after 10 blocks
// testCoordinator.advanceBlocksBy(1).awaitSync()
// validator.validateTransactionExpired(id)
//
// validator.validateBalance(initialBalance)
// }
//
//
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val initialBalance = 1.234
// private const val txAmount = 1.1
// private const val initialTxCount = 3
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator
// .enterTheDarkside()
// .resetBlocks(blocksUrl)
// }
// }
//}

View File

@ -0,0 +1,53 @@
//package cash.z.wallet.sdk.integration
//
//import cash.z.wallet.sdk.ext.ScopedTest
//import cash.z.wallet.sdk.util.DarksideTestCoordinator
//import org.junit.Assert.assertFalse
//import org.junit.Assert.assertTrue
//import org.junit.BeforeClass
//import org.junit.Test
//
//class ReorgBasicTest : ScopedTest() {
//
// private var callbackTriggered = false
//
// @Test
// fun testReorgChangesBlockHash() {
// testCoordinator.resetBlocks(blocksUrl)
// validator.validateBlockHash(targetHeight, targetHash)
// testCoordinator.updateBlocks(reorgUrl)
// validator.validateBlockHash(targetHeight, reorgHash)
// }
//
// @Test
// fun testReorgTriggersCallback() {
// callbackTriggered = false
// testCoordinator.resetBlocks(blocksUrl)
// testCoordinator.synchronizer.registerReorgListener(reorgCallback)
// assertFalse(callbackTriggered)
//
// testCoordinator.updateBlocks(reorgUrl).awaitSync()
// assertTrue(callbackTriggered)
// testCoordinator.synchronizer.unregisterReorgListener()
// }
//
// fun reorgCallback() {
// callbackTriggered = true
// }
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val reorgUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
// private const val targetHeight = 663250
// private const val targetHash = "tbd"
// private const val reorgHash = "tbd"
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator.enterTheDarkside()
// }
// }
//}

View File

@ -0,0 +1,240 @@
//package cash.z.wallet.sdk.integration
//
//import androidx.test.platform.app.InstrumentationRegistry
//import cash.z.wallet.sdk.Initializer
//import cash.z.wallet.sdk.SdkSynchronizer
//import cash.z.wallet.sdk.Synchronizer
//import cash.z.wallet.sdk.ext.ScopedTest
//import cash.z.wallet.sdk.ext.import
//import cash.z.wallet.sdk.ext.twig
//import cash.z.wallet.sdk.util.DarksideApi
//import io.grpc.StatusRuntimeException
//import kotlinx.coroutines.delay
//import kotlinx.coroutines.flow.filter
//import kotlinx.coroutines.flow.first
//import kotlinx.coroutines.flow.onEach
//import kotlinx.coroutines.runBlocking
//import org.junit.Assert.*
//import org.junit.Before
//import org.junit.BeforeClass
//import org.junit.Test
//
//class ReorgHandlingTest : ScopedTest() {
//
// @Before
// fun setup() {
// timeout(30_000L) {
// synchronizer.awaitSync()
// }
// }
//
// @Test
// fun testBeforeReorg_minHeight() = timeout(30_000L) {
// // validate that we are synced, at least to the birthday height
// synchronizer.validateMinSyncHeight(birthdayHeight)
// }
//
// @Test
// fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// // validate that we are not synced beyond the target height
// synchronizer.validateMaxSyncHeight(targetHeight)
// }
//
// @Test
// fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
// val latestBlock = getBlock(targetHeight)
// assertEquals("foo", latestBlock.header.toStringUtf8())
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
//// @Test
//// fun testSync_100Blocks()= timeout(10_000L) {
//// // validate that we are synced below the target height, at first
//// synchronizer.validateMaxSyncHeight(targetHeight - 1)
//// // then trigger and await more blocks
//// synchronizer.awaitHeight(targetHeight)
//// // validate that we are above the target height afterward
//// synchronizer.validateMinSyncHeight(targetHeight)
//// }
//
// private fun Synchronizer.awaitSync() = runBlocking<Unit> {
// twig("*** Waiting for sync ***")
// status.onEach {
// twig("got processor status $it")
// assertTrue("Error: Cannot complete test because the server is disconnected.", it != Synchronizer.Status.DISCONNECTED)
// delay(1000)
// }.filter { it == Synchronizer.Status.SYNCED }.first()
// twig("*** Done waiting for sync! ***")
// }
//
// private fun Synchronizer.awaitHeight(height: Int) = runBlocking<Unit> {
// twig("*** Waiting for block $height ***")
//// processorInfo.first { it.lastScannedHeight >= height }
// processorInfo.onEach {
// twig("got processor info $it")
// delay(1000)
// }.first { it.lastScannedHeight >= height }
// twig("*** Done waiting for block $height! ***")
// }
//
// private fun Synchronizer.validateMinSyncHeight(minHeight: Int) = runBlocking<Unit> {
// val info = processorInfo.first()
// val lastDownloadedHeight = info.lastDownloadedHeight
// assertTrue("Expected to be synced beyond $minHeight but the last downloaded block was" +
// " $lastDownloadedHeight details: $info", lastDownloadedHeight >= minHeight)
// }
//
// private fun Synchronizer.validateMaxSyncHeight(maxHeight: Int) = runBlocking<Unit> {
// val lastDownloadedHeight = processorInfo.first().lastScannedHeight
// assertTrue("Did not expect to be synced beyond $maxHeight but we are synced to" +
// " $lastDownloadedHeight", lastDownloadedHeight <= maxHeight)
// }
//
// private fun getBlock(height: Int) =
// lightwalletd.getBlockRange(height..height).first()
//
// private val lightwalletd
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightwalletService
//
// companion object {
// private const val host = "192.168.1.134"
// private const val port = 9067
// private const val birthdayHeight = 663150
// private const val targetHeight = 663200
// private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
// private val context = InstrumentationRegistry.getInstrumentation().context
// private val initializer = Initializer(context, host, port, "ReorgHandlingTests")
// private lateinit var synchronizer: Synchronizer
// private lateinit var sithLord: DarksideApi
//
// @BeforeClass
// @JvmStatic
// fun startOnce() {
//
// sithLord = DarksideApi(context, host, port)
// enterTheDarkside()
//
// // don't start until after we enter the darkside (otherwise the we find no blocks to begin with and sleep for an interval)
// synchronizer.start(classScope)
// }
//
// private fun enterTheDarkside() = runBlocking<Unit> {
// // verify that we are on the darkside
// try {
// twig("entering the darkside")
// var info = synchronizer.getServerInfo()
// assertTrue(
// "Error: not on the darkside",
// info.chainName.contains("darkside")
// or info.vendor.toLowerCase().contains("darkside", true)
// )
// twig("initiating the darkside")
// sithLord.initiate(birthdayHeight + 10)
// info = synchronizer.getServerInfo()
// assertTrue(
// "Error: server not configured for the darkside. Expected initial height of" +
// " $birthdayHeight but found ${info.blockHeight}", birthdayHeight <= info.blockHeight)
// twig("darkside initiation complete!")
// } catch (error: StatusRuntimeException) {
// fail("Error while fetching server status. Testing cannot begin due to:" +
// " ${error.message}. Verify that the server is running")
// }
// }
// }
// /*
//
//beginning to process new blocks (with lower bound: 663050)...
//downloading blocks in range 663202..663202
//found 1 missing blocks, downloading in 1 batches of 100...
//downloaded 663202..663202 (batch 1 of 1) [663202..663202] | 10ms
//validating blocks in range 663202..663202 in db: /data/user/0/cash.z.wallet.sdk.test/databases/ReorgTest22_Cache.db
//offset = min(100, 10 * (1)) = 10
//lowerBound = max(663201 - 10, 663050) = 663191
//handling chain error at 663201 by rewinding to block 663191
//Chain error detected at height: 663201. Rewinding to: 663191
//beginning to process new blocks (with lower bound: 663050)...
//downloading blocks in range 663192..663202
//found 11 missing blocks, downloading in 1 batches of 100...
//downloaded 663192..663202 (batch 1 of 1) [663192..663202] | 8ms
//validating blocks in range 663192..663202 in db: /data/user/0/cash.z.wallet.sdk.test/databases/ReorgTest22_Cache.db
//offset = min(100, 10 * (2)) = 20
//lowerBound = max(663191 - 20, 663050) = 663171
//handling chain error at 663191 by rewinding to block 663171
//Chain error detected at height: 663191. Rewinding to: 663171
//beginning to process new blocks (with lower bound: 663050)...
//downloading blocks in range 663172..663202
//found 31 missing blocks, downloading in 1 batches of 100...
//downloaded 663172..663202 (batch 1 of 1) [663172..663202] | 15ms
//validating blocks in range 663172..663202 in db: /data/user/0/cash.z.wallet.sdk.test/databases/ReorgTest22_Cache.db
//scanning blocks for range 663172..663202 in batches
//batch scanned: 663202/663202
//batch scan complete!
//Successfully processed new blocks. Sleeping for 20000ms
//
// */
////
//// @Test
//// fun testHeightChange() {
//// setTargetHeight(targetHeight)
//// synchronizer.validateSyncedTo(targetHeight)
//// }
////
//// @Test
//// fun testSmallReorgSync() {
//// verifyReorgSync(smallReorgSize)
//// }
////
//// @Test
//// fun testSmallReorgCallback() {
//// verifyReorgCallback(smallReorgSize)
//// }
////
//// @Test
//// fun testLargeReorgSync() {
//// verifyReorgSync(largeReorgSize)
//// }
////
//// @Test
//// fun testLargeReorgCallback() {
//// verifyReorgCallback(largeReorgSize)
//// }
////
////
//// //
//// // Helper Functions
//// //
////
//// fun verifyReorgSync(reorgSize: Int) {
//// setTargetHeight(targetHeight)
//// synchronizer.validateSyncedTo(targetHeight)
//// getHash(targetHeight).let { initialHash ->
//// setReorgHeight(targetHeight - reorgSize)
//// synchronizer.validateSyncedTo(targetHeight)
//// assertNotEquals("Hash should change after a reorg", initialHash, getHash(targetHeight))
//// }
//// }
////
//// fun verifyReorgCallback(reorgSize: Int) {
//// setTargetHeight(targetHeight)
//// synchronizer.validateSyncedTo(targetHeight)
//// getHash(targetHeight).let { initialHash ->
//// setReorgHeight(targetHeight - 10)
//// synchronizer.validateReorgCallback()
//// }
//// }
//
//
//}
//

View File

@ -0,0 +1,45 @@
package cash.z.wallet.sdk.integration
import cash.z.wallet.sdk.ext.ScopedTest
import cash.z.wallet.sdk.util.DarksideTestCoordinator
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150
private val targetHeight = 663250
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_minHeight() = timeout(30_000L) {
// validate that we are synced, at least to the birthday height
validator.validateMinHeightDownloaded(birthdayHeight)
}
@Test
fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// validate that we are not synced beyond the target height
validator.validateMaxHeightScanned(targetHeight)
}
companion object {
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val validator = sithLord.validator
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
}
}
}

View File

@ -0,0 +1,60 @@
package cash.z.wallet.sdk.integration
import cash.z.wallet.sdk.ext.ScopedTest
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.util.DarksideTestCoordinator
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd"
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashBeforeReorg)
}
@Test
fun testAfterReorg_callbackTriggered() = timeout(30_000L) {
hadReorg = false
// sithLord.triggerSmallReorg()
sithLord.await()
twig("checking whether a reorg happened (spoiler: ${if (hadReorg) "yep" else "nope"})")
assertTrue(hadReorg)
}
@Test
fun testAfterReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashAfterReorg)
}
companion object {
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val validator = sithLord.validator
private var hadReorg = false
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
validator.onReorg { _, _ ->
hadReorg = true
}
sithLord.synchronizer.start(classScope)
}
}
}

View File

@ -0,0 +1,99 @@
package cash.z.wallet.sdk.integration.reorgs
import cash.z.wallet.sdk.ext.ScopedTest
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.util.DarksideTestCoordinator
import org.junit.BeforeClass
import org.junit.Test
class InboundTxTests : ScopedTest() {
@Test
fun testTargetBlock_downloaded() {
validator.validateMinHeightDownloaded(firstBlock)
}
@Test
fun testTargetBlock_scanned() {
validator.validateMinHeightScanned(targetTxBlock - 1)
}
@Test
fun testLatestHeight() {
validator.validateLatestHeight(targetTxBlock - 1)
}
@Test
fun testTxCountInitial() {
validator.validateTxCount(0)
}
@Test
fun testTxCountAfter() {
twig("ADDING TRANSACTIONS!!!")
// add 2 transactions to block 663188 and 'mine' that block
addTransactions(targetTxBlock, tx663174, tx663188)
sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
validator.validateTxCount(2)
}
private fun addTransactions(targetHeight: Int, vararg txs: String) {
val overwriteBlockCount = 5
chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
.stageTransactions(targetHeight, *txs)
.applyTipHeight(targetHeight)
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val tx663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"
private const val tx663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"
private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"
private val txSend = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"
)
private val txRecv = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
)
private const val firstBlock = 663150
private const val targetTxBlock = 663188
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val validator = sithLord.validator
private val chainMaker = sithLord.chainMaker
@BeforeClass
@JvmStatic
fun startAllTests() {
sithLord.enterTheDarkside()
chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1)
sithLord.startSync(classScope).await()
}
}
}

View File

@ -0,0 +1,70 @@
package cash.z.wallet.sdk.integration.reorgs
import cash.z.wallet.sdk.ext.ScopedTest
import cash.z.wallet.sdk.ext.toHex
import cash.z.wallet.sdk.util.DarksideTestCoordinator
import cash.z.wallet.sdk.util.SimpleMnemonics
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
class SetupTest : ScopedTest() {
// @Test
// fun testFirstBlockExists() {
// validator.validateHasBlock(
// firstBlock
// )
// }
//
// @Test
// fun testLastBlockExists() {
// validator.validateHasBlock(
// lastBlock
// )
// }
//
// @Test
// fun testLastBlockHash() {
// validator.validateBlockHash(
// lastBlock,
// lastBlockHash
// )
// }
@Test
fun tempTest() {
val phrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val result = SimpleMnemonics().toSeed(phrase.toCharArray()).toHex()
assertEquals("abc", result)
}
@Test
fun tempTest2() {
val s = SimpleMnemonics()
val ent = s.nextEntropy()
val phrase = s.nextMnemonic(ent)
assertEquals("a", "${ent.toHex()}|${String(phrase)}")
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val firstBlock = 663150
private const val lastBlock = 663200
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator("192.168.1.134")
private val validator = sithLord.validator
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord
// .enterTheDarkside()
// // TODO: fix this
//// .resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = lastBlock)
// .startSync(classScope)
// .await()
// }
}
}

View File

@ -1,25 +1,26 @@
package cash.z.wallet.sdk.db
package cash.z.wallet.sdk.util
//import cash.z.wallet.sdk.secure.Wallet
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.block.CompactBlockDbStore
import cash.z.wallet.sdk.block.CompactBlockDownloader
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.service.LightWalletGrpcService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import okio.Okio
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.IOException
import kotlin.properties.Delegates
/**
* A tool for checking transactions since the given birthday and printing balances. This was useful for the Zcon1 app to
@ -28,36 +29,47 @@ import kotlin.properties.Delegates
@ExperimentalCoroutinesApi
class BalancePrinterUtil {
private val host = "34.65.230.46"
private val host = "lightd-main.zecwallet.co"
private val port = 443
private val downloadBatchSize = 9_000
private val birthday = 523240
private val birthdayHeight = 523240
private val mnemonics = SimpleMnemonics()
private val context = InstrumentationRegistry.getInstrumentation().context
private val cacheDbName = "BalanceUtilCache.db"
private val dataDbName = "BalanceUtilData.db"
private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val alias = "BalanceUtil"
private val caceDbPath = Initializer.cacheDbPath(context, alias)
private val downloader = CompactBlockDownloader(
LightWalletGrpcService(context, host),
CompactBlockDbStore(context)
LightWalletGrpcService(context, host, port),
CompactBlockDbStore(context, caceDbPath)
)
// private val processor = CompactBlockProcessor(downloader)
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val initializer = Initializer(context, host, port, alias)
private lateinit var birthday: Initializer.WalletBirthday
private var synchronizer: Synchronizer? = null
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
cacheBlocks()
birthday = Initializer.DefaultBirthdayStore(context, birthdayHeight, alias).getBirthday()
}
private fun cacheBlocks() = runBlocking {
twig("downloading compact blocks...")
val latestBlockHeight = downloader.getLatestBlockHeight()
val lastDownloaded = downloader.getLastDownloadedHeight()
val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
downloadNewBlocks(blockRange)
val error = validateNewBlocks(blockRange)
twig("validation completed with result $error")
assertEquals(-1, error)
// twig("downloading compact blocks...")
// val latestBlockHeight = downloader.getLatestBlockHeight()
// val lastDownloaded = downloader.getLastDownloadedHeight()
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
// downloadNewBlocks(blockRange)
// val error = validateNewBlocks(blockRange)
// twig("validation completed with result $error")
// assertEquals(-1, error)
}
private fun deleteDb(dbName: String) {
@ -66,23 +78,65 @@ class BalancePrinterUtil {
@Test
fun printBalances() = runBlocking {
readLines().collect { seed ->
deleteDb(dataDbName)
initWallet(seed)
twig("scanning blocks for seed <$seed>")
rustBackend.scanBlocks()
twig("done scanning blocks for seed $seed")
val total = rustBackend.getBalance(0)
twig("found total: $total")
val available = rustBackend.getVerifiedBalance(0)
twig("found available: $available")
twig("xrxrx2\t$seed\t$total\t$available")
println("xrxrx2\t$seed\t$total\t$available")
readLines()
.map { seedPhrase ->
twig("checking balance for: $seedPhrase")
mnemonics.toSeed(seedPhrase.toCharArray())
}.collect { seed ->
initializer.import(seed, birthday, clearDataDb = true, clearCacheDb = false)
/*
what I need to do right now
- for each seed
- I can reuse the cache of blocks... so just like get the cache once
- I need to scan into a new database
- I don't really need a new rustbackend
- I definitely don't need a new grpc connection
- can I just use a processor and point it to a different DB?
+ so yeah, I think I need to use the processor directly right here and just swap out its pieces
- perhaps create a new initializer and use that to configure the processor?
- or maybe just set the data destination for the processor
- I might need to consider how state is impacting this design
- can we be more stateless and thereby improve the flexibility of this code?!!!
*/
synchronizer?.stop()
synchronizer = Synchronizer(initializer)
// deleteDb(dataDbPath)
// initWallet(seed)
// twig("scanning blocks for seed <$seed>")
//// rustBackend.scanBlocks()
// twig("done scanning blocks for seed $seed")
//// val total = rustBackend.getBalance(0)
// twig("found total: $total")
//// val available = rustBackend.getVerifiedBalance(0)
// twig("found available: $available")
// twig("xrxrx2\t$seed\t$total\t$available")
// println("xrxrx2\t$seed\t$total\t$available")
}
Thread.sleep(5000)
Thread.sleep(3000)
assertEquals("foo", "bar")
}
// @Test
// fun printBalances() = runBlocking {
// readLines().collect { seed ->
// deleteDb(dataDbName)
// initWallet(seed)
// twig("scanning blocks for seed <$seed>")
//// rustBackend.scanBlocks()
// twig("done scanning blocks for seed $seed")
//// val total = rustBackend.getBalance(0)
// twig("found total: $total")
//// val available = rustBackend.getVerifiedBalance(0)
// twig("found available: $available")
// twig("xrxrx2\t$seed\t$total\t$available")
// println("xrxrx2\t$seed\t$total\t$available")
// }
// Thread.sleep(5000)
// assertEquals("foo", "bar")
// }
@Throws(IOException::class)
fun readLines() = flow<String> {
@ -96,20 +150,20 @@ class BalancePrinterUtil {
}
}
private fun initWallet(seed: String): Wallet {
val spendingKeyProvider = Delegates.notNull<String>()
return Wallet(
context,
rustBackend,
SampleSeedProvider(seed),
spendingKeyProvider,
Wallet.loadBirthdayFromAssets(context, birthday)
).apply {
runCatching {
initialize()
}
}
}
// private fun initWallet(seed: String): Wallet {
// val spendingKeyProvider = Delegates.notNull<String>()
// return Wallet(
// context,
// rustBackend,
// SampleSeedProvider(seed),
// spendingKeyProvider,
// Wallet.loadBirthdayFromAssets(context, birthday)
// ).apply {
// runCatching {
// initialize()
// }
// }
// }
private fun downloadNewBlocks(range: IntRange) = runBlocking {
Twig.sprout("downloading")
@ -123,7 +177,7 @@ class BalancePrinterUtil {
val end = Math.min(range.first + (i * downloadBatchSize), range.last + 1)
val batchRange = downloadedBlockHeight until end
twig("downloaded $batchRange (batch $i of $batches)") {
downloader.downloadBlockRange(batchRange)
// downloader.downloadBlockRange(batchRange)
}
downloadedBlockHeight = end
@ -131,13 +185,13 @@ class BalancePrinterUtil {
Twig.clip("downloading")
}
private fun validateNewBlocks(range: IntRange?): Int {
val dummyWallet = initWallet("dummySeed")
Twig.sprout("validating")
twig("validating blocks in range $range")
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
return result
}
// private fun validateNewBlocks(range: IntRange?): Int {
//// val dummyWallet = initWallet("dummySeed")
// Twig.sprout("validating")
// twig("validating blocks in range $range")
//// val result = rustBackend.validateCombinedChain()
// Twig.clip("validating")
// return result
// }
}

View File

@ -0,0 +1,119 @@
package cash.z.wallet.sdk.util
import android.content.Context
import cash.z.wallet.sdk.R
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.rpc.Darkside
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
import cash.z.wallet.sdk.service.LightWalletGrpcService
import io.grpc.ManagedChannel
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class DarksideApi(
private val channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L
) {
constructor(
appContext: Context,
host: String,
port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
usePlainText: Boolean = appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
) : this(
LightWalletGrpcService.createDefaultChannel(
appContext,
host,
port,
usePlainText
)
)
//
// Service APIs
//
fun reset(
saplingActivationHeight: Int = 419200,
branchId: String = "2bb40e60", // Blossom,
chainName: String = "darkside"
) = apply {
twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight)
.build().let { request ->
createStub().reset(request)
}
}
fun stageBlocks(url: String) = apply {
twig("staging block url=$url")
createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: Int) = apply {
twig("staging transaction at height=$targetHeight from url=$url")
createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight).setUrl(url).build()
)
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight).setCount(count).setNonce(nonce).build()
)
}
fun applyBlocks(tipHeight: Int) {
twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight())
}
// fun setMetaState(
// branchId: String = "2bb40e60", // Blossom,
// chainName: String = "darkside",
// saplingActivationHeight: Int = 419200
// ): DarksideApi = apply {
// createStub().setMetaState(
// Darkside.DarksideMetaState.newBuilder()
// .setBranchID(branchId)
// .setChainName(chainName)
// .setSaplingActivation(saplingActivationHeight)
// .build()
// )
// }
// fun setLatestHeight(latestHeight: Int) = setState(latestHeight, reorgHeight)
//
// fun setReorgHeight(reorgHeight: Int)
// = setState(latestHeight.coerceAtLeast(reorgHeight), reorgHeight)
//
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
// this.latestHeight = latestHeight
// this.reorgHeight = reorgHeight
// // TODO: change this service to accept ints as heights, like everywhere else
// createStub().darksideSetState(
// Darkside.DarksideState.newBuilder()
// .setLatestHeight(latestHeight.toLong())
// .setReorgHeight(reorgHeight.toLong())
// .build()
// )
// return this
// }
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
DarksideStreamerGrpc
.newBlockingStub(channel)
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
private fun Int.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this).build()
}

View File

@ -0,0 +1,244 @@
package cash.z.wallet.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.ext.ScopedTest
import cash.z.wallet.sdk.ext.import
import cash.z.wallet.sdk.ext.twig
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
class DarksideTestCoordinator(val host: String = "127.0.0.1") {
private val port = 9067
private val birthdayHeight = 663150
private val targetHeight = 663250
private val seedPhrase =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
private val context = InstrumentationRegistry.getInstrumentation().context
// dependencies: private
private val initializer = Initializer(context, host, port, "DarksideTestCoordinator")
private lateinit var darkside: DarksideApi
// dependencies: public
val validator = DarksideTestValidator()
val chainMaker = DarksideChainMaker()
lateinit var synchronizer: Synchronizer
//
// High-level APIs
//
/**
* Setup dependencies, including the synchronizer and the darkside API connection
*/
fun enterTheDarkside(): DarksideTestCoordinator = runBlocking {
// verify that we are on the darkside
try {
twig("entering the darkside")
initiate()
synchronizer.getServerInfo().apply {
assertTrue(
"Error: not on the darkside",
vendor.contains("dark", true)
or chainName.contains("dark", true)
)
}
twig("darkside initiation complete!")
} catch (error: StatusRuntimeException) {
Assert.fail(
"Error while fetching server status. Testing cannot begin due to:" +
" ${error.message}. Verify that the server is running"
)
}
this@DarksideTestCoordinator
}
/**
* Setup the synchronizer and darksidewalletd with their initial state
*/
fun initiate() {
twig("*************** INITIALIZING TEST COORDINATOR (ONLY ONCE) ***********************")
initializer.import(
seedPhrase,
birthdayHeight
)
synchronizer = Synchronizer(initializer) as SdkSynchronizer
val channel = (synchronizer as SdkSynchronizer).channel
darkside = DarksideApi(channel)
}
// fun triggerSmallReorg() {
// darkside.setBlocksUrl(smallReorg)
// }
//
// fun triggerLargeReorg() {
// darkside.setBlocksUrl(largeReorg)
// }
fun startSync(scope: CoroutineScope): DarksideTestCoordinator = apply {
synchronizer.start(scope)
}
/**
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status.
*/
fun await(timeout: Long = 10_000L, targetHeight: Int = -1) = runBlocking {
ScopedTest.timeoutWith(this, timeout) {
twig("*** Waiting up to ${timeout / 1_000}s for sync ***")
synchronizer.status.onEach {
twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...")
} else if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("awaiting new blocks from server...")
}
}.map {
// whenever we're waiting for a target height, for simplicity, if we're sleeping,
// and in between polls, then consider it that we're not synced
if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING
} else {
it
}
}.filter { it == Synchronizer.Status.SYNCED }.first()
twig("*** Done waiting for sync! ***")
}
}
fun stall(delay: Long = 5000L) = runBlocking {
twig("*** Stalling for ${delay}ms ***")
delay(delay)
}
//
// Validation
//
inner class DarksideTestValidator {
fun validateHasBlock(height: Int) {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
}
fun validateLatestHeight(height: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight
assertTrue(
"Expected latestHeight of $height but the server last reported a height of" +
" $networkBlockHeight! Full details: $info", networkBlockHeight == height
)
}
fun validateMinHeightDownloaded(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastDownloadedHeight = info.lastDownloadedHeight
assertTrue(
"Expected to have at least downloaded $minHeight but the last downloaded block was" +
" $lastDownloadedHeight! Full details: $info", lastDownloadedHeight >= minHeight
)
}
fun validateMinHeightScanned(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastScannedHeight = info.lastScannedHeight
assertTrue(
"Expected to have at least scanned $minHeight but the last scanned block was" +
" $lastScannedHeight! Full details: $info", lastScannedHeight >= minHeight
)
}
fun validateMaxHeightScanned(maxHeight: Int) = runBlocking<Unit> {
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
assertTrue(
"Did not expect to be synced beyond $maxHeight but we are synced to" +
" $lastDownloadedHeight", lastDownloadedHeight <= maxHeight
)
}
fun validateBlockHash(height: Int, expectedHash: String) {
val hash = (synchronizer as SdkSynchronizer).findBlockHashAsHex(height)
assertEquals(expectedHash, hash)
}
fun onReorg(callback: (errorHeight: Int, rewindHeight: Int) -> Unit) {
synchronizer.onChainErrorHandler = callback
}
fun validateTxCount(count: Int) {
val txCount = (synchronizer as SdkSynchronizer).getTransactionCount()
assertEquals("Expected $count transactions but found $txCount instead!", count, txCount)
}
}
//
// Chain Creations
//
inner class DarksideChainMaker {
/**
* Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then
* applies those changes and waits for them to take effect.
*/
fun resetBlocks(
blocksUrl: String,
startHeight: Int = DEFAULT_START_HEIGHT,
tipHeight: Int = startHeight + 100
): DarksideChainMaker = apply {
darkside
.reset(startHeight)
.stageBlocks(blocksUrl)
}
fun stageTransaction(url: String, targetHeight: Int): DarksideChainMaker = apply {
darkside.stageTransactions(url, targetHeight)
}
fun stageTransactions(targetHeight: Int, vararg urls: String): DarksideChainMaker = apply {
urls.forEach {
darkside.stageTransactions(it, targetHeight)
}
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10): DarksideChainMaker = apply {
darkside.stageEmptyBlocks(startHeight, count)
}
fun applyTipHeight(tipHeight: Int): DarksideChainMaker = apply {
darkside.applyBlocks(tipHeight)
}
}
companion object {
// Block URLS
private const val beforeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val smallReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private const val DEFAULT_START_HEIGHT = 663150
}
}

View File

@ -1,82 +1,20 @@
package cash.z.wallet.sdk.util
import cash.z.android.plugin.MnemonicPlugin
import io.github.novacrypto.bip39.MnemonicGenerator
import io.github.novacrypto.bip39.SeedCalculator
import io.github.novacrypto.bip39.Words
import io.github.novacrypto.bip39.wordlists.English
import java.security.SecureRandom
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.*
class SimpleMnemonics : MnemonicPlugin {
override fun nextEntropy(): ByteArray {
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
SecureRandom().nextBytes(this)
}
}
override fun nextMnemonic(): CharArray {
return nextMnemonic(nextEntropy())
}
override fun nextMnemonic(entropy: ByteArray): CharArray {
return StringBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.toString().toCharArray()
}
}
override fun nextMnemonicList(): List<CharArray> {
return nextMnemonicList(nextEntropy())
}
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> {
return WordListBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.wordList
}
}
override fun toSeed(mnemonic: CharArray): ByteArray {
return SeedCalculator().calculateSeed(String(mnemonic), "")
}
override fun toWordList(mnemonic: CharArray): List<CharArray> {
val wordList = mutableListOf<CharArray>()
var cursor = 0
repeat(mnemonic.size) { i ->
val isSpace = mnemonic[i] == ' '
if (isSpace || i == (mnemonic.size - 1)) {
val wordSize = i - cursor + if (isSpace) 0 else 1
wordList.add(CharArray(wordSize).apply {
repeat(wordSize) {
this[it] = mnemonic[cursor + it]
}
})
cursor = i + 1
}
}
return wordList
}
class WordListBuilder {
val wordList = mutableListOf<CharArray>()
fun append(c: CharSequence) {
if (c[0] != English.INSTANCE.space) addWord(c)
}
private fun addWord(c: CharSequence) {
c.length.let { size ->
val word = CharArray(size)
repeat(size) {
word[it] = c[it]
}
wordList.add(word)
}
}
}
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="lightwalletd_allow_very_insecure_connections">false</bool>
</resources>

View File

@ -1,3 +1,4 @@
kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree shock scan alpha just spot fluid toilet view dinner
urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top
wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame
labor elite banana cement royal tiger smile robust talk street bread bitter admit spy leg alcohol opinion mimic crane bid damp trigger wagon share

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 663150,
"hash": "0000000002fd3be4c24c437bd22620901617125ec2a3a6c902ec9a6c06f734fc",
"time": 1576821833,
"tree": "01ec6278a1bed9e1b080fd60ef50eb17411645e3746ff129283712bc4757ecc833001001b4e1d4a26ac4a2810b57a14f4ffb69395f55dde5674ecd2462af96f9126e054701a36afb68534f640938bdffd80dfcb3f4d5e232488abbf67d049b33a761e7ed6901a16e35205fb7fe626a9b13fc43e1d2b98a9c241f99f93d5e93a735454073025401f5b9bcbf3d0e3c83f95ee79299e8aeadf30af07717bda15ffb7a3d00243b58570001fa6d4c2390e205f81d86b85ace0b48f3ce0afb78eeef3e14c70bcfd7c5f0191c0000011bc9521263584de20822f9483e7edb5af54150c4823c775b2efc6a1eded9625501a6030f8d4b588681eddb66cad63f09c5c7519db49500fc56ebd481ce5e903c22000163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 663150,
"hash": "0000000002fd3be4c24c437bd22620901617125ec2a3a6c902ec9a6c06f734fc",
"time": 1576821833,
"tree": "01ec6278a1bed9e1b080fd60ef50eb17411645e3746ff129283712bc4757ecc833001001b4e1d4a26ac4a2810b57a14f4ffb69395f55dde5674ecd2462af96f9126e054701a36afb68534f640938bdffd80dfcb3f4d5e232488abbf67d049b33a761e7ed6901a16e35205fb7fe626a9b13fc43e1d2b98a9c241f99f93d5e93a735454073025401f5b9bcbf3d0e3c83f95ee79299e8aeadf30af07717bda15ffb7a3d00243b58570001fa6d4c2390e205f81d86b85ace0b48f3ce0afb78eeef3e14c70bcfd7c5f0191c0000011bc9521263584de20822f9483e7edb5af54150c4823c775b2efc6a1eded9625501a6030f8d4b588681eddb66cad63f09c5c7519db49500fc56ebd481ce5e903c22000163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,119 @@
package cash.z.wallet.sdk.integration
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.entity.isSubmitSuccess
import cash.z.wallet.sdk.ext.*
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.service.LightWalletGrpcService
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import java.util.concurrent.CountDownLatch
class TestnetIntegrationTest : ScopedTest() {
var stopWatch = CountDownLatch(1)
@Test
fun testLatestBlockTest() {
val service = LightWalletGrpcService(
context,
host,
port)
val height = service.getLatestBlockHeight()
assertTrue(height > ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
}
@Test
fun testLoadBirthday() {
val (height, hash, time, tree) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(context, ZcashSdk.SAPLING_ACTIVATION_HEIGHT + 1)
assertEquals(ZcashSdk.SAPLING_ACTIVATION_HEIGHT, height)
}
@Test
fun getAddress() = runBlocking {
assertEquals(address, synchronizer.getAddress())
}
@Test
fun testBalance() = runBlocking {
var availableBalance: Long = 0L
synchronizer.balances.onFirst {
availableBalance = it.availableZatoshi
}
synchronizer.status.filter { it == SYNCED }.onFirst {
delay(100)
}
assertTrue("No funds available when we expected a balance greater than zero!",
availableBalance > 0)
}
@Test
@Ignore
fun testSpend() = runBlocking {
var success = false
synchronizer.balances.filter { it.availableZatoshi > 0 }.onEach {
success = sendFunds()
}.first()
log("asserting $success")
assertTrue(success)
}
private suspend fun sendFunds(): Boolean {
val spendingKey = RustBackend().deriveSpendingKeys(seed)[0]
log("sending to address")
synchronizer.sendToAddress(
spendingKey,
ZcashSdk.MINERS_FEE_ZATOSHI,
toAddress,
"first mainnet tx from the SDK"
).filter { it?.isSubmitSuccess() == true }.onFirst {
log("DONE SENDING!!!")
}
log("returning true from sendFunds")
return true
}
fun log(message: String) {
twig("\n---\n[TESTLOG]: $message\n---\n")
}
companion object {
init { Twig.plant(TroubleshootingTwig()) }
const val host = "lightwalletd.testnet.z.cash"
const val port = 9067
private const val birthdayHeight = 963150
private const val targetHeight = 663250
private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val seed = "cash.z.wallet.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"
val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"
private val context = InstrumentationRegistry.getInstrumentation().context
private val initializer = Initializer(context, host, port, "TestnetIntegrationTests")
private lateinit var synchronizer: Synchronizer
@JvmStatic
@BeforeClass
fun startUp() {
initializer.import(seedPhrase, birthdayHeight)
synchronizer = Synchronizer(initializer)
synchronizer.start(classScope)
}
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) 2019-2020 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = ".;walletrpc";
option swift_prefix = "";
import "service.proto";
message DarksideMetaState {
int32 saplingActivation = 1;
string branchID = 2;
string chainName = 3;
}
// A block is a hex-encoded string.
message DarksideBlock {
string block = 1;
}
// DarksideBlocksURL is typically something like:
// https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt
message DarksideBlocksURL {
string url = 1;
}
// DarksideTransactionsURL refers to an HTTP source that contains a list
// of hex-encoded transactions, one per line, that are to be associated
// with the given height (fake-mined into the block at that height)
message DarksideTransactionsURL {
int32 height = 1;
string url = 2;
}
message DarksideHeight {
int32 height = 1;
}
message DarksideEmptyBlocks {
int32 height = 1;
int32 nonce = 2;
int32 count = 3;
}
// Darksidewalletd maintains two staging areas, blocks and transactions. The
// Stage*() gRPCs add items to the staging area; ApplyStaged() "applies" everything
// in the staging area to the working (operational) state that the mock zcashd
// serves; transactions are placed into their corresponding blocks (by height).
service DarksideStreamer {
// Reset reverts all darksidewalletd state (active block range, latest height,
// staged blocks and transactions) and lightwalletd state (cache) to empty,
// the same as the initial state. This occurs synchronously and instantaneously;
// no reorg happens in lightwalletd. This is good to do before each independent
// test so that no state leaks from one test to another.
// Also sets (some of) the values returned by GetLightdInfo(). The Sapling
// activation height specified here must be where the block range starts.
rpc Reset(DarksideMetaState) returns (Empty) {}
// StageBlocksStream accepts a list of blocks and saves them into the blocks
// staging area until ApplyStaged() is called; there is no immediate effect on
// the mock zcashd. Blocks are hex-encoded. Order is important, see ApplyStaged.
rpc StageBlocksStream(stream DarksideBlock) returns (Empty) {}
// StageBlocks is the same as StageBlocksStream() except the blocks are fetched
// from the given URL. Blocks are one per line, hex-encoded (not JSON).
rpc StageBlocks(DarksideBlocksURL) returns (Empty) {}
// StageBlocksCreate is like the previous two, except it creates 'count'
// empty blocks at consecutive heights starting at height 'height'. The
// 'nonce' is part of the header, so it contributes to the block hash; this
// lets you create two fake blocks with the same transactions (or no
// transactions) and same height, with two different hashes.
rpc StageBlocksCreate(DarksideEmptyBlocks) returns (Empty) {}
// StageTransactionsStream stores the given transaction-height pairs in the
// staging area until ApplyStaged() is called. Note that these transactions
// are not returned by the production GetTransaction() gRPC until they
// appear in a "mined" block (contained in the active blockchain presented
// by the mock zcashd).
rpc StageTransactionsStream(stream RawTransaction) returns (Empty) {}
// StageTransactions is the same except the transactions are fetched
// from the given url. They are all staged into the block at the given
// height. Staging transactions at multiple different heights requires
// multiple calls.
rpc StageTransactions(DarksideTransactionsURL) returns (Empty) {}
// ApplyStaged iterates the list of blocks that were staged by the
// StageBlocks*() gRPCs, in the order they were staged, and "merges" each
// into the active, working blocks list that the mock zcashd is presenting
// to lightwalletd. The resulting working block list can't have gaps; if the
// working block range is 1000-1006, and the staged block range is 1003-1004,
// the resulting range is 1000-1004, with 1000-1002 unchanged, blocks
// 1003-1004 from the new range, and 1005-1006 dropped. After merging all
// blocks, ApplyStaged() appends staged transactions (in the order received)
// into each one's corresponding block. The staging area is then cleared.
//
// The argument specifies the latest block height that mock zcashd reports
// (i.e. what's returned by GetLatestBlock). Note that ApplyStaged() can
// also be used to simply advance the latest block height presented by mock
// zcashd. That is, there doesn't need to be anything in the staging area.
rpc ApplyStaged(DarksideHeight) returns (Empty) {}
// Calls to the production gRPC SendTransaction() store the transaction in
// a separate area (not the staging area); this method returns all transactions
// in this separate area, which is then cleared. The height returned
// with each transaction is -1 (invalid) since these transactions haven't
// been mined yet. The intention is that the transactions returned here can
// then, for example, be given to StageTransactions() to get them "mined"
// into a specified block on the next ApplyStaged().
rpc GetIncomingTransactions(Empty) returns (stream RawTransaction) {}
// Clear the incoming transaction pool.
rpc ClearIncomingTransactions(Empty) returns (Empty) {}
}

View File

@ -1,3 +1,7 @@
// Copyright (c) 2019-2020 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "walletrpc";
@ -5,15 +9,14 @@ option swift_prefix = "";
import "compact_formats.proto";
// A BlockID message contains identifiers to select a block: a height or a
// hash. If the hash is present it takes precedence.
// hash. Specification by hash is not implemented, but may be in the future.
message BlockID {
uint64 height = 1;
bytes hash = 2;
}
// BlockRange technically allows ranging from hash to hash etc but this is not
// currently intended for support, though there is no reason you couldn't do
// it. Further permutations are left as an exercise.
// BlockRange specifies a series of blocks from start to end inclusive.
// Both BlockIDs must be heights; specification by hash is not yet supported.
message BlockRange {
BlockID start = 1;
BlockID end = 2;
@ -21,29 +24,86 @@ message BlockRange {
// A TxFilter contains the information needed to identify a particular
// transaction: either a block and an index, or a direct transaction hash.
// Currently, only specification by hash is supported.
message TxFilter {
BlockID block = 1;
uint64 index = 2;
bytes hash = 3;
BlockID block = 1; // block identifier, height or hash
uint64 index = 2; // index within the block
bytes hash = 3; // transaction ID (hash, txid)
}
// RawTransaction contains the complete transaction data.
// RawTransaction contains the complete transaction data. It also optionally includes
// the block height in which the transaction was included.
message RawTransaction {
bytes data = 1;
bytes data = 1; // exact data returned by zcash 'getrawtransaction'
uint64 height = 2; // height that the transaction was mined (or -1)
}
// A SendResponse encodes an error code and a string. It is currently used
// only by SendTransaction(). If error code is zero, the operation was
// successful; if non-zero, it and the message specify the failure.
message SendResponse {
int32 errorCode = 1;
string errorMessage = 2;
}
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
// Chainspec is a placeholder to allow specification of a particular chain fork.
message ChainSpec {}
// Empty is for gRPCs that take no arguments, currently only GetLightdInfo.
message Empty {}
// LightdInfo returns various information about this lightwalletd instance
// and the state of the blockchain.
message LightdInfo {
string version = 1;
string vendor = 2;
bool taddrSupport = 3; // true
string chainName = 4; // either "main" or "test"
uint64 saplingActivationHeight = 5; // depends on mainnet or testnet
string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp
uint64 blockHeight = 7; // latest block on the best chain
}
// TransparentAddressBlockFilter restricts the results to the given address
// or block range.
message TransparentAddressBlockFilter {
string address = 1; // t-address
BlockRange range = 2; // start, end heights
}
// Duration is currently used only for testing, so that the Ping rpc
// can simulate a delay, to create many simultaneous connections. Units
// are microseconds.
message Duration {
int64 intervalUs = 1;
}
// PingResponse is used to indicate concurrency, how many Ping rpcs
// are executing upon entry and upon exit (after the delay).
// This rpc is used for testing only.
message PingResponse {
int64 entry = 1;
int64 exit = 2;
}
service CompactTxStreamer {
// Return the height of the tip of the best chain
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
// Return the compact block corresponding to the given block identifier
rpc GetBlock(BlockID) returns (CompactBlock) {}
// Return a list of consecutive compact blocks
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
// Return the requested full (not compact) transaction (as from zcashd)
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
// Submit the given transaction to the zcash network
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
// Return the txids corresponding to the given t-address within the given block range
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
// Return information about this lightwalletd instance and the blockchain
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
// Testing-only
rpc Ping(Duration) returns (PingResponse) {}
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 800000,
"hash": "00000000013f1f4e5634e896ebdbe63dec115547c1480de0d83c64426f913c27",
"time": 1587134100,
"tree": "01a8689663d3c4ff2bb9aaeb7b75e66a7c68705d147bea9fe67cf7f1ffa9a071720011000196bff8c908d7015ad5df6bc5f5268a157da076c5d2ba63d222f0353c8810320100000001b0a0c048035d6f3f229bedadb2fb7688d03bc49062eae092666a8a55883afb0d000000013a365306be60039724201a594b84cb24ae09ea92a3077722da662289471f6658000184abf3f567f42573deb9e2ae56557fb6a763f16f90577ba5511b55c090eb6f2e00014da3714363acb83872f51c87fed3d42a1093420c3cb96b74ad65966ce27e8c4e0001e2bf698f5ac10b44da560d11a5e1d5c191a82a968a2be0a6948aa8748b54516001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 820000,
"hash": "00000000001bde1cd7d26b21c7c0a4eccf3c16805c6e68f499398463fa91ed93",
"time": 1588640783,
"tree": "0127679ec6e5f7205bd7088782922da4afd0606bf5404faf74feeebe5f548d3639018f80d63358b79bdbdd5a19c6fe3c76e8542f6a1a309ed1dedad95fe3ca5016681101f1a4674cf60a7a56e52fde0a308481a4c9b77f059687b965dffcd67be7c4a83101ed990844ef4a7751262755367e6e16d5e351f190fca5f07124a8f71efee2cc1900000001e83b41c625d651681527f2a95ff2925b96f77c1311144e191da1a59303e39a4200000001f33bcadb822e6d55ba2fa044e2c790d957bc8dd7a6ea773d92114517a97ea76501e0b92e780e46114e308d615f417d051613215bc5974946a4b056013d6351f52401ae652143148288b1328e1b511803db03fbdf319ee20da5c7e49374331c525d3a0000011e76743978cad6dd5263d5a654db307368cb997b0c984ad51018d007c955972101e2bf698f5ac10b44da560d11a5e1d5c191a82a968a2be0a6948aa8748b54516001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}