Completed
Pull Request — master (#389)
by thomas
402:01 queued 399:14
created

InputSigner::solve()   C

Complexity

Conditions 8
Paths 11

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 8.5784

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 11
nop 1
dl 0
loc 34
ccs 19
cts 24
cp 0.7917
crap 8.5784
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\Transaction\Factory;
4
5
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
6
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
8
use BitWasp\Bitcoin\Crypto\Hash;
9
use BitWasp\Bitcoin\Crypto\Random\Rfc6979;
10
use BitWasp\Bitcoin\Key\PublicKeyFactory;
11
use BitWasp\Bitcoin\Script\Classifier\OutputClassifier;
12
use BitWasp\Bitcoin\Script\Classifier\OutputData;
13
use BitWasp\Bitcoin\Script\Interpreter\Checker;
14
use BitWasp\Bitcoin\Script\Interpreter\Interpreter;
15
use BitWasp\Bitcoin\Script\Interpreter\Stack;
16
use BitWasp\Bitcoin\Script\Opcodes;
17
use BitWasp\Bitcoin\Script\Script;
18
use BitWasp\Bitcoin\Script\ScriptFactory;
19
use BitWasp\Bitcoin\Script\ScriptInfo\Multisig;
20
use BitWasp\Bitcoin\Script\ScriptInterface;
21
use BitWasp\Bitcoin\Script\ScriptWitness;
22
use BitWasp\Bitcoin\Signature\SignatureSort;
23
use BitWasp\Bitcoin\Signature\TransactionSignature;
24
use BitWasp\Bitcoin\Signature\TransactionSignatureFactory;
25
use BitWasp\Bitcoin\Signature\TransactionSignatureInterface;
26
use BitWasp\Bitcoin\Transaction\SignatureHash\Hasher;
27
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHash;
28
use BitWasp\Bitcoin\Transaction\SignatureHash\V1Hasher;
29
use BitWasp\Bitcoin\Transaction\Transaction;
30
use BitWasp\Bitcoin\Transaction\TransactionInterface;
31
use BitWasp\Bitcoin\Transaction\TransactionOutputInterface;
32
use BitWasp\Buffertools\Buffer;
33
use BitWasp\Buffertools\BufferInterface;
34
35
class InputSigner
36
{
37
    /**
38
     * @var EcAdapterInterface
39
     */
40
    private $ecAdapter;
41
42
    /**
43
     * @var OutputData $scriptPubKey
44
     */
45
    private $scriptPubKey;
46
47
    /**
48
     * @var OutputData $redeemScript
49
     */
50
    private $redeemScript;
51
52
    /**
53
     * @var OutputData $witnessScript
54
     */
55
    private $witnessScript;
56
57
    /**
58
     * @var TransactionInterface
59
     */
60
    private $tx;
61
62
    /**
63
     * @var int
64
     */
65
    private $nInput;
66
67
    /**
68
     * @var TransactionOutputInterface
69
     */
70
    private $txOut;
71
72
    /**
73
     * @var PublicKeyInterface[]
74
     */
75
    private $publicKeys = [];
76
77
    /**
78
     * @var TransactionSignatureInterface[]
79
     */
80
    private $signatures = [];
81
82
    /**
83
     * @var int
84
     */
85
    private $requiredSigs = 0;
86
87
    /**
88
     * @var OutputClassifier
89
     */
90
    private $classifier;
91
92
    /**
93
     * TxInputSigning constructor.
94
     * @param EcAdapterInterface $ecAdapter
95
     * @param TransactionInterface $tx
96
     * @param int $nInput
97
     * @param TransactionOutputInterface $txOut
98
     * @param SignData $signData
99
     */
100 84
    public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $tx, $nInput, TransactionOutputInterface $txOut, SignData $signData)
101
    {
102 84
        $this->ecAdapter = $ecAdapter;
103 84
        $this->tx = $tx;
104 84
        $this->nInput = $nInput;
105 84
        $this->txOut = $txOut;
106 84
        $this->classifier = new OutputClassifier();
107 84
        $this->publicKeys = [];
108 84
        $this->signatures = [];
109
110 84
        $this->solve($signData);
111 84
        $this->extractSignatures();
112 84
    }
113
114
    /**
115
     * @param int $sigVersion
116
     * @param TransactionSignatureInterface[] $stack
117
     * @param ScriptInterface $scriptCode
118
     * @return \SplObjectStorage
119
     */
120 26
    private function sortMultiSigs($sigVersion, $stack, ScriptInterface $scriptCode)
121
    {
122 24
        $sigSort = new SignatureSort($this->ecAdapter);
123 24
        $sigs = new \SplObjectStorage;
124
125 24
        foreach ($stack as $txSig) {
126
            $hash = $this->calculateSigHash($scriptCode, $txSig->getHashType(), $sigVersion);
127
            $linked = $sigSort->link([$txSig->getSignature()], $this->publicKeys, $hash);
128
            foreach ($this->publicKeys as $key) {
129
                if ($linked->contains($key)) {
130
                    $sigs[$key] = $txSig;
131
                }
132
            }
133 8
        }
134
135 26
        return $sigs;
136
    }
137
138
    /**
139
     * @param string $type
140
     * @param ScriptInterface $scriptCode
141
     * @param BufferInterface[] $stack
142
     * @param int $sigVersion
143
     * @return string
144
     */
145 78
    public function extractFromValues($type, ScriptInterface $scriptCode, array $stack, $sigVersion)
146
    {
147 78
        $size = count($stack);
148 78
        if ($type === OutputClassifier::PAYTOPUBKEYHASH) {
149 42
            $this->requiredSigs = 1;
150 42
            if ($size === 2) {
151 24
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
152 24
                $this->publicKeys = [PublicKeyFactory::fromHex($stack[1], $this->ecAdapter)];
153 8
            }
154 14
        }
155
156 78
        if ($type === OutputClassifier::PAYTOPUBKEY) {
157 12
            $this->requiredSigs = 1;
158 12
            if ($size === 1) {
159 6
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
160 2
            }
161 4
        }
162
163 78
        if ($type === OutputClassifier::MULTISIG) {
164 24
            $info = new Multisig($scriptCode);
165 24
            $this->requiredSigs = $info->getRequiredSigCount();
166 24
            $this->publicKeys = $info->getKeys();
167
168 24
            if ($size > 1) {
169 24
                $vars = [];
170 24
                for ($i = 1, $j = $size - 1; $i < $j; $i++) {
171
                    $vars[] = TransactionSignatureFactory::fromHex($stack[$i], $this->ecAdapter);
172
                }
173
174 24
                $sigs = $this->sortMultiSigs($sigVersion, $vars, $scriptCode);
175 24
                foreach ($this->publicKeys as $idx => $key) {
176 24
                    $this->signatures[$idx] = isset($sigs[$key]) ? $sigs[$key]->getBuffer() : null;
177 8
                }
178 8
            }
179 8
        }
180
181 78
        return $type;
182
    }
183
184
    /**
185
     * @param SignData $signData
186
     * @return $this
187
     * @throws \Exception
188
     */
189 84
    private function solve(SignData $signData)
190
    {
191 84
        $flags = Interpreter::VERIFY_NONE;
192 84
        $interpreter = new Interpreter();
193 84
        $scriptPubKey = $this->txOut->getScript();
194 84
        $solution = $this->scriptPubKey = $this->classifier->decode($scriptPubKey);
195 84
        if ($solution->getType() === OutputClassifier::UNKNOWN) {
196
            throw new \RuntimeException('scriptPubKey type is unknown');
197
        }
198
199 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
200 24
            $redeemScript = $signData->getRedeemScript();
201 24
            if (!$interpreter->verify(ScriptFactory::sequence([$redeemScript->getBuffer()]), $solution->getScript(), $flags, new Checker($this->ecAdapter, $this->tx, $this->nInput, $this->txOut->getValue()))) {
202
                throw new \Exception('Redeem script fails to solve pay-to-script-hash');
203
            }
204 24
            $solution = $this->redeemScript = $this->classifier->decode($redeemScript);
205 24
            if (!in_array($solution->getType(), [OutputClassifier::WITNESS_V0_SCRIPTHASH, OutputClassifier::WITNESS_V0_KEYHASH, OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
206
                throw new \Exception('Unsupported pay-to-script-hash script');
207
            }
208 8
        }
209
        // WitnessKeyHash doesn't require further solving until signing
210 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
211 18
            $witnessScript = $signData->getWitnessScript();
212 18
            if (!$interpreter->verify(ScriptFactory::sequence([$witnessScript->getBuffer()]), $solution->getScript(), $flags, new Checker($this->ecAdapter, $this->tx, $this->nInput, $this->txOut->getValue()))) {
213
                throw new \Exception('Witness script fails to solve pay-to-script-hash');
214
            }
215 18
            $solution = $this->witnessScript = $this->classifier->decode($witnessScript);
216 18
            if (!in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
217
                throw new \Exception('Unsupported witness-script-hash script');
218
            }
219 6
        }
220
221 84
        return $this;
222
    }
223
224
    /**
225
     * @return $this
226
     */
227 84
    public function extractSignatures()
228
    {
229 84
        $solution = $this->scriptPubKey;
230 84
        $scriptSig = $this->tx->getInput($this->nInput)->getScript();
231 84
        if (in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
232 42
            $this->extractFromValues($solution->getType(), $solution->getScript(), $this->evalPushOnly($scriptSig), 0);
233 14
        }
234
235 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
236 24
            $stack = $this->evalPushOnly($scriptSig);
237 24
            if (count($stack) > 0) {
238 18
                $redeemScript = new Script(end($stack));
0 ignored issues
show
Security Bug introduced by
It seems like end($stack) targeting end() can also be of type false; however, BitWasp\Bitcoin\Script\Script::__construct() does only seem to accept null|object<BitWasp\Buffertools\BufferInterface>, did you maybe forget to handle an error condition?
Loading history...
239 18
                if (!$redeemScript->getBuffer()->equals($this->redeemScript->getScript()->getBuffer())) {
0 ignored issues
show
Documentation introduced by
$this->redeemScript->getScript()->getBuffer() is of type object<BitWasp\Buffertools\Buffer>, but the function expects a object<self>.

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
                    throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
241
                }
242
243 18
                $solution = $this->redeemScript;
244 18
                $this->extractFromValues($solution->getType(), $solution->getScript(), array_slice($stack, 0, -1), 0);
245 6
            }
246 8
        }
247
248 84
        $witnesses = $this->tx->getWitnesses();
249 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
250 22
            $wit = isset($witnesses[$this->nInput]) ? $witnesses[$this->nInput]->all() : [];
251 12
            $keyHashCode = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
252 12
            $this->extractFromValues(OutputClassifier::PAYTOPUBKEYHASH, $keyHashCode, $wit, 1);
253 80
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
254 18
            if (isset($witnesses[$this->nInput])) {
255 18
                $witness = $witnesses[$this->nInput];
256 18
                $witCount = count($witnesses[$this->nInput]);
257 18
                if ($witCount > 0) {
258 18
                    if (!$witness[$witCount - 1]->equals($this->witnessScript->getScript()->getBuffer())) {
259
                        throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
260
                    }
261
262 18
                    $solution = $this->witnessScript;
263 18
                    $this->extractFromValues($solution->getType(), $solution->getScript(), array_slice($witness->all(), 0, -1), 1);
264 6
                }
265 6
            }
266 6
        }
