Class: Bitcoin::PSBT::Tx

Inherits:
Object
  • Object
show all
Includes:
HexConverter
Defined in:
lib/bitcoin/psbt/tx.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HexConverter

#to_hex

Constructor Details

#initialize(tx = nil) ⇒ Tx

Returns a new instance of Tx.



39
40
41
42
43
44
45
46
# File 'lib/bitcoin/psbt/tx.rb', line 39

def initialize(tx = nil)
  @tx = tx
  @xpubs = []
  @inputs = tx ? tx.in.map{Input.new}: []
  @outputs = tx ? tx.out.map{Output.new}: []
  @proprietaries = []
  @unknowns = {}
end

Instance Attribute Details

#inputsObject (readonly)

Returns the value of attribute inputs.



33
34
35
# File 'lib/bitcoin/psbt/tx.rb', line 33

def inputs
  @inputs
end

#outputsObject (readonly)

Returns the value of attribute outputs.



34
35
36
# File 'lib/bitcoin/psbt/tx.rb', line 34

def outputs
  @outputs
end

#proprietariesObject

Returns the value of attribute proprietaries.



35
36
37
# File 'lib/bitcoin/psbt/tx.rb', line 35

def proprietaries
  @proprietaries
end

#txObject

Returns the value of attribute tx.



31
32
33
# File 'lib/bitcoin/psbt/tx.rb', line 31

def tx
  @tx
end

#unknownsObject

Returns the value of attribute unknowns.



36
37
38
# File 'lib/bitcoin/psbt/tx.rb', line 36

def unknowns
  @unknowns
end

#version_numberObject

Returns the value of attribute version_number.



37
38
39
# File 'lib/bitcoin/psbt/tx.rb', line 37

def version_number
  @version_number
end

#xpubsObject

Returns the value of attribute xpubs.



32
33
34
# File 'lib/bitcoin/psbt/tx.rb', line 32

def xpubs
  @xpubs
end

Class Method Details

.parse_from_base64(base64) ⇒ Bitcoin::PartiallySignedTx

parse Partially Signed Bitcoin Transaction data with Base64 format.

Parameters:

  • base64 (String)

    a Partially Signed Bitcoin Transaction data with Base64 format.

Returns:

  • (Bitcoin::PartiallySignedTx)


51
52
53
# File 'lib/bitcoin/psbt/tx.rb', line 51

def self.parse_from_base64(base64)
  self.parse_from_payload(Base64.decode64(base64))
end

.parse_from_payload(payload) ⇒ Bitcoin::PartiallySignedTx

parse Partially Signed Bitcoin Transaction data.

Parameters:

  • payload (String)

    a Partially Signed Bitcoin Transaction data with binary format.

Returns:

  • (Bitcoin::PartiallySignedTx)

Raises:

  • (ArgumentError)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/bitcoin/psbt/tx.rb', line 58

