Skip to content

Commit 1fd1b69

Browse files
authored
Add basic support for wallet descriptors (#57)
* Add basic descriptor support * Remove unnecessary optin annotations
1 parent a2aa096 commit 1fd1b69

File tree

14 files changed

+128
-19
lines changed

14 files changed

+128
-19
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ kotlin {
6767
}
6868

6969
all {
70-
languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn")
70+
languageSettings.optIn("kotlin.RequiresOptIn")
7171
}
7272
}
7373

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import kotlin.jvm.JvmStatic
3232
* @param bits The calculated difficulty target being used for this block
3333
* @param nonce The nonce used to generate this block… to allow variations of the header and compute different hashes
3434
*/
35-
@OptIn(ExperimentalUnsignedTypes::class)
3635
public data class BlockHeader(
3736
@JvmField val version: Long,
3837
@JvmField val hashPreviousBlock: ByteVector32,
@@ -103,7 +102,6 @@ public data class BlockHeader(
103102
}
104103

105104
@JvmStatic
106-
@OptIn(ExperimentalUnsignedTypes::class)
107105
public fun getDifficulty(header: BlockHeader): UInt256 {
108106
val (diff, neg, _) = UInt256.decodeCompact(header.bits)
109107
return if (neg) -diff else diff
@@ -116,7 +114,6 @@ public data class BlockHeader(
116114
* by bitcoin core
117115
*/
118116
@JvmStatic
119-
@OptIn(ExperimentalUnsignedTypes::class)
120117
public fun blockProof(bits: Long): UInt256 {
121118
val (target, negative, overflow) = UInt256.decodeCompact(bits)
122119
return if (target == UInt256.Zero || negative || overflow) UInt256.Zero else {
@@ -186,7 +183,6 @@ public object MerkleTree {
186183
}
187184
}
188185

189-
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class)
190186
public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: List<Transaction>) {
191187
@JvmField
192188
val hash: ByteVector32 = header.hash

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import fr.acinq.bitcoin.io.Output
2424
import fr.acinq.secp256k1.Hex
2525
import kotlin.jvm.JvmStatic
2626

27-
@OptIn(ExperimentalUnsignedTypes::class)
2827
public abstract class BtcSerializer<T> {
2928
/**
3029
* write a message to a stream
@@ -196,7 +195,6 @@ public abstract class BtcSerializer<T> {
196195
}
197196

198197
@JvmStatic
199-
@OptIn(ExperimentalStdlibApi::class)
200198
public fun writeVarstring(input: String, out: Output) {
201199
writeVarint(input.length, out)
202200
writeBytes(input.encodeToByteArray(), out)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package fr.acinq.bitcoin
2+
3+
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
4+
import fr.acinq.bitcoin.DeterministicWallet.publicKey
5+
import kotlin.jvm.JvmStatic
6+
7+
public object Descriptor {
8+
private fun polyMod(cc: Long, value: Int): Long {
9+
var c = cc
10+
val c0 = c shr 35
11+
c = ((c and 0x7ffffffffL) shl 5) xor value.toLong()
12+
if ((c0 and 1L) != 0L) c = c xor 0xf5dee51989L
13+
if ((c0 and 2L) != 0L) c = c xor 0xa9fdca3312L
14+
if ((c0 and 4L) != 0L) c = c xor 0x1bab10e32dL
15+
if ((c0 and 8L) != 0L) c = c xor 0x3706b1677aL
16+
if ((c0 and 16L) != 0L) c = c xor 0x644d626ffdL
17+
return c
18+
}
19+
20+
@JvmStatic
21+
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+
*/
35+
val INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}" + "IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~" + "ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
36+
37+
/** The character set for the checksum itself (same as bech32). */
38+
val CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
39+
40+
var c = 1L
41+
var cls = 0
42+
var clscount = 0
43+
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 += 1
49+
if (clscount == 3) {
50+
// Emit an extra symbol representing the group numbers, for every 3 characters.
51+
c = polyMod(c, cls)
52+
cls = 0
53+
clscount = 0
54+
}
55+
}
56+
if (clscount > 0) c = polyMod(c, cls)
57+
for (j in 0 until 8) c = polyMod(c, 0) // Shift further to determine the checksum.
58+
c = c xor 1 // Prevent appending zeroes from not affecting the checksum.
59+
60+
val ret = StringBuilder(" ")
61+
for (j in 0 until 8) {
62+
val pos1 = (c shr (5 * (7 - j))) and 31
63+
val char = CHECKSUM_CHARSET[pos1.toInt()]
64+
ret[j] = char
65+
}
66+
return ret.toString()
67+
}
68+
69+
private fun getBIP84KeyPath(chainHash: ByteVector32): Pair<String, Int> = when (chainHash) {
70+
Block.RegtestGenesisBlock.hash, Block.TestnetGenesisBlock.hash -> "84'/1'/0'/0" to DeterministicWallet.tpub
71+
Block.LivenetGenesisBlock.hash -> "84'/0'/0'/0" to DeterministicWallet.xpub
72+
else -> error("invalid chain hash $chainHash")
73+
}
74+
75+
@JvmStatic
76+
public fun BIP84Descriptors(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): Pair<String, String> {
77+
val (keyPath, _) = getBIP84KeyPath(chainHash)
78+
val accountPub = publicKey(derivePrivateKey(master, KeyPath(keyPath)))
79+
val fingerprint = DeterministicWallet.fingerprint(master) and 0xFFFFFFFFL
80+
return BIP84Descriptors(chainHash, fingerprint, accountPub)
81+
}
82+
83+
@JvmStatic
84+
public fun BIP84Descriptors(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): Pair<String, String> {
85+
val (keyPath, prefix) = getBIP84KeyPath(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+
)
92+
}
93+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import kotlin.jvm.JvmStatic
2727
/**
2828
* see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
2929
*/
30-
@OptIn(ExperimentalUnsignedTypes::class)
3130
public object DeterministicWallet {
3231
public const val hardenedKeyIndex: Long = 0x80000000L
3332

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import kotlin.jvm.JvmStatic
2525

2626
public typealias RunnerCallback = (List<ScriptElt>, List<ByteVector>, Script.Runner.Companion.State) -> Boolean
2727

28-
@OptIn(ExperimentalUnsignedTypes::class)
2928
public object Script {
3029
public const val MaxScriptElementSize: Int = 520
3130
public const val LockTimeThreshold: Long = 500000000L

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import kotlin.jvm.JvmStatic
3030
* @param hash reversed sha256(sha256(tx)) where tx is the transaction we want to refer to
3131
* @param index index of the output in tx that we want to refer to
3232
*/
33-
@OptIn(ExperimentalUnsignedTypes::class)
3433
public data class OutPoint(@JvmField val hash: ByteVector32, @JvmField val index: Long) : BtcSerializable<OutPoint> {
3534
public constructor(hash: ByteArray, index: Long) : this(hash.byteVector32(), index)
3635

@@ -129,7 +128,6 @@ public data class ScriptWitness(@JvmField val stack: List<ByteVector>) : BtcSeri
129128
* information is updated before inclusion into a block. Repurposed for OP_CSV (see BIPs 68 & 112)
130129
* @param witness Transaction witness (i.e. what is in sig script for standard transactions).
131130
*/
132-
@OptIn(ExperimentalUnsignedTypes::class)
133131
public data class TxIn(
134132
@JvmField val outPoint: OutPoint,
135133
@JvmField val signatureScript: ByteVector,
@@ -212,7 +210,6 @@ public data class TxIn(
212210
override fun serializer(): BtcSerializer<TxIn> = TxIn
213211
}
214212

215-
@OptIn(ExperimentalUnsignedTypes::class)
216213
public data class TxOut(@JvmField val amount: Satoshi, @JvmField val publicKeyScript: ByteVector) : BtcSerializable<TxOut> {
217214

218215
public constructor(amount: Satoshi, publicKeyScript: ByteArray) : this(amount, publicKeyScript.byteVector())
@@ -259,7 +256,6 @@ public data class TxOut(@JvmField val amount: Satoshi, @JvmField val publicKeySc
259256
override fun serializer(): BtcSerializer<TxOut> = TxOut
260257
}
261258

262-
@OptIn(ExperimentalUnsignedTypes::class)
263259
public data class Transaction(
264260
@JvmField val version: Long,
265261
@JvmField val txIn: List<TxIn>,

src/commonMain/kotlin/fr/acinq/bitcoin/io/ByteArrayOutput.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public class ByteArrayOutput : Output {
2020
private var array: ByteArray = ByteArray(32)
2121
private var position: Int = 0
2222

23-
@OptIn(ExperimentalStdlibApi::class)
2423
private fun ensureCapacity(elementsToAppend: Int) {
2524
if (position + elementsToAppend <= array.size) return
2625
val newArray = ByteArray((position + elementsToAppend).takeHighestOneBit() shl 1)

src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import kotlin.jvm.JvmStatic
3030
* @param inputs signing data for each input of the transaction to be signed (order matches the unsigned tx).
3131
* @param outputs signing data for each output of the transaction to be signed (order matches the unsigned tx).
3232
*/
33-
@OptIn(ExperimentalUnsignedTypes::class)
3433
public data class Psbt(val global: Global, val inputs: List<Input>, val outputs: List<Output>) {
3534

3635
init {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package fr.acinq.bitcoin
1919
import kotlin.test.Test
2020
import kotlin.test.assertEquals
2121

22-
@OptIn(ExperimentalStdlibApi::class)
2322
class Base58TestsCommon {
2423
@Test
2524
fun `basic encode-decode tests`() {

0 commit comments

Comments
 (0)