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

Add support for testnet4 #136

Merged
merged 2 commits into from
Aug 22, 2024
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
3 changes: 2 additions & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/Bech32.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public object Bech32 {

@JvmStatic
public fun hrp(chainHash: BlockHash): String = when (chainHash) {
Block.TestnetGenesisBlock.hash -> "tb"
Block.Testnet4GenesisBlock.hash -> "tb"
Block.Testnet3GenesisBlock.hash -> "tb"
Block.SignetGenesisBlock.hash -> "tb"
Block.RegtestGenesisBlock.hash -> "bcrt"
Block.LivenetGenesisBlock.hash -> "bc"
Expand Down
17 changes: 10 additions & 7 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Bitcoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public object Bitcoin {
Script.isPay2pkh(pubkeyScript) -> {
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.PubkeyAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.PubkeyAddressTestnet
Block.Testnet4GenesisBlock.hash, Block.Testnet3GenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.PubkeyAddressTestnet
else -> return Either.Left(BitcoinError.InvalidChainHash)
}
Either.Right(Base58Check.encode(prefix, (pubkeyScript[2] as OP_PUSHDATA).data))
Expand All @@ -125,7 +125,7 @@ public object Bitcoin {
Script.isPay2sh(pubkeyScript) -> {
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.ScriptAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.ScriptAddressTestnet
Block.Testnet4GenesisBlock.hash, Block.Testnet3GenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.ScriptAddressTestnet
else -> return Either.Left(BitcoinError.InvalidChainHash)
}
Either.Right(Base58Check.encode(prefix, (pubkeyScript[1] as OP_PUSHDATA).data))
Expand Down Expand Up @@ -204,13 +204,13 @@ public object Bitcoin {
return runCatching { Base58Check.decode(address) }.fold(
onSuccess = {
when {
it.first == Base58.Prefix.PubkeyAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
it.first == Base58.Prefix.PubkeyAddressTestnet && (chainHash == Block.Testnet4GenesisBlock.hash || chainHash == Block.Testnet3GenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
Either.Right(Script.pay2pkh(it.second))

it.first == Base58.Prefix.PubkeyAddress && chainHash == Block.LivenetGenesisBlock.hash ->
Either.Right(Script.pay2pkh(it.second))

it.first == Base58.Prefix.ScriptAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
it.first == Base58.Prefix.ScriptAddressTestnet && (chainHash == Block.Testnet4GenesisBlock.hash || chainHash == Block.Testnet3GenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
Either.Right(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))

it.first == Base58.Prefix.ScriptAddress && chainHash == Block.LivenetGenesisBlock.hash ->
Expand All @@ -227,7 +227,8 @@ public object Bitcoin {
witnessVersion == null -> Either.Left(BitcoinError.InvalidWitnessVersion(it.second.toInt()))
it.third.size != 20 && it.third.size != 32 -> Either.Left(BitcoinError.InvalidBech32Address)
it.first == "bc" && chainHash == Block.LivenetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.TestnetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.Testnet4GenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.Testnet3GenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.SignetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "bcrt" && chainHash == Block.RegtestGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
else -> Either.Left(BitcoinError.ChainHashMismatch)
Expand All @@ -244,12 +245,14 @@ public object Bitcoin {

public sealed class Chain(public val name: String, private val genesis: Block) {
public object Regtest : Chain("Regtest", Block.RegtestGenesisBlock)
public object Testnet : Chain("Testnet", Block.TestnetGenesisBlock)
public object Testnet3 : Chain("Testnet3", Block.Testnet3GenesisBlock)
public object Testnet4 : Chain("Testnet4", Block.Testnet4GenesisBlock)
public object Signet : Chain("Signet", Block.SignetGenesisBlock)
public object Mainnet : Chain("Mainnet", Block.LivenetGenesisBlock)

public fun isMainnet(): Boolean = this is Mainnet
public fun isTestnet(): Boolean = this is Testnet
public fun isTestnet3(): Boolean = this is Testnet3
public fun isTestnet4(): Boolean = this is Testnet4

public val chainHash: BlockHash get() = genesis.hash

Expand Down
33 changes: 32 additions & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,41 @@ public data class Block(@JvmField val header: BlockHeader, @JvmField val tx: Lis
}

@JvmField
public val TestnetGenesisBlock: Block = LivenetGenesisBlock.copy(
public val Testnet3GenesisBlock: Block = LivenetGenesisBlock.copy(
header = LivenetGenesisBlock.header.copy(time = 1296688602, nonce = 414098458)
)

@JvmField
public val Testnet4GenesisBlock: Block = run {
val script = listOf(
OP_PUSHDATA(writeUInt32(486604799u)),
OP_PUSHDATA(ByteVector("04")),
OP_PUSHDATA("03/May/2024 000000000000000000001ebd58c244970b3aa9d783bb001011fbe8ea8e98e00e".encodeToByteArray())
)
val scriptPubKey = listOf(
OP_PUSHDATA(ByteVector("000000000000000000000000000000000000000000000000000000000000000000")),
OP_CHECKSIG
)
Block(
BlockHeader(
version = 1,
hashPreviousBlock = BlockHash(ByteVector32.Zeroes),
hashMerkleRoot = ByteVector32("7aa0a7ae1e223414cb807e40cd57e667b718e42aaf9306db9102fe28912b7b4e").reversed(),
time = 1714777860,
bits = 0x1d00ffff,
nonce = 393743547
),
listOf(
Transaction(
version = 1,
txIn = listOf(TxIn.coinbase(script)),
txOut = listOf(TxOut(amount = 5000000000.toSatoshi(), publicKeyScript = scriptPubKey)),
lockTime = 0
)
)
)
}

@JvmField
public val RegtestGenesisBlock: Block = LivenetGenesisBlock.copy(
header = LivenetGenesisBlock.header.copy(
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/Descriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public object Descriptor {
}

private fun getBIP84KeyPath(chainHash: BlockHash): Pair<String, Int> = when (chainHash) {
Block.RegtestGenesisBlock.hash, Block.TestnetGenesisBlock.hash -> "84'/1'/0'/0" to DeterministicWallet.tpub
Block.Testnet4GenesisBlock.hash, Block.Testnet3GenesisBlock.hash, Block.RegtestGenesisBlock.hash -> "84'/1'/0'/0" to DeterministicWallet.tpub
Block.LivenetGenesisBlock.hash -> "84'/0'/0'/0" to DeterministicWallet.xpub
else -> error("invalid chain hash $chainHash")
}
Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/fr/acinq/bitcoin/PublicKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public data class PublicKey(@JvmField val value: ByteVector) {
* @return the "legacy" p2pkh address for this key
*/
public fun p2pkhAddress(chainHash: BlockHash): String = when (chainHash) {
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, hash160())
Block.Testnet4GenesisBlock.hash, Block.Testnet3GenesisBlock.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 @@ -91,7 +91,7 @@ public data class PublicKey(@JvmField val value: ByteVector) {
val script = Script.pay2wpkh(this)
val hash = Crypto.hash160(Script.write(script))
return when (chainHash) {
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
Block.Testnet4GenesisBlock.hash, Block.Testnet3GenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
Block.LivenetGenesisBlock.hash -> Base58Check.encode(Base58.Prefix.ScriptAddress, hash)
else -> error("invalid chain hash $chainHash")
}
Expand Down
2 changes: 1 addition & 1 deletion src/commonTest/kotlin/fr/acinq/bitcoin/BIP49TestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ class BIP49TestsCommon {
key.publicKey,
PublicKey.fromHex("03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f")
)
assertEquals(computeBIP49Address(key.publicKey, Block.TestnetGenesisBlock.hash), "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2")
assertEquals(computeBIP49Address(key.publicKey, Block.Testnet3GenesisBlock.hash), "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2")
}
}
4 changes: 2 additions & 2 deletions src/commonTest/kotlin/fr/acinq/bitcoin/BIP86TestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class BIP86TestsCommon {
val internalKey = XonlyPublicKey(key.publicKey)
val outputKey = internalKey.outputKey(Crypto.TaprootTweak.NoScriptTweak).first
assertEquals("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c", Bech32.encodeWitnessAddress("tb", 1, outputKey.value.toByteArray()))
assertEquals("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c", internalKey.p2trAddress(Block.TestnetGenesisBlock.hash))
assertEquals("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c", internalKey.p2trAddress(Block.Testnet3GenesisBlock.hash))
}

@Test
Expand All @@ -78,7 +78,7 @@ class BIP86TestsCommon {
val (_, master) = DeterministicWallet.ExtendedPrivateKey.decode("tprv8ZgxMBicQKsPdyyuveRPhVYogdPXBDqRiUXDo5TcLKe3f9YfonipqbgJD7pCXdovZTfTyj6SjZ928SkPunnDTiXV7Y2HSsG9XAGki6n8dRF")
for (i in 0 until 10) {
val key = DeterministicWallet.derivePrivateKey(master, "86'/1'/0'/0/$i")
assertEquals(expected[i], key.publicKey.p2trAddress(Block.TestnetGenesisBlock.hash))
assertEquals(expected[i], key.publicKey.p2trAddress(Block.Testnet3GenesisBlock.hash))
}
}
}
Loading
Loading