Completed
Pull Request — master (#286)
by thomas
24:23
created

OutputClassifier::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
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
    const NONSTANDARD = 'nonstandard';
21
22
    /**
23
     * @param ScriptInterface $script
24
     * @param BufferInterface $publicKey
25
     * @return bool
26
     */
27 204
    public function isPayToPublicKey(ScriptInterface $script, & $publicKey = null)
28
    {
29
        try {
30
31 204
            $decoded = $script->getScriptParser()->decode();
32 204
            if (count($decoded) !== 2 || $decoded[0]->isPush() === false || $decoded[1]->isPush() === true) {
33 168
                return false;
34
            }
35
36 60
            $size = $decoded[0]->getDataSize();
37 60
            if ($size === 33 || $size === 65) {
38 54
                $op = $decoded[1];
39 54
                if ($op->getOp() === Opcodes::OP_CHECKSIG) {
40 48
                    $publicKey = $decoded[0]->getData();
41 48
                    return true;
42
                }
43 12
            }
44
45 18
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
46
47
        }
48
49 18
        return false;
50 6
    }
51
52
    /**
53
     * @param ScriptInterface $script
54
     * @param null $pubKeyHash
55
     * @return bool
56
     */
57 168
    public function isPayToPublicKeyHash(ScriptInterface $script, & $pubKeyHash = null)
58
    {
59
        try {
60
61 168
            $decoded = $script->getScriptParser()->decode();
62 168
            if (count($decoded) !== 5) {
63 84
                return false;
64
            }
65
66 96
            $dup = $decoded[0];
67 96
            $hash = $decoded[1];
68 96
            $buf = $decoded[2];
69 96
            $eq = $decoded[3];
70 96
            $checksig = $decoded[4];
71
72 96
            foreach ([$dup, $hash, $eq, $checksig] as $op) {
73
                /** @var Operation $op */
74 96
                if ($op->isPush()) {
75 18
                    return false;
76
                }
77 96
            }
78
79 84
            if ($dup->getOp() === Opcodes::OP_DUP
80 84
                && $hash->getOp() === Opcodes::OP_HASH160
81 84
                && $buf->isPush() && $buf->getDataSize() === 20
82 84
                && $eq->getOp() === Opcodes::OP_EQUALVERIFY
83 84
                && $checksig->getOp() === Opcodes::OP_CHECKSIG) {
84 84
                $pubKeyHash = $decoded[2]->getData();
85 84
                return true;
86
            }
87
88 6
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
89
90
        }
91
92 6
        return false;
93
    }
94
95
    /**
96
     * @param ScriptInterface $script
97
     * @param null $scriptHash
98
     * @return bool
99
     */
100 228
    public function isPayToScriptHash(ScriptInterface $script, & $scriptHash = null)
101
    {
102
        try {
103
104 228
            $decoded = $script->getScriptParser()->decode();
105 228
            if (count($decoded) !== 3) {
106 192
                return false;
107
            }
108
109 84
            $op_hash = $decoded[0];
110 84
            if ($op_hash->isPush() || $op_hash->getOp() !== Opcodes::OP_HASH160) {
111 6
                return false;
112
            }
113
114 84
            $buffer = $decoded[1];
115 84
            if (!$buffer->isPush() || $buffer->getDataSize() !== 20) {
116 6
                return false;
117
            }
118
119 84
            $eq = $decoded[2];
120 84
            if (!$eq->isPush() && $eq->getOp() === Opcodes::OP_EQUAL) {
121 84
                $scriptHash = $decoded[1]->getData();
122 84
                return true;
123
            }
124
125 6
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
126
127
        }
128
129 6
        return false;
130
    }
131
132
    /**
133
     * @param ScriptInterface $script
134
     * @param array $keys
135
     * @return bool
136
     */
137 126
    public function isMultisig(ScriptInterface $script, & $keys = [])
138
    {
139
        try {
140
141 126
            $decoded = $script->getScriptParser()->decode();
142 126
            $count = count($decoded);
143 126
            if ($count <= 3) {
144 60
                return false;
145
            }
146
147 72
            $mOp = $decoded[0];
148 72
            $nOp = $decoded[$count - 2];
149 72
            $checksig = $decoded[$count - 1];
150 72
            if ($mOp->isPush() || $nOp->isPush() || $checksig->isPush()) {
151 6
                return false;
152
            }
153
154
            /** @var Operation[] $vKeys */
155 72
            $vKeys = array_slice($decoded, 1, -2);
156 72
            $solutions = [];
157 72
            foreach ($vKeys as $key) {
158 72
                if (!$key->isPush() || !PublicKey::isCompressedOrUncompressed($key->getData())) {
159 12
                    return false;
160
                }
161 66
                $solutions[] = $key->getData();
162 66
            }
163
164 66
            if ($mOp->getOp() >= Opcodes::OP_0
165 66
                && $nOp->getOp() <= Opcodes::OP_16
166 66
                && $checksig->getOp() === Opcodes::OP_CHECKMULTISIG) {
167 66
                $keys = $solutions;
168 66
                return true;
169
            }
170 6
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
171
172
        }
173
174 6
        return false;
175
    }
176
177
    /**
178
     * @param ScriptInterface $script
179
     * @param null $programHash
180
     * @return bool
181
     */
182 282
    public function isWitness(ScriptInterface $script, & $programHash = null)
183
    {
184
        try {
185
186 192
            $decoded = $script->getScriptParser()->decode();
187 192
            $size = $script->getBuffer()->getSize();
188 192
            if ($size < 4 || $size > 34) {
189 102
                return false;
190
            }
191
192 114
            if (count($decoded) !== 2 || !$decoded[1]->isPush()) {
193 90
                return false;
194 282
            }
195
196 48
            $version = $decoded[0]->getOp();
197 48
            if ($version != Opcodes::OP_0 && ($version < Opcodes::OP_1 || $version > Opcodes::OP_16)) {
198 6
                return false;
199
            }
200
201 48
            $witness = $decoded[1];
202 48
            if ($size === $witness->getDataSize() + 2) {
203 48
                $programHash = $witness->getData();
204 48
                return true;
205
            }
206
207 6
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
208
209
        }
210
211 6
        return false;
212
    }
213
214
    /**
215
     * @param ScriptInterface $script
216
     * @param null $solution
217
     * @return string
218
     */
219 192
    public function classify(ScriptInterface $script, &$solution = null)
220
    {
221 192
        $type = self::UNKNOWN;
222 192
        if ($this->isPayToScriptHash($script, $solution)) {
223
            /** @var BufferInterface $solution */
224 48
            $type = self::PAYTOSCRIPTHASH;
225 192
        } elseif ($this->isWitness($script, $solution)) {
226
            /** @var BufferInterface $solution */
227 42
            $size = $solution->getSize();
228 42
            if (20 === $size) {
229 18
                $type = self::WITNESS_V0_KEYHASH;
230 42
            } elseif (32 === $size) {
231 24
                $type = self::WITNESS_V0_SCRIPTHASH;
232 24
            }
233 180
        } elseif ($this->isPayToPublicKey($script, $solution)) {
234
            /** @var BufferInterface $solution */
235 30
            $type = self::PAYTOPUBKEY;
236 168
        } elseif ($this->isPayToPublicKeyHash($script, $solution)) {
0 ignored issues
show
Bug introduced by
It seems like $solution can also be of type object<BitWasp\Buffertools\BufferInterface>; however, BitWasp\Bitcoin\Script\C...:isPayToPublicKeyHash() does only seem to accept null, 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...
237
            /** @var BufferInterface $solution */
238 78
            $type = self::PAYTOPUBKEYHASH;
239 138
        } elseif ($this->isMultisig($script, $solution)) {
0 ignored issues
show
Documentation introduced by
$solution is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
240
            /** @var BufferInterface[] $solution */
241 48
            $type = self::MULTISIG;
242 48
        }
243
244 192
        return $type;
245
    }
246
}
247