File Coverage

blib/lib/Bitcoin/Crypto/Role/ExtendedKey.pm
Criterion Covered Total %
statement 120 122 98.3
branch 29 38 76.3
condition 25 32 78.1
subroutine 26 27 96.3
pod 0 7 0.0
total 200 226 88.5


line stmt bran cond sub pod time code
1             package Bitcoin::Crypto::Role::ExtendedKey;
2             $Bitcoin::Crypto::Role::ExtendedKey::VERSION = '1.008_01'; # TRIAL
3             $Bitcoin::Crypto::Role::ExtendedKey::VERSION = '1.00801';
4 8     8   62672 use v5.10;
  8         36  
5 8     8   54 use strict;
  8         18  
  8         199  
6 8     8   45 use warnings;
  8         21  
  8         260  
7 8     8   66 use List::Util qw(first);
  8         16  
  8         611  
8 8     8   61 use Scalar::Util qw(blessed);
  8         31  
  8         499  
9 8     8   58 use Mooish::AttributeBuilder -standard;
  8         22  
  8         94  
10              
11 8     8   4720 use Bitcoin::Crypto::Key::Private;
  8         28  
  8         295  
12 8     8   69 use Bitcoin::Crypto::Key::Public;
  8         18  
  8         205  
13 8     8   50 use Bitcoin::Crypto::Config;
  8         20  
  8         239  
14 8     8   46 use Bitcoin::Crypto::Types qw(IntMaxBits StrLength);
  8         40  
  8         99  
15 8     8   20940 use Bitcoin::Crypto::Util qw(get_path_info);
  8         22  
  8         420  
16 8     8   64 use Bitcoin::Crypto::Helpers qw(ensure_length hash160 verify_bytestring);
  8         45  
  8         441  
17 8     8   59 use Bitcoin::Crypto::Network;
  8         33  
  8         262  
18 8     8   52 use Bitcoin::Crypto::Base58 qw(encode_base58check decode_base58check);
  8         33  
  8         388  
19 8     8   51 use Bitcoin::Crypto::Exception;
  8         26  
  8         225  
20 8     8   49 use Moo::Role;
  8         22  
  8         74  
