File Coverage

blib/lib/Bitcoin/Crypto/Role/ExtendedKey.pm
Criterion Covered Total %
statement 57 64 89.0
branch 10 10 100.0
condition 16 18 88.8
subroutine 17 19 89.4
pod 0 2 0.0
total 100 113 88.5


line stmt bran cond sub pod time code
1             package Bitcoin::Crypto::Role::ExtendedKey;
2             $Bitcoin::Crypto::Role::ExtendedKey::VERSION = '2.000_01'; # TRIAL
3             $Bitcoin::Crypto::Role::ExtendedKey::VERSION = '2.00001';
4 9     9   80549 use v5.10;
  9         54  
5 9     9   170 use strict;
  9         117  
  9         316  
6 9     9   93 use warnings;
  9         25  
  9         308  
7 9     9   60 use Scalar::Util qw(blessed);
  9         27  
  9         565  
8 9     9   60 use Mooish::AttributeBuilder -standard;
  9         36  
  9         128  
9 9     9   1304 use Type::Params -sigs;
  9         23  
  9         83  
10              
11 9     9   8313 use Bitcoin::Crypto::Key::Private;
  9         41  
  9         355  
12 9     9   67 use Bitcoin::Crypto::Key::Public;
  9         28  
  9         196  
13 9     9   45 use Bitcoin::Crypto::Constants;
  9         32  
  9         310  
14 9     9   48 use Bitcoin::Crypto::Types qw(IntMaxBits StrLength Str Object Maybe ByteStr PositiveInt InstanceOf);
  9         42  
  9         79  
15 9     9   35199 use Bitcoin::Crypto::Util qw(get_path_info hash160 to_format);
  9         40  
  9         533  
16 9     9   70 use Bitcoin::Crypto::Helpers qw(ensure_length carp_once);
  9         32  
  9         389  
17 9     9   86 use Bitcoin::Crypto::Network;
  9         23  
  9         244  
18 9     9   53 use Bitcoin::Crypto::Exception;
  9         28  
  9         260  
19 9     9   61 use Moo::Role;
  9         23  
  9         46  
