Completed
Pull Request — master (#412)
by thomas
246:53 queued 176:30
created

OutputClassifier::classify()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 34
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 29
nc 9
nop 2
dl 0
loc 34
ccs 0
cts 0
cp 0
crap 90
rs 4.909
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\Script\Classifier;
4
5
use BitWasp\Bitcoin\Script\Parser\Operation;
6
use BitWasp\Bitcoin\Crypto\EcAdapter\Impl\PhpEcc\Key\PublicKey;
7
use BitWasp\Bitcoin\Script\Opcodes;
8
use BitWasp\Bitcoin\Script\ScriptInterface;
9
use BitWasp\Buffertools\Buffer;
10
use BitWasp\Buffertools\BufferInterface;
11
12
class OutputClassifier
13
{
14
    const PAYTOPUBKEY = 'pubkey';
15
    const PAYTOPUBKEYHASH = 'pubkeyhash';
16
    const PAYTOSCRIPTHASH = 'scripthash';
17
    const WITNESS_V0_KEYHASH = 'witness_v0_keyhash';
18
    const WITNESS_V0_SCRIPTHASH = 'witness_v0_scripthash';
19
    const MULTISIG = 'multisig';
20
    const NULLDATA = 'nulldata';
21
    const UNKNOWN = 'nonstandard';
22
    const NONSTANDARD = 'nonstandard';
23
24
    const P2PK = 'pubkey';
25
    const P2PKH = 'pubkeyhash';
26 372
    const P2SH = 'scripthash';
27
    const P2WSH = 'witness_v0_scripthash';
28 372
    const P2WKH = 'witness_v0_keyhash';
29 306
30
    const WITNESS_COINBASE_COMMITMENT = 'witness_coinbase_commitment';
31
32 174
    /**
33 174
     * @param Operation[] $decoded
34 84
     * @return false|BufferInterface
35 84
     */
36 78
    private function decodeP2PK(array $decoded)
37
    {
38 6
        if (count($decoded) !== 2 || !$decoded[0]->isPush()) {
39
            return false;
40 120
        }
41
42
        $size = $decoded[0]->getDataSize();
43
        if ($size === 33 || $size === 65) {
44
            $op = $decoded[1];
45
            if ($op->getOp() === Opcodes::OP_CHECKSIG) {
46
                return $decoded[0]->getData();
47 51
            }
48
        }
49
50 51
        return false;
51
    }
52
53
    /**
54
     * @param ScriptInterface $script
55
     * @return bool
56
     */
57
    public function isPayToPublicKey(ScriptInterface $script)
58
    {
59
        try {
60
            return $this->decodeP2PK($script->getScriptParser()->decode()) !== false;
61
        } catch (\Exception $e) {
62 330
            /** Return false later */
63
        }
64 330
65 228
        return false;
66
    }
67
68 180
    /**
69 180
     * @param Operation[] $decoded
70 180
     * @return BufferInterface|false
71 180
     */
72 180
    private function decodeP2PKH(array $decoded)
73
    {
74 180
        if (count($decoded) !== 5) {
75
            return false;
76 180
        }
77 99
78
        $dup = $decoded[0];
79 90
        $hash = $decoded[1];
80
        $buf = $decoded[2];
81 168
        $eq = $decoded[3];
82 168
        $checksig = $decoded[4];
83 168
84 168
        foreach ([$dup, $hash, $eq, $checksig] as $op) {
85 168
            /** @var Operation $op */
86 168
            if ($op->isPush()) {
87
                return false;
88
            }
89 6
        }
90
91
        if ($dup->getOp() === Opcodes::OP_DUP
92
            && $hash->getOp() === Opcodes::OP_HASH160
93
            && $buf->isPush() && $buf->getDataSize() === 20
94
            && $eq->getOp() === Opcodes::OP_EQUALVERIFY
95
            && $checksig->getOp() === Opcodes::OP_CHECKSIG) {
96 36
            return $decoded[2]->getData();
97
        }
98
99 36
        return false;
100
    }
101
102
    /**
103
     * @param ScriptInterface $script
104
     * @return bool
105
     */
106
    public function isPayToPublicKeyHash(ScriptInterface $script)
107
    {
108
        try {
109
            return $this->decodeP2PKH($script->getScriptParser()->decode()) !== false;
110
        } catch (\Exception $e) {
111 1232
            /** Return false later */
112
        }
113 1232
114 984
        return false;
115
    }
116
117 314
    /**
118 314
     * @param array $decoded
119 118
     * @return bool
120
     */
121
    private function decodeP2SH(array $decoded)
122 202
    {
123 202
        if (count($decoded) !== 3) {
124 8
            return false;
125
        }
126
127 200
        $op_hash = $decoded[0];
128 200
        if ($op_hash->isPush() || $op_hash->getOp() !== Opcodes::OP_HASH160) {
129 200
            return false;
130
        }
131
132 6
        $buffer = $decoded[1];
133
        if (!$buffer->isPush() || $buffer->getOp() !== 20) {
134
            return false;
135
        }
136
137
        $eq = $decoded[2];
138
        if (!$eq->isPush() && $eq->getOp() === Opcodes::OP_EQUAL) {
139 1080
            return $decoded[1]->getData();
140
        }
141
142 1080
        return false;
143
    }
144
145
    /**
146
     * @param ScriptInterface $script
147
     * @return bool
148
     */
149
    public function isPayToScriptHash(ScriptInterface $script)
150
    {
151
        try {
152
            return $this->decodeP2SH($script->getScriptParser()->decode()) !== false;
153
        } catch (\Exception $e) {
154 264
            /** Return false later */
155
        }
156 264
157 264
        return false;
158 222
    }
159
160
    /**
161 78
     * @param Operation[] $decoded
162 78
     * @return bool|BufferInterface[]
163 78
     */
164 78
    private function decodeMultisig(array $decoded)
165 6
    {
166
        $count = count($decoded);
167
        if ($count <= 3) {
168
            return false;
169 78
        }
170 78
171 78
        $mOp = $decoded[0];
172 78
        $nOp = $decoded[$count - 2];
173 12
        $checksig = $decoded[$count - 1];
174
        if ($mOp->isPush() || $nOp->isPush() || $checksig->isPush()) {
175 72
            return false;
176 36
        }
177
178 72
        /** @var Operation[] $vKeys */
179 72
        $vKeys = array_slice($decoded, 1, -2);
180 72
        $solutions = [];
181 72
        foreach ($vKeys as $key) {
182
            if (!$key->isPush() || !PublicKey::isCompressedOrUncompressed($key->getData())) {
183
                return false;
184 6
            }
185
            $solutions[] = $key->getData();
186
        }
187
188
        if ($mOp->getOp() >= Opcodes::OP_0
189
            && $nOp->getOp() <= Opcodes::OP_16
190
            && $checksig->getOp() === Opcodes::OP_CHECKMULTISIG) {
191 719
            return $solutions;
192
        }
193
194 719
        return false;
195
    }
196
197
    /**
198
     * @param ScriptInterface $script
199
     * @return bool
200
     */
201
    public function isMultisig(ScriptInterface $script)
202
    {
203
        try {
204
            return $this->decodeMultisig($script->getScriptParser()->decode()) !== false;
205
        } catch (\Exception $e) {
206
            /** Return false later */
207 168
        }
208
209 168
        return false;
210 168
    }
211 60
212
    /**
213 120
     * @param ScriptInterface $script
214 18
     * @param Operation[] $decoded
215
     * @return false|BufferInterface
216
     */
217 108
    private function decodeWitnessNoLimit(ScriptInterface $script, array $decoded)
218 108
    {
219 6
        $size = $script->getBuffer()->getSize();
220
        if ($size < 4 || $size > 40) {
221
            return false;
222 108
        }
223 108
        if (count($decoded) !== 2 || !$decoded[1]->isPush()) {
224 108
            return false;
225
        }
226
227
        $version = $decoded[0]->getOp();
228
        if ($version !== Opcodes::OP_0 && ($version < Opcodes::OP_1 || $version > Opcodes::OP_16)) {
229
            return false;
230
        }
231
232
        $witness = $decoded[1];
233
        if ($size === $witness->getDataSize() + 2) {
234
            return $witness->getData();
235
        }
236 138
237
        return false;
238 138
    }
239 102
240 30
    /**
241
     * @param ScriptInterface $script
242
     * @param int $limit
243 102
     * @param array $decoded
244
     * @return BufferInterface|false
245
     */
246 42
    private function decodeWithLimit(ScriptInterface $script, $limit, array $decoded)
247
    {
248
        if (($data = $this->decodeWitnessNoLimit($script, $decoded))) {
249
            if ($data->getSize() !== $limit) {
250
                return false;
251
            }
252
253
            return $data;
254 72
        }
255
256 72
        return false;
257
    }
258
259
    /**
260
     * @param ScriptInterface $script
261
     * @param Operation[] $decoded
262
     * @return BufferInterface|false
263
     */
264 138
    private function decodeP2WKH(ScriptInterface $script, array $decoded)
265
    {
266 138
        return $this->decodeWithLimit($script, 20, $decoded);
267
    }
268
269
    /**
270
     * @param ScriptInterface $script
271
     * @param Operation[] $decoded
272
     * @return BufferInterface|false
273 30
     */
274
    private function decodeP2WSH(ScriptInterface $script, array $decoded)
275
    {
276 30
        return $this->decodeWithLimit($script, 32, $decoded);
277 6
    }
278
279
    /**
280
     * @param ScriptInterface $script
281 6
     * @return bool
282
     */
283
    public function isWitness(ScriptInterface $script)
284
    {
285
        try {
286
            return $this->decodeWitnessNoLimit($script, $script->getScriptParser()->decode())!== false;
287
        } catch (\Exception $e) {
288 42
            /** Return false later */
289
        }
290 42
291 36
        return false;
292
    }
293
294 12
    /**
295 6
     * @param Operation[] $decoded
296
     * @return false|BufferInterface
297
     */
298 6
    private function decodeNullData(array $decoded)
299
    {
300
        if (count($decoded) !== 2) {
301
            return false;
302
        }
303
304
        if ($decoded[0]->getOp() === Opcodes::OP_RETURN && $decoded[1]->isPush()) {
305 6
            return $decoded[1]->getData();
306
        }
307
308 6
        return false;
309
    }
310
311
    /**
312
     * @param ScriptInterface $script
313
     * @return bool
314
     */
315
    public function isNullData(ScriptInterface $script)
316
    {
317
        try {
318
            return $this->decodeNullData($script->getScriptParser()->decode()) !== false;
319
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
320 354
        }
321
322 354
        return false;
323 354
    }
324 354
325
    /**
326 354
     * @param array $decoded
327 66
     * @return bool|BufferInterface
328 66
     */
329 336
    private function decodeWitnessCoinbaseCommitment(array $decoded)
330 162
    {
331 162
        if (count($decoded) !== 2) {
332 273
            return false;
333 60
        }
334 60
335 213
        if ($decoded[0]->isPush() || $decoded[0]->getOp() !== Opcodes::OP_RETURN) {
336 114
            return false;
337 114
        }
338 168
339 72
        if ($decoded[1]->isPush()) {
340 72
            $data = $decoded[1]->getData()->getBinary();
341 105
            if ($decoded[1]->getDataSize() === 0x24 && substr($data, 0, 4) === "\xaa\x21\xa9\xed") {
342 30
                return new Buffer(substr($data, 4));
343 30
            }
344 57
        }
345 6
346 6
        return false;
347 3
    }
348
349 354
    /**
350
     * @param ScriptInterface $script
351
     * @return bool
352
     */
353
    public function isWitnessCoinbaseCommitment(ScriptInterface $script)
354
    {
355
        try {
356 258
            return $this->decodeWitnessCoinbaseCommitment($script->getScriptParser()->decode()) !== false;
357
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
358 258
        }
359 258
360 258
        return false;
361
    }
362
363
    /**
364
     * @param ScriptInterface $script
365
     * @param mixed $solution
366
     * @return string
367
     */
368
    public function classify(ScriptInterface $script, &$solution = null)
369
    {
370
        $decoded = $script->getScriptParser()->decode();
371
        $type = self::NONSTANDARD;
372
        $solution = null;
373
374
        if (($pubKey = $this->decodeP2PK($decoded))) {
375
            $type = self::PAYTOPUBKEY;
376
            $solution = $pubKey;
377
        } else if (($pubKeyHash = $this->decodeP2PKH($decoded))) {
378
            $type = self::PAYTOPUBKEYHASH;
379
            $solution = $pubKeyHash;
380
        } else if (($multisig = $this->decodeMultisig($decoded))) {
381
            $type = self::MULTISIG;
382
            $solution = $multisig;
383
        } else if (($scriptHash = $this->decodeP2SH($decoded))) {
384
            $type = self::PAYTOSCRIPTHASH;
385
            $solution = $scriptHash;
386
        } else if (($witnessScriptHash = $this->decodeP2WSH($script, $decoded))) {
387
            $type = self::WITNESS_V0_SCRIPTHASH;
388
            $solution = $witnessScriptHash;
389
        } else if (($witnessKeyHash = $this->decodeP2WKH($script, $decoded))) {
390
            $type = self::WITNESS_V0_KEYHASH;
391
            $solution = $witnessKeyHash;
392
        } else if (($witCommitHash = $this->decodeWitnessCoinbaseCommitment($decoded))) {
393
            $type = self::WITNESS_COINBASE_COMMITMENT;
394
            $solution = $witCommitHash;
395
        } else if (($nullData = $this->decodeNullData($decoded))) {
396
            $type = self::NULLDATA;
397
            $solution = $nullData;
398
        }
399
400
        return $type;
401
    }
402
403
    /**
404
     * @param ScriptInterface $script
405
     * @return OutputData
406
     */
407
    public function decode(ScriptInterface $script)
408
    {
409
        $solution = null;
410
        $type = $this->classify($script, $solution);
411
        return new OutputData($type, $script, $solution);
412
    }
413
}
414