Completed
Push — master ( abcf31...a8a46d )
by thomas
28:08 queued 25:46
created

HierarchicalKey   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Test Coverage

Coverage 94.39%

Importance

Changes 0
Metric Value
wmc 46
lcom 2
cbo 13
dl 0
loc 363
ccs 101
cts 107
cp 0.9439
rs 8.3999
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 30 9
A getDepth() 0 4 1
A getSequence() 0 4 1
A getFingerprint() 0 8 2
A getChildFingerprint() 0 5 1
A getChainCode() 0 4 1
A getPrivateKey() 0 10 2
A getPublicKey() 0 10 2
A withoutPrivateKey() 0 6 1
A getScriptDataFactory() 0 4 1
A getScriptAndSignData() 0 8 2
A getAddress() 0 4 1
A isPrivate() 0 4 1
A isHardened() 0 4 1
B getHmacSeed() 0 18 5
B deriveChild() 0 28 4
B deriveFromList() 0 13 5
A derivePath() 0 5 1
A toExtendedKey() 0 8 2
A toExtendedPrivateKey() 0 8 2
A toExtendedPublicKey() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like HierarchicalKey often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HierarchicalKey, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Key\Deterministic;
6
7
use BitWasp\Bitcoin\Address\BaseAddressCreator;
8
use BitWasp\Bitcoin\Bitcoin;
9
use BitWasp\Bitcoin\Crypto\EcAdapter\Adapter\EcAdapterInterface;
10
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\KeyInterface;
11
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PrivateKeyInterface;
12
use BitWasp\Bitcoin\Crypto\EcAdapter\Key\PublicKeyInterface;
13
use BitWasp\Bitcoin\Crypto\Hash;
14
use BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData;
15
use BitWasp\Bitcoin\Key\KeyToScript\ScriptDataFactory;
16
use BitWasp\Bitcoin\Network\NetworkInterface;
17
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\Base58ExtendedKeySerializer;
18
use BitWasp\Bitcoin\Serializer\Key\HierarchicalKey\ExtendedKeySerializer;
19
use BitWasp\Bitcoin\Util\IntRange;
20
use BitWasp\Buffertools\Buffer;
21
use BitWasp\Buffertools\BufferInterface;
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 KeyInterface
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 73
    public function __construct(EcAdapterInterface $ecAdapter, ScriptDataFactory $scriptDataFactory, int $depth, int $parentFingerprint, int $sequence, BufferInterface $chainCode, KeyInterface $key)
75
    {
76 73
        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 73
        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 73
        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 73
        if ($chainCode->getSize() !== 32) {
89
            throw new \RuntimeException('Chaincode should be 32 bytes');
90
        }
91
92 73
        if (!$key->isCompressed()) {
93 1
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
94
        }
95
96 72
        $this->ecAdapter = $ecAdapter;
97 72
        $this->depth = $depth;
98 72
        $this->sequence = $sequence;
99 72
        $this->parentFingerprint = $parentFingerprint;
100 72
        $this->chainCode = $chainCode;
101 72
        $this->key = $key;
102 72
        $this->scriptDataFactory = $scriptDataFactory;
103 72
    }
104
105
    /**
106
     * Return the depth of this key. This is limited to 256 sequential derivations.
107
     *
108
     * @return int
109
     */
110 30
    public function getDepth(): int
