btcruby 1.3 → 1.4

Sign up to get free protection for your applications and to get access to all the features.
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