Analyze PSBT function (#2240)

* Analyze PSBT function

* Add to tests

* Add docs

* Add more functions

* Fix compile issues

* Fix compile issue
This commit is contained in:
Ben Carman 2020-11-09 15:15:32 -06:00 committed by GitHub
parent 6043e4efa8
commit c167bc04a0
8 changed files with 409 additions and 10 deletions

View File

@ -931,6 +931,20 @@ object ConsoleCli {
decode.copy(psbt = psbt)
case other => other
}))),
cmd("analyzepsbt")
.action((_, conf) => conf.copy(command = AnalyzePSBT(PSBT.empty)))
.text("Analyzes and provides information about the current status of a PSBT and its inputs")
.children(
arg[PSBT]("psbt")
.text("PSBT serialized in hex or base64 format")
.required()
.action((psbt, conf) =>
conf.copy(command = conf.command match {
case analyzePSBT: AnalyzePSBT =>
analyzePSBT.copy(psbt = psbt)
case other => other
}))
),
cmd("combinepsbts")
.action((_, conf) => conf.copy(command = CombinePSBTs(Seq.empty)))
.text("Combines all the given PSBTs")
@ -1305,6 +1319,9 @@ object ConsoleCli {
case DecodeRawTransaction(tx) =>
RequestParam("decoderawtransaction", Seq(up.writeJs(tx)))
case AnalyzePSBT(psbt) =>
RequestParam("analyzepsbt", Seq(up.writeJs(psbt)))
// Oracle
case GetPublicKey =>
RequestParam("getpublickey")
@ -1592,6 +1609,7 @@ object CliCommand {
case class FinalizePSBT(psbt: PSBT) extends CliCommand
case class ExtractFromPSBT(psbt: PSBT) extends CliCommand
case class ConvertToPSBT(transaction: Transaction) extends CliCommand
case class AnalyzePSBT(psbt: PSBT) extends CliCommand
// Oracle
case object GetPublicKey extends CliCommand

View File

@ -5,7 +5,9 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import org.bitcoins.commons.jsonmodels.{SerializedPSBT, SerializedTransaction}
import org.bitcoins.core.api.core.CoreApi
import ujson._
import scala.collection.mutable
import scala.util.{Failure, Success}
case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem)
@ -96,5 +98,62 @@ case class CoreRoutes(core: CoreApi)(implicit system: ActorSystem)
Server.httpSuccess(uJson)
}
}
case ServerCommand("analyzepsbt", arr) =>
AnalyzePSBT.fromJsArr(arr) match {
case Failure(exception) =>
reject(ValidationRejection("failure", Some(exception)))
case Success(AnalyzePSBT(psbt)) =>
complete {
val inputs = psbt.inputMaps.zipWithIndex.map {
case (inputMap, index) =>
val txIn = psbt.transaction.inputs(index)
val vout = txIn.previousOutput.vout.toInt
val nextRole = inputMap.nextRole(txIn)
val hasUtxo = inputMap.prevOutOpt(vout).isDefined
val isFinalized = inputMap.isFinalized
val missingSigs = inputMap.missingSignatures(vout)
if (missingSigs.isEmpty) {
Obj(
"has_utxo" -> Bool(hasUtxo),
"is_final" -> Bool(isFinalized),
"next" -> Str(nextRole.shortName)
)
} else {
Obj(
"has_utxo" -> Bool(hasUtxo),
"is_final" -> Bool(isFinalized),
"missing_sigs" -> missingSigs.map(hash => Str(hash.hex)),
"next" -> Str(nextRole.shortName)
)
}
}
val optionalsJson: Vector[(String, Num)] = {
val fee = psbt.feeOpt.map(fee =>
"fee" -> Num(fee.satoshis.toLong.toDouble))
val vsize =
psbt.estimateVSize.map(vsize =>
"estimated_vsize" -> Num(vsize.toDouble))
val feeRate = psbt.estimateSatsPerVByte.map(feeRate =>
"estimated_sats_vbyte" -> Num(feeRate.toLong.toDouble))
Vector(fee, vsize, feeRate).flatten
}
val inputJson = Vector("inputs" -> Arr.from(inputs))
val nextRoleJson: Vector[(String, Str)] =
Vector("next" -> Str(psbt.nextRole.shortName))
val jsonVec: Vector[(String, Value)] =
inputJson ++ optionalsJson ++ nextRoleJson
val jsonMap = mutable.LinkedHashMap(jsonVec: _*)
val json = Obj(jsonMap)
Server.httpSuccess(json)
}
}
}
}