def self.parse_from_payload(payload)
  buf = StringIO.new(payload)
  raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack1('N') == PSBT_MAGIC_BYTES
  raise ArgumentError, 'Invalid PSBT separator.' unless buf.read(1).bth.to_i(16) == 0xff
  partial_tx = self.new
  found_sep = false
  # read global data.
  until buf.eof?
    key_len = Bitcoin.unpack_var_int_from_io(buf)
    if key_len == 0
      found_sep = true
      break
    end
    key_type = Bitcoin.unpack_var_int_from_io(buf)
    key = buf.read(key_len - 1)
    value = buf.read(Bitcoin.unpack_var_int_from_io(buf))

    case key_type
    when PSBT_GLOBAL_TYPES[:unsigned_tx]
      raise ArgumentError, 'Invalid global transaction typed key.' unless key_len == 1
      raise ArgumentError, 'Duplicate Key, unsigned tx already provided.' if partial_tx.tx
      partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true, strict: true)
      partial_tx.tx.in.each do |tx_in|
        raise ArgumentError, 'Unsigned tx does not have empty scriptSigs and scriptWitnesses.' if !tx_in.script_sig.empty? || !tx_in.script_witness.empty?
      end
    when PSBT_GLOBAL_TYPES[:xpub]
      raise ArgumentError, 'Size of key was not the expected size for the type global xpub.' unless key.size == Bitcoin::BIP32_EXTKEY_WITH_VERSION_SIZE
      xpub = Bitcoin::ExtPubkey.parse_from_payload(key)
      raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless xpub.key.fully_valid_pubkey?
      raise ArgumentError, 'Duplicate key, global xpub already provided' if partial_tx.xpubs.any?{|x|x.xpub == xpub}
      info = Bitcoin::PSBT::KeyOriginInfo.parse_from_payload(value)
      raise ArgumentError, "global xpub's depth and the number of indexes not matched." unless xpub.depth == info.key_paths.size
      partial_tx.xpubs << Bitcoin::PSBT::GlobalXpub.new(xpub, info)
    when PSBT_GLOBAL_TYPES[:ver]
      partial_tx.version_number = value.unpack1('V')
      raise ArgumentError, "An unsupported version was detected." if SUPPORT_VERSION < partial_tx.version_number
    when PSBT_GLOBAL_TYPES[:proprietary]
      raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if partial_tx.proprietaries.any?{|p| p.key == key}
      partial_tx.proprietaries << Proprietary.new(key, value)
    else
      raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[key]
      partial_tx.unknowns[([key_type].pack('C') + key).bth] = value
    end
  end

  raise ArgumentError, 'Separator is missing at the end of an output map.' unless found_sep
  raise ArgumentError, 'No unsigned transaction was provided.' unless partial_tx.tx

  # read input data.
  partial_tx.tx.in.each do |tx_in|
    break if buf.eof?
    input = Input.parse_from_buf(buf)
    partial_tx.inputs << input
    if input.non_witness_utxo && input.non_witness_utxo.tx_hash != tx_in.prev_hash
      raise ArgumentError, 'Non-witness UTXO does not match outpoint hash.'
    end
  end

  raise ArgumentError, 'Inputs provided does not match the number of inputs in transaction.' unless partial_tx.inputs.size == partial_tx.tx.in.size

  # read output data.
  partial_tx.tx.outputs.each do
    break if buf.eof?
    output = Output.parse_from_buf(buf)
    break unless output
    partial_tx.outputs << output
  end

  raise ArgumentError, 'Outputs provided does not match the number of outputs in transaction.' unless partial_tx.outputs.size == partial_tx.tx.out.size

  partial_tx
end

Instance Method Details

#extract_txBitcoin::Tx

extract final tx.

Returns:



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/bitcoin/psbt/tx.rb', line 269

def extract_tx
  extract_tx = tx.dup
  inputs.each_with_index do |input, index|
    extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig
    extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness
  end
  # validate signature
  tx.in.each_with_index do |tx_in, index|
    input = inputs[index]
    if input.non_witness_utxo
      utxo = input.non_witness_utxo.out[tx_in.out_point.index]
      raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey)
    else
      utxo = input.witness_utxo
      raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey, amount: input.witness_utxo.value)
    end
  end
  extract_tx
end

#finalize!Bitcoin::PSBT::Tx

finalize tx. TODO This feature is experimental and support only multisig.

Returns:



262
263
264
265
# File 'lib/bitcoin/psbt/tx.rb', line 262

def finalize!
  inputs.each {|input|input.finalize!}
  self
end

#input_utxo(index) ⇒ Bitcoin::TxOut

Finds the UTXO for a given input index

Parameters:

  • index (Integer)

    input_index Index of the input to retrieve the UTXO of

Returns:



140
141
142
143
144
145
146
# File 'lib/bitcoin/psbt/tx.rb', line 140

def input_utxo(index)
  input = inputs[index]
  prevout_index = tx.in[index].out_point.index
  return input.non_witness_utxo.out[prevout_index] if input.non_witness_utxo
  return input.witness_utxo if input.witness_utxo
  nil
end

#merge(psbt) ⇒ Bitcoin::PartiallySignedTx

merge two PSBTs to create one PSBT. TODO This feature is experimental.

Parameters:

  • psbt (Bitcoin::PartiallySignedTx)

    PSBT to be combined which must have same property in PartiallySignedTx.

Returns:

  • (Bitcoin::PartiallySignedTx)

    combined object.

Raises:

  • (ArgumentError)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/bitcoin/psbt/tx.rb', line 241

def merge(psbt)
  raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Tx.' unless psbt.is_a?(Bitcoin::PSBT::Tx)
  raise ArgumentError, 'The combined transactions are different.' unless tx == psbt.tx
  raise ArgumentError, 'The Partially Signed Input\'s count are different.' unless inputs.size == psbt.inputs.size
  raise ArgumentError, 'The Partially Signed Output\'s count are different.' unless outputs.size == psbt.outputs.size

  combined = Bitcoin::PSBT::Tx.new(tx)
  inputs.each_with_index do |i, index|
    combined.inputs[index] = i.merge(psbt.inputs[index])
  end
  outputs.each_with_index do |o, index|
    combined.outputs[index] = o.merge(psbt.outputs[index])
  end

  combined.unknowns = Hash[unknowns.merge(psbt.unknowns).sort]
  combined
