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,422 @@
1
+ module Tapyrus
2
+ module PSTT
3
+ # An input map of a PSTT.
4
+ class Input
5
+ # @!attribute [rw] out_point
6
+ # @return [Tapyrus::OutPoint] PSTT_IN_PREVIOUS_TXID and PSTT_IN_OUTPUT_INDEX.
7
+ attr_accessor :out_point
8
+
9
+ # @!attribute [rw] utxo
10
+ # @return [Tapyrus::Tx] the complete previous transaction which contains the output being spent.
11
+ attr_accessor :utxo
12
+
13
+ # @!attribute [rw] sighash_type
14
+ # @return [Integer] the sighash type signers must use for this input.
15
+ attr_accessor :sighash_type
16
+
17
+ # @!attribute [rw] redeem_script
18
+ # @return [Tapyrus::Script] the redeem script, if the output being spent is P2SH or CP2SH.
19
+ attr_accessor :redeem_script
20
+
21
+ # @!attribute [rw] final_script_sig
22
+ # @return [Tapyrus::Script] the complete scriptSig for this input.
23
+ attr_accessor :final_script_sig
24
+
25
+ # @!attribute [rw] sequence
26
+ # @return [Integer] the sequence number. nil means the default 0xFFFFFFFF.
27
+ attr_accessor :sequence
28
+
29
+ # @!attribute [rw] required_time_locktime
30
+ # @return [Integer] the minimum Unix timestamp the transaction's locktime must be.
31
+ attr_accessor :required_time_locktime
32
+
33
+ # @!attribute [rw] required_height_locktime
34
+ # @return [Integer] the minimum block height the transaction's locktime must be.
35
+ attr_accessor :required_height_locktime
36
+
37
+ # @!attribute [r] partial_sigs
38
+ # @return [Hash] the signatures for this input, keyed by public key with hex format.
39
+ attr_reader :partial_sigs
40
+
41
+ # @!attribute [r] bip32_derivations
42
+ # @return [Hash] Tapyrus::PSTT::KeyOriginInfo, keyed by public key with hex format.
43
+ attr_reader :bip32_derivations
44
+
45
+ # @!attribute [r] ripemd160_preimages
46
+ # @return [Hash] the preimages, keyed by hash with hex format.
47
+ attr_reader :ripemd160_preimages
48
+
49
+ # @!attribute [r] sha256_preimages
50
+ # @return [Hash] the preimages, keyed by hash with hex format.
51
+ attr_reader :sha256_preimages
52
+
53
+ # @!attribute [r] hash160_preimages
54
+ # @return [Hash] the preimages, keyed by hash with hex format.
55
+ attr_reader :hash160_preimages
56
+
57
+ # @!attribute [r] hash256_preimages
58
+ # @return [Hash] the preimages, keyed by hash with hex format.
59
+ attr_reader :hash256_preimages
60
+
61
+ # @!attribute [r] proprietaries
62
+ # @return [Array[Tapyrus::PSTT::Proprietary]] the proprietary records.
63
+ attr_reader :proprietaries
64
+
65
+ # @!attribute [r] unknowns
66
+ # @return [Hash] the records whose key type this implementation does not know, keyed by complete key.
67
+ attr_reader :unknowns
68
+
69
+ # @!attribute [rw] record_order
70
+ # @return [Array[String]] the complete keys of the map this input was parsed from. See
71
+ # Tapyrus::PSTT.order_records.
72
+ attr_accessor :record_order
73
+
74
+ # @param [Tapyrus::OutPoint] out_point the output being spent.
75
+ def initialize(out_point: nil, utxo: nil, sequence: nil)
76
+ @out_point = out_point
77
+ @utxo = utxo
78
+ @sequence = sequence
79
+ @partial_sigs = {}
80
+ @bip32_derivations = {}
81
+ @ripemd160_preimages = {}
82
+ @sha256_preimages = {}
83
+ @hash160_preimages = {}
84
+ @hash256_preimages = {}
85
+ @proprietaries = []
86
+ @unknowns = {}
87
+ @record_order = []
88
+ end
89
+
90
+ # Build an input map from the records of an input map.
91
+ # @param [Array[Tapyrus::PSTT::KeyValue]] records the records.
92
+ # @return [Tapyrus::PSTT::Input]
93
+ # @raise [Tapyrus::PSTT::Error] if a required field is missing or a field is malformed.
94
+ def self.parse_from_records(records)
95
+ input = new
96
+ txid = nil
97
+ index = nil
98
+ records.each do |record|
99
+ if InputTypes::RESERVED.include?(record.type)
100
+ raise Error, format("Input key type 0x%02x is reserved.", record.type)
101
+ end
102
+ case record.type
103
+ when InputTypes::UTXO
104
+ PSTT.validate_empty_keydata!(record)
105
+ input.utxo = parse_utxo(record.value)
106
+ when InputTypes::PARTIAL_SIG
107
+ PSTT.validate_pubkey_keydata!(record)
108
+ if record.value.empty?
109
+ raise Error, "PSTT_IN_PARTIAL_SIG must carry a signature followed by the 1-byte sighash type."
110
+ end
111
+ input.partial_sigs[record.keydata.bth] = record.value
112
+ when InputTypes::SIGHASH_TYPE
113
+ PSTT.validate_empty_keydata!(record)
114
+ input.sighash_type = PSTT.read_u32(record)
115
+ PSTT.validate_sighash_type!(input.sighash_type)
116
+ when InputTypes::REDEEM_SCRIPT
117
+ PSTT.validate_empty_keydata!(record)
118
+ input.redeem_script =
119
+ PSTT.parse_field("PSTT_IN_REDEEM_SCRIPT") { Tapyrus::Script.parse_from_payload(record.value) }
120
+ when InputTypes::BIP32_DERIVATION
121
+ PSTT.validate_pubkey_keydata!(record)
122
+ input.bip32_derivations[record.keydata.bth] = KeyOriginInfo.parse_from_payload(record.value)
123
+ when InputTypes::FINAL_SCRIPTSIG
124
+ PSTT.validate_empty_keydata!(record)
125
+ input.final_script_sig =
126
+ PSTT.parse_field("PSTT_IN_FINAL_SCRIPTSIG") { Tapyrus::Script.parse_from_payload(record.value) }
127
+ when InputTypes::RIPEMD160, InputTypes::HASH160
128
+ raise Error, "The preimage hash must be 20 bytes." unless record.keydata.bytesize == 20
129
+ input.preimages_for(record.type)[record.keydata.bth] = record.value
130
+ when InputTypes::SHA256, InputTypes::HASH256
131
+ raise Error, "The preimage hash must be 32 bytes." unless record.keydata.bytesize == 32
132
+ input.preimages_for(record.type)[record.keydata.bth] = record.value
133
+ when InputTypes::PREVIOUS_TXID
134
+ PSTT.validate_empty_keydata!(record)
135
+ PSTT.validate_value_size!(record, 32)
136
+ txid = record.value.bth
137
+ when InputTypes::OUTPUT_INDEX
138
+ PSTT.validate_empty_keydata!(record)
139
+ index = PSTT.read_u32(record)
140
+ when InputTypes::SEQUENCE
141
+ PSTT.validate_empty_keydata!(record)
142
+ input.sequence = PSTT.read_u32(record)
143
+ when InputTypes::REQUIRED_TIME_LOCKTIME
144
+ PSTT.validate_empty_keydata!(record)
145
+ input.required_time_locktime = PSTT.read_u32(record)
146
+ PSTT.validate_time_locktime!(input.required_time_locktime)
147
+ when InputTypes::REQUIRED_HEIGHT_LOCKTIME
148
+ PSTT.validate_empty_keydata!(record)
149
+ input.required_height_locktime = PSTT.read_u32(record)
150
+ PSTT.validate_height_locktime!(input.required_height_locktime)
151
+ when InputTypes::PROPRIETARY
152
+ input.proprietaries << Proprietary.parse_from_record(record)
153
+ else
154
+ input.unknowns[record.key] = record.value
155
+ end
156
+ end
157
+ raise Error, "PSTT_IN_PREVIOUS_TXID is required." unless txid
158
+ raise Error, "PSTT_IN_OUTPUT_INDEX is required." unless index
159
+ input.out_point = Tapyrus::OutPoint.new(txid, index)
160
+ input.record_order = records.map(&:key)
161
+ input.validate_sighash_type!
162
+ input
163
+ end
164
+
165
+ # Parse the value of a PSTT_IN_UTXO record.
166
+ # @param [String] payload the complete previous transaction with binary format.
167
+ # @return [Tapyrus::Tx]
168
+ # @raise [Tapyrus::PSTT::Error] if the value is not exactly one Tapyrus transaction.
169
+ def self.parse_utxo(payload)
170
+ utxo = PSTT.parse_field("PSTT_IN_UTXO") { Tapyrus::Tx.parse_from_payload(payload) }
171
+ # Tapyrus::Tx.parse_from_payload ignores trailing bytes. They would be lost when this
172
+ # input is serialized again, which a Combiner must not do.
173
+ raise Error, "PSTT_IN_UTXO has trailing bytes after the transaction." unless utxo.to_payload == payload
174
+ utxo
175
+ end
176
+ private_class_method :parse_utxo
177
+
178
+ # @return [Array[Tapyrus::PSTT::KeyValue]] the records of this input map.
179
+ def to_records
180
+ validate_sighash_type!
181
+ records = []
182
+ records << KeyValue.new(InputTypes::UTXO, "".b, utxo.to_payload) if utxo
183
+ partial_sigs.each { |pubkey, sig| records << KeyValue.new(InputTypes::PARTIAL_SIG, pubkey.htb, sig) }
184
+ records << KeyValue.new(InputTypes::SIGHASH_TYPE, "".b, [sighash_type].pack("V")) if sighash_type
185
+ records << KeyValue.new(InputTypes::REDEEM_SCRIPT, "".b, redeem_script.to_payload) if redeem_script
186
+ bip32_derivations.each do |pubkey, info|
187
+ records << KeyValue.new(InputTypes::BIP32_DERIVATION, pubkey.htb, info.to_payload)
188
+ end
189
+ records << KeyValue.new(InputTypes::FINAL_SCRIPTSIG, "".b, final_script_sig.to_payload) if final_script_sig
190
+ {
191
+ InputTypes::RIPEMD160 => ripemd160_preimages,
192
+ InputTypes::SHA256 => sha256_preimages,
193
+ InputTypes::HASH160 => hash160_preimages,
194
+ InputTypes::HASH256 => hash256_preimages
195
+ }.each { |type, preimages| preimages.each { |hash, image| records << KeyValue.new(type, hash.htb, image) } }
196
+ raise Error, "PSTT_IN_PREVIOUS_TXID is required." unless out_point
197
+ records << KeyValue.new(InputTypes::PREVIOUS_TXID, "".b, out_point.tx_hash.htb)
198
+ records << KeyValue.new(InputTypes::OUTPUT_INDEX, "".b, [out_point.index].pack("V"))
199
+ records << KeyValue.new(InputTypes::SEQUENCE, "".b, [sequence].pack("V")) if sequence
200
+ if required_time_locktime
201
+ PSTT.validate_time_locktime!(required_time_locktime)
202
+ records << KeyValue.new(InputTypes::REQUIRED_TIME_LOCKTIME, "".b, [required_time_locktime].pack("V"))
203
+ end
204
+ if required_height_locktime
205
+ PSTT.validate_height_locktime!(required_height_locktime)
206
+ records << KeyValue.new(InputTypes::REQUIRED_HEIGHT_LOCKTIME, "".b, [required_height_locktime].pack("V"))
207
+ end
208
+ proprietaries.each { |p| records << p.to_record(InputTypes::PROPRIETARY) }
209
+ unknowns.each { |key, value| records << unknown_record(key, value) }
210
+ records
211
+ end
212
+
213
+ def to_payload
214
+ PSTT.serialize_map(to_records, record_order)
215
+ end
216
+
217
+ # @return [Integer] the sequence number used in the transaction.
218
+ def final_sequence
219
+ sequence || Tapyrus::TxIn::SEQUENCE_FINAL
220
+ end
221
+
222
+ # @return [Tapyrus::TxOut] the output being spent, or nil if PSTT_IN_UTXO is absent.
223
+ def utxo_output
224
+ return nil unless utxo
225
+ utxo.outputs[out_point.index]
226
+ end
227
+
228
+ # @return [Boolean] whether this input has at least one signature.
229
+ def signed?
230
+ !partial_sigs.empty?
231
+ end
232
+
233
+ # @return [Boolean] whether this input has been finalized.
234
+ def finalized?
235
+ !final_script_sig.nil?
236
+ end
237
+
238
+ # The scriptCode used to compute the signature hash of this input.
239
+ # @return [Tapyrus::Script]
240
+ # @raise [Tapyrus::PSTT::Error] if the scriptCode cannot be determined.
241
+ def script_code
242
+ script = spent_script_pubkey
243
+ return script unless script.p2sh? || script.cp2sh?
244
+ raise Error, "PSTT_IN_REDEEM_SCRIPT is required to spend a P2SH or CP2SH output." unless redeem_script
245
+ validate_redeem_script!
246
+ redeem_script
247
+ end
248
+
249
+ # The scriptPubKey of the output being spent.
250
+ # @return [Tapyrus::Script]
251
+ # @raise [Tapyrus::PSTT::Error] if PSTT_IN_UTXO is absent or does not contain the output.
252
+ def spent_script_pubkey
253
+ validate_utxo!
254
+ utxo_output.script_pubkey
255
+ end
256
+
257
+ # Check that PSTT_IN_UTXO is present and identifies the output being spent.
258
+ # @raise [Tapyrus::PSTT::Error] if it is not.
259
+ def validate_utxo!
260
+ raise Error, "PSTT_IN_UTXO is required." unless utxo
261
+ raise Error, "The txid of PSTT_IN_UTXO does not match PSTT_IN_PREVIOUS_TXID." unless utxo.txid == out_point.txid
262
+ raise Error, "PSTT_IN_OUTPUT_INDEX does not exist in PSTT_IN_UTXO." unless utxo.outputs[out_point.index]
263
+ end
264
+
265
+ # Check that the redeem script hashes to the value committed in the scriptPubKey being spent.
266
+ # @raise [Tapyrus::PSTT::Error] if it does not.
267
+ def validate_redeem_script!
268
+ return unless redeem_script
269
+ script = spent_script_pubkey
270
+ committed =
271
+ if script.p2sh?
272
+ script.chunks[1].pushed_data
273
+ elsif script.cp2sh?
274
+ script.chunks[3].pushed_data
275
+ else
276
+ raise Error, "PSTT_IN_REDEEM_SCRIPT is set but the output being spent is neither P2SH nor CP2SH."
277
+ end
278
+ unless redeem_script.to_hash160 == committed.bth
279
+ raise Error, "PSTT_IN_REDEEM_SCRIPT does not hash to the value committed in the scriptPubKey."
280
+ end
281
+ end
282
+
283
+ # Check that this input requests a sighash type TIP-0174 defines, and that every signature
284
+ # it carries uses it. TIP-0174 obliges a Signer to use PSTT_IN_SIGHASH_TYPE, so an input
285
+ # which carries a signature made with another type contradicts itself: the signature covers
286
+ # a different transaction than the input asks for.
287
+ # @raise [Tapyrus::PSTT::Error] if a signature uses another sighash type.
288
+ def validate_sighash_type!
289
+ return unless sighash_type
290
+ PSTT.validate_sighash_type!(sighash_type)
291
+ partial_sigs.each do |pubkey, sig|
292
+ next if sig.empty?
293
+ hash_type = sig[-1].unpack1("C")
294
+ next if hash_type == sighash_type
295
+ raise Error,
296
+ format(
297
+ "PSTT_IN_PARTIAL_SIG for %s uses sighash type 0x%x, but PSTT_IN_SIGHASH_TYPE requires 0x%x.",
298
+ pubkey,
299
+ hash_type,
300
+ sighash_type
301
+ )
302
+ end
303
+ end
304
+
305
+ # Check everything a Signer must verify before signing this input.
306
+ # @raise [Tapyrus::PSTT::Error] if a check fails.
307
+ def validate_for_sign!
308
+ validate_utxo!
309
+ validate_redeem_script!
310
+ validate_sighash_type!
311
+ end
312
+
313
+ # Check that +algo+ does not conflict with the scheme of the signatures already collected.
314
+ # Tapyrus does not allow mixing ECDSA and Schnorr signatures within a single OP_CHECKMULTISIG.
315
+ # @param [Symbol] algo :ecdsa or :schnorr.
316
+ # @raise [Tapyrus::PSTT::Error] if the schemes conflict.
317
+ def validate_signature_algo!(algo)
318
+ return if partial_sigs.empty?
319
+ current = partial_sigs.values.first.bytesize == 65 ? :schnorr : :ecdsa
320
+ unless current == algo
321
+ raise Error, "This input already has #{current} signatures. #{algo} signatures must not be mixed."
322
+ end
323
+ end
324
+
325
+ # Whether the given signature commits to the sequence number of every input.
326
+ # A SIGHASH_ALL signature without SIGHASH_ANYONECANPAY does.
327
+ # @param [String] sig a signature with binary format.
328
+ # @return [Boolean]
329
+ def self.commits_to_all_sequences?(sig)
330
+ return false if sig.nil? || sig.empty?
331
+ hash_type = sig[-1].unpack1("C")
332
+ (hash_type & SIGHASH_TYPE[:anyonecanpay]).zero? && (hash_type & 0x1f) == SIGHASH_TYPE[:all]
333
+ end
334
+
335
+ # Assemble the complete scriptSig from the collected records and store it in PSTT_IN_FINAL_SCRIPTSIG.
336
+ # The records which are no longer needed are removed.
337
+ # @param [Hash] verified_sigs the signatures which verify against the transaction, keyed by
338
+ # public key with hex format. Deciding that needs the whole transaction, which an input
339
+ # map does not hold, so Tapyrus::PSTT::Tx#finalize! computes it and passes it here.
340
+ # @return [Tapyrus::PSTT::Input] self.
341
+ # @raise [Tapyrus::PSTT::Error] if the verified signatures do not satisfy the script being spent.
342
+ def finalize!(verified_sigs)
343
+ return self if finalized?
344
+ validate_for_sign!
345
+ self.final_script_sig = build_final_script_sig(verified_sigs)
346
+ partial_sigs.clear
347
+ bip32_derivations.clear
348
+ ripemd160_preimages.clear
349
+ sha256_preimages.clear
350
+ hash160_preimages.clear
351
+ hash256_preimages.clear
352
+ self.sighash_type = nil
353
+ self.redeem_script = nil
354
+ self
355
+ end
356
+
357
+ # @param [Integer] type a preimage key type.
358
+ # @return [Hash] the preimages of the type.
359
+ def preimages_for(type)
360
+ case type
361
+ when InputTypes::RIPEMD160
362
+ ripemd160_preimages
363
+ when InputTypes::SHA256
364
+ sha256_preimages
365
+ when InputTypes::HASH160
366
+ hash160_preimages
367
+ when InputTypes::HASH256
368
+ hash256_preimages
369
+ end
370
+ end
371
+
372
+ def ==(other)
373
+ other.is_a?(Input) && to_payload == other.to_payload
374
+ end
375
+
376
+ private
377
+
378
+ def unknown_record(key, value)
379
+ buf = StringIO.new(key)
380
+ type = PSTT.read_compact_size(buf)
381
+ KeyValue.new(type, buf.read || "".b, value)
382
+ end
383
+
384
+ # Build the complete scriptSig for the script being spent.
385
+ def build_final_script_sig(verified_sigs)
386
+ script = spent_script_pubkey
387
+ p2sh = script.p2sh? || script.cp2sh?
388
+ target = p2sh ? redeem_script : script
389
+ raise Error, "PSTT_IN_REDEEM_SCRIPT is required to finalize a P2SH or CP2SH input." if p2sh && !target
390
+ script_sig =
391
+ if target.p2pkh? || target.cp2pkh?
392
+ build_p2pkh_script_sig(target, verified_sigs)
393
+ elsif target.multisig?
394
+ build_multisig_script_sig(target, verified_sigs)
395
+ else
396
+ raise Error, "Unsupported script type. The scriptSig must be assembled by the application."
397
+ end
398
+ script_sig << redeem_script.to_payload if p2sh
399
+ script_sig
400
+ end
401
+
402
+ def build_p2pkh_script_sig(target, verified_sigs)
403
+ pubkey_hash = target.chunks[target.cp2pkh? ? 4 : 2].pushed_data.bth
404
+ pubkey, sig = verified_sigs.find { |key, _| Tapyrus.hash160(key) == pubkey_hash }
405
+ raise Error, "No valid signature for the public key committed in the scriptPubKey." unless sig
406
+ Tapyrus::Script.new << sig << pubkey.htb
407
+ end
408
+
409
+ def build_multisig_script_sig(target, verified_sigs)
410
+ threshold = Tapyrus::Opcodes.opcode_to_small_int(target.chunks[0].opcode)
411
+ sigs = target.get_multisig_pubkeys.map { |pubkey| verified_sigs[pubkey.bth] }.compact
412
+ if sigs.size < threshold
413
+ raise Error, "#{threshold} signatures are required, but only #{sigs.size} of the collected ones are valid."
414
+ end
415
+ # OP_0 is the dummy element OP_CHECKMULTISIG pops off the stack.
416
+ script_sig = Tapyrus::Script.new << Tapyrus::Opcodes::OP_0
417
+ sigs.take(threshold).each { |sig| script_sig << sig }
418
+ script_sig
419
+ end
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,63 @@
1
+ module Tapyrus
2
+ module PSTT
3
+ # The value of a BIP 32 derivation record: a master key fingerprint followed by a derivation path.
4
+ class KeyOriginInfo
5
+ include Tapyrus::KeyPath
6
+ extend Tapyrus::KeyPath
7
+
8
+ # @!attribute [r] fingerprint
9
+ # @return [String] the master key fingerprint with hex format.
10
+ attr_reader :fingerprint
11
+
12
+ # @!attribute [r] key_paths
13
+ # @return [Array[Integer]] the derivation path as a sequence of 32-bit unsigned integers.
14
+ attr_reader :key_paths
15
+
16
+ # @param [String] fingerprint the master key fingerprint with hex format.
17
+ # @param [Array[Integer]] key_paths the derivation path.
18
+ def initialize(fingerprint:, key_paths: [])
19
+ raise Error, "fingerprint must be 4 bytes." unless fingerprint.htb.bytesize == 4
20
+ # Each element occupies 4 bytes of the record. Array#pack truncates a larger value to its
21
+ # lowest 32 bits without raising, which would turn a path this object was built with into
22
+ # a different one once it is serialized.
23
+ unless key_paths.all? { |index| index.is_a?(Integer) && index >= 0 && index <= 0xffffffff }
24
+ raise Error, "Each derivation path element must be a 32-bit unsigned integer."
25
+ end
26
+ @fingerprint = fingerprint
27
+ @key_paths = key_paths
28
+ end
29
+
30
+ # Parse the value of a BIP 32 derivation record.
31
+ # @param [String] payload the value with binary format.
32
+ # @return [Tapyrus::PSTT::KeyOriginInfo]
33
+ def self.parse_from_payload(payload)
34
+ if payload.bytesize < 4 || ((payload.bytesize - 4) % 4) != 0
35
+ raise Error, "BIP 32 derivation value must be a 4-byte fingerprint followed by 32-bit path elements."
36
+ end
37
+ fingerprint, *key_paths = payload.unpack("H8V*")
38
+ new(fingerprint: fingerprint, key_paths: key_paths)
39
+ end
40
+
41
+ # Build from a derivation path string such as "m/44'/2377'/0'/0/0".
42
+ # @param [String] fingerprint the master key fingerprint with hex format.
43
+ # @param [String] path the derivation path string.
44
+ # @return [Tapyrus::PSTT::KeyOriginInfo]
45
+ def self.from_path(fingerprint, path)
46
+ new(fingerprint: fingerprint, key_paths: parse_key_path(path))
47
+ end
48
+
49
+ def to_payload
50
+ fingerprint.htb + key_paths.pack("V*")
51
+ end
52
+
53
+ # @return [String] the derivation path string.
54
+ def path
55
+ to_key_path(key_paths)
56
+ end
57
+
58
+ def ==(other)
59
+ other.is_a?(KeyOriginInfo) && to_payload == other.to_payload
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,129 @@
1
+ module Tapyrus
2
+ module PSTT
3
+ # An output map of a PSTT.
4
+ class Output
5
+ # @!attribute [rw] amount
6
+ # @return [Integer] the amount of this output. Tokens for a colored output, otherwise tapy.
7
+ attr_accessor :amount
8
+
9
+ # @!attribute [rw] script_pubkey
10
+ # @return [Tapyrus::Script] the scriptPubKey of this output.
11
+ attr_accessor :script_pubkey
12
+
13
+ # @!attribute [rw] redeem_script
14
+ # @return [Tapyrus::Script] the redeem script, if this output is P2SH or CP2SH.
15
+ attr_accessor :redeem_script
16
+
17
+ # @!attribute [r] bip32_derivations
18
+ # @return [Hash] Tapyrus::PSTT::KeyOriginInfo, keyed by public key with hex format.
19
+ attr_reader :bip32_derivations
20
+
21
+ # @!attribute [r] proprietaries
22
+ # @return [Array[Tapyrus::PSTT::Proprietary]] the proprietary records.
23
+ attr_reader :proprietaries
24
+
25
+ # @!attribute [r] unknowns
26
+ # @return [Hash] the records whose key type this implementation does not know, keyed by complete key.
27
+ attr_reader :unknowns
28
+
29
+ # @!attribute [rw] record_order
30
+ # @return [Array[String]] the complete keys of the map this output was parsed from. See
31
+ # Tapyrus::PSTT.order_records.
32
+ attr_accessor :record_order
33
+
34
+ def initialize(amount: nil, script_pubkey: nil, redeem_script: nil)
35
+ @amount = amount
36
+ @script_pubkey = script_pubkey
37
+ @redeem_script = redeem_script
38
+ @bip32_derivations = {}
39
+ @proprietaries = []
40
+ @unknowns = {}
41
+ @record_order = []
42
+ end
43
+
44
+ # Build an output map from the records of an output map.
45
+ # @param [Array[Tapyrus::PSTT::KeyValue]] records the records.
46
+ # @return [Tapyrus::PSTT::Output]
47
+ # @raise [Tapyrus::PSTT::Error] if a required field is missing or a field is malformed.
48
+ def self.parse_from_records(records)
49
+ output = new
50
+ records.each do |record|
51
+ if OutputTypes::RESERVED.include?(record.type)
52
+ raise Error, format("Output key type 0x%02x is reserved.", record.type)
53
+ end
54
+ case record.type
55
+ when OutputTypes::REDEEM_SCRIPT
56
+ PSTT.validate_empty_keydata!(record)
57
+ output.redeem_script =
58
+ PSTT.parse_field("PSTT_OUT_REDEEM_SCRIPT") { Tapyrus::Script.parse_from_payload(record.value) }
59
+ when OutputTypes::BIP32_DERIVATION
60
+ PSTT.validate_pubkey_keydata!(record)
61
+ output.bip32_derivations[record.keydata.bth] = KeyOriginInfo.parse_from_payload(record.value)
62
+ when OutputTypes::AMOUNT
63
+ PSTT.validate_empty_keydata!(record)
64
+ output.amount = PSTT.read_i64(record)
65
+ PSTT.validate_amount!(output.amount)
66
+ when OutputTypes::SCRIPT
67
+ PSTT.validate_empty_keydata!(record)
68
+ output.script_pubkey =
69
+ PSTT.parse_field("PSTT_OUT_SCRIPT") { Tapyrus::Script.parse_from_payload(record.value) }
70
+ when OutputTypes::PROPRIETARY
71
+ output.proprietaries << Proprietary.parse_from_record(record)
72
+ else
73
+ output.unknowns[record.key] = record.value
74
+ end
75
+ end
76
+ raise Error, "PSTT_OUT_AMOUNT is required." unless output.amount
77
+ raise Error, "PSTT_OUT_SCRIPT is required." unless output.script_pubkey
78
+ output.record_order = records.map(&:key)
79
+ output
80
+ end
81
+
82
+ # @return [Array[Tapyrus::PSTT::KeyValue]] the records of this output map.
83
+ def to_records
84
+ raise Error, "PSTT_OUT_AMOUNT is required." unless amount
85
+ raise Error, "PSTT_OUT_SCRIPT is required." unless script_pubkey
86
+ PSTT.validate_amount!(amount)
87
+ records = []
88
+ records << KeyValue.new(OutputTypes::REDEEM_SCRIPT, "".b, redeem_script.to_payload) if redeem_script
89
+ bip32_derivations.each do |pubkey, info|
90
+ records << KeyValue.new(OutputTypes::BIP32_DERIVATION, pubkey.htb, info.to_payload)
91
+ end
92
+ records << KeyValue.new(OutputTypes::AMOUNT, "".b, [amount].pack("q<"))
93
+ records << KeyValue.new(OutputTypes::SCRIPT, "".b, script_pubkey.to_payload)
94
+ proprietaries.each { |p| records << p.to_record(OutputTypes::PROPRIETARY) }
95
+ unknowns.each do |key, value|
96
+ buf = StringIO.new(key)
97
+ type = PSTT.read_compact_size(buf)
98
+ records << KeyValue.new(type, buf.read || "".b, value)
99
+ end
100
+ records
101
+ end
102
+
103
+ def to_payload
104
+ PSTT.serialize_map(to_records, record_order)
105
+ end
106
+
107
+ # @return [Tapyrus::TxOut] the transaction output this map represents.
108
+ # @raise [Tapyrus::PSTT::Error] if the amount is out of range.
109
+ def to_tx_out
110
+ PSTT.validate_amount!(amount)
111
+ Tapyrus::TxOut.new(value: amount, script_pubkey: script_pubkey)
112
+ end
113
+
114
+ # @return [Boolean] whether this output holds Colored Coins.
115
+ def colored?
116
+ script_pubkey.colored?
117
+ end
118
+
119
+ # @return [Tapyrus::Color::ColorIdentifier] the color of this output, or nil for TPC.
120
+ def color_id
121
+ script_pubkey.color_id
122
+ end
123
+
124
+ def ==(other)
125
+ other.is_a?(Output) && to_payload == other.to_payload
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,54 @@
1
+ module Tapyrus
2
+ module PSTT
3
+ # A proprietary record, for applications that need non-standardized fields.
4
+ class Proprietary
5
+ # @!attribute [r] identifier
6
+ # @return [String] the identifier of the proprietary type user with binary format.
7
+ attr_reader :identifier
8
+
9
+ # @!attribute [r] subtype
10
+ # @return [Integer] the subtype defined by the proprietary type user.
11
+ attr_reader :subtype
12
+
13
+ # @!attribute [r] subkeydata
14
+ # @return [String] the sub key data with binary format.
15
+ attr_reader :subkeydata
16
+
17
+ # @!attribute [rw] value
18
+ # @return [String] the value data with binary format.
19
+ attr_accessor :value
20
+
21
+ def initialize(identifier:, subtype:, subkeydata: "".b, value: "".b)
22
+ @identifier = identifier
23
+ @subtype = subtype
24
+ @subkeydata = subkeydata
25
+ @value = value
26
+ end
27
+
28
+ # Parse a proprietary record.
29
+ # @param [Tapyrus::PSTT::KeyValue] record a record whose key type is 0xFC.
30
+ # @return [Tapyrus::PSTT::Proprietary]
31
+ def self.parse_from_record(record)
32
+ buf = StringIO.new(record.keydata)
33
+ identifier = PSTT.read_bytes(buf, PSTT.read_compact_size(buf))
34
+ subtype = PSTT.read_compact_size(buf)
35
+ new(identifier: identifier, subtype: subtype, subkeydata: buf.read || "".b, value: record.value)
36
+ end
37
+
38
+ # @return [String] the key data of this record with binary format.
39
+ def to_keydata
40
+ Tapyrus.pack_var_string(identifier) + Tapyrus.pack_var_int(subtype) + subkeydata
41
+ end
42
+
43
+ # @param [Integer] type the key type of the proprietary field in the map this record belongs to.
44
+ # @return [Tapyrus::PSTT::KeyValue]
45
+ def to_record(type)
46
+ KeyValue.new(type, to_keydata, value)
47
+ end
48
+
49
+ def ==(other)
50
+ other.is_a?(Proprietary) && to_keydata == other.to_keydata && value == other.value
51
+ end
52
+ end
53
+ end
54
+ end