mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
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:
parent
d39d89bfed
commit
a6d93622f8
@ -1,7 +1,7 @@
|
||||
package org.bitcoins.core.crypto.bip32
|
||||
|
||||
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.{
|
||||
CryptoGenerators,
|
||||
HDGenerators,
|
||||
@ -21,8 +21,8 @@ class BIP32PathTest extends BitcoinSUnitTest {
|
||||
behavior of "BIP32Child"
|
||||
|
||||
it must "fail to make children of out negative integers" in {
|
||||
forAll(NumberGenerator.negativeInts, Gen.oneOf(true, false)) { (i, bool) =>
|
||||
assertThrows[IllegalArgumentException](BIP32Node(i, bool))
|
||||
forAll(NumberGenerator.negativeInts, HDGenerators.hardenedType) { (i, h) =>
|
||||
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 {
|
||||
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)
|
||||
|
||||
val expected2 = BIP32Path(
|
||||
Vector(BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true)))
|
||||
Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt)))
|
||||
assert(BIP32Path.fromString("m/0'/1/2'") == expected2)
|
||||
|
||||
val expected3 = BIP32Path(
|
||||
Vector(BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true),
|
||||
BIP32Node(2, hardened = false)))
|
||||
Vector(
|
||||
BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(2, hardenedOpt = None)
|
||||
))
|
||||
assert(BIP32Path.fromString("m/0'/1/2'/2") == expected3)
|
||||
|
||||
val expected4 = BIP32Path(
|
||||
Vector(
|
||||
BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true),
|
||||
BIP32Node(2, hardened = false),
|
||||
BIP32Node(1000000000, hardened = false)
|
||||
BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(2, hardenedOpt = None),
|
||||
BIP32Node(1000000000, hardenedOpt = None)
|
||||
))
|
||||
assert(BIP32Path.fromString("m/0'/1/2'/2/1000000000") == expected4)
|
||||
}
|
||||
|
||||
it must "parse the paths from the BIP32 test vector from bytes" in {
|
||||
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)
|
||||
|
||||
val expected2 = BIP32Path(
|
||||
Vector(BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true)))
|
||||
Vector(BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt)))
|
||||
assert(BIP32Path.fromBytes(hex"0x800000000000000180000002") == expected2)
|
||||
|
||||
val expected3 = BIP32Path(
|
||||
Vector(BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true),
|
||||
BIP32Node(2, hardened = false)))
|
||||
Vector(
|
||||
BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(2, hardenedOpt = None)
|
||||
))
|
||||
assert(
|
||||
BIP32Path.fromBytes(hex"0x80000000000000018000000200000002") == expected3)
|
||||
|
||||
val expected4 = BIP32Path(
|
||||
Vector(
|
||||
BIP32Node(0, hardened = true),
|
||||
BIP32Node(1, hardened = false),
|
||||
BIP32Node(2, hardened = true),
|
||||
BIP32Node(2, hardened = false),
|
||||
BIP32Node(1000000000, hardened = false)
|
||||
BIP32Node(0, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(1, hardenedOpt = None),
|
||||
BIP32Node(2, hardenedOpt = HardenedType.defaultOpt),
|
||||
BIP32Node(2, hardenedOpt = None),
|
||||
BIP32Node(1000000000, hardenedOpt = None)
|
||||
))
|
||||
assert(BIP32Path
|
||||
.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 {
|
||||
{
|
||||
val first = BIP32Path.fromString("m/44'/1'")
|
||||
|
@ -40,7 +40,7 @@ class HDAccountTest extends BitcoinSUnitTest {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
@ -129,7 +129,7 @@ class HDPathTest extends BitcoinSUnitTest {
|
||||
forAll(HDGenerators.hdPathWithConstructor) { case (hd, hdApply) =>
|
||||
val nonHardenedCoinChildren = hd.path.zipWithIndex.map {
|
||||
case (child, index) =>
|
||||
if (index == LegacyHDPath.COIN_INDEX) child.copy(hardened = false)
|
||||
if (index == LegacyHDPath.COIN_INDEX) child.copy(hardenedOpt = None)
|
||||
else child
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ class HDPathTest extends BitcoinSUnitTest {
|
||||
val nonHardenedAccountChildren = hd.path.zipWithIndex.map {
|
||||
case (child, index) =>
|
||||
if (index == LegacyHDPath.ACCOUNT_INDEX)
|
||||
child.copy(hardened = false)
|
||||
child.copy(hardenedOpt = None)
|
||||
else child
|
||||
}
|
||||
val badAccountAttempt = hdApply(nonHardenedAccountChildren)
|
||||
@ -157,7 +157,8 @@ class HDPathTest extends BitcoinSUnitTest {
|
||||
|
||||
val hardenedChainChildren = hd.path.zipWithIndex.map {
|
||||
case (child, index) =>
|
||||
if (index == LegacyHDPath.CHAIN_INDEX) child.copy(hardened = true)
|
||||
if (index == LegacyHDPath.CHAIN_INDEX)
|
||||
child.copy(hardenedOpt = HardenedType.defaultOpt)
|
||||
else child
|
||||
}
|
||||
val badChainAttempt =
|
||||
@ -171,7 +172,8 @@ class HDPathTest extends BitcoinSUnitTest {
|
||||
|
||||
val hardenedAddressChildren = hd.path.zipWithIndex.map {
|
||||
case (child, index) =>
|
||||
if (index == LegacyHDPath.ADDRESS_INDEX) child.copy(hardened = true)
|
||||
if (index == LegacyHDPath.ADDRESS_INDEX)
|
||||
child.copy(hardenedOpt = HardenedType.defaultOpt)
|
||||
else child
|
||||
}
|
||||
val badAddrAttempt =
|
||||
|
@ -7,32 +7,43 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
|
||||
|
||||
behavior of "DescriptorChecksumTest"
|
||||
|
||||
val expression =
|
||||
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef"))
|
||||
val descriptor =
|
||||
RawDescriptor(
|
||||
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef")),
|
||||
None)
|
||||
it must "calculate correct checksums from BIP380 examples" in {
|
||||
val str0 = "raw(deadbeef)#89f8spxm"
|
||||
val split0 = str0.split("#")
|
||||
val (payload, checksum) = (split0(0), split0(1))
|
||||
assert(Descriptor.createChecksum(payload) == checksum)
|
||||
|
||||
assert(Descriptor.isValidChecksum(expression, Some(checksum)))
|
||||
assert(Descriptor.isValidChecksum(descriptor, Some(checksum)))
|
||||
|
||||
//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 {
|
||||
//Missing checksum
|
||||
assert(!Descriptor.isValidChecksum(expression, Some("#")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("#")))
|
||||
//Too long checksum (9 chars)
|
||||
assert(!Descriptor.isValidChecksum(expression, Some("89f8spxmx")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spxmx")))
|
||||
//Too short checksum (7 chars)
|
||||
assert(!Descriptor.isValidChecksum(expression, Some("89f8spx")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spx")))
|
||||
//Error in payload
|
||||
val bad =
|
||||
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deedbeef"))
|
||||
RawDescriptor(
|
||||
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deedbeef")),
|
||||
None)
|
||||
assert(!Descriptor.isValidChecksum(bad, Some("89f8spxm")))
|
||||
//Error in checksum
|
||||
assert(!Descriptor.isValidChecksum(expression, Some("#9f8spxm")))
|
||||
assert(!Descriptor.isValidChecksum(descriptor, Some("#9f8spxm")))
|
||||
}
|
||||
}
|
||||
|
@ -452,6 +452,14 @@ class DescriptorTest extends BitcoinSUnitTest {
|
||||
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 = {
|
||||
val desc = ScriptDescriptor.fromString(descriptor)
|
||||
assert(desc.toString == descriptor)
|
||||
|
@ -13,8 +13,8 @@ class KeyExpressionTest extends BitcoinSUnitTest {
|
||||
val str2 = "[deadbeef/0'/0h/0']"
|
||||
val keyOrigin = KeyOriginExpression.fromString(str0)
|
||||
assert(str0 == keyOrigin.toString)
|
||||
assert(keyOrigin == KeyOriginExpression.fromString(str1))
|
||||
keyOrigin == KeyOriginExpression.fromString(str2)
|
||||
assert(KeyOriginExpression.fromString(str1).toString == str1)
|
||||
assert(KeyOriginExpression.fromString(str2).toString == str2)
|
||||
}
|
||||
|
||||
it must "parse valid private key expressions from BIP380" in {
|
||||
|
@ -80,13 +80,14 @@ abstract class BIP32Path extends SeqWrapper[BIP32Node] {
|
||||
}
|
||||
}
|
||||
|
||||
override def toString: String =
|
||||
override def toString: String = {
|
||||
path
|
||||
.map { case BIP32Node(index, hardened) =>
|
||||
val isHardened = if (hardened) "'" else ""
|
||||
.map { case BIP32Node(index, hardenedOpt) =>
|
||||
val isHardened = hardenedOpt.map(_.toString).getOrElse("")
|
||||
index.toString + isHardened
|
||||
}
|
||||
.fold("m")((accum, curr) => accum + "/" + curr)
|
||||
}
|
||||
|
||||
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"""")
|
||||
|
||||
val path = rest.map { str =>
|
||||
val (index: String, hardened: Boolean) =
|
||||
if (str.endsWith("'") || str.endsWith("h")) {
|
||||
(str.dropRight(1), true)
|
||||
} else {
|
||||
(str, false)
|
||||
val (index: String, hardenedOpt: Option[HardenedType]) = {
|
||||
HardenedType.fromStringOpt(str.last.toString) match {
|
||||
case Some(h) => (str.dropRight(1), Some(h))
|
||||
case None => (str, None)
|
||||
}
|
||||
BIP32Node(index.toInt, hardened)
|
||||
}
|
||||
BIP32Node(index.toInt, hardenedOpt)
|
||||
}
|
||||
|
||||
BIP32PathImpl(path)
|
||||
@ -173,7 +174,7 @@ object BIP32Path extends Factory[BIP32Path] with StringFactory[BIP32Path] {
|
||||
if (littleEndian) UInt32.fromBytesLE(part) else UInt32.fromBytes(part)
|
||||
val hardened = uInt32 >= ExtKey.hardenedIdx
|
||||
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)
|
||||
@ -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")
|
||||
|
||||
def hardened: Boolean = hardenedOpt.isDefined
|
||||
|
||||
/** Converts this node to a BIP32 notation
|
||||
* unsigned 32 bit integer
|
||||
*/
|
||||
def toUInt32: UInt32 =
|
||||
if (hardened) ExtKey.hardenedIdx + UInt32(index.toLong)
|
||||
if (hardenedOpt.isDefined) ExtKey.hardenedIdx + UInt32(index.toLong)
|
||||
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)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ case class HDAccount(
|
||||
require(index >= 0, s"Account index ($index) must be positive!")
|
||||
|
||||
override val path: Vector[BIP32Node] = {
|
||||
coin.path :+ BIP32Node(index, hardened = true)
|
||||
coin.path :+ BIP32Node(index, hardenedOpt = HardenedType.defaultOpt)
|
||||
}
|
||||
|
||||
def purpose: HDPurpose = coin.purpose
|
||||
|
@ -8,7 +8,7 @@ sealed abstract class HDAddress extends BIP32Path {
|
||||
require(index >= 0, s"Address index ($index) must be positive!")
|
||||
|
||||
override val path: Vector[BIP32Node] = {
|
||||
chain.path :+ BIP32Node(index, hardened = false)
|
||||
chain.path :+ BIP32Node(index, hardenedOpt = None)
|
||||
}
|
||||
|
||||
def purpose: HDPurpose
|
||||
|
@ -7,7 +7,7 @@ package org.bitcoins.core.hd
|
||||
sealed abstract class HDChain extends BIP32Path {
|
||||
|
||||
override val path: Vector[BIP32Node] = {
|
||||
account.path :+ BIP32Node(toInt, hardened = false)
|
||||
account.path :+ BIP32Node(toInt, hardenedOpt = None)
|
||||
}
|
||||
|
||||
def purpose: HDPurpose
|
||||
|
@ -7,7 +7,7 @@ package org.bitcoins.core.hd
|
||||
case class HDCoin(purpose: HDPurpose, coinType: HDCoinType) extends BIP32Path {
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -41,15 +41,16 @@ private[hd] trait HDPathFactory[PathType <: BIP32Path]
|
||||
val maybePurpose = children.head
|
||||
|
||||
val purpose: HDPurpose = maybePurpose match {
|
||||
case BIP32Node(_, false) =>
|
||||
case BIP32Node(_, None) =>
|
||||
throw new IllegalArgumentException(
|
||||
"The first child in a HD path must be hardened")
|
||||
case BIP32Node(HDPurposes.Legacy.constant, true) => HDPurposes.Legacy
|
||||
case BIP32Node(HDPurposes.SegWit.constant, true) => HDPurposes.SegWit
|
||||
case BIP32Node(HDPurposes.NestedSegWit.constant, true) =>
|
||||
case BIP32Node(HDPurposes.Legacy.constant, Some(_)) => HDPurposes.Legacy
|
||||
case BIP32Node(HDPurposes.SegWit.constant, Some(_)) => HDPurposes.SegWit
|
||||
case BIP32Node(HDPurposes.NestedSegWit.constant, Some(_)) =>
|
||||
HDPurposes.NestedSegWit
|
||||
case BIP32Node(HDPurposes.Multisig.constant, true) => HDPurposes.Multisig
|
||||
case BIP32Node(unknown, true) =>
|
||||
case BIP32Node(HDPurposes.Multisig.constant, Some(_)) =>
|
||||
HDPurposes.Multisig
|
||||
case BIP32Node(unknown, Some(_)) =>
|
||||
throw new IllegalArgumentException(
|
||||
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 =
|
||||
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
|
||||
*/
|
||||
|
@ -18,7 +18,7 @@ package org.bitcoins.core.hd
|
||||
case class HDPurpose(constant: Int) extends BIP32Path {
|
||||
|
||||
override val path: Vector[BIP32Node] = Vector(
|
||||
BIP32Node(constant, hardened = true))
|
||||
BIP32Node(constant, HardenedType.defaultOpt))
|
||||
}
|
||||
|
||||
object HDPurposes {
|
||||
|
@ -141,7 +141,9 @@ sealed abstract class DescriptorFactory[
|
||||
//now check for a valid checksum
|
||||
val checksumOpt =
|
||||
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) {
|
||||
createDescriptor(expression, checksumOpt)
|
||||
} else {
|
||||
@ -468,13 +470,17 @@ object Descriptor extends StringFactory[Descriptor] {
|
||||
builder.result()
|
||||
}
|
||||
|
||||
def createChecksum(descriptor: Descriptor): String = {
|
||||
createChecksum(descriptor.toString)
|
||||
}
|
||||
|
||||
def isValidChecksum(
|
||||
expression: DescriptorExpression,
|
||||
descriptor: Descriptor,
|
||||
checksumOpt: Option[String]): Boolean = {
|
||||
checksumOpt match {
|
||||
case None => true //trivially true if we have no checksum
|
||||
case Some(checksum) =>
|
||||
val t = Try(createChecksum(expression.toString))
|
||||
val t = Try(createChecksum(descriptor.toString))
|
||||
if (t.isFailure) false
|
||||
else t.get == checksum
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import org.bitcoins.core.crypto.{
|
||||
ExtPrivateKey,
|
||||
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.script.ScriptType
|
||||
import org.bitcoins.crypto._
|
||||
@ -175,20 +175,25 @@ sealed abstract class ExtECPublicKeyExpression
|
||||
|
||||
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
|
||||
|
||||
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("") +
|
||||
ExtKey.toString(extKey) +
|
||||
pathOpt.map(_.toString.drop(1)).getOrElse("") +
|
||||
childrenHardenedOpt
|
||||
.map {
|
||||
case true => "/*'"
|
||||
case false => "/*"
|
||||
}
|
||||
.getOrElse("")
|
||||
hardenedStr
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +209,7 @@ sealed abstract class ExtXOnlyPublicKeyExpression
|
||||
|
||||
def pathOpt: Option[BIP32Path] = ecPublicKeyExpression.pathOpt
|
||||
|
||||
def childrenHardenedOpt: Option[Boolean] =
|
||||
def childrenHardenedOpt: Option[Option[HardenedType]] =
|
||||
ecPublicKeyExpression.childrenHardenedOpt
|
||||
|
||||
def deriveChild(idx: Int): BaseECKey = ecPublicKeyExpression.deriveChild(idx)
|
||||
@ -221,7 +226,7 @@ case class XprvECPublicKeyExpression(
|
||||
override val extKey: ExtPrivateKey,
|
||||
originOpt: Option[KeyOriginExpression],
|
||||
pathOpt: Option[BIP32Path],
|
||||
childrenHardenedOpt: Option[Boolean])
|
||||
childrenHardenedOpt: Option[Option[HardenedType]])
|
||||
extends ExtECPublicKeyExpression
|
||||
with ECPublicKeyExpression {
|
||||
|
||||
@ -242,7 +247,7 @@ case class XprvECPublicKeyExpression(
|
||||
childrenHardenedOpt.isDefined,
|
||||
s"Cannot derive child keys from descriptor that does not allow children, got=${toString}")
|
||||
val node =
|
||||
BIP32Node(index = idx, hardened = childrenHardenedOpt.getOrElse(false))
|
||||
BIP32Node(index = idx, hardenedOpt = childrenHardenedOpt.get)
|
||||
val fullPath: BIP32Path = pathOpt match {
|
||||
case Some(p) => BIP32Path(p.path.appended(node))
|
||||
case None => BIP32Path(node)
|
||||
@ -270,7 +275,7 @@ case class XpubECPublicKeyExpression(
|
||||
override val extKey: ExtPublicKey,
|
||||
originOpt: Option[KeyOriginExpression],
|
||||
pathOpt: Option[BIP32Path],
|
||||
childrenHardenedOpt: Option[Boolean])
|
||||
childrenHardenedOpt: Option[Option[HardenedType]])
|
||||
extends ExtECPublicKeyExpression
|
||||
with ECPublicKeyExpression {
|
||||
|
||||
@ -290,7 +295,7 @@ case class XpubECPublicKeyExpression(
|
||||
require(
|
||||
childrenHardenedOpt.isDefined,
|
||||
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 {
|
||||
case Some(p) => BIP32Path(p.path.appended(node))
|
||||
case None => BIP32Path(node)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bitcoins.core.protocol.script.descriptor
|
||||
|
||||
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.{
|
||||
MultiSignatureScriptPubKey,
|
||||
RawScriptPubKey
|
||||
@ -11,10 +11,6 @@ import org.bitcoins.crypto._
|
||||
case class DescriptorIterator(descriptor: String) {
|
||||
private var index: Int = 0
|
||||
|
||||
private val hardenedChars: Vector[Char] = {
|
||||
Vector('\'', 'h', 'H')
|
||||
}
|
||||
|
||||
def current: String = {
|
||||
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) == '*') {
|
||||
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)
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
Some(hardenedOpt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -749,8 +749,8 @@ abstract class DLCWallet
|
||||
account: AccountDb,
|
||||
keyIndex: Int): AdaptorSign = {
|
||||
val bip32Path = BIP32Path(
|
||||
account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false),
|
||||
BIP32Node(keyIndex, hardened = false)))
|
||||
account.hdAccount.path ++ Vector(BIP32Node(0, hardenedOpt = None),
|
||||
BIP32Node(keyIndex, hardenedOpt = None)))
|
||||
val privKeyPath = HDPath.fromString(bip32Path.toString)
|
||||
keyManager.toSign(privKeyPath)
|
||||
}
|
||||
|
@ -572,8 +572,8 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit
|
||||
|
||||
val bip32Path = BIP32Path(
|
||||
dlcDb.account.path ++ Vector(
|
||||
BIP32Node(dlcDb.changeIndex.index, hardened = false),
|
||||
BIP32Node(dlcDb.keyIndex, hardened = false)))
|
||||
BIP32Node(dlcDb.changeIndex.index, hardenedOpt = None),
|
||||
BIP32Node(dlcDb.keyIndex, hardenedOpt = None)))
|
||||
|
||||
val privKeyPath = HDPath.fromString(bip32Path.toString)
|
||||
val fundingPrivKey = keyManager.toSign(privKeyPath)
|
||||
|
@ -51,7 +51,7 @@ val extPrivKey = ExtPrivateKey(ExtKeyVersion.SegWitMainNetPriv)
|
||||
|
||||
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)
|
||||
```
|
||||
|
@ -8,6 +8,8 @@ import scala.util.Try
|
||||
*/
|
||||
object HDGenerators {
|
||||
|
||||
def hardenedType: Gen[HardenedType] = Gen.oneOf(HardenedType.all)
|
||||
|
||||
/** Generates a BIP 32 path segment
|
||||
*/
|
||||
def bip32Child: Gen[BIP32Node] = Gen.oneOf(softBip32Child, hardBip32Child)
|
||||
@ -17,14 +19,15 @@ object HDGenerators {
|
||||
def softBip32Child: Gen[BIP32Node] =
|
||||
for {
|
||||
index <- NumberGenerator.positiveInts
|
||||
} yield BIP32Node(index, hardened = false)
|
||||
} yield BIP32Node(index, hardenedOpt = None)
|
||||
|
||||
/** Generates a hardened BIP 32 path segment
|
||||
*/
|
||||
def hardBip32Child: Gen[BIP32Node] =
|
||||
for {
|
||||
soft <- softBip32Child
|
||||
} yield soft.copy(hardened = true)
|
||||
hardened <- hardenedType
|
||||
} yield soft.copy(hardenedOpt = Some(hardened))
|
||||
|
||||
/** Generates a BIP32 path
|
||||
*/
|
||||
|
@ -37,9 +37,9 @@ object GetAddresses extends App {
|
||||
accountIndex <- 0 until 3
|
||||
} yield {
|
||||
val accountPath = BIP32Path(
|
||||
BIP32Node(constant.constant, hardened = true),
|
||||
BIP32Node(coin.toInt, hardened = true),
|
||||
BIP32Node(accountIndex, hardened = true)
|
||||
BIP32Node(constant.constant, HardenedType.defaultOpt),
|
||||
BIP32Node(coin.toInt, HardenedType.defaultOpt),
|
||||
BIP32Node(accountIndex, HardenedType.defaultOpt)
|
||||
)
|
||||
|
||||
val pathType =
|
||||
@ -68,11 +68,11 @@ object GetAddresses extends App {
|
||||
addressIndex <- 0 until 3
|
||||
} yield {
|
||||
val path = BIP32Path(
|
||||
BIP32Node(constant.constant, hardened = true),
|
||||
BIP32Node(coin.toInt, hardened = true),
|
||||
BIP32Node(accountIndex, hardened = true),
|
||||
BIP32Node(chainType.index, hardened = false),
|
||||
BIP32Node(addressIndex, hardened = false)
|
||||
BIP32Node(constant.constant, HardenedType.defaultOpt),
|
||||
BIP32Node(coin.toInt, HardenedType.defaultOpt),
|
||||
BIP32Node(accountIndex, HardenedType.defaultOpt),
|
||||
BIP32Node(chainType.index, HardenedType.defaultOpt),
|
||||
BIP32Node(addressIndex, None)
|
||||
)
|
||||
|
||||
val addressCmd = s"trezorctl get-address -n $path -t $trezorPathType"
|
||||
|
Loading…
Reference in New Issue
Block a user