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

InputSigner::extractFromValues()   D

Complexity

Conditions 10
Paths 27

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 10.0033

Importance

Changes 0
Metric Value
cc 10
eloc 23
c 0
b 0
f 0
nc 27
nop 4
dl 0
loc 39
ccs 30
cts 31
cp 0.9677
crap 10.0033
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\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