FullyQualifiedScript   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Test Coverage

Coverage 93.7%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 121
c 2
b 1
f 0
dl 0
loc 345
ccs 119
cts 127
cp 0.937
rs 7.44
wmc 52

13 Methods

Rating   Name   Duplication   Size   Complexity  
B extractStack() 0 27 9
A findRedeemScript() 0 21 6
A isP2WSH() 0 3 1
A signScript() 0 3 1
A fromTxData() 0 30 6
A findWitnessScript() 0 21 6
A sigVersion() 0 3 1
A encodeStack() 0 28 5
A witnessScript() 0 7 2
A isP2SH() 0 3 1
B __construct() 0 43 11
A redeemScript() 0 7 2
A scriptPubKey() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like FullyQualifiedScript often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FullyQualifiedScript, and based on these observations, apply Extract Interface, too.

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