devp2p 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +22 -0
- data/lib/devp2p.rb +57 -0
- data/lib/devp2p/app_helper.rb +85 -0
- data/lib/devp2p/base_app.rb +80 -0
- data/lib/devp2p/base_protocol.rb +136 -0
- data/lib/devp2p/base_service.rb +55 -0
- data/lib/devp2p/command.rb +82 -0
- data/lib/devp2p/configurable.rb +32 -0
- data/lib/devp2p/connection_monitor.rb +77 -0
- data/lib/devp2p/control.rb +32 -0
- data/lib/devp2p/crypto.rb +73 -0
- data/lib/devp2p/crypto/ecc_x.rb +133 -0
- data/lib/devp2p/crypto/ecies.rb +134 -0
- data/lib/devp2p/discovery.rb +118 -0
- data/lib/devp2p/discovery/address.rb +83 -0
- data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
- data/lib/devp2p/discovery/node.rb +32 -0
- data/lib/devp2p/discovery/protocol.rb +342 -0
- data/lib/devp2p/discovery/transport.rb +105 -0
- data/lib/devp2p/exception.rb +30 -0
- data/lib/devp2p/frame.rb +197 -0
- data/lib/devp2p/kademlia.rb +48 -0
- data/lib/devp2p/kademlia/k_bucket.rb +178 -0
- data/lib/devp2p/kademlia/node.rb +40 -0
- data/lib/devp2p/kademlia/protocol.rb +284 -0
- data/lib/devp2p/kademlia/routing_table.rb +131 -0
- data/lib/devp2p/kademlia/wire_interface.rb +30 -0
- data/lib/devp2p/multiplexed_session.rb +110 -0
- data/lib/devp2p/multiplexer.rb +358 -0
- data/lib/devp2p/p2p_protocol.rb +170 -0
- data/lib/devp2p/packet.rb +35 -0
- data/lib/devp2p/peer.rb +329 -0
- data/lib/devp2p/peer_errors.rb +35 -0
- data/lib/devp2p/peer_manager.rb +274 -0
- data/lib/devp2p/rlpx_session.rb +434 -0
- data/lib/devp2p/sync_queue.rb +76 -0
- data/lib/devp2p/utils.rb +106 -0
- data/lib/devp2p/version.rb +13 -0
- data/lib/devp2p/wired_service.rb +30 -0
- metadata +227 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c9730d697b44cd5f35301e1010636cfa941e30db
|
4
|
+
data.tar.gz: dc7bed10ffed00dc8353e459906014d541738fcc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3b8635533c9b4e6211972ea1d0e3973317553b991dcfd664823ed3cbee5c627b7f68e72d411974d47ceafe8eb241e9137563af6b944f09549b18b56f178c3e45
|
7
|
+
data.tar.gz: 974eae8a51cc88d2668b07c4baa0f75aa305ab59b283307abea19c7edccc9cf5f84e2567dceb223f96739c820394e597fd0c5deb853172d96fab951de00812c1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Jan Xie
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# ruby-devp2p
|
2
|
+
|
3
|
+
[](LICENSE)
|
4
|
+
[](https://travis-ci.org/janx/ruby-devp2p)
|
5
|
+
|
6
|
+
A ruby implementation of Ethereum's DEVp2p framework.
|
7
|
+
|
8
|
+
## Fiber Stack Size
|
9
|
+
|
10
|
+
DEVp2p is build on [Celluloid](https://github.com/celluloid/celluloid/), which
|
11
|
+
uses fibers to schedule tasks. Ruby's default limit on fiber stack size is quite
|
12
|
+
small, which need to be increased by setting environment variables:
|
13
|
+
|
14
|
+
```
|
15
|
+
export RUBY_FIBER_VM_STACK_SIZE=104857600 # 100MB
|
16
|
+
export RUBY_FIBER_MACHINE_STACK_SIZE=1048576000
|
17
|
+
```
|
18
|
+
|
19
|
+
## Resources
|
20
|
+
|
21
|
+
* [DEVp2p Whitepaper](https://github.com/ethereum/wiki/wiki/libp2p-Whitepaper)
|
22
|
+
* [RLPx](https://github.com/ethereum/devp2p/blob/master/rlpx.md)
|
data/lib/devp2p.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'block_logger'
|
4
|
+
require 'celluloid/io'
|
5
|
+
|
6
|
+
require 'rlp'
|
7
|
+
|
8
|
+
#Celluloid.exception_handler do |ex|
|
9
|
+
# puts "!!!!! Exception raised in celluloid actor:"
|
10
|
+
# puts ex
|
11
|
+
# puts ex.backtrace[0,20].join("\n")
|
12
|
+
#end
|
13
|
+
|
14
|
+
module DEVp2p
|
15
|
+
Logger = BlockLogger
|
16
|
+
|
17
|
+
TT16 = 2**16
|
18
|
+
TT256 = 2**256
|
19
|
+
|
20
|
+
NODE_URI_SCHEME = 'enode://'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'devp2p/version'
|
24
|
+
|
25
|
+
require 'devp2p/configurable'
|
26
|
+
require 'devp2p/utils'
|
27
|
+
require 'devp2p/exception'
|
28
|
+
|
29
|
+
require 'devp2p/control'
|
30
|
+
require 'devp2p/base_service'
|
31
|
+
require 'devp2p/wired_service'
|
32
|
+
|
33
|
+
require 'devp2p/kademlia'
|
34
|
+
require 'devp2p/discovery'
|
35
|
+
|
36
|
+
require 'devp2p/sync_queue'
|
37
|
+
|
38
|
+
require 'devp2p/packet'
|
39
|
+
require 'devp2p/frame'
|
40
|
+
require 'devp2p/multiplexer'
|
41
|
+
|
42
|
+
require 'devp2p/crypto'
|
43
|
+
require 'devp2p/rlpx_session'
|
44
|
+
require 'devp2p/multiplexed_session'
|
45
|
+
|
46
|
+
require 'devp2p/command'
|
47
|
+
require 'devp2p/base_protocol'
|
48
|
+
require 'devp2p/connection_monitor'
|
49
|
+
require 'devp2p/p2p_protocol'
|
50
|
+
|
51
|
+
require 'devp2p/peer'
|
52
|
+
require 'devp2p/peer_errors'
|
53
|
+
require 'devp2p/peer_manager'
|
54
|
+
|
55
|
+
require 'devp2p/base_app'
|
56
|
+
require 'devp2p/app_helper'
|
57
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
class AppHelper
|
5
|
+
|
6
|
+
def run(app_class, service_class, num_nodes: 3, seed: 0, min_peers: 2, max_peers: 2, random_port: false)
|
7
|
+
base_port = random_port ? SecureRandom.random_number(50000) + 10000 : 29870
|
8
|
+
|
9
|
+
bootstrap_node_privkey = Crypto.mk_privkey "#{seed}:udp:0"
|
10
|
+
bootstrap_node_pubkey = Crypto.privtopub bootstrap_node_privkey
|
11
|
+
enode = Utils.host_port_pubkey_to_uri "0.0.0.0", base_port, bootstrap_node_pubkey
|
12
|
+
|
13
|
+
services = [Discovery::Transport, PeerManager]#, service_class]
|
14
|
+
|
15
|
+
base_config = {}
|
16
|
+
services.each {|s| Utils.update_config_with_defaults base_config, s.default_config }
|
17
|
+
|
18
|
+
base_config[:discovery][:bootstrap_nodes] = [enode]
|
19
|
+
base_config[:seed] = seed
|
20
|
+
base_config[:base_port] = base_port
|
21
|
+
base_config[:num_nodes] = num_nodes
|
22
|
+
base_config[:min_peers] = min_peers
|
23
|
+
base_config[:max_peers] = max_peers
|
24
|
+
|
25
|
+
apps = []
|
26
|
+
num_nodes.times do |node_num|
|
27
|
+
app = create_app node_num, base_config, services, app_class
|
28
|
+
apps.push app
|
29
|
+
end
|
30
|
+
|
31
|
+
serve_until_stopped apps
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_app(node_num, config, services, app_class)
|
35
|
+
num_nodes = config[:num_nodes]
|
36
|
+
base_port = config[:base_port]
|
37
|
+
seed = config[:seed]
|
38
|
+
min_peers = config[:min_peers]
|
39
|
+
max_peers = config[:max_peers]
|
40
|
+
|
41
|
+
raise "invalid node_num" unless node_num < num_nodes
|
42
|
+
#raise "invalid min/max peers" unless min_peers <= max_peers && max_peers < num_nodes
|
43
|
+
|
44
|
+
config = Marshal.load Marshal.dump(config)
|
45
|
+
config[:node_num] = node_num
|
46
|
+
|
47
|
+
config[:node][:privkey_hex] = Utils.encode_hex Crypto.mk_privkey("#{seed}:udp:#{node_num}")
|
48
|
+
config[:discovery][:listen_port] = base_port + node_num
|
49
|
+
config[:p2p][:listen_port] = base_port + node_num
|
50
|
+
config[:p2p][:min_peers] = [min_peers, 10].min
|
51
|
+
config[:p2p][:max_peers] = max_peers
|
52
|
+
config[:client_version_string] = "NODE#{node_num}"
|
53
|
+
|
54
|
+
app = app_class.new config
|
55
|
+
|
56
|
+
services.each do |service|
|
57
|
+
raise "invalid service" unless service.instance_of?(Class) && service < BaseService
|
58
|
+
|
59
|
+
if !app.config[:deactivated_services].include?(service.name)
|
60
|
+
raise "service should not be active" if app.services.has_key?(service.name)
|
61
|
+
service.register_with_app app
|
62
|
+
raise "servier should be active" unless app.services.has_key?(service.name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
app
|
67
|
+
end
|
68
|
+
|
69
|
+
def serve_until_stopped(apps)
|
70
|
+
apps.each do |app|
|
71
|
+
app.start
|
72
|
+
|
73
|
+
if app.config[:post_app_start_callback]
|
74
|
+
app.config[:post_app_start_callback].call(app)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
apps.each(&:join)
|
79
|
+
|
80
|
+
# finally stop
|
81
|
+
apps.each(&:stop)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
require 'hashie'
|
3
|
+
|
4
|
+
module DEVp2p
|
5
|
+
class BaseApp
|
6
|
+
|
7
|
+
extend Configurable
|
8
|
+
add_config(
|
9
|
+
default_config: {
|
10
|
+
client_version_string: "ruby-devp2p #{VersionString}",
|
11
|
+
deactivated_services: []
|
12
|
+
}
|
13
|
+
)
|
14
|
+
|
15
|
+
attr :config, :services
|
16
|
+
|
17
|
+
def initialize(config=default_config)
|
18
|
+
@config = Utils.update_config_with_defaults config, default_config
|
19
|
+
@registry = Celluloid::Supervision::Configuration.new
|
20
|
+
@services = Hashie::Mash.new
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Registers protocol with app, which will be accessible as
|
25
|
+
# `app.services.<protocol_name>` (e.g. `app.services.p2p` or
|
26
|
+
# `app.services.eth`)
|
27
|
+
#
|
28
|
+
def register_service(klass, *args)
|
29
|
+
raise ArgumentError, "service must be instance of BaseService" unless klass.instance_of?(Class) && klass < BaseService
|
30
|
+
raise ArgumentError, "service #{klass.name} already registered" if services.has_key?(klass.name)
|
31
|
+
|
32
|
+
logger.info "registering service", service: klass.name
|
33
|
+
|
34
|
+
registry_name = "#{object_id}__#{klass.name}"
|
35
|
+
@registry.define type: klass, as: registry_name, args: args
|
36
|
+
services[klass.name] = nil
|
37
|
+
|
38
|
+
klass
|
39
|
+
end
|
40
|
+
|
41
|
+
def deregister_service(service)
|
42
|
+
raies NotImplemented
|
43
|
+
#raise ArgumentError, "service must be instance of BaseService" unless service.is_a?(BaseService)
|
44
|
+
#services.delete(service.name)
|
45
|
+
#unlink service
|
46
|
+
end
|
47
|
+
|
48
|
+
def start
|
49
|
+
@registry.deploy
|
50
|
+
|
51
|
+
services.keys.each do |k|
|
52
|
+
registry_name = "#{object_id}__#{k}"
|
53
|
+
services[k] = Celluloid::Actor[registry_name]
|
54
|
+
services[k].start
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop
|
59
|
+
services.keys.each do |k|
|
60
|
+
services[k].stop if services[k].alive?
|
61
|
+
services[k] = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
@registry.shutdown
|
65
|
+
end
|
66
|
+
|
67
|
+
def join
|
68
|
+
@services.each_value do |service|
|
69
|
+
Celluloid::Actor.join service
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def logger
|
76
|
+
@logger ||= Logger.new 'app'
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
##
|
6
|
+
# A protocol mediates between the network and the service. It implements a
|
7
|
+
# collection of commands.
|
8
|
+
#
|
9
|
+
# For each command X the following methods are created at initialization:
|
10
|
+
#
|
11
|
+
# * `packet = protocol.create_X(*args, **kwargs)`
|
12
|
+
# * `protocol.send_X(*args, **kwargs)`, which is a shortcut for `send_packet`
|
13
|
+
# plus `create_X`.
|
14
|
+
# * `protocol.receive_X(data)`
|
15
|
+
#
|
16
|
+
# On `protocol.receive_packet`, the packet is deserialized according to the
|
17
|
+
# `command.structure` and the `command.receive` method called with a hash
|
18
|
+
# containing the received data.
|
19
|
+
#
|
20
|
+
# The default implementation of `command.receive` calls callbacks which can
|
21
|
+
# be registered in a list which is available as: `protocol.receive_X_callbacks`.
|
22
|
+
#
|
23
|
+
class BaseProtocol
|
24
|
+
include Celluloid
|
25
|
+
include Control
|
26
|
+
|
27
|
+
extend Configurable
|
28
|
+
add_config(
|
29
|
+
name: '',
|
30
|
+
protocol_id: 0,
|
31
|
+
version: 0,
|
32
|
+
max_cmd_id: 0 # reserved cmd space
|
33
|
+
)
|
34
|
+
|
35
|
+
attr :peer, :service, :cmd_by_id
|
36
|
+
|
37
|
+
def initialize(peer, service)
|
38
|
+
raise ArgumentError, 'service must be WiredService' unless service.is_a?(WiredService)
|
39
|
+
raise ArgumentError, 'peer.send_packet must be callable' unless peer.respond_to?(:send_packet)
|
40
|
+
|
41
|
+
@peer = peer
|
42
|
+
@service = service
|
43
|
+
|
44
|
+
initialize_control
|
45
|
+
|
46
|
+
setup
|
47
|
+
end
|
48
|
+
|
49
|
+
def start
|
50
|
+
logger.debug 'starting', proto: Actor.current
|
51
|
+
service.on_wire_protocol_start Actor.current
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
logger.debug 'stopping', proto: Actor.current
|
57
|
+
service.on_wire_protocol_stop Actor.current
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def _run
|
62
|
+
# pass
|
63
|
+
end
|
64
|
+
|
65
|
+
def receive_packet(packet)
|
66
|
+
cmd_name = @cmd_by_id[packet.cmd_id]
|
67
|
+
cmd = "receive_#{cmd_name}"
|
68
|
+
send cmd, packet
|
69
|
+
rescue ProtocolError => e
|
70
|
+
logger.warn "protocol exception, stopping", error: e
|
71
|
+
stop
|
72
|
+
end
|
73
|
+
|
74
|
+
def send_packet(packet)
|
75
|
+
peer.send_packet packet
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
"<#{name} #{peer}>"
|
80
|
+
end
|
81
|
+
alias :inspect :to_s
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def logger
|
86
|
+
@logger ||= Logger.new('protocol')
|
87
|
+
end
|
88
|
+
|
89
|
+
def setup
|
90
|
+
klasses = []
|
91
|
+
self.class.constants.each do |name|
|
92
|
+
c = self.class.const_get name
|
93
|
+
klasses.push(c) if c.instance_of?(Class) && c < Command
|
94
|
+
end
|
95
|
+
|
96
|
+
raise DuplicatedCommand unless klasses.map(&:cmd_id).uniq.size == klasses.size
|
97
|
+
|
98
|
+
proto = Actor.current
|
99
|
+
|
100
|
+
klasses.each do |klass|
|
101
|
+
instance = klass.new
|
102
|
+
|
103
|
+
# decode rlp, create hash, call receive
|
104
|
+
receive = lambda do |packet|
|
105
|
+
raise ArgumentError unless packet.is_a?(Packet)
|
106
|
+
instance.receive proto, klass.decode_payload(packet.payload)
|
107
|
+
end
|
108
|
+
|
109
|
+
# get data, rlp encode, return packet
|
110
|
+
create = lambda do |*args|
|
111
|
+
res = instance.create(proto, *args)
|
112
|
+
payload = klass.encode_payload res
|
113
|
+
Packet.new protocol_id, klass.cmd_id, payload
|
114
|
+
end
|
115
|
+
|
116
|
+
# create and send packet
|
117
|
+
send_packet = lambda do |*args|
|
118
|
+
packet = create.call *args
|
119
|
+
send_packet packet
|
120
|
+
end
|
121
|
+
|
122
|
+
name = klass.name.split('::').last.downcase
|
123
|
+
singleton_class.send(:define_method, "receive_#{name}", &receive)
|
124
|
+
singleton_class.send(:define_method, "create_#{name}", &create)
|
125
|
+
singleton_class.send(:define_method, "send_#{name}", &send_packet)
|
126
|
+
singleton_class.send(:define_method, "receive_#{name}_callbacks") do
|
127
|
+
instance.receive_callbacks
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
@cmd_by_id = klasses.map {|k| [k.cmd_id, Utils.underscore(k.name)] }.to_h
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
module DEVp2p
|
3
|
+
|
4
|
+
##
|
5
|
+
# Service instances are added to the application under
|
6
|
+
# `app.services.<service_name>`.
|
7
|
+
#
|
8
|
+
# App should be passed to the service in order to query other services.
|
9
|
+
#
|
10
|
+
# Services must be an actor. If a service spawns additional services, it's
|
11
|
+
# responsible to stop them.
|
12
|
+
#
|
13
|
+
class BaseService
|
14
|
+
include Celluloid
|
15
|
+
#finalizer :stop
|
16
|
+
include Control
|
17
|
+
|
18
|
+
extend Configurable
|
19
|
+
add_config(
|
20
|
+
name: '',
|
21
|
+
default_config: {name: {}},
|
22
|
+
required_services: []
|
23
|
+
)
|
24
|
+
|
25
|
+
class <<self
|
26
|
+
##
|
27
|
+
# Services know best how to initiate themselves. Create a service
|
28
|
+
# instance, probably based on `app.config` and `app.services`.
|
29
|
+
#
|
30
|
+
def register_with_app(app)
|
31
|
+
app.register_service self, app
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr :app, :config
|
36
|
+
|
37
|
+
def initialize(app)
|
38
|
+
@app = app
|
39
|
+
@config = Utils.update_config_with_defaults app.config, default_config
|
40
|
+
|
41
|
+
initialize_control
|
42
|
+
|
43
|
+
available_services = app.services.each_value.map(&:class)
|
44
|
+
required_services.each do |r|
|
45
|
+
raise MissingRequiredServiceError, "require service #{r}" unless available_services.include?(r)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
"<Service #{name}##{object_id}>"
|
51
|
+
end
|
52
|
+
alias inspect to_s
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|