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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -2
- data/.ruby-version +1 -1
- data/README.md +43 -2
- data/lib/tapyrus/ext_key.rb +8 -4
- data/lib/tapyrus/key.rb +1 -1
- data/lib/tapyrus/key_path.rb +17 -13
- data/lib/tapyrus/message/fee_filter.rb +2 -2
- data/lib/tapyrus/message/header_and_short_ids.rb +4 -4
- data/lib/tapyrus/message/inventory.rb +1 -1
- data/lib/tapyrus/message/network_addr.rb +2 -2
- data/lib/tapyrus/message/ping.rb +2 -2
- data/lib/tapyrus/message/pong.rb +2 -2
- data/lib/tapyrus/message/send_cmpct.rb +2 -2
- data/lib/tapyrus/message/version.rb +3 -3
- data/lib/tapyrus/mnemonic.rb +9 -4
- data/lib/tapyrus/pstt/input.rb +422 -0
- data/lib/tapyrus/pstt/key_origin_info.rb +63 -0
- data/lib/tapyrus/pstt/output.rb +129 -0
- data/lib/tapyrus/pstt/proprietary.rb +54 -0
- data/lib/tapyrus/pstt/tx.rb +658 -0
- data/lib/tapyrus/pstt.rb +335 -0
- data/lib/tapyrus/rpc.rb +0 -2
- data/lib/tapyrus/script/script.rb +14 -4
- data/lib/tapyrus/secp256k1/rfc6979.rb +6 -3
- data/lib/tapyrus/tip0020.rb +341 -0
- data/lib/tapyrus/tx.rb +6 -2
- data/lib/tapyrus/tx_out.rb +2 -2
- data/lib/tapyrus/util.rb +3 -3
- data/lib/tapyrus/version.rb +1 -1
- data/lib/tapyrus/wallet/account.rb +2 -2
- data/lib/tapyrus/wallet/db.rb +3 -3
- data/lib/tapyrus/wallet/master_key.rb +2 -2
- data/lib/tapyrus.rb +3 -3
- data/tapyrusrb.gemspec +0 -4
- metadata +10 -81
- data/exe/tapyrusrb-cli +0 -5
- data/exe/tapyrusrbd +0 -42
- data/lib/tapyrus/network/connection.rb +0 -70
- data/lib/tapyrus/network/message_handler.rb +0 -244
- data/lib/tapyrus/network/peer.rb +0 -210
- data/lib/tapyrus/network/peer_discovery.rb +0 -43
- data/lib/tapyrus/network/pool.rb +0 -135
- data/lib/tapyrus/network.rb +0 -11
- data/lib/tapyrus/node/cli.rb +0 -114
- data/lib/tapyrus/node/configuration.rb +0 -36
- data/lib/tapyrus/node/spv.rb +0 -78
- data/lib/tapyrus/node.rb +0 -7
- data/lib/tapyrus/rpc/http_server.rb +0 -64
- 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
|