HierarchicalKey   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Test Coverage

Coverage 91.38%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 109
c 5
b 0
f 0
dl 0
loc 369
ccs 106
cts 116
cp 0.9138
rs 8.48
wmc 49

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getFingerprint() 0 7 2
A isHardened() 0 3 1
A getScriptAndSignData() 0 7 2
A getAddress() 0 3 1
B __construct() 0 29 9
A getDepth() 0 3 1
A getChainCode() 0 3 1
A getPrivateKey() 0 9 2
A getSequence() 0 3 1
A isPrivate() 0 3 1
A getChildFingerprint() 0 4 1
A getScriptDataFactory() 0 3 1
A getPublicKey() 0 8 2
A getHmacSeed() 0 17 5
A withoutPrivateKey() 0 5 1
A deriveChild() 0 26 4
A toExtendedPublicKey() 0 3 1
B derivePath() 0 31 9
A toExtendedPrivateKey() 0 7 2
A toExtendedKey() 0 7 2

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.

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