Completed
Pull Request — master (#386)
by thomas
71:11
created

InputSigner::serializeSignatures()   D

Complexity

Conditions 10
Paths 96

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10.0064

Importance

Changes 0
Metric Value
cc 10
eloc 28
c 0
b 0
f 0
nc 96
nop 0
dl 0
loc 42
ccs 24
cts 25
cp 0.96
crap 10.0064
rs 4.8196

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
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\Opcodes;
14
use BitWasp\Bitcoin\Script\Parser\Operation;
15
use BitWasp\Bitcoin\Script\Script;
16
use BitWasp\Bitcoin\Script\ScriptFactory;
17
use BitWasp\Bitcoin\Script\ScriptInfo\Multisig;
18
use BitWasp\Bitcoin\Script\ScriptInterface;
19
use BitWasp\Bitcoin\Script\ScriptWitness;
20
use BitWasp\Bitcoin\Signature\SignatureSort;
21
use BitWasp\Bitcoin\Signature\TransactionSignature;
22
use BitWasp\Bitcoin\Signature\TransactionSignatureFactory;
23
use BitWasp\Bitcoin\Signature\TransactionSignatureInterface;
24
use BitWasp\Bitcoin\Transaction\SignatureHash\Hasher;
25
use BitWasp\Bitcoin\Transaction\SignatureHash\SigHashInterface;
26
use BitWasp\Bitcoin\Transaction\SignatureHash\V1Hasher;
27
use BitWasp\Bitcoin\Transaction\TransactionInterface;
28
use BitWasp\Bitcoin\Transaction\TransactionOutputInterface;
29
use BitWasp\Buffertools\Buffer;
30
use BitWasp\Buffertools\BufferInterface;
31
32
class InputSigner
33
{
34
    /**
35
     * @var EcAdapterInterface
36
     */
37
    private $ecAdapter;
38
39
    /**
40
     * @var OutputData $scriptPubKey
41
     */
42
    private $scriptPubKey;
43
44
    /**
45
     * @var OutputData $redeemScript
46
     */
47
    private $redeemScript;
48
49
    /**
50
     * @var OutputData $witnessScript
51
     */
52
    private $witnessScript;
53
54
    /**
55
     * @var TransactionInterface
56
     */
57
    private $tx;
58
59
    /**
60
     * @var int
61
     */
62
    private $nInput;
63
64
    /**
65
     * @var TransactionOutputInterface
66
     */
67
    private $txOut;
68
69
    /**
70
     * @var PublicKeyInterface[]
71
     */
72
    private $publicKeys = [];
73
74
    /**
75
     * @var TransactionSignatureInterface[]
76
     */
77
    private $signatures = [];
78
79
    /**
80
     * @var int
81
     */
82
    private $requiredSigs = 0;
83
84
    /**
85
     * @var OutputClassifier
86
     */
87
    private $classifier;
88 84
89
    /**
90 84
     * TxInputSigning constructor.
91 84
     * @param EcAdapterInterface $ecAdapter
92 84
     * @param TransactionInterface $tx
93 84
     * @param int $nInput
94 84
     * @param TransactionOutputInterface $txOut
95 84
     * @param SignData $signData
96 84
     */
97
    public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $tx, $nInput, TransactionOutputInterface $txOut, SignData $signData)
98 84
    {
99 84
        $this->ecAdapter = $ecAdapter;
100
        $this->tx = $tx;
101
        $this->nInput = $nInput;
102
        $this->txOut = $txOut;
103
        $this->classifier = new OutputClassifier();
104
        $this->publicKeys = [];
105
        $this->signatures = [];
106
107 24
        $this->solve($signData);
108
        $this->extractSignatures();
109 24
    }
110 12
111 4
    /**
112 12
     * @param int $sigVersion
113
     * @param TransactionSignatureInterface[] $stack
114
     * @param ScriptInterface $scriptCode
115 24
     * @return \SplObjectStorage
116 24
     */
117
    private function sortMultiSigs($sigVersion, $stack, ScriptInterface $scriptCode)
118 24
    {
119
        if ($sigVersion === 1) {
120
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
121
        } else {
122
            $hasher = new Hasher($this->tx);
123
        }
124
125
        $sigSort = new SignatureSort($this->ecAdapter);
126
        $sigs = new \SplObjectStorage;
127 8
128
        foreach ($stack as $txSig) {
129 24
            $hash = $hasher->calculate($scriptCode, $this->nInput, $txSig->getHashType());
130
            $linked = $sigSort->link([$txSig->getSignature()], $this->publicKeys, $hash);
131
132
            foreach ($this->publicKeys as $key) {
133
                if ($linked->contains($key)) {
134
                    $sigs[$key] = $txSig;
135
                }
136
            }
137
        }
138
139 72
        return $sigs;
140
    }
141 72
142 72
    /**
143 30
     * @param string $type
144 30
     * @param ScriptInterface $scriptCode
145 12
     * @param BufferInterface[] $stack
146 12
     * @param int $sigVersion
147 4
     * @return string
148 10
     */
149
    public function extractFromValues($type, ScriptInterface $scriptCode, array $stack, $sigVersion)
150 72
    {
151 12
        $size = count($stack);
152 12
        if ($type === OutputClassifier::PAYTOPUBKEYHASH) {
153 6
            $this->requiredSigs = 1;
154 2
            if ($size === 2) {
155 4
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
156
                $this->publicKeys = [PublicKeyFactory::fromHex($stack[1], $this->ecAdapter)];
157 72
            }
158 24
        }
159 24
160 24
        if ($type === OutputClassifier::PAYTOPUBKEY) {
161
            $this->requiredSigs = 1;
162 24
            if ($size === 1) {
163 24
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
164 24
            }
165
        }
166 8
167
        if ($type === OutputClassifier::MULTISIG) {
168 24
            $info = new Multisig($scriptCode);
169
            $this->requiredSigs = $info->getRequiredSigCount();
170 24
            $this->publicKeys = $info->getKeys();
171 24
172 8
            if ($size > 1) {
173 8
                $vars = [];
174 8
                for ($i = 1, $j = $size - 1; $i < $j; $i++) {
175
                    $vars[] = TransactionSignatureFactory::fromHex($stack[$i], $this->ecAdapter);
176 72
                }
177
178
                $sigs = $this->sortMultiSigs($sigVersion, $vars, $scriptCode);
179
                foreach ($this->publicKeys as $idx => $key) {
180
                    $this->signatures[$idx] = isset($sigs[$key]) ? $sigs[$key]->getBuffer() : null;
181
                }
182 84
            }
183
        }
184 84
185 84
        return $type;
186 84
    }
187 84
188 42
    /**
189 42
     * @param SignData $signData
190 18
     * @return $this
191 14
     * @throws \Exception
192
     */
193 42
    private function solve(SignData $signData)
194 26
    {
195
        $scriptPubKey = $this->txOut->getScript();
196 84
        $solution = $this->scriptPubKey = $this->classifier->decode($scriptPubKey);
197 24
        if ($solution->getType() === OutputClassifier::UNKNOWN) {
198 24
            throw new \RuntimeException('scriptPubKey type is unknown');
199 18
        }
200 18
201 18
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
202 6
            $redeemScript = $signData->getRedeemScript();
203 2
            if (!$solution->getSolution()->equals(Hash::sha256ripe160($redeemScript->getBuffer()))) {
204
                throw new \Exception('Redeem script doesn\'t match script-hash');
205 18
            }
206 18
            $solution = $this->redeemScript = $this->classifier->decode($redeemScript);
207 18
            if (!in_array($solution->getType(), [OutputClassifier::WITNESS_V0_SCRIPTHASH, OutputClassifier::WITNESS_V0_KEYHASH, OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
208 6
                throw new \Exception('Unsupported pay-to-script-hash script');
209
            }
210 18
        }
211 18
        // WitnessKeyHash doesn't require further solving until signing
212 6
        if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
213 8
            $witnessScript = $signData->getWitnessScript();
214
            if (!$solution->getSolution()->equals(Hash::sha256($witnessScript->getBuffer()))) {
215 84
                throw new \Exception('Witness script doesn\'t match witness-script-hash');
216 84
            }
217 12
            $solution = $this->witnessScript = $this->classifier->decode($witnessScript);
218 12
            if (!in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
219 12
                throw new \Exception('Unsupported witness-script-hash script');
220 12
            }
221 12
        }
222 4
223 80
        return $this;
224 18
    }
225 18
226 18
    /**
227 18
     * @return $this
228 18
     */
229 18
    public function extractSignatures()
230 18
    {
231 18
        $solution = $this->scriptPubKey;
232 6
        $scriptSig = $this->tx->getInput($this->nInput)->getScript();
233
        if (in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
234 18
            $stack = [];
235 18
            foreach ($scriptSig->getScriptParser()->decode() as $op) {
236 18
                $stack[] = $op->getData();
237 6
            }
238 6
            $this->extractFromValues($solution->getType(), $solution->getScript(), $stack, 0);
239 6
        }
240
241 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
242
            $decodeSig = $scriptSig->getScriptParser()->decode();
243
            if (count($decodeSig) > 0) {
244
                $redeemScript = new Script(end($decodeSig)->getData());
245
                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...
246
                    throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
247
                }
248
249
                $internalSig = [];
250 84
                foreach (array_slice($decodeSig, 0, -1) as $operation) {
251
                    /** @var Operation $operation */
252 84
                    $internalSig[] = $operation->getData();
253 30
                }
254 10
255 54
                $solution = $this->redeemScript;
256
                $this->extractFromValues($solution->getType(), $solution->getScript(), $internalSig, 0);
257
            }
258 84
        }
