Create signedCSVScriptSignature and spendableCSVTransaction generators, add CSVScriptSignature trait and object, added condition to fail CSV if txInput sequence has lockTimeDisableFlag set

This commit is contained in:
Tom McCabe 2016-09-08 14:10:21 -05:00
parent f7b209709b
commit b13d42f9bf
6 changed files with 104 additions and 25 deletions

View file

@ -71,7 +71,7 @@ trait TransactionSignatureChecker extends BitcoinSLogger {
val sigsRemoved = removeSignaturesFromScript(s.signatures, s.redeemScript.asm)
sigsRemoved
case _ : P2PKHScriptSignature | _ : P2PKScriptSignature | _ : NonStandardScriptSignature
| _ : MultiSignatureScriptSignature | _ : CLTVScriptSignature | EmptyScriptSignature =>
| _ : MultiSignatureScriptSignature | _ : CLTVScriptSignature | _ : CSVScriptSignature | EmptyScriptSignature =>
logger.debug("Script before sigRemoved: " + script)
logger.debug("Signature: " + signature)
logger.debug("PubKey: " + pubKey)
@ -147,10 +147,7 @@ trait TransactionSignatureChecker extends BitcoinSLogger {
/**
* Removes the given digital signature from the list of script tokens if it exists
*
* @param signature
* @param script
* Removes the given [[ECDigitalSignature]] from the list of [[ScriptToken]] if it exists
* @return
*/
def removeSignatureFromScript(signature : ECDigitalSignature, script : Seq[ScriptToken]) : Seq[ScriptToken] = {
@ -165,10 +162,7 @@ trait TransactionSignatureChecker extends BitcoinSLogger {
}
/**
* Removes the list of digital signatures from the list of script tokens
*
* @param sigs
* @param script
* Removes the list of [[ECDigitalSignature]] from the list of [[ScriptToken]]
* @return
*/
def removeSignaturesFromScript(sigs : Seq[ECDigitalSignature], script : Seq[ScriptToken]) : Seq[ScriptToken] = {

View file

@ -339,11 +339,18 @@ object P2PKScriptPubKey extends Factory[P2PKScriptPubKey] {
sealed trait CLTVScriptPubKey extends ScriptPubKey {
/**
* Determines the nested ScriptPubKey inside the CLTVScriptPubKey
*
* @return
*/
def scriptPubKeyAfterCLTV : ScriptPubKey = ScriptPubKey(asm.slice(4, asm.length))
}
/**
* The absolute CLTV-LockTime value (i.e. the output will remain unspendable until this timestamp or block height)
* @return
*/
def locktime : UInt32 = {
val hex = BitcoinSUtil.flipEndianess(asm(1).hex)
UInt32(hex)
}}
object CLTVScriptPubKey extends Factory[CLTVScriptPubKey] {
private case class CLTVScriptPubKeyImpl(hex : String) extends CLTVScriptPubKey
@ -383,13 +390,16 @@ object CLTVScriptPubKey extends Factory[CLTVScriptPubKey] {
sealed trait CSVScriptPubKey extends ScriptPubKey {
/**
* Determines the nested ScriptPubKey inside the CSVScriptPubKey
*
* @return
*/
def scriptPubKeyAfterCSV : ScriptPubKey = ScriptPubKey(asm.slice(4, asm.length))
/**
* The relative CSV-LockTime value (i.e. the amount of time the output should remain unspendable)
* @return
*/
def locktime : UInt32 = {
val hex = BitcoinSUtil.flipEndianess(asm.head.hex)
val hex = BitcoinSUtil.flipEndianess(asm(1).hex)
UInt32(hex)
}
}
@ -424,7 +434,7 @@ object CSVScriptPubKey extends Factory[CSVScriptPubKey] {
}
}
trait NonStandardScriptPubKey extends ScriptPubKey
sealed trait NonStandardScriptPubKey extends ScriptPubKey
object NonStandardScriptPubKey extends Factory[NonStandardScriptPubKey] {
private case class NonStandardScriptPubKeyImpl(hex : String) extends NonStandardScriptPubKey

View file

@ -3,6 +3,7 @@ package org.bitcoins.core.protocol.script
import org.bitcoins.core.crypto.{ECDigitalSignature, ECPublicKey}
import org.bitcoins.core.number.Int32
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.protocol.script.CLTVScriptSignature.CLTVScriptSignatureImpl
import org.bitcoins.core.script.constant._
import org.bitcoins.core.script.crypto.{HashType, SIGHASH_ALL}
import org.bitcoins.core.serializers.script.{RawScriptSignatureParser, ScriptParser}
@ -58,8 +59,6 @@ object NonStandardScriptSignature extends Factory[NonStandardScriptSignature] {
}
}
/**
* P2PKH script signatures have only one public key
* https://bitcoin.org/en/developer-guide#pay-to-public-key-hash-p2pkh
@ -120,6 +119,7 @@ object P2PKHScriptSignature extends Factory[P2PKHScriptSignature] {
/**
* Determines if the given asm matches a [[P2PKHScriptSignature]]
*
* @param asm
* @return
*/
@ -215,6 +215,7 @@ object P2SHScriptSignature extends Factory[P2SHScriptSignature] with BitcoinSLog
/**
* Tests if the given asm tokens are a [[P2SHScriptSignature]]
*
* @param asm
* @return
*/
@ -376,6 +377,7 @@ object P2PKScriptSignature extends Factory[P2PKScriptSignature] {
/**
* P2PK scriptSigs always have the pattern [pushop, digitalSignature]
*
* @param asm
* @return
*/
@ -396,16 +398,60 @@ sealed trait CLTVScriptSignature extends ScriptSignature {
object CLTVScriptSignature {
private case class CLTVScriptSignatureImpl(scriptSig : ScriptSignature) extends CLTVScriptSignature
def apply(scriptPubKey: ScriptPubKey, sigs : Seq[ECDigitalSignature], pubKeys : Seq[ECPublicKey]) : CLTVScriptSignature = scriptPubKey match {
/**
* Creates a CLTVScriptSignature out the [[ScriptPubKey]] we are satisfying, a sequence of [[ECDigitalSignature]], and a sequence
* of [[ECPublicKey]] needed to satisfy the scriptPubKey. If a [[P2SHScriptPubKey]] is provided, a redeemScript must also be provided.
* @return
*/
def apply(scriptPubKey: ScriptPubKey, sigs : Seq[ECDigitalSignature], pubKeys : Seq[ECPublicKey], redeemScript : Option[ScriptPubKey]) : CLTVScriptSignature = scriptPubKey match {
case p2pkScriptPubKey : P2PKScriptPubKey => CLTVScriptSignatureImpl(P2PKScriptSignature(sigs.head))
case p2pkhScriptPubKey : P2PKHScriptPubKey => CLTVScriptSignatureImpl(P2PKHScriptSignature(sigs.head, pubKeys.head))
case multiSigScriptPubKey : MultiSignatureScriptPubKey => CLTVScriptSignatureImpl(MultiSignatureScriptSignature(sigs))
case cltvScriptPubKey : CLTVScriptPubKey => apply(cltvScriptPubKey.scriptPubKeyAfterCLTV, sigs, pubKeys)
case p2shScriptPubKey : P2SHScriptPubKey => ???
case cltvScriptPubKey : CLTVScriptPubKey => apply(cltvScriptPubKey.scriptPubKeyAfterCLTV, sigs, pubKeys, redeemScript)
case csvScriptPubKey : CSVScriptPubKey => apply(csvScriptPubKey.scriptPubKeyAfterCSV, sigs, pubKeys, redeemScript)
case p2shScriptPubKey : P2SHScriptPubKey =>
require(redeemScript.isDefined, "If the underlying scriptSig is a P2SHScriptSignature, a redeemScript must be defined.")
val cltvScriptSigBeforeRedeemScript = apply(redeemScript.get, sigs, pubKeys, None)
CLTVScriptSignatureImpl(P2SHScriptSignature(cltvScriptSigBeforeRedeemScript, redeemScript.get))
case EmptyScriptPubKey => CLTVScriptSignatureImpl(EmptyScriptSignature)
case nonstandard : NonStandardScriptPubKey => throw new IllegalArgumentException("A NonStandardScriptSignature cannot be" +
"the underlying scriptSig in a CLTVScriptSignature.")
}
}
sealed trait CSVScriptSignature extends ScriptSignature {
def scriptSig : ScriptSignature
override def signatures : Seq[ECDigitalSignature] = scriptSig.signatures
override def hex = scriptSig.hex
}
object CSVScriptSignature {
private case class CSVScriptSignatureImpl(scriptSig : ScriptSignature) extends CSVScriptSignature
/**
* Creates a CSVScriptSignature out the [[ScriptPubKey]] we are satisfying, a sequence of [[ECDigitalSignature]], and a sequence
* of [[ECPublicKey]] needed to satisfy the scriptPubKey. If a [[P2SHScriptPubKey]] is provided, a redeemScript must also be provided.
* @return
*/
def apply(scriptPubKey: ScriptPubKey, sigs : Seq[ECDigitalSignature], pubKeys : Seq[ECPublicKey], redeemScript : Option[ScriptPubKey]) : CSVScriptSignature = scriptPubKey match {
case p2pkScriptPubKey : P2PKScriptPubKey => CSVScriptSignatureImpl(P2PKScriptSignature(sigs.head))
case p2pkhScriptPubKey : P2PKHScriptPubKey => CSVScriptSignatureImpl(P2PKHScriptSignature(sigs.head, pubKeys.head))
case multiSigScriptPubKey : MultiSignatureScriptPubKey => CSVScriptSignatureImpl(MultiSignatureScriptSignature(sigs))
case cltvScriptPubKey : CLTVScriptPubKey => apply(cltvScriptPubKey.scriptPubKeyAfterCLTV, sigs, pubKeys, redeemScript)
case csvScriptPubKey : CSVScriptPubKey => apply(csvScriptPubKey.scriptPubKeyAfterCSV, sigs, pubKeys, redeemScript)
case p2shScriptPubKey : P2SHScriptPubKey =>
require(redeemScript.isDefined, "If the underlying scriptSig is a P2SHScriptSignature, a redeemScript must be defined.")
val cltvScriptSigBeforeRedeemScript = apply(redeemScript.get, sigs, pubKeys, None)
CSVScriptSignatureImpl(P2SHScriptSignature(cltvScriptSigBeforeRedeemScript, redeemScript.get))
case EmptyScriptPubKey => CSVScriptSignatureImpl(EmptyScriptSignature)
case nonstandard : NonStandardScriptPubKey => throw new IllegalArgumentException("A NonStandardScriptSignature cannot be" +
"the underlying scriptSig in a CSVScriptSignature.")
}
}
/**
* Represents the empty script signature

View file

@ -106,11 +106,11 @@ trait LockTimeInterpreter extends BitcoinSLogger {
case s : ScriptNumber =>
if (s.bytes.size > 5) {
//if the number size is larger than 5 bytes the number is invalid
logger.error("The OP_CSV value in the script was larger than 5 bytes in size.")
ScriptProgram(program,ScriptErrorUnknownError)
} else if (checkSequence(program,s)) {
ScriptProgram(program, program.stack, program.script.tail)
} else {
logger.error("Stack top sequence and transaction input's sequence number comparison failed")
ScriptProgram(program, ScriptErrorUnsatisfiedLocktime)
}
case s : ScriptConstant =>
@ -125,7 +125,7 @@ trait LockTimeInterpreter extends BitcoinSLogger {
/**
* Mimics this function inside of bitcoin core
* https://github.com/bitcoin/bitcoin/blob/e26b62093ae21e89ed7d36a24a6b863f38ec631d/src/script/interpreter.cpp#L1196
*
* https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki#specification
* @param program the program whose transaction input's sequence is being compared
* @param nSequence the script number on the stack top to compare to the input's sequence number
* @return if the given script number is valid or not
@ -134,13 +134,24 @@ trait LockTimeInterpreter extends BitcoinSLogger {
val inputIndex = program.txSignatureComponent.inputIndex.toInt
logger.debug("inputIndex: " + inputIndex)
val transaction = program.txSignatureComponent.transaction
// Relative lock times are supported by comparing the passed
// in operand to the sequence number of the input.
val txToSequence : Int64 = Int64(transaction.inputs(inputIndex).sequence.underlying)
// Fail if the transaction's version number is not set high
// enough to trigger BIP 68 rules.
if (program.txSignatureComponent.transaction.version < UInt32(2)) {
logger.error("Transaction version is too low for OP_CSV")
logger.error("OP_CSV fails the script if the transaction's version is less than 2.")
return false
}
// Sequence numbers with their most significant bit set are not
// consensus constrained. Testing that the transaction's sequence
// number do not have this bit set prevents using this property
// to get around a CHECKSEQUENCEVERIFY check.
if (!isLockTimeBitOff(txToSequence)) return false
val nLockTimeMask : UInt32 = TransactionConstants.sequenceLockTimeTypeFlag | TransactionConstants.sequenceLockTimeMask
val txToSequenceMasked : Int64 = Int64(txToSequence.underlying & nLockTimeMask.underlying)
val nSequenceMasked : ScriptNumber = nSequence & Int64(nLockTimeMask.underlying)
@ -150,6 +161,7 @@ trait LockTimeInterpreter extends BitcoinSLogger {
logger.info("nSequence: " + nSequence)
logger.info("nSequenceMasked: " + nSequenceMasked)
logger.info("Sequence locktime flag: " + TransactionConstants.sequenceLockTimeTypeFlag)
// There are two kinds of nSequence: lock-by-blockheight
// and lock-by-blocktime, distinguished by whether
// nSequenceMasked < CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG.
@ -170,7 +182,8 @@ trait LockTimeInterpreter extends BitcoinSLogger {
// Now that we know we're comparing apples-to-apples, the
// comparison is a simple numeric one.
if (nSequenceMasked > Int64(txToSequenceMasked.underlying)) {
logger.error("OP_CSV fails because locktime in transaction has not been met yet")
logger.error("OP_CSV fails because relative locktime in transaction has not been met yet. " +
"(OP_CSV value was greater than the txInput's sequence)")
return false
}
@ -221,9 +234,10 @@ trait LockTimeInterpreter extends BitcoinSLogger {
/**
* The script number on the stack has the disable flag (1 << 31) unset
*
* @param s
* @return
*/
def isLockTimeBitOff(s : ScriptNumber) : Boolean = (s.underlying & TransactionConstants.locktimeDisabledFlag.underlying) == 0
def isLockTimeBitOff(num : Int64) : Boolean = isLockTimeBitOff(ScriptNumber(num.hex))
}

View file

@ -71,5 +71,14 @@ class TransactionSignatureCreatorSpec extends Properties("TransactionSignatureCr
Seq(ScriptErrorUnsatisfiedLocktime).contains(result)
}
property("generate a valid signature for a valid and spendable csv transaction") =
Prop.forAllNoShrink(TransactionGenerators.spendableCSVTransaction :| "csv") {
case (txSignatureComponent: TransactionSignatureComponent, keys, scriptNumber, sequence) =>
//run it through the interpreter
val program = ScriptProgram(txSignatureComponent)
val result = ScriptInterpreter.run(program)
Seq(ScriptOk).contains(result)
}
}

View file

@ -1,7 +1,7 @@
package org.bitcoins.core.protocol.script
import org.bitcoins.core.crypto.{DoubleSha256Digest, ECDigitalSignature, TransactionSignatureSerializer}
import org.bitcoins.core.crypto._
import org.bitcoins.core.number.Int32
import org.bitcoins.core.protocol.script.testprotocol.SignatureHashTestCase
import org.bitcoins.core.script.crypto.{HashType, SIGHASH_ALL, SIGHASH_SINGLE}
@ -116,6 +116,12 @@ class ScriptSignatureTest extends FlatSpec with MustMatchers {
val flipHash = BitcoinSUtil.flipEndianess(testCase.hash.hex)
hashForSig must be (DoubleSha256Digest(flipHash))
}
}
it must "create a cltvScriptSig with the correct underlying scriptSig" in {
val cltvScriptPubKey = CLTVScriptPubKey("04e71bbe57b17576a914da88dc82530f0a4d1327dcfe75cc60c44277532c88ac")
val pubKey = ECPublicKey("039ba48e162b1f47246f4ce9dc40f197fab7bde11da1b2fe9ac21113959e9f381b")
val sig = ECDigitalSignature("3045022100d71cfe32fa4545c5a0fd665b3701eb458a1bacbba868a05fa703fd1fa4b4f5c502204ee706334f976d0bee9b0f0ff919c1dfe9ba027993bf3e39fc03416ba4255b2401")
CLTVScriptSignature(cltvScriptPubKey, Seq(sig), Seq(pubKey), None).scriptSig.isInstanceOf[P2PKHScriptSignature]
}
}