Conditional Signing Tests (#865)

* Added ConditionalScriptPubKeys to CreditingTxGen so that we are actually testing Conditional signing now

* Responded to code review

* Renamed scriptPubKeyTooBig => redeemScriptTooBig
This commit is contained in:
Nadav Kohen 2019-11-15 13:26:21 -07:00 committed by Chris Stewart
parent fad72c66d5
commit ab00fffffc
5 changed files with 170 additions and 33 deletions

View file

@ -39,7 +39,7 @@ sealed abstract class ScriptInterpreter extends BitcoinSLogger {
private lazy val MAX_SCRIPT_OPS = 201 private lazy val MAX_SCRIPT_OPS = 201
/** We cannot push an element larger than 520 bytes onto the stack */ /** We cannot push an element larger than 520 bytes onto the stack */
private lazy val MAX_PUSH_SIZE = 520 val MAX_PUSH_SIZE: Int = 520
/** /**
* Runs an entire script though our script programming language and * Runs an entire script though our script programming language and

View file

@ -537,13 +537,11 @@ trait BitcoinScriptUtil extends BitcoinSLogger {
/** Since witnesses are not run through the interpreter, replace /** Since witnesses are not run through the interpreter, replace
* `OP_0`/`OP_1` with `ScriptNumber.zero`/`ScriptNumber.one` */ * `OP_0`/`OP_1` with `ScriptNumber.zero`/`ScriptNumber.one` */
def minimalIfOp(asm: Seq[ScriptToken]): Seq[ScriptToken] = { def minimalIfOp(asm: Seq[ScriptToken]): Seq[ScriptToken] = {
if (asm == Nil) asm asm.map {
else if (asm.last == OP_0) { case OP_0 => ScriptNumber.zero
asm.dropRight(1) ++ Seq(ScriptNumber.zero) case OP_1 => ScriptNumber.one
} else if (asm.last == OP_1) { case token: ScriptToken => token
asm.dropRight(1) ++ Seq(ScriptNumber.one) }
} else asm
} }
/** Replaces the [[org.bitcoins.core.script.constant.OP_0 OP_0]] dummy for /** Replaces the [[org.bitcoins.core.script.constant.OP_0 OP_0]] dummy for

View file

@ -11,6 +11,7 @@ import org.bitcoins.core.wallet.builder.TxBuilderError
import org.bitcoins.core.wallet.utxo.{ import org.bitcoins.core.wallet.utxo.{
BitcoinUTXOSpendingInfo, BitcoinUTXOSpendingInfo,
ConditionalSpendingInfo, ConditionalSpendingInfo,
EmptySpendingInfo,
LockTimeSpendingInfo, LockTimeSpendingInfo,
MultiSignatureSpendingInfo, MultiSignatureSpendingInfo,
P2PKHSpendingInfo, P2PKHSpendingInfo,
@ -185,6 +186,8 @@ object BitcoinSigner {
spendingInfoToSatisfy: UTXOSpendingInfo)( spendingInfoToSatisfy: UTXOSpendingInfo)(
implicit ec: ExecutionContext): Future[TxSigComponent] = { implicit ec: ExecutionContext): Future[TxSigComponent] = {
spendingInfoToSatisfy match { spendingInfoToSatisfy match {
case empty: EmptySpendingInfo =>
EmptySigner.sign(spendingInfo, unsignedTx, isDummySignature, empty)
case p2pk: P2PKSpendingInfo => case p2pk: P2PKSpendingInfo =>
P2PKSigner.sign(spendingInfo, unsignedTx, isDummySignature, p2pk) P2PKSigner.sign(spendingInfo, unsignedTx, isDummySignature, p2pk)
case p2pkh: P2PKHSpendingInfo => case p2pkh: P2PKHSpendingInfo =>
@ -217,6 +220,30 @@ object BitcoinSigner {
} }
} }
/** For signing EmptyScriptPubKeys in tests, should probably not be used in real life. */
sealed abstract class EmptySigner extends BitcoinSigner[EmptySpendingInfo] {
override def sign(
spendingInfo: UTXOSpendingInfo,
unsignedTx: Transaction,
isDummySignature: Boolean,
spendingInfoToSatisfy: EmptySpendingInfo)(
implicit ec: ExecutionContext): Future[TxSigComponent] = {
val (_, output, inputIndex, _) = relevantInfo(spendingInfo, unsignedTx)
// This script pushes an OP_TRUE onto the stack, causing a successful spend
val satisfyEmptyScriptSig =
Future.successful(NonStandardScriptSignature("0151"))
updateScriptSigInSigComponent(unsignedTx,
inputIndex.toInt,
output,
satisfyEmptyScriptSig)
}
}
object EmptySigner extends EmptySigner
/** Used to sign a [[org.bitcoins.core.protocol.script.P2PKScriptPubKey]] */ /** Used to sign a [[org.bitcoins.core.protocol.script.P2PKScriptPubKey]] */
sealed abstract class P2PKSigner extends BitcoinSigner[P2PKSpendingInfo] { sealed abstract class P2PKSigner extends BitcoinSigner[P2PKSpendingInfo] {

View file

@ -5,9 +5,16 @@ import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.crypto.HashType import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.wallet.utxo.{BitcoinUTXOSpendingInfo, ConditionalPath} import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.core.wallet.utxo.{
BitcoinUTXOSpendingInfo,
ConditionalPath,
P2SHNestedSegwitV0UTXOSpendingInfo
}
import org.scalacheck.Gen import org.scalacheck.Gen
import scala.annotation.tailrec
sealed abstract class CreditingTxGen { sealed abstract class CreditingTxGen {
/** Minimum amount of outputs to generate */ /** Minimum amount of outputs to generate */
@ -22,15 +29,17 @@ sealed abstract class CreditingTxGen {
Gen.listOfN(n, TransactionGenerators.realisticOutput) Gen.listOfN(n, TransactionGenerators.realisticOutput)
} }
def rawOutput: Gen[BitcoinUTXOSpendingInfo] = { /** Generator for non-script hash based output */
def nonSHOutput: Gen[BitcoinUTXOSpendingInfo] = {
Gen.oneOf(p2pkOutput, Gen.oneOf(p2pkOutput,
p2pkhOutput, p2pkhOutput,
multiSigOutput, /*cltvOutput,*/ csvOutput, multiSigOutput, /*cltvOutput,*/ csvOutput,
conditionalOutput,
p2wpkhOutput) p2wpkhOutput)
} }
def rawOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = def nonSHOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] =
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, rawOutput)) Gen.choose(min, max).flatMap(n => Gen.listOfN(n, nonSHOutput))
def basicOutput: Gen[BitcoinUTXOSpendingInfo] = { def basicOutput: Gen[BitcoinUTXOSpendingInfo] = {
Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput) Gen.oneOf(p2pkOutput, p2pkhOutput, multiSigOutput)
@ -40,15 +49,33 @@ sealed abstract class CreditingTxGen {
//note, cannot put a p2wpkh here //note, cannot put a p2wpkh here
Gen.oneOf(p2pkOutput, Gen.oneOf(p2pkOutput,
p2pkhOutput, p2pkhOutput,
multiSigOutput, /*cltvOutput,*/ csvOutput) multiSigOutput, /*cltvOutput,*/ csvOutput,
conditionalOutput)
} }
def nonP2SHOutput: Gen[BitcoinUTXOSpendingInfo] = { /** Only for use in constructing P2SH outputs */
Gen.oneOf(p2pkOutput, private def nonP2SHOutput: Gen[BitcoinUTXOSpendingInfo] = {
p2pkhOutput, Gen
multiSigOutput, /*cltvOutput,*/ csvOutput, .oneOf(p2pkOutput,
p2wpkhOutput, p2pkhOutput,
p2wshOutput) multiSigOutput, /*cltvOutput,*/ csvOutput,
conditionalOutput,
p2wpkhOutput,
p2wshOutput)
.suchThat(output =>
!ScriptGenerators.redeemScriptTooBig(output.scriptPubKey))
.suchThat {
case P2SHNestedSegwitV0UTXOSpendingInfo(_,
_,
_,
_,
_,
_,
witness: P2WSHWitnessV0,
_) =>
witness.stack.exists(_.length > ScriptInterpreter.MAX_PUSH_SIZE)
case _ => true
}
} }
def output: Gen[BitcoinUTXOSpendingInfo] = def output: Gen[BitcoinUTXOSpendingInfo] =
@ -57,6 +84,7 @@ sealed abstract class CreditingTxGen {
multiSigOutput, multiSigOutput,
p2shOutput, p2shOutput,
csvOutput, /*cltvOutput,*/ csvOutput, /*cltvOutput,*/
conditionalOutput,
p2wpkhOutput, p2wpkhOutput,
p2wshOutput) p2wshOutput)
@ -98,6 +126,31 @@ sealed abstract class CreditingTxGen {
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, multiSigOutput)) Gen.choose(min, max).flatMap(n => Gen.listOfN(n, multiSigOutput))
} }
@tailrec
private def noRelevantCLTV(spk: RawScriptPubKey): Boolean = {
spk match {
case _: CLTVScriptPubKey => false
case csv: CSVScriptPubKey => noRelevantCLTV(csv.nestedScriptPubKey)
case conditional: ConditionalScriptPubKey =>
noRelevantCLTV(conditional.trueSPK)
case _: RawScriptPubKey => true
}
}
def conditionalOutput: Gen[BitcoinUTXOSpendingInfo] = {
ScriptGenerators
.nonLocktimeConditionalScriptPubKey(ScriptGenerators.defaultMaxDepth)
//.suchThat { case (spk, _) => noRelevantCLTV(spk) }
.flatMap {
case (conditional, keys) =>
build(conditional, keys, None, None)
}
}
def conditionalOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = {
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, conditionalOutput))
}
def p2shOutput: Gen[BitcoinUTXOSpendingInfo] = nonP2SHOutput.flatMap { o => def p2shOutput: Gen[BitcoinUTXOSpendingInfo] = nonP2SHOutput.flatMap { o =>
CryptoGenerators.hashType.map { hashType => CryptoGenerators.hashType.map { hashType =>
val oldOutput = o.output val oldOutput = o.output
@ -111,7 +164,7 @@ sealed abstract class CreditingTxGen {
Some(redeemScript), Some(redeemScript),
o.scriptWitnessOpt, o.scriptWitnessOpt,
hashType, hashType,
ConditionalPath.NoConditionsLeft computeAllTrueConditionalPath(redeemScript, None, o.scriptWitnessOpt)
) )
} }
} }
@ -177,13 +230,17 @@ sealed abstract class CreditingTxGen {
def p2wpkhOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = def p2wpkhOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] =
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wpkhOutput)) Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wpkhOutput))
def p2wshOutput: Gen[BitcoinUTXOSpendingInfo] = nonP2WSHOutput.flatMap { def p2wshOutput: Gen[BitcoinUTXOSpendingInfo] =
case BitcoinUTXOSpendingInfo(_, txOutput, signer, _, _, _, _) => nonP2WSHOutput
val spk = txOutput.scriptPubKey .suchThat(output =>
val scriptWit = P2WSHWitnessV0(spk) !ScriptGenerators.redeemScriptTooBig(output.scriptPubKey))
val witSPK = P2WSHWitnessSPKV0(spk) .flatMap {
build(witSPK, signer, None, Some(scriptWit)) case BitcoinUTXOSpendingInfo(_, txOutput, signer, _, _, _, _) =>
} val spk = txOutput.scriptPubKey
val scriptWit = P2WSHWitnessV0(spk)
val witSPK = P2WSHWitnessSPKV0(spk)
build(witSPK, signer, None, Some(scriptWit))
}
def p2wshOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] = def p2wshOutputs: Gen[Seq[BitcoinUTXOSpendingInfo]] =
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wshOutput)) Gen.choose(min, max).flatMap(n => Gen.listOfN(n, p2wshOutput))
@ -230,6 +287,42 @@ sealed abstract class CreditingTxGen {
def randoms: Gen[Seq[BitcoinUTXOSpendingInfo]] = def randoms: Gen[Seq[BitcoinUTXOSpendingInfo]] =
Gen.choose(min, max).flatMap(n => Gen.listOfN(n, random)) Gen.choose(min, max).flatMap(n => Gen.listOfN(n, random))
private def computeAllTrueConditionalPath(
spk: ScriptPubKey,
redeemScript: Option[ScriptPubKey],
scriptWitness: Option[ScriptWitness]): ConditionalPath = {
spk match {
case conditional: ConditionalScriptPubKey =>
ConditionalPath.ConditionTrue(
computeAllTrueConditionalPath(conditional.trueSPK, None, None))
case lockTimeScriptPubKey: LockTimeScriptPubKey =>
computeAllTrueConditionalPath(lockTimeScriptPubKey.nestedScriptPubKey,
None,
None)
case _: RawScriptPubKey | _: P2WPKHWitnessSPKV0 =>
ConditionalPath.NoConditionsLeft
case _: P2SHScriptPubKey =>
redeemScript match {
case None =>
throw new IllegalArgumentException(
"Expected redeem script for P2SH")
case Some(script) =>
computeAllTrueConditionalPath(script, None, scriptWitness)
}
case _: P2WSHWitnessSPKV0 =>
scriptWitness match {
case Some(witness: P2WSHWitnessV0) =>
computeAllTrueConditionalPath(witness.redeemScript, None, None)
case _ =>
throw new IllegalArgumentException(
"Expected P2WSHWitness for P2WSH")
}
case _: UnassignedWitnessScriptPubKey =>
throw new IllegalArgumentException(
s"Unexpected unassigned witness SPK: $spk")
}
}
private def build( private def build(
spk: ScriptPubKey, spk: ScriptPubKey,
signers: Seq[Sign], signers: Seq[Sign],
@ -249,7 +342,7 @@ sealed abstract class CreditingTxGen {
redeemScript, redeemScript,
scriptWitness, scriptWitness,
hashType, hashType,
ConditionalPath.NoConditionsLeft computeAllTrueConditionalPath(spk, redeemScript, scriptWitness)
) )
} }
} }

