Completed
Pull Request — master (#256)
by thomas
24:35
created

InputSigner::sortMultiSigs()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.7458

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
ccs 10
cts 17
cp 0.5881
rs 8.5125
cc 5
eloc 14
nc 8
nop 3
crap 6.7458
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\SigHash;
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
     * TxInputSigning constructor.
83
     * @param EcAdapterInterface $ecAdapter
84
     * @param TransactionInterface $tx
85
     * @param int $nInput
86
     * @param TransactionOutputInterface $txOut
87
     * @param int $sigHashType
88
     */
89 54
    public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $tx, $nInput, TransactionOutputInterface $txOut, $sigHashType = SigHash::ALL)
90
    {
91 54
        $this->ecAdapter = $ecAdapter;
92 54
        $this->tx = $tx;
93 54
        $this->nInput = $nInput;
94 54
        $this->txOut = $txOut;
95 54
        $this->sigHashType = $sigHashType;
96 54
        $this->publicKeys = [];
97 54
        $this->signatures = [];
98
99 54
        $this->extractSignatures();
100 54
    }
101
102
    /**
103
     * @param int $sigVersion
104
     * @param $stack
105
     * @param ScriptInterface $scriptCode
106
     * @return \SplObjectStorage
107
     */
108 24
    private function sortMultiSigs($sigVersion, $stack, ScriptInterface $scriptCode)
109
    {
110 24
        if ($sigVersion === 1) {
111 12
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
112 12
        } else {
113 12
            $hasher = new Hasher($this->tx);
114
        }
115
116 24
        $sigSort = new SignatureSort($this->ecAdapter);
117 24
        $sigs = new \SplObjectStorage;
118
119 24
        foreach ($stack as $txSig) {
120
            $hash = $hasher->calculate($scriptCode, $this->nInput, $txSig->getHashType());
121
            $linked = $sigSort->link([$txSig->getSignature()], $this->publicKeys, $hash);
122
123
            foreach ($this->publicKeys as $key) {
124
                if ($linked->contains($key)) {
125
                    $sigs[$key] = $txSig;
126
                }
127
            }
128 24
        }
129
130 24
        return $sigs;
131
    }
132
133
    /**
134
     * @param string $type
135
     * @param ScriptInterface $scriptCode
136
     * @param BufferInterface[] $stack
137
     * @param int $sigVersion
138
     * @return mixed
139
     */
140 54
    public function extractFromValues($type, ScriptInterface $scriptCode, array $stack, $sigVersion)
141
    {
142 48
        $size = count($stack);
143 48
        if ($type === OutputClassifier::PAYTOPUBKEYHASH) {
144 12
            $this->requiredSigs = 1;
145 12
            if ($size === 2) {
146 12
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
147 12
                $this->publicKeys = [PublicKeyFactory::fromHex($stack[1], $this->ecAdapter)];
148 12
            }
149 12
        }
150
151 48
        if ($type === OutputClassifier::PAYTOPUBKEY) {
152 54
            $this->requiredSigs = 1;
153 6
            if ($size === 1) {
154 6
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
155 6
            }
156 6
        }
157
158 48
        if ($type === OutputClassifier::MULTISIG) {
159 24
            $info = new Multisig($scriptCode);
160 24
            $this->requiredSigs = $info->getRequiredSigCount();
161 24
            $this->publicKeys = $info->getKeys();
162
163 24
            if ($size > 1) {
164 24
                $vars = [];
165 24
                foreach (array_slice($stack, 1, -1) as $sig) {
166
                    $vars[] = TransactionSignatureFactory::fromHex($sig, $this->ecAdapter);
167 24
                }
168
169 24
                $sigs = $this->sortMultiSigs($sigVersion, $vars, $scriptCode);
170
171 24
                foreach ($this->publicKeys as $idx => $key) {
172 24
                    $this->signatures[$idx] = isset($sigs[$key]) ? $sigs[$key]->getBuffer() : null;
173 24
                }
174 24
            }
175 24
        }
176
177 48
        return $type;
178
    }
179
180
    /**
181
     * @return $this
182
     */
