tapyrus 0.3.8 → 0.4.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.ruby-version +1 -1
  4. data/README.md +43 -2
  5. data/lib/tapyrus/ext_key.rb +8 -4
  6. data/lib/tapyrus/key.rb +1 -1
  7. data/lib/tapyrus/key_path.rb +17 -13
  8. data/lib/tapyrus/message/fee_filter.rb +2 -2
  9. data/lib/tapyrus/message/header_and_short_ids.rb +4 -4
  10. data/lib/tapyrus/message/inventory.rb +1 -1
  11. data/lib/tapyrus/message/network_addr.rb +2 -2
  12. data/lib/tapyrus/message/ping.rb +2 -2
  13. data/lib/tapyrus/message/pong.rb +2 -2
  14. data/lib/tapyrus/message/send_cmpct.rb +2 -2
  15. data/lib/tapyrus/message/version.rb +3 -3
  16. data/lib/tapyrus/mnemonic.rb +9 -4
  17. data/lib/tapyrus/pstt/input.rb +422 -0
  18. data/lib/tapyrus/pstt/key_origin_info.rb +63 -0
  19. data/lib/tapyrus/pstt/output.rb +129 -0
  20. data/lib/tapyrus/pstt/proprietary.rb +54 -0
  21. data/lib/tapyrus/pstt/tx.rb +658 -0
  22. data/lib/tapyrus/pstt.rb +335 -0
  23. data/lib/tapyrus/rpc.rb +0 -2
  24. data/lib/tapyrus/script/script.rb +14 -4
  25. data/lib/tapyrus/secp256k1/rfc6979.rb +6 -3
  26. data/lib/tapyrus/tip0020.rb +341 -0
  27. data/lib/tapyrus/tx.rb +6 -2
  28. data/lib/tapyrus/tx_out.rb +2 -2
  29. data/lib/tapyrus/util.rb +3 -3
  30. data/lib/tapyrus/version.rb +1 -1
  31. data/lib/tapyrus/wallet/account.rb +2 -2
  32. data/lib/tapyrus/wallet/db.rb +3 -3
  33. data/lib/tapyrus/wallet/master_key.rb +2 -2
  34. data/lib/tapyrus.rb +3 -3
  35. data/tapyrusrb.gemspec +0 -4
  36. metadata +10 -81
  37. data/exe/tapyrusrb-cli +0 -5
  38. data/exe/tapyrusrbd +0 -42
  39. data/lib/tapyrus/network/connection.rb +0 -70
  40. data/lib/tapyrus/network/message_handler.rb +0 -244
  41. data/lib/tapyrus/network/peer.rb +0 -210
  42. data/lib/tapyrus/network/peer_discovery.rb +0 -43
  43. data/lib/tapyrus/network/pool.rb +0 -135
  44. data/lib/tapyrus/network.rb +0 -11
  45. data/lib/tapyrus/node/cli.rb +0 -114
  46. data/lib/tapyrus/node/configuration.rb +0 -36
  47. data/lib/tapyrus/node/spv.rb +0 -78
  48. data/lib/tapyrus/node.rb +0 -7
  49. data/lib/tapyrus/rpc/http_server.rb +0 -64
  50. data/lib/tapyrus/rpc/request_handler.rb +0 -146
