btcruby 1.3 → 1.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ade4ab5ec4e048a83afbdf1d80da87d6cb78b65
4
- data.tar.gz: df6796d9c48c3b71aad7916fec52a82bff17e09b
3
+ metadata.gz: 162d9f3dbb8974140831c1e831c72ffaa4ec0c01
4
+ data.tar.gz: 6c429f7f67eb3fc066fb83e3501f9a26ea6d55a0
5
5
  SHA512:
6
- metadata.gz: 115dcbaf2fdc9a208b317502767ece0935d5d186dca3ce8e59f666236b499f374044c307b800091f7eaf6e9759dd41daa68db7f06293b7e65de5a635f3558044
7
- data.tar.gz: 795b12ce7b5d35aaa82b0c929facaf1532792f06cbe96727585649ec824009bea6f495a686f35f9626e1404d05b96026167bbe81ee6d3e0ae52bef9e677a0109
6
+ metadata.gz: ff5d4970c74486586c1a4d0ea95b7b92eb8b635324bfc1a8bfbc5ee571af44ce079d381704a6b10a82946e920a2d84d7b6c6f7cfd17db5e518d148f4f651f3e0
7
+ data.tar.gz: 3b7bd61ff37ba5d332b197b2503b5427be9ff3448298e7adcb79284373f9ba8a6d7dc6cf019f11d4d34ed164781907cba3e620eb0900d1ae2947a49706f448e0
@@ -2,6 +2,12 @@
2
2
  BTCRuby Release Notes
3
3
  =====================
4
4
 
5
+ 1.4 (November 26, 2015)
6
+ -----------------------
7
+
8
+ * `BTC::SecretSharing` implements Shamir's Secret Sharing Scheme (SSSS) for splitting 128-bit secrets up to 16 shares.
9
+
10
+
5
11
  1.3 (November 24, 2015)
6
12
  -----------------------
7
13
 
@@ -51,6 +51,7 @@ require_relative 'btcruby/block_header.rb'
51
51
  require_relative 'btcruby/block.rb'
52
52
  require_relative 'btcruby/merkle_tree.rb'
53
53
  require_relative 'btcruby/open_assets.rb'
54
+ require_relative 'btcruby/ssss.rb'
54
55
 
55
56
  # TODO:
56
57
  # require_relative 'btcruby/curve_point.rb'
