dropzone_ruby 0.1

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