diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index 6e6322f4d7..09f78404b2 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -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 diff --git a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala index a6e03c3004..d8f31c883f 100644 --- a/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/CoreRoutes.scala @@ -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) + } + } } } diff --git a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala index 5003601f9c..8da1856eb2 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -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], diff --git a/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala b/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala index 68ed75232e..c599948e03 100644 --- a/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/psbt/PSBTUnitTest.scala @@ -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 { diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala index ef474e3be4..e630b3b4a7 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala @@ -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 diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala index a167661930..a2dc23ebe1 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBTMap.scala @@ -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 } diff --git a/core/src/main/scala/org/bitcoins/core/psbt/PSBTRole.scala b/core/src/main/scala/org/bitcoins/core/psbt/PSBTRole.scala new file mode 100644 index 0000000000..3d797e1612 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBTRole.scala @@ -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") + } + } +} diff --git a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala index 18840bd777..434cf082e0 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/utxo/InputSigningInfo.scala @@ -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 {