Completed
Pull Request — master (#591)
by thomas
34:55
created

OutputClassifier::decodeP2WSH()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 134
     * @return false|BufferInterface
96
     */
97 134
    private function decodeP2PK(array $decoded)
98 118
    {
99
        if (count($decoded) !== 2 || !$decoded[0]->isPush()) {
100
            return false;
101 55
        }
102 55
103 16
        $size = $decoded[0]->getDataSize();
104 16
        if ($size === 33 || $size === 65) {
105 15
            $op = $decoded[1];
106
            if ($op->getOp() === Opcodes::OP_CHECKSIG) {
107
                return $decoded[0]->getData();
108
            }
109 44
        }
110
111
        return false;
112
    }
113
114
    /**
115
     * @param ScriptInterface $script
116 10
     * @return bool
117
     */
118
    public function isPayToPublicKey(ScriptInterface $script): bool
119 10
    {
120
        try {
121
            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 125
     * @return BufferInterface|false
132
     */
133 125
    private function decodeP2PKH(array $decoded)
134 96
    {
135
        if (count($decoded) !== 5) {
136
            return false;
137 54
        }
138 54
139 54
        $dup = $decoded[0];
140 54
        $hash = $decoded[1];
141 54
        $buf = $decoded[2];
142
        $eq = $decoded[3];
143 54
        $checksig = $decoded[4];
144
145 54
        foreach ([$dup, $hash, $eq, $checksig] as $op) {
146 54
            /** @var Operation $op */
147
            if ($op->isPush()) {
148
                return false;
149
            }
150 34
        }
151 34
152 34
        if ($dup->getOp() === Opcodes::OP_DUP
153 34
            && $hash->getOp() === Opcodes::OP_HASH160
154 34
            && $buf->isPush() && $buf->getDataSize() === 20
155 34
            && $eq->getOp() === Opcodes::OP_EQUALVERIFY
156
            && $checksig->getOp() === Opcodes::OP_CHECKSIG) {
157
            return $decoded[2]->getData();
158 1
        }
159
160
        return false;
161
    }
162
163
    /**
164
     * @param ScriptInterface $script
165 8
     * @return bool
166
     */
167
    public function isPayToPublicKeyHash(ScriptInterface $script): bool
168 8
    {
169
        try {
170
            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 1154
     * @return bool|BufferInterface
181
     */
182 1154
    private function decodeP2SH(array $decoded)
183 878
    {
184
        if (count($decoded) !== 3) {
185
            return false;
186 302
        }
187 302
188 123
        $op_hash = $decoded[0];
189
        if ($op_hash->isPush() || $op_hash->getOp() !== Opcodes::OP_HASH160) {
190
            return false;
191 180
        }
192 180
193 3
        $buffer = $decoded[1];
194
        if (!$buffer->isPush() || $buffer->getOp() !== 20) {
195
            return false;
196 178
        }
197 178
198 178
        $eq = $decoded[2];
199
        if (!$eq->isPush() && $eq->getOp() === Opcodes::OP_EQUAL) {
200
            return $decoded[1]->getData();
201 1
        }
202
203
        return false;
204
    }
205
206
    /**
207
     * @param ScriptInterface $script
208 1104
     * @return bool
209
     */
210
    public function isPayToScriptHash(ScriptInterface $script): bool
211 1104
    {
212 1
        try {
213
            return $this->decodeP2SH($script->getScriptParser()->decode()) !== false;
214
        } catch (\Exception $e) {
215
            /** Return false later */
216 1
        }
217
218
        return false;
219
    }
220
221
    /**
222
     * @param Operation[] $decoded
223 104
     * @return bool|BufferInterface[]
224
     */
225 104
    private function decodeMultisig(array $decoded)
226 104
    {
227 76
        $count = count($decoded);
228
        if ($count <= 3) {
229
            return false;
230 63
        }
231 63
232 63
        $mOp = $decoded[0];
233 63
        $nOp = $decoded[$count - 2];
234 22
        $checksig = $decoded[$count - 1];
235
        if ($mOp->isPush() || $nOp->isPush() || $checksig->isPush()) {
236
            return false;
237
        }
238 42
239 42
        /** @var Operation[] $vKeys */
240 42
        $vKeys = array_slice($decoded, 1, -2);
241 42
        $solutions = [];
242 13
        foreach ($vKeys as $key) {
243
            if (!$key->isPush() || !PublicKey::isCompressedOrUncompressed($key->getData())) {
244 39
                return false;
245
            }
246
            $solutions[] = $key->getData();
247 30
        }
248 30
249 30
        if ($mOp->getOp() >= Opcodes::OP_0
250 30
            && $nOp->getOp() <= Opcodes::OP_16
251
            && $checksig->getOp() === Opcodes::OP_CHECKMULTISIG) {
252
            return $solutions;
253 1
        }
254
255
        return false;
256
    }
257
258
    /**
259
     * @param ScriptInterface $script
260 10
     * @return bool
261
     */
262
    public function isMultisig(ScriptInterface $script): bool
263 10
    {
264
        try {
265
            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 7
     * @return false|BufferInterface
277
     */
278 7
    private function decodeWitnessNoLimit(ScriptInterface $script, array $decoded)
279 7
    {
280 3
        $size = $script->getBuffer()->getSize();
281
        if ($size < 4 || $size > 40) {
282 5
            return false;
283 2
        }
284
        if (count($decoded) !== 2 || !$decoded[1]->isPush()) {
285
            return false;
286 4
        }
287 4
288 2
        $version = $decoded[0]->getOp();
289
        if ($version !== Opcodes::OP_0 && ($version < Opcodes::OP_1 || $version > Opcodes::OP_16)) {
290
            return false;
291 3
        }
292 3
293 3
        $witness = $decoded[1];
294
        if ($size === $witness->getDataSize() + 2) {
295
            return $witness->getData();
296
        }
297
298
        return false;
299
    }
300
301
    /**
302
     * @param Operation[] $decoded
303
     * @return BufferInterface|false
304
     */
305
    private function decodeP2WKH2(array $decoded)
306
    {
307
        if (count($decoded) === 2
308
            && $decoded[0]->getOp() === Opcodes::OP_0
309
            && $decoded[1]->isPush()
310
            && $decoded[1]->getDataSize() === 20) {
311
            return $decoded[1]->getData();
312
        }
313
314
        return false;
315
    }
316
317
    /**
318
     * @param Operation[] $decoded
319
     * @return BufferInterface|false
320
     */
321
    private function decodeP2WSH2(array $decoded)
322
    {
323
        if (count($decoded) === 2
324
            && $decoded[0]->getOp() === Opcodes::OP_0
325
            && $decoded[1]->isPush()
326
            && $decoded[1]->getDataSize() === 32) {
327
            return $decoded[1]->getData();
328
        }
329
330
        return false;
331
    }
332
333
    /**
334
     * @param ScriptInterface $script
335
     * @return bool
336
     */
337
    public function isWitness(ScriptInterface $script): bool
338
    {
339
        try {
340
            return $this->decodeWitnessNoLimit($script, $script->getScriptParser()->decode())!== false;
341
        } catch (\Exception $e) {
342 46
            /** Return false later */
343
        }
344 46
345 46
        return false;
346 46
    }
347 46
348 5
    /**
349
     * @param Operation[] $decoded
350
     * @return false|BufferInterface
351 41
     */
352
    private function decodeNullData(array $decoded)
353
    {
354
        if (count($decoded) !== 2) {
355
            return false;
356
        }
357
358 65
        if ($decoded[0]->getOp() === Opcodes::OP_RETURN && $decoded[1]->isPush()) {
359
            return $decoded[1]->getData();
360 65
        }
361 65
362 65
        return false;
363 65
    }
364 36
365
    /**
366
     * @param ScriptInterface $script
367 46
     * @return bool
368
     */
369
    public function isNullData(ScriptInterface $script): bool
370
    {
371
        try {
372
            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 8
        }
375
376
        return false;
377 8
    }
378 2
379
    /**
380
     * @param array $decoded
381
     * @return bool|BufferInterface
382 2
     */
383
    private function decodeWitnessCoinbaseCommitment(array $decoded)
384
    {
385
        if (count($decoded) !== 2) {
386
            return false;
387
        }
388
389 40
        if ($decoded[0]->isPush() || $decoded[0]->getOp() !== Opcodes::OP_RETURN) {
390
            return false;
391 40
        }
392 39
393
        if ($decoded[1]->isPush()) {
394
            $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 1
400
        return false;
401
    }
402
403
    /**
404
     * @param ScriptInterface $script
405
     * @return bool
406 1
     */
407
    public function isWitnessCoinbaseCommitment(ScriptInterface $script): bool
408
    {
409 1
        try {
410
            return $this->decodeWitnessCoinbaseCommitment($script->getScriptParser()->decode()) !== false;
411
        } 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
        return false;
415
    }
416
417
    /**
418
     * @param array $decoded
419
     * @param null $solution
420 46
     * @return string
421
     */
422 46
    private function classifyDecoded(array $decoded, &$solution = null): string
423 40
    {
424
        $type = ScriptType::NONSTANDARD;
425
426 6
        if (($pubKey = $this->decodeP2PK($decoded))) {
427 4
            $type = ScriptType::P2PK;
428
            $solution = $pubKey;
429
        } else if (($pubKeyHash = $this->decodeP2PKH($decoded))) {
430 2
            $type = ScriptType::P2PKH;
431 2
            $solution = $pubKeyHash;
432 2
        } else if (($multisig = $this->decodeMultisig($decoded))) {
433 1
            $type = ScriptType::MULTISIG;
434
            $solution = $multisig;
435
        } else if (($scriptHash = $this->decodeP2SH($decoded))) {
436
            $type = ScriptType::P2SH;
437 1
            $solution = $scriptHash;
438
        } else if (($witnessScriptHash = $this->decodeP2WSH2($decoded))) {
439
            $type = ScriptType::P2WSH;
440
            $solution = $witnessScriptHash;
441
        } else if (($witnessKeyHash = $this->decodeP2WKH2($decoded))) {
442
            $type = ScriptType::P2WKH;
443
            $solution = $witnessKeyHash;
444 7
        } else if (($witCommitHash = $this->decodeWitnessCoinbaseCommitment($decoded))) {
445
            $type = ScriptType::WITNESS_COINBASE_COMMITMENT;
446
            $solution = $witCommitHash;
447 7
        } else if (($nullData = $this->decodeNullData($decoded))) {
448 1
            $type = ScriptType::NULLDATA;
449
            $solution = $nullData;
450
        }
451 1
452
        return $type;
453
    }
454
455
    /**
456
     * @param ScriptInterface $script
457
     * @param mixed $solution
458
     * @return string
459 131
     */
460
    public function classify(ScriptInterface $script, &$solution = null): string
461 131
    {
462
        $decoded = $script->getScriptParser()->decode();
463 131
464 13
        $type = $this->classifyDecoded($decoded, $solution);
465 13
466 123
        return $type;
467 33
    }
468 33
469 99
    /**
470 28
     * @param ScriptInterface $script
471 28
     * @return OutputData
472 90
     */
473 49
    public function decode(ScriptInterface $script): OutputData
474 49
    {
475 65
        $solution = null;
476 36
        $type = $this->classify($script, $solution);
477 36
        return new OutputData($type, $script, $solution);
478 46
    }
479 5
480 5
    /**
481 41
     * @param ScriptInterface $script
482 1
     * @param bool $allowNonstandard
483 1
     * @return OutputData[]
484 40
     */
485 1
    public function decodeSequence(ScriptInterface $script, bool $allowNonstandard = false): array
486 1
    {
487
        $decoded = $script->getScriptParser()->decode();
488
489 131
        $j = 0;
490
        $l = count($decoded);
491
        $result = [];
492
        while ($j < $l) {
493
            $type = null;
494
            $slice = null;
495
            $solution = null;
496
497 131
            // increment the $last, and break if it's valid
498
            for ($i = 0; $i < ($l - $j) + 1; $i++) {
499 131
                $slice = array_slice($decoded, $j, $i);
500
                $chkType = $this->classifyDecoded($slice, $solution);
501 131
                if ($chkType !== ScriptType::NONSTANDARD) {
502
                    $type = $chkType;
503 131
                    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 121
                }
511
                $j++;
512 121
            } else {
513 121
                $j += $i;
514 121
                /** @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 defined by null on line 494 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...
517
            }
518
        }
519
520
        return $result;
521
    }
522
}
523