Completed
Pull Request — 0.0.35 (#660)
by thomas
28:46
created

HierarchicalKey::childKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 4
dl 0
loc 12
ccs 9
cts 9
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin\Key\Deterministic;
4
5
use BitWasp\Bitcoin\Address\BaseAddressCreator;
6
use BitWasp\Bitcoin\Bitcoin;
7
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
8
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
11
use BitWasp\Bitcoin\Crypto\Hash;
12
use BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData;
13
use BitWasp\Bitcoin\Key\KeyToScript\ScriptDataFactory;
14
use BitWasp\Bitcoin\Network\NetworkInterface;
15
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\Base58ExtendedKeySerializer;
16
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\ExtendedKeySerializer;
17
use BitWasp\Bitcoin\Util\IntRange;
18
use BitWasp\Buffertools\Buffer;
19
use BitWasp\Buffertools\BufferInterface;
20
use BitWasp\Buffertools\Buffertools;
21
use BitWasp\Buffertools\Parser;
22
23
class HierarchicalKey
24
{
25
    /**
26
     * @var EcAdapterInterface
27
     */
28
    protected $ecAdapter;
29
30
    /**
31
     * @var int
32
     */
33
    private $depth;
34
35
    /**
36
     * @var int
37
     */
38
    private $parentFingerprint;
39
40
    /**
41
     * @var int
42
     */
43
    private $sequence;
44
45
    /**
46
     * @var BufferInterface
47
     */
48
    private $chainCode;
49
50
    /**
51
     * @var PublicKeyInterface|PrivateKeyInterface
52
     */
53
    private $key;
54
55
    /**
56
     * @var ScriptDataFactory
57
     */
58
    private $scriptDataFactory;
59
60
    /**
61
     * @var ScriptAndSignData|null
62
     */
63
    private $scriptAndSignData;
64
65
    /**
66
     * @param EcAdapterInterface $ecAdapter
67
     * @param ScriptDataFactory $scriptDataFactory
68
     * @param int $depth
69
     * @param int $parentFingerprint
70
     * @param int $sequence
71
     * @param BufferInterface $chainCode
72
     * @param KeyInterface $key
73
     */
74 75
    public function __construct(EcAdapterInterface $ecAdapter, ScriptDataFactory $scriptDataFactory, $depth, $parentFingerprint, $sequence, BufferInterface $chainCode, KeyInterface $key)
75
    {
76 75
        if ($depth < 0 || $depth > IntRange::U8_MAX) {
77
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, must be in range [0 - 255] inclusive');
78
        }
79
80 75
        if ($parentFingerprint < 0 || $parentFingerprint > IntRange::U32_MAX) {
81
            throw new \InvalidArgumentException('Invalid fingerprint for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
82
        }
83
84 75
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
85
            throw new \InvalidArgumentException('Invalid sequence for BIP32 key, must be in range [0 - (2^31)-1] inclusive');
86
        }
87
88 75
        if ($chainCode->getSize() !== 32) {
89
            throw new \RuntimeException('Chaincode should be 32 bytes');
90
        }
91
92 75
        if (!$key->isCompressed()) {
93 1
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
94
        }
95
96 74
        $this->ecAdapter = $ecAdapter;
97 74
        $this->depth = $depth;
98 74
        $this->sequence = $sequence;
99 74
        $this->parentFingerprint = $parentFingerprint;
100 74
        $this->chainCode = $chainCode;
101 74
        $this->key = $key;
0 ignored issues
show
Documentation Bug introduced by
It seems like $key of type object<BitWasp\Bitcoin\C...apter\Key\KeyInterface> is incompatible with the declared type object<BitWasp\Bitcoin\C...ey\PrivateKeyInterface> of property $key.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
102 74
        $this->scriptDataFactory = $scriptDataFactory;
103 74
    }
104
105
    /**
106
     * Return the depth of this key. This is limited to 256 sequential derivations.
107
     *
108
     * @return int
109
     */
110 34
    public function getDepth()
111
    {
112 34
        return $this->depth;
113
    }
114
115
    /**
116
     * Get the sequence number for this address. Hardened keys are
117
     * created with sequence > 0x80000000. a sequence number lower
118
     * than this can be derived with the public key.
119
     *
120
     * @return int
121
     */
122 29
    public function getSequence()
123
    {
124 29
        return $this->sequence;
125
    }
126
127
    /**
128
     * Get the fingerprint of the parent key. For master keys, this is 00000000.
129
     *
130
     * @return string
131
     */
132 30
    public function getFingerprint()
133
    {
134 30
        if ($this->getDepth() === 0) {
135 14
            return 0;
136
        }
137
138 30
        return $this->parentFingerprint;
139
    }
140
141
    /**
142
     * Return the fingerprint to be used for child keys.
143
     * @return int
144
     */
145 37
    public function getChildFingerprint()
146
    {
147 37
        return $this->getPublicKey()->getPubKeyHash()->slice(0, 4)->getInt();
148
    }
149
150
    /**
151
     * Return the chain code - a deterministic 'salt' for HMAC-SHA512
152
     * in child derivations
153
     *
154
     * @return BufferInterface
155
     */
156 28
    public function getChainCode()
157
    {
158 28
        return $this->chainCode;
159
    }
160
161
    /**
162
     * @return PrivateKeyInterface
163
     */
164 46
    public function getPrivateKey()
165
    {
166 46
        if ($this->key->isPrivate()) {
167 44
            return $this->key;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->key; of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface adds the type BitWasp\Bitcoin\Crypto\E...\Key\PublicKeyInterface to the return on line 167 which is incompatible with the return type documented by BitWasp\Bitcoin\Key\Dete...hicalKey::getPrivateKey of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface.
Loading history...
168
        }
169
170 2
        throw new \RuntimeException('Unable to get private key, not known');
171
    }
172
173
    /**
174
     * Get the public key the private key or public key.
175
     *
176
     * @return PublicKeyInterface
177
     */
178 48
    public function getPublicKey()
179
    {
180 48
        if ($this->isPrivate()) {
181 40
            return $this->getPrivateKey()->getPublicKey();
182
        } else {
183 31
            return $this->key;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->key; of type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface adds the type BitWasp\Bitcoin\Crypto\E...Key\PrivateKeyInterface to the return on line 183 which is incompatible with the return type documented by BitWasp\Bitcoin\Key\Dete...chicalKey::getPublicKey of type BitWasp\Bitcoin\Crypto\E...\Key\PublicKeyInterface.
Loading history...
184
        }
185
    }
186
187
    /**
188
     * @return HierarchicalKey
189
     */
190 25
    public function withoutPrivateKey()
191
    {
192 25
        $clone = clone $this;
193 25
        $clone->key = $clone->getPublicKey();
194 25
        return $clone;
195
    }
196
197
    /**
198
    public function withScriptFactory(ScriptDataFactory $factory)
199
    {
200
        $clone = clone $this;
201
        $clone->scriptDataFactory = $factory;
202
        return $clone;
203
    }
204
205
    /**
206
     * @return ScriptDataFactory
207
     */
208 28
    public function getScriptDataFactory()
209
    {
210 28
        return $this->scriptDataFactory;
211
    }
212
213
    /**
214
     * @return \BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData
215
     */
216 22
    public function getScriptAndSignData()
217
    {
218 22
        if (null === $this->scriptAndSignData) {
219 22
            $this->scriptAndSignData = $this->scriptDataFactory->convertKey($this->key);
220
        }
221
222 22
        return $this->scriptAndSignData;
223
    }
224
225
    /**
226
     * @param BaseAddressCreator $addressCreator
227
     * @return \BitWasp\Bitcoin\Address\Address
228
     * @throws \BitWasp\Bitcoin\Exceptions\UnrecognizedScriptForAddressException
229
     */
230 16
    public function getAddress(BaseAddressCreator $addressCreator)
231
    {
232 16
        return $this->getScriptAndSignData()->getAddress($addressCreator);
0 ignored issues
show
Compatibility introduced by
$addressCreator of type object<BitWasp\Bitcoin\A...ess\BaseAddressCreator> is not a sub-type of object<BitWasp\Bitcoin\Address\AddressCreator>. It seems like you assume a child class of the class BitWasp\Bitcoin\Address\BaseAddressCreator to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
233
    }
234
235
    /**
236
     * Return whether this is a private key
237
     *
238
     * @return bool
239
     */
240 52
    public function isPrivate()
241
    {
242 52
        return $this->key->isPrivate();
243
    }
244
245
    /**
246
     * Return whether the key is hardened
247
     *
248
     * @return bool
249
     */
250 2
    public function isHardened()
251
    {
252 2
        return ($this->sequence >> 31) === 1;
253
    }
254
255
    /**
256
     * Create a buffer containing data to be hashed hashed to yield the child offset
257
     *
258
     * @param int $sequence
259
     * @return BufferInterface
260
     * @throws \Exception
261
     */
262 43
    public function getHmacSeed($sequence)
263
    {
264 43
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
265 6
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
266
        }
267
268 37
        if (($sequence >> 31) === 1) {
269 34
            if ($this->isPrivate() === false) {
270 2
                throw new \Exception("Can't derive a hardened key without the private key");
271
            }
272
273 32
            $buffer = Buffertools::concat(new Buffer("\x00"), $this->getPrivateKey()->getBuffer());
274
        } else {
275 35
            $buffer = $this->getPublicKey()->getBuffer();
276
        }
277
278 35
        return (new Parser($buffer))
279 35
            ->writeBytes(4, Buffer::int($sequence, 4))
280 35
            ->getBuffer();
281
    }
282
283
    /**
284
     * @param $nextDepth
285
     * @param $sequence
286
     * @param BufferInterface $chainCode
287
     * @param KeyInterface $key
288
     * @return HierarchicalKey
289
     * @throws \Exception
290
     */
291 35
    protected function childKey($nextDepth, $sequence, BufferInterface $chainCode, KeyInterface $key)
292
    {
293 35
        return new HierarchicalKey(
294 35
            $this->ecAdapter,
295 35
            $this->scriptDataFactory,
296 35
            $nextDepth,
297 35
            $this->getChildFingerprint(),
298 35
            $sequence,
299 35
            $chainCode,
300 35
            $key
301
        );
302
    }
303
304
    /**
305
     * Derive a child key
306
     *
307
     * @param int $sequence
308
     * @return HierarchicalKey
309
     * @throws \Exception
310
     */
311 40
    public function deriveChild($sequence)
312
    {
313 40
        $nextDepth = $this->depth + 1;
314 40
        if ($nextDepth > 255) {
315
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
316
        }
317
318 40
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
319 35
        $offset = $hash->slice(0, 32);
320 35
        $chain = $hash->slice(32);
321
322 35
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
323 1
            return $this->deriveChild($sequence + 1);
324
        }
325
326 35
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
327 35
        $key = $key->tweakAdd($offset->getGmp());
328
329 35
        return $this->childKey($nextDepth, $sequence, $chain, $key);
330
    }
