Completed
Push — master ( 26690c...ea008d )
by thomas
177:34 queued 174:51
created

InputSigner::sortMultiSigs()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.4984

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 3
dl 0
loc 17
ccs 6
cts 13
cp 0.4615
crap 6.4984
rs 9.2
c 0
b 0
f 0
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
89
    /**
90
     * TxInputSigning constructor.
91
     * @param EcAdapterInterface $ecAdapter
92
     * @param TransactionInterface $tx
93
     * @param int $nInput
94
     * @param TransactionOutputInterface $txOut
95
     * @param SignData $signData
96
     */
97 84
    public function __construct(EcAdapterInterface $ecAdapter, TransactionInterface $tx, $nInput, TransactionOutputInterface $txOut, SignData $signData)
98
    {
99 84
        $this->ecAdapter = $ecAdapter;
100 84
        $this->tx = $tx;
101 84
        $this->nInput = $nInput;
102 84
        $this->txOut = $txOut;
103 84
        $this->classifier = new OutputClassifier();
104 84
        $this->publicKeys = [];
105 84
        $this->signatures = [];
106
107 84
        $this->solve($signData);
108 84
        $this->extractSignatures();
109 84
    }
110
111
    /**
112
     * @param int $sigVersion
113
     * @param TransactionSignatureInterface[] $stack
114
     * @param ScriptInterface $scriptCode
115
     * @return \SplObjectStorage
116
     */
117 24
    private function sortMultiSigs($sigVersion, $stack, ScriptInterface $scriptCode)
118
    {
119 24
        $sigSort = new SignatureSort($this->ecAdapter);
120 24
        $sigs = new \SplObjectStorage;
121
122 24
        foreach ($stack as $txSig) {
123
            $hash = $this->calculateSigHash($scriptCode, $txSig->getHashType(), $sigVersion);
124
            $linked = $sigSort->link([$txSig->getSignature()], $this->publicKeys, $hash);
125
            foreach ($this->publicKeys as $key) {
126
                if ($linked->contains($key)) {
127
                    $sigs[$key] = $txSig;
128
                }
129
            }
130 8
        }
131
132 24
        return $sigs;
133
    }
134
135
    /**
136
     * @param string $type
137
     * @param ScriptInterface $scriptCode
138
     * @param BufferInterface[] $stack
139
     * @param int $sigVersion
140
     * @return string
141
     */
142 78
    public function extractFromValues($type, ScriptInterface $scriptCode, array $stack, $sigVersion)
143
    {
144 78
        $size = count($stack);
145 78
        if ($type === OutputClassifier::PAYTOPUBKEYHASH) {
146 42
            $this->requiredSigs = 1;
147 42
            if ($size === 2) {
148 24
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
149 24
                $this->publicKeys = [PublicKeyFactory::fromHex($stack[1], $this->ecAdapter)];
150 8
            }
151 14
        }
152
153 78
        if ($type === OutputClassifier::PAYTOPUBKEY) {
154 12
            $this->requiredSigs = 1;
155 12
            if ($size === 1) {
156 6
                $this->signatures = [TransactionSignatureFactory::fromHex($stack[0], $this->ecAdapter)];
157 2
            }
158 4
        }
159
160 78
        if ($type === OutputClassifier::MULTISIG) {
161 24
            $info = new Multisig($scriptCode);
162 24
            $this->requiredSigs = $info->getRequiredSigCount();
163 24
            $this->publicKeys = $info->getKeys();
164
165 24
            if ($size > 1) {
166 24
                $vars = [];
167 24
                for ($i = 1, $j = $size - 1; $i < $j; $i++) {
168
                    $vars[] = TransactionSignatureFactory::fromHex($stack[$i], $this->ecAdapter);
169
                }
170
171 24
                $sigs = $this->sortMultiSigs($sigVersion, $vars, $scriptCode);
172 24
                foreach ($this->publicKeys as $idx => $key) {
173 24
                    $this->signatures[$idx] = isset($sigs[$key]) ? $sigs[$key]->getBuffer() : null;
174 8
                }
175 8
            }
176 8
        }
177
178 78
        return $type;
179
    }
180
181
    /**
182
     * @param SignData $signData
183
     * @return $this
184
     * @throws \Exception
185
     */
186 84
    private function solve(SignData $signData)
