Completed
Pull Request — master (#346)
by thomas
58:32 queued 55:13
created

InputSigner   F

Complexity

Total Complexity 90

Size/Duplication

Total Lines 516
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 28

Test Coverage

Coverage 95.09%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 516
ccs 271
cts 285
cp 0.9509
rs 1.3043
wmc 90
lcom 1
cbo 28

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
B calculateSignature() 0 25 2
A isFullySigned() 0 4 2
C sign() 0 57 20
C serializeSignatures() 0 49 12
D extractFromValues() 0 39 10
C extractSignatures() 0 65 15
D doSignature() 0 85 14
D serializeSimpleSig() 0 31 9
B sortMultiSigs() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like InputSigner 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 InputSigner, and based on these observations, apply Extract Interface, too.

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 42
    public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $tx, $nInput, TransactionOutputInterface $txOut, $sigHashType = SigHashInterface::ALL)
95
    {
96 42
        $this->ecAdapter = $ecAdapter;
97 42
        $this->tx = $tx;
98 42
        $this->nInput = $nInput;
99 42
        $this->txOut = $txOut;
100 42
        $this->classifier = new OutputClassifier();
101 42
        $this->sigHashType = $sigHashType;
102 42
        $this->publicKeys = [];
103 42
        $this->signatures = [];
104
105 42
        $this->extractSignatures();
106 42
    }
107
108
    /**
109
     * @param int $sigVersion
110
     * @param $stack
111
     * @param ScriptInterface $scriptCode
112
     * @return \SplObjectStorage
113
     */
114 32
    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 14
        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 28
            }
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 36
    public function extractFromValues($type, ScriptInterface $scriptCode, array $stack, $sigVersion)
147
    {
148 36
        $size = count($stack);
149 36
        if ($type === OutputClassifier::PAYTOPUBKEYHASH) {
150 15
            $this->requiredSigs = 1;
151 15
            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 10
        }
156
157 36
        if ($type === OutputClassifier::PAYTOPUBKEY) {
158 6
            $this->requiredSigs = 1;
159 6
            if ($size === 1) {
160 3
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
161 2
            }
162 4
        }
163
164 36
        if ($type === OutputClassifier::MULTISIG) {
165 12
            $info = new Multisig($scriptCode);
166 12
            $this->requiredSigs = $info->getRequiredSigCount();
167 12
            $this->publicKeys = $info->getKeys();
168
169 12
            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 8
        }
182
183 36
        return $type;
184
    }
185
186
    /**
187
     * @return $this
188
     */
189 42
    public function extractSignatures()
190
    {
191 42
        $scriptPubKey = $this->txOut->getScript();
192 42
        $scriptSig = $this->tx->getInput($this->nInput)->getScript();
193 42
        $type = $this->classifier->classify($scriptPubKey);
194
195 42
        if ($type === OutputClassifier::PAYTOPUBKEYHASH || $type === OutputClassifier::PAYTOPUBKEY || $type === OutputClassifier::MULTISIG) {
196 21
            $values = [];
197 21
            foreach ($scriptSig->getScriptParser()->decode() as $o) {
198 9
                $values[] = $o->getData();
199 14
            }
200
201 21
            $this->extractFromValues($type, $scriptPubKey, $values, 0);
202 14
        }
203
204 42
        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 42
        $witnesses = $this->tx->getWitnesses();
227 42
        if ($type === OutputClassifier::WITNESS_V0_KEYHASH) {
228 6
            $this->requiredSigs = 1;
229 6
            if (isset($witnesses[$this->nInput])) {
230 6
                $witness = $witnesses[$this->nInput];
231 6
                $this->signatures = [TransactionSignatureFactory::fromHex($witness[0], $this->ecAdapter)];
232 6
                $this->publicKeys = [PublicKeyFactory::fromHex($witness[1], $this->ecAdapter)];
233 4
            }
234 41
        } else if ($type === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
235 9
            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 6
        }
251
252 42
        return $this;
253
    }
254
255
    /**
256
     * @param PrivateKeyInterface $key
257
     * @param ScriptInterface $scriptCode
258
     * @param int $sigVersion
259
     * @return TransactionSignature
260
     */
261 42
    public function calculateSignature(PrivateKeyInterface $key, ScriptInterface $scriptCode, $sigVersion)
