Completed
Push — master ( 6a5739...b59571 )
by thomas
103:17 queued 33:16
created

FullyQualifiedScript::extractStack()   D

Complexity

Conditions 9
Paths 13

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 13
nop 2
dl 0
loc 28
rs 4.909
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\Script;
4
5
use BitWasp\Bitcoin\Exceptions\MissingScriptException;
6
use BitWasp\Bitcoin\Exceptions\ScriptHashMismatch;
7
use BitWasp\Bitcoin\Exceptions\ScriptQualificationError;
8
use BitWasp\Bitcoin\Exceptions\SuperfluousScriptData;
9
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
10
use BitWasp\Bitcoin\Script\Classifier\OutputData;
11
use BitWasp\Bitcoin\Script\Interpreter\Stack;
12
use BitWasp\Bitcoin\Transaction\Factory\SignData;
13
use BitWasp\Bitcoin\Transaction\Factory\SigValues;
14
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
15
use BitWasp\Buffertools\BufferInterface;
16
17
class FullyQualifiedScript
18
{
19
20
    /**
21
     * @var OutputData
22
     */
23
    private $spkData;
24
25
    /**
26
     * @var OutputData|null
27
     */
28
    private $rsData;
29
30
    /**
31
     * @var OutputData|null
32
     */
33
    private $wsData;
34
35
    /**
36
     * @var OutputData
37
     */
38
    private $signData;
39
40
    /**
41
     * @var int
42
     */
43
    private $sigVersion;
44
45
    /**
46
     * This is responsible for checking that the script-hash
47
     * commitments between scripts were satisfied, and determines
48
     * the sigVersion.
49
     *
50
     * It rejects superfluous redeem & witness scripts, and refuses
51
     * to construct unless all necessary scripts are provided.
52
     *
53
     * @param OutputData $spkData
54
     * @param OutputData|null $rsData
55
     * @param OutputData|null $wsData
56
     */
57
    public function __construct(
58
        OutputData $spkData,
59
        OutputData $rsData = null,
60
        OutputData $wsData = null
61
    ) {
62
        $signScript = $spkData;
63
        $sigVersion = SigHash::V0;
64
65
        if ($spkData->getType() === ScriptType::P2SH) {
66
            if (!($rsData instanceof OutputData)) {
67
                throw new MissingScriptException("Missing redeemScript");
68
            }
69
            if (!$rsData->getScript()->getScriptHash()->equals($spkData->getSolution())) {
70
                throw new ScriptHashMismatch("Redeem script fails to solve pay-to-script-hash");
71
            }
72
            $signScript = $rsData;
73
        } else if ($rsData) {
74
            throw new SuperfluousScriptData("Data provided for redeemScript was not necessary");
75
        }
76
77
        if ($signScript->getType() === ScriptType::P2WKH) {
78
            $classifier = new OutputClassifier();
79
            $signScript = $classifier->decode(ScriptFactory::scriptPubKey()->p2pkh($signScript->getSolution()));
80
            $sigVersion = SigHash::V1;
81
        } else if ($signScript->getType() === ScriptType::P2WSH) {
82
            if (!($wsData instanceof OutputData)) {
83
                throw new MissingScriptException("Missing witnessScript");
84
            }
85
            if (!$wsData->getScript()->getWitnessScriptHash()->equals($signScript->getSolution())) {
86
                $origin = $rsData ? "redeemScript" : "scriptPubKey";
87
                throw new ScriptHashMismatch("Witness script does not match witness program in $origin");
88
            }
89
            $signScript = $wsData;
90
            $sigVersion = SigHash::V1;
91
        } else if ($wsData) {
92
            throw new SuperfluousScriptData("Data provided for witnessScript was not necessary");
93
        }
94
95
        $this->spkData = $spkData;
96
        $this->rsData = $rsData;
97
        $this->wsData = $wsData;
98
        $this->signData = $signScript;
99
        $this->sigVersion = $sigVersion;
100
    }
101
102
    /**
103
     * Checks $chunks (a decompiled scriptSig) for it's last element,
104
     * or defers to SignData. If both are provided, it checks the
105
     * value obtained from $chunks against SignData.
106
     *
107
     * @param BufferInterface[] $chunks
108
     * @param SignData $signData
109
     * @return P2shScript
110
     */
111
    public static function findRedeemScript(array $chunks, SignData $signData)
112
    {
113
        if (count($chunks) > 0) {
114
            $redeemScript = new Script($chunks[count($chunks) - 1]);
115
            if ($signData->hasRedeemScript()) {
116
                if (!$redeemScript->equals($signData->getRedeemScript())) {
117
                    throw new ScriptQualificationError('Extracted redeemScript did not match sign data');
118
                }
119
            }
120
        } else {
121
            if (!$signData->hasRedeemScript()) {
122
                throw new ScriptQualificationError('Redeem script not provided in sign data or scriptSig');
123
            }
124
            $redeemScript = $signData->getRedeemScript();
125
        }
126
127
        if (!($redeemScript instanceof P2shScript)) {
128
            $redeemScript = new P2shScript($redeemScript);
129
        }
130
131
        return $redeemScript;
132
    }
133
134
    /**
135
     * Checks the witness for it's last element, or whatever
136
     * the SignData happens to have. If SignData has a WS,
137
     * it will ensure that if chunks has a script, it matches WS.
138
     * @param ScriptWitnessInterface $witness
139
     * @param SignData $signData
140
     * @return Script|ScriptInterface|WitnessScript
141
     */
142
    public static function findWitnessScript(ScriptWitnessInterface $witness, SignData $signData)
143
    {
144
        if (count($witness) > 0) {
145
            $witnessScript = new Script($witness->bottom());
146
            if ($signData->hasWitnessScript()) {
147
                if (!$witnessScript->equals($signData->getWitnessScript())) {
148
                    throw new ScriptQualificationError('Extracted witnessScript did not match sign data');
149
                }
150
            }
151
        } else {
152
            if (!$signData->hasWitnessScript()) {
153
                throw new ScriptQualificationError('Witness script not provided in sign data or witness');
154
            }
155
            $witnessScript = $signData->getWitnessScript();
156
        }
157
158
        if (!($witnessScript instanceof WitnessScript)) {
159
            $witnessScript = new WitnessScript($witnessScript);
160
        }
161
162
        return $witnessScript;
163
    }
164
165
    /**
166
     * This function attempts to produce a FQS from
167
     * raw scripts and witnesses. High level checking
168
     * of script types is done to determine what we need
169
     * from all this, before initializing the constructor
170
     * for final validation.
171
     *
172
     * @param ScriptInterface $scriptPubKey
173
     * @param ScriptInterface|null $scriptSig
174
     * @param ScriptWitnessInterface|null $witness
175
     * @param SignData|null $signData
176
     * @param OutputClassifier|null $classifier
177
     * @return FullyQualifiedScript
178
     */
179
    public static function fromTxData(
180
        ScriptInterface $scriptPubKey,
181
        ScriptInterface $scriptSig = null,
182
        ScriptWitnessInterface $witness = null,
183
        SignData $signData = null,
184
        OutputClassifier $classifier = null
185
    ) {
186
        $classifier = $classifier ?: new OutputClassifier();
187
        $signData = $signData ?: new SignData();
188
189
        $wsData = null;
190
        $rsData = null;
191
        $solution = $spkData = $classifier->decode($scriptPubKey);
192
193
        $sigChunks = [];
194
        if (!$scriptSig->isPushOnly($sigChunks)) {
0 ignored issues
show
Bug introduced by
It seems like $scriptSig is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
195
            throw new ScriptQualificationError("Invalid script signature - must be PUSHONLY.");
196
        }
197
198
        if ($solution->getType() === ScriptType::P2SH) {
199
            $redeemScript = self::findRedeemScript($sigChunks, $signData);
200
            $solution = $rsData = $classifier->decode($redeemScript);
201
        }
202
203
        if ($solution->getType() === ScriptType::P2WSH) {
204
            $witnessScript = self::findWitnessScript($witness, $signData);
0 ignored issues
show
Bug introduced by
It seems like $witness defined by parameter $witness on line 182 can be null; however, BitWasp\Bitcoin\Script\F...pt::findWitnessScript() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
205
            $wsData = $classifier->decode($witnessScript);
206
        }
207
208
        return new FullyQualifiedScript($spkData, $rsData, $wsData);
209
    }
210
211
    /**
212
     * Was the FQS's scriptPubKey P2SH?
213
     * @return bool
214
     */
215
    public function isP2SH()
216
    {
217
        return $this->rsData instanceof OutputData;
218
    }
219
220
    /**
221
     * Was the FQS's scriptPubKey, or redeemScript, P2WSH?
222
     * @return bool
223
     */
224
    public function isP2WSH()
225
    {
226
        return $this->wsData instanceof OutputData;
227
    }
228
229
    /**
230
     * Returns the scriptPubKey.
231
     * @return OutputData
232
     */
233
    public function scriptPubKey()
234
    {
235
        return $this->spkData;
236
    }
237
238
    /**
239
     * Returns the sign script we qualified from
240
     * the spk/rs/ws. Essentially this is the script
241
     * that actually locks the coins (the CScript
242
     * passed into EvalScript in interpreter.cpp)
243
     *
244
     * @return OutputData
245
     */
246
    public function signScript()
247
    {
248
        return $this->signData;
249
    }
250
251
    /**
252
     * Returns the signature hashing algorithm version.
253
     * Defaults to V0, unless script was segwit.
254
     * @return int
255
     */
256
    public function sigVersion()
257
    {
258
        return $this->sigVersion;
259
    }
260
261
    /**
262
     * Returns the redeemScript, if we had one.
263
     * Throws an exception otherwise.
264
     * @return OutputData
265
     * @throws \RuntimeException
266
     */
267
    public function redeemScript()
268
    {
269
        if (null === $this->rsData) {
270
            throw new \RuntimeException("No redeemScript for this script!");
271
        }
272
273
        return $this->rsData;
274
    }
275
276
    /**
277
     * Returns the witnessScript, if we had one.
278
     * Throws an exception otherwise.
279
     * @return OutputData
280
     * @throws \RuntimeException
281
     */
282
    public function witnessScript()
283
    {
284
        if (null === $this->wsData) {
285
            throw new \RuntimeException("No witnessScript for this script!");
286
        }
287
288
        return $this->wsData;
289
    }
290
291
    /**
292
     * Encodes the stack (the stack passed as an
293
     * argument to EvalScript in interpreter.cpp)
294
     * into a scriptSig and witness structure. These
295
     * are suitable for directly encoding in a transaction.
296
     * @param Stack $stack
297
     * @return SigValues
298
     */
299
    public function encodeStack(Stack $stack)
300
    {
301
        $scriptSigChunks = $stack->all();
302
        $witness = [];
303
304
        $solution = $this->spkData;
305
        $p2sh = false;
306
        if ($solution->getType() === ScriptType::P2SH) {
307
            $p2sh = true;
308
            $solution = $this->rsData;
309
        }
310
311
        if ($solution->getType() === ScriptType::P2WKH) {
312
            $witness = $stack->all();
313
            $scriptSigChunks = [];
314
        } else if ($solution->getType() === ScriptType::P2WSH) {
315
            $witness = $stack->all();
316
            $witness[] = $this->wsData->getScript()->getBuffer();
317
            $scriptSigChunks = [];
318
        }
319
320
        if ($p2sh) {
321
            $scriptSigChunks[] = $this->rsData->getScript()->getBuffer();
322
        }
323
324
        return new SigValues(
325
            ScriptFactory::pushAll($scriptSigChunks),
326
            new ScriptWitness($witness)
327
        );
328
    }
329
330
    /**
331
     * @param ScriptInterface $scriptSig
332
     * @param ScriptWitnessInterface $witness
333
     * @return Stack
334
     */
335
    public function extractStack(ScriptInterface $scriptSig, ScriptWitnessInterface $witness)
336
    {
337
        $sigChunks = [];
338
        if (!$scriptSig->isPushOnly($sigChunks)) {
339
            throw new \RuntimeException("Invalid signature script - must be push only");
340
        }
341
342
        $solution = $this->spkData;
343
        if ($solution->getType() === ScriptType::P2SH) {
344
            $solution = $this->rsData;
345
            $nChunks = count($sigChunks);
346
            if ($nChunks > 0 && $sigChunks[$nChunks - 1]->equals($this->rsData->getScript()->getBuffer())) {
347
                $sigChunks = array_slice($sigChunks, 0, -1);
348
            }
349
        }
350
351
        if ($solution->getType() === ScriptType::P2WKH) {
352
            $sigChunks = $witness->all();
353
        } else if ($solution->getType() === ScriptType::P2WSH) {
354
            $sigChunks = $witness->all();
355
            $nChunks = count($sigChunks);
356
            if ($nChunks > 0 && $sigChunks[$nChunks - 1]->equals($this->wsData->getScript()->getBuffer())) {
357
                $sigChunks = array_slice($sigChunks, 0, -1);
358
            }
359
        }
360
361
        return new Stack($sigChunks);
0 ignored issues
show
Documentation introduced by
$sigChunks is of type null|array, but the function expects a array<integer,object<Bit...tools\BufferInterface>>.

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...
362
    }
363
}
364