20              
21             has param 'depth' => (
22             isa => IntMaxBits [8],
23             default => 0
24             );
25              
26             has param 'parent_fingerprint' => (
27             isa => StrLength [4, 4],
28             default => (pack 'x4'),
29             );
30              
31             has param 'child_number' => (
32             isa => IntMaxBits [32],
33             default => 0
34             );
35              
36             has param 'chain_code' => (
37             isa => StrLength [32, 32],
38             );
39              
40             with qw(Bitcoin::Crypto::Role::Key);
41              
42             requires '_derive_key_partial';
43              
44             sub _get_network_extkey_version
45             {
46 295     295   588 my ($self, $network, $purpose) = @_;
47 295   66     1064 $network //= $self->network;
48 295   100     851 $purpose //= $self->purpose;
49              
50 295         463 my $name = 'ext';
51 295 100       712 $name .= $self->_is_private ? 'prv' : 'pub';
52 295 100 100     1073 $name .= '_compat' if $purpose && $purpose eq Bitcoin::Crypto::Constants::bip44_compat_purpose;
53 295 100 100     865 $name .= '_segwit' if $purpose && $purpose eq Bitcoin::Crypto::Constants::bip44_segwit_purpose;
54 295         460 $name .= '_version';
55              
56 295         989 return $network->$name;
57             }
58              
59             signature_for to_serialized => (
60             method => Object,
61             positional => [],
62             );
63              
64             sub to_serialized
65             {
66             my ($self) = @_;
67              
68             my $version = $self->_get_network_extkey_version;
69              
70             # network field is not required, lazy check for completeness
71             Bitcoin::Crypto::Exception::NetworkConfig->raise(
72             'no extended key version found in network configuration'
73             ) unless defined $version;
74              
75             # version number (4B)
76             my $serialized = ensure_length pack('N', $version), 4;
77              
78             # depth (1B)
79             $serialized .= ensure_length pack('C', $self->depth), 1;
80              
81             # parent's fingerprint (4B) - ensured
82             $serialized .= $self->parent_fingerprint;
83              
84             # child number (4B)
85             $serialized .= ensure_length pack('N', $self->child_number), 4;
86              
87             # chain code (32B) - ensured
88             $serialized .= $self->chain_code;
89              
90             # key entropy (1 + 32B or 33B)
91             $serialized .= ensure_length $self->raw_key, Bitcoin::Crypto::Constants::key_max_length + 1;
92              
93             return $serialized;
94             }
95              
96             signature_for from_serialized => (
97             method => Str,
98             positional => [ByteStr, Maybe [Str], {optional => 1}],
99             );
100              
101             sub from_serialized
102             {
103             my ($class, $serialized, $network) = @_;
104              
105             # expected length is 78
106             if (defined $serialized && length $serialized == 78) {
107             my $format = 'a4aa4a4a32a33';
108             my ($version, $depth, $fingerprint, $number, $chain_code, $data) =
109             unpack($format, $serialized);
110              
111             my $is_private = pack('x') eq substr $data, 0, 1;
112              
113             Bitcoin::Crypto::Exception::KeyCreate->raise(
114             'invalid class used, key is ' . ($is_private ? 'private' : 'public')
115             ) if $is_private != $class->_is_private;
116              
117             $data = substr $data, 1, Bitcoin::Crypto::Constants::key_max_length
118             if $is_private;
119              
120             $version = unpack 'N', $version;
121              
122             my $purpose;
123             my @found_networks;
124              
125             for my $check_purpose (
126             Bitcoin::Crypto::Constants::bip44_purpose,
127             Bitcoin::Crypto::Constants::bip44_compat_purpose,
128             Bitcoin::Crypto::Constants::bip44_segwit_purpose
129             )
130             {
131             $purpose = $check_purpose;
132              
133             @found_networks = Bitcoin::Crypto::Network->find(
134             sub {
135             my ($inst) = @_;
136             my $this_version = $class->_get_network_extkey_version($inst, $purpose);
137             return $this_version && $this_version eq $version;
138             }
139             );
140             @found_networks = grep { $_ eq $network } @found_networks
141             if defined $network;
142              
143             last if @found_networks > 0;
144             }
145              
146             Bitcoin::Crypto::Exception::KeyCreate->raise(
147             'found multiple networks possible for given serialized key'
148             ) if @found_networks > 1;
149              
150             Bitcoin::Crypto::Exception::KeyCreate->raise(
151             "network name $network cannot be used for given serialized key"
152             ) if @found_networks == 0 && defined $network;
153              
154             Bitcoin::Crypto::Exception::NetworkConfig->raise(
155             "couldn't find network for serialized key version $version"
156             ) if @found_networks == 0;
157              
158             my $key = $class->new(
159             key_instance => $data,
160             chain_code => $chain_code,
161             child_number => unpack('N', $number),
162             parent_fingerprint => $fingerprint,
163             depth => unpack('C', $depth),
164             network => $found_networks[0],
165             purpose => $purpose,
166             );
167              
168             return $key;
169             }
170             else {
171             Bitcoin::Crypto::Exception::KeyCreate->raise(
172             'input data does not look like a valid serialized extended key'
173             );
174             }
175             }
176              
177             signature_for get_basic_key => (
178             method => Object,
179             positional => [],
180             );
181              
182             sub get_basic_key
183             {
184             my ($self) = @_;
185             my $base_class = 'Bitcoin::Crypto::Key::' . ($self->_is_private ? 'Private' : 'Public');
186             my $basic_key = $base_class->new(
187             key_instance => $self->key_instance,
188             network => $self->network,
189             purpose => $self->purpose,
190             );
191              
192             return $basic_key;
193             }
194              
195             signature_for get_fingerprint => (
196             method => Object,
197             positional => [PositiveInt, {default => 4}],
198             );
199              
200             sub get_fingerprint
201             {
202             my ($self, $len) = @_;
203              
204             my $pubkey = $self->raw_key('public_compressed');
205             my $identifier = hash160($pubkey);
206             return substr $identifier, 0, 4;
207             }
208              
209             sub _get_purpose_from_BIP44
210             {
211 73     73   211 my ($self, $path) = @_;
212              
213             # NOTE: only handles BIP44 correctly when it is constructed with Bitcoin::Crypto::BIP44
214             # NOTE: when deriving new keys, we do not care about previous state:
215             # - if BIP44 is further derived, it is not BIP44 anymore
216             # - if BIP44 is derived as a new BIP44, the old one is like the new master key
217             # because of that, set purpose to undef if path is not BIP44
218              
219             return undef
220 73 100 66     1325 unless blessed $path && $path->isa('Bitcoin::Crypto::BIP44');
221              
222 29 100 100     436 return $self->purpose
223             if $path->get_from_account || $path->public;
224              
225 17         412 return $path->purpose;
226             }
227              
228             signature_for derive_key => (
229             method => Object,
230             positional => [Str | InstanceOf ['Bitcoin::Crypto::BIP44']],
231             );
232              
233             sub derive_key
234             {
235             my ($self, $path) = @_;
236             my $path_info = get_path_info $path;
237              
238             Bitcoin::Crypto::Exception::KeyDerive->raise(
239             'invalid key derivation path supplied'
240             ) unless defined $path_info;
241              
242             Bitcoin::Crypto::Exception::KeyDerive->raise(
243             'cannot derive key: key type mismatch'
244             ) if !!$self->_is_private ne !!$path_info->{private};
245              
246             my $key = $self;
247             for my $child_num (@{$path_info->{path}}) {
248             my $hardened = $child_num >= Bitcoin::Crypto::Constants::max_child_keys;
249              
250             # dies if hardened-from-public requested
251             # dies if key is invalid
252             $key = $key->_derive_key_partial($child_num, $hardened);
253             }
254              
255             $key->set_network($self->network);
256             $key->set_purpose($self->_get_purpose_from_BIP44($path));
257              
258             return $key;
259             }
260              
261             ### DEPRECATED
262              
263             sub to_serialized_base58
264             {
265 0     0 0   my ($self) = @_;
266              
267 0           my $class = ref $self;
268 0           carp_once "$class->to_serialized_base58 is now deprecated. Use to_format [base58 => $class->to_serialized] instead";
269              
270 0           return to_format [base58 => $self->to_serialized];
271             }
272              
273             sub from_serialized_base58
274             {
275 0     0 0   my ($class, $base58, $network) = @_;
276              
277 0           carp_once
278             "$class->from_serialized_base58(\$base58) is now deprecated. Use $class->from_serialized([base58 => \$base58]) instead";
279              
280 0           return $class->from_serialized([base58 => $base58], $network);
281             }
282              
283             1;
284