Skip to content

Commit

Permalink
Add support for testnet4
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Aug 12, 2024
1 parent 9b7acd3 commit 4e88fcc
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 37 deletions.
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
19 changes: 12 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,16 @@ 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)

@Deprecated("use Testnet3 explicitly", replaceWith = ReplaceWith("Testnet3"))
public object Testnet : Chain("Testnet", Block.Testnet3GenesisBlock)
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 isTestnet(): Boolean = this is Testnet3 || this is Testnet4

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

Expand Down
37 changes: 36 additions & 1 deletion src/commonMain/kotlin/fr/acinq/bitcoin/Block.kt
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,45 @@ 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
@Deprecated("testnet is the deprecated testnet3 network, use testnet3 explicitly", replaceWith = ReplaceWith("Testnet3GenesisBlock", "fr.acinq.bitcoin.Block"))
public val TestnetGenesisBlock: Block = Testnet3GenesisBlock

@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

0 comments on commit 4e88fcc

Please sign in to comment.