mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 01:40:55 +01:00
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:
parent
6f6315c1e7
commit
11f6c8f024
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user