cryptoconditions_ruby 0.5.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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.gitmodules +3 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +26 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +201 -0
  10. data/README.md +22 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/cryptoconditions_ruby.gemspec +34 -0
  15. data/lib/cryptoconditions_ruby/condition.rb +129 -0
  16. data/lib/cryptoconditions_ruby/crypto.rb +168 -0
  17. data/lib/cryptoconditions_ruby/exceptions.rb +8 -0
  18. data/lib/cryptoconditions_ruby/fulfillment.rb +138 -0
  19. data/lib/cryptoconditions_ruby/type_registry.rb +27 -0
  20. data/lib/cryptoconditions_ruby/types/base_sha_256_fulfillment.rb +15 -0
  21. data/lib/cryptoconditions_ruby/types/ed25519_fulfillment.rb +79 -0
  22. data/lib/cryptoconditions_ruby/types/inverted_threshold_sha_256_fulfillment.rb +15 -0
  23. data/lib/cryptoconditions_ruby/types/preimage_sha_256_fulfillment.rb +64 -0
  24. data/lib/cryptoconditions_ruby/types/threshold_sha_256_fulfillment.rb +343 -0
  25. data/lib/cryptoconditions_ruby/types/timeout_fulfillment.rb +46 -0
  26. data/lib/cryptoconditions_ruby/utils/base16.rb +28 -0
  27. data/lib/cryptoconditions_ruby/utils/base58.rb +53 -0
  28. data/lib/cryptoconditions_ruby/utils/byte_array.rb +16 -0
  29. data/lib/cryptoconditions_ruby/utils/bytes.rb +13 -0
  30. data/lib/cryptoconditions_ruby/utils/hasher.rb +27 -0
  31. data/lib/cryptoconditions_ruby/utils/hexlify.rb +13 -0
  32. data/lib/cryptoconditions_ruby/utils/predictor.rb +58 -0
  33. data/lib/cryptoconditions_ruby/utils/reader.rb +167 -0
  34. data/lib/cryptoconditions_ruby/utils/writer.rb +81 -0
  35. data/lib/cryptoconditions_ruby/version.rb +3 -0
  36. data/lib/cryptoconditions_ruby.rb +28 -0
  37. metadata +191 -0