331
332
    /**
333
     * @param array|\stdClass|\Traversable $list
334
     * @return HierarchicalKey
335
     */
336 30
    public function deriveFromList($list)
337
    {
338 30
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
339
            throw new \InvalidArgumentException('List must be an array or \Traversable');
340
        }
341
342 30
        $key = $this;
343 30
        foreach ($list as $sequence) {
0 ignored issues
show
Bug introduced by
The expression $list of type array|object<Traversable>|object<stdClass> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
344 30
            $key = $key->deriveChild($sequence);
345
        }
346
347 30
        return $key;
348
    }
349
350
    /**
351
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
352
     *
353
     * @param string $path
354
     * @return HierarchicalKey
355
     * @throws \Exception
356
     */
357 30
    public function derivePath($path)
358
    {
359 30
        $sequences = new HierarchicalKeySequence();
360 30
        return $this->deriveFromList($sequences->decodePath($path));
361
    }
362
363
    /**
364
     * Serializes the instance according to whether it wraps a private or public key.
365
     * @param NetworkInterface $network
366
     * @return string
367
     */
368 16
    public function toExtendedKey(NetworkInterface $network = null)
369
    {
370 16
        $network = $network ?: Bitcoin::getNetwork();
371
372 16
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
373 16
        $extended = $extendedSerializer->serialize($network, $this);
374 16
        return $extended;
375
    }
376
377
    /**
378
     * Explicitly serialize as a private key. Throws an exception if
379
     * the key isn't private.
380
     *
381
     * @param NetworkInterface $network
382
     * @return string
383
     */
384 16
    public function toExtendedPrivateKey(NetworkInterface $network = null)
385
    {
386 16
        if (!$this->isPrivate()) {
387 2
            throw new \LogicException('Cannot create extended private key from public');
388
        }
389
390 14
        return $this->toExtendedKey($network);
391
    }
392
393
    /**
394
     * Explicitly serialize as a public key. This will always work.
395
     *
396
     * @param NetworkInterface $network
397
     * @return string
398
     */
399 13
    public function toExtendedPublicKey(NetworkInterface $network = null)
400
    {
401 13
        return $this->withoutPrivateKey()->toExtendedKey($network);
402
    }
403
}
404