FullyQualifiedScript::__construct()   B
last analyzed

Complexity

Conditions 11
Paths 17

Size

Total Lines 43
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 11.2865

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 29
c 1
b 0
f 0
nc 17
nop 3
dl 0
loc 43
ccs 26
cts 30
cp 0.8667
crap 11.2865
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Script;
6
7
use BitWasp\Bitcoin\Exceptions\MissingScriptException;
8
use BitWasp\Bitcoin\Exceptions\ScriptHashMismatch;
9
use BitWasp\Bitcoin\Exceptions\ScriptQualificationError;
10
use BitWasp\Bitcoin\Exceptions\SuperfluousScriptData;
11
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
12
use BitWasp\Bitcoin\Script\Classifier\OutputData;
13
use BitWasp\Bitcoin\Script\Interpreter\Stack;
14
use BitWasp\Bitcoin\Transaction\Factory\SignData;
15
use BitWasp\Bitcoin\Transaction\Factory\SigValues;
16
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
17
use BitWasp\Buffertools\BufferInterface;
18
19
class FullyQualifiedScript
20
{
21
22
    /**
23
     * @var OutputData
24
     */
25
    private $spkData;
26
27
    /**
28
     * @var OutputData|null
29
     */
30
    private $rsData;
31
32
    /**
33
     * @var OutputData|null
34
     */
35
    private $wsData;
36
37
    /**
38
     * @var OutputData
39
     */
40
    private $signData;
41
42
    /**
43
     * @var int
44
     */
45
    private $sigVersion;
46
47
    /**
48
     * This is responsible for checking that the script-hash
49
     * commitments between scripts were satisfied, and determines
50
     * the sigVersion.
51
     *
52
     * It rejects superfluous redeem & witness scripts, and refuses
53
     * to construct unless all necessary scripts are provided.
54
     *
55
     * @param OutputData $spkData
56
     * @param OutputData|null $rsData
57
     * @param OutputData|null $wsData
58
     */
59 122
    public function __construct(
60
        OutputData $spkData,
61
        OutputData $rsData = null,
62
        OutputData $wsData = null
63
    ) {
64 122
        $signScript = $spkData;
65 122
        $sigVersion = SigHash::V0;
66
67 122
        if ($spkData->getType() === ScriptType::P2SH) {
68 45
            if (!($rsData instanceof OutputData)) {
69
                throw new MissingScriptException("Missing redeemScript");
70
            }
71 45
            if (!$rsData->getScript()->getScriptHash()->equals($spkData->getSolution())) {
72 1
                throw new ScriptHashMismatch("Redeem script fails to solve pay-to-script-hash");
73
            }
74 44
            $signScript = $rsData;
75 77
        } else if ($rsData) {
76
            throw new SuperfluousScriptData("Data provided for redeemScript was not necessary");
77
        }
78
79 121
        if ($signScript->getType() === ScriptType::P2WKH) {
80 12
            $classifier = new OutputClassifier();
81 12
            $signScript = $classifier->decode(ScriptFactory::scriptPubKey()->p2pkh($signScript->getSolution()));
82 12
            $sigVersion = SigHash::V1;
83 111
        } else if ($signScript->getType() === ScriptType::P2WSH) {
84 38
            if (!($wsData instanceof OutputData)) {
85
                throw new MissingScriptException("Missing witnessScript");
86
            }
87 38
            if (!$wsData->getScript()->getWitnessScriptHash()->equals($signScript->getSolution())) {
88 1
                $origin = $rsData ? "redeemScript" : "scriptPubKey";
89 1
                throw new ScriptHashMismatch("Witness script does not match witness program in $origin");
90
            }
91 37
            $signScript = $wsData;
92 37
            $sigVersion = SigHash::V1;
93 73
        } else if ($wsData) {
94
            throw new SuperfluousScriptData("Data provided for witnessScript was not necessary");
95
        }
96
97 120
        $this->spkData = $spkData;
98 120
        $this->rsData = $rsData;
99 120
        $this->wsData = $wsData;
100 120
        $this->signData = $signScript;
101 120
        $this->sigVersion = $sigVersion;
102 120
    }
103
104
    /**
105
     * Checks $chunks (a decompiled scriptSig) for it's last element,
106
     * or defers to SignData. If both are provided, it checks the
107
     * value obtained from $chunks against SignData.
108
     *
109
     * @param BufferInterface[] $chunks
110
     * @param SignData $signData
111
     * @return P2shScript
112
     */
113 48
    public static function findRedeemScript(array $chunks, SignData $signData)
114
    {
115 48
        if (count($chunks) > 0) {
116 27
            $redeemScript = new Script($chunks[count($chunks) - 1]);
117 27
            if ($signData->hasRedeemScript()) {
118 27
                if (!$redeemScript->equals($signData->getRedeemScript())) {
119 27
                    throw new ScriptQualificationError('Extracted redeemScript did not match sign data');
120
                }
121
            }
122
        } else {
123 46
            if (!$signData->hasRedeemScript()) {
124 1
                throw new ScriptQualificationError('Redeem script not provided in sign data or scriptSig');
125
            }
126 45
            $redeemScript = $signData->getRedeemScript();
127
        }
128
129 46
        if (!($redeemScript instanceof P2shScript)) {
130 29
            $redeemScript = new P2shScript($redeemScript);
131
        }
132
133 46
        return $redeemScript;
134
    }
135
136
    /**
137
     * Checks the witness for it's last element, or whatever
138
     * the SignData happens to have. If SignData has a WS,
139
     * it will ensure that if chunks has a script, it matches WS.
140
     * @param ScriptWitnessInterface $witness
141
     * @param SignData $signData
142
     * @return Script|ScriptInterface|WitnessScript
143
     */
144 42
    public static function findWitnessScript(ScriptWitnessInterface $witness, SignData $signData)
145
    {
146 42
        if (count($witness) > 0) {
147 23
            $witnessScript = new Script($witness->bottom());
148 23
            if ($signData->hasWitnessScript()) {
149 23
                if (!$witnessScript->equals($signData->getWitnessScript())) {
150 23
                    throw new ScriptQualificationError('Extracted witnessScript did not match sign data');
151
                }
152
            }
153
        } else {
154 40
            if (!$signData->hasWitnessScript()) {
155 2
                throw new ScriptQualificationError('Witness script not provided in sign data or witness');
156
            }
157 38
            $witnessScript = $signData->getWitnessScript();
158
        }
159
160 38
        if (!($witnessScript instanceof WitnessScript)) {
161 23
            $witnessScript = new WitnessScript($witnessScript);
162
        }
163
164 38
        return $witnessScript;
165
    }
166
167
    /**
168
     * This function attempts to produce a FQS from
169
     * raw scripts and witnesses. High level checking
170
     * of script types is done to determine what we need
171
     * from all this, before initializing the constructor
172
     * for final validation.
173
     *
174
     * @param ScriptInterface $scriptPubKey
175
     * @param ScriptInterface $scriptSig
176
     * @param ScriptWitnessInterface $witness
177
     * @param SignData|null $signData
178
     * @param OutputClassifier|null $classifier
179
     * @return FullyQualifiedScript
180
     */
181 128
    public static function fromTxData(
182
        ScriptInterface $scriptPubKey,
183
        ScriptInterface $scriptSig,
184
        ScriptWitnessInterface $witness,
185
        SignData $signData = null,
186
        OutputClassifier $classifier = null
187
    ) {
188 128
        $classifier = $classifier ?: new OutputClassifier();
189 128
        $signData = $signData ?: new SignData();
190
191 128
        $wsData = null;
192 128
        $rsData = null;
193 128
        $solution = $spkData = $classifier->decode($scriptPubKey);
194
195 128
        $sigChunks = [];
196 128
        if (!$scriptSig->isPushOnly($sigChunks)) {
197
            throw new ScriptQualificationError("Invalid script signature - must be PUSHONLY.");
198
        }
199
200 128
        if ($solution->getType() === ScriptType::P2SH) {
201 48
            $redeemScript = self::findRedeemScript($sigChunks, $signData);
202 46
            $solution = $rsData = $classifier->decode($redeemScript);
203
        }
204
205 126
        if ($solution->getType() === ScriptType::P2WSH) {
206 42
            $witnessScript = self::findWitnessScript($witness, $signData);
207 38
            $wsData = $classifier->decode($witnessScript);
208
        }
209
210 122
        return new FullyQualifiedScript($spkData, $rsData, $wsData);
211
    }
212
213
    /**
214
     * Was the FQS's scriptPubKey P2SH?
215
     * @return bool
216
     */
217 50
    public function isP2SH(): bool
218
    {
219 50
        return $this->rsData instanceof OutputData;
220
    }
221
222
    /**
223
     * Was the FQS's scriptPubKey, or redeemScript, P2WSH?
224
     * @return bool
225
     */
226 50
    public function isP2WSH(): bool
227
    {
228 50
        return $this->wsData instanceof OutputData;
229
    }
230
231
    /**
232
     * Returns the scriptPubKey.
233
     * @return OutputData
234
     */
235 87
    public function scriptPubKey(): OutputData
236
    {
237 87
        return $this->spkData;
238
    }
239
240
    /**
241
     * Returns the sign script we qualified from
242
     * the spk/rs/ws. Essentially this is the script
243
     * that actually locks the coins (the CScript
244
     * passed into EvalScript in interpreter.cpp)
245
     *
246
     * @return OutputData
247
     */
248 117
    public function signScript(): OutputData
249
    {
250 117
        return $this->signData;
251
    }
252
253
    /**
254
     * Returns the signature hashing algorithm version.
255
     * Defaults to V0, unless script was segwit.
256
     * @return int
257
     */
258 107
    public function sigVersion(): int
259
    {
260 107
        return $this->sigVersion;
261
    }
262
263
    /**
264
     * Returns the redeemScript, if we had one.
265
     * Throws an exception otherwise.
266
     * @return OutputData
267
     * @throws \RuntimeException
268
     */
269 28
    public function redeemScript(): OutputData
270
    {
271 28
        if (null === $this->rsData) {
272
            throw new \RuntimeException("No redeemScript for this script!");
273
        }
274
275 28
        return $this->rsData;
276
    }
277
278
    /**
279
     * Returns the witnessScript, if we had one.
280
     * Throws an exception otherwise.
281
     * @return OutputData
282
     * @throws \RuntimeException
283
     */
284 22
    public function witnessScript(): OutputData
285
    {
286 22
        if (null === $this->wsData) {
287
            throw new \RuntimeException("No witnessScript for this script!");
288
        }
289
290 22
        return $this->wsData;
291
    }
292
293
    /**
294
     * Encodes the stack (the stack passed as an
295
     * argument to EvalScript in interpreter.cpp)
296
     * into a scriptSig and witness structure. These
297
     * are suitable for directly encoding in a transaction.
298
     * @param Stack $stack
299
     * @return SigValues
300
     */
301 93
    public function encodeStack(Stack $stack): SigValues
302
    {
303 93
        $scriptSigChunks = $stack->all();
304 93
        $witness = [];
305
306 93
        $solution = $this->spkData;
307 93
        $p2sh = false;
308 93
        if ($solution->getType() === ScriptType::P2SH) {
309 32
            $p2sh = true;
310 32
            $solution = $this->rsData;
311
        }
312
313 93
        if ($solution->getType() === ScriptType::P2WKH) {
0 ignored issues
show
Bug introduced by
The method getType() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

313
        if ($solution->/** @scrutinizer ignore-call */ getType() === ScriptType::P2WKH) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
314 12
            $witness = $stack->all();
315 12
            $scriptSigChunks = [];
316 83
        } else if ($solution->getType() === ScriptType::P2WSH) {
317 26
            $witness = $stack->all();
318 26
            $witness[] = $this->wsData->getScript()->getBuffer();
0 ignored issues
show
Bug introduced by
The method getScript() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
            $witness[] = $this->wsData->/** @scrutinizer ignore-call */ getScript()->getBuffer();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
319 26
            $scriptSigChunks = [];
320
        }
321
322 93
        if ($p2sh) {
323 32
            $scriptSigChunks[] = $this->rsData->getScript()->getBuffer();
0 ignored issues
show
Bug introduced by
The method getScript() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

323
            $scriptSigChunks[] = $this->rsData->/** @scrutinizer ignore-call */ getScript()->getBuffer();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
324
        }
325
326 93
        return new SigValues(
327 93
            ScriptFactory::pushAll($scriptSigChunks),
328 93
            new ScriptWitness(...$witness)
329
        );
330
    }
331
332
    /**
333
     * @param ScriptInterface $scriptSig
334
     * @param ScriptWitnessInterface $witness
335
     * @return Stack
336
     */
337 117
    public function extractStack(ScriptInterface $scriptSig, ScriptWitnessInterface $witness): Stack
338
    {
339 117
        $sigChunks = [];
340 117
        if (!$scriptSig->isPushOnly($sigChunks)) {
341
            throw new \RuntimeException("Invalid signature script - must be push only");
342
        }
343
344 117
        $solution = $this->spkData;
345 117
        if ($solution->getType() === ScriptType::P2SH) {
346 42
            $solution = $this->rsData;
347 42
            $nChunks = count($sigChunks);
348 42
            if ($nChunks > 0 && $sigChunks[$nChunks - 1]->equals($this->rsData->getScript()->getBuffer())) {
349 25
                $sigChunks = array_slice($sigChunks, 0, -1);
350
            }
351
        }
352
353 117
        if ($solution->getType() === ScriptType::P2WKH) {
354 12
            $sigChunks = $witness->all();
355 107
        } else if ($solution->getType() === ScriptType::P2WSH) {
356 36
            $sigChunks = $witness->all();
357 36
            $nChunks = count($sigChunks);
358 36
            if ($nChunks > 0 && $sigChunks[$nChunks - 1]->equals($this->wsData->getScript()->getBuffer())) {
359 21
                $sigChunks = array_slice($sigChunks, 0, -1);
360
            }
361
        }
362
363 117
        return new Stack($sigChunks);
364
    }
365
}
366