Adding property to make sure multisig works in CSVEscrowTimeoutScriptPubKey

This commit is contained in:
Chris Stewart 2017-03-31 12:22:12 -05:00
parent 02e94ba1b6
commit 948b0021fe
6 changed files with 158 additions and 19 deletions

View file

@ -7,7 +7,7 @@ import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.script.ScriptSettings
import org.bitcoins.core.script.bitwise.{OP_EQUAL, OP_EQUALVERIFY}
import org.bitcoins.core.script.constant.{BytesToPushOntoStack, _}
import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.script.control.{OP_ELSE, OP_ENDIF, OP_IF, OP_RETURN}
import org.bitcoins.core.script.crypto.{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_HASH160}
import org.bitcoins.core.script.locktime.{OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY}
import org.bitcoins.core.script.reserved.UndefinedOP_NOP
@ -515,6 +515,7 @@ object ScriptPubKey extends Factory[ScriptPubKey] with BitcoinSLogger {
case _ if CSVScriptPubKey.isCSVScriptPubKey(asm) => CSVScriptPubKey(asm)
case _ if WitnessScriptPubKey.isWitnessScriptPubKey(asm) => WitnessScriptPubKey(asm).get
case _ if WitnessCommitment.isWitnessCommitment(asm) => WitnessCommitment(asm)
case _ if CSVEscrowTimeoutScriptPubKey.isValidCSVEscrowTimeout(asm) => CSVEscrowTimeoutScriptPubKey.fromAsm(asm)
case _ => NonStandardScriptPubKey(asm)
}
@ -664,3 +665,58 @@ object WitnessCommitment extends ScriptFactory[WitnessCommitment] {
}
}
}
/** Represents a [[ScriptPubKey]] that either times out allowing Alice to spend from the scriptpubkey
* or allows a federation to spend from the escrow
* [[https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki#contracts-with-expiration-deadlines]]
* Format: OP_IF <multsig scriptpubkey> OP_ELSE <csv scriptPubKey> OP_ENDIF
*/
sealed trait CSVEscrowTimeoutScriptPubKey extends ScriptPubKey {
/** The [[MultiSignatureScriptPubKey]] that can be used to spend funds */
def escrow: MultiSignatureScriptPubKey = {
val escrowAsm = asm.slice(1,opElseIndex)
MultiSignatureScriptPubKey(escrowAsm)
}
/** The [[CSVScriptPubKey]] that you can spend funds from after a certain timeout */
def timeout: CSVScriptPubKey = {
val timeoutAsm = asm.slice(opElseIndex+1, asm.length-1)
CSVScriptPubKey(timeoutAsm)
}
private def opElseIndex: Int = {
val idx = asm.indexOf(OP_ELSE)
require(idx != -1, "CSVEscrowWithTimeout has to contain OP_ELSE asm token")
idx
}
}
object CSVEscrowTimeoutScriptPubKey extends ScriptFactory[CSVEscrowTimeoutScriptPubKey] {
private case class CSVEscrowTimeoutScriptPubKeyImpl(hex: String) extends CSVEscrowTimeoutScriptPubKey
override def fromBytes(bytes: Seq[Byte]): CSVEscrowTimeoutScriptPubKey = {
val asm = RawScriptPubKeyParser.read(bytes).asm
fromAsm(asm)
}
override def fromAsm(asm: Seq[ScriptToken]): CSVEscrowTimeoutScriptPubKey = {
buildScript(asm, CSVEscrowTimeoutScriptPubKeyImpl(_), isValidCSVEscrowTimeout(_), "Given asm was not a valid CSVEscrowTimeout, got: " + asm)
}
def isValidCSVEscrowTimeout(asm: Seq[ScriptToken]): Boolean = {
val opElseIndex = asm.indexOf(OP_ELSE)
val correctControlStructure = asm.headOption.contains(OP_IF) && asm.last == OP_ENDIF && opElseIndex != -1
if (correctControlStructure) {
val escrowAsm = asm.slice(1,opElseIndex)
val escrow = Try(MultiSignatureScriptPubKey(escrowAsm))
val timeoutAsm = asm.slice(opElseIndex+1, asm.length-1)
val timeout = Try(CSVScriptPubKey(timeoutAsm))
escrow.isSuccess && timeout.isSuccess
} else false
}
def apply(escrow: MultiSignatureScriptPubKey, timeout: CSVScriptPubKey): CSVEscrowTimeoutScriptPubKey = {
fromAsm(Seq(OP_IF) ++ escrow.asm ++ Seq(OP_ELSE) ++ timeout.asm ++ Seq(OP_ENDIF))
}
}

