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,658 @@
1
+ module Tapyrus
2
+ module PSTT
3
+ # A Partially Signed Tapyrus Transaction.
4
+ #
5
+ # The methods are grouped by the roles TIP-0174 defines: Creator(::new), Constructor(#add_input,
6
+ # #add_output, #add_pair, #finish_construction!), Updater(#update_input, #set_sequence),
7
+ # Signer(#sign), Combiner(#combine), Input Finalizer(#finalize!) and
8
+ # Transaction Extractor(#extract_tx).
9
+ class Tx
10
+ include Tapyrus::HexConverter
11
+
12
+ # @!attribute [r] features
13
+ # @return [Integer] the features field of the transaction. Tapyrus currently defines only 1.
14
+ attr_reader :features
15
+
16
+ # @!attribute [r] fallback_locktime
17
+ # @return [Integer] the locktime to use if no input specifies a required locktime.
18
+ attr_reader :fallback_locktime
19
+
20
+ # @!attribute [r] tx_modifiable
21
+ # @return [Integer] the bitfield of PSTT_GLOBAL_TX_MODIFIABLE. nil means not modifiable.
22
+ attr_reader :tx_modifiable
23
+
24
+ # @!attribute [rw] version
25
+ # @return [Integer] the version number of this PSTT.
26
+ attr_accessor :version
27
+
28
+ # @!attribute [r] inputs
29
+ # @return [Array[Tapyrus::PSTT::Input]] the input maps.
30
+ attr_reader :inputs
31
+
32
+ # @!attribute [r] outputs
33
+ # @return [Array[Tapyrus::PSTT::Output]] the output maps.
34
+ attr_reader :outputs
35
+
36
+ # @!attribute [r] xpubs
37
+ # @return [Hash] Tapyrus::PSTT::KeyOriginInfo, keyed by the Base58 extended public key.
38
+ attr_reader :xpubs
39
+
40
+ # @!attribute [r] proprietaries
41
+ # @return [Array[Tapyrus::PSTT::Proprietary]] the proprietary records.
42
+ attr_reader :proprietaries
43
+
44
+ # @!attribute [r] unknowns
45
+ # @return [Hash] the records whose key type this implementation does not know, keyed by complete key.
46
+ attr_reader :unknowns
47
+
48
+ # @!attribute [rw] global_record_order
49
+ # @return [Array[String]] the complete keys of the global map this PSTT was parsed from.
50
+ # See Tapyrus::PSTT.order_records.
51
+ attr_accessor :global_record_order
52
+
53
+ # Create a new PSTT. This is the Creator role.
54
+ # @param [Integer] features the features field of the transaction.
55
+ # @param [Integer] tx_modifiable the initial value of PSTT_GLOBAL_TX_MODIFIABLE.
56
+ # Set Tapyrus::PSTT::TxModifiable::INPUTS and/or OUTPUTS if other parties will add
57
+ # inputs or outputs. nil fixes the transaction at creation.
58
+ # @param [Array[Tapyrus::PSTT::Input]] inputs the inputs the Creator creates.
59
+ # @param [Array[Tapyrus::PSTT::Output]] outputs the outputs the Creator creates.
60
+ def initialize(features: 1, fallback_locktime: nil, tx_modifiable: nil, version: VERSION, inputs: [], outputs: [])
61
+ @features = features
62
+ @fallback_locktime = fallback_locktime
63
+ @tx_modifiable = tx_modifiable
64
+ @version = version
65
+ @inputs = inputs
66
+ @outputs = outputs
67
+ @xpubs = {}
68
+ @proprietaries = []
69
+ @unknowns = {}
70
+ @global_record_order = []
71
+ end
72
+
73
+ # Set the features field of the transaction. The signature hash covers it, so it must not
74
+ # change once the PSTT holds a signature.
75
+ # @param [Integer] value the features field.
76
+ # @raise [Tapyrus::PSTT::Error] if the PSTT already contains signatures.
77
+ def features=(value)
78
+ if value != @features && signed?
79
+ raise Error, "The features of a PSTT which already contains signatures must not be changed."
80
+ end
81
+ @features = value
82
+ end
83
+
84
+ # Set PSTT_GLOBAL_FALLBACK_LOCKTIME. The signature hash covers the locktime of the
85
+ # transaction, so a change which alters it is refused once the PSTT holds a signature.
86
+ # @param [Integer] value the fallback locktime.
87
+ # @raise [Tapyrus::PSTT::Error] if the change alters the locktime.
88
+ def fallback_locktime=(value)
89
+ return @fallback_locktime = value if value == @fallback_locktime
90
+ previous = @fallback_locktime
91
+ keeping_locktime("PSTT_GLOBAL_FALLBACK_LOCKTIME") do
92
+ @fallback_locktime = value
93
+ -> { @fallback_locktime = previous }
94
+ end
95
+ end
96
+
97
+ # Set PSTT_GLOBAL_TX_MODIFIABLE. Once the PSTT holds a signature the field records what
98
+ # those signatures still allow, so it may only be tightened: setting Inputs Modifiable or
99
+ # Outputs Modifiable again would re-open the very modification a Signer closed by signing,
100
+ # and clearing Has SIGHASH_SINGLE would drop the pairing rule which keeps a SIGHASH_SINGLE
101
+ # signature covering its own output.
102
+ # @param [Integer] value the bitfield, or nil to omit the field.
103
+ # @raise [Tapyrus::PSTT::Error] if a reserved bit is set, or the value relaxes the field.
104
+ def tx_modifiable=(value)
105
+ PSTT.validate_tx_modifiable!(value) if value
106
+ validate_tx_modifiable_tightens!(value) if signed?
107
+ @tx_modifiable = value
108
+ end
109
+
110
+ # Parse a PSTT from its raw binary format.
111
+ # @param [String] payload a PSTT with binary format.
112
+ # @return [Tapyrus::PSTT::Tx]
113
+ # @raise [Tapyrus::PSTT::Error] if the payload is not a valid PSTT.
114
+ def self.parse_from_payload(payload)
115
+ buf = payload.is_a?(String) ? StringIO.new(payload) : payload
116
+ unless PSTT.read_bytes(buf, MAGIC.bytesize) == MAGIC
117
+ raise Error, "Invalid PSTT magic. The payload does not begin with 'pstt' 0xFF."
118
+ end
119
+ # Every map is read before any of them is interpreted, so that the declared counts are
120
+ # compared against the number of maps which is actually there. Assigning maps to inputs
121
+ # and outputs by the declared counts alone would let a wrong count hand an output map to
122
+ # the input parser, which reports whichever field of the misread map fails first.
123
+ maps = []
124
+ maps << PSTT.parse_map(buf) until buf.eof?
125
+ raise Error, "The PSTT global map is missing." if maps.empty?
126
+ pstt, input_count, output_count = parse_global_map(maps.shift)
127
+ unless maps.size == input_count + output_count
128
+ raise Error, "The number of maps does not match PSTT_GLOBAL_INPUT_COUNT and PSTT_GLOBAL_OUTPUT_COUNT."
129
+ end
130
+ maps.shift(input_count).each { |records| pstt.inputs << Input.parse_from_records(records) }
131
+ maps.each { |records| pstt.outputs << Output.parse_from_records(records) }
132
+ pstt
133
+ end
134
+
135
+ # Parse a PSTT from its Base64 format.
136
+ # @param [String] base64 a PSTT with Base64 format.
137
+ # @return [Tapyrus::PSTT::Tx]
138
+ def self.from_base64(base64)
139
+ payload = base64.unpack1("m0")
140
+ raise Error, "Invalid Base64 encoding." if payload.nil?
141
+ parse_from_payload(payload)
142
+ rescue ArgumentError
143
+ raise Error, "Invalid Base64 encoding."
144
+ end
145
+
146
+ def to_payload
147
+ MAGIC + PSTT.serialize_map(global_records, global_record_order) + inputs.map(&:to_payload).join +
148
+ outputs.map(&:to_payload).join
149
+ end
150
+
151
+ # @return [String] this PSTT with Base64 format, which is used for display and text transport.
152
+ def to_base64
153
+ [to_payload].pack("m0")
154
+ end
155
+
156
+ # @return [Array[Tapyrus::PSTT::KeyValue]] the records of the global map.
157
+ def global_records
158
+ raise Error, "PSTT_GLOBAL_TX_FEATURES is required." unless features
159
+ PSTT.validate_tx_modifiable!(tx_modifiable) if tx_modifiable
160
+ PSTT.validate_version!(version) if version
161
+ records = []
162
+ xpubs.each do |xpub, info|
163
+ keydata = PSTT.parse_field("PSTT_GLOBAL_XPUB") { Tapyrus::ExtPubkey.from_base58(xpub).to_payload }
164
+ records << KeyValue.new(GlobalTypes::XPUB, keydata, info.to_payload)
165
+ end
166
+ records << KeyValue.new(GlobalTypes::TX_FEATURES, "".b, [features].pack("l<"))
167
+ if fallback_locktime
168
+ records << KeyValue.new(GlobalTypes::FALLBACK_LOCKTIME, "".b, [fallback_locktime].pack("V"))
169
+ end
170
+ records << KeyValue.new(GlobalTypes::INPUT_COUNT, "".b, Tapyrus.pack_var_int(inputs.size))
171
+ records << KeyValue.new(GlobalTypes::OUTPUT_COUNT, "".b, Tapyrus.pack_var_int(outputs.size))
172
+ records << KeyValue.new(GlobalTypes::TX_MODIFIABLE, "".b, [tx_modifiable].pack("C")) if tx_modifiable
173
+ records << KeyValue.new(GlobalTypes::VERSION, "".b, [version].pack("V")) if version && version > 0
174
+ proprietaries.each { |p| records << p.to_record(GlobalTypes::PROPRIETARY) }
175
+ unknowns.each do |key, value|
176
+ buf = StringIO.new(key)
177
+ type = PSTT.read_compact_size(buf)
178
+ records << KeyValue.new(type, buf.read || "".b, value)
179
+ end
180
+ records
181
+ end
182
+
183
+ # The locktime of the transaction, computed as Determining the Locktime describes.
184
+ # @return [Integer] the locktime.
185
+ # @raise [Tapyrus::PSTT::Error] if no locktime kind is acceptable to every input.
186
+ def locktime
187
+ time_locktimes = []
188
+ height_locktimes = []
189
+ only_time = false
190
+ only_height = false
191
+ inputs.each do |input|
192
+ time = input.required_time_locktime
193
+ height = input.required_height_locktime
194
+ next unless time || height
195
+ time_locktimes << time if time
196
+ height_locktimes << height if height
197
+ only_time = true if time && !height
198
+ only_height = true if height && !time
199
+ end
200
+ return fallback_locktime || 0 if time_locktimes.empty? && height_locktimes.empty?
201
+ if only_time && only_height
202
+ raise Error, "No locktime kind is acceptable to every input which requires a locktime."
203
+ end
204
+ # If both kinds are acceptable, the height-based locktime is chosen.
205
+ only_time ? time_locktimes.max : height_locktimes.max
206
+ end
207
+
208
+ # Build the transaction determined by the fields of this PSTT.
209
+ # @param [Integer] sequence override the sequence number of every input.
210
+ # @param [Boolean] final whether to attach PSTT_IN_FINAL_SCRIPTSIG to each input.
211
+ # @return [Tapyrus::Tx]
212
+ def build_tx(sequence: nil, final: false)
213
+ tx = Tapyrus::Tx.new
214
+ tx.features = features
215
+ tx.lock_time = locktime
216
+ inputs.each do |input|
217
+ script_sig = final ? input.final_script_sig : Tapyrus::Script.new
218
+ tx.inputs << Tapyrus::TxIn.new(
219
+ out_point: input.out_point,
220
+ script_sig: script_sig,
221
+ sequence: sequence || input.final_sequence
222
+ )
223
+ end
224
+ outputs.each { |output| tx.outputs << output.to_tx_out }
225
+ tx
226
+ end
227
+
228
+ # The identifier of this PSTT: the txid of the transaction built from its fields with the
229
+ # sequence number of every input set to 0.
230
+ # @return [String] the txid with hex format.
231
+ def identification_txid
232
+ build_tx(sequence: 0).txid
233
+ end
234
+
235
+ # @return [Boolean] whether inputs may still be added.
236
+ def inputs_modifiable?
237
+ !(tx_modifiable.to_i & TxModifiable::INPUTS).zero?
238
+ end
239
+
240
+ # @return [Boolean] whether outputs may still be added.
241
+ def outputs_modifiable?
242
+ !(tx_modifiable.to_i & TxModifiable::OUTPUTS).zero?
243
+ end
244
+
245
+ # @return [Boolean] whether this PSTT contains a signature made with SIGHASH_SINGLE.
246
+ def has_sighash_single?
247
+ !(tx_modifiable.to_i & TxModifiable::HAS_SIGHASH_SINGLE).zero?
248
+ end
249
+
250
+ # Whether any input carries a signature. A finalized input counts as signed even though its
251
+ # PSTT_IN_PARTIAL_SIG records are gone: its signatures moved into PSTT_IN_FINAL_SCRIPTSIG,
252
+ # where they still commit to everything they committed to before.
253
+ # @return [Boolean]
254
+ def signed?
255
+ inputs.any? { |input| input.signed? || input.finalized? }
256
+ end
257
+
258
+ # Add an input. This is the Constructor role.
259
+ # @param [Tapyrus::PSTT::Input] input the input to be added.
260
+ # @return [Tapyrus::PSTT::Tx] self.
261
+ # @raise [Tapyrus::PSTT::Error] if the input must not be added.
262
+ def add_input(input)
263
+ raise Error, "Inputs are not modifiable." unless inputs_modifiable?
264
+ if has_sighash_single?
265
+ raise Error, "This PSTT contains a SIGHASH_SINGLE signature. Use #add_pair to keep inputs and outputs paired."
266
+ end
267
+ keeping_locktime("The added input") do
268
+ inputs << input
269
+ -> { inputs.pop }
270
+ end
271
+ self
272
+ end
273
+
274
+ # Add an output. This is the Constructor role.
275
+ # @param [Tapyrus::PSTT::Output] output the output to be added.
276
+ # @return [Tapyrus::PSTT::Tx] self.
277
+ # @raise [Tapyrus::PSTT::Error] if the output must not be added.
278
+ def add_output(output)
279
+ raise Error, "Outputs are not modifiable." unless outputs_modifiable?
280
+ if has_sighash_single?
281
+ raise Error, "This PSTT contains a SIGHASH_SINGLE signature. Use #add_pair to keep inputs and outputs paired."
282
+ end
283
+ outputs << output
284
+ self
285
+ end
286
+
287
+ # Add an input and an output at matching positions after the existing ones. A PSTT which
288
+ # contains a SIGHASH_SINGLE signature must preserve that correspondence.
289
+ # @param [Tapyrus::PSTT::Input] input the input to be added.
290
+ # @param [Tapyrus::PSTT::Output] output the output to be added.
291
+ # @return [Tapyrus::PSTT::Tx] self.
292
+ # @raise [Tapyrus::PSTT::Error] if the pair must not be added.
293
+ def add_pair(input, output)
294
+ raise Error, "Inputs are not modifiable." unless inputs_modifiable?
295
+ raise Error, "Outputs are not modifiable." unless outputs_modifiable?
296
+ # Appending never moves an existing input or output, so the only thing to enforce is that
297
+ # the added input has an output at its own position.
298
+ if inputs.size > outputs.size
299
+ raise Error, "The added input would have no corresponding output at a matching position."
300
+ end
301
+ keeping_locktime("The added input") do
302
+ inputs << input
303
+ -> { inputs.pop }
304
+ end
305
+ outputs << output
306
+ self
307
+ end
308
+
309
+ # Declare construction finished by clearing the Inputs Modifiable and Outputs Modifiable flags.
310
+ # @return [Tapyrus::PSTT::Tx] self.
311
+ def finish_construction!
312
+ self.tx_modifiable = tx_modifiable.to_i & ~(TxModifiable::INPUTS | TxModifiable::OUTPUTS)
313
+ self
314
+ end
315
+
316
+ # Set the sequence number of an input. This is the Updater role.
317
+ # @param [Integer] index the input index.
318
+ # @param [Integer] sequence the sequence number.
319
+ # @return [Tapyrus::PSTT::Tx] self.
320
+ # @raise [Tapyrus::PSTT::Error] if a collected signature commits to the sequence number.
321
+ def set_sequence(index, sequence)
322
+ input = input_at(index)
323
+ if input.signed? || input.finalized?
324
+ raise Error, "Input #{index} is already signed. Its sequence number must not be changed."
325
+ end
326
+ # A finalized input no longer carries the sighash types of the signatures it was built
327
+ # from, so which of them commit to which sequence number can no longer be decided. Any
328
+ # finalized input therefore blocks every sequence change in the PSTT.
329
+ if inputs.any?(&:finalized?)
330
+ raise Error, "A finalized input carries signatures which may commit to the sequence number of every input."
331
+ end
332
+ if inputs.any? { |i| i.partial_sigs.values.any? { |sig| Input.commits_to_all_sequences?(sig) } }
333
+ raise Error, "A SIGHASH_ALL signature commits to the sequence number of every input. It must not be changed."
334
+ end
335
+ input.sequence = sequence
336
+ self
337
+ end
338
+
339
+ # The scriptCode used to compute the signature hash of an input.
340
+ # @param [Integer] index the input index.
341
+ # @return [Tapyrus::Script]
342
+ def script_code(index)
343
+ input_at(index).script_code
344
+ end
345
+
346
+ # Compute the signature hash of an input.
347
+ # @param [Integer] index the input index.
348
+ # @param [Integer] sighash_type the sighash type. Defaults to PSTT_IN_SIGHASH_TYPE, or SIGHASH_ALL.
349
+ # @return [String] the signature hash with binary format.
350
+ # @raise [Tapyrus::PSTT::Error] if the input must not be signed.
351
+ def sighash_for_input(index, sighash_type: nil)
352
+ input = input_at(index)
353
+ hash_type = resolve_sighash_type(input, sighash_type)
354
+ if (hash_type & 0x1f) == SIGHASH_TYPE[:single] && index >= outputs.size
355
+ raise Error, "SIGHASH_SINGLE must not be used for an input which has no corresponding output."
356
+ end
357
+ input.validate_for_sign!
358
+ build_tx.sighash_for_input(index, input.script_code, hash_type: hash_type)
359
+ end
360
+
361
+ # Sign an input. This is the Signer role.
362
+ # @param [Integer] index the input index.
363
+ # @param [Tapyrus::Key] key the key to sign with.
364
+ # @param [Integer] sighash_type the sighash type. Defaults to PSTT_IN_SIGHASH_TYPE, or SIGHASH_ALL.
365
+ # @param [Symbol] algo the signature scheme. :ecdsa or :schnorr.
366
+ # @param [Boolean] low_r whether to apply low-R grinding to an ECDSA signature.
367
+ # @return [String] the signature which was added, with binary format.
368
+ # @raise [Tapyrus::PSTT::Error] if the input must not be signed.
369
+ def sign(index, key, sighash_type: nil, algo: :ecdsa, low_r: true)
370
+ input = input_at(index)
371
+ # The Input Finalizer removed the PSTT_IN_PARTIAL_SIG records of this input, so a
372
+ # signature added now would recreate a record the format says is no longer there.
373
+ raise Error, "Input #{index} is finalized. It takes no further signature." if input.finalized?
374
+ hash_type = resolve_sighash_type(input, sighash_type)
375
+ input.validate_signature_algo!(algo)
376
+ sighash = sighash_for_input(index, sighash_type: hash_type)
377
+ sig = key.sign(sighash, low_r, nil, algo: algo) + [hash_type].pack("C")
378
+ input.partial_sigs[key.pubkey] = sig
379
+ update_tx_modifiable_after_sign(hash_type)
380
+ sig
381
+ end
382
+
383
+ # Merge another PSTT which has the same identifier into this one. This is the Combiner role.
384
+ # @param [Tapyrus::PSTT::Tx] other the PSTT to be merged.
385
+ # @return [Tapyrus::PSTT::Tx] the merged PSTT.
386
+ # @raise [Tapyrus::PSTT::Error] if the identifiers differ.
387
+ def combine(other)
388
+ unless identification_txid == other.identification_txid
389
+ raise Error, "PSTTs with different identifiers must not be combined."
390
+ end
391
+ validate_sequences_agree!(other)
392
+ modifiable = merge_tx_modifiable(tx_modifiable, other.tx_modifiable)
393
+ payload =
394
+ MAGIC + PSTT.serialize_map(merge_records(global_records, other.global_records), global_record_order) +
395
+ inputs
396
+ .zip(other.inputs)
397
+ .map { |a, b| PSTT.serialize_map(merge_records(a.to_records, b.to_records), a.record_order) }
398
+ .join +
399
+ outputs
400
+ .zip(other.outputs)
401
+ .map { |a, b| PSTT.serialize_map(merge_records(a.to_records, b.to_records), a.record_order) }
402
+ .join
403
+ combined = self.class.parse_from_payload(payload)
404
+ combined.tx_modifiable = modifiable
405
+ combined
406
+ end
407
+
408
+ # Assemble the complete scriptSig of every input. This is the Input Finalizer role.
409
+ # @return [Tapyrus::PSTT::Tx] self.
410
+ # @raise [Tapyrus::PSTT::Error] if the PSTT is still modifiable, or an input cannot be finalized.
411
+ def finalize!
412
+ raise Error, "This PSTT has no inputs to finalize." if inputs.empty?
413
+ if inputs_modifiable? || outputs_modifiable?
414
+ raise Error, "A PSTT must not be finalized while it is still modifiable."
415
+ end
416
+ inputs.each_with_index { |input, index| input.finalize!(verified_partial_sigs(index)) }
417
+ self
418
+ end
419
+
420
+ # The signatures of an input which verify against the transaction this PSTT describes.
421
+ #
422
+ # TIP-0174 has the Input Finalizer check that the collected records are sufficient to
423
+ # satisfy the script of the output being spent, and a signature which does not verify is
424
+ # not. It also matters which ones are dropped: the multisig scriptSig takes the first
425
+ # +threshold+ public keys of the redeem script which carry a signature, so one bad
426
+ # signature among otherwise good ones would displace a good one and yield a scriptSig
427
+ # which fails validation, with the signatures needed to spend the output sitting unused in
428
+ # the same input.
429
+ # @param [Integer] index the input index.
430
+ # @return [Hash] the signatures which verify, keyed by public key with hex format.
431
+ def verified_partial_sigs(index)
432
+ input = input_at(index)
433
+ return {} if input.partial_sigs.empty?
434
+ script_code = input.script_code
435
+ tx = build_tx
436
+ input.partial_sigs.select { |pubkey, sig| verified_sig?(tx, index, script_code, pubkey, sig) }
437
+ end
438
+
439
+ # @return [Boolean] whether every input has been finalized.
440
+ def finalized?
441
+ !inputs.empty? && inputs.all?(&:finalized?)
442
+ end
443
+
444
+ # Build the final transaction. This is the Transaction Extractor role.
445
+ # @return [Tapyrus::Tx] the transaction in Tapyrus network serialization.
446
+ # @raise [Tapyrus::PSTT::Error] if an input has no PSTT_IN_FINAL_SCRIPTSIG.
447
+ def extract_tx
448
+ raise Error, "This PSTT has no inputs. A transaction must spend at least one output." if inputs.empty?
449
+ inputs.each_with_index do |input, index|
450
+ raise Error, "PSTT_IN_FINAL_SCRIPTSIG for input #{index} is missing." unless input.finalized?
451
+ end
452
+ build_tx(final: true)
453
+ end
454
+
455
+ # The amounts being spent, per color. Requires PSTT_IN_UTXO on every input.
456
+ # @return [Hash] Tapyrus::Color::ColorIdentifier => amount. TPC uses the default color identifier.
457
+ def input_amounts
458
+ inputs.each_with_object(Hash.new(0)) do |input, totals|
459
+ input.validate_utxo!
460
+ out = input.utxo_output
461
+ totals[out.color_id || Tapyrus::Color::ColorIdentifier.default] += out.value
462
+ end
463
+ end
464
+
465
+ # The amounts being created, per color.
466
+ # @return [Hash] Tapyrus::Color::ColorIdentifier => amount. TPC uses the default color identifier.
467
+ def output_amounts
468
+ outputs.each_with_object(Hash.new(0)) do |output, totals|
469
+ totals[output.color_id || Tapyrus::Color::ColorIdentifier.default] += output.amount
470
+ end
471
+ end
472
+
473
+ # The TPC fee this transaction pays. Requires PSTT_IN_UTXO on every input.
474
+ # @return [Integer] the fee in tapy.
475
+ def fee
476
+ tpc = Tapyrus::Color::ColorIdentifier.default
477
+ input_amounts[tpc] - output_amounts[tpc]
478
+ end
479
+
480
+ def ==(other)
481
+ other.is_a?(Tx) && to_payload == other.to_payload
482
+ end
483
+
484
+ private
485
+
486
+ # Build a PSTT from the records of the global map.
487
+ # @return [Array] the PSTT, the declared input count and the declared output count.
488
+ def self.parse_global_map(records)
489
+ pstt = new(features: nil)
490
+ input_count = nil
491
+ output_count = nil
492
+ records.each do |record|
493
+ case record.type
494
+ when GlobalTypes::UNSIGNED_TX
495
+ raise Error, "Global key type 0x00 is reserved."
496
+ when GlobalTypes::XPUB
497
+ unless record.keydata.bytesize == 78
498
+ raise Error, "PSTT_GLOBAL_XPUB keydata must be a 78-byte serialized extended public key."
499
+ end
500
+ xpub = PSTT.parse_field("PSTT_GLOBAL_XPUB") { Tapyrus::ExtPubkey.parse_from_payload(record.keydata) }
501
+ # This PSTT holds the key by its Base58 form, so an extended public key which does not
502
+ # survive that conversion would be re-serialized as different bytes.
503
+ unless xpub.to_payload == record.keydata
504
+ raise Error, "PSTT_GLOBAL_XPUB is not a canonical serialized extended public key."
505
+ end
506
+ pstt.xpubs[xpub.to_base58] = KeyOriginInfo.parse_from_payload(record.value)
507
+ when GlobalTypes::TX_FEATURES
508
+ PSTT.validate_empty_keydata!(record)
509
+ pstt.features = PSTT.read_i32(record)
510
+ PSTT.validate_features!(pstt.features)
511
+ when GlobalTypes::FALLBACK_LOCKTIME
512
+ PSTT.validate_empty_keydata!(record)
513
+ pstt.fallback_locktime = PSTT.read_u32(record)
514
+ when GlobalTypes::INPUT_COUNT
515
+ PSTT.validate_empty_keydata!(record)
516
+ input_count = PSTT.read_count(record)
517
+ when GlobalTypes::OUTPUT_COUNT
518
+ PSTT.validate_empty_keydata!(record)
519
+ output_count = PSTT.read_count(record)
520
+ when GlobalTypes::TX_MODIFIABLE
521
+ PSTT.validate_empty_keydata!(record)
522
+ pstt.tx_modifiable = PSTT.read_u8(record)
523
+ when GlobalTypes::VERSION
524
+ PSTT.validate_empty_keydata!(record)
525
+ pstt.version = PSTT.read_u32(record)
526
+ PSTT.validate_version!(pstt.version)
527
+ when GlobalTypes::PROPRIETARY
528
+ pstt.proprietaries << Proprietary.parse_from_record(record)
529
+ else
530
+ pstt.unknowns[record.key] = record.value
531
+ end
532
+ end
533
+ raise Error, "PSTT_GLOBAL_TX_FEATURES is required." unless pstt.features
534
+ raise Error, "PSTT_GLOBAL_INPUT_COUNT is required." unless input_count
535
+ raise Error, "PSTT_GLOBAL_OUTPUT_COUNT is required." unless output_count
536
+ pstt.global_record_order = records.map(&:key)
537
+ [pstt, input_count, output_count]
538
+ end
539
+ private_class_method :parse_global_map
540
+
541
+ def input_at(index)
542
+ input = inputs[index]
543
+ raise Error, "Input #{index} does not exist." unless input
544
+ input
545
+ end
546
+
547
+ def resolve_sighash_type(input, sighash_type)
548
+ if input.sighash_type && sighash_type && input.sighash_type != sighash_type
549
+ raise Error, "The sighash type of this input is fixed to #{input.sighash_type} by PSTT_IN_SIGHASH_TYPE."
550
+ end
551
+ hash_type = sighash_type || input.sighash_type || SIGHASH_TYPE[:all]
552
+ # Only the low byte of the sighash type is appended to a signature, while the whole value
553
+ # is committed to by the signature hash. A value which does not fit in a byte would
554
+ # therefore produce a signature nobody can verify.
555
+ PSTT.validate_sighash_type!(hash_type)
556
+ hash_type
557
+ end
558
+
559
+ # Whether a signature verifies against +tx+ under the scheme its length selects and the
560
+ # sighash type its last byte names. A record which is not a signature at all, such as
561
+ # malformed DER or a public key which is not a point, is not valid either, so the decoders
562
+ # are allowed to fail here rather than to raise.
563
+ def verified_sig?(tx, index, script_code, pubkey, sig)
564
+ hash_type = sig[-1].unpack1("C")
565
+ return false unless SIGHASH_TYPES.include?(hash_type)
566
+ input = inputs[index]
567
+ return false if input.sighash_type && input.sighash_type != hash_type
568
+ return false if (hash_type & 0x1f) == SIGHASH_TYPE[:single] && index >= outputs.size
569
+ sighash = tx.sighash_for_input(index, script_code, hash_type: hash_type)
570
+ algo = sig.bytesize == 65 ? :schnorr : :ecdsa
571
+ Tapyrus::Key.new(pubkey: pubkey).verify(sig[0...-1], sighash, algo: algo)
572
+ rescue StandardError
573
+ false
574
+ end
575
+
576
+ # The locktime of the transaction, or nil when the required locktimes of the inputs
577
+ # contradict each other. Used to compare a hypothetical set of fields against the current
578
+ # one, where a contradiction counts as a change rather than as an error of its own.
579
+ def locktime_or_nil
580
+ locktime
581
+ rescue Error
582
+ nil
583
+ end
584
+
585
+ # Apply the change +block+ makes and keep it only if the locktime of the transaction is
586
+ # unchanged, because every signature commits to the locktime. The block returns a lambda
587
+ # which undoes its change. A PSTT which holds no signature is changed unconditionally.
588
+ # @param [String] what the subject of the error message.
589
+ # @raise [Tapyrus::PSTT::Error] if the change alters the locktime.
590
+ def keeping_locktime(what)
591
+ unless signed?
592
+ yield
593
+ return
594
+ end
595
+ before = locktime_or_nil
596
+ undo = yield
597
+ after = locktime_or_nil
598
+ return if before && after && before == after
599
+ undo.call
600
+ raise Error, "#{what} changes the locktime of a PSTT which already contains signatures."
601
+ end
602
+
603
+ # Check that +value+ does not relax PSTT_GLOBAL_TX_MODIFIABLE. See #tx_modifiable=.
604
+ def validate_tx_modifiable_tightens!(value)
605
+ before = tx_modifiable.to_i
606
+ after = value.to_i
607
+ permissive = TxModifiable::INPUTS | TxModifiable::OUTPUTS
608
+ loosened = (after & ~before & permissive) | (before & ~after & TxModifiable::HAS_SIGHASH_SINGLE)
609
+ return if loosened.zero?
610
+ raise Error, "PSTT_GLOBAL_TX_MODIFIABLE must not be relaxed once the PSTT contains signatures."
611
+ end
612
+
613
+ # Check that the two PSTTs agree on the sequence number of every input.
614
+ #
615
+ # The identifier is computed with every sequence number set to 0, so two PSTTs which
616
+ # disagree about one still identify as the same PSTT. Merging records by their complete key
617
+ # keeps this PSTT's value and discards the other's, which would invalidate every signature
618
+ # of the other which commits to the value it carried. This is therefore the conflict a
619
+ # Combiner refuses rather than resolves.
620
+ def validate_sequences_agree!(other)
621
+ inputs.each_with_index do |input, index|
622
+ next if input.final_sequence == other.inputs[index].final_sequence
623
+ raise Error, "Input #{index} has a different sequence number in the two PSTTs."
624
+ end
625
+ end
626
+
627
+ # PSTT_GLOBAL_TX_MODIFIABLE of a combined PSTT.
628
+ #
629
+ # Merging records by their complete key keeps one copy's value and discards the other's,
630
+ # which would resurrect a flag the other copy cleared when it collected a signature and
631
+ # leave a PSTT which carries a SIGHASH_ALL signature and still claims to accept further
632
+ # inputs. A flag which permits a modification therefore survives only while both copies
633
+ # permit it; every other bit, Has SIGHASH_SINGLE included, is the union of the two.
634
+ def merge_tx_modifiable(mine, theirs)
635
+ return nil if mine.nil? && theirs.nil?
636
+ permissive = TxModifiable::INPUTS | TxModifiable::OUTPUTS
637
+ a = mine.to_i
638
+ b = theirs.to_i
639
+ (a & b & permissive) | ((a | b) & ~permissive)
640
+ end
641
+
642
+ def update_tx_modifiable_after_sign(hash_type)
643
+ base = hash_type & 0x1f
644
+ flags = tx_modifiable
645
+ if flags
646
+ flags &= ~TxModifiable::INPUTS if (hash_type & SIGHASH_TYPE[:anyonecanpay]).zero?
647
+ flags &= ~TxModifiable::OUTPUTS unless base == SIGHASH_TYPE[:none]
648
+ end
649
+ flags = flags.to_i | TxModifiable::HAS_SIGHASH_SINGLE if base == SIGHASH_TYPE[:single]
650
+ self.tx_modifiable = flags
651
+ end
652
+
653
+ def merge_records(a, b)
654
+ (a + b).each_with_object({}) { |record, merged| merged[record.key] ||= record }.values
655
+ end
656
+ end
657
+ end
658
+ end