267
268 84
        return $this;
269
    }
270
271
    /**
272
     * @param ScriptInterface $scriptCode
273
     * @param int $sigHashType
274
     * @param int $sigVersion
275
     * @return BufferInterface
276
     */
277 84
    public function calculateSigHash(ScriptInterface $scriptCode, $sigHashType, $sigVersion)
278
    {
279 84
        if ($sigVersion === 1) {
280 30
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
281 10
        } else {
282 54
            $hasher = new Hasher($this->tx);
283
        }
284
285 84
        return $hasher->calculate($scriptCode, $this->nInput, $sigHashType);
286
    }
287
288
    /**
289
     * @param PrivateKeyInterface $key
290
     * @param ScriptInterface $scriptCode
291
     * @param int $sigHashType
292
     * @param int $sigVersion
293
     * @return TransactionSignature
294
     */
295 84
    public function calculateSignature(PrivateKeyInterface $key, ScriptInterface $scriptCode, $sigHashType, $sigVersion)
296
    {
297 84
        $hash = $this->calculateSigHash($scriptCode, $sigHashType, $sigVersion);
298 84
        $ecSignature = $this->ecAdapter->sign($hash, $key, new Rfc6979($this->ecAdapter, $key, $hash, 'sha256'));
299 84
        return new TransactionSignature($this->ecAdapter, $ecSignature, $sigHashType);
300
    }