111
    {
112 30
        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 25
    public function getSequence(): int
123
    {
124 25
        return $this->sequence;
125
    }
126
127
    /**
128
     * Get the fingerprint of the parent key. For master keys, this is 00000000.
129
     *
130
     * @return int
131
     */
132 26
    public function getFingerprint(): int
133
    {
134 26
        if ($this->getDepth() === 0) {
135 14
            return 0;
136
        }
137
138 22
        return $this->parentFingerprint;
139
    }
140
141
    /**
142
     * Return the fingerprint to be used for child keys.
143
     * @return int
144
     */
145 33
    public function getChildFingerprint(): int
146
    {
147 33
        $pubKeyHash = $this->getPublicKey()->getPubKeyHash();
148 33
        return (int) $pubKeyHash->slice(0, 4)->getInt();
149
    }
150
151
    /**
152
     * Return the chain code - a deterministic 'salt' for HMAC-SHA512
153
     * in child derivations
154
     *
155
     * @return BufferInterface
156
     */
157 24
    public function getChainCode(): BufferInterface
158
    {
159 24
        return $this->chainCode;
160
    }
161
162
    /**
163
     * @return PrivateKeyInterface
164
     */
165 42
    public function getPrivateKey(): PrivateKeyInterface
166
    {
167 42
        if ($this->key->isPrivate()) {
168
            /** @var PrivateKeyInterface $key */
169 40
            $key = $this->key;
170 40
            return $key;
171
        }
172
173 2
        throw new \RuntimeException('Unable to get private key, not known');
174
    }
175
176
    /**
177
     * Get the public key the private key or public key.
178
     *
179
     * @return PublicKeyInterface
180
     */
181 44
    public function getPublicKey(): PublicKeyInterface
182
    {
183 44
        if ($this->isPrivate()) {
184 36
            return $this->getPrivateKey()->getPublicKey();
185
        } else {
186
            /** @var PublicKeyInterface $key */
187 27
            $key = $this->key;
188 27
            return $key;
189
        }
190
    }
191
192
    /**
193
     * @return HierarchicalKey
194
     */
195 21
    public function withoutPrivateKey(): HierarchicalKey
196
    {
197 21
        $clone = clone $this;
198 21
        $clone->key = $clone->getPublicKey();
199 21
        return $clone;
200
    }
201
202
    /**
203
     * @return ScriptDataFactory
204
     */
205 26
    public function getScriptDataFactory()
206
    {
207 26
        return $this->scriptDataFactory;
208
    }
209
210
    /**
211
     * @return \BitWasp\Bitcoin\Key\KeyToScript\ScriptAndSignData
212
     */
213 22
    public function getScriptAndSignData()
214
    {
215 22
        if (null === $this->scriptAndSignData) {
216 22
            $this->scriptAndSignData = $this->scriptDataFactory->convertKey($this->key);
217
        }
218
219 22
        return $this->scriptAndSignData;
220
    }
221
222
    /**
223
     * @param BaseAddressCreator $addressCreator
224
     * @return \BitWasp\Bitcoin\Address\Address
225
     */
226 16
    public function getAddress(BaseAddressCreator $addressCreator)
227
    {
228 16
        return $this->getScriptAndSignData()->getAddress($addressCreator);
229
    }
230
231
    /**
232
     * Return whether this is a private key
233
     *
234
     * @return bool
235
     */
236 48
    public function isPrivate(): bool
237
    {
238 48
        return $this->key->isPrivate();
239
    }
240
241
    /**
242
     * Return whether the key is hardened
243
     *
244
     * @return bool
245
     */
246 2
    public function isHardened(): bool
247
    {
248 2
        return ($this->sequence >> 31) === 1;
249
    }
250
251
    /**
252
     * Create a buffer containing data to be hashed hashed to yield the child offset
253
     *
254
     * @param int $sequence
255
     * @return BufferInterface
256
     * @throws \Exception
257
     */
258 39
    public function getHmacSeed(int $sequence): BufferInterface
259
    {
260 39
        if ($sequence < 0 || $sequence > IntRange::U32_MAX) {
261 6
            throw new \InvalidArgumentException("Sequence is outside valid range, must be >= 0 && <= (2^31)-1");
262
        }
263
264 33
        if (($sequence >> 31) === 1) {
265 30
            if ($this->isPrivate() === false) {
266 2
                throw new \Exception("Can't derive a hardened key without the private key");
267
            }
268
269 28
            $data = "\x00{$this->getPrivateKey()->getBinary()}";
270
        } else {
271 31
            $data = $this->getPublicKey()->getBinary();
272
        }
273
274 31
        return new Buffer($data . pack("N", $sequence));
275
    }
276
277
    /**
278
     * Derive a child key
279
     *
280
     * @param int $sequence
281
     * @return HierarchicalKey
282
     * @throws \Exception
283
     */
284 36
    public function deriveChild(int $sequence): HierarchicalKey
285
    {
286 36
        $nextDepth = $this->depth + 1;
287 36
        if ($nextDepth > 255) {
288
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
289
        }
290
291 36
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
292 31
        $offset = $hash->slice(0, 32);
293 31
        $chain = $hash->slice(32);
294
295 31
        if (false === $this->ecAdapter->validatePrivateKey($offset)) {
296 1
            return $this->deriveChild($sequence + 1);
297
        }
298
299 31
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
300 31
        $key = $key->tweakAdd($offset->getGmp());
301
302 31
        return new HierarchicalKey(
303 31
            $this->ecAdapter,
304 31
            $this->scriptDataFactory,
305 31
            $nextDepth,
306 31
            $this->getChildFingerprint(),
307 31
            $sequence,
308 31
            $chain,
309 31
            $key
310
        );
311
    }
312
313
    /**
314
     * @param array|\stdClass|\Traversable $list
315
     * @return HierarchicalKey
316
     * @throws \Exception
317
     */
318 26
    public function deriveFromList($list): HierarchicalKey
319
    {
320 26
        if (!is_array($list) && !$list instanceof \Traversable && !$list instanceof \stdClass) {
321
            throw new \InvalidArgumentException('List must be an array or \Traversable');
322
        }
323
324 26
        $key = $this;
325 26
        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...
326 26
            $key = $key->deriveChild((int) $sequence);
327
        }
328
329 26
        return $key;
330
    }
331
332
    /**
333
     * Decodes a BIP32 path into actual 32bit sequence numbers and derives the child key
334
     *
335
     * @param string $path
336
     * @return HierarchicalKey
337
     * @throws \Exception
338
     */
339 26
    public function derivePath(string $path): HierarchicalKey
340
    {
341 26
        $sequences = new HierarchicalKeySequence();
342 26
        return $this->deriveFromList($sequences->decodePath($path));
343
    }
344
345
    /**
346
     * Serializes the instance according to whether it wraps a private or public key.
347
     * @param NetworkInterface $network
348
     * @return string
349
     */
350 16
    public function toExtendedKey(NetworkInterface $network = null): string
351
    {
352 16
        $network = $network ?: Bitcoin::getNetwork();
353
354 16
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
355 16
        $extended = $extendedSerializer->serialize($network, $this);
356 16
        return $extended;
357
    }
358
359
    /**
360
     * Explicitly serialize as a private key. Throws an exception if
361
     * the key isn't private.
362
     *
363
     * @param NetworkInterface $network
364
     * @return string
365
     */
366 16
    public function toExtendedPrivateKey(NetworkInterface $network = null): string
367
    {
368 16
        if (!$this->isPrivate()) {
369 2
            throw new \LogicException('Cannot create extended private key from public');
370
        }
371
372 14
        return $this->toExtendedKey($network);
373
    }
374
375
    /**
376
     * Explicitly serialize as a public key. This will always work.
377
     *
378
     * @param NetworkInterface $network
379
     * @return string
380
     */
381 13
    public function toExtendedPublicKey(NetworkInterface $network = null): string
382
    {
383 13
        return $this->withoutPrivateKey()->toExtendedKey($network);
384
    }
385
}
386