@@ -0,0 +1,335 @@
1
+ module Tapyrus
2
+ # Partially Signed Tapyrus Transaction (PSTT) defined by TIP-0174.
3
+ #
4
+ # A PSTT holds a not-yet-signed Tapyrus transaction together with the metadata signers need.
5
+ # The data model follows BIP-370: the transaction is represented by per-input and per-output
6
+ # fields, so inputs and outputs can be added after creation.
7
+ module PSTT
8
+ autoload :Input, "tapyrus/pstt/input"
9
+ autoload :KeyOriginInfo, "tapyrus/pstt/key_origin_info"
10
+ autoload :Output, "tapyrus/pstt/output"
11
+ autoload :Proprietary, "tapyrus/pstt/proprietary"
12
+ autoload :Tx, "tapyrus/pstt/tx"
13
+
14
+ # The magic bytes which begin every PSTT. ASCII "pstt" followed by the separator 0xFF.
15
+ MAGIC = "70737474ff".htb
16
+
17
+ # The only version this TIP defines.
18
+ VERSION = 0
19
+
20
+ # The boundary between height-based and time-based locktime.
21
+ LOCKTIME_THRESHOLD = 500_000_000
22
+
23
+ # The sighash types TIP-0174 defines: SIGHASH_ALL, SIGHASH_NONE and SIGHASH_SINGLE, each
24
+ # optionally combined with SIGHASH_ANYONECANPAY.
25
+ SIGHASH_BASE_TYPES = [SIGHASH_TYPE[:all], SIGHASH_TYPE[:none], SIGHASH_TYPE[:single]].freeze
26
+ SIGHASH_TYPES = (SIGHASH_BASE_TYPES + SIGHASH_BASE_TYPES.map { |t| t | SIGHASH_TYPE[:anyonecanpay] }).freeze
27
+
28
+ # Global map key types.
29
+ module GlobalTypes
30
+ # Reserved. The global unsigned transaction of BIP-174 has no counterpart in this format.
31
+ UNSIGNED_TX = 0x00
32
+ XPUB = 0x01
33
+ TX_FEATURES = 0x02
34
+ FALLBACK_LOCKTIME = 0x03
35
+ INPUT_COUNT = 0x04
36
+ OUTPUT_COUNT = 0x05
37
+ TX_MODIFIABLE = 0x06
38
+ VERSION = 0xfb
39
+ PROPRIETARY = 0xfc
40
+ end
41
+
42
+ # Input map key types.
43
+ module InputTypes
44
+ UTXO = 0x00
45
+ PARTIAL_SIG = 0x02
46
+ SIGHASH_TYPE = 0x03
47
+ REDEEM_SCRIPT = 0x04
48
+ BIP32_DERIVATION = 0x06
49
+ FINAL_SCRIPTSIG = 0x07
50
+ RIPEMD160 = 0x0a
51
+ SHA256 = 0x0b
52
+ HASH160 = 0x0c
53
+ HASH256 = 0x0d
54
+ PREVIOUS_TXID = 0x0e
55
+ OUTPUT_INDEX = 0x0f
56
+ SEQUENCE = 0x10
57
+ REQUIRED_TIME_LOCKTIME = 0x11
58
+ REQUIRED_HEIGHT_LOCKTIME = 0x12
59
+ PROPRIETARY = 0xfc
60
+
61
+ # Segregated Witness(0x01, 0x05, 0x08, 0x09) and Taproot(0x13..0x18, defined by BIP-371)
62
+ # key types. They have no meaning in Tapyrus. Key types which BIPs later assigned to
63
+ # MuSig2 and other schemes are not listed here, so that they are carried through as
64
+ # unknown records rather than rejected.
65
+ RESERVED = [0x01, 0x05, 0x08, 0x09, *(0x13..0x18)].freeze
66
+ end
67
+
68
+ # Output map key types.
69
+ module OutputTypes
70
+ REDEEM_SCRIPT = 0x00
71
+ BIP32_DERIVATION = 0x02
72
+ AMOUNT = 0x03
73
+ SCRIPT = 0x04
74
+ PROPRIETARY = 0xfc
75
+
76
+ # Segregated Witness(0x01) and Taproot(0x05..0x07, defined by BIP-371) key types.
77
+ # They have no meaning in Tapyrus. See InputTypes::RESERVED.
78
+ RESERVED = [0x01, *(0x05..0x07)].freeze
79
+ end
80
+
81
+ # The bitfield of PSTT_GLOBAL_TX_MODIFIABLE.
82
+ module TxModifiable
83
+ INPUTS = 0x01
84
+ OUTPUTS = 0x02
85
+ HAS_SIGHASH_SINGLE = 0x04
86
+ ALL = INPUTS | OUTPUTS | HAS_SIGHASH_SINGLE
87
+ end
88
+
89
+ # Raised when a PSTT is malformed, or when an operation violates a rule of TIP-0174.
90
+ class Error < StandardError
91
+ end
92
+
93
+ # A single key-value record of a PSTT map.
94
+ KeyValue =
95
+ Struct.new(:type, :keydata, :value) do
96
+ # The complete key(<keytype> and <keydata>). Records are unique by this value within a map.
97
+ # @return [String] the complete key with binary format.
98
+ def key
99
+ Tapyrus.pack_var_int(type) + keydata
100
+ end
101
+
102
+ def to_payload
103
+ k = key
104
+ Tapyrus.pack_var_int(k.bytesize) + k + Tapyrus.pack_var_int(value.bytesize) + value
105
+ end
106
+ end
107
+
108
+ class << self
109
+ # Read a compact size unsigned integer from +buf+.
110
+ #
111
+ # Every compact size integer of the format must be minimally encoded. TIP-0174 states the
112
+ # requirement for <keytype>, and holding the whole container to it is what makes the byte
113
+ # representation of a PSTT unique: two encodings of the same length would otherwise produce
114
+ # two different byte strings carrying the same records. It also removes the need to treat
115
+ # the map separator specially, since a non-minimally encoded 0 is rejected here.
116
+ # @param [StringIO] buf a buffer.
117
+ # @return [Integer] the value.
118
+ # @raise [Tapyrus::PSTT::Error] if the buffer is truncated or the encoding is not minimal.
119
+ def read_compact_size(buf)
120
+ prefix = read_bytes(buf, 1).unpack1("C")
121
+ case prefix
122
+ when 0xfd
123
+ value = read_bytes(buf, 2).unpack1("v")
124
+ raise Error, "compact size is not minimally encoded." if value < 0xfd
125
+ when 0xfe
126
+ value = read_bytes(buf, 4).unpack1("V")
127
+ raise Error, "compact size is not minimally encoded." if value <= 0xffff
128
+ when 0xff
129
+ value = read_bytes(buf, 8).unpack1("Q<")
130
+ raise Error, "compact size is not minimally encoded." if value <= 0xffffffff
131
+ else
132
+ value = prefix
133
+ end
134
+ value
135
+ end
136
+
137
+ # Read exactly +size+ bytes from +buf+.
138
+ # @param [StringIO] buf a buffer.
139
+ # @param [Integer] size the byte size to be read.
140
+ # @return [String] the read data with binary format.
141
+ # @raise [Tapyrus::PSTT::Error] if the buffer holds fewer bytes.
142
+ def read_bytes(buf, size)
143
+ data = buf.read(size)
144
+ raise Error, "PSTT payload is truncated." if data.nil? || data.bytesize != size
145
+ data
146
+ end
147
+
148
+ # Parse a single map, which is a sequence of key-value records terminated by 0x00.
149
+ # @param [StringIO] buf a buffer.
150
+ # @return [Array[Tapyrus::PSTT::KeyValue]] the records of the map.
151
+ # @raise [Tapyrus::PSTT::Error] if the map is malformed.
152
+ def parse_map(buf)
153
+ records = []
154
+ keys = {}
155
+ loop do
156
+ raise Error, "PSTT map is not terminated." if buf.eof?
157
+ key_len = read_compact_size(buf)
158
+ break if key_len.zero? # the 0x00 separator
159
+ key = read_bytes(buf, key_len)
160
+ key_buf = StringIO.new(key)
161
+ type = read_compact_size(key_buf)
162
+ keydata = key_buf.read || ""
163
+ raise Error, "PSTT map contains a duplicated key." if keys.key?(key)
164
+ keys[key] = true
165
+ value_len = read_compact_size(buf)
166
+ records << KeyValue.new(type, keydata, read_bytes(buf, value_len))
167
+ end
168
+ records
169
+ end
170
+
171
+ # Serialize +records+ as a map.
172
+ # @param [Array[Tapyrus::PSTT::KeyValue]] records the records of the map.
173
+ # @param [Array[String]] order the complete keys of the map this PSTT was parsed from.
174
+ # @return [String] the serialized map with binary format.
175
+ def serialize_map(records, order = nil)
176
+ order_records(records, order).map(&:to_payload).join + "\x00".b
177
+ end
178
+
179
+ # Put +records+ back into the order the map was parsed in, so that parsing a PSTT and
180
+ # serializing it again yields the same bytes. TIP-0174 prescribes no order - either order is
181
+ # a valid PSTT carrying the same records - but a byte-stable round trip lets a caller hash,
182
+ # cache or diff a PSTT without normalizing it first. A record which was not in the parsed
183
+ # map, such as a signature collected since, follows the records which were, in the order
184
+ # this implementation generates them. A map which was not parsed is emitted in ascending
185
+ # order of the complete key.
186
+ # @param [Array[Tapyrus::PSTT::KeyValue]] records the records of the map.
187
+ # @param [Array[String]] order the complete keys of the map this PSTT was parsed from.
188
+ # @return [Array[Tapyrus::PSTT::KeyValue]] the ordered records.
189
+ def order_records(records, order)
190
+ return records.sort_by { |r| [r.type, r.keydata] } if order.nil? || order.empty?
191
+ rank = {}
192
+ order.each_with_index { |key, i| rank[key] ||= i }
193
+ records.each_with_index.sort_by { |record, i| [rank.fetch(record.key, order.size + i), i] }.map(&:first)
194
+ end
195
+
196
+ # Check that +record+ has no key data, as its field definition requires.
197
+ # @param [Tapyrus::PSTT::KeyValue] record a record.
198
+ # @raise [Tapyrus::PSTT::Error] if the record has key data.
199
+ def validate_empty_keydata!(record)
200
+ raise Error, format("keydata for type 0x%02x must be empty.", record.type) unless record.keydata.empty?
201
+ end
202
+
203
+ # Check that the key data of +record+ is a public key.
204
+ # @param [Tapyrus::PSTT::KeyValue] record a record.
205
+ # @raise [Tapyrus::PSTT::Error] if the key data is not a 33- or 65-byte public key.
206
+ def validate_pubkey_keydata!(record)
207
+ size = record.keydata.bytesize
208
+ unless [Tapyrus::Key::COMPRESSED_PUBLIC_KEY_SIZE, Tapyrus::Key::PUBLIC_KEY_SIZE].include?(size)
209
+ raise Error,
210
+ format("keydata for type 0x%02x must be a 33- or 65-byte public key, but %d bytes.", record.type, size)
211
+ end
212
+ end
213
+
214
+ # Check that the value of +record+ has +size+ bytes.
215
+ # @param [Tapyrus::PSTT::KeyValue] record a record.
216
+ # @param [Integer] size the required byte size.
217
+ # @raise [Tapyrus::PSTT::Error] if the value has another size.
218
+ def validate_value_size!(record, size)
219
+ unless record.value.bytesize == size
220
+ raise Error,
221
+ format(
222
+ "value for type 0x%02x must be %d bytes, but %d bytes.",
223
+ record.type,
224
+ size,
225
+ record.value.bytesize
226
+ )
227
+ end
228
+ end
229
+
230
+ # Read a 32-bit little endian unsigned integer from the value of +record+.
231
+ def read_u32(record)
232
+ validate_value_size!(record, 4)
233
+ record.value.unpack1("V")
234
+ end
235
+
236
+ # Read a 32-bit little endian signed integer from the value of +record+.
237
+ def read_i32(record)
238
+ validate_value_size!(record, 4)
239
+ record.value.unpack1("l<")
240
+ end
241
+
242
+ # Read a 64-bit little endian signed integer from the value of +record+.
243
+ def read_i64(record)
244
+ validate_value_size!(record, 8)
245
+ record.value.unpack1("q<")
246
+ end
247
+
248
+ # Read an 8-bit unsigned integer from the value of +record+.
249
+ def read_u8(record)
250
+ validate_value_size!(record, 1)
251
+ record.value.unpack1("C")
252
+ end
253
+
254
+ # Read a compact size unsigned integer from the value of +record+.
255
+ def read_count(record)
256
+ buf = StringIO.new(record.value)
257
+ count = read_compact_size(buf)
258
+ raise Error, format("value for type 0x%02x is not a compact size uint.", record.type) unless buf.eof?
259
+ count
260
+ end
261
+
262
+ # Parse the value or the key data of a record with +block+, and report any failure as a
263
+ # Tapyrus::PSTT::Error. The records of a PSTT come from another party, so a malformed one
264
+ # must not surface as an exception of the parser the field happens to be built on.
265
+ # @param [String] name the name of the field, used in the error message.
266
+ # @raise [Tapyrus::PSTT::Error] if the block raises.
267
+ def parse_field(name)
268
+ yield
269
+ rescue Error
270
+ raise
271
+ rescue StandardError => e
272
+ raise Error, "#{name} is malformed. #{e.message}"
273
+ end
274
+
275
+ # Check that +hash_type+ is one of the sighash types TIP-0174 defines.
276
+ # @param [Integer] hash_type a sighash type.
277
+ # @raise [Tapyrus::PSTT::Error] if it is not.
278
+ def validate_sighash_type!(hash_type)
279
+ unless SIGHASH_TYPES.include?(hash_type)
280
+ raise Error, format("Sighash type 0x%x is not defined by TIP-0174.", hash_type)
281
+ end
282
+ end
283
+
284
+ # Check that +amount+ is within the range Tapyrus allows.
285
+ # @param [Integer] amount an amount of tapy or of tokens.
286
+ # @raise [Tapyrus::PSTT::Error] if it is not.
287
+ def validate_amount!(amount)
288
+ unless amount.is_a?(Integer) && amount >= 0 && amount <= MAX_MONEY
289
+ raise Error, "PSTT_OUT_AMOUNT must be between 0 and #{MAX_MONEY}, but #{amount}."
290
+ end
291
+ end
292
+
293
+ # Check that +features+ is a positive value. Tapyrus currently defines only 1, but a
294
+ # transaction feature which a later version adds is accepted so that this implementation
295
+ # can carry it through.
296
+ # @param [Integer] features the value of PSTT_GLOBAL_TX_FEATURES.
297
+ # @raise [Tapyrus::PSTT::Error] if it is not positive.
298
+ def validate_features!(features)
299
+ raise Error, "PSTT_GLOBAL_TX_FEATURES must be greater than 0, but #{features}." unless features.positive?
300
+ end
301
+
302
+ # Check that +flags+ sets no reserved bit of PSTT_GLOBAL_TX_MODIFIABLE.
303
+ # @param [Integer] flags the bitfield.
304
+ # @raise [Tapyrus::PSTT::Error] if a reserved bit is set.
305
+ def validate_tx_modifiable!(flags)
306
+ unless (flags & ~TxModifiable::ALL).zero?
307
+ raise Error, "The reserved bits of PSTT_GLOBAL_TX_MODIFIABLE must be 0."
308
+ end
309
+ end
310
+
311
+ # Check that +version+ is a version this implementation supports.
312
+ # @param [Integer] version the value of PSTT_GLOBAL_VERSION.
313
+ # @raise [Tapyrus::PSTT::Error] if it is greater than the version TIP-0174 defines.
314
+ def validate_version!(version)
315
+ raise Error, "PSTT version #{version} is not supported." if version > VERSION
316
+ end
317
+
318
+ # Check that +value+ is a valid PSTT_IN_REQUIRED_TIME_LOCKTIME.
319
+ # @raise [Tapyrus::PSTT::Error] if it is below the threshold.
320
+ def validate_time_locktime!(value)
321
+ if value < LOCKTIME_THRESHOLD
322
+ raise Error, "PSTT_IN_REQUIRED_TIME_LOCKTIME must be greater than or equal to #{LOCKTIME_THRESHOLD}."
323
+ end
324
+ end
325
+
326
+ # Check that +value+ is a valid PSTT_IN_REQUIRED_HEIGHT_LOCKTIME.
327
+ # @raise [Tapyrus::PSTT::Error] if it is out of range.
328
+ def validate_height_locktime!(value)
329
+ if value.zero? || value >= LOCKTIME_THRESHOLD
330
+ raise Error, "PSTT_IN_REQUIRED_HEIGHT_LOCKTIME must be greater than 0 and less than #{LOCKTIME_THRESHOLD}."
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
data/lib/tapyrus/rpc.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  module Tapyrus
2
2
  module RPC
