Skip to content

Commit

Permalink
Add support for combined descriptor
Browse files Browse the repository at this point in the history
As proposed by achow101 on the mailing list, see
https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-July/020791.html

Also fix a few compiler warnings.
  • Loading branch information
t-bast committed Aug 3, 2022
1 parent 1fd1b69 commit 80ed6a5
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 36 deletions.
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)
}

}

0 comments on commit 80ed6a5

Please sign in to comment.