From 914c905bd7c7a06dc86b9c8e86777475cad7c617 Mon Sep 17 00:00:00 2001 From: Nadav Kohen Date: Wed, 17 Jun 2020 14:34:01 -0500 Subject: [PATCH] Lots of misc. improvements from dlc branch (#1583) --- .../bitcoins/rpc/config/BitcoindConfig.scala | 11 +- build.sbt | 2 +- .../core/config/NetworkParametersTest.scala | 10 +- .../protocol/script/ScriptPubKeyTest.scala | 9 +- .../script/WitnessScriptPubKeySpec.scala | 2 +- .../core/wallet/builder/RawTxSignerTest.scala | 101 +------------ .../core/config/NetworkParameters.scala | 43 ++++-- .../org/bitcoins/core/currency/package.scala | 15 ++ .../org/bitcoins/core/hd/HDAccount.scala | 12 ++ .../scala/org/bitcoins/core/hd/HDCoin.scala | 15 ++ .../org/bitcoins/core/hd/HDCoinType.scala | 7 + .../org/bitcoins/core/hd/HDPurpose.scala | 6 + .../org/bitcoins/core/protocol/Address.scala | 56 +++---- .../core/protocol/script/ScriptPubKey.scala | 141 +++++++++++------- .../protocol/script/ScriptSignature.scala | 2 +- .../scala/org/bitcoins/core/psbt/PSBT.scala | 85 ++++++++--- .../core/util/BitcoinScriptUtil.scala | 107 ++++++++++--- .../core/wallet/builder/RawTxBuilder.scala | 7 +- .../core/wallet/builder/RawTxFinalizer.scala | 68 ++++++++- .../core/wallet/utxo/InputSigningInfo.scala | 4 + .../bitcoins/db/DbCommonsColumnMappers.scala | 85 ++++++++++- .../testkit/rpc/BitcoindRpcTestUtil.scala | 25 +++- .../scala/org/bitcoins/wallet/Wallet.scala | 2 +- .../internal/FundTransactionHandling.scala | 5 +- .../wallet/internal/UtxoHandling.scala | 8 + 25 files changed, 558 insertions(+), 270 deletions(-) diff --git a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindConfig.scala b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindConfig.scala index 478f76156b..e8c8a14287 100644 --- a/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindConfig.scala +++ b/bitcoind-rpc/src/main/scala/org/bitcoins/rpc/config/BitcoindConfig.scala @@ -55,7 +55,7 @@ case class BitcoindConfig( /** The optional index of the first header section encountered */ private lazy val firstHeaderSectionIndex: Option[Int] = { val indices = - List(RegTest, TestNet3, MainNet).map(headerSectionIndex).flatten + List(RegTest, TestNet3, MainNet).flatMap(headerSectionIndex) if (indices.nonEmpty) Some(indices.min) else None } @@ -111,13 +111,13 @@ case class BitcoindConfig( String] = collectFrom(lines)(_) /** The blockchain network associated with this `bitcoind` config */ - lazy val network = { + lazy val network: NetworkParameters = { val networkStrOpt = collectAllLines { case (network @ ("testnet" | "regtest" | "mainnet"), "1") => network }.lastOption - val networkOpt = networkStrOpt.flatMap(Networks.fromString) + val networkOpt = networkStrOpt.flatMap(Networks.fromStringOpt) (networkOpt, networkStrOpt) match { case (None, Some(badStr)) => @@ -323,7 +323,7 @@ object BitcoindConfig extends BitcoinSLogger { * default configuration is returned. */ def fromDefaultDatadir: BitcoindConfig = { - if (DEFAULT_CONF_FILE.isFile()) { + if (DEFAULT_CONF_FILE.isFile) { apply(DEFAULT_CONF_FILE) } else { BitcoindConfig.empty @@ -353,8 +353,7 @@ object BitcoindConfig extends BitcoinSLogger { } /** Default location of bitcoind conf file */ - val DEFAULT_CONF_FILE: File = DEFAULT_DATADIR - .toPath() + val DEFAULT_CONF_FILE: File = DEFAULT_DATADIR.toPath .resolve("bitcoin.conf") .toFile diff --git a/build.sbt b/build.sbt index 58cfc6b3e0..e3aea535a0 100644 --- a/build.sbt +++ b/build.sbt @@ -314,7 +314,7 @@ lazy val dbCommons = project name := "bitcoin-s-db-commons", libraryDependencies ++= Deps.dbCommons ) - .dependsOn(core) + .dependsOn(core, appCommons) lazy val dbCommonsTest = project .in(file("db-commons-test")) diff --git a/core-test/src/test/scala/org/bitcoins/core/config/NetworkParametersTest.scala b/core-test/src/test/scala/org/bitcoins/core/config/NetworkParametersTest.scala index 146d63f7e3..df29e2d149 100644 --- a/core-test/src/test/scala/org/bitcoins/core/config/NetworkParametersTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/config/NetworkParametersTest.scala @@ -23,10 +23,10 @@ class NetworkParametersTest extends BitcoinSUnitTest { } it must "get the correct Network from string" in { - assert(Networks.fromString("mainnet").contains(MainNet)) - assert(Networks.fromString("testnet").contains(TestNet3)) - assert(Networks.fromString("regtest").contains(RegTest)) - assert(Networks.fromString("").isEmpty) - assert(Networks.fromString("craig wright is a fraud").isEmpty) + assert(Networks.fromString("mainnet") == MainNet) + assert(Networks.fromString("testnet") == TestNet3) + assert(Networks.fromString("regtest") == RegTest) + assert(Networks.fromStringOpt("").isEmpty) + assert(Networks.fromStringOpt("craig wright is a fraud").isEmpty) } } diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala index c7ab12370f..741942a031 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/ScriptPubKeyTest.scala @@ -22,8 +22,8 @@ class ScriptPubKeyTest extends BitcoinSUnitTest { OP_EQUALVERIFY, OP_CHECKSIG) //from b30d3148927f620f5b1228ba941c211fdabdae75d0ba0b688a58accbf018f3cc - val rawScriptPubKey = TestUtil.rawP2PKHScriptPubKey - val scriptPubKey = ScriptPubKey(rawScriptPubKey) + val rawScriptPubKey: String = TestUtil.rawP2PKHScriptPubKey + val scriptPubKey: ScriptPubKey = ScriptPubKey(rawScriptPubKey) "ScriptPubKey" must "give the expected asm from creating a scriptPubKey from hex" in { scriptPubKey.asm must be(expectedAsm) @@ -35,9 +35,8 @@ class ScriptPubKeyTest extends BitcoinSUnitTest { val witnessProgram = Seq(ScriptConstant(pubKeyHash.bytes)) val asm = OP_0 +: BytesToPushOntoStack(20) +: witnessProgram val witnessScriptPubKey = WitnessScriptPubKey(asm) - witnessScriptPubKey.isDefined must be(true) - witnessScriptPubKey.get.witnessVersion must be(WitnessVersion0) - witnessScriptPubKey.get.witnessProgram must be(witnessProgram) + witnessScriptPubKey.witnessVersion must be(WitnessVersion0) + witnessScriptPubKey.witnessProgram must be(witnessProgram) } it must "determine the correct descriptors" in { diff --git a/core-test/src/test/scala/org/bitcoins/core/protocol/script/WitnessScriptPubKeySpec.scala b/core-test/src/test/scala/org/bitcoins/core/protocol/script/WitnessScriptPubKeySpec.scala index 547b666388..935bc14732 100644 --- a/core-test/src/test/scala/org/bitcoins/core/protocol/script/WitnessScriptPubKeySpec.scala +++ b/core-test/src/test/scala/org/bitcoins/core/protocol/script/WitnessScriptPubKeySpec.scala @@ -45,7 +45,7 @@ class WitnessScriptPubKeySpec extends Properties("WitnessScriptPubKeySpec") { property("witnessScriptPubKey fromAsm symmetry") = { Prop.forAll(ScriptGenerators.witnessScriptPubKey) { case (witScriptPubKey, _) => - WitnessScriptPubKey(witScriptPubKey.asm).get == witScriptPubKey + WitnessScriptPubKey(witScriptPubKey.asm) == witScriptPubKey } } } diff --git a/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxSignerTest.scala b/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxSignerTest.scala index 54f1050a3a..afcef70acf 100644 --- a/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxSignerTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxSignerTest.scala @@ -1,49 +1,16 @@ package org.bitcoins.core.wallet.builder -import org.bitcoins.core.crypto.{ - BaseTxSigComponent, - WitnessTxSigComponentP2SH, - WitnessTxSigComponentRaw -} import org.bitcoins.core.currency.{Bitcoins, CurrencyUnits, Satoshis} import org.bitcoins.core.number.UInt32 -import org.bitcoins.core.policy.Policy -import org.bitcoins.core.protocol.script.{ - CLTVScriptPubKey, - CSVScriptPubKey, - ConditionalScriptPubKey, - EmptyScriptPubKey, - MultiSignatureScriptPubKey, - NonStandardScriptPubKey, - P2PKHScriptPubKey, - P2PKScriptPubKey, - P2PKWithTimeoutScriptPubKey, - P2SHScriptPubKey, - P2SHScriptSignature, - P2WSHWitnessV0, - UnassignedWitnessScriptPubKey, - WitnessCommitment, - WitnessScriptPubKey, - WitnessScriptPubKeyV0 -} -import org.bitcoins.core.protocol.transaction.{ - BaseTransaction, - Transaction, - TransactionConstants, - TransactionInput, - TransactionOutPoint, - TransactionOutput, - WitnessTransaction -} -import org.bitcoins.core.script.PreExecutionScriptProgram +import org.bitcoins.core.protocol.script._ +import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.script.constant.ScriptNumber import org.bitcoins.core.script.crypto.HashType -import org.bitcoins.core.script.interpreter.ScriptInterpreter +import org.bitcoins.core.util.BitcoinScriptUtil import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte} import org.bitcoins.core.wallet.utxo.{ ConditionalPath, InputInfo, - InputSigningInfo, LockTimeInputInfo, ScriptSignatureParams } @@ -297,64 +264,6 @@ class RawTxSignerTest extends BitcoinSAsyncTest { ) } - def verifyScript( - tx: Transaction, - utxos: Vector[InputSigningInfo[InputInfo]]): Boolean = { - val programs: Vector[PreExecutionScriptProgram] = - tx.inputs.zipWithIndex.toVector.map { - case (input: TransactionInput, idx: Int) => - val outpoint = input.previousOutput - - val creditingTx = - utxos.find(u => u.outPoint.txId == outpoint.txId).get - - val output = creditingTx.output - - val spk = output.scriptPubKey - - val amount = output.value - - val txSigComponent = spk match { - case witSPK: WitnessScriptPubKeyV0 => - val o = TransactionOutput(amount, witSPK) - WitnessTxSigComponentRaw(tx.asInstanceOf[WitnessTransaction], - UInt32(idx), - o, - Policy.standardFlags) - case _: UnassignedWitnessScriptPubKey => ??? - case x @ (_: P2PKScriptPubKey | _: P2PKHScriptPubKey | - _: P2PKWithTimeoutScriptPubKey | _: MultiSignatureScriptPubKey | - _: WitnessCommitment | _: CSVScriptPubKey | - _: CLTVScriptPubKey | _: ConditionalScriptPubKey | - _: NonStandardScriptPubKey | EmptyScriptPubKey) => - val o = TransactionOutput(CurrencyUnits.zero, x) - BaseTxSigComponent(tx, UInt32(idx), o, Policy.standardFlags) - - case _: P2SHScriptPubKey => - val p2shScriptSig = - tx.inputs(idx).scriptSignature.asInstanceOf[P2SHScriptSignature] - p2shScriptSig.redeemScript match { - - case _: WitnessScriptPubKey => - WitnessTxSigComponentP2SH( - transaction = tx.asInstanceOf[WitnessTransaction], - inputIndex = UInt32(idx), - output = output, - flags = Policy.standardFlags) - - case _ => - BaseTxSigComponent(tx, - UInt32(idx), - output, - Policy.standardFlags) - } - } - - PreExecutionScriptProgram(txSigComponent) - } - ScriptInterpreter.runAllVerify(programs) - } - it should "sign a mix of spks in a tx and then have it verified" in { forAllAsync(CreditingTxGen.inputsAndOutputs(), ScriptGenerators.scriptPubKey) { @@ -369,7 +278,7 @@ class RawTxSignerTest extends BitcoinSAsyncTest { RawTxSigner.sign(utx, creditingTxsInfo.toVector, fee)) txF.map { tx => - assert(verifyScript(tx, creditingTxsInfo.toVector)) + assert(BitcoinScriptUtil.verifyScript(tx, creditingTxsInfo.toVector)) } } } @@ -388,7 +297,7 @@ class RawTxSignerTest extends BitcoinSAsyncTest { RawTxSigner.sign(utx, creditingTxsInfo.toVector, fee)) txF.map { tx => - assert(verifyScript(tx, creditingTxsInfo.toVector)) + assert(BitcoinScriptUtil.verifyScript(tx, creditingTxsInfo.toVector)) } } } diff --git a/core/src/main/scala/org/bitcoins/core/config/NetworkParameters.scala b/core/src/main/scala/org/bitcoins/core/config/NetworkParameters.scala index ec4db8c710..1705864460 100644 --- a/core/src/main/scala/org/bitcoins/core/config/NetworkParameters.scala +++ b/core/src/main/scala/org/bitcoins/core/config/NetworkParameters.scala @@ -1,6 +1,7 @@ package org.bitcoins.core.config import org.bitcoins.core.protocol.blockchain._ +import org.bitcoins.crypto.StringFactory import scodec.bits.ByteVector sealed abstract class NetworkParameters { @@ -73,8 +74,8 @@ sealed abstract class MainNet extends BitcoinNetwork { /** * @inheritdoc */ - override def dnsSeeds = { - List( + override def dnsSeeds: Vector[String] = { + Vector( "seed.bitcoin.sipa.be", "dnsseed.bluematt.me", "dnsseed.bitcoin.dashjr.org", @@ -89,7 +90,7 @@ sealed abstract class MainNet extends BitcoinNetwork { /** * @inheritdoc */ - override def magicBytes = ByteVector(0xf9, 0xbe, 0xb4, 0xd9) + override def magicBytes: ByteVector = ByteVector(0xf9, 0xbe, 0xb4, 0xd9) } @@ -119,7 +120,7 @@ sealed abstract class TestNet3 extends BitcoinNetwork { /* * @inheritdoc */ - override def magicBytes = ByteVector(0x0b, 0x11, 0x09, 0x07) + override def magicBytes: ByteVector = ByteVector(0x0b, 0x11, 0x09, 0x07) } @@ -146,24 +147,44 @@ sealed abstract class RegTest extends BitcoinNetwork { /** * @inheritdoc */ - override def magicBytes = ByteVector(0xfa, 0xbf, 0xb5, 0xda) + override def magicBytes: ByteVector = ByteVector(0xfa, 0xbf, 0xb5, 0xda) } final case object RegTest extends RegTest // $COVERAGE-ON$ -object Networks { +object Networks extends StringFactory[NetworkParameters] { + val knownNetworks: Seq[NetworkParameters] = BitcoinNetworks.knownNetworks + val secretKeyBytes: Seq[ByteVector] = BitcoinNetworks.secretKeyBytes + val p2pkhNetworkBytes: Seq[ByteVector] = BitcoinNetworks.p2pkhNetworkBytes + val p2shNetworkBytes: Seq[ByteVector] = BitcoinNetworks.p2shNetworkBytes + + def fromString(string: String): NetworkParameters = + BitcoinNetworks.fromString(string) + + def magicToNetwork: Map[ByteVector, NetworkParameters] = + BitcoinNetworks.magicToNetwork + + def bytesToNetwork: Map[ByteVector, NetworkParameters] = + BitcoinNetworks.bytesToNetwork +} + +object BitcoinNetworks extends StringFactory[BitcoinNetwork] { val knownNetworks: Seq[NetworkParameters] = Seq(MainNet, TestNet3, RegTest) val secretKeyBytes: Seq[ByteVector] = knownNetworks.map(_.privateKey) val p2pkhNetworkBytes: Seq[ByteVector] = knownNetworks.map(_.p2pkhNetworkByte) val p2shNetworkBytes: Seq[ByteVector] = knownNetworks.map(_.p2shNetworkByte) /** Uses the notation used in `bitcoin.conf` */ - def fromString(string: String): Option[NetworkParameters] = string match { - case "mainnet" => Some(MainNet) - case "testnet" => Some(TestNet3) - case "regtest" => Some(RegTest) - case _: String => None + override def fromString(string: String): BitcoinNetwork = string match { + case "mainnet" => MainNet + case "main" => MainNet + case "testnet3" => TestNet3 + case "testnet" => TestNet3 + case "test" => TestNet3 + case "regtest" => RegTest + case _: String => + throw new IllegalArgumentException(s"Invalid network $string") } /** Map of magic network bytes to the corresponding network */ diff --git a/core/src/main/scala/org/bitcoins/core/currency/package.scala b/core/src/main/scala/org/bitcoins/core/currency/package.scala index ae41ad99ff..6279908717 100644 --- a/core/src/main/scala/org/bitcoins/core/currency/package.scala +++ b/core/src/main/scala/org/bitcoins/core/currency/package.scala @@ -12,30 +12,40 @@ package object currency { /** Provides natural language syntax for bitcoins */ implicit class BitcoinsInt(private val i: Int) extends AnyVal { def bitcoins: Bitcoins = Bitcoins(i) + def bitcoin: Bitcoins = bitcoins + def BTC: Bitcoins = bitcoins } /** Provides natural language syntax for bitcoins */ implicit class BitcoinsLong(private val i: Long) extends AnyVal { def bitcoins: Bitcoins = Bitcoins(i) + def bitcoin: Bitcoins = bitcoins + def BTC: Bitcoins = bitcoins } /** Provides natural language syntax for satoshis */ implicit class SatoshisInt(private val i: Int) extends AnyVal { def satoshis: Satoshis = Satoshis(i) + def satoshi: Satoshis = satoshis + def sats: Satoshis = satoshis + def sat: Satoshis = satoshis } /** Provides natural language syntax for satoshis */ implicit class SatoshisLong(private val i: Long) extends AnyVal { def satoshis: Satoshis = Satoshis(i) + def satoshi: Satoshis = satoshis + def sats: Satoshis = satoshis + def sat: Satoshis = satoshis } @@ -80,4 +90,9 @@ package object currency { } } } + + implicit val satoshisOrdering: Ordering[Satoshis] = + new Ordering[Satoshis] { + override def compare(x: Satoshis, y: Satoshis): Int = x.compare(y) + } } diff --git a/core/src/main/scala/org/bitcoins/core/hd/HDAccount.scala b/core/src/main/scala/org/bitcoins/core/hd/HDAccount.scala index ec4c3689f2..d29bdf3b70 100644 --- a/core/src/main/scala/org/bitcoins/core/hd/HDAccount.scala +++ b/core/src/main/scala/org/bitcoins/core/hd/HDAccount.scala @@ -28,6 +28,18 @@ case class HDAccount( object HDAccount { + def fromPath(path: BIP32Path): Option[HDAccount] = { + + HDCoin.fromPath(BIP32Path(path.path.init)).flatMap { coin => + val lastNode = path.path.last + if (lastNode.hardened) { + Some(HDAccount(coin, lastNode.index)) + } else { + None + } + } + } + /** This method is meant to take in an arbitrary bip32 path and see * if it has the same account as the given account * diff --git a/core/src/main/scala/org/bitcoins/core/hd/HDCoin.scala b/core/src/main/scala/org/bitcoins/core/hd/HDCoin.scala index 78caee8535..81e90d57bf 100644 --- a/core/src/main/scala/org/bitcoins/core/hd/HDCoin.scala +++ b/core/src/main/scala/org/bitcoins/core/hd/HDCoin.scala @@ -6,3 +6,18 @@ case class HDCoin(purpose: HDPurpose, coinType: HDCoinType) extends BIP32Path { def toAccount(index: Int): HDAccount = HDAccount(this, index) } + +object HDCoin { + + def fromPath(path: BIP32Path): Option[HDCoin] = { + if (path.path.length == 2) { + HDPurposes.fromNode(path.path.head).map { purpose => + val coinType = HDCoinType.fromInt(path.path.last.index) + + HDCoin(purpose, coinType) + } + } else { + None + } + } +} diff --git a/core/src/main/scala/org/bitcoins/core/hd/HDCoinType.scala b/core/src/main/scala/org/bitcoins/core/hd/HDCoinType.scala index 9395a305e9..38489014ad 100644 --- a/core/src/main/scala/org/bitcoins/core/hd/HDCoinType.scala +++ b/core/src/main/scala/org/bitcoins/core/hd/HDCoinType.scala @@ -42,4 +42,11 @@ object HDCoinType { case TestNet3 | RegTest => Testnet } } + + def fromNode(node: BIP32Node): HDCoinType = { + require(node.hardened, + s"Cannot construct HDCoinType from un-hardened node: $node") + + fromInt(node.index) + } } diff --git a/core/src/main/scala/org/bitcoins/core/hd/HDPurpose.scala b/core/src/main/scala/org/bitcoins/core/hd/HDPurpose.scala index c8695a0fb9..b78fb55197 100644 --- a/core/src/main/scala/org/bitcoins/core/hd/HDPurpose.scala +++ b/core/src/main/scala/org/bitcoins/core/hd/HDPurpose.scala @@ -28,4 +28,10 @@ object HDPurposes { /** Tries to turn the provided integer into a HD purpose path segment */ def fromConstant(i: Int): Option[HDPurpose] = all.find(_.constant == i) + + def fromNode(node: BIP32Node): Option[HDPurpose] = { + require(node.hardened, + s"Cannot construct HDPurpose from un-hardened node: $node") + fromConstant(node.index) + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala index e5f0c3f3f2..e294898369 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/Address.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/Address.scala @@ -36,7 +36,7 @@ sealed abstract class Address { /** The [[org.bitcoins.core.protocol.script.ScriptPubKey ScriptPubKey]] the address represents */ def scriptPubKey: ScriptPubKey - override def toString = value + override def toString: String = value } sealed abstract class BitcoinAddress extends Address @@ -67,7 +67,7 @@ sealed abstract class P2SHAddress extends BitcoinAddress { Base58.encode(bytes ++ checksum) } - override def scriptPubKey = P2SHScriptPubKey(hash) + override def scriptPubKey: P2SHScriptPubKey = P2SHScriptPubKey(hash) override def hash: Sha256Hash160Digest } @@ -122,6 +122,9 @@ object Bech32Address extends AddressFactory[Bech32Address] { //require(verifyChecksum(hrp, data), "checksum did not pass") } + def empty(network: NetworkParameters = MainNet): Bech32Address = + fromScriptPubKey(P2WSHWitnessSPKV0(EmptyScriptPubKey), network) + def apply( witSPK: WitnessScriptPubKey, networkParameters: NetworkParameters): Bech32Address = { @@ -153,31 +156,30 @@ object Bech32Address extends AddressFactory[Bech32Address] { * [[org.bitcoins.core.protocol.script.WitnessScriptPubKey WitnessScriptPubKey]] */ def fromStringToWitSPK(string: String): Try[WitnessScriptPubKey] = { val decoded = fromStringT(string) - decoded.flatMap { - case bech32Addr => - val bytes = bech32Addr.data - val (v, _) = (bytes.head, bytes.tail) - val convertedProg = NumberUtil.convertUInt5sToUInt8(bytes.tail) - val progBytes = UInt8.toBytes(convertedProg) - val witVersion = WitnessVersion(v.toInt) - val pushOp = BitcoinScriptUtil.calculatePushOp(progBytes) - witVersion match { - case Some(v) => - val witSPK = WitnessScriptPubKey( - List(v.version) ++ pushOp ++ List(ScriptConstant(progBytes))) - witSPK match { - case Some(spk) => Success(spk) - case None => - Failure( - new IllegalArgumentException( - "Failed to decode bech32 into a witSPK")) - } - case None => - Failure( - new IllegalArgumentException( - "Witness version was not valid, got: " + v)) - } - + decoded.flatMap { bech32Addr => + val bytes = bech32Addr.data + val (v, _) = (bytes.head, bytes.tail) + val convertedProg = NumberUtil.convertUInt5sToUInt8(bytes.tail) + val progBytes = UInt8.toBytes(convertedProg) + val witVersion = WitnessVersion(v.toInt) + val pushOp = BitcoinScriptUtil.calculatePushOp(progBytes) + witVersion match { + case Some(v) => + val witSPK = Try( + WitnessScriptPubKey( + List(v.version) ++ pushOp ++ List(ScriptConstant(progBytes)))) + witSPK match { + case Success(spk) => Success(spk) + case Failure(err) => + Failure( + new IllegalArgumentException( + "Failed to decode bech32 into a witSPK: " + err.getMessage)) + } + case None => + Failure( + new IllegalArgumentException( + "Witness version was not valid, got: " + v)) + } } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala index f4c15d7b18..c159019ef6 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptPubKey.scala @@ -80,8 +80,8 @@ object P2PKHScriptPubKey extends ScriptFactory[P2PKHScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): P2PKHScriptPubKey = { buildScript(asm.toVector, - P2PKHScriptPubKeyImpl(_), - isP2PKHScriptPubKey(_), + P2PKHScriptPubKeyImpl.apply, + isP2PKHScriptPubKey, "Given asm was not a p2pkh scriptPubKey, got: " + asm) } @@ -118,8 +118,7 @@ sealed trait MultiSignatureScriptPubKey extends RawScriptPubKey { asmWithoutPushOps.indexOf(OP_CHECKMULTISIG) else asmWithoutPushOps.indexOf(OP_CHECKMULTISIGVERIFY) //magic number 2 represents the maxSig operation and the OP_CHECKMULTISIG operation at the end of the asm - val numSigsRequired = asmWithoutPushOps( - opCheckMultiSigIndex - maxSigs.toInt - 2) + val numSigsRequired = asmWithoutPushOps(opCheckMultiSigIndex - maxSigs - 2) numSigsRequired match { case x: ScriptNumber => x.toInt case c: ScriptConstant @@ -216,8 +215,8 @@ object MultiSignatureScriptPubKey def fromAsm(asm: Seq[ScriptToken]): MultiSignatureScriptPubKey = { buildScript(asm.toVector, - MultiSignatureScriptPubKeyImpl(_), - isMultiSignatureScriptPubKey(_), + MultiSignatureScriptPubKeyImpl.apply, + isMultiSignatureScriptPubKey, "Given asm was not a MultSignatureScriptPubKey, got: " + asm) } @@ -225,11 +224,10 @@ object MultiSignatureScriptPubKey /** Determines if the given script tokens are a multisignature `scriptPubKey` */ def isMultiSignatureScriptPubKey(asm: Seq[ScriptToken]): Boolean = { - val isNotEmpty = asm.size > 0 val containsMultiSigOp = asm.contains(OP_CHECKMULTISIG) || asm.contains( OP_CHECKMULTISIGVERIFY) - if (isNotEmpty && containsMultiSigOp) { + if (asm.nonEmpty && containsMultiSigOp) { //we need either the first or second asm operation to indicate how many signatures are required val hasRequiredSignaturesTry = Try { asm.headOption match { @@ -258,7 +256,7 @@ object MultiSignatureScriptPubKey .isInstanceOf[ScriptNumber] || op == OP_CHECKMULTISIG || op == OP_CHECKMULTISIGVERIFY) - val result = isNotEmpty && containsMultiSigOp && hasRequiredSignatures && + val result = asm.nonEmpty && containsMultiSigOp && hasRequiredSignatures && hasMaximumSignatures && isStandardOps result case (Success(_), Failure(_)) => false @@ -330,8 +328,8 @@ object P2SHScriptPubKey extends ScriptFactory[P2SHScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): P2SHScriptPubKey = { buildScript(asm.toVector, - P2SHScriptPubKeyImpl(_), - isP2SHScriptPubKey(_), + P2SHScriptPubKeyImpl.apply, + isP2SHScriptPubKey, "Given asm was not a p2sh scriptPubkey, got: " + asm) } @@ -366,8 +364,8 @@ object P2PKScriptPubKey extends ScriptFactory[P2PKScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): P2PKScriptPubKey = { buildScript(asm.toVector, - P2PKScriptPubKeyImpl(_), - isP2PKScriptPubKey(_), + P2PKScriptPubKeyImpl.apply, + isP2PKScriptPubKey, "Given asm was not a p2pk scriptPubKey, got: " + asm) } @@ -386,10 +384,9 @@ sealed trait LockTimeScriptPubKey extends RawScriptPubKey { /** Determines the nested `ScriptPubKey` inside the `LockTimeScriptPubKey` */ def nestedScriptPubKey: RawScriptPubKey = { - val bool: Boolean = asm.head.isInstanceOf[ScriptNumberOperation] - bool match { - case true => RawScriptPubKey(asm.slice(3, asm.length)) - case false => RawScriptPubKey(asm.slice(4, asm.length)) + asm.head match { + case _: ScriptNumberOperation => RawScriptPubKey(asm.slice(3, asm.length)) + case _: ScriptToken => RawScriptPubKey(asm.slice(4, asm.length)) } } @@ -442,8 +439,8 @@ object CLTVScriptPubKey extends ScriptFactory[CLTVScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): CLTVScriptPubKey = { buildScript(asm.toVector, - CLTVScriptPubKeyImpl(_), - isCLTVScriptPubKey(_), + CLTVScriptPubKeyImpl.apply, + isCLTVScriptPubKey, "Given asm was not a CLTVScriptPubKey, got: " + asm) } @@ -530,8 +527,8 @@ object CSVScriptPubKey extends ScriptFactory[CSVScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): CSVScriptPubKey = { buildScript(asm.toVector, - CSVScriptPubKeyImpl(_), - isCSVScriptPubKey(_), + CSVScriptPubKeyImpl.apply, + isCSVScriptPubKey, "Given asm was not a CSVScriptPubKey, got: " + asm) } @@ -887,10 +884,25 @@ sealed trait P2PKWithTimeoutScriptPubKey extends RawScriptPubKey { lazy val pubKey: ECPublicKey = ECPublicKey.fromBytes(asm(2).bytes) - lazy val lockTime: ScriptNumber = ScriptNumber.fromBytes(asm(5).bytes) + private lazy val smallCSVOpt: Option[Long] = { + asm(4) match { + case num: ScriptNumberOperation => Some(num.toLong) + case _: ScriptToken => None + } + } - lazy val timeoutPubKey: ECPublicKey = - ECPublicKey.fromBytes(asm(9).bytes) + lazy val lockTime: ScriptNumber = { + smallCSVOpt + .map(ScriptNumber.apply) + .getOrElse(ScriptNumber(asm(5).bytes)) + } + + lazy val timeoutPubKey: ECPublicKey = { + smallCSVOpt match { + case Some(_) => ECPublicKey.fromBytes(asm(8).bytes) + case None => ECPublicKey.fromBytes(asm(9).bytes) + } + } } object P2PKWithTimeoutScriptPubKey @@ -930,18 +942,35 @@ object P2PKWithTimeoutScriptPubKey } def isP2PKWithTimeoutScriptPubKey(asm: Seq[ScriptToken]): Boolean = { - if (asm.length == 12) { - val pubKey = ECPublicKey.fromBytes(asm(2).bytes) - val lockTimeTry = Try(ScriptNumber.fromBytes(asm(5).bytes)) - val timeoutPubKey = ECPublicKey.fromBytes(asm(9).bytes) - - lockTimeTry match { - case Success(lockTime) => - asm == P2PKWithTimeoutScriptPubKey(pubKey, lockTime, timeoutPubKey).asm - case Failure(_) => false - } - } else { + if (asm.length < 5) { false + } else { + val (smallCSVOpt, requiredSize) = asm(4) match { + case num: ScriptNumberOperation => (Some(num.toLong), 11) + case _: ScriptToken => (None, 12) + } + + if (asm.length == requiredSize) { + val pubKey = ECPublicKey.fromBytes(asm(2).bytes) + + val lockTimeTry = smallCSVOpt match { + case Some(num) => Success(ScriptNumber(num)) + case None => Try(ScriptNumber.fromBytes(asm(5).bytes)) + } + + val timeoutPubKey = smallCSVOpt match { + case Some(_) => ECPublicKey.fromBytes(asm(8).bytes) + case None => ECPublicKey.fromBytes(asm(9).bytes) + } + + lockTimeTry match { + case Success(lockTime) => + asm == P2PKWithTimeoutScriptPubKey(pubKey, lockTime, timeoutPubKey).asm + case Failure(_) => false + } + } else { + false + } } } } @@ -958,7 +987,7 @@ object NonStandardScriptPubKey extends ScriptFactory[NonStandardScriptPubKey] { def fromAsm(asm: Seq[ScriptToken]): NonStandardScriptPubKey = { //everything can be a NonStandardScriptPubkey, thus the trivially true function - buildScript(asm.toVector, NonStandardScriptPubKeyImpl(_), { _ => + buildScript(asm.toVector, NonStandardScriptPubKeyImpl.apply, { _ => true }, "") } @@ -1030,7 +1059,7 @@ object ScriptPubKey extends ScriptFactory[ScriptPubKey] { if (nonWitnessScriptPubKey .isInstanceOf[NonStandardScriptPubKey] && WitnessScriptPubKey .isWitnessScriptPubKey(asm)) { - WitnessScriptPubKey(asm).get + WitnessScriptPubKey(asm) } else { nonWitnessScriptPubKey } @@ -1044,10 +1073,10 @@ object ScriptPubKey extends ScriptFactory[ScriptPubKey] { * [[org.bitcoins.core.protocol.script.ScriptWitness ScriptWitness]] */ sealed trait WitnessScriptPubKey extends ScriptPubKey { def witnessProgram: Seq[ScriptToken] - def witnessVersion = WitnessVersion(asm.head) + def witnessVersion: WitnessVersion = WitnessVersion(asm.head) } -object WitnessScriptPubKey { +object WitnessScriptPubKey extends ScriptFactory[WitnessScriptPubKey] { /** Witness scripts must begin with one of these operations, see * [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki BIP141]] */ @@ -1069,18 +1098,20 @@ object WitnessScriptPubKey { OP_15, OP_16) - val unassignedWitVersions = validWitVersions.tail + val unassignedWitVersions: Seq[ScriptNumberOperation] = validWitVersions.tail - def apply(asm: Seq[ScriptToken]): Option[WitnessScriptPubKey] = fromAsm(asm) + def apply(asm: Seq[ScriptToken]): WitnessScriptPubKey = fromAsm(asm) - def fromAsm(asm: Seq[ScriptToken]): Option[WitnessScriptPubKey] = asm match { + def fromAsm(asm: Seq[ScriptToken]): WitnessScriptPubKey = asm match { case _ if P2WPKHWitnessSPKV0.isValid(asm) => - Some(P2WPKHWitnessSPKV0.fromAsm(asm)) + P2WPKHWitnessSPKV0.fromAsm(asm) case _ if P2WSHWitnessSPKV0.isValid(asm) => - Some(P2WSHWitnessSPKV0.fromAsm(asm)) + P2WSHWitnessSPKV0.fromAsm(asm) case _ if WitnessScriptPubKey.isWitnessScriptPubKey(asm) => - Some(UnassignedWitnessScriptPubKey(asm)) - case _ => None + UnassignedWitnessScriptPubKey(asm) + case _ => + throw new IllegalArgumentException( + "Given asm was not a WitnessScriptPubKey, got: " + asm) } /** @@ -1130,7 +1161,7 @@ object WitnessScriptPubKeyV0 { * [[https://github.com/bitcoin/bitcoin/blob/449f9b8debcceb61a92043bc7031528a53627c47/src/script/script.cpp#L215-L229]] */ def isValid(asm: Seq[ScriptToken]): Boolean = { - WitnessScriptPubKey.isWitnessScriptPubKey(asm) && asm.headOption == Some( + WitnessScriptPubKey.isWitnessScriptPubKey(asm) && asm.headOption.contains( OP_0) } } @@ -1152,8 +1183,8 @@ object P2WPKHWitnessSPKV0 extends ScriptFactory[P2WPKHWitnessSPKV0] { override def fromAsm(asm: Seq[ScriptToken]): P2WPKHWitnessSPKV0 = { buildScript(asm.toVector, - P2WPKHWitnessSPKV0Impl(_), - isValid(_), + P2WPKHWitnessSPKV0Impl.apply, + isValid, s"Given asm was not a P2WPKHWitnessSPKV0, got $asm") } @@ -1197,8 +1228,8 @@ object P2WSHWitnessSPKV0 extends ScriptFactory[P2WSHWitnessSPKV0] { override def fromAsm(asm: Seq[ScriptToken]): P2WSHWitnessSPKV0 = { buildScript(asm.toVector, - P2WSHWitnessSPKV0Impl(_), - isValid(_), + P2WSHWitnessSPKV0Impl.apply, + isValid, s"Given asm was not a P2WSHWitnessSPKV0, got $asm") } @@ -1242,8 +1273,8 @@ object UnassignedWitnessScriptPubKey override def fromAsm(asm: Seq[ScriptToken]): UnassignedWitnessScriptPubKey = { buildScript( asm.toVector, - UnassignedWitnessScriptPubKeyImpl(_), - WitnessScriptPubKey.isWitnessScriptPubKey(_), + UnassignedWitnessScriptPubKeyImpl.apply, + WitnessScriptPubKey.isWitnessScriptPubKey, "Given asm was not a valid witness script pubkey: " + asm ) } @@ -1284,8 +1315,8 @@ object WitnessCommitment extends ScriptFactory[WitnessCommitment] { override def fromAsm(asm: Seq[ScriptToken]): WitnessCommitment = { buildScript(asm.toVector, - WitnessCommitmentImpl(_), - isWitnessCommitment(_), + WitnessCommitmentImpl.apply, + isWitnessCommitment, "Given asm was not a valid witness commitment, got: " + asm) } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptSignature.scala b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptSignature.scala index d228487107..3ea3c29a0b 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptSignature.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/script/ScriptSignature.scala @@ -148,7 +148,7 @@ sealed trait P2SHScriptSignature extends ScriptSignature { //if we have an EmptyScriptSignature, we need to check if the rest of the asm //is a Witness script. It is not necessarily a witness script, since this code //path might be used for signing a normal p2sh spk in TransactionSignatureSerializer - WitnessScriptPubKey(asm.tail).get + WitnessScriptPubKey(asm.tail) } else { ScriptPubKey.fromAsmBytes(asm.last.bytes) } 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 1cfc61d531..e60f257afd 100644 --- a/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala +++ b/core/src/main/scala/org/bitcoins/core/psbt/PSBT.scala @@ -88,6 +88,22 @@ case class PSBT( PSBT(global, inputs, outputs) } + def finalizeInput(index: Int): Try[PSBT] = { + require(index >= 0 && index < inputMaps.size, + s"Index must be within 0 and the number of inputs, got: $index") + val inputMap = inputMaps(index) + if (inputMap.isFinalized) { + Success(this) + } else { + inputMap.finalize(transaction.inputs(index)).map { finalizedInputMap => + val newInputMaps = + inputMaps.updated(index, finalizedInputMap) + + PSBT(globalMap, newInputMaps, outputMaps) + } + } + } + /** Finalizes this PSBT if possible, returns a Failure otherwise * @see [[https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#input-finalizer]] */ @@ -502,28 +518,8 @@ case class PSBT( inputIndex: Int): PSBT = addSignature(PartialSignature(pubKey, sig), inputIndex) - def addSignature( - partialSignature: PartialSignature, - inputIndex: Int): PSBT = { - require( - inputIndex < inputMaps.size, - s"index must be less than the number of input maps present in the psbt, $inputIndex >= ${inputMaps.size}") - require( - !inputMaps(inputIndex).isFinalized, - s"Cannot update an InputPSBTMap that is finalized, index: $inputIndex") - require( - !inputMaps(inputIndex).partialSignatures - .exists(_.pubKey == partialSignature.pubKey), - s"Input has already been signed by ${partialSignature.pubKey}" - ) - - val newElements = inputMaps(inputIndex).elements :+ partialSignature - - val newInputMaps = - inputMaps.updated(inputIndex, InputPSBTMap(newElements)) - - PSBT(globalMap, newInputMaps, outputMaps) - } + def addSignature(partialSignature: PartialSignature, inputIndex: Int): PSBT = + addSignatures(Vector(partialSignature), inputIndex) /** Adds all the PartialSignatures to the input map at the given index */ def addSignatures( @@ -551,6 +547,51 @@ case class PSBT( PSBT(globalMap, newInputMaps, outputMaps) } + def verifyFinalizedInput(index: Int): Boolean = { + val inputMap = inputMaps(index) + require(inputMap.isFinalized, "Input must be finalized to verify") + + val wUtxoOpt = inputMap.witnessUTXOOpt + val utxoOpt = inputMap.nonWitnessOrUnknownUTXOOpt + + val newInput = { + val input = transaction.inputs(index) + val scriptSigOpt = inputMap.finalizedScriptSigOpt + val scriptSig = + scriptSigOpt.map(_.scriptSig).getOrElse(EmptyScriptSignature) + TransactionInput(input.previousOutput, scriptSig, input.sequence) + } + + val tx = transaction.updateInput(index, newInput) + + wUtxoOpt match { + case Some(wUtxo) => + inputMap.finalizedScriptWitnessOpt match { + case Some(scriptWit) => + val wtx = { + val wtx = WitnessTransaction.toWitnessTx(transaction) + wtx.updateWitness(index, scriptWit.scriptWitness) + } + val output = wUtxo.witnessUTXO + + ScriptInterpreter.verifyInputScript(wtx, index, output) + case None => + false + } + case None => + utxoOpt match { + case Some(utxo) => + val input = tx.inputs(index) + val output = + utxo.transactionSpent.outputs(input.previousOutput.vout.toInt) + + ScriptInterpreter.verifyInputScript(tx, index, output) + case None => + false + } + } + } + /** * Extracts the serialized from the serialized, fully signed transaction from * this PSBT and validates the script signatures using the ScriptInterpreter. diff --git a/core/src/main/scala/org/bitcoins/core/util/BitcoinScriptUtil.scala b/core/src/main/scala/org/bitcoins/core/util/BitcoinScriptUtil.scala index d63c5016eb..0f77af91a8 100644 --- a/core/src/main/scala/org/bitcoins/core/util/BitcoinScriptUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/util/BitcoinScriptUtil.scala @@ -2,7 +2,9 @@ package org.bitcoins.core.util import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.crypto._ +import org.bitcoins.core.currency.CurrencyUnits import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.CompactSizeUInt import org.bitcoins.core.protocol.script.{ CLTVScriptPubKey, @@ -10,6 +12,12 @@ import org.bitcoins.core.protocol.script.{ EmptyScriptPubKey, _ } +import org.bitcoins.core.protocol.transaction.{ + Transaction, + TransactionInput, + TransactionOutput, + WitnessTransaction +} import org.bitcoins.core.script.constant._ import org.bitcoins.core.script.crypto.{ OP_CHECKMULTISIG, @@ -18,13 +26,18 @@ import org.bitcoins.core.script.crypto.{ OP_CHECKSIGVERIFY } import org.bitcoins.core.script.flag.{ScriptFlag, ScriptFlagUtil} +import org.bitcoins.core.script.interpreter.ScriptInterpreter import org.bitcoins.core.script.result.{ ScriptError, ScriptErrorPubKeyType, ScriptErrorWitnessPubKeyType } -import org.bitcoins.core.script.ExecutionInProgressScriptProgram +import org.bitcoins.core.script.{ + ExecutionInProgressScriptProgram, + PreExecutionScriptProgram +} import org.bitcoins.core.serializers.script.ScriptParser +import org.bitcoins.core.wallet.utxo.{InputInfo, ScriptSignatureParams} import org.bitcoins.crypto.{ECDigitalSignature, ECPublicKey} import scodec.bits.ByteVector @@ -92,8 +105,8 @@ trait BitcoinScriptUtil extends BitcoinSLogger { * which is how Bitcoin Core handles this */ def countsTowardsScriptOpLimit(token: ScriptToken): Boolean = token match { - case scriptOp: ScriptOperation if (scriptOp.opCode > OP_16.opCode) => true - case _: ScriptToken => false + case scriptOp: ScriptOperation if scriptOp.opCode > OP_16.opCode => true + case _: ScriptToken => false } /** @@ -130,8 +143,9 @@ trait BitcoinScriptUtil extends BitcoinSLogger { def numPossibleSignaturesOnStack( program: ExecutionInProgressScriptProgram): ScriptNumber = { require( - program.script.headOption == Some(OP_CHECKMULTISIG) || program.script.headOption == Some( - OP_CHECKMULTISIGVERIFY), + program.script.headOption + .contains(OP_CHECKMULTISIG) || program.script.headOption + .contains(OP_CHECKMULTISIGVERIFY), "We can only parse the nubmer of signatures the stack when we are executing a OP_CHECKMULTISIG or OP_CHECKMULTISIGVERIFY op" ) val nPossibleSignatures: ScriptNumber = program.stack.head match { @@ -151,8 +165,9 @@ trait BitcoinScriptUtil extends BitcoinSLogger { def numRequiredSignaturesOnStack( program: ExecutionInProgressScriptProgram): ScriptNumber = { require( - program.script.headOption == Some(OP_CHECKMULTISIG) || program.script.headOption == Some( - OP_CHECKMULTISIGVERIFY), + program.script.headOption + .contains(OP_CHECKMULTISIG) || program.script.headOption + .contains(OP_CHECKMULTISIGVERIFY), "We can only parse the nubmer of signatures the stack when we are executing a OP_CHECKMULTISIG or OP_CHECKMULTISIGVERIFY op" ) val nPossibleSignatures = numPossibleSignaturesOnStack(program) @@ -209,18 +224,18 @@ trait BitcoinScriptUtil extends BitcoinSLogger { //so we can push the constant "00" or "81" onto the stack with a BytesToPushOntoStack pushop pushOp == BytesToPushOntoStack(1) case _: ScriptToken - if (token.bytes.size == 1 && ScriptNumberOperation + if token.bytes.size == 1 && ScriptNumberOperation .fromNumber(token.toLong.toInt) - .isDefined) => + .isDefined => //could have used the ScriptNumberOperation to push the number onto the stack false case token: ScriptToken => token.bytes.size match { - case size if (size == 0) => pushOp == OP_0 - case size if (size <= 75) => token.bytes.size == pushOp.toLong - case size if (size <= 255) => pushOp == OP_PUSHDATA1 - case size if (size <= 65535) => pushOp == OP_PUSHDATA2 - case _: Long => + case size if size == 0 => pushOp == OP_0 + case size if size <= 75 => token.bytes.size == pushOp.toLong + case size if size <= 255 => pushOp == OP_PUSHDATA1 + case size if size <= 65535 => pushOp == OP_PUSHDATA2 + case _: Long => //default case is true because we have to use the largest push op as possible which is OP_PUSHDATA4 true } @@ -270,7 +285,7 @@ trait BitcoinScriptUtil extends BitcoinSLogger { // If the most-significant-byte - excluding the sign bit - is zero // then we're not minimal. Note how this test also rejects the // negative-zero encoding, 0x80. - if ((bytes.size > 0 && (bytes.last & 0x7f) == 0)) { + if (bytes.size > 0 && (bytes.last & 0x7f) == 0) { // One exception: if there's more than one byte and the most // significant bit of the second-most-significant-byte is set // it would conflict with the sign bit. An example of this case @@ -548,15 +563,13 @@ trait BitcoinScriptUtil extends BitcoinSLogger { * [[org.bitcoins.core.script.crypto.OP_CHECKMULTISIG OP_CHECKMULTISIG]] with * [[org.bitcoins.core.script.constant.ScriptNumber.zero ScriptNumber.zero]] */ def minimalDummy(asm: Seq[ScriptToken]): Seq[ScriptToken] = { - if (asm.headOption == Some(OP_0)) ScriptNumber.zero +: asm.tail + if (asm.headOption.contains(OP_0)) ScriptNumber.zero +: asm.tail else asm } /** * Checks that all the [[org.bitcoins.crypto.ECPublicKey ECPublicKey]] in this script * is compressed public keys, this is required for BIP143 - * @param spk - * @return */ def isOnlyCompressedPubKey(spk: ScriptPubKey): Boolean = { spk match { @@ -564,7 +577,7 @@ trait BitcoinScriptUtil extends BitcoinSLogger { case p2pkWithTimeout: P2PKWithTimeoutScriptPubKey => p2pkWithTimeout.pubKey.isCompressed && p2pkWithTimeout.timeoutPubKey.isCompressed case m: MultiSignatureScriptPubKey => - !m.publicKeys.exists(k => !k.isCompressed) + m.publicKeys.forall(_.isCompressed) case l: LockTimeScriptPubKey => isOnlyCompressedPubKey(l.nestedScriptPubKey) case conditional: ConditionalScriptPubKey => @@ -592,6 +605,62 @@ trait BitcoinScriptUtil extends BitcoinSLogger { val script: Vector[ScriptToken] = ScriptParser.fromBytes(scriptPubKeyBytes) f(script) } + + def verifyScript( + tx: Transaction, + utxos: Seq[ScriptSignatureParams[InputInfo]]): Boolean = { + val programs: Seq[PreExecutionScriptProgram] = tx.inputs.zipWithIndex.map { + case (input: TransactionInput, idx: Int) => + val outpoint = input.previousOutput + + val creditingTx = utxos.find(u => u.outPoint == outpoint).get + + val output = creditingTx.output + + val spk = output.scriptPubKey + + val amount = output.value + + val txSigComponent = spk match { + case witSPK: WitnessScriptPubKeyV0 => + val o = TransactionOutput(amount, witSPK) + WitnessTxSigComponentRaw(tx.asInstanceOf[WitnessTransaction], + UInt32(idx), + o, + Policy.standardFlags) + case _: UnassignedWitnessScriptPubKey => ??? + case x @ (_: P2PKScriptPubKey | _: P2PKHScriptPubKey | + _: P2PKWithTimeoutScriptPubKey | _: MultiSignatureScriptPubKey | + _: WitnessCommitment | _: CSVScriptPubKey | _: CLTVScriptPubKey | + _: ConditionalScriptPubKey | _: NonStandardScriptPubKey | + EmptyScriptPubKey) => + val o = TransactionOutput(CurrencyUnits.zero, x) + BaseTxSigComponent(tx, UInt32(idx), o, Policy.standardFlags) + + case _: P2SHScriptPubKey => + val p2shScriptSig = + tx.inputs(idx).scriptSignature.asInstanceOf[P2SHScriptSignature] + p2shScriptSig.redeemScript match { + + case _: WitnessScriptPubKey => + WitnessTxSigComponentP2SH(transaction = + tx.asInstanceOf[WitnessTransaction], + inputIndex = UInt32(idx), + output = output, + flags = Policy.standardFlags) + + case _ => + BaseTxSigComponent(tx, + UInt32(idx), + output, + Policy.standardFlags) + } + } + + PreExecutionScriptProgram(txSigComponent) + } + ScriptInterpreter.runAllVerify(programs) + } } object BitcoinScriptUtil extends BitcoinScriptUtil diff --git a/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxBuilder.scala b/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxBuilder.scala index c29aa2c103..2b1bac3c0e 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxBuilder.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxBuilder.scala @@ -60,7 +60,8 @@ case class RawTxBuilder() { /** Returns a RawTxBuilderWithFinalizer where building can continue * and where buildTx can be called once building is completed. */ - def setFinalizer(finalizer: RawTxFinalizer): RawTxBuilderWithFinalizer = { + def setFinalizer[F <: RawTxFinalizer]( + finalizer: F): RawTxBuilderWithFinalizer[F] = { RawTxBuilderWithFinalizer(this, finalizer) } @@ -141,9 +142,9 @@ case class RawTxBuilder() { * access to the RawTxFinalizer's buildTx method which * completes the RawTxBuilder and then finalized the result. */ -case class RawTxBuilderWithFinalizer( +case class RawTxBuilderWithFinalizer[F <: RawTxFinalizer]( builder: RawTxBuilder, - finalizer: RawTxFinalizer) { + finalizer: F) { /** Completes the builder and finalizes the result */ def buildTx()(implicit ec: ExecutionContext): Future[Transaction] = { diff --git a/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxFinalizer.scala b/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxFinalizer.scala index e705fb141d..fabd7cd94f 100644 --- a/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxFinalizer.scala +++ b/core/src/main/scala/org/bitcoins/core/wallet/builder/RawTxFinalizer.scala @@ -1,6 +1,7 @@ package org.bitcoins.core.wallet.builder import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} +import org.bitcoins.core.number.Int64 import org.bitcoins.core.policy.Policy import org.bitcoins.core.protocol.script.ScriptPubKey import org.bitcoins.core.protocol.transaction._ @@ -248,7 +249,8 @@ object StandardNonInteractiveFinalizer { outputs: Seq[TransactionOutput], utxos: Seq[InputSigningInfo[InputInfo]], feeRate: FeeUnit, - changeSPK: ScriptPubKey): RawTxBuilderWithFinalizer = { + changeSPK: ScriptPubKey): RawTxBuilderWithFinalizer[ + StandardNonInteractiveFinalizer] = { val inputs = InputUtil.calcSequenceForInputs(utxos, Policy.isRBFEnabled) val lockTime = TxUtil.calcLockTime(utxos).get val builder = RawTxBuilder().setLockTime(lockTime) ++= outputs ++= inputs @@ -271,3 +273,67 @@ object StandardNonInteractiveFinalizer { builderF.flatMap(_.buildTx()) } } + +case class SubtractFeeFromOutputsFinalizer( + inputInfos: Vector[InputInfo], + feeRate: FeeUnit) + extends RawTxFinalizer { + override def buildTx(txBuilderResult: RawTxBuilderResult)( + implicit ec: ExecutionContext): Future[Transaction] = { + val RawTxBuilderResult(version, inputs, outputs, lockTime) = txBuilderResult + + val witnesses = inputInfos.map(InputInfo.getScriptWitness) + val txWithPossibleWitness = TransactionWitness.fromWitOpt(witnesses) match { + case _: EmptyWitness => + BaseTransaction(version, inputs, outputs, lockTime) + case wit: TransactionWitness => + WitnessTransaction(version, inputs, outputs, lockTime, wit) + } + + val dummyTxF = TxUtil.addDummySigs(txWithPossibleWitness, inputInfos) + + val outputsAfterFeeF = dummyTxF.map { dummyTx => + SubtractFeeFromOutputsFinalizer.subtractFees(dummyTx, + feeRate, + outputs.map(_.scriptPubKey)) + } + + outputsAfterFeeF.map { outputsAfterFee => + BaseTransaction(version, inputs, outputsAfterFee, lockTime) + } + } +} + +object SubtractFeeFromOutputsFinalizer { + + def subtractFees( + tx: Transaction, + feeRate: FeeUnit, + spks: Vector[ScriptPubKey]): Vector[TransactionOutput] = { + val fee = feeRate.calc(tx) + + val outputs = tx.outputs.zipWithIndex.filter { + case (output, _) => spks.contains(output.scriptPubKey) + } + val unchangedOutputs = tx.outputs.zipWithIndex.filterNot { + case (output, _) => spks.contains(output.scriptPubKey) + } + + val feePerOutput = Satoshis(Int64(fee.satoshis.toLong / outputs.length)) + val feeRemainder = Satoshis(Int64(fee.satoshis.toLong % outputs.length)) + + val newOutputsWithoutRemainder = outputs.map { + case (output, index) => + (TransactionOutput(output.value - feePerOutput, output.scriptPubKey), + index) + } + val (lastOutput, lastOutputIndex) = newOutputsWithoutRemainder.last + val newLastOutput = TransactionOutput(lastOutput.value - feeRemainder, + lastOutput.scriptPubKey) + val newOutputs = newOutputsWithoutRemainder + .dropRight(1) + .:+((newLastOutput, lastOutputIndex)) + + (newOutputs ++ unchangedOutputs).sortBy(_._2).map(_._1).toVector + } +} 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 fcf0ab45af..fe0f7f67a8 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 @@ -81,6 +81,10 @@ case class ECSignatureParams[+InputType <: InputInfo]( extends InputSigningInfo[InputType] { override def signers: Vector[Sign] = Vector(signer) + def toScriptSignatureParams: ScriptSignatureParams[InputType] = { + ScriptSignatureParams(inputInfo, signer, hashType) + } + def mapInfo[T <: InputInfo](func: InputType => T): ECSignatureParams[T] = { this.copy(inputInfo = func(this.inputInfo)) } diff --git a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala index 5aa54d1505..e77705444e 100644 --- a/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala +++ b/db-commons/src/main/scala/org/bitcoins/db/DbCommonsColumnMappers.scala @@ -1,26 +1,30 @@ package org.bitcoins.db +import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.ContractInfo +import org.bitcoins.core.config.{BitcoinNetwork, BitcoinNetworks} import org.bitcoins.core.crypto._ import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} import org.bitcoins.core.gcs.FilterType import org.bitcoins.core.hd._ import org.bitcoins.core.number.{Int32, UInt32, UInt64} -import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness} import org.bitcoins.core.protocol.transaction.{ Transaction, TransactionOutPoint, TransactionOutput } +import org.bitcoins.core.protocol.{ + Bech32Address, + BitcoinAddress, + BlockTimeStamp +} +import org.bitcoins.core.psbt.InputPSBTMap +import org.bitcoins.core.psbt.InputPSBTRecord.PartialSignature import org.bitcoins.core.script.ScriptType import org.bitcoins.core.serializers.script.RawScriptWitnessParser -import org.bitcoins.core.wallet.fee.SatoshisPerByte +import org.bitcoins.core.wallet.fee.{SatoshisPerByte, SatoshisPerVirtualByte} import org.bitcoins.core.wallet.utxo.TxoState -import org.bitcoins.crypto.{ - DoubleSha256DigestBE, - ECPublicKey, - Sha256Hash160Digest -} +import org.bitcoins.crypto._ import scodec.bits.ByteVector import slick.jdbc.{GetResult, JdbcProfile} @@ -76,9 +80,19 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) { MappedColumnType .base[BigInt, BigDecimal](BigDecimal(_), _.toBigInt) + implicit val sha256DigestBEMapper: BaseColumnType[Sha256DigestBE] = + MappedColumnType.base[Sha256DigestBE, String](_.hex, Sha256DigestBE.fromHex) + implicit val ecPublicKeyMapper: BaseColumnType[ECPublicKey] = MappedColumnType.base[ECPublicKey, String](_.hex, ECPublicKey.fromHex) + implicit val schnorrPublicKeyMapper: BaseColumnType[SchnorrPublicKey] = + MappedColumnType + .base[SchnorrPublicKey, String](_.hex, SchnorrPublicKey.fromHex) + + implicit val schnorrNonceMapper: BaseColumnType[SchnorrNonce] = + MappedColumnType.base[SchnorrNonce, String](_.hex, SchnorrNonce.fromHex) + implicit val sha256Hash160DigestMapper: BaseColumnType[Sha256Hash160Digest] = MappedColumnType .base[Sha256Hash160Digest, String](_.hex, Sha256Hash160Digest.fromHex) @@ -150,7 +164,7 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) { implicit val segwitPathMappper: BaseColumnType[SegWitHDPath] = MappedColumnType - .base[SegWitHDPath, String](_.toString, SegWitHDPath.fromString(_)) // hm rethink .get? + .base[SegWitHDPath, String](_.toString, SegWitHDPath.fromString) // hm rethink .get? implicit val hdChainTypeMapper: BaseColumnType[HDChainType] = MappedColumnType.base[HDChainType, Int](_.index, HDChainType.fromInt) @@ -163,6 +177,10 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) { MappedColumnType .base[BitcoinAddress, String](_.value, BitcoinAddress.fromStringExn) + implicit val bech32AddressMapper: BaseColumnType[Bech32Address] = + MappedColumnType + .base[Bech32Address, String](_.value, Bech32Address.fromStringExn) + implicit val scriptTypeMapper: BaseColumnType[ScriptType] = MappedColumnType .base[ScriptType, String](_.toString, ScriptType.fromStringExn) @@ -187,4 +205,55 @@ class DbCommonsColumnMappers(val profile: JdbcProfile) { MappedColumnType .base[SatoshisPerByte, Long](_.toLong, SatoshisPerByte.fromLong) } + + implicit val hdAccountMapper: BaseColumnType[HDAccount] = { + MappedColumnType.base[HDAccount, String]( + _.toString, + str => HDAccount.fromPath(BIP32Path.fromString(str)).get) + } + + implicit val contractInfoMapper: BaseColumnType[ContractInfo] = { + MappedColumnType + .base[ContractInfo, String](_.hex, ContractInfo.fromHex) + } + + implicit val blockStampWithFutureMapper: BaseColumnType[BlockTimeStamp] = { + MappedColumnType.base[BlockTimeStamp, Long]( + _.toUInt32.toLong, + long => BlockTimeStamp(UInt32(long))) + } + + implicit val partialSigMapper: BaseColumnType[PartialSignature] = { + MappedColumnType + .base[PartialSignature, String](_.hex, PartialSignature.fromHex) + } + + implicit val partialSigsMapper: BaseColumnType[Vector[PartialSignature]] = { + MappedColumnType + .base[Vector[PartialSignature], String]( + _.foldLeft("")(_ ++ _.hex), + hex => + if (hex.isEmpty) Vector.empty + else InputPSBTMap(hex ++ "00").partialSignatures) + } + + implicit val satoshisPerVirtualByteMapper: BaseColumnType[ + SatoshisPerVirtualByte] = { + MappedColumnType + .base[SatoshisPerVirtualByte, String]( + _.currencyUnit.hex, + hex => SatoshisPerVirtualByte(Satoshis.fromHex(hex))) + } + + implicit val networkMapper: BaseColumnType[BitcoinNetwork] = { + MappedColumnType + .base[BitcoinNetwork, String](_.name, BitcoinNetworks.fromString) + } + + implicit val schnorrDigitalSignatureMapper: BaseColumnType[ + SchnorrDigitalSignature] = { + MappedColumnType.base[SchnorrDigitalSignature, String]( + _.hex, + SchnorrDigitalSignature.fromHex) + } } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/rpc/BitcoindRpcTestUtil.scala b/testkit/src/main/scala/org/bitcoins/testkit/rpc/BitcoindRpcTestUtil.scala index 1eadcbac7e..ac3adc27f3 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/rpc/BitcoindRpcTestUtil.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/rpc/BitcoindRpcTestUtil.scala @@ -140,7 +140,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { val datadir = conf.datadir val written = BitcoindConfig.writeConfigToFile(conf, datadir) - logger.debug(s"Wrote conf to ${written}") + logger.debug(s"Wrote conf to $written") written } @@ -178,7 +178,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { if (filtered.isEmpty) throw new RuntimeException( - s"bitcoind ${known.toString} is not installed in ${binaryDirectory}. Run `sbt downloadBitcoind`") + s"bitcoind ${known.toString} is not installed in $binaryDirectory. Run `sbt downloadBitcoind`") // might be multiple versions downloaded for // each major version, i.e. 0.16.2 and 0.16.3 @@ -187,7 +187,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { versionFolder .resolve("bin") .resolve(if (Properties.isWin) "bitcoind.exe" else "bitcoind") - .toFile() + .toFile } /** Creates a `bitcoind` instance within the user temporary directory */ @@ -229,7 +229,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { authCredentials = auth, zmqConfig = ZmqConfig.fromPort(zmqPort), binary = binary, - datadir = configFile.getParent.toFile()) + datadir = configFile.getParent.toFile) instance } @@ -485,7 +485,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { .map(info => info.isEmpty || info.head.connected.contains(false)) .recoverWith { case exception: BitcoindException - if exception.getMessage.contains("Node has not been added") => + if exception.getMessage().contains("Node has not been added") => from.getPeerInfo.map( _.forall(_.networkInfo.addr != to.instance.uri)) } @@ -848,6 +848,19 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { .flatMap(node.getBlockWithTransactions) } + /** Mines blocks until the specified block height. */ + def waitUntilBlock( + blockHeight: Int, + client: BitcoindRpcClient, + addressForMining: BitcoinAddress)( + implicit ec: ExecutionContext): Future[Unit] = { + for { + currentCount <- client.getBlockCount + blocksToMine = blockHeight - currentCount + _ <- client.generateToAddress(blocks = blocksToMine, addressForMining) + } yield () + } + /** * Produces a confirmed transaction from `sender` to `address` * for `amount` @@ -941,7 +954,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger { implicit system: ActorSystem): Future[BitcoindRpcClient] = { implicit val ec: ExecutionContextExecutor = system.dispatcher require( - instance.datadir.getPath().startsWith(Properties.tmpDir), + instance.datadir.getPath.startsWith(Properties.tmpDir), s"${instance.datadir} is not in user temp dir! This could lead to bad things happening.") //start the bitcoind instance so eclair can properly use it diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index b5273d7cad..56905c43c7 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -209,7 +209,7 @@ abstract class Wallet * finalizing and signing the transaction, then correctly processing and logging it */ private def finishSend( - txBuilder: RawTxBuilderWithFinalizer, + txBuilder: RawTxBuilderWithFinalizer[StandardNonInteractiveFinalizer], utxoInfos: Vector[ScriptSignatureParams[InputInfo]], sentAmount: CurrencyUnit, feeRate: FeeUnit): Future[Transaction] = { diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala index 25c3f891b7..5d84a42ea4 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/FundTransactionHandling.scala @@ -70,8 +70,9 @@ trait FundTransactionHandling extends WalletLogger { self: WalletApi => fromAccount: AccountDb, keyManagerOpt: Option[BIP39KeyManager], coinSelectionAlgo: CoinSelectionAlgo = CoinSelectionAlgo.AccumulateLargest, - markAsReserved: Boolean = false): Future[ - (RawTxBuilderWithFinalizer, Vector[ScriptSignatureParams[InputInfo]])] = { + markAsReserved: Boolean = false): Future[( + RawTxBuilderWithFinalizer[StandardNonInteractiveFinalizer], + Vector[ScriptSignatureParams[InputInfo]])] = { val utxosF = for { utxos <- listUtxos(fromAccount.hdAccount) diff --git a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala index f28ce9058f..dc76c32e1d 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/internal/UtxoHandling.scala @@ -44,6 +44,14 @@ private[wallet] trait UtxoHandling extends WalletLogger { spendingInfoDAO.findAllUnspentForAccount(hdAccount) } + /** Returns all the utxos originating from the given outpoints */ + def listUtxos(outPoints: Vector[TransactionOutPoint]): Future[ + Vector[SpendingInfoDb]] = { + spendingInfoDAO + .findAll() + .map(_.filter(spendingInfo => outPoints.contains(spendingInfo.outPoint))) + } + protected def updateUtxoConfirmedState( txo: SpendingInfoDb, blockHash: DoubleSha256DigestBE): Future[SpendingInfoDb] = {