Completed
Push — master ( f684d6...af1b10 )
by thomas
25:38
created

BloomFilter::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

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