OutputClassifier   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 505
Duplicated Lines 0 %

Test Coverage

Coverage 83.51%

Importance

Changes 0
Metric Value
eloc 192
dl 0
loc 505
ccs 157
cts 188
cp 0.8351
rs 2
c 0
b 0
f 0
wmc 96

20 Methods

Rating   Name   Duplication   Size   Complexity  
A isWitnessCoinbaseCommitment() 0 8 2
A isWitness() 0 9 2
B decodeSequence() 0 36 6
B decodeP2SH() 0 22 8
A isNullData() 0 8 2
B decodeP2PKH() 0 28 10
A decodeP2WKH2() 0 10 5
B decodeMultisig() 0 31 11
A isPayToPublicKey() 0 9 2
A isPayToPublicKeyHash() 0 9 2
B decodeWitnessCoinbaseCommitment() 0 18 7
A decodeP2WSH2() 0 10 5
B decodeWitnessNoLimit() 0 21 9
A decode() 0 5 1
A decodeP2PK() 0 15 6
A classify() 0 7 1
A isMultisig() 0 9 2
B classifyDecoded() 0 31 9
A decodeNullData() 0 11 4
A isPayToScriptHash() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like OutputClassifier 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.

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 OutputClassifier, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Script\Classifier;
6
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey;
8
use BitWasp\Bitcoin\Script\Opcodes;
9
use BitWasp\Bitcoin\Script\Parser\Operation;
10
use BitWasp\Bitcoin\Script\ScriptFactory;
11
use BitWasp\Bitcoin\Script\ScriptInterface;
12
use BitWasp\Bitcoin\Script\ScriptType;
13
use BitWasp\Buffertools\Buffer;
14
use BitWasp\Buffertools\BufferInterface;
15
16
class OutputClassifier
17
{
18
    /**
19
     * @deprecated
20
     */
21
    const PAYTOPUBKEY = 'pubkey';
22
23
    /**
24
     * @deprecated
25
     */
26
    const PAYTOPUBKEYHASH = 'pubkeyhash';
27
28
    /**
29
     * @deprecated
30
     */
31
    const PAYTOSCRIPTHASH = 'scripthash';
32
33
    /**
34
     * @deprecated
35
     */
36
    const WITNESS_V0_KEYHASH = 'witness_v0_keyhash';
37
38
    /**
39
     * @deprecated
40
     */
41
    const WITNESS_V0_SCRIPTHASH = 'witness_v0_scripthash';
42
43
    /**
44
     * @deprecated
45
     */
46
    const MULTISIG = 'multisig';
47
48
    /**
49
     * @deprecated
50
     */
51
    const NULLDATA = 'nulldata';
52
53
    /**
54
     * @deprecated
55
     */
56
    const UNKNOWN = 'nonstandard';
57
58
    /**
59
     * @deprecated
60
     */
61
    const NONSTANDARD = 'nonstandard';
62
63
    /**
64
     * @deprecated
65
     */
66
    const P2PK = 'pubkey';
67
68
    /**
69
     * @deprecated
70
     */
71
    const P2PKH = 'pubkeyhash';
72
73
    /**
74
     * @deprecated
75
     */
76
    const P2SH = 'scripthash';
77
78
    /**
79
     * @deprecated
80
     */
81
    const P2WSH = 'witness_v0_scripthash';
82
83
    /**
84
     * @deprecated
85
     */
86
    const P2WKH = 'witness_v0_keyhash';
87
88
    /**
89
     * @deprecated
90
     */
91
    const WITNESS_COINBASE_COMMITMENT = 'witness_coinbase_commitment';
92
93
    /**
94
     * @param Operation[] $decoded
95
     * @return false|BufferInterface
96
     */
97 182
    private function decodeP2PK(array $decoded)
98
    {
99 182
        if (count($decoded) !== 2 || !$decoded[0]->isPush()) {
100 164
            return false;
101
        }
102
103 71
        $size = $decoded[0]->getDataSize();
104 71
        if ($size === 33 || $size === 65) {
105 20
            $op = $decoded[1];
106 20
            if ($op->getOp() === Opcodes::OP_CHECKSIG) {
107 19
                return $decoded[0]->getData();
108
            }
109
        }
110
111 59
        return false;
112
    }
113
114
    /**
115
     * @param ScriptInterface $script
116
     * @return bool
117
     */
118 10
    public function isPayToPublicKey(ScriptInterface $script): bool
119
    {
120
        try {
121 10
            return $this->decodeP2PK($script->getScriptParser()->decode()) !== false;
122
        } catch (\Exception $e) {
123
            /** Return false later */
124
        }
125
126
        return false;
127
    }
128
129
    /**
130
     * @param Operation[] $decoded
131
     * @return BufferInterface|false
132
     */
133 172
    private function decodeP2PKH(array $decoded)
134
    {
135 172
        if (count($decoded) !== 5) {
136 120
            return false;
137
        }
138
139 87
        $dup = $decoded[0];
140 87
        $hash = $decoded[1];
141 87
        $buf = $decoded[2];
142 87
        $eq = $decoded[3];
143 87
        $checksig = $decoded[4];
144
145 87
        foreach ([$dup, $hash, $eq, $checksig] as $op) {
146
            /** @var Operation $op */
147 87
            if ($op->isPush()) {
148 21
                return false;
149
            }
150
        }
151
152 67
        if ($dup->getOp() === Opcodes::OP_DUP
153 67
            && $hash->getOp() === Opcodes::OP_HASH160
154 67
            && $buf->isPush() && $buf->getDataSize() === 20
155 67
            && $eq->getOp() === Opcodes::OP_EQUALVERIFY
156 67
            && $checksig->getOp() === Opcodes::OP_CHECKSIG) {
157 67
            return $decoded[2]->getData();
158
        }
159
160 1
        return false;
161
    }
162
163
    /**
164
     * @param ScriptInterface $script
165
     * @return bool
166
     */
167 8
    public function isPayToPublicKeyHash(ScriptInterface $script): bool
168
    {
169
        try {
170 8
            return $this->decodeP2PKH($script->getScriptParser()->decode()) !== false;
171
        } catch (\Exception $e) {
172
            /** Return false later */
173
        }
174
175
        return false;
176
    }
177
178
    /**
179
     * @param array $decoded
180
     * @return bool|BufferInterface
181
     */
182 2206
    private function decodeP2SH(array $decoded)
183
    {
184 2206
        if (count($decoded) !== 3) {
185 1676
            return false;
186
        }
187
188 564
        $op_hash = $decoded[0];
189 564
        if ($op_hash->isPush() || $op_hash->getOp() !== Opcodes::OP_HASH160) {
190 245
            return false;
191
        }
192
193 320
        $buffer = $decoded[1];
194 320
        if (!$buffer->isPush() || $buffer->getOp() !== 20) {
195 5
            return false;
196
        }
197
198 316
        $eq = $decoded[2];
199 316
        if (!$eq->isPush() && $eq->getOp() === Opcodes::OP_EQUAL) {
200 316
            return $decoded[1]->getData();
201
        }
202
203 1
        return false;
204
    }
205
206
    /**
207
     * @param ScriptInterface $script
208
     * @return bool
209
     */
210 2098
    public function isPayToScriptHash(ScriptInterface $script): bool
211
    {
212
        try {
213 2098
            return $this->decodeP2SH($script->getScriptParser()->decode()) !== false;
214 1
        } catch (\Exception $e) {
215
            /** Return false later */
216
        }
217
218 1
        return false;
219
    }
220
221
    /**
222
     * @param Operation[] $decoded
223
     * @return bool|BufferInterface[]
224
     */
225 128
    private function decodeMultisig(array $decoded)
226
    {
227 128
        $count = count($decoded);
228 128
        if ($count <= 3) {
229 98
            return false;
230
        }
231
232 69
        $mOp = $decoded[0];
233 69
        $nOp = $decoded[$count - 2];
234 69
        $checksig = $decoded[$count - 1];
235 69
        if ($mOp->isPush() || $nOp->isPush() || $checksig->isPush()) {
236 22
            return false;
237
        }
238
239
        /** @var Operation[] $vKeys */
240 48
        $vKeys = array_slice($decoded, 1, -2);
241 48
        $solutions = [];
242 48
        foreach ($vKeys as $key) {
243 48
            if (!$key->isPush() || !PublicKey::isCompressedOrUncompressed($key->getData())) {
244 13
                return false;
245
            }
246 45
            $solutions[] = $key->getData();
247
        }
248
249 36
        if ($mOp->getOp() >= Opcodes::OP_0
250 36
            && $nOp->getOp() <= Opcodes::OP_16
251 36
            && $checksig->getOp() === Opcodes::OP_CHECKMULTISIG) {
252 36
            return $solutions;
253
        }
254
255 1
        return false;
256
    }
257
258
    /**
259
     * @param ScriptInterface $script
260
     * @return bool
261
     */
262 10
    public function isMultisig(ScriptInterface $script): bool
263
    {
264
        try {
265 10
            return $this->decodeMultisig($script->getScriptParser()->decode()) !== false;
266
        } catch (\Exception $e) {
267
            /** Return false later */
268
        }
269
270
        return false;
271
    }
272
273
    /**
274
     * @param ScriptInterface $script
275
     * @param Operation[] $decoded
276
     * @return false|BufferInterface
277
     */
278 7
    private function decodeWitnessNoLimit(ScriptInterface $script, array $decoded)
279
    {
280 7
        $size = $script->getBuffer()->getSize();
281 7
        if ($size < 4 || $size > 40) {
282 3
            return false;
283
        }
284 5
        if (count($decoded) !== 2 || !$decoded[1]->isPush()) {
285 2
            return false;
286
        }
287
288 4
        $version = $decoded[0]->getOp();
289 4
        if ($version !== Opcodes::OP_0 && ($version < Opcodes::OP_1 || $version > Opcodes::OP_16)) {
290 2
            return false;
291
        }
292
293 3
        $witness = $decoded[1];
294 3
        if ($size === $witness->getDataSize() + 2) {
295 3
            return $witness->getData();
296
        }
297
298
        return false;
299
    }
300
301
    /**
302
     * @param Operation[] $decoded
303
     * @return BufferInterface|false
304
     */
305 53
    private function decodeP2WKH2(array $decoded)
306
    {
307 53
        if (count($decoded) === 2
308 53
            && $decoded[0]->getOp() === Opcodes::OP_0
309 53
            && $decoded[1]->isPush()
310 53
            && $decoded[1]->getDataSize() === 20) {
311 13
            return $decoded[1]->getData();
312
        }
313
314 40
        return false;
315
    }
316
317
    /**
318
     * @param Operation[] $decoded
319
     * @return BufferInterface|false
320
     */
321 79
    private function decodeP2WSH2(array $decoded)
322
    {
323 79
        if (count($decoded) === 2
324 79
            && $decoded[0]->getOp() === Opcodes::OP_0
325 79
            && $decoded[1]->isPush()
326 79
            && $decoded[1]->getDataSize() === 32) {
327 43
            return $decoded[1]->getData();
328
        }
329
330 53
        return false;
331
    }
332
333
    /**
334
     * @param ScriptInterface $script
335
     * @return bool
336
     */
337 8
    public function isWitness(ScriptInterface $script): bool
338
    {
339
        try {
340 8
            return $this->decodeWitnessNoLimit($script, $script->getScriptParser()->decode())!== false;
341 2
        } catch (\Exception $e) {
342
            /** Return false later */
343
        }
344
345 2
        return false;
346
    }
347
348
    /**
349
     * @param Operation[] $decoded
350
     * @return false|BufferInterface
351
     */
352 39
    private function decodeNullData(array $decoded)
353
    {
354 39
        if (count($decoded) !== 2) {
355 38
            return false;
356
        }
357
358 2
        if ($decoded[0]->getOp() === Opcodes::OP_RETURN && $decoded[1]->isPush()) {
359 1
            return $decoded[1]->getData();
360
        }
361
362 1
        return false;
363
    }
364
365
    /**
366
     * @param ScriptInterface $script
367
     * @return bool
368
     */
369 1
    public function isNullData(ScriptInterface $script): bool
370
    {
371
        try {
372 1
            return $this->decodeNullData($script->getScriptParser()->decode()) !== false;
373
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
374
        }
375
376
        return false;
377
    }
378
379
    /**
380
     * @param array $decoded
381
     * @return bool|BufferInterface
382
     */
383 45
    private function decodeWitnessCoinbaseCommitment(array $decoded)
384
    {
385 45
        if (count($decoded) !== 2) {
386 39
            return false;
387
        }
388
389 6
        if ($decoded[0]->isPush() || $decoded[0]->getOp() !== Opcodes::OP_RETURN) {
390 4
            return false;
391
        }
392
393 2
        if ($decoded[1]->isPush()) {
394 2
            $data = $decoded[1]->getData()->getBinary();
395 2
            if ($decoded[1]->getDataSize() === 0x24 && substr($data, 0, 4) === "\xaa\x21\xa9\xed") {
396 1
                return new Buffer(substr($data, 4));
397
            }
398
        }
399
400 1
        return false;
401
    }
402
403
    /**
404
     * @param ScriptInterface $script
405
     * @return bool
406
     */
407 7
    public function isWitnessCoinbaseCommitment(ScriptInterface $script): bool
408
    {
409
        try {
410 7
            return $this->decodeWitnessCoinbaseCommitment($script->getScriptParser()->decode()) !== false;
411 1
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
412
        }
413
414 1
        return false;
415
    }
416
417
    /**
418
     * @param array $decoded
419
     * @param null $solution
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $solution is correct as it would always require null to be passed?
Loading history...
420
     * @return string
421
     */
422 179
    private function classifyDecoded(array $decoded, &$solution = null): string
423
    {
424 179
        $type = ScriptType::NONSTANDARD;
425
426 179
        if (($pubKey = $this->decodeP2PK($decoded))) {
427 17
            $type = ScriptType::P2PK;
428 17
            $solution = $pubKey;
429 170
        } else if (($pubKeyHash = $this->decodeP2PKH($decoded))) {
430 66
            $type = ScriptType::P2PKH;
431 66
            $solution = $pubKeyHash;
432 123
        } else if (($multisig = $this->decodeMultisig($decoded))) {
433 34
            $type = ScriptType::MULTISIG;
434 34
            $solution = $multisig;
435 112
        } else if (($scriptHash = $this->decodeP2SH($decoded))) {
436 65
            $type = ScriptType::P2SH;
437 65
            $solution = $scriptHash;
438 79
        } else if (($witnessScriptHash = $this->decodeP2WSH2($decoded))) {
439 43
            $type = ScriptType::P2WSH;
440 43
            $solution = $witnessScriptHash;
441 53
        } else if (($witnessKeyHash = $this->decodeP2WKH2($decoded))) {
442 13
            $type = ScriptType::P2WKH;
443 13
            $solution = $witnessKeyHash;
444 40
        } else if (($witCommitHash = $this->decodeWitnessCoinbaseCommitment($decoded))) {
445 1
            $type = ScriptType::WITNESS_COINBASE_COMMITMENT;
446 1
            $solution = $witCommitHash;
447 39
        } else if (($nullData = $this->decodeNullData($decoded))) {
448 1
            $type = ScriptType::NULLDATA;
449 1
            $solution = $nullData;
450
        }
451
452 179
        return $type;
453
    }
454
455
    /**
456
     * @param ScriptInterface $script
457
     * @param mixed $solution
458
     * @return string
459
     */
460 179
    public function classify(ScriptInterface $script, &$solution = null): string
461
    {
462 179
        $decoded = $script->getScriptParser()->decode();
463
464 179
        $type = $this->classifyDecoded($decoded, $solution);
465
466 179
        return $type;
467
    }
468
469
    /**
470
     * @param ScriptInterface $script
471
     * @return OutputData
472
     */
473 170
    public function decode(ScriptInterface $script): OutputData
474
    {
475 170
        $solution = null;
476 170
        $type = $this->classify($script, $solution);
477 170
        return new OutputData($type, $script, $solution);
478
    }
479
480
    /**
481
     * @param ScriptInterface $script
482
     * @param bool $allowNonstandard
483
     * @return OutputData[]
484
     */
485
    public function decodeSequence(ScriptInterface $script, bool $allowNonstandard = false): array
486
    {
487
        $decoded = $script->getScriptParser()->decode();
488
489
        $j = 0;
490
        $l = count($decoded);
491
        $result = [];
492
        while ($j < $l) {
493
            $type = null;
494
            $slice = null;
495
            $solution = null;
496
497
            // increment the $last, and break if it's valid
498
            for ($i = 0; $i < ($l - $j) + 1; $i++) {
499
                $slice = array_slice($decoded, $j, $i);
500
                $chkType = $this->classifyDecoded($slice, $solution);
501
                if ($chkType !== ScriptType::NONSTANDARD) {
502
                    $type = $chkType;
503
                    break;
504
                }
505
            }
506
507
            if (null === $type) {
508
                if (!$allowNonstandard) {
509
                    throw new \RuntimeException("Unable to classify script as a sequence of templated types");
510
                }
511
                $j++;
512
            } else {
513
                $j += $i;
514
                /** @var Operation[] $slice */
515
                /** @var mixed $solution */
516
                $result[] = new OutputData($type, ScriptFactory::fromOperations($slice), $solution);
0 ignored issues
show
Bug introduced by
It seems like $slice can also be of type null; however, parameter $operations of BitWasp\Bitcoin\Script\S...ctory::fromOperations() does only seem to accept array, 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

516
                $result[] = new OutputData($type, ScriptFactory::fromOperations(/** @scrutinizer ignore-type */ $slice), $solution);
Loading history...
517
            }
518
        }
519
520
        return $result;
521
    }
522
}
523