dropzone_ruby 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Drop Zone - An Anonymous Peer-To-Peer Local Contraband Marketplace.pdf +0 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +69 -0
  6. data/README.md +62 -0
  7. data/bin/dropzone +487 -0
  8. data/dropzone-screenshot.jpg +0 -0
  9. data/dropzone_ruby.gemspec +31 -0
  10. data/lib/blockrio_ext.rb +52 -0
  11. data/lib/dropzone/buyer.rb +21 -0
  12. data/lib/dropzone/command.rb +488 -0
  13. data/lib/dropzone/communication.rb +43 -0
  14. data/lib/dropzone/connection.rb +312 -0
  15. data/lib/dropzone/invoice.rb +23 -0
  16. data/lib/dropzone/item.rb +160 -0
  17. data/lib/dropzone/listing.rb +64 -0
  18. data/lib/dropzone/message_base.rb +178 -0
  19. data/lib/dropzone/payment.rb +36 -0
  20. data/lib/dropzone/profile.rb +86 -0
  21. data/lib/dropzone/record_base.rb +34 -0
  22. data/lib/dropzone/seller.rb +21 -0
  23. data/lib/dropzone/session.rb +161 -0
  24. data/lib/dropzone/state_accumulator.rb +39 -0
  25. data/lib/dropzone/version.rb +4 -0
  26. data/lib/dropzone_ruby.rb +14 -0
  27. data/lib/veto_checks.rb +74 -0
  28. data/spec/bitcoin_spec.rb +115 -0
  29. data/spec/buyer_profile_spec.rb +279 -0
  30. data/spec/buyer_spec.rb +109 -0
  31. data/spec/command_spec.rb +353 -0
  32. data/spec/config.yml +5 -0
  33. data/spec/invoice_spec.rb +129 -0
  34. data/spec/item_spec.rb +294 -0
  35. data/spec/lib/fake_connection.rb +97 -0
  36. data/spec/listing_spec.rb +150 -0
  37. data/spec/payment_spec.rb +152 -0
  38. data/spec/seller_profile_spec.rb +290 -0
  39. data/spec/seller_spec.rb +120 -0
  40. data/spec/session_spec.rb +303 -0
  41. data/spec/sham/buyer.rb +5 -0
  42. data/spec/sham/invoice.rb +5 -0
  43. data/spec/sham/item.rb +8 -0
  44. data/spec/sham/payment.rb +13 -0
  45. data/spec/sham/seller.rb +7 -0
  46. data/spec/spec_helper.rb +49 -0
  47. metadata +267 -0
