2024 04 16 bitcoindrpc descriptor (#5530)

* Integrate Descriptor class into 'getdescriptorinfo' RPC

* WIP: Invalid checksum that is valid according to bitcoin core

* Add descriptor.py in comments

* Get deriveaddresses RPC working with descriptors

* Parse descriptors in DescriptorsResult to Descriptor data type
This commit is contained in:
Chris Stewart 2024-04-18 11:46:33 -05:00 committed by GitHub
parent 6f6a78ab52
commit e143792fb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 115 additions and 77 deletions

View file

@ -5,6 +5,7 @@ import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.BlockHeader
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.util.SeqWrapper
import org.bitcoins.core.wallet.fee.BitcoinFeeUnit
@ -197,7 +198,7 @@ final case class DeriveAddressesResult(addresses: Vector[BitcoinAddress])
}
final case class GetDescriptorInfoResult(
descriptor: String,
descriptor: Descriptor,
checksum: Option[String],
isrange: Boolean,
issolvable: Boolean,

View file

@ -5,6 +5,7 @@ import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.core.hd.BIP32Path
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.core.protocol.script.{ScriptPubKey, WitnessVersion}
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.script.ScriptType
@ -468,11 +469,11 @@ object AddressInfoResultPostV21 {
case class ListDescriptorsResult(
wallet_name: String,
descriptors: Vector[descriptorsResult]
descriptors: Vector[DescriptorsResult]
) extends WalletResult
case class descriptorsResult(
desc: String,
case class DescriptorsResult(
desc: Descriptor,
timestamp: ZonedDateTime,
active: Boolean,
internal: Option[Boolean],

View file

@ -21,6 +21,7 @@ import org.bitcoins.core.protocol.ln.fee.FeeProportionalMillionths
import org.bitcoins.core.protocol.ln.node.{Feature, FeatureSupport, NodeId}
import org.bitcoins.core.protocol.ln.routing.{ChannelRoute, NodeRoute, Route}
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.core.protocol.tlv.{
OracleAnnouncementV0TLV,
OracleAttestmentV0TLV
@ -325,6 +326,14 @@ object JsonReaders {
ScriptWitness.fromHexOpt)(json)
}
implicit object DescriptorReads extends Reads[Descriptor] {
override def reads(json: JsValue): JsResult[Descriptor] = {
SerializerUtil.processJsStringOpt[Descriptor](Descriptor.fromStringOpt)(
json)
}
}
implicit object BlockReads extends Reads[Block] {
override def reads(json: JsValue): JsResult[Block] =

View file

@ -521,8 +521,8 @@ object JsonSerializers {
)
}
implicit val descriptorsResultReads: Reads[descriptorsResult] =
Json.reads[descriptorsResult]
implicit val descriptorsResultReads: Reads[DescriptorsResult] =
Json.reads[DescriptorsResult]
implicit val listDescriptorsReads: Reads[ListDescriptorsResult] =
Json.reads[ListDescriptorsResult]
@ -687,9 +687,6 @@ object JsonSerializers {
implicit val listWalletsDirResultReads: Reads[ListWalletDirResult] =
Json.reads[ListWalletDirResult]
implicit val deriveAddressesResultReads: Reads[DeriveAddressesResult] =
Json.reads[DeriveAddressesResult]
implicit val submitHeaderResultReads: Reads[SubmitHeaderResult] =
Json.reads[SubmitHeaderResult]
@ -915,4 +912,22 @@ object JsonSerializers {
implicit val outputMapWrites: Writes[Map[BitcoinAddress, Bitcoins]] =
mapWrites[BitcoinAddress, Bitcoins](_.value)
implicit object DeriveAddressResults extends Reads[DeriveAddressesResult] {
override def reads(json: JsValue): JsResult[DeriveAddressesResult] = {
json match {
case str: JsString =>
bitcoinAddressReads
.reads(str)
.map(addr => DeriveAddressesResult(Vector(addr)))
case arr: JsArray =>
val addresses: Vector[BitcoinAddress] =
arr.as[Vector[BitcoinAddress]]
JsSuccess(DeriveAddressesResult(addresses))
case x =>
JsError(s"Invalid response for deriveaddresses rpc, got=$x")
}
}
}
}

View file

@ -7,6 +7,7 @@ import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.ln.LnInvoice
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt._
import org.bitcoins.core.script.ScriptType
@ -110,6 +111,13 @@ object JsonWriters {
ScriptPubKeyWrites.writes(o)
}
implicit object DescriptorWrites extends Writes[Descriptor] {
override def writes(d: Descriptor): JsValue = {
JsString(d.toString)
}
}
implicit object TransactionInputWrites extends Writes[TransactionInput] {
override def writes(o: TransactionInput): JsValue =

View file

@ -20,8 +20,6 @@ import org.bitcoins.testkit.rpc.{
BitcoindRpcTestUtil
}
import org.scalatest.Assertion
import java.time.ZonedDateTime
import scala.concurrent.Future
class BitcoindV22RpcClientTest extends BitcoindFixturesCachedPairV22 {
@ -192,45 +190,6 @@ class BitcoindV22RpcClientTest extends BitcoindFixturesCachedPairV22 {
}
}
it should "output wallet name from listdescriptors" in {
nodePair: FixtureParam =>
val client = nodePair.node1
for {
_ <- client.unloadWallet("")
_ <- client.createWallet("descriptorWalletThree", descriptors = true)
resultWallets <- client.listDescriptors(walletName =
"descriptorWalletThree")
_ <- client.unloadWallet("descriptorWalletThree")
_ <- client.loadWallet("")
} yield {
assert(resultWallets.wallet_name == "descriptorWalletThree")
}
}
it should "output descriptors from listdescriptors" in {
nodePair: FixtureParam =>
val client = nodePair.node1
for {
_ <- client.unloadWallet("")
_ <- client.createWallet("descriptorWalletTwo", descriptors = true)
resultWallet <- client.listDescriptors(walletName =
"descriptorWalletTwo")
_ <- client.unloadWallet("descriptorWalletTwo")
_ <- client.loadWallet("")
} yield {
resultWallet.descriptors.map { d =>
assert(
d.desc.isInstanceOf[String] && d.timestamp
.isInstanceOf[ZonedDateTime]
&& d.active.isInstanceOf[Boolean] && d.internal
.isInstanceOf[Option[Boolean]]
&& d.range.isInstanceOf[Option[Vector[Int]]] && d.next
.isInstanceOf[Option[Int]])
}
succeed
}
}
it should "be able to decode a reedem script" in { nodePair: FixtureParam =>
val client = nodePair.node1
val ecPrivKey1 = ECPrivateKey.freshPrivateKey

View file

@ -1,14 +1,15 @@
package org.bitcoins.rpc.v23
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.AddressType
import org.bitcoins.commons.jsonmodels.bitcoind.{
AddressInfoResultPostV18,
AddressInfoResultPostV21,
AddressInfoResultPreV18
}
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.AddressType
import org.bitcoins.core.api.chain.db.BlockHeaderDbHelper
import org.bitcoins.core.protocol.Bech32mAddress
import org.bitcoins.core.protocol.blockchain.RegTestNetChainParams
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.rpc.client.common.BitcoindVersion
import org.bitcoins.rpc.client.v23.BitcoindV23RpcClient
import org.bitcoins.testkit.chain.BlockHeaderHelper
@ -79,11 +80,12 @@ class BitcoindV23RpcClientTest extends BitcoindFixturesFundedCachedV23 {
it should "analyze a descriptor" in { client =>
val descriptor =
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
Descriptor.fromString(
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#gn28ywm7")
val descriptorF = client.getDescriptorInfo(descriptor)
descriptorF.map { result =>
assert(result.descriptor == descriptor)
assert(result.isrange.==(false))
assert(result.issolvable.==(true))
assert(result.hasprivatekeys.==(false))

View file

@ -10,6 +10,7 @@ import org.bitcoins.core.api.chain.db.BlockHeaderDbHelper
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.{Bech32mAddress, BitcoinAddress}
import org.bitcoins.core.protocol.blockchain.RegTestNetChainParams
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import org.bitcoins.rpc.client.common.BitcoindVersion
import org.bitcoins.rpc.client.v24.BitcoindV24RpcClient
import org.bitcoins.testkit.chain.BlockHeaderHelper
@ -74,11 +75,13 @@ class BitcoindV24RpcClientTest extends BitcoindFixturesFundedCachedV24 {
it should "analyze a descriptor" in { client =>
val descriptor =
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"
Descriptor.fromString(
"pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#gn28ywm7")
val descriptorF = client.getDescriptorInfo(descriptor)
descriptorF.map { result =>
assert(result.descriptor == descriptor)
assert(result.isrange.==(false))
assert(result.issolvable.==(true))
assert(result.hasprivatekeys.==(false))
@ -116,4 +119,39 @@ class BitcoindV24RpcClientTest extends BitcoindFixturesFundedCachedV24 {
spending <- client.getTxSpendingPrevOut(tx.inputs.head.previousOutput)
} yield assert(spending.spendingtxid.contains(txid))
}
it should "derive addresses from a descriptor" in { client =>
val str0 =
"wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)#t6wfjs64"
val descriptor0 = Descriptor.fromString(str0)
assert(descriptor0.toString == str0)
val addresses0F =
client.deriveAddresses(descriptor0, None).map(_.addresses)
val expected0 =
Vector("bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5").map(
BitcoinAddress.fromString)
val assert0 = addresses0F.map { addresses =>
assert(addresses == expected0)
}
val str1 =
"wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)#kft60nuy"
val descriptor1 = Descriptor.fromString(str1)
assert(descriptor1.toString == str1)
val addresses1F =
client.deriveAddresses(descriptor1, Some(Vector(0, 2))).map(_.addresses)
val expected1 =
Vector("bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5",
"bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy",
"bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq")
.map(BitcoinAddress.fromString)
val assert1 = assert0.flatMap(_ =>
addresses1F.map { addresses =>
assert(addresses == expected1)
})
assert1
}
}

View file

@ -5,7 +5,9 @@ import org.bitcoins.commons.jsonmodels.bitcoind.{
GetDescriptorInfoResult
}
import org.bitcoins.commons.serializers.JsonSerializers._
import play.api.libs.json.{JsString, Json}
import org.bitcoins.commons.serializers.JsonWriters.DescriptorWrites
import org.bitcoins.core.protocol.script.descriptor.Descriptor
import play.api.libs.json.Json
import scala.concurrent.Future
@ -17,16 +19,19 @@ trait DescriptorRpc {
self: Client =>
def deriveAddresses(
descriptor: String,
descriptor: Descriptor,
range: Option[Vector[Double]]): Future[DeriveAddressesResult] = {
val params =
if (range.isDefined) List(JsString(descriptor), Json.toJson(range))
else List(JsString(descriptor))
if (range.isDefined)
List(DescriptorWrites.writes(descriptor), Json.toJson(range))
else List(DescriptorWrites.writes(descriptor))
bitcoindCall[DeriveAddressesResult]("deriveaddresses", params)
}
def getDescriptorInfo(descriptor: String): Future[GetDescriptorInfoResult] = {
bitcoindCall[GetDescriptorInfoResult]("getdescriptorinfo",
List(JsString(descriptor)))
def getDescriptorInfo(
descriptor: Descriptor): Future[GetDescriptorInfoResult] = {
bitcoindCall[GetDescriptorInfoResult](
"getdescriptorinfo",
List(DescriptorWrites.writes(descriptor)))
}
}

View file

@ -7,7 +7,7 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
behavior of "DescriptorChecksumTest"
val descriptor =
val descriptor0 =
RawDescriptor(
RawScriptExpression(NonStandardScriptPubKey.fromAsmHex("deadbeef")),
None)
@ -17,26 +17,26 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
val (payload, checksum) = (split0(0), split0(1))
assert(Descriptor.createChecksum(payload) == checksum)
assert(Descriptor.isValidChecksum(descriptor, Some(checksum)))
assert(Descriptor.isValidChecksum(descriptor0, Some(checksum)))
//expression with nochecksum should be valid
assert(Descriptor.isValidChecksum(descriptor, None))
assert(Descriptor.isValidChecksum(descriptor0, 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)))
val descriptor1 =
Descriptor.fromString(
"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)")
val checksum1 = "cjjspncu"
assert(Descriptor.isValidChecksum(descriptor1, Some(checksum1)))
assert(Descriptor.createChecksum(descriptor1) == checksum1)
}
it must "fail when a bad checksum is given" in {
//Missing checksum
assert(!Descriptor.isValidChecksum(descriptor, Some("#")))
assert(!Descriptor.isValidChecksum(descriptor0, Some("#")))
//Too long checksum (9 chars)
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spxmx")))
assert(!Descriptor.isValidChecksum(descriptor0, Some("89f8spxmx")))
//Too short checksum (7 chars)
assert(!Descriptor.isValidChecksum(descriptor, Some("89f8spx")))
assert(!Descriptor.isValidChecksum(descriptor0, Some("89f8spx")))
//Error in payload
val bad =
RawDescriptor(
@ -44,6 +44,6 @@ class DescriptorChecksumTest extends BitcoinSUnitTest {
None)
assert(!Descriptor.isValidChecksum(bad, Some("89f8spxm")))
//Error in checksum
assert(!Descriptor.isValidChecksum(descriptor, Some("#9f8spxm")))
assert(!Descriptor.isValidChecksum(descriptor0, Some("#9f8spxm")))
}
}

View file

@ -114,7 +114,7 @@ object ExtKey extends Factory[ExtKey] with StringFactory[ExtKey] {
val masterFingerprint: ByteVector = hex"00000000"
val prefixes: Vector[String] = Vector("xprv", "xpub")
val prefixes: Vector[String] = Vector("xprv", "xpub", "tprv", "tpub")
/** Takes in a base58 string and tries to convert it to an extended key */
override def fromString(base58: String): ExtKey = {

View file

@ -142,8 +142,8 @@ sealed abstract class DescriptorFactory[
val checksumOpt =
if (checksum.nonEmpty) Some(checksum.tail) else None //drop '#'
val isValidChecksum = Descriptor.isValidChecksum(
descriptor = createDescriptor(expression, None),
checksumOpt = checksumOpt)
createDescriptor(expression, None),
checksumOpt)
if (isValidChecksum) {
createDescriptor(expression, checksumOpt)
} else {