2024 01 23 Validate we have maxSigs number of public keys in MultiSignatureScriptPubKey (#5369)

* Add test case for tx for 109c38

* Check that we have maxSigs' public keys in OP_CMS.isValidAsm()

* Fix val names
This commit is contained in:
Chris Stewart 2024-01-23 16:43:23 -06:00 committed by GitHub
parent 039144c2c8
commit fd09724f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 30 deletions

View file

@ -419,6 +419,13 @@ class TransactionTest extends BitcoinSUnitTest {
assert(tx.hex == hex)
}
it must "parse 109c3834d74fc4788b42ca884f89c3fe3f95c9bba6d1b5b10ec4d3f426a7d12a" in {
val hex =
"0100000001214a8d0ae8eecd5a42dccfe5d9e9ab4738f28237651ed697139836007419e56b000000006b483045022100f501ce60b5b26d6b24f987378b32d64ce47124004f4e63be06a7748fcc078c160220444619b548700b6105bc896b36017d0f01999f5020ca1eeca3c313e9c2e3252c012103bc161c3a4663978ebfb7561588c85414fbcce0222ed19cdd525267e4ba1bbd37ffffffff0280841e0000000000035152ae50604302000000001976a914c2e88b055bdaae6c7996db9784433e9e7cbc96e088ac00000000"
val tx = Transaction.fromHex(hex)
assert(tx.hex == hex)
}
private def findInput(
tx: Transaction,
outPoint: TransactionOutPoint): Option[(TransactionInput, Int)] = {

View file

@ -177,10 +177,7 @@ sealed trait MultiSignatureScriptPubKey extends RawScriptPubKey {
/** Returns the public keys encoded into the `scriptPubKey` */
def publicKeys: Seq[ECPublicKeyBytes] = {
asm
.filter(_.isInstanceOf[ScriptConstant])
.slice(1, maxSigs + 1)
.map(key => ECPublicKeyBytes(key.bytes))
MultiSignatureScriptPubKey.parsePublicKeys(maxSigs, asm)
}
override def toString = s"multi($requiredSigs,${publicKeys.mkString(",")})"
@ -252,36 +249,35 @@ object MultiSignatureScriptPubKey
val containsMultiSigOp = cmsIdx != -1
if (asm.nonEmpty && containsMultiSigOp) {
//we need either the first or second asm operation to indicate how many signatures are required
val hasRequiredSignaturesTry = Try {
val hasRequiredSignaturesOpt: Option[Int] = {
asm.headOption match {
case None => false
case None => None
case Some(token) =>
//this is for the case that we have more than 16 public keys, the
//first operation will be a push op, the second operation being the actual number of keys
if (token.isInstanceOf[BytesToPushOntoStack]) {
isValidPubKeyNumber(asm.tail.head)
} else isValidPubKeyNumber(token)
token match {
case _: BytesToPushOntoStack =>
isValidPubKeyNumber(asm.tail.head)
case _ => isValidPubKeyNumber(token)
}
}
}
//the second to last asm operation should be the maximum amount of public keys
val hasMaximumSignaturesTry = {
val hasMaximumSignaturesOpt: Option[Int] = {
val maxSigsIdx = asm.length - 2
if (maxSigsIdx >= cmsIdx) {
val exn = new IllegalAccessException(
s"maxSigsIdx is after OP_CHECKMULTISIG/OP_CHECKMULTISIGVERIFY, maxSigsIx=$maxSigsIdx")
Failure(exn)
None
} else {
Try {
asm(maxSigsIdx) match {
case token: ScriptToken => isValidPubKeyNumber(token)
}
asm(maxSigsIdx) match {
case token: ScriptToken => isValidPubKeyNumber(token)
}
}
}
(hasRequiredSignaturesTry, hasMaximumSignaturesTry) match {
case (Success(hasRequiredSignatures), Success(hasMaximumSignatures)) =>
(hasRequiredSignaturesOpt, hasMaximumSignaturesOpt) match {
case (Some(_), Some(maximumSignatures)) =>
val isStandardOps = asm.forall(op =>
op.isInstanceOf[ScriptConstant] || op
.isInstanceOf[BytesToPushOntoStack] || op
@ -289,32 +285,57 @@ object MultiSignatureScriptPubKey
op == OP_CHECKMULTISIGVERIFY)
val result =
asm.nonEmpty && containsMultiSigOp && hasRequiredSignatures &&
hasMaximumSignatures && isStandardOps
asm.nonEmpty && containsMultiSigOp &&
hasMaximumSignatures(maximumSignatures, asm) && isStandardOps
result
case (Success(_), Failure(_)) => false
case (Failure(_), Success(_)) => false
case (Failure(_), Failure(_)) => false
case (Some(_), None) => false
case (None, Some(_)) => false
case (None, None) => false
}
} else {
false
}
}
def parsePublicKeys(
maxSigs: Int,
asm: Seq[ScriptToken]): Seq[ECPublicKeyBytes] = {
asm
.filter(_.isInstanceOf[ScriptConstant])
.slice(1, maxSigs + 1)
.map(t => ECPublicKeyBytes(t.bytes))
}
private def hasMaximumSignatures(
maxSigs: Int,
asm: Seq[ScriptToken]): Boolean = {
parsePublicKeys(maxSigs, asm).size == maxSigs
}
/** Checks that the given script token is with the range of the maximum amount of
* public keys we can have in a
* [[org.bitcoins.core.protocol.script.MultiSignatureScriptPubKey MultiSignatureScriptPubKey]]
*/
@tailrec private def isValidPubKeyNumber(token: ScriptToken): Boolean = {
@tailrec private def isValidPubKeyNumber(token: ScriptToken): Option[Int] = {
token match {
case sn: ScriptNumber =>
sn >= ScriptNumber.zero && sn <= ScriptNumber(
Consensus.maxPublicKeysPerMultiSig)
if (
sn >= ScriptNumber.zero && sn <= ScriptNumber(
Consensus.maxPublicKeysPerMultiSig)
) {
Some(sn.toInt)
} else {
None
}
case constant: ScriptConstant =>
val sn = ScriptNumber(constant.bytes)
isValidPubKeyNumber(sn)
case _: ScriptToken => false
//Consensus.maxPublicKeysPerMultiSig is at most 20, which can be represented by 2 bytes
if (constant.bytes.size <= 2) {
val sn = ScriptNumber.fromBytes(constant.bytes)
isValidPubKeyNumber(sn)
} else {
None
}
case _: ScriptToken => None
}
}
}