View file

@ -3,12 +3,14 @@ package org.bitcoins.testkit.core.gen
import org.bitcoins.core.consensus.Consensus import org.bitcoins.core.consensus.Consensus
import org.bitcoins.core.crypto.{TransactionSignatureCreator, _} import org.bitcoins.core.crypto.{TransactionSignatureCreator, _}
import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits} import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits}
import org.bitcoins.core.number.UInt32 import org.bitcoins.core.number.{UInt32, UInt64}
import org.bitcoins.core.policy.Policy import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.CompactSizeUInt
import org.bitcoins.core.protocol.script.{P2SHScriptPubKey, _} import org.bitcoins.core.protocol.script.{P2SHScriptPubKey, _}
import org.bitcoins.core.protocol.transaction._ import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.script.constant.ScriptNumber import org.bitcoins.core.script.constant.ScriptNumber
import org.bitcoins.core.script.crypto.HashType import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.script.interpreter.ScriptInterpreter
import org.bitcoins.core.util.BitcoinSLogger import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.core.wallet.signer.{MultiSigSigner, P2PKHSigner, P2PKSigner} import org.bitcoins.core.wallet.signer.{MultiSigSigner, P2PKHSigner, P2PKSigner}
import org.bitcoins.core.wallet.utxo.{ import org.bitcoins.core.wallet.utxo.{
@ -26,7 +28,15 @@ import scala.concurrent.duration.DurationInt
//TODO: Need to provide generators for [[NonStandardScriptSignature]] and [[NonStandardScriptPubKey]] //TODO: Need to provide generators for [[NonStandardScriptSignature]] and [[NonStandardScriptPubKey]]
sealed abstract class ScriptGenerators extends BitcoinSLogger { sealed abstract class ScriptGenerators extends BitcoinSLogger {
val timeout = 5.seconds val timeout = 5.seconds
private val defaultMaxDepth = 2 val defaultMaxDepth: Int = 2
/** Since redeem scripts are pushed onto the stack, this function
* checks that the redeem script is not too large for a push operation.
*/
private[gen] def redeemScriptTooBig(redeemScript: ScriptPubKey): Boolean = {
redeemScript.compactSizeUInt.toInt + CompactSizeUInt(UInt64(
ScriptInterpreter.MAX_PUSH_SIZE)).bytes.length >= ScriptInterpreter.MAX_PUSH_SIZE
}
def p2pkScriptSignature: Gen[P2PKScriptSignature] = def p2pkScriptSignature: Gen[P2PKScriptSignature] =
for { for {
@ -220,6 +230,10 @@ sealed abstract class ScriptGenerators extends BitcoinSLogger {
def p2shScriptPubKey: Gen[(P2SHScriptPubKey, Seq[ECPrivateKey])] = def p2shScriptPubKey: Gen[(P2SHScriptPubKey, Seq[ECPrivateKey])] =
for { for {
(randomScriptPubKey, privKeys) <- randomNonP2SHScriptPubKey (randomScriptPubKey, privKeys) <- randomNonP2SHScriptPubKey
.suchThat {
case (spk, _) =>
!redeemScriptTooBig(spk)
}
p2sh = P2SHScriptPubKey(randomScriptPubKey) p2sh = P2SHScriptPubKey(randomScriptPubKey)
} yield (p2sh, privKeys) } yield (p2sh, privKeys)
@ -268,9 +282,14 @@ sealed abstract class ScriptGenerators extends BitcoinSLogger {
} yield (P2WPKHWitnessSPKV0(privKey.publicKey), Seq(privKey)) } yield (P2WPKHWitnessSPKV0(privKey.publicKey), Seq(privKey))
def p2wshSPKV0: Gen[(P2WSHWitnessSPKV0, Seq[ECPrivateKey])] = def p2wshSPKV0: Gen[(P2WSHWitnessSPKV0, Seq[ECPrivateKey])] =
randomNonP2SHScriptPubKey.map { spk => randomNonP2SHScriptPubKey
(P2WSHWitnessSPKV0(spk._1), spk._2) .suchThat {
} case (spk, _) =>
!redeemScriptTooBig(spk)
}
.map { spk =>
(P2WSHWitnessSPKV0(spk._1), spk._2)
}
def witnessScriptPubKeyV0: Gen[(WitnessScriptPubKeyV0, Seq[ECPrivateKey])] = def witnessScriptPubKeyV0: Gen[(WitnessScriptPubKeyV0, Seq[ECPrivateKey])] =
Gen.oneOf(p2wpkhSPKV0, p2wshSPKV0) Gen.oneOf(p2wpkhSPKV0, p2wshSPKV0)