tapyrus 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +100 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/tapyrusrb-cli +5 -0
- data/exe/tapyrusrbd +41 -0
- data/lib/openassets/marker_output.rb +20 -0
- data/lib/openassets/payload.rb +54 -0
- data/lib/openassets/util.rb +28 -0
- data/lib/openassets.rb +9 -0
- data/lib/tapyrus/base58.rb +38 -0
- data/lib/tapyrus/block.rb +77 -0
- data/lib/tapyrus/block_header.rb +88 -0
- data/lib/tapyrus/bloom_filter.rb +78 -0
- data/lib/tapyrus/chain_params.rb +90 -0
- data/lib/tapyrus/chainparams/mainnet.yml +41 -0
- data/lib/tapyrus/chainparams/regtest.yml +38 -0
- data/lib/tapyrus/chainparams/testnet.yml +41 -0
- data/lib/tapyrus/constants.rb +195 -0
- data/lib/tapyrus/descriptor.rb +147 -0
- data/lib/tapyrus/ext_key.rb +337 -0
- data/lib/tapyrus/key.rb +296 -0
- data/lib/tapyrus/key_path.rb +26 -0
- data/lib/tapyrus/logger.rb +42 -0
- data/lib/tapyrus/merkle_tree.rb +149 -0
- data/lib/tapyrus/message/addr.rb +35 -0
- data/lib/tapyrus/message/base.rb +28 -0
- data/lib/tapyrus/message/block.rb +46 -0
- data/lib/tapyrus/message/block_transaction_request.rb +45 -0
- data/lib/tapyrus/message/block_transactions.rb +31 -0
- data/lib/tapyrus/message/block_txn.rb +27 -0
- data/lib/tapyrus/message/cmpct_block.rb +42 -0
- data/lib/tapyrus/message/error.rb +10 -0
- data/lib/tapyrus/message/fee_filter.rb +27 -0
- data/lib/tapyrus/message/filter_add.rb +28 -0
- data/lib/tapyrus/message/filter_clear.rb +17 -0
- data/lib/tapyrus/message/filter_load.rb +39 -0
- data/lib/tapyrus/message/get_addr.rb +17 -0
- data/lib/tapyrus/message/get_block_txn.rb +27 -0
- data/lib/tapyrus/message/get_blocks.rb +29 -0
- data/lib/tapyrus/message/get_data.rb +21 -0
- data/lib/tapyrus/message/get_headers.rb +28 -0
- data/lib/tapyrus/message/header_and_short_ids.rb +57 -0
- data/lib/tapyrus/message/headers.rb +35 -0
- data/lib/tapyrus/message/headers_parser.rb +24 -0
- data/lib/tapyrus/message/inv.rb +21 -0
- data/lib/tapyrus/message/inventories_parser.rb +23 -0
- data/lib/tapyrus/message/inventory.rb +51 -0
- data/lib/tapyrus/message/mem_pool.rb +17 -0
- data/lib/tapyrus/message/merkle_block.rb +42 -0
- data/lib/tapyrus/message/network_addr.rb +63 -0
- data/lib/tapyrus/message/not_found.rb +21 -0
- data/lib/tapyrus/message/ping.rb +30 -0
- data/lib/tapyrus/message/pong.rb +26 -0
- data/lib/tapyrus/message/prefilled_tx.rb +29 -0
- data/lib/tapyrus/message/reject.rb +46 -0
- data/lib/tapyrus/message/send_cmpct.rb +43 -0
- data/lib/tapyrus/message/send_headers.rb +16 -0
- data/lib/tapyrus/message/tx.rb +30 -0
- data/lib/tapyrus/message/ver_ack.rb +17 -0
- data/lib/tapyrus/message/version.rb +69 -0
- data/lib/tapyrus/message.rb +70 -0
- data/lib/tapyrus/mnemonic/wordlist/chinese_simplified.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/chinese_traditional.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/english.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/french.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/italian.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/japanese.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/spanish.txt +2048 -0
- data/lib/tapyrus/mnemonic.rb +77 -0
- data/lib/tapyrus/network/connection.rb +73 -0
- data/lib/tapyrus/network/message_handler.rb +241 -0
- data/lib/tapyrus/network/peer.rb +223 -0
- data/lib/tapyrus/network/peer_discovery.rb +42 -0
- data/lib/tapyrus/network/pool.rb +135 -0
- data/lib/tapyrus/network.rb +13 -0
- data/lib/tapyrus/node/cli.rb +112 -0
- data/lib/tapyrus/node/configuration.rb +38 -0
- data/lib/tapyrus/node/spv.rb +79 -0
- data/lib/tapyrus/node.rb +7 -0
- data/lib/tapyrus/opcodes.rb +178 -0
- data/lib/tapyrus/out_point.rb +44 -0
- data/lib/tapyrus/rpc/http_server.rb +65 -0
- data/lib/tapyrus/rpc/request_handler.rb +150 -0
- data/lib/tapyrus/rpc/tapyrus_core_client.rb +72 -0
- data/lib/tapyrus/rpc.rb +7 -0
- data/lib/tapyrus/script/multisig.rb +92 -0
- data/lib/tapyrus/script/script.rb +551 -0
- data/lib/tapyrus/script/script_error.rb +111 -0
- data/lib/tapyrus/script/script_interpreter.rb +668 -0
- data/lib/tapyrus/script/tx_checker.rb +81 -0
- data/lib/tapyrus/script_witness.rb +38 -0
- data/lib/tapyrus/secp256k1/native.rb +174 -0
- data/lib/tapyrus/secp256k1/ruby.rb +123 -0
- data/lib/tapyrus/secp256k1.rb +12 -0
- data/lib/tapyrus/slip39/share.rb +122 -0
- data/lib/tapyrus/slip39/sss.rb +245 -0
- data/lib/tapyrus/slip39/wordlist/english.txt +1024 -0
- data/lib/tapyrus/slip39.rb +93 -0
- data/lib/tapyrus/store/chain_entry.rb +67 -0
- data/lib/tapyrus/store/db/level_db.rb +98 -0
- data/lib/tapyrus/store/db.rb +9 -0
- data/lib/tapyrus/store/spv_chain.rb +101 -0
- data/lib/tapyrus/store.rb +9 -0
- data/lib/tapyrus/tx.rb +347 -0
- data/lib/tapyrus/tx_in.rb +89 -0
- data/lib/tapyrus/tx_out.rb +74 -0
- data/lib/tapyrus/util.rb +133 -0
- data/lib/tapyrus/validation.rb +115 -0
- data/lib/tapyrus/version.rb +3 -0
- data/lib/tapyrus/wallet/account.rb +151 -0
- data/lib/tapyrus/wallet/base.rb +162 -0
- data/lib/tapyrus/wallet/db.rb +81 -0
- data/lib/tapyrus/wallet/master_key.rb +110 -0
- data/lib/tapyrus/wallet.rb +8 -0
- data/lib/tapyrus.rb +219 -0
- data/tapyrusrb.conf.sample +0 -0
- data/tapyrusrb.gemspec +47 -0
- metadata +451 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
module Tapyrus
|
|
2
|
+
|
|
3
|
+
# Integers modulo the order of the curve(secp256k1)
|
|
4
|
+
CURVE_ORDER = ECDSA::Group::Secp256k1.order
|
|
5
|
+
|
|
6
|
+
# BIP32 Extended private key
|
|
7
|
+
class ExtKey
|
|
8
|
+
|
|
9
|
+
attr_accessor :ver
|
|
10
|
+
attr_accessor :depth
|
|
11
|
+
attr_accessor :number
|
|
12
|
+
attr_accessor :chain_code
|
|
13
|
+
attr_accessor :key # Tapyrus::Key
|
|
14
|
+
attr_accessor :parent_fingerprint
|
|
15
|
+
|
|
16
|
+
# generate master key from seed.
|
|
17
|
+
# @params [String] seed a seed data with hex format.
|
|
18
|
+
def self.generate_master(seed)
|
|
19
|
+
ext_key = ExtKey.new
|
|
20
|
+
ext_key.depth = ext_key.number = 0
|
|
21
|
+
ext_key.parent_fingerprint = '00000000'
|
|
22
|
+
l = Tapyrus.hmac_sha512('Bitcoin seed', seed.htb)
|
|
23
|
+
left = l[0..31].bth.to_i(16)
|
|
24
|
+
raise 'invalid key' if left >= CURVE_ORDER || left == 0
|
|
25
|
+
ext_key.key = Tapyrus::Key.new(priv_key: l[0..31].bth, key_type: Tapyrus::Key::TYPES[:compressed])
|
|
26
|
+
ext_key.chain_code = l[32..-1]
|
|
27
|
+
ext_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# get ExtPubkey from priv_key
|
|
31
|
+
def ext_pubkey
|
|
32
|
+
k = ExtPubkey.new
|
|
33
|
+
k.depth = depth
|
|
34
|
+
k.number = number
|
|
35
|
+
k.parent_fingerprint = parent_fingerprint
|
|
36
|
+
k.chain_code = chain_code
|
|
37
|
+
k.pubkey = key.pubkey
|
|
38
|
+
k.ver = priv_ver_to_pub_ver
|
|
39
|
+
k
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# serialize extended private key
|
|
43
|
+
def to_payload
|
|
44
|
+
version.htb << [depth].pack('C') << parent_fingerprint.htb <<
|
|
45
|
+
[number].pack('N') << chain_code << [0x00].pack('C') << key.priv_key.htb
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Base58 encoded extended private key
|
|
49
|
+
def to_base58
|
|
50
|
+
h = to_payload.bth
|
|
51
|
+
hex = h + Tapyrus.calc_checksum(h)
|
|
52
|
+
Base58.encode(hex)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# get private key(hex)
|
|
56
|
+
def priv
|
|
57
|
+
key.priv_key
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# get public key(hex)
|
|
61
|
+
def pub
|
|
62
|
+
key.pubkey
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def hash160
|
|
66
|
+
Tapyrus.hash160(pub)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# get address
|
|
70
|
+
def addr
|
|
71
|
+
ext_pubkey.addr
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# get key identifier
|
|
75
|
+
def identifier
|
|
76
|
+
Tapyrus.hash160(key.pubkey)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# get fingerprint
|
|
80
|
+
def fingerprint
|
|
81
|
+
identifier.slice(0..7)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# whether hardened key.
|
|
85
|
+
def hardened?
|
|
86
|
+
number >= Tapyrus::HARDENED_THRESHOLD
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# derive new key
|
|
90
|
+
# @param [Integer] number a child index
|
|
91
|
+
# @param [Boolean] harden whether hardened key or not. If true, 2^31 is added to +number+.
|
|
92
|
+
# @return [Tapyrus::ExtKey] derived new key.
|
|
93
|
+
def derive(number, harden = false)
|
|
94
|
+
number += Tapyrus::HARDENED_THRESHOLD if harden
|
|
95
|
+
new_key = ExtKey.new
|
|
96
|
+
new_key.depth = depth + 1
|
|
97
|
+
new_key.number = number
|
|
98
|
+
new_key.parent_fingerprint = fingerprint
|
|
99
|
+
if number > (Tapyrus::HARDENED_THRESHOLD - 1)
|
|
100
|
+
data = [0x00].pack('C') << key.priv_key.htb << [number].pack('N')
|
|
101
|
+
else
|
|
102
|
+
data = key.pubkey.htb << [number].pack('N')
|
|
103
|
+
end
|
|
104
|
+
l = Tapyrus.hmac_sha512(chain_code, data)
|
|
105
|
+
left = l[0..31].bth.to_i(16)
|
|
106
|
+
raise 'invalid key' if left >= CURVE_ORDER
|
|
107
|
+
child_priv = (left + key.priv_key.to_i(16)) % CURVE_ORDER
|
|
108
|
+
raise 'invalid key ' if child_priv >= CURVE_ORDER
|
|
109
|
+
new_key.key = Tapyrus::Key.new(
|
|
110
|
+
priv_key: child_priv.to_even_length_hex.rjust(64, '0'), key_type: key_type)
|
|
111
|
+
new_key.chain_code = l[32..-1]
|
|
112
|
+
new_key.ver = version
|
|
113
|
+
new_key
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# get version bytes using serialization format
|
|
117
|
+
def version
|
|
118
|
+
return ExtKey.version_from_purpose(number) if depth == 1
|
|
119
|
+
ver ? ver : Tapyrus.chain_params.extended_privkey_version
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# get key type defined by BIP-178 using version.
|
|
123
|
+
def key_type
|
|
124
|
+
v = version
|
|
125
|
+
case v
|
|
126
|
+
when Tapyrus.chain_params.bip49_privkey_p2wpkh_p2sh_version
|
|
127
|
+
Tapyrus::Key::TYPES[:p2wpkh_p2sh]
|
|
128
|
+
when Tapyrus.chain_params.bip84_privkey_p2wpkh_version
|
|
129
|
+
Tapyrus::Key::TYPES[:p2wpkh]
|
|
130
|
+
when Tapyrus.chain_params.extended_privkey_version
|
|
131
|
+
Tapyrus::Key::TYPES[:compressed]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ==(other)
|
|
136
|
+
to_payload == other.to_payload
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.parse_from_payload(payload)
|
|
140
|
+
buf = StringIO.new(payload)
|
|
141
|
+
ext_key = ExtKey.new
|
|
142
|
+
ext_key.ver = buf.read(4).bth # version
|
|
143
|
+
raise 'An unsupported version byte was specified.' unless ExtKey.support_version?(ext_key.ver)
|
|
144
|
+
ext_key.depth = buf.read(1).unpack('C').first
|
|
145
|
+
ext_key.parent_fingerprint = buf.read(4).bth
|
|
146
|
+
ext_key.number = buf.read(4).unpack('N').first
|
|
147
|
+
ext_key.chain_code = buf.read(32)
|
|
148
|
+
buf.read(1) # 0x00
|
|
149
|
+
ext_key.key = Tapyrus::Key.new(priv_key: buf.read(32).bth, key_type: Tapyrus::Key::TYPES[:compressed])
|
|
150
|
+
ext_key
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# import private key from Base58 private key address
|
|
154
|
+
def self.from_base58(address)
|
|
155
|
+
ExtKey.parse_from_payload(Base58.decode(address).htb)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# get version bytes from purpose' value.
|
|
159
|
+
def self.version_from_purpose(purpose)
|
|
160
|
+
v = purpose - Tapyrus::HARDENED_THRESHOLD
|
|
161
|
+
case v
|
|
162
|
+
when 49
|
|
163
|
+
Tapyrus.chain_params.bip49_privkey_p2wpkh_p2sh_version
|
|
164
|
+
when 84
|
|
165
|
+
Tapyrus.chain_params.bip84_privkey_p2wpkh_version
|
|
166
|
+
else
|
|
167
|
+
Tapyrus.chain_params.extended_privkey_version
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# check whether +version+ is supported version bytes.
|
|
172
|
+
def self.support_version?(version)
|
|
173
|
+
p = Tapyrus.chain_params
|
|
174
|
+
[p.bip49_privkey_p2wpkh_p2sh_version, p.bip84_privkey_p2wpkh_version, p.extended_privkey_version].include?(version)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# convert privkey version to pubkey version
|
|
178
|
+
def priv_ver_to_pub_ver
|
|
179
|
+
case version
|
|
180
|
+
when Tapyrus.chain_params.bip49_privkey_p2wpkh_p2sh_version
|
|
181
|
+
Tapyrus.chain_params.bip49_pubkey_p2wpkh_p2sh_version
|
|
182
|
+
when Tapyrus.chain_params.bip84_privkey_p2wpkh_version
|
|
183
|
+
Tapyrus.chain_params.bip84_pubkey_p2wpkh_version
|
|
184
|
+
else
|
|
185
|
+
Tapyrus.chain_params.extended_pubkey_version
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# BIP-32 Extended public key
|
|
192
|
+
class ExtPubkey
|
|
193
|
+
|
|
194
|
+
attr_accessor :ver
|
|
195
|
+
attr_accessor :depth
|
|
196
|
+
attr_accessor :number
|
|
197
|
+
attr_accessor :chain_code
|
|
198
|
+
attr_accessor :pubkey # hex format
|
|
199
|
+
attr_accessor :parent_fingerprint
|
|
200
|
+
|
|
201
|
+
# serialize extended pubkey
|
|
202
|
+
def to_payload
|
|
203
|
+
version.htb << [depth].pack('C') <<
|
|
204
|
+
parent_fingerprint.htb << [number].pack('N') << chain_code << pub.htb
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def pub
|
|
208
|
+
pubkey
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def hash160
|
|
212
|
+
Tapyrus.hash160(pub)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# get address
|
|
216
|
+
def addr
|
|
217
|
+
case version
|
|
218
|
+
when Tapyrus.chain_params.bip49_pubkey_p2wpkh_p2sh_version
|
|
219
|
+
key.to_nested_p2wpkh
|
|
220
|
+
when Tapyrus.chain_params.bip84_pubkey_p2wpkh_version
|
|
221
|
+
key.to_p2wpkh
|
|
222
|
+
else
|
|
223
|
+
key.to_p2pkh
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# get key object
|
|
228
|
+
# @return [Tapyrus::Key]
|
|
229
|
+
def key
|
|
230
|
+
Tapyrus::Key.new(pubkey: pubkey, key_type: key_type)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# get key identifier
|
|
234
|
+
def identifier
|
|
235
|
+
Tapyrus.hash160(pub)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# get fingerprint
|
|
239
|
+
def fingerprint
|
|
240
|
+
identifier.slice(0..7)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Base58 encoded extended pubkey
|
|
244
|
+
def to_base58
|
|
245
|
+
h = to_payload.bth
|
|
246
|
+
hex = h + Tapyrus.calc_checksum(h)
|
|
247
|
+
Base58.encode(hex)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# whether hardened key.
|
|
251
|
+
def hardened?
|
|
252
|
+
number >= Tapyrus::HARDENED_THRESHOLD
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# derive child key
|
|
256
|
+
def derive(number)
|
|
257
|
+
new_key = ExtPubkey.new
|
|
258
|
+
new_key.depth = depth + 1
|
|
259
|
+
new_key.number = number
|
|
260
|
+
new_key.parent_fingerprint = fingerprint
|
|
261
|
+
raise 'hardened key is not support' if number > (Tapyrus::HARDENED_THRESHOLD - 1)
|
|
262
|
+
data = pub.htb << [number].pack('N')
|
|
263
|
+
l = Tapyrus.hmac_sha512(chain_code, data)
|
|
264
|
+
left = l[0..31].bth.to_i(16)
|
|
265
|
+
raise 'invalid key' if left >= CURVE_ORDER
|
|
266
|
+
p1 = Tapyrus::Secp256k1::GROUP.generator.multiply_by_scalar(left)
|
|
267
|
+
p2 = Tapyrus::Key.new(pubkey: pubkey, key_type: key_type).to_point
|
|
268
|
+
new_key.pubkey = ECDSA::Format::PointOctetString.encode(p1 + p2, compression: true).bth
|
|
269
|
+
new_key.chain_code = l[32..-1]
|
|
270
|
+
new_key.ver = version
|
|
271
|
+
new_key
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# get version bytes using serialization format
|
|
275
|
+
def version
|
|
276
|
+
return ExtPubkey.version_from_purpose(number) if depth == 1
|
|
277
|
+
ver ? ver : Tapyrus.chain_params.extended_pubkey_version
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# get key type defined by BIP-178 using version.
|
|
281
|
+
def key_type
|
|
282
|
+
v = version
|
|
283
|
+
case v
|
|
284
|
+
when Tapyrus.chain_params.bip49_pubkey_p2wpkh_p2sh_version
|
|
285
|
+
Tapyrus::Key::TYPES[:p2wpkh_p2sh]
|
|
286
|
+
when Tapyrus.chain_params.bip84_pubkey_p2wpkh_version
|
|
287
|
+
Tapyrus::Key::TYPES[:p2wpkh]
|
|
288
|
+
when Tapyrus.chain_params.extended_pubkey_version
|
|
289
|
+
Tapyrus::Key::TYPES[:compressed]
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def ==(other)
|
|
294
|
+
to_payload == other.to_payload
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.parse_from_payload(payload)
|
|
298
|
+
buf = StringIO.new(payload)
|
|
299
|
+
ext_pubkey = ExtPubkey.new
|
|
300
|
+
ext_pubkey.ver = buf.read(4).bth # version
|
|
301
|
+
raise 'An unsupported version byte was specified.' unless ExtPubkey.support_version?(ext_pubkey.ver)
|
|
302
|
+
ext_pubkey.depth = buf.read(1).unpack('C').first
|
|
303
|
+
ext_pubkey.parent_fingerprint = buf.read(4).bth
|
|
304
|
+
ext_pubkey.number = buf.read(4).unpack('N').first
|
|
305
|
+
ext_pubkey.chain_code = buf.read(32)
|
|
306
|
+
ext_pubkey.pubkey = buf.read(33).bth
|
|
307
|
+
ext_pubkey
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# import pub key from Base58 private key address
|
|
312
|
+
def self.from_base58(address)
|
|
313
|
+
ExtPubkey.parse_from_payload(Base58.decode(address).htb)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# get version bytes from purpose' value.
|
|
317
|
+
def self.version_from_purpose(purpose)
|
|
318
|
+
v = purpose - Tapyrus::HARDENED_THRESHOLD
|
|
319
|
+
case v
|
|
320
|
+
when 49
|
|
321
|
+
Tapyrus.chain_params.bip49_pubkey_p2wpkh_p2sh_version
|
|
322
|
+
when 84
|
|
323
|
+
Tapyrus.chain_params.bip84_pubkey_p2wpkh_version
|
|
324
|
+
else
|
|
325
|
+
Tapyrus.chain_params.extended_pubkey_version
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# check whether +version+ is supported version bytes.
|
|
330
|
+
def self.support_version?(version)
|
|
331
|
+
p = Tapyrus.chain_params
|
|
332
|
+
[p.bip49_pubkey_p2wpkh_p2sh_version, p.bip84_pubkey_p2wpkh_version, p.extended_pubkey_version].include?(version)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
end
|
data/lib/tapyrus/key.rb
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Porting part of the code from bitcoin-ruby. see the license.
|
|
2
|
+
# https://github.com/lian/bitcoin-ruby/blob/master/COPYING
|
|
3
|
+
|
|
4
|
+
module Tapyrus
|
|
5
|
+
|
|
6
|
+
# tapyrus key class
|
|
7
|
+
class Key
|
|
8
|
+
|
|
9
|
+
PUBLIC_KEY_SIZE = 65
|
|
10
|
+
COMPRESSED_PUBLIC_KEY_SIZE = 33
|
|
11
|
+
SIGNATURE_SIZE = 72
|
|
12
|
+
COMPACT_SIGNATURE_SIZE = 65
|
|
13
|
+
|
|
14
|
+
attr_accessor :priv_key
|
|
15
|
+
attr_accessor :pubkey
|
|
16
|
+
attr_accessor :key_type
|
|
17
|
+
attr_reader :secp256k1_module
|
|
18
|
+
|
|
19
|
+
TYPES = {uncompressed: 0x00, compressed: 0x01, p2pkh: 0x10, p2wpkh: 0x11, p2wpkh_p2sh: 0x12}
|
|
20
|
+
|
|
21
|
+
MIN_PRIV_KEY_MOD_ORDER = 0x01
|
|
22
|
+
# Order of secp256k1's generator minus 1.
|
|
23
|
+
MAX_PRIV_KEY_MOD_ORDER = ECDSA::Group::Secp256k1.order - 1
|
|
24
|
+
|
|
25
|
+
# initialize private key
|
|
26
|
+
# @param [String] priv_key a private key with hex format.
|
|
27
|
+
# @param [String] pubkey a public key with hex format.
|
|
28
|
+
# @param [Integer] key_type a key type which determine address type.
|
|
29
|
+
# @param [Boolean] compressed [Deprecated] whether public key is compressed.
|
|
30
|
+
# @return [Tapyrus::Key] a key object.
|
|
31
|
+
def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true)
|
|
32
|
+
puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
|
|
33
|
+
if key_type
|
|
34
|
+
@key_type = key_type
|
|
35
|
+
compressed = @key_type != TYPES[:uncompressed]
|
|
36
|
+
else
|
|
37
|
+
@key_type = compressed ? TYPES[:compressed] : TYPES[:uncompressed]
|
|
38
|
+
end
|
|
39
|
+
@secp256k1_module = Tapyrus.secp_impl
|
|
40
|
+
@priv_key = priv_key
|
|
41
|
+
if @priv_key
|
|
42
|
+
raise ArgumentError, 'private key is not on curve' unless validate_private_key_range(@priv_key)
|
|
43
|
+
end
|
|
44
|
+
if pubkey
|
|
45
|
+
@pubkey = pubkey
|
|
46
|
+
else
|
|
47
|
+
@pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# generate key pair
|
|
52
|
+
def self.generate(key_type = TYPES[:compressed])
|
|
53
|
+
priv_key, pubkey = Tapyrus.secp_impl.generate_key_pair
|
|
54
|
+
new(priv_key: priv_key, pubkey: pubkey, key_type: key_type)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# import private key from wif format
|
|
58
|
+
# https://en.bitcoin.it/wiki/Wallet_import_format
|
|
59
|
+
def self.from_wif(wif)
|
|
60
|
+
hex = Base58.decode(wif)
|
|
61
|
+
raise ArgumentError, 'data is too short' if hex.htb.bytesize < 4
|
|
62
|
+
version = hex[0..1]
|
|
63
|
+
data = hex[2...-8].htb
|
|
64
|
+
checksum = hex[-8..-1]
|
|
65
|
+
raise ArgumentError, 'invalid version' unless version == Tapyrus.chain_params.privkey_version
|
|
66
|
+
raise ArgumentError, 'invalid checksum' unless Tapyrus.calc_checksum(version + data.bth) == checksum
|
|
67
|
+
key_len = data.bytesize
|
|
68
|
+
if key_len == COMPRESSED_PUBLIC_KEY_SIZE && data[-1].unpack('C').first == 1
|
|
69
|
+
key_type = TYPES[:compressed]
|
|
70
|
+
data = data[0..-2]
|
|
71
|
+
elsif key_len == 32
|
|
72
|
+
key_type = TYPES[:uncompressed]
|
|
73
|
+
else
|
|
74
|
+
raise ArgumentError, 'Wrong number of bytes for a private key, not 32 or 33'
|
|
75
|
+
end
|
|
76
|
+
new(priv_key: data.bth, key_type: key_type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# export private key with wif format
|
|
80
|
+
def to_wif
|
|
81
|
+
version = Tapyrus.chain_params.privkey_version
|
|
82
|
+
hex = version + priv_key
|
|
83
|
+
hex += '01' if compressed?
|
|
84
|
+
hex += Tapyrus.calc_checksum(hex)
|
|
85
|
+
Base58.encode(hex)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# sign +data+ with private key
|
|
89
|
+
# @param [String] data a data to be signed with binary format
|
|
90
|
+
# @param [Boolean] low_r flag to apply low-R.
|
|
91
|
+
# @param [String] extra_entropy the extra entropy for rfc6979.
|
|
92
|
+
# @return [String] signature data with binary format
|
|
93
|
+
def sign(data, low_r = true, extra_entropy = nil)
|
|
94
|
+
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
|
|
95
|
+
if low_r && !sig_has_low_r?(sig)
|
|
96
|
+
counter = 1
|
|
97
|
+
until sig_has_low_r?(sig)
|
|
98
|
+
extra_entropy = [counter].pack('I*').bth.ljust(64, '0').htb
|
|
99
|
+
sig = secp256k1_module.sign_data(data, priv_key, extra_entropy)
|
|
100
|
+
counter += 1
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
sig
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# verify signature using public key
|
|
107
|
+
# @param [String] sig signature data with binary format
|
|
108
|
+
# @param [String] origin original message
|
|
109
|
+
# @return [Boolean] verify result
|
|
110
|
+
def verify(sig, origin)
|
|
111
|
+
return false unless valid_pubkey?
|
|
112
|
+
begin
|
|
113
|
+
sig = ecdsa_signature_parse_der_lax(sig)
|
|
114
|
+
secp256k1_module.verify_sig(origin, sig, pubkey)
|
|
115
|
+
rescue Exception
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# get hash160 public key.
|
|
121
|
+
def hash160
|
|
122
|
+
Tapyrus.hash160(pubkey)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# get pay to pubkey hash address
|
|
126
|
+
# @deprecated
|
|
127
|
+
def to_p2pkh
|
|
128
|
+
Tapyrus::Script.to_p2pkh(hash160).addresses.first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# get pay to witness pubkey hash address
|
|
132
|
+
# @deprecated
|
|
133
|
+
def to_p2wpkh
|
|
134
|
+
Tapyrus::Script.to_p2wpkh(hash160).addresses.first
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# get p2wpkh address nested in p2sh.
|
|
138
|
+
# @deprecated
|
|
139
|
+
def to_nested_p2wpkh
|
|
140
|
+
Tapyrus::Script.to_p2wpkh(hash160).to_p2sh.addresses.first
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def compressed?
|
|
144
|
+
key_type != TYPES[:uncompressed]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# generate pubkey ec point
|
|
148
|
+
# @return [ECDSA::Point]
|
|
149
|
+
def to_point
|
|
150
|
+
p = pubkey
|
|
151
|
+
p ||= generate_pubkey(priv_key, compressed: compressed)
|
|
152
|
+
ECDSA::Format::PointOctetString.decode(p.htb, Tapyrus::Secp256k1::GROUP)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# check +pubkey+ (hex) is compress or uncompress pubkey.
|
|
156
|
+
def self.compress_or_uncompress_pubkey?(pubkey)
|
|
157
|
+
p = pubkey.htb
|
|
158
|
+
return false if p.bytesize < COMPRESSED_PUBLIC_KEY_SIZE
|
|
159
|
+
case p[0]
|
|
160
|
+
when "\x04"
|
|
161
|
+
return false unless p.bytesize == PUBLIC_KEY_SIZE
|
|
162
|
+
when "\x02", "\x03"
|
|
163
|
+
return false unless p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE
|
|
164
|
+
else
|
|
165
|
+
return false
|
|
166
|
+
end
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# check +pubkey+ (hex) is compress pubkey.
|
|
171
|
+
def self.compress_pubkey?(pubkey)
|
|
172
|
+
p = pubkey.htb
|
|
173
|
+
p.bytesize == COMPRESSED_PUBLIC_KEY_SIZE && ["\x02", "\x03"].include?(p[0])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# check +sig+ is low.
|
|
177
|
+
def self.low_signature?(sig)
|
|
178
|
+
s = sig.unpack('C*')
|
|
179
|
+
len_r = s[3]
|
|
180
|
+
len_s = s[5 + len_r]
|
|
181
|
+
val_s = s.slice(6 + len_r, len_s)
|
|
182
|
+
max_mod_half_order = [
|
|
183
|
+
0x7f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
|
184
|
+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
|
185
|
+
0x5d,0x57,0x6e,0x73,0x57,0xa4,0x50,0x1d,
|
|
186
|
+
0xdf,0xe9,0x2f,0x46,0x68,0x1b,0x20,0xa0]
|
|
187
|
+
compare_big_endian(val_s, [0]) > 0 &&
|
|
188
|
+
compare_big_endian(val_s, max_mod_half_order) <= 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# check +sig+ is correct der encoding.
|
|
193
|
+
# This function is consensus-critical since BIP66.
|
|
194
|
+
def self.valid_signature_encoding?(sig)
|
|
195
|
+
return false if sig.bytesize < 9 || sig.bytesize > 73 # Minimum and maximum size check
|
|
196
|
+
|
|
197
|
+
s = sig.unpack('C*')
|
|
198
|
+
|
|
199
|
+
return false if s[0] != 0x30 || s[1] != s.size - 3 # A signature is of type 0x30 (compound). Make sure the length covers the entire signature.
|
|
200
|
+
|
|
201
|
+
len_r = s[3]
|
|
202
|
+
return false if 5 + len_r >= s.size # Make sure the length of the S element is still inside the signature.
|
|
203
|
+
|
|
204
|
+
len_s = s[5 + len_r]
|
|
205
|
+
return false unless len_r + len_s + 7 == s.size #Verify that the length of the signature matches the sum of the length of the elements.
|
|
206
|
+
|
|
207
|
+
return false unless s[2] == 0x02 # Check whether the R element is an integer.
|
|
208
|
+
|
|
209
|
+
return false if len_r == 0 # Zero-length integers are not allowed for R.
|
|
210
|
+
|
|
211
|
+
return false unless s[4] & 0x80 == 0 # Negative numbers are not allowed for R.
|
|
212
|
+
|
|
213
|
+
# Null bytes at the start of R are not allowed, unless R would otherwise be interpreted as a negative number.
|
|
214
|
+
return false if len_r > 1 && (s[4] == 0x00) && (s[5] & 0x80 == 0)
|
|
215
|
+
|
|
216
|
+
return false unless s[len_r + 4] == 0x02 # Check whether the S element is an integer.
|
|
217
|
+
|
|
218
|
+
return false if len_s == 0 # Zero-length integers are not allowed for S.
|
|
219
|
+
return false unless (s[len_r + 6] & 0x80) == 0 # Negative numbers are not allowed for S.
|
|
220
|
+
|
|
221
|
+
# Null bytes at the start of S are not allowed, unless S would otherwise be interpreted as a negative number.
|
|
222
|
+
return false if len_s > 1 && (s[len_r + 6] == 0x00) && (s[len_r + 7] & 0x80 == 0)
|
|
223
|
+
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# fully validate whether this is a valid public key (more expensive than IsValid())
|
|
228
|
+
def fully_valid_pubkey?
|
|
229
|
+
return false unless valid_pubkey?
|
|
230
|
+
begin
|
|
231
|
+
point = ECDSA::Format::PointOctetString.decode(pubkey.htb, ECDSA::Group::Secp256k1)
|
|
232
|
+
ECDSA::Group::Secp256k1.valid_public_key?(point)
|
|
233
|
+
rescue ECDSA::Format::DecodeError
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def self.compare_big_endian(c1, c2)
|
|
241
|
+
c1, c2 = c1.dup, c2.dup # Clone the arrays
|
|
242
|
+
|
|
243
|
+
while c1.size > c2.size
|
|
244
|
+
return 1 if c1.shift > 0
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
while c2.size > c1.size
|
|
248
|
+
return -1 if c2.shift > 0
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
c1.size.times{|idx| return c1[idx] - c2[idx] if c1[idx] != c2[idx] }
|
|
252
|
+
0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# generate publick key from private key
|
|
256
|
+
# @param [String] privkey a private key with string format
|
|
257
|
+
# @param [Boolean] compressed pubkey compressed?
|
|
258
|
+
# @return [String] a pubkey which generate from privkey
|
|
259
|
+
def generate_pubkey(privkey, compressed: true)
|
|
260
|
+
@secp256k1_module.generate_pubkey(privkey, compressed: compressed)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# check private key range.
|
|
264
|
+
def validate_private_key_range(private_key)
|
|
265
|
+
value = private_key.to_i(16)
|
|
266
|
+
MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Supported violations include negative integers, excessive padding, garbage
|
|
270
|
+
# at the end, and overly long length descriptors. This is safe to use in
|
|
271
|
+
# Bitcoin because since the activation of BIP66, signatures are verified to be
|
|
272
|
+
# strict DER before being passed to this module, and we know it supports all
|
|
273
|
+
# violations present in the blockchain before that point.
|
|
274
|
+
def ecdsa_signature_parse_der_lax(sig)
|
|
275
|
+
sig_array = sig.unpack('C*')
|
|
276
|
+
len_r = sig_array[3]
|
|
277
|
+
r = sig_array[4...(len_r+4)].pack('C*').bth
|
|
278
|
+
len_s = sig_array[len_r + 5]
|
|
279
|
+
s = sig_array[(len_r + 6)...(len_r + 6 + len_s)].pack('C*').bth
|
|
280
|
+
ECDSA::Signature.new(r.to_i(16), s.to_i(16)).to_der
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def valid_pubkey?
|
|
284
|
+
!pubkey.nil? && pubkey.size > 0
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# check whether the signature is low-R
|
|
288
|
+
# @param [String] sig the signature data
|
|
289
|
+
# @return [Boolean] result
|
|
290
|
+
def sig_has_low_r?(sig)
|
|
291
|
+
sig[3].bth.to_i(16) == 0x20 && sig[4].bth.to_i(16) < 0x80
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Tapyrus
|
|
2
|
+
module KeyPath
|
|
3
|
+
|
|
4
|
+
# key path convert an array of derive number
|
|
5
|
+
# @param [String] path_string
|
|
6
|
+
# @return [Array[Integer]] key path numbers.
|
|
7
|
+
def parse_key_path(path_string)
|
|
8
|
+
path_string.split('/').map.with_index do|p, index|
|
|
9
|
+
if index == 0
|
|
10
|
+
raise ArgumentError.new("#{path_string} is invalid format.") unless p == 'm'
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
raise ArgumentError.new("#{path_string} is invalid format.") unless p.delete("'") =~ /^[0-9]+$/
|
|
14
|
+
(p[-1] == "'" ? p.delete("'").to_i + Tapyrus::HARDENED_THRESHOLD : p.to_i)
|
|
15
|
+
end[1..-1]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# key path numbers convert to path string.
|
|
19
|
+
# @param [Array[Integer]] key path numbers.
|
|
20
|
+
# @return [String] path string.
|
|
21
|
+
def to_key_path(numbers)
|
|
22
|
+
"m/#{numbers.map{|p| p >= Tapyrus::HARDENED_THRESHOLD ? "#{p - Tapyrus::HARDENED_THRESHOLD}'" : p.to_s}.join('/')}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module Tapyrus
|
|
4
|
+
|
|
5
|
+
# Simple Logger module
|
|
6
|
+
module Logger
|
|
7
|
+
|
|
8
|
+
Format = "%s, [%s#%d #%d] %5s -- %s: %s\n".freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Create a logger with given +name+.log in $HOME/.tapyrusrb/log.
|
|
13
|
+
def create(name, level = ::Logger::INFO)
|
|
14
|
+
dir = "#{Tapyrus.base_dir}/log"
|
|
15
|
+
FileUtils.mkdir_p(dir)
|
|
16
|
+
logger = ::Logger.new(dir + "/#{name}.log", 10)
|
|
17
|
+
logger.level = level
|
|
18
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
19
|
+
Format % [severity[0..0], format_datetime(datetime), $$,
|
|
20
|
+
Thread.current.object_id, severity, progname, msg2str(msg)]
|
|
21
|
+
end
|
|
22
|
+
logger
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.msg2str(msg)
|
|
26
|
+
case msg
|
|
27
|
+
when ::String
|
|
28
|
+
msg
|
|
29
|
+
when ::Exception
|
|
30
|
+
"#{ msg.message } (#{ msg.class })\n" <<
|
|
31
|
+
(msg.backtrace || []).join("\n")
|
|
32
|
+
else
|
|
33
|
+
msg.inspect
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_datetime(time)
|
|
38
|
+
time.strftime(@datetime_format || "%Y-%m-%dT%H:%M:%S.%6N ".freeze)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|