File Coverage

blib/lib/Algorithm/Kademlia.pm
Criterion Covered Total %
statement 25 26 96.1
branch 4 4 100.0
condition n/a
subroutine 5 5 100.0
pod 2 2 100.0
total 36 37 97.3


line stmt bran cond sub pod time code
1 4     4   832607 use v5.42;
  4         16  
2 4     4   1808 use experimental 'class';
  4         15141  
  4         23  
3             #
4             package Algorithm::Kademlia v1.0.1 {
5 4     4   2682 use parent 'Exporter';
  4         1211  
  4         26  
6             our @EXPORT_OK = qw[xor_distance xor_bucket_index];
7             #
8 1     1 1 212090 sub xor_distance ( $id1_bin, $id2_bin ) { $id1_bin^.$id2_bin }
  1         4  
  1         2  
  1         2  
  1         6  
9              
10 9     9 1 2967 sub xor_bucket_index ( $id1_bin, $id2_bin ) {
  9         16  
  9         26  
  9         14  
11 9         26 my $dist = $id1_bin^.$id2_bin;
12 9         51 my @bytes = unpack( 'C*', $dist );
13 9         21 my $len = scalar @bytes;
14 9         36 for my $i ( 0 .. $#bytes ) {
15 40 100       102 next if $bytes[$i] == 0;
16 9         16 my $byte = $bytes[$i];
17 9         29 for ( my $j = 7; $j >= 0; $j-- ) {
18              
19             # Standard Kademlia: bucket i covers distance [2^i, 2^{i+1})
20 58 100       213 return ( ( $len - 1 - $i ) * 8 ) + $j if $byte & ( 1 << $j );
21             }
22             }
23 0           return undef; # Same ID
24             }
25             class Algorithm::Kademlia::RoutingTable v1.0.1 {
26             field $local_id_bin : param : writer : reader;
27             field $k : param //= 20;
28             field @buckets : reader;
29             #
30             ADJUST {
31             my $id_len = length $local_id_bin;
32             my $num_buckets = $id_len * 8;
33             @buckets = map { [] } 0 .. $num_buckets - 1
34             }
35              
36             method add_peer ( $peer_id_bin, $peer_data ) {
37             my $idx = Algorithm::Kademlia::xor_bucket_index( $local_id_bin, $peer_id_bin );
38             return undef unless defined $idx;
39             my $bucket = $buckets[$idx];
40              
41             # Find existing
42             my $existing_idx = -1;
43             for my $i ( 0 .. $#$bucket ) {
44             if ( $bucket->[$i]{id} eq $peer_id_bin ) {
45             $existing_idx = $i;
46             last;
47             }
48             }
49             if ( $existing_idx != -1 ) { # Move to tail (most recent)
50             my $peer = splice( @$bucket, $existing_idx, 1 );
51             $peer->{data} = $peer_data; # Update data
52             push @$bucket, $peer;
53             return undef;
54             }
55             if ( scalar @$bucket < $k ) {
56             push @$bucket, { id => $peer_id_bin, data => $peer_data };
57             return undef;
58             }
59             $bucket->[0]; # Bucket is full. Return oldest peer to be pinged.
60             }
61              
62             method evict_peer ($peer_id_bin) {
63             my $idx = Algorithm::Kademlia::xor_bucket_index( $local_id_bin, $peer_id_bin ) // return;
64             my $bucket = $buckets[$idx];
65             @$bucket = grep { $_->{id} ne $peer_id_bin } @$bucket;
66             }
67              
68             method find_closest ( $target_id_bin, $count = undef ) {
69             $count //= $k;
70             my @all_peers;
71             push @all_peers, @$_ for @buckets;
72             my @sorted = sort { ( $a->{id} ^.$target_id_bin ) cmp( $b->{id} ^.$target_id_bin ) } @all_peers;
73             splice @sorted, 0, $count;
74             }
75              
76             method size () {
77             my $count = 0;
78             $count += scalar @$_ for @buckets;
79             $count;
80             }
81              
82             method import_peers ($peer_list) {
83             for my $p (@$peer_list) {
84              
85             # Directly push to avoid eviction logic during restore
86             my $idx = Algorithm::Kademlia::xor_bucket_index( $local_id_bin, $p->{id} );
87             next unless defined $idx;
88             push $buckets[$idx]->@*, $p;
89             }
90             }
91             };
92             class Algorithm::Kademlia::Storage v1.0.1 {
93             field %store : reader(entries);
94             field $ttl : reader : param //= 86400; # 24 hours
95              
96             method put ( $key_bin, $value_bin, $publisher_id_bin //= (), $seeds //= [], $leechers //= [] ) {
97             $store{$key_bin} = Algorithm::Kademlia::Storage::Entry->new(
98             key => $key_bin,
99             value => $value_bin,
100             time => time(),
101             publisher => $publisher_id_bin,
102             seeds => $seeds,
103             leechers => $leechers
104             );
105             }
106              
107             method get ($key_bin) {
108             my $entry = $store{$key_bin} // return;
109             if ( time() - $entry->time > $ttl ) {
110             delete $store{$key_bin};
111             return undef;
112             }
113             $entry; # Return the full entry now
114             }
115             };
116             class Algorithm::Kademlia::Storage::Entry v1.0.1 {
117             field $key : param : reader;
118             field $leechers : param : reader : writer;
119             field $seeds : param : reader : writer;
120             field $time : param : reader : writer;
121             field $value : param : reader : writer;
122             field $publisher : param : reader;
123             };
124             class Algorithm::Kademlia::Search v1.0.1 {
125             field $target_id_bin : param;
126             field $k : param //= 20;
127             field $alpha : param //= 3;
128             field %nodes; # id_bin -> { data => ..., queried => 0, responded => 0, failed => 0 }
129              
130             method add_candidates (@peers) {
131             for my $peer (@peers) {
132             my $id = $peer->{id};
133             next if $nodes{$id} && ( $nodes{$id}{queried} || $nodes{$id}{failed} );
134             $nodes{$id} //= { data => $peer->{data}, queried => 0, responded => 0, failed => 0 };
135             }
136             }
137              
138             method pending_queries () {
139             grep { $_->{queried} && !$_->{responded} && !$_->{failed} } values %nodes;
140             }
141              
142             method next_to_query () {
143             my @sorted = sort { ( $a^.$target_id_bin ) cmp( $b^.$target_id_bin ) } keys %nodes;
144             my @to_query;
145             for my $id (@sorted) {
146             next if $nodes{$id}{queried} || $nodes{$id}{failed};
147             push @to_query, { id => $id, data => $nodes{$id}{data} };
148             $nodes{$id}{queried} = 1;
149             last if @to_query >= $alpha;
150             }
151             @to_query;
152             }
153              
154             method mark_responded ( $id_bin, @new_peers ) {
155             return unless $nodes{$id_bin};
156             $nodes{$id_bin}{responded} = 1;
157             $self->add_candidates(@new_peers);
158             }
159              
160             method mark_failed ($id_bin) {
161             return unless $nodes{$id_bin};
162             $nodes{$id_bin}{failed} = 1;
163             }
164              
165             method best_results () {
166             my @sorted = sort { ( $a^.$target_id_bin ) cmp( $b^.$target_id_bin ) } grep { $nodes{$_}{responded} } keys %nodes;
167             my @results = map { { id => $_, data => $nodes{$_}{data} } } splice( @sorted, 0, $k );
168             @results;
169             }
170              
171             method is_finished () {
172             my @responded = grep { $_->{responded} } values %nodes;
173             return 1 if @responded >= $k;
174             my @available = grep { !$_->{queried} && !$_->{failed} } values %nodes;
175             return 1 if !@available && !$self->pending_queries;
176             0;
177             }
178             };
179             };
180             1;