183 54
    public function extractSignatures()
184
    {
185 54
        $type = (new OutputClassifier($this->txOut->getScript()))->classify();
186 54
        $scriptPubKey = $this->txOut->getScript();
187 54
        $scriptSig = $this->tx->getInput($this->nInput)->getScript();
188
189 54
        if ($type === OutputClassifier::PAYTOPUBKEYHASH || $type === OutputClassifier::PAYTOPUBKEY || $type === OutputClassifier::MULTISIG) {
190 18
            $values = [];
191 18
            foreach ($scriptSig->getScriptParser()->decode() as $o) {
192 18
                $values[] = $o->getData();
193 18
            }
194
195 18
            $this->extractFromValues($type, $scriptPubKey, $values, 0);
196 18
        }
197
198 54
        if ($type === OutputClassifier::PAYTOSCRIPTHASH) {
199 18
            $decodeSig = $scriptSig->getScriptParser()->decode();
200 18
            if (count($decodeSig) > 0) {
201 18
                $redeemScript = new Script(end($decodeSig)->getData());
202 18
                $p2shType = (new OutputClassifier($redeemScript))->classify();
203
204 18
                if (count($decodeSig) > 1) {
205 6
                    $decodeSig = array_slice($decodeSig, 0, -1);
206 6
                }
207
208 18
                $internalSig = [];
209 18
                foreach ($decodeSig as $operation) {
210 18
                    $internalSig[] = $operation->getData();
211 18
                }
212
213 18
                $this->redeemScript = $redeemScript;
214 18
                $this->extractFromValues($p2shType, $redeemScript, $internalSig, 0);
215
216 18
                $type = $p2shType;
217 18
            }
218 18
        }
219
220 54
        $witnesses = $this->tx->getWitnesses();
221 54
        if ($type === OutputClassifier::WITNESS_V0_KEYHASH) {
222 12
            $this->requiredSigs = 1;
223 12
            if (isset($witnesses[$this->nInput])) {
224 12
                $witness = $witnesses[$this->nInput];
225 12
                $this->signatures = [TransactionSignatureFactory::fromHex($witness[0], $this->ecAdapter)];
226 12
                $this->publicKeys = [PublicKeyFactory::fromHex($witness[1], $this->ecAdapter)];
227 12
            }
228
229 54
        } else if ($type === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
230 18
            if (isset($witnesses[$this->nInput])) {
231 18
                $witness = $witnesses[$this->nInput];
232 18
                $witCount = count($witnesses[$this->nInput]);
233 18
                if ($witCount > 0) {
234 18
                    $witnessScript = new Script($witness[$witCount - 1]);
235 18
                    $vWitness = $witness->all();
236 18
                    if (count($vWitness) > 1) {
237 18
                        $vWitness = array_slice($witness->all(), 0, -1);
238 18
                    }
239
240 18
                    $witnessType = (new OutputClassifier($witnessScript))->classify();
241 18
                    $this->extractFromValues($witnessType, $witnessScript, $vWitness, 1);
242 18
                    $this->witnessScript = $witnessScript;
243 18
                }
244 18
            }
245 18
        }
246
247 54
        return $this;
248
    }
249
250
    /**
251
     * @param PrivateKeyInterface $key
252
     * @param ScriptInterface $scriptCode
253
     * @param int $sigVersion
254
     * @return TransactionSignature
255
     */
256 54
    public function calculateSignature(PrivateKeyInterface $key, ScriptInterface $scriptCode, $sigVersion)
257
    {
258 54
        if ($sigVersion == 1) {
259 30
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
260 30
        } else {
261 24
            $hasher = new Hasher($this->tx);
262
        }
263
264 54
        $hash = $hasher->calculate($scriptCode, $this->nInput, $this->sigHashType);
265
266 54
        return new TransactionSignature(
267 54
            $this->ecAdapter,
268 54
            $this->ecAdapter->sign(
269 54
                $hash,
270 54
                $key,
271 54
                new Rfc6979(
272 54
                    $this->ecAdapter,
273 54
                    $key,
274 54
                    $hash,
275
                    'sha256'
276 54
                )
277 54
            ),
278 54
            $this->sigHashType
279 54
        );
280
    }
