dcha 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ module Dcha
2
+ # :nodoc:
3
+ module Store
4
+ class DataUnavailableError < StandardError; end
5
+
6
+ autoload :Memory, 'dcha/store/memory'
7
+ autoload :File, 'dcha/store/file'
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,12 @@
1
+ module Dcha
2
+ module Store
3
+ # :nodoc:
4
+ class Memory < Hash
5
+ def [](key)
6
+ value = super
7
+ raise DataUnavailableError, key if value.nil?
8
+ value
9
+ end
10
+ end
11
+ end
12
+ 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