21              
22             has param 'depth' => (
23             isa => IntMaxBits [8],
24             default => 0
25             );
26              
27             has param 'parent_fingerprint' => (
28             isa => StrLength[4, 4],
29             default => (pack 'x4'),
30             );
31              
32             has param 'child_number' => (
33             isa => IntMaxBits [32],
34             default => 0
35             );
36              
37             has param 'chain_code' => (
38             isa => StrLength[32, 32],
39             );
40              
41             with qw(Bitcoin::Crypto::Role::Key);
42              
43             requires '_derive_key_partial';
44              
45             sub _get_network_extkey_version
46             {
47 293     293   725 my ($self, $network, $purpose) = @_;
48 293   66     1475 $network //= $self->network;
49 293   100     854 $purpose //= $self->purpose;
50              
51 293         478 my $name = 'ext';
52 293 100       675 $name .= $self->_is_private ? 'prv' : 'pub';
53 293 100 100     960 $name .= '_compat' if $purpose && $purpose eq 49;
54 293 100 100     890 $name .= '_segwit' if $purpose && $purpose eq 84;
55 293         436 $name .= '_version';
56              
57 293         1088 return $network->$name;
58             }
59              
60             sub to_serialized
61             {
62 117     117 0 2466 my ($self) = @_;
63              
64 117         356 my $version = $self->_get_network_extkey_version;
65              
66             # network field is not required, lazy check for completeness
67 117 50       316 Bitcoin::Crypto::Exception::NetworkConfig->raise(
68             'no extended key version found in network configuration'
69             ) unless defined $version;
70              
71             # version number (4B)
72 117         680 my $serialized = ensure_length pack('N', $version), 4;
73              
74             # depth (1B)
75 117         632 $serialized .= ensure_length pack('C', $self->depth), 1;
76              
77             # parent's fingerprint (4B) - ensured
78 117         423 $serialized .= $self->parent_fingerprint;
79              
80             # child number (4B)
81 117         485 $serialized .= ensure_length pack('N', $self->child_number), 4;
82              
83             # chain code (32B) - ensured
84 117         440 $serialized .= $self->chain_code;
85              
86             # key entropy (1 + 32B or 33B)
87 117         399 $serialized .= ensure_length $self->raw_key, Bitcoin::Crypto::Config::key_max_length + 1;
88              
89 117         442 return $serialized;
90             }
91              
92             sub from_serialized
93             {
94 32     32 0 93 my ($class, $serialized, $network) = @_;
95 32         106 verify_bytestring($serialized);
96              
97             # expected length is 78
98 32 50 33     225 if (defined $serialized && length $serialized == 78) {
99 32         82 my $format = 'a4aa4a4a32a33';
100 32         177 my ($version, $depth, $fingerprint, $number, $chain_code, $data) =
101             unpack($format, $serialized);
102              
103 32         113 my $is_private = pack('x') eq substr $data, 0, 1;
104              
105 32 0       125 Bitcoin::Crypto::Exception::KeyCreate->raise(
    50          
106             'invalid class used, key is ' . ($is_private ? 'private' : 'public')
107             ) if $is_private != $class->_is_private;
108              
109 32 100       105 $data = substr $data, 1, Bitcoin::Crypto::Config::key_max_length
110             if $is_private;
111              
112 32         113 $version = unpack 'N', $version;
113              
114 32         74 my $purpose;
115             my @found_networks;
116              
117 32         94 for my $check_purpose (qw(44 49 84)) {
118 44         79 $purpose = $check_purpose;
119              
120             @found_networks = Bitcoin::Crypto::Network->find(
121             sub {
122 176     176   335 my ($inst) = @_;
123 176         344 my $this_version = $class->_get_network_extkey_version($inst, $purpose);
124 176   100     802 return $this_version && $this_version eq $version;
125             }
126 44         388 );
127 44 50   0   229 @found_networks = first { $_ eq $network } @found_networks if defined $network;
  0         0  
128              
129 44 100       133 last if @found_networks > 0;
130             }
131              
132             Bitcoin::Crypto::Exception::KeyCreate->raise(
133 32 50       88 'found multiple networks possible for given serialized key'
134             ) if @found_networks > 1;
135              
136 32 50 33     105 Bitcoin::Crypto::Exception::KeyCreate->raise(
137             "network name $network cannot be used for given serialized key"
138             ) if @found_networks == 0 && defined $network;
139              
140 32 50       105 Bitcoin::Crypto::Exception::NetworkConfig->raise(
141             "couldn't find network for serialized key version $version"
142             ) if @found_networks == 0;
143              
144 32         844 my $key = $class->new(
145             key_instance => $data,
146             chain_code => $chain_code,
147             child_number => unpack('N', $number),
148             parent_fingerprint => $fingerprint,
149             depth => unpack('C', $depth),
150             network => $found_networks[0],
151             purpose => $purpose,
152             );
153              
154 32         205 return $key;
155             }
156             else {
157 0         0 Bitcoin::Crypto::Exception::KeyCreate->raise(
158             'input data does not look like a valid serialized extended key'
159             );
160             }
161             }
162              
163             sub to_serialized_base58
164             {
165 96     96 0 28544 my ($self) = @_;
166 96         307 my $serialized = $self->to_serialized();
167 96         447 return encode_base58check $serialized;
168             }
169              
170             sub from_serialized_base58
171             {
172 34     34 0 20867 my ($class, $base58, $network) = @_;
173 34         151 return $class->from_serialized(decode_base58check($base58), $network);
174             }
175              
176             sub get_basic_key
177             {
178 60     60 0 5538 my ($self) = @_;
179 60 100       172 my $base_class = 'Bitcoin::Crypto::Key::' . ($self->_is_private ? 'Private' : 'Public');
180 60         1415 my $basic_key = $base_class->new(
181             key_instance => $self->key_instance,
182             network => $self->network,
183             purpose => $self->purpose,
184             );
185              
186 60         246 return $basic_key;
187             }
188              
189             sub get_fingerprint
190             {
191 276     276 0 8064 my ($self, $len) = @_;
192 276   50     1402 $len //= 4;
193              
194 276         868 my $pubkey = $self->raw_key('public_compressed');
195 276         1032 my $identifier = hash160($pubkey);
196 276         11422 return substr $identifier, 0, 4;
197             }
198              
199             sub _get_purpose_from_BIP44
200             {
201 87     87   290 my ($self, $path) = @_;
202              
203             # NOTE: only handles BIP44 correctly when it is constructed with Bitcoin::Crypto::BIP44
204             # NOTE: when deriving new keys, we do not care about previous state:
205             # - if BIP44 is further derived, it is not BIP44 anymore
206             # - if BIP44 is derived as a new BIP44, the old one is like the new master key
207             # because of that, set purpose to undef if path is not BIP44
208              
209             return undef
210 87 100 66     1697 unless blessed $path && $path->isa('Bitcoin::Crypto::BIP44');
211              
212 24 100       330 return $self->purpose
213             if $path->get_from_account;
214              
215 14         344 return $path->purpose;
216             }
217              
218             sub derive_key
219             {
220 90     90 0 35629 my ($self, $path) = @_;
221 90         398 my $path_info = get_path_info $path;
222              
223 90 100       310 Bitcoin::Crypto::Exception::KeyDerive->raise(
224             'invalid key derivation path supplied'
225             ) unless defined $path_info;
226              
227             Bitcoin::Crypto::Exception::KeyDerive->raise(
228             'cannot derive private key from public key'
229 88 100 100     316 ) if !$self->_is_private && $path_info->{private};
230              
231 87         352 my $key = $self;
232 87         161 for my $child_num (@{$path_info->{path}}) {
  87         314  
233 276         639 my $hardened = $child_num >= Bitcoin::Crypto::Config::max_child_keys;
234              
235             # dies if hardened-from-public requested
236             # dies if key is invalid
237 276         875 $key = $key->_derive_key_partial($child_num, $hardened);
238             }
239              
240 87         576 $key->set_network($self->network);
241 87         335 $key->set_purpose($self->_get_purpose_from_BIP44($path));
242              
243             $key = $key->get_public_key()
244 87 100 100     2721 if $self->_is_private && !$path_info->{private};
245              
246 87         554 return $key;
247             }
248              
249             1;
250