diff --git a/src/main/kotlin/com/radixdlt/bip44/BIP44.kt b/src/main/kotlin/com/radixdlt/bip44/BIP44.kt index 39cbc65..406c5f4 100644 --- a/src/main/kotlin/com/radixdlt/bip44/BIP44.kt +++ b/src/main/kotlin/com/radixdlt/bip44/BIP44.kt @@ -44,4 +44,5 @@ data class BIP44(val path: List) { fun increment() = BIP44(path.subList(0, path.size - 1) + path.last().let { BIP44Element(it.hardened, it.number + 1) }) + } diff --git a/src/main/kotlin/com/radixdlt/derivation/AccountHDDerivationPath.kt b/src/main/kotlin/com/radixdlt/derivation/AccountHDDerivationPath.kt new file mode 100644 index 0000000..be8a488 --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/AccountHDDerivationPath.kt @@ -0,0 +1,33 @@ +package com.radixdlt.derivation + +import com.radixdlt.bip44.BIP44_PREFIX +import com.radixdlt.derivation.model.CoinType +import com.radixdlt.derivation.model.EntityType +import com.radixdlt.derivation.model.KeyType +import com.radixdlt.derivation.model.NetworkId + +/** + * The **default** derivation path used to derive `Account` keys for signing off transactions or for signing authentication, at a certain account index (`ENTITY_INDEX`) + * and **unique per network** (`NETWORK_ID`) as per [CAP-26][cap26]. + * + * Note that users can choose to use custom derivation path instead of this default + * one when deriving keys for accounts. + * + * The format is: + * `m/44'/1022'/'/525'/'/'` + * + * Where `'` denotes hardened path, which is **required** as per [SLIP-10][slip10]. + * where `525` is ASCII sum of `"ACCOUNT"`, i.e. `"ACCOUNT".map{ $0.asciiValue! }.reduce(0, +)` + * + */ +data class AccountHDDerivationPath( + private val networkId: NetworkId, + private val accountIndex: Int, + private val keyType: KeyType +) { + private val coinType: CoinType = CoinType.RadixDlt + private val entityType: EntityType = EntityType.Account + + val path: String + get() = "$BIP44_PREFIX/44'/${coinType.value}'/${networkId.value}'/${entityType.value}'/${accountIndex}'/${keyType.value}'" +} \ No newline at end of file diff --git a/src/main/kotlin/com/radixdlt/derivation/CustomHDDerivationPath.kt b/src/main/kotlin/com/radixdlt/derivation/CustomHDDerivationPath.kt new file mode 100644 index 0000000..c526edd --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/CustomHDDerivationPath.kt @@ -0,0 +1,27 @@ +package com.radixdlt.derivation + +import com.radixdlt.bip44.BIP44 +import com.radixdlt.bip44.BIP44_PREFIX +import com.radixdlt.derivation.model.CoinType + +/** + * A custom derivation path used to derive keys for whatever purpose. [CAP-26][cap26] states + * The format is: + * `m/44'/1022'` + * Where `'` denotes hardened path, which is **required** as per [SLIP-10][slip10]. + */ +data class CustomHDDerivationPath( + val bip44: BIP44 +) { + private val coinType: CoinType = CoinType.RadixDlt + + /** + * Check if path starts with m/44'/1022' (hardened or not). Otherwise throw exception + */ + val path: String + get() = if ( + bip44.toString().startsWith("$BIP44_PREFIX/44'/${coinType.value}") || + bip44.toString().startsWith("$BIP44_PREFIX/44/${coinType.value}") + ) bip44.toString() else throw IllegalArgumentException("Invalid derivation path") + +} diff --git a/src/main/kotlin/com/radixdlt/derivation/IdentityHDDerivationPath.kt b/src/main/kotlin/com/radixdlt/derivation/IdentityHDDerivationPath.kt new file mode 100644 index 0000000..ee97450 --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/IdentityHDDerivationPath.kt @@ -0,0 +1,34 @@ +package com.radixdlt.derivation + +import com.radixdlt.bip44.BIP44_PREFIX +import com.radixdlt.derivation.model.CoinType +import com.radixdlt.derivation.model.EntityType +import com.radixdlt.derivation.model.KeyType +import com.radixdlt.derivation.model.NetworkId + +/** + * The **default** derivation path used to derive `Identity` (Persona) keys for signing authentication, + * at a certain (Persona) index (`ENTITY_INDEX`) and **unique per network** (`NETWORK_ID`) as per [CAP-26][cap26]. + * + * Note that users can choose to use custom derivation path instead of this default one + * when deriving keys for identities (personas). + * + * The format is: + * `m/44'/1022'/'/618'/'/'` + * + * Where `'` denotes hardened path, which is **required** as per [SLIP-10][slip10]. + * where `618` is ASCII sum of `"IDENTITY"`, i.e. `"IDENTITY".map{ $0.asciiValue! }.reduce(0, +)` + * + */ +data class IdentityHDDerivationPath( + private val networkId: NetworkId, + private val identityIndex: Int, + private val keyType: KeyType +) { + private val coinType: CoinType = CoinType.RadixDlt + private val entityType: EntityType = EntityType.Identity + + val path: String + get() = "$BIP44_PREFIX/44'/${coinType.value}'/${networkId.value}'/${entityType.value}'/${identityIndex}'/${keyType.value}'" + +} \ No newline at end of file diff --git a/src/main/kotlin/com/radixdlt/derivation/model/CoinType.kt b/src/main/kotlin/com/radixdlt/derivation/model/CoinType.kt new file mode 100644 index 0000000..d50d078 --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/model/CoinType.kt @@ -0,0 +1,8 @@ +package com.radixdlt.derivation.model + +/** + * Currently we only support Radix coin which is documented here -> https://github.com/satoshilabs/slips/pull/1137 + */ +enum class CoinType(val value: Int) { + RadixDlt(1022) +} diff --git a/src/main/kotlin/com/radixdlt/derivation/model/EntityType.kt b/src/main/kotlin/com/radixdlt/derivation/model/EntityType.kt new file mode 100644 index 0000000..62d4210 --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/model/EntityType.kt @@ -0,0 +1,6 @@ +package com.radixdlt.derivation.model + +enum class EntityType(val value: Int) { + Account(525), + Identity(618) +} diff --git a/src/main/kotlin/com/radixdlt/derivation/model/KeyType.kt b/src/main/kotlin/com/radixdlt/derivation/model/KeyType.kt new file mode 100644 index 0000000..ab2eb2c --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/model/KeyType.kt @@ -0,0 +1,8 @@ +package com.radixdlt.derivation.model + +enum class KeyType(val value: Int) { + // Key to be used for signing transactions. + SignTransaction(1238), + // Key to be used for signing authentication. + SignAuth(706) +} \ No newline at end of file diff --git a/src/main/kotlin/com/radixdlt/derivation/model/NetworkId.kt b/src/main/kotlin/com/radixdlt/derivation/model/NetworkId.kt new file mode 100644 index 0000000..516609b --- /dev/null +++ b/src/main/kotlin/com/radixdlt/derivation/model/NetworkId.kt @@ -0,0 +1,11 @@ +package com.radixdlt.derivation.model + +/** + * Full list of networks is documented here -> https://github.com/radixdlt/babylon-node/blob/main/common/src/main/java + * /com/radixdlt/networks/Network.java + */ +enum class NetworkId(val value: Int) { + Mainnet(1), + Adapanet(10), + Enkinet(33) +} \ No newline at end of file diff --git a/src/test/kotlin/com/radixdlt/bip44/BIP44Test.kt b/src/test/kotlin/com/radixdlt/bip44/BIP44Test.kt index f0f625b..1299c68 100644 --- a/src/test/kotlin/com/radixdlt/bip44/BIP44Test.kt +++ b/src/test/kotlin/com/radixdlt/bip44/BIP44Test.kt @@ -3,6 +3,7 @@ package com.radixdlt.bip44 import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import kotlin.test.assertEquals class BIP44Test { @@ -79,4 +80,46 @@ class BIP44Test { assertThat(BIP44("m/0/1/2").increment()) .isEqualTo(BIP44("m/0/1/3")) } + + @Test + fun verifyHardenedDerivationPath() { + val bip44 = BIP44( + path = listOf( + BIP44Element( + hardened = true, + number = 10 + ), + BIP44Element( + hardened = true, + number = 20 + ), + BIP44Element( + hardened = true, + number = 30 + ) + ) + ) + assertEquals(bip44.toString(), "m/10'/20'/30'") + } + + @Test + fun verifyUnhardenedDerivationPath() { + val bip44 = BIP44( + path = listOf( + BIP44Element( + hardened = false, + number = 10 + ), + BIP44Element( + hardened = false, + number = 20 + ), + BIP44Element( + hardened = false, + number = 30 + ) + ) + ) + assertEquals(bip44.toString(), "m/10/20/30") + } } \ No newline at end of file diff --git a/src/test/kotlin/com/radixdlt/derivation/AccountHDDerivationPathTest.kt b/src/test/kotlin/com/radixdlt/derivation/AccountHDDerivationPathTest.kt new file mode 100644 index 0000000..4bfe8f6 --- /dev/null +++ b/src/test/kotlin/com/radixdlt/derivation/AccountHDDerivationPathTest.kt @@ -0,0 +1,53 @@ +package com.radixdlt.derivation + +import com.radixdlt.derivation.model.KeyType +import com.radixdlt.derivation.model.NetworkId +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class AccountHDDerivationPathTest { + + @Test + fun `verify account derivation path for mainnet at index 0 for signing authentication`() { + val accountHDDerivationPath = AccountHDDerivationPath( + networkId = NetworkId.Mainnet, + accountIndex = 0, + keyType = KeyType.SignAuth + ) + + assertEquals(accountHDDerivationPath.path, "m/44'/1022'/1'/525'/0'/706'") + } + + @Test + fun `verify account derivation path for betanet at index 0 for signing authentication`() { + val accountHDDerivationPath = AccountHDDerivationPath( + networkId = NetworkId.Adapanet, + accountIndex = 0, + keyType = KeyType.SignAuth + ) + + assertEquals(accountHDDerivationPath.path, "m/44'/1022'/10'/525'/0'/706'") + } + + @Test + fun `verify account derivation path for betanet at index 1 for signing authentication`() { + val accountHDDerivationPath = AccountHDDerivationPath( + networkId = NetworkId.Adapanet, + accountIndex = 1, + keyType = KeyType.SignAuth + ) + + assertEquals(accountHDDerivationPath.path, "m/44'/1022'/10'/525'/1'/706'") + } + + @Test + fun `verify account derivation path for betanet at index 1 for signing transaction`() { + val accountHDDerivationPath = AccountHDDerivationPath( + networkId = NetworkId.Adapanet, + accountIndex = 1, + keyType = KeyType.SignTransaction + ) + + assertEquals(accountHDDerivationPath.path, "m/44'/1022'/10'/525'/1'/1238'") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/radixdlt/derivation/CustomHDDerivationPathTest.kt b/src/test/kotlin/com/radixdlt/derivation/CustomHDDerivationPathTest.kt new file mode 100644 index 0000000..41c874a --- /dev/null +++ b/src/test/kotlin/com/radixdlt/derivation/CustomHDDerivationPathTest.kt @@ -0,0 +1,85 @@ +package com.radixdlt.derivation + +import com.radixdlt.bip44.BIP44 +import com.radixdlt.bip44.BIP44Element +import com.radixdlt.derivation.model.CoinType +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class CustomHDDerivationPathTest { + + @Test + fun `verify custom derivation path for 3 hardened bip44 elements`() { + val bip44 = BIP44( + path = listOf( + BIP44Element( + hardened = true, + number = 44 + ), + BIP44Element( + hardened = true, + number = CoinType.RadixDlt.value + ), + BIP44Element( + hardened = true, + number = 10 + ), + BIP44Element( + hardened = true, + number = 20 + ), + BIP44Element( + hardened = true, + number = 30 + ) + ) + ) + val customHDDerivationPath = CustomHDDerivationPath( + bip44 = bip44 + ) + + assertEquals(customHDDerivationPath.path, "m/44'/1022'/10'/20'/30'") + } + + @Test + fun `verify invalid custom derivation path without radix coin`() { + val bip44 = BIP44( + path = listOf( + BIP44Element( + hardened = true, + number = 10 + ), + BIP44Element( + hardened = false, + number = 20 + ), + BIP44Element( + hardened = false, + number = 30 + ) + ) + ) + val customHDDerivationPath = CustomHDDerivationPath( + bip44 = bip44 + ) + assertThrows(IllegalArgumentException::class.java) { + customHDDerivationPath.path + } + } + + @Test + fun `verify empty custom derivation path for no bip44 elements`() { + val bip44 = BIP44( + path = emptyList() + ) + val customHDDerivationPath = CustomHDDerivationPath( + bip44 = bip44 + ) + + assertThrows(IllegalArgumentException::class.java) { + customHDDerivationPath.path + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/radixdlt/derivation/IdentityHDDerivationPathTest.kt b/src/test/kotlin/com/radixdlt/derivation/IdentityHDDerivationPathTest.kt new file mode 100644 index 0000000..d75b97e --- /dev/null +++ b/src/test/kotlin/com/radixdlt/derivation/IdentityHDDerivationPathTest.kt @@ -0,0 +1,53 @@ +package com.radixdlt.derivation + +import com.radixdlt.derivation.model.KeyType +import com.radixdlt.derivation.model.NetworkId +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class IdentityHDDerivationPathTest { + + @Test + fun `verify identity derivation path for mainnet at index 0 for signing authentication`() { + val identityHDDerivationPath = IdentityHDDerivationPath( + networkId = NetworkId.Mainnet, + identityIndex = 0, + keyType = KeyType.SignAuth + ) + + assertEquals(identityHDDerivationPath.path, "m/44'/1022'/1'/618'/0'/706'") + } + + @Test + fun `verify identity derivation path for alphanet at index 0 for signing authentication`() { + val identityHDDerivationPath = IdentityHDDerivationPath( + networkId = NetworkId.Adapanet, + identityIndex = 0, + keyType = KeyType.SignAuth + ) + + assertEquals(identityHDDerivationPath.path, "m/44'/1022'/10'/618'/0'/706'") + } + + @Test + fun `verify identity derivation path for mainnet at index 1 for signing authentication`() { + val identityHDDerivationPath = IdentityHDDerivationPath( + networkId = NetworkId.Mainnet, + identityIndex = 1, + keyType = KeyType.SignAuth + ) + + assertEquals(identityHDDerivationPath.path, "m/44'/1022'/1'/618'/1'/706'") + } + + @Test + fun `verify identity derivation path for mainnet at index 0 for signing transaction`() { + val identityHDDerivationPath = IdentityHDDerivationPath( + networkId = NetworkId.Mainnet, + identityIndex = 0, + keyType = KeyType.SignTransaction + ) + + assertEquals(identityHDDerivationPath.path, "m/44'/1022'/1'/618'/0'/1238'") + } +} \ No newline at end of file