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.



10
11
12
13
14
15
# File 'lib/bitcoin/psbt/tx.rb', line 10

def initialize(tx = nil)
  @tx = tx
  @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.



6
7
8
# File 'lib/bitcoin/psbt/tx.rb', line 6

def inputs
  @inputs
end

#outputsObject (readonly)

Returns the value of attribute outputs.



7
8
9
# File 'lib/bitcoin/psbt/tx.rb', line 7

def outputs
  @outputs
end

#txObject

Returns the value of attribute tx.



5
6
7
# File 'lib/bitcoin/psbt/tx.rb', line 5

def tx
  @tx
end

#unknownsObject

Returns the value of attribute unknowns.



8
9
10
# File 'lib/bitcoin/psbt/tx.rb', line 8

def unknowns
  @unknowns
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)


20
21
22
# File 'lib/bitcoin/psbt/tx.rb', line 20

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)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
# File 'lib/bitcoin/psbt/tx.rb', line 27

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

  # read global data.
  until buf.eof?
    key_len = Bitcoin.unpack_var_int_from_io(buf)
    break if key_len == 0
    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, 'Duplicate Key, unsigned tx already provided' if partial_tx.tx
      partial_tx.tx = Bitcoin::Tx.parse_from_payload(value)
      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
    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, 'No unsigned transcation 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.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:



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/bitcoin/psbt/tx.rb', line 174

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:



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

def finalize!
  inputs.each {|input|input.finalize!}
  self
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)


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

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 = unknowns.merge(psbt.unknowns)
  combined
end

#signature_script(index) ⇒ Bitcoin::Script

get signature script of input specified by index

Parameters:

Returns:



134
135
136
137
138
139
140
141
# File 'lib/bitcoin/psbt/tx.rb', line 134

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.



103
104
105
# File 'lib/bitcoin/psbt/tx.rb', line 103

def to_base64
  Base64.strict_encode64(to_payload)
end

#to_payloadString

generate payload.

Returns:

  • (String)

    a payload with binary format.



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/bitcoin/psbt/tx.rb', line 87

def to_payload
  payload = PSBT_MAGIC_BYTES.to_even_length_hex.htb << 0xff.to_even_length_hex.htb

  payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:unsigned_tx], value: tx.to_payload)

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

  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.



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 112

def update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: [])
  prev_hash = prev_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