shilling 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+
2
+
3
+ Block = BlockchainLite::ProofOfWork::Block
4
+
5
+ ## see https://github.com/openblockchains/blockchain.lite.rb/blob/master/lib/blockchain-lite/proof_of_work/block.rb
6
+
7
+ ######
8
+ ## add more methods
9
+
10
+ class Block
11
+
12
+
13
+ def to_h
14
+ { index: @index,
15
+ timestamp: @timestamp,
16
+ nonce: @nonce,
17
+ transactions: @transactions.map { |tx| tx.to_h },
18
+ transactions_hash: @transactions_hash,
19
+ previous_hash: @previous_hash,
20
+ hash: @hash }
21
+ end
22
+
23
+ def self.from_h( h )
24
+ transactions = h['transactions'].map { |h_tx| Tx.from_h( h_tx ) }
25
+
26
+ ## todo: use hash and transactions_hash to check integrity of block - why? why not?
27
+
28
+ ## parse iso8601 format e.g 2017-10-05T22:26:12-04:00
29
+ timestamp = Time.parse( h['timestamp'] )
30
+
31
+ self.new( h['index'],
32
+ transactions,
33
+ h['previous_hash'],
34
+ timestamp: timestamp,
35
+ nonce: h['nonce'].to_i )
36
+ end
37
+
38
+
39
+ def valid?
40
+ true ## for now always valid
41
+ end
42
+
43
+
44
+ end # class Block
@@ -0,0 +1,47 @@
1
+
2
+
3
+
4
+ class Blockchain
5
+ extend Forwardable
6
+ def_delegators :@chain, :[], :size, :each, :empty?, :any?, :last
7
+
8
+
9
+ def initialize( chain=[] )
10
+ @chain = chain
11
+ end
12
+
13
+ def <<( txs )
14
+ ## todo: check if is block or array
15
+ ## if array (of transactions) - auto-add (build) block
16
+ ## allow block - why? why not?
17
+ ## for now just use transactions (keep it simple :-)
18
+
19
+ if @chain.size == 0
20
+ block = Block.first( txs )
21
+ else
22
+ block = Block.next( @chain.last, txs )
23
+ end
24
+ @chain << block
25
+ end
26
+
27
+
28
+
29
+ def as_json
30
+ @chain.map { |block| block.to_h }
31
+ end
32
+
33
+ def transactions
34
+ ## "accumulate" get all transactions from all blocks "reduced" into a single array
35
+ @chain.reduce( [] ) { |acc, block| acc + block.transactions }
36
+ end
37
+
38
+
39
+
40
+ def self.from_json( data )
41
+ ## note: assumes data is an array of block records/objects in json
42
+ chain = data.map { |h| Block.from_h( h ) }
43
+ self.new( chain )
44
+ end
45
+
46
+
47
+ end # class Blockchain
@@ -0,0 +1,22 @@
1
+
2
+
3
+ class Cache
4
+ def initialize( name )
5
+ @name = name
6
+ end
7
+
8
+ def write( data )
9
+ File.open( @name, 'w:utf-8' ) do |f|
10
+ f.write JSON.pretty_generate( data )
11
+ end
12
+ end
13
+
14
+ def read
15
+ if File.exists?( @name )
16
+ data = File.open( @name, 'r:bom|utf-8' ).read
17
+ JSON.parse( data )
18
+ else
19
+ nil
20
+ end
21
+ end
22
+ end ## class Cache
@@ -0,0 +1,30 @@
1
+
2
+ class Ledger
3
+ attr_reader :wallets ## use addresses - why? why not? for now single address wallet (wallet==address)
4
+
5
+ def initialize( chain=[] )
6
+ @wallets = {}
7
+ chain.each do |block|
8
+ apply_transactions( block.transactions )
9
+ end
10
+ end
11
+
12
+ def sufficient_funds?( wallet, amount )
13
+ return true if Shilling.config.coinbase?( wallet )
14
+ @wallets.has_key?( wallet ) && @wallets[wallet] - amount >= 0
15
+ end
16
+
17
+
18
+ private
19
+
20
+ def apply_transactions( transactions )
21
+ transactions.each do |tx|
22
+ if sufficient_funds?(tx.from, tx.amount)
23
+ @wallets[tx.from] -= tx.amount unless Shilling.config.coinbase?( tx.from )
24
+ @wallets[tx.to] ||= 0
25
+ @wallets[tx.to] += tx.amount
26
+ end
27
+ end
28
+ end
29
+
30
+ end ## class Ledger
@@ -0,0 +1,82 @@
1
+
2
+
3
+ class Node
4
+ attr_reader :id, :peers, :wallet, :bank
5
+
6
+ def initialize( address: )
7
+ @id = SecureRandom.uuid
8
+ @peers = []
9
+ @wallet = Wallet.new( address )
10
+ @bank = Bank.new @wallet.address
11
+ end
12
+
13
+
14
+
15
+ def on_add_peer( host, port )
16
+ @peers << [host, port]
17
+ @peers.uniq!
18
+ # TODO/FIX: no need to send to every peer, just the new one
19
+ send_chain_to_peers
20
+ @bank.pending.each { |tx| send_transaction_to_peers( tx ) }
21
+ end
22
+
23
+ def on_delete_peer( index )
24
+ @peers.delete_at( index )
25
+ end
26
+
27
+
28
+ def on_add_transaction( from, to, amount, id )
29
+ ## note: for now must always pass in id - why? why not? possible tx without id???
30
+ tx = Tx.new( from, to, amount, id )
31
+ if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
32
+ send_transaction_to_peers( tx )
33
+ return true
34
+ else
35
+ return false
36
+ end
37
+ end
38
+
39
+ def on_send( to, amount )
40
+ tx = @wallet.generate_transaction( to, amount )
41
+ if @bank.sufficient_funds?( tx.from, tx.amount ) && @bank.add_transaction( tx )
42
+ send_transaction_to_peers( tx )
43
+ return true
44
+ else
45
+ return false
46
+ end
47
+ end
48
+
49
+
50
+ def on_mine!
51
+ @bank.mine_block!
52
+ send_chain_to_peers
53
+ end
54
+
55
+ def on_resolve( data )
56
+ chain_new = Blockchain.from_json( data )
57
+ if @bank.resolve!( chain_new )
58
+ send_chain_to_peers
59
+ return true
60
+ else
61
+ return false
62
+ end
63
+ end
64
+
65
+
66
+
67
+ private
68
+
69
+ def send_chain_to_peers
70
+ data = JSON.pretty_generate( @bank.as_json ) ## payload in json
71
+ @peers.each do |(host, port)|
72
+ Net::HTTP.post(URI::HTTP.build(host: host, port: port, path: '/resolve'), data )
73
+ end
74
+ end
75
+
76
+ def send_transaction_to_peers( tx )
77
+ @peers.each do |(host, port)|
78
+ Net::HTTP.post_form(URI::HTTP.build(host: host, port: port, path: '/transactions'), tx.to_h )
79
+ end
80
+ end
81
+
82
+ end ## class Node
@@ -0,0 +1,42 @@
1
+ ####################################
2
+ # pending (unconfirmed) transactions (mem) pool
3
+
4
+ class Pool
5
+ extend Forwardable
6
+ def_delegators :@transactions, :[], :size, :each, :empty?, :any?
7
+
8
+
9
+ def initialize( transactions=[] )
10
+ @transactions = transactions
11
+ end
12
+
13
+ def transactions() @transactions; end
14
+
15
+ def <<( tx )
16
+ @transactions << tx
17
+ end
18
+
19
+
20
+ def update!( txns_confirmed )
21
+ ## find a better name?
22
+ ## remove confirmed transactions from pool
23
+
24
+ ## document - keep only pending transaction not yet (confirmed) in blockchain ????
25
+ @transactions = @transactions.select do |tx_unconfirmed|
26
+ txns_confirmed.none? { |tx_confirmed| tx_confirmed.id == tx_unconfirmed.id }
27
+ end
28
+ end
29
+
30
+
31
+
32
+ def as_json
33
+ @transactions.map { |tx| tx.to_h }
34
+ end
35
+
36
+ def self.from_json( data )
37
+ ## note: assumes data is an array of block records/objects in json
38
+ transactions = data.map { |h| Tx.from_h( h ) }
39
+ self.new( transactions )
40
+ end
41
+
42
+ end # class Pool
@@ -0,0 +1,113 @@
1
+ # encoding: utf-8
2
+
3
+ module Shilling
4
+
5
+ class Service < Sinatra::Base
6
+
7
+ def self.banner
8
+ "shilling/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] on Sinatra/#{Sinatra::VERSION} (#{ENV['RACK_ENV']})"
9
+ end
10
+
11
+
12
+ PUBLIC_FOLDER = "#{Shilling.root}/lib/shilling/public"
13
+ VIEWS_FOLDER = "#{Shilling.root}/lib/shilling/views"
14
+
15
+ set :public_folder, PUBLIC_FOLDER # set up the static dir (with images/js/css inside)
16
+ set :views, VIEWS_FOLDER # set up the views dir
17
+
18
+ set :static, true # set up static file routing -- check - still needed?
19
+
20
+
21
+ set connections: []
22
+
23
+
24
+
25
+ get '/style.css' do
26
+ scss :style ## note: converts (pre-processes) style.scss to style.css
27
+ end
28
+
29
+
30
+ get '/' do
31
+ @node = node ## todo: pass along node as hash varialbe / assigns to erb
32
+ erb :index
33
+ end
34
+
35
+
36
+ post '/send' do
37
+ node.on_send( params[:to], params[:amount].to_i )
38
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
39
+ redirect '/'
40
+ end
41
+
42
+
43
+ post '/transactions' do
44
+ if node.on_add_transaction(
45
+ params[:from],
46
+ params[:to],
47
+ params[:amount].to_i,
48
+ params[:id]
49
+ )
50
+ settings.connections.each { |out| out << "data: added transaction\n\n" }
51
+ end
52
+ redirect '/'
53
+ end
54
+
55
+ post '/mine' do
56
+ node.on_mine!
57
+ redirect '/'
58
+ end
59
+
60
+ post '/peers' do
61
+ node.on_add_peer( params[:host], params[:port].to_i )
62
+ redirect '/'
63
+ end
64
+
65
+ post '/peers/:index/delete' do
66
+ node.on_delete_peer( params[:index].to_i )
67
+ redirect '/'
68
+ end
69
+
70
+
71
+
72
+ post '/resolve' do
73
+ data = JSON.parse(request.body.read)
74
+ if data['chain'] && node.on_resolve( data['chain'] )
75
+ status 202 ### 202 Accepted; see httpstatuses.com/202
76
+ settings.connections.each { |out| out << "data: resolved\n\n" }
77
+ else
78
+ status 200 ### 200 OK
79
+ end
80
+ end
81
+
82
+
83
+ get '/events', provides: 'text/event-stream' do
84
+ stream :keep_open do |out|
85
+ settings.connections << out
86
+ out.callback { settings.connections.delete(out) }
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ #########
93
+ ## return network node (built and configured on first use)
94
+ ## fix: do NOT use @@ - use a class level method or something
95
+ def node
96
+ if defined?( @@node )
97
+ @@node
98
+ else
99
+ puts "[debug] shilling - build (network) node (address: #{Shilling.config.address})"
100
+ @@node = Node.new( address: Shilling.config.address )
101
+ @@node
102
+ end
103
+ ####
104
+ ## check why this is a syntax error:
105
+ ## @node ||= do
106
+ ## puts "[debug] shilling - build (network) node (address: #{Shilling.config.address})"
107
+ ## @node = Node.new( address: Shilling.config.address )
108
+ ## end
109
+ end
110
+
111
+ end # class Service
112
+
113
+ end # module Shilling
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ module Shilling
5
+
6
+ class Tool
7
+
8
+ def run( args )
9
+ opts = {}
10
+
11
+ parser = OptionParser.new do |cmd|
12
+ cmd.banner = "Usage: shilling [options]"
13
+
14
+ cmd.separator ""
15
+ cmd.separator " Wallet options:"
16
+
17
+ cmd.on("-n", "--name=NAME", "Address name (default: Theresa)") do |name|
18
+ ## use -a or --adr or --address as option flag - why? why not?
19
+ ## note: default now picks a random address from WALLET_ADDRESSES
20
+ opts[:address] = name
21
+ end
22
+
23
+
24
+ cmd.separator ""
25
+ cmd.separator " Server (node) options:"
26
+
27
+ cmd.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |host|
28
+ opts[:Host] = host ## note: rack server handler expects :Host
29
+ end
30
+
31
+ cmd.on("-p", "--port PORT", "use PORT (default: 4567)") do |port|
32
+ opts[:Port] = port ## note: rack server handler expects :Post
33
+ end
34
+
35
+ cmd.on("-h", "--help", "Prints this help") do
36
+ puts cmd
37
+ exit
38
+ end
39
+ end
40
+
41
+ parser.parse!( args )
42
+ pp opts
43
+
44
+
45
+ ###################
46
+ ## startup server (via rack interface/handler)
47
+
48
+ app_class = Service ## use app = Service.new -- why? why not?
49
+ host = opts[:Host] || '0.0.0.0'
50
+ port = opts[:Port] || '4567'
51
+
52
+ Shilling.configure do |config|
53
+ config.address = opts[:address] || 'Theresa'
54
+ end
55
+
56
+ Rack::Handler::WEBrick.run( app_class, Host: host, Port: port ) do |server|
57
+ ## todo: add traps here - why, why not??
58
+ end
59
+
60
+
61
+ end ## method run
62
+
63
+
64
+ end ## class Tool
65
+
66
+ end ## module Shilling