Completed
Pull Request — master (#255)
by thomas
90:53 queued 19:51
created

BloomFilter   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 14

Test Coverage

Coverage 98.7%

Importance

Changes 5
Bugs 0 Features 2
Metric Value
wmc 48
c 5
b 0
f 2
lcom 2
cbo 14
dl 0
loc 396
ccs 152
cts 154
cp 0.987
rs 8.4864

23 Methods

Rating   Name   Duplication   Size   Complexity  
A emptyFilter() 0 4 1
A __construct() 0 9 1
A create() 0 12 1
A isUpdateNone() 0 4 1
A isUpdateAll() 0 4 1
A isUpdatePubKeyOnly() 0 4 1
A isEmpty() 0 4 1
A isFull() 0 4 1
A getData() 0 4 1
A getNumHashFuncs() 0 4 1
A getTweak() 0 4 1
A getFlags() 0 4 1
B idealSize() 0 24 1
A idealNumHashFuncs() 0 21 1
A hash() 0 7 1
A insertData() 0 14 3
A insertOutPoint() 0 4 1
B containsData() 0 20 5
A containsOutPoint() 0 4 1
A hasAcceptableSize() 0 4 2
C isRelevantAndUpdate() 0 56 18
A updateEmptyFull() 0 13 2
A getBuffer() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like BloomFilter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BloomFilter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace BitWasp\Bitcoin\Bloom;
4
5
use BitWasp\Bitcoin\Crypto\Hash;
6
use BitWasp\Bitcoin\Math\Math;
7
use BitWasp\Bitcoin\Serializer\Bloom\BloomFilterSerializer;
8
use BitWasp\Bitcoin\Script\ScriptFactory;
9
use BitWasp\Bitcoin\Serializable;
10
use BitWasp\Bitcoin\Transaction\OutPointInterface;
11
use BitWasp\Bitcoin\Transaction\TransactionInterface;
12
use BitWasp\Buffertools\Buffer;
13
use BitWasp\Buffertools\BufferInterface;
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 int
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 int $flags
69
     */
70 132
    public function __construct(Math $math, array $vFilter, $numHashFuncs, $nTweak, $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 int $flags
98
     * @return BloomFilter
99
     */
100 102
    public static function create(Math $math, $nElements, $nFpRate, $nTweak, $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
     * @return bool
115
     */
116
    public function isUpdateNone()
117
    {
118
        return (($this->flags & self::UPDATE_MASK) == self::UPDATE_NONE);
119
    }
120
121
    /**
122
     * @return bool
123
     */
124
    public function isUpdateAll()
125 6
    {
126
        return (($this->flags & self::UPDATE_MASK) == self::UPDATE_ALL);
127 6
    }
128
129
    /**
130
     * @return bool
131
     */
132
    public function isUpdatePubKeyOnly()
133 36
    {
134
        return (($this->flags & self::UPDATE_MASK) == self::UPDATE_P2PUBKEY_ONLY);
135 36
    }
136
137
    /**
138
     * @return bool
139
     */
140
    public function isEmpty()
141 24
    {
142
        return $this->empty;
143 24
    }
144
145
    /**
146
     * @return bool
147
     */
148
    public function isFull()
149 96
    {
150 3
        return $this->full;
151 96
    }
152
153
    /**
154
     * @return array
155
     */
156
    public function getData()
157 120
    {
158
        return $this->vFilter;
159 120
    }
160
161
    /**
162
     * @return int
163
     */
164
    public function getNumHashFuncs()
165 24
    {
166
        return $this->numHashFuncs;
167 24
    }
168
169
    /**
170
     * @return int|string
171
     */
172
    public function getTweak()
173 24
    {
174
        return $this->nTweak;
175 24
    }
176
177
    /**
178
     * @return int
179
     */
180
    public function getFlags()
181 24
    {
182
        return $this->flags;
183 24
    }
184
185
    /**
186
     * @param int $nElements
187
     * @param float $fpRate
188
     * @return int
189 24
     */
190
    public static function idealSize($nElements, $fpRate)
191 24
    {
192
        return (int) floor(
193
            bcdiv(
194
                min(
195
                    bcmul(
196
                        bcmul(
197
                            bcdiv(
198
                                -1,
199 102
                                self::LN2SQUARED
200
                            ),
201 102
                            $nElements
202 102
                        ),
203 102
                        log($fpRate)
204 102
                    ),
205 102
                    bcmul(
206 102
                        self::MAX_FILTER_SIZE,
207 102
                        8
208
                    )
209 102
                ),
210
                8
211 102
            )
212 102
        );
213 102
    }
214 102
215 102
    /**
216
     * @param int $filterSize
217 102
     * @param int $nElements
218 102
     * @return int
219
     */
220 102
    public static function idealNumHashFuncs($filterSize, $nElements)
221 102
    {
222
        return (int) floor(
223
            min(
224
                bcmul(
225
                    bcdiv(
226
                        bcmul(
227
                            $filterSize,
228
                            8
229 102
                        ),
230
                        $nElements
231 102
                    ),
232 102
                    self::LN2
233 102
                ),
234 102
                bcmul(
235 102
                    self::MAX_FILTER_SIZE,
236 102
                    8
237
                )
238 102
            )
239
        );
240 102
    }
241
242 102
    /**
243 102
     * @param int $nHashNum
244 102
     * @param BufferInterface $data
245
     * @return string
246 102
     */
247 102
    public function hash($nHashNum, BufferInterface $data)
248 102
    {
249
        return $this->math->mod(
250
            Hash::murmur3($data, ($nHashNum * self::TWEAK_START + $this->nTweak) & 0xffffffff)->getInt(),
251
            count($this->vFilter) * 8
252
        );
253
    }
254
255
    /**
256 78
     * @param BufferInterface $data
257
     * @return $this
258 78
     */
259 78
    public function insertData(BufferInterface $data)
260 78
    {
261 78
        if ($this->isFull()) {
262
            return $this;
263
        }
264
265
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
266
            $index = $this->hash($i, $data);
267
            $this->vFilter[$index >> 3] |= (1 << (7 & $index));
268 84
        }
269
270 84
        $this->updateEmptyFull();
271 6
        return $this;
272
    }
273
274 78
    /**
275 78
     * @param OutPointInterface $outPoint
276 78
     * @return BloomFilter
277 78
     */
278
    public function insertOutPoint(OutPointInterface $outPoint)
279 78
    {
280 78
        return $this->insertData($outPoint->getBuffer());
281
    }
282
283
    /**
284
     * @param BufferInterface $data
285
     * @return bool
286
     */
287 24
    public function containsData(BufferInterface $data)
288
    {
289 24
        if ($this->isFull()) {
290
            return true;
291
        }
292
293
        if ($this->isEmpty()) {
294
            return false;
295
        }
296 84
297
        for ($i = 0; $i < $this->numHashFuncs; $i++) {
298 84
            $index = $this->hash($i, $data);
299 6
300
            if (!($this->vFilter[($index >> 3)] & (1 << (7 & $index)))) {
301
                return false;
302 78
            }
303 6
        }
304
305
        return true;
306 72
    }
307 72
308
    /**
309 72
     * @param OutPointInterface $outPoint
310 72
     * @return bool
311
     */
312 72
    public function containsOutPoint(OutPointInterface $outPoint)
313
    {
314 72
        return $this->containsData($outPoint->getBuffer());
315
    }
316
317
    /**
318
     * @return bool
319
     */
320
    public function hasAcceptableSize()
321 30
    {
322
        return count($this->vFilter) <= self::MAX_FILTER_SIZE && $this->numHashFuncs <= self::MAX_HASH_FUNCS;
323 30
    }
324
325
    /**
326
     * @param TransactionInterface $tx
327
     * @return bool
328
     */
329 6
    public function isRelevantAndUpdate(TransactionInterface $tx)
330
    {
331 6
        $this->updateEmptyFull();
332
        $found = false;
333
        if ($this->isFull()) {
334
            return true;
335
        }
336
337
        if ($this->isEmpty()) {
338 72
            return false;
339
        }
340 72
341 72
        // Check if the txid hash is in the filter
342 72
        $txHash = $tx->getTxId();
343 6
        if ($this->containsData($txHash)) {
344
            $found = true;
345
        }
346 66
347 12
        // Check for relevant output scripts. We add the outpoint to the filter if found.
348
        foreach ($tx->getOutputs() as $vout => $output) {
349
            $script = $output->getScript();
350
            $parser = $script->getScriptParser();
351 54
            foreach ($parser as $exec) {
352 54
                if ($exec->isPush() && $this->containsData($exec->getData())) {
353 42
                    $found = true;
354 42
                    if ($this->isUpdateAll()) {
355
                        $this->insertOutPoint($tx->makeOutPoint($vout));
356
                    } else if ($this->isUpdatePubKeyOnly()) {
357 54
                        $type = ScriptFactory::scriptPubKey()->classify($script);
358 54
                        if ($type->isMultisig() || $type->isPayToPublicKey()) {
359 54
                            $this->insertOutPoint($tx->makeOutPoint($vout));
360 54
                        }
361 54
                    }
362 30
                }
363 30
            }
364 12
        }
365 30
366 12
        if ($found) {
367 12
            return true;
368 12
        }
369 12
370 12
        foreach ($tx->getInputs() as $txIn) {
371 30
            if ($this->containsOutPoint($txIn->getOutPoint())) {
372 54
                return true;
373 54
            }
374
375 54
            $parser = $txIn->getScript()->getScriptParser();
376 54
            foreach ($parser as $exec) {
377
                if ($exec->isPush() > 0 && $this->containsData($exec->getData())) {
378
                    return true;
379 30
                }
380 30
            }
381 12
        }
382
383
        return false;
384 30
    }
385 30
386 30
    /**
387 6
     *
388
     */
389 30
    public function updateEmptyFull()
390 30
    {
391
        $full = true;
392 30
        $empty = true;
393
        for ($i = 0, $size = count($this->vFilter); $i < $size; $i++) {
394
            $byte = (int) $this->vFilter[$i];
395
            $full &= ($byte === 0xff);
396
            $empty &= ($byte === 0x0);
397
        }
398 132
399
        $this->full = (bool)$full;
400 132
        $this->empty = (bool)$empty;
401 132
    }
402 132
403 132
    /**
404 132
     * @return BufferInterface
405 132
     */
406 132
    public function getBuffer()
407
    {
408 132
        return (new BloomFilterSerializer())->serialize($this);
409 132
    }
410
}
411