@@ -0,0 +1,265 @@
1
+ # Shamir's Secret Sharing Scheme (SSSS) with m-of-n rule for 128-bit numbers.
2
+ # Author: Oleg Andreev <oleganza@gmail.com>
3
+ #
4
+ # * Deterministic, extensible algorithm: every combination of secret and threshold produces exactly the same shares on each run. More shares can be generated without invalidating the first ones.
5
+ # * This algorithm splits and restores 128-bit secrets with up to 16 shares and up to 16 shares threshold.
6
+ # * Secret is a binary 16-byte string below ffffffffffffffffffffffffffffff61.
7
+ # * Shares are 17-byte binary strings with first byte indicating threshold and share index (these are necessary for recovery).
8
+ #
9
+ # See also: https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing
10
+ require 'digest/sha2'
11
+ require 'securerandom'
12
+ module BTC
13
+ module SecretSharing
14
+ extend self
15
+ Order = 0xffffffffffffffffffffffffffffff61 # Largest prime below 2**128: (2**128 - 159)
16
+
17
+ def random
18
+ be_from_int(SecureRandom.random_number(Order))
19
+ end
20
+
21
+ def prng(seed)
22
+ x = Order
23
+ s = nil
24
+ pad = "".b
25
+ while x >= Order
26
+ s = Digest::SHA2.digest(Digest::SHA2.digest(seed + pad))[0,16]
27
+ x = int_from_be(s)
28
+ pad = pad + "\x00".b
29
+ end
30
+ s
31
+ end
32
+
33
+ # Returns N strings, any M of them are enough to retrieve a secret.
34
+ # Each string encodes X and Y coordinates and also M. X & M takes one byte, Y takes 16 bytes:
35
+ # MMMMXXXX YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY YYYYYYYY
36
+ def split(secret, m, n)
37
+ prime = Order
38
+ secret_num = int_from_be(secret)
39
+ if secret_num >= Order
40
+ raise "Secret cannot be encoded with 128-bit SSSS"
41
+ end
42
+ if !(n >= 1 && n <= 16)
43
+ raise "N must be between 1 and 16"
44
+ end
45
+ if !(m >= 1 && m <= n)
46
+ raise "M must be between 1 and N"
47
+ end
48
+ shares = []
49
+ coef = [secret_num] # polynomial coefficients
50
+ (m - 1).times do |i|
51
+ # Generate unpredictable yet deterministic coefficients for each secret and M.
52
+ coef << int_from_be(prng(secret + m.chr + i.chr))
53
+ end
54
+ n.times do |i|
55
+ x = i + 1
56
+ exp = 1
57
+ y = coef[0]
58
+ while exp < m
59
+ y = (y + (coef[exp] * ((x**exp) % prime) % prime)) % prime
60
+ exp += 1
61
+ end
62
+ # Encode share
63
+ shares << string_from_point(m, x, y)
64
+ end
65
+ shares
66
+ end
67
+
68
+ # Transforms M 17-byte binary strings into original secret 16-byte binary string.
69
+ # Each share string must be well-formed.
70
+ def restore(shares)
71
+ prime = Order
72
+ shares = shares.dup.uniq
73
+ raise "No shares provided" if shares.size == 0
74
+ points = shares.map{|s| point_from_string(s) } # [[m,x,y],...]
75
+ ms = points.map{|p| p[0]}.uniq
76
+ xs = points.map{|p| p[1]}.uniq
77
+ raise "Shares do not use the same M value" if ms.size > 1
78
+ m = ms.first
79
+ raise "All shares must have unique X values" if xs.size != points.size
80
+ raise "Number of shares should be M or more" if points.size < m
81
+ points = points[0, m] # make sure we have exactly M points
82
+ y = 0
83
+ points.size.times do |formula| # 0..(m-1)
84
+ # Multiply the numerator across the top and denominators across the bottom to do Lagrange's interpolation
85
+ numerator = 1
86
+ denominator = 1
87
+ points.size.times do |count| # 0..(m-1)
88
+ if formula != count # skip element with i == j
89
+ startposition = points[formula][1]
90
+ nextposition = points[count][1]
91
+ numerator = (numerator * -nextposition) % prime
92
+ denominator = (denominator * (startposition - nextposition)) % prime
93
+ end
94
+ end
95
+ value = points[formula][2]
96
+ y = (prime + y + (value * numerator * modinv(denominator, prime))) % prime
97
+ end
98
+ return be_from_int(y)
99
+ end
100
+
101
+ # Returns mmmmxxxx yyyyyyyy yyyyyyyy ... (16 bytes of y)
102
+ def string_from_point(m, x, y)
103
+ m = to_nibble(m)
104
+ x = to_nibble(x)
105
+ byte = [(m << 4) + x].pack("C")
106
+ byte + be_from_int(y)
107
+ end
108
+
109
+ # returns [m, x, y]
110
+ def point_from_string(s)
111
+ byte = s.bytes.first
112
+ m = from_nibble(byte >> 4)
113
+ x = from_nibble(byte & 0x0f)
114
+ y = int_from_be(s[1..-1])
115
+ [m, x, y]
116
+ end
117
+
118
+ # Encodes values in range 1..16 to one nibble where all values are encoded as-is,
119
+ # except for 16 which becomes 0. This is to make strings look friendly for common cases when M,N < 16
120
+ def to_nibble(x)
121
+ x == 16 ? 0 : x
122
+ end
123
+
124
+ def from_nibble(x)
125
+ x == 0 ? 16 : x
126
+ end
127
+
128
+ # Gives the decomposition of the gcd of a and b. Returns [x,y,z] such that x = gcd(a,b) and y*a + z*b = x
129
+ def gcd_decomposition(a,b)
130
+ if b == 0
131
+ [a, 1, 0]
132
+ else
133
+ n = a/b
134
+ c = a % b
135
+ r = gcd_decomposition(b,c)
136
+ [r[0], r[2], r[1]-r[2]*n]
137
+ end
138
+ end
139
+
140
+ # Gives the multiplicative inverse of k mod prime. In other words (k * modInverse(k)) % prime = 1 for all prime > k >= 1
141
+ def modinv(k, prime)
142
+ k = k % prime
143
+ r = (k < 0) ? -gcd_decomposition(prime,-k)[2] : gcd_decomposition(prime,k)[2]
144
+ return (prime + r) % prime
145
+ end
146
+
147
+ def int_from_be(data)
148
+ r = data.unpack("C*").reverse.inject({pos:0, total:0}) do |ctx, c|
149
+ c = c << ctx[:pos]
150
+ ctx[:pos] += 8
151
+ ctx[:total] += c
152
+ ctx
153
+ end
154
+ r[:total]
155
+ end
156
+
157
+ def be_from_int(i, pad = 128)
158
+ a = []
159
+ while i > 0
160
+ a.unshift(i % 256)
161
+ i /= 256
162
+ end
163
+ a.pack("C*").rjust(pad / 8, "\x00")
164
+ end
165
+
166
+ end
167
+ end
168
+
169
+ if $0 == __FILE__
170
+
171
+ SSSS = BTC::SecretSharing
172
+ require_relative 'data.rb'
173
+
174
+ # Usage
175
+ secret = SSSS.random
176
+ puts "Secret: #{BTC.to_hex(secret)}"
177
+ shares = SSSS.split(secret, 2, 3)
178
+ shares.each do |share|
179
+ puts "Share: #{BTC.to_hex(share)}"
180
+ end
181
+ restored_secret = SSSS.restore([shares[1], shares[0]])
182
+ puts "Recovered secret with shares 2 and 1: #{BTC.to_hex(restored_secret)}"
183
+ restored_secret = SSSS.restore([shares[0], shares[2]])
184
+ puts "Recovered secret with shares 1 and 3: #{BTC.to_hex(restored_secret)}"
185
+ restored_secret = SSSS.restore([shares[1], shares[2]])
186
+ puts "Recovered secret with shares 2 and 3: #{BTC.to_hex(restored_secret)}"
187
+
188
+ # Output:
189
+ # Secret: d881c6f74ccac24997bb27040640a8eb
190
+ # Share: 2147018841997da8d92211ad7590f85754
191
+ # Share: 22b581498be6308f68ac6833e71bb0051e
192
+ # Share: 2324010ad632e375f836beba58a667b387
193
+ # Recovered secret with shares 2 and 1: d881c6f74ccac24997bb27040640a8eb
194
+ # Recovered secret with shares 1 and 3: d881c6f74ccac24997bb27040640a8eb
195
+ # Recovered secret with shares 2 and 3: d881c6f74ccac24997bb27040640a8eb
196
+
197
+ # Test Vectors
198
+
199
+ test_vectors = [
200
+ {
201
+ "secret" => "31415926535897932384626433832795",
202
+ "1-of-1" => ["1131415926535897932384626433832795"],
203
+ "1-of-2" => ["1131415926535897932384626433832795", "1231415926535897932384626433832795"],
204
+ "2-of-2" => ["215af384f05d9b45f0e4e348f95b371acd", "2284a5b0ba67ddf44ea6422f8e82eb0e05"],
205
+ "1-of-3" => ["1131415926535897932384626433832795", "1231415926535897932384626433832795", "1331415926535897932384626433832795"],
206
+ "2-of-3" => ["215af384f05d9b45f0e4e348f95b371acd", "2284a5b0ba67ddf44ea6422f8e82eb0e05", "23ae57dc847220a2ac67a11623aa9f013d"],
207
+ "3-of-3" => ["316cb005ab037e85ed9c8befbe72fef75c", "321387c8a1b34863197fae486ca60c1b97", "3325c8a20a62b62f16cceb6c6eccaa93a7"],
208
+ "4-of-6" => ["416c4b3a8dc218696f8b1aed23385496eb", "429b14a744ce462bdc71b910b5cf0890ba", "4384d4d7881b01db3881cd0f17457112c8",
209
+ "44f0c303944b6b73e265c52a42e9601a3c", "45a61663a602a2f238c80fa43408a7a57b", "466c062ff9e3c8529a531abee5f119b1ac"],
210
+ "10-of-16"=>["a1a8b4077b75b0b18aefa63399d0b8d749", "a2e015e817190296d9ebe29f1c8cdc21c7", "a3c65760010c358c9760cece5da815edb4", "a4129891c5efd375a8367c854ab08010d6",
211
+ "a53c138386a55b0b35447ca03e44ab4eeb", "a6182993f21038c5d3bf548dac9dee7e20", "a769f010c04a4996b471a82addd4ea05d4", "a88e27a316dda9822f81616b2d48cb5e23",
212
+ "a9b0298820dc8c26989b6f8a2e8b00c3c4", "aa98042e1bcdf63b7283503ac4ad364380", "ab27bed0235b651dd92e764fa8cea25ba8", "ac05890d2177c48f4ec6cabd1047d9dbdc",
213
+ "adba7838775b82e4022af68f19d9985368", "aeb96045352c20fd24c6de8563cb2446f2", "af4f51af0a774592f9eabb71aaf0348def", "a06f50a680d22280f31b853d941c7eb158"],
214
+ },
215
+ {
216
+ "secret" => "deadbeefcafebabedeadbeefcafebabe",
217
+ "1-of-1" => ["11deadbeefcafebabedeadbeefcafebabe"],
218
+ "2-of-2" => ["217f21b8a8329e69ea75a518485c8da19d", "221f95b2609a3e19160c9c71a0ee1c887c"],
219
+ "2-of-3" => ["217f21b8a8329e69ea75a518485c8da19d", "221f95b2609a3e19160c9c71a0ee1c887c", "23c009ac1901ddc841a393caf97fab6ebc"],
220
+ "3-of-3" => ["31d6b7c83a2587dd06be735c2ba5c719c0", "32762d76edcca00dd227bccb825a8daa75", "33bd0ecb0ac0474d211a8a0cf3e9526c3e"],
221
+ },
222
+ {
223
+ "secret" => "ffffffffffffffffffffffffffffff60",
224
+ "1-of-1" => ["11ffffffffffffffffffffffffffffff60"],
225
+ "2-of-2" => ["21375c71bcaf077f5946f9e901efb9cf70", "226eb8e3795e0efeb28df3d203df739ee1"],
226
+ "2-of-3" => ["21375c71bcaf077f5946f9e901efb9cf70", "226eb8e3795e0efeb28df3d203df739ee1", "23a61555360d167e0bd4edbb05cf2d6e52"],
227
+ "3-of-3" => ["3112dac40bb910928263e5cf3971c39c8b", "32dec3f6359b1f7671aa60dd821c4969d3", "3363bb967da62cabcdd3712ad9ff916915"],
228
+ },
229
+ {
230
+ "secret" => "00000000000000000000000000000000",
231
+ "1-of-1" => ["1100000000000000000000000000000000"],
232
+ "2-of-2" => ["2125df3f1da76af07c37689382bc8201a6", "224bbe7e3b4ed5e0f86ed127057904034c"],
233
+ "2-of-3" => ["2125df3f1da76af07c37689382bc8201a6", "224bbe7e3b4ed5e0f86ed127057904034c", "23719dbd58f640d174a639ba88358604f2"],
234
+ "3-of-3" => ["31651161eeddabb39134be97908f0d7d9e", "32671d1a7e6d7ef24037990a5285a75164", "33062329aeaf79bc0d088f5845e3cd7b52"],
235
+ }
236
+ ]
237
+
238
+ test_vectors.each do |test|
239
+ hexsecret = test.delete("secret")
240
+ secret = BTC.from_hex(hexsecret)
241
+ test.each do |rule, defined_shares|
242
+ m, n = rule.split("-of-").map{|x|x.to_i}
243
+ puts "Testing #{hexsecret} #{rule}:"
244
+ shares = SSSS.split(secret, m, n)
245
+ hexshares = shares.map{|s| BTC.to_hex(s)}
246
+ failed = false
247
+ if hexshares != defined_shares
248
+ failed = true
249
+ puts "Failed test:"
250
+ puts " Expected: #{defined_shares.inspect}"
251
+ puts " Generated: #{hexshares.inspect}"
252
+ end
253
+ subshares = hexshares[0...m] # TODO: iterate over various combinations
254
+ restored_secret = SSSS.restore(subshares.map{|s| BTC.from_hex(s)})
255
+ if restored_secret != secret
256
+ failed = true
257
+ puts "Failed #{hexsecret} #{rule} test: failed to restore secret using #{subshares.inspect}"
258
+ end
259
+ if !failed
260
+ puts "Ok."
261
+ end
262
+ end
263
+ end
264
+
265
+ end
@@ -1,3 +1,3 @@
1
1
  module BTC
