diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt index 8e48aa39..ffa5e085 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt @@ -4,6 +4,9 @@ import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey import fr.acinq.bitcoin.DeterministicWallet.publicKey import kotlin.jvm.JvmStatic +/** + * Output Script Descriptors: see https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki + */ public object Descriptor { private fun polyMod(cc: Long, value: Int): Long { var c = cc @@ -17,34 +20,20 @@ public object Descriptor { return c } + // Taken from: https://github.com/bitcoin/bitcoin/blob/207a22877330709e4462e6092c265ab55c8653ac/src/script/descriptor.cpp @JvmStatic public fun checksum(span: String): String { - /** A character set designed such that: - * - The most common 'unprotected' descriptor characters (hex, keypaths) are in the first group of 32. - * - Case errors cause an offset that's a multiple of 32. - * - As many alphabetic characters are in the same group (while following the above restrictions). - * - * If p(x) gives the position of a character c in this character set, every group of 3 characters - * (a,b,c) is encoded as the 4 symbols (p(a) & 31, p(b) & 31, p(c) & 31, (p(a) / 32) + 3 * (p(b) / 32) + 9 * (p(c) / 32). - * This means that changes that only affect the lower 5 bits of the position, or only the higher 2 bits, will just - * affect a single symbol. - * - * As a result, within-group-of-32 errors count as 1 symbol, as do cross-group errors that don't affect - * the position within the groups. - */ val INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}" + "IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~" + "ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " - - /** The character set for the checksum itself (same as bech32). */ - val CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + val CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" var c = 1L var cls = 0 var clscount = 0 span.forEach { ch -> - val pos = INPUT_CHARSET.indexOf(ch); - if (pos == -1) return ""; - c = polyMod(c, pos and 31); // Emit a symbol for the position inside the group, for every character. - cls = cls * 3 + (pos shr 5); // Accumulate the group numbers + val pos = INPUT_CHARSET.indexOf(ch) + if (pos == -1) return "" + c = polyMod(c, pos and 31) // Emit a symbol for the position inside the group, for every character. + cls = cls * 3 + (pos shr 5) // Accumulate the group numbers clscount += 1 if (clscount == 3) { // Emit an extra symbol representing the group numbers, for every 3 characters. @@ -60,8 +49,7 @@ public object Descriptor { val ret = StringBuilder(" ") for (j in 0 until 8) { val pos1 = (c shr (5 * (7 - j))) and 31 - val char = CHECKSUM_CHARSET[pos1.toInt()] - ret[j] = char + ret[j] = CHECKSUM_CHARSET[pos1.toInt()] } return ret.toString() } @@ -73,21 +61,17 @@ public object Descriptor { } @JvmStatic - public fun BIP84Descriptors(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): Pair { + public fun BIP84Descriptor(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): String { val (keyPath, _) = getBIP84KeyPath(chainHash) val accountPub = publicKey(derivePrivateKey(master, KeyPath(keyPath))) val fingerprint = DeterministicWallet.fingerprint(master) and 0xFFFFFFFFL - return BIP84Descriptors(chainHash, fingerprint, accountPub) + return BIP84Descriptor(chainHash, fingerprint, accountPub) } @JvmStatic - public fun BIP84Descriptors(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): Pair { + public fun BIP84Descriptor(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): String { val (keyPath, prefix) = getBIP84KeyPath(chainHash) - val accountDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/0/*)" - val changeDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/1/*)" - return Pair( - "$accountDesc#${checksum(accountDesc)}", - "$changeDesc#${checksum(changeDesc)}" - ) + val accountDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/<0;1>/*)" + return "$accountDesc#${checksum(accountDesc)}" } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt index 243dd266..6ed7b05b 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt @@ -19,16 +19,14 @@ class DescriptorTestsCommon { data.forEach { dnc -> val (desc, checksum) = dnc.split('#').toTypedArray() assertEquals(checksum, Descriptor.checksum(desc)) - } + } } @Test fun `compute BIP84 descriptors`() { val seed = ByteVector.fromHex("817a9c8e6ba36f083d7e68b5ee89ce74fde9ef294a724a5efc5cef2b88db057f") val master = DeterministicWallet.generate(seed) - val (accountDesc, changeDesc) = Descriptor.BIP84Descriptors(Block.RegtestGenesisBlock.hash, master) - assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/0/*)#uysr3s9y", accountDesc) - assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/1/*)#ds4zv94u", changeDesc) + val descriptor = Descriptor.BIP84Descriptor(Block.RegtestGenesisBlock.hash, master) + assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/<0;1>/*)#rw8h72wu", descriptor) } - } \ No newline at end of file