259
260
        $witnesses = $this->tx->getWitnesses();
261
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
262
            $wit = isset($witnesses[$this->nInput]) ? $witnesses[$this->nInput]->all() : [];
263
            $keyHashCode = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
264
            $this->extractFromValues(OutputClassifier::PAYTOPUBKEYHASH, $keyHashCode, $wit, 1);
265
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
266
            if (isset($witnesses[$this->nInput])) {
267
                $witness = $witnesses[$this->nInput];
268 84
                $witCount = count($witnesses[$this->nInput]);
269
                if ($witCount > 0) {
270 84
                    if (!$witness[$witCount - 1]->equals($this->witnessScript->getScript()->getBuffer())) {
271 84
                        throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
272 84
                    }
273 84
274 28
                    $solution = $this->witnessScript;
275 28
                    $this->extractFromValues($solution->getType(), $solution->getScript(), array_slice($witness->all(), 0, -1), 1);
276 84
                }
277 84
            }
278 28
        }
279 28
280 56
        return $this;
281 28
    }
282 28
283
    /**
284 28
     * @param ScriptInterface $scriptCode
285
     * @param int $sigHashType
286
     * @param int $sigVersion
287
     * @return BufferInterface
288
     */
289
    public function calculateSigHash(ScriptInterface $scriptCode, $sigHashType, $sigVersion)