View File

@ -305,6 +305,24 @@ object DecodePSBT extends ServerJsonModels {
}
}
case class AnalyzePSBT(psbt: PSBT)
object AnalyzePSBT extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[AnalyzePSBT] = {
jsArr.arr.toList match {
case psbtJs :: Nil =>
Try {
AnalyzePSBT(jsToPSBT(psbtJs))
}
case other =>
Failure(
new IllegalArgumentException(
s"Bad number of arguments: ${other.length}. Expected: 1"))
}
}
}
case class Rescan(
batchSize: Option[Int],
startBlock: Option[BlockStamp],

View File

@ -15,11 +15,13 @@ import org.bitcoins.core.psbt.PSBTGlobalKeyId.XPubKeyKeyId
import org.bitcoins.core.script.constant._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.utxo.{ConditionalPath, InputInfo}
import org.bitcoins.crypto.{ECPublicKey, Sha256Hash160Digest, Sign}
import org.bitcoins.crypto._
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.bitcoins.testkit.util.TransactionTestUtil._
import scodec.bits._
import scala.util.{Failure, Success}
class PSBTUnitTest extends BitcoinSAsyncTest {
behavior of "PSBT"
@ -37,6 +39,7 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000")
assert(emptyPsbt == expectedPsbt)
assert(expectedPsbt.nextRole == PSBTRole.UpdaterPSBTRole)
}
it must "fail to create a PSBT of an unknown version" in {
@ -125,6 +128,9 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
hex"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f000000800000008001000080000100f80200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f79650000000103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000")
assert(psbtWithSigHash.bytes == nextExpected.bytes)
assert(psbtWithSigHash.nextRole == PSBTRole.SignerPSBTRole)
assert(psbtWithSigHash.feeOpt.isDefined)
assert(psbtWithSigHash.feeOpt.get == Satoshis(10000))
}
it must "create a InputPSBTMap with both a NonWitness and Witness UTXO" in {
@ -248,13 +254,17 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
it must "successfully finalize a PSBT" in {
val psbt = PSBT(
hex"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f000000800000008001000080000100f80200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f79650000002202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d201220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000")
assert(psbt.nextRole == PSBTRole.FinalizerPSBTRole)
val expected = PSBT(
"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae000100f80200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f79650000000107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000")
val finalizedPSBT = psbt.finalizePSBT
assert(finalizedPSBT.isSuccess)
assert(finalizedPSBT.get == expected)
psbt.finalizePSBT match {
case Failure(exception) => fail(exception)
case Success(finalizedPSBT) =>
assert(finalizedPSBT == expected)
assert(finalizedPSBT.nextRole == PSBTRole.ExtractorPSBTRole)
}
}
private def getDummySigners(
@ -327,6 +337,8 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
val unsignedPsbt = PSBT(
hex"70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f000000800000008001000080000100f80200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f79650000000103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000")
assert(unsignedPsbt.nextRole == PSBTRole.SignerPSBTRole)
val privKey0 = ECPrivateKeyUtil.fromWIFToPrivateKey(
"cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr")
val privKey1 = ECPrivateKeyUtil.fromWIFToPrivateKey(
@ -344,6 +356,18 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
val privKey3 = ECPrivateKeyUtil.fromWIFToPrivateKey(
"cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE")
val expectedPubKeyHashes =
Vector(privKey0, privKey1, privKey2, privKey3).map { key =>
CryptoUtil.sha256Hash160(key.publicKey.bytes)
}
unsignedPsbt.inputMaps.zip(unsignedPsbt.transaction.inputs).foreach {
case (inputMap, input) =>
val vout = input.previousOutput.vout.toInt
val missingSigs = inputMap.missingSignatures(vout)
assert(missingSigs.forall(expectedPubKeyHashes.contains))
}
for {
firstSig0 <- unsignedPsbt.sign(inputIndex = 0, signer = privKey0)
signedPsbt0 <- firstSig0.sign(inputIndex = 1, signer = privKey1)
@ -370,6 +394,8 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
val inputMap =
InputPSBTMap(Vector(InputPSBTRecord.NonWitnessOrUnknownUTXO(tx)))
assert(inputMap.prevOutOpt(0).isDefined)
val expectedInputMap =
InputPSBTMap(Vector(InputPSBTRecord.WitnessUTXO(output)))
@ -381,6 +407,7 @@ class PSBTUnitTest extends BitcoinSAsyncTest {
val compressedInputMap = inputMap.compressMap(input)
assert(compressedInputMap == expectedInputMap)
assert(compressedInputMap.prevOutOpt(0).isDefined)
}
it must "do nothing when compressing a finalized InputPSBTMap" in {

View File

@ -1,6 +1,7 @@
package org.bitcoins.core.psbt
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.hd.BIP32Path
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
@ -8,6 +9,7 @@ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.core.util.{BitcoinSLogger, BitcoinScriptUtil}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.signer.BitcoinSigner
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
@ -79,6 +81,70 @@ case class PSBT(
this
}
/** The next [[PSBTRole]] that should be used for this PSBT */
lazy val nextRole: PSBTRole = {
val roles = inputMaps.zip(transaction.inputs).map {
case (inputMap, txIn) =>
inputMap.nextRole(txIn)
}
roles.minBy(_.order)
}
lazy val feeOpt: Option[CurrencyUnit] = {
val hasPrevUtxos =
inputMaps.zipWithIndex.forall(i => i._1.prevOutOpt(i._2).isDefined)
if (hasPrevUtxos) {
val inputAmount = inputMaps.zipWithIndex.foldLeft(CurrencyUnits.zero) {
case (accum, (input, index)) =>
// .get is safe because of hasPrevUtxos
val prevOut = input.prevOutOpt(index).get
accum + prevOut.value
}
val outputAmount =
transaction.outputs.foldLeft(CurrencyUnits.zero)(_ + _.value)
Some(inputAmount - outputAmount)
} else None
}
lazy val estimateWeight: Option[Long] = {
if (nextRole.order >= PSBTRole.SignerPSBTRole.order) {
// Need a exe context for maxScriptSigAndWitnessWeight
import scala.concurrent.ExecutionContext.Implicits.global
val dummySigner = Sign.dummySign(ECPublicKey.freshPublicKey)
val inputWeight =
inputMaps.zip(transaction.inputs).foldLeft(0L) {
case (weight, (inputMap, txIn)) =>
val (scriptSigLen, maxWitnessLen) = inputMap
.toUTXOSatisfyingInfoUsingSigners(txIn, Vector(dummySigner))
.maxScriptSigAndWitnessWeight
weight + 164 + maxWitnessLen + scriptSigLen
}
val outputWeight = transaction.outputs.foldLeft(0L)(_ + _.byteSize)
val weight = 107 + outputWeight + inputWeight
Some(weight)
} else None
}
lazy val estimateVSize: Option[Long] = {
estimateWeight.map { weight =>
Math.ceil(weight / 4.0).toLong
}
}
lazy val estimateSatsPerVByte: Option[SatoshisPerVirtualByte] = {
(feeOpt, estimateVSize) match {
case (Some(fee), Some(vsize)) =>
val rate = SatoshisPerVirtualByte.fromLong(fee.satoshis.toLong / vsize)
Some(rate)
case (None, None) | (Some(_), None) | (None, Some(_)) =>
None
}
}
/**
* Combiner defined by https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#combiner
* Takes another PSBT and adds all records that are not contained in this PSBT

View File

@ -154,6 +154,131 @@ case class InputPSBTMap(elements: Vector[InputPSBTRecord])
import org.bitcoins.core.psbt.InputPSBTRecord._
import org.bitcoins.core.psbt.PSBTInputKeyId._
/** The next [[PSBTRole]] that should be used for this input */
def nextRole(txIn: TransactionInput): PSBTRole = {
if (isFinalized) {
PSBTRole.ExtractorPSBTRole
} else {
(nonWitnessOrUnknownUTXOOpt, witnessUTXOOpt) match {
case (None, None) =>
PSBTRole.UpdaterPSBTRole
case (Some(_), None) | (None, Some(_)) | (Some(_), Some(_)) =>
val finalizeT = finalize(txIn)
finalizeT match {
case Failure(_) =>
// Try to create a dummy signer, if we can then this input is signable
val dummySigner = Sign.dummySign(ECPublicKey.freshPublicKey)
Try(toUTXOSigningInfo(txIn, dummySigner)) match {
case Failure(_) =>
PSBTRole.SignerPSBTRole
case Success(_) =>
PSBTRole.FinalizerPSBTRole
}
case Success(_) =>
PSBTRole.FinalizerPSBTRole
}
}
}
}
/** The previous output for this input
*
* @param vout The vout from the input's out point
*/
def prevOutOpt(vout: Int): Option[TransactionOutput] = {
witnessUTXOOpt match {
case Some(witnessUTXO) =>
Some(witnessUTXO.witnessUTXO)
case None =>
nonWitnessOrUnknownUTXOOpt match {
case Some(tx) =>
Some(tx.transactionSpent.outputs(vout))
case None => None
}
}
}
/** The HASH160 of each public key that could be used to sign the input,
* if calculable. [[Sha256Hash160Digest]] is used because we won't know the
* raw public key for P2PKH scripts
*
* @param spk The [[ScriptPubKey]] of the script to calculate from
*/
private def missingSigsFromScript(
spk: ScriptPubKey): Vector[Sha256Hash160Digest] = {
spk match {
case EmptyScriptPubKey | _: WitnessCommitment |
_: NonStandardScriptPubKey | _: UnassignedWitnessScriptPubKey =>
Vector.empty
case p2pk: P2PKScriptPubKey =>
if (partialSignatures.isEmpty) {
Vector(CryptoUtil.sha256Hash160(p2pk.publicKey.bytes))
} else Vector.empty
case p2pkh: P2PKHScriptPubKey =>
if (partialSignatures.isEmpty) {
Vector(p2pkh.pubKeyHash)
} else Vector.empty
case multi: MultiSignatureScriptPubKey =>
if (partialSignatures.size < multi.requiredSigs) {
val keys = multi.publicKeys.filterNot(key =>
partialSignatures.exists(_.pubKey == key))
keys.map(key => CryptoUtil.sha256Hash160(key.bytes)).toVector
} else Vector.empty
case p2wpkh: P2WPKHWitnessSPKV0 =>
if (partialSignatures.isEmpty) {
Vector(p2wpkh.pubKeyHash)
} else Vector.empty
case p2pkTime: P2PKWithTimeoutScriptPubKey =>
if (partialSignatures.isEmpty) {
val keyA = CryptoUtil.sha256Hash160(p2pkTime.pubKey.bytes)
val keyB = CryptoUtil.sha256Hash160(p2pkTime.lockTime.bytes)
Vector(keyA, keyB)
} else Vector.empty
case _: P2WSHWitnessSPKV0 =>
witnessScriptOpt match {
case Some(script) =>
missingSigsFromScript(script.witnessScript)
case None => Vector.empty
}
case _: P2SHScriptPubKey =>
redeemScriptOpt match {
case Some(script) =>
missingSigsFromScript(script.redeemScript)
case None => Vector.empty
}
case locktime: LockTimeScriptPubKey =>
missingSigsFromScript(locktime.nestedScriptPubKey)
case cond: ConditionalScriptPubKey =>
val first = missingSigsFromScript(cond.firstSPK)
val second = missingSigsFromScript(cond.secondSPK)
first ++ second
}
}
/** The HASH160 of each public key that could be used to sign the input,
* if calculable. [[Sha256Hash160Digest]] is used because we won't know the
* raw public key for P2PKH scripts
*
* @param vout The vout from the input's out point
*/
def missingSignatures(vout: Int): Vector[Sha256Hash160Digest] = {
prevOutOpt(vout) match {
case Some(output) =>
missingSigsFromScript(output.scriptPubKey)
case None =>
redeemScriptOpt.map(_.redeemScript) match {
case Some(script) =>
missingSigsFromScript(script)
case None =>
witnessScriptOpt.map(_.witnessScript) match {
case Some(script) =>
missingSigsFromScript(script)
case None => Vector.empty
}
}
}
}
def nonWitnessOrUnknownUTXOOpt: Option[NonWitnessOrUnknownUTXO] = {
getRecords(NonWitnessUTXOKeyId).headOption
}

View File

@ -0,0 +1,57 @@
package org.bitcoins.core.psbt
import org.bitcoins.crypto.StringFactory
abstract class PSBTRole {
def shortName: String
def order: Int
}
/** The different roles of operations that can be preformed on a PSBT
* [[https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#roles]]
*/
object PSBTRole extends StringFactory[PSBTRole] {
final case object CreatorPSBTRole extends PSBTRole {
override def shortName: String = "creator"
override def order: Int = 0
}
final case object UpdaterPSBTRole extends PSBTRole {
override def shortName: String = "updater"
override def order: Int = 1
}
final case object SignerPSBTRole extends PSBTRole {
override def shortName: String = "signer"
override def order: Int = 2
}
final case object FinalizerPSBTRole extends PSBTRole {
override def shortName: String = "finalizer"
override def order: Int = 3
}
final case object ExtractorPSBTRole extends PSBTRole {
override def shortName: String = "extractor"
override def order: Int = 4
}
val all: Vector[PSBTRole] = Vector(CreatorPSBTRole,
UpdaterPSBTRole,
SignerPSBTRole,
FinalizerPSBTRole,
ExtractorPSBTRole)
override def fromStringOpt(string: String): Option[PSBTRole] = {
all.find(_.toString.toLowerCase == string.toLowerCase)
}
override def fromString(string: String): PSBTRole = {
fromStringOpt(string) match {
case Some(role) => role
case None =>
sys.error(s"Could not find PSBT role for string=$string")
}
}
}

View File

@ -1,15 +1,16 @@
package org.bitcoins.core.wallet.utxo
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.protocol.script.{
SigVersionBase,
SigVersionWitnessV0,
SignatureVersion
}
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.signer.BitcoinSigner
import org.bitcoins.crypto.Sign
import scala.concurrent.duration.DurationInt
import scala.concurrent.{Await, ExecutionContext}
/** Stores the information required to generate a signature (ECSignatureParams)
* or to generate a script signature (ScriptSignatureParams) for a given satisfaction
* condition on a UTXO.
@ -86,6 +87,34 @@ case class ScriptSignatureParams[+InputType <: InputInfo](
func: InputType => T): ScriptSignatureParams[T] = {
this.copy(inputInfo = func(this.inputInfo))
}
def maxScriptSigAndWitnessWeight(implicit
ec: ExecutionContext): (Long, Long) = {
val dummyTx = BaseTransaction(
TransactionConstants.validLockVersion,
Vector(
TransactionInput(inputInfo.outPoint,
EmptyScriptSignature,
UInt32.zero)),
Vector(TransactionOutput(Satoshis.zero, EmptyScriptPubKey)),
UInt32.zero
)
val maxWitnessLenF = BitcoinSigner
.sign(this, unsignedTx = dummyTx, isDummySignature = true)
.map(_.transaction)
.map {
case wtx: WitnessTransaction =>
val scriptSigSize = wtx.inputs.head.scriptSignature.asmBytes.size
val witnessSize = wtx.witness.head.byteSize
(scriptSigSize * 4, witnessSize)
case tx: NonWitnessTransaction =>
val scriptSigSize = tx.inputs.head.scriptSignature.asmBytes.size
(scriptSigSize * 4, 0L)
}
Await.result(maxWitnessLenF, 30.seconds)
}
}
object ScriptSignatureParams {