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
![]() |
|||||
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
|
|||||
412 | } |
||||
413 | |||||
414 | 1 | return false; |
|||
415 | } |
||||
416 | |||||
417 | /** |
||||
418 | * @param array $decoded |
||||
419 | * @param null $solution |
||||
0 ignored issues
–
show
|
|||||
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
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
![]() |
|||||
517 | } |
||||
518 | } |
||||
519 | |||||
520 | return $result; |
||||
521 | } |
||||
522 | } |
||||
523 |