dcha 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +47 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dcha.gemspec +29 -0
- data/exe/dcha +10 -0
- data/lib/dcha.rb +24 -0
- data/lib/dcha/block.rb +36 -0
- data/lib/dcha/chain.rb +49 -0
- data/lib/dcha/chunk.rb +43 -0
- data/lib/dcha/config.rb +24 -0
- data/lib/dcha/mpt.rb +8 -0
- data/lib/dcha/mpt/nibble_key.rb +110 -0
- data/lib/dcha/mpt/node.rb +64 -0
- data/lib/dcha/mpt/node/deletable.rb +83 -0
- data/lib/dcha/mpt/node/editable.rb +135 -0
- data/lib/dcha/mpt/node/findable.rb +37 -0
- data/lib/dcha/mpt/node/to_hashable.rb +42 -0
- data/lib/dcha/mpt/trie.rb +73 -0
- data/lib/dcha/packet_manager.rb +28 -0
- data/lib/dcha/peer.rb +90 -0
- data/lib/dcha/peer/can_heartbeat.rb +17 -0
- data/lib/dcha/peer/has_blockchain.rb +59 -0
- data/lib/dcha/peer/has_trie.rb +54 -0
- data/lib/dcha/peer/remote_executable.rb +25 -0
- data/lib/dcha/store.rb +9 -0
- data/lib/dcha/store/file.rb +37 -0
- data/lib/dcha/store/memory.rb +12 -0
- data/lib/dcha/ui.rb +84 -0
- data/lib/dcha/ui/window.rb +52 -0
- data/lib/dcha/version.rb +3 -0
- metadata +164 -0
data/lib/dcha/peer.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'dcha/peer/remote_executable'
|
2
|
+
require 'dcha/peer/can_heartbeat'
|
3
|
+
require 'dcha/peer/has_trie'
|
4
|
+
require 'dcha/peer/has_blockchain'
|
5
|
+
|
6
|
+
module Dcha
|
7
|
+
# :nodoc:
|
8
|
+
class Peer
|
9
|
+
include Observable
|
10
|
+
include RemoteExecutable
|
11
|
+
include HasTrie
|
12
|
+
include HasBlockchain
|
13
|
+
include CanHeartbeat
|
14
|
+
|
15
|
+
MULTICAST_ADDR = '224.5.5.55'.freeze
|
16
|
+
BIND_ADDR = '0.0.0.0'.freeze
|
17
|
+
PORT = '5555'.freeze
|
18
|
+
|
19
|
+
attr_reader :peers, :hostname, :ipaddr
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@hostname = Socket.gethostname
|
23
|
+
@ipaddr = Addrinfo.getaddrinfo(hostname, nil, :INET).first
|
24
|
+
@peers = []
|
25
|
+
@thread = nil
|
26
|
+
@packets = PacketManager.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def join
|
30
|
+
return if listening?
|
31
|
+
listen
|
32
|
+
ping
|
33
|
+
end
|
34
|
+
|
35
|
+
def listening?
|
36
|
+
@listening == true
|
37
|
+
end
|
38
|
+
|
39
|
+
def transmit(data)
|
40
|
+
transmit_to MULTICAST_ADDR, data
|
41
|
+
end
|
42
|
+
|
43
|
+
def transmit_to(address, data)
|
44
|
+
Chunk.split(data).each do |bytes|
|
45
|
+
socket.send(bytes.pack('C*'), 0, address, PORT)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def listen
|
52
|
+
socket.bind(BIND_ADDR, PORT)
|
53
|
+
puts "Listen #{ipaddr.ip_address}:#{PORT} on #{hostname} "
|
54
|
+
@thread = Thread.new { process }
|
55
|
+
@listening = true
|
56
|
+
end
|
57
|
+
|
58
|
+
def process
|
59
|
+
loop do
|
60
|
+
resolve @packets.todo.pop(true) until @packets.todo.empty?
|
61
|
+
receive
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def resolve(event)
|
66
|
+
raise StandardError, event if event == 'store_set'
|
67
|
+
execute event[:action], event[:on], event[:params]
|
68
|
+
changed
|
69
|
+
notify_observers event[:action], event[:on], event[:params], Time.now
|
70
|
+
end
|
71
|
+
|
72
|
+
def receive
|
73
|
+
bytes, = socket.recvfrom(512)
|
74
|
+
chunk = Chunk.new(bytes.unpack('C*'))
|
75
|
+
@packets << chunk
|
76
|
+
end
|
77
|
+
|
78
|
+
def socket
|
79
|
+
@socket ||= UDPSocket.open.tap do |socket|
|
80
|
+
socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, bind_address)
|
81
|
+
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
|
82
|
+
socket.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def bind_address
|
87
|
+
IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Dcha
|
2
|
+
class Peer
|
3
|
+
# :nodoc:
|
4
|
+
module CanHeartbeat
|
5
|
+
def ping
|
6
|
+
transmit action: :pong, params: [ipaddr.ip_address]
|
7
|
+
end
|
8
|
+
|
9
|
+
def pong(address)
|
10
|
+
@peers.push(address).uniq!
|
11
|
+
transmit action: :mine, params: [chain.blocks]
|
12
|
+
return if ipaddr.ip_address == address
|
13
|
+
transmit_to address, action: :pong, params: [ipaddr.ip_address]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Dcha
|
2
|
+
class Peer
|
3
|
+
# :nodoc:
|
4
|
+
module HasBlockchain
|
5
|
+
def chain
|
6
|
+
@chain ||= Chain.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def blocks(_address)
|
10
|
+
transmit action: :mine, params: [chain.blocks]
|
11
|
+
end
|
12
|
+
|
13
|
+
def mine(blocks)
|
14
|
+
blocks.sort! { |x, y| x.index <=> y.index }
|
15
|
+
return unless blocks.last.index > chain.blocks.last.index
|
16
|
+
append_blocks(blocks)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_block(root_hash)
|
20
|
+
return unless chain.create_and_add_block(root_hash)
|
21
|
+
transmit action: :mine, params: [chain.blocks.last(1)]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def append_blocks(blocks)
|
27
|
+
if block_linked?(blocks)
|
28
|
+
add_block(blocks)
|
29
|
+
elsif blocks.length == 1
|
30
|
+
ask_blocks
|
31
|
+
elsif blocks.length > 1
|
32
|
+
replace_blocks(blocks)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_block(blocks)
|
37
|
+
return unless blocks.last.valid_proof?
|
38
|
+
chain.add_block(blocks.last)
|
39
|
+
reset(blocks.last.root_hash)
|
40
|
+
transmit action: :mine, params: [blocks.last(1)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def ask_blocks
|
44
|
+
transmit action: :blocks, params: [ipaddr.ip_address]
|
45
|
+
end
|
46
|
+
|
47
|
+
def replace_blocks(blocks)
|
48
|
+
blocks.shift if blocks.first.index.zero?
|
49
|
+
chain.replace_with(blocks)
|
50
|
+
reset(blocks.last.root_hash)
|
51
|
+
transmit action: :mine, params: [blocks.last(1)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def block_linked?(blocks)
|
55
|
+
blocks.last.parent_hash == chain.blocks.last.hash
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Dcha
|
2
|
+
class Peer
|
3
|
+
# :nodoc:
|
4
|
+
module HasTrie
|
5
|
+
def trie
|
6
|
+
@retry = false
|
7
|
+
@trie ||= MPT::Trie.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset(root_hash)
|
11
|
+
return if root_hash == trie.root_hash
|
12
|
+
@trie = MPT::Trie.new(root_hash)
|
13
|
+
rescue Store::DataUnavailableError => e
|
14
|
+
transmit action: :store_get, params: [
|
15
|
+
root_hash,
|
16
|
+
e.message,
|
17
|
+
ipaddr.ip_address
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def store_get(root_hash, key, _address)
|
22
|
+
return if root_hash != trie.root_hash
|
23
|
+
changed
|
24
|
+
transmit action: :store_set, params: [
|
25
|
+
root_hash,
|
26
|
+
key,
|
27
|
+
Config.store[key]
|
28
|
+
]
|
29
|
+
rescue Store::DataUnavailableError
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def store_set(root_hash, key, value)
|
34
|
+
Config.store[key] = value
|
35
|
+
reset(root_hash) if root_hash != trie.root_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def write(key, value)
|
39
|
+
trie[key] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def read(key)
|
43
|
+
trie[key]
|
44
|
+
rescue Store::DataUnavailableError => e
|
45
|
+
transmit action: :store_get, params: [
|
46
|
+
trie.root_hash,
|
47
|
+
e.message,
|
48
|
+
ipaddr.ip_address
|
49
|
+
]
|
50
|
+
'[SYNCING]'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dcha
|
2
|
+
class Peer
|
3
|
+
# :nodoc:
|
4
|
+
module RemoteExecutable
|
5
|
+
EXECUTABLE_OBJECT = %w[trie chain].freeze
|
6
|
+
|
7
|
+
def execute(action, object_name = nil, params = [])
|
8
|
+
return execute_on(self, action, params) if object_name.nil?
|
9
|
+
object = pickup_object(object_name)
|
10
|
+
return if object.nil?
|
11
|
+
execute_on(object, action, params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute_on(object, action, params = [])
|
15
|
+
return unless object.respond_to?(action)
|
16
|
+
object.send(action, *params)
|
17
|
+
end
|
18
|
+
|
19
|
+
def pickup_object(name)
|
20
|
+
return unless EXECUTABLE_OBJECT.include?(name)
|
21
|
+
instance_variable_get("@#{name}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/dcha/store.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dcha
|
2
|
+
module Store
|
3
|
+
# :nodoc:
|
4
|
+
class File < Hash
|
5
|
+
def initialize(path)
|
6
|
+
@path = Pathname.new(path).realdirpath
|
7
|
+
Dir.mkdir(path) unless Dir.exist?(path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
super || load_from(key)
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(key, value)
|
15
|
+
save_to(key, value)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear!
|
20
|
+
FileUtils.rm_rf(path)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def load_from(key)
|
26
|
+
path = "#{@path}/#{key}"
|
27
|
+
raise DataUnavailableError, key unless File.exist?(path)
|
28
|
+
self[key] = File.read(path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def save_to(key, value)
|
32
|
+
path = "#{@path}/#{key}"
|
33
|
+
File.write(path, value) unless File.exist?(path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/dcha/ui.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'dcha/ui/window'
|
2
|
+
|
3
|
+
module Dcha
|
4
|
+
# TODO: Implement `curses` ui
|
5
|
+
# :nodoc:
|
6
|
+
class UI
|
7
|
+
include Curses
|
8
|
+
|
9
|
+
def initialize(peer)
|
10
|
+
@window = Window.new(0, 0, 0, 0)
|
11
|
+
@peer = peer
|
12
|
+
@logs = []
|
13
|
+
@input = ''
|
14
|
+
@peer.add_observer(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def show
|
18
|
+
until @input == 'exit'
|
19
|
+
parse
|
20
|
+
refresh
|
21
|
+
@input = @window.getstr
|
22
|
+
end
|
23
|
+
@window.close
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(action, _, _params, time)
|
27
|
+
@logs.push("Execute #{action} at #{time}")
|
28
|
+
refresh
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def refresh
|
34
|
+
@window.update do
|
35
|
+
show_logs
|
36
|
+
show_peers
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def read
|
41
|
+
key = @input.split(' ').last
|
42
|
+
@logs.push "#{key} -> #{@peer.read(key)}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def write
|
46
|
+
_, key, value = @input.split(' ')
|
47
|
+
@peer.write(key, value)
|
48
|
+
@peer.create_block(@peer.trie.root_hash)
|
49
|
+
end
|
50
|
+
|
51
|
+
def chain
|
52
|
+
@logs.push "CHAIN BLOCKS: #{@peer.chain.blocks.size}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def root
|
56
|
+
@logs.push "TRIE ROOT: #{@peer.trie.root_hash}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse
|
60
|
+
return if @input.empty?
|
61
|
+
return read if @input.start_with?('GET')
|
62
|
+
return write if @input.start_with?('SET')
|
63
|
+
return chain if @input.start_with?('CHAIN')
|
64
|
+
return root if @input.start_with?('ROOT')
|
65
|
+
end
|
66
|
+
|
67
|
+
def show_peers
|
68
|
+
message = "Peers: #{@peer.peers.size} >"
|
69
|
+
offset = message.size + 2
|
70
|
+
@window.setpos(@window.maxy - 2, offset)
|
71
|
+
@window.heading = message
|
72
|
+
@window.peers = @peer.peers
|
73
|
+
end
|
74
|
+
|
75
|
+
def show_logs
|
76
|
+
max_log = @window.maxy - 5
|
77
|
+
start_pos = @window.maxx * 0.3 + 2
|
78
|
+
@logs.last(max_log).each.with_index do |log, index|
|
79
|
+
@window.setpos(index + 1, start_pos)
|
80
|
+
@window.addstr(log)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Dcha
|
2
|
+
class UI
|
3
|
+
# :nodoc:
|
4
|
+
class Window < Curses::Window
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
|
8
|
+
@sidebar = subwin(maxy - 2, maxx * 0.3, 0, 0)
|
9
|
+
@input = subwin(3, 0, maxy - 3, 0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(&_block)
|
13
|
+
clear
|
14
|
+
update_sidebar
|
15
|
+
box('|', '-')
|
16
|
+
update_input
|
17
|
+
yield if block_given?
|
18
|
+
refresh
|
19
|
+
end
|
20
|
+
|
21
|
+
def heading=(head)
|
22
|
+
@input.setpos(1, 1)
|
23
|
+
@input.addstr(head)
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: Cut off string if overflow
|
27
|
+
def peers=(peers)
|
28
|
+
peers = peers.take(maxy - 5)
|
29
|
+
peers.push('And more ...') if peers.size == maxy - 5
|
30
|
+
peers.each.with_index do |peer, index|
|
31
|
+
@sidebar.setpos(index + 1, 2)
|
32
|
+
@sidebar.addstr(peer)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def update_sidebar
|
39
|
+
@sidebar.clear
|
40
|
+
@sidebar.resize(maxy - 2, maxx * 0.3)
|
41
|
+
@sidebar.box('|', '-')
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: Fix position incorrect after resize
|
45
|
+
def update_input
|
46
|
+
@input.clear
|
47
|
+
@input.move(maxy - 3, 0)
|
48
|
+
@input.box('|', '-')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|