bitcoinrb 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|