Class: Bitcoin::PSBT::Tx

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tx = nil) ⇒ Tx

Returns a new instance of Tx.



34
35
36
37
38
39
40
# File 'lib/bitcoin/psbt/tx.rb', line 34

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

Instance Attribute Details

#inputsObject (readonly)

Returns the value of attribute inputs.



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

def inputs
  @inputs
end

#outputsObject (readonly)

Returns the value of attribute outputs.



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

def outputs
  @outputs
end

#txObject

Returns the value of attribute tx.



28
29
30
# File 'lib/bitcoin/psbt/tx.rb', line 28

def tx
  @tx
end

#unknownsObject

Returns the value of attribute unknowns.



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

def unknowns
  @unknowns
end

#xpubsObject

Returns the value of attribute xpubs.



29
30
31
# File 'lib/bitcoin/psbt/tx.rb', line 29

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)


45
46
47
# File 'lib/bitcoin/psbt/tx.rb', line 45

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)


52
53
54
55
56
57
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
# File 'lib/bitcoin/psbt/tx.rb', line 52

def self.parse_from_payload(payload)
  buf = StringIO.new(payload)
  raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack('N').first == 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 = buf.read(1).unpack('C').first
    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)
      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, 'Invalid pubkey.' 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)
    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.inputs.each do |input|
    raise ArgumentError, 'PSBT is not sane.' unless input.sane?
  end

  partial_tx
end

Instance Method Details

#extract_txBitcoin::Tx

extract final tx.

Returns:



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/bitcoin/psbt/tx.rb', line 236

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:



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

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:



126
127
128
129
130
131
132
# File 'lib/bitcoin/psbt/tx.rb', line 126

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)


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/bitcoin/psbt/tx.rb', line 208

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)


187
188
189
190
# File 'lib/bitcoin/psbt/tx.rb', line 187

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:



195
196
197
198
199
200
201
202
# File 'lib/bitcoin/psbt/tx.rb', line 195

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.



153
154
155
# File 'lib/bitcoin/psbt/tx.rb', line 153

def to_base64
  Base64.strict_encode64(to_payload)
end

#to_payloadString

generate payload.

Returns:

  • (String)

    a payload with binary format.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/bitcoin/psbt/tx.rb', line 136

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



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/bitcoin/psbt/tx.rb', line 162

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)
      if utxo.script_pubkey.witness_program? || (redeem_script && redeem_script.witness_program?)
        inputs[i].witness_utxo = utxo
      else
        inputs[i].non_witness_utxo = prev_tx
      end
      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
    end
  end
end