262
    {
263 42
        if ($sigVersion == 1) {
264 15
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
265 10
        } else {
266 27
            $hasher = new Hasher($this->tx);
267
        }
268
269 42
        $hash = $hasher->calculate($scriptCode, $this->nInput, $this->sigHashType);
270
271 42
        return new TransactionSignature(
272 42
            $this->ecAdapter,
273 42
            $this->ecAdapter->sign(
274 28
                $hash,
275 28
                $key,
276 42
                new Rfc6979(
277 42
                    $this->ecAdapter,
278 28
                    $key,
279 28
                    $hash,
280 14
                    'sha256'
281 28
                )
282 28
            ),
283 42
            $this->sigHashType
284 28
        );
285
    }
286
287
    /**
288
     * @return bool
289
     */
290 39
    public function isFullySigned()
291
    {
292 39
        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 42
    private function doSignature(PrivateKeyInterface $key, ScriptInterface $scriptPubKey, &$outputType, array &$results, $sigVersion = 0)
306
    {
307 42
        $return = [];
308 42
        $outputType = $this->classifier->classify($scriptPubKey, $return);
309 42
        if ($outputType === OutputClassifier::UNKNOWN) {
310
            throw new \RuntimeException('Cannot sign unknown script type');
311
        }
312
313 42
        if ($outputType === OutputClassifier::PAYTOPUBKEY) {
314 6
            $publicKeyBuffer = $return;
315 6
            $results[] = $publicKeyBuffer;
316 6
            $this->requiredSigs = 1;
317 6
            $publicKey = PublicKeyFactory::fromHex($publicKeyBuffer);
318
319 6
            if ($publicKey->getBinary() === $key->getPublicKey()->getBinary()) {
320 6
                $this->signatures[0] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
321 4
            }
322
323 6
            return true;
324
        }
325
326 36
        if ($outputType === OutputClassifier::PAYTOPUBKEYHASH) {
327
            /** @var BufferInterface $pubKeyHash */
328 21
            $pubKeyHash = $return;
329 21
            $results[] = $pubKeyHash;
330 21
            $this->requiredSigs = 1;
331 21
            if ($pubKeyHash->getBinary() === $key->getPublicKey()->getPubKeyHash()->getBinary()) {
332 18
                $this->signatures[0] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
333 18
                $this->publicKeys[0] = $key->getPublicKey();
334 12
            }
335
336 21
            return true;
337
        }
338
339 24
        if ($outputType === OutputClassifier::MULTISIG) {
340 15
            $info = new Multisig($scriptPubKey);
341
342 15
            foreach ($info->getKeys() as $publicKey) {
343 15
                $results[] = $publicKey->getBuffer();
344 10
            }
345
346 15
            $this->publicKeys = $info->getKeys();
347 15
            $this->requiredSigs = $info->getKeyCount();
348
349 15
            foreach ($this->publicKeys as $keyIdx => $publicKey) {
350 15
                if ($publicKey->getBinary() == $key->getPublicKey()->getBinary()) {
351 15
                    $this->signatures[$keyIdx] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
352 10
                }
353 10
            }
354
355 15
            return true;
356
        }
357
358 21
        if ($outputType === OutputClassifier::PAYTOSCRIPTHASH) {
359
            /** @var BufferInterface $scriptHash */
360 12
            $scriptHash = $return;
361 12
            $results[] = $scriptHash;
362 12
            return true;
363
        }
364
365 15
        if ($outputType === OutputClassifier::WITNESS_V0_KEYHASH) {
366
            /** @var BufferInterface $pubKeyHash */
367 6
            $pubKeyHash = $return;
368 6
            $results[] = $pubKeyHash;
369 6
            $this->requiredSigs = 1;
370
371 6
            if ($pubKeyHash->getBinary() === $key->getPublicKey()->getPubKeyHash()->getBinary()) {
372 6
                $script = ScriptFactory::sequence([Opcodes::OP_DUP, Opcodes::OP_HASH160, $pubKeyHash, Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG]);
373 6
                $this->signatures[0] = $this->calculateSignature($key, $script, 1);
374 6
                $this->publicKeys[0] = $key->getPublicKey();
375 4
            }
376
377 6
            return true;
378
        }
379
380 9
        if ($outputType === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
381
            /** @var BufferInterface $scriptHash */
382 9
            $scriptHash = $return;
383 9
            $results[] = $scriptHash;
384
385 9
            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 42
    public function sign(PrivateKeyInterface $key, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null)
398
    {
399
        /** @var BufferInterface[] $return */
400 42
        $type = null;
401 42
        $return = [];
402 42
        $solved = $this->doSignature($key, $this->txOut->getScript(), $type, $return, 0);
403
404 42
        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 42
        if ($solved && $type === OutputClassifier::WITNESS_V0_KEYHASH) {
423 6
            $pubKeyHash = $return[0];
424 6
            $witnessScript = ScriptFactory::sequence([Opcodes::OP_DUP, Opcodes::OP_HASH160, $pubKeyHash, Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG]);
425 6
            $subType = null;
426 6
            $subResults = [];
427 6
            $solved = $solved && $this->doSignature($key, $witnessScript, $subType, $subResults, 1);
428 40
        } else if ($solved && $type === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
429 9
            $scriptHash = $return[0];
430
431 9
            if (!$witnessScript instanceof ScriptInterface) {
432
                throw new \InvalidArgumentException('Must provide witness script for witness v0 scripthash');
433
            }
434
435 9
            if (!Hash::sha256($witnessScript->getBuffer())->getBinary() === $scriptHash->getBinary()) {
436
                throw new \InvalidArgumentException("Incorrect witness script - hash doesn't match");
437
            }
438
439 9
            $subType = null;
440 9
            $subResults = [];
441
442 9
            $solved = $solved && $this->doSignature($key, $witnessScript, $subType, $subResults, 1)
443 9
                && $subType !== OutputClassifier::PAYTOSCRIPTHASH
444 9
                && $subType !== OutputClassifier::WITNESS_V0_SCRIPTHASH
445 9
                && $subType !== OutputClassifier::WITNESS_V0_KEYHASH;
446
447 9
            if ($solved) {
448 9
                $this->witnessScript = $witnessScript;
449 6
            }
450 6
        }
451
452 42
        return $solved;
453
    }
454
455
    /**
456
     * @param string $outputType
457
     * @param $answer
458
     * @return bool
459
     */
460 42
    private function serializeSimpleSig($outputType, &$answer)
461
    {
462 42
        if ($outputType === OutputClassifier::UNKNOWN) {
463
            throw new \RuntimeException('Cannot sign unknown script type');
464
        }
465
466 42
        if ($outputType === OutputClassifier::PAYTOPUBKEY && $this->isFullySigned()) {
467 6
            $answer = new SigValues(ScriptFactory::sequence([$this->signatures[0]->getBuffer()]), new ScriptWitness([]));
468 6
            return true;
469
        }
470
471 36
        if ($outputType === OutputClassifier::PAYTOPUBKEYHASH && $this->isFullySigned()) {
472 15
            $answer = new SigValues(ScriptFactory::sequence([$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()]), new ScriptWitness([]));
473 15
            return true;
474
        }
475
476 24
        if ($outputType === OutputClassifier::MULTISIG) {
477 15
            $sequence = [Opcodes::OP_0];
478 15
            $nPubKeys = count($this->publicKeys);
479 15
            for ($i = 0; $i < $nPubKeys; $i++) {
480 15
                if (isset($this->signatures[$i])) {
481 15
                    $sequence[] = $this->signatures[$i]->getBuffer();
482 10
                }
483 10
            }
484
485 15
            $answer = new SigValues(ScriptFactory::sequence($sequence), new ScriptWitness([]));
486 15
            return true;
487
        }
488
489 21
        return false;
490
    }
491
492
    /**
493
     * @return SigValues
494
     */
495 42
    public function serializeSignatures()
496
    {
497 42
        static $emptyScript = null;
498 42
        static $emptyWitness = null;
499 42
        if (is_null($emptyScript) || is_null($emptyWitness)) {
500 3
            $emptyScript = new Script();
501 3
            $emptyWitness = new ScriptWitness([]);
502 2
        }
503
504
        /** @var BufferInterface[] $return */
505 42
        $outputType = $this->classifier->classify($this->txOut->getScript());
506
507
        /** @var SigValues $answer */
508 42
        $answer = new SigValues($emptyScript, $emptyWitness);
509 42
        $serialized = $this->serializeSimpleSig($outputType, $answer);
510
511 42
        $p2sh = false;
512 42
        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 42
        if (!$serialized && $outputType === OutputClassifier::WITNESS_V0_KEYHASH) {
519 6
            $answer = new SigValues($emptyScript, new ScriptWitness([$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()]));
520 40
        } else if (!$serialized && $outputType === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
521 9
            $outputType = $this->classifier->classify($this->witnessScript);
522 9
            $serialized = $this->serializeSimpleSig($outputType, $answer);
523
524 9
            if ($serialized) {
525 9
                $data = [];
526 9
                foreach ($answer->getScriptSig()->getScriptParser()->decode() as $o) {
527 9
                    $data[] = $o->getData();
528 6
                }
529
530 9
                $data[] = $this->witnessScript->getBuffer();
531 9
                $answer = new SigValues($emptyScript, new ScriptWitness($data));
532 6
            }
533 6
        }
534
535 42
        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 42
        return $answer;
543
    }
544
}
545