Completed
Pull Request — master (#332)
by thomas
38:04
created

InputSigner::serializeSignatures()   C

Complexity

Conditions 12
Paths 32

Size

Total Lines 49
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 12.0768

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 30
c 2
b 0
f 0
nc 32
nop 0
dl 0
loc 49
ccs 34
cts 37
cp 0.9189
crap 12.0768
rs 5.1474

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