Class: Tapyrus::PSTT::Tx

Inherits:
Object
  • Object
show all
Includes:
HexConverter
Defined in:
lib/tapyrus/pstt/tx.rb

Overview

A Partially Signed Tapyrus Transaction.

The methods are grouped by the roles TIP-0174 defines: Creator(::new), Constructor(#add_input, #add_output, #add_pair, #finish_construction!), Updater(#update_input, #set_sequence), Signer(#sign), Combiner(#combine), Input Finalizer(#finalize!) and Transaction Extractor(#extract_tx).

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HexConverter

#to_hex

Constructor Details

#initialize(features: 1, fallback_locktime: nil, tx_modifiable: nil, version: VERSION, inputs: [], outputs: []) ⇒ Tx

Create a new PSTT. This is the Creator role.

Parameters:

  • features (Integer) (defaults to: 1)

    the features field of the transaction.

  • tx_modifiable (Integer) (defaults to: nil)

    the initial value of PSTT_GLOBAL_TX_MODIFIABLE. Set Tapyrus::PSTT::TxModifiable::INPUTS and/or OUTPUTS if other parties will add inputs or outputs. nil fixes the transaction at creation.

  • inputs (Array[Tapyrus::PSTT::Input]) (defaults to: [])

    the inputs the Creator creates.

  • outputs (Array[Tapyrus::PSTT::Output]) (defaults to: [])

    the outputs the Creator creates.



60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/tapyrus/pstt/tx.rb', line 60

def initialize(features: 1, fallback_locktime: nil, tx_modifiable: nil, version: VERSION, inputs: [], outputs: [])
  @features = features
  @fallback_locktime = fallback_locktime
  @tx_modifiable = tx_modifiable
  @version = version
  @inputs = inputs
  @outputs = outputs
  @xpubs = {}
  @proprietaries = []
  @unknowns = {}
  @global_record_order = []
end

Instance Attribute Details

#fallback_locktimeObject

Returns the value of attribute fallback_locktime.



18
19
20
# File 'lib/tapyrus/pstt/tx.rb', line 18

def fallback_locktime
  @fallback_locktime
end

#featuresObject

Returns the value of attribute features.



14
15
16
# File 'lib/tapyrus/pstt/tx.rb', line 14

def features
  @features
end

#global_record_orderArray[String]

Returns the complete keys of the global map this PSTT was parsed from. See Tapyrus::PSTT.order_records.

Returns:

  • (Array[String])

    the complete keys of the global map this PSTT was parsed from. See Tapyrus::PSTT.order_records.



51
52
53
# File 'lib/tapyrus/pstt/tx.rb', line 51

def global_record_order
  @global_record_order
end

#inputsObject (readonly)

Returns the value of attribute inputs.



30
31
32
# File 'lib/tapyrus/pstt/tx.rb', line 30

def inputs
  @inputs
end

#outputsObject (readonly)

Returns the value of attribute outputs.



34
35
36
# File 'lib/tapyrus/pstt/tx.rb', line 34

def outputs
  @outputs
end

#proprietariesObject (readonly)

Returns the value of attribute proprietaries.



42
43
44
# File 'lib/tapyrus/pstt/tx.rb', line 42

def proprietaries
  @proprietaries
end

#tx_modifiableObject

Returns the value of attribute tx_modifiable.



22
23
24
# File 'lib/tapyrus/pstt/tx.rb', line 22

def tx_modifiable
  @tx_modifiable
end

#unknownsObject (readonly)

Returns the value of attribute unknowns.



46
47
48
# File 'lib/tapyrus/pstt/tx.rb', line 46

def unknowns
  @unknowns
end

#versionInteger

Returns the version number of this PSTT.

Returns:

  • (Integer)

    the version number of this PSTT.



26
27
28
# File 'lib/tapyrus/pstt/tx.rb', line 26

def version
  @version
end

#xpubsObject (readonly)

Returns the value of attribute xpubs.



38
39
40
# File 'lib/tapyrus/pstt/tx.rb', line 38

def xpubs
  @xpubs
end

Class Method Details

.from_base64(base64) ⇒ Tapyrus::PSTT::Tx

Parse a PSTT from its Base64 format.

Parameters:

  • base64 (String)

    a PSTT with Base64 format.

Returns:



138
139
140
141
142
143
144
# File 'lib/tapyrus/pstt/tx.rb', line 138

def self.from_base64(base64)
  payload = base64.unpack1("m0")
  raise Error, "Invalid Base64 encoding." if payload.nil?
  parse_from_payload(payload)
rescue ArgumentError
  raise Error, "Invalid Base64 encoding."
end

.parse_from_payload(payload) ⇒ Tapyrus::PSTT::Tx

Parse a PSTT from its raw binary format.

Parameters:

  • payload (String)

    a PSTT with binary format.

Returns:

Raises:



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tapyrus/pstt/tx.rb', line 114

def self.parse_from_payload(payload)
  buf = payload.is_a?(String) ? StringIO.new(payload) : payload
  unless PSTT.read_bytes(buf, MAGIC.bytesize) == MAGIC
    raise Error, "Invalid PSTT magic. The payload does not begin with 'pstt' 0xFF."
  end
  # Every map is read before any of them is interpreted, so that the declared counts are
  # compared against the number of maps which is actually there. Assigning maps to inputs
  # and outputs by the declared counts alone would let a wrong count hand an output map to
  # the input parser, which reports whichever field of the misread map fails first.
  maps = []
  maps << PSTT.parse_map(buf) until buf.eof?
  raise Error, "The PSTT global map is missing." if maps.empty?
  pstt, input_count, output_count = parse_global_map(maps.shift)
  unless maps.size == input_count + output_count
    raise Error, "The number of maps does not match PSTT_GLOBAL_INPUT_COUNT and PSTT_GLOBAL_OUTPUT_COUNT."
  end
  maps.shift(input_count).each { |records| pstt.inputs << Input.parse_from_records(records) }
  maps.each { |records| pstt.outputs << Output.parse_from_records(records) }
  pstt
end

Instance Method Details

#==(other) ⇒ Object



480
481
482
# File 'lib/tapyrus/pstt/tx.rb', line 480

def ==(other)
  other.is_a?(Tx) && to_payload == other.to_payload
end

#add_input(input) ⇒ Tapyrus::PSTT::Tx

Add an input. This is the Constructor role.

Parameters:

Returns:

Raises:



262
263
264
265
266
267
268
269
270
271
272
# File 'lib/tapyrus/pstt/tx.rb', line 262

def add_input(input)
  raise Error, "Inputs are not modifiable." unless inputs_modifiable?
  if has_sighash_single?
    raise Error, "This PSTT contains a SIGHASH_SINGLE signature. Use #add_pair to keep inputs and outputs paired."
  end
  keeping_locktime("The added input") do
    inputs << input
    -> { inputs.pop }
  end
  self
end

#add_output(output) ⇒ Tapyrus::PSTT::Tx

Add an output. This is the Constructor role.

Parameters:

Returns:

Raises:



278
279
280
281
282
283
284
285
# File 'lib/tapyrus/pstt/tx.rb', line 278

def add_output(output)
  raise Error, "Outputs are not modifiable." unless outputs_modifiable?
  if has_sighash_single?
    raise Error, "This PSTT contains a SIGHASH_SINGLE signature. Use #add_pair to keep inputs and outputs paired."
  end
  outputs << output
  self
end

#add_pair(input, output) ⇒ Tapyrus::PSTT::Tx

Add an input and an output at matching positions after the existing ones. A PSTT which contains a SIGHASH_SINGLE signature must preserve that correspondence.

Parameters:

Returns:

Raises:



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/tapyrus/pstt/tx.rb', line 293

def add_pair(input, output)
  raise Error, "Inputs are not modifiable." unless inputs_modifiable?
  raise Error, "Outputs are not modifiable." unless outputs_modifiable?
  # Appending never moves an existing input or output, so the only thing to enforce is that
  # the added input has an output at its own position.
  if inputs.size > outputs.size
    raise Error, "The added input would have no corresponding output at a matching position."
  end
  keeping_locktime("The added input") do
    inputs << input
    -> { inputs.pop }
  end
  outputs << output
  self
end

#build_tx(sequence: nil, final: false) ⇒ Tapyrus::Tx

Build the transaction determined by the fields of this PSTT.

Parameters:

  • sequence (Integer) (defaults to: nil)

    override the sequence number of every input.

  • final (Boolean) (defaults to: false)

    whether to attach PSTT_IN_FINAL_SCRIPTSIG to each input.

Returns:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/tapyrus/pstt/tx.rb', line 212

def build_tx(sequence: nil, final: false)
  tx = Tapyrus::Tx.new
  tx.features = features
  tx.lock_time = locktime
  inputs.each do |input|
    script_sig = final ? input.final_script_sig : Tapyrus::Script.new
    tx.inputs << Tapyrus::TxIn.new(
      out_point: input.out_point,
      script_sig: script_sig,
      sequence: sequence || input.final_sequence
    )
  end
  outputs.each { |output| tx.outputs << output.to_tx_out }
  tx
end

#combine(other) ⇒ Tapyrus::PSTT::Tx

Merge another PSTT which has the same identifier into this one. This is the Combiner role.

Parameters:

Returns:

Raises:



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/tapyrus/pstt/tx.rb', line 387

def combine(other)
  unless identification_txid == other.identification_txid
    raise Error, "PSTTs with different identifiers must not be combined."
  end
  validate_sequences_agree!(other)
  modifiable = merge_tx_modifiable(tx_modifiable, other.tx_modifiable)
  payload =
    MAGIC + PSTT.serialize_map(merge_records(global_records, other.global_records), global_record_order) +
      inputs
        .zip(other.inputs)
        .map { |a, b| PSTT.serialize_map(merge_records(a.to_records, b.to_records), a.record_order) }
        .join +
      outputs
        .zip(other.outputs)
        .map { |a, b| PSTT.serialize_map(merge_records(a.to_records, b.to_records), a.record_order) }
        .join
  combined = self.class.parse_from_payload(payload)
  combined.tx_modifiable = modifiable
  combined
end

#extract_txTapyrus::Tx

Build the final transaction. This is the Transaction Extractor role.

Returns:

  • (Tapyrus::Tx)

    the transaction in Tapyrus network serialization.

Raises:



447
448
449
450
451
452
453
# File 'lib/tapyrus/pstt/tx.rb', line 447

def extract_tx
  raise Error, "This PSTT has no inputs. A transaction must spend at least one output." if inputs.empty?
  inputs.each_with_index do |input, index|
    raise Error, "PSTT_IN_FINAL_SCRIPTSIG for input #{index} is missing." unless input.finalized?
  end
  build_tx(final: true)
end

#feeInteger

The TPC fee this transaction pays. Requires PSTT_IN_UTXO on every input.

Returns:



475
476
477
478
# File 'lib/tapyrus/pstt/tx.rb', line 475

def fee
  tpc = Tapyrus::Color::ColorIdentifier.default
  input_amounts[tpc] - output_amounts[tpc]
end

#finalize!Tapyrus::PSTT::Tx

Assemble the complete scriptSig of every input. This is the Input Finalizer role.

Returns:

Raises:



411
412
413
414
415
416
417
418
# File 'lib/tapyrus/pstt/tx.rb', line 411

def finalize!
  raise Error, "This PSTT has no inputs to finalize." if inputs.empty?
  if inputs_modifiable? || outputs_modifiable?
    raise Error, "A PSTT must not be finalized while it is still modifiable."
  end
  inputs.each_with_index { |input, index| input.finalize!(verified_partial_sigs(index)) }
  self
end

#finalized?Boolean

Returns whether every input has been finalized.

Returns:

  • (Boolean)

    whether every input has been finalized.



440
441
442
# File 'lib/tapyrus/pstt/tx.rb', line 440

def finalized?
  !inputs.empty? && inputs.all?(&:finalized?)
end

#finish_construction!Tapyrus::PSTT::Tx

Declare construction finished by clearing the Inputs Modifiable and Outputs Modifiable flags.

Returns:



311
312
313
314
# File 'lib/tapyrus/pstt/tx.rb', line 311

def finish_construction!
  self.tx_modifiable = tx_modifiable.to_i & ~(TxModifiable::INPUTS | TxModifiable::OUTPUTS)
  self
end

#global_recordsArray[Tapyrus::PSTT::KeyValue]

Returns the records of the global map.

Returns:

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/tapyrus/pstt/tx.rb', line 157

def global_records
  raise Error, "PSTT_GLOBAL_TX_FEATURES is required." unless features
  PSTT.validate_tx_modifiable!(tx_modifiable) if tx_modifiable
  PSTT.validate_version!(version) if version
  records = []
  xpubs.each do |xpub, info|
    keydata = PSTT.parse_field("PSTT_GLOBAL_XPUB") { Tapyrus::ExtPubkey.from_base58(xpub).to_payload }
    records << KeyValue.new(GlobalTypes::XPUB, keydata, info.to_payload)
  end
  records << KeyValue.new(GlobalTypes::TX_FEATURES, "".b, [features].pack("l<"))
  if fallback_locktime
    records << KeyValue.new(GlobalTypes::FALLBACK_LOCKTIME, "".b, [fallback_locktime].pack("V"))
  end
  records << KeyValue.new(GlobalTypes::INPUT_COUNT, "".b, Tapyrus.pack_var_int(inputs.size))
  records << KeyValue.new(GlobalTypes::OUTPUT_COUNT, "".b, Tapyrus.pack_var_int(outputs.size))
  records << KeyValue.new(GlobalTypes::TX_MODIFIABLE, "".b, [tx_modifiable].pack("C")) if tx_modifiable
  records << KeyValue.new(GlobalTypes::VERSION, "".b, [version].pack("V")) if version && version > 0
  proprietaries.each { |p| records << p.to_record(GlobalTypes::PROPRIETARY) }
  unknowns.each do |key, value|
    buf = StringIO.new(key)
    type = PSTT.read_compact_size(buf)
    records << KeyValue.new(type, buf.read || "".b, value)
  end
  records
end

#has_sighash_single?Boolean

Returns whether this PSTT contains a signature made with SIGHASH_SINGLE.

Returns:

  • (Boolean)

    whether this PSTT contains a signature made with SIGHASH_SINGLE.



246
247
248
# File 'lib/tapyrus/pstt/tx.rb', line 246

def has_sighash_single?
  !(tx_modifiable.to_i & TxModifiable::HAS_SIGHASH_SINGLE).zero?
end

#identification_txidString

The identifier of this PSTT: the txid of the transaction built from its fields with the sequence number of every input set to 0.

Returns:

  • (String)

    the txid with hex format.



231
232
233
# File 'lib/tapyrus/pstt/tx.rb', line 231

def identification_txid
  build_tx(sequence: 0).txid
end

#input_amountsHash

The amounts being spent, per color. Requires PSTT_IN_UTXO on every input.

Returns:

  • (Hash)

    Tapyrus::Color::ColorIdentifier => amount. TPC uses the default color identifier.



457
458
459
460
461
462
463
# File 'lib/tapyrus/pstt/tx.rb', line 457

def input_amounts
  inputs.each_with_object(Hash.new(0)) do |input, totals|
    input.validate_utxo!
    out = input.utxo_output
    totals[out.color_id || Tapyrus::Color::ColorIdentifier.default] += out.value
  end
end

#inputs_modifiable?Boolean

Returns whether inputs may still be added.

Returns:

  • (Boolean)

    whether inputs may still be added.



236
237
238
# File 'lib/tapyrus/pstt/tx.rb', line 236

def inputs_modifiable?
  !(tx_modifiable.to_i & TxModifiable::INPUTS).zero?
end

#locktimeInteger

The locktime of the transaction, computed as Determining the Locktime describes.

Returns:

Raises:



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/tapyrus/pstt/tx.rb', line 186

def locktime
  time_locktimes = []
  height_locktimes = []
  only_time = false
  only_height = false
  inputs.each do |input|
    time = input.required_time_locktime
    height = input.required_height_locktime
    next unless time || height
    time_locktimes << time if time
    height_locktimes << height if height
    only_time = true if time && !height
    only_height = true if height && !time
  end
  return fallback_locktime || 0 if time_locktimes.empty? && height_locktimes.empty?
  if only_time && only_height
    raise Error, "No locktime kind is acceptable to every input which requires a locktime."
  end
  # If both kinds are acceptable, the height-based locktime is chosen.
  only_time ? time_locktimes.max : height_locktimes.max
end

#output_amountsHash

The amounts being created, per color.

Returns:

  • (Hash)

    Tapyrus::Color::ColorIdentifier => amount. TPC uses the default color identifier.



467
468
469
470
471
# File 'lib/tapyrus/pstt/tx.rb', line 467

def output_amounts
  outputs.each_with_object(Hash.new(0)) do |output, totals|
    totals[output.color_id || Tapyrus::Color::ColorIdentifier.default] += output.amount
  end
end

#outputs_modifiable?Boolean

Returns whether outputs may still be added.

Returns:

  • (Boolean)

    whether outputs may still be added.



241
242
243
# File 'lib/tapyrus/pstt/tx.rb', line 241

def outputs_modifiable?
  !(tx_modifiable.to_i & TxModifiable::OUTPUTS).zero?
end

#script_code(index) ⇒ Tapyrus::Script

The scriptCode used to compute the signature hash of an input.

Parameters:

  • index (Integer)

    the input index.

Returns:



342
343
344
# File 'lib/tapyrus/pstt/tx.rb', line 342

def script_code(index)
  input_at(index).script_code
end

#set_sequence(index, sequence) ⇒ Tapyrus::PSTT::Tx

Set the sequence number of an input. This is the Updater role.

Parameters:

  • index (Integer)

    the input index.

  • sequence (Integer)

    the sequence number.

Returns:

Raises:



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/tapyrus/pstt/tx.rb', line 321

def set_sequence(index, sequence)
  input = input_at(index)
  if input.signed? || input.finalized?
    raise Error, "Input #{index} is already signed. Its sequence number must not be changed."
  end
  # A finalized input no longer carries the sighash types of the signatures it was built
  # from, so which of them commit to which sequence number can no longer be decided. Any
  # finalized input therefore blocks every sequence change in the PSTT.
  if inputs.any?(&:finalized?)
    raise Error, "A finalized input carries signatures which may commit to the sequence number of every input."
  end
  if inputs.any? { |i| i.partial_sigs.values.any? { |sig| Input.commits_to_all_sequences?(sig) } }
    raise Error, "A SIGHASH_ALL signature commits to the sequence number of every input. It must not be changed."
  end
  input.sequence = sequence
  self
end

#sighash_for_input(index, sighash_type: nil) ⇒ String

Compute the signature hash of an input.

Parameters:

  • index (Integer)

    the input index.

  • sighash_type (Integer) (defaults to: nil)

    the sighash type. Defaults to PSTT_IN_SIGHASH_TYPE, or SIGHASH_ALL.

Returns:

  • (String)

    the signature hash with binary format.

Raises:



351
352
353
354
355
356
357
358
359
# File 'lib/tapyrus/pstt/tx.rb', line 351

def sighash_for_input(index, sighash_type: nil)
  input = input_at(index)
  hash_type = resolve_sighash_type(input, sighash_type)
  if (hash_type & 0x1f) == SIGHASH_TYPE[:single] && index >= outputs.size
    raise Error, "SIGHASH_SINGLE must not be used for an input which has no corresponding output."
  end
  input.validate_for_sign!
  build_tx.sighash_for_input(index, input.script_code, hash_type: hash_type)
end

#sign(index, key, sighash_type: nil, algo: :ecdsa, low_r: true) ⇒ String

Sign an input. This is the Signer role.

Parameters:

  • index (Integer)

    the input index.

  • key (Tapyrus::Key)

    the key to sign with.

  • sighash_type (Integer) (defaults to: nil)

    the sighash type. Defaults to PSTT_IN_SIGHASH_TYPE, or SIGHASH_ALL.

  • algo (Symbol) (defaults to: :ecdsa)

    the signature scheme. :ecdsa or :schnorr.

  • low_r (Boolean) (defaults to: true)

    whether to apply low-R grinding to an ECDSA signature.

Returns:

  • (String)

    the signature which was added, with binary format.

Raises:



369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/tapyrus/pstt/tx.rb', line 369

def sign(index, key, sighash_type: nil, algo: :ecdsa, low_r: true)
  input = input_at(index)
  # The Input Finalizer removed the PSTT_IN_PARTIAL_SIG records of this input, so a
  # signature added now would recreate a record the format says is no longer there.
  raise Error, "Input #{index} is finalized. It takes no further signature." if input.finalized?
  hash_type = resolve_sighash_type(input, sighash_type)
  input.validate_signature_algo!(algo)
  sighash = sighash_for_input(index, sighash_type: hash_type)
  sig = key.sign(sighash, low_r, nil, algo: algo) + [hash_type].pack("C")
  input.partial_sigs[key.pubkey] = sig
  update_tx_modifiable_after_sign(hash_type)
  sig
end

#signed?Boolean

Whether any input carries a signature. A finalized input counts as signed even though its PSTT_IN_PARTIAL_SIG records are gone: its signatures moved into PSTT_IN_FINAL_SCRIPTSIG, where they still commit to everything they committed to before.

Returns:

  • (Boolean)


254
255
256
# File 'lib/tapyrus/pstt/tx.rb', line 254

def signed?
  inputs.any? { |input| input.signed? || input.finalized? }
end

#to_base64String

Returns this PSTT with Base64 format, which is used for display and text transport.

Returns:

  • (String)

    this PSTT with Base64 format, which is used for display and text transport.



152
153
154
# File 'lib/tapyrus/pstt/tx.rb', line 152

def to_base64
  [to_payload].pack("m0")
end

#to_payloadObject



146
147
148
149
# File 'lib/tapyrus/pstt/tx.rb', line 146

def to_payload
  MAGIC + PSTT.serialize_map(global_records, global_record_order) + inputs.map(&:to_payload).join +
    outputs.map(&:to_payload).join
end

#verified_partial_sigs(index) ⇒ Hash

The signatures of an input which verify against the transaction this PSTT describes.

TIP-0174 has the Input Finalizer check that the collected records are sufficient to satisfy the script of the output being spent, and a signature which does not verify is not. It also matters which ones are dropped: the multisig scriptSig takes the first threshold public keys of the redeem script which carry a signature, so one bad signature among otherwise good ones would displace a good one and yield a scriptSig which fails validation, with the signatures needed to spend the output sitting unused in the same input.

Parameters:

  • index (Integer)

    the input index.

Returns:

  • (Hash)

    the signatures which verify, keyed by public key with hex format.



431
432
433
434
435
436
437
# File 'lib/tapyrus/pstt/tx.rb', line 431

def verified_partial_sigs(index)
  input = input_at(index)
  return {} if input.partial_sigs.empty?
  script_code = input.script_code
  tx = build_tx
  input.partial_sigs.select { |pubkey, sig| verified_sig?(tx, index, script_code, pubkey, sig) }
end