Completed
Pull Request — master (#524)
by thomas
71:45
created

OutputClassifier::classify()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
553
            }
554
        }
555
556
        return $result;
557
    }
558
}
559