281
282
    /**
283
     * @return int
284
     */
285 54
    public function isFullySigned()
286
    {
287 54
        return $this->requiredSigs !== 0 && $this->requiredSigs === count($this->signatures);
288
    }
289
290
    /**
291
     * The function only returns true when $scriptPubKey could be classified
292
     *
293
     * @param PrivateKeyInterface $key
294
     * @param ScriptInterface $scriptPubKey
295
     * @param int $outputType
296
     * @param BufferInterface[] $results
297
     * @param int $sigVersion
298
     * @return bool
299
     */
300 54
    private function doSignature(PrivateKeyInterface $key, ScriptInterface $scriptPubKey, &$outputType, array &$results, $sigVersion = 0)
301
    {
302 54
        $return = [];
303 54
        $outputType = (new OutputClassifier($scriptPubKey))->classify($return);
304 54
        if ($outputType === OutputClassifier::UNKNOWN) {
305
            throw new \RuntimeException('Cannot sign unknown script type');
306
        }
307
308 54
        if ($outputType === OutputClassifier::PAYTOPUBKEY) {
309 6
            $publicKeyBuffer = $return;
310 6
            $results[] = $publicKeyBuffer;
311 6
            $this->requiredSigs = 1;
312 6
            $publicKey = PublicKeyFactory::fromHex($publicKeyBuffer);
0 ignored issues
show
Bug introduced by
It seems like $publicKeyBuffer defined by $return on line 309 can also be of type array<integer,object<Bit...tools\BufferInterface>> or null; however, BitWasp\Bitcoin\Key\PublicKeyFactory::fromHex() does only seem to accept object<BitWasp\Buffertoo...BufferInterface>|string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
313
314 6
            if ($publicKey->getBinary() === $key->getPublicKey()->getBinary()) {
315 6
                $this->signatures[0] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
316 6
            }
317
318 6
            return true;
319
        }
320
321 48
        if ($outputType === OutputClassifier::PAYTOPUBKEYHASH) {
322
            /** @var BufferInterface $pubKeyHash */
323 24
            $pubKeyHash = $return;
324 24
            $results[] = $pubKeyHash;
325 24
            $this->requiredSigs = 1;
326 24
            if ($pubKeyHash->getBinary() === $key->getPublicKey()->getPubKeyHash()->getBinary()) {
327 18
                $this->signatures[0] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
328 18
                $this->publicKeys[0] = $key->getPublicKey();
329 18
            }
330
331 24
            return true;
332
        }
333
334 42
        if ($outputType === OutputClassifier::MULTISIG) {
335 24
            $info = new Multisig($scriptPubKey);
336
337 24
            foreach ($info->getKeys() as $publicKey) {
338 24
                $results[] = $publicKey->getBuffer();
339 24
            }
340
341 24
            $this->publicKeys = $info->getKeys();
342 24
            $this->requiredSigs = $info->getKeyCount();
343
344 24
            foreach ($this->publicKeys as $keyIdx => $publicKey) {
345 24
                if ($publicKey->getBinary() == $key->getPublicKey()->getBinary()) {
346 24
                    $this->signatures[$keyIdx] = $this->calculateSignature($key, $scriptPubKey, $sigVersion);
347 24
                }
348 24
            }
349
350 24
            return true;
351
        }
352
353 36
        if ($outputType === OutputClassifier::PAYTOSCRIPTHASH) {
354
            /** @var BufferInterface $scriptHash */
355 18
            $scriptHash = $return;
356 18
            $results[] = $scriptHash;
357 18
            return true;
358
        }
359
360 30
        if ($outputType === OutputClassifier::WITNESS_V0_KEYHASH) {
361
            /** @var BufferInterface $pubKeyHash */
362 12
            $pubKeyHash = $return;
363 12
            $results[] = $pubKeyHash;
364 12
            $this->requiredSigs = 1;
365
366 12
            if ($pubKeyHash->getBinary() === $key->getPublicKey()->getPubKeyHash()->getBinary()) {
367 12
                $script = ScriptFactory::sequence([Opcodes::OP_DUP, Opcodes::OP_HASH160, $pubKeyHash, Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG]);
368 12
                $this->signatures[0] = $this->calculateSignature($key, $script, 1);
369 12
                $this->publicKeys[0] = $key->getPublicKey();
370 12
            }
371
372 12
            return true;
373
        }
374
375 18
        if ($outputType === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
376
            /** @var BufferInterface $scriptHash */
377 18
            $scriptHash = $return;
378 18
            $results[] = $scriptHash;
379
380 18
            return true;
381
        }
382
383
        return false;
384
    }
385
386
    /**
387
     * @param PrivateKeyInterface $key
388
     * @param ScriptInterface|null $redeemScript
389
     * @param ScriptInterface|null $witnessScript
390
     * @return bool
391
     */
392 54
    public function sign(PrivateKeyInterface $key, ScriptInterface $redeemScript = null, ScriptInterface $witnessScript = null)
393
    {
394
        /** @var BufferInterface[] $return */
395 54
        $type = null;
396 54
        $return = [];
397 54
        $solved = $this->doSignature($key, $this->txOut->getScript(), $type, $return, 0);
398
399 54
        if ($solved && $type === OutputClassifier::PAYTOSCRIPTHASH) {
400 18
            $redeemScriptBuffer = $return[0];
401
402 18
            if (!$redeemScript instanceof ScriptInterface) {
403
                throw new \InvalidArgumentException('Must provide redeem script for P2SH');
404
            }
405
406 18
            if (!$redeemScript->getScriptHash()->getBinary() === $redeemScriptBuffer->getBinary()) {
407
                throw new \InvalidArgumentException("Incorrect redeem script - hash doesn't match");
408
            }
409
410 18
            $results = []; // ???
411 18
            $solved = $solved && $this->doSignature($key, $redeemScript, $type, $results, 0) && $type !== OutputClassifier::PAYTOSCRIPTHASH;
412 18
            if ($solved) {
413 18
                $this->redeemScript = $redeemScript;
414 18
            }
415 18
        }
416
417 54
        if ($solved && $type === OutputClassifier::WITNESS_V0_KEYHASH) {
418 12
            $pubKeyHash = $return[0];
419 12
            $witnessScript = ScriptFactory::sequence([Opcodes::OP_DUP, Opcodes::OP_HASH160, $pubKeyHash, Opcodes::OP_EQUALVERIFY, Opcodes::OP_CHECKSIG]);
420 12
            $subType = null;
421 12
            $subResults = [];
422 12
            $solved = $solved && $this->doSignature($key, $witnessScript, $subType, $subResults, 1);
423 54
        } else if ($solved && $type === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
424 18
            $scriptHash = $return[0];
425
426 18
            if (!$witnessScript instanceof ScriptInterface) {
427
                throw new \InvalidArgumentException('Must provide witness script for witness v0 scripthash');
428
            }
429
430 18
            if (!Hash::sha256($witnessScript->getBuffer())->getBinary() === $scriptHash->getBinary()) {
431
                throw new \InvalidArgumentException("Incorrect witness script - hash doesn't match");
432
            }
433
434 18
            $subType = null;
435 18
            $subResults = [];
436
437 18
            $solved = $solved && $this->doSignature($key, $witnessScript, $subType, $subResults, 1)
438 18
                && $subType !== OutputClassifier::PAYTOSCRIPTHASH
439 18
                && $subType !== OutputClassifier::WITNESS_V0_SCRIPTHASH
440 18
                && $subType !== OutputClassifier::WITNESS_V0_KEYHASH;
441
442 18
            if ($solved) {
443 18
                $this->witnessScript = $witnessScript;
444 18
            }
445 18
        }
446
447 54
        return $solved;
448
    }
449
450
    /**
451
     * @param $outputType
452
     * @param $answer
453
     * @return bool
454
     */
455 54
    private function serializeSimpleSig($outputType, &$answer)
456
    {
457 54
        if ($outputType === OutputClassifier::UNKNOWN) {
458
            throw new \RuntimeException('Cannot sign unknown script type');
459
        }
460
461 54
        if ($outputType === OutputClassifier::PAYTOPUBKEY && $this->isFullySigned()) {
462 6
            $answer = new SigValues(ScriptFactory::sequence([$this->signatures[0]->getBuffer()]), new ScriptWitness([]));
463 6
            return true;
464
        }
465
466 48
        if ($outputType === OutputClassifier::PAYTOPUBKEYHASH && $this->isFullySigned()) {
467 12
            $answer = new SigValues(ScriptFactory::sequence([$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()]), new ScriptWitness([]));
468 12
            return true;
469
        }
470
471 42
        if ($outputType === OutputClassifier::MULTISIG) {
472 24
            $sequence = [Opcodes::OP_0];
473
474 24
            for ($i = 0; $i < count($this->publicKeys); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Performance Best Practice introduced by
Consider avoiding function calls on each iteration of the for loop.

If you have a function call in the test part of a for loop, this function is executed on each iteration. Often such a function, can be moved to the initialization part and be cached.

// count() is called on each iteration
for ($i=0; $i < count($collection); $i++) { }

// count() is only called once
for ($i=0, $c=count($collection); $i<$c; $i++) { }
Loading history...
475 24
                if (isset($this->signatures[$i])) {
476 24
                    $sequence[] = $this->signatures[$i]->getBuffer();
477 24
                }
478 24
            }
479
480 24
            $answer = new SigValues(ScriptFactory::sequence($sequence), new ScriptWitness([]));
481 24
            return true;
482
        }
483
484 36
        return false;
485
    }
486
487
    /**
488
     * @return SigValues
489
     */
490 54
    public function serializeSignatures()
491
    {
492 54
        static $emptyScript = null;
493 54
        static $emptyWitness = null;
494 54
        if (is_null($emptyScript) || is_null($emptyWitness)) {
495 6
            $emptyScript = new Script();
496 6
            $emptyWitness = new ScriptWitness([]);
497 6
        }
498
499
        /** @var BufferInterface[] $return */
500 54
        $outputType = (new OutputClassifier($this->txOut->getScript()))->classify();
501
502
        /** @var SigValues $answer */
503 54
        $answer = new SigValues($emptyScript, $emptyWitness);
504 54
        $serialized = $this->serializeSimpleSig($outputType, $answer);
505
506 54
        $p2sh = false;
507 54
        if (!$serialized && $outputType === OutputClassifier::PAYTOSCRIPTHASH) {
508 18
            $p2sh = true;
509 18
            $outputType = (new OutputClassifier($this->redeemScript))->classify();
510 18
            $serialized = $this->serializeSimpleSig($outputType, $answer);
511 18
        }
512
513 54
        if (!$serialized && $outputType === OutputClassifier::WITNESS_V0_KEYHASH) {
514 12
            $answer = new SigValues($emptyScript, new ScriptWitness([$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()]));
515
516 54
        } else if (!$serialized && $outputType === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
517 18
            $outputType = (new OutputClassifier($this->witnessScript))->classify();
518 18
            $serialized = $this->serializeSimpleSig($outputType, $answer);
519
520 18
            if ($serialized) {
521 18
                $data = [];
522 18
                foreach ($answer->getScriptSig()->getScriptParser()->decode() as $o) {
523 18
                    $data[] = $o->getData();
524 18
                }
525
526 18
                $data[] = $this->witnessScript->getBuffer();
527 18
                $answer = new SigValues($emptyScript, new ScriptWitness($data));
528 18
            }
529 18
        }
530
531 54
        if ($p2sh) {
532 18
            $answer = new SigValues(
533 18
                ScriptFactory::create($answer->getScriptSig()->getBuffer())->push($this->redeemScript->getBuffer())->getScript(),
534 18
                $answer->getScriptWitness()
535 18
            );
536 18
        }
537
538 54
        return $answer;
539
    }
540
}
541