From 8b8b8dd34c59fd62e4ce9552e3f244b23913d744 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Mon, 2 May 2016 21:10:29 -0500 Subject: [PATCH 1/4] Implementating base functionality for when OP_CSV should fail or be treated as a NOP --- .../bitcoins/script/constant/Constants.scala | 4 ++ .../script/locktime/LockTimeInterpreter.scala | 55 +++++++++++++++++-- .../script/locktime/LocktimeOperations.scala | 19 ++++++- .../locktime/LockTimeInterpreterTest.scala | 45 +++++++++++++-- 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/main/scala/org/bitcoins/script/constant/Constants.scala b/src/main/scala/org/bitcoins/script/constant/Constants.scala index 04cdfccb15..6c12346b8b 100644 --- a/src/main/scala/org/bitcoins/script/constant/Constants.scala +++ b/src/main/scala/org/bitcoins/script/constant/Constants.scala @@ -80,6 +80,10 @@ sealed trait ScriptNumber extends ScriptConstant { def > (that : ScriptNumber) : Boolean = num > that.num def >= (that : ScriptNumber) : Boolean = num >= that.num + def &(that : ScriptNumber) : ScriptNumber = ScriptNumber(num & that.num) + + def | (that : ScriptNumber) : ScriptNumber = ScriptNumber(num | that.num) + /** * This equality just checks that the underlying scala numbers are equivalent, NOT if the numbers * are bitwise equivalent in Script. For instance ScriptNumber(0x01).numEqual(ScriptNumber(0x00000000001)) == true diff --git a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala index c4eb0f00d6..e48bdd230e 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala @@ -2,9 +2,10 @@ package org.bitcoins.script.locktime import org.bitcoins.protocol.transaction.TransactionConstants -import org.bitcoins.script.constant.{ScriptToken, ScriptNumber} +import org.bitcoins.script.constant.{ScriptNumber, ScriptToken} import org.bitcoins.script.result._ -import org.bitcoins.script.{ScriptProgram} +import org.bitcoins.script.ScriptProgram +import org.bitcoins.script.flag.ScriptFlagUtil import org.bitcoins.util.BitcoinSLogger /** * Created by chris on 2/8/16. @@ -29,10 +30,10 @@ trait LockTimeInterpreter extends BitcoinSLogger { require(program.script.headOption.isDefined && program.script.head == OP_CHECKLOCKTIMEVERIFY, "Script top must be OP_CHECKLOCKTIMEVERIFY") if (program.stack.size == 0) { - logger.warn("Transaction validation failing in OP_CHECKLOCKTIMEVERIFY because we have no stack items") + logger.error("Transaction validation failing in OP_CHECKLOCKTIMEVERIFY because we have no stack items") ScriptProgram(program, ScriptErrorInvalidStackOperation) } else if (program.txSignatureComponent.transaction.inputs(program.txSignatureComponent.inputIndex).sequence == TransactionConstants.sequence) { - logger.warn("Transaction validation failing in OP_CHECKLOCKTIMEVERIFY because the sequence number is 0xffffffff") + logger.error("Transaction validation failing in OP_CHECKLOCKTIMEVERIFY because the sequence number is 0xffffffff") ScriptProgram(program, ScriptErrorUnsatisfiedLocktime) } else { @@ -46,11 +47,55 @@ trait LockTimeInterpreter extends BitcoinSLogger { case s : ScriptNumber if (s < ScriptNumber(500000000) && program.txSignatureComponent.transaction.lockTime >= 500000000) => logger.warn("OP_CHECKLOCKTIMEVERIFY marks the tx as invalid if stack top < 500000000 & tx locktime >= 500000000") Some(ScriptErrorUnsatisfiedLocktime) - case _ : ScriptToken => None + case _ : ScriptNumber => None + case _ : ScriptToken => Some(ScriptErrorUnknownError) } if (isError.isDefined) ScriptProgram(program,isError.get) else ScriptProgram(program,program.stack, program.script.tail) } } + /** + * When executed, if any of the following conditions are true, the script interpreter will terminate with an error: + * 1.) the stack is empty; or + * 2.) the top item on the stack is less than 0; or + * 3.) the top item on the stack has the disable flag (1 << 31) unset; and + * the transaction version is less than 2; or + * the transaction input sequence number disable flag (1 << 31) is set; or + * the relative lock-time type is not the same; or + * the top stack item is greater than the transaction sequence (when masked according to the BIP68); + * Otherwise, script execution will continue as if a NOP had been executed. + * See BIP112 for more information + * https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki + * @param program + * @return + */ + def opCheckSequenceVerify(program : ScriptProgram) : ScriptProgram = { + if (program.stack.isEmpty) { + logger.error("Cannot execute OP_CHECKSEQUENCEVERIFY on an empty stack") + ScriptProgram(program,ScriptErrorInvalidStackOperation) + } else { + program.stack.head match { + case ScriptNumber.negativeOne => ScriptProgram(program,ScriptErrorNegativeLockTime) + case s : ScriptNumber if (ScriptFlagUtil.requireMinimalData(program.flags) && !s.isShortestEncoding) => + logger.error("Sequence number is not encoded in the shortest way possible") + ScriptProgram(program,ScriptErrorUnknownError) + case s : ScriptNumber if ((s.num & locktimeDisabledFlag) != 0) => + //see BIP68 for sematnic of locktimeDisableFalg + logger.info("Locktime disable flag was set so OP_CHECKSEQUENCEVERIFY is treated as a NOP") + ScriptProgram(program,program.script.tail,ScriptProgram.Script) + case _ => ScriptProgram(program, program.stack.tail, program.script.tail) + } + } + + } + + /** + * If bit (1 << 31) of the sequence number is set, + * then no consensus meaning is applied to the sequence number and can be included + * in any block under all currently possible circumstances. + * @return the mask that ben used with a bitwise and to indicate if the sequence number has any meaning + */ + def locktimeDisabledFlag = 1 << 31 + } diff --git a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala index 45c0636117..8de88f1bab 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala @@ -21,6 +21,23 @@ case object OP_CHECKLOCKTIMEVERIFY extends LocktimeOperation { override def opCode = 177 } +/** + * When executed, if any of the following conditions are true, the script interpreter will terminate with an error: + * 1.) the stack is empty; or + * 2.) the top item on the stack is less than 0; or + * 3.) the top item on the stack has the disable flag (1 << 31) unset; and + * the transaction version is less than 2; or + * the transaction input sequence number disable flag (1 << 31) is set; or + * the relative lock-time type is not the same; or + * the top stack item is greater than the transaction sequence (when masked according to the BIP68); + * Otherwise, script execution will continue as if a NOP had been executed. + * See BIP112 for more information + * https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki + */ +case object OP_CHECKSEQUENCEVERIFY extends LocktimeOperation { + override def opCode = 178 +} + object LocktimeOperation extends ScriptOperationFactory[LocktimeOperation] { - override def operations = Seq(OP_CHECKLOCKTIMEVERIFY) + override def operations = Seq(OP_CHECKLOCKTIMEVERIFY/*, OP_CHECKSEQUENCEVERIFY*/) } \ No newline at end of file diff --git a/src/test/scala/org/bitcoins/script/locktime/LockTimeInterpreterTest.scala b/src/test/scala/org/bitcoins/script/locktime/LockTimeInterpreterTest.scala index c1e48dce63..799dd14bc0 100644 --- a/src/test/scala/org/bitcoins/script/locktime/LockTimeInterpreterTest.scala +++ b/src/test/scala/org/bitcoins/script/locktime/LockTimeInterpreterTest.scala @@ -1,12 +1,13 @@ package org.bitcoins.script.locktime -import org.bitcoins.protocol.transaction.{TransactionInput, Transaction, UpdateTransactionInputs} +import org.bitcoins.policy.Policy +import org.bitcoins.protocol.transaction.{Transaction, TransactionInput, UpdateTransactionInputs} import org.bitcoins.script.result._ -import org.bitcoins.script.{ExecutionInProgressScriptProgram, ExecutedScriptProgram, PreExecutionScriptProgram, ScriptProgram} -import org.bitcoins.script.constant.{ScriptNumber, OP_0} +import org.bitcoins.script.{ExecutedScriptProgram, ExecutionInProgressScriptProgram, PreExecutionScriptProgram, ScriptProgram} +import org.bitcoins.script.constant.{OP_0, ScriptNumber} import org.bitcoins.util.{ScriptProgramTestUtil, TestUtil} -import org.scalatest.{MustMatchers, FlatSpec} +import org.scalatest.{FlatSpec, MustMatchers} /** * Created by chris on 3/30/16. @@ -97,5 +98,41 @@ class LockTimeInterpreterTest extends FlatSpec with MustMatchers with LockTimeIn //if an error is not hit it will still be a ExecutionInProgressScriptProgram newProgram.isInstanceOf[ExecutedScriptProgram] must be (false) } + + it must "mark the script as invalid for OP_CHECKSEQUENCEVERIFY if there are no tokens on the stack" in { + val stack = List() + val script = List(OP_CHECKSEQUENCEVERIFY) + val program = ScriptProgram(TestUtil.testProgramExecutionInProgress,stack,script) + val newProgram = opCheckSequenceVerify(program) + newProgram.isInstanceOf[ExecutedScriptProgram] must be (true) + newProgram.asInstanceOf[ExecutedScriptProgram].error must be (Some(ScriptErrorInvalidStackOperation)) + } + + it must "mark the script as invalid for OP_CHECKSEQUENCEVERIFY if the stack top is negative" in { + val stack = List(ScriptNumber.negativeOne) + val script = List(OP_CHECKSEQUENCEVERIFY) + val program = ScriptProgram(TestUtil.testProgramExecutionInProgress,stack,script) + val newProgram = opCheckSequenceVerify(program) + newProgram.isInstanceOf[ExecutedScriptProgram] must be (true) + newProgram.asInstanceOf[ExecutedScriptProgram].error must be (Some(ScriptErrorNegativeLockTime)) + } + + it must "mark the script as invalid if we are requiring minimal encoding of numbers and the stack top is not minimal" in { + val stack = List(ScriptNumber("0100")) + val script = List(OP_CHECKSEQUENCEVERIFY) + val program = ScriptProgram(TestUtil.testProgramExecutionInProgress,stack,script) + val newProgram = opCheckSequenceVerify(program) + newProgram.isInstanceOf[ExecutedScriptProgram] must be (true) + newProgram.asInstanceOf[ExecutedScriptProgram].error must be (Some(ScriptErrorUnknownError)) + } + + it must "treat OP_CHECKSEQUENCEVERIFY as a NOP if the locktime disabled flag is set in the sequence number" in { + val stack = List(ScriptNumber(locktimeDisabledFlag)) + val script = List(OP_CHECKSEQUENCEVERIFY) + val program = ScriptProgram(TestUtil.testProgramExecutionInProgress,stack,script) + val newProgram = opCheckSequenceVerify(program) + newProgram.stack must be (stack) + newProgram.script.isEmpty must be (true) + } } From 28ebed9b0cea126316a8ec165d1d8db20267c753 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Tue, 3 May 2016 18:34:42 -0500 Subject: [PATCH 2/4] Implementing OP_CSV from pull request submitted to bitcoin core --- .../bitcoins/script/flag/ScriptFlagUtil.scala | 9 +++++++ .../interpreter/ScriptInterpreter.scala | 12 ++++++++- .../script/locktime/LockTimeInterpreter.scala | 27 ++++++++++++++----- .../script/locktime/LocktimeOperations.scala | 2 +- .../interpreter/ScriptInterpreterTest.scala | 7 ++--- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala b/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala index bd26639b6a..169337e1e8 100644 --- a/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala +++ b/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala @@ -32,6 +32,15 @@ trait ScriptFlagUtil { flags.contains(ScriptVerifyCheckLocktimeVerify) } + /** + * Checks if the script flag for checksequenceverify is enabled + * @param flags + * @return + */ + def checkSequenceVerifyEnabled(flags : Seq[ScriptFlag]) : Boolean = { + flags.contains(ScriptVerifyCheckSequenceVerify) + } + /** * Checks to see if the script flag is set to discourage NOPs that are not in use * NOPs are used by soft forks to repurpose NOPs to actual functionality such as checklocktimeverify diff --git a/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala b/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala index dc9e4f94db..e04a5592ba 100644 --- a/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala @@ -4,7 +4,7 @@ package org.bitcoins.script.interpreter import org.bitcoins.protocol.script._ import org.bitcoins.protocol.transaction.Transaction import org.bitcoins.script.flag._ -import org.bitcoins.script.locktime.{LockTimeInterpreter, OP_CHECKLOCKTIMEVERIFY} +import org.bitcoins.script.locktime.{LockTimeInterpreter, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY} import org.bitcoins.script.splice._ import org.bitcoins.script.{ExecutedScriptProgram, ExecutionInProgressScriptProgram, PreExecutionScriptProgram, ScriptProgram} import org.bitcoins.script.arithmetic._ @@ -259,6 +259,16 @@ trait ScriptInterpreter extends CryptoInterpreter with StackInterpreter with Con } //in this case, just reat OP_CLTV just like a NOP and remove it from the stack else loop(ScriptProgram(p, p.script.tail, ScriptProgram.Script)) + case OP_CHECKSEQUENCEVERIFY :: t => + //check if CLTV is enforced yet + if (ScriptFlagUtil.checkSequenceVerifyEnabled(p.flags)) loop(opCheckSequenceVerify(p)) + //if not, check to see if we should discourage p + else if (ScriptFlagUtil.discourageUpgradableNOPs(p.flags)) { + logger.error("We cannot execute a NOP when the ScriptVerifyDiscourageUpgradableNOPs is set") + loop(ScriptProgram(p, ScriptErrorDiscourageUpgradableNOPs)) + } + //in this case, just reat OP_CSV just like a NOP and remove it from the stack + else loop(ScriptProgram(p, p.script.tail, ScriptProgram.Script)) //no more script operations to run, return whether the program is valid and the final state of the program case Nil => loop(ScriptProgram.toExecutedProgram(p)) case h :: t => throw new RuntimeException(h + " was unmatched") diff --git a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala index e48bdd230e..617e50e436 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala @@ -2,11 +2,13 @@ package org.bitcoins.script.locktime import org.bitcoins.protocol.transaction.TransactionConstants -import org.bitcoins.script.constant.{ScriptNumber, ScriptToken} +import org.bitcoins.script.constant.{ScriptConstant, ScriptNumber, ScriptToken} import org.bitcoins.script.result._ import org.bitcoins.script.ScriptProgram import org.bitcoins.script.flag.ScriptFlagUtil import org.bitcoins.util.BitcoinSLogger + +import scala.annotation.tailrec /** * Created by chris on 2/8/16. */ @@ -70,7 +72,8 @@ trait LockTimeInterpreter extends BitcoinSLogger { * @param program * @return */ - def opCheckSequenceVerify(program : ScriptProgram) : ScriptProgram = { + @tailrec + final def opCheckSequenceVerify(program : ScriptProgram) : ScriptProgram = { if (program.stack.isEmpty) { logger.error("Cannot execute OP_CHECKSEQUENCEVERIFY on an empty stack") ScriptProgram(program,ScriptErrorInvalidStackOperation) @@ -80,11 +83,16 @@ trait LockTimeInterpreter extends BitcoinSLogger { case s : ScriptNumber if (ScriptFlagUtil.requireMinimalData(program.flags) && !s.isShortestEncoding) => logger.error("Sequence number is not encoded in the shortest way possible") ScriptProgram(program,ScriptErrorUnknownError) - case s : ScriptNumber if ((s.num & locktimeDisabledFlag) != 0) => - //see BIP68 for sematnic of locktimeDisableFalg + case s : ScriptNumber if (!isLockTimeBitOff(s)) => + //see BIP68 for semantic of locktimeDisableFalg logger.info("Locktime disable flag was set so OP_CHECKSEQUENCEVERIFY is treated as a NOP") ScriptProgram(program,program.script.tail,ScriptProgram.Script) - case _ => ScriptProgram(program, program.stack.tail, program.script.tail) + case s : ScriptNumber if (isLockTimeBitOff(s) && program.txSignatureComponent.transaction.version < 2) => + logger.error("OP_CSV fails if locktime bit is not set and the tx version < 2") + ScriptProgram(program, ScriptErrorUnsatisfiedLocktime) + case s : ScriptConstant => + opCheckSequenceVerify(ScriptProgram(program, ScriptNumber(s.hex) :: program.stack.tail, ScriptProgram.Stack)) + case _ : ScriptToken => ScriptProgram(program, program.stack.tail, program.script.tail) } } @@ -96,6 +104,13 @@ trait LockTimeInterpreter extends BitcoinSLogger { * in any block under all currently possible circumstances. * @return the mask that ben used with a bitwise and to indicate if the sequence number has any meaning */ - def locktimeDisabledFlag = 1 << 31 + def locktimeDisabledFlag = 1L << 31 + + /** + * The script number on the stack has the disable flag (1 << 31) unset + * @param s + * @return + */ + def isLockTimeBitOff(s : ScriptNumber) : Boolean = (s.num & locktimeDisabledFlag) == 0 } diff --git a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala index 8de88f1bab..f5eaccf2eb 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala @@ -39,5 +39,5 @@ case object OP_CHECKSEQUENCEVERIFY extends LocktimeOperation { } object LocktimeOperation extends ScriptOperationFactory[LocktimeOperation] { - override def operations = Seq(OP_CHECKLOCKTIMEVERIFY/*, OP_CHECKSEQUENCEVERIFY*/) + override def operations = Seq(OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY) } \ No newline at end of file diff --git a/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala b/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala index 193274e2f9..b48625d5ec 100644 --- a/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala +++ b/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala @@ -32,11 +32,12 @@ class ScriptInterpreterTest extends FlatSpec with MustMatchers with ScriptInterp val source = Source.fromURL(getClass.getResource("/script_tests.json")) - //use this to represent a single test case from script_valid.json -/* val lines = +/* //use this to represent a single test case from script_valid.json + val lines = """ | - |[["1", "IF ELSE ENDIF ELSE", "P2SH,STRICTENC", "UNBALANCED_CONDITIONAL"]] + |[["4294967296", "CHECKSEQUENCEVERIFY", "CHECKSEQUENCEVERIFY", "UNSATISFIED_LOCKTIME", + "CSV fails if stack top bit 1 << 31 is not set, and tx version < 2"]] """.stripMargin*/ val lines = try source.getLines.filterNot(_.isEmpty).map(_.trim) mkString "\n" finally source.close() val json = lines.parseJson From 87b0234e1d64cee7bd6f10411feeb701257beab7 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Tue, 3 May 2016 18:34:42 -0500 Subject: [PATCH 3/4] Implementing OP_CSV from pull request submitted to bitcoin core --- .../bitcoins/script/flag/ScriptFlagUtil.scala | 9 +++++++ .../interpreter/ScriptInterpreter.scala | 12 ++++++++- .../script/locktime/LockTimeInterpreter.scala | 27 ++++++++++++++----- .../script/locktime/LocktimeOperations.scala | 2 +- .../interpreter/ScriptInterpreterTest.scala | 7 ++--- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala b/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala index bd26639b6a..169337e1e8 100644 --- a/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala +++ b/src/main/scala/org/bitcoins/script/flag/ScriptFlagUtil.scala @@ -32,6 +32,15 @@ trait ScriptFlagUtil { flags.contains(ScriptVerifyCheckLocktimeVerify) } + /** + * Checks if the script flag for checksequenceverify is enabled + * @param flags + * @return + */ + def checkSequenceVerifyEnabled(flags : Seq[ScriptFlag]) : Boolean = { + flags.contains(ScriptVerifyCheckSequenceVerify) + } + /** * Checks to see if the script flag is set to discourage NOPs that are not in use * NOPs are used by soft forks to repurpose NOPs to actual functionality such as checklocktimeverify diff --git a/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala b/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala index dc9e4f94db..e04a5592ba 100644 --- a/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/interpreter/ScriptInterpreter.scala @@ -4,7 +4,7 @@ package org.bitcoins.script.interpreter import org.bitcoins.protocol.script._ import org.bitcoins.protocol.transaction.Transaction import org.bitcoins.script.flag._ -import org.bitcoins.script.locktime.{LockTimeInterpreter, OP_CHECKLOCKTIMEVERIFY} +import org.bitcoins.script.locktime.{LockTimeInterpreter, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY} import org.bitcoins.script.splice._ import org.bitcoins.script.{ExecutedScriptProgram, ExecutionInProgressScriptProgram, PreExecutionScriptProgram, ScriptProgram} import org.bitcoins.script.arithmetic._ @@ -259,6 +259,16 @@ trait ScriptInterpreter extends CryptoInterpreter with StackInterpreter with Con } //in this case, just reat OP_CLTV just like a NOP and remove it from the stack else loop(ScriptProgram(p, p.script.tail, ScriptProgram.Script)) + case OP_CHECKSEQUENCEVERIFY :: t => + //check if CLTV is enforced yet + if (ScriptFlagUtil.checkSequenceVerifyEnabled(p.flags)) loop(opCheckSequenceVerify(p)) + //if not, check to see if we should discourage p + else if (ScriptFlagUtil.discourageUpgradableNOPs(p.flags)) { + logger.error("We cannot execute a NOP when the ScriptVerifyDiscourageUpgradableNOPs is set") + loop(ScriptProgram(p, ScriptErrorDiscourageUpgradableNOPs)) + } + //in this case, just reat OP_CSV just like a NOP and remove it from the stack + else loop(ScriptProgram(p, p.script.tail, ScriptProgram.Script)) //no more script operations to run, return whether the program is valid and the final state of the program case Nil => loop(ScriptProgram.toExecutedProgram(p)) case h :: t => throw new RuntimeException(h + " was unmatched") diff --git a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala index e48bdd230e..617e50e436 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala @@ -2,11 +2,13 @@ package org.bitcoins.script.locktime import org.bitcoins.protocol.transaction.TransactionConstants -import org.bitcoins.script.constant.{ScriptNumber, ScriptToken} +import org.bitcoins.script.constant.{ScriptConstant, ScriptNumber, ScriptToken} import org.bitcoins.script.result._ import org.bitcoins.script.ScriptProgram import org.bitcoins.script.flag.ScriptFlagUtil import org.bitcoins.util.BitcoinSLogger + +import scala.annotation.tailrec /** * Created by chris on 2/8/16. */ @@ -70,7 +72,8 @@ trait LockTimeInterpreter extends BitcoinSLogger { * @param program * @return */ - def opCheckSequenceVerify(program : ScriptProgram) : ScriptProgram = { + @tailrec + final def opCheckSequenceVerify(program : ScriptProgram) : ScriptProgram = { if (program.stack.isEmpty) { logger.error("Cannot execute OP_CHECKSEQUENCEVERIFY on an empty stack") ScriptProgram(program,ScriptErrorInvalidStackOperation) @@ -80,11 +83,16 @@ trait LockTimeInterpreter extends BitcoinSLogger { case s : ScriptNumber if (ScriptFlagUtil.requireMinimalData(program.flags) && !s.isShortestEncoding) => logger.error("Sequence number is not encoded in the shortest way possible") ScriptProgram(program,ScriptErrorUnknownError) - case s : ScriptNumber if ((s.num & locktimeDisabledFlag) != 0) => - //see BIP68 for sematnic of locktimeDisableFalg + case s : ScriptNumber if (!isLockTimeBitOff(s)) => + //see BIP68 for semantic of locktimeDisableFalg logger.info("Locktime disable flag was set so OP_CHECKSEQUENCEVERIFY is treated as a NOP") ScriptProgram(program,program.script.tail,ScriptProgram.Script) - case _ => ScriptProgram(program, program.stack.tail, program.script.tail) + case s : ScriptNumber if (isLockTimeBitOff(s) && program.txSignatureComponent.transaction.version < 2) => + logger.error("OP_CSV fails if locktime bit is not set and the tx version < 2") + ScriptProgram(program, ScriptErrorUnsatisfiedLocktime) + case s : ScriptConstant => + opCheckSequenceVerify(ScriptProgram(program, ScriptNumber(s.hex) :: program.stack.tail, ScriptProgram.Stack)) + case _ : ScriptToken => ScriptProgram(program, program.stack.tail, program.script.tail) } } @@ -96,6 +104,13 @@ trait LockTimeInterpreter extends BitcoinSLogger { * in any block under all currently possible circumstances. * @return the mask that ben used with a bitwise and to indicate if the sequence number has any meaning */ - def locktimeDisabledFlag = 1 << 31 + def locktimeDisabledFlag = 1L << 31 + + /** + * The script number on the stack has the disable flag (1 << 31) unset + * @param s + * @return + */ + def isLockTimeBitOff(s : ScriptNumber) : Boolean = (s.num & locktimeDisabledFlag) == 0 } diff --git a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala index 8de88f1bab..f5eaccf2eb 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LocktimeOperations.scala @@ -39,5 +39,5 @@ case object OP_CHECKSEQUENCEVERIFY extends LocktimeOperation { } object LocktimeOperation extends ScriptOperationFactory[LocktimeOperation] { - override def operations = Seq(OP_CHECKLOCKTIMEVERIFY/*, OP_CHECKSEQUENCEVERIFY*/) + override def operations = Seq(OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY) } \ No newline at end of file diff --git a/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala b/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala index 193274e2f9..b48625d5ec 100644 --- a/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala +++ b/src/test/scala/org/bitcoins/script/interpreter/ScriptInterpreterTest.scala @@ -32,11 +32,12 @@ class ScriptInterpreterTest extends FlatSpec with MustMatchers with ScriptInterp val source = Source.fromURL(getClass.getResource("/script_tests.json")) - //use this to represent a single test case from script_valid.json -/* val lines = +/* //use this to represent a single test case from script_valid.json + val lines = """ | - |[["1", "IF ELSE ENDIF ELSE", "P2SH,STRICTENC", "UNBALANCED_CONDITIONAL"]] + |[["4294967296", "CHECKSEQUENCEVERIFY", "CHECKSEQUENCEVERIFY", "UNSATISFIED_LOCKTIME", + "CSV fails if stack top bit 1 << 31 is not set, and tx version < 2"]] """.stripMargin*/ val lines = try source.getLines.filterNot(_.isEmpty).map(_.trim) mkString "\n" finally source.close() val json = lines.parseJson From 42a7e1457df81e031903e21a54da11b47c60f595 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Tue, 3 May 2016 19:35:00 -0500 Subject: [PATCH 4/4] Adding checkSequence function found in bitcoin core - this completes the implementation for OP_CSV --- .../script/locktime/LockTimeInterpreter.scala | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala index 617e50e436..b3cf67d197 100644 --- a/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala +++ b/src/main/scala/org/bitcoins/script/locktime/LockTimeInterpreter.scala @@ -90,9 +90,17 @@ trait LockTimeInterpreter extends BitcoinSLogger { case s : ScriptNumber if (isLockTimeBitOff(s) && program.txSignatureComponent.transaction.version < 2) => logger.error("OP_CSV fails if locktime bit is not set and the tx version < 2") ScriptProgram(program, ScriptErrorUnsatisfiedLocktime) + case s : ScriptNumber => + if (checkSequence(program,s)) { + ScriptProgram(program, program.stack.tail, program.script.tail) + } else { + logger.error("Stack top sequence and transaction input's sequence number comparison failed") + ScriptProgram(program, ScriptErrorUnsatisfiedLocktime) + } case s : ScriptConstant => opCheckSequenceVerify(ScriptProgram(program, ScriptNumber(s.hex) :: program.stack.tail, ScriptProgram.Stack)) - case _ : ScriptToken => ScriptProgram(program, program.stack.tail, program.script.tail) + case token : ScriptToken => + throw new RuntimeException("Stack top must be either a ScriptConstant or a ScriptNumber, we got: " + token) } } @@ -113,4 +121,56 @@ trait LockTimeInterpreter extends BitcoinSLogger { */ def isLockTimeBitOff(s : ScriptNumber) : Boolean = (s.num & locktimeDisabledFlag) == 0 + + /** + * If a transaction's input's sequence number encodes a relative lock-time, this mask is + * applied to extract that lock-time from the sequence field. + */ + def sequenceLockTimeMask = 0x0000ffff + + /** + * If the transaction input sequence number encodes a relative lock-time and this flag + * is set, the relative lock-time has units of 512 seconds, + * otherwise it specifies blocks with a granularity of 1. + */ + def sequenceLockTimeTypeFlag = (1L << 22) + + /** + * Mimics this function inside of bitcoin core + * https://github.com/bitcoin/bitcoin/blob/e26b62093ae21e89ed7d36a24a6b863f38ec631d/src/script/interpreter.cpp#L1196 + * @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 + */ + def checkSequence(program : ScriptProgram, nSequence : ScriptNumber) : Boolean = { + val inputIndex = program.txSignatureComponent.inputIndex + val txToSequence : ScriptNumber = ScriptNumber(program.txSignatureComponent.transaction.inputs(inputIndex).sequence) + + if (program.txSignatureComponent.transaction.version < 2) return false + + val nLockTimeMask : Long = sequenceLockTimeTypeFlag | sequenceLockTimeMask + val txToSequenceMasked : ScriptNumber = txToSequence & ScriptNumber(nLockTimeMask) + + val nSequenceMasked : ScriptNumber = nSequence & ScriptNumber(nLockTimeMask) + + // There are two kinds of nSequence: lock-by-blockheight + // and lock-by-blocktime, distinguished by whether + // nSequenceMasked < CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG. + // + // We want to compare apples to apples, so fail the script + // unless the type of nSequenceMasked being tested is the same as + // the nSequenceMasked in the transaction. + if (!( + (txToSequenceMasked < ScriptNumber(sequenceLockTimeTypeFlag) && + nSequenceMasked < ScriptNumber(sequenceLockTimeTypeFlag)) || + (txToSequenceMasked >= ScriptNumber(sequenceLockTimeTypeFlag) && + nSequenceMasked >= ScriptNumber(sequenceLockTimeTypeFlag)) + )) return false + + // Now that we know we're comparing apples-to-apples, the + // comparison is a simple numeric one. + if (nSequenceMasked > txToSequenceMasked) return false + + true + } }