301
302
    /**
303
     * @return bool
304
     */
305 54
    public function isFullySigned()
306
    {
307 54
        return $this->requiredSigs !== 0 && $this->requiredSigs === count($this->signatures);
308
    }
309
310
    /**
311
     * The function only returns true when $scriptPubKey could be classified
312
     *
313
     * @param PrivateKeyInterface $key
314
     * @param OutputData $solution
315
     * @param int $sigHashType
316
     * @param int $sigVersion
317
     */
318 84
    private function doSignature(PrivateKeyInterface $key, OutputData $solution, $sigHashType, $sigVersion = 0)
319
    {
320 84
        if ($solution->getType() === OutputClassifier::PAYTOPUBKEY) {
321 12
            if (!$key->getPublicKey()->getBuffer()->equals($solution->getSolution())) {
322
                throw new \RuntimeException('Signing with the wrong private key');
323
            }
324 12
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
325 12
            $this->publicKeys[0] = $key->getPublicKey();
326 12
            $this->requiredSigs = 1;
327 76
        } else if ($solution->getType() === OutputClassifier::PAYTOPUBKEYHASH) {
328 42
            if (!$key->getPubKeyHash()->equals($solution->getSolution())) {
329
                throw new \RuntimeException('Signing with the wrong private key');
330
            }
331 42
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
332 42
            $this->publicKeys[0] = $key->getPublicKey();
333 42
            $this->requiredSigs = 1;
334 44
        } else if ($solution->getType() === OutputClassifier::MULTISIG) {
335 30
            $info = new Multisig($solution->getScript());
336 30
            $this->publicKeys = $info->getKeys();
337 30
            $this->requiredSigs = $info->getRequiredSigCount();
338
339 30
            $myKey = $key->getPublicKey()->getBuffer();
340 30
            $signed = false;
341 30
            foreach ($info->getKeys() as $keyIdx => $publicKey) {
342 30
                if ($publicKey->getBuffer()->equals($myKey)) {
0 ignored issues
show
Documentation introduced by
$myKey is of type object<BitWasp\Buffertools\BufferInterface>, but the function expects a object<self>.

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...
343 30
                    $this->signatures[$keyIdx] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
344 30
                    $signed = true;
345 10
                }
346 10
            }
347
348 30
            if (!$signed) {
349 20
                throw new \RuntimeException('Signing with the wrong private key');
350
            }
351 10
        } else {
352
            throw new \RuntimeException('Cannot sign unknown script type');
353
        }
354 84
    }
