2022 07 05 UnknownControlBlock (#4449)

* Add TaprootUnknownPath and UnknownControlBlock

* Fix small bugs and make validation more resilient against exceptions

* Take ben's suggestion and use abstract class

* Fix bug in TaprootKeyPath's default hash type

* Fix comment
This commit is contained in:
Chris Stewart 2022-07-05 19:26:28 -05:00 committed by GitHub
parent 6f6315c1e7
commit 11f6c8f024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 41 deletions

View File

@ -1,28 +1,32 @@
package org.bitcoins.core.protocol.script
import org.bitcoins.testkitcore.gen.{ScriptGenerators, WitnessGenerators}
import org.scalacheck.{Prop, Properties}
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
class ScriptWitnessSpec extends Properties("ScriptWitnessSpec") {
class ScriptWitnessSpec extends BitcoinSUnitTest {
property("serialization symmetry") = {
Prop.forAll(WitnessGenerators.scriptWitness) { scriptWit =>
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
it must "have serialization symmetry" in {
forAll(WitnessGenerators.scriptWitness) { scriptWit =>
val x = ScriptWitness(scriptWit.stack)
scriptWit == x
val fromBytes = ScriptWitness.fromBytes(scriptWit.bytes)
assert(scriptWit == x)
assert(fromBytes == scriptWit)
}
}
property("pull redeem script out of p2wsh witness") = {
Prop.forAll(ScriptGenerators.rawScriptPubKey) { case (spk, _) =>
P2WSHWitnessV0(spk).redeemScript == spk
it must "pull redeem script out of p2wsh witness" in {
forAll(ScriptGenerators.rawScriptPubKey) { case (spk, _) =>
assert(P2WSHWitnessV0(spk).redeemScript == spk)
}
}
property("pull script signature out of p2wsh witness") = {
Prop.forAll(ScriptGenerators.rawScriptPubKey,
ScriptGenerators.rawScriptSignature) {
case ((spk, _), scriptSig) =>
P2WSHWitnessV0(spk, scriptSig).scriptSignature == scriptSig
it must "pull script signature out of p2wsh witness" in {
forAll(ScriptGenerators.rawScriptPubKey,
ScriptGenerators.rawScriptSignature) { case ((spk, _), scriptSig) =>
assert(P2WSHWitnessV0(spk, scriptSig).scriptSignature == scriptSig)
}
}
}

View File

@ -417,8 +417,8 @@ case class TaprootTxSigComponent(
override def sigVersion: SigVersionTaproot = {
witness match {
case _: TaprootKeyPath => SigVersionTaprootKeySpend
case _: TaprootScriptPath => SigVersionTapscript
case _: TaprootKeyPath => SigVersionTaprootKeySpend
case _: TaprootScriptPath | _: TaprootUnknownPath => SigVersionTapscript
}
}
}

View File

@ -10,12 +10,8 @@ import scodec.bits.ByteVector
*
* @see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules
*/
case class ControlBlock(bytes: ByteVector) extends NetworkElement {
//invariants from: https://github.com/bitcoin/bitcoin/blob/37633d2f61697fc719390767aae740ece978b074/src/script/interpreter.cpp#L1835
require(bytes.size >= TaprootScriptPath.TAPROOT_CONTROL_BASE_SIZE)
require(bytes.size <= TaprootScriptPath.TAPROOT_CONTROL_MAX_SIZE)
require(
(bytes.size - TaprootScriptPath.TAPROOT_CONTROL_BASE_SIZE) % TaprootScriptPath.TAPROOT_CONTROL_NODE_SIZE == 0)
sealed abstract class ControlBlock extends NetworkElement {
require(ControlBlock.isValid(bytes), s"Bytes for control block are not valid")
/** Let p = c[1:33] and let P = lift_x(int(p)) where lift_x and [:] are defined as in BIP340. Fail if this point is not on the curve.
*/
@ -31,9 +27,60 @@ case class ControlBlock(bytes: ByteVector) extends NetworkElement {
}
}
case class TapscriptControlBlock(bytes: ByteVector) extends ControlBlock {
require(TapscriptControlBlock.isValid(bytes),
s"Invalid leaf version for tapscript control block, got=$bytes")
}
/** A control block that does not have a leaf version defined as per BIP342
* This is needed for future soft fork compatability where we introduce new leaf versions
* to correspond to new spending rules
*/
case class UnknownControlBlock(bytes: ByteVector) extends ControlBlock
object ControlBlock extends Factory[ControlBlock] {
override def fromBytes(bytes: ByteVector): ControlBlock = {
new ControlBlock(bytes)
TapscriptControlBlock(bytes)
}
/** invariants from: https://github.com/bitcoin/bitcoin/blob/37633d2f61697fc719390767aae740ece978b074/src/script/interpreter.cpp#L1835
*/
def isValid(bytes: ByteVector): Boolean = {
bytes.size >= TaprootScriptPath.TAPROOT_CONTROL_BASE_SIZE &&
bytes.size <= TaprootScriptPath.TAPROOT_CONTROL_MAX_SIZE &&
(bytes.size - TaprootScriptPath.TAPROOT_CONTROL_BASE_SIZE) % TaprootScriptPath.TAPROOT_CONTROL_NODE_SIZE == 0
}
}
object TapscriptControlBlock extends Factory[TapscriptControlBlock] {
/** BIP342 specifies validity rules that apply for leaf version 0xc0,
* but future proposals can introduce rules for other leaf versions.
*
* @see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#rationale
*/
val knownLeafVersions: Vector[Byte] = Vector(0xc0.toByte, 0xc1.toByte)
/** invariants from: https://github.com/bitcoin/bitcoin/blob/37633d2f61697fc719390767aae740ece978b074/src/script/interpreter.cpp#L1835
*/
def isValid(bytes: ByteVector): Boolean = {
if (bytes.isEmpty) {
false
} else {
knownLeafVersions.contains(bytes.head) &&
ControlBlock.isValid(bytes) &&
XOnlyPubKey.fromBytesT(bytes.slice(1, 33)).isSuccess
}
}
override def fromBytes(bytes: ByteVector): TapscriptControlBlock = {
new TapscriptControlBlock(bytes)
}
}
object UnknownControlBlock extends Factory[UnknownControlBlock] {
override def fromBytes(bytes: ByteVector): UnknownControlBlock =
new UnknownControlBlock(bytes)
}

View File

@ -179,15 +179,22 @@ object ScriptWitness extends Factory[ScriptWitness] {
|| (stack.head.size == 65 && stack.head.head == 0x04 && CryptoUtil
.isValidPubKey(ECPublicKeyBytes(stack.head))))
}
if (stack.isEmpty) {
EmptyScriptWitness
} else if (TaprootKeyPath.isValid(stack.toVector)) {
//taproot key path spend
TaprootKeyPath.fromStack(stack.toVector)
} else if (isPubKey && stack.size == 1) {
val pubKey = ECPublicKeyBytes(stack.head)
P2WPKHWitnessV0(pubKey)
} else if (TaprootScriptPath.isValid(stack.toVector)) {
TaprootScriptPath.fromStack(stack.toVector)
} else if (isPubKey && stack.size == 2) {
val pubKey = ECPublicKeyBytes(stack.head)
val sig = ECDigitalSignature(stack(1))
P2WPKHWitnessV0(pubKey, sig)
} else if (isPubKey && stack.size == 1) {
val pubKey = ECPublicKeyBytes(stack.head)
P2WPKHWitnessV0(pubKey)
} else {
//wont match a Vector if I don't convert to list
val s = stack.toList
@ -227,7 +234,9 @@ object TaprootWitness {
if ((hasAnnex && stack.length == 2) || stack.length == 1) {
TaprootKeyPath.fromStack(stack)
} else TaprootScriptPath(stack)
} else {
TaprootScriptPath(stack)
}
}
}
@ -269,10 +278,10 @@ object TaprootKeyPath {
}
val keyPath = if (sigBytes.length == 64) {
//means SIGHASH_ALL is implicitly encoded
//means SIGHASH_DEFAULT is implicitly encoded
//see: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#Common_signature_message
val sig = SchnorrDigitalSignature.fromBytes(sigBytes)
TaprootKeyPath(sig, HashType.sigHashAll, annexOpt)
TaprootKeyPath(sig, HashType.sigHashDefault, annexOpt)
} else if (sigBytes.length == 65) {
val sig = SchnorrDigitalSignature.fromBytes(sigBytes.dropRight(1))
val hashType = HashType.fromByte(sigBytes.last)
@ -295,16 +304,16 @@ case class TaprootScriptPath(stack: Vector[ByteVector]) extends TaprootWitness {
require(TaprootScriptPath.isValid(stack),
s"Invalid witness stack for TaprootScriptPath, got=$stack")
val controlBlock: ControlBlock = {
val controlBlock: TapscriptControlBlock = {
if (TaprootScriptPath.hasAnnex(stack)) {
//If there are at least two witness elements, and the first byte of the last element is 0x50[4],
// this last element is called annex a[5] and is removed from the witness stack.
// The annex (or the lack of thereof) is always covered by the signature and contributes to transaction weight,
// but is otherwise ignored during taproot validation.
//see: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules
ControlBlock.fromBytes(stack(1))
TapscriptControlBlock.fromBytes(stack(1))
} else {
ControlBlock.fromBytes(stack.head)
TapscriptControlBlock.fromBytes(stack.head)
}
}
@ -369,15 +378,7 @@ object TaprootScriptPath {
stack.head
}
}
val m = controlBlock.drop(33).length / 32.0
if (m >= 0 && m <= 128) {
val pubKeyBytes = controlBlock.slice(1, 33)
// if not whole, we do not have correct # of bytes for control block
m.isWhole && SchnorrPublicKey.fromBytesOpt(pubKeyBytes).isDefined
} else {
false
}
TapscriptControlBlock.isValid(controlBlock)
} else {
false
}
@ -441,10 +442,37 @@ object TaprootScriptPath {
/** Checks the witness stack has an annex in it */
def hasAnnex(stack: Vector[ByteVector]): Boolean = {
stack.headOption.map(_.head) == annexOpt
stack.headOption
.map(_.headOption == annexOpt)
.getOrElse(false)
}
private def hashTapBranch(bytes: ByteVector): Sha256Digest = {
CryptoUtil.taggedSha256(bytes, "TapBranch")
}
}
case class TaprootUnknownPath(stack: Vector[ByteVector])
extends TaprootWitness {
val controlBlock: UnknownControlBlock = {
if (TaprootScriptPath.hasAnnex(stack)) {
//If there are at least two witness elements, and the first byte of the last element is 0x50[4],
// this last element is called annex a[5] and is removed from the witness stack.
// The annex (or the lack of thereof) is always covered by the signature and contributes to transaction weight,
// but is otherwise ignored during taproot validation.
//see: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#script-validation-rules
UnknownControlBlock.fromBytes(stack(1))
} else {
UnknownControlBlock.fromBytes(stack.head)
}
}
override def annexOpt: Option[ByteVector] = {
if (TaprootScriptPath.hasAnnex(stack)) {
Some(stack.head)
} else {
None
}
}
}

View File

@ -86,6 +86,8 @@ case object WitnessVersion1 extends WitnessVersion {
Right(witnessSPK)
case sp: TaprootScriptPath =>
Right(sp.script)
case _: TaprootUnknownPath =>
Right(witnessSPK)
case w @ (EmptyScriptWitness | _: P2WPKHWitnessV0 |
_: P2WSHWitnessV0) =>
sys.error(