@@ -0,0 +1,343 @@
1
+ require 'duplicate'
2
+ module CryptoconditionsRuby
3
+ module Types
4
+ CONDITION = 'condition'
5
+ FULFILLMENT = 'fulfillment'
6
+
7
+ class ThresholdSha256Fulfillment < BaseSha256Fulfillment
8
+ TYPE_ID = 2
9
+ FEATURE_BITMASK = 0x09
10
+
11
+ attr_accessor :bitmask, :threshold, :subconditions
12
+ private :bitmask
13
+ def initialize(threshold = nil)
14
+ if threshold && (!threshold.is_a?(Integer) || threshold < 1)
15
+ raise StandardError, "Threshold must be a integer greater than zero, was: #{threshold}"
16
+ end
17
+ self.threshold = threshold
18
+ self.subconditions = []
19
+ end
20
+
21
+ def add_subcondition(subcondition, weight = 1)
22
+ if subcondition.is_a?(String)
23
+ subcondition = Condition.from_uri(subcondition)
24
+ end
25
+ unless subcondition.is_a?(Condition)
26
+ raise TypeError, 'Subconditions must be URIs or objects of type Condition'
27
+ end
28
+ unless weight.is_a?(Integer) || weight < 1
29
+ raise StandardError, "Invalid weight: #{weight}"
30
+ end
31
+
32
+ subconditions.push(
33
+ 'type' => CONDITION,
34
+ 'body' => subcondition,
35
+ 'weight' => weight
36
+ )
37
+ end
38
+
39
+ def add_subcondition_uri(subcondition_uri)
40
+ unless subcondition_uri.is_a?(String)
41
+ raise TypeError, "Subcondition must be provided as a URI string, was #{subcondition_uri}"
42
+ end
43
+
44
+ add_subcondition(Condition.from_uri(subcondition_uri))
45
+ end
46
+
47
+ def add_subfulfillment(subfulfillment, weight = 1)
48
+ if subfulfillment.is_a?(String)
49
+ subfulfillment = Fulfillment.from_uri(subfulfillment)
50
+ end
51
+ unless subfulfillment.is_a?(Fulfillment)
52
+ raise TypeError, 'Subfulfillments must be URIs or objects of type Fulfillment'
53
+ end
54
+ if !weight.is_a?(Integer) || weight < 0
55
+ raise StandardError, "Invalid weight: #{weight}"
56
+ end
57
+ subconditions.push(
58
+ 'type' => FULFILLMENT,
59
+ 'body' => subfulfillment,
60
+ 'weight' => weight
61
+ )
62
+ end
63
+
64
+ def add_subfulfillment_uri(subfulfillment_uri)
65
+ unless subfulfillment_uri.is_a?(String)
66
+ raise TypeError, "Subfulfillment must be provided as a URI string, was: #{subfulfillment_uri}"
67
+ end
68
+ add_subfulfillment(Fulfillment.from_uri(subfulfillment_uri))
69
+ end
70
+
71
+ def bitmask
72
+ bitmask = super
73
+ subconditions.each do |cond|
74
+ bitmask |= cond['body'].bitmask
75
+ end
76
+ bitmask
77
+ end
78
+
79
+ def get_subcondition_from_vk(vk)
80
+ subconditions.inject([]) do |store, c|
81
+ if c['body'].is_a?(Ed25519Fulfillment) && Utils::Base58.encode(c['body'].public_key.to_s) == vk
82
+ store.push(c)
83
+ elsif c['body'].is_a?(ThresholdSha256Fulfillment)
84
+ result = c['body'].get_subcondition_from_vk(vk)
85
+ store += result if result
86
+ end
87
+ store
88
+ end
89
+ end
90
+
91
+ def write_hash_payload(hasher)
92
+ raise StandardError, 'Requires subconditions' if subconditions.empty?
93
+
94
+ _subconditions = subconditions.inject([]) do |store, c|
95
+ writer = Utils::Writer.new
96
+ writer.write_var_uint(c['weight'])
97
+ writer.write(
98
+ c['type'] == FULFILLMENT ? c['body'].condition_binary : c['body'].serialize_binary
99
+ )
100
+ store.push(writer.buffer)
101
+ end
102
+ sorted_subconditions = ThresholdSha256Fulfillment.sort_buffers(_subconditions)
103
+
104
+ hasher.write_uint32(threshold)
105
+ hasher.write_var_uint(sorted_subconditions.length)
106
+ sorted_subconditions.each do |cond|
107
+ hasher.write(cond)
108
+ end
109
+ hasher
110
+ end
111
+
112
+ def calculate_max_fulfillment_length
113
+ total_condition_len = 0
114
+
115
+ _subconditions = subconditions.map do |c|
116
+ condition_len = ThresholdSha256Fulfillment.predict_subcondition_length(c)
117
+ fulfillment_len = ThresholdSha256Fulfillment.predict_subfulfillment_length(c)
118
+ total_condition_len += condition_len
119
+ {
120
+ 'weight' => c['weight'],
121
+ 'size' => fulfillment_len - condition_len
122
+ }
123
+ end
124
+
125
+ _subconditions.sort_by! { |x| x['weight'].abs }
126
+
127
+ worst_case_fulfillments_length = total_condition_len + ThresholdSha256Fulfillment.calculate_worst_case_length(threshold, _subconditions)
128
+
129
+ if worst_case_fulfillments_length == -Float::INFINITY
130
+ raise StandardError, 'Insufficient subconditions/weights to meet the threshold'
131
+ end
132
+
133
+ # Calculate resulting total maximum fulfillment size
134
+ predictor = Utils::Predictor.new
135
+ predictor.write_uint32(threshold)
136
+ predictor.write_var_uint(subconditions.length)
137
+ subconditions.each do |c|
138
+ predictor.write_uint8(nil)
139
+ predictor.write_var_uint(c['weight']) unless c['weight'] == 1
140
+ end
141
+
142
+ predictor.skip(worst_case_fulfillments_length)
143
+
144
+ predictor.size
145
+ end
146
+
147
+ def self.predict_subcondition_length(cond)
148
+ return cond['body'].condition_binary.length if cond['type'] == FULFILLMENT
149
+
150
+ cond['body'].serialize_binary.length
151
+ end
152
+
153
+ def self.predict_subfulfillment_length(cond)
154
+ fulfillment_len = if cond['type'] == FULFILLMENT
155
+ cond['body'].condition.max_fulfillment_length
156
+ else
157
+ cond['body'].max_fulfillment_length
158
+ end
159
+
160
+ predictor = Utils::Predictor.new
161
+ predictor.write_uint16(nil)
162
+ predictor.write_var_octet_string('0' * fulfillment_len)
163
+ predictor.size
164
+ end
165
+
166
+ def self.calculate_worst_case_length(threshold, subconditions, index = 0)
167
+ return 0 if threshold <= 0
168
+ if index < subconditions.length
169
+ next_condition = subconditions[index]
170
+
171
+ [
172
+ next_condition['size'] + ThresholdSha256Fulfillment.calculate_worst_case_length(
173
+ threshold - next_condition['weight'].abs,
174
+ subconditions,
175
+ index + 1
176
+ ),
177
+ ThresholdSha256Fulfillment.calculate_worst_case_length(
178
+ threshold,
179
+ subconditions,
180
+ index + 1
181
+ )
182
+ ].max
183
+ else
184
+ -Float::INFINITY
185
+ end
186
+ end
187
+
188
+ def parse_payload(reader, *args)
189
+ raise TypeError, 'reader must be a Reader instance' unless reader.is_a?(Utils::Reader)
190
+
191
+ self.threshold = reader.read_var_uint
192
+ condition_count = reader.read_var_uint
193
+
194
+ condition_count.times do
195
+ weight = reader.read_var_uint
196
+ fulfillment = reader.read_var_octet_string
197
+ condition = reader.read_var_octet_string
198
+ if !fulfillment.empty? && !condition.empty?
199
+ raise TypeError, 'Subconditions may not provide both subcondition and fulfillment.'
200
+ elsif
201
+ if !fulfillment.empty?
202
+ add_subfulfillment(Fulfillment.from_binary(fulfillment), weight)
203
+ elsif !condition.empty?
204
+ add_subcondition(Condition.from_binary(condition), weight)
205
+ else
206
+ raise TypeError, 'Subconditions must provide either subcondition or fulfillment.'
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ def write_payload(writer)
213
+ raise TypeError, 'writer must be a Writer instance' unless writer.is_a?(Utils::Writer)
214
+
215
+ subfulfillments = subconditions.each_with_index.map do |c, i|
216
+ next unless c['type'] == FULFILLMENT
217
+
218
+ subfulfillment = c.dup
219
+ subfulfillment.merge!(
220
+ 'index' => i,
221
+ 'size' => c['body'].serialize_binary.length,
222
+ 'omit_size' => c['body'].condition_binary.length
223
+ )
224
+ end.compact
225
+
226
+ smallest_set = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
227
+ threshold, subfulfillments
228
+ )['set']
229
+
230
+ optimized_subfulfillments = subconditions.each_with_index.map do |c, i|
231
+ if c['type'] == FULFILLMENT && !smallest_set.include?(i)
232
+ subfulfillment = c.dup
233
+ subfulfillment.update(
234
+ 'type' => CONDITION,
235
+ 'body' => c['body'].condition
236
+ )
237
+ else
238
+ c
239
+ end
240
+ end
241
+
242
+ serialized_subconditions = optimized_subfulfillments.map do |c|
243
+ writer_ = Utils::Writer.new
244
+ writer_.write_var_uint(c['weight'])
245
+ writer_.write_var_octet_string(c['type'] == FULFILLMENT ? c['body'].serialize_binary : '')
246
+ writer_.write_var_octet_string(c['type'] == CONDITION ? c['body'].serialize_binary : '')
247
+ writer_.buffer
248
+ end
249
+
250
+ sorted_subconditions = ThresholdSha256Fulfillment.sort_buffers(serialized_subconditions)
251
+
252
+ writer.write_var_uint(threshold)
253
+ writer.write_var_uint(sorted_subconditions.length)
254
+ sorted_subconditions.each { |c| writer.write(c) }
255
+ writer
256
+ end
257
+
258
+ def self.calculate_smallest_valid_fulfillment_set(threshold, fulfillments, state = nil)
259
+ state ||= { 'index' => 0, 'size' => 0, 'set' => [] }
260
+
261
+ if threshold <= 0
262
+ { 'size' => state['size'], 'set' => state['set'] }
263
+ elsif state['index'] < fulfillments.length
264
+ next_fulfillment = fulfillments[state['index']]
265
+ with_next = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
266
+ threshold - next_fulfillment['weight'].abs,
267
+ fulfillments,
268
+ 'size' => state['size'] + next_fulfillment['size'],
269
+ 'index' => state['index'] + 1,
270
+ 'set' => state['set'] + [next_fulfillment['index']]
271
+ )
272
+
273
+ without_next = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
274
+ threshold,
275
+ fulfillments,
276
+ 'size' => state['size'] + next_fulfillment['omit_size'],
277
+ 'index' => state['index'] + 1,
278
+ 'set' => state['set']
279
+ )
280
+ with_next['size'] < without_next['size'] ? with_next : without_next
281
+ else
282
+ { 'size' => Float::INFINITY }
283
+ end
284
+ end
285
+
286
+ def self.sort_buffers(buffers)
287
+ Duplicate.duplicate(buffers).sort_by { |item| [item.length, item] }
288
+ end
289
+
290
+ def to_dict
291
+ subfulfillments = subconditions.map do |c|
292
+ subcondition = c['body'].to_dict
293
+ subcondition.merge!('weight' => c['weight'])
294
+ end
295
+
296
+ {
297
+ 'type' => 'fulfillment',
298
+ 'type_id' => type_id,
299
+ 'bitmask' => bitmask,
300
+ 'threshold' => threshold,
301
+ 'subfulfillments' => subfulfillments
302
+ }
303
+ end
304
+
305
+ def parse_dict(data)
306
+ raise TypeError, 'reader must be a dict instance' unless data.is_a?(Hash)
307
+ self.threshold = data['threshold']
308
+
309
+ data['subfulfillments'].each do |subfulfillments|
310
+ weight = subfulfillments['weight']
311
+ if subfulfillments['type'] == FULFILLMENT
312
+ add_subfulfillment(Fulfillment.from_dict(subfulfillments), weight)
313
+ elsif subfulfillments['type'] == CONDITION
314
+ add_subcondition(Condition.from_dict(subfulfillments), weight)
315
+ else
316
+ raise TypeError, 'Subconditions must provide either subcondition or fulfillment.'
317
+ end
318
+ end
319
+ end
320
+
321
+ def validate(message: nil, **_kwargs)
322
+ fulfillments = subconditions.select { |c| c['type'] == FULFILLMENT }
323
+
324
+ min_weight = Float::INFINITY
325
+ total_weight = 0
326
+ fulfillments.each do |fulfillment|
327
+ min_weight = [min_weight, fulfillment['weight'].abs].max
328
+ total_weight += min_weight
329
+ end
330
+
331
+ # Total weight must meet the threshold
332
+ return if total_weight < threshold
333
+
334
+ valid_decisions = fulfillments.map do |fulfillment|
335
+ if fulfillment['body'].validate(message: message, **_kwargs)
336
+ [true] * fulfillment['weight']
337
+ end
338
+ end.compact.flatten
339
+ valid_decisions.count >= threshold
340
+ end
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,46 @@
1
+ module CryptoconditionsRuby
2
+ TIMESTAMP_REGEX = /^\d{10}(\.\d+)?$/
3
+
4
+ module Types
5
+ class TimeoutFulfillment < PreimageSha256Fulfillment
6
+ TYPE_ID = 99
7
+ FEATURE_BITMASK = 0x09
8
+ REGEX = TIMESTAMP_REGEX
9
+
10
+ def self.timestamp(time)
11
+ format('%6f', time.to_f)
12
+ end
13
+
14
+ def initialize(expire_time = nil)
15
+ if expire_time.is_a?(String) && !expire_time.match(REGEX)
16
+ raise TypeError, "Expire time must be conform UTC unix time, was: #{expire_time}"
17
+ end
18
+ super if expire_time
19
+ end
20
+
21
+ def expire_time
22
+ preimage
23
+ end
24
+
25
+ def to_dict
26
+ {
27
+ 'type' => 'fulfillment',
28
+ 'type_id' => TYPE_ID,
29
+ 'bitmask' => bitmask,
30
+ 'expire_time' => expire_time
31
+ }
32
+ end
33
+
34
+ def parse_dict(data)
35
+ self.preimage = data['expire_time']
36
+ end
37
+
38
+ def validate(message: nil, now: nil, **_kwargs)
39
+ unless now || now.match(REGEX)
40
+ raise TypeError, "message must be of unix time format, was: #{message}"
41
+ end
42
+ now.to_f <= expire_time.to_f
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ module CryptoconditionsRuby
2
+ module Utils
3
+ class Base16
4
+ def self.encode(data)
5
+ ret = ''
6
+ data.each_char do |c|
7
+ ch = c.ord.to_s(16)
8
+ ch = '0' + ch if ch.size == 1
9
+ ret += ch
10
+ end
11
+ ret.upcase
12
+ end
13
+
14
+ def self.decode(data)
15
+ chars = ''
16
+ ret = ''
17
+ data.each_char do |c|
18
+ chars += c
19
+ if chars.size == 2
20
+ ret += chars.to_i(16).chr
21
+ chars = ''
22
+ end
23
+ end
24
+ ret
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ module CryptoconditionsRuby
2
+ module Utils
3
+ class Base58
4
+ CHARS = %w(123456789 ABCDEFGHJKLMNPQRSTUVWXYZ abcdefghijkmnopqrstuvwxyz).freeze
5
+
6
+ def self.decoding_table
7
+ @decoding_table ||= generate_decoding_table
8
+ end
9
+
10
+ def self.generate_decoding_table
11
+ CHARS.join.split('').each_with_index.each_with_object({}) do |(c, i), store|
12
+ store[c] = i
13
+ end
14
+ end
15
+
16
+ def self.encoding_table
17
+ @encoding_table ||= decoding_table.invert
18
+ end
19
+
20
+ def self.encode(data)
21
+ cdata = data.unpack('C*')
22
+ vlong = cdata.inject(0) { |store, v| v + store * 256 }
23
+ result = ''
24
+ while vlong > 0
25
+ result += encoding_table[vlong % 58]
26
+ vlong /= 58
27
+ end
28
+ while cdata.first.zero?
29
+ result += '1'
30
+ cdata = cdata[1..-1]
31
+ end
32
+ result.reverse
33
+ end
34
+
35
+ def self.decode(data_string)
36
+ vlong = data_string.each_byte.inject(0) do |store, b|
37
+ store = decoding_table[b.chr] + (58 * store)
38
+ store
39
+ end
40
+ result = ''
41
+ while vlong > 0
42
+ result += (vlong % 256).chr
43
+ vlong /= 256
44
+ end
45
+ while data_string[0] == '1'
46
+ result += "\x00"
47
+ data_string = data_string[1..-1]
48
+ end
49
+ result.reverse
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ module CryptoconditionsRuby
2
+ module Utils
3
+ class ByteArray
4
+ include Enumerable
5
+
6
+ def initialize(string_or_array)
7
+ @original = string_or_array
8
+ @collection = string_or_array.is_a?(Array) ? string_or_array : string_or_array.bytes
9
+ end
10
+
11
+ def each
12
+ @collection.each
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ class CryptoconditionsRuby::Utils::Bytes
2
+ attr_reader :bytes
3
+ private :bytes
4
+ def initialize(input)
5
+ @bytes = input.is_a?(Array) ? input : input.bytes
6
+ end
7
+
8
+ def to_i(base)
9
+ bytes.reverse.each_with_index.inject(0) do |store, (byte, index)|
10
+ store += byte * base**(index * 2)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require 'digest'
2
+
3
+ class CryptoconditionsRuby::Utils::Hasher < CryptoconditionsRuby::Utils::Writer
4
+ attr_reader :digest_instance
5
+ private :digest_instance
6
+
7
+ def initialize(algorithm)
8
+ if algorithm == 'sha256'
9
+ @digest_instance = Digest::SHA256.new
10
+ else
11
+ raise NotImplementedError
12
+ end
13
+ super()
14
+ end
15
+
16
+ def write(in_bytes)
17
+ digest_instance.update(in_bytes)
18
+ end
19
+
20
+ def digest
21
+ digest_instance.digest
22
+ end
23
+
24
+ def self.length(algorithm)
25
+ new(algorithm).digest.length
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module CryptoconditionsRuby
2
+ module Utils
3
+ module Hexlify
4
+ def hexlify(msg)
5
+ msg.unpack('H*')[0]
6
+ end
7
+
8
+ def unhexlify(msg)
9
+ [msg].pack('H*')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ class CryptoconditionsRuby::Utils::Predictor
2
+ attr_accessor :size
3
+
4
+ def initialize
5
+ @size = 0
6
+ end
7
+
8
+ def write_uint(_value, length)
9
+ skip(length)
10
+ end
11
+
12
+ def write_var_uint(value)
13
+ return write_var_octet_string(value) if value.is_a?(String)
14
+ raise TypeError.new('UInt must be an integer') unless value.is_a?(Integer)
15
+ raise TypeError.new('UInt must be positive') unless value > 0
16
+
17
+ length_of_value = (sprintf('%02b', value).length / 8.0).ceil.to_i
18
+ buffer = (length_of_value - 1).times.map { 0 }.push(value).pack('C*')
19
+ write_var_octet_string(buffer)
20
+ end
21
+
22
+ def write_octet_string(_value, length)
23
+ skip(length)
24
+ end
25
+
26
+ def write_var_octet_string(value)
27
+ skip(1)
28
+ if value.length > 127
29
+ length_of_length = (sprintf('%02b', value.length).length / 8.0).ceil.to_i
30
+ skip(length_of_length)
31
+ end
32
+ skip(value.length)
33
+ end
34
+
35
+ def write(in_bytes)
36
+ self.size += in_bytes.length
37
+ end
38
+
39
+ def skip(byte_count)
40
+ self.size += byte_count
41
+ end
42
+
43
+ def write_uint8(value)
44
+ self.write_uint(value, 1)
45
+ end
46
+
47
+ def write_uint16(value)
48
+ self.write_uint(value, 2)
49
+ end
50
+
51
+ def write_uint32(value)
52
+ self.write_uint(value, 4)
53
+ end
54
+
55
+ def write_uint64(value)
56
+ self.write_uint(value, 8)
57
+ end
58
+ end