290 78
    {
291
        if ($sigVersion === 1) {
292 78
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
293
        } else {
294
            $hasher = new Hasher($this->tx);
295
        }
296
297
        return $hasher->calculate($scriptCode, $this->nInput, $sigHashType);
298
    }
299
300
    /**
301
     * @param PrivateKeyInterface $key
302
     * @param ScriptInterface $scriptCode
303
     * @param int $sigHashType
304
     * @param int $sigVersion
305
     * @return TransactionSignature
306 84
     */
307
    public function calculateSignature(PrivateKeyInterface $key, ScriptInterface $scriptCode, $sigHashType, $sigVersion)
308 84
    {
309 84
        $hash = $this->calculateSigHash($scriptCode, $sigHashType, $sigVersion);
310 84
        $ecSignature = $this->ecAdapter->sign($hash, $key, new Rfc6979($this->ecAdapter, $key, $hash, 'sha256'));
311
        return new TransactionSignature($this->ecAdapter, $ecSignature, $sigHashType);
312
    }
313
314 84
    /**
315
     * @return bool
316 12
     */
317 12
    public function isFullySigned()
318 12
    {
319 12
        return $this->requiredSigs !== 0 && $this->requiredSigs === count($this->signatures);
320 4
    }
321
322 12
    /**
323
     * The function only returns true when $scriptPubKey could be classified
324
     *
325 72
     * @param PrivateKeyInterface $key
326
     * @param OutputData $solution
327 42
     * @param int $sigHashType
328 42
     * @param int $sigVersion
329 42
     */
