commonMain.fr.acinq.bitcoin.DeterministicWallet.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bitcoin-kmp Show documentation
Show all versions of bitcoin-kmp Show documentation
A simple Kotlin Multiplatform library which implements most of the bitcoin protocol
The newest version!
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.bitcoin
import fr.acinq.bitcoin.DeterministicWallet.hardened
import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Output
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic
/**
* see https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
*/
public object DeterministicWallet {
public const val hardenedKeyIndex: Long = 0x80000000L
@JvmStatic
public fun hardened(index: Long): Long = hardenedKeyIndex + index
@JvmStatic
public fun isHardened(index: Long): Boolean = index >= hardenedKeyIndex
public data class ExtendedPrivateKey(
@JvmField val secretkeybytes: ByteVector32,
@JvmField val chaincode: ByteVector32,
@JvmField val depth: Int,
@JvmField val path: KeyPath,
@JvmField val parent: Long
) {
init {
require(Crypto.isPrivKeyValid(secretkeybytes.toByteArray())) { "private key is invalid" }
require(depth != 0 || parent == 0L) { "zero depth with non-zero parent fingerprint" }
require(depth != 0 || path.lastChildNumber == 0L) { "zero depth with non-zero child number" }
}
val privateKey: PrivateKey get() = PrivateKey(secretkeybytes)
val publicKey: PublicKey get() = privateKey.publicKey()
val extendedPublicKey: ExtendedPublicKey get() = ExtendedPublicKey(publicKey.value, chaincode, depth = depth, path = path, parent = parent)
/**
* @param index index of the child key
* @return the derived private key at the specified index
*/
public fun derivePrivateKey(index: Long): ExtendedPrivateKey {
val I = if (isHardened(index)) {
val data = arrayOf(0.toByte()).toByteArray() + secretkeybytes.toByteArray() + Pack.writeInt32BE(index.toInt())
Crypto.hmac512(chaincode.toByteArray(), data)
} else {
val data = extendedPublicKey.publickeybytes.toByteArray() + Pack.writeInt32BE(index.toInt())
Crypto.hmac512(chaincode.toByteArray(), data)
}
val IL = I.take(32).toByteArray()
val IR = I.takeLast(32).toByteArray()
require(Crypto.isPrivKeyValid(IL)) { "cannot generate child private key: IL is invalid" }
val key = PrivateKey(IL) + privateKey
require(Crypto.isPrivKeyValid(key.value.toByteArray())) { "cannot generate child private key: resulting private key is invalid" }
return ExtendedPrivateKey(
secretkeybytes = key.value,
chaincode = IR.byteVector32(),
depth = depth + 1,
path = path.derive(index),
parent = fingerprint()
)
}
public fun derivePrivateKey(chain: List): ExtendedPrivateKey = chain.fold(this) { k, i -> k.derivePrivateKey(i) }
public fun derivePrivateKey(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(keyPath.path)
public fun derivePrivateKey(keyPath: String): ExtendedPrivateKey = derivePrivateKey(KeyPath.fromPath(keyPath))
public fun encode(testnet: Boolean): String = this.encode(if (testnet) tprv else xprv)
public fun encode(prefix: Int): String {
val out = ByteArrayOutput()
out.write(depth)
Pack.writeInt32BE(parent.toInt(), out)
Pack.writeInt32BE(path.lastChildNumber.toInt(), out)
out.write(chaincode.toByteArray())
out.write(0)
out.write(secretkeybytes.toByteArray())
val buffer = out.toByteArray()
return Base58Check.encode(prefix, buffer)
}
public fun fingerprint(): Long = extendedPublicKey.fingerprint()
/**
* We avoid accidentally logging extended private keys.
* You should use an explicit method if you want to convert the extended private key to a string representation.
*/
override fun toString(): String = ""
public companion object {
@JvmStatic
public fun decode(input: String, parentPath: KeyPath = KeyPath.empty): Pair {
val (prefix, bin) = Base58Check.decodeWithIntPrefix(input)
require(prefix == xprv || prefix == yprv || prefix == zprv || prefix == tprv || prefix == uprv || prefix == vprv) { "invalid prefix" }
val bis = ByteArrayInput(bin)
val depth = bis.read()
val parent = Pack.int32BE(bis).toLong() and 0xffffffff
val childNumber = Pack.int32BE(bis).toLong() and 0xffffffff
val chaincode = ByteArray(32)
bis.read(chaincode, 0)
require(bis.read() == 0)
val secretKeyBytes = ByteArray(32)
bis.read(secretKeyBytes, 0)
return Pair(prefix, ExtendedPrivateKey(secretKeyBytes.byteVector32(), chaincode.byteVector32(), depth, parentPath.derive(childNumber), parent))
}
}
}
public data class ExtendedPublicKey(
@JvmField val publickeybytes: ByteVector,
@JvmField val chaincode: ByteVector32,
@JvmField val depth: Int,
@JvmField val path: KeyPath,
@JvmField val parent: Long
) {
init {
require(publickeybytes.size() == 33)
require(Crypto.isPubKeyValid(publickeybytes.toByteArray())) { "public key is invalid" }
require(depth != 0 || parent == 0L) { "zero depth with non-zero parent fingerprint" }
require(depth != 0 || path.lastChildNumber == 0L) { "zero depth with non-zero child number" }
}
val publicKey: PublicKey get() = PublicKey(publickeybytes)
/**
* @param index index of the child key
* @return the derived public key at the specified index
*/
public fun derivePublicKey(index: Long): ExtendedPublicKey {
require(!isHardened(index)) { "Cannot derive public keys from public hardened keys" }
val I = Crypto.hmac512(
chaincode.toByteArray(), publickeybytes.toByteArray() + Pack.writeInt32BE(index.toInt())
)
val IL = I.take(32).toByteArray()
val IR = I.takeLast(32).toByteArray()
require(Crypto.isPrivKeyValid(IL)) { "cannot generate child public key: IL is invalid" }
val Ki = PrivateKey(IL).publicKey() + publicKey
require(Crypto.isPubKeyValid(Ki.value.toByteArray())) { "cannot generate child public key: resulting public key is invalid" }
return ExtendedPublicKey(
publickeybytes = Ki.value,
chaincode = IR.byteVector32(),
depth = depth + 1,
path = path.derive(index),
parent = fingerprint()
)
}
public fun derivePublicKey(chain: List): ExtendedPublicKey = chain.fold(this) { k, i -> k.derivePublicKey(i) }
public fun derivePublicKey(keyPath: KeyPath): ExtendedPublicKey = derivePublicKey(keyPath.path)
public fun derivePublicKey(keyPath: String): ExtendedPublicKey = derivePublicKey(KeyPath.fromPath(keyPath))
public fun fingerprint(): Long = Pack.int32LE(ByteArrayInput(Crypto.hash160(publickeybytes).take(4).reversed().toByteArray())).toLong()
public fun encode(prefix: Int): String {
val out = ByteArrayOutput()
this.write(out)
val buffer = out.toByteArray()
return Base58Check.encode(prefix, buffer)
}
public fun encode(testnet: Boolean): String = encode(if (testnet) tpub else xpub)
public fun write(out: Output) {
out.write(depth)
Pack.writeInt32BE(parent.toInt(), out)
Pack.writeInt32BE(path.lastChildNumber.toInt(), out)
out.write(chaincode.toByteArray())
out.write(publickeybytes.toByteArray())
}
public companion object {
@JvmStatic
public fun decode(input: String, parentPath: KeyPath = KeyPath.empty): Pair {
val (prefix, bin) = Base58Check.decodeWithIntPrefix(input)
require(prefix == xpub || prefix == ypub || prefix == zpub || prefix == tpub || prefix == upub || prefix == vpub) { "invalid prefix" }
val bis = ByteArrayInput(bin)
val depth = bis.read()
val parent = Pack.int32BE(bis).toLong() and 0xffffffff
val childNumber = Pack.int32BE(bis).toLong() and 0xffffffff
val chaincode = ByteArray(32)
bis.read(chaincode, 0)
val publicKeyBytes = ByteArray(33)
bis.read(publicKeyBytes, 0)
return Pair(prefix, ExtendedPublicKey(publicKeyBytes.byteVector(), chaincode.byteVector32(), depth, parentPath.derive(childNumber), parent))
}
}
}
@JvmStatic
public fun encode(input: ExtendedPrivateKey, testnet: Boolean): String = encode(input, if (testnet) tprv else xprv)
@JvmStatic
public fun encode(input: ExtendedPrivateKey, prefix: Int): String = input.encode(prefix)
@JvmStatic
public fun encode(input: ExtendedPublicKey, testnet: Boolean): String = encode(input, if (testnet) tpub else xpub)
@JvmStatic
public fun encode(input: ExtendedPublicKey, prefix: Int): String = input.encode(prefix)
@JvmStatic
public fun write(input: ExtendedPublicKey, out: Output): Unit = input.write(out)
/**
* @param seed random seed
* @return a "master" private key
*/
@JvmStatic
public fun generate(seed: ByteArray): ExtendedPrivateKey {
val I = Crypto.hmac512("Bitcoin seed".encodeToByteArray(), seed)
val IL = I.take(32).toByteArray().byteVector32()
val IR = I.takeLast(32).toByteArray().byteVector32()
return ExtendedPrivateKey(IL, IR, depth = 0, path = KeyPath.empty, parent = 0L)
}
/**
* @param seed random seed
* @return a "master" private key
*/
@JvmStatic
public fun generate(seed: ByteVector): ExtendedPrivateKey = generate(seed.toByteArray())
/**
* @param input extended private key
* @return the public key for this private key
*/
@JvmStatic
public fun publicKey(input: ExtendedPrivateKey): ExtendedPublicKey = input.extendedPublicKey
/**
* @param input extended public key
* @return the fingerprint for this public key
*/
@JvmStatic
public fun fingerprint(input: ExtendedPublicKey): Long = input.fingerprint()
/**
* @param input extended private key
* @return the fingerprint for this private key (which is based on the corresponding public key)
*/
@JvmStatic
public fun fingerprint(input: ExtendedPrivateKey): Long = fingerprint(publicKey(input))
/**
* @param parent extended private key
* @param index index of the child key
* @return the derived private key at the specified index
*/
@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, index: Long): ExtendedPrivateKey = parent.derivePrivateKey(index)
/**
* @param parent extended public key
* @param index index of the child key
* @return the derived public key at the specified index
*/
@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = parent.derivePublicKey(index)
@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, chain: List): ExtendedPrivateKey = parent.derivePrivateKey(chain)
@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: KeyPath): ExtendedPrivateKey = parent.derivePrivateKey(keyPath.path)
@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: String): ExtendedPrivateKey = parent.derivePrivateKey(KeyPath.fromPath(keyPath))
@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, chain: List): ExtendedPublicKey = parent.derivePublicKey(chain)
@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: KeyPath): ExtendedPublicKey = parent.derivePublicKey(keyPath.path)
@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: String): ExtendedPublicKey = parent.derivePublicKey(KeyPath.fromPath(keyPath))
// p2pkh mainnet
public const val xprv: Int = 0x0488ade4
public const val xpub: Int = 0x0488b21e
// p2sh-of-p2wpkh mainnet
public const val yprv: Int = 0x049d7878
public const val ypub: Int = 0x049d7cb2
// p2wpkh mainnet
public const val zprv: Int = 0x04b2430c
public const val zpub: Int = 0x04b24746
// p2pkh testnet
public const val tprv: Int = 0x04358394
public const val tpub: Int = 0x043587cf
// p2sh-of-p2wpkh testnet
public const val uprv: Int = 0x044a4e28
public const val upub: Int = 0x044a5262
// p2wpkh testnet
public const val vprv: Int = 0x045f18bc
public const val vpub: Int = 0x045f1cf6
}
public data class KeyPath(@JvmField val path: List) {
public constructor(path: String) : this(computePath(path))
public val lastChildNumber: Long get() = if (path.isEmpty()) 0L else path.last()
public fun derive(number: Long): KeyPath = KeyPath(path + listOf(number))
public fun append(index: Long): KeyPath {
return KeyPath(path + listOf(index))
}
public fun append(indexes: List): KeyPath {
return KeyPath(path + indexes)
}
public fun append(that: KeyPath): KeyPath {
return KeyPath(path + that.path)
}
override fun toString(): String = asString('\'')
public fun asString(hardenedSuffix: Char): String = path.map { childNumberToString(it, hardenedSuffix) }.fold("m") { a, b -> "$a/$b" }
public companion object {
public val empty: KeyPath = KeyPath(listOf())
@JvmStatic
public fun computePath(path: String): List {
fun toNumber(value: String): Long = if (value.last() == '\'' || value.last() == 'h') hardened(value.dropLast(1).toLong()) else value.toLong()
val path1 = path.removePrefix("m").removePrefix("/")
return if (path1.isEmpty()) {
listOf()
} else {
path1.split('/').map { toNumber(it) }
}
}
@JvmStatic
public fun fromPath(path: String): KeyPath = KeyPath(path)
public fun childNumberToString(childNumber: Long, hardenedSuffix: Char = '\''): String = if (DeterministicWallet.isHardened(childNumber)) {
((childNumber - DeterministicWallet.hardenedKeyIndex).toString() + hardenedSuffix)
} else {
childNumber.toString()
}
}
}