mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-25 15:20:17 +01:00
Merge pull request #2 from Christewart/op_csv
OP_CHECKSEQUENCEVERIFY implementation
This commit is contained in:
commit
5a70a7141a
8 changed files with 226 additions and 21 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue