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

InputSigner::calculateSignature()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.0156

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 4
dl 0
loc 6
ccs 3
cts 4
cp 0.75
crap 1.0156
rs 9.4285
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