reth 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.
@@ -0,0 +1,24 @@
1
+ module Reth
2
+ module JSONRPC
3
+
4
+ class Server
5
+ include Concurrent::Async
6
+
7
+ def initialize(app, host, port)
8
+ super()
9
+
10
+ @app = app
11
+ @host = host
12
+ @port = port
13
+ end
14
+
15
+ def start
16
+ Rack::Handler::WEBrick.run App.new(@app), Host: @host, Port: @port
17
+ rescue
18
+ puts $!
19
+ puts $!.backtrace[0,10].join("\n")
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module Reth
2
+ module JSONRPC
3
+
4
+ class Service < ::DEVp2p::Service
5
+
6
+ class <<self
7
+ def register_with_app(app)
8
+ config = default_config[:jsonrpc]
9
+ app.register_service self, app, config[:host], config[:port]
10
+ end
11
+ end
12
+
13
+ name 'jsonrpc'
14
+ default_config(
15
+ jsonrpc: {
16
+ host: '127.0.0.1',
17
+ port: 8333
18
+ }
19
+ )
20
+
21
+ def initialize(app, host, port)
22
+ super(app)
23
+
24
+ @app = app
25
+ @host = host
26
+ @port = port
27
+ end
28
+
29
+ def start
30
+ @server = Server.new @app, @host, @port
31
+ @server.async.start
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,150 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'openssl'
4
+
5
+ module Reth
6
+
7
+ class Keystore
8
+
9
+ class PBKDF2
10
+ attr :name, :params
11
+
12
+ def initialize(params=nil)
13
+ @name = 'pbkdf2'
14
+ @params = params || mkparams
15
+ end
16
+
17
+ def eval(pw)
18
+ OpenSSL::PKCS5.pbkdf2_hmac(
19
+ pw,
20
+ Utils.decode_hex(params[:salt]),
21
+ params[:c],
22
+ params[:dklen],
23
+ 'SHA256'
24
+ )
25
+ end
26
+
27
+ def mkparams
28
+ { prf: 'hmac-sha256',
29
+ dklen: 32,
30
+ c: 262144,
31
+ salt: Utils.encode_hex(SecureRandom.random_bytes(16)) }
32
+ end
33
+ end
34
+
35
+ class AES128CTR
36
+ attr :name, :params
37
+
38
+ def initialize(params=nil)
39
+ @name = 'aes-128-ctr'
40
+ @params = params || mkparams
41
+ end
42
+
43
+ def encrypt(text, key)
44
+ cipher = OpenSSL::Cipher.new name
45
+ cipher.encrypt
46
+ cipher.key = key
47
+ cipher.iv = Utils.decode_hex(params[:iv])
48
+ cipher.update(text) + cipher.final
49
+ end
50
+
51
+ def decrypt(text, key)
52
+ cipher = OpenSSL::Cipher.new name
53
+ cipher.decrypt
54
+ cipher.key = key
55
+ cipher.iv = Utils.decode_hex(params[:iv])
56
+ cipher.update(text) + cipher.final
57
+ end
58
+
59
+ def mkparams
60
+ {iv: Utils.encode_hex(SecureRandom.random_bytes(16))}
61
+ end
62
+ end
63
+
64
+ KDF = {
65
+ 'pbkdf2' => PBKDF2
66
+ }.freeze
67
+
68
+ CIPHER = {
69
+ 'aes-128-ctr' => AES128CTR
70
+ }.freeze
71
+
72
+ class <<self
73
+
74
+ def make_json(priv, pw, kdf=PBKDF2.new, cipher=AES128CTR.new)
75
+ derivedkey = kdf.eval pw
76
+
77
+ enckey = derivedkey[0,16]
78
+ c = cipher.encrypt priv, enckey
79
+
80
+ mac = Utils.keccak256 "#{derivedkey[16,16]}#{c}"
81
+ uuid = SecureRandom.uuid
82
+
83
+ {
84
+ crypto: {
85
+ cipher: cipher.name,
86
+ ciphertext: Utils.encode_hex(c),
87
+ cipherparams: cipher.params,
88
+ kdf: kdf.name,
89
+ kdfparams: kdf.params,
90
+ mac: Utils.encode_hex(mac),
91
+ version: 1
92
+ },
93
+ id: uuid,
94
+ version: 3
95
+ }
96
+ end
97
+
98
+ def decode_json(jsondata, pw)
99
+ jsondata = Hashie::Mash.new jsondata
100
+
101
+ cryptdata = jsondata.crypto || jsondata.Crypto
102
+ raise ArgumentError, "JSON data must contain 'crypto' object" unless cryptdata
103
+
104
+ kdfparams = cryptdata.kdfparams
105
+ kdf = KDF[cryptdata.kdf].new kdfparams
106
+
107
+ cipherparams = cryptdata.cipherparams
108
+ cipher = CIPHER[cryptdata.cipher].new cipherparams
109
+
110
+ derivedkey = kdf.eval pw
111
+ raise ValueError, "Derived key must be at least 32 bytes long" unless derivedkey.size >= 32
112
+
113
+ enckey = derivedkey[0,16]
114
+ ct = Utils.decode_hex cryptdata.ciphertext
115
+ o = cipher.decrypt ct, enckey
116
+
117
+ mac1 = Utils.keccak256 "#{derivedkey[16,16]}#{ct}"
118
+ mac2 = Utils.decode_hex cryptdata.mac
119
+ raise ValueError, "MAC mismatch. Password incorrect?" unless mac1 == mac2
120
+
121
+ o
122
+ end
123
+
124
+ ##
125
+ # Check if json has the structure of a keystore file version 3.
126
+ #
127
+ # Note that this test is not complete, e.g. it doesn't check key
128
+ # derivation or cipher parameters.
129
+ #
130
+ # @param json [Hash] data load from json file
131
+ # @return [Bool] `true` if the data appears to be valid, otherwise
132
+ # `false`
133
+ #
134
+ def validate(json)
135
+ return false unless json.has_key?('crypto') || json.has_key?('Crypto')
136
+ return false unless json['version'] == 3
137
+
138
+ crypto = json['crypto'] || json['Crypto']
139
+ return false unless crypto.has_key?('cipher')
140
+ return false unless crypto.has_key?('ciphertext')
141
+ return false unless crypto.has_key?('kdf')
142
+ return false unless crypto.has_key?('mac')
143
+
144
+ true
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ end
@@ -0,0 +1,79 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module Reth
4
+
5
+ class LevelDBService < ::DEVp2p::Service
6
+ name 'leveldb'
7
+
8
+ attr :db # implement DB::BaseDB interface
9
+
10
+ def initialize(app)
11
+ super(app)
12
+ @db = DB::LevelDB.new File.join(app.config[:data_dir], 'leveldb')
13
+ end
14
+
15
+ def start
16
+ # do nothing
17
+ end
18
+
19
+ def stop
20
+ # do nothing
21
+ end
22
+
23
+ def get(k)
24
+ @db.get(k)
25
+ rescue KeyError
26
+ nil
27
+ end
28
+
29
+ def put(k, v)
30
+ @db.put(k, v)
31
+ end
32
+
33
+ def commit
34
+ @db.commit
35
+ end
36
+
37
+ def delete(k)
38
+ @db.delete(k)
39
+ end
40
+
41
+ def include?(k)
42
+ @db.include?(k)
43
+ end
44
+ alias has_key? include?
45
+
46
+ def inc_refcount(k, v)
47
+ put(k, v)
48
+ end
49
+
50
+ def dec_refcount(k)
51
+ # do nothing
52
+ end
53
+
54
+ def revert_refcount_changes(epoch)
55
+ # do nothing
56
+ end
57
+
58
+ def commit_refcount_changes(epoch)
59
+ # do nothing
60
+ end
61
+
62
+ def cleanup(epoch)
63
+ # do nothing
64
+ end
65
+
66
+ def put_temporarily(k, v)
67
+ inc_refcount(k, v)
68
+ dec_refcount(k)
69
+ end
70
+
71
+ private
72
+
73
+ def logger
74
+ @logger ||= Logger.new 'db'
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,66 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module Reth
4
+
5
+ class Profile
6
+
7
+ GENESIS_DIR = File.expand_path('../genesisdata', __FILE__)
8
+
9
+ ALL = {
10
+ livenet: {
11
+ eth: {
12
+ network_id: 1,
13
+ genesis: File.join(GENESIS_DIR, 'genesis_frontier.json'),
14
+ genesis_hash: 'd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3',
15
+ },
16
+ discovery: {
17
+ bootstrap_nodes: [
18
+ 'enode://487611428e6c99a11a9795a6abe7b529e81315ca6aad66e2a2fc76e3adf263faba0d35466c2f8f68d561dbefa8878d4df5f1f2ddb1fbeab7f42ffb8cd328bd4a@5.1.83.226:30303', # C++
19
+ 'enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303', # GO
20
+ 'enode://de471bccee3d042261d52e9bff31458daecc406142b401d4cd848f677479f73104b9fdeb090af9583d3391b7f10cb2ba9e26865dd5fca4fcdc0fb1e3b723c786@54.94.239.50:30303', # GO2
21
+ 'enode://2676755dd8477ad3beea32b4e5a144fa10444b70dfa3e05effb0fdfa75683ebd4f75709e1f8126cb5317c5a35cae823d503744e790a3a038ae5dd60f51ee9101@144.76.62.101:30303', # Python
22
+ ]
23
+ },
24
+ },
25
+ testnet: {
26
+ eth: {
27
+ network_id: 2,
28
+ genesis: File.join(GENESIS_DIR, 'genesis_morden.json'),
29
+ genesis_hash: '0cd786a2425d16f152c658316c423e6ce1181e15c3295826d7c9904cba9ce303',
30
+ block: {
31
+ account_initial_nonce: 2 ** 20,
32
+ homestead_fork_blknum: 494000,
33
+ },
34
+ },
35
+ discovery: {
36
+ bootstrap_nodes: [
37
+ 'enode://e4533109cc9bd7604e4ff6c095f7a1d807e15b38e9bfeb05d3b7c423ba86af0a9e89abbf40bd9dde4250fef114cd09270fa4e224cbeef8b7bf05a51e8260d6b8@94.242.229.4:40404' # Go
38
+ ]
39
+ },
40
+ }
41
+ }
42
+
43
+ class <<self
44
+ def all
45
+ @all ||= Hashie::Mash.new ALL
46
+ end
47
+
48
+ def public(name)
49
+ all[name]
50
+ end
51
+
52
+ def private(network_id)
53
+ Hashie::Mash.new({
54
+ eth: {
55
+ network_id: network_id
56
+ },
57
+ discovery: {
58
+ bootstrap_nodes: []
59
+ }
60
+ })
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,273 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module Reth
4
+
5
+ ##
6
+ # Synchronizes the chain starting from a given blockhash. Blockchain hash
7
+ # is fetched from a single peer (which led to the unknown blockhash).
8
+ # Blocks are fetched from the best peers.
9
+ #
10
+ class SyncTask
11
+ MAX_BLOCKS_PER_REQUEST = 32
12
+ INITIAL_BLOCKHASHES_PER_REQUEST = 16
13
+ MAX_BLOCKHASHES_PER_REQUEST = 512
14
+
15
+ BLOCKS_REQUEST_TIMEOUT = 32
16
+ BLOCKHASHES_REQUEST_TIMEOUT = 32
17
+
18
+ attr :start_block_number, :end_block_number
19
+
20
+ def initialize(synchronizer, proto, blockhash, chain_difficulty=0, originator_only=false)
21
+ @synchronizer = synchronizer
22
+ @chain = synchronizer.chain
23
+ @chainservice = synchronizer.chainservice
24
+
25
+ @originating_proto = proto
26
+ @originator_only = originator_only
27
+
28
+ @blockhash = blockhash
29
+ @chain_difficulty = chain_difficulty
30
+
31
+ @requests = {} # proto => [cond, result]
32
+ @start_block_number = @chain.head.number
33
+ @end_block_number = @start_block_number + 1 # minimum synctask
34
+
35
+ @run = Thread.new { run }
36
+ end
37
+
38
+ def run
39
+ logger.info 'spawning new synctask'
40
+
41
+ fetch_hashchain
42
+ rescue
43
+ logger.error $!
44
+ logger.error $!.backtrace[0,20].join("\n")
45
+ task_exit false
46
+ end
47
+
48
+ def task_exit(success=false)
49
+ if success
50
+ logger.debug 'successfully synced'
51
+ else
52
+ logger.warn 'syncing failed'
53
+ end
54
+
55
+ @synchronizer.synctask_exited(success)
56
+ end
57
+
58
+ def protocols
59
+ return [@originating_proto] if @originator_only
60
+ @synchronizer.protocols
61
+ end
62
+
63
+ def fetch_hashchain
64
+ logger.debug 'fetching hashchain'
65
+
66
+ blockhashes_chain = [@blockhash] # youngest to oldest
67
+ blockhash = @blockhash = blockhashes_chain.last
68
+ raise AssertError if @chain.include?(blockhash)
69
+
70
+ # get block hashes until we found a known one
71
+ max_blockhashes_per_request = INITIAL_BLOCKHASHES_PER_REQUEST
72
+ chain_head_number = @chain.head.number
73
+ while !@chain.include?(blockhash)
74
+ blockhashes_batch = []
75
+
76
+ # proto with highest difficulty should be the proto we got the
77
+ # newblock from
78
+ protos = self.protocols
79
+ if protos.nil? || protos.empty?
80
+ logger.warn 'no protocols available'
81
+ return task_exit(false)
82
+ end
83
+
84
+ protos.each do |proto|
85
+ logger.debug "syncing with", proto: proto
86
+ next if proto.stopped?
87
+
88
+ raise AssertError if @requests.has_key?(proto)
89
+ deferred = Concurrent::IVar.new
90
+ @requests[proto] = deferred
91
+
92
+ proto.async.send_getblockhashes blockhash, max_blockhashes_per_request
93
+ begin
94
+ blockhashes_batch = deferred.value(BLOCKHASHES_REQUEST_TIMEOUT)
95
+ rescue Defer::TimedOut
96
+ logger.warn 'syncing hashchain timed out'
97
+ next
98
+ ensure
99
+ @requests.delete proto
100
+ end
101
+
102
+ if blockhashes_batch.empty?
103
+ logger.warn 'empty getblockhashes result'
104
+ next
105
+ end
106
+
107
+ unless blockhashes_batch.all? {|bh| bh.instance_of?(String) }
108
+ logger.warn "get wrong data type", expected: 'String', received: blockhashes_batch.map(&:class).uniq
109
+ next
110
+ end
111
+
112
+ break
113
+ end
114
+
115
+ if blockhashes_batch.empty?
116
+ logger.warn 'syncing failed with all peers', num_protos: protos.size
117
+ return task_exit(false)
118
+ end
119
+
120
+ if @chain.include?(blockhashes_batch.last)
121
+ blockhashes_batch.each do |bh| # youngest to oldest
122
+ blockhash = bh
123
+
124
+ if @chain.include?(blockhash)
125
+ logger.debug "found known blockhash", blockhash: Utils.encode_hex(blockhash), is_genesis: (blockhash == @chain.genesis.full_hash)
126
+ break
127
+ else
128
+ blockhashes_chain.push(blockhash)
129
+ end
130
+ end
131
+ else # no overlap
132
+ blockhashes_chain.concat blockhashes_batch
133
+ blockhash = blockhashes_batch.last
134
+ end
135
+
136
+ logger.debug "downloaded #{blockhashes_chain.size} block hashes, ending with #{Utils.encode_hex(blockhashes_chain.last)}"
137
+ @end_block_number = chain_head_number + blockhashes_chain.size
138
+ max_blockhashes_per_request = MAX_BLOCKHASHES_PER_REQUEST
139
+ end
140
+
141
+ @start_block_number = @chain.get(blockhash).number
142
+ @end_block_number = @start_block_number + blockhashes_chain.size
143
+
144
+ logger.debug 'computed missing numbers', start_number: @start_block_number, end_number: @end_block_number
145
+
146
+ fetch_blocks blockhashes_chain
147
+ end
148
+
149
+ def fetch_blocks(blockhashes_chain)
150
+ raise ArgumentError, 'no blockhashes' if blockhashes_chain.empty?
151
+ logger.debug 'fetching blocks', num: blockhashes_chain.size
152
+
153
+ blockhashes_chain.reverse! # oldest to youngest
154
+ num_blocks = blockhashes_chain.size
155
+ num_fetched = 0
156
+
157
+ while !blockhashes_chain.empty?
158
+ blockhashes_batch = blockhashes_chain[0, MAX_BLOCKS_PER_REQUEST]
159
+ t_blocks = []
160
+
161
+ protos = self.protocols
162
+ if protos.empty?
163
+ logger.warn 'no protocols available'
164
+ return task_exit(false)
165
+ end
166
+
167
+ proto = nil
168
+ reply_proto = nil
169
+ protos.each do |_proto|
170
+ proto = _proto
171
+
172
+ next if proto.stopped?
173
+ raise AssertError if @requests.has_key?(proto)
174
+
175
+ logger.debug 'requesting blocks', num: blockhashes_batch.size
176
+ deferred = Concurrent::IVar.new
177
+ @requests[proto] = deferred
178
+
179
+ proto.async.send_getblocks *blockhashes_batch
180
+ begin
181
+ t_blocks = deferred.value(BLOCKS_REQUEST_TIMEOUT)
182
+ rescue Defer::TimedOut
183
+ logger.warn 'getblocks timed out, trying next proto'
184
+ next
185
+ ensure
186
+ @requests.delete proto
187
+ end
188
+
189
+ if t_blocks.empty?
190
+ logger.warn 'empty getblocks reply, trying next proto'
191
+ next
192
+ elsif !t_blocks.all? {|b| b.instance_of?(TransientBlock) }
193
+ logger.warn 'received unexpected data', data: t_blocks
194
+ t_blocks = []
195
+ next
196
+ end
197
+
198
+ unless t_blocks.map {|b| b.header.full_hash } == blockhashes_batch[0, t_blocks.size]
199
+ logger.warn 'received wrong blocks, should ban peer'
200
+ t_blocks = []
201
+ next
202
+ end
203
+
204
+ reply_proto = proto
205
+ break
206
+ end
207
+
208
+ # add received t_blocks
209
+ num_fetched += t_blocks.size
210
+ logger.debug "received blocks", num: t_blocks.size, num_fetched: num_fetched, total: num_blocks, missing: (num_blocks - num_fetched)
211
+
212
+ if t_blocks.empty?
213
+ logger.warn 'failed to fetch blocks', missing: blockhashes_chain.size
214
+ return task_exit(false)
215
+ end
216
+
217
+ t = Time.now
218
+ logger.debug 'adding blocks', qsize: @chainservice.block_queue.size
219
+ t_blocks.each do |blk|
220
+ b = blockhashes_chain.shift
221
+ raise AssertError unless blk.header.full_hash == b
222
+ raise AssertError if blockhashes_chain.include?(blk.header.full_hash)
223
+
224
+ @chainservice.add_block blk, reply_proto # this blocks if the queue is full
225
+ end
226
+ logger.debug 'adding blocks done', took: (Time.now - t)
227
+ end
228
+
229
+ # done
230
+ last_block = t_blocks.last
231
+ raise AssertError, 'still missing blocks' unless blockhashes_chain.empty?
232
+ raise AssertError, 'still missing blocks' unless last_block.header.full_hash == @blockhash
233
+ logger.debug 'syncing finished'
234
+
235
+ # at this time blocks are not in the chain yet, but in the add_block queue
236
+ if @chain_difficulty >= @chain.head.chain_difficulty
237
+ @chainservice.broadcast_newblock last_block, @chain_difficulty, proto
238
+ end
239
+
240
+ task_exit(true)
241
+ rescue
242
+ logger.error $!
243
+ logger.error $!.backtrace[0,10].join("\n")
244
+ task_exit(false)
245
+ end
246
+
247
+ def receive_blocks(proto, t_blocks)
248
+ logger.debug 'blocks received', proto: proto, num: t_blocks.size
249
+ unless @requests.has_key?(proto)
250
+ logger.debug 'unexpected blocks'
251
+ return
252
+ end
253
+ @requests[proto].set t_blocks
254
+ end
255
+
256
+ def receive_blockhashes(proto, blockhashes)
257
+ logger.debug 'blockhashes received', proto: proto, num: blockhashes.size
258
+ unless @requests.has_key?(proto)
259
+ logger.debug 'unexpected blockhashes'
260
+ return
261
+ end
262
+ @requests[proto].set blockhashes
263
+ end
264
+
265
+ private
266
+
267
+ def logger
268
+ @logger ||= Logger.new('eth.sync.task')
269
+ end
270
+
271
+ end
272
+
273
+ end