Skip to content

Commit 59b4bcd

Browse files
committed
Refactor Ethereum txId logic and update tests
Standardizes Ethereum transaction hash calculation to use keccak256 of the signed/encoded transaction, removing previous logic that returned preHash. Updates related Android and iOS tests and wallet code to match this behavior, clarifies distinction between signing hash and transaction hash, and improves cache deserialization robustness.
1 parent 63ca730 commit 59b4bcd

File tree

11 files changed

+324
-254
lines changed

11 files changed

+324
-254
lines changed

Android/wallet/src/androidTest/java/com/flow/wallet/keys/EthereumKeyTests.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ class EthereumKeyTests {
3030
mnemonic,
3131
passphrase = "",
3232
derivationPath = "m/44'/539'/0'/0/0",
33-
keyPair = null,
3433
storage = storage
3534
)
3635

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package com.flow.wallet.keys
2+
3+
import com.flow.wallet.errors.WalletError
4+
import com.flow.wallet.crypto.ChaChaPolyCipher
5+
import com.flow.wallet.storage.StorageProtocol
6+
import com.flow.wallet.storage.InMemoryStorage
7+
import junit.framework.TestCase.assertEquals
8+
import junit.framework.TestCase.assertNotNull
9+
import junit.framework.TestCase.assertTrue
10+
import kotlinx.coroutines.runBlocking
11+
import org.junit.Before
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.mockito.Mock
15+
import org.mockito.Mockito.`when`
16+
import org.mockito.Mockito.verify
17+
import org.mockito.MockitoAnnotations
18+
import org.mockito.junit.MockitoJUnitRunner
19+
import org.mockito.kotlin.any
20+
import org.onflow.flow.models.HashingAlgorithm
21+
import org.onflow.flow.models.SigningAlgorithm
22+
import kotlin.test.assertFailsWith
23+
import kotlin.test.assertFalse
24+
25+
@RunWith(MockitoJUnitRunner::class)
26+
class SeedPhraseKeyProviderTest {
27+
28+
@Mock
29+
private lateinit var mockStorage: StorageProtocol
30+
31+
private lateinit var seedPhraseKeyProvider: SeedPhraseKeyProvider
32+
private val validSeedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
33+
34+
@Before
35+
fun setup() {
36+
MockitoAnnotations.openMocks(this)
37+
seedPhraseKeyProvider = SeedPhraseKey(validSeedPhrase, "", "m/44'/539'/0'/0/0", mockStorage)
38+
}
39+
40+
@Test
41+
fun `test key derivation`() {
42+
val derivedKey = seedPhraseKeyProvider.deriveKey(0)
43+
assertNotNull(derivedKey)
44+
assertTrue(derivedKey is PrivateKey)
45+
}
46+
47+
@Test
48+
fun `test key derivation with different indices`() {
49+
val key1 = seedPhraseKeyProvider.deriveKey(0)
50+
val key2 = seedPhraseKeyProvider.deriveKey(1)
51+
52+
assertNotNull(key1)
53+
assertNotNull(key2)
54+
assertTrue(key1 is PrivateKey)
55+
assertTrue(key2 is PrivateKey)
56+
57+
// Different indices should produce different keys
58+
assertTrue(!key1.secret.contentEquals(key2.secret))
59+
}
60+
61+
@Test
62+
fun `test key creation with default options`() {
63+
runBlocking {
64+
val key = seedPhraseKeyProvider.create(mockStorage)
65+
assertNotNull(key)
66+
assertEquals(KeyType.SEED_PHRASE, key.keyType)
67+
}
68+
}
69+
70+
@Test
71+
fun `test key creation with advanced options`() {
72+
runBlocking {
73+
val key = seedPhraseKeyProvider.create(Unit, mockStorage)
74+
assertNotNull(key)
75+
assertEquals(KeyType.SEED_PHRASE, key.keyType)
76+
}
77+
}
78+
79+
@Test
80+
fun `test key storage and retrieval`() {
81+
runBlocking {
82+
val testId = "test_key"
83+
val testPassword = "test_password"
84+
val encryptedData = "encrypted_data".toByteArray()
85+
86+
`when`(mockStorage.get(testId)).thenReturn(encryptedData)
87+
88+
val key = seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage)
89+
assertNotNull(key)
90+
assertEquals(KeyType.SEED_PHRASE, key.keyType)
91+
verify(mockStorage).set(testId, any())
92+
}
93+
}
94+
95+
@Test
96+
fun `test storage failure scenarios`() {
97+
runBlocking {
98+
val testId = "test_key"
99+
val testPassword = "test_password"
100+
101+
`when`(mockStorage.set(any(), any())).thenThrow(RuntimeException("Storage error"))
102+
103+
assertFailsWith<WalletError> {
104+
seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage)
105+
}
106+
}
107+
}
108+
109+
@Test
110+
fun `test key retrieval with invalid password`() {
111+
runBlocking {
112+
val testId = "test_key"
113+
val testPassword = "invalid_password"
114+
val encryptedData = "encrypted_data".toByteArray()
115+
116+
`when`(mockStorage.get(testId)).thenReturn(encryptedData)
117+
118+
assertFailsWith<WalletError> {
119+
seedPhraseKeyProvider.get(testId, testPassword, mockStorage)
120+
}
121+
}
122+
}
123+
124+
@Test
125+
fun `test key restoration`() {
126+
runBlocking {
127+
val secret = "test_secret".toByteArray()
128+
val restoredKey = seedPhraseKeyProvider.restore(secret, mockStorage)
129+
assertNotNull(restoredKey)
130+
assertEquals(KeyType.SEED_PHRASE, restoredKey.keyType)
131+
}
132+
}
133+
134+
@Test
135+
fun `test key restoration with invalid data`() {
136+
runBlocking {
137+
val invalidSecret = ByteArray(32) { it.toByte() }
138+
assertFailsWith<WalletError> {
139+
seedPhraseKeyProvider.restore(invalidSecret, mockStorage)
140+
}
141+
}
142+
}
143+
144+
@Test
145+
fun `test public key retrieval for different algorithms`() {
146+
val p256Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_P256)
147+
val secp256k1Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_secp256k1)
148+
149+
assertNotNull(p256Key)
150+
assertNotNull(secp256k1Key)
151+
if (p256Key != null) {
152+
assertTrue(p256Key.isNotEmpty())
153+
}
154+
if (secp256k1Key != null) {
155+
assertTrue(secp256k1Key.isNotEmpty())
156+
}
157+
}
158+
159+
@Test
160+
fun `test signing and verification`() {
161+
runBlocking {
162+
val message = "test message".toByteArray()
163+
val signature = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)
164+
165+
assertTrue(signature.isNotEmpty())
166+
assertTrue(seedPhraseKeyProvider.isValidSignature(signature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256))
167+
}
168+
}
169+
170+
@Test
171+
fun `test signing with different hashing algorithms`() {
172+
runBlocking {
173+
val message = "test message".toByteArray()
174+
175+
val sha2_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)
176+
val sha3_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA3_256)
177+
178+
assertTrue(sha2_256.isNotEmpty())
179+
assertTrue(sha3_256.isNotEmpty())
180+
}
181+
}
182+
183+
@Test
184+
fun `test invalid signature verification`() {
185+
val message = "test message".toByteArray()
186+
val invalidSignature = "invalid signature".toByteArray()
187+
188+
assertFalse(seedPhraseKeyProvider.isValidSignature(invalidSignature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256))
189+
}
190+
191+
@Test
192+
fun `test key removal`() {
193+
runBlocking {
194+
val testId = "test_key"
195+
seedPhraseKeyProvider.remove(testId)
196+
verify(mockStorage).remove(testId)
197+
}
198+
}
199+
200+
@Test
201+
fun `test getting all keys`() {
202+
val testKeys = listOf("key1", "key2")
203+
`when`(mockStorage.allKeys).thenReturn(testKeys)
204+
205+
val allKeys = seedPhraseKeyProvider.allKeys()
206+
assertEquals(testKeys, allKeys)
207+
verify(mockStorage).allKeys
208+
}
209+
210+
@Test
211+
fun `test hardware backed property`() {
212+
assertFalse(seedPhraseKeyProvider.isHardwareBacked)
213+
}
214+
215+
@Test
216+
fun `legacy keydata with length field is still readable`() {
217+
runBlocking {
218+
val storage = InMemoryStorage()
219+
val password = "test_password"
220+
val testId = "legacy_seed"
221+
222+
// Simulate old app writing a JSON blob that includes an extra "length" field.
223+
// Cover both numeric and string enum representations to be safe.
224+
val legacyJsonNumeric = """
225+
{
226+
"mnemonic": "$validSeedPhrase",
227+
"passphrase": "",
228+
"path": "m/44'/539'/0'/0/0",
229+
"length": 12
230+
}
231+
""".trimIndent().toByteArray()
232+
val legacyJsonStringEnum = """
233+
{
234+
"mnemonic": "$validSeedPhrase",
235+
"passphrase": "",
236+
"path": "m/44'/539'/0'/0/0",
237+
"length": "TWELVE"
238+
}
239+
""".trimIndent().toByteArray()
240+
241+
val cipher = ChaChaPolyCipher(password)
242+
val encryptedNumeric = cipher.encrypt(legacyJsonNumeric)
243+
storage.set(testId, encryptedNumeric)
244+
val restoredNumeric = seedPhraseKeyProvider.get(testId, password, storage) as SeedPhraseKey
245+
assertEquals(validSeedPhrase, restoredNumeric.mnemonic.joinToString(" "))
246+
assertEquals("m/44'/539'/0'/0/0", restoredNumeric.derivationPath)
247+
248+
val encryptedEnum = cipher.encrypt(legacyJsonStringEnum)
249+
storage.set(testId, encryptedEnum)
250+
val restoredEnum = seedPhraseKeyProvider.get(testId, password, storage) as SeedPhraseKey
251+
assertEquals(validSeedPhrase, restoredEnum.mnemonic.joinToString(" "))
252+
assertEquals("m/44'/539'/0'/0/0", restoredEnum.derivationPath)
253+
254+
}
255+
}
256+
}