@@ -0,0 +1,43 @@
1
+ module Dropzone
2
+ class Communication < MessageBase
3
+ class NoSymmKey < StandardError; end
4
+
5
+ attr_message i: :iv, c: :contents, d: :der, p: :session_pkey
6
+
7
+ message_type 'COMMUN'
8
+
9
+ attr_accessor :symm_key
10
+
11
+ def contents_plain
12
+ raise NoSymmKey unless symm_key
13
+
14
+ aes = OpenSSL::Cipher::Cipher.new Session::CIPHER_ALGORITHM
15
+ aes.decrypt
16
+ aes.key = symm_key
17
+ aes.iv = iv
18
+
19
+ aes.update(contents) + aes.final
20
+ end
21
+
22
+ def is_init?; (der && session_pkey); end
23
+ def is_auth?; !session_pkey.nil?; end
24
+ end
25
+
26
+ class Communication::Validator < ValidatorBase
27
+ validates :message_type, format: /\ACOMMUN\Z/
28
+
29
+ validates_if_present :der, is_string: true
30
+ validates_if_present :session_pkey, is_string: true
31
+ validates_if_present :contents, is_string: true
32
+ validates_if_present :iv, is_string: true
33
+
34
+ # Ders always need session_pkey:
35
+ validates :session_pkey, not_null: true, unless: 'self.der.nil?'
36
+
37
+ # Content always needs an iv:
38
+ validates :iv, not_null: true, unless: 'self.contents.nil?'
39
+
40
+ # We should always have either contents or a pkey:
41
+ validates :contents, not_null: true, if: 'self.session_pkey.nil?'
42
+ end
43
+ end
@@ -0,0 +1,312 @@
1
+ module Dropzone
2
+ class BitcoinConnection
3
+ class WalletFundsTooLowOrNoUTXOs < StandardError; end
4
+
5
+ SATOSHIS_IN_BTC = 100_000_000
6
+
7
+ TXO_DUST = 5430 # 0.0000543 BTC
8
+
9
+ PREFIX = 'DZ'
10
+
11
+ attr_reader :bitcoin
12
+
13
+ def initialize(network, options = {})
14
+ @network = network
15
+ @bitcoin = options[:bitcoin] if options.has_key? :bitcoin
16
+ @is_testing = (/\Atestnet/.match network.to_s) ? true : false
17
+ @bitcoin ||= BlockrIo.new is_testing?
18
+ end
19
+
20
+ def is_testing?; @is_testing; end
21
+
22
+ def privkey_to_addr(key)
23
+ set_network_mode!
24
+ Bitcoin::Key.from_base58(key).addr
25
+ end
26
+
27
+ def hash160_to_address(hash160)
28
+ set_network_mode!
29
+ Bitcoin.hash160_to_address hash160
30
+ end
31
+
32
+ def hash160_from_address(addr)
33
+ set_network_mode!
34
+ Bitcoin.hash160_from_address addr
35
+ end
36
+
37
+ def valid_address?(addr)
38
+ set_network_mode!
39
+ Bitcoin.valid_address? addr
40
+ end
41
+
42
+ # NOTE:
43
+ # - This needs to return the messages in Descending order by block
44
+ # In the case that two transactions are in the same block, it goes by time
45
+ # - This should only 'valid' return messages. Not transactions
46
+ def messages_by_addr(addr, options = {})
47
+ ret = cache.listtransactions addr, true, is_testing? if cache
48
+
49
+ unless ret
50
+ ret = bitcoin.listtransactions addr, true
51
+ cache.listtransactions! addr, true, ret, is_testing? if cache
52
+ end
53
+
54
+ ret = ret.collect{ |tx_h|
55
+ begin
56
+ msg = Dropzone::MessageBase.new_message_from tx_by_id(tx_h['tx'])
57
+
58
+ (msg && msg.valid?) ? msg : nil
59
+ rescue Counterparty::TxDecode::InvalidOutput,
60
+ Counterparty::TxDecode::MultisigUnsupported
61
+ next
62
+ end
63
+ }.compact
64
+
65
+ filter_messages ret, options
66
+ end
67
+
68
+ def messages_in_block(at_height, options = {})
69
+ ret = bitcoin.getblock(at_height).collect{ |tx_h|
70
+
71
+ # This is a speed hack that drastically reduces query times:
72
+ next if options[:type] == 'ITCRTE' && !tx_h_create?(tx_h)
73
+
74
+ begin
75
+ msg = Dropzone::MessageBase.new_message_from tx_by_id( tx_h['tx'],
76
+ block_height: at_height )
77
+
78
+ (msg && msg.valid?) ? msg : nil
79
+ rescue Counterparty::TxDecode::InvalidOutput,
80
+ Counterparty::TxDecode::MultisigUnsupported,
81
+ Counterparty::TxDecode::UndefinedBehavior,
82
+ Counterparty::TxDecode::InvalidOpReturn
83
+ next
84
+ end
85
+ }.compact
86
+
87
+ filter_messages ret, options
88
+ end
89
+
90
+ def send_value(from_key, to_addr, send_satoshis, tip_satoshis)
91
+ set_network_mode!
92
+
93
+ new_tx = create_tx(from_key, send_satoshis+tip_satoshis){ |tx, allocated|
94
+ [ [send_satoshis, Bitcoin.hash160_from_address(to_addr)],
95
+ [(allocated-send_satoshis-tip_satoshis), from_key.hash160]
96
+ ].each do |(amt, to)|
97
+ tx.add_out Bitcoin::P::TxOut.new( amt,
98
+ Bitcoin::Script.to_hash160_script(to) )
99
+ end
100
+ }
101
+
102
+ sign_and_send new_tx, from_key
103
+ end
104
+
105
+ def sign_tx(tx, key)
106
+ set_network_mode!
107
+
108
+ # Sign the transaction:
109
+ tx.inputs.length.times do |i|
110
+ # Fetch the previous input:
111
+ prev_out_tx_hash = tx.inputs[i].prev_out.reverse.unpack('H*').first
112
+ prev_tx_raw = bitcoin.getrawtransaction(prev_out_tx_hash)['hex']
113
+ prev_tx = Bitcoin::P::Tx.new [prev_tx_raw].pack('H*')
114
+
115
+ # Now we actually sign
116
+ sig = Bitcoin.sign_data Bitcoin.open_key(key.priv),
117
+ tx.signature_hash_for_input(i, prev_tx)
118
+ tx.in[i].script_sig = Bitcoin::Script.to_signature_pubkey_script( sig,
119
+ [key.pub].pack("H*"))
120
+ end
121
+ tx
122
+ end
123
+
124
+ def tx_by_id(id, options = {})
125
+ set_network_mode!
126
+
127
+ ret = cache.tx_by_id id, is_testing? if cache
128
+
129
+ unless ret
130
+ tx_h = bitcoin.getrawtransaction(id)
131
+ tx = Bitcoin::P::Tx.new [tx_h['hex']].pack('H*')
132
+
133
+ if tx_h.has_key? 'blockhash'
134
+ options[:block_height] = bitcoin.getblockinfo(tx_h['blockhash'])['nb']
135
+ end
136
+
137
+ record = Counterparty::TxDecode.new tx,
138
+ prefix: Dropzone::BitcoinConnection::PREFIX
139
+
140
+ ret = options.merge({ data: record.data,
141
+ receiver_addr: record.receiver_addr,
142
+ txid: id,
143
+ sender_addr: record.sender_addr})
144
+
145
+ # NOTE that in the case of a reorg, this might have incorrect block
146
+ # heights cached. It's probable that we can/should cache these, and
147
+ # merely set the block height when it's confirmed, and/or set the
148
+ # height to current_height+1
149
+ cache.tx_by_id! id, ret, is_testing? if cache && options[:block_height]
150
+ end
151
+
152
+ ret
153
+ end
154
+
155
+ def save!(data, private_key_wif)
156
+ set_network_mode!
157
+
158
+ from_key = Bitcoin::Key.from_base58 private_key_wif
159
+
160
+ # We need to know how many transactions we'll have in order to know how
161
+ # many satoshi's to allocate. We start with 1, since that's the return
162
+ # address of the allocated input satoshis
163
+ data_outputs_needed = 1
164
+
165
+ bytes_in_output = Counterparty::TxEncode::BYTES_IN_MULTISIG
166
+
167
+ # 3 is for the two-byte DZ prefix, and the 1-byte length
168
+ data_outputs_needed += ((data[:data].length+3) / bytes_in_output).ceil
169
+
170
+ # We'll need a P2PSH for the destination if that applies
171
+ data_outputs_needed += 1 if data.has_key? :receiver_addr
172
+ tip = data[:tip] || 0
173
+
174
+ new_tx = create_tx(from_key,data_outputs_needed * TXO_DUST+tip) do |tx, amount_allocated|
175
+ outputs = Counterparty::TxEncode.new(
176
+ [tx.inputs[0].prev_out.reverse_hth].pack('H*'),
177
+ data[:data], receiver_addr: data[:receiver_addr],
178
+ sender_pubkey: from_key.pub, prefix: PREFIX).to_opmultisig
179
+
180
+ outputs.each_with_index do |output,i|
181
+ tx.add_out Bitcoin::P::TxOut.new( (i == (outputs.length-1)) ?
182
+ (amount_allocated - tip - TXO_DUST*(outputs.length - 1)) : TXO_DUST,
183
+ Bitcoin::Script.binary_from_string(output) )
184
+ end
185
+ end
186
+
187
+ sign_and_send new_tx, from_key
188
+ end
189
+
190
+ def block_height
191
+ bitcoin.getblockinfo('last')['nb']
192
+ end
193
+
194
+ private
195
+
196
+ def filter_messages(messages, options = {})
197
+ messages = messages.find_all{|msg|
198
+ msg.block_height.nil? || (msg.block_height >= options[:start_block])
199
+ } if options[:start_block]
200
+
201
+ messages = messages.find_all{|msg|
202
+ msg.block_height.nil? || (msg.block_height <= options[:end_block])
203
+ } if options[:end_block]
204
+
205
+ if messages && options.has_key?(:type)
206
+ messages = messages.find_all{|msg| msg.message_type == options[:type]}
207
+ end
208
+
209
+ if options.has_key?(:between)
210
+ messages = messages.find_all{|c|
211
+ [c.receiver_addr, c.sender_addr].all?{|a| options[:between].include?(a) } }
212
+ end
213
+
214
+ (messages) ? messages : []
215
+ end
216
+
217
+
218
+ # This is a speed hack which keeps us from traversing through entire blocks
219
+ # by filtering based on the destination addresses
220
+ def tx_h_create?(tx_h)
221
+ address = tx_h['out'][0]['addr'] if [
222
+ tx_h.has_key?('out'), tx_h['out'][0], tx_h['out'][0]['addr'] ].all?
223
+
224
+ (address && Dropzone::Item::HASH_160_PARTS.match(address)) ? true : false
225
+ end
226
+
227
+ # Since the Bitcoin object is a singleton, and we'll be working alongside
228
+ # testnet, we need to start our methods by setting the correct network mode
229
+ def set_network_mode!
230
+ Bitcoin.network = @network
231
+ end
232
+
233
+ def sign_and_send(tx, key)
234
+ signed_hex = sign_tx(tx, key).to_payload.unpack('H*')[0]
235
+
236
+ bitcoin.sendrawtransaction signed_hex
237
+ end
238
+
239
+ def create_tx(key, allocate, &block)
240
+ # create a new transaction (and sign the inputs)
241
+ tx = Bitcoin::P::Tx.new(nil)
242
+ tx.ver, tx.lock_time = 1, 0
243
+
244
+ # allocate some inputs here:
245
+ amount_allocated = 0
246
+ allocate_inputs_for(key.addr, allocate).each do |unspent|
247
+ amount_allocated += to_satoshis(unspent['amount'])
248
+ tx.add_in Bitcoin::P::TxIn.new [unspent['tx'] ].pack('H*').reverse,
249
+ unspent['n'].to_i
250
+ end
251
+
252
+ block.call(tx, amount_allocated)
253
+
254
+ tx
255
+ end
256
+
257
+ # We expect the amount to be in satoshis. As-is this method does not allocate
258
+ # mempool utxos for a spend.
259
+ def allocate_inputs_for(addr, amount)
260
+ allocated = 0
261
+
262
+ # NOTE: I think we're issuing more queries here than we should be due to a
263
+ # fetch for every utxo, instead of one fetch for every transaction.
264
+ # (Which may have many utxo's per transaction.)
265
+ mempooled_utxos = bitcoin.listunconfirmed(addr).collect{|tx|
266
+ # Note that some api's may not retrieve the contents of an unconfirmed
267
+ # raw transaction.
268
+ unconfirmed_tx = bitcoin.getrawtransaction(tx['tx'])['hex']
269
+
270
+ if unconfirmed_tx
271
+ tx = Bitcoin::P::Tx.new [unconfirmed_tx].pack('H*')
272
+ tx.inputs.collect{|input| input.prev_out.reverse.unpack('H*').first}
273
+ else
274
+ nil
275
+ end
276
+ }.compact.flatten.uniq
277
+
278
+ utxos = []
279
+ bitcoin.listunspent(addr).sort_by{|utxo| utxo['confirmations']}.each{|utxo|
280
+ next if mempooled_utxos.include? utxo['tx']
281
+ utxos << utxo
282
+ allocated += to_satoshis(utxo['amount'])
283
+ break if allocated >= amount
284
+ }
285
+
286
+ raise WalletFundsTooLowOrNoUTXOs if allocated < amount
287
+
288
+ utxos
289
+ end
290
+
291
+ def to_satoshis(string)
292
+ self.class.to_satoshis string
293
+ end
294
+
295
+ def is_opreturn?(output)
296
+ /\AOP_RETURN/.match output
297
+ end
298
+
299
+ def self.to_satoshis(string)
300
+ (BigDecimal.new(string) * SATOSHIS_IN_BTC).to_i
301
+ end
302
+
303
+ def cache
304
+ self.class.cache
305
+ end
306
+
307
+ class << self
308
+ # This is a hook to speed up some operations via a local cache
309
+ attr_accessor :cache
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,23 @@
1
+ module Dropzone
2
+ class Invoice < MessageBase
3
+ attr_message_int p: :amount_due, e: :expiration_in
4
+
5
+ message_type 'INCRTE'
6
+
7
+ def payments
8
+ blockchain.messages_by_addr(sender_addr, type: 'INPAID',
9
+ start_block: block_height).find_all{|p| p.invoice_txid == txid }
10
+ end
11
+ end
12
+
13
+ class Invoice::Validator < ValidatorBase
14
+ include MessageValidations
15
+ include BillingValidations
16
+
17
+ validates :message_type, format: /\AINCRTE\Z/
18
+
19
+ [:amount_due, :expiration_in].each do |attr|
20
+ validates_if_present attr, integer: true, greater_than_or_equal_to: 0
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,160 @@
1
+ module Dropzone
2
+ class Item < MessageBase
3
+ EARTH_RADIUS_IN_METERS = 6_371_000
4
+
5
+ HASH_160_PARTS = /\A(?:mfZ|1DZ)([1-9X]{9})([1-9X]{9})([1-9X]{6}).+/
6
+
7
+ attr_message d: :description, c: :price_currency, t: :create_txid
8
+ attr_message_int p: :price_in_units, e: :expiration_in
9
+
10
+ # These are receiver address attributes, not message attribs:
11
+ attr_reader :latitude, :longitude, :radius
12
+
13
+ @types_include = ['ITUPDT', 'ITCRTE']
14
+
15
+ def latitude
16
+ (@receiver_addr) ?
17
+ integer_to_latlon( address_parts(@receiver_addr, 0) ) :
18
+ @latitude
19
+ end
20
+
21
+ def longitude
22
+ (@receiver_addr) ?
23
+ integer_to_latlon( address_parts(@receiver_addr, 1), 180 ) :
24
+ @longitude
25
+ end
26
+
27
+ def radius
28
+ (@receiver_addr) ?
29
+ address_parts(@receiver_addr, 2) :
30
+ @radius
31
+ end
32
+
33
+ def message_type
34
+ return @message_type if @message_type
35
+
36
+ create_txid ? 'ITUPDT' : 'ITCRTE'
37
+ end
38
+
39
+ # This is an easy guide to what we're doing here:
40
+ # http://www.reddit.com/r/Bitcoin/comments/2ss3en/calculating_checksum_for_bitcoin_address/
41
+ #
42
+ # NOTE: There was a digit off in the reference spec, this radius is a seven
43
+ # digit number, not an eight digit number.
44
+ def receiver_addr
45
+ case
46
+ when @receiver_addr then @receiver_addr
47
+ when create_txid then @sender_addr
48
+ when latitude && longitude && radius
49
+ receiver_addr_base = ('%s%09d%09d%06d' % [
50
+ (blockchain.is_testing?) ? 'mfZ' : ('1' + BitcoinConnection::PREFIX),
51
+ latlon_to_integer(latitude.to_f),
52
+ latlon_to_integer(longitude.to_f, 180),
53
+ radius.abs ]).tr('0','X')
54
+
55
+ # The x's pad the checksum component for us to ensure the base conversion
56
+ # produces the correct output. Similarly, we ignore them after the decode:
57
+ hex_address = Bitcoin.decode_base58(receiver_addr_base+'XXXXXXX')[0...42]
58
+
59
+ hash160 = [hex_address].pack('H*')
60
+
61
+ # Bitcoin-ruby has a method to do much of this for us, but it is
62
+ # broken in that it only supports main-net addresses, and not testnet3
63
+ checksum = Digest::SHA256.digest(Digest::SHA256.digest(hash160))[0,4]
64
+
65
+ # Return the checksum'd receiver_addr
66
+ Bitcoin.encode_base58((hash160 + checksum).unpack('H*').first)
67
+ else
68
+ nil
69
+ end
70
+ end
71
+
72
+ def self.blockchain
73
+ Dropzone::RecordBase.blockchain
74
+ end
75
+
76
+ # Returns all *Items created* since (and including) the provided block
77
+ # These are items and not listings, so as to query faster.
78
+ # Items are returned in the order of newest to oldest
79
+ def self.find_creates_since_block(starting_at, block_depth, &block)
80
+ starting_at.downto(starting_at-block_depth).collect{|i|
81
+ blockchain.messages_in_block(i, type: 'ITCRTE').collect do |item|
82
+ (block_given?) ? block.call(item, i) : item
83
+ end
84
+ }.flatten.compact
85
+ end
86
+
87
+ def self.find_in_radius(starting_at, block_depth, lat, long, in_meters, &block)
88
+ find_creates_since_block(starting_at, block_depth) do |item, nb|
89
+ if distance_between(item.latitude, item.longitude, lat, long) <= in_meters
90
+ (block_given?) ? block.call(item, nb) : item
91
+ end
92
+ end
93
+ end
94
+
95
+ # haversine formula, pulled from :
96
+ # http://www.movable-type.co.uk/scripts/latlong.html
97
+ def self.distance_between(lat1, lon1, lat2, lon2)
98
+ delta_phi = to_rad(lat2-lat1)
99
+ delta_lamba = to_rad(lon2-lon1)
100
+
101
+ a = Math.sin(delta_phi/2) ** 2 + [ Math.cos(to_rad(lat1)),
102
+ Math.cos(to_rad(lat2)), Math.sin(delta_lamba/2),
103
+ Math.sin(delta_lamba/2) ].reduce(:*)
104
+
105
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
106
+
107
+ EARTH_RADIUS_IN_METERS * c
108
+ end
109
+
110
+ def self.to_rad(angle)
111
+ angle.to_f / 180 * Math::PI
112
+ end
113
+
114
+ private
115
+
116
+ def address_parts(addr, part)
117
+ parts = HASH_160_PARTS.match(addr)
118
+ (parts.length > 0) ? parts[part+1].tr('X','0').to_i : nil
119
+ end
120
+
121
+ def latlon_to_integer(lat_or_lon, unsigned_offset = 90)
122
+ ((lat_or_lon + unsigned_offset) * 1_000_000).floor.abs unless lat_or_lon.nil?
123
+ end
124
+
125
+ def integer_to_latlon(lat_or_lon, unsigned_offset = 90)
126
+ (BigDecimal.new(lat_or_lon) / 1_000_000 - unsigned_offset).to_f unless lat_or_lon.nil?
127
+ end
128
+ end
129
+
130
+ class Item::Validator < ValidatorBase
131
+ include MessageValidations
132
+
133
+ validates :receiver_addr, equals_attribute: { attribute: :sender_addr },
134
+ if: "self.sender_addr && self.create_txid"
135
+
136
+ validates :latitude, numeric: true, unless: 'create_txid'
137
+ validates_if_present :latitude, greater_than_or_equal_to: -90, if: 'create_txid.nil?'
138
+ validates_if_present :latitude, less_than_or_equal_to: 90, if: 'create_txid.nil?'
139
+
140
+ validates :longitude, numeric: true, unless: 'create_txid'
141
+ validates_if_present :longitude, greater_than_or_equal_to: -180, if: 'create_txid.nil?'
142
+ validates_if_present :longitude, less_than_or_equal_to: 180, if: 'create_txid.nil?'
143
+
144
+ validates :radius, integer: true, unless: 'create_txid'
145
+ validates_if_present :radius, greater_than_or_equal_to: 0, if: 'create_txid.nil?'
146
+ validates_if_present :radius, less_than: 1000000, if: 'create_txid.nil?'
147
+
148
+ validates :message_type, format: /\AIT(?:CRTE|UPDT)\Z/
149
+
150
+ validates :price_currency, is_string: {
151
+ message: 'is required if price is specified' },
152
+ unless: "price_in_units.nil? || create_txid"
153
+
154
+ validates_if_present :description, is_string: true
155
+ validates_if_present :price_in_units, integer: true,
156
+ greater_than_or_equal_to: 0
157
+ validates_if_present :expiration_in, integer: true,
158
+ greater_than_or_equal_to: 0
159
+ end
160
+ end