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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +22 -0
  4. data/lib/devp2p.rb +57 -0
  5. data/lib/devp2p/app_helper.rb +85 -0
  6. data/lib/devp2p/base_app.rb +80 -0
  7. data/lib/devp2p/base_protocol.rb +136 -0
  8. data/lib/devp2p/base_service.rb +55 -0
  9. data/lib/devp2p/command.rb +82 -0
  10. data/lib/devp2p/configurable.rb +32 -0
  11. data/lib/devp2p/connection_monitor.rb +77 -0
  12. data/lib/devp2p/control.rb +32 -0
  13. data/lib/devp2p/crypto.rb +73 -0
  14. data/lib/devp2p/crypto/ecc_x.rb +133 -0
  15. data/lib/devp2p/crypto/ecies.rb +134 -0
  16. data/lib/devp2p/discovery.rb +118 -0
  17. data/lib/devp2p/discovery/address.rb +83 -0
  18. data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
  19. data/lib/devp2p/discovery/node.rb +32 -0
  20. data/lib/devp2p/discovery/protocol.rb +342 -0
  21. data/lib/devp2p/discovery/transport.rb +105 -0
  22. data/lib/devp2p/exception.rb +30 -0
  23. data/lib/devp2p/frame.rb +197 -0
  24. data/lib/devp2p/kademlia.rb +48 -0
  25. data/lib/devp2p/kademlia/k_bucket.rb +178 -0
  26. data/lib/devp2p/kademlia/node.rb +40 -0
  27. data/lib/devp2p/kademlia/protocol.rb +284 -0
  28. data/lib/devp2p/kademlia/routing_table.rb +131 -0
  29. data/lib/devp2p/kademlia/wire_interface.rb +30 -0
  30. data/lib/devp2p/multiplexed_session.rb +110 -0
  31. data/lib/devp2p/multiplexer.rb +358 -0
  32. data/lib/devp2p/p2p_protocol.rb +170 -0
  33. data/lib/devp2p/packet.rb +35 -0
  34. data/lib/devp2p/peer.rb +329 -0
  35. data/lib/devp2p/peer_errors.rb +35 -0
  36. data/lib/devp2p/peer_manager.rb +274 -0
  37. data/lib/devp2p/rlpx_session.rb +434 -0
  38. data/lib/devp2p/sync_queue.rb +76 -0
  39. data/lib/devp2p/utils.rb +106 -0
  40. data/lib/devp2p/version.rb +13 -0
  41. data/lib/devp2p/wired_service.rb +30 -0
  42. metadata +227 -0