3
- autoload :HttpServer, "tapyrus/rpc/http_server"
4
- autoload :RequestHandler, "tapyrus/rpc/request_handler"
5
3
  autoload :TapyrusCoreClient, "tapyrus/rpc/tapyrus_core_client"
6
4
  autoload :Error, "tapyrus/rpc/tapyrus_core_client"
7
5
  end
@@ -107,12 +107,22 @@ module Tapyrus
107
107
  if opcode
108
108
  script << (v =~ /^\d/ && Opcodes.small_int_to_opcode(v.ord) ? v.ord : opcode)
109
109
  else
110
- script << (v =~ /^[0-9]+$/ ? v.to_i : v)
110
+ script << (number_token?(v) ? v.to_i : v)
111
111
  end
112
112
  end
113
113
  script
114
114
  end
115
115
 
116
+ # Whether +token+ is a script number rather than pushed data with hex format.
117
+ # A digit only token is ambiguous since #to_s emits pushed data as hex. #to_s never emits
118
+ # a number with more than 10 digits, so a longer even-length token must be pushed data.
119
+ # @param [String] token a token of the script string.
120
+ # @return [Boolean] whether +token+ should be interpreted as a script number.
121
+ def self.number_token?(token)
122
+ return false unless token =~ /^[0-9]+$/
123
+ token.length < 12 || token.length.odd?
124
+ end
125
+
116
126
  # generate script from addr.
