Completed
Pull Request — master (#383)
by thomas
71:09
created

OutputClassifier::decodeWithLimit()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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