Skip to content

Commit

Permalink
Introduce TxId and TxHash types (#90)
Browse files Browse the repository at this point in the history
It was really footgunny to only use a `ByteVector32` here, as users could
use a `TxHash` when a `TxId` was expected. Introducing explicit types
ensures that functions can clearly specify which one they expect.

We also introduce a `BlockId` and `BlockHash` type.
It was footgunny to use a `ByteVector32` here as well, as we can easily
mistake one for the other.
  • Loading branch information
t-bast authored Sep 13, 2023
1 parent f0fecaf commit 42039aa
Show file tree
Hide file tree
Showing 18 changed files with 213 additions and 138 deletions.
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/Bech32.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public object Bech32 {
}

@JvmStatic
public fun hrp(chainHash: ByteVector32): String = when (chainHash) {
public fun hrp(chainHash: BlockHash): String = when (chainHash) {
Block.TestnetGenesisBlock.hash -> "tb"
Block.SignetGenesisBlock.hash -> "tb"
Block.RegtestGenesisBlock.hash -> "bcrt"
Expand Down
18 changes: 9 additions & 9 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Bitcoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,21 @@ public sealed class AddressFromPublicKeyScriptResult {

public object Bitcoin {
@JvmStatic
public fun computeP2PkhAddress(pub: PublicKey, chainHash: ByteVector32): String = pub.p2pkhAddress(chainHash)
public fun computeP2PkhAddress(pub: PublicKey, chainHash: BlockHash): String = pub.p2pkhAddress(chainHash)

@JvmStatic
public fun computeBIP44Address(pub: PublicKey, chainHash: ByteVector32): String = computeP2PkhAddress(pub, chainHash)
public fun computeBIP44Address(pub: PublicKey, chainHash: BlockHash): String = computeP2PkhAddress(pub, chainHash)

/**
* @param pub public key
* @param chainHash chain hash (i.e. hash of the genesic block of the chain we're on)
* @return the p2swh-of-p2pkh address for this key. It is a Base58 address that is compatible with most bitcoin wallets
*/
@JvmStatic
public fun computeP2ShOfP2WpkhAddress(pub: PublicKey, chainHash: ByteVector32): String = pub.p2shOfP2wpkhAddress(chainHash)
public fun computeP2ShOfP2WpkhAddress(pub: PublicKey, chainHash: BlockHash): String = pub.p2shOfP2wpkhAddress(chainHash)

@JvmStatic
public fun computeBIP49Address(pub: PublicKey, chainHash: ByteVector32): String = computeP2ShOfP2WpkhAddress(pub, chainHash)
public fun computeBIP49Address(pub: PublicKey, chainHash: BlockHash): String = computeP2ShOfP2WpkhAddress(pub, chainHash)

/**
* @param pub public key
Expand All @@ -110,18 +110,18 @@ public object Bitcoin {
* understood only by native segwit wallets
*/
@JvmStatic
public fun computeP2WpkhAddress(pub: PublicKey, chainHash: ByteVector32): String = pub.p2wpkhAddress(chainHash)
public fun computeP2WpkhAddress(pub: PublicKey, chainHash: BlockHash): String = pub.p2wpkhAddress(chainHash)

@JvmStatic
public fun computeBIP84Address(pub: PublicKey, chainHash: ByteVector32): String = computeP2WpkhAddress(pub, chainHash)
public fun computeBIP84Address(pub: PublicKey, chainHash: BlockHash): String = computeP2WpkhAddress(pub, chainHash)

/**
* Compute an address from a public key script
* @param chainHash chain hash (i.e. hash of the genesis block of the chain we're on)
* @param pubkeyScript public key script
*/
@JvmStatic
public fun addressFromPublicKeyScript(chainHash: ByteVector32, pubkeyScript: List<ScriptElt>): AddressFromPublicKeyScriptResult {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): AddressFromPublicKeyScriptResult {
try {
return when {
Script.isPay2pkh(pubkeyScript) -> {
Expand Down Expand Up @@ -179,7 +179,7 @@ public object Bitcoin {
}

@JvmStatic
public fun addressFromPublicKeyScript(chainHash: ByteVector32, pubkeyScript: ByteArray): AddressFromPublicKeyScriptResult {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): AddressFromPublicKeyScriptResult {
return runCatching { Script.parse(pubkeyScript) }.fold(
onSuccess = {
addressFromPublicKeyScript(chainHash, it)
Expand All @@ -191,7 +191,7 @@ public object Bitcoin {
}

@JvmStatic
public fun addressToPublicKeyScript(chainHash: ByteVector32, address: String): AddressToPublicKeyScriptResult {
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): AddressToPublicKeyScriptResult {
val witnessVersions = mapOf(
0.toByte() to OP_0,
1.toByte() to OP_1,
Expand Down
46 changes: 31 additions & 15 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,49 @@ import kotlin.experimental.and
import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic

/** This is the double hash of a serialized block header. */
public data class BlockHash(@JvmField val value: ByteVector32) {
public constructor(hash: ByteArray) : this(hash.byteVector32())
public constructor(hash: String) : this(ByteVector32(hash))
public constructor(blockId: BlockId) : this(blockId.value.reversed())

override fun toString(): String = value.toString()
}

/** This contains the same data as [BlockHash], but encoded with the opposite endianness. */
public data class BlockId(@JvmField val value: ByteVector32) {
public constructor(blockId: ByteArray) : this(blockId.byteVector32())
public constructor(blockId: String) : this(ByteVector32(blockId))
public constructor(hash: BlockHash) : this(hash.value.reversed())

override fun toString(): String = value.toString()
}

/**
*
* @param version Block version information, based upon the software version creating this block
* @param hashPreviousBlock The hash value of the previous block this particular block references. Please note that
* this hash is not reversed (as opposed to Block.hash)
* @param hashPreviousBlock The hash value of the previous block this particular block references.
* @param hashMerkleRoot The reference to a Merkle tree collection which is a hash of all transactions related to this block
* @param time A timestamp recording when this block was created (Will overflow in 2106[2])
* @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
*/
public data class BlockHeader(
@JvmField val version: Long,
@JvmField val hashPreviousBlock: ByteVector32,
@JvmField val hashPreviousBlock: BlockHash,
@JvmField val hashMerkleRoot: ByteVector32,
@JvmField val time: Long,
@JvmField val bits: Long,
@JvmField val nonce: Long
) : BtcSerializable<BlockHeader> {
@JvmField
public val hash: ByteVector32 = ByteVector32(Crypto.hash256(write(this)))
public val hash: BlockHash = BlockHash(Crypto.hash256(write(this)))

@JvmField
public val blockId: ByteVector32 = hash.reversed()
public val blockId: BlockId = BlockId(hash)

public fun setVersion(input: Long): BlockHeader = this.copy(version = input)

public fun setHashPreviousBlock(input: ByteVector32): BlockHeader = this.copy(hashPreviousBlock = input)
public fun setHashPreviousBlock(input: BlockHash): BlockHeader = this.copy(hashPreviousBlock = input)

public fun setHashMerkleRoot(input: ByteVector32): BlockHeader = this.copy(hashMerkleRoot = input)

Expand All @@ -64,14 +80,14 @@ public data class BlockHeader(
public companion object : BtcSerializer<BlockHeader>() {
override fun read(input: Input, protocolVersion: Long): BlockHeader {
val version = uint32(input)
val hashPreviousBlock = hash(input)
val hashPreviousBlock = BlockHash(hash(input))
val hashMerkleRoot = hash(input)
val time = uint32(input)
val bits = uint32(input)
val nonce = uint32(input)
return BlockHeader(
version.toLong(),
hashPreviousBlock.byteVector32(),
hashPreviousBlock,
hashMerkleRoot.byteVector32(),
time.toLong(),
bits.toLong(),
Expand All @@ -91,7 +107,7 @@ public data class BlockHeader(

override fun write(message: BlockHeader, output: Output, protocolVersion: Long) {
writeUInt32(message.version.toUInt(), output)
writeBytes(message.hashPreviousBlock, output)
writeBytes(message.hashPreviousBlock.value, output)
writeBytes(message.hashMerkleRoot, output)
writeUInt32(message.time.toUInt(), output)
writeUInt32(message.bits.toUInt(), output)
Expand Down Expand Up @@ -138,7 +154,7 @@ public data class BlockHeader(
@JvmStatic
public fun checkProofOfWork(header: BlockHeader): Boolean {
val (target, _, _) = UInt256.decodeCompact(header.bits)
val hash = UInt256(header.blockId.toByteArray())
val hash = UInt256(header.blockId.value.toByteArray())
return hash <= target
}

Expand Down Expand Up @@ -187,10 +203,10 @@ public object MerkleTree {

public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: List<Transaction>) {
@JvmField
val hash: ByteVector32 = header.hash
val hash: BlockHash = header.hash

@JvmField
val blockId: ByteVector32 = hash.reversed()
val blockId: BlockId = header.blockId

public companion object : BtcSerializer<Block>() {
override fun write(message: Block, out: Output, protocolVersion: Long) {
Expand Down Expand Up @@ -222,7 +238,7 @@ public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: Lis
@JvmStatic
override fun validate(message: Block) {
BlockHeader.validate(message.header)
require(message.header.hashMerkleRoot == MerkleTree.computeRoot(message.tx.map { it.hash })) { "invalid block: merkle root mismatch" }
require(message.header.hashMerkleRoot == MerkleTree.computeRoot(message.tx.map { it.hash.value })) { "invalid block: merkle root mismatch" }
require(message.tx.map { it.hash }.toSet().size == message.tx.size) { "invalid block: duplicate transactions" }
message.tx.map { Transaction.validate(it) }
}
Expand Down Expand Up @@ -333,7 +349,7 @@ public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: Lis
Block(
BlockHeader(
version = 1,
hashPreviousBlock = ByteVector32.Zeroes,
hashPreviousBlock = BlockHash(ByteVector32.Zeroes),
hashMerkleRoot = ByteVector32("3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a"),
time = 1231006505,
bits = 0x1d00ffff,
Expand Down
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,22 @@ public object Descriptor {
return ret.toString()
}

private fun getBIP84KeyPath(chainHash: ByteVector32): Pair<String, Int> = when (chainHash) {
private fun getBIP84KeyPath(chainHash: BlockHash): Pair<String, Int> = 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<String, String> {
public fun BIP84Descriptors(chainHash: BlockHash, master: DeterministicWallet.ExtendedPrivateKey): Pair<String, String> {
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<String, String> {
public fun BIP84Descriptors(chainHash: BlockHash, fingerprint: Long, accountPub: DeterministicWallet.ExtendedPublicKey): Pair<String, String> {
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/*)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public object LexicographicalOrdering {

@JvmStatic
public fun isLessThan(a: OutPoint, b: OutPoint): Boolean {
return if (a.txid == b.txid) a.index < b.index else isLessThan(a.txid, b.txid)
return if (a.txid == b.txid) a.index < b.index else isLessThan(a.txid.value, b.txid.value)
}

@JvmStatic
Expand Down
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/fr/acinq/bitcoin/PublicKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public data class PublicKey(@JvmField val value: ByteVector) {
* @param chainHash chain hash (i.e. hash of the genesis block of the chain we're on)
* @return the "legacy" p2pkh address for this key
*/
public fun p2pkhAddress(chainHash: ByteVector32): String = when (chainHash) {
public fun p2pkhAddress(chainHash: BlockHash): String = when (chainHash) {
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, hash160())
Block.LivenetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.PubkeyAddress, hash160())
else -> error("invalid chain hash $chainHash")
Expand All @@ -87,7 +87,7 @@ public data class PublicKey(@JvmField val value: ByteVector) {
* @return the p2swh-of-p2pkh address for this key.
* It is a Base58 address that is compatible with most bitcoin wallets.
*/
public fun p2shOfP2wpkhAddress(chainHash: ByteVector32): String {
public fun p2shOfP2wpkhAddress(chainHash: BlockHash): String {
val script = Script.pay2wpkh(this)
val hash = Crypto.hash160(Script.write(script))
return when (chainHash) {
Expand All @@ -102,7 +102,7 @@ public data class PublicKey(@JvmField val value: ByteVector) {
* @return the BIP84 address for this key (i.e. the p2wpkh address for this key).
* It is a Bech32 address that will be understood only by native segwit wallets.
*/
public fun p2wpkhAddress(chainHash: ByteVector32): String {
public fun p2wpkhAddress(chainHash: BlockHash): String {
return Bech32.encodeWitnessAddress(Bech32.hrp(chainHash), 0, hash160())
}

Expand Down
54 changes: 38 additions & 16 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Transaction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,54 @@ import kotlin.jvm.JvmField
import kotlin.jvm.JvmStatic

/**
* an out point is a reference to a specific output in a specific transaction that we want to claim
*
* @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
* This is the double hash of a transaction serialized without witness data.
* Note that this is confusingly called `txid` in some context (e.g. in lightning messages).
*/
public data class OutPoint(@JvmField val hash: ByteVector32, @JvmField val index: Long) : BtcSerializable<OutPoint> {
public constructor(hash: ByteArray, index: Long) : this(hash.byteVector32(), index)
public data class TxHash(@JvmField val value: ByteVector32) {
public constructor(hash: ByteArray) : this(hash.byteVector32())
public constructor(hash: String) : this(ByteVector32(hash))
public constructor(txid: TxId) : this(txid.value.reversed())

override fun toString(): String = value.toString()
}

/**
* This contains the same data as [TxHash], but encoded with the opposite endianness.
* Some explorers and bitcoin RPCs use this encoding for their inputs.
*/
public data class TxId(@JvmField val value: ByteVector32) {
public constructor(txid: ByteArray) : this(txid.byteVector32())
public constructor(txid: String) : this(ByteVector32(txid))
public constructor(hash: TxHash) : this(hash.value.reversed())

override fun toString(): String = value.toString()
}

/**
* An OutPoint is a reference to a specific output in a specific transaction.
*
* @param hash 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.
*/
public data class OutPoint(@JvmField val hash: TxHash, @JvmField val index: Long) : BtcSerializable<OutPoint> {
public constructor(tx: Transaction, index: Long) : this(tx.hash, index)
public constructor(txid: TxId, index: Long) : this(TxHash(txid), index)

init {
// The genesis block contains inputs with index = -1, so we cannot require it to be >= 0
require(index >= -1)
}

/**
* @return the id of the transaction this output belongs to
*/
@JvmField
public val txid: ByteVector32 = hash.reversed()
public val txid: TxId = TxId(hash)

public val isCoinbase: Boolean get() = isCoinbase(this)

override fun toString(): String = "$txid:$index"

public companion object : BtcSerializer<OutPoint>() {
@JvmStatic
override fun read(input: Input, protocolVersion: Long): OutPoint = OutPoint(hash(input), uint32(input).toLong())
override fun read(input: Input, protocolVersion: Long): OutPoint = OutPoint(TxHash(hash(input)), uint32(input).toLong())

@JvmStatic
override fun read(input: ByteArray): OutPoint {
Expand All @@ -59,7 +81,7 @@ public data class OutPoint(@JvmField val hash: ByteVector32, @JvmField val index

@JvmStatic
override fun write(message: OutPoint, out: Output, protocolVersion: Long) {
out.write(message.hash.toByteArray())
out.write(message.hash.value.toByteArray())
writeUInt32(message.index.toUInt(), out)
}

Expand All @@ -69,7 +91,7 @@ public data class OutPoint(@JvmField val hash: ByteVector32, @JvmField val index
}

@JvmStatic
public fun isCoinbase(input: OutPoint): Boolean = input.index == 0xffffffffL && input.hash == ByteVector32.Zeroes
public fun isCoinbase(input: OutPoint): Boolean = input.index == 0xffffffffL && input.hash.value == ByteVector32.Zeroes

@JvmStatic
public fun isNull(input: OutPoint): Boolean = isCoinbase(input)
Expand Down Expand Up @@ -201,7 +223,7 @@ public data class TxIn(
@JvmStatic
public fun coinbase(script: ByteArray): TxIn {
require(script.size in 2..100) { "coinbase script length must be between 2 and 100" }
return TxIn(OutPoint(ByteArray(32), 0xffffffffL), script, sequence = 0xffffffffL)
return TxIn(OutPoint(TxHash(ByteArray(32)), 0xffffffffL), script, sequence = 0xffffffffL)
}

@JvmStatic
Expand Down Expand Up @@ -282,10 +304,10 @@ public data class Transaction(
public val hasWitness: Boolean get() = txIn.any { it.hasWitness }

@JvmField
public val hash: ByteVector32 = Crypto.hash256(Transaction.write(this, SERIALIZE_TRANSACTION_NO_WITNESS)).byteVector32()
public val hash: TxHash = TxHash(Crypto.hash256(Transaction.write(this, SERIALIZE_TRANSACTION_NO_WITNESS)))

@JvmField
public val txid: ByteVector32 = hash.reversed()
public val txid: TxId = TxId(hash)

/**
* @param i index of the tx input to update
Expand Down
Loading

0 comments on commit 42039aa

Please sign in to comment.