2024 04 16 descriptor fidelity (#5529)

* Implement unit tests for key expression fidelity to user input for hardened paths

* Create HardenedType, rework BIP32Node to take Option[HardenedType] as a parameter

* Fix docs
This commit is contained in:
Chris Stewart 2024-04-17 18:35:33 -05:00 committed by GitHub
parent d39d89bfed
commit a6d93622f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 184 additions and 117 deletions

View File

@ -1,7 +1,7 @@
package org.bitcoins.core.crypto.bip32 package org.bitcoins.core.crypto.bip32
import org.bitcoins.core.crypto.{ExtKey, ExtPublicKey} import org.bitcoins.core.crypto.{ExtKey, ExtPublicKey}
import org.bitcoins.core.hd.{BIP32Node, BIP32Path} import org.bitcoins.core.hd.{BIP32Node, BIP32Path, HardenedType}
import org.bitcoins.testkitcore.gen.{ import org.bitcoins.testkitcore.gen.{
CryptoGenerators, CryptoGenerators,
HDGenerators, HDGenerators,
@ -21,8 +21,8 @@ class BIP32PathTest extends BitcoinSUnitTest {
behavior of "BIP32Child" behavior of "BIP32Child"
it must "fail to make children of out negative integers" in { it must "fail to make children of out negative integers" in {
forAll(NumberGenerator.negativeInts, Gen.oneOf(true, false)) { (i, bool) => forAll(NumberGenerator.negativeInts, HDGenerators.hardenedType) { (i, h) =>
assertThrows[IllegalArgumentException](BIP32Node(i, bool)) assertThrows[IllegalArgumentException](BIP32Node(i, Some(h)))
} }
} }
@ -104,59 +104,65 @@ class BIP32PathTest extends BitcoinSUnitTest {
it must "parse the paths from the BIP32 test vectors" in { it must "parse the paths from the BIP32 test vectors" in {
val expected1 = BIP32Path( val expected1 = BIP32Path(
Vector(BIP32Node(0, hardened = true), BIP32Node(1, hardened = false))) Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardenedOpt = None)))
assert(BIP32Path.fromString("m/0'/1") == expected1) assert(BIP32Path.fromString("m/0'/1") == expected1)
val expected2 = BIP32Path( val expected2 = BIP32Path(
Vector(BIP32Node(0, hardened = true), Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardened = false), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = true))) BIP32Node(2, hardenedOpt = HardenedType.defaultOpt)))
assert(BIP32Path.fromString("m/0'/1/2'") == expected2) assert(BIP32Path.fromString("m/0'/1/2'") == expected2)
val expected3 = BIP32Path( val expected3 = BIP32Path(
Vector(BIP32Node(0, hardened = true), Vector(
BIP32Node(1, hardened = false), BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardened = true), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = false))) BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardenedOpt = None)
))
assert(BIP32Path.fromString("m/0'/1/2'/2") == expected3) assert(BIP32Path.fromString("m/0'/1/2'/2") == expected3)
val expected4 = BIP32Path( val expected4 = BIP32Path(
Vector( Vector(
BIP32Node(0, hardened = true), BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardened = false), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = true), BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardened = false), BIP32Node(2, hardenedOpt = None),
BIP32Node(1000000000, hardened = false) BIP32Node(1000000000, hardenedOpt = None)
)) ))
assert(BIP32Path.fromString("m/0'/1/2'/2/1000000000") == expected4) assert(BIP32Path.fromString("m/0'/1/2'/2/1000000000") == expected4)
} }
it must "parse the paths from the BIP32 test vector from bytes" in { it must "parse the paths from the BIP32 test vector from bytes" in {
val expected1 = BIP32Path( val expected1 = BIP32Path(
Vector(BIP32Node(0, hardened = true), BIP32Node(1, hardened = false))) Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardenedOpt = None)))
assert(BIP32Path.fromBytes(hex"0x8000000000000001") == expected1) assert(BIP32Path.fromBytes(hex"0x8000000000000001") == expected1)
val expected2 = BIP32Path( val expected2 = BIP32Path(
Vector(BIP32Node(0, hardened = true), Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardened = false), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = true))) BIP32Node(2, hardenedOpt = HardenedType.defaultOpt)))
assert(BIP32Path.fromBytes(hex"0x800000000000000180000002") == expected2) assert(BIP32Path.fromBytes(hex"0x800000000000000180000002") == expected2)
val expected3 = BIP32Path( val expected3 = BIP32Path(
Vector(BIP32Node(0, hardened = true), Vector(
BIP32Node(1, hardened = false), BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardened = true), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = false))) BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardenedOpt = None)
))
assert( assert(
BIP32Path.fromBytes(hex"0x80000000000000018000000200000002") == expected3) BIP32Path.fromBytes(hex"0x80000000000000018000000200000002") == expected3)
val expected4 = BIP32Path( val expected4 = BIP32Path(
Vector( Vector(
BIP32Node(0, hardened = true), BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(1, hardened = false), BIP32Node(1, hardenedOpt = None),
BIP32Node(2, hardened = true), BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
BIP32Node(2, hardened = false), BIP32Node(2, hardenedOpt = None),
BIP32Node(1000000000, hardened = false) BIP32Node(1000000000, hardenedOpt = None)
)) ))
assert(BIP32Path assert(BIP32Path
.fromBytes(hex"0x800000000000000180000002000000023B9ACA00") == expected4) .fromBytes(hex"0x800000000000000180000002000000023B9ACA00") == expected4)
@ -169,13 +175,6 @@ class BIP32PathTest extends BitcoinSUnitTest {
} }
} }
it must "have fromBytes and bytes symmetry" in {
forAll(HDGenerators.bip32Path) { path =>
val bytes = path.bytes
assert(path == BIP32Path.fromBytes(bytes))
}
}
it must "do path diffing" in { it must "do path diffing" in {
{ {
val first = BIP32Path.fromString("m/44'/1'") val first = BIP32Path.fromString("m/44'/1'")

View File

@ -40,7 +40,7 @@ class HDAccountTest extends BitcoinSUnitTest {
} }
it must "succeed if we add an arbitrary element onto the end of the path" in { it must "succeed if we add an arbitrary element onto the end of the path" in {
val extraNode = defaultPath.:+(BIP32Node(0, true)) val extraNode = defaultPath.:+(BIP32Node(0, HardenedType.defaultOpt))
val isSame = HDAccount.isSameAccount(extraNode, defaultAcct) val isSame = HDAccount.isSameAccount(extraNode, defaultAcct)

View File

@ -129,7 +129,7 @@ class HDPathTest extends BitcoinSUnitTest {
forAll(HDGenerators.hdPathWithConstructor) { case (hd, hdApply) => forAll(HDGenerators.hdPathWithConstructor) { case (hd, hdApply) =>
val nonHardenedCoinChildren = hd.path.zipWithIndex.map { val nonHardenedCoinChildren = hd.path.zipWithIndex.map {
case (child, index) => case (child, index) =>
if (index == LegacyHDPath.COIN_INDEX) child.copy(hardened = false) if (index == LegacyHDPath.COIN_INDEX) child.copy(hardenedOpt = None)
else child else child
} }
@ -144,7 +144,7 @@ class HDPathTest extends BitcoinSUnitTest {
val nonHardenedAccountChildren = hd.path.zipWithIndex.map { val nonHardenedAccountChildren = hd.path.zipWithIndex.map {
case (child, index) => case (child, index) =>
if (index == LegacyHDPath.ACCOUNT_INDEX) if (index == LegacyHDPath.ACCOUNT_INDEX)
child.copy(hardened = false) child.copy(hardenedOpt = None)
else child else child
} }
val badAccountAttempt = hdApply(nonHardenedAccountChildren) val badAccountAttempt = hdApply(nonHardenedAccountChildren)
@ -157,7 +157,8 @@ class HDPathTest extends BitcoinSUnitTest {
val hardenedChainChildren = hd.path.zipWithIndex.map { val hardenedChainChildren = hd.path.zipWithIndex.map {
case (child, index) => case (child, index) =>
if (index == LegacyHDPath.CHAIN_INDEX) child.copy(hardened = true) if (index == LegacyHDPath.CHAIN_INDEX)
child.copy(hardenedOpt = HardenedType.defaultOpt)
else child else child
} }
val badChainAttempt = val badChainAttempt =
@ -171,7 +172,8 @@ class HDPathTest extends BitcoinSUnitTest {
val hardenedAddressChildren = hd.path.zipWithIndex.map { val hardenedAddressChildren = hd.path.zipWithIndex.map {
case (child, index) => case (child, index) =>
if (index == LegacyHDPath.ADDRESS_INDEX) child.copy(hardened = true) if (index == LegacyHDPath.ADDRESS_INDEX)
child.copy(hardenedOpt = HardenedType.defaultOpt)
else child else child
} }
val badAddrAttempt = val badAddrAttempt =

View File

@ -7,32 +7,43 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
behavior of "DescriptorChecksumTest" behavior of "DescriptorChecksumTest"
val expression = val descriptor =
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef")) RawDescriptor(
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef")),
None)
it must "calculate correct checksums from BIP380 examples" in { it must "calculate correct checksums from BIP380 examples" in {
val str0 = "raw(deadbeef)#89f8spxm" val str0 = "raw(deadbeef)#89f8spxm"
val split0 = str0.split("#") val split0 = str0.split("#")
val (payload, checksum) = (split0(0), split0(1)) val (payload, checksum) = (split0(0), split0(1))
assert(Descriptor.createChecksum(payload) == checksum) assert(Descriptor.createChecksum(payload) == checksum)
assert(Descriptor.isValidChecksum(expression, Some(checksum))) assert(Descriptor.isValidChecksum(descriptor, Some(checksum)))
//expression with nochecksum should be valid //expression with nochecksum should be valid
assert(Descriptor.isValidChecksum(expression, None)) assert(Descriptor.isValidChecksum(descriptor, None))
// val descriptor1 =
// Descriptor.fromString(
// "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)")
// val checksum1 = "cjjspncu"
// assert(Descriptor.createChecksum(descriptor1) == checksum1)
// assert(Descriptor.isValidChecksum(descriptor1, Some(checksum1)))
} }
it must "fail when a bad checksum is given" in { it must "fail when a bad checksum is given" in {
//Missing checksum //Missing checksum
assert(!Descriptor.isValidChecksum(expression, Some("#"))) assert(!Descriptor.isValidChecksum(descriptor, Some("#")))
//Too long checksum (9 chars) //Too long checksum (9 chars)
assert(!Descriptor.isValidChecksum(expression, Some("89f8spxmx"))) assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spxmx")))
//Too short checksum (7 chars) //Too short checksum (7 chars)
assert(!Descriptor.isValidChecksum(expression, Some("89f8spx"))) assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spx")))
//Error in payload //Error in payload
val bad = val bad =
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deedbeef")) RawDescriptor(
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deedbeef")),
None)
assert(!Descriptor.isValidChecksum(bad, Some("89f8spxm"))) assert(!Descriptor.isValidChecksum(bad, Some("89f8spxm")))
//Error in checksum //Error in checksum
assert(!Descriptor.isValidChecksum(expression, Some("#9f8spxm"))) assert(!Descriptor.isValidChecksum(descriptor, Some("#9f8spxm")))
} }
} }

View File

@ -452,6 +452,14 @@ class DescriptorTest extends BitcoinSUnitTest {
runFailTest(str4) runFailTest(str4)
} }
it must "have fidelity with the type of hardened derivation used as input" in {
//note using h instead of ' for hardened derivation path
val str =
"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)"
val desc = Descriptor.fromString(str)
assert(desc.toString == str)
}
def runTest(descriptor: String, expectedSPK: String): Assertion = { def runTest(descriptor: String, expectedSPK: String): Assertion = {
val desc = ScriptDescriptor.fromString(descriptor) val desc = ScriptDescriptor.fromString(descriptor)
assert(desc.toString == descriptor) assert(desc.toString == descriptor)

View File

@ -13,8 +13,8 @@ class KeyExpressionTest extends BitcoinSUnitTest {
val str2 = "[deadbeef/0'/0h/0']" val str2 = "[deadbeef/0'/0h/0']"
val keyOrigin = KeyOriginExpression.fromString(str0) val keyOrigin = KeyOriginExpression.fromString(str0)
assert(str0 == keyOrigin.toString) assert(str0 == keyOrigin.toString)
assert(keyOrigin == KeyOriginExpression.fromString(str1)) assert(KeyOriginExpression.fromString(str1).toString == str1)
keyOrigin == KeyOriginExpression.fromString(str2) assert(KeyOriginExpression.fromString(str2).toString == str2)
} }
it must "parse valid private key expressions from BIP380" in { it must "parse valid private key expressions from BIP380" in {

View File

@ -80,13 +80,14 @@ abstract class BIP32Path extends SeqWrapper[BIP32Node] {
} }
} }
override def toString: String = override def toString: String = {
path path
.map { case BIP32Node(index, hardened) => .map { case BIP32Node(index, hardenedOpt) =>
val isHardened = if (hardened) "'" else "" val isHardened = hardenedOpt.map(_.toString).getOrElse("")
index.toString + isHardened index.toString + isHardened
} }
.fold("m")((accum, curr) => accum + "/" + curr) .fold("m")((accum, curr) => accum + "/" + curr)
}
def bytes: ByteVector = path.foldLeft(ByteVector.empty)(_ ++ _.toUInt32.bytes) def bytes: ByteVector = path.foldLeft(ByteVector.empty)(_ ++ _.toUInt32.bytes)
@ -139,13 +140,13 @@ object BIP32Path extends Factory[BIP32Path] with StringFactory[BIP32Path] {
"""The first element in a BIP32 path string must be "m"""") """The first element in a BIP32 path string must be "m"""")
val path = rest.map { str => val path = rest.map { str =>
val (index: String, hardened: Boolean) = val (index: String, hardenedOpt: Option[HardenedType]) = {
if (str.endsWith("'") || str.endsWith("h")) { HardenedType.fromStringOpt(str.last.toString) match {
(str.dropRight(1), true) case Some(h) => (str.dropRight(1), Some(h))
} else { case None => (str, None)
(str, false)
} }
BIP32Node(index.toInt, hardened) }
BIP32Node(index.toInt, hardenedOpt)
} }
BIP32PathImpl(path) BIP32PathImpl(path)
@ -173,7 +174,7 @@ object BIP32Path extends Factory[BIP32Path] with StringFactory[BIP32Path] {
if (littleEndian) UInt32.fromBytesLE(part) else UInt32.fromBytes(part) if (littleEndian) UInt32.fromBytesLE(part) else UInt32.fromBytes(part)
val hardened = uInt32 >= ExtKey.hardenedIdx val hardened = uInt32 >= ExtKey.hardenedIdx
val index = if (hardened) uInt32 - ExtKey.hardenedIdx else uInt32 val index = if (hardened) uInt32 - ExtKey.hardenedIdx else uInt32
BIP32Node(index.toInt, hardened) BIP32Node(index.toInt, if (hardened) Some(HardenedType.default) else None)
} }
BIP32Path(path) BIP32Path(path)
@ -187,13 +188,43 @@ object BIP32Path extends Factory[BIP32Path] with StringFactory[BIP32Path] {
} }
case class BIP32Node(index: Int, hardened: Boolean) { case class BIP32Node(index: Int, hardenedOpt: Option[HardenedType]) {
require(index >= 0, s"BIP32 node index must be positive! Got $index") require(index >= 0, s"BIP32 node index must be positive! Got $index")
def hardened: Boolean = hardenedOpt.isDefined
/** Converts this node to a BIP32 notation /** Converts this node to a BIP32 notation
* unsigned 32 bit integer * unsigned 32 bit integer
*/ */
def toUInt32: UInt32 = def toUInt32: UInt32 =
if (hardened) ExtKey.hardenedIdx + UInt32(index.toLong) if (hardenedOpt.isDefined) ExtKey.hardenedIdx + UInt32(index.toLong)
else UInt32(index) else UInt32(index)
} }
sealed abstract class HardenedType
object HardenedType extends StringFactory[HardenedType] {
case object Tick extends HardenedType {
override def toString: String = {
"'"
}
}
case object h extends HardenedType {
override def toString(): String = "h"
}
val all: Set[HardenedType] = Set(Tick, h)
override def fromString(string: String): HardenedType = {
all
.find(_.toString == string)
.getOrElse(sys.error(s"Cannot find HardenedType for string=$string"))
}
val default: HardenedType = Tick
val defaultOpt: Option[HardenedType] = Some(default)
}

View File

@ -16,7 +16,7 @@ case class HDAccount(
require(index >= 0, s"Account index ($index) must be positive!") require(index >= 0, s"Account index ($index) must be positive!")
override val path: Vector[BIP32Node] = { override val path: Vector[BIP32Node] = {
coin.path :+ BIP32Node(index, hardened = true) coin.path :+ BIP32Node(index, hardenedOpt = HardenedType.defaultOpt)
} }
def purpose: HDPurpose = coin.purpose def purpose: HDPurpose = coin.purpose

View File

@ -8,7 +8,7 @@ sealed abstract class HDAddress extends BIP32Path {
require(index >= 0, s"Address index ($index) must be positive!") require(index >= 0, s"Address index ($index) must be positive!")
override val path: Vector[BIP32Node] = { override val path: Vector[BIP32Node] = {
chain.path :+ BIP32Node(index, hardened = false) chain.path :+ BIP32Node(index, hardenedOpt = None)
} }
def purpose: HDPurpose def purpose: HDPurpose

View File

@ -7,7 +7,7 @@ package org.bitcoins.core.hd
sealed abstract class HDChain extends BIP32Path { sealed abstract class HDChain extends BIP32Path {
override val path: Vector[BIP32Node] = { override val path: Vector[BIP32Node] = {
account.path :+ BIP32Node(toInt, hardened = false) account.path :+ BIP32Node(toInt, hardenedOpt = None)
} }
def purpose: HDPurpose def purpose: HDPurpose

View File

@ -7,7 +7,7 @@ package org.bitcoins.core.hd
case class HDCoin(purpose: HDPurpose, coinType: HDCoinType) extends BIP32Path { case class HDCoin(purpose: HDPurpose, coinType: HDCoinType) extends BIP32Path {
override def path: Vector[BIP32Node] = override def path: Vector[BIP32Node] =
purpose.path :+ BIP32Node(coinType.toInt, hardened = true) purpose.path :+ BIP32Node(coinType.toInt, HardenedType.defaultOpt)
def toAccount(index: Int): HDAccount = HDAccount(this, index) def toAccount(index: Int): HDAccount = HDAccount(this, index)
} }

View File

@ -41,15 +41,16 @@ private[hd] trait HDPathFactory[PathType <: BIP32Path]
val maybePurpose = children.head val maybePurpose = children.head
val purpose: HDPurpose = maybePurpose match { val purpose: HDPurpose = maybePurpose match {
case BIP32Node(_, false) => case BIP32Node(_, None) =>
throw new IllegalArgumentException( throw new IllegalArgumentException(
"The first child in a HD path must be hardened") "The first child in a HD path must be hardened")
case BIP32Node(HDPurposes.Legacy.constant, true) => HDPurposes.Legacy case BIP32Node(HDPurposes.Legacy.constant, Some(_)) => HDPurposes.Legacy
case BIP32Node(HDPurposes.SegWit.constant, true) => HDPurposes.SegWit case BIP32Node(HDPurposes.SegWit.constant, Some(_)) => HDPurposes.SegWit
case BIP32Node(HDPurposes.NestedSegWit.constant, true) => case BIP32Node(HDPurposes.NestedSegWit.constant, Some(_)) =>
HDPurposes.NestedSegWit HDPurposes.NestedSegWit
case BIP32Node(HDPurposes.Multisig.constant, true) => HDPurposes.Multisig case BIP32Node(HDPurposes.Multisig.constant, Some(_)) =>
case BIP32Node(unknown, true) => HDPurposes.Multisig
case BIP32Node(unknown, Some(_)) =>
throw new IllegalArgumentException( throw new IllegalArgumentException(
s"Purpose constant ($unknown) is not a known purpose constant") s"Purpose constant ($unknown) is not a known purpose constant")
} }
@ -103,7 +104,7 @@ private[hd] trait HDPathFactory[PathType <: BIP32Path]
protected lazy val hdPurpose: HDPurpose = protected lazy val hdPurpose: HDPurpose =
HDPurposes.fromConstant(PURPOSE).get // todo HDPurposes.fromConstant(PURPOSE).get // todo
lazy val purposeChild: BIP32Node = BIP32Node(PURPOSE, hardened = true) lazy val purposeChild: BIP32Node = BIP32Node(PURPOSE, HardenedType.defaultOpt)
/** The index of the coin segement of a BIP44 path /** The index of the coin segement of a BIP44 path
*/ */

View File

@ -18,7 +18,7 @@ package org.bitcoins.core.hd
case class HDPurpose(constant: Int) extends BIP32Path { case class HDPurpose(constant: Int) extends BIP32Path {
override val path: Vector[BIP32Node] = Vector( override val path: Vector[BIP32Node] = Vector(
BIP32Node(constant, hardened = true)) BIP32Node(constant, HardenedType.defaultOpt))
} }
object HDPurposes { object HDPurposes {

View File

@ -141,7 +141,9 @@ sealed abstract class DescriptorFactory[
//now check for a valid checksum //now check for a valid checksum
val checksumOpt = val checksumOpt =
if (checksum.nonEmpty) Some(checksum.tail) else None //drop '#' if (checksum.nonEmpty) Some(checksum.tail) else None //drop '#'
val isValidChecksum = Descriptor.isValidChecksum(expression, checksumOpt) val isValidChecksum = Descriptor.isValidChecksum(
descriptor = createDescriptor(expression, None),
checksumOpt = checksumOpt)
if (isValidChecksum) { if (isValidChecksum) {
createDescriptor(expression, checksumOpt) createDescriptor(expression, checksumOpt)
} else { } else {
@ -468,13 +470,17 @@ object Descriptor extends StringFactory[Descriptor] {
builder.result() builder.result()
} }
def createChecksum(descriptor: Descriptor): String = {
createChecksum(descriptor.toString)
}
def isValidChecksum( def isValidChecksum(
expression: DescriptorExpression, descriptor: Descriptor,
checksumOpt: Option[String]): Boolean = { checksumOpt: Option[String]): Boolean = {
checksumOpt match { checksumOpt match {
case None => true //trivially true if we have no checksum case None => true //trivially true if we have no checksum
case Some(checksum) => case Some(checksum) =>
val t = Try(createChecksum(expression.toString)) val t = Try(createChecksum(descriptor.toString))
if (t.isFailure) false if (t.isFailure) false
else t.get == checksum else t.get == checksum
} }

View File

@ -7,7 +7,7 @@ import org.bitcoins.core.crypto.{
ExtPrivateKey, ExtPrivateKey,
ExtPublicKey ExtPublicKey
} }
import org.bitcoins.core.hd.{BIP32Node, BIP32Path} import org.bitcoins.core.hd.{BIP32Node, BIP32Path, HardenedType}
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
import org.bitcoins.core.script.ScriptType import org.bitcoins.core.script.ScriptType
import org.bitcoins.crypto._ import org.bitcoins.crypto._
@ -175,20 +175,25 @@ sealed abstract class ExtECPublicKeyExpression
def pathOpt: Option[BIP32Path] def pathOpt: Option[BIP32Path]
def childrenHardenedOpt: Option[Boolean] /** Outer Option represents if we use this key or derive children
* Inner option represents whether child keys are hardened or not
* if they are hardedned, return the specifi [[HardenedType]]
*/
def childrenHardenedOpt: Option[Option[HardenedType]]
def deriveChild(idx: Int): BaseECKey def deriveChild(idx: Int): BaseECKey
override def toString(): String = { override def toString(): String = {
val hardenedStr: String = childrenHardenedOpt match {
case Some(Some(h)) => s"/*${h.toString}"
case Some(None) => "/*"
case None => ""
}
originOpt.map(_.toString).getOrElse("") + originOpt.map(_.toString).getOrElse("") +
ExtKey.toString(extKey) + ExtKey.toString(extKey) +
pathOpt.map(_.toString.drop(1)).getOrElse("") + pathOpt.map(_.toString.drop(1)).getOrElse("") +
childrenHardenedOpt hardenedStr
.map {
case true => "/*'"
case false => "/*"
}
.getOrElse("")
} }
} }
@ -204,7 +209,7 @@ sealed abstract class ExtXOnlyPublicKeyExpression
def pathOpt: Option[BIP32Path] = ecPublicKeyExpression.pathOpt def pathOpt: Option[BIP32Path] = ecPublicKeyExpression.pathOpt
def childrenHardenedOpt: Option[Boolean] = def childrenHardenedOpt: Option[Option[HardenedType]] =
ecPublicKeyExpression.childrenHardenedOpt ecPublicKeyExpression.childrenHardenedOpt
def deriveChild(idx: Int): BaseECKey = ecPublicKeyExpression.deriveChild(idx) def deriveChild(idx: Int): BaseECKey = ecPublicKeyExpression.deriveChild(idx)
@ -221,7 +226,7 @@ case class XprvECPublicKeyExpression(
override val extKey: ExtPrivateKey, override val extKey: ExtPrivateKey,
originOpt: Option[KeyOriginExpression], originOpt: Option[KeyOriginExpression],
pathOpt: Option[BIP32Path], pathOpt: Option[BIP32Path],
childrenHardenedOpt: Option[Boolean]) childrenHardenedOpt: Option[Option[HardenedType]])
extends ExtECPublicKeyExpression extends ExtECPublicKeyExpression
with ECPublicKeyExpression { with ECPublicKeyExpression {
@ -242,7 +247,7 @@ case class XprvECPublicKeyExpression(
childrenHardenedOpt.isDefined, childrenHardenedOpt.isDefined,
s"Cannot derive child keys from descriptor that does not allow children, got=${toString}") s"Cannot derive child keys from descriptor that does not allow children, got=${toString}")
val node = val node =
BIP32Node(index = idx, hardened = childrenHardenedOpt.getOrElse(false)) BIP32Node(index = idx, hardenedOpt = childrenHardenedOpt.get)
val fullPath: BIP32Path = pathOpt match { val fullPath: BIP32Path = pathOpt match {
case Some(p) => BIP32Path(p.path.appended(node)) case Some(p) => BIP32Path(p.path.appended(node))
case None => BIP32Path(node) case None => BIP32Path(node)
@ -270,7 +275,7 @@ case class XpubECPublicKeyExpression(
override val extKey: ExtPublicKey, override val extKey: ExtPublicKey,
originOpt: Option[KeyOriginExpression], originOpt: Option[KeyOriginExpression],
pathOpt: Option[BIP32Path], pathOpt: Option[BIP32Path],
childrenHardenedOpt: Option[Boolean]) childrenHardenedOpt: Option[Option[HardenedType]])
extends ExtECPublicKeyExpression extends ExtECPublicKeyExpression
with ECPublicKeyExpression { with ECPublicKeyExpression {
@ -290,7 +295,7 @@ case class XpubECPublicKeyExpression(
require( require(
childrenHardenedOpt.isDefined, childrenHardenedOpt.isDefined,
s"Cannot derive child keys from descriptor that does not allow children, got=${toString}") s"Cannot derive child keys from descriptor that does not allow children, got=${toString}")
val node = BIP32Node(index = idx, hardened = childrenHardenedOpt.get) val node = BIP32Node(index = idx, hardenedOpt = childrenHardenedOpt.get)
val fullPath: BIP32Path = pathOpt match { val fullPath: BIP32Path = pathOpt match {
case Some(p) => BIP32Path(p.path.appended(node)) case Some(p) => BIP32Path(p.path.appended(node))
case None => BIP32Path(node) case None => BIP32Path(node)

View File

@ -1,7 +1,7 @@
package org.bitcoins.core.protocol.script.descriptor package org.bitcoins.core.protocol.script.descriptor
import org.bitcoins.core.crypto.{ECPrivateKeyUtil, ExtKey, ExtPublicKey} import org.bitcoins.core.crypto.{ECPrivateKeyUtil, ExtKey, ExtPublicKey}
import org.bitcoins.core.hd.BIP32Path import org.bitcoins.core.hd.{BIP32Path, HardenedType}
import org.bitcoins.core.protocol.script.{ import org.bitcoins.core.protocol.script.{
MultiSignatureScriptPubKey, MultiSignatureScriptPubKey,
RawScriptPubKey RawScriptPubKey
@ -11,10 +11,6 @@ import org.bitcoins.crypto._
case class DescriptorIterator(descriptor: String) { case class DescriptorIterator(descriptor: String) {
private var index: Int = 0 private var index: Int = 0
private val hardenedChars: Vector[Char] = {
Vector('\'', 'h', 'H')
}
def current: String = { def current: String = {
descriptor.drop(index) descriptor.drop(index)
} }
@ -46,15 +42,20 @@ case class DescriptorIterator(descriptor: String) {
} }
} }
def takeChildrenHardenedOpt(): Option[Boolean] = { def takeChildrenHardenedOpt(): Option[Option[HardenedType]] = {
if (current.nonEmpty && current.charAt(0) == '*') { if (current.nonEmpty && current.charAt(0) == '*') {
skip(1) skip(1)
if (current.nonEmpty && hardenedChars.exists(_ == current.charAt(0))) { val hardenedOpt = HardenedType.fromStringOpt(current.take(1))
// if (current.nonEmpty && hardenedChars.exists(_ == current.charAt(0))) {
// skip(1)
// Some(true)
// } else {
// Some(false)
// }
if (hardenedOpt.isDefined) {
skip(1) skip(1)
Some(true)
} else {
Some(false)
} }
Some(hardenedOpt)
} else { } else {
None None
} }

View File

@ -749,8 +749,8 @@ abstract class DLCWallet
account: AccountDb, account: AccountDb,
keyIndex: Int): AdaptorSign = { keyIndex: Int): AdaptorSign = {
val bip32Path = BIP32Path( val bip32Path = BIP32Path(
account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false), account.hdAccount.path ++ Vector(BIP32Node(0, hardenedOpt = None),
BIP32Node(keyIndex, hardened = false))) BIP32Node(keyIndex, hardenedOpt = None)))
val privKeyPath = HDPath.fromString(bip32Path.toString) val privKeyPath = HDPath.fromString(bip32Path.toString)
keyManager.toSign(privKeyPath) keyManager.toSign(privKeyPath)
} }

View File

@ -572,8 +572,8 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit
val bip32Path = BIP32Path( val bip32Path = BIP32Path(
dlcDb.account.path ++ Vector( dlcDb.account.path ++ Vector(
BIP32Node(dlcDb.changeIndex.index, hardened = false), BIP32Node(dlcDb.changeIndex.index, hardenedOpt = None),
BIP32Node(dlcDb.keyIndex, hardened = false))) BIP32Node(dlcDb.keyIndex, hardenedOpt = None)))
val privKeyPath = HDPath.fromString(bip32Path.toString) val privKeyPath = HDPath.fromString(bip32Path.toString)
val fundingPrivKey = keyManager.toSign(privKeyPath) val fundingPrivKey = keyManager.toSign(privKeyPath)

View File

@ -51,7 +51,7 @@ val extPrivKey = ExtPrivateKey(ExtKeyVersion.SegWitMainNetPriv)
extPrivKey.sign(DoubleSha256Digest.empty.bytes) extPrivKey.sign(DoubleSha256Digest.empty.bytes)
val path = BIP32Path(Vector(BIP32Node(0,false))) val path = BIP32Path(Vector(BIP32Node(0,HardenedType.defaultOpt)))
extPrivKey.sign(DoubleSha256Digest.empty.bytes,path) extPrivKey.sign(DoubleSha256Digest.empty.bytes,path)
``` ```

View File

@ -8,6 +8,8 @@ import scala.util.Try
*/ */
object HDGenerators { object HDGenerators {
def hardenedType: Gen[HardenedType] = Gen.oneOf(HardenedType.all)
/** Generates a BIP 32 path segment /** Generates a BIP 32 path segment
*/ */
def bip32Child: Gen[BIP32Node] = Gen.oneOf(softBip32Child, hardBip32Child) def bip32Child: Gen[BIP32Node] = Gen.oneOf(softBip32Child, hardBip32Child)
@ -17,14 +19,15 @@ object HDGenerators {
def softBip32Child: Gen[BIP32Node] = def softBip32Child: Gen[BIP32Node] =
for { for {
index <- NumberGenerator.positiveInts index <- NumberGenerator.positiveInts
} yield BIP32Node(index, hardened = false) } yield BIP32Node(index, hardenedOpt = None)
/** Generates a hardened BIP 32 path segment /** Generates a hardened BIP 32 path segment
*/ */
def hardBip32Child: Gen[BIP32Node] = def hardBip32Child: Gen[BIP32Node] =
for { for {
soft <- softBip32Child soft <- softBip32Child
} yield soft.copy(hardened = true) hardened <- hardenedType
} yield soft.copy(hardenedOpt = Some(hardened))
/** Generates a BIP32 path /** Generates a BIP32 path
*/ */

View File

@ -37,9 +37,9 @@ object GetAddresses extends App {
accountIndex <- 0 until 3 accountIndex <- 0 until 3
} yield { } yield {
val accountPath = BIP32Path( val accountPath = BIP32Path(
BIP32Node(constant.constant, hardened = true), BIP32Node(constant.constant, HardenedType.defaultOpt),
BIP32Node(coin.toInt, hardened = true), BIP32Node(coin.toInt, HardenedType.defaultOpt),
BIP32Node(accountIndex, hardened = true) BIP32Node(accountIndex, HardenedType.defaultOpt)
) )
val pathType = val pathType =
@ -68,11 +68,11 @@ object GetAddresses extends App {
addressIndex <- 0 until 3 addressIndex <- 0 until 3
} yield { } yield {
val path = BIP32Path( val path = BIP32Path(
BIP32Node(constant.constant, hardened = true), BIP32Node(constant.constant, HardenedType.defaultOpt),
BIP32Node(coin.toInt, hardened = true), BIP32Node(coin.toInt, HardenedType.defaultOpt),
BIP32Node(accountIndex, hardened = true), BIP32Node(accountIndex, HardenedType.defaultOpt),
BIP32Node(chainType.index, hardened = false), BIP32Node(chainType.index, HardenedType.defaultOpt),
BIP32Node(addressIndex, hardened = false) BIP32Node(addressIndex, None)
) )
val addressCmd = s"trezorctl get-address -n $path -t $trezorPathType" val addressCmd = s"trezorctl get-address -n $path -t $trezorPathType"