Skip to content

Commit

Permalink
Refactor compact filter matching (#838)
Browse files Browse the repository at this point in the history
* Refactor compact filter matching

* cleanup

* some more changes

* addressed the PR comments

* cleanup

* cleanup
  • Loading branch information
rorp authored and Christewart committed Nov 26, 2019
1 parent 2527354 commit 804f18f
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.bitcoins.chain.api.ChainApi
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.chain.models._
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.gcs.FilterHeader
import org.bitcoins.core.gcs.{FilterHeader, SimpleFilterMatcher}
import org.bitcoins.core.p2p.CompactFilterMessage
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.blockchain.BlockHeader
Expand Down Expand Up @@ -421,7 +421,8 @@ case class ChainHandler(
// Find any matches in the group and add the corresponding block hashes into the result
filterGroup.foldLeft(Vector.empty[DoubleSha256DigestBE]) {
(blocks, filter) =>
if (filter.golombFilter.matchesAny(bytes)) {
val matcher = new SimpleFilterMatcher(filter.golombFilter)
if (matcher.matchesAny(bytes)) {
blocks :+ filter.blockHashBE
} else {
blocks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ class BlockFilterTest extends BitcoinSUnitTest {
def runTest(): org.scalatest.Assertion = {
val constructedFilter = BlockFilter(block, prevOutputScripts)

assert(constructedFilter.decodedHashes == filter.decodedHashes, clue)

assert(constructedFilter.encodedData.bytes == filter.encodedData.bytes,
clue)

val matcher = new BinarySearchFilterMatcher(filter)
val constructedMatcher = new BinarySearchFilterMatcher(constructedFilter)

assert(constructedMatcher.decodedHashes == matcher.decodedHashes, clue)

val constructedHeader = constructedFilter.getHeader(prevHeader.flip)

assert(constructedHeader.hash == header.flip, clue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ class GolombFilterTest extends BitcoinSUnitTest {
val encodedData = GCS.encodeSortedSet(data, p)
val filter =
GolombFilter(k, m, p, CompactSizeUInt(UInt64(2)), encodedData)
val binarySearchMatcher = new BinarySearchFilterMatcher(filter)

assert(!filter.matchesHash(rand))
assert(filter.matchesHash(data1))
assert(filter.matchesHash(data2))
assert(!filter.matchesAnyHash(Vector(rand)))
assert(filter.matchesAnyHash(Vector(rand, data1, data2)))
assert(!binarySearchMatcher.matchesHash(rand))
assert(binarySearchMatcher.matchesHash(data1))
assert(binarySearchMatcher.matchesHash(data2))
assert(!binarySearchMatcher.matchesAnyHash(Vector(rand)))
assert(binarySearchMatcher.matchesAnyHash(Vector(rand, data1, data2)))

val simpleMatcher = new SimpleFilterMatcher(filter)

assert(!simpleMatcher.matchesHash(rand))
assert(simpleMatcher.matchesHash(data1))
assert(simpleMatcher.matchesHash(data2))
assert(!simpleMatcher.matchesAnyHash(Vector(rand)))
assert(simpleMatcher.matchesAnyHash(Vector(rand, data1, data2)))
}
}

Expand All @@ -50,15 +59,19 @@ class GolombFilterTest extends BitcoinSUnitTest {
forAll(genKey, genData, genRandHashes) {
case (k, data, randHashes) =>
val filter = GCS.buildBasicBlockFilter(data, k)
val hashes = filter.decodedHashes
val binarySearchMatcher = new BinarySearchFilterMatcher(filter)
val simpleMatcher = new SimpleFilterMatcher(filter)
val hashes = binarySearchMatcher.decodedHashes

data.foreach(element => assert(filter.matches(element)))
assert(filter.matchesAny(data))
data.foreach(element => assert(binarySearchMatcher.matches(element)))
assert(binarySearchMatcher.matchesAny(data))
assert(simpleMatcher.matchesAny(data))

val hashesNotInData: Vector[UInt64] =
randHashes.filterNot(hashes.contains)

hashesNotInData.foreach(hash => assert(!filter.matchesHash(hash)))
hashesNotInData.foreach(hash =>
assert(!binarySearchMatcher.matchesHash(hash)))
}
}

Expand Down
72 changes: 72 additions & 0 deletions core/src/main/scala/org/bitcoins/core/gcs/BlockFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.bitcoins.core.gcs

import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.core.protocol.CompactSizeUInt
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.script.{EmptyScriptPubKey, ScriptPubKey}
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.util.BitcoinSUtil
import scodec.bits.ByteVector

object BlockFilter {

/**
* Returns all ScriptPubKeys from a Block's outputs that are relevant
* to BIP 158 Basic Block Filters
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki#contents]]
*/
def getOutputScriptPubKeysFromBlock(block: Block): Vector[ScriptPubKey] = {
val transactions: Vector[Transaction] = block.transactions.toVector

val newOutputs: Vector[TransactionOutput] = transactions.flatMap(_.outputs)

newOutputs
.filterNot(_.scriptPubKey.asm.contains(OP_RETURN))
.filterNot(_.scriptPubKey == EmptyScriptPubKey)
.map(_.scriptPubKey)
}

/**
* Given a Block and access to the previous output scripts, constructs a Block Filter for that block
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki#block-filters]]
*/
def apply(
block: Block,
prevOutputScripts: Vector[ScriptPubKey]): GolombFilter = {
val keyBytes: ByteVector = block.blockHeader.hash.bytes.take(16)

val key: SipHashKey = SipHashKey(keyBytes)

val newScriptPubKeys: Vector[ByteVector] =
getOutputScriptPubKeysFromBlock(block).map(_.asmBytes)

val prevOutputScriptBytes: Vector[ByteVector] =
prevOutputScripts
.filterNot(_ == EmptyScriptPubKey)
.map(_.asmBytes)

val allOutputs = (prevOutputScriptBytes ++ newScriptPubKeys).distinct

GCS.buildBasicBlockFilter(allOutputs, key)
}

def fromBytes(
bytes: ByteVector,
blockHash: DoubleSha256Digest): GolombFilter = {
val n = CompactSizeUInt.fromBytes(bytes)
val filterBytes = bytes.drop(n.bytes.length)
val keyBytes: ByteVector = blockHash.bytes.take(16)
val key: SipHashKey = SipHashKey(keyBytes)

GolombFilter(key,
FilterType.Basic.M,
FilterType.Basic.P,
n,
filterBytes.toBitVector)
}

def fromHex(hex: String, blockHash: DoubleSha256Digest): GolombFilter = {
fromBytes(BitcoinSUtil.decodeHex(hex), blockHash)
}
}
128 changes: 128 additions & 0 deletions core/src/main/scala/org/bitcoins/core/gcs/BlockFilterMatcher.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package org.bitcoins.core.gcs

import org.bitcoins.core.number.UInt64
import scodec.bits.ByteVector

import scala.annotation.tailrec

sealed trait BlockFilterMatcher {

/**
* Checks if the underlying filter matches the given data
*/
def matches(data: ByteVector): Boolean

/**
* Checks if the underlying filter matches any item from the given collection
*/
def matchesAny(data: Vector[ByteVector]): Boolean
}

case class SimpleFilterMatcher(filter: GolombFilter)
extends BlockFilterMatcher {

override def matches(data: ByteVector): Boolean = {
val hash = filter.hashToRange(data)
matchesHash(hash)
}

/** Hashes the given vector of data and calls [[matchesAnyHash()]] to find a match */
override def matchesAny(data: Vector[ByteVector]): Boolean = {
val hashes = data.map(filter.hashToRange)
matchesAnyHash(hashes)
}

def matchesHash(hash: UInt64): Boolean = {
var matches = false
GCS.golombDecodeSetsWithPredicate(filter.encodedData, filter.p) {
decodedHash =>
if (hash > decodedHash) {
true
} else {
if (hash == decodedHash) {
matches = true
}
false
}
}
matches
}

/** It implements https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki#golomb-coded-set-multi-match */
def matchesAnyHash(hashes: Vector[UInt64]): Boolean = {
val sortedHashes = hashes.sorted
var matches = false
var i = 0

def predicate(decodedHash: UInt64): Boolean = {
while (i < sortedHashes.size) {
val hash = sortedHashes(i)
if (hash == decodedHash) {
matches = true
return false
} else if (hash > decodedHash) {
return true
} else {
i += 1
}
}
false
}

GCS.golombDecodeSetsWithPredicate(filter.encodedData, filter.p)(predicate)

matches
}

}

case class BinarySearchFilterMatcher(filter: GolombFilter)
extends BlockFilterMatcher {

lazy val decodedHashes: Vector[UInt64] =
GCS.golombDecodeSet(filter.encodedData, filter.p)

override def matches(data: ByteVector): Boolean = {
val hash = filter.hashToRange(data)

matchesHash(hash)
}

/** Hashes the given vector of data and calls [[matchesAnyHash()]] to find a match */
override def matchesAny(data: Vector[ByteVector]): Boolean = {
val hashes = data.map(filter.hashToRange)
matchesAnyHash(hashes)
}

def matchesHash(hash: UInt64): Boolean = {
@tailrec
def binarySearch(
from: Int,
to: Int,
hash: UInt64,
set: Vector[UInt64]): Boolean = {
if (to < from) {
false
} else {
val index = (to + from) / 2
val otherHash = set(index)

if (hash == otherHash) {
true
} else if (hash < otherHash) {
binarySearch(from, index - 1, hash, set)
} else {
binarySearch(index + 1, to, hash, set)
}
}
}

binarySearch(from = 0, to = filter.n.toInt - 1, hash, decodedHashes)
}

/** Checks whether there's a match for at least one of the given hashes
*/
def matchesAnyHash(hashes: Vector[UInt64]): Boolean =
hashes.exists(matchesHash)

}
25 changes: 19 additions & 6 deletions core/src/main/scala/org/bitcoins/core/gcs/GCS.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,23 +191,36 @@ object GCS {
* Decodes all hashes from golomb-encoded data, reversing [[GCS.encodeSortedSet]]
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki#set-queryingdecompression]]
*/
def golombDecodeSet(encodedData: BitVector, p: UInt8): Vector[UInt64] = {
def golombDecodeSet(encodedData: BitVector, p: UInt8): Vector[UInt64] =
golombDecodeSetsWithPredicate(encodedData, p) { _ =>
true
}

/**
* Decodes all hashes while the given predicate returns true
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki#set-queryingdecompression]]
*/
def golombDecodeSetsWithPredicate(encodedData: BitVector, p: UInt8)(
predicate: UInt64 => Boolean): Vector[UInt64] = {
@tailrec
def loop(
encoded: BitVector,
decoded: Vector[UInt64],
lastHash: UInt64): Vector[UInt64] = {
lastHash: UInt64,
decoded: Vector[UInt64]): Vector[UInt64] = {
if (encoded.length < p.toInt + 1) { // Only padding left
decoded
} else {
val (delta, encodedLeft) = golombDecodeItemFromSet(encoded, p)
val hash = lastHash + delta

loop(encodedLeft, decoded.:+(hash), hash)
if (predicate(hash)) {
loop(encodedLeft, hash, decoded :+ hash)
} else {
decoded
}
}
}

loop(encoded = encodedData, decoded = Vector.empty, lastHash = UInt64.zero)
loop(encoded = encodedData, lastHash = UInt64.zero, Vector.empty)
}

/**
Expand Down
Loading

0 comments on commit 804f18f

Please sign in to comment.