[#270] Fallback for bad JSON files

This commit is contained in:
Carter Jernigan 2021-09-11 11:21:38 -04:00
parent a58a235d2c
commit 568cfa6c4b
7 changed files with 148 additions and 33 deletions

View File

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1290000,
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
"time": 1624075741,
"tree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1290000,
"hash": "00000000014836c3cbc011276cbd3702a76a1fea7eb2c0c2c257321220376450",
"time": 1624075741,
"tree": "01accf4fc3dc4233bbe757f94e0d4cd23b4aa2e6ac472601f4f53ca4dc86a8a05901fae977171a6103a0338990e073ffe50e29fc8bf0400dcd3378ebfe7a146ed1481300014f7b33dd5159ac66f2670b7db8925065e7154e0199ff7ee7559b276ba56ad1200173e9881f21357e54027a4275114f0f6ad4ca17143554182f63c77f3288a23a20011d65465ab942440e200d429ef892452b4b05c5b21e9a6e6d968a719c67b5e85b000000000000000150926c74975e2d8ff095defb75a4a6d9f17007e87a74230a65a3265d8f45032900012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@ -0,0 +1,7 @@
{
"network": "main",
"height": 1300000,
"hash": "00000000027222bdbcf9c5f807f851f97312ac6e0dbbc2b93f2be21a69c59d44",
"time": 1624830312,
"tree": "01f5a97e2679a2bb9103caf37b825f92fcd73fff836234844dfcf1815394522b2c01526587b9b9e8aeb0eb572d81fec1f5127b8278ba0f57e451bd6b796596940a2213000131c7ff90fafff6159b8fb6544a2bcbba6c102903158fce8f9a9d3c6654abb23300013555cb7f4f79badeaca9bf2dca5a8704f0929053d50e95c03002f9a4d5286c3a01ad3557e11c1607ec888dc84f5f8899c3c79fb1f50b613946452ec7dd5e53763c0001c4583f4482b949390dba355fc8fa63019c83acd644ddd633cb50211d236f870600000001088da0d78eefd0c222507927e403b972d0890d0c31e08b02268fbe39ac4a6e170001edf82d4e2b4893ea2028ca8c5149e50a4c358b856d73f2de2b9a22034fa78f22012ffde6dccbef68b60cd7b4e7a8fe7989f5954fa4bacad01b247d16b9bfa5084000000125911f4524469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
}

View File

@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
@ -11,15 +10,8 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AssetTest {
@Test
@SmallTest
fun birthday_height_from_filename() {
assertEquals(123, WalletBirthdayTool.birthdayHeight("123.json"))
}
@Test
@SmallTest

View File

@ -0,0 +1,52 @@
package cash.z.ecc.android.sdk.tool
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.Companion.IS_FALLBACK_ON_FAILURE
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class WalletBirthdayToolTest {
@Test
@SmallTest
fun birthday_height_from_filename() {
assertEquals(123, WalletBirthdayTool.birthdayHeight("123.json"))
}
@Test
@SmallTest
fun load_latest_birthday() {
// Using a separate directory, so that we don't have to keep updating this test each time
// mainnet or testnet changes
val directory = "saplingtree/goodnet"
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = WalletBirthdayTool.getFirstValidWalletBirthday(
context,
directory,
listOf("1300000.json", "1290000.json")
)
assertEquals(1300000, birthday.height)
}
@Test
@SmallTest
fun load_latest_birthday_fallback_on_bad_json() {
if (!IS_FALLBACK_ON_FAILURE) {
return
}
val directory = "saplingtree/badnet"
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = WalletBirthdayTool.getFirstValidWalletBirthday(
context,
directory,
listOf("1300000.json", "1290000.json")
)
assertEquals(1290000, birthday.height)
}
}

View File

@ -10,7 +10,6 @@ import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.Arrays
/**
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
@ -30,11 +29,21 @@ class WalletBirthdayTool(appContext: Context) {
companion object {
// Behavior change implemented as a fix for issue #270. Temporarily adding a boolean
// that allows the change to be rolled back quickly if needed, although long-term
// this flag should be removed.
@VisibleForTesting
internal val IS_FALLBACK_ON_FAILURE = true
/**
* Load the nearest checkpoint to the given birthday height. If null is given, then this
* will load the most recent checkpoint available.
*/
fun loadNearest(context: Context, network: ZcashNetwork, birthdayHeight: Int? = null): WalletBirthday {
fun loadNearest(
context: Context,
network: ZcashNetwork,
birthdayHeight: Int? = null
): WalletBirthday {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, network, birthdayHeight)
}
@ -68,7 +77,8 @@ class WalletBirthdayTool(appContext: Context) {
internal fun birthdayHeight(fileName: String) = fileName.split('.').first().toInt()
private fun Array<String>.sortDescending() = apply { sortByDescending { birthdayHeight(it) } }
private fun Array<String>.sortDescending() =
apply { sortByDescending { birthdayHeight(it) } }
/**
* Load the given birthday file from the assets of the given context. When no height is
@ -87,35 +97,75 @@ class WalletBirthdayTool(appContext: Context) {
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val directory = birthdayDirectory(network)
val treeFiles = listBirthdayDirectoryContents(context, directory)?.sortDescending()
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
directory
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
birthdayHeight(it) <= birthdayHeight
}
val treeFiles = getFilteredFileNames(context, directory, birthdayHeight)
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
return getFirstValidWalletBirthday(context, directory, treeFiles)
}
private fun getFilteredFileNames(
context: Context,
directory: String,
birthdayHeight: Int? = null,
): List<String> {
val unfilteredTreeFiles = listBirthdayDirectoryContents(context, directory)
if (unfilteredTreeFiles.isNullOrEmpty()) {
throw BirthdayException.MissingBirthdayFilesException(directory)
}
val filteredTreeFiles = unfilteredTreeFiles
.sortDescending()
.filter { filename ->
birthdayHeight?.let { birthdayHeight(filename) <= it } ?: true
}
} catch (t: Throwable) {
if (filteredTreeFiles.isEmpty()) {
throw BirthdayException.BirthdayFileNotFoundException(
directory,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("$directory/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
directory,
treeFiles[0]
)
return filteredTreeFiles
}
/**
* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.
*/
@VisibleForTesting
internal fun getFirstValidWalletBirthday(
context: Context,
directory: String,
treeFiles: List<String>
): WalletBirthday {
var lastException: Exception? = null
treeFiles.forEach { treefile ->
try {
context.assets.open("$directory/$treefile").use { inputStream ->
InputStreamReader(inputStream).use { inputStreamReader ->
JsonReader(inputStreamReader).use { jsonReader ->
return Gson().fromJson(jsonReader, WalletBirthday::class.java)
}
}
}
} catch (t: Throwable) {
val exception = BirthdayException.MalformattedBirthdayFilesException(
directory,
treefile
)
lastException = exception
if (IS_FALLBACK_ON_FAILURE) {
// TODO: If we ever add crash analytics hooks, this would be something to report
twig("Malformed birthday file $t")
} else {
throw exception
}
}
}
throw lastException!!
}
}
}