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