dcha 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/.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
|