Skip to content

Commit

Permalink
Add member methods to improve usability (#132)
Browse files Browse the repository at this point in the history
Add member methods to improve usability

Member methods (in addition to static methods) will make this library easier to use, especially from IDE/editors that implement code completion features.
  • Loading branch information
sstone authored Aug 20, 2024
1 parent bc2d1e4 commit e52c575
Show file tree
Hide file tree
Showing 4 changed files with 537 additions and 290 deletions.
42 changes: 32 additions & 10 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ public data class BlockHeader(

public fun setNonce(input: Long): BlockHeader = this.copy(nonce = input)

public fun difficulty(): UInt256 {
val (diff, neg, _) = UInt256.decodeCompact(bits)
return if (neg) -diff else diff
}

/**
*
* @return the amount of work represented by this block's difficulty target, as displayed by bitcoin core
*/
public fun blockProof(): UInt256 = blockProof(bits)

/**
* Proof of work: hash(header) <= target difficulty
*
* @return true if this block header validates its expected proof of work
*/
public fun checkProofOfWork(): Boolean {
val (target, _, _) = UInt256.decodeCompact(bits)
val hash = UInt256(blockId.value.toByteArray())
return hash <= target
}

@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
public companion object : BtcSerializer<BlockHeader>() {
override fun read(input: Input, protocolVersion: Long): BlockHeader {
Expand Down Expand Up @@ -120,10 +142,7 @@ public data class BlockHeader(
}

@JvmStatic
public fun getDifficulty(header: BlockHeader): UInt256 {
val (diff, neg, _) = UInt256.decodeCompact(header.bits)
return if (neg) -diff else diff
}
public fun getDifficulty(header: BlockHeader): UInt256 = header.difficulty()

/**
*
Expand Down Expand Up @@ -152,11 +171,7 @@ public data class BlockHeader(
* @return true if the input block header validates its expected proof of work
*/
@JvmStatic
public fun checkProofOfWork(header: BlockHeader): Boolean {
val (target, _, _) = UInt256.decodeCompact(header.bits)
val hash = UInt256(header.blockId.value.toByteArray())
return hash <= target
}
public fun checkProofOfWork(header: BlockHeader): Boolean = header.checkProofOfWork()

@JvmStatic
public fun calculateNextWorkRequired(lastHeader: BlockHeader, lastRetargetTime: Long): Long {
Expand Down Expand Up @@ -208,6 +223,13 @@ public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: Lis
@JvmField
val blockId: BlockId = header.blockId

/**
* Proof of work: hash(block) <= target difficulty
*
* @return true if the input block validates its expected proof of work
*/
public fun checkProofOfWork(): Boolean = BlockHeader.checkProofOfWork(header)

public companion object : BtcSerializer<Block>() {
override fun write(message: Block, out: Output, protocolVersion: Long) {
BlockHeader.write(message.header, out)
Expand Down Expand Up @@ -250,7 +272,7 @@ public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: Lis
* @return true if the input block validates its expected proof of work
*/
@JvmStatic
public fun checkProofOfWork(block: Block): Boolean = BlockHeader.checkProofOfWork(block.header)
public fun checkProofOfWork(block: Block): Boolean = block.checkProofOfWork()

/**
* Verify a tx inclusion proof (a merkle proof that a set of transactions are included in a given block)
Expand Down
196 changes: 113 additions & 83 deletions src/commonMain/kotlin/fr/acinq/bitcoin/DeterministicWallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,56 @@ public object DeterministicWallet {

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<Long>): 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.
Expand Down Expand Up @@ -93,6 +143,56 @@ public object DeterministicWallet {

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<Long>): 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<Int, ExtendedPublicKey> {
Expand All @@ -115,37 +215,16 @@ public object DeterministicWallet {
public fun encode(input: ExtendedPrivateKey, testnet: Boolean): String = encode(input, if (testnet) tprv else xprv)

@JvmStatic
public fun encode(input: ExtendedPrivateKey, prefix: Int): String {
val out = ByteArrayOutput()
out.write(input.depth)
Pack.writeInt32BE(input.parent.toInt(), out)
Pack.writeInt32BE(input.path.lastChildNumber.toInt(), out)
out.write(input.chaincode.toByteArray())
out.write(0)
out.write(input.secretkeybytes.toByteArray())
val buffer = out.toByteArray()
return Base58Check.encode(prefix, buffer)
}
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 {
val out = ByteArrayOutput()
write(input, out)
val buffer = out.toByteArray()
return Base58Check.encode(prefix, buffer)
}
public fun encode(input: ExtendedPublicKey, prefix: Int): String = input.encode(prefix)

@JvmStatic
public fun write(input: ExtendedPublicKey, out: Output) {
out.write(input.depth)
Pack.writeInt32BE(input.parent.toInt(), out)
Pack.writeInt32BE(input.path.lastChildNumber.toInt(), out)
out.write(input.chaincode.toByteArray())
out.write(input.publickeybytes.toByteArray())
}
public fun write(input: ExtendedPublicKey, out: Output): Unit = input.write(out)

/**
* @param seed random seed
Expand All @@ -171,16 +250,14 @@ public object DeterministicWallet {
* @return the public key for this private key
*/
@JvmStatic
public fun publicKey(input: ExtendedPrivateKey): ExtendedPublicKey {
return ExtendedPublicKey(input.publicKey.value, input.chaincode, depth = input.depth, path = input.path, parent = input.parent)
}
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 = Pack.int32LE(ByteArrayInput(Crypto.hash160(input.publickeybytes).take(4).reversed().toByteArray())).toLong()
public fun fingerprint(input: ExtendedPublicKey): Long = input.fingerprint()

/**
* @param input extended private key
Expand All @@ -195,80 +272,33 @@ public object DeterministicWallet {
* @return the derived private key at the specified index
*/
@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, index: Long): ExtendedPrivateKey {
val I = if (isHardened(index)) {
val data = arrayOf(0.toByte()).toByteArray() + parent.secretkeybytes.toByteArray() + Pack.writeInt32BE(index.toInt())
Crypto.hmac512(parent.chaincode.toByteArray(), data)
} else {
val data = publicKey(parent).publickeybytes.toByteArray() + Pack.writeInt32BE(index.toInt())
Crypto.hmac512(parent.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) + parent.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 = parent.depth + 1,
path = parent.path.derive(index),
parent = fingerprint(parent)
)
}
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 {
require(!isHardened(index)) { "Cannot derive public keys from public hardened keys" }

val I = Crypto.hmac512(
parent.chaincode.toByteArray(),
parent.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() + parent.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 = parent.depth + 1,
path = parent.path.derive(index),
parent = fingerprint(parent)
)
}
public fun derivePublicKey(parent: ExtendedPublicKey, index: Long): ExtendedPublicKey = parent.derivePublicKey(index)

@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, chain: List<Long>): ExtendedPrivateKey =
chain.fold(parent, DeterministicWallet::derivePrivateKey)
public fun derivePrivateKey(parent: ExtendedPrivateKey, chain: List<Long>): ExtendedPrivateKey = parent.derivePrivateKey(chain)

@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: KeyPath): ExtendedPrivateKey =
derivePrivateKey(parent, keyPath.path)
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: KeyPath): ExtendedPrivateKey = parent.derivePrivateKey(keyPath.path)

@JvmStatic
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: String): ExtendedPrivateKey =
derivePrivateKey(parent, KeyPath.fromPath(keyPath))
public fun derivePrivateKey(parent: ExtendedPrivateKey, keyPath: String): ExtendedPrivateKey = parent.derivePrivateKey(KeyPath.fromPath(keyPath))

@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, chain: List<Long>): ExtendedPublicKey =
chain.fold(parent, DeterministicWallet::derivePublicKey)
public fun derivePublicKey(parent: ExtendedPublicKey, chain: List<Long>): ExtendedPublicKey = parent.derivePublicKey(chain)

@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: KeyPath): ExtendedPublicKey =
derivePublicKey(parent, keyPath.path)
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: KeyPath): ExtendedPublicKey = parent.derivePublicKey(keyPath.path)

@JvmStatic
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: String): ExtendedPublicKey =
derivePublicKey(parent, KeyPath.fromPath(keyPath))
public fun derivePublicKey(parent: ExtendedPublicKey, keyPath: String): ExtendedPublicKey = parent.derivePublicKey(KeyPath.fromPath(keyPath))

// p2pkh mainnet
public const val xprv: Int = 0x0488ade4
Expand Down
Loading

0 comments on commit e52c575

Please sign in to comment.