devp2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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