187
    {
188 84
        $scriptPubKey = $this->txOut->getScript();
189 84
        $solution = $this->scriptPubKey = $this->classifier->decode($scriptPubKey);
190 84
        if ($solution->getType() === OutputClassifier::UNKNOWN) {
191
            throw new \RuntimeException('scriptPubKey type is unknown');
192
        }
193
194 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
195 24
            $redeemScript = $signData->getRedeemScript();
196 24
            if (!$solution->getSolution()->equals(Hash::sha256ripe160($redeemScript->getBuffer()))) {
197
                throw new \Exception('Redeem script doesn\'t match script-hash');
198
            }
199 24
            $solution = $this->redeemScript = $this->classifier->decode($redeemScript);
200 24
            if (!in_array($solution->getType(), [OutputClassifier::WITNESS_V0_SCRIPTHASH, OutputClassifier::WITNESS_V0_KEYHASH, OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
201
                throw new \Exception('Unsupported pay-to-script-hash script');
202
            }
203 8
        }
204
        // WitnessKeyHash doesn't require further solving until signing
205 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
206 18
            $witnessScript = $signData->getWitnessScript();
207 18
            if (!$solution->getSolution()->equals(Hash::sha256($witnessScript->getBuffer()))) {
208
                throw new \Exception('Witness script doesn\'t match witness-script-hash');
209
            }
210 18
            $solution = $this->witnessScript = $this->classifier->decode($witnessScript);
211 18
            if (!in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
212
                throw new \Exception('Unsupported witness-script-hash script');
213
            }
214 6
        }
215
216 84
        return $this;
217
    }
218
219
    /**
220
     * @return $this
221
     */
222 84
    public function extractSignatures()
223
    {
224 84
        $solution = $this->scriptPubKey;
225 84
        $scriptSig = $this->tx->getInput($this->nInput)->getScript();
226 84
        if (in_array($solution->getType(), [OutputClassifier::PAYTOPUBKEYHASH , OutputClassifier::PAYTOPUBKEY, OutputClassifier::MULTISIG])) {
227 42
            $stack = [];
228 42
            foreach ($scriptSig->getScriptParser()->decode() as $op) {
229 18
                $stack[] = $op->getData();
230 14
            }
231 42
            $this->extractFromValues($solution->getType(), $solution->getScript(), $stack, 0);
232 14
        }
233
234 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
235 24
            $decodeSig = $scriptSig->getScriptParser()->decode();
236 24
            if (count($decodeSig) > 0) {
237 18
                $redeemScript = new Script(end($decodeSig)->getData());
238 18
                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...
239
                    throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
240
                }
241
242 18
                $internalSig = [];
243 18
                foreach (array_slice($decodeSig, 0, -1) as $operation) {
244
                    /** @var Operation $operation */
245 6
                    $internalSig[] = $operation->getData();
246 6
                }
247
248 18
                $solution = $this->redeemScript;
249 18
                $this->extractFromValues($solution->getType(), $solution->getScript(), $internalSig, 0);
250 6
            }
251 8
        }
252
253 84
        $witnesses = $this->tx->getWitnesses();
254 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
255 12
            $wit = isset($witnesses[$this->nInput]) ? $witnesses[$this->nInput]->all() : [];
256 12
            $keyHashCode = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
257 12
            $this->extractFromValues(OutputClassifier::PAYTOPUBKEYHASH, $keyHashCode, $wit, 1);
258 80
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
259 18
            if (isset($witnesses[$this->nInput])) {
260 18
                $witness = $witnesses[$this->nInput];
261 18
                $witCount = count($witnesses[$this->nInput]);
262 18
                if ($witCount > 0) {
263 18
                    if (!$witness[$witCount - 1]->equals($this->witnessScript->getScript()->getBuffer())) {
264
                        throw new \RuntimeException('Redeem script from scriptSig doesn\'t match script-hash');
265
                    }
266
267 18
                    $solution = $this->witnessScript;
268 18
                    $this->extractFromValues($solution->getType(), $solution->getScript(), array_slice($witness->all(), 0, -1), 1);
269 6
                }
270 6
            }
271 6
        }
272
273 84
        return $this;
274
    }
275
276
    /**
277
     * @param ScriptInterface $scriptCode
278
     * @param int $sigHashType
279
     * @param int $sigVersion
280
     * @return BufferInterface
281
     */
282 84
    public function calculateSigHash(ScriptInterface $scriptCode, $sigHashType, $sigVersion)
