| 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; |