355
356
    /**
357
     * @param PrivateKeyInterface $key
358
     * @param int $sigHashType
359
     * @return bool
360
     */
361 84
    public function sign(PrivateKeyInterface $key, $sigHashType = SigHashInterface::ALL)
362
    {
363 84
        if ($this->scriptPubKey->canSign()) {
364 42
            $this->doSignature($key, $this->scriptPubKey, $sigHashType, 0);
365 42
            return true;
366
        }
367 42
        $solution = $this->scriptPubKey;
368 42
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
369 24
            if ($this->redeemScript->canSign()) {
370 12
                $this->doSignature($key, $this->redeemScript, $sigHashType, 0);
371 12
                return true;
372
            }
373 12
            $solution = $this->redeemScript;
374 4
        }
375
376 30
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
377 12
            $keyHashScript = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
378 12
            $this->doSignature($key, $this->classifier->decode($keyHashScript), $sigHashType, 1);
379 32
            return true;
380 18
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
381 18
            if ($this->witnessScript->canSign()) {
382 18
                $this->doSignature($key, $this->witnessScript, $sigHashType, 1);
383 18
                return true;
384
            }
385
        }
386
387
        return false;
388
    }
389
390
    /**
391
     * @param string $outputType
392
     * @return BufferInterface[]
393
     */
394 84
    private function serializeSolution($outputType)