283
    {
284 84
        if ($sigVersion === 1) {
285 30
            $hasher = new V1Hasher($this->tx, $this->txOut->getValue());
286 10
        } else {
287 54
            $hasher = new Hasher($this->tx);
288
        }
289
290 84
        return $hasher->calculate($scriptCode, $this->nInput, $sigHashType);
291
    }
292
293
    /**
294
     * @param PrivateKeyInterface $key
295
     * @param ScriptInterface $scriptCode
296
     * @param int $sigHashType
297
     * @param int $sigVersion
298
     * @return TransactionSignature
299
     */
300 84
    public function calculateSignature(PrivateKeyInterface $key, ScriptInterface $scriptCode, $sigHashType, $sigVersion)
301
    {
302 84
        $hash = $this->calculateSigHash($scriptCode, $sigHashType, $sigVersion);
303 84
        $ecSignature = $this->ecAdapter->sign($hash, $key, new Rfc6979($this->ecAdapter, $key, $hash, 'sha256'));
304 84
        return new TransactionSignature($this->ecAdapter, $ecSignature, $sigHashType);
305
    }
306
307
    /**
308
     * @return bool
309
     */
310 54
    public function isFullySigned()
311
    {
312 54
        return $this->requiredSigs !== 0 && $this->requiredSigs === count($this->signatures);
313
    }
314
315
    /**
316
     * The function only returns true when $scriptPubKey could be classified
317
     *
318
     * @param PrivateKeyInterface $key
319
     * @param OutputData $solution
320
     * @param int $sigHashType
321
     * @param int $sigVersion
322
     */
323 84
    private function doSignature(PrivateKeyInterface $key, OutputData $solution, $sigHashType, $sigVersion = 0)
324
    {
325 84
        if ($solution->getType() === OutputClassifier::PAYTOPUBKEY) {
326 12
            if (!$key->getPublicKey()->getBuffer()->equals($solution->getSolution())) {
327
                throw new \RuntimeException('Signing with the wrong private key');
328
            }
329 12
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
330 12
            $this->publicKeys[0] = $key->getPublicKey();
331 12
            $this->requiredSigs = 1;
332 76
        } else if ($solution->getType() === OutputClassifier::PAYTOPUBKEYHASH) {
333 42
            if (!$key->getPubKeyHash()->equals($solution->getSolution())) {
334
                throw new \RuntimeException('Signing with the wrong private key');
335
            }
336 42
            $this->signatures[0] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
337 42
            $this->publicKeys[0] = $key->getPublicKey();
338 42
            $this->requiredSigs = 1;
339 44
        } else if ($solution->getType() === OutputClassifier::MULTISIG) {
340 30
            $info = new Multisig($solution->getScript());
341 30
            $this->publicKeys = $info->getKeys();
342 30
            $this->requiredSigs = $info->getRequiredSigCount();
343
344 30
            $myKey = $key->getPublicKey()->getBuffer();
345 30
            $signed = false;
346 30
            foreach ($info->getKeys() as $keyIdx => $publicKey) {
347 30
                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...
348 30
                    $this->signatures[$keyIdx] = $this->calculateSignature($key, $solution->getScript(), $sigHashType, $sigVersion);
349 30
                    $signed = true;
350 10
                }
351 10
            }
352
353 30
            if (!$signed) {
354 20
                throw new \RuntimeException('Signing with the wrong private key');
355
            }
356 10
        } else {
357 2
            throw new \RuntimeException('Cannot sign unknown script type');
358
        }
359 84
    }
360
361
    /**
362
     * @param PrivateKeyInterface $key
363
     * @param int $sigHashType
364
     * @return bool
365
     */
366 84
    public function sign(PrivateKeyInterface $key, $sigHashType = SigHashInterface::ALL)
367
    {
368 84
        if ($this->scriptPubKey->canSign()) {
369 42
            $this->doSignature($key, $this->scriptPubKey, $sigHashType, 0);
370 42
            return true;
371
        }
372 42
        $solution = $this->scriptPubKey;
373 42
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
374 24
            if ($this->redeemScript->canSign()) {
375 12
                $this->doSignature($key, $this->redeemScript, $sigHashType, 0);
376 12
                return true;
377
            }
378 12
            $solution = $this->redeemScript;
379 4
        }
380
381 30
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
382 12
            $keyHashScript = ScriptFactory::scriptPubKey()->payToPubKeyHashFromHash($solution->getSolution());
