File Coverage

blib/lib/Blockchain/Ethereum/RLP.pm
Criterion Covered Total %
statement 90 92 97.8
branch 32 38 84.2
condition 17 24 70.8
subroutine 10 10 100.0
pod 2 2 100.0
total 151 166 90.9


line stmt bran cond sub pod time code
1 4     4   418259 use v5.26;
  4         52  
2 4     4   2583 use Object::Pad;
  4         45847  
  4         19  
3              
4             package Blockchain::Ethereum::RLP 0.006;
5             class Blockchain::Ethereum::RLP;
6              
7             =encoding utf8
8              
9             =head1 NAME
10              
11             Blockchain::Ethereum::RLP - Ethereum RLP encoding/decoding utility
12              
13             =head1 SYNOPSIS
14              
15             Allow RLP encoding and decoding
16              
17             my $rlp = Blockchain::Ethereum::RLP->new();
18              
19             my $tx_params = ['0x9', '0x4a817c800', '0x5208', '0x3535353535353535353535353535353535353535', '0xde0b6b3a7640000', '0x', '0x1', '0x', '0x'];
20             my $encoded = $rlp->encode($params); #ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080
21              
22             my $encoded_tx_params = 'ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080';
23             my $decoded = $rlp->decode(pack "H*", $encoded_tx_params); #['0x9', '0x4a817c800', '0x5208', '0x3535353535353535353535353535353535353535', '0xde0b6b3a7640000', '0x', '0x1', '0x', '0x']
24              
25             =cut
26              
27 4     4   1866 use Carp;
  4         16  
  4         361  
28              
29             use constant {
30 4         9521 STRING => 'string',
31             LIST => 'list',
32             SINGLE_BYTE_MAX_LENGTH => 128,
33             SHORT_STRING_MAX_LENGTH => 183,
34             LONG_STRING_MAX_LENGTH => 192,
35             LIST_MAX_LENGTH => 247,
36             LONG_LIST_MAX_LENGTH => 255,
37             BYTE_LENGTH_DELIMITER => 55,
38             INPUT_LENGTH_DELIMITER => 256,
39 4     4   28 };
  4         21  