330 36
    private function doSignature(PrivateKeyInterface $key, OutputData $solution, $sigHashType, $sigVersion = 0)
331 36
    {
332 12
        if ($solution->getType() === OutputClassifier::PAYTOPUBKEY) {
333
            if (!$key->getPublicKey()->getBuffer()->equals($solution->getSolution())) {
334 42
                throw new \RuntimeException('Signing with the wrong private key');
335
            }
336
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
337 48
            $this->publicKeys[0] = $key->getPublicKey();
338 30
            $this->requiredSigs = 1;
339 30
        } else if ($solution->getType() === OutputClassifier::PAYTOPUBKEYHASH) {
340 30
            if (!$key->getPubKeyHash()->equals($solution->getSolution())) {
341
                throw new \RuntimeException('Signing with the wrong private key');
342 30
            }
343 30
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
344 30
            $this->publicKeys[0] = $key->getPublicKey();
345 30
            $this->requiredSigs = 1;
346 30
        } else if ($solution->getType() === OutputClassifier::MULTISIG) {
347 10
            $info = new Multisig($solution->getScript());
348 10
            $this->publicKeys = $info->getKeys();
349
            $this->requiredSigs = $info->getKeyCount();
350 30
351
            $myKey = $key->getPublicKey()->getBuffer();
352
            $signed = false;
353 42
            foreach ($info->getKeys() as $keyIdx => $publicKey) {
354
                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...
355 24
                    $this->signatures[$keyIdx] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
356 24
                    $signed = true;
357 24
                }
358
            }
359
360 30
            if (!$signed) {
361
                throw new \RuntimeException('Signing with the wrong private key');
362 12
            }
363 12
        } else {
364 12
            throw new \RuntimeException('Cannot sign unknown script type');
365
        }
366 12
    }
367 12
368 12
    /**
369 12
     * @param PrivateKeyInterface $key
370 4
     * @param int $sigHashType
371
     * @return bool
372 12
     */
373
    public function sign(PrivateKeyInterface $key, $sigHashType = SigHashInterface::ALL)
374
    {
375 18
        if ($this->scriptPubKey->canSign()) {
376
            $this->doSignature($key, $this->scriptPubKey, $sigHashType, 0);
377 18
            return true;
378 18
        }
379
        $solution = $this->scriptPubKey;
380 18
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
381
            if ($this->redeemScript->canSign()) {
382
                $this->doSignature($key, $this->redeemScript, $sigHashType, 0);
383
                return true;
384
            }
385
            $solution = $this->redeemScript;
386
        }
387
388
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
389
            $keyHashScript = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
390
            $this->doSignature($key, $this->classifier->decode($keyHashScript), $sigHashType, 1);
391
            return true;
392
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
393 84
            if ($this->witnessScript->canSign()) {
394
                $this->doSignature($key, $this->witnessScript, $sigHashType, 1);
395
                return true;
396 84
            }
