mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-19 05:43:51 +01:00
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:
parent
6043e4efa8
commit
c167bc04a0
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
57
core/src/main/scala/org/bitcoins/core/psbt/PSBTRole.scala
Normal file
57
core/src/main/scala/org/bitcoins/core/psbt/PSBTRole.scala
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user