Adding properties to make sure address from string fails if we have mixed case or we replace a random char in the string

This commit is contained in:
Chris Stewart 2017-10-27 12:47:51 -05:00
parent 661b25acbb
commit e3311b6694
3 changed files with 49 additions and 11 deletions

View File

@ -128,7 +128,7 @@ sealed abstract class Bech32Address extends BitcoinAddress {
val checksum = Bech32Address.createChecksum(hrp,data)
val all = data ++ checksum
val encoding = Bech32Address.encodeToString(all)
hrp.toString + "1" + encoding
hrp.toString + Bech32Address.separator + encoding
}
override def scriptPubKey: WitnessScriptPubKey = {
@ -142,9 +142,10 @@ sealed abstract class Bech32Address extends BitcoinAddress {
object Bech32Address {
private case class Bech32AddressImpl(hrp: HumanReadablePart, data: Seq[UInt8]) extends Bech32Address
private val logger = BitcoinSLogger.logger
/** Separator used to separate the hrp & data parts of a bech32 addr */
val separator = '1'
def isValid(bytes: Seq[Byte]): Boolean = ???
private val logger = BitcoinSLogger.logger
def apply(witSPK: WitnessScriptPubKey,
networkParameters: NetworkParameters): Try[Bech32Address] = {
@ -155,8 +156,8 @@ object Bech32Address {
case _: MainNet => bc
case _: TestNet3 | _: RegTest => tb
}
//add witversion
encoded.map(e => Bech32Address(hrp,Seq(UInt8.zero) ++ e))
val witVersion = witSPK.witnessVersion.version.underlying.toShort
encoded.map(e => Bech32Address(hrp,Seq(UInt8(witVersion)) ++ e))
}
@ -220,7 +221,7 @@ object Bech32Address {
private val u32Five = UInt32(5)
private val u32Eight = UInt32(8)
/** The separator between the hrp and payload of bech 32 */
private val separator = '1'
/** Converts a byte array from base 8 to base 5 */
def encode(bytes: Seq[UInt8]): Try[Seq[UInt8]] = {
NumberUtil.convertUInt8s(bytes,u32Eight,u32Five,true)

View File

@ -491,9 +491,11 @@ sealed trait WitnessScriptPubKey extends ScriptPubKey {
object WitnessScriptPubKey {
/** Witness scripts must begin with one of these operations, see BIP141 */
private val validFirstOps: Seq[ScriptNumberOperation] = Seq(OP_0,OP_1,OP_2,OP_3,OP_4,OP_5,OP_6, OP_7, OP_8,
val validWitVersions: Seq[ScriptNumberOperation] = Seq(OP_0,OP_1,OP_2,OP_3,OP_4,OP_5,OP_6, OP_7, OP_8,
OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16)
val unassignedWitVersions = validWitVersions.tail
def apply(asm: Seq[ScriptToken]): Option[WitnessScriptPubKey] = fromAsm(asm)
def fromAsm(asm: Seq[ScriptToken]): Option[WitnessScriptPubKey] = asm match {
@ -510,7 +512,7 @@ object WitnessScriptPubKey {
val bytes = asm.flatMap(_.bytes)
val firstOp = asm.headOption
if (bytes.size < 4 || bytes.size > 42) false
else if (!validFirstOps.contains(firstOp.getOrElse(OP_1NEGATE))) false
else if (!validWitVersions.contains(firstOp.getOrElse(OP_1NEGATE))) false
else if (asm(1).toLong + 2 == bytes.size) true
else false
}

View File

@ -1,11 +1,14 @@
package org.bitcoins.core.protocol
import org.bitcoins.core.gen.{AddressGenerator, ChainParamsGenerator, ScriptGenerators}
import org.bitcoins.core.util.BitcoinSLogger
import org.scalacheck.{Gen, Prop, Properties}
import scala.annotation.tailrec
import scala.util.{Random, Success}
class Bech32Spec extends Properties("Bech32Spec") {
private val logger = BitcoinSLogger.logger
property("serialization symmetry") = {
Prop.forAll(ScriptGenerators.witnessScriptPubKeyV0,ChainParamsGenerator.networkParams) { case ((witSPK,_),network) =>
@ -16,15 +19,47 @@ class Bech32Spec extends Properties("Bech32Spec") {
}
property("checksum must not work if we modify a char") = {
Prop.forAll(AddressGenerator.bech32Address) { case addr : Bech32Address =>
val old = addr. value
Prop.forAll(AddressGenerator.bech32Address) { case addr: Bech32Address =>
val old = addr.value
val rand = Math.abs(Random.nextInt)
val idx = rand % old.size
val replacementChar = Bech32Address.charset(rand % Bech32Address.charset.size)
val (f,l) = old.splitAt(idx)
val replacementChar = pickReplacementChar(l.head)
val replaced = f ++ Seq(replacementChar) ++ l.tail
//should fail because we replaced a char in the addr, so checksum invalid
Bech32Address.fromString(replaced).isFailure
}
}
property("must fail if we have a mixed case") = {
Prop.forAllNoShrink(AddressGenerator.bech32Address) { case addr: Bech32Address =>
val old = addr.value
val replaced = switchCaseRandChar(old)
//should fail because we we switched the case of a random char
Bech32Address.fromString(replaced).isFailure
}
}
@tailrec
private def pickReplacementChar(oldChar: Char): Char = {
val rand = Math.abs(Random.nextInt)
val newChar = Bech32Address.charset(rand % Bech32Address.charset.size)
//make sure we don't pick the same char we are replacing in the bech32 address
if (oldChar == newChar) pickReplacementChar(oldChar)
else newChar
}
@tailrec
private def switchCaseRandChar(addr: String): String = {
val rand = Math.abs(Random.nextInt)
val idx = rand % addr.size
val (f,l) = addr.splitAt(idx)
if (l.head.isDigit) {
switchCaseRandChar(addr)
} else if (l.head.isUpper) {
f ++ Seq(l.head.toLower) ++ l.tail
} else {
f ++ Seq(l.head.toUpper) ++ l.tail
}
}
}