117
127
  # @param [String] addr address.
118
128
  # @return [Tapyrus::Script] parsed script.
@@ -439,11 +449,11 @@ module Tapyrus
439
449
  header =
440
450
  if size < OP_PUSHDATA1
441
451
  [size].pack("C")
442
- elsif size < 0xff
452
+ elsif size <= 0xff
443
453
  [OP_PUSHDATA1, size].pack("CC")
444
- elsif size < 0xffff
454
+ elsif size <= 0xffff
445
455
  [OP_PUSHDATA2, size].pack("Cv")
446
- elsif size < 0xffffffff
456
+ elsif size <= 0xffffffff
447
457
  [OP_PUSHDATA4, size].pack("CV")
448
458
  else
449
459
  raise ArgumentError, "data size is too big."
@@ -30,10 +30,13 @@ module Tapyrus
30
30
  v = Tapyrus.hmac_sha256(k, v)
31
31
 
32
32
  # 3.2.h
33
- t = ""
34
33
  10_000.times do
35
- v = Tapyrus.hmac_sha256(k, v)
36
- t = (t + v)
34
+ # T is reset for each candidate, otherwise a retry can never produce a value below the order.
35
+ t = ""
36
+ while t.bytesize < 32
37
+ v = Tapyrus.hmac_sha256(k, v)
38
+ t = (t + v)
39
+ end
37
40
  t_num = t.bth.to_i(16)
38
41
  return t_num if 1 <= t_num && t_num < Tapyrus::Secp256k1::GROUP.order
39
42
  k = Tapyrus.hmac_sha256(k, v + "00".htb)