View file

@ -210,12 +210,9 @@ object P2SHScriptSignature extends ScriptFactory[P2SHScriptSignature] {
/** Detects if the given script token is a redeem script */
def isRedeemScript(token : ScriptToken) : Boolean = {
logger.debug("Checking if last token is redeem script")
val redeemScriptTry : Try[ScriptPubKey] = parseRedeemScript(token)
redeemScriptTry match {
case Success(redeemScript) =>
logger.debug("Possible redeemScript: " + redeemScript.asm)
logger.debug("Redeem script: " + redeemScript)
redeemScript match {
case x : P2PKHScriptPubKey => true
case x : MultiSignatureScriptPubKey => true
@ -299,8 +296,6 @@ object MultiSignatureScriptSignature extends ScriptFactory[MultiSignatureScriptS
val restOfScriptIsPushOpsOrScriptConstants = asm.tail.map(
token => token.isInstanceOf[ScriptConstant] || StackPushOperationFactory.isPushOperation(token)
).exists(_ == false)
logger.debug("First number is script op: " + firstTokenIsScriptNumberOperation)
logger.debug("tail is true: " +restOfScriptIsPushOpsOrScriptConstants )
firstTokenIsScriptNumberOperation && !restOfScriptIsPushOpsOrScriptConstants
}
}
@ -423,11 +418,13 @@ object CSVScriptSignature extends Factory[CSVScriptSignature] {
* @return
*/
def apply(scriptPubKey: ScriptPubKey, sigs : Seq[ECDigitalSignature], pubKeys : Seq[ECPublicKey]) : CSVScriptSignature = scriptPubKey match {
case p2pkScriptPubKey : P2PKScriptPubKey => CSVScriptSignature(P2PKScriptSignature(sigs.head))
case p2pkhScriptPubKey : P2PKHScriptPubKey => CSVScriptSignature(P2PKHScriptSignature(sigs.head, pubKeys.head))
case multiSigScriptPubKey : MultiSignatureScriptPubKey => CSVScriptSignature(MultiSignatureScriptSignature(sigs))
case _: P2PKScriptPubKey => CSVScriptSignature(P2PKScriptSignature(sigs.head))
case _: P2PKHScriptPubKey => CSVScriptSignature(P2PKHScriptSignature(sigs.head, pubKeys.head))
case _: MultiSignatureScriptPubKey => CSVScriptSignature(MultiSignatureScriptSignature(sigs))
case cltvScriptPubKey : CLTVScriptPubKey => CSVScriptSignature(cltvScriptPubKey.scriptPubKeyAfterCLTV, sigs, pubKeys)
case csvScriptPubKey : CSVScriptPubKey => CSVScriptSignature(csvScriptPubKey.scriptPubKeyAfterCSV, sigs, pubKeys)
case csvEscrowTimeout: CSVEscrowTimeoutScriptPubKey =>
CSVScriptSignature(csvEscrowTimeout.timeout.scriptPubKeyAfterCSV,sigs,pubKeys)
case EmptyScriptPubKey => CSVScriptSignature(EmptyScriptSignature)
case _: WitnessScriptPubKeyV0 | _ : UnassignedWitnessScriptPubKey =>
//bare segwit always has an empty script sig, see BIP141
@ -480,6 +477,10 @@ object ScriptSignature extends Factory[ScriptSignature] with BitcoinSLogger {
case _: NonStandardScriptPubKey => Try(NonStandardScriptSignature.fromAsm(tokens))
case s : CLTVScriptPubKey => fromScriptPubKey(tokens, s.scriptPubKeyAfterCLTV)
case s : CSVScriptPubKey => fromScriptPubKey(tokens, s.scriptPubKeyAfterCSV)
case escrowWithTimeout : CSVEscrowTimeoutScriptPubKey =>
val isMultiSig = BitcoinScriptUtil.castToBool(tokens.head)
if (isMultiSig) Try(MultiSignatureScriptSignature.fromAsm(tokens.tail))
else Try(CSVEscrowTimeoutScriptSignature.fromAsm(tokens,escrowWithTimeout))
case _: WitnessScriptPubKeyV0 | _: UnassignedWitnessScriptPubKey => Success(EmptyScriptSignature)
case EmptyScriptPubKey =>
if (tokens.isEmpty) Success(EmptyScriptSignature) else Try(NonStandardScriptSignature.fromAsm(tokens))
@ -491,3 +492,73 @@ object ScriptSignature extends Factory[ScriptSignature] with BitcoinSLogger {
}
}
/** [[ScriptSignature]] that spends a [[CSVEscrowTimeoutScriptPubKey]], the underlying script signature can be
* a [[MultiSignatureScriptSignature]] or a [[CSVScriptSignature]] as those are te two underlying scripts
* of a [[CSVEscrowTimeoutScriptPubKey]]
*
* If the last element of the [[asm]] evaluates to true, it is a scriptsig that attempts to spend the escrow
* if the last element of the [[asm]] evaluates to false, it is a scriptsig that attempts to spend the timeout
* */
sealed trait CSVEscrowTimeoutScriptSignature extends ScriptSignature {
def scriptSig: ScriptSignature = ScriptSignature(hex)
override def signatures = scriptSig.signatures
override def toString = "CSVEscrowWithTimeoutScriptSignature(" + scriptSig + ")"
/** Checks if the given asm fulfills the timeout or escrow of the [[CSVEscrowTimeoutScriptPubKey]] */
def isEscrow: Boolean = BitcoinScriptUtil.castToBool(asm.last)
def isTimeout: Boolean = !isEscrow
}
object CSVEscrowTimeoutScriptSignature extends Factory[CSVEscrowTimeoutScriptSignature] {
private case class CSVEscrowTimeoutScriptSignatureImpl(hex: String) extends CSVEscrowTimeoutScriptSignature
override def fromBytes(bytes: Seq[Byte]): CSVEscrowTimeoutScriptSignature = {
CSVEscrowTimeoutScriptSignatureImpl(BitcoinSUtil.encodeHex(bytes))
}
def fromAsm(asm: Seq[ScriptToken], scriptPubKey: CSVEscrowTimeoutScriptPubKey): CSVEscrowTimeoutScriptSignature = {
require(isValidCSVEscrowTimeoutScriptSig(asm,scriptPubKey), "Given asm was not a CSVEscrowWithTimeoutScriptSignature, got: " + asm)
val asmHex = asm.map(_.hex).mkString
val c = CompactSizeUInt.calculateCompactSizeUInt(asmHex)
val fullHex = c.hex + asmHex
fromHex(fullHex)
}
def fromAsm(asm: Seq[ScriptToken]): CSVEscrowTimeoutScriptSignature = {
require(asm.nonEmpty)
val nestedScriptSig = if (BitcoinScriptUtil.castToBool(asm.head)) {
require(MultiSignatureScriptSignature.isMultiSignatureScriptSignature(asm.tail), "Need multisigScriptSig, got: " + asm.tail)
MultiSignatureScriptSignature.fromAsm(asm.tail)
} else {
CSVScriptSignature(ScriptSignature.fromAsm(asm.tail))
}
val bytes = asm.head.bytes ++ nestedScriptSig.asmBytes
val c = CompactSizeUInt.calculateCompactSizeUInt(bytes)
val fullBytes = c.bytes ++ bytes
fromBytes(fullBytes)
}
def isValidCSVEscrowTimeoutScriptSig(asm: Seq[ScriptToken],
scriptPubKey: CSVEscrowTimeoutScriptPubKey): Boolean = {
if (MultiSignatureScriptSignature.isMultiSignatureScriptSignature(asm.tail)) {
true
} else {
val locktimeScript = scriptPubKey.timeout.scriptPubKeyAfterCSV
Try(ScriptSignature(asm,locktimeScript)).isSuccess
}
}
def apply(scriptSig: ScriptSignature): CSVEscrowTimeoutScriptSignature = fromBytes(scriptSig.bytes)
def apply(multiSigScriptSig: MultiSignatureScriptSignature): CSVEscrowTimeoutScriptSignature = {
val asm = multiSigScriptSig.asm ++ Seq(OP_1)
fromAsm(asm)
}
def apply(csv: CSVScriptSignature): CSVEscrowTimeoutScriptSignature = {
val asm = csv.asm ++ Seq(OP_0)
fromAsm(asm)
}
}

View file

@ -43,7 +43,8 @@ trait ControlOperationsInterpreter extends BitcoinSLogger {
//remove OP_ELSE from binary tree
val newTreeWithoutOpElse = removeFirstOpElse(binaryTree)
val newScript = newTreeWithoutOpElse.toList
logger.debug("New script after removing OP_ELSE branch " + newScript)
logger.debug("New script after removing OP_ELSE branch " + newScript.tail)
logger.debug("New stack after removing OP_ELSE branch: " + program.stack.tail)
ScriptProgram(program, program.stack.tail,newScript.tail)
} else {
logger.debug("OP_IF stack top was false")

View file

@ -76,8 +76,10 @@ trait ScriptInterpreter extends CryptoInterpreter with StackInterpreter with Con
case p2sh : P2SHScriptPubKey =>
if (p2shEnabled) executeP2shScript(scriptSigExecutedProgram, program, p2sh)
else scriptPubKeyExecutedProgram
case _ : P2PKHScriptPubKey | _: P2PKScriptPubKey | _: MultiSignatureScriptPubKey | _: CSVScriptPubKey |
_ : CLTVScriptPubKey | _ : NonStandardScriptPubKey | _ : WitnessCommitment | EmptyScriptPubKey =>
case _: P2PKHScriptPubKey | _: P2PKScriptPubKey | _: MultiSignatureScriptPubKey | _: CSVScriptPubKey
| _: CLTVScriptPubKey | _: NonStandardScriptPubKey | _: WitnessCommitment
| _: CSVEscrowTimeoutScriptPubKey | EmptyScriptPubKey =>
logger.info("")
scriptPubKeyExecutedProgram
}
}
@ -161,8 +163,9 @@ trait ScriptInterpreter extends CryptoInterpreter with StackInterpreter with Con
//treat the segwit scriptpubkey as any other redeem script
run(scriptPubKeyExecutedProgram,stack,w)
}
case s @ (_ : P2SHScriptPubKey | _ : P2PKHScriptPubKey | _ : P2PKScriptPubKey | _ : MultiSignatureScriptPubKey |
_ : CLTVScriptPubKey | _ : CSVScriptPubKey | _: NonStandardScriptPubKey | _ : WitnessCommitment | EmptyScriptPubKey) =>
case s @ (_ : P2SHScriptPubKey | _ : P2PKHScriptPubKey | _ : P2PKScriptPubKey | _ : MultiSignatureScriptPubKey
| _ : CLTVScriptPubKey | _ : CSVScriptPubKey | _: NonStandardScriptPubKey | _ : WitnessCommitment
| _: CSVEscrowTimeoutScriptPubKey | EmptyScriptPubKey) =>
logger.debug("redeemScript: " + s.asm)
run(scriptPubKeyExecutedProgram,stack,s)
}
@ -515,8 +518,9 @@ trait ScriptInterpreter extends CryptoInterpreter with StackInterpreter with Con
case _ : P2SHScriptPubKey =>
val p2shScriptSig = P2SHScriptSignature(txSigComponent.scriptSignature.bytes)
p2shScriptSig.redeemScript.isInstanceOf[WitnessScriptPubKey]
case _ : CLTVScriptPubKey | _ : CSVScriptPubKey | _ : MultiSignatureScriptPubKey | _ : NonStandardScriptPubKey |
_ : P2PKScriptPubKey | _ : P2PKHScriptPubKey | _ : WitnessCommitment | EmptyScriptPubKey =>
case _: CLTVScriptPubKey | _: CSVScriptPubKey | _: MultiSignatureScriptPubKey | _: NonStandardScriptPubKey
| _: P2PKScriptPubKey | _: P2PKHScriptPubKey | _: WitnessCommitment
| _: CSVEscrowTimeoutScriptPubKey | EmptyScriptPubKey =>
w.witness.stack.isEmpty
}
!witnessedUsed

View file

@ -306,9 +306,9 @@ trait BitcoinScriptUtil extends BitcoinSLogger {
//with a witness scriptPubKey
script
}
case _ : P2PKHScriptPubKey | _ : P2PKScriptPubKey | _ : MultiSignatureScriptPubKey |
_ : NonStandardScriptPubKey | _ : CLTVScriptPubKey | _ : CSVScriptPubKey | _ : WitnessCommitment | EmptyScriptPubKey =>
script
case _: P2PKHScriptPubKey | _: P2PKScriptPubKey | _: MultiSignatureScriptPubKey
| _: NonStandardScriptPubKey | _: CLTVScriptPubKey | _: CSVScriptPubKey
| _: WitnessCommitment | _: CSVEscrowTimeoutScriptPubKey | EmptyScriptPubKey => script
}
/** Removes the given [[ECDigitalSignature]] from the list of [[ScriptToken]] if it exists. */

View file

@ -138,4 +138,11 @@ class TransactionSignatureCreatorSpec extends Properties("TransactionSignatureCr
if (result != ScriptOk) logger.warn("Result: " + result)
Seq(ScriptErrorPushSize, ScriptOk).contains(result)
}
property("generate a valid signature for a csv escrow timeout transaction") =
Prop.forAll(TransactionGenerators.spendableCSVMultiSigEscrowTimeoutTransaction) { txSigComponent: TransactionSignatureComponent =>
val program = ScriptProgram(txSigComponent)
val result = ScriptInterpreter.run(program)
result == ScriptOk
}
}