Completed
Pull Request — master (#220)
by thomas
73:29 queued 68:53
created

BloomFilter::containsOutPoint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace BitWasp\Bitcoin\Bloom;
4
5
use BitWasp\Bitcoin\Crypto\Hash;
6
use BitWasp\Bitcoin\Flags;
7
use BitWasp\Bitcoin\Math\Math;
8
use BitWasp\Bitcoin\Serializer\Bloom\BloomFilterSerializer;
9
use BitWasp\Bitcoin\Script\ScriptFactory;
10
use BitWasp\Bitcoin\Serializable;
11
use BitWasp\Bitcoin\Transaction\OutPointInterface;
12
use BitWasp\Bitcoin\Transaction\TransactionInterface;
13
use BitWasp\Buffertools\Buffer;
14
15
class BloomFilter extends Serializable
16
{
17
    const LN2SQUARED = '0.4804530139182014246671025263266649717305529515945455';
18
    const LN2 = '0.6931471805599453094172321214581765680755001343602552';
19
    const MAX_HASH_FUNCS = '50';
20
    const MAX_FILTER_SIZE = 36000; // bytes
21
    const TWEAK_START = '4221880213'; // 0xFBA4C795
22
23
    const UPDATE_NONE = 0;
24
    const UPDATE_ALL = 1;
25
    const UPDATE_P2PUBKEY_ONLY = 2;
26
    const UPDATE_MASK = 3;
27
28
    /**
29
     * @var Math
30
     */
31
    private $math;
32
33
    /**
34
     * @var bool
35
     */
36
    private $empty = true;
37
38
    /**
39
     * @var bool
40
     */
41
    private $full = false;
42
43
    /**
44
     * @var int
45
     */
46
    private $numHashFuncs;
47
48
    /**
49
     * @var array
50
     */
51
    private $vFilter = [];
52
53
    /**
54
     * @var int|string
55
     */
56
    private $nTweak;
57
58
    /**
59
     * @var Flags
60
     */
61
    private $flags;
62
63
    /**
64
     * @param Math $math
65
     * @param array $vFilter
66
     * @param int $numHashFuncs
67
     * @param int|string $nTweak
68
     * @param Flags $flags
69
     */
70 132
    public function __construct(Math $math, array $vFilter, $numHashFuncs, $nTweak, Flags $flags)
71
    {
72 132
        $this->math = $math;
73 132
        $this->vFilter = $vFilter;
74 132
        $this->numHashFuncs = $numHashFuncs;
75 132
        $this->nTweak = $nTweak;
76 132
        $this->flags = $flags;
77 132
        $this->updateEmptyFull();
78 132
    }
79
80
    /**
81
     * @param int $size
82
     * @return array
83
     */
84 102
    public static function emptyFilter($size)
85
    {
86 102
        return str_split(str_pad('', $size, '0'), 1);
87
    }
88
89
    /**
90
     * Create the Bloom Filter given the number of elements, a false positive rate,
91
     * and the flags governing how the filter should be updated.
92
     *
93
     * @param Math $math
94
     * @param int $nElements
95
     * @param float $nFpRate
96
     * @param int $nTweak
97
     * @param Flags $flags
98
     * @return BloomFilter
99
     */
100 102
    public static function create(Math $math, $nElements, $nFpRate, $nTweak, Flags $flags)
101
    {
102 102
        $size = self::idealSize($nElements, $nFpRate);
103
104 102
        return new self(
105 102
            $math,
106 102
            self::emptyFilter($size),
107 102
            self::idealNumHashFuncs($size, $nElements),
108 102
            $nTweak,
109
            $flags
110 102
        );
111
    }
112
113
    /**
114
     * @param int $flag
115
     * @return bool
116
     */
117 36
    public function checkFlag($flag)
118
    {
119 36
        return $this->math->cmp($this->math->bitwiseAnd($this->flags->getFlags(), self::UPDATE_MASK), $flag) === 0;
120
    }
121
122
    /**
123
     * @return bool
124
     */
125 6
    public function isUpdateNone()
126
    {
127 6
        return $this->checkFlag(self::UPDATE_NONE);
128
    }
129
130
    /**
131
     * @return bool
132
     */
133 36
    public function isUpdateAll()
134
    {
135 36
        return $this->checkFlag(self::UPDATE_ALL);
136
    }
137
138
    /**
139
     * @return bool
140
     */
141 24
    public function isUpdatePubKeyOnly()
142
    {
143 24
        return $this->checkFlag(self::UPDATE_P2PUBKEY_ONLY);
144
    }
145
146
    /**
147
     * @return bool
148
     */
149 96
    public function isEmpty()
150
    {
151 96
        return $this->empty;
152
    }
153
154
    /**
155
     * @return bool
156
     */
157 120
    public function isFull()
158
    {
159 120
        return $this->full;
160
    }
161
162
    /**
163
     * @return array
164
     */
165 24
    public function getData()
166
    {
167 24
        return $this->vFilter;
168
    }
169
170
    /**
171
     * @return int
172
     */
173 24
    public function getNumHashFuncs()
174
    {
175 24
        return $this->numHashFuncs;
176
    }
177
178
    /**
179
     * @return int|string
180
     */
181 24
    public function getTweak()
182
    {
183 24
        return $this->nTweak;
184
    }
185
186
    /**
187
     * @return Flags
188
     */
189 24
    public function getFlags()
190
    {
191 24
        return $this->flags;
192
    }
193
194
    /**
195
     * @param int $nElements
196
     * @param float $fpRate
197
     * @return int
198
     */
199 102
    public static function idealSize($nElements, $fpRate)
200
    {
201 102
        return (int) floor(
202 102
            bcdiv(
203 102
                min(
204 102
                    bcmul(
205 102
                        bcmul(
206 102
                            bcdiv(
207 102
                                -1,
208
                                self::LN2SQUARED
209 102
                            ),
210
                            $nElements
211 102
                        ),
212 102
                        log($fpRate)
213 102
                    ),
214 102
                    bcmul(
215 102
                        self::MAX_FILTER_SIZE,
216
                        8
217 102
                    )
218 102
                ),
219
                8
220 102
            )
221 102
        );
222
    }
223
224
    /**
225
     * @param int $filterSize
226
     * @param int $nElements
227
     * @return int
228
     */
229 102
    public static function idealNumHashFuncs($filterSize, $nElements)
230
    {
231 102
        return (int) floor(
232 102
            min(
233 102
                bcmul(
234 102
                    bcdiv(
235 102
                        bcmul(
236 102
                            $filterSize,
237
                            8
238 102
                        ),
239
                        $nElements
240 102
                    ),
241
                    self::LN2
242 102
                ),
243 102
                bcmul(
244 102
                    self::MAX_FILTER_SIZE,
245
                    8
246 102
                )
247 102
            )
248 102
        );
249
    }
250
251
    /**
252
     * @param int $nHashNum
253
     * @param Buffer $data
254
     * @return string
255
     */
256 78
    public function hash($nHashNum, Buffer $data)
257
    {
258 78
        return $this->math->mod(
259 78
            Hash::murmur3($data, ($nHashNum * self::TWEAK_START + $this->nTweak) & 0xffffffff)->getInt(),
260 78
            count($this->vFilter) * 8
261 78
        );
262
    }
263
264
    /**
265
     * @param Buffer $data
266
     * @return $this
267
     */
268 84
    public function insertData(Buffer $data)
269
    {
270 84
        if ($this->isFull()) {
271 6
            return $this;
272
        }
273
274 78
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
275 78
            $index = $this->hash($i, $data);
276 78
            $this->vFilter[$index >> 3] |= (1 << (7 & $index));
277 78
        }
278
279 78
        $this->updateEmptyFull();
280 78
        return $this;
281
    }
282
283
    /**
284
     * @param OutPointInterface $outPoint
285
     * @return BloomFilter
286
     */
287 24
    public function insertOutPoint(OutPointInterface $outPoint)
288
    {
289 24
        return $this->insertData($outPoint->getBuffer());
290
    }
291
292
    /**
293
     * @param Buffer $data
294
     * @return bool
295
     */
296 84
    public function containsData(Buffer $data)
297
    {
298 84
        if ($this->isFull()) {
299 6
            return true;
300
        }
301
302 78
        if ($this->isEmpty()) {
303 6
            return false;
304
        }
305
306 72
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
307 72
            $index = $this->hash($i, $data);
308
309 72
            if (!($this->vFilter[($index >> 3)] & (1 << (7 & $index)))) {
310 72
                return false;
311
            }
312 72
        }
313
314 72
        return true;
315
    }
