Skip to content

Commit

Permalink
Introduce BlockId and BlockHash
Browse files Browse the repository at this point in the history
It was footgunny to use a `ByteVector32` here as well, as we can easily
mistake one for the other.
  • Loading branch information
t-bast committed Apr 11, 2023
1 parent 785bcc8 commit b9b8397
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 43 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 @@ -28,21 +28,21 @@ public fun <T> List<T>.updated(i: Int, t: T): List<T> = when (i) {

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 @@ -51,13 +51,13 @@ public object Bitcoin {
* understood only by native sewgit 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)

@JvmStatic
public fun addressFromPublicKeyScript(chainHash: ByteVector32, pubkeyScript: List<ScriptElt>): String? {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): String? {
return try {
when {
Script.isPay2pkh(pubkeyScript) -> {
Expand Down Expand Up @@ -111,7 +111,7 @@ public object Bitcoin {
}

@JvmStatic
public fun addressFromPublicKeyScript(chainHash: ByteVector32, pubkeyScript: ByteArray): String? {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): String? {
return try {
addressFromPublicKeyScript(chainHash, Script.parse(pubkeyScript))
} catch (e: Exception) {
Expand All @@ -120,7 +120,7 @@ public object Bitcoin {
}

@JvmStatic
public fun addressToPublicKeyScript(chainHash: ByteVector32, address: String): List<ScriptElt> {
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): List<ScriptElt> {
val witnessVersions = mapOf(
0.toByte() to OP_0,
1.toByte() to OP_1,
Expand Down
44 changes: 30 additions & 14 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 @@ -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
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
16 changes: 8 additions & 8 deletions src/commonTest/kotlin/fr/acinq/bitcoin/BitcoinTestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ class BitcoinTestsCommon {
fun `compute address from pubkey script`() {
val pub = PrivateKey.fromHex("0101010101010101010101010101010101010101010101010101010101010101").publicKey()

fun address(script: List<ScriptElt>, chainHash: ByteVector32) = addressFromPublicKeyScript(chainHash, script)
fun address(script: List<ScriptElt>, chainHash: BlockHash) = addressFromPublicKeyScript(chainHash, script)

listOf(Block.LivenetGenesisBlock.hash, Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash).forEach {
assertEquals(address(Script.pay2pkh(pub), it), computeP2PkhAddress(pub, it))
assertEquals(address(Script.pay2wpkh(pub), it), computeP2WpkhAddress(pub, it))
assertEquals(address(Script.pay2sh(Script.pay2wpkh(pub)), it), computeP2ShOfP2WpkhAddress(pub, it))
// all these chain hashes are invalid
assertEquals(address(Script.pay2pkh(pub), it.reversed()), null)
assertEquals(address(Script.pay2wpkh(pub), it.reversed()), null)
assertEquals(address(Script.pay2sh(Script.pay2wpkh(pub)), it.reversed()), null)
assertEquals(address(Script.pay2pkh(pub), BlockHash(it.value.reversed())), null)
assertEquals(address(Script.pay2wpkh(pub), BlockHash(it.value.reversed())), null)
assertEquals(address(Script.pay2sh(Script.pay2wpkh(pub)), BlockHash(it.value.reversed())), null)
}

listOf(
Expand Down Expand Up @@ -115,9 +115,9 @@ class BitcoinTestsCommon {

@Test
fun `check genesis block hashes`() {
assertEquals(Block.RegtestGenesisBlock.blockId, ByteVector32.fromValidHex("0x0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"))
assertEquals(Block.SignetGenesisBlock.blockId, ByteVector32.fromValidHex("0x00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6"))
assertEquals(Block.TestnetGenesisBlock.blockId, ByteVector32.fromValidHex("0x000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"))
assertEquals(Block.LivenetGenesisBlock.blockId, ByteVector32.fromValidHex("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"))
assertEquals(Block.RegtestGenesisBlock.blockId, BlockId("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"))
assertEquals(Block.SignetGenesisBlock.blockId, BlockId("00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6"))
assertEquals(Block.TestnetGenesisBlock.blockId, BlockId("000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"))
assertEquals(Block.LivenetGenesisBlock.blockId, BlockId("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"))
}
}
Loading

0 comments on commit b9b8397

Please sign in to comment.