mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 14:55:48 +00:00
Fix crash reading MetaAccountSecrets for accounts predating Tron/Bitcoin/Solana
Found on a real device: opening the backup/export screen for an existing wallet crashed with java.lang.IndexOutOfBoundsException: Cannot read 412 of 412 at .../scale/dataType/optional.read at .../scale/Schema.read (x3) at SecretStoreV2$getMetaAccountSecrets$2.invokeSuspend Root cause: MetaAccountSecrets is a positional (SCALE) binary schema. optional<T>.read() unconditionally reads a presence-flag byte with no EOF handling - reading a blob that predates a trailing field (written before that field existed) throws instead of yielding null. This is also why Solana's own address was never getting backfilled: the migration's own read of the account's stored secrets was throwing on every attempted run, silently caught by its own per-account try/catch. Confirmed via substrate-sdk-android's actual source (Dsl.kt/Delegate.kt) that Tron/Bitcoin's identical schema-growth pattern carried the exact same latent risk - it likely only avoided crashing so far by chance byte-alignment in previously-stored blobs, not because the format tolerates it. Fix: read Tron/Bitcoin/Solana's keypair/derivationPath fields (every field added after the original Substrate+Ethereum schema) through a SafeOptional DataType that catches a failed read and returns null, via the library's own custom() field-declaration escape hatch. Write behavior is byte-for-byte unchanged - only reading a shorter/older blob now correctly yields null for fields it doesn't have, instead of crashing every caller of getMetaAccountSecrets (balance sync, signing, backup/export, this Solana backfill migration itself).
This commit is contained in:
+44
-6
@@ -1,11 +1,18 @@
|
||||
package io.novafoundation.nova.common.data.secrets.v2
|
||||
|
||||
import io.emeraldpay.polkaj.scale.ScaleCodecReader
|
||||
import io.emeraldpay.polkaj.scale.ScaleCodecWriter
|
||||
import io.novafoundation.nova.common.utils.invoke
|
||||
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
|
||||
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.Sr25519Keypair
|
||||
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
||||
import io.novasama.substrate_sdk_android.scale.Schema
|
||||
import io.novasama.substrate_sdk_android.scale.byteArray
|
||||
import io.novasama.substrate_sdk_android.scale.custom
|
||||
import io.novasama.substrate_sdk_android.scale.dataType.DataType
|
||||
import io.novasama.substrate_sdk_android.scale.dataType.optional
|
||||
import io.novasama.substrate_sdk_android.scale.dataType.scalable
|
||||
import io.novasama.substrate_sdk_android.scale.dataType.string as stringDataType
|
||||
import io.novasama.substrate_sdk_android.scale.schema
|
||||
import io.novasama.substrate_sdk_android.scale.string
|
||||
|
||||
@@ -16,6 +23,33 @@ object KeyPairSchema : Schema<KeyPairSchema>() {
|
||||
val Nonce by byteArray().optional()
|
||||
}
|
||||
|
||||
/**
|
||||
* [io.novasama.substrate_sdk_android.scale.dataType.optional]'ın `read()`'i, arabellek (stored blob) bu alan
|
||||
* eklenmeden ÖNCE yazılmış eski verilerde tükendiğinde - yani alan gerçekten "yok" demek istediğinde - zarifçe
|
||||
* null dönmek yerine çöküyor (`reader.readBoolean()` EOF'ta istisna fırlatıyor, yakalama yok). Bu, Tron/Bitcoin
|
||||
* eklenirken de aynı riskti; muhtemelen şans eseri (byte hizalaması tesadüfen tutmuş) hiç yüzeye çıkmamıştı -
|
||||
* Solana'nın 2 yeni alanı arabelleği gerçekten aştırdı ve gerçek cihazda "Cannot read 412 of 412" ile çöktü
|
||||
* (secrets okunan HER yerde: bakiye senkronu, backup ekranı, vs).
|
||||
*
|
||||
* Yazma tarafı [optional]'ın kendisiyle byte-byte aynı kalır - sadece okuma, bu alanın zaten taşıması gereken
|
||||
* anlamı (eskiden hiç yazılmamışsa = yok = null) gerçekten sağlar, kütüphanenin eksik bıraktığı yerde.
|
||||
*/
|
||||
private class SafeOptional<T>(private val dataType: DataType<T>) : DataType<T?>() {
|
||||
override fun read(reader: ScaleCodecReader): T? {
|
||||
return try {
|
||||
optional(dataType).read(reader)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(writer: ScaleCodecWriter, value: T?) {
|
||||
optional(dataType).write(writer, value)
|
||||
}
|
||||
|
||||
override fun conformsType(value: Any?) = value == null || dataType.conformsType(value)
|
||||
}
|
||||
|
||||
object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
||||
val Entropy by byteArray().optional()
|
||||
val SubstrateSeed by byteArray().optional()
|
||||
@@ -26,14 +60,18 @@ object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
||||
val EthereumKeypair by schema(KeyPairSchema).optional()
|
||||
val EthereumDerivationPath by string().optional()
|
||||
|
||||
val TronKeypair by schema(KeyPairSchema).optional()
|
||||
val TronDerivationPath by string().optional()
|
||||
// Tron/Bitcoin/Solana are every chain-family added after the original Substrate+Ethereum schema - each is a
|
||||
// trailing addition an old, pre-existing stored blob may simply not have any bytes for at all. custom()
|
||||
// + SafeOptional (not the bare `.optional()` used above) makes reading such a blob correctly yield null for
|
||||
// all of them, instead of crashing on the first one whose bytes don't exist - see SafeOptional's doc.
|
||||
val TronKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||
val TronDerivationPath by custom(SafeOptional(stringDataType))
|
||||
|
||||
val BitcoinKeypair by schema(KeyPairSchema).optional()
|
||||
val BitcoinDerivationPath by string().optional()
|
||||
val BitcoinKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||
val BitcoinDerivationPath by custom(SafeOptional(stringDataType))
|
||||
|
||||
val SolanaKeypair by schema(KeyPairSchema).optional()
|
||||
val SolanaDerivationPath by string().optional()
|
||||
val SolanaKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||
val SolanaDerivationPath by custom(SafeOptional(stringDataType))
|
||||
}
|
||||
|
||||
object ChainAccountSecrets : Schema<ChainAccountSecrets>() {
|
||||
|
||||
Reference in New Issue
Block a user