316
317
    /**
318
     * @param OutPointInterface $outPoint
319
     * @return bool
320
     */
321 30
    public function containsOutPoint(OutPointInterface $outPoint)
322
    {
323 30
        return $this->containsData($outPoint->getBuffer());
324
    }
325
326
    /**
327
     * @return bool
328
     */
329 6
    public function hasAcceptableSize()
330
    {
331 6
        return count($this->vFilter) <= self::MAX_FILTER_SIZE && $this->numHashFuncs <= self::MAX_HASH_FUNCS;
332
    }
333
334
    /**
335
     * @param TransactionInterface $tx
336
     * @return bool
337
     */
338 72
    public function isRelevantAndUpdate(TransactionInterface $tx)
339
    {
340 72
        $this->updateEmptyFull();
341 72
        $found = false;
342 72
        if ($this->isFull()) {
343 6
            return true;
344
        }
345
346 66
        if ($this->isEmpty()) {
347 12
            return false;
348
        }
349
350
        // Check if the txid hash is in the filter
351 54
        $txHash = $tx->getTxId();
352 54
        if ($this->containsData($txHash)) {
353 42
            $found = true;
354 42
        }
355
356
        // Check for relevant output scripts. We add the outpoint to the filter if found.
357 54
        foreach ($tx->getOutputs() as $vout => $output) {
358 54
            $script = $output->getScript();
359 54
            $parser = $script->getScriptParser();
360 54
            foreach ($parser as $exec) {
361 54
                if ($exec->isPush() && $this->containsData($exec->getData())) {
362 30
                    $found = true;
363 30
                    if ($this->isUpdateAll()) {
364 12
                        $this->insertOutPoint($tx->makeOutPoint($vout));
365 30
                    } else if ($this->isUpdatePubKeyOnly()) {
366 12
                        $type = ScriptFactory::scriptPubKey()->classify($script);
367 12
                        if ($type->isMultisig() || $type->isPayToPublicKey()) {
368 12
                            $this->insertOutPoint($tx->makeOutPoint($vout));
369 12
                        }
370 12
                    }
371 30
                }
372 54
            }
373 54
        }
374
375 54
        if ($found) {
376 54
            return true;
377
        }
378
379 30
        foreach ($tx->getInputs() as $txIn) {
380 30
            if ($this->containsOutPoint($txIn->getOutPoint())) {
381 12
                return true;
382
            }
383
384 30
            $parser = $txIn->getScript()->getScriptParser();
385 30
            foreach ($parser as $exec) {
386 30
                if ($exec->isPush() > 0 && $this->containsData($exec->getData())) {
387 6
                    return true;
388
                }
389 30
            }
390 30
        }
391
392 30
        return false;
393
    }
394
395
    /**
396
     *
397
     */
398 132
    public function updateEmptyFull()
399
    {
400 132
        $full = true;
401 132
        $empty = true;
402 132
        for ($i = 0, $size = count($this->vFilter); $i < $size; $i++) {
403 132
            $byte = (int) $this->vFilter[$i];
404 132
            $full &= ($byte === 0xff);
405 132
            $empty &= ($byte === 0x0);
406 132
        }
407
408 132
        $this->full = (bool)$full;
409 132
        $this->empty = (bool)$empty;
410 132
    }
411
412
    /**
413
     * @return Buffer
414
     */
415 24
    public function getBuffer()
416
    {
417 24
        return (new BloomFilterSerializer())->serialize($this);
418
    }
419
}
420