end

#ready_to_sign?Boolean

Check whether the signer can sign. Specifically, check the following.

  • If a non-witness UTXO is provided, its hash must match the hash specified in the prevout

  • If a witness UTXO is provided, no non-witness signature may be created

  • If a redeemScript is provided, the scriptPubKey must be for that redeemScript

  • If a witnessScript is provided, the scriptPubKey or the redeemScript must be for that witnessScript

Returns:

  • (Boolean)


220
221
222
223
# File 'lib/bitcoin/psbt/tx.rb', line 220

def ready_to_sign?
  inputs.each.with_index{|psbt_in, index|return false unless psbt_in.ready_to_sign?(input_utxo(index))}
  true
end

#signature_script(index) ⇒ Bitcoin::Script

get signature script of input specified by index

Parameters:

Returns:



228
229
230
231
232
233
234
235
# File 'lib/bitcoin/psbt/tx.rb', line 228

def signature_script(index)
  i = inputs[index]
  if i.non_witness_utxo
    i.redeem_script ? i.redeem_script : i.non_witness_utxo.out[tx.in[index].out_point.index].script_pubkey
  else
    i.witness_script ? i.witness_script : i.witness_utxo
  end
end

#to_base64String

generate payload with Base64 format.

Returns:

  • (String)

    a payload with Base64 format.



167
168
169
# File 'lib/bitcoin/psbt/tx.rb', line 167

def to_base64
  Base64.strict_encode64(to_payload)
end

#to_file(path) ⇒ Object

Store the PSBT to a file.

Parameters:

  • path (String)

    File path to store.

Raises:

  • (ArgumentError)


173
174
175
176
177
178
# File 'lib/bitcoin/psbt/tx.rb', line 173

def to_file(path)
  raise ArgumentError, 'The file already exists' if File.exist?(path)
  File.open(path, 'w') do |f|
    f.write(to_payload)
  end
end

#to_hObject



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/bitcoin/psbt/tx.rb', line 180

def to_h
  {
    tx: tx.to_h,
    global_xpubs: xpubs.map(&:to_h),
    psbt_version: version,
    proprietary: proprietaries.map(&:to_h),
    unknown: unknowns.map {|k, v| {"#{k}": v}},
    inputs: inputs.map(&:to_h),
    outputs: outputs.map(&:to_h)
  }
end

#to_payloadString

generate payload.

Returns:

  • (String)

    a payload with binary format.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/bitcoin/psbt/tx.rb', line 150

def to_payload
  payload = PSBT_MAGIC_BYTES.itb << 0xff.itb

  payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:unsigned_tx], value: tx.to_payload)
  payload << xpubs.map(&:to_payload).join
  payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:ver], value: [version_number].pack('V')) if version_number
  payload << proprietaries.map(&:to_payload).join
  payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join

  payload << PSBT_SEPARATOR.itb
  payload << inputs.map(&:to_payload).join
  payload << outputs.map(&:to_payload).join
  payload
end

#update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: []) ⇒ Object

update input key-value maps.

Parameters:

  • prev_tx (Bitcoin::Tx)

    previous tx reference by input.

  • redeem_script (Bitcoin::Script) (defaults to: nil)

    redeem script to set input.

  • witness_script (Bitcoin::Script) (defaults to: nil)

    witness script to set input.

  • hd_key_paths (Hash) (defaults to: [])

    bip 32 hd key paths to set input.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/bitcoin/psbt/tx.rb', line 197

def update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: [])
  prev_hash = prev_tx.tx_hash
  tx.in.each_with_index do|tx_in, i|
    if tx_in.prev_hash == prev_hash
      utxo = prev_tx.out[tx_in.out_point.index]
      raise ArgumentError, 'redeem script does not match utxo.' if redeem_script && !utxo.script_pubkey.include?(redeem_script.to_hash160)
      raise ArgumentError, 'witness script does not match redeem script.' if redeem_script && witness_script && !redeem_script.include?(witness_script.to_sha256)
      inputs[i].witness_utxo = utxo if utxo.script_pubkey.witness_program? || redeem_script&.witness_program?
      inputs[i].non_witness_utxo = prev_tx
      inputs[i].redeem_script = redeem_script if redeem_script
      inputs[i].witness_script = witness_script if witness_script
      inputs[i].hd_key_paths = hd_key_paths.map(&:pubkey).zip(hd_key_paths).to_h
      break
    end
  end
end

#versionInteger

get PSBT version

Returns:

  • (Integer)

    PSBT version number



133
134
135
# File 'lib/bitcoin/psbt/tx.rb', line 133

def version
  version_number ? version_number : 0
end