[#666] Download sapling files atomically (#707)

- Enhanced implementation of SaplingParamTool component. It got sapling files move from legacy folder to the preferred one functionality. It downloads the sapling files atomically now (through the temporary file names). It contains all related constants now. It works with SaplingParamToolProperties, which allows us to easily test the SaplingParamTool functionalities. It also now checks file hashes. Removed unnecessary clear function from the component. Changed valid function.
- Moved related constants from ZcashSdk class to SaplingParamTool class
- Changed Initializer, WalletTransactionEncoder and RustBackend classes to work with File instead of path String
- Minor changes in comments in other classes
- Added getSha1Hash() extension function to the FileExt class

* Related tests

- Two new test fixtures to simplify our tests
- Test for getSha1Hash() extension function
- Minor changes in existing tests
- Created new SaplingParamToolBasicTest, which covers non-integration functionality of SaplingParamTool
- Moved integration tests of the SaplingParamTool to the new SaplingParamToolIntegrationTest and added some new

* Related manual tests

- Created Download sapling files manual test
- Created Move sapling files to no_backup manual test
- Update existing Move database files to no_bakcup manual test


Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
Honza Rychnovsky 2022-09-06 12:44:33 +02:00 committed by GitHub
parent b7df183634
commit 35e38ddb19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 988 additions and 342 deletions

View File

@ -4,12 +4,14 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="detektAll" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
<list>
<option value="detektAll" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>

View File

@ -4,14 +4,19 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="assembleAndroidTest assembleDebug assembleZcashmainnetDebug assembleZcashtestnetDebug" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
<list>
<option value="assembleAndroidTest" />
<option value="assembleDebug" />
<option value="assembleZcashmainnetDebug" />
<option value="assembleZcashtestnetDebug" />
</list>
</option>
<option name="vmOptions" />
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>

View File

@ -4,12 +4,14 @@
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="ktlintFormat" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
<list>
<option value="ktlintFormat" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>

View File

@ -1,6 +1,12 @@
Change Log
==========
Version 1.9.0-beta04
------------------------------------
- The SDK now stores sapling param files in `no_backup/co.electricoin.zcash` folder instead of the `cache/params`
folder. Besides that, `SaplingParamTool` also does validation of downloaded sapling param file hash and size.
**No action required from client app**.
Version 1.9.0-beta03
------------------------------------
- No changes; this release is a test of a new deployment process

View File

@ -0,0 +1,32 @@
# About
This manual test case provides information on how to manually test an implemented action of downloading both of our
sapling params files (`sapling-spend.params`, `sapling-output.params`) to the preferred location
`/no_backup/co.electricoin.zcash/`. The benefit of this approach is that the content of `no_backup` folder is not part
of automatic user data backup to user's cloud storage. Our sapling files are quite big (up to 50MB).
# Prerequisite
- Installed [Android Studio](https://developer.android.com/studio)
- Ideally two emulators with min and max supported API level
- A working git client
- Cloned [Zcash Android SDK repository](https://github.com/zcash/zcash-android-wallet-sdk)
- A wallet seed phrase with available funds
# Download files steps
1. Remove a previous version of the demo-app from the emulator, if there is any
2. Install the latest version of the demo-app from the latest commit on the **Main** branch
3. Run the demo-app on selected emulator
4. Once it's opened on the Home screen, change the wallet seed phrase to your preferred one to have some funds
available, which can be spent for the purpose of this test
5. Go to the Send screen and wait for Downloading and Syncing processes to finish
6. Then type the ZEC amount you want to send and the Address to which you want the Zec amount sent
7. Wait for send confirmation
8. Sapling params files should be now downloaded in the preferred location. Open Device File Explorer from Android
Studio bottom-left corner, select the same emulator device from the top. Go to
`/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/no_backup/co.electricoin.zcash`, which should be created
automatically
9. Now verify there both of our sapling params files (`sapling-spend.params`, `sapling-output.params`) placed in the
`no_backup/co.electricoin.zcash` folder
# Check result
Ideally run this test for both emulators (min and max supported API level) to ensure the correct functionality on both
Android version. There is a difference in implementation for these Android versions, but the result should be the same.

View File

@ -1,37 +1,55 @@
# About
This manual test case provides information on how to manually test an implemented action of moving all of our databases files from default `/databases/` to preferred `/no_backup/co.electricoin.zcash` directory. The benefit of this approach is that the content `no_backup` folder is not part of automatic user data backup to user's cloud storage. Our databases can contain potentially big and sensitive data.
This manual test case provides information on how to manually test an implemented action of moving all of our databases
files from default `/databases/` to preferred `/no_backup/co.electricoin.zcash` directory. The benefit of this approach
is that the content `no_backup` folder is not part of automatic user data backup to user's cloud storage. Our databases
can contain potentially big and sensitive data.
The move feature takes all related files (database file itself as well as `journal` and `wal` rollback files) and moves them only once on app start (before first database access) when a client app uses an updated version of this SDK.
The move feature takes all related files (database file itself as well as `journal` and `wal` rollback files) and moves
them only once on app start (before first database access) when a client app uses an updated version of this SDK.
# Prerequisite
- Installed Android Studio
- Installed [Android Studio](https://developer.android.com/studio)
- Ideally two emulators with min and max supported API level
- A working git client
- Cloned Zcash Android SDK repository
- Cloned [Zcash Android SDK repository](https://github.com/zcash/zcash-android-wallet-sdk)
# Prepare steps
1. Install a previous version of the SDK and its demo-app to create database files in the original `database` folder
1. Switch back to commit **Bump version to 1.8.0-beta01 [3fda6da]** from Jul 11 2022 on the **Main** branch in your git client, or with this git command `git checkout 3fda6da1cae5b83174e5b1e020c91dfe95d93458`
2. Update dependencies lock (if needed) and sync Gradle files
3. Run the demo-app on selected emulator
4. Once it's opened go through the app to let the SDK create all the database files. Visit these screens step by step from the side menu:
2. Switch back to commit **Bump version to 1.8.0-beta01 [3fda6da]** from Jul 11 2022 on the **Main** branch in your
git client, or with this git command `git checkout 3fda6da1cae5b83174e5b1e020c91dfe95d93458`
3. Update dependencies lock (if needed) and sync Gradle files
4. Run the demo-app on selected emulator
5. Once it's opened go through the app to let the SDK create all the database files. Visit these screens step by step
from the side menu:
1. Get Balance
2. List Transactions
3. List UTXOs
2. Open Device File Explorer from Android Studio bottom-left corner, select the same emulator device from the top drop-down menu
3. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases`
4. Verify there are `data.db`, `cache.db` and `utxos.db` files (their names can vary, depends on the current build variant). There can be several rollback files created.
6. Open Device File Explorer from Android Studio bottom-left corner, select the same emulator device from the top
drop-down menu
7. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases`
8. Verify there are `data.db`, `cache.db` and `utxos.db` files (their names can vary, depends on the current build
variant). There can be several rollback files created.
# Move steps
1. Install the newer version of the SDK and its demo-app to the same device to check the database files move operation result
1. Install the newer version of the SDK and its demo-app to the same device to check the database files move operation
result
1. Switch to the latest commit on the **Main** branch in your git client
2. Update dependencies lock (if needed) and sync Gradle files
3. Run the demo-app on the same emulator device as previously
2. Once the app is opened, go to the Device File Explorer from Android Studio bottom-left corner again
3. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases` again, now there shouldn't be any files placed in the `database` folder
4. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/no_backup/co.electricoin.zcash`, which should be created automatically
5. Now verify there are the same files placed in the `no_backup/co.electricoin.zcash` folder as in `databases` were
6. To be sure everything is alright, just visit several screens from the side-menu and see no unexpected behavior
2. Once the app is opened go through the same steps as previously to let the SDK apply the move mechanisms to all our
database files. Visit these screens step by step from the side menu:
1. Get Balance
2. List Transactions
3. List UTXOs
3. Go to the Device File Explorer from Android Studio bottom-left corner again
4. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases` again, now there shouldn't be any files placed
in the `database` folder
5. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/no_backup/co.electricoin.zcash`, which should be created
automatically
6. Now verify there are the same files placed in the `no_backup/co.electricoin.zcash` folder as in `databases` were
7. To be sure everything is alright, just visit several screens from the side-menu and see no unexpected behavior
# Check result
Ideally run this test (Prepare and Move steps) for both emulators (Android SDK 21 and 31) to ensure the correct functionality on both Android version. There is a difference in implementation for these Android versions, but the result should be the same.
Ideally run this test (Prepare and Move steps) for both emulators (min and max supported API level) to ensure the
correct functionality on both Android version. There is a difference in implementation for these Android versions, but
the result should be the same.

View File

@ -0,0 +1,47 @@
# About
This manual test case provides information on how to manually test an implemented action of moving both of our
sapling params files (`sapling-spend.params`, `sapling-output.params`) from legacy location `/cache/params/` to
the preferred location `/no_backup/co.electricoin.zcash/`. The benefit of this approach is that the content of
`no_backup` folder is not part of automatic user data backup to user's cloud storage. Our sapling files are quite big
(up to 50MB).
# Prerequisite
- Installed [Android Studio](https://developer.android.com/studio)
- Ideally two emulators with min and max supported API level
- A working git client
- Cloned [Zcash Android SDK repository](https://github.com/zcash/zcash-android-wallet-sdk)
- A wallet seed phrase with available funds
# Prepare steps
1. Install a previous version of the SDK and its demo-app to create sapling files in the original `cache/params` folder
2. Switch back to commit **Check sapling files size [12c23dd0]** from Aug 26 2022 on the **Main** branch in your
git client, or with this git command `git checkout 12c23dd054c687431aaf51bfc5f67d5dbc08625b`
3. Update dependencies lock (if needed) and sync Gradle files
4. Run the demo-app on selected emulator
5. Once it's opened on the Home screen, change the wallet seed phrase to your preferred one to have some funds
available, which can be spent for the purpose of this test
6. Go to the Send screen and wait for Downloading and Syncing processes to finish
7. Then type the ZEC amount you want to send and the Address to which you want the Zec amount sent
8. Wait for send confirmation
9. Sapling params files should be now moved to the original location. Open Device File
Explorer from Android Studio bottom-left corner, select the same emulator device from the top
drop-down menu. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/cache/params`
10. Verify there are `sapling-spend.params` and `sapling-output.params`
# Move steps
1. Install the newer version of the SDK and its demo-app to the same device to check the database files move operation
result
1. Switch to the latest commit on the **Main** branch in your git client
2. Update dependencies lock (if needed) and sync Gradle files
3. Run the demo-app on the same emulator device as previously
2. Once the app is opened, go to the Device File Explorer from Android Studio bottom-left corner again
3. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/cache/params` again, now there shouldn't be our sapling
params files placed in the folder and the folder `/params/` should be missing
4. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/no_backup/co.electricoin.zcash`, which should be created
automatically
5. Now verify there are the same files placed in the `no_backup/co.electricoin.zcash` folder as in `cache/params` were
# Check result
Ideally run this test (Prepare and Move steps) for both emulators (min and max supported API level) to ensure the
correct functionality on both Android version. There is a difference in implementation for these Android versions,
but the result should be the same.

View File

@ -3,6 +3,8 @@ package cash.z.ecc.android.sdk.db
import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.DatabaseNameFixture
@ -165,39 +167,39 @@ class DatabaseCoordinatorTest {
DatabaseNameFixture.newDbWal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
assertTrue(originalDbFile.exists())
assertTrue(originalDbJournalFile.exists())
assertTrue(originalDbWalFile.exists())
assertTrue(originalDbFile.existsSuspend())
assertTrue(originalDbJournalFile.existsSuspend())
assertTrue(originalDbWalFile.existsSuspend())
assertFalse(expectedDbFile.exists())
assertFalse(expectedDbJournalFile.exists())
assertFalse(expectedDbWalFile.exists())
assertFalse(expectedDbFile.existsSuspend())
assertFalse(expectedDbJournalFile.existsSuspend())
assertFalse(expectedDbWalFile.existsSuspend())
dbCoordinator.cacheDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
).also { resultFile ->
assertTrue(resultFile.exists())
assertTrue(resultFile.existsSuspend())
assertEquals(expectedDbFile.absolutePath, resultFile.absolutePath)
assertTrue(expectedDbFile.exists())
assertTrue(expectedDbJournalFile.exists())
assertTrue(expectedDbWalFile.exists())
assertTrue(expectedDbFile.existsSuspend())
assertTrue(expectedDbJournalFile.existsSuspend())
assertTrue(expectedDbWalFile.existsSuspend())
assertFalse(originalDbFile.exists())
assertFalse(originalDbJournalFile.exists())
assertFalse(originalDbWalFile.exists())
assertFalse(originalDbFile.existsSuspend())
assertFalse(originalDbJournalFile.existsSuspend())
assertFalse(originalDbWalFile.existsSuspend())
}
}
private fun getEmptyFile(parent: File, fileName: String): File {
private suspend fun getEmptyFile(parent: File, fileName: String): File {
return File(parent, fileName).apply {
assertTrue(parentFile != null)
parentFile!!.mkdirs()
assertTrue(parentFile!!.exists())
assertTrue(parentFile!!.existsSuspend())
createNewFile()
assertTrue(exists())
createNewFileSuspend()
assertTrue(existsSuspend())
}
}
@ -226,14 +228,14 @@ class DatabaseCoordinatorTest {
fileName = DatabaseNameFixture.newDbWal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
assertTrue(dbFile.exists())
assertTrue(dbJournalFile.exists())
assertTrue(dbWalFile.exists())
assertTrue(dbFile.existsSuspend())
assertTrue(dbJournalFile.existsSuspend())
assertTrue(dbWalFile.existsSuspend())
dbCoordinator.deleteDatabases(DatabaseNameFixture.TEST_DB_NETWORK, DatabaseNameFixture.TEST_DB_ALIAS).also {
assertFalse(dbFile.exists())
assertFalse(dbJournalFile.exists())
assertFalse(dbWalFile.exists())
assertFalse(dbFile.existsSuspend())
assertFalse(dbJournalFile.existsSuspend())
assertFalse(dbWalFile.existsSuspend())
}
}
}

View File

@ -0,0 +1,55 @@
package cash.z.ecc.android.sdk.ext
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.getSha1Hash
import cash.z.ecc.android.sdk.test.getAppContext
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FileExtTest {
private val testFile = File(getAppContext().filesDir, "test_file")
@Before
@After
fun remove_test_files() {
testFile.delete()
}
@Test
@SmallTest
fun check_empty_file_sha1_result() = runTest {
testFile.apply {
createNewFileSuspend()
assertTrue(existsSuspend())
assertEquals(
expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709",
actual = getSha1Hash(),
message = "SHA1 hashes are not the same."
)
}
}
@Test
@SmallTest
fun check_not_empty_file_sha1_result() = runTest {
testFile.apply {
createNewFileSuspend()
assertTrue(existsSuspend())
writeText("Hey! It compiles! Ship it!")
assertTrue(length() > 0)
assertEquals(
expected = "28756ec5d3a73f1e8993bdd46de74b79453ff21c",
actual = getSha1Hash(),
message = "SHA1 hashes are not the same."
)
}
}
}

View File

@ -17,6 +17,7 @@ import kotlin.test.DefaultAsserter.assertEquals
import kotlin.test.DefaultAsserter.assertTrue
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
// TODO [#650]: Move integration tests to separate module
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
@ -51,8 +52,8 @@ class SanityTest(
)
assertTrue(
"$name has invalid CacheDB params dir",
wallet.initializer.rustBackend.pathParamsDir.endsWith(
"cache/params"
wallet.initializer.rustBackend.saplingParamDir.endsWith(
"no_backup/co.electricoin.zcash"
)
)
}
@ -80,11 +81,11 @@ class SanityTest(
)
}
@Test
@Ignore(
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
"validation failed on CI"
)
@Test
fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight()
@ -104,11 +105,11 @@ class SanityTest(
}
}
@Test
@Ignore(
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
"validation failed on CI"
)
@Test
fun testSingleBlockDownload() = runBlocking {
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
// result, only if there is no server communication problem.

View File

@ -36,7 +36,9 @@ class SmokeTest {
)
assertTrue(
"Invalid CacheDB params dir",
wallet.initializer.rustBackend.pathParamsDir.endsWith("cache/params")
wallet.initializer.rustBackend.saplingParamDir.endsWith(
"no_backup/co.electricoin.zcash"
)
)
}

View File

@ -0,0 +1,148 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.internal.ext.getSha1Hash
import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.SaplingParamToolFixture
import cash.z.ecc.fixture.SaplingParamsFixture
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class SaplingParamToolBasicTest {
@Before
fun setup() {
// clear the param files
runBlocking {
SaplingParamsFixture.clearAllFilesFromDirectory(SaplingParamsFixture.DESTINATION_DIRECTORY)
SaplingParamsFixture.clearAllFilesFromDirectory(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY)
}
}
@Test
@SmallTest
fun init_sapling_param_tool_test() = runTest {
val spendSaplingParams = SaplingParamsFixture.new()
val outputSaplingParams = SaplingParamsFixture.new(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME,
SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE,
SaplingParamsFixture.OUTPUT_FILE_HASH
)
val saplingParamTool = SaplingParamTool(
SaplingParamToolProperties(
emptyList(),
SaplingParamsFixture
.DESTINATION_DIRECTORY,
SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY
)
)
// we inject params files to let the ensureParams() finish successfully without executing its extended operation
// like fetchParams, etc.
SaplingParamsFixture.createFile(File(spendSaplingParams.destinationDirectory, spendSaplingParams.fileName))
SaplingParamsFixture.createFile(File(outputSaplingParams.destinationDirectory, outputSaplingParams.fileName))
saplingParamTool.ensureParams(spendSaplingParams.destinationDirectory)
}
@Test
@SmallTest
fun init_and_get_params_destination_dir_test() = runTest {
val destDir = SaplingParamTool.new(getAppContext()).properties.paramsDirectory
assertNotNull(destDir)
assertEquals(
SaplingParamsFixture.DESTINATION_DIRECTORY.absolutePath,
destDir.absolutePath,
"Failed to validate init operation's destination directory."
)
}
@Test
@MediumTest
fun move_files_from_legacy_destination_test() = runTest {
SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY.mkdirs()
val spendFile = File(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, SaplingParamsFixture.SPEND_FILE_NAME)
val outputFile = File(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, SaplingParamsFixture.OUTPUT_FILE_NAME)
// now we inject params files to the legacy location to be "moved" to the preferred location
SaplingParamsFixture.createFile(spendFile)
SaplingParamsFixture.createFile(outputFile)
assertTrue(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, spendFile))
assertTrue(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, outputFile))
assertFalse(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY, spendFile))
assertFalse(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY, outputFile))
// we need to use modified array of sapling parameters to pass through the SHA1 hashes validation
val destDir = SaplingParamTool.initAndGetParamsDestinationDir(
SaplingParamToolFixture.new(
saplingParamsFiles = SaplingParamToolFixture.SAPLING_PARAMS_FILES
.also {
it[0].fileHash = spendFile.getSha1Hash()
it[1].fileHash = outputFile.getSha1Hash()
}
)
)
assertEquals(
SaplingParamsFixture.DESTINATION_DIRECTORY.absolutePath,
destDir.absolutePath
)
assertFalse(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, spendFile))
assertFalse(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY, outputFile))
assertTrue(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY, spendFile))
assertTrue(isFileInPlace(SaplingParamsFixture.DESTINATION_DIRECTORY, outputFile))
}
private suspend fun isFileInPlace(directory: File, file: File): Boolean {
return directory.listFilesSuspend()?.any { it.name == file.name } ?: false
}
@Test
@MediumTest
fun ensure_params_exception_thrown_test() = runTest {
val saplingParamTool = SaplingParamTool(
SaplingParamToolFixture.new(
saplingParamsFiles = SaplingParamToolFixture.SAPLING_PARAMS_FILES
.also {
it[0].fileName = "test_file_0"
it[1].fileName = "test_file_1"
}
)
)
// now we inject params files to the preferred location to pass through the check missing files phase
SaplingParamsFixture.createFile(
File(
SaplingParamToolFixture.SAPLING_PARAMS_FILES[0].destinationDirectory,
SaplingParamToolFixture.SAPLING_PARAMS_FILES[0].fileName
)
)
SaplingParamsFixture.createFile(
File(
SaplingParamToolFixture.SAPLING_PARAMS_FILES[1].destinationDirectory,
SaplingParamToolFixture.SAPLING_PARAMS_FILES[1].fileName
)
)
// the ensure params block should fail in validation phase, because we use a different params file names
assertFailsWith<TransactionEncoderException.MissingParamsException> {
saplingParamTool.ensureParams(SaplingParamToolFixture.PARAMS_DIRECTORY)
}
}
}

View File

@ -0,0 +1,195 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.SaplingParamsFixture
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import kotlin.test.assertContains
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
// TODO [#650]: Move integration tests to separate module
@Ignore(
"These tests need to be refactored to a separate test module. They cause SSLHandshakeException: Chain " +
"validation failed on CI."
)
@RunWith(AndroidJUnit4::class)
class SaplingParamToolIntegrationTest {
private val spendSaplingParams = SaplingParamsFixture.new()
private val outputSaplingParams = SaplingParamsFixture.new(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME,
SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE,
SaplingParamsFixture.OUTPUT_FILE_HASH
)
@Before
fun setup() {
// clear and prepare the param files
runBlocking {
SaplingParamsFixture.clearAllFilesFromDirectory(SaplingParamsFixture.DESTINATION_DIRECTORY)
SaplingParamsFixture.clearAllFilesFromDirectory(SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY)
}
}
@Test
@LargeTest
fun test_files_exists() = runBlocking {
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.fetchParams(spendSaplingParams)
saplingParamTool.fetchParams(outputSaplingParams)
val result = saplingParamTool.validate(
SaplingParamsFixture.DESTINATION_DIRECTORY
)
assertTrue(result)
}
@Test
@LargeTest
fun output_file_exists() = runBlocking {
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.fetchParams(spendSaplingParams)
File(spendSaplingParams.destinationDirectory, spendSaplingParams.fileName).delete()
val result = saplingParamTool.validate(spendSaplingParams.destinationDirectory)
assertFalse(result, "Validation should fail as the spend param file is missing.")
}
@Test
@LargeTest
fun spend_file_exists() = runBlocking {
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.fetchParams(outputSaplingParams)
File(outputSaplingParams.destinationDirectory, outputSaplingParams.fileName).delete()
val result = saplingParamTool.validate(outputSaplingParams.destinationDirectory)
assertFalse(result, "Validation should fail as the output param file is missing.")
}
@Test
@LargeTest
fun check_all_files_fetched() = runBlocking {
val expectedSpendFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.SPEND_FILE_NAME
)
val expectedOutputFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME
)
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.ensureParams(SaplingParamsFixture.DESTINATION_DIRECTORY)
val actualFiles = SaplingParamsFixture.DESTINATION_DIRECTORY.listFilesSuspend()
assertNotNull(actualFiles)
assertContains(actualFiles, expectedSpendFile)
assertContains(actualFiles, expectedOutputFile)
}
@Test
@LargeTest
fun check_correct_spend_param_file_size() = runBlocking {
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.fetchParams(spendSaplingParams)
val expectedSpendFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.SPEND_FILE_NAME
)
assertTrue(expectedSpendFile.length() < SaplingParamsFixture.SPEND_FILE_MAX_SIZE)
assertFalse(expectedSpendFile.length() < SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE)
}
@Test
@LargeTest
fun check_correct_output_param_file_size() = runBlocking {
val saplingParamTool = SaplingParamTool.new(getAppContext())
saplingParamTool.fetchParams(outputSaplingParams)
val expectedOutputFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME
)
assertTrue(expectedOutputFile.length() < SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE)
assertFalse(expectedOutputFile.length() > SaplingParamsFixture.SPEND_FILE_MAX_SIZE)
}
@Test
@LargeTest
fun fetch_params_uninitialized_test() = runTest {
val saplingParamTool = SaplingParamTool.new(getAppContext())
SaplingParamsFixture.DESTINATION_DIRECTORY.delete()
assertFailsWith<TransactionEncoderException.FetchParamsException> {
saplingParamTool.fetchParams(spendSaplingParams)
}
assertFalse(saplingParamTool.validate(SaplingParamsFixture.DESTINATION_DIRECTORY))
}
@Test
@LargeTest
fun fetch_params_incorrect_hash_test() = runTest {
val saplingParamTool = SaplingParamTool.new(getAppContext())
assertFailsWith<TransactionEncoderException.FetchParamsException> {
saplingParamTool.fetchParams(
SaplingParamsFixture.new(
fileName = SaplingParamsFixture.OUTPUT_FILE_NAME,
fileMaxSize = SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE,
fileHash = "test_hash_which_causes_failure_of_validation"
)
)
}
assertFalse(saplingParamTool.validate(SaplingParamsFixture.DESTINATION_DIRECTORY))
}
@Test
@LargeTest
fun fetch_params_incorrect_max_file_size_test() = runTest {
val saplingParamTool = SaplingParamTool.new(getAppContext())
assertFailsWith<TransactionEncoderException.FetchParamsException> {
saplingParamTool.fetchParams(
SaplingParamsFixture.new(
fileName = SaplingParamsFixture.OUTPUT_FILE_NAME,
fileHash = SaplingParamsFixture.OUTPUT_FILE_HASH,
fileMaxSize = 0
)
)
}
assertFalse(saplingParamTool.validate(SaplingParamsFixture.DESTINATION_DIRECTORY))
}
}

View File

@ -1,137 +0,0 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.fixture.SaplingParamsFixture
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import kotlin.test.assertContains
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@Ignore(
"These tests need to be refactored to a separate test module. They cause SSLHandshakeException: Chain " +
"validation failed on CI"
)
@RunWith(AndroidJUnit4::class)
class SaplingParamToolTest {
private val spendSaplingParams = SaplingParamsFixture.newFile()
private val outputSaplingParams = SaplingParamsFixture.newFile(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME,
SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE
)
@Before
fun setup() {
// clear the param files
runBlocking { SaplingParamTool.clear(spendSaplingParams.destinationDirectoryPath) }
}
@Test
fun test_files_exists() = runBlocking {
// Given
SaplingParamTool.fetchParams(spendSaplingParams)
SaplingParamTool.fetchParams(outputSaplingParams)
// When
val result = SaplingParamTool.validate(SaplingParamsFixture.DESTINATION_DIRECTORY)
// Then
assertTrue(result)
}
@Test
fun output_file_exists() = runBlocking {
// Given
SaplingParamTool.fetchParams(spendSaplingParams)
File(spendSaplingParams.destinationDirectoryPath, spendSaplingParams.fileName).delete()
// When
val result = SaplingParamTool.validate(spendSaplingParams.destinationDirectoryPath)
// Then
assertFalse(result, "Validation should fail when the spend params are missing")
}
@Test
fun spend_file_exists() = runBlocking {
// Given
SaplingParamTool.fetchParams(outputSaplingParams)
File(outputSaplingParams.destinationDirectoryPath, outputSaplingParams.fileName).delete()
// When
val result = SaplingParamTool.validate(outputSaplingParams.destinationDirectoryPath)
// Then
assertFalse(result, "Validation should fail when the output params are missing")
}
@Test
fun testInsufficientDeviceStorage() = runBlocking {
// Given
SaplingParamTool.fetchParams(spendSaplingParams)
assertFalse(false, "insufficient storage")
}
@Test
fun testSufficientDeviceStorageForOnlyOneFile() = runBlocking {
SaplingParamTool.fetchParams(spendSaplingParams)
assertFalse(false, "insufficient storage")
}
@Test
fun check_all_files_fetched() = runBlocking {
val expectedSpendFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.SPEND_FILE_NAME
)
val expectedOutputFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME
)
SaplingParamTool.ensureParams(SaplingParamsFixture.DESTINATION_DIRECTORY)
val actualFiles = File(SaplingParamsFixture.DESTINATION_DIRECTORY).listFiles()
assertNotNull(actualFiles)
assertContains(actualFiles, expectedSpendFile)
assertContains(actualFiles, expectedOutputFile)
}
@Test
fun check_correct_spend_param_file_size() = runBlocking {
SaplingParamTool.fetchParams(spendSaplingParams)
val expectedSpendFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.SPEND_FILE_NAME
)
assertTrue(expectedSpendFile.length() < SaplingParamsFixture.SPEND_FILE_MAX_SIZE)
assertFalse(expectedSpendFile.length() < SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE)
}
@Test
fun check_correct_output_param_file_size() = runBlocking {
SaplingParamTool.fetchParams(outputSaplingParams)
val expectedOutputFile = File(
SaplingParamsFixture.DESTINATION_DIRECTORY,
SaplingParamsFixture.OUTPUT_FILE_NAME
)
assertTrue(expectedOutputFile.length() < SaplingParamsFixture.OUTPUT_FILE_MAX_SIZE)
assertFalse(expectedOutputFile.length() > SaplingParamsFixture.SPEND_FILE_MAX_SIZE)
}
}

View File

@ -9,6 +9,7 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
/**
* This test is intended to run to make sure that branch ID logic works across all target devices.
@ -45,8 +46,24 @@ class BranchIdTest internal constructor(
// is an abnormal use of the SDK because this really should run at the rust level
// However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }
val testnetBackend = runBlocking {
RustBackend.init(
File(""),
File(""),
File(""),
ZcashNetwork.Testnet,
ZcashNetwork.Testnet.saplingActivationHeight
)
}
val mainnetBackend = runBlocking {
RustBackend.init(
File(""),
File(""),
File(""),
ZcashNetwork.Mainnet,
ZcashNetwork.Mainnet.saplingActivationHeight
)
}
return listOf(
// Mainnet Cases
arrayOf("Sapling", BlockHeight.new(ZcashNetwork.Mainnet, 419_200), 1991772603L, "76b809bb", mainnetBackend),

View File

@ -1,6 +1,7 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.internal.Files
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
import cash.z.ecc.android.sdk.internal.ext.getNoBackupFilesDirCompat
import cash.z.ecc.android.sdk.test.getAppContext
@ -16,7 +17,7 @@ object DatabasePathFixture {
assert(parentFile != null) { "Failed to create database folder." }
parentFile!!.mkdirs()
assert(parentFile.exists()) { "Failed to check database folder." }
assert(parentFile.existsSuspend()) { "Failed to check database folder." }
parentFile.absolutePath
}
}

View File

@ -0,0 +1,36 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.SaplingParamToolProperties
import cash.z.ecc.android.sdk.internal.SaplingParameters
import java.io.File
object SaplingParamToolFixture {
internal val PARAMS_DIRECTORY = SaplingParamsFixture.DESTINATION_DIRECTORY
internal val PARAMS_LEGACY_DIRECTORY = SaplingParamsFixture.DESTINATION_DIRECTORY_LEGACY
internal val SAPLING_PARAMS_FILES = listOf(
SaplingParameters(
PARAMS_DIRECTORY,
SaplingParamTool.SPEND_PARAM_FILE_NAME,
SaplingParamTool.SPEND_PARAM_FILE_MAX_BYTES_SIZE,
SaplingParamTool.SPEND_PARAM_FILE_SHA1_HASH
),
SaplingParameters(
PARAMS_DIRECTORY,
SaplingParamTool.OUTPUT_PARAM_FILE_NAME,
SaplingParamTool.OUTPUT_PARAM_FILE_MAX_BYTES_SIZE,
SaplingParamTool.OUTPUT_PARAM_FILE_SHA1_HASH
)
)
internal fun new(
saplingParamsFiles: List<SaplingParameters> = SAPLING_PARAMS_FILES,
paramsDirectory: File = PARAMS_DIRECTORY,
paramsLegacyDirectory: File = PARAMS_LEGACY_DIRECTORY
) = SaplingParamToolProperties(
saplingParams = saplingParamsFiles,
paramsDirectory = paramsDirectory,
paramsLegacyDirectory = paramsLegacyDirectory
)
}

View File

@ -1,28 +1,58 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SaplingFileParameters
import cash.z.ecc.android.sdk.internal.Files
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.SaplingParameters
import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.listFilesSuspend
import cash.z.ecc.android.sdk.test.getAppContext
import kotlinx.coroutines.runBlocking
import java.io.File
object SaplingParamsFixture {
val DESTINATION_DIRECTORY: String = File(getAppContext().cacheDir, "params").absolutePath
const val SPEND_FILE_NAME = ZcashSdk.SPEND_PARAM_FILE_NAME
const val SPEND_FILE_MAX_SIZE = SaplingParamTool.SPEND_PARAM_FILE_MAX_BYTES_SIZE
const val OUTPUT_FILE_NAME = ZcashSdk.OUTPUT_PARAM_FILE_NAME
const val OUTPUT_FILE_MAX_SIZE = SaplingParamTool.OUTPUT_PARAM_FILE_MAX_BYTES_SIZE
internal fun newFile(
destinationDirectoryPath: String = DESTINATION_DIRECTORY,
fileName: String = SPEND_FILE_NAME,
fileMaxSize: Long = SPEND_FILE_MAX_SIZE
) = SaplingFileParameters(
destinationDirectoryPath = destinationDirectoryPath,
fileName = fileName,
fileMaxSizeBytes = fileMaxSize
internal val DESTINATION_DIRECTORY_LEGACY: File = File(
getAppContext().cacheDir,
SaplingParamTool.SAPLING_PARAMS_LEGACY_SUBDIRECTORY
)
internal val DESTINATION_DIRECTORY: File
get() = runBlocking {
Files.getZcashNoBackupSubdirectory(getAppContext())
}
internal const val SPEND_FILE_NAME = SaplingParamTool.SPEND_PARAM_FILE_NAME
internal const val SPEND_FILE_MAX_SIZE = SaplingParamTool.SPEND_PARAM_FILE_MAX_BYTES_SIZE
internal const val SPEND_FILE_HASH = SaplingParamTool.SPEND_PARAM_FILE_SHA1_HASH
internal const val OUTPUT_FILE_NAME = SaplingParamTool.OUTPUT_PARAM_FILE_NAME
internal const val OUTPUT_FILE_MAX_SIZE = SaplingParamTool.OUTPUT_PARAM_FILE_MAX_BYTES_SIZE
internal const val OUTPUT_FILE_HASH = SaplingParamTool.OUTPUT_PARAM_FILE_SHA1_HASH
internal fun new(
destinationDirectoryPath: File = DESTINATION_DIRECTORY,
fileName: String = SPEND_FILE_NAME,
fileMaxSize: Long = SPEND_FILE_MAX_SIZE,
fileHash: String = SPEND_FILE_HASH
) = SaplingParameters(
destinationDirectory = destinationDirectoryPath,
fileName = fileName,
fileMaxSizeBytes = fileMaxSize,
fileHash = fileHash
)
internal suspend fun createFile(paramsFile: File) {
paramsFile.createNewFileSuspend()
}
internal suspend fun clearAllFilesFromDirectory(destinationDir: File) {
if (!destinationDir.existsSuspend()) {
return
}
for (file in destinationDir.listFilesSuspend()!!) {
file.deleteSuspend()
}
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend
@ -15,7 +15,6 @@ import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import kotlinx.coroutines.runBlocking
import java.io.File
/**
* Simplified Initializer focused on starting from a ViewingKey.
@ -29,7 +28,8 @@ class Initializer private constructor(
val lightWalletEndpoint: LightWalletEndpoint,
val viewingKeys: List<UnifiedViewingKey>,
val overwriteVks: Boolean,
internal val checkpoint: Checkpoint
internal val checkpoint: Checkpoint,
internal val saplingParamTool: SaplingParamTool
) {
suspend fun erase() = erase(context, network, alias)
@ -334,7 +334,10 @@ class Initializer private constructor(
)
}
val rustBackend = initRustBackend(context, config.network, config.alias, loadedCheckpoint.height)
val saplingParamTool = SaplingParamTool.new(context.applicationContext)
val rustBackend =
initRustBackend(context, config.network, config.alias, loadedCheckpoint.height, saplingParamTool)
return Initializer(
context.applicationContext,
@ -344,7 +347,8 @@ class Initializer private constructor(
config.lightWalletEndpoint,
config.viewingKeys,
config.overwriteVks,
loadedCheckpoint
loadedCheckpoint,
saplingParamTool
)
}
@ -375,14 +379,15 @@ class Initializer private constructor(
context: Context,
network: ZcashNetwork,
alias: String,
blockHeight: BlockHeight
blockHeight: BlockHeight,
saplingParamTool: SaplingParamTool
): RustBackend {
val coordinator = DatabaseCoordinator.getInstance(context)
return RustBackend.init(
coordinator.cacheDbFile(network, alias).absolutePath,
coordinator.dataDbFile(network, alias).absolutePath,
File(context.getCacheDirSuspend(), "params").absolutePath,
coordinator.cacheDbFile(network, alias),
coordinator.dataDbFile(network, alias),
saplingParamTool.properties.paramsDirectory,
network,
blockHeight
)

View File

@ -32,6 +32,7 @@ import cash.z.ecc.android.sdk.db.entity.isSubmitted
import cash.z.ecc.android.sdk.exception.SynchronizerException
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.block.CompactBlockDbStore
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
@ -808,10 +809,11 @@ object DefaultSynchronizerFactory {
fun defaultService(initializer: Initializer): LightWalletService =
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
fun defaultEncoder(
internal fun defaultEncoder(
initializer: Initializer,
saplingParamTool: SaplingParamTool,
repository: TransactionRepository
): TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository)
): TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, saplingParamTool, repository)
fun defaultDownloader(
service: LightWalletService,

View File

@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
@ -433,10 +434,11 @@ interface Synchronizer {
suspend fun new(
initializer: Initializer
): Synchronizer {
val saplingParamTool = SaplingParamTool.new(initializer.context)
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer)
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
val service = DefaultSynchronizerFactory.defaultService(initializer)
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository)
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, saplingParamTool, repository)
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
val txManager =
DefaultSynchronizerFactory.defaultTxManager(initializer, encoder, service)

View File

@ -260,11 +260,11 @@ internal class DatabaseCoordinator private constructor(context: Context) {
}
/**
* The purpose of this function is to move database files between the old location (given by
* the legacyLocationDbFile parameter) and the new location (given by preferredLocationDbFile).
* The actual move operation is performed with the renameTo function, which simply renames
* a file path and persists the metadata information. The mechanism deals with the additional
* database files -journal and -wal too, if they exist.
* The purpose of this function is to move database files between the old location (given by the {@code
* legacyLocationDbFile} parameter) and the new location (given by {@code preferredLocationDbFile}). The actual
* move operation is performed with the renameTo function, which simply renames a file path and persists the
* metadata information. The mechanism deals with the additional database files -journal and -wal too, if they
* exist.
*
* @param legacyLocationDbFile the previously used file location (rename from)
* @param preferredLocationDbFile the newly used file location (rename to)

View File

@ -74,23 +74,6 @@ object ZcashSdk {
*/
const val REWIND_DISTANCE = 10
/**
* File name for the sapling spend params
*/
const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
* File name for the sapling output params
*/
const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
/**
* The Url that is used by default in zcashd.
* We'll want to make this externally configurable, rather than baking it into the SDK but
* this will do for now, since we're using a cloudfront URL that already redirects.
*/
const val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
/**
* The default memo to use when shielding transparent funds.
*/

View File

@ -1,60 +1,189 @@
package cash.z.ecc.android.sdk.internal
import android.content.Context
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
import cash.z.ecc.android.sdk.internal.ext.getSha1Hash
import cash.z.ecc.android.sdk.internal.ext.mkdirsSuspend
import cash.z.ecc.android.sdk.internal.ext.renameToSuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.net.URL
import java.nio.channels.Channels
// TODO [#666]: https://github.com/zcash/zcash-android-wallet-sdk/issues/666
// TODO [#666]: Download sapling-spend.params and sapling-output.params atomically
internal class SaplingParamTool(val properties: SaplingParamToolProperties) {
companion object {
/**
* Maximum file size for the sapling spend params - 50MB
*/
internal const val SPEND_PARAM_FILE_MAX_BYTES_SIZE = 50L * 1024L * 1024L
// TODO [#665]: https://github.com/zcash/zcash-android-wallet-sdk/issues/665
// TODO [#665]: Recover from corrupted sapling-spend.params and sapling-output.params
/**
* Maximum file size for the sapling spend params - 5MB
*/
internal const val OUTPUT_PARAM_FILE_MAX_BYTES_SIZE = 5L * 1024L * 1024L
// TODO [#611]: https://github.com/zcash/zcash-android-wallet-sdk/issues/611
// TODO [#611]: Move Params Directory to No Backup Directory
/**
* Subdirectory name, in which are the sapling params files stored.
*/
internal const val SAPLING_PARAMS_LEGACY_SUBDIRECTORY = "params"
object SaplingParamTool {
/**
* Maximum file size for the sapling spend params - 50MB
*/
internal const val SPEND_PARAM_FILE_MAX_BYTES_SIZE = 50L * 1024L * 1024L
/**
* File name for the sapling spend params
*/
internal const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
* Maximum file size for the sapling spend params - 5MB
*/
internal const val OUTPUT_PARAM_FILE_MAX_BYTES_SIZE = 5L * 1024L * 1024L
/**
* File name for the sapling output params
*/
internal const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
/**
* Temporary file prefix to fulfill atomicity requirement of file handling
*/
private const val TEMPORARY_FILE_NAME_PREFIX = "_"
/**
* File SHA1 hash for the sapling spend params
*/
internal const val SPEND_PARAM_FILE_SHA1_HASH = "a15ab54c2888880e53c823a3063820c728444126"
/**
* File SHA1 hash for the sapling output params
*/
internal const val OUTPUT_PARAM_FILE_SHA1_HASH = "0ebc5a1ef3653948e1c46cf7a16071eac4b7e352"
/**
* The Url that is used by default in zcashd
*/
private const val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
private val checkFilesMutex = Mutex()
/**
* Initialization of needed properties. This is necessary entry point for other operations from {@code
* SaplingParamTool}. This type of implementation also simplifies its testing.
*
* @param context
*/
internal suspend fun new(context: Context): SaplingParamTool {
val paramsDirectory = Files.getZcashNoBackupSubdirectory(context)
val toolProperties = SaplingParamToolProperties(
paramsDirectory = paramsDirectory,
paramsLegacyDirectory = File(context.getCacheDirSuspend(), SAPLING_PARAMS_LEGACY_SUBDIRECTORY),
saplingParams = listOf(
SaplingParameters(
paramsDirectory,
SPEND_PARAM_FILE_NAME,
SPEND_PARAM_FILE_MAX_BYTES_SIZE,
SPEND_PARAM_FILE_SHA1_HASH
),
SaplingParameters(
paramsDirectory,
OUTPUT_PARAM_FILE_NAME,
OUTPUT_PARAM_FILE_MAX_BYTES_SIZE,
OUTPUT_PARAM_FILE_SHA1_HASH
)
)
)
return SaplingParamTool(toolProperties)
}
/**
* Returns file object pointing to the parameters files parent directory. We need to check if the parameters
* files don't sit in the legacy folder first. If they do, then we move the files to the currently used
* directory and validate files hashes.
*
* @return params destination directory file
*/
internal suspend fun initAndGetParamsDestinationDir(toolProperties: SaplingParamToolProperties): File {
checkFilesMutex.withLock {
toolProperties.saplingParams.forEach {
val legacyFile = File(toolProperties.paramsLegacyDirectory, it.fileName)
val currentFile = File(toolProperties.paramsDirectory, it.fileName)
if (legacyFile.existsSuspend() && isFileHashValid(legacyFile, it.fileHash)) {
twig("Moving params file: ${it.fileName} from legacy folder to the currently used folder.")
currentFile.parentFile?.mkdirsSuspend()
if (!renameParametersFile(legacyFile, currentFile)) {
twig("Failed while moving the params file: ${it.fileName} to the preferred location.")
}
} else {
twig(
"Legacy file either does not exist or is not valid. Will be fetched to the preferred " +
"location."
)
}
}
// remove the params folder and its files - a new sapling files will be fetched to the preferred
// location
toolProperties.paramsLegacyDirectory.deleteRecursivelySuspend()
}
return toolProperties.paramsDirectory
}
/**
* Compares the input file parameter SHA1 hash with the given input hash.
*
* @param parametersFile file of which SHA1 hash will be checked
* @param fileHash hash to compare with
*
* @return true in case of hashes are the same, false otherwise
*/
private suspend fun isFileHashValid(parametersFile: File, fileHash: String): Boolean {
return try {
fileHash == parametersFile.getSha1Hash()
} catch (e: IOException) {
twig("Failed in comparing file's hashes with: ${e.message}, caused by: ${e.cause}.")
false
}
}
/**
* The purpose of this function is to rename parameters file from the old name (given by the {@code
* fromParamFile} parameter) to the new name (given by {@code toParamFile}). This operation covers also the file
* move, if it's in a different location.
*
* @param fromParamFile the previously used file name/location
* @param toParamFile the newly used file name/location
*/
private suspend fun renameParametersFile(
fromParamFile: File,
toParamFile: File
): Boolean {
return runCatching {
return@runCatching fromParamFile.renameToSuspend(toParamFile)
}.onFailure {
twig("Failed while renaming parameters file with: $it")
}.getOrDefault(false)
}
}
/**
* Checks the given directory for the output and spending params and calls [fetchParams] for those, which are
* missing.
*
* Note: Don't forget to call the entry point function {@code initSaplingParamTool} first. Make sure you also
* called {@code initAndGetParamsDestinationDir} previously, as it's always better to check the
* legacy destination folder first.
*
* @param destinationDir the directory where the params should be stored.
*
* @throws TransactionEncoderException.MissingParamsException in case of failure while checking sapling params
* files
*/
@Throws(TransactionEncoderException.MissingParamsException::class)
suspend fun ensureParams(destinationDir: String) {
arrayOf(
SaplingFileParameters(
destinationDir,
ZcashSdk.SPEND_PARAM_FILE_NAME,
SPEND_PARAM_FILE_MAX_BYTES_SIZE
),
SaplingFileParameters(
destinationDir,
ZcashSdk.OUTPUT_PARAM_FILE_NAME,
OUTPUT_PARAM_FILE_MAX_BYTES_SIZE
)
).filter {
!File(it.destinationDirectoryPath, it.fileName).existsSuspend()
internal suspend fun ensureParams(destinationDir: File) {
properties.saplingParams.filter {
!File(it.destinationDirectory, it.fileName).existsSuspend()
}.forEach {
try {
twig("Attempting to download missing params: ${it.fileName}.")
@ -80,21 +209,17 @@ object SaplingParamTool {
* @throws TransactionEncoderException.FetchParamsException if any error while downloading the params file occurs
*/
@Throws(TransactionEncoderException.FetchParamsException::class)
internal suspend fun fetchParams(paramsToFetch: SaplingFileParameters) {
val url = URL("${ZcashSdk.CLOUD_PARAM_DIR_URL}/${paramsToFetch.fileName}")
val file = File(paramsToFetch.destinationDirectoryPath, paramsToFetch.fileName)
if (file.parentFile?.existsSuspend() == true) {
twig("Directory ${file.parentFile?.name} exists!")
} else {
twig("Directory did not exist attempting to make it.")
file.parentFile?.mkdirsSuspend()
}
internal suspend fun fetchParams(paramsToFetch: SaplingParameters) {
val url = URL("$CLOUD_PARAM_DIR_URL/${paramsToFetch.fileName}")
val temporaryFile = File(
paramsToFetch.destinationDirectory,
"$TEMPORARY_FILE_NAME_PREFIX${paramsToFetch.fileName}"
)
withContext(Dispatchers.IO) {
runCatching {
Channels.newChannel(url.openStream()).use { readableByteChannel ->
file.outputStream().use { fileOutputStream ->
temporaryFile.outputStream().use { fileOutputStream ->
fileOutputStream.channel.use { fileChannel ->
// Transfers bytes from stream to file from position 0 to end position or to max
// file size limit. This eliminates the risk of downloading potentially large files
@ -119,44 +244,69 @@ object SaplingParamTool {
throw TransactionEncoderException.FetchParamsException(it)
}
}.onSuccess {
twig("Fetch and write of ${paramsToFetch.fileName} succeeded.")
}
}
}
suspend fun clear(destinationDir: String) {
if (validate(destinationDir)) {
arrayOf(
ZcashSdk.SPEND_PARAM_FILE_NAME,
ZcashSdk.OUTPUT_PARAM_FILE_NAME
).forEach { paramFileName ->
val file = File(destinationDir, paramFileName)
if (file.deleteRecursivelySuspend()) {
twig("Files deleted successfully")
} else {
twig("Error: Files not able to be deleted!")
twig(
"Fetch and write of the temporary ${temporaryFile.name} succeeded. Validating and moving it to " +
"the final destination"
)
if (!isFileHashValid(temporaryFile, paramsToFetch.fileHash)) {
finalizeAndReportError(
temporaryFile,
message = "Failed while validating fetched params file: ${paramsToFetch.fileName}"
)
}
val resultFile = File(paramsToFetch.destinationDirectory, paramsToFetch.fileName)
if (!renameParametersFile(temporaryFile, resultFile)) {
finalizeAndReportError(
temporaryFile,
resultFile,
message = "Failed while renaming result params file: ${paramsToFetch.fileName}"
)
}
// TODO [#665]: https://github.com/zcash/zcash-android-wallet-sdk/issues/665
// TODO [#665]: Recover from corrupted sapling-spend.params and sapling-output.params
}
}
}
suspend fun validate(destinationDir: String): Boolean {
@Throws(TransactionEncoderException.FetchParamsException::class)
private suspend fun finalizeAndReportError(vararg files: File, message: String) {
files.forEach {
it.deleteSuspend()
}
message.also {
twig(it)
throw TransactionEncoderException.FetchParamsException(it)
}
}
internal suspend fun validate(destinationDir: File): Boolean {
return arrayOf(
ZcashSdk.SPEND_PARAM_FILE_NAME,
ZcashSdk.OUTPUT_PARAM_FILE_NAME
SPEND_PARAM_FILE_NAME,
OUTPUT_PARAM_FILE_NAME
).all { paramFileName ->
File(destinationDir, paramFileName).existsSuspend()
}.also {
println("Param files ${if (!it) "did not" else ""} both exist!")
twig("Param files ${if (!it) "did not" else ""} both exist!")
}
}
}
/**
* Sapling file parameters class to hold each sapling file attributes.
* Sapling file parameter class to hold each sapling file attributes.
*/
internal data class SaplingFileParameters(
val destinationDirectoryPath: String,
val fileName: String,
val fileMaxSizeBytes: Long
internal data class SaplingParameters(
val destinationDirectory: File,
var fileName: String,
val fileMaxSizeBytes: Long,
var fileHash: String
)
/**
* Sapling param tool helper properties. The goal of this implementation is to ease its testing.
*/
internal data class SaplingParamToolProperties(
val saplingParams: List<SaplingParameters>,
val paramsDirectory: File,
val paramsLegacyDirectory: File
)

View File

@ -5,6 +5,9 @@ package cash.z.ecc.android.sdk.internal.ext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.security.DigestInputStream
import java.security.MessageDigest
internal suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) { delete() }
@ -17,3 +20,37 @@ internal suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) { canW
internal suspend fun File.renameToSuspend(dest: File) = withContext(Dispatchers.IO) { renameTo(dest) }
suspend fun File.deleteRecursivelySuspend() = withContext(Dispatchers.IO) { deleteRecursively() }
suspend fun File.listFilesSuspend(): Array<File>? = withContext(Dispatchers.IO) { listFiles() }
suspend fun File.inputStreamSuspend(): FileInputStream = withContext(Dispatchers.IO) { inputStream() }
suspend fun File.createNewFileSuspend() = withContext(Dispatchers.IO) { createNewFile() }
/**
* Preferred buffer size. We use the same buffer size as BufferedInputStream does.
*/
private const val BUFFER_SIZE_BYTES_SIZE = 8192
/**
* Encrypts File to SHA1 format.
*
* @return String SHA1 encryption of the input file
*/
suspend fun File.getSha1Hash(): String {
return withContext(Dispatchers.IO) {
val messageDigest = MessageDigest.getInstance("SHA-1")
inputStreamSuspend().use { fis ->
DigestInputStream(fis, messageDigest).use { dis ->
val buffer = ByteArray(BUFFER_SIZE_BYTES_SIZE)
while (dis.read(buffer) >= 0) {
// reading the whole buffered stream, which results in update on the message digest
}
return@withContext messageDigest.digest().joinToString(
separator = "",
transform = { "%02x".format(it) }
)
}
}
}
}

View File

@ -6,9 +6,9 @@ import cash.z.ecc.android.sdk.ext.masked
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.Zatoshi
import java.io.IOException
/**
* Class responsible for encoding a transaction in a consistent way. This bridges the gap by
@ -21,6 +21,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi
*/
internal class WalletTransactionEncoder(
private val rustBackend: RustBackendWelding,
private val saplingParamTool: SaplingParamTool,
private val repository: TransactionRepository
) : TransactionEncoder {
@ -109,15 +110,12 @@ internal class WalletTransactionEncoder(
memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0
): Long {
return twigTask(
"creating transaction to spend $amount zatoshi to" +
" ${toAddress.masked()} with memo $memo"
) {
@Suppress("TooGenericExceptionCaught")
return twigTask("Creating transaction to spend $amount zatoshi to ${toAddress.masked()} with memo $memo") {
saplingParamTool.ensureParams(rustBackend.saplingParamDir)
twig("Params exist! Attempting to send.")
try {
val branchId = getConsensusBranchId()
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to send with consensus branchId $branchId...")
twig("Attempting to send with consensus branchId $branchId...")
rustBackend.createToAddress(
branchId,
fromAccountIndex,
@ -126,9 +124,15 @@ internal class WalletTransactionEncoder(
amount.value,
memo
)
} catch (t: Throwable) {
twig("Caught exception while creating transaction ${t.message}, caused by: ${t.cause}.")
throw t
} catch (e: IOException) {
twig("Caught IO exception while creating transaction ${e.message}, caused by: ${e.cause}.")
throw e
} catch (e: TransactionEncoderException.IncompleteScanException) {
twig(
"Caught TransactionEncoderException.IncompleteScanException while creating transaction" +
" ${e.message}, caused by: ${e.cause}."
)
throw e
}
}.also { result ->
twig("result of sendToAddress: $result")
@ -140,22 +144,21 @@ internal class WalletTransactionEncoder(
transparentSecretKey: String,
memo: ByteArray? = byteArrayOf()
): Long {
return twigTask("creating transaction to shield all UTXOs") {
@Suppress("TooGenericExceptionCaught")
return twigTask("Creating transaction to shield all UTXOs.") {
saplingParamTool.ensureParams(rustBackend.saplingParamDir)
twig("Params exist! attempting to shield...")
try {
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to shield...")
rustBackend.shieldToAddress(
spendingKey,
transparentSecretKey,
memo
)
} catch (t: Throwable) {
} catch (e: IOException) {
// TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)
// then consider custom error that says no UTXOs existed to shield
// TODO [#680]: https://github.com/zcash/zcash-android-wallet-sdk/issues/680
twig("Shield failed due to: ${t.message}, caused by: ${t.cause}.")
throw t
twig("Caught IO exception - shield failed due to: ${e.message}, caused by: ${e.cause}.")
throw e
}
}.also { result ->
twig("result of shieldToAddress: $result")

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
@ -26,7 +25,7 @@ internal class RustBackend private constructor(
val birthdayHeight: BlockHeight,
val dataDbFile: File,
val cacheDbFile: File,
val pathParamsDir: String
override val saplingParamDir: File
) : RustBackendWelding {
suspend fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
@ -236,8 +235,8 @@ internal class RustBackend private constructor(
to,
value,
memo ?: ByteArray(0),
"$pathParamsDir/$SPEND_PARAM_FILE_NAME",
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME",
File(saplingParamDir, SaplingParamTool.SPEND_PARAM_FILE_NAME).absolutePath,
File(saplingParamDir, SaplingParamTool.OUTPUT_PARAM_FILE_NAME).absolutePath,
networkId = network.id
)
}
@ -255,8 +254,8 @@ internal class RustBackend private constructor(
extsk,
tsk,
memo ?: ByteArray(0),
"$pathParamsDir/$SPEND_PARAM_FILE_NAME",
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME",
File(saplingParamDir, SaplingParamTool.SPEND_PARAM_FILE_NAME).absolutePath,
File(saplingParamDir, SaplingParamTool.OUTPUT_PARAM_FILE_NAME).absolutePath,
networkId = network.id
)
}
@ -356,9 +355,9 @@ internal class RustBackend private constructor(
* function once, it is idempotent.
*/
suspend fun init(
cacheDbPath: String,
dataDbPath: String,
paramsPath: String,
cacheDbFile: File,
dataDbFile: File,
saplingParamsDir: File,
zcashNetwork: ZcashNetwork,
birthdayHeight: BlockHeight
): RustBackend {
@ -367,9 +366,9 @@ internal class RustBackend private constructor(
return RustBackend(
zcashNetwork,
birthdayHeight,
dataDbFile = File(dataDbPath),
cacheDbFile = File(cacheDbPath),
pathParamsDir = paramsPath
dataDbFile = dataDbFile,
cacheDbFile = cacheDbFile,
saplingParamDir = saplingParamsDir
)
}

View File

@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import java.io.File
/**
* Contract defining the exposed capabilities of the Rust backend.
@ -18,6 +19,8 @@ internal interface RustBackendWelding {
val network: ZcashNetwork
val saplingParamDir: File
@Suppress("LongParameterList")
suspend fun createToAddress(
consensusBranchId: Long,