diff --git a/build.gradle.kts b/build.gradle.kts index ec524f3e..ffdecb69 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,7 +67,7 @@ kotlin { } all { - languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + languageSettings.optIn("kotlin.RequiresOptIn") } } diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt index b68f4d4c..806dd45c 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt @@ -32,7 +32,6 @@ import kotlin.jvm.JvmStatic * @param bits The calculated difficulty target being used for this block * @param nonce The nonce used to generate this block… to allow variations of the header and compute different hashes */ -@OptIn(ExperimentalUnsignedTypes::class) public data class BlockHeader( @JvmField val version: Long, @JvmField val hashPreviousBlock: ByteVector32, @@ -103,7 +102,6 @@ public data class BlockHeader( } @JvmStatic - @OptIn(ExperimentalUnsignedTypes::class) public fun getDifficulty(header: BlockHeader): UInt256 { val (diff, neg, _) = UInt256.decodeCompact(header.bits) return if (neg) -diff else diff @@ -116,7 +114,6 @@ public data class BlockHeader( * by bitcoin core */ @JvmStatic - @OptIn(ExperimentalUnsignedTypes::class) public fun blockProof(bits: Long): UInt256 { val (target, negative, overflow) = UInt256.decodeCompact(bits) return if (target == UInt256.Zero || negative || overflow) UInt256.Zero else { @@ -186,7 +183,6 @@ public object MerkleTree { } } -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: List) { @JvmField val hash: ByteVector32 = header.hash diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/BtcSerializer.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/BtcSerializer.kt index 72592aca..28e8f70b 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/BtcSerializer.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/BtcSerializer.kt @@ -24,7 +24,6 @@ import fr.acinq.bitcoin.io.Output import fr.acinq.secp256k1.Hex import kotlin.jvm.JvmStatic -@OptIn(ExperimentalUnsignedTypes::class) public abstract class BtcSerializer { /** * write a message to a stream @@ -196,7 +195,6 @@ public abstract class BtcSerializer { } @JvmStatic - @OptIn(ExperimentalStdlibApi::class) public fun writeVarstring(input: String, out: Output) { writeVarint(input.length, out) writeBytes(input.encodeToByteArray(), out) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt new file mode 100644 index 00000000..8e48aa39 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt @@ -0,0 +1,93 @@ +package fr.acinq.bitcoin + +import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey +import fr.acinq.bitcoin.DeterministicWallet.publicKey +import kotlin.jvm.JvmStatic + +public object Descriptor { + private fun polyMod(cc: Long, value: Int): Long { + var c = cc + val c0 = c shr 35 + c = ((c and 0x7ffffffffL) shl 5) xor value.toLong() + if ((c0 and 1L) != 0L) c = c xor 0xf5dee51989L + if ((c0 and 2L) != 0L) c = c xor 0xa9fdca3312L + if ((c0 and 4L) != 0L) c = c xor 0x1bab10e32dL + if ((c0 and 8L) != 0L) c = c xor 0x3706b1677aL + if ((c0 and 16L) != 0L) c = c xor 0x644d626ffdL + return c + } + + @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"; + + 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 + clscount += 1 + if (clscount == 3) { + // Emit an extra symbol representing the group numbers, for every 3 characters. + c = polyMod(c, cls) + cls = 0 + clscount = 0 + } + } + if (clscount > 0) c = polyMod(c, cls) + for (j in 0 until 8) c = polyMod(c, 0) // Shift further to determine the checksum. + c = c xor 1 // Prevent appending zeroes from not affecting the checksum. + + 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 + } + return ret.toString() + } + + private fun getBIP84KeyPath(chainHash: ByteVector32): Pair = when (chainHash) { + Block.RegtestGenesisBlock.hash, Block.TestnetGenesisBlock.hash -> "84'/1'/0'/0" to DeterministicWallet.tpub + Block.LivenetGenesisBlock.hash -> "84'/0'/0'/0" to DeterministicWallet.xpub + else -> error("invalid chain hash $chainHash") + } + + @JvmStatic + public fun BIP84Descriptors(chainHash: ByteVector32, master: DeterministicWallet.ExtendedPrivateKey): Pair { + val (keyPath, _) = getBIP84KeyPath(chainHash) + val accountPub = publicKey(derivePrivateKey(master, KeyPath(keyPath))) + val fingerprint = DeterministicWallet.fingerprint(master) and 0xFFFFFFFFL + return BIP84Descriptors(chainHash, fingerprint, accountPub) + } + + @JvmStatic + public fun BIP84Descriptors(chainHash: ByteVector32, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): Pair { + 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)}" + ) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/DeterministicWallet.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/DeterministicWallet.kt index 43dec222..b2c050a8 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/DeterministicWallet.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/DeterministicWallet.kt @@ -27,7 +27,6 @@ import kotlin.jvm.JvmStatic /** * see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki */ -@OptIn(ExperimentalUnsignedTypes::class) public object DeterministicWallet { public const val hardenedKeyIndex: Long = 0x80000000L diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt index 8fbe861e..326b8e03 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Script.kt @@ -25,7 +25,6 @@ import kotlin.jvm.JvmStatic public typealias RunnerCallback = (List, List, Script.Runner.Companion.State) -> Boolean -@OptIn(ExperimentalUnsignedTypes::class) public object Script { public const val MaxScriptElementSize: Int = 520 public const val LockTimeThreshold: Long = 500000000L diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt index 4e92ee3d..64462d91 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt @@ -30,7 +30,6 @@ import kotlin.jvm.JvmStatic * @param hash reversed sha256(sha256(tx)) where tx is the transaction we want to refer to * @param index index of the output in tx that we want to refer to */ -@OptIn(ExperimentalUnsignedTypes::class) public data class OutPoint(@JvmField val hash: ByteVector32, @JvmField val index: Long) : BtcSerializable { public constructor(hash: ByteArray, index: Long) : this(hash.byteVector32(), index) @@ -129,7 +128,6 @@ public data class ScriptWitness(@JvmField val stack: List) : BtcSeri * information is updated before inclusion into a block. Repurposed for OP_CSV (see BIPs 68 & 112) * @param witness Transaction witness (i.e. what is in sig script for standard transactions). */ -@OptIn(ExperimentalUnsignedTypes::class) public data class TxIn( @JvmField val outPoint: OutPoint, @JvmField val signatureScript: ByteVector, @@ -212,7 +210,6 @@ public data class TxIn( override fun serializer(): BtcSerializer = TxIn } -@OptIn(ExperimentalUnsignedTypes::class) public data class TxOut(@JvmField val amount: Satoshi, @JvmField val publicKeyScript: ByteVector) : BtcSerializable { 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 override fun serializer(): BtcSerializer = TxOut } -@OptIn(ExperimentalUnsignedTypes::class) public data class Transaction( @JvmField val version: Long, @JvmField val txIn: List, diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/io/ByteArrayOutput.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/io/ByteArrayOutput.kt index 08cdf392..cbab812f 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/io/ByteArrayOutput.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/io/ByteArrayOutput.kt @@ -20,7 +20,6 @@ public class ByteArrayOutput : Output { private var array: ByteArray = ByteArray(32) private var position: Int = 0 - @OptIn(ExperimentalStdlibApi::class) private fun ensureCapacity(elementsToAppend: Int) { if (position + elementsToAppend <= array.size) return val newArray = ByteArray((position + elementsToAppend).takeHighestOneBit() shl 1) diff --git a/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt b/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt index bc8f1958..f43aed4f 100644 --- a/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt +++ b/src/commonMain/kotlin/fr/acinq/bitcoin/psbt/Psbt.kt @@ -30,7 +30,6 @@ import kotlin.jvm.JvmStatic * @param inputs signing data for each input of the transaction to be signed (order matches the unsigned tx). * @param outputs signing data for each output of the transaction to be signed (order matches the unsigned tx). */ -@OptIn(ExperimentalUnsignedTypes::class) public data class Psbt(val global: Global, val inputs: List, val outputs: List) { init { diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/Base58TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/Base58TestsCommon.kt index 60460d61..62b552df 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/Base58TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/Base58TestsCommon.kt @@ -19,7 +19,6 @@ package fr.acinq.bitcoin import kotlin.test.Test import kotlin.test.assertEquals -@OptIn(ExperimentalStdlibApi::class) class Base58TestsCommon { @Test fun `basic encode-decode tests`() { diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt new file mode 100644 index 00000000..243dd266 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/DescriptorTestsCommon.kt @@ -0,0 +1,34 @@ +package fr.acinq.bitcoin + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DescriptorTestsCommon { + @Test + fun `compute descriptor checksums`() { + val data = listOf( + "pkh([6ded4eb8/44h/0h/0h]xpub6C6N5WVF5zmurBR52MZZj8Jxm6eDiKyM4wFCm7xTYBEsAvJPqBKp2u2K7RTsZaYDN8duBWq4acrD4vrwjaKHTYuntGjL334nVHtLNuaj5Mu/0/*)#5mzpq0w6", + "wpkh([6ded4eb8/84h/0h/0h]xpub6CDeom4xT3Wg7BuyXU2Sd9XerTKttyfxRwJE36mi5HxFYpYdtdwM76Zx8swPnc6zxuArMYJgjNy91fJ13YtGPHgf49YqA8KdXg6D69tzNFh/0/*)#refya6f0", + "sh(wpkh([6ded4eb8/49h/0h/0h]xpub6Cb8jR9kYsfC6kj9CsE18SyudWjW2V3FnBFkT2oqq6n7NWWvJrjhFin3sAYg8X7ApX8iPophBa98mo4nMvSxnqrXvpnwaRopecQz859Ai1s/0/*))#xrhyhtvl", + "tr([6ded4eb8/86h/0h/0h]xpub6CDp1iw76taes3pkqfiJ6PYhwURkaYksJ62CrrdTVr6ow9wR9mKAtUGoZQqb8pRDiq2F8k31tYrrJjVGTRSLYGQ7nYpmewH94ThsAgDxJ4h/0/*)#2nm7drky", + "pkh([6ded4eb8/44h/0h/0h]xpub6C6N5WVF5zmurBR52MZZj8Jxm6eDiKyM4wFCm7xTYBEsAvJPqBKp2u2K7RTsZaYDN8duBWq4acrD4vrwjaKHTYuntGjL334nVHtLNuaj5Mu/1/*)#908qa67z", + "wpkh([6ded4eb8/84h/0h/0h]xpub6CDeom4xT3Wg7BuyXU2Sd9XerTKttyfxRwJE36mi5HxFYpYdtdwM76Zx8swPnc6zxuArMYJgjNy91fJ13YtGPHgf49YqA8KdXg6D69tzNFh/1/*)#jdv9q0eh", + "sh(wpkh([6ded4eb8/49h/0h/0h]xpub6Cb8jR9kYsfC6kj9CsE18SyudWjW2V3FnBFkT2oqq6n7NWWvJrjhFin3sAYg8X7ApX8iPophBa98mo4nMvSxnqrXvpnwaRopecQz859Ai1s/1/*))#nzej05eq", + "tr([6ded4eb8/86h/0h/0h]xpub6CDp1iw76taes3pkqfiJ6PYhwURkaYksJ62CrrdTVr6ow9wR9mKAtUGoZQqb8pRDiq2F8k31tYrrJjVGTRSLYGQ7nYpmewH94ThsAgDxJ4h/1/*)#m87lskxu" + ) + 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) + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Ripemd160TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Ripemd160TestsCommon.kt index 45dbd1af..6ca9c891 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Ripemd160TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Ripemd160TestsCommon.kt @@ -20,7 +20,6 @@ import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertTrue -@OptIn(ExperimentalStdlibApi::class) class Ripemd160TestsCommon { @Test // from https://homes.esat.kuleuven.be/~bosselae/ripemd160.html fun `reference tests`() { diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha1TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha1TestsCommon.kt index 4d65491b..8038ce63 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha1TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha1TestsCommon.kt @@ -20,7 +20,6 @@ import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertTrue -@OptIn(ExperimentalStdlibApi::class) class Sha1TestsCommon { val testVectors = arrayOf( "" to "da39a3ee5e6b4b0d3255bfef95601890afd80709", diff --git a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha512TestsCommon.kt b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha512TestsCommon.kt index c5a9d734..6d332e5f 100644 --- a/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha512TestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/bitcoin/crypto/Sha512TestsCommon.kt @@ -20,7 +20,6 @@ import fr.acinq.secp256k1.Hex import kotlin.test.Test import kotlin.test.assertTrue -@OptIn(ExperimentalStdlibApi::class) class Sha512TestsCommon { @Test fun `reference tests`() {