397 84
        }
398 84
399
        return false;
400 84
    }
401 24
402 24
    /**
403
     * @param string $outputType
404
     * @return BufferInterface[]
405
     */
406 24
    private function serializeSolution($outputType)
407
    {
408
        if ($outputType === OutputClassifier::PAYTOPUBKEY ) {
409
            return [$this->signatures[0]->getBuffer()];
410 24
        } else if ($outputType === OutputClassifier::PAYTOPUBKEYHASH ) {
411 24
            return [$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()];
412 24
        } else if ($outputType === OutputClassifier::MULTISIG) {
413 24
            $sequence = [new Buffer()];
414 8
            for ($i = 0, $nPubKeys = count($this->publicKeys); $i < $nPubKeys; $i++) {
415 8
                if (isset($this->signatures[$i])) {
416
                    $sequence[] = $this->signatures[$i]->getBuffer();
417 84
                }
418 12
            }
419 12
420 12
            return $sequence;
421 12
        } else {
422 12
            throw new \RuntimeException('Cannot serialize this script sig');
423 76
        }
424 18
    }
425
426 18
    /**
427
     * @param BufferInterface[] $buffers
428
     * @return ScriptInterface
429
     */
430 18
    public function pushAll(array $buffers)
431
    {
432
        return ScriptFactory::sequence(array_map(function ($buffer) {
433
            if (!($buffer instanceof BufferInterface)) {
434 18
                throw new \RuntimeException('Script contained a non-push opcode');
435 18
            }
436
437 18
            $size = $buffer->getSize();
438 18
            if ($size === 0) {
439 18
                return Opcodes::OP_0;
440 18
            }
441
442 18
            $first = ord($buffer->getBinary());
443 18
            if ($size === 1 && $first >= 1 && $first <= 16) {
444 6
                return \BitWasp\Bitcoin\Script\encodeOpN($first);
445 6
            } else {
446
                return $buffer;
447 84
            }
448
        }, $buffers));
449
    }
450
451
    /**
452
     * @return SigValues
453
     */
454
    public function serializeSignatures()
455 84
    {
456
        static $emptyScript = null;
457 84
        static $emptyWitness = null;
458
        if (is_null($emptyScript) || is_null($emptyWitness)) {
459
            $emptyScript = new Script();
460
            $emptyWitness = new ScriptWitness([]);
461 84
        }
462 12
463 12
        $scriptSig = $emptyScript;
464
        $witness = [];
465
        $solution = $this->scriptPubKey;
466 72
        if ($solution->canSign()) {
467 30
            $scriptSig = $this->pushAll($this->serializeSolution($this->scriptPubKey->getType()));
468 30
        }
469
470
        $p2sh = false;
471 48
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
472 30
            $p2sh = true;
473 30
            if ($this->redeemScript->canSign()) {
474 30
                $scriptSig = $this->pushAll($this->serializeSolution($this->redeemScript->getType()));
475 30
            }
476 30
            $solution = $this->redeemScript;
477 10
        }
478 10
479
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
480 30
            $scriptSig = $emptyScript;
481 30
            $witness = $this->serializeSolution(OutputClassifier::PAYTOPUBKEYHASH);
482
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
483
            if ($this->witnessScript->canSign()) {
484 42
                $scriptSig = $emptyScript;
485
                $witness = $this->serializeSolution($this->witnessScript->getType());
486
                $witness[] = $this->witnessScript->getScript()->getBuffer();
487
            }
488
        }
489
490 84
        if ($p2sh) {
491
            $scriptSig = ScriptFactory::create($scriptSig->getBuffer())->push($this->redeemScript->getScript()->getBuffer())->getScript();
492 84
        }
493 84
494 84
        return new SigValues($scriptSig, new ScriptWitness($witness));
495 6
    }
496
}
497