2
- VERSION = "1.3".freeze
2
+ VERSION = "1.4".freeze
3
3
  end
@@ -0,0 +1,62 @@
1
+ require_relative 'spec_helper'
2
+ describe BTC::SecretSharing do
3
+
4
+ SSSS = BTC::SecretSharing
5
+
6
+ test_vectors = [
7
+ {
8
+ "secret" => "31415926535897932384626433832795",
9
+ "1-of-1" => ["1131415926535897932384626433832795"],
10
+ "1-of-2" => ["1131415926535897932384626433832795", "1231415926535897932384626433832795"],
11
+ "2-of-2" => ["215af384f05d9b45f0e4e348f95b371acd", "2284a5b0ba67ddf44ea6422f8e82eb0e05"],
12
+ "1-of-3" => ["1131415926535897932384626433832795", "1231415926535897932384626433832795", "1331415926535897932384626433832795"],
13
+ "2-of-3" => ["215af384f05d9b45f0e4e348f95b371acd", "2284a5b0ba67ddf44ea6422f8e82eb0e05", "23ae57dc847220a2ac67a11623aa9f013d"],
14
+ "3-of-3" => ["316cb005ab037e85ed9c8befbe72fef75c", "321387c8a1b34863197fae486ca60c1b97", "3325c8a20a62b62f16cceb6c6eccaa93a7"],
15
+ "4-of-6" => ["416c4b3a8dc218696f8b1aed23385496eb", "429b14a744ce462bdc71b910b5cf0890ba", "4384d4d7881b01db3881cd0f17457112c8",
16
+ "44f0c303944b6b73e265c52a42e9601a3c", "45a61663a602a2f238c80fa43408a7a57b", "466c062ff9e3c8529a531abee5f119b1ac"],
17
+ "10-of-16"=>["a1a8b4077b75b0b18aefa63399d0b8d749", "a2e015e817190296d9ebe29f1c8cdc21c7", "a3c65760010c358c9760cece5da815edb4", "a4129891c5efd375a8367c854ab08010d6",
18
+ "a53c138386a55b0b35447ca03e44ab4eeb", "a6182993f21038c5d3bf548dac9dee7e20", "a769f010c04a4996b471a82addd4ea05d4", "a88e27a316dda9822f81616b2d48cb5e23",
19
+ "a9b0298820dc8c26989b6f8a2e8b00c3c4", "aa98042e1bcdf63b7283503ac4ad364380", "ab27bed0235b651dd92e764fa8cea25ba8", "ac05890d2177c48f4ec6cabd1047d9dbdc",
20
+ "adba7838775b82e4022af68f19d9985368", "aeb96045352c20fd24c6de8563cb2446f2", "af4f51af0a774592f9eabb71aaf0348def", "a06f50a680d22280f31b853d941c7eb158"],
21
+ },
22
+ {
23
+ "secret" => "deadbeefcafebabedeadbeefcafebabe",
24
+ "1-of-1" => ["11deadbeefcafebabedeadbeefcafebabe"],
25
+ "2-of-2" => ["217f21b8a8329e69ea75a518485c8da19d", "221f95b2609a3e19160c9c71a0ee1c887c"],
26
+ "2-of-3" => ["217f21b8a8329e69ea75a518485c8da19d", "221f95b2609a3e19160c9c71a0ee1c887c", "23c009ac1901ddc841a393caf97fab6ebc"],
27
+ "3-of-3" => ["31d6b7c83a2587dd06be735c2ba5c719c0", "32762d76edcca00dd227bccb825a8daa75", "33bd0ecb0ac0474d211a8a0cf3e9526c3e"],
28
+ },
29
+ {
30
+ "secret" => "ffffffffffffffffffffffffffffff60",
31
+ "1-of-1" => ["11ffffffffffffffffffffffffffffff60"],
32
+ "2-of-2" => ["21375c71bcaf077f5946f9e901efb9cf70", "226eb8e3795e0efeb28df3d203df739ee1"],
33
+ "2-of-3" => ["21375c71bcaf077f5946f9e901efb9cf70", "226eb8e3795e0efeb28df3d203df739ee1", "23a61555360d167e0bd4edbb05cf2d6e52"],
34
+ "3-of-3" => ["3112dac40bb910928263e5cf3971c39c8b", "32dec3f6359b1f7671aa60dd821c4969d3", "3363bb967da62cabcdd3712ad9ff916915"],
35
+ },
36
+ {
37
+ "secret" => "00000000000000000000000000000000",
38
+ "1-of-1" => ["1100000000000000000000000000000000"],
39
+ "2-of-2" => ["2125df3f1da76af07c37689382bc8201a6", "224bbe7e3b4ed5e0f86ed127057904034c"],
40
+ "2-of-3" => ["2125df3f1da76af07c37689382bc8201a6", "224bbe7e3b4ed5e0f86ed127057904034c", "23719dbd58f640d174a639ba88358604f2"],
41
+ "3-of-3" => ["31651161eeddabb39134be97908f0d7d9e", "32671d1a7e6d7ef24037990a5285a75164", "33062329aeaf79bc0d088f5845e3cd7b52"],
42
+ }
43
+ ]
44
+
45
+ test_vectors.each do |test|
46
+ hexsecret = test.delete("secret")
47
+ secret = BTC.from_hex(hexsecret)
48
+ test.each do |rule, defined_shares|
49
+ m, n = rule.split("-of-").map{|x|x.to_i}
50
+ it "Should split and restore #{rule} shares for #{hexsecret}" do
51
+ shares = SSSS.split(secret, m, n)
52
+ hexshares = shares.map{|s| BTC.to_hex(s)}
53
+ failed = false
54
+ hexshares.must_equal defined_shares
55
+ subshares = hexshares[0...m] # TODO: iterate over various combinations
56
+ restored_secret = SSSS.restore(subshares.map{|s| BTC.from_hex(s)})
57
+ BTC.to_hex(restored_secret).must_equal hexsecret
58
+ end
59
+ end
60
+ end
61
+
62
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: btcruby
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.3'
4
+ version: '1.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleg Andreev
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-11-24 00:00:00.000000000 Z
12
+ date: 2015-11-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ffi
@@ -119,6 +119,7 @@ files:
119
119
  - lib/btcruby/script/test_signature_checker.rb
120
120
  - lib/btcruby/script/transaction_signature_checker.rb
121
121
  - lib/btcruby/secp256k1.rb
122
+ - lib/btcruby/ssss.rb
122
123
  - lib/btcruby/transaction.rb
123
124
  - lib/btcruby/transaction_builder.rb
124
125
  - lib/btcruby/transaction_builder/errors.rb
@@ -160,6 +161,7 @@ files:
160
161
  - spec/script_spec.rb
161
162
  - spec/secp256k1_spec.rb
162
163
  - spec/spec_helper.rb
164
+ - spec/ssss_spec.rb
163
165
  - spec/transaction_builder_spec.rb
164
166
  - spec/transaction_spec.rb
165
167
  - spec/wire_format_spec.rb
@@ -211,6 +213,7 @@ test_files:
211
213
  - spec/script_number_spec.rb
212
214
  - spec/script_spec.rb
213
215
  - spec/secp256k1_spec.rb
216
+ - spec/ssss_spec.rb
214
217
  - spec/transaction_builder_spec.rb
215
218
  - spec/transaction_spec.rb
216
219
  - spec/wire_format_spec.rb