Skip to content

Commit 0418d77

Browse files
committed
Add support for combined descriptor
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.
1 parent 2e30b88 commit 0418d77

File tree

2 files changed

+20
-38
lines changed

2 files changed

+20
-38
lines changed

src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
44
import fr.acinq.bitcoin.DeterministicWallet.publicKey
55
import kotlin.jvm.JvmStatic
66

7+
/**
8+
* Output Script Descriptors: see https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki
9+
*/
710
public object Descriptor {
811
private fun polyMod(cc: Long, value: Int): Long {
912
var c = cc
@@ -17,35 +20,21 @@ public object Descriptor {
1720
return c
1821
}
1922

23+
// Taken from: https://github.com/bitcoin/bitcoin/blob/207a22877330709e4462e6092c265ab55c8653ac/src/script/descriptor.cpp
2024
@JvmStatic
2125
public fun checksum(span: String): String {
22-
/** A character set designed such that:
23-
* - The most common 'unprotected' descriptor characters (hex, keypaths) are in the first group of 32.
24-
* - Case errors cause an offset that's a multiple of 32.
25-
* - As many alphabetic characters are in the same group (while following the above restrictions).
26-
*
27-
* If p(x) gives the position of a character c in this character set, every group of 3 characters
28-
* (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).
29-
* This means that changes that only affect the lower 5 bits of the position, or only the higher 2 bits, will just
30-
* affect a single symbol.
31-
*
32-
* As a result, within-group-of-32 errors count as 1 symbol, as do cross-group errors that don't affect
33-
* the position within the groups.
34-
*/
3526
val INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}" + "IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~" + "ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
36-
37-
/** The character set for the checksum itself (same as bech32). */
38-
val CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
27+
val CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
3928

4029
var c = 1L
4130
var cls = 0
4231
var clscount = 0
4332
span.forEach { ch ->
44-
val pos = INPUT_CHARSET.indexOf(ch);
45-
if (pos == -1) return "";
46-
c = polyMod(c, pos and 31); // Emit a symbol for the position inside the group, for every character.
47-
cls = cls * 3 + (pos shr 5); // Accumulate the group numbers
48-
clscount = clscount + 1
33+
val pos = INPUT_CHARSET.indexOf(ch)
34+
if (pos == -1) return ""
35+
c = polyMod(c, pos and 31) // Emit a symbol for the position inside the group, for every character.
36+
cls = cls * 3 + (pos shr 5) // Accumulate the group numbers
37+
clscount += 1
4938
if (clscount == 3) {
5039
// Emit an extra symbol representing the group numbers, for every 3 characters.
5140
c = polyMod(c, cls)
@@ -57,11 +46,10 @@ public object Descriptor {
5746
for (j in 0 until 8) c = polyMod(c, 0) // Shift further to determine the checksum.
5847
c = c xor 1 // Prevent appending zeroes from not affecting the checksum.
5948

60-
var ret = StringBuilder(" ")
49+
val ret = StringBuilder(" ")
6150
for (j in 0 until 8) {
6251
val pos1 = (c shr (5 * (7 - j))) and 31
63-
val char = CHECKSUM_CHARSET.get(pos1.toInt())
64-
ret.set(j, char)
52+
ret[j] = CHECKSUM_CHARSET[pos1.toInt()]
6553
}
6654
return ret.toString()
6755
}
@@ -73,21 +61,17 @@ public object Descriptor {
7361
}
7462

7563
@JvmStatic
76-
public fun BIP84Descriptors(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): Pair<String, String> {
64+
public fun createBIP84Descriptor(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): String {
7765
val (keyPath, _) = getKeyPath(chainHash)
7866
val accountPub = publicKey(derivePrivateKey(master, KeyPath(keyPath)))
7967
val fingerprint = DeterministicWallet.fingerprint(master) and 0xFFFFFFFFL
80-
return BIP84Descriptors(chainHash, fingerprint, accountPub)
68+
return createBIP84Descriptor(chainHash, fingerprint, accountPub)
8169
}
8270

8371
@JvmStatic
84-
public fun BIP84Descriptors(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): Pair<String, String> {
72+
public fun createBIP84Descriptor(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): String {
8573
val (keyPath, prefix) = getKeyPath(chainHash)
86-
val accountDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/0/*)"
87-
val changeDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/1/*)"
88-
return Pair(
89-
"$accountDesc#${checksum(accountDesc)}",
90-
"$changeDesc#${checksum(changeDesc)}"
91-
)
74+
val accountDesc = "wpkh([${fingerprint.toString(16)}/$keyPath]${DeterministicWallet.encode(accountPub, prefix)}/<0;1>/*)"
75+
return "$accountDesc#${checksum(accountDesc)}"
9276
}
9377
}

src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,14 @@ class DescriptorTestsCommon {
1919
data.forEach { dnc ->
2020
val (desc, checksum) = dnc.split('#').toTypedArray()
2121
assertEquals(checksum, Descriptor.checksum(desc))
22-
}
22+
}
2323
}
2424

2525
@Test
2626
fun `compute BIP84 descriptors`() {
2727
val seed = ByteVector.fromHex("817a9c8e6ba36f083d7e68b5ee89ce74fde9ef294a724a5efc5cef2b88db057f")
2828
val master = DeterministicWallet.generate(seed)
29-
val (accountDesc, changeDesc) = Descriptor.BIP84Descriptors(Block.RegtestGenesisBlock.hash, master)
30-
assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/0/*)#uysr3s9y", accountDesc)
31-
assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/1/*)#ds4zv94u", changeDesc)
29+
val descriptor = Descriptor.createBIP84Descriptor(Block.RegtestGenesisBlock.hash, master)
30+
assertEquals("wpkh([189ef5fe/84'/1'/0'/0]tpubDFTu6FhLqfTBLMd7BvGkyH1h4XBw7XoKWfnNNWw5Sp8V6aC55EhgPTVNAYvBwBXQ8EGnMqaZi3dpdSzhMbD4Z7ivZiaVKNMUkXVjDU1CDuE/<0;1>/*)#rw8h72wu", descriptor)
3231
}
33-
3432
}

0 commit comments

Comments
 (0)