bitcoinrb 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bitcoin/key.rb +10 -5
- data/lib/bitcoin/psbt/hd_key_path.rb +37 -0
- data/lib/bitcoin/psbt/input.rb +172 -0
- data/lib/bitcoin/psbt/output.rb +75 -0
- data/lib/bitcoin/psbt/tx.rb +197 -0
- data/lib/bitcoin/psbt.rb +17 -277
- data/lib/bitcoin/script/script.rb +41 -1
- data/lib/bitcoin/script_witness.rb +9 -0
- data/lib/bitcoin/tx.rb +1 -5
- data/lib/bitcoin/tx_in.rb +6 -0
- data/lib/bitcoin/util.rb +17 -0
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +1 -2
- data/lib/openassets/util.rb +28 -0
- data/lib/openassets.rb +1 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bff3d97122521e4859ca7dc471eeda350a5156d763731384c1ed8c3e54b7117
|
4
|
+
data.tar.gz: 95a8ef84f38ced9e9f07279eefac81e774a923c0d048593783c4c4c2d5dda49f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c493349ad0cb5ac702b88a080cb5880407b5b37fc08171d450454e977874e056fcd21b5022c7aee5ffc4f40f43bfd8f63eabdec2e4ba5c6d5655a950da74cea
|
7
|
+
data.tar.gz: 84d4d0e3c1b93ea90af3414223b5c83a5753d66bc174a101b4e10388d74ba0077431bedc67ab670ada33226c5a73339973afeda87fda97d1877983f318cf5e29
|
data/lib/bitcoin/key.rb
CHANGED
@@ -6,6 +6,11 @@ module Bitcoin
|
|
6
6
|
# bitcoin key class
|
7
7
|
class Key
|
8
8
|
|
9
|
+
PUBLIC_KEY_SIZE = 65
|
10
|
+
COMPRESSED_PUBLIC_KEY_SIZE = 33
|
11
|
+
SIGNATURE_SIZE = 72
|
12
|
+
COMPACT_SIGNATURE_SIZE = 65
|
13
|
+
|
9
14
|
attr_accessor :priv_key
|
10
15
|
attr_accessor :pubkey
|
11
16
|
attr_accessor :key_type
|
@@ -60,7 +65,7 @@ module Bitcoin
|
|
60
65
|
raise ArgumentError, 'invalid version' unless version == Bitcoin.chain_params.privkey_version
|
61
66
|
raise ArgumentError, 'invalid checksum' unless Bitcoin.calc_checksum(version + data.bth) == checksum
|
62
67
|
key_len = data.bytesize
|
63
|
-
if key_len ==
|
68
|
+
if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack('C').first == 1
|
64
69
|
key_type = TYPES[:compressed]
|
65
70
|
data = data[0..-2]
|
66
71
|
elsif key_len == 32
|
@@ -139,12 +144,12 @@ module Bitcoin
|
|
139
144
|
# check +pubkey+ (hex) is compress or uncompress pubkey.
|
140
145
|
def self.compress_or_uncompress_pubkey?(pubkey)
|
141
146
|
p = pubkey.htb
|
142
|
-
return false if p.bytesize <
|
147
|
+
return false if p.bytesize < COMPRESSED_PUBLIC_KEY_SIZE
|
143
148
|
case p[0]
|
144
149
|
when "\x04"
|
145
|
-
return false unless p.bytesize ==
|
150
|
+
return false unless p.bytesize == PUBLIC_KEY_SIZE
|
146
151
|
when "\x02", "\x03"
|
147
|
-
return false unless p.bytesize ==
|
152
|
+
return false unless p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE
|
148
153
|
else
|
149
154
|
return false
|
150
155
|
end
|
@@ -154,7 +159,7 @@ module Bitcoin
|
|
154
159
|
# check +pubkey+ (hex) is compress pubkey.
|
155
160
|
def self.compress_pubkey?(pubkey)
|
156
161
|
p = pubkey.htb
|
157
|
-
p.bytesize ==
|
162
|
+
p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE && ["\x02", "\x03"].include?(p[0])
|
158
163
|
end
|
159
164
|
|
160
165
|
# check +sig+ is low.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module PSBT
|
3
|
+
|
4
|
+
# HD Key path data structure.
|
5
|
+
# see https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Specification
|
6
|
+
class HDKeyPath
|
7
|
+
|
8
|
+
attr_reader :pubkey
|
9
|
+
attr_accessor :fingerprint
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
def initialize(pubkey, fingerprint: nil, path: [])
|
13
|
+
@pubkey = pubkey
|
14
|
+
@fingerprint = fingerprint
|
15
|
+
@path = path
|
16
|
+
end
|
17
|
+
|
18
|
+
# parse hd key path from payload.
|
19
|
+
# @param [String] pubkey a public key with binary format.
|
20
|
+
# @param [String] payload hd key path value with binary format.
|
21
|
+
# @return [Bitcoin::PSBT::HDKeyPath]
|
22
|
+
def self.parse_from_payload(pubkey, payload)
|
23
|
+
raise 'Size of key was not the expected size for the type BIP32 keypath' unless [Bitcoin::Key::PUBLIC_KEY_SIZE, Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE].include?(pubkey.bytesize)
|
24
|
+
pubkey = Bitcoin::Key.new(pubkey: pubkey.bth)
|
25
|
+
raise 'Invalid pubkey' unless pubkey.fully_valid_pubkey?
|
26
|
+
self.new(pubkey.pubkey, fingerprint: payload[0...4].bth, path: payload[4..-1].unpack('I*'))
|
27
|
+
end
|
28
|
+
|
29
|
+
# generate payload which consist of pubkey and fingerprint, hd key path payload.
|
30
|
+
# @return [String] a payload
|
31
|
+
def to_payload(type = PSBT_IN_TYPES[:bip32_derivation])
|
32
|
+
PSBT.serialize_to_vector(type, key: pubkey.htb, value: fingerprint.htb + path.pack('I*'))
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
|
3
|
+
module PSBT
|
4
|
+
|
5
|
+
# Class for PSBTs which contain per-input information
|
6
|
+
class Input
|
7
|
+
|
8
|
+
attr_accessor :non_witness_utxo # Bitcoin::Tx
|
9
|
+
attr_accessor :witness_utxo # Bitcoin::TxOut
|
10
|
+
attr_accessor :redeem_script
|
11
|
+
attr_accessor :witness_script
|
12
|
+
attr_accessor :final_script_sig
|
13
|
+
attr_accessor :final_script_witness
|
14
|
+
attr_accessor :hd_key_paths
|
15
|
+
attr_accessor :partial_sigs
|
16
|
+
attr_accessor :sighash_type
|
17
|
+
attr_accessor :unknowns
|
18
|
+
|
19
|
+
def initialize(non_witness_utxo: nil, witness_utxo: nil)
|
20
|
+
@non_witness_utxo = non_witness_utxo
|
21
|
+
@witness_utxo = witness_utxo
|
22
|
+
@partial_sigs = {}
|
23
|
+
@hd_key_paths = {}
|
24
|
+
@unknowns = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# parse PSBT input data form buffer.
|
28
|
+
# @param [StringIO] buf psbt buffer.
|
29
|
+
# @return [Bitcoin::PSBTInput] psbt input.
|
30
|
+
def self.parse_from_buf(buf)
|
31
|
+
input = self.new
|
32
|
+
until buf.eof?
|
33
|
+
key_len = Bitcoin.unpack_var_int_from_io(buf)
|
34
|
+
break if key_len == 0
|
35
|
+
key_type = buf.read(1).unpack('C').first
|
36
|
+
key = buf.read(key_len - 1)
|
37
|
+
value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
|
38
|
+
|
39
|
+
case key_type
|
40
|
+
when PSBT_IN_TYPES[:non_witness_utxo]
|
41
|
+
raise ArgumentError, 'Duplicate Key, input non-witness utxo already provided.' if input.non_witness_utxo
|
42
|
+
input.non_witness_utxo = Bitcoin::Tx.parse_from_payload(value)
|
43
|
+
when PSBT_IN_TYPES[:witness_utxo]
|
44
|
+
raise ArgumentError, 'Duplicate Key, input witness utxo already provided.' if input.witness_utxo
|
45
|
+
input.witness_utxo = Bitcoin::TxOut.parse_from_payload(value)
|
46
|
+
when PSBT_IN_TYPES[:partial_sig]
|
47
|
+
if key.size != Bitcoin::Key::PUBLIC_KEY_SIZE && key.size != Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
|
48
|
+
raise ArgumentError, 'Size of key was not the expected size for the type partial signature pubkey.'
|
49
|
+
end
|
50
|
+
pubkey = Bitcoin::Key.new(pubkey: key.bth)
|
51
|
+
raise ArgumentError, 'Invalid pubkey.' unless pubkey.fully_valid_pubkey?
|
52
|
+
raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if input.partial_sigs[pubkey.pubkey]
|
53
|
+
input.partial_sigs[pubkey.pubkey] = value
|
54
|
+
when PSBT_IN_TYPES[:sighash]
|
55
|
+
raise ArgumentError 'Duplicate Key, input sighash type already provided.' if input.sighash_type
|
56
|
+
input.sighash_type = value.unpack('I').first
|
57
|
+
when PSBT_IN_TYPES[:redeem_script]
|
58
|
+
raise ArgumentError, 'Duplicate Key, input redeemScript already provided.' if input.redeem_script
|
59
|
+
input.redeem_script = Bitcoin::Script.parse_from_payload(value)
|
60
|
+
when PSBT_IN_TYPES[:witness_script]
|
61
|
+
raise ArgumentError, 'Duplicate Key, input witnessScript already provided.' if input.witness_script
|
62
|
+
input.witness_script = Bitcoin::Script.parse_from_payload(value)
|
63
|
+
when PSBT_IN_TYPES[:bip32_derivation]
|
64
|
+
raise ArgumentError, 'Duplicate Key, pubkey derivation path already provided.' if input.hd_key_paths[key.bth]
|
65
|
+
input.hd_key_paths[key.bth] = Bitcoin::PSBT::HDKeyPath.parse_from_payload(key, value)
|
66
|
+
when PSBT_IN_TYPES[:script_sig]
|
67
|
+
raise ArgumentError, 'Duplicate Key, input final scriptSig already provided.' if input.final_script_sig
|
68
|
+
input.final_script_sig = Bitcoin::Script.parse_from_payload(value)
|
69
|
+
when PSBT_IN_TYPES[:script_witness]
|
70
|
+
raise ArgumentError, 'Duplicate Key, input final scriptWitness already provided.' if input.final_script_witness
|
71
|
+
input.final_script_witness = Bitcoin::ScriptWitness.parse_from_payload(value)
|
72
|
+
else
|
73
|
+
unknown_key = ([key_type].pack('C') + key).bth
|
74
|
+
raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if input.unknowns[unknown_key]
|
75
|
+
input.unknowns[unknown_key] = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
input
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_payload
|
82
|
+
payload = ''
|
83
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:non_witness_utxo], value: non_witness_utxo.to_payload) if non_witness_utxo
|
84
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:witness_utxo], value: witness_utxo.to_payload) if witness_utxo
|
85
|
+
if final_script_sig.nil? && final_script_witness.nil?
|
86
|
+
payload << partial_sigs.map{|k, v|PSBT.serialize_to_vector(PSBT_IN_TYPES[:partial_sig], key: k.htb, value: v)}.join
|
87
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:sighash], value: [sighash_type].pack('I')) if sighash_type
|
88
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:redeem_script], value: redeem_script.to_payload) if redeem_script
|
89
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:witness_script], value: witness_script.to_payload) if witness_script
|
90
|
+
payload << hd_key_paths.values.map(&:to_payload).join
|
91
|
+
end
|
92
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:script_sig], value: final_script_sig.to_payload) if final_script_sig
|
93
|
+
payload << PSBT.serialize_to_vector(PSBT_IN_TYPES[:script_witness], value: final_script_witness.to_payload) if final_script_witness
|
94
|
+
payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join
|
95
|
+
payload << PSBT_SEPARATOR.to_even_length_hex.htb
|
96
|
+
payload
|
97
|
+
end
|
98
|
+
|
99
|
+
# Sanity check
|
100
|
+
def sane?
|
101
|
+
return false if non_witness_utxo && witness_utxo
|
102
|
+
return false if witness_script && witness_utxo.nil?
|
103
|
+
return false if final_script_witness && witness_utxo.nil?
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
# add signature as partial sig.
|
108
|
+
# @param [String] pubkey a public key with hex format.
|
109
|
+
# @param [String] sig a signature.
|
110
|
+
def add_sig(pubkey, sig)
|
111
|
+
raise ArgumentError, 'The sighash in signature is invalid.' unless sig.unpack('C*')[-1] == sighash_type
|
112
|
+
raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if partial_sigs[pubkey]
|
113
|
+
partial_sigs[pubkey] = sig
|
114
|
+
end
|
115
|
+
|
116
|
+
# merge two PSBT inputs to create one PSBT.
|
117
|
+
# @param [Bitcoin::PSBT::Input] psbi PSBT input to be combined which must have same property in PSBT Input.
|
118
|
+
# @return [Bitcoin::PSBT::Input] combined object.
|
119
|
+
def merge(psbi)
|
120
|
+
raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Input.' unless psbi.is_a?(Bitcoin::PSBT::Input)
|
121
|
+
raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless non_witness_utxo == psbi.non_witness_utxo
|
122
|
+
raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless witness_utxo == psbi.witness_utxo
|
123
|
+
raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.' unless sighash_type == psbi.sighash_type
|
124
|
+
raise ArgumentError, 'The Partially Signed Input\'s redeem_script are different.' unless redeem_script == psbi.redeem_script
|
125
|
+
raise ArgumentError, 'The Partially Signed Input\'s witness_script are different.' unless witness_script == psbi.witness_script
|
126
|
+
combined = Bitcoin::PSBT::Input.new(non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
|
127
|
+
combined.unknowns = unknowns.merge(psbi.unknowns)
|
128
|
+
combined.redeem_script = redeem_script
|
129
|
+
combined.witness_script = witness_script
|
130
|
+
combined.sighash_type = sighash_type
|
131
|
+
sigs = Hash[partial_sigs.merge(psbi.partial_sigs)]
|
132
|
+
redeem_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth]} if redeem_script && redeem_script.multisig?
|
133
|
+
witness_script.get_multisig_pubkeys.each{|pubkey|combined.partial_sigs[pubkey.bth] = sigs[pubkey.bth]} if witness_script && witness_script.multisig?
|
134
|
+
combined.hd_key_paths = hd_key_paths.merge(psbi.hd_key_paths)
|
135
|
+
combined
|
136
|
+
end
|
137
|
+
|
138
|
+
# finalize input.
|
139
|
+
# TODO This feature is experimental and support only multisig.
|
140
|
+
# @return [Bitcoin::PSBT::Input] finalized input.
|
141
|
+
def finalize!
|
142
|
+
if non_witness_utxo
|
143
|
+
self.final_script_sig = Bitcoin::Script.new << Bitcoin::Opcodes::OP_0 if redeem_script.multisig?
|
144
|
+
partial_sigs.values.each {|sig|final_script_sig << sig.bth}
|
145
|
+
final_script_sig << redeem_script.to_payload.bth
|
146
|
+
self.partial_sigs = {}
|
147
|
+
self.hd_key_paths = {}
|
148
|
+
self.redeem_script = nil
|
149
|
+
self.sighash_type = nil
|
150
|
+
else
|
151
|
+
if redeem_script
|
152
|
+
self.final_script_sig = Bitcoin::Script.parse_from_payload(Bitcoin::Script.pack_pushdata(redeem_script.to_payload))
|
153
|
+
self.redeem_script = nil
|
154
|
+
end
|
155
|
+
if witness_script
|
156
|
+
self.final_script_witness = Bitcoin::ScriptWitness.new
|
157
|
+
final_script_witness.stack << '' if witness_script.multisig?
|
158
|
+
partial_sigs.values.each {|sig| final_script_witness.stack << sig}
|
159
|
+
final_script_witness.stack << witness_script.to_payload
|
160
|
+
self.witness_script = nil
|
161
|
+
end
|
162
|
+
self.sighash_type = nil
|
163
|
+
self.partial_sigs = {}
|
164
|
+
self.hd_key_paths = {}
|
165
|
+
end
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module PSBT
|
3
|
+
|
4
|
+
# Class for PSBTs which contains per output information
|
5
|
+
class Output
|
6
|
+
|
7
|
+
attr_accessor :redeem_script
|
8
|
+
attr_accessor :witness_script
|
9
|
+
attr_accessor :hd_key_paths
|
10
|
+
attr_accessor :unknowns
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@hd_key_paths = {}
|
14
|
+
@unknowns = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# parse PSBT output data form buffer.
|
18
|
+
# @param [StringIO] buf psbt buffer.
|
19
|
+
# @return [Bitcoin::PSBTOutput] psbt output.
|
20
|
+
def self.parse_from_buf(buf)
|
21
|
+
output = self.new
|
22
|
+
until buf.eof?
|
23
|
+
key_len = Bitcoin.unpack_var_int_from_io(buf)
|
24
|
+
break if key_len == 0
|
25
|
+
key_type = buf.read(1).unpack('C').first
|
26
|
+
key = buf.read(key_len - 1)
|
27
|
+
value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
|
28
|
+
case key_type
|
29
|
+
when PSBT_OUT_TYPES[:redeem_script]
|
30
|
+
raise ArgumentError, 'Duplicate Key, output redeemScript already provided' if output.redeem_script
|
31
|
+
output.redeem_script = value
|
32
|
+
when PSBT_OUT_TYPES[:witness_script]
|
33
|
+
raise ArgumentError, 'Duplicate Key, output witnessScript already provided' if output.witness_script
|
34
|
+
output.witness_script = value
|
35
|
+
when PSBT_OUT_TYPES[:bip32_derivation]
|
36
|
+
raise ArgumentError, 'Duplicate Key, pubkey derivation path already provided' if output.hd_key_paths[key.bth]
|
37
|
+
output.hd_key_paths[key.bth] = Bitcoin::PSBT::HDKeyPath.parse_from_payload(key, value)
|
38
|
+
else
|
39
|
+
unknown_key = ([key_type].pack('C') + key).bth
|
40
|
+
raise ArgumentError, 'Duplicate Key, key for unknown value already provided' if output.unknowns[unknown_key]
|
41
|
+
output.unknowns[unknown_key] = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
output
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_payload
|
48
|
+
payload = ''
|
49
|
+
payload << PSBT.serialize_to_vector(PSBT_OUT_TYPES[:redeem_script], value: redeem_script) if redeem_script
|
50
|
+
payload << PSBT.serialize_to_vector(PSBT_OUT_TYPES[:witness_script], value: witness_script) if witness_script
|
51
|
+
payload << hd_key_paths.values.map{|v|v.to_payload(PSBT_OUT_TYPES[:bip32_derivation])}.join
|
52
|
+
payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join
|
53
|
+
payload << PSBT_SEPARATOR.to_even_length_hex.htb
|
54
|
+
payload
|
55
|
+
end
|
56
|
+
|
57
|
+
# merge two PSBT outputs to create one PSBT.
|
58
|
+
# @param [Bitcoin::PSBT::Output] psbo PSBT output to be combined which must have same property in PSBT Output.
|
59
|
+
# @return [Bitcoin::PSBT::Output] combined object.
|
60
|
+
def merge(psbo)
|
61
|
+
raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Output.' unless psbo.is_a?(Bitcoin::PSBT::Output)
|
62
|
+
raise ArgumentError, 'The Partially Signed Output\'s redeem_script are different.' unless redeem_script == psbo.redeem_script
|
63
|
+
raise ArgumentError, 'The Partially Signed Output\'s witness_script are different.' unless witness_script == psbo.witness_script
|
64
|
+
combined = Bitcoin::PSBT::Output.new
|
65
|
+
combined.redeem_script = redeem_script
|
66
|
+
combined.witness_script = witness_script
|
67
|
+
combined.unknowns = unknowns.merge(psbo.unknowns)
|
68
|
+
combined.hd_key_paths = hd_key_paths.merge(psbo.hd_key_paths)
|
69
|
+
combined
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module PSBT
|
3
|
+
|
4
|
+
class Tx
|
5
|
+
attr_accessor :tx
|
6
|
+
attr_reader :inputs
|
7
|
+
attr_reader :outputs
|
8
|
+
attr_accessor :unknowns
|
9
|
+
|
10
|
+
def initialize(tx = nil)
|
11
|
+
@tx = tx
|
12
|
+
@inputs = tx ? tx.in.map{Input.new}: []
|
13
|
+
@outputs = tx ? tx.out.map{Output.new}: []
|
14
|
+
@unknowns = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# parse Partially Signed Bitcoin Transaction data with Base64 format.
|
18
|
+
# @param [String] base64 a Partially Signed Bitcoin Transaction data with Base64 format.
|
19
|
+
# @return [Bitcoin::PartiallySignedTx]
|
20
|
+
def self.parse_from_base64(base64)
|
21
|
+
self.parse_from_payload(Base64.decode64(base64))
|
22
|
+
end
|
23
|
+
|
24
|
+
# parse Partially Signed Bitcoin Transaction data.
|
25
|
+
# @param [String] payload a Partially Signed Bitcoin Transaction data with binary format.
|
26
|
+
# @return [Bitcoin::PartiallySignedTx]
|
27
|
+
def self.parse_from_payload(payload)
|
28
|
+
buf = StringIO.new(payload)
|
29
|
+
raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack('N').first == PSBT_MAGIC_BYTES
|
30
|
+
raise ArgumentError, 'Invalid PSBT separator.' unless buf.read(1).bth.to_i(16) == 0xff
|
31
|
+
partial_tx = self.new
|
32
|
+
|
33
|
+
# read global data.
|
34
|
+
until buf.eof?
|
35
|
+
key_len = Bitcoin.unpack_var_int_from_io(buf)
|
36
|
+
break if key_len == 0
|
37
|
+
key_type = buf.read(1).unpack('C').first
|
38
|
+
key = buf.read(key_len - 1)
|
39
|
+
value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
|
40
|
+
|
41
|
+
case key_type
|
42
|
+
when PSBT_GLOBAL_TYPES[:unsigned_tx]
|
43
|
+
raise ArgumentError, 'Duplicate Key, unsigned tx already provided' if partial_tx.tx
|
44
|
+
partial_tx.tx = Bitcoin::Tx.parse_from_payload(value)
|
45
|
+
partial_tx.tx.in.each do |tx_in|
|
46
|
+
raise ArgumentError, 'Unsigned tx does not have empty scriptSigs and scriptWitnesses.' if !tx_in.script_sig.empty? || !tx_in.script_witness.empty?
|
47
|
+
end
|
48
|
+
else
|
49
|
+
raise ArgumentError, 'Duplicate Key, key for unknown value already provided' if partial_tx.unknowns[key]
|
50
|
+
partial_tx.unknowns[([key_type].pack('C') + key).bth] = value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise ArgumentError, 'No unsigned transcation was provided.' unless partial_tx.tx
|
55
|
+
|
56
|
+
# read input data.
|
57
|
+
partial_tx.tx.in.each do |tx_in|
|
58
|
+
break if buf.eof?
|
59
|
+
input = Input.parse_from_buf(buf)
|
60
|
+
partial_tx.inputs << input
|
61
|
+
if input.non_witness_utxo && input.non_witness_utxo.hash != tx_in.prev_hash
|
62
|
+
raise ArgumentError, 'Non-witness UTXO does not match outpoint hash'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
raise ArgumentError, 'Inputs provided does not match the number of inputs in transaction.' unless partial_tx.inputs.size == partial_tx.tx.in.size
|
67
|
+
|
68
|
+
# read output data.
|
69
|
+
partial_tx.tx.outputs.each do
|
70
|
+
break if buf.eof?
|
71
|
+
output = Output.parse_from_buf(buf)
|
72
|
+
break unless output
|
73
|
+
partial_tx.outputs << output
|
74
|
+
end
|
75
|
+
|
76
|
+
raise ArgumentError, 'Outputs provided does not match the number of outputs in transaction.' unless partial_tx.outputs.size == partial_tx.tx.out.size
|
77
|
+
|
78
|
+
partial_tx.inputs.each do |input|
|
79
|
+
raise ArgumentError, 'PSBT is not sane.' unless input.sane?
|
80
|
+
end
|
81
|
+
|
82
|
+
partial_tx
|
83
|
+
end
|
84
|
+
|
85
|
+
# generate payload.
|
86
|
+
# @return [String] a payload with binary format.
|
87
|
+
def to_payload
|
88
|
+
payload = PSBT_MAGIC_BYTES.to_even_length_hex.htb << 0xff.to_even_length_hex.htb
|
89
|
+
|
90
|
+
payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:unsigned_tx], value: tx.to_payload)
|
91
|
+
|
92
|
+
payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join
|
93
|
+
|
94
|
+
payload << PSBT_SEPARATOR.to_even_length_hex.htb
|
95
|
+
|
96
|
+
payload << inputs.map(&:to_payload).join
|
97
|
+
payload << outputs.map(&:to_payload).join
|
98
|
+
payload
|
99
|
+
end
|
100
|
+
|
101
|
+
# generate payload with Base64 format.
|
102
|
+
# @return [String] a payload with Base64 format.
|
103
|
+
def to_base64
|
104
|
+
Base64.strict_encode64(to_payload)
|
105
|
+
end
|
106
|
+
|
107
|
+
# update input key-value maps.
|
108
|
+
# @param [Bitcoin::Tx] prev_tx previous tx reference by input.
|
109
|
+
# @param [Bitcoin::Script] redeem_script redeem script to set input.
|
110
|
+
# @param [Bitcoin::Script] witness_script witness script to set input.
|
111
|
+
# @param [Hash] hd_key_paths bip 32 hd key paths to set input.
|
112
|
+
def update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: [])
|
113
|
+
prev_hash = prev_tx.hash
|
114
|
+
tx.in.each_with_index do|tx_in, i|
|
115
|
+
if tx_in.prev_hash == prev_hash
|
116
|
+
utxo = prev_tx.out[tx_in.out_point.index]
|
117
|
+
raise ArgumentError, 'redeem script does not match utxo.' if redeem_script && !utxo.script_pubkey.include?(redeem_script.to_hash160)
|
118
|
+
raise ArgumentError, 'witness script does not match redeem script.' if redeem_script && witness_script && !redeem_script.include?(witness_script.to_sha256)
|
119
|
+
if utxo.script_pubkey.witness_program? || (redeem_script && redeem_script.witness_program?)
|
120
|
+
inputs[i].witness_utxo = utxo
|
121
|
+
else
|
122
|
+
inputs[i].non_witness_utxo = prev_tx
|
123
|
+
end
|
124
|
+
inputs[i].redeem_script = redeem_script if redeem_script
|
125
|
+
inputs[i].witness_script = witness_script if witness_script
|
126
|
+
inputs[i].hd_key_paths = hd_key_paths.map(&:pubkey).zip(hd_key_paths).to_h
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# get signature script of input specified by +index+
|
132
|
+
# @param [Integer] index input index.
|
133
|
+
# @return [Bitcoin::Script]
|
134
|
+
def signature_script(index)
|
135
|
+
i = inputs[index]
|
136
|
+
if i.non_witness_utxo
|
137
|
+
i.redeem_script ? i.redeem_script : i.non_witness_utxo.out[tx.in[index].out_point.index].script_pubkey
|
138
|
+
else
|
139
|
+
i.witness_script ? i.witness_script : i.witness_utxo
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# merge two PSBTs to create one PSBT.
|
144
|
+
# TODO This feature is experimental.
|
145
|
+
# @param [Bitcoin::PartiallySignedTx] psbt PSBT to be combined which must have same property in PartiallySignedTx.
|
146
|
+
# @return [Bitcoin::PartiallySignedTx] combined object.
|
147
|
+
def merge(psbt)
|
148
|
+
raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Tx.' unless psbt.is_a?(Bitcoin::PSBT::Tx)
|
149
|
+
raise ArgumentError, 'The combined transactions are different.' unless tx == psbt.tx
|
150
|
+
raise ArgumentError, 'The Partially Signed Input\'s count are different.' unless inputs.size == psbt.inputs.size
|
151
|
+
raise ArgumentError, 'The Partially Signed Output\'s count are different.' unless outputs.size == psbt.outputs.size
|
152
|
+
|
153
|
+
combined = Bitcoin::PSBT::Tx.new(tx)
|
154
|
+
inputs.each_with_index do |i, index|
|
155
|
+
combined.inputs[index] = i.merge(psbt.inputs[index])
|
156
|
+
end
|
157
|
+
outputs.each_with_index do |o, index|
|
158
|
+
combined.outputs[index] = o.merge(psbt.outputs[index])
|
159
|
+
end
|
160
|
+
combined.unknowns = unknowns.merge(psbt.unknowns)
|
161
|
+
combined
|
162
|
+
end
|
163
|
+
|
164
|
+
# finalize tx.
|
165
|
+
# TODO This feature is experimental and support only multisig.
|
166
|
+
# @return [Bitcoin::PSBT::Tx] finalized PSBT.
|
167
|
+
def finalize!
|
168
|
+
inputs.each {|input|input.finalize!}
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
# extract final tx.
|
173
|
+
# @return [Bitcoin::Tx] final tx.
|
174
|
+
def extract_tx
|
175
|
+
extract_tx = tx.dup
|
176
|
+
inputs.each_with_index do |input, index|
|
177
|
+
extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig
|
178
|
+
extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness
|
179
|
+
end
|
180
|
+
# validate signature
|
181
|
+
tx.in.each_with_index do |tx_in, index|
|
182
|
+
input = inputs[index]
|
183
|
+
if input.non_witness_utxo
|
184
|
+
utxo = input.non_witness_utxo.out[tx_in.out_point.index]
|
185
|
+
raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey)
|
186
|
+
else
|
187
|
+
utxo = input.witness_utxo
|
188
|
+
raise "input[#{index}]'s signature is invalid.'" unless tx.verify_input_sig(index, utxo.script_pubkey, amount: input.witness_utxo.value)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
extract_tx
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
data/lib/bitcoin/psbt.rb
CHANGED
@@ -1,248 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# constants for PSBT
|
4
|
-
PSBT_MAGIC_BYTES = 0x70736274
|
5
|
-
PSBT_TYPES = {tx_or_non_witness: 0x00, redeem_or_witness_utxo: 0x01,
|
6
|
-
witness_or_partial_sig: 0x02, key_path_or_sighash: 0x03, input_num_or_index: 0x04}
|
7
|
-
PSBT_SEPARATOR = 0x00
|
8
|
-
|
9
|
-
# Partially Signed Bitcoin Transaction
|
10
|
-
class PartiallySignedTx
|
11
|
-
|
12
|
-
attr_accessor :tx
|
13
|
-
attr_accessor :redeem_scripts
|
14
|
-
attr_accessor :witness_scripts
|
15
|
-
attr_accessor :partial_inputs
|
16
|
-
attr_accessor :hd_key_paths
|
17
|
-
attr_accessor :num_input
|
18
|
-
attr_accessor :unknowns
|
19
|
-
attr_accessor :use_in_index
|
20
|
-
|
21
|
-
def initialize(tx = nil)
|
22
|
-
@tx = tx
|
23
|
-
@redeem_scripts = {}
|
24
|
-
@witness_scripts = {}
|
25
|
-
@partial_inputs = []
|
26
|
-
@hd_key_paths = {}
|
27
|
-
@unknowns = {}
|
28
|
-
@use_in_index = false
|
29
|
-
end
|
30
|
-
|
31
|
-
# parse Partially Signed Bitcoin Transaction data.
|
32
|
-
# @param [String] payload a Partially Signed Bitcoin Transaction data with binary format.
|
33
|
-
# @return [Bitcoin::PartiallySignedTx]
|
34
|
-
def self.parse_from_payload(payload)
|
35
|
-
buf = StringIO.new(payload)
|
36
|
-
raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack('N').first == PSBT_MAGIC_BYTES
|
37
|
-
raise ArgumentError, 'Invalid PSBT separator.' unless buf.read(1).bth.to_i(16) == 0xff
|
38
|
-
partial_tx = self.new
|
39
|
-
is_global = true
|
40
|
-
separators = 0
|
41
|
-
input = PartiallySignedInput.new
|
42
|
-
until buf.eof?
|
43
|
-
key_len = Bitcoin.unpack_var_int_from_io(buf)
|
44
|
-
if key_len == 0
|
45
|
-
puts "use child"
|
46
|
-
is_global = false
|
47
|
-
if separators > 0
|
48
|
-
if partial_tx.use_in_index
|
49
|
-
raise ArgumentError, 'Input indexes being used but an input was provided without an index' unless input.use_in_index
|
50
|
-
(partial_tx.partial_inputs.size...input.index).each{partial_tx.partial_inputs << PartiallySignedInput.new}
|
51
|
-
end
|
52
|
-
partial_tx.partial_inputs << input
|
53
|
-
input = PartiallySignedInput.new(separators)
|
54
|
-
end
|
55
|
-
separators += 1
|
56
|
-
next
|
57
|
-
end
|
58
|
-
|
59
|
-
key_type = buf.read(1).unpack('C').first
|
60
|
-
key = buf.read(key_len - 1)
|
61
|
-
value = buf.read(Bitcoin.unpack_var_int_from_io(buf))
|
62
|
-
puts "key_len = #{key_len}[#{key_type}] key: #{key.nil? ? key : key.bth}, value: #{value.bth}"
|
63
|
-
|
64
|
-
raise ArgumentError, 'Missing null terminator.' if buf.eof? && (key_type != 0 || !key.empty? || !value.empty?)
|
65
|
-
|
66
|
-
case key_type
|
67
|
-
when PSBT_TYPES[:tx_or_non_witness]
|
68
|
-
tx = Bitcoin::Tx.parse_from_payload(value)
|
69
|
-
if is_global
|
70
|
-
partial_tx.tx = tx
|
71
|
-
else
|
72
|
-
raise ArgumentError, 'Provided non witness utxo does not match the required utxo for input' unless partial_tx.tx.in[input.index].out_point.hash == tx.hash
|
73
|
-
input.non_witness_utxo = tx
|
74
|
-
end
|
75
|
-
when PSBT_TYPES[:redeem_or_witness_utxo]
|
76
|
-
if is_global
|
77
|
-
redeem_hash = key
|
78
|
-
raise ArgumentError, 'Size of key was not the expected size for the type redeem script' unless redeem_hash.bytesize == 20
|
79
|
-
raise ArgumentError, 'Provided hash160 does not match the redeemscript\'s hash160' unless Bitcoin.hash160(value.bth) == redeem_hash.bth
|
80
|
-
partial_tx.redeem_scripts[redeem_hash.bth] = value
|
81
|
-
else
|
82
|
-
input.witness_utxo = Bitcoin::TxOut.parse_from_payload(value)
|
83
|
-
end
|
84
|
-
when PSBT_TYPES[:witness_or_partial_sig]
|
85
|
-
if is_global
|
86
|
-
witness_hash = key
|
87
|
-
raise ArgumentError, 'Size of key was not the expected size for the type witness script' unless witness_hash.bytesize == 32
|
88
|
-
raise ArgumentError, 'Provided sha256 does not match the witnessscript\'s sha256' unless Bitcoin.sha256(value) == witness_hash
|
89
|
-
partial_tx.witness_scripts[witness_hash.bth] = value
|
90
|
-
else
|
91
|
-
raise ArgumentError, 'Size of key was not the expected size for the type partial signature pubkey' unless [33, 65].include?(key.bytesize)
|
92
|
-
pubkey = Bitcoin::Key.new(pubkey: key.bth)
|
93
|
-
raise ArgumentError, 'Invalid pubkey' unless pubkey.fully_valid_pubkey?
|
94
|
-
input.partial_sigs[pubkey.pubkey] = value
|
95
|
-
end
|
96
|
-
when PSBT_TYPES[:key_path_or_sighash]
|
97
|
-
if is_global
|
98
|
-
raise ArgumentError, 'Size of key was not the expected size for the type BIP32 keypath' unless [33, 65].include?(key.bytesize)
|
99
|
-
pubkey = Bitcoin::Key.new(pubkey: key.bth)
|
100
|
-
raise ArgumentError, 'Invalid pubkey' unless pubkey.fully_valid_pubkey?
|
101
|
-
key_paths = []
|
102
|
-
key_paths << value[0...4] # fingerprint
|
103
|
-
key_paths += value[4..-1].unpack('N*')
|
104
|
-
partial_tx.hd_key_paths[pubkey.pubkey] = key_paths
|
105
|
-
else
|
106
|
-
input.sighash_type = value.unpack('N').first
|
107
|
-
end
|
108
|
-
when PSBT_TYPES[:input_num_or_index]
|
109
|
-
if is_global
|
110
|
-
partial_tx.num_input = Bitcoin.unpack_var_int(value).first
|
111
|
-
else
|
112
|
-
input.index = Bitcoin.unpack_var_int(value).first
|
113
|
-
input.use_in_index = true
|
114
|
-
partial_tx.use_in_index = true
|
115
|
-
end
|
116
|
-
else
|
117
|
-
if is_global
|
118
|
-
partial_tx.unknowns[([key_type].pack('C') + key).bth] = value
|
119
|
-
else
|
120
|
-
input.unknowns[([key_type].pack('C') + key).bth] = value
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
raise ArgumentError, 'Inputs provided does not match the number of inputs stated.' if (separators - 1) != partial_tx.num_input && partial_tx.use_in_index
|
125
|
-
raise ArgumentError, 'Inputs provided does not match the number of inputs in transaction.' unless partial_tx.tx.in.size == partial_tx.partial_inputs.size
|
126
|
-
partial_tx
|
127
|
-
end
|
128
|
-
|
129
|
-
# generate payload.
|
130
|
-
# @return [String] a payload with binary format.
|
131
|
-
def to_payload
|
132
|
-
payload = PSBT_MAGIC_BYTES.to_even_length_hex.htb << 0xff.to_even_length_hex.htb
|
133
|
-
payload << serialize_to_vector(PSBT_TYPES[:tx_or_non_witness], value: tx.to_payload) if tx
|
134
|
-
|
135
|
-
redeem_scripts.each do |k, v|
|
136
|
-
payload << serialize_to_vector(PSBT_TYPES[:redeem_or_witness_utxo], key: k.htb, value: v)
|
137
|
-
end
|
138
|
-
|
139
|
-
witness_scripts.each do |k, v|
|
140
|
-
payload << serialize_to_vector(PSBT_TYPES[:witness_or_partial_sig], key: k.htb, value: v)
|
141
|
-
end
|
142
|
-
|
143
|
-
hd_key_paths.each do |k, v|
|
144
|
-
value = v.map.with_index{|v, i| i == 0 ? v : [v].pack('N')}.join
|
145
|
-
payload << serialize_to_vector(PSBT_TYPES[:key_path_or_sighash], key: k.htb, value: value)
|
146
|
-
end
|
147
|
-
|
148
|
-
payload << serialize_to_vector(PSBT_TYPES[:input_num_or_index], value: Bitcoin.pack_var_int(num_input)) if num_input
|
1
|
+
require 'base64'
|
149
2
|
|
150
|
-
|
151
|
-
puts "key = #{k}, v = #{v.bth}"
|
152
|
-
payload << Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v
|
153
|
-
end
|
154
|
-
|
155
|
-
payload << PSBT_SEPARATOR.to_even_length_hex.htb
|
156
|
-
|
157
|
-
tx.inputs.each_with_index do|tx_in, index|
|
158
|
-
partial_input = partial_inputs[index]
|
159
|
-
next if use_in_index && !partial_input.use_in_index
|
160
|
-
if tx_in.script_sig.empty? && tx_in.script_witness.empty?
|
161
|
-
if partial_input.non_witness_utxo
|
162
|
-
payload << serialize_to_vector(PSBT_TYPES[:tx_or_non_witness], value: partial_input.non_witness_utxo.to_payload)
|
163
|
-
elsif partial_input.witness_utxo
|
164
|
-
payload << serialize_to_vector(PSBT_TYPES[:redeem_or_witness_utxo], value: partial_input.witness_utxo.to_payload)
|
165
|
-
end
|
166
|
-
partial_input.partial_sigs.each do |k, v|
|
167
|
-
payload << serialize_to_vector(PSBT_TYPES[:witness_or_partial_sig], key: k.htb, value: v)
|
168
|
-
end
|
169
|
-
payload << serialize_to_vector(PSBT_TYPES[:key_path_or_sighash], value: [partial_input.sighash_type].pack('N')) if partial_input.sighash_type
|
170
|
-
payload << serialize_to_vector(PSBT_TYPES[:input_num_or_index], value: Bitcoin.pack_var_int(partial_input.index)) if partial_input.use_in_index
|
171
|
-
partial_input.unknowns.each do |k,v|
|
172
|
-
payload << Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v
|
173
|
-
end
|
174
|
-
end
|
175
|
-
payload << PSBT_SEPARATOR.to_even_length_hex.htb
|
176
|
-
end
|
3
|
+
module Bitcoin
|
177
4
|
|
178
|
-
|
179
|
-
end
|
5
|
+
module PSBT
|
180
6
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
def combine(psbt)
|
186
|
-
raise ArgumentError, 'The argument psbt must be an instance of PartiallySignedTx.' unless psbt.is_a?(PartiallySignedTx)
|
187
|
-
raise ArgumentError, 'The combined transactions are different.' unless tx == psbt.tx
|
188
|
-
raise ArgumentError, 'The use_in_index are different.' unless use_in_index == psbt.use_in_index
|
189
|
-
raise ArgumentError, 'The partial_inputs size are different.' unless partial_inputs.size == psbt.partial_inputs.size
|
190
|
-
raise ArgumentError, 'The unknowns are different.' unless unknowns == psbt.unknowns
|
191
|
-
combined = PartiallySignedTx.new(tx)
|
192
|
-
combined.redeem_scripts = redeem_scripts.merge(psbt.redeem_scripts)
|
193
|
-
combined.witness_scripts = witness_scripts.merge(psbt.witness_scripts)
|
194
|
-
combined.hd_key_paths = hd_key_paths.merge(psbt.hd_key_paths)
|
195
|
-
combined.witness_scripts = witness_scripts.merge(psbt.witness_scripts)
|
196
|
-
partial_inputs.each_with_index {|i, index|combined.partial_inputs[index] = i.combine(psbt.partial_inputs[index])}
|
197
|
-
combined
|
198
|
-
end
|
7
|
+
autoload :Tx, 'bitcoin/psbt/tx'
|
8
|
+
autoload :Input, 'bitcoin/psbt/input'
|
9
|
+
autoload :Output, 'bitcoin/psbt/output'
|
10
|
+
autoload :HDKeyPath, 'bitcoin/psbt/hd_key_path'
|
199
11
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
finalize_tx.inputs.each_with_index do |tx_in, i|
|
209
|
-
partial_input = partial_inputs[i]
|
210
|
-
if partial_input.non_witness_utxo
|
211
|
-
utxo = partial_input.non_witness_utxo.out[tx_in.out_point.index]
|
212
|
-
redeem_script = Bitcoin::Script.parse_from_payload(redeem_scripts[utxo.script_pubkey.chunks[1].pushed_data.bth])
|
213
|
-
tx_in.script_sig << Bitcoin::Opcodes::OP_0
|
214
|
-
redeem_script.chunks[1..-3].each do |pubkey|
|
215
|
-
tx_in.script_sig << partial_input.partial_sigs[pubkey.pushed_data.bth].bth
|
216
|
-
end
|
217
|
-
tx_in.script_sig << redeem_script.to_payload.bth
|
218
|
-
# tx_in.script_sig = Bitcoin::Script.new unless finalize_tx.verify_input_sig(i, utxo.script_pubkey)
|
219
|
-
elsif partial_input.witness_utxo
|
220
|
-
utxo = partial_input.witness_utxo
|
221
|
-
tx_in.script_witness.stack << ''
|
222
|
-
witness_scripts.each {|k, v|
|
223
|
-
redeem_script = Bitcoin::Script.parse_from_payload(v)
|
224
|
-
p2wsh = Bitcoin::Script.to_p2wsh(redeem_script)
|
225
|
-
if p2wsh.to_p2sh == utxo.script_pubkey
|
226
|
-
redeem_script.chunks[1..-3].each do |pubkey|
|
227
|
-
tx_in.script_witness.stack << partial_input.partial_sigs[pubkey.pushed_data.bth]
|
228
|
-
end
|
229
|
-
tx_in.script_witness.stack << v
|
230
|
-
tx_in.script_sig << p2wsh.to_payload.bth
|
231
|
-
break
|
232
|
-
end
|
233
|
-
}
|
234
|
-
unless finalize_tx.verify_input_sig(i, utxo.script_pubkey, amount: utxo.value)
|
235
|
-
tx_in.script_sig = Bitcoin::Script.new
|
236
|
-
tx_in.script_witness = Bitcoin::ScriptWitness.new
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
finalize_tx
|
241
|
-
end
|
12
|
+
# constants for PSBT
|
13
|
+
PSBT_MAGIC_BYTES = 0x70736274
|
14
|
+
PSBT_GLOBAL_TYPES = {unsigned_tx: 0x00}
|
15
|
+
PSBT_IN_TYPES = {non_witness_utxo: 0x00, witness_utxo: 0x01, partial_sig: 0x02,
|
16
|
+
sighash: 0x03, redeem_script: 0x04, witness_script: 0x05,
|
17
|
+
bip32_derivation: 0x06, script_sig: 0x07, script_witness: 0x08}
|
18
|
+
PSBT_OUT_TYPES = {redeem_script: 0x00, witness_script: 0x01, bip32_derivation: 0x02}
|
19
|
+
PSBT_SEPARATOR = 0x00
|
242
20
|
|
243
|
-
|
21
|
+
module_function
|
244
22
|
|
245
|
-
def serialize_to_vector(key_type, key: nil, value: nil)
|
23
|
+
def self.serialize_to_vector(key_type, key: nil, value: nil)
|
246
24
|
key_len = key_type.to_even_length_hex.htb.bytesize
|
247
25
|
key_len += key.bytesize if key
|
248
26
|
s = Bitcoin.pack_var_int(key_len) << Bitcoin.pack_var_int(key_type)
|
@@ -250,44 +28,6 @@ module Bitcoin
|
|
250
28
|
s << Bitcoin.pack_var_int(value.bytesize) << value
|
251
29
|
s
|
252
30
|
end
|
253
|
-
|
254
|
-
end
|
255
|
-
|
256
|
-
class PartiallySignedInput
|
257
|
-
|
258
|
-
attr_accessor :non_witness_utxo # Bitcoin::Tx
|
259
|
-
attr_accessor :witness_utxo # Bitcoin::TxOut
|
260
|
-
attr_accessor :partial_sigs
|
261
|
-
attr_accessor :sighash_type
|
262
|
-
attr_accessor :index
|
263
|
-
attr_accessor :unknowns
|
264
|
-
attr_accessor :use_in_index
|
265
|
-
|
266
|
-
def initialize(index = 0, non_witness_utxo: nil, witness_utxo: nil)
|
267
|
-
@non_witness_utxo = non_witness_utxo
|
268
|
-
@witness_utxo = witness_utxo
|
269
|
-
@partial_sigs = {}
|
270
|
-
@index = index
|
271
|
-
@unknowns = {}
|
272
|
-
@use_in_index = false
|
273
|
-
end
|
274
|
-
|
275
|
-
# combine two PSBTs to create one PSBT.
|
276
|
-
# @param [Bitcoin::PartiallySignedInput] psbi PSBI to be combined which must have same property in PartiallySignedInput.
|
277
|
-
# @return [Bitcoin::PartiallySignedInput] combined object.
|
278
|
-
def combine(psbi)
|
279
|
-
raise ArgumentError, 'The argument psbt must be an instance of PartiallySignedInput.' unless psbi.is_a?(PartiallySignedInput)
|
280
|
-
raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless non_witness_utxo == psbi.non_witness_utxo
|
281
|
-
raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless witness_utxo == psbi.witness_utxo
|
282
|
-
raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.' unless sighash_type == psbi.sighash_type
|
283
|
-
raise ArgumentError, 'The Partially Signed Input\'s use_in_index are different.' unless use_in_index == psbi.use_in_index
|
284
|
-
combined = PartiallySignedInput.new(index, non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
|
285
|
-
combined.use_in_index = use_in_index
|
286
|
-
combined.unknowns = unknowns.merge(psbi.unknowns)
|
287
|
-
combined.partial_sigs = Hash[partial_sigs.merge(psbi.partial_sigs).sort]
|
288
|
-
combined
|
289
|
-
end
|
290
|
-
|
291
31
|
end
|
292
32
|
|
293
33
|
end
|
@@ -32,10 +32,17 @@ module Bitcoin
|
|
32
32
|
[redeem_script.to_p2sh, redeem_script]
|
33
33
|
end
|
34
34
|
|
35
|
+
# generate p2sh script.
|
36
|
+
# @param [String] script_hash script hash for P2SH
|
37
|
+
# @return [Script] P2SH script
|
38
|
+
def self.to_p2sh(script_hash)
|
39
|
+
Script.new << OP_HASH160 << script_hash << OP_EQUAL
|
40
|
+
end
|
41
|
+
|
35
42
|
# generate p2sh script with this as a redeem script
|
36
43
|
# @return [Script] P2SH script
|
37
44
|
def to_p2sh
|
38
|
-
Script.
|
45
|
+
Script.to_p2sh(to_hash160)
|
39
46
|
end
|
40
47
|
|
41
48
|
def get_multisig_pubkeys
|
@@ -73,6 +80,27 @@ module Bitcoin
|
|
73
80
|
script
|
74
81
|
end
|
75
82
|
|
83
|
+
# generate script from addr.
|
84
|
+
# @param [String] addr address.
|
85
|
+
# @return [Bitcoin::Script] parsed script.
|
86
|
+
def self.parse_from_addr(addr)
|
87
|
+
begin
|
88
|
+
segwit_addr = Bech32::SegwitAddr.new(addr)
|
89
|
+
raise 'Invalid hrp.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
|
90
|
+
Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
|
91
|
+
rescue Exception => e
|
92
|
+
hex, addr_version = Bitcoin.decode_base58_address(addr)
|
93
|
+
case addr_version
|
94
|
+
when Bitcoin.chain_params.address_version
|
95
|
+
Bitcoin::Script.to_p2pkh(hex)
|
96
|
+
when Bitcoin.chain_params.p2sh_version
|
97
|
+
Bitcoin::Script.to_p2sh(hex)
|
98
|
+
else
|
99
|
+
throw e
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
76
104
|
def self.parse_from_payload(payload)
|
77
105
|
s = new
|
78
106
|
buf = StringIO.new(payload)
|
@@ -271,6 +299,18 @@ module Bitcoin
|
|
271
299
|
self
|
272
300
|
end
|
273
301
|
|
302
|
+
# Check the item is in the chunk of the script.
|
303
|
+
def include?(item)
|
304
|
+
chunk_item = if item.is_a?(Integer)
|
305
|
+
item.chr
|
306
|
+
elsif item.is_a?(String)
|
307
|
+
data = Encoding::ASCII_8BIT == item.encoding ? item : item.htb
|
308
|
+
Bitcoin::Script.pack_pushdata(data)
|
309
|
+
end
|
310
|
+
return false unless chunk_item
|
311
|
+
chunks.include?(chunk_item)
|
312
|
+
end
|
313
|
+
|
274
314
|
def to_s
|
275
315
|
chunks.map { |c|
|
276
316
|
case c
|
@@ -9,6 +9,15 @@ module Bitcoin
|
|
9
9
|
@stack = stack
|
10
10
|
end
|
11
11
|
|
12
|
+
def self.parse_from_payload(payload)
|
13
|
+
buf = payload.is_a?(StringIO) ? payload : StringIO.new(payload)
|
14
|
+
size = Bitcoin.unpack_var_int_from_io(buf)
|
15
|
+
stack = size.times.map do
|
16
|
+
buf.read(Bitcoin.unpack_var_int_from_io(buf))
|
17
|
+
end
|
18
|
+
self.new(stack)
|
19
|
+
end
|
20
|
+
|
12
21
|
def empty?
|
13
22
|
stack.empty?
|
14
23
|
end
|
data/lib/bitcoin/tx.rb
CHANGED
@@ -60,11 +60,7 @@ module Bitcoin
|
|
60
60
|
|
61
61
|
if witness
|
62
62
|
in_count.times do |i|
|
63
|
-
|
64
|
-
witness_count.times do
|
65
|
-
size = Bitcoin.unpack_var_int_from_io(buf)
|
66
|
-
tx.inputs[i].script_witness.stack << buf.read(size)
|
67
|
-
end
|
63
|
+
tx.inputs[i].script_witness = Bitcoin::ScriptWitness.parse_from_payload(buf)
|
68
64
|
end
|
69
65
|
end
|
70
66
|
|
data/lib/bitcoin/tx_in.rb
CHANGED
data/lib/bitcoin/util.rb
CHANGED
@@ -85,11 +85,28 @@ module Bitcoin
|
|
85
85
|
Digest::RMD160.hexdigest(Digest::SHA256.digest(hex.htb))
|
86
86
|
end
|
87
87
|
|
88
|
+
# encode Base58 check address.
|
89
|
+
# @param [String] hex the address payload.
|
90
|
+
# @param [String] addr_version the address version for P2PKH and P2SH.
|
91
|
+
# @return [String] Base58 check encoding address.
|
88
92
|
def encode_base58_address(hex, addr_version)
|
89
93
|
base = addr_version + hex
|
90
94
|
Base58.encode(base + calc_checksum(base))
|
91
95
|
end
|
92
96
|
|
97
|
+
# decode Base58 check encoding address.
|
98
|
+
# @param [String] addr address.
|
99
|
+
# @return [Array] hex and address version
|
100
|
+
def decode_base58_address(addr)
|
101
|
+
hex = Base58.decode(addr)
|
102
|
+
if hex.size == 50 && calc_checksum(hex[0...-8]) == hex[-8..-1]
|
103
|
+
raise 'Invalid version bytes.' unless [Bitcoin.chain_params.address_version, Bitcoin.chain_params.p2sh_version].include?(hex[0..1])
|
104
|
+
[hex[2...-8], hex[0..1]]
|
105
|
+
else
|
106
|
+
raise 'Invalid address.'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
93
110
|
def calc_checksum(hex)
|
94
111
|
double_sha256(hex.htb).bth[0..7]
|
95
112
|
end
|
data/lib/bitcoin/version.rb
CHANGED
data/lib/bitcoin.rb
CHANGED
@@ -46,8 +46,7 @@ module Bitcoin
|
|
46
46
|
autoload :Wallet, 'bitcoin/wallet'
|
47
47
|
autoload :BloomFilter, 'bitcoin/bloom_filter'
|
48
48
|
autoload :Payments, 'bitcoin/payments'
|
49
|
-
autoload :
|
50
|
-
autoload :PartiallySignedInput, 'bitcoin/psbt'
|
49
|
+
autoload :PSBT, 'bitcoin/psbt'
|
51
50
|
|
52
51
|
require_relative 'bitcoin/constants'
|
53
52
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenAssets
|
4
|
+
module Util
|
5
|
+
class << self
|
6
|
+
OA_VERSION_BYTE = '17' # 0x23
|
7
|
+
OA_VERSION_BYTE_TESTNET = '73' # 0x115
|
8
|
+
|
9
|
+
def script_to_asset_id(script)
|
10
|
+
hash_to_asset_id(Bitcoin.hash160(script))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def hash_to_asset_id(hash)
|
16
|
+
hash = oa_version_byte + hash
|
17
|
+
Bitcoin::Base58.encode(hash + Bitcoin.calc_checksum(hash))
|
18
|
+
end
|
19
|
+
|
20
|
+
def oa_version_byte
|
21
|
+
case Bitcoin.chain_params.network
|
22
|
+
when 'mainnet' then OA_VERSION_BYTE
|
23
|
+
when 'testnet', 'regtest' then OA_VERSION_BYTE_TESTNET
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/openassets.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitcoinrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- azuchi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ecdsa
|
@@ -366,6 +366,10 @@ files:
|
|
366
366
|
- lib/bitcoin/payments/payment_request.pb.rb
|
367
367
|
- lib/bitcoin/payments/x509_certificates.pb.rb
|
368
368
|
- lib/bitcoin/psbt.rb
|
369
|
+
- lib/bitcoin/psbt/hd_key_path.rb
|
370
|
+
- lib/bitcoin/psbt/input.rb
|
371
|
+
- lib/bitcoin/psbt/output.rb
|
372
|
+
- lib/bitcoin/psbt/tx.rb
|
369
373
|
- lib/bitcoin/rpc.rb
|
370
374
|
- lib/bitcoin/rpc/http_server.rb
|
371
375
|
- lib/bitcoin/rpc/request_handler.rb
|
@@ -397,6 +401,7 @@ files:
|
|
397
401
|
- lib/openassets.rb
|
398
402
|
- lib/openassets/marker_output.rb
|
399
403
|
- lib/openassets/payload.rb
|
404
|
+
- lib/openassets/util.rb
|
400
405
|
homepage: https://github.com/haw-itn/bitcoinrb
|
401
406
|
licenses:
|
402
407
|
- MIT
|