Class: Tapyrus::PSTT::Tx
- Inherits:
-
Object
- Object
- Tapyrus::PSTT::Tx
- 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
-
#fallback_locktime ⇒ Object
Returns the value of attribute fallback_locktime.
-
#features ⇒ Object
Returns the value of attribute features.
-
#global_record_order ⇒ Array[String]
The complete keys of the global map this PSTT was parsed from.
-
#inputs ⇒ Object
readonly
Returns the value of attribute inputs.
-
#outputs ⇒ Object
readonly
Returns the value of attribute outputs.
-
#proprietaries ⇒ Object
readonly
Returns the value of attribute proprietaries.
-
#tx_modifiable ⇒ Object
Returns the value of attribute tx_modifiable.
-
#unknowns ⇒ Object
readonly
Returns the value of attribute unknowns.
-
#version ⇒ Integer
The version number of this PSTT.
-
#xpubs ⇒ Object
readonly
Returns the value of attribute xpubs.
Class Method Summary collapse
-
.from_base64(base64) ⇒ Tapyrus::PSTT::Tx
Parse a PSTT from its Base64 format.
-
.parse_from_payload(payload) ⇒ Tapyrus::PSTT::Tx
Parse a PSTT from its raw binary format.
Instance Method Summary collapse
- #==(other) ⇒ Object
-
#add_input(input) ⇒ Tapyrus::PSTT::Tx
Add an input.
-
#add_output(output) ⇒ Tapyrus::PSTT::Tx
Add an output.
-
#add_pair(input, output) ⇒ Tapyrus::PSTT::Tx
Add an input and an output at matching positions after the existing ones.
-
#build_tx(sequence: nil, final: false) ⇒ Tapyrus::Tx
Build the transaction determined by the fields of this PSTT.
-
#combine(other) ⇒ Tapyrus::PSTT::Tx
Merge another PSTT which has the same identifier into this one.
-
#extract_tx ⇒ Tapyrus::Tx
Build the final transaction.
-
#fee ⇒ Integer
The TPC fee this transaction pays.
-
#finalize! ⇒ Tapyrus::PSTT::Tx
Assemble the complete scriptSig of every input.
-
#finalized? ⇒ Boolean
Whether every input has been finalized.
-
#finish_construction! ⇒ Tapyrus::PSTT::Tx
Declare construction finished by clearing the Inputs Modifiable and Outputs Modifiable flags.
-
#global_records ⇒ Array[Tapyrus::PSTT::KeyValue]
The records of the global map.
-
#has_sighash_single? ⇒ Boolean
Whether this PSTT contains a signature made with SIGHASH_SINGLE.
-
#identification_txid ⇒ String
The identifier of this PSTT: the txid of the transaction built from its fields with the sequence number of every input set to 0.
-
#initialize(features: 1, fallback_locktime: nil, tx_modifiable: nil, version: VERSION, inputs: [], outputs: []) ⇒ Tx
constructor
Create a new PSTT.
-
#input_amounts ⇒ Hash
The amounts being spent, per color.
-
#inputs_modifiable? ⇒ Boolean
Whether inputs may still be added.
-
#locktime ⇒ Integer
The locktime of the transaction, computed as Determining the Locktime describes.
-
#output_amounts ⇒ Hash
The amounts being created, per color.
-
#outputs_modifiable? ⇒ Boolean
Whether outputs may still be added.
-
#script_code(index) ⇒ Tapyrus::Script
The scriptCode used to compute the signature hash of an input.
-
#set_sequence(index, sequence) ⇒ Tapyrus::PSTT::Tx
Set the sequence number of an input.
-
#sighash_for_input(index, sighash_type: nil) ⇒ String
Compute the signature hash of an input.
-
#sign(index, key, sighash_type: nil, algo: :ecdsa, low_r: true) ⇒ String
Sign an input.
-
#signed? ⇒ Boolean
Whether any input carries a signature.
-
#to_base64 ⇒ String
This PSTT with Base64 format, which is used for display and text transport.
- #to_payload ⇒ Object
-
#verified_partial_sigs(index) ⇒ Hash
The signatures of an input which verify against the transaction this PSTT describes.
Methods included from HexConverter
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.
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_locktime ⇒ Object
Returns the value of attribute fallback_locktime.
18 19 20 |
# File 'lib/tapyrus/pstt/tx.rb', line 18 def fallback_locktime @fallback_locktime end |
#features ⇒ Object
Returns the value of attribute features.
14 15 16 |
# File 'lib/tapyrus/pstt/tx.rb', line 14 def features @features end |
#global_record_order ⇒ Array[String]
Returns 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 |
#inputs ⇒ Object (readonly)
Returns the value of attribute inputs.
30 31 32 |
# File 'lib/tapyrus/pstt/tx.rb', line 30 def inputs @inputs end |
#outputs ⇒ Object (readonly)
Returns the value of attribute outputs.
34 35 36 |
# File 'lib/tapyrus/pstt/tx.rb', line 34 def outputs @outputs end |
#proprietaries ⇒ Object (readonly)
Returns the value of attribute proprietaries.
42 43 44 |
# File 'lib/tapyrus/pstt/tx.rb', line 42 def proprietaries @proprietaries end |
#tx_modifiable ⇒ Object
Returns the value of attribute tx_modifiable.
22 23 24 |
# File 'lib/tapyrus/pstt/tx.rb', line 22 def tx_modifiable @tx_modifiable end |
#unknowns ⇒ Object (readonly)
Returns the value of attribute unknowns.
46 47 48 |
# File 'lib/tapyrus/pstt/tx.rb', line 46 def unknowns @unknowns end |
#version ⇒ Integer
Returns the version number of this PSTT.
26 27 28 |
# File 'lib/tapyrus/pstt/tx.rb', line 26 def version @version end |
#xpubs ⇒ Object (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.
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.
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.
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.
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.
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.
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.
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_tx ⇒ Tapyrus::Tx
Build the final transaction. This is the Transaction Extractor role.
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 |
#fee ⇒ Integer
The TPC fee this transaction pays. Requires PSTT_IN_UTXO on every input.
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.
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.
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.
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_records ⇒ Array[Tapyrus::PSTT::KeyValue]
Returns the records of the global map.
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.
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_txid ⇒ String
The identifier of this PSTT: the txid of the transaction built from its fields with the sequence number of every input set to 0.
231 232 233 |
# File 'lib/tapyrus/pstt/tx.rb', line 231 def identification_txid build_tx(sequence: 0).txid end |
#input_amounts ⇒ Hash
The amounts being spent, per color. Requires PSTT_IN_UTXO on every input.
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.
236 237 238 |
# File 'lib/tapyrus/pstt/tx.rb', line 236 def inputs_modifiable? !(tx_modifiable.to_i & TxModifiable::INPUTS).zero? end |
#locktime ⇒ Integer
The locktime of the transaction, computed as Determining the Locktime describes.
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_amounts ⇒ Hash
The amounts being created, per color.
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.
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.
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.
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.
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.
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.
254 255 256 |
# File 'lib/tapyrus/pstt/tx.rb', line 254 def signed? inputs.any? { |input| input.signed? || input.finalized? } end |
#to_base64 ⇒ String
Returns 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_payload ⇒ Object
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.
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 |