Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 15 additions & 31 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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()
}
Expand All @@ -73,21 +61,17 @@ public object Descriptor {
}

@JvmStatic
public fun BIP84Descriptors(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): Pair<String, String> {
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<String, String> {
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)}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}