395
    {
396 84
        if ($outputType === OutputClassifier::PAYTOPUBKEY) {
397 12
            return [$this->signatures[0]->getBuffer()];
398 72
        } else if ($outputType === OutputClassifier::PAYTOPUBKEYHASH) {
399 42
            return [$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()];
400 30
        } else if ($outputType === OutputClassifier::MULTISIG) {
401 30
            $sequence = [new Buffer()];
402 30
            for ($i = 0, $nPubKeys = count($this->publicKeys); $i < $nPubKeys; $i++) {
403 30
                if (isset($this->signatures[$i])) {
404 30
                    $sequence[] = $this->signatures[$i]->getBuffer();
405 10
                }
406 10
            }
407
408 30
            return $sequence;
409
        } else {
410
            throw new \RuntimeException('Cannot serialize this script sig');
411
        }
412
    }
413
414
    /**
415
     * @param ScriptInterface $script
416
     * @param int $flags
417
     * @return \BitWasp\Buffertools\BufferInterface[]
418
     */
419 66
    private function evalPushOnly(ScriptInterface $script, $flags = Interpreter::VERIFY_NONE)
420
    {
421 66
        $stack = new Stack();
422 66
        $interpreter = new Interpreter();
423 66
        $interpreter->evaluate($script, $stack, 0, $flags | Interpreter::VERIFY_SIGPUSHONLY, new Checker($this->ecAdapter, new Transaction(), 0, 0));
424 66
        return $stack->all();
425
    }
426
427
    /**
428
     * @param BufferInterface[] $buffers
429
     * @return ScriptInterface
430
     */
431
    public function pushAll(array $buffers)
432
    {
433 54
        return ScriptFactory::sequence(array_map(function ($buffer) {
434 54
            if (!($buffer instanceof BufferInterface)) {
435
                throw new \RuntimeException('Script contained a non-push opcode');
436
            }
437
438 54
            $size = $buffer->getSize();
439 54
            if ($size === 0) {
440 18
                return Opcodes::OP_0;
441
            }
442
443 54
            $first = ord($buffer->getBinary());
444 54
            if ($size === 1 && $first >= 1 && $first <= 16) {
445
                return \BitWasp\Bitcoin\Script\encodeOpN($first);
446
            } else {
447 54
                return $buffer;
448
            }
449 54
        }, $buffers));
450
    }
451
452
    /**
453
     * @return SigValues
454
     */
455 84
    public function serializeSignatures()
456
    {
457 84
        static $emptyScript = null;
458 84
        static $emptyWitness = null;
459 84
        if (is_null($emptyScript) || is_null($emptyWitness)) {
460 6
            $emptyScript = new Script();
461 6
            $emptyWitness = new ScriptWitness([]);
462 2
        }
463
464 84
        $scriptSig = $emptyScript;
465 84
        $witness = [];
466 84
        $solution = $this->scriptPubKey;
467 84
        if ($solution->canSign()) {
468 42
            $scriptSig = $this->pushAll($this->serializeSolution($this->scriptPubKey->getType()));
469 14
        }
470
471 84
        $p2sh = false;
472 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
473 24
            $p2sh = true;
474 24
            if ($this->redeemScript->canSign()) {
475 12
                $scriptSig = $this->pushAll($this->serializeSolution($this->redeemScript->getType()));
476 4
            }
477 24
            $solution = $this->redeemScript;
478 8
        }
479
480 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
481 12
            $scriptSig = $emptyScript;
482 12
            $witness = $this->serializeSolution(OutputClassifier::PAYTOPUBKEYHASH);
483 76
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
484 18
            if ($this->witnessScript->canSign()) {
485 18
                $scriptSig = $emptyScript;
486 18
                $witness = $this->serializeSolution($this->witnessScript->getType());
487 18
                $witness[] = $this->witnessScript->getScript()->getBuffer();
488 6
            }
489 6
        }
490
491 84
        if ($p2sh) {
492 24
            $scriptSig = ScriptFactory::create($scriptSig->getBuffer())->push($this->redeemScript->getScript()->getBuffer())->getScript();
493 8
        }
494
495 84
        return new SigValues($scriptSig, new ScriptWitness($witness));
496
    }
497
}
498