383 12
            $this->doSignature($key, $this->classifier->decode($keyHashScript), $sigHashType, 1);
384 12
            return true;
385 18
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
386 18
            if ($this->witnessScript->canSign()) {
387 18
                $this->doSignature($key, $this->witnessScript, $sigHashType, 1);
388 18
                return true;
389
            }
390
        }
391
392
        return false;
393
    }
394
395
    /**
396
     * @param string $outputType
397
     * @return BufferInterface[]
398
     */
399 84
    private function serializeSolution($outputType)
400
    {
401 84
        if ($outputType === OutputClassifier::PAYTOPUBKEY) {
402 12
            return [$this->signatures[0]->getBuffer()];
403 72
        } else if ($outputType === OutputClassifier::PAYTOPUBKEYHASH) {
404 42
            return [$this->signatures[0]->getBuffer(), $this->publicKeys[0]->getBuffer()];
405 30
        } else if ($outputType === OutputClassifier::MULTISIG) {
406 30
            $sequence = [new Buffer()];
407 30
            for ($i = 0, $nPubKeys = count($this->publicKeys); $i < $nPubKeys; $i++) {
408 30
                if (isset($this->signatures[$i])) {
409 30
                    $sequence[] = $this->signatures[$i]->getBuffer();
410 10
                }
411 10
            }
412
413 30
            return $sequence;
414
        } else {
415
            throw new \RuntimeException('Cannot serialize this script sig');
416
        }
417
    }
418
419
    /**
420
     * @param BufferInterface[] $buffers
421
     * @return ScriptInterface
422
     */
423
    public function pushAll(array $buffers)
424
    {
425 54
        return ScriptFactory::sequence(array_map(function ($buffer) {
426 54
            if (!($buffer instanceof BufferInterface)) {
427
                throw new \RuntimeException('Script contained a non-push opcode');
428
            }
429
430 54
            $size = $buffer->getSize();
431 54
            if ($size === 0) {
432 18
                return Opcodes::OP_0;
433
            }
434
435 54
            $first = ord($buffer->getBinary());
436 54
            if ($size === 1 && $first >= 1 && $first <= 16) {
437
                return \BitWasp\Bitcoin\Script\encodeOpN($first);
438
            } else {
439 54
                return $buffer;
440
            }
441 54
        }, $buffers));
442
    }
443
444
    /**
445
     * @return SigValues
446
     */
447 84
    public function serializeSignatures()
448
    {
449 84
        static $emptyScript = null;
450 84
        static $emptyWitness = null;
451 84
        if (is_null($emptyScript) || is_null($emptyWitness)) {
452 6
            $emptyScript = new Script();
453 6
            $emptyWitness = new ScriptWitness([]);
454 2
        }
455
456 84
        $scriptSig = $emptyScript;
457 84
        $witness = [];
458 84
        $solution = $this->scriptPubKey;
459 84
        if ($solution->canSign()) {
460 42
            $scriptSig = $this->pushAll($this->serializeSolution($this->scriptPubKey->getType()));
461 14
        }
462
463 84
        $p2sh = false;
464 84
        if ($solution->getType() === OutputClassifier::PAYTOSCRIPTHASH) {
465 24
            $p2sh = true;
466 24
            if ($this->redeemScript->canSign()) {
467 12
                $scriptSig = $this->pushAll($this->serializeSolution($this->redeemScript->getType()));
468 4
            }
469 24
            $solution = $this->redeemScript;
470 8
        }
471
472 84
        if ($solution->getType() === OutputClassifier::WITNESS_V0_KEYHASH) {
473 12
            $scriptSig = $emptyScript;
474 12
            $witness = $this->serializeSolution(OutputClassifier::PAYTOPUBKEYHASH);
475 76
        } else if ($solution->getType() === OutputClassifier::WITNESS_V0_SCRIPTHASH) {
476 18
            if ($this->witnessScript->canSign()) {
477 18
                $scriptSig = $emptyScript;
478 18
                $witness = $this->serializeSolution($this->witnessScript->getType());
479 18
                $witness[] = $this->witnessScript->getScript()->getBuffer();
480 6
            }
481 6
        }
482
483 84
        if ($p2sh) {
484 24
            $scriptSig = ScriptFactory::create($scriptSig->getBuffer())->push($this->redeemScript->getScript()->getBuffer())->getScript();
485 8
        }
486
487 84
        return new SigValues($scriptSig, new ScriptWitness($witness));
488
    }
489
}
490