Every comment is reduced to its first sentence plus @param, @return and @throws tags. Comment lines: 20040 to 8526. ktlint 1.8 applied with its default (official Kotlin) style across all modules; 262 files reformatted. The 264 remaining ktlint findings are naming and filename rules, which are code changes, and are left alone. Comments and formatting only. 1208 tests pass, counted from the XML. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
176 lines
6.2 KiB
Kotlin
176 lines
6.2 KiB
Kotlin
package io.flamenet.e2e
|
|
|
|
import kotlin.test.Test
|
|
import kotlin.test.assertContentEquals
|
|
import kotlin.test.assertEquals
|
|
import kotlin.test.assertFalse
|
|
import kotlin.test.assertNotEquals
|
|
import kotlin.test.assertTrue
|
|
|
|
/**
|
|
* From a recovery phrase to an address somebody can be paid at.
|
|
*/
|
|
class HdAccountTest {
|
|
private val words by lazy { Bip39.englishWordlist() }
|
|
private val phrase by lazy { Bip39.phrase(ByteArray(16), words) }
|
|
private val seed by lazy { Bip39.seed(phrase) }
|
|
|
|
private fun account() = HdAccount(seed)
|
|
|
|
// ---- The path --------------------------------------------------------.
|
|
|
|
/**
|
|
* BIP-44 with every level hardened, which is what this curve requires and
|
|
* what every other Solana wallet uses.
|
|
*/
|
|
@Test
|
|
fun `the derivation path is the standard Solana shape`() {
|
|
assertEquals("m/44H/501H/0H/0H/0H", account().path(0))
|
|
assertEquals("m/44H/501H/0H/0H/7H", account().path(7))
|
|
assertEquals("m/44H/501H/3H/0H/0H", HdAccount(seed, account = 3).path(0))
|
|
}
|
|
|
|
// ---- The property that guards the money ------------------------------.
|
|
|
|
/**
|
|
* The signing key owns the address that was displayed.
|
|
*/
|
|
@Test
|
|
fun `the signing key owns the address that was shown`() {
|
|
val account = account()
|
|
for (index in 0L..4L) {
|
|
val shown = account.receive(index)
|
|
val signing = account.signingSeed(index)
|
|
val fromSigningKey = Base58.encode(Ed25519.fromSeed(signing).publicKey)
|
|
assertEquals(
|
|
shown.address,
|
|
fromSigningKey,
|
|
"index $index would sign with a key that does not own its address",
|
|
)
|
|
}
|
|
}
|
|
|
|
/** And that key can actually sign, the engine carries RFC 8032 itself. */
|
|
@Test
|
|
fun `a derived key can sign and the signature verifies under its address`() {
|
|
val account = account()
|
|
val shown = account.receive(0)
|
|
val seedAt0 = account.signingSeed(0)
|
|
val message = "a transaction".toByteArray()
|
|
val signature = Ed25519.sign(seedAt0, message)
|
|
val publicKey = Base58.decode(shown.address)!!
|
|
// `(publicKey, signature, message)`.
|
|
assertTrue(
|
|
Ed25519.verify(publicKey, signature, message),
|
|
"the signature does not verify under the address it was made for",
|
|
)
|
|
}
|
|
|
|
// ---- Fresh addresses -------------------------------------------------.
|
|
|
|
/**
|
|
* A new address per receive is the point.
|
|
*/
|
|
@Test
|
|
fun `every index gives a different address`() {
|
|
val account = account()
|
|
val addresses = (0L..9L).map { account.receive(it).address }
|
|
assertEquals(10, addresses.toSet().size, "two indices produced one address")
|
|
}
|
|
|
|
@Test
|
|
fun `next advances the counter and never repeats`() {
|
|
var account = account()
|
|
val seen = mutableListOf<String>()
|
|
repeat(5) {
|
|
val (receive, advanced) = account.next()
|
|
seen += receive.address
|
|
account = advanced
|
|
}
|
|
assertEquals(5, seen.toSet().size)
|
|
assertEquals(5L, account.nextIndex)
|
|
}
|
|
|
|
/** Different accounts under one phrase are different wallets. */
|
|
@Test
|
|
fun `account numbers separate`() {
|
|
assertNotEquals(
|
|
HdAccount(seed, account = 0).receive(0).address,
|
|
HdAccount(seed, account = 1).receive(0).address,
|
|
)
|
|
}
|
|
|
|
// ---- Restoring -------------------------------------------------------.
|
|
|
|
/**
|
|
* The whole promise of a recovery phrase: the same words give back the same
|
|
* addresses, on any device and any implementation.
|
|
*/
|
|
@Test
|
|
fun `the same phrase restores the same addresses`() {
|
|
val restored = HdAccount(Bip39.seed(Bip39.phrase(ByteArray(16), words)))
|
|
assertEquals(account().receive(0).address, restored.receive(0).address)
|
|
assertEquals(account().receive(3).address, restored.receive(3).address)
|
|
}
|
|
|
|
/** A different phrase is a different wallet, not a near-miss. */
|
|
@Test
|
|
fun `a different phrase gives entirely different addresses`() {
|
|
val other = HdAccount(Bip39.seed(Bip39.phrase(ByteArray(16) { 1 }, words)))
|
|
assertNotEquals(account().receive(0).address, other.receive(0).address)
|
|
}
|
|
|
|
// ---- What an address is, and is not ----------------------------------.
|
|
|
|
/** An address is Base58 of 32 bytes, so it decodes back to the key. */
|
|
@Test
|
|
fun `an address decodes back to a 32-byte key`() {
|
|
val shown = account().receive(0)
|
|
val decoded = Base58.decode(shown.address)
|
|
assertContentEquals(
|
|
Ed25519.fromSeed(account().signingSeed(0)).publicKey,
|
|
decoded,
|
|
)
|
|
assertEquals(32, decoded!!.size)
|
|
}
|
|
|
|
@Test
|
|
fun `a well-formed address is recognised`() {
|
|
assertTrue(HdAccount.looksLikeAddress(account().receive(0).address))
|
|
}
|
|
|
|
@Test
|
|
fun `something that is not an address is rejected`() {
|
|
assertFalse(HdAccount.looksLikeAddress(""))
|
|
assertFalse(HdAccount.looksLikeAddress("not an address"))
|
|
// Right alphabet, wrong length.
|
|
assertFalse(HdAccount.looksLikeAddress("abc"))
|
|
// Characters the alphabet excludes so a hand-copied address cannot
|
|
// become a different valid one.
|
|
assertFalse(HdAccount.looksLikeAddress("0OIl0OIl0OIl0OIl0OIl0OIl0OIl0OIl"))
|
|
}
|
|
|
|
/**
|
|
* The limit, asserted as a passing test so nobody reads `looksLikeAddress`
|
|
* as stronger than it is.
|
|
*/
|
|
@Test
|
|
fun `a corrupted address can still look valid, and that is not fixable here`() {
|
|
val real = account().receive(0).address
|
|
// Walk the alphabet at one position until the result still decodes to
|
|
// 32 bytes.
|
|
val nearMiss =
|
|
Base58.ALPHABET
|
|
.asSequence()
|
|
.map { real.take(3) + it + real.drop(4) }
|
|
.firstOrNull { it != real && HdAccount.looksLikeAddress(it) }
|
|
assertTrue(
|
|
nearMiss != null,
|
|
"no single-character change produced a still-valid address, if this " +
|
|
"ever becomes true the format has gained a checksum, and the " +
|
|
"warnings written around typed addresses can be revisited",
|
|
)
|
|
assertNotEquals(real, nearMiss)
|
|
}
|
|
}
|