Completed
Push — master ( 460d12...eabe8c )
by thomas
24:52
created

HierarchicalKey   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 14

Test Coverage

Coverage 91.74%

Importance

Changes 0
Metric Value
dl 0
loc 360
ccs 100
cts 109
cp 0.9174
rs 8.8798
c 0
b 0
f 0
wmc 44
lcom 2
cbo 14

20 Methods

Rating   Name   Duplication   Size   Complexity  
A deriveChild() 0 28 4
B __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
A getHmacSeed() 0 18 5
A derivePath() 0 21 4
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\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 79
    public function __construct(EcAdapterInterface $ecAdapter, ScriptDataFactory $scriptDataFactory, int $depth, int $parentFingerprint, int $sequence, BufferInterface $chainCode, KeyInterface $key)
76
    {
77 79
        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 79
        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 79
        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 79
        if ($chainCode->getSize() !== 32) {
90
            throw new \RuntimeException('Chaincode should be 32 bytes');
91
        }
92
93 79
        if (!$key->isCompressed()) {
94 1
            throw new \InvalidArgumentException('A HierarchicalKey must always be compressed');
95
        }
96
97 78
        $this->ecAdapter = $ecAdapter;
98 78
        $this->depth = $depth;
99 78
        $this->sequence = $sequence;
100 78
        $this->parentFingerprint = $parentFingerprint;
101 78
        $this->chainCode = $chainCode;
102 78
        $this->key = $key;
103 78
        $this->scriptDataFactory = $scriptDataFactory;
104 78
    }
105
106
    /**
107
     * Return the depth of this key. This is limited to 256 sequential derivations.
108
     *
109
     * @return int
110
     */
111 34
    public function getDepth(): int
112
    {
113 34
        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 28
    public function getSequence(): int
124
    {
125 28
        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 30
    public function getFingerprint(): int
134
    {
135 30
        if ($this->getDepth() === 0) {
136 14
            return 0;
137
        }
138
139 26
        return $this->parentFingerprint;
140
    }
141
142
    /**
143
     * Return the fingerprint to be used for child keys.
144
     * @return int
145
     */
146 36
    public function getChildFingerprint(): int
147
    {
148 36
        $pubKeyHash = $this->getPublicKey()->getPubKeyHash();
149 36
        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 28
    public function getChainCode(): BufferInterface
159
    {
160 28
        return $this->chainCode;
161
    }
162
163
    /**
164
     * @return PrivateKeyInterface
165
     */
166 42
    public function getPrivateKey(): PrivateKeyInterface
167
    {
168 42
        if ($this->key->isPrivate()) {
169
            /** @var PrivateKeyInterface $key */
170 40
            $key = $this->key;
171 40
            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 50
    public function getPublicKey(): PublicKeyInterface
183
    {
184 50
        if ($this->isPrivate()) {
185 36
            return $this->getPrivateKey()->getPublicKey();
186
        } else {
187
            /** @var PublicKeyInterface $key */
188 33
            $key = $this->key;
189 33
            return $key;
190
        }
191
    }
192
193
    /**
194
     * @return HierarchicalKey
195
     */
196 21
    public function withoutPrivateKey(): HierarchicalKey
197
    {
198 21
        $clone = clone $this;
199 21
        $clone->key = $clone->getPublicKey();
200 21
        return $clone;
201
    }
202
203
    /**
204
     * @return ScriptDataFactory
205
     */
206 30
    public function getScriptDataFactory()
207
    {
208 30
        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 54
    public function isPrivate(): bool
238
    {
239 54
        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 43
    public function getHmacSeed(int $sequence): BufferInterface
260
    {
261 43
        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 37
        if (($sequence >> 31) === 1) {
266 32
            if ($this->isPrivate() === false) {
267 2
                throw new \Exception("Can't derive a hardened key without the private key");
268
            }
269
270 30
            $data = "\x00{$this->getPrivateKey()->getBinary()}";
271
        } else {
272 35
            $data = $this->getPublicKey()->getBinary();
273
        }
274
275 35
        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 40
    public function deriveChild(int $sequence): HierarchicalKey
286
    {
287 40
        $nextDepth = $this->depth + 1;
288 40
        if ($nextDepth > 255) {
289
            throw new \InvalidArgumentException('Invalid depth for BIP32 key, cannot exceed 255');
290
        }
291
292 40
        $hash = Hash::hmac('sha512', $this->getHmacSeed($sequence), $this->chainCode);
293 35
        $offset = $hash->slice(0, 32);
294 35
        $chain = $hash->slice(32);
295
296 35
        if (!$this->ecAdapter->validatePrivateKey($offset)) {
297 1
            throw new InvalidDerivationException("Derived invalid key for index {$sequence}, use next index");
298
        }
299
300 34
        $key = $this->isPrivate() ? $this->getPrivateKey() : $this->getPublicKey();
301 34
        $key = $key->tweakAdd($offset->getGmp());
302
303 34
        return new HierarchicalKey(
304 34
            $this->ecAdapter,
305 34
            $this->scriptDataFactory,
306 34
            $nextDepth,
307 34
            $this->getChildFingerprint(),
308 34
            $sequence,
309 34
            $chain,
310 34
            $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 26
    public function derivePath(string $path): HierarchicalKey
322
    {
323 26
        $sequences = new HierarchicalKeySequence();
324 26
        $parts = $sequences->decodeRelative($path);
325 26
        $numParts = count($parts);
326
327 26
        $key = $this;
328 26
        for ($i = 0; $i < $numParts; $i++) {
329
            try {
330 26
                $key = $key->deriveChild((int) $parts[$i]);
331
            } catch (InvalidDerivationException $e) {
332
                if ($i === $numParts - 1) {
333
                    throw new InvalidDerivationException($e->getMessage());
334
                } else {
335
                    throw new InvalidDerivationException("Invalid derivation for non-terminal index: cannot use this path!");
336
                }
337
            }
338
        }
339
340 26
        return $key;
341
    }
342
343
    /**
344
     * Serializes the instance according to whether it wraps a private or public key.
345
     * @param NetworkInterface $network
346
     * @return string
347
     */
348 18
    public function toExtendedKey(NetworkInterface $network = null): string
349
    {
350 18
        $network = $network ?: Bitcoin::getNetwork();
351
352 18
        $extendedSerializer = new Base58ExtendedKeySerializer(new ExtendedKeySerializer($this->ecAdapter));
353 18
        $extended = $extendedSerializer->serialize($network, $this);
354 18
        return $extended;
355
    }
356
357
    /**
358
     * Explicitly serialize as a private key. Throws an exception if
359
     * the key isn't private.
360
     *
361
     * @param NetworkInterface $network
362
     * @return string
363
     */
364 18
    public function toExtendedPrivateKey(NetworkInterface $network = null): string
365
    {
366 18
        if (!$this->isPrivate()) {
367 2
            throw new \LogicException('Cannot create extended private key from public');
368
        }
369
370 16
        return $this->toExtendedKey($network);
371
    }
372
373
    /**
374
     * Explicitly serialize as a public key. This will always work.
375
     *
376
     * @param NetworkInterface $network
377
     * @return string
378
     */
379 13
    public function toExtendedPublicKey(NetworkInterface $network = null): string
380
    {
381 13
        return $this->withoutPrivateKey()->toExtendedKey($network);
382
    }
383
}
384