Android/wallet/src/androidTest/java/com/flow/wallet/wallet/EthereumWalletTests.kt

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,21 @@ class EthereumWalletTests {
2424

2525
private val mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
2626
private val privateKeyHex = "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"
27-
private val expectedAddress = "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1"
27+
private val expectedAddress = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
28+
private val storage = InMemoryStorage()
29+
private lateinit var wallet: TestWallet
30+
private lateinit var key: SeedPhraseKey
2831

2932
@Before
3033
fun setup() {
3134
Assert.assertTrue(NativeLibraryManager.ensureLibraryLoaded())
35+
key = SeedPhraseKey(mnemonic, storage = storage)
36+
wallet = TestWallet(key, storage)
37+
}
38+
39+
@Test
40+
fun eoaAddress() = runBlocking {
41+
assertEquals(expectedAddress, wallet.ethAddress())
3242
}
3343

3444
@Test
@@ -76,16 +86,6 @@ class EthereumWalletTests {
7686

7787
@Test
7888
fun walletTypedDataSigningMatchesDirectSignature() = runBlocking {
79-
val storage = InMemoryStorage()
80-
val key = SeedPhraseKey(
81-
mnemonic,
82-
passphrase = "",
83-
derivationPath = "m/44'/539'/0'/0/0",
84-
keyPair = null,
85-
storage = storage
86-
)
87-
val wallet = TestWallet(key, storage)
88-
8989
val typedData = """
9090
{
9191
"types": {
@@ -168,9 +168,13 @@ class EthereumWalletTests {
168168
"f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83",
169169
output.encoded.toByteArray().toHexString()
170170
)
171-
val expectedHash = HasherImpl.keccak256(output.encoded.toByteArray()).toHexString()
172-
assertEquals(expectedHash, output.preHash.toByteArray().toHexString())
173-
assertEquals(expectedHash, output.txId().toHexString())
171+
val expectedSigningHash = "daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53"
172+
val expectedTxHash = HasherImpl.keccak256(output.encoded.toByteArray()).toHexString()
173+
174+
// WalletCore's preHash is the signing hash (hash of the unsigned payload)
175+
assertEquals(expectedSigningHash, output.preHash.toByteArray().toHexString())
176+
// txId should be keccak of the signed transaction
177+
assertEquals(expectedTxHash, output.txId().toHexString())
174178
}
175179

176180
@Test
@@ -208,7 +212,6 @@ class EthereumWalletTests {
208212
mnemonic,
209213
passphrase = "",
210214
derivationPath = "m/44'/539'/0'/0/0",
211-
keyPair = null,
212215
storage = storage
213216
)
214217
val wallet = TestWallet(key, storage)

Android/wallet/src/androidTest/java/com/flow/wallet/wallet/WalletInstrumentedTest.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.junit.runner.RunWith
1717
import org.onflow.flow.ChainId
1818
import org.onflow.flow.models.HashingAlgorithm
1919
import org.onflow.flow.models.Signer
20+
import org.onflow.flow.models.Transaction
2021
import kotlin.test.assertEquals
2122
import kotlin.test.assertNotNull
2223
import kotlin.test.assertTrue
@@ -42,11 +43,18 @@ class WalletInstrumentedTest {
4243
override fun getPublicKey(): String = "test_public_key"
4344
override suspend fun getUserSignature(jwt: String): String = "test_signature"
4445
override suspend fun signData(data: ByteArray): String = "test_signed_data"
45-
override fun getSigner(hashingAlgorithm: HashingAlgorithm): Signer = object : org.onflow.flow.models.Signer {
46+
override fun getSigner(hashingAlgorithm: HashingAlgorithm): Signer = object : org.onflow.flow.models.Signer {
4647
override var address: String = testAddress
4748
override var keyIndex: Int = 0
48-
override suspend fun sign(transaction: org.onflow.flow.models.Transaction?, bytes: ByteArray): ByteArray = "test_signature".toByteArray()
49-
override suspend fun sign(bytes: ByteArray): ByteArray = "test_signature".toByteArray()
49+
override suspend fun sign(
50+
bytes: ByteArray,
51+
transaction: Transaction?
52+
): ByteArray {
53+
TODO("Not yet implemented")
54+
return ByteArray(1)
55+
}
56+
suspend fun sign(transaction: org.onflow.flow.models.Transaction?, bytes: ByteArray): ByteArray = "test_signature".toByteArray()
57+
suspend fun sign(bytes: ByteArray): ByteArray = "test_signature".toByteArray()
5058
}
5159
override fun getHashAlgorithm(): org.onflow.flow.models.HashingAlgorithm = org.onflow.flow.models.HashingAlgorithm.SHA2_256
5260
override fun getSignatureAlgorithm(): org.onflow.flow.models.SigningAlgorithm = org.onflow.flow.models.SigningAlgorithm.ECDSA_P256

0 commit comments

Comments
 (0)