BloomFilter::isRelevantAndUpdate()   D
last analyzed

Complexity

Conditions 18
Paths 86

Size

Total Lines 56
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 18

Importance

Changes 0
Metric Value
cc 18
eloc 31
nc 86
nop 1
dl 0
loc 56
ccs 32
cts 32
cp 1
crap 18
rs 4.8666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Bloom;
6
7
use BitWasp\Bitcoin\Crypto\Hash;
8
use BitWasp\Bitcoin\Math\Math;
9
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
10
use BitWasp\Bitcoin\Serializable;
11
use BitWasp\Bitcoin\Serializer\Bloom\BloomFilterSerializer;
12
use BitWasp\Bitcoin\Transaction\OutPointInterface;
13
use BitWasp\Bitcoin\Transaction\TransactionInterface;
14
use BitWasp\Buffertools\BufferInterface;
15
16
class BloomFilter extends Serializable
17
{
18
    const LN2SQUARED = '0.4804530139182014246671025263266649717305529515945455';
19
    const LN2 = '0.6931471805599453094172321214581765680755001343602552';
20
    const MAX_HASH_FUNCS = '50';
21
    const MAX_FILTER_SIZE = 36000; // bytes
22
    const TWEAK_START = 0xFBA4C795;
23
24
    const UPDATE_NONE = 0;
25
    const UPDATE_ALL = 1;
26
    const UPDATE_P2PUBKEY_ONLY = 2;
27
    const UPDATE_MASK = 3;
28
29
    /**
30
     * @var Math
31
     */
32
    private $math;
33
34
    /**
35
     * @var bool
36
     */
37
    private $empty = true;
38
39
    /**
40
     * @var bool
41
     */
42
    private $full = false;
43
44
    /**
45
     * @var int
46
     */
47
    private $numHashFuncs;
48
49
    /**
50
     * @var array
51
     */
52
    private $vFilter = [];
53
54
    /**
55
     * @var int
56
     */
57
    private $nTweak;
58
59
    /**
60
     * @var int
61
     */
62
    private $flags;
63
64
    /**
65
     * @param Math $math
66
     * @param array $vFilter
67
     * @param int $numHashFuncs
68
     * @param int $nTweak
69
     * @param int $flags
70
     */
71 22
    public function __construct(Math $math, array $vFilter, int $numHashFuncs, int $nTweak, int $flags)
72
    {
73 22
        $this->math = $math;
74 22
        $this->vFilter = $vFilter;
75 22
        $this->numHashFuncs = $numHashFuncs;
76 22
        $this->nTweak = $nTweak;
77 22
        $this->flags = $flags;
78 22
        $this->updateEmptyFull();
79 22
    }
80
81
    /**
82
     * @param int $size
83
     * @return array
84
     */
85 17
    public static function emptyFilter(int $size): array
86
    {
87 17
        return str_split(str_pad('', $size, '0'), 1);
0 ignored issues
show
Bug Best Practice introduced by
The expression return str_split(str_pad('', $size, '0'), 1) could return the type true which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
88
    }
89
90
    /**
91
     * Create the Bloom Filter given the number of elements, a false positive rate,
92
     * and the flags governing how the filter should be updated.
93
     *
94
     * @param Math $math
95
     * @param int $nElements
96
     * @param float $nFpRate
97
     * @param int $nTweak
98
     * @param int $flags
99
     * @return BloomFilter
100
     */
101 17
    public static function create(Math $math, int $nElements, float $nFpRate, int $nTweak, int $flags): BloomFilter
102
    {
103 17
        $size = self::idealSize($nElements, $nFpRate);
104
105 17
        return new self(
106 17
            $math,
107 17
            self::emptyFilter($size),
108 17
            self::idealNumHashFuncs($size, $nElements),
109 17
            $nTweak,
110 17
            $flags
111
        );
112
    }
113
114
    /**
115
     * @return bool
116
     */
117 1
    public function isUpdateNone(): bool
118
    {
119 1
        return (($this->flags & self::UPDATE_MASK) === self::UPDATE_NONE);
120
    }
121
122
    /**
123
     * @return bool
124
     */
125 6
    public function isUpdateAll(): bool
126
    {
127 6
        return (($this->flags & self::UPDATE_MASK) === self::UPDATE_ALL);
128
    }
129
130
    /**
131
     * @return bool
132
     */
133 4
    public function isUpdatePubKeyOnly(): bool
134
    {
135 4
        return (($this->flags & self::UPDATE_MASK) === self::UPDATE_P2PUBKEY_ONLY);
136
    }
137
138
    /**
139
     * @return bool
140
     */
141 16
    public function isEmpty(): bool
142
    {
143 16
        return $this->empty;
144
    }
145
146
    /**
147
     * @return bool
148
     */
149 20
    public function isFull(): bool
150
    {
151 20
        return $this->full;
152
    }
153
154
    /**
155
     * @return array
156
     */
157 4
    public function getData(): array
158
    {
159 4
        return $this->vFilter;
160
    }
161
162
    /**
163
     * @return int
164
     */
165 4
    public function getNumHashFuncs(): int
166
    {
167 4
        return $this->numHashFuncs;
168
    }
169
170
    /**
171
     * @return int
172
     */
173 4
    public function getTweak(): int
174
    {
175 4
        return $this->nTweak;
176
    }
177
178
    /**
179
     * @return int
180
     */
181 4
    public function getFlags(): int
182
    {
183 4
        return $this->flags;
184
    }
185
186
    /**
187
     * @param int $nElements
188
     * @param float $fpRate
189
     * @return int
190
     */
191 17
    public static function idealSize(int $nElements, float $fpRate): int
192
    {
193 17
        return (int) floor(
194 17
            bcdiv(
0 ignored issues
show
Bug introduced by
bcdiv(min(bcmul(bcmul(bc...ILTER_SIZE, '8')), '8') of type null|string is incompatible with the type double|integer expected by parameter $num of floor(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

194
            /** @scrutinizer ignore-type */ bcdiv(
Loading history...
195 17
                min(
196 17
                    bcmul(
197 17
                        bcmul(
198 17
                            bcdiv(
199 17
                                '-1',
200 17
                                (string) self::LN2SQUARED
201
                            ),
202 17
                            (string) $nElements
203
                        ),
204 17
                        (string) log($fpRate)
205
                    ),
206 17
                    bcmul(
207 17
                        (string) self::MAX_FILTER_SIZE,
208 17
                        '8'
209
                    )
210
                ),
211 17
                '8'
212
            )
213
        );
214
    }
215
216
    /**
217
     * @param int $filterSize
218
     * @param int $nElements
219
     * @return int
220
     */
221 17
    public static function idealNumHashFuncs(int $filterSize, int $nElements)
222
    {
223 17
        return (int) floor(
224 17
            min(
225 17
                bcmul(
226 17
                    bcdiv(
227 17
                        bcmul(
228 17
                            (string) $filterSize,
229 17
                            '8'
230
                        ),
231 17
                        (string) $nElements
232
                    ),
233 17
                    (string) self::LN2
234
                ),
235 17
                bcmul(
236 17
                    (string) self::MAX_FILTER_SIZE,
237 17
                    '8'
238
                )
239
            )
240
        );
241
    }
242
243
    /**
244
     * @param int $nHashNum
245
     * @param BufferInterface $data
246
     * @return string
247
     */
248 13
    public function hash(int $nHashNum, BufferInterface $data): string
249
    {
250 13
        $hash = Hash::murmur3($data, ($nHashNum * self::TWEAK_START + $this->nTweak) & 0xffffffff)->getInt();
251 13
        $hash = gmp_init($hash, 10);
252 13
        $hash = $this->math->mod($hash, gmp_init(count($this->vFilter) * 8));
0 ignored issues
show
Bug introduced by
It seems like gmp_init(count($this->vFilter) * 8) can also be of type resource; however, parameter $modulus of Mdanter\Ecc\Math\GmpMath::mod() does only seem to accept GMP, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
        $hash = $this->math->mod($hash, /** @scrutinizer ignore-type */ gmp_init(count($this->vFilter) * 8));
Loading history...
Bug introduced by
It seems like $hash can also be of type resource; however, parameter $number of Mdanter\Ecc\Math\GmpMath::mod() does only seem to accept GMP, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
        $hash = $this->math->mod(/** @scrutinizer ignore-type */ $hash, gmp_init(count($this->vFilter) * 8));
Loading history...
253 13
        return gmp_strval($hash, 10);
254
    }
255
256
    /**
257
     * @param BufferInterface $data
258
     * @return $this
259
     */
260 14
    public function insertData(BufferInterface $data)
261
    {
262 14
        if ($this->isFull()) {
263 1
            return $this;
264
        }
265
266 13
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
267 13
            $index = $this->hash($i, $data);
268 13
            $this->vFilter[$index >> 3] |= (1 << (7 & $index));
269
        }
270
271 13
        $this->updateEmptyFull();
272 13
        return $this;
273
    }
274
275
    /**
276
     * @param OutPointInterface $outPoint
277
     * @return BloomFilter
278
     */
279 4
    public function insertOutPoint(OutPointInterface $outPoint): BloomFilter
280
    {
281 4
        return $this->insertData($outPoint->getBuffer());
282
    }
283
284
    /**
285
     * @param BufferInterface $data
286
     * @return bool
287
     */
288 14
    public function containsData(BufferInterface $data): bool
289
    {
290 14
        if ($this->isFull()) {
291 1
            return true;
292
        }
293
294 13
        if ($this->isEmpty()) {
295 1
            return false;
296
        }
297
298 12
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
299 12
            $index = $this->hash($i, $data);
300
301 12
            if (!($this->vFilter[($index >> 3)] & (1 << (7 & $index)))) {
302 12
                return false;
303
            }
304
        }
305
306 12
        return true;
307
    }
308
309
    /**
310
     * @param OutPointInterface $outPoint
311
     * @return bool
312
     */
313 5
    public function containsOutPoint(OutPointInterface $outPoint): bool
314
    {
315 5
        return $this->containsData($outPoint->getBuffer());
316
    }
317
318
    /**
319
     * @return bool
320
     */
321 1
    public function hasAcceptableSize(): bool
322
    {
323 1
        return count($this->vFilter) <= self::MAX_FILTER_SIZE && $this->numHashFuncs <= self::MAX_HASH_FUNCS;
324
    }
325
326
    /**
327
     * @param TransactionInterface $tx
328
     * @return bool
329
     */
330 12
    public function isRelevantAndUpdate(TransactionInterface $tx): bool
331
    {
332 12
        $this->updateEmptyFull();
333 12
        $found = false;
334 12
        if ($this->isFull()) {
335 1
            return true;
336
        }
337
338 11
        if ($this->isEmpty()) {
339 2
            return false;
340
        }
341
342
        // Check if the txid hash is in the filter
343 9
        $txHash = $tx->getTxId();
344 9
        if ($this->containsData($txHash)) {
345 7
            $found = true;
346
        }
347
348 9
        $classifier = new OutputClassifier();
349
        
350
        // Check for relevant output scripts. We add the outpoint to the filter if found.
351 9
        foreach ($tx->getOutputs() as $vout => $output) {
352 9
            $script = $output->getScript();
353 9
            $parser = $script->getScriptParser();
354 9
            foreach ($parser as $exec) {
355 9
                if ($exec->isPush() && $this->containsData($exec->getData())) {
356 5
                    $found = true;
357 5
                    if ($this->isUpdateAll()) {
358 2
                        $this->insertOutPoint($tx->makeOutPoint($vout));
359 3
                    } else if ($this->isUpdatePubKeyOnly()) {
360 2
                        if ($classifier->isMultisig($script) || $classifier->isPayToPublicKey($script)) {
361 2
                            $this->insertOutPoint($tx->makeOutPoint($vout));
362
                        }
363
                    }
364
                }
365
            }
366
        }
367
368 9
        if ($found) {
369 9
            return true;
370
        }
371
372 5
        foreach ($tx->getInputs() as $txIn) {
373 5
            if ($this->containsOutPoint($txIn->getOutPoint())) {
374 2
                return true;
375
            }
376
377 5
            $parser = $txIn->getScript()->getScriptParser();
378 5
            foreach ($parser as $exec) {
379 5
                if ($exec->isPush() > 0 && $this->containsData($exec->getData())) {
380 1
                    return true;
381
                }
382
            }
383
        }
384
385 5
        return false;
386
    }
387
388
    /**
389
     *
390
     */
391 22
    public function updateEmptyFull()
392
    {
393 22
        $full = true;
394 22
        $empty = true;
395 22
        for ($i = 0, $size = count($this->vFilter); $i < $size; $i++) {
396 22
            $byte = (int) $this->vFilter[$i];
397 22
            $full &= ($byte === 0xff);
398 22
            $empty &= ($byte === 0x0);
399
        }
400
401 22
        $this->full = (bool)$full;
402 22
        $this->empty = (bool)$empty;
403 22
    }
404
405
    /**
406
     * @return BufferInterface
407
     */
408 4
    public function getBuffer(): BufferInterface
409
    {
410 4
        return (new BloomFilterSerializer())->serialize($this);
411
    }
412
}
413