core: Implement BIP86 (#5768)

* core: Implement BIP86

* Fix HDUtil.getXprivVersion()

* Fix WalletUnitTest
This commit is contained in:
Chris Stewart 2024-11-14 11:16:25 -06:00 committed by GitHub
parent cccaa582bd
commit fc4802d4b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 156 additions and 11 deletions

View file

@ -1,13 +1,16 @@
package org.bitcoins.core.hd
import org.bitcoins.core.config.MainNet
import org.bitcoins.core.crypto.{ExtKeyVersion, _}
import org.bitcoins.core.protocol.Bech32Address
import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0
import org.bitcoins.crypto.{ECPrivateKey, ECPublicKey}
import org.bitcoins.core.crypto.{ExtKeyVersion, *}
import org.bitcoins.core.protocol.{Bech32Address, Bech32mAddress}
import org.bitcoins.core.protocol.script.{
P2WPKHWitnessSPKV0,
TaprootScriptPubKey
}
import org.bitcoins.crypto.{ECPrivateKey, ECPublicKey, XOnlyPubKey}
import org.bitcoins.testkitcore.gen.{HDGenerators, NumberGenerator}
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
import scodec.bits._
import scodec.bits.*
import scala.util.{Failure, Success}
@ -741,4 +744,120 @@ class HDPathTest extends BitcoinSUnitTest {
}
}
it must "pass examples from BIP86" in {
val words = Vector(
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"abandon",
"about"
)
val mnemonic = MnemonicCode.fromWords(words)
val seed = BIP39Seed.fromMnemonic(mnemonic)
val rootXpriv =
ExtPrivateKey.fromBIP39Seed(ExtKeyVersion.LegacyMainNetPriv, seed)
{
val taprootPathString = "m/86'/0'/0'/0/0"
val taprootPath = TaprootHDPath.fromString(taprootPathString)
val taprootPathAccount = taprootPath.account
val accountXpriv = rootXpriv.deriveChildPrivKey(taprootPathAccount)
val accountXpub = accountXpriv.extPublicKey
val expectedAccountXpriv = ExtPrivateKey.fromString(
"xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk"
)
val expectedAccountXpub = ExtPublicKey.fromString(
"xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ"
)
assert(expectedAccountXpriv == accountXpriv)
assert(expectedAccountXpub == accountXpub)
val first = rootXpriv.deriveChildPrivKey(taprootPath)
assert(first == ExtPrivateKey.fromString(
"xprvA449goEeU9okwCzzZaxiy475EQGQzBkc65su82nXEvcwzfSskb2hAt2WymrjyRL6kpbVTGL3cKtp9herYXSjjQ1j4stsXXiRF7kXkCacK3T"))
val firstXPub = first.extPublicKey
assert(firstXPub == ExtPublicKey.fromString(
"xpub6H3W6JmYJXN49h5TfcVjLC3onS6uPeUTTJoVvRC8oG9vsTn2J8LwigLzq5tHbrwAzH9DGo6ThGUdWsqce8dGfwHVBxSbixjDADGGdzF7t2B"))
val firstInternalKey = first.publicKey
assert(
firstInternalKey.toXOnly == XOnlyPubKey.fromHex(
"cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"))
val firstOutputKey =
TaprootScriptPubKey.fromInternalKey(firstInternalKey.toXOnly)
assert(
firstOutputKey.pubKey == XOnlyPubKey.fromHex(
"a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
val firstSPK = TaprootScriptPubKey(firstOutputKey.pubKey)
assert(firstSPK == TaprootScriptPubKey.fromAsmHex(
"5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
val firstAddress = Bech32mAddress(firstSPK, MainNet)
assert(
firstAddress == Bech32mAddress.fromString(
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"))
}
{
val secondReceivingPath = TaprootHDPath.fromString("m/86'/0'/0'/0/1")
val second = rootXpriv.deriveChildPrivKey(secondReceivingPath)
assert(second == ExtPrivateKey.fromString(
"xprvA449goEeU9okyiF1LmKiDaTgeXvmh87DVyRd35VPbsSop8n8uALpbtrUhUXByPFKK7C2yuqrB1FrhiDkEMC4RGmA5KTwsE1aB5jRu9zHsuQ"))
val secondXPub = second.extPublicKey
assert(secondXPub == ExtPublicKey.fromString(
"xpub6H3W6JmYJXN4CCKUSnriaiQRCZmG6aq4sCMDqTu1ACyngw7HShf59hAxYjXgKDuuHThVEUzdHrc3aXCr9kfvQvZPit5dnD3K9xVRBzjK3rX"))
val secondInternalKey = second.publicKey
assert(
secondInternalKey.toXOnly == XOnlyPubKey.fromHex(
"83dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"))
val secondOutputKey =
TaprootScriptPubKey.fromInternalKey(secondInternalKey.toXOnly)
assert(
secondOutputKey.pubKey == XOnlyPubKey.fromHex(
"a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
val secondSPK = TaprootScriptPubKey(secondOutputKey.pubKey)
assert(secondSPK == TaprootScriptPubKey.fromAsmHex(
"5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
val secondAddress = Bech32mAddress(secondSPK, MainNet)
assert(
secondAddress == Bech32mAddress.fromString(
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"))
}
{
val changePath = TaprootHDPath.fromString("m/86'/0'/0'/1/0")
val changeRootKey = rootXpriv.deriveChildPrivKey(changePath)
assert(changeRootKey == ExtPrivateKey.fromString(
"xprvA3Ln3Gt3aphvUgzgEDT8vE2cYqb4PjFfpmbiFKphxLg1FjXQpkAk5M1ZKDY15bmCAHA35jTiawbFuwGtbDZogKF1WfjwxML4gK7WfYW5JRP"))
val changeXPub = changeRootKey.extPublicKey
assert(changeXPub == ExtPublicKey.fromString(
"xpub6GL8SnQwRCGDhB59LEz9HMyM6sRYoByXBzXK3iEKWgCz8XrZNHUzd9L3AUBELW5NzA7dEFvMas1F84TuPH3xqdUA5tumaGWFgihJzWytXe3"))
val changeInternalKey = changeRootKey.publicKey
assert(
changeInternalKey.toXOnly == XOnlyPubKey.fromHex(
"399f1b2f4393f29a18c937859c5dd8a77350103157eb880f02e8c08214277cef"))
val changeOutputKey =
TaprootScriptPubKey.fromInternalKey(changeInternalKey.toXOnly)
assert(
changeOutputKey.pubKey == XOnlyPubKey.fromHex(
"882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc "))
val changeSPK = TaprootScriptPubKey(changeOutputKey.pubKey)
assert(changeSPK == TaprootScriptPubKey.fromAsmHex(
"5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"))
val changeAddress = Bech32mAddress(changeSPK, MainNet)
assert(
changeAddress == Bech32mAddress.fromString(
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"))
}
}
}

View file

@ -74,6 +74,8 @@ object HDPath extends StringFactory[HDPath] {
SegWitHDPath.fromStringT(string)
} else if (purpose == NestedSegWitHDPath.PURPOSE) {
NestedSegWitHDPath.fromStringT(string)
} else if (purpose == TaprootHDPath.PURPOSE) {
TaprootHDPath.fromStringT(string)
} else {
Failure(new IllegalArgumentException(s"Unknown purpose=$purpose"))
}

View file

@ -50,6 +50,7 @@ private[hd] trait HDPathFactory[PathType <: BIP32Path]
HDPurpose.NestedSegWit
case BIP32Node(HDPurpose.Multisig.constant, Some(_)) =>
HDPurpose.Multisig
case BIP32Node(HDPurpose.Taproot.constant, Some(_)) => HDPurpose.Taproot
case BIP32Node(unknown, Some(_)) =>
throw new IllegalArgumentException(
s"Purpose constant ($unknown) is not a known purpose constant")

View file

@ -32,13 +32,15 @@ object HDPurpose extends StringFactory[HDPurpose] {
final val Multisig = HDPurpose(MultisigHDPath.PURPOSE)
final val SegWit = HDPurpose(SegWitHDPath.PURPOSE)
final val NestedSegWit = HDPurpose(NestedSegWitHDPath.PURPOSE)
final val Taproot = HDPurpose(TaprootHDPath.PURPOSE)
final val default: HDPurpose = SegWit
lazy val singleSigPurposes = Vector(Legacy, SegWit, NestedSegWit)
lazy val singleSigPurposes: Vector[HDPurpose] =
Vector(Legacy, SegWit, NestedSegWit, Taproot)
lazy val all: Vector[HDPurpose] =
Vector(Legacy, Multisig, SegWit, NestedSegWit)
Vector(Legacy, Multisig, SegWit, NestedSegWit, Taproot)
/** Tries to turn the provided integer into a HD purpose path segment */
def fromConstant(i: Int): Option[HDPurpose] = all.find(_.constant == i)

View file

@ -0,0 +1,20 @@
package org.bitcoins.core.hd
sealed abstract class TaprootHDPath extends HDPath {
override protected type NextPath = TaprootHDPath
}
object TaprootHDPath extends HDPathFactory[TaprootHDPath] {
override val PURPOSE: Int = 86
private case class TaprootHDPathImpl(address: HDAddress) extends TaprootHDPath
override def apply(
coin: HDCoinType,
accountIndex: Int,
chainType: HDChainType,
addressIndex: Int): TaprootHDPath = {
val address = assembleAddress(coin, accountIndex, chainType, addressIndex)
TaprootHDPathImpl(address)
}
}

View file

@ -14,9 +14,10 @@ object HDUtil {
import org.bitcoins.core.hd.HDPurpose._
(hdPurpose, network) match {
case (SegWit, MainNet) => SegWitMainNetPriv
case (SegWit, TestNet3 | RegTest | SigNet) => SegWitTestNet3Priv
case (NestedSegWit, MainNet) => NestedSegWitMainNetPriv
case (SegWit, MainNet) | (Taproot, MainNet) => SegWitMainNetPriv
case (SegWit, TestNet3 | RegTest | SigNet) => SegWitTestNet3Priv
case (Taproot, TestNet3 | RegTest | SigNet) => SegWitTestNet3Priv
case (NestedSegWit, MainNet) => NestedSegWitMainNetPriv
case (NestedSegWit, TestNet3 | RegTest | SigNet) =>
NestedSegWitTestNet3Priv
case (Multisig, MainNet) => LegacyMainNetPriv

View file

@ -39,7 +39,7 @@ class WalletUnitTest extends BitcoinSWalletTest {
accounts <- wallet.accountHandling.listAccounts()
addresses <- wallet.addressHandling.listAddresses()
} yield {
assert(accounts.length == 3) // legacy, segwit and nested segwit
assert(accounts.length == 4) // legacy, segwit, nested segwit, taproot
assert(addresses.isEmpty)
}
}