@@ -0,0 +1,82 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ class Command
6
+
7
+ extend Configurable
8
+ add_config(
9
+ cmd_id: 0,
10
+ structure: {}, # {arg_name: RLP::Sedes.type}
11
+ decode_strict: true
12
+ )
13
+
14
+ class <<self
15
+ def encode_payload(data)
16
+ if data.is_a?(Hash)
17
+ raise ArgumentError, 'structure must be hash of arg names and sedes' unless structure.instance_of?(Hash)
18
+ data = structure.keys.map {|k| data[k] }
19
+ end
20
+
21
+ case structure
22
+ when RLP::Sedes::CountableList
23
+ RLP.encode data, structure
24
+ when Hash
25
+ raise ArgumentError, 'structure and data length mismatch' unless data.size == structure.size
26
+ RLP.encode data, sedes: RLP::Sedes::List.new(elements: sedes)
27
+ else
28
+ raise InvalidCommandStructure
29
+ end
30
+ end
31
+
32
+ def decode_payload(rlp_data)
33
+ case structure
34
+ when RLP::Sedes::CountableList
35
+ decoder = structure
36
+ when Hash
37
+ decoder = RLP::Sedes::List.new(elements: sedes, strict: decode_strict)
38
+ else
39
+ raise InvalidCommandStructure
40
+ end
41
+
42
+ data = RLP.decode rlp_data, sedes: decoder
43
+ data = structure.keys.zip(data).to_h if structure.is_a?(Hash)
44
+ data
45
+ rescue
46
+ puts "error in decode: #{$!}"
47
+ puts "rlp:"
48
+ puts RLP.decode(rlp_data)
49
+ raise $!
50
+ end
51
+
52
+ def sedes
53
+ @sedes ||= structure.values
54
+ end
55
+ end
56
+
57
+ attr :receive_callbacks
58
+
59
+ def initialize
60
+ raise InvalidCommandStructure unless [Hash, RLP::Sedes::CountableList].any? {|c| structure.is_a?(c) }
61
+ @receive_callbacks = []
62
+ end
63
+
64
+ # optionally implement create
65
+ def create(proto, *args)
66
+ options = args.last.is_a?(Hash) ? args.pop : {}
67
+ raise ArgumentError, "proto #{proto} must be protocol" unless proto.is_a?(BaseProtocol)
68
+ raise ArgumentError, "command structure mismatch" if !options.empty? && structure.instance_of?(RLP::Sedes::CountableList)
69
+ options.empty? ? args : options
70
+ end
71
+
72
+ # optionally implement receive
73
+ def receive(proto, data)
74
+ if structure.instance_of?(RLP::Sedes::CountableList)
75
+ receive_callbacks.each {|cb| cb.call(proto, data) }
76
+ else
77
+ receive_callbacks.each {|cb| cb.call(proto, **data) }
78
+ end
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Configurable
5
+
6
+ def add_config(configs)
7
+ raise ArgumentError, 'self must be a class' unless self.class == Class
8
+
9
+ configs.each do |name, default|
10
+ singleton_class.send(:define_method, name) do |*args|
11
+ iv = "@#{name}"
12
+ if args.empty?
13
+ if instance_variable_defined?(iv)
14
+ instance_variable_get(iv)
15
+ else
16
+ v = superclass.respond_to?(:add_config) && superclass.respond_to?(name) ?
17
+ superclass.public_send(name) : default
18
+ instance_variable_set(iv, v)
19
+ end
20
+ else
21
+ instance_variable_set(iv, args.first)
22
+ end
23
+ end
24
+
25
+ define_method(name) do |*args|
26
+ self.class.public_send name, *args
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # monitors the connection by sending pings and checking pongs
7
+ #
8
+ class ConnectionMonitor
9
+ include Celluloid
10
+
11
+ def initialize(proto)
12
+ @proto = proto
13
+
14
+ logger.debug "init"
15
+ raise ArgumentError, 'protocol must be P2PProtocol' unless proto.is_a?(P2PProtocol)
16
+
17
+ @samples = []
18
+ @last_response = @last_request = Time.now
19
+
20
+ @ping_interval = 15
21
+ @response_delay_threshold = 120
22
+ @max_samples = 1000
23
+
24
+ track_response = ->(proto, **data) {
25
+ @last_response = Time.now
26
+ @samples.unshift(@last_response - @last_request)
27
+ @samples.pop if @samples.size > @max_samples
28
+ }
29
+ @proto.receive_pong_callbacks.push(track_response)
30
+
31
+ monitor = Actor.current
32
+ @proto.receive_hello_callbacks.push(->(p, **kwargs) { monitor.start })
33
+ end
34
+
35
+ def latency(num_samples=@max_samples)
36
+ num_samples = [num_samples, @samples.size].min
37
+ return 1 unless num_samples > 0
38
+ (0...num_samples).map {|i| @samples[i] }.reduce(0, &:+)
39
+ end
40
+
41
+ def run
42
+ logger.debug 'started', monitor: Actor.current
43
+ loop do
44
+ logger.debug 'pinging', monitor: Actor.current
45
+ @proto.send_ping
46
+
47
+ now = @last_request = Time.now
48
+ sleep @ping_interval
49
+ logger.debug('latency', peer: @proto, latency: ("%.3f" % latency))
50
+
51
+ if now - @last_response > @response_delay_threshold
52
+ logger.debug "unresponsive_peer", monitor: Actor.current
53
+ @proto.peer.report_error 'not responding to ping'
54
+ @proto.stop
55
+ terminate
56
+ end
57
+ end
58
+ end
59
+
60
+ def start
61
+ async.run
62
+ end
63
+
64
+ def stop
65
+ logger.debug 'stopped', monitor: Actor.current
66
+ terminate
67
+ end
68
+
69
+ private
70
+
71
+ def logger
72
+ @logger ||= Logger.new("#{@proto.peer.config[:p2p][:listen_port]}.p2p.ctxmonitor.#{object_id}")
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,32 @@
1
+ module DEVp2p
2
+ module Control
3
+
4
+ def initialize_control
5
+ @stopped = false
6
+ @killed = false
7
+ end
8
+
9
+ def run
10
+ _run
11
+ end
12
+
13
+ def start
14
+ @stopped = false
15
+ async.run unless killed?
16
+ end
17
+
18
+ def stop
19
+ @stopped = true
20
+ @killed = true
21
+ end
22
+
23
+ def stopped?
24
+ @stopped
25
+ end
26
+
27
+ def killed?
28
+ @killed
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,73 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'secp256k1' # bitcoin-secp256k1
4
+ require 'digest/sha3'
5
+
6
+ require 'devp2p/crypto/ecies'
7
+ require 'devp2p/crypto/ecc_x'
8
+
9
+ module DEVp2p
10
+ module Crypto
11
+
12
+ extend self
13
+
14
+ def mk_privkey(seed)
15
+ Crypto.keccak256 seed
16
+ end
17
+
18
+ def privtopub(privkey)
19
+ priv = Secp256k1::PrivateKey.new privkey: privkey, raw: true
20
+
21
+ pub = priv.pubkey.serialize(compressed: false)
22
+ raise InvalidKeyError, 'invalid pubkey' unless pub.size == 65 && pub[0] == "\x04"
23
+
24
+ pub[1,64]
25
+ end
26
+
27
+ def keccak256(x)
28
+ Digest::SHA3.new(256).digest(x)
29
+ end
30
+
31
+ def hmac_sha256(key, msg)
32
+ OpenSSL::HMAC.digest 'sha256', key, msg
33
+ end
34
+
35
+ def ecdsa_sign(msghash, privkey)
36
+ raise ArgumentError, 'msghash length must be 32' unless msghash.size == 32
37
+
38
+ priv = Secp256k1::PrivateKey.new privkey: privkey, raw: true
39
+ sig = priv.ecdsa_recoverable_serialize priv.ecdsa_sign_recoverable(msghash, raw: true)
40
+ "#{sig[0]}#{sig[1].chr}"
41
+ end
42
+
43
+ def ecdsa_recover(msghash, sig)
44
+ raise ArgumentError, 'msghash length must be 32' unless msghash.size == 32
45
+ raise ArgumentError, 'signature length must be 65' unless sig.size == 65
46
+
47
+ pub = Secp256k1::PublicKey.new flags: Secp256k1::ALL_FLAGS
48
+ recsig = pub.ecdsa_recoverable_deserialize sig[0,64], sig[64].ord
49
+ pub.public_key = pub.ecdsa_recover msghash, recsig, raw: true
50
+ pub.serialize(compressed: false)[1..-1]
51
+ end
52
+
53
+ def ecdsa_verify(pubkey, sig, msg)
54
+ raise ArgumentError, 'invalid signature length' unless sig.size == 65
55
+ raise ArgumentError, 'invalid pubkey length' unless pubkey.size == 64
56
+
57
+ pub = Secp256k1::PublicKey.new pubkey: "\x04#{pubkey}", raw: true
58
+ raw_sig = pub.ecdsa_recoverable_convert pub.ecdsa_recoverable_deserialize(sig[0,64], sig[64].ord)
59
+
60
+ pub.ecdsa_verify msg, raw_sig, raw: true
61
+ end
62
+ alias verify ecdsa_verify
63
+
64
+ ##
65
+ # Encrypt data with ECIES method using the public key of the recipient.
66
+ #
67
+ def encrypt(data, raw_pubkey)
68
+ raise ArgumentError, "invalid pubkey of length #{raw_pubkey.size}" unless raw_pubkey.size == 64
69
+ Crypto::ECIES.encrypt data, raw_pubkey
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,133 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'securerandom'
4
+
5
+ module DEVp2p
6
+ module Crypto
7
+ class ECCx
8
+
9
+ CURVE = 'secp256k1'.freeze
10
+
11
+ class <<self
12
+ def valid_key?(raw_pubkey, raw_privkey=nil)
13
+ return false unless raw_pubkey.size == 64
14
+
15
+ group = OpenSSL::PKey::EC::Group.new CURVE
16
+ bn = OpenSSL::BN.new Utils.encode_hex("\x04#{raw_pubkey}"), 16
17
+ point = OpenSSL::PKey::EC::Point.new group, bn
18
+
19
+ key = OpenSSL::PKey::EC.new(CURVE)
20
+ key.public_key = point
21
+ key.private_key = OpenSSL::BN.new Utils.big_endian_to_int(raw_privkey) if raw_privkey
22
+ key.check_key
23
+
24
+ true
25
+ rescue
26
+ false
27
+ end
28
+
29
+ def generate_key
30
+ curve = OpenSSL::PKey::EC.new(CURVE)
31
+ curve.generate_key
32
+
33
+ raw_privkey = Utils.zpad Utils.int_to_big_endian(curve.private_key.to_i), 32
34
+ raw_pubkey = Utils.int_to_big_endian(curve.public_key.to_bn.to_i)
35
+ raise InvalidKeyError, 'invalid pubkey' unless raw_pubkey.size == 65 && raw_pubkey[0] == "\x04"
36
+
37
+ [raw_privkey, raw_pubkey[1,64]]
38
+ end
39
+
40
+ ##
41
+ # Compute public key with the local private key and returns a 256bits
42
+ # shared key
43
+ #
44
+ def get_ecdh_key(curve, raw_pubkey)
45
+ pubkey = raw_pubkey_to_openssl_pubkey raw_pubkey
46
+ curve.dh_compute_key pubkey
47
+ end
48
+
49
+ def raw_pubkey_to_openssl_pubkey(raw_pubkey)
50
+ return unless raw_pubkey
51
+
52
+ bn = OpenSSL::BN.new Utils.encode_hex("\x04#{raw_pubkey}"), 16
53
+ group = OpenSSL::PKey::EC::Group.new CURVE
54
+ OpenSSL::PKey::EC::Point.new group, bn
55
+ end
56
+
57
+ def raw_privkey_to_openssl_privkey(raw_privkey)
58
+ return unless raw_privkey
59
+
60
+ OpenSSL::BN.new Utils.big_endian_to_int(raw_privkey)
61
+ end
62
+ end
63
+
64
+ attr :raw_pubkey
65
+
66
+ def initialize(raw_privkey=nil, raw_pubkey=nil)
67
+ if raw_privkey && raw_pubkey
68
+ raise ArgumentError, 'must not provide pubkey with privkey'
69
+ elsif raw_privkey
70
+ raw_pubkey = Crypto.privtopub raw_privkey
71
+ elsif raw_pubkey
72
+ raise ArgumentError, 'invalid pubkey length' unless raw_pubkey.size == 64
73
+ else
74
+ raw_privkey, raw_pubkey = self.class.generate_key
75
+ end
76
+
77
+ if self.class.valid_key?(raw_pubkey, raw_privkey)
78
+ @raw_pubkey, @raw_privkey = raw_pubkey, raw_privkey
79
+ @pubkey_x, @pubkey_y = decode_pubkey raw_pubkey
80
+ set_curve
81
+ else
82
+ @raw_pubkey, @raw_privkey = nil, nil
83
+ @pubkey_x, @pubkey_y = nil, nil
84
+ raise InvalidKeyError, "bad ECC keys"
85
+ end
86
+ end
87
+
88
+ def sign(data)
89
+ sig = Crypto.ecdsa_sign data, @raw_privkey
90
+ raise InvalidSignatureError unless sig.size == 65
91
+ sig
92
+ end
93
+
94
+ def verify(sig, msg)
95
+ raise ArgumentError, 'invalid signature length' unless sig.size == 65
96
+ Crypto.ecdsa_verify @raw_pubkey, sig, msg
97
+ end
98
+
99
+ def get_ecdh_key(raw_pubkey)
100
+ self.class.get_ecdh_key curve, raw_pubkey
101
+ end
102
+
103
+ def ecies_encrypt(*args)
104
+ ECIES.encrypt *args
105
+ end
106
+ alias encrypt ecies_encrypt
107
+
108
+ def ecies_decrypt(*args)
109
+ ECIES.decrypt curve, *args
110
+ end
111
+ alias decrypt ecies_decrypt
112
+
113
+ def curve
114
+ @curve ||= OpenSSL::PKey::EC.new(CURVE)
115
+ end
116
+
117
+ private
118
+
119
+ def decode_pubkey(raw_pubkey)
120
+ return [nil, nil] unless raw_pubkey
121
+
122
+ raise ArgumentError, 'invalid pubkey length' unless raw_pubkey.size == 64
123
+ [raw_pubkey[0,32], raw_pubkey[32,32]]
124
+ end
125
+
126
+ def set_curve
127
+ curve.public_key = self.class.raw_pubkey_to_openssl_pubkey(@raw_pubkey)
128
+ curve.private_key = self.class.raw_privkey_to_openssl_privkey(@raw_privkey)
129
+ end
130
+
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,134 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+ module Crypto
5
+ class ECIES
6
+
7
+ CIPHER = 'AES-128-CTR'.freeze
8
+ CIPHER_BLOCK_SIZE = 16 # 128 / 8
9
+
10
+ ENCRYPT_OVERHEAD_LENGTH = 113
11
+
12
+ class <<self
13
+
14
+ ##
15
+ # ECIES Encrypt, where P = recipient publie key, is:
16
+ #
17
+ # 1. generate r = random value
18
+ # 2. generate shared-secret = kdf( ecdhAgree(r, P) )
19
+ # 3. generate R = rG [ same op as generating a public key ]
20
+ # 4. send 0x04 || R || AsymmetricEncrypt(shared-secret, plaintext) || tag
21
+ #
22
+ def encrypt(data, remote_pubkey, shared_mac_data='')
23
+ # 1. generate r = random value
24
+ ephem = ECCx.new
25
+
26
+ # 2. generate shared-secret = kdf( ecdhAgree(r, P) )
27
+ key_material = ephem.get_ecdh_key remote_pubkey
28
+ raise InvalidKeyError unless key_material.size == 32
29
+
30
+ key = kdf key_material, 32
31
+ raise InvalidKeyError unless key.size == 32
32
+ key_enc, key_mac = key[0,16], key[16,16]
33
+
34
+ key_mac = Digest::SHA256.digest(key_mac)
35
+ raise InvalidKeyError unless key_mac.size == 32
36
+
37
+ # 3. generate R = rG
38
+ ephem_pubkey = ephem.raw_pubkey
39
+
40
+ ctx = OpenSSL::Cipher.new(CIPHER)
41
+ ctx.encrypt
42
+ ctx.key = key_enc
43
+ iv = ctx.random_iv
44
+ ctx.iv = iv
45
+
46
+ ciphertext = ctx.update(data) + ctx.final
47
+ raise EncryptionError unless ciphertext.size == data.size
48
+
49
+ # 4. send 0x04 || R || AsymmetricEncrypt(shared-secret, plaintext) || tag
50
+ tag = Crypto.hmac_sha256 key_mac, "#{iv}#{ciphertext}#{shared_mac_data}"
51
+ raise InvalidMACError unless tag.size == 32
52
+ msg = "\x04#{ephem_pubkey}#{iv}#{ciphertext}#{tag}"
53
+
54
+ raise EncryptionError unless msg.size == ENCRYPT_OVERHEAD_LENGTH + data.size
55
+ msg
56
+ end
57
+
58
+ ##
59
+ # Decrypt data with ECIES method using the local private key
60
+ #
61
+ # ECIES Decrypt (performed by recipient):
62
+ #
63
+ # 1. generate shared-secret = kdf( ecdhAgree(myPrivKey, msg[1,64]) )
64
+ # 2. verify tag
65
+ # 3. decrypt
66
+ #
67
+ # ecdhAgree(r, recipientPublic) == ecdhAgree(recipientPrivate, R)
68
+ # where R = r*G, recipientPublic = recipientPrivate * G
69
+ #
70
+ def decrypt(curve, data, shared_mac_data='')
71
+ raise DecryptionError, 'wrong ecies header' unless data[0] == "\x04"
72
+
73
+ # 1. generate shared-secret = kdf( ecdhAgree(myPrivKey, msg[1,64]) )
74
+ shared = data[1,64] # ephem_pubkey
75
+ raise DecryptionError, 'invalid shared secret' unless ECCx.valid_key?(shared)
76
+
77
+ key_material = ECCx.get_ecdh_key curve, shared
78
+ raise InvalidKeyError unless key_material.size == 32
79
+
80
+ key = kdf key_material, 32
81
+ raise InvalidKeyError unless key.size == 32
82
+ key_enc, key_mac = key[0,16], key[16,16]
83
+
84
+ key_mac = Digest::SHA256.digest(key_mac)
85
+ raise InvalidKeyError unless key_mac.size == 32
86
+
87
+ tag = data[-32..-1]
88
+ raise InvalidMACError unless tag.size == 32
89
+
90
+ # 2. verify tag
91
+ raise DecryptionError, 'Fail to verify data' unless Crypto.hmac_sha256(key_mac, "#{data[65...-32]}#{shared_mac_data}") == tag
92
+
93
+ # 3. decrypt
94
+ iv = data[65,CIPHER_BLOCK_SIZE]
95
+ ciphertext = data[(65+CIPHER_BLOCK_SIZE)...-32]
96
+ raise DecryptionError unless 1 + shared.size + iv.size + ciphertext.size + tag.size == data.size
97
+
98
+ ctx = OpenSSL::Cipher.new CIPHER
99
+ ctx.decrypt
100
+ ctx.key = key_enc
101
+ ctx.iv = iv
102
+
103
+ ctx.update(ciphertext) + ctx.final
104
+ end
105
+
106
+ ##
107
+ # interop w/go ecies implementation
108
+ #
109
+ # for sha3, blocksize is 136 bytes
110
+ # for sha256, blocksize is 64 bytes
111
+ #
112
+ # NIST SP 800-56a Concatenation Key Derivation Function (section 5.8.1)
113
+ #
114
+ def kdf(key_material, key_len)
115
+ s1 = ""
116
+ key = ""
117
+ hash_blocksize = 64
118
+ reps = ((key_len + 7) * 8) / (hash_blocksize * 8)
119
+ counter = 0
120
+ while counter <= reps
121
+ counter += 1
122
+ ctx = Digest::SHA256.new
123
+ ctx.update [counter].pack('I>')
124
+ ctx.update key_material
125
+ ctx.update s1
126
+ key += ctx.digest
127
+ end
128
+ key[0,key_len]
129
+ end
130
+
131
+ end
132
+ end
133
+ end
134
+ end