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,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
|