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 8e10a5492d..6e6322f4d7 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -823,6 +823,20 @@ object ConsoleCli { case other => other })) ), + cmd("signpsbt") + .action((_, conf) => conf.copy(command = SignPSBT(PSBT.empty))) + .text("Signs the PSBT's inputs with keys that are associated with the wallet") + .children( + arg[PSBT]("psbt") + .text("PSBT to sign") + .required() + .action((psbt, conf) => + conf.copy(command = conf.command match { + case signPSBT: SignPSBT => + signPSBT.copy(psbt = psbt) + case other => other + })) + ), cmd("opreturncommit") .action((_, conf) => conf.copy(command = OpReturnCommit("", hashMessage = false, None))) @@ -1259,6 +1273,8 @@ object ConsoleCli { Seq(up.writeJs(message), up.writeJs(hashMessage), up.writeJs(satoshisPerVirtualByte))) + case SignPSBT(psbt) => + RequestParam("signpsbt", Seq(up.writeJs(psbt))) // height case GetBlockCount => RequestParam("getblockcount") // filter count @@ -1518,6 +1534,8 @@ object CliCommand { feeRateOpt: Option[SatoshisPerVirtualByte]) extends CliCommand + case class SignPSBT(psbt: PSBT) extends CliCommand + case class LockUnspent( unlock: Boolean, outPoints: Vector[LockUnspentOutputParameter]) diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 3fd2cce60d..58d873963b 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -958,6 +958,28 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + "sign a psbt" in { + val tx = Transaction( + "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000") + + (mockWalletApi + .signPSBT(_: PSBT)(_: ExecutionContext)) + .expects(PSBT.empty, executor) + .returning(Future.successful(PSBT.fromUnsignedTx(tx))) + .anyNumberOfTimes() + + val route = + walletRoutes.handleCommand( + ServerCommand("signpsbt", Arr(Str(PSBT.empty.hex)))) + + Post() ~> route ~> check { + assert(contentType == `application/json`) + assert(responseAs[String] == s"""{"result":"${PSBT + .fromUnsignedTx(tx) + .base64}","error":null}""") + } + } + "make an OP_RETURN commitment" in { val message = "Never gonna give you up, never gonna let you down" 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 499c900a3e..5003601f9c 100644 --- a/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala +++ b/app/server/src/main/scala/org/bitcoins/server/ServerJsonModels.scala @@ -476,6 +476,18 @@ object SendWithAlgo extends ServerJsonModels { } +case class SignPSBT(psbt: PSBT) + +object SignPSBT extends ServerJsonModels { + + def fromJsArr(jsArr: ujson.Arr): Try[SignPSBT] = { + require(jsArr.arr.size == 1, + s"Bad number of arguments: ${jsArr.arr.size}. Expected: 1") + + Try(SignPSBT(jsToPSBT(jsArr.arr.head))) + } +} + case class OpReturnCommit( message: String, hashMessage: Boolean, diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index 9b8042be7a..ffdb2cfefd 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -266,6 +266,18 @@ case class WalletRoutes(wallet: AnyHDWalletApi)(implicit system: ActorSystem) } } + case ServerCommand("signpsbt", arr) => + SignPSBT.fromJsArr(arr) match { + case Failure(exception) => + reject(ValidationRejection("failure", Some(exception))) + case Success(SignPSBT(psbt)) => + complete { + wallet.signPSBT(psbt).map { signed => + Server.httpSuccess(signed.base64) + } + } + } + case ServerCommand("opreturncommit", arr) => OpReturnCommit.fromJsArr(arr) match { case Failure(exception) => 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 394564db4a..68ed75232e 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 @@ -3,7 +3,7 @@ package org.bitcoins.core.psbt import org.bitcoins.core.crypto._ import org.bitcoins.core.currency.{CurrencyUnits, Satoshis} import org.bitcoins.core.hd.BIP32Path -import org.bitcoins.core.number.{Int32, UInt32} +import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.psbt.GlobalPSBTRecord.{UnsignedTransaction, Version} @@ -15,13 +15,9 @@ 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.{ - DoubleSha256Digest, - ECPublicKey, - Sha256Hash160Digest, - Sign -} +import org.bitcoins.crypto.{ECPublicKey, Sha256Hash160Digest, Sign} import org.bitcoins.testkit.util.BitcoinSAsyncTest +import org.bitcoins.testkit.util.TransactionTestUtil._ import scodec.bits._ class PSBTUnitTest extends BitcoinSAsyncTest { @@ -74,6 +70,19 @@ class PSBTUnitTest extends BitcoinSAsyncTest { .fromString( "tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF") + val bip32Paths = Vector( + BIP32Path.fromString("m/0'/0'/0'"), + BIP32Path.fromString("m/0'/0'/1'"), + BIP32Path.fromString("m/0'/0'/2'"), + BIP32Path.fromString("m/0'/0'/3'"), + BIP32Path.fromString("m/0'/0'/4'"), + BIP32Path.fromString("m/0'/0'/5'") + ) + + val keys = bip32Paths.map { path => + extKey.deriveChildPubKey(path).get.key + } + val psbt = start .addUTXOToInput( Transaction( @@ -99,12 +108,12 @@ class PSBTUnitTest extends BitcoinSAsyncTest { hex"522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae"), 1 ) - .addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/0'"), 0) - .addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/1'"), 0) - .addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/2'"), 1) - .addKeyPathToInput(extKey, BIP32Path.fromString("m/0'/0'/3'"), 1) - .addKeyPathToOutput(extKey, BIP32Path.fromString("m/0'/0'/4'"), 0) - .addKeyPathToOutput(extKey, BIP32Path.fromString("m/0'/0'/5'"), 1) + .addKeyPathToInput(extKey, bip32Paths(0), keys(0), 0) + .addKeyPathToInput(extKey, bip32Paths(1), keys(1), 0) + .addKeyPathToInput(extKey, bip32Paths(2), keys(2), 1) + .addKeyPathToInput(extKey, bip32Paths(3), keys(3), 1) + .addKeyPathToOutput(extKey, bip32Paths(4), keys(4), 0) + .addKeyPathToOutput(extKey, bip32Paths(5), keys(5), 1) assert(psbt == expected) @@ -347,29 +356,6 @@ class PSBTUnitTest extends BitcoinSAsyncTest { } } - def dummyTx( - prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, - scriptSig: ScriptSignature = EmptyScriptSignature, - spk: ScriptPubKey = EmptyScriptPubKey): Transaction = { - BaseTransaction( - version = Int32.zero, - inputs = Vector( - TransactionInput(outPoint = TransactionOutPoint(txId = prevTxId, - vout = UInt32.zero), - scriptSignature = scriptSig, - sequenceNumber = UInt32.zero)), - outputs = Vector(TransactionOutput(CurrencyUnits.oneBTC, spk)), - lockTime = UInt32.zero - ) - } - - def dummyPSBT( - prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, - scriptSig: ScriptSignature = EmptyScriptSignature, - spk: ScriptPubKey = EmptyScriptPubKey): PSBT = { - PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk)) - } - it must "successfully change a NonWitnessUTXO to a WitnessUTXO when compressing" in { // Create non BIP-143 vulnerable witness utxo val dummyData = ECPublicKey.freshPublicKey.bytes diff --git a/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala b/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala index e81d941901..202fb8cef3 100644 --- a/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/wallet/HDWalletApi.scala @@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.transaction.{ TransactionOutPoint, TransactionOutput } +import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.keymanagement.KeyManagerParams import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState} @@ -406,6 +407,8 @@ trait HDWalletApi extends WalletApi { } yield tx } + def signPSBT(psbt: PSBT)(implicit ec: ExecutionContext): Future[PSBT] + def makeOpReturnCommitment( message: String, hashMessage: Boolean, 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 aa69da5d60..ef474e3be4 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala @@ -486,6 +486,7 @@ case class PSBT( MapType <: PSBTMap[RecordType]]( extKey: ExtKey, path: BIP32Path, + pubKey: ECPublicKey, index: Int, keyIdByte: Byte, maps: Vector[MapType], @@ -497,30 +498,23 @@ case class PSBT( require(!isFinalized, "Cannot update a PSBT that is finalized") val previousElements = maps(index).elements - val keyT = extKey.deriveChildPubKey(path) + lazy val expectedBytes = pubKey.bytes.+:(keyIdByte) - keyT match { - case Success(key) => - lazy val expectedBytes = key.bytes.+:(keyIdByte) - - val elements = - if (!previousElements.exists(_.key == expectedBytes)) { - val fp = - if (extKey.fingerprint == ExtKey.masterFingerprint) { - extKey.deriveChildPubKey(path.head).get.fingerprint - } else { - extKey.fingerprint - } - - previousElements :+ makeRecord(key.key, fp, path) + val elements = + if (!previousElements.exists(_.key == expectedBytes)) { + val fp = + if (extKey.fingerprint == ExtKey.masterFingerprint) { + extKey.deriveChildPubKey(path.head).get.fingerprint } else { - previousElements + extKey.fingerprint } - maps.updated(index, makeMap(elements)) - case Failure(err) => - throw err - } + previousElements :+ makeRecord(pubKey, fp, path) + } else { + previousElements + } + + maps.updated(index, makeMap(elements)) } /** @@ -530,10 +524,15 @@ case class PSBT( * @param index index of the InputPSBTMap to add the BIP32Path to * @return PSBT with added BIP32Path */ - def addKeyPathToInput(extKey: ExtKey, path: BIP32Path, index: Int): PSBT = { + def addKeyPathToInput( + extKey: ExtKey, + path: BIP32Path, + pubKey: ECPublicKey, + index: Int): PSBT = { val newInputMaps = addKeyPathToMap[InputPSBTRecord, InputPSBTMap]( extKey = extKey, path = path, + pubKey = pubKey, index = index, keyIdByte = PSBTInputKeyId.BIP32DerivationPathKeyId.byte, maps = inputMaps, @@ -551,10 +550,15 @@ case class PSBT( * @param index index of the OutputPSBTMap to add the BIP32Path to * @return PSBT with added BIP32Path */ - def addKeyPathToOutput(extKey: ExtKey, path: BIP32Path, index: Int): PSBT = { + def addKeyPathToOutput( + extKey: ExtKey, + path: BIP32Path, + pubKey: ECPublicKey, + index: Int): PSBT = { val newOutputMaps = addKeyPathToMap[OutputPSBTRecord, OutputPSBTMap]( extKey = extKey, path = path, + pubKey = pubKey, index = index, keyIdByte = PSBTOutputKeyId.BIP32DerivationPathKeyId.byte, maps = outputMaps, diff --git a/docs/applications/server.md b/docs/applications/server.md index abd685e8d9..18acd6395c 100644 --- a/docs/applications/server.md +++ b/docs/applications/server.md @@ -178,6 +178,8 @@ For more information on how to use our built in `cli` to interact with the serve - `amount` - Amount to send in BTC - `algo` - Coin selection algo - `--feerate ` - Fee rate in sats per virtual byte + - `signpsbt` `psbt` - Signs the PSBT's inputs with keys that are associated with the wallet + - `psbt` - PSBT to sign - `opreturncommit` `message` `[options]` - Creates OP_RETURN commitment transaction - `message` - message to put into OP_RETURN commitment - `--hashMessage` - should the message be hashed before commitment @@ -206,3 +208,19 @@ For more information on how to use our built in `cli` to interact with the serve - `psbt` - PSBT serialized in hex or base64 format - `converttopsbt` `unsignedTx` - Creates an empty psbt from the given transaction - `unsignedTx` - serialized unsigned transaction in hex + + +### Sign PSBT with Wallet Example + +Bitcoin-S CLI: + +```bash +$ bitcoin-s-cli signpsbt cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA +cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA +``` + +CURL: +```bash +$ curl --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "signpsbt", "params": ["cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA"]}' -H "Content-Type: application/json" http://127.0.0.1:9999/ +{"result":"cHNidP8BAP0FAQIAAAABWUWxYiPKgdGfXcIxJ6MRDxEpUecw59Gk4NpROI5oukoBAAAAAAAAAAAEPttkvdwAAAAXqRSOVAp6Qe/u2hq74e/ThB8foBKn7IfZYMgGCAAAAADbmaQ2nwAAAEdRIQLpfVqyaL9Jb/IkveatNyVeONE8Q/6TzXAWosxLo9e21SECc5G3XiK7xKLlkBG7prMx7p0fMeQwMH5e9H10mBon39JSrtgtgjjLAQAAUGMhAn2YaZnv25I6d6vbb1kw6Xp5IToDrEzl/0VBIW21gHrTZwXg5jGdALJ1IQKyNpDNiOiN6lWpYethib04+XC9bpFXrdpec+xO3U5IM2is9ckf5AABAD0CAAAAAALuiOL0rRcAABYAFPnpLByQq1Gg3vwiP6qR8FmOOjwxvVllM08DAAALBfXJH+QAsXUAAK4AAAAAAQcBAAAAAAAA","error":null} +``` diff --git a/testkit/src/main/scala/org/bitcoins/testkit/util/TransactionTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/util/TransactionTestUtil.scala index 803a508cbb..f8f254440e 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/util/TransactionTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/util/TransactionTestUtil.scala @@ -5,8 +5,9 @@ import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits} import org.bitcoins.core.number.{Int32, UInt32} import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.util.BitcoinSLogger -import org.bitcoins.crypto.ECPublicKey +import org.bitcoins.crypto.{DoubleSha256Digest, ECPublicKey} /** * Created by chris on 2/12/16. @@ -249,6 +250,29 @@ trait TransactionTestUtil extends BitcoinSLogger { outputs = Vector(output), lockTime = TransactionConstants.lockTime) } + + def dummyTx( + prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, + scriptSig: ScriptSignature = EmptyScriptSignature, + spk: ScriptPubKey = EmptyScriptPubKey): Transaction = { + BaseTransaction( + version = Int32.zero, + inputs = Vector( + TransactionInput(outPoint = TransactionOutPoint(txId = prevTxId, + vout = UInt32.zero), + scriptSignature = scriptSig, + sequenceNumber = UInt32.zero)), + outputs = Vector(TransactionOutput(CurrencyUnits.oneBTC, spk)), + lockTime = UInt32.zero + ) + } + + def dummyPSBT( + prevTxId: DoubleSha256Digest = DoubleSha256Digest.empty, + scriptSig: ScriptSignature = EmptyScriptSignature, + spk: ScriptPubKey = EmptyScriptPubKey): PSBT = { + PSBT.fromUnsignedTx(dummyTx(prevTxId, scriptSig, spk)) + } } object TransactionTestUtil extends TransactionTestUtil diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index e09b1bd7e1..1ef47a25c6 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -3,15 +3,22 @@ package org.bitcoins.wallet import java.nio.file.Files import org.bitcoins.core.api.wallet.NeutrinoWalletApi.BlockMatchingResponse -import org.bitcoins.core.api.wallet.db.AddressDb +import org.bitcoins.core.api.wallet.db.{AddressDb, TransactionDbHelper} import org.bitcoins.core.hd.HDChainType.{Change, External} -import org.bitcoins.core.hd.{HDAccount, HDChainType} +import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType} import org.bitcoins.core.protocol.BitcoinAddress +import org.bitcoins.core.protocol.script.{ + MultiSignatureScriptPubKey, + P2PKHScriptPubKey, + P2SHScriptPubKey, + P2WPKHWitnessSPKV0 +} import org.bitcoins.core.util.FutureUtil import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError.MnemonicNotFound -import org.bitcoins.crypto.AesPassword +import org.bitcoins.crypto.{AesPassword, ECPublicKey} import org.bitcoins.keymanager.WalletStorage +import org.bitcoins.testkit.util.TransactionTestUtil._ import org.bitcoins.testkit.wallet.BitcoinSWalletTest import org.scalatest.FutureOutcome import org.scalatest.compatible.Assertion @@ -196,4 +203,104 @@ class WalletUnitTest extends BitcoinSWalletTest { } } } + + it must "be able to sign a psbt with a key path" in { wallet: Wallet => + val dummyKey = ECPublicKey.freshPublicKey + + for { + accountDb <- wallet.accountDAO.findAll().map(_.head) + addr <- wallet.getNewAddress(accountDb) + addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) + walletKey = addrDb.ecPublicKey + walletPath = addrDb.path + + spk = MultiSignatureScriptPubKey(2, Vector(dummyKey, walletKey)) + dummyPrevTx = dummyTx(spk = spk) + prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx) + _ <- wallet.transactionDAO.create(prevTxDb) + + psbt = dummyPSBT(prevTxId = dummyPrevTx.txId) + .addKeyPathToInput(accountDb.xpub, walletPath, walletKey, 0) + + signed <- wallet.signPSBT(psbt) + } yield { + assert(signed != psbt) + assert( + signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey)) + } + } + + it must "be able to sign a psbt with our own p2pkh utxo" in { + wallet: Wallet => + for { + addr <- wallet.getNewAddress(AddressType.Legacy) + addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) + walletKey = addrDb.ecPublicKey + + spk = addr.scriptPubKey + _ = assert(spk == P2PKHScriptPubKey(walletKey)) + dummyPrevTx = dummyTx(spk = spk) + _ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None) + + psbt = dummyPSBT(prevTxId = dummyPrevTx.txId) + + signed <- wallet.signPSBT(psbt) + } yield { + assert(signed != psbt) + assert( + signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey)) + } + } + + it must "be able to sign a psbt with our own p2sh segwit utxo" in { + wallet: Wallet => + for { + addr <- wallet.getNewAddress(AddressType.NestedSegWit) + addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) + walletKey = addrDb.ecPublicKey + + spk = addr.scriptPubKey + _ = assert(spk == P2SHScriptPubKey(P2WPKHWitnessSPKV0(walletKey))) + dummyPrevTx = dummyTx(spk = spk) + _ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None) + + psbt = dummyPSBT(prevTxId = dummyPrevTx.txId) + + signed <- wallet.signPSBT(psbt) + } yield { + assert(signed != psbt) + assert( + signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey)) + } + } + + it must "be able to sign a psbt with our own p2wpkh utxo" in { + wallet: Wallet => + for { + addr <- wallet.getNewAddress(AddressType.SegWit) + addrDb <- wallet.addressDAO.findAddress(addr).map(_.get) + walletKey = addrDb.ecPublicKey + + spk = addr.scriptPubKey + _ = assert(spk == P2WPKHWitnessSPKV0(walletKey)) + dummyPrevTx = dummyTx(spk = spk) + _ <- wallet.processTransaction(dummyPrevTx, blockHashOpt = None) + + psbt = dummyPSBT(prevTxId = dummyPrevTx.txId) + .addUTXOToInput(dummyPrevTx, 0) + + signed <- wallet.signPSBT(psbt) + } yield { + assert(signed != psbt) + assert( + signed.inputMaps.head.partialSignatures.exists(_.pubKey == walletKey)) + } + } + + it must "be able to sign a psbt with no wallet utxos" in { wallet: Wallet => + val psbt = dummyPSBT() + for { + signed <- wallet.signPSBT(psbt) + } yield assert(signed == psbt) + } } diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index f38b7caa94..17470edc70 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -13,11 +13,12 @@ import org.bitcoins.core.config.NetworkParameters import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.currency._ import org.bitcoins.core.gcs.{GolombFilter, SimpleFilterMatcher} -import org.bitcoins.core.hd.{HDAccount, HDCoin, HDPurpose, HDPurposes} +import org.bitcoins.core.hd._ import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.blockchain.ChainParams import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.core.psbt.PSBT import org.bitcoins.core.script.constant.ScriptConstant import org.bitcoins.core.script.control.OP_RETURN import org.bitcoins.core.util.{BitcoinScriptUtil, FutureUtil, HDUtil} @@ -612,6 +613,73 @@ abstract class Wallet } yield tx } + override def signPSBT(psbt: PSBT)(implicit + ec: ExecutionContext): Future[PSBT] = { + val inputTxIds = psbt.transaction.inputs.zipWithIndex.map { + case (input, index) => + input.previousOutput.txIdBE -> index + }.toMap + for { + accountDbs <- accountDAO.findAll() + ourXpubs = accountDbs.map(_.xpub) + utxos <- spendingInfoDAO.findAll() + txs <- transactionDAO.findByTxIds(inputTxIds.keys.toVector) + + updated = txs.foldLeft(psbt) { (accum, tx) => + val index = inputTxIds(tx.txIdBE) + accum.addUTXOToInput(tx.transaction, index) + } + + signed <- + FutureUtil.foldLeftAsync(updated, updated.inputMaps.zipWithIndex) { + case (unsigned, (input, index)) => + val xpubKeyPaths = input.BIP32DerivationPaths + .filter { path => + ourXpubs.exists(_.fingerprint == path.masterFingerprint) + } + .map(bip32Path => + HDPath.fromString( + bip32Path.path.toString + )) // TODO add a way to get a HDPath from a BIP32 Path + + val (utxoPath, withData) = { + val outPoint = unsigned.transaction.inputs(index).previousOutput + utxos.find(_.outpoint == outPoint) match { + case Some(utxo) => + val psbtWithUtxoData = utxo.redeemScript match { + case Some(redeemScript) => + unsigned.addRedeemOrWitnessScriptToInput(redeemScript, + index) + case None => unsigned + } + + (Vector(utxo.path), psbtWithUtxoData) + case None => (Vector.empty, unsigned) + } + } + + val keyPaths = xpubKeyPaths ++ utxoPath + + FutureUtil.foldLeftAsync(withData, keyPaths) { (accum, hdPath) => + val sign = keyManager.toSign(hdPath) + // Only sign if that key doesn't have a signature yet + if (!input.partialSignatures.exists(_.pubKey == sign.publicKey)) { + logger.debug( + s"Signing input $index with key ${sign.publicKey.hex}") + accum.sign(index, sign) + } else { + Future.successful(accum) + } + } + } + } yield { + if (updated == signed) { + logger.warn("Did not find any keys or utxos that belong to this wallet") + } + signed + } + } + protected def getLastAccountOpt( purpose: HDPurpose): Future[Option[AccountDb]] = { accountDAO diff --git a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala index fe9ef3cd2c..21b66be39f 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/models/TransactionDAO.scala @@ -58,6 +58,13 @@ trait TxDAO[DbEntryType <: TxDB] findByTxId(outPoint.txId) } + def findByTxIds( + txIdBEs: Vector[DoubleSha256DigestBE]): Future[Vector[DbEntryType]] = { + val q = table.filter(_.txIdBE.inSet(txIdBEs)) + + safeDatabase.runVec(q.result.transactionally) + } + def findByTxId(txIdBE: DoubleSha256DigestBE): Future[Option[DbEntryType]] = { val q = table .filter(_.txIdBE === txIdBE)