From af67b9ada327b84875a971b4803b6397c992433d Mon Sep 17 00:00:00 2001 From: Ben Carman Date: Thu, 16 Jul 2020 11:37:43 -0500 Subject: [PATCH] Create BIP 69 finalizer (#1661) --- .../wallet/builder/RawTxFinalizerTest.scala | 146 ++++++++++++++++++ .../scala/org/bitcoins/core/package.scala | 20 +++ .../transaction/TransactionOutPoint.scala | 6 + .../core/wallet/builder/RawTxFinalizer.scala | 12 ++ 4 files changed, 184 insertions(+) create mode 100644 core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxFinalizerTest.scala diff --git a/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxFinalizerTest.scala b/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxFinalizerTest.scala new file mode 100644 index 0000000000..d43aa171b7 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/wallet/builder/RawTxFinalizerTest.scala @@ -0,0 +1,146 @@ +package org.bitcoins.core.wallet.builder + +import org.bitcoins.core.currency.Satoshis +import org.bitcoins.core.number.UInt32 +import org.bitcoins.core.protocol.script.{EmptyScriptSignature, ScriptPubKey} +import org.bitcoins.core.protocol.transaction._ +import org.bitcoins.crypto.DoubleSha256DigestBE +import org.bitcoins.testkit.util.BitcoinSAsyncTest +import org.scalatest.Assertion + +import scala.concurrent.Future +import scala.util.Random + +class RawTxFinalizerTest extends BitcoinSAsyncTest { + + behavior of "RawTxFinalizer" + + it should "correctly sort inputs and outputs with a BIP 69 Finalizer, BIP example 1" in { + + val outpoints = Vector( + TransactionOutPoint( + DoubleSha256DigestBE( + "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "26aa6e6d8b9e49bb0630aac301db6757c02e3619feb4ee0eea81eb1672947024"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "28e0fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "381de9b9ae1a94d9c17f6a08ef9d341a5ce29e2e60c36a52d333ff6203e58d5d"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "3b8b2f8efceb60ba78ca8bba206a137f14cb5ea4035e761ee204302d46b98de2"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "402b2c02411720bf409eff60d05adad684f135838962823f3614cc657dd7bc0a"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "54ffff182965ed0957dba1239c27164ace5a73c9b62a660c74b7b7f15ff61e7a"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "643e5f4e66373a57251fb173151e838ccd27d279aca882997e005016bb53d5aa"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "6c1d56f31b2de4bfc6aaea28396b333102b1f600da9c6d6149e96ca43f1102b1"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "7a1de137cbafb5c70405455c49c5104ca3057a1f1243e6563bb9245c9c88c191"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "7d037ceb2ee0dc03e82f17be7935d238b35d1deabf953a892a4507bfbeeb3ba4"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "a5e899dddb28776ea9ddac0a502316d53a4a3fca607c72f66c470e0412e34086"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "b4112b8f900a7ca0c8b0e7c4dfad35c6be5f6be46b3458974988e1cdb2fa61b8"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "bafd65e3c7f3f9fdfdc1ddb026131b278c3be1af90a4a6ffa78c4658f9ec0c85"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "de0411a1e97484a2804ff1dbde260ac19de841bebad1880c782941aca883b4e9"), + UInt32.one), + TransactionOutPoint( + DoubleSha256DigestBE( + "f0a130a84912d03c1d284974f563c5949ac13f8342b8112edff52971599e6a45"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "f320832a9d2e2452af63154bc687493484a0e7745ebd3aaf9ca19eb80834ad60"), + UInt32.zero) + ) + val sortedInputs = + outpoints.map(TransactionInput(_, EmptyScriptSignature, UInt32.zero)) + val sortedOutputs: Vector[TransactionOutput] = Vector( + TransactionOutput( + Satoshis(400057456), + ScriptPubKey("76a9144a5fba237213a062f6f57978f796390bdcf8d01588ac")), + TransactionOutput( + Satoshis(40000000000L), + ScriptPubKey("76a9145be32612930b8323add2212a4ec03c1562084f8488ac")) + ) + + testBIP69Finalizer(sortedInputs, sortedOutputs) + } + + it should "correctly sort inputs and outputs with a BIP 69 Finalizer, BIP example 2" in { + + val outpoints = Vector( + TransactionOutPoint( + DoubleSha256DigestBE( + "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055"), + UInt32.zero), + TransactionOutPoint( + DoubleSha256DigestBE( + "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055"), + UInt32.one) + ) + val sortedInputs = + outpoints.map(TransactionInput(_, EmptyScriptSignature, UInt32.zero)) + val sortedOutputs: Vector[TransactionOutput] = Vector( + TransactionOutput( + Satoshis(100000000), + ScriptPubKey( + "41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac") + ), + TransactionOutput( + Satoshis(2400000000L), + ScriptPubKey( + "41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac") + ) + ) + + testBIP69Finalizer(sortedInputs, sortedOutputs) + } + + def testBIP69Finalizer( + sortedInputs: Vector[TransactionInput], + sortedOutputs: Vector[TransactionOutput]): Future[Assertion] = { + val inputs = Random.shuffle(sortedInputs) + val outputs = Random.shuffle(sortedOutputs) + + val txBuilder = RawTxBuilder() ++= inputs ++= outputs + txBuilder.setFinalizer(BIP69Finalizer).buildTx().map { tx => + assert(tx.inputs == sortedInputs) + assert(tx.outputs == sortedOutputs) + } + } +} diff --git a/core/src/main/scala/org/bitcoins/core/package.scala b/core/src/main/scala/org/bitcoins/core/package.scala index ef1411ced1..2db0606acb 100644 --- a/core/src/main/scala/org/bitcoins/core/package.scala +++ b/core/src/main/scala/org/bitcoins/core/package.scala @@ -1,5 +1,9 @@ package org.bitcoins +import org.bitcoins.core.protocol.transaction.{ + TransactionInput, + TransactionOutput +} import org.bitcoins.core.wallet.fee.SatoshisPerKiloByte import scodec.bits._ @@ -33,4 +37,20 @@ package object core { } } } + + implicit val transactionInputOrder: Ordering[TransactionInput] = + new Ordering[TransactionInput] { + + override def compare(x: TransactionInput, y: TransactionInput): Int = + x.previousOutput.compare(y.previousOutput) + } + + implicit val transactionOutputOrder: Ordering[TransactionOutput] = + new Ordering[TransactionOutput] { + + override def compare(x: TransactionOutput, y: TransactionOutput): Int = + if (x.value == y.value) { + x.scriptPubKey.hex.compare(y.scriptPubKey.hex) + } else x.value.compare(y.value) + } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/transaction/TransactionOutPoint.scala b/core/src/main/scala/org/bitcoins/core/protocol/transaction/TransactionOutPoint.scala index 9eb06b5a71..d2ae977764 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/transaction/TransactionOutPoint.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/transaction/TransactionOutPoint.scala @@ -29,6 +29,12 @@ case class TransactionOutPoint(txId: DoubleSha256Digest, vout: UInt32) def !=(outPoint: TransactionOutPoint): Boolean = !(this == outPoint) + + def compare(other: TransactionOutPoint): Int = { + if (txId == other.txId) { + vout.compare(other.vout) + } else txIdBE.hex.compareTo(other.txIdBE.hex) + } } /** 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 953bfa8eaf..e7956ed679 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 @@ -83,6 +83,18 @@ case object FilterDustFinalizer extends RawTxFinalizer { } } +case object BIP69Finalizer extends RawTxFinalizer { + + override def buildTx(txBuilderResult: RawTxBuilderResult)(implicit + ec: ExecutionContext): Future[Transaction] = { + val sortedInputs = txBuilderResult.inputs.sorted + val sortedOutputs = txBuilderResult.outputs.sorted + Future.successful( + txBuilderResult.toBaseTransaction.copy(inputs = sortedInputs, + outputs = sortedOutputs)) + } +} + /** A finalizer who's Future fails if its sanity checks are not passed, * otherwise it does nothing. */