Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce TxId and TxHash types #90

Merged
merged 3 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
t-bast marked this conversation as resolved.
Show resolved Hide resolved

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