Merge pull request #2 from Christewart/op_csv

OP_CHECKSEQUENCEVERIFY implementation
This commit is contained in:
Chris Stewart 2016-05-04 19:32:19 -05:00
commit 5a70a7141a
8 changed files with 226 additions and 21 deletions

View file

@ -1,11 +1,39 @@
package org.bitcoins.protocol.transaction
/**
* Created by chris on 2/12/16.
*/
object TransactionConstants {
import org.bitcoins.script.constant.ScriptNumber
val version = 1
val lockTime = 0
val sequence = 4294967295L
/**
* Created by chris on 2/12/16.
*/
trait TransactionConstants {
lazy val version = 1
lazy val lockTime = 0
lazy val sequence = 4294967295L
/**
* 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 = 1L << 31
/**
* 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)
}
object TransactionConstants extends TransactionConstants

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -2,10 +2,13 @@ package org.bitcoins.script.locktime
import org.bitcoins.protocol.transaction.TransactionConstants
import org.bitcoins.script.constant.{ScriptToken, ScriptNumber}
import org.bitcoins.script.constant.{ScriptConstant, 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
import scala.annotation.tailrec
/**
* Created by chris on 2/8/16.
*/
@ -29,10 +32,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 +49,107 @@ 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
*/
@tailrec
final 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 (!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 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 token : ScriptToken =>
throw new RuntimeException("Stack top must be either a ScriptConstant or a ScriptNumber, we got: " + token)
}
}
}
/**
* 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 = TransactionConstants.sequenceLockTimeTypeFlag | TransactionConstants.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(TransactionConstants.sequenceLockTimeTypeFlag) &&
nSequenceMasked < ScriptNumber(TransactionConstants.sequenceLockTimeTypeFlag)) ||
(txToSequenceMasked >= ScriptNumber(TransactionConstants.sequenceLockTimeTypeFlag) &&
nSequenceMasked >= ScriptNumber(TransactionConstants.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
}
/**
* The script number on the stack has the disable flag (1 << 31) unset
* @param s
* @return
*/
def isLockTimeBitOff(s : ScriptNumber) : Boolean = (s.num & TransactionConstants.locktimeDisabledFlag) == 0
}

View file

@ -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)
}

View file

@ -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

View file

@ -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, TransactionConstants, 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(TransactionConstants.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)
}
}