40              
41             =head2 encode
42              
43             Encodes the given input to RLP
44              
45             Usage:
46              
47             encode(hex string / hex array reference) -> encoded bytes
48              
49             =over 4
50              
51             =item * C<$input> hexadecimal string or reference to an hexadecimal string array
52              
53             =back
54              
55             Return the encoded bytes
56              
57             =cut
58              
59 65     65 1 10604 method encode ($input) {
  65         92  
  65         101  
  65         83  
60              
61 65 50       125 croak 'No input given' unless defined $input;
62              
63 65 100       143 if (ref $input eq 'ARRAY') {
64 16         24 my $output = '';
65 16         48 $output .= $self->encode($_) for $input->@*;
66              
67 16         34 return $self->_encode_length(length($output), LONG_STRING_MAX_LENGTH) . $output;
68             }
69              
70 49         160 my $hex = $input =~ s/^0x//r;
71              
72             # zero will be considered empty as per RLP specification
73 49 100       104 unless ($hex) {
74 11         18 $hex = chr(0x80);
75 11         35 return $hex;
76             }
77              
78             # pack will add a null character at the end if the length is odd
79             # RLP expects this to be added at the left instead.
80 38 100       107 $hex = "0$hex" if length($hex) % 2 != 0;
81 38         114 $hex = pack("H*", $hex);
82              
83 38         66 my $input_length = length $hex;
84              
85 38 100 100     124 return $hex if $input_length == 1 && ord $hex < SINGLE_BYTE_MAX_LENGTH;
86 31         64 return $self->_encode_length($input_length, SINGLE_BYTE_MAX_LENGTH) . $hex;
87             }
88              
89 47     47   83 method _encode_length ($length, $offset) {
  47         78  
  47         66  
  47         91  
  47         58  
90              
91 47 100       234 return chr($length + $offset) if $length <= BYTE_LENGTH_DELIMITER;
92              
93 5 50       18 if ($length < INPUT_LENGTH_DELIMITER**8) {
94 5         18 my $bl = $self->_to_binary($length);
95 5         48 return chr(length($bl) + $offset + BYTE_LENGTH_DELIMITER) . $bl;
96             }
97              
98 0         0 croak "Input too long";
99             }
100              
101 10     10   20 method _to_binary ($x) {
  10         14  
  10         17  
  10         13  
102              
103 10 100       47 return '' unless $x;
104 5         27 return $self->_to_binary(int($x / INPUT_LENGTH_DELIMITER)) . chr($x % INPUT_LENGTH_DELIMITER);
105             }
106              
107             =head2 decode
108              
109             Decode the given input from RLP to the specific return type
110              
111             Usage:
112              
113             decode(RLP encoded bytes) -> hexadecimal string / array reference
114              
115             =over 4
116              
117             =item * C<$input> RLP encoded bytes
118              
119             =back
120              
121             Returns an hexadecimals string or an array reference in case of multiple items
122              
123             =cut
124              
125 65     65 1 14895 method decode ($input) {
  65         92  
  65         124  
  65         82  
126              
127 65 50       136 return [] unless length $input;
128              
129 65         126 my ($offset, $data_length, $type) = $self->_decode_length($input);
130              
131             # string
132 65 100       163 if ($type eq STRING) {
133 49         142 my $hex = unpack("H*", substr($input, $offset, $data_length));
134             # same as for the encoding we do expect an prefixed 0 for
135             # odd length hexadecimal values, this just removes the 0 prefix.
136 49 100 66     187 $hex = substr($hex, 1) if $hex =~ /^0/ && (length($hex) - 1) % 2 != 0;
137 49         137 return '0x' . $hex;
138             }
139              
140             # list
141 16         26 my @output;
142 16         34 my $list_data = substr($input, $offset, $data_length);
143 16         26 my $list_offset = 0;
144             # recursive arrays
145 16         52 while ($list_offset < length($list_data)) {
146 51         135 my ($item_offset, $item_length, $item_type) = $self->_decode_length(substr($list_data, $list_offset));
147 51         179 my $list_item = $self->decode(substr($list_data, $list_offset, $item_offset + $item_length));
148 51         111 push @output, $list_item;
149 51         112 $list_offset += $item_offset + $item_length;
150             }
151              
152 16         43 return \@output;
153             }
154              
155 116     116   192 method _decode_length ($input) {
  116         156  
  116         203  
  116         148  
156              
157 116         163 my $length = length($input);
158 116 50       202 croak "Invalid empty input" unless $length;
159              
160 116         192 my $prefix = ord(substr($input, 0, 1));
161              
162 116         161 my $short_string = $prefix - SINGLE_BYTE_MAX_LENGTH;
163 116         161 my $long_string = $prefix - SHORT_STRING_MAX_LENGTH;
164 116         160 my $list = $prefix - LONG_STRING_MAX_LENGTH;
165 116         182 my $long_list = $prefix - LIST_MAX_LENGTH;
166              
167 116 100 66     506 if ($prefix < SINGLE_BYTE_MAX_LENGTH) {
    100 100        
    100 100        
    100 66        
    50 33        
      33        
168             # single byte
169 13         54 return (0, 1, STRING);
170             } elsif ($prefix <= SHORT_STRING_MAX_LENGTH && $length > $short_string) {
171             # short string
172 75         230 return (1, $short_string, STRING);
173             } elsif ($prefix <= LONG_STRING_MAX_LENGTH
174             && $length > $long_string
175             && $length > $long_string + $self->_to_integer(substr($input, 1, $long_string)))
176             {
177             # long string
178 3         8 my $str_length = $self->_to_integer(substr($input, 1, $long_string));
179 3         12 return (1 + $long_string, $str_length, STRING);
180             } elsif ($prefix < LIST_MAX_LENGTH && $length > $list) {
181             # list
182 22         66 return (1, $list, LIST);
183             } elsif ($prefix <= LONG_LIST_MAX_LENGTH
184             && $length > $long_list
185             && $length > $long_list + $self->_to_integer(substr($input, 1, $long_list)))
186             {
187             # long list
188 3         11 my $list_length = $self->_to_integer(substr($input, 1, $long_list));
189 3         15 return (1 + $long_list, $list_length, LIST);
190             }
191              
192 0         0 croak "Invalid RLP input";
193             }
194              
195 30     30   60 method _to_integer ($b) {
  30         37  
  30         73  
  30         39  
196              
197 30         46 my $length = length($b);
198 30 50       55 croak "Invalid empty input" unless $length;
199              
200 30 100       107 return ord($b) if $length == 1;
201              
202 16         56 return ord(substr($b, -1)) + $self->_to_integer(substr($b, 0, -1)) * INPUT_LENGTH_DELIMITER;
203             }
204              
205             1;
206              
207             __END__
208              
209             =head1 AUTHOR
210              
211             Reginaldo Costa, C<< <refeco at cpan.org> >>
212              
213             =head1 BUGS
214              
215             Please report any bugs or feature requests to L<https://github.com/refeco/perl-RPL>
216              
217             =head1 LICENSE AND COPYRIGHT
218              
219             This software is Copyright (c) 2023 by REFECO.
220              
221             This is free software, licensed under:
222              
223             The MIT License
224              
225             =cut