bitcoinrb 1.15.0 → 1.16.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/bitcoinrb.gemspec +5 -4
- data/lib/bitcoin/bip321_uri.rb +95 -32
- data/lib/bitcoin/bip324/ell_swift_pubkey.rb +3 -1
- data/lib/bitcoin/bip324/fs_chacha20.rb +3 -0
- data/lib/bitcoin/bip324.rb +1 -1
- data/lib/bitcoin/bip85_entropy.rb +79 -29
- data/lib/bitcoin/bit_stream.rb +2 -2
- data/lib/bitcoin/block.rb +46 -7
- data/lib/bitcoin/block_filter.rb +3 -2
- data/lib/bitcoin/block_header.rb +31 -8
- data/lib/bitcoin/bloom_filter.rb +5 -2
- data/lib/bitcoin/chainparams/regtest.yml +0 -1
- data/lib/bitcoin/chainparams/signet.yml +1 -1
- data/lib/bitcoin/constants.rb +13 -3
- data/lib/bitcoin/descriptor/combo.rb +13 -1
- data/lib/bitcoin/descriptor/expression.rb +35 -15
- data/lib/bitcoin/descriptor/multi.rb +9 -1
- data/lib/bitcoin/descriptor/multi_a.rb +4 -0
- data/lib/bitcoin/descriptor/musig.rb +1 -0
- data/lib/bitcoin/descriptor/pk.rb +7 -0
- data/lib/bitcoin/descriptor/pkh.rb +5 -0
- data/lib/bitcoin/descriptor/raw.rb +1 -1
- data/lib/bitcoin/descriptor/sh.rb +1 -0
- data/lib/bitcoin/descriptor/sp.rb +2 -2
- data/lib/bitcoin/descriptor/tr.rb +29 -3
- data/lib/bitcoin/descriptor/wpkh.rb +1 -0
- data/lib/bitcoin/descriptor/wsh.rb +1 -0
- data/lib/bitcoin/descriptor.rb +59 -35
- data/lib/bitcoin/ext/array_ext.rb +3 -0
- data/lib/bitcoin/ext/ecdsa.rb +7 -1
- data/lib/bitcoin/ext_key.rb +10 -0
- data/lib/bitcoin/gcs_filter.rb +21 -6
- data/lib/bitcoin/key.rb +66 -14
- data/lib/bitcoin/key_path.rb +8 -5
- data/lib/bitcoin/message/addr.rb +1 -0
- data/lib/bitcoin/message/addr_v2.rb +1 -0
- data/lib/bitcoin/message/base.rb +8 -1
- data/lib/bitcoin/message/block.rb +4 -8
- data/lib/bitcoin/message/block_transaction_request.rb +11 -8
- data/lib/bitcoin/message/block_transactions.rb +2 -1
- data/lib/bitcoin/message/cf_parser.rb +1 -0
- data/lib/bitcoin/message/cfcheckpt.rb +3 -3
- data/lib/bitcoin/message/cfheaders.rb +5 -4
- data/lib/bitcoin/message/cfilter.rb +3 -3
- data/lib/bitcoin/message/error.rb +3 -1
- data/lib/bitcoin/message/fee_filter.rb +1 -0
- data/lib/bitcoin/message/filter_add.rb +3 -1
- data/lib/bitcoin/message/filter_load.rb +4 -4
- data/lib/bitcoin/message/get_cfcheckpt.rb +1 -0
- data/lib/bitcoin/message/header_and_short_ids.rb +26 -11
- data/lib/bitcoin/message/headers.rb +10 -2
- data/lib/bitcoin/message/headers_parser.rb +4 -2
- data/lib/bitcoin/message/inventories_parser.rb +2 -1
- data/lib/bitcoin/message/inventory.rb +15 -6
- data/lib/bitcoin/message/merkle_block.rb +4 -4
- data/lib/bitcoin/message/network_addr.rb +13 -13
- data/lib/bitcoin/message/ping.rb +1 -0
- data/lib/bitcoin/message/pong.rb +1 -0
- data/lib/bitcoin/message/reject.rb +1 -0
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/send_tx_rcncl.rb +1 -1
- data/lib/bitcoin/message/version.rb +23 -8
- data/lib/bitcoin/message.rb +15 -0
- data/lib/bitcoin/message_sign.rb +44 -6
- data/lib/bitcoin/mnemonic.rb +8 -0
- data/lib/bitcoin/out_point.rb +6 -2
- data/lib/bitcoin/partial_tree.rb +41 -28
- data/lib/bitcoin/payment_code.rb +26 -8
- data/lib/bitcoin/psbt/hd_key_path.rb +1 -2
- data/lib/bitcoin/psbt/input.rb +199 -48
- data/lib/bitcoin/psbt/key_origin_info.rb +2 -0
- data/lib/bitcoin/psbt/output.rb +9 -6
- data/lib/bitcoin/psbt/proprietary.rb +6 -3
- data/lib/bitcoin/psbt/tx.rb +9 -4
- data/lib/bitcoin/psbt.rb +46 -0
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +54 -13
- data/lib/bitcoin/rpc/error.rb +43 -0
- data/lib/bitcoin/rpc.rb +1 -0
- data/lib/bitcoin/script/script.rb +114 -33
- data/lib/bitcoin/script/script_interpreter.rb +11 -6
- data/lib/bitcoin/script/tx_checker.rb +7 -2
- data/lib/bitcoin/script_witness.rb +5 -1
- data/lib/bitcoin/secp256k1/ruby.rb +10 -4
- data/lib/bitcoin/silent_payment.rb +6 -3
- data/lib/bitcoin/slip39/share.rb +35 -9
- data/lib/bitcoin/slip39/sss.rb +26 -11
- data/lib/bitcoin/slip39.rb +13 -5
- data/lib/bitcoin/taproot/control_block.rb +6 -1
- data/lib/bitcoin/taproot/custom_depth_builder.rb +12 -0
- data/lib/bitcoin/tx.rb +35 -14
- data/lib/bitcoin/tx_in.rb +2 -2
- data/lib/bitcoin/tx_out.rb +2 -2
- data/lib/bitcoin/util.rb +49 -28
- data/lib/bitcoin/validation.rb +64 -12
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet/account.rb +35 -11
- data/lib/bitcoin/wallet/base.rb +40 -15
- data/lib/bitcoin/wallet/utxo.rb +22 -4
- data/lib/bitcoin.rb +16 -4
- metadata +24 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e3ac48f9f618bd15f40353c7ad8bf122f0600d2fc6ab53d07aaa0827cb8a766
|
|
4
|
+
data.tar.gz: 47c187e47c83ec2e13343200cd48ebee2690b4b2b67485115a199473390fdfa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af9833c6b9e3592ef67d41a0df565c0993654892629ff94842d111e1e04458f3c5386474735fa6056def1b8b3c6649dfc383688adb22fc041d52999e99eb26b5
|
|
7
|
+
data.tar.gz: '08a894cf0539aad6a5e01fc37c261f674f47e74c78c8491e8bbc600b451435c163aacc84816065dc35ccc7369074ce86fa3cb8aa6b3349297b2fb4e0db69e535'
|
data/bitcoinrb.gemspec
CHANGED
|
@@ -20,14 +20,15 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
22
|
|
|
23
|
-
spec.add_runtime_dependency 'ecdsa_ext', '~> 0.
|
|
23
|
+
spec.add_runtime_dependency 'ecdsa_ext', '~> 0.6.0'
|
|
24
24
|
spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
|
|
25
|
-
spec.add_runtime_dependency 'bech32', '>= 1.
|
|
25
|
+
spec.add_runtime_dependency 'bech32', '>= 1.6.0'
|
|
26
26
|
spec.add_runtime_dependency 'siphash'
|
|
27
|
-
spec.add_runtime_dependency 'bip-schnorr', '>= 0.
|
|
27
|
+
spec.add_runtime_dependency 'bip-schnorr', '>= 0.8.0'
|
|
28
28
|
spec.add_runtime_dependency 'base32', '>= 0.3.4'
|
|
29
29
|
spec.add_runtime_dependency 'base64', '~> 0.2.0'
|
|
30
|
-
spec.add_runtime_dependency '
|
|
30
|
+
spec.add_runtime_dependency 'bigdecimal'
|
|
31
|
+
spec.add_runtime_dependency 'secp256k1rb', '0.8.1'
|
|
31
32
|
spec.add_runtime_dependency 'logger'
|
|
32
33
|
spec.add_runtime_dependency 'merkle', '1.0.0'
|
|
33
34
|
spec.add_runtime_dependency 'dnsruby', '1.73.0'
|
data/lib/bitcoin/bip321_uri.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'uri'
|
|
2
2
|
require 'cgi'
|
|
3
|
+
require 'bigdecimal'
|
|
3
4
|
|
|
4
5
|
module Bitcoin
|
|
5
6
|
# BIP-321 URI
|
|
@@ -42,6 +43,7 @@ module Bitcoin
|
|
|
42
43
|
if amount
|
|
43
44
|
amount = BigDecimal(amount) if amount.is_a?(Integer)
|
|
44
45
|
raise ArgumentError, "amount must be BigDecimal or integer." unless amount.is_a?(BigDecimal)
|
|
46
|
+
validate_amount!(amount)
|
|
45
47
|
@amount = amount
|
|
46
48
|
end
|
|
47
49
|
raise ArgumentError, "label must be string." if label && !label.is_a?(String)
|
|
@@ -81,54 +83,105 @@ module Bitcoin
|
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
raise ArgumentError, "other_params must be Hash." unless other_params.is_a?(Hash)
|
|
84
|
-
other_params.
|
|
85
|
-
raise ArgumentError, '
|
|
86
|
+
other_params.each do |key, value|
|
|
87
|
+
raise ArgumentError, 'other_params keys must be String.' unless key.is_a?(String)
|
|
88
|
+
raise ArgumentError, 'An unsupported reqparam is included.' if key.downcase.start_with?('req-')
|
|
89
|
+
# A key without a value is nil, and a key given more than once holds its values in an Array.
|
|
90
|
+
values = value.is_a?(Array) ? value : [value]
|
|
91
|
+
unless !values.empty? && values.all? { |v| v.nil? || v.is_a?(String) }
|
|
92
|
+
raise ArgumentError, "other_params['#{key}'] must be a String, nil or an Array of them."
|
|
93
|
+
end
|
|
86
94
|
end
|
|
87
95
|
@other_params = other_params
|
|
96
|
+
|
|
97
|
+
# BIP-321: the address may be left empty only if a query parameter carries a payment instruction.
|
|
98
|
+
# label, message, pop and amount describe a payment but are not one. A key this class does not know
|
|
99
|
+
# may be a payment instruction added after it, so it counts as one.
|
|
100
|
+
unless @addr || @query_addrs.any? || @lightning || @lno || @pay || @sp || @other_params.any?
|
|
101
|
+
raise ArgumentError, 'A URI without an address must have at least one payment instruction.'
|
|
102
|
+
end
|
|
88
103
|
end
|
|
89
104
|
|
|
105
|
+
# The keys which hold one value. BIP-321 forbids a second label, message, pop or amount, and this
|
|
106
|
+
# class holds one value for the other payment instructions it knows.
|
|
107
|
+
SINGLE_VALUE_KEYS = %w[amount label message pop lightning lno pay sp].freeze
|
|
108
|
+
# The largest amount a URI may ask for, in BTC.
|
|
109
|
+
MAX_AMOUNT = BigDecimal(21_000_000)
|
|
110
|
+
|
|
90
111
|
# Parse BIP-321 URI string.
|
|
112
|
+
# The scheme and the query keys are case-insensitive, values are percent-decoded as RFC 3986 says
|
|
113
|
+
# (a '+' stays a '+'), the amount has to match the grammar *digit ["." *digit], and a key which is not
|
|
114
|
+
# known here may appear more than once, in which case its values are collected in an Array.
|
|
91
115
|
# @param [String] BIP-321 URI.
|
|
92
116
|
# @return [Bitcoin::BIP321URI]
|
|
93
117
|
# @raise [ArgumentError]
|
|
94
118
|
def self.parse(uri)
|
|
95
119
|
raise ArgumentError, "uri must be string." unless uri.is_a?(String)
|
|
96
120
|
raise ArgumentError, "Invalid uri scheme." unless uri.downcase.start_with?('bitcoin:')
|
|
97
|
-
|
|
98
|
-
addr
|
|
121
|
+
body, query = uri[8..-1].split('?', 2)
|
|
122
|
+
addr = body.nil? || body.empty? ? nil : body
|
|
123
|
+
params = {}
|
|
124
|
+
others = {}
|
|
99
125
|
req_pop = false
|
|
100
126
|
query_addrs = []
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
(query ? query.split('&') : []).each do |pair|
|
|
128
|
+
next if pair.empty?
|
|
129
|
+
raw_key, raw_value = pair.split('=', 2)
|
|
130
|
+
key = percent_decode(raw_key).downcase
|
|
131
|
+
next if key.empty?
|
|
132
|
+
value = raw_value.nil? ? nil : percent_decode(raw_value)
|
|
133
|
+
if key == 'req-pop'
|
|
134
|
+
req_pop = true
|
|
135
|
+
key = 'pop'
|
|
136
|
+
end
|
|
137
|
+
if %w[bc tb bcrt].include?(key)
|
|
138
|
+
raise ArgumentError, "#{key} not allowed in current network." unless Bitcoin.chain_params.bech32_hrp == key
|
|
139
|
+
# The key names the human readable part of the address it carries.
|
|
140
|
+
raise ArgumentError, "#{key} must hold a bech32 address with the prefix #{key}." unless value.to_s.downcase.start_with?("#{key}1")
|
|
141
|
+
query_addrs << value
|
|
142
|
+
elsif SINGLE_VALUE_KEYS.include?(key)
|
|
143
|
+
raise ArgumentError, "#{key} must not appear twice." if params.key?(key)
|
|
144
|
+
params[key] = value
|
|
145
|
+
else
|
|
146
|
+
raise ArgumentError, 'An unsupported reqparam is included.' if key.start_with?('req-')
|
|
147
|
+
# BIP-321: a key this implementation does not know may appear more than once. The values are kept in
|
|
148
|
+
# order, and a key given without a value is kept as nil.
|
|
149
|
+
others[key] = if others.key?(key)
|
|
150
|
+
values = others[key].is_a?(Array) ? others[key] : [others[key]]
|
|
151
|
+
values + [value]
|
|
152
|
+
else
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
amount = params['amount'] ? parse_amount(params['amount']) : nil
|
|
127
158
|
BIP321URI.new(address: addr, amount: amount, label: params['label'], message: params['message'],
|
|
128
159
|
pop: params['pop'], lightning: params['lightning'], lno: params['lno'],
|
|
129
160
|
pay: params['pay'], sp: params['sp'], other_params: others, req_pop: req_pop, query_addrs: query_addrs)
|
|
130
161
|
end
|
|
131
162
|
|
|
163
|
+
# Parse the value of an amount parameter.
|
|
164
|
+
# @param [String] str The value, which has to be *digit ["." *digit] with at most 8 decimal places.
|
|
165
|
+
# @return [BigDecimal]
|
|
166
|
+
# @raise [ArgumentError]
|
|
167
|
+
def self.parse_amount(str)
|
|
168
|
+
m = str.match(/\A([0-9]*)(?:\.([0-9]*))?\z/)
|
|
169
|
+
raise ArgumentError, "Invalid amount: #{str}" if m.nil? || (m[1].empty? && m[2].to_s.empty?)
|
|
170
|
+
raise ArgumentError, "Invalid amount: #{str}. An amount has at most 8 decimal places." if m[2].to_s.length > 8
|
|
171
|
+
amount = BigDecimal(str.end_with?('.') ? str + '0' : (str.start_with?('.') ? '0' + str : str))
|
|
172
|
+
raise ArgumentError, "Invalid amount: #{str}. It exceeds #{MAX_AMOUNT.to_i} BTC." if amount > MAX_AMOUNT
|
|
173
|
+
amount
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Percent-decode a URI component as RFC 3986 does. Unlike form decoding, a '+' is a literal '+'.
|
|
177
|
+
# @param [String] str
|
|
178
|
+
# @return [String]
|
|
179
|
+
def self.percent_decode(str)
|
|
180
|
+
URI.decode_www_form_component(str.gsub('+', '%2B'))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private_class_method :parse_amount, :percent_decode
|
|
184
|
+
|
|
132
185
|
# Get all addresses contained in the URI body and query parameters
|
|
133
186
|
# @return [Array] An array of address.
|
|
134
187
|
def addresses
|
|
@@ -158,12 +211,22 @@ module Bitcoin
|
|
|
158
211
|
base_params['pay'] = pay if pay
|
|
159
212
|
|
|
160
213
|
all_params = base_params.merge other_params
|
|
161
|
-
params = all_params.
|
|
162
|
-
|
|
214
|
+
params = all_params.flat_map do |k, values|
|
|
215
|
+
values = values.nil? ? [nil] : Array(values) # a key given without a value is written as the key alone
|
|
216
|
+
values.map { |v| v.nil? ? k : "#{k}=#{CGI.escape(v).gsub('+', '%20')}" }
|
|
163
217
|
end
|
|
164
218
|
params += query_addrs.map {|addr| "#{Bitcoin.chain_params.bech32_hrp}=#{addr}"}
|
|
165
219
|
uri << "?#{params.join('&')}" unless params.empty?
|
|
166
220
|
uri
|
|
167
221
|
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# @raise [ArgumentError] If +amount+ is negative, above MAX_AMOUNT or finer than a satoshi.
|
|
226
|
+
def validate_amount!(amount)
|
|
227
|
+
raise ArgumentError, 'amount must not be negative.' if amount.negative?
|
|
228
|
+
raise ArgumentError, "amount must not exceed #{MAX_AMOUNT.to_i} BTC." if amount > MAX_AMOUNT
|
|
229
|
+
raise ArgumentError, 'amount must not have more than 8 decimal places.' unless (amount * 100_000_000).frac.zero?
|
|
230
|
+
end
|
|
168
231
|
end
|
|
169
232
|
end
|
|
@@ -26,7 +26,9 @@ module Bitcoin
|
|
|
26
26
|
u = key[0...32].bth
|
|
27
27
|
t = key[32..-1].bth
|
|
28
28
|
x = BIP324.xswiftec(u, t)
|
|
29
|
-
|
|
29
|
+
# The Y coordinate has the parity of t, as BIP-324 and libsecp256k1 decode it.
|
|
30
|
+
prefix = BIP324::FIELD.mod(t.hex).odd? ? '03' : '02'
|
|
31
|
+
Bitcoin::Key.new(pubkey: "#{prefix}#{x}", key_type: Bitcoin::Key::TYPES[:compressed])
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
@@ -121,6 +121,9 @@ module Bitcoin
|
|
|
121
121
|
if (self.chunk_counter + 1) % rekey_interval == 0
|
|
122
122
|
self.key = key_stream_bytes(32)
|
|
123
123
|
self.block_counter = 0
|
|
124
|
+
# The rest of the current block belongs to the old key. The reference implementation starts the
|
|
125
|
+
# new key with a fresh keystream, and so does Bitcoin Core by seeking the cipher after SetKey.
|
|
126
|
+
self.key_stream = ''.b
|
|
124
127
|
end
|
|
125
128
|
self.chunk_counter += 1
|
|
126
129
|
ret
|
data/lib/bitcoin/bip324.rb
CHANGED
|
@@ -99,7 +99,7 @@ module Bitcoin
|
|
|
99
99
|
# @return [String] ElligatorSwift public key with hex format.
|
|
100
100
|
def xelligatorswift(x)
|
|
101
101
|
loop do
|
|
102
|
-
u = SecureRandom.random_number(1
|
|
102
|
+
u = SecureRandom.random_number(1...FIELD_SIZE).to_s(16) # a non-zero field element
|
|
103
103
|
c = SecureRandom.random_number(CASE_COUNT)
|
|
104
104
|
t = xswiftec_inv(x, u, c)
|
|
105
105
|
unless t.nil?
|
|
@@ -18,23 +18,40 @@ module Bitcoin
|
|
|
18
18
|
self.new(key)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# The alphabet of the Base85 encoding BIP-85 uses, which is the one of RFC 1924 and Python's base64.b85encode.
|
|
22
|
+
BASE85_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~'.freeze
|
|
23
|
+
|
|
21
24
|
# derive entropy
|
|
22
|
-
#
|
|
25
|
+
# The applications of BIP-85 are understood: BIP39 (39'), HD-Seed WIF (2'), XPRV (32'), HEX (128169'),
|
|
26
|
+
# PWD BASE64 (707764') and PWD BASE85 (707785'). For any other application the 64 byte entropy is returned
|
|
27
|
+
# as it is.
|
|
28
|
+
# @param [String] path derive path. Every index has to be hardened.
|
|
23
29
|
# @return [Tuple(String, Object)] a tuple of entropy with hex format and results depending the application.
|
|
30
|
+
# @raise [ArgumentError] If +path+ is not a BIP-85 path, or a parameter of the application is out of range.
|
|
24
31
|
def derive(path)
|
|
25
32
|
raise ArgumentError, "Invalid BIP85 path format." unless path.start_with?("m/83696968'")
|
|
33
|
+
numbers = parse_key_path(path)
|
|
34
|
+
raise ArgumentError, 'Every index of a BIP85 path must be hardened.' unless numbers.all? { |n| n >= HARDENED_THRESHOLD }
|
|
26
35
|
derived_key = root_key
|
|
27
|
-
|
|
36
|
+
numbers.each{|num| derived_key = derived_key.derive(num)}
|
|
28
37
|
derived_key = derived_key.priv
|
|
29
38
|
entropy = Bitcoin.hmac_sha512("bip-entropy-from-k", derived_key.htb).bth
|
|
30
|
-
app_no =
|
|
39
|
+
app_no, *params = numbers[1..-1].map { |n| n - HARDENED_THRESHOLD }
|
|
31
40
|
case app_no
|
|
32
|
-
when
|
|
33
|
-
bip39_entropy(
|
|
34
|
-
when
|
|
41
|
+
when 39
|
|
42
|
+
bip39_entropy(params, entropy)
|
|
43
|
+
when 2
|
|
44
|
+
validate_params!(params, 1)
|
|
35
45
|
hd_seed_entropy(entropy)
|
|
36
|
-
when
|
|
46
|
+
when 32
|
|
47
|
+
validate_params!(params, 1)
|
|
37
48
|
xprv_entropy(entropy)
|
|
49
|
+
when 128169
|
|
50
|
+
hex_entropy(params, entropy)
|
|
51
|
+
when 707764
|
|
52
|
+
pwd_base64_entropy(params, entropy)
|
|
53
|
+
when 707785
|
|
54
|
+
pwd_base85_entropy(params, entropy)
|
|
38
55
|
else
|
|
39
56
|
[entropy, entropy]
|
|
40
57
|
end
|
|
@@ -46,21 +63,21 @@ module Bitcoin
|
|
|
46
63
|
@root_key = root_key
|
|
47
64
|
end
|
|
48
65
|
|
|
66
|
+
# Check that the application has +count+ parameters after its number, the last one being the index.
|
|
67
|
+
def validate_params!(params, count)
|
|
68
|
+
raise ArgumentError, "Invalid BIP85 path format." unless params.size == count
|
|
69
|
+
end
|
|
70
|
+
|
|
49
71
|
# derive BIP39 entropy.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
language = code_to_language(params[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
when "24'"
|
|
60
|
-
entropy[0...64]
|
|
61
|
-
else
|
|
62
|
-
raise ArgumentError, "Word length #{word_len} does not supported."
|
|
63
|
-
end
|
|
72
|
+
# @param [Array[Integer]] params language, word count and index.
|
|
73
|
+
def bip39_entropy(params, entropy)
|
|
74
|
+
validate_params!(params, 3)
|
|
75
|
+
language = code_to_language(params[0])
|
|
76
|
+
words = params[1]
|
|
77
|
+
unless Bitcoin::Mnemonic::WORD_COUNTS.include?(words)
|
|
78
|
+
raise ArgumentError, "Word length #{words}' does not supported."
|
|
79
|
+
end
|
|
80
|
+
entropy = entropy[0...(words / 3 * 8)] # 12 words use 128 bits, and every 3 words add 32 bits.
|
|
64
81
|
mnemonic = Bitcoin::Mnemonic.new(language)
|
|
65
82
|
[entropy, mnemonic.to_mnemonic(entropy)]
|
|
66
83
|
end
|
|
@@ -84,25 +101,58 @@ module Bitcoin
|
|
|
84
101
|
[entropy, ext_key.to_base58]
|
|
85
102
|
end
|
|
86
103
|
|
|
104
|
+
# derive HEX entropy: the first num_bytes bytes, 16 <= num_bytes <= 64.
|
|
105
|
+
# @param [Array[Integer]] params num_bytes and index.
|
|
106
|
+
def hex_entropy(params, entropy)
|
|
107
|
+
validate_params!(params, 2)
|
|
108
|
+
num_bytes = params[0]
|
|
109
|
+
raise ArgumentError, "num_bytes #{num_bytes}' must be between 16 and 64." unless num_bytes.between?(16, 64)
|
|
110
|
+
result = entropy[0...(num_bytes * 2)]
|
|
111
|
+
[result, result]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# derive PWD BASE64: the first pwd_len characters of the Base64 encoding of the entropy, 20 <= pwd_len <= 86.
|
|
115
|
+
# @param [Array[Integer]] params pwd_len and index.
|
|
116
|
+
def pwd_base64_entropy(params, entropy)
|
|
117
|
+
validate_params!(params, 2)
|
|
118
|
+
pwd_len = params[0]
|
|
119
|
+
raise ArgumentError, "pwd_len #{pwd_len}' must be between 20 and 86." unless pwd_len.between?(20, 86)
|
|
120
|
+
[entropy, Base64.strict_encode64(entropy.htb)[0, pwd_len]]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# derive PWD BASE85: the first pwd_len characters of the Base85 encoding of the entropy, 10 <= pwd_len <= 80.
|
|
124
|
+
# @param [Array[Integer]] params pwd_len and index.
|
|
125
|
+
def pwd_base85_entropy(params, entropy)
|
|
126
|
+
validate_params!(params, 2)
|
|
127
|
+
pwd_len = params[0]
|
|
128
|
+
raise ArgumentError, "pwd_len #{pwd_len}' must be between 10 and 80." unless pwd_len.between?(10, 80)
|
|
129
|
+
[entropy, base85(entropy.htb)[0, pwd_len]]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Base85 encode +data+, whose size is a multiple of 4 bytes, as Python's base64.b85encode does.
|
|
133
|
+
def base85(data)
|
|
134
|
+
data.unpack('N*').flat_map { |word| 5.times.map { |i| BASE85_ALPHABET[(word / 85**(4 - i)) % 85] } }.join
|
|
135
|
+
end
|
|
136
|
+
|
|
87
137
|
# convert language code to language string.
|
|
88
138
|
def code_to_language(code)
|
|
89
139
|
case code
|
|
90
|
-
when
|
|
140
|
+
when 0
|
|
91
141
|
"english"
|
|
92
|
-
when
|
|
142
|
+
when 1
|
|
93
143
|
"japanese"
|
|
94
|
-
when
|
|
144
|
+
when 3
|
|
95
145
|
"spanish"
|
|
96
|
-
when
|
|
146
|
+
when 4
|
|
97
147
|
"chinese_simplified"
|
|
98
|
-
when
|
|
148
|
+
when 5
|
|
99
149
|
"chinese_traditional"
|
|
100
|
-
when
|
|
150
|
+
when 6
|
|
101
151
|
"french"
|
|
102
|
-
when
|
|
152
|
+
when 7
|
|
103
153
|
"italian"
|
|
104
154
|
else
|
|
105
|
-
raise ArgumentError, "bitcoinrb does not support language: #{code}"
|
|
155
|
+
raise ArgumentError, "bitcoinrb does not support language: #{code}'"
|
|
106
156
|
end
|
|
107
157
|
end
|
|
108
158
|
|
data/lib/bitcoin/bit_stream.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Bitcoin
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def write(data, nbits)
|
|
18
|
-
raise
|
|
18
|
+
raise ArgumentError, 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
|
|
19
19
|
while nbits > 0
|
|
20
20
|
bits = [8 - offset, nbits].min
|
|
21
21
|
tmp = (data << (64 - nbits)) & 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111
|
|
@@ -49,7 +49,7 @@ module Bitcoin
|
|
|
49
49
|
|
|
50
50
|
# offset
|
|
51
51
|
def read(nbits)
|
|
52
|
-
raise 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
|
|
52
|
+
raise ArgumentError, 'nbits must be between 0 and 64' if nbits < 0 || nbits > 64
|
|
53
53
|
data = 0
|
|
54
54
|
while nbits > 0
|
|
55
55
|
if offset == 8
|
data/lib/bitcoin/block.rb
CHANGED
|
@@ -66,26 +66,62 @@ module Bitcoin
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# check the merkle root in the block header matches merkle root calculated from tx list.
|
|
69
|
+
# A tx list with the duplication of CVE-2012-2459 is reported as invalid even though its root
|
|
70
|
+
# matches, as Bitcoin Core does, since the same root also belongs to the list without the duplicate.
|
|
71
|
+
# A block without any tx has no merkle root, and is not valid (bad-blk-length in Bitcoin Core).
|
|
69
72
|
def valid_merkle_root?
|
|
70
|
-
|
|
73
|
+
return false if transactions.empty?
|
|
74
|
+
!merkle_root_mutated? && calculate_merkle_root == header.merkle_root
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether the tx list is a mutation of another list with the same merkle root (CVE-2012-2459).
|
|
78
|
+
# Duplicating the last tx of a level with an odd count leaves the root unchanged, and it shows up as
|
|
79
|
+
# two identical hashes next to each other at some level of the tree. The last hash of an odd level is
|
|
80
|
+
# paired with itself by construction and is not a mutation. See ComputeMerkleRoot in Bitcoin Core.
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def merkle_root_mutated?
|
|
83
|
+
hashes = transactions.map(&:tx_hash)
|
|
84
|
+
while hashes.size > 1
|
|
85
|
+
hashes.each_slice(2) { |left, right| return true if right && left == right }
|
|
86
|
+
hashes << hashes.last if hashes.size.odd?
|
|
87
|
+
hashes = hashes.each_slice(2).map { |left, right| Bitcoin.double_sha256([left + right].pack('H*')).bth }
|
|
88
|
+
end
|
|
89
|
+
false
|
|
71
90
|
end
|
|
72
91
|
|
|
73
92
|
# calculate merkle root from tx list.
|
|
93
|
+
# @raise [ArgumentError] If the block has no tx.
|
|
74
94
|
def calculate_merkle_root
|
|
95
|
+
raise ArgumentError, 'A block without any tx has no merkle root.' if transactions.empty?
|
|
75
96
|
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: transactions.map(&:tx_hash))
|
|
76
97
|
tree.compute_root
|
|
77
98
|
end
|
|
78
99
|
|
|
79
100
|
# check the witness commitment in coinbase tx matches witness commitment calculated from tx list.
|
|
101
|
+
# As in Bitcoin Core, the commitment is the last output of the coinbase which looks like one, and the
|
|
102
|
+
# witness of the coinbase input has to be exactly one item of 32 bytes, the witness reserved value.
|
|
103
|
+
# A block without a commitment output is not valid here.
|
|
80
104
|
def valid_witness_commitment?
|
|
81
|
-
transactions
|
|
105
|
+
return false if transactions.empty?
|
|
106
|
+
commitment = transactions[0].witness_commitment
|
|
107
|
+
return false if commitment.nil? || !valid_witness_reserved_value?
|
|
108
|
+
commitment == calculate_witness_commitment
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Whether the witness of the coinbase input is one item of 32 bytes, see valid_witness_commitment?.
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def valid_witness_reserved_value?
|
|
114
|
+
stack = transactions[0]&.inputs&.first&.script_witness&.stack
|
|
115
|
+
!stack.nil? && stack.size == 1 && stack[0].bytesize == 32
|
|
82
116
|
end
|
|
83
117
|
|
|
84
118
|
# calculate witness commitment from tx list.
|
|
119
|
+
# @raise [ArgumentError] If the coinbase input does not carry the witness reserved value.
|
|
85
120
|
def calculate_witness_commitment
|
|
121
|
+
raise ArgumentError, 'The witness reserved value is missing.' unless valid_witness_reserved_value?
|
|
86
122
|
witness_hashes = [COINBASE_WTXID]
|
|
87
123
|
witness_hashes += (transactions[1..-1].map(&:witness_hash))
|
|
88
|
-
reserved_value = transactions[0].inputs[0].script_witness.stack.
|
|
124
|
+
reserved_value = transactions[0].inputs[0].script_witness.stack[0].bth
|
|
89
125
|
tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: witness_hashes)
|
|
90
126
|
root_hash = tree.compute_root
|
|
91
127
|
Bitcoin.double_sha256([root_hash + reserved_value].pack('H*')).bth
|
|
@@ -96,10 +132,13 @@ module Bitcoin
|
|
|
96
132
|
def height
|
|
97
133
|
return nil if header.version < 2
|
|
98
134
|
coinbase_tx = transactions[0]
|
|
99
|
-
return nil unless coinbase_tx
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
return nil unless coinbase_tx&.coinbase_tx?
|
|
136
|
+
# BIP-34: the height is the first push of the coinbase scriptSig, as a script number. A height from
|
|
137
|
+
# 1 to 16 is pushed with OP_1 to OP_16, which is not a data push.
|
|
138
|
+
chunk = coinbase_tx.inputs[0].script_sig.chunks.first
|
|
139
|
+
return nil if chunk.nil?
|
|
140
|
+
return Bitcoin::Script.decode_number(chunk.pushed_data.bth) if chunk.pushdata?
|
|
141
|
+
Bitcoin::Opcodes.opcode_to_small_int(chunk.opcode)
|
|
103
142
|
end
|
|
104
143
|
|
|
105
144
|
end
|
data/lib/bitcoin/block_filter.rb
CHANGED
|
@@ -45,15 +45,16 @@ module Bitcoin
|
|
|
45
45
|
|
|
46
46
|
# Decode Block Filter from encoded filter
|
|
47
47
|
# @param [Integer] filter_type filter type.
|
|
48
|
-
# @param [String] block_hash block hash with hex format
|
|
48
|
+
# @param [String] block_hash block hash with hex format, in the byte order Bitcoin::Block#block_hash returns it.
|
|
49
49
|
# @param [String] encoded encoded_filter with hex format.
|
|
50
50
|
# @return [Bitcoin::BlockFilter] block filter object.
|
|
51
|
+
# @raise [ArgumentError] If +encoded+ is not a well formed filter, see Bitcoin::GCSFilter.
|
|
51
52
|
def self.decode(filter_type, block_hash, encoded)
|
|
52
53
|
filter = case filter_type
|
|
53
54
|
when TYPE[:basic]
|
|
54
55
|
GCSFilter.new(block_hash.htb[0...16], BASIC_FILTER_P, BASIC_FILTER_M, encoded_filter: encoded)
|
|
55
56
|
else
|
|
56
|
-
raise "unknown filter type: #{filter_type}."
|
|
57
|
+
raise ArgumentError, "unknown filter type: #{filter_type}."
|
|
57
58
|
end
|
|
58
59
|
BlockFilter.new(filter_type, filter, block_hash)
|
|
59
60
|
end
|
data/lib/bitcoin/block_header.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Bitcoin
|
|
|
26
26
|
# @return [Bitcoin::BlockHeader]
|
|
27
27
|
def self.parse_from_payload(payload)
|
|
28
28
|
buf = payload.is_a?(String) ? StringIO.new(payload) : payload
|
|
29
|
-
version, prev_hash, merkle_root, time, bits, nonce =
|
|
29
|
+
version, prev_hash, merkle_root, time, bits, nonce = Bitcoin.read_exact(buf, 80).unpack('Va32a32VVV')
|
|
30
30
|
new(version, prev_hash.bth, merkle_root.bth, time, bits, nonce)
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -34,12 +34,30 @@ module Bitcoin
|
|
|
34
34
|
[version, prev_hash.htb, merkle_root.htb, time, bits, nonce].pack('Va32a32VVV')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Decode a compact target the way Bitcoin Core's arith_uint256::SetCompact does.
|
|
38
|
+
# @param [Integer] compact The 32 bit compact encoding, such as the bits of a block header.
|
|
39
|
+
# @return [Integer] The target. It is negative when the sign bit is set, and it may not fit in
|
|
40
|
+
# 256 bits, see .compact_overflow?.
|
|
41
|
+
def self.decode_compact(compact)
|
|
42
|
+
exponent = (compact >> 24) & 0xff
|
|
43
|
+
mantissa = compact & 0x7fffff
|
|
44
|
+
target = exponent <= 3 ? mantissa >> (8 * (3 - exponent)) : mantissa << (8 * (exponent - 3))
|
|
45
|
+
(compact & 0x800000).zero? ? target : -target
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether the target of +compact+ does not fit in 256 bits, as Bitcoin Core's SetCompact reports it.
|
|
49
|
+
# @param [Integer] compact The 32 bit compact encoding.
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def self.compact_overflow?(compact)
|
|
52
|
+
exponent = (compact >> 24) & 0xff
|
|
53
|
+
mantissa = compact & 0x7fffff
|
|
54
|
+
mantissa != 0 && (exponent > 34 || (mantissa > 0xff && exponent > 33) || (mantissa > 0xffff && exponent > 32))
|
|
55
|
+
end
|
|
56
|
+
|
|
37
57
|
# compute difficulty target from bits.
|
|
58
|
+
# @return [Integer] see .decode_compact.
|
|
38
59
|
def difficulty_target
|
|
39
|
-
|
|
40
|
-
mantissa = bits & 0x7fffff
|
|
41
|
-
mantissa *= -1 if (bits & 0x800000) > 0
|
|
42
|
-
(mantissa * 2 ** (8 * (exponent - 3)))
|
|
60
|
+
BlockHeader.decode_compact(bits)
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
def hash
|
|
@@ -60,9 +78,14 @@ module Bitcoin
|
|
|
60
78
|
valid_pow? && valid_timestamp?
|
|
61
79
|
end
|
|
62
80
|
|
|
63
|
-
# evaluate valid proof of work
|
|
81
|
+
# evaluate valid proof of work, as Bitcoin Core's CheckProofOfWork does: the target has to be positive,
|
|
82
|
+
# fit in 256 bits and be no easier than the proof of work limit of the current chain, and the block
|
|
83
|
+
# hash has to be no greater than the target.
|
|
64
84
|
def valid_pow?
|
|
65
|
-
|
|
85
|
+
target = difficulty_target
|
|
86
|
+
return false if target <= 0 || BlockHeader.compact_overflow?(bits)
|
|
87
|
+
return false if target > BlockHeader.decode_compact(Bitcoin.chain_params.proof_of_work_limit)
|
|
88
|
+
block_id.hex <= target
|
|
66
89
|
end
|
|
67
90
|
|
|
68
91
|
# evaluate valid timestamp.
|
|
@@ -75,7 +98,7 @@ module Bitcoin
|
|
|
75
98
|
# @return [Integer] a chain work.
|
|
76
99
|
def work
|
|
77
100
|
target = difficulty_target
|
|
78
|
-
return 0 if target < 1
|
|
101
|
+
return 0 if target < 1 || BlockHeader.compact_overflow?(bits)
|
|
79
102
|
115792089237316195423570985008687907853269984665640564039457584007913129639936.div(target + 1) # 115792089237316195423570985008687907853269984665640564039457584007913129639936 is 2**256
|
|
80
103
|
end
|
|
81
104
|
|
data/lib/bitcoin/bloom_filter.rb
CHANGED
|
@@ -19,12 +19,15 @@ module Bitcoin
|
|
|
19
19
|
# @param [Integer] elements_length the number of elements
|
|
20
20
|
# @param [Float] fp_rate the false positive rate chosen by the client
|
|
21
21
|
# @param [Integer] tweak A random value to add to the seed value in the hash function used by the bloom filter
|
|
22
|
+
# @raise [ArgumentError] If +elements_length+ is not a positive integer.
|
|
22
23
|
def self.create_filter(elements_length, fp_rate, tweak = 0)
|
|
24
|
+
raise ArgumentError, 'elements_length must be a positive integer.' unless elements_length.is_a?(Integer) && elements_length > 0
|
|
23
25
|
# The size S of the filter in bytes is given by (-1 / pow(log(2), 2) * N * log(P)) / 8
|
|
24
26
|
len = [[(-elements_length * Math.log(fp_rate) / (LN2_SQUARED * 8)).to_i, MAX_BLOOM_FILTER_SIZE].min, 1].max
|
|
25
27
|
filter = Array.new(len, 0)
|
|
26
|
-
# The number of hash functions required is given by S * 8 / N * log(2)
|
|
27
|
-
|
|
28
|
+
# The number of hash functions required is given by S * 8 / N * log(2).
|
|
29
|
+
# S * 8 / N is an integer division, as in Bitcoin Core's CBloomFilter, so the same parameters make the same filter.
|
|
30
|
+
hash_funcs = [[(filter.size * 8 / elements_length * LN2).to_i, MAX_HASH_FUNCS].min, 1].max
|
|
28
31
|
BloomFilter.new(filter, hash_funcs, tweak)
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -23,7 +23,7 @@ retarget_time: 1209600 # 2 weeks
|
|
|
23
23
|
target_spacing: 600 # block interval
|
|
24
24
|
max_money: 21000000
|
|
25
25
|
bip34_height: 1
|
|
26
|
-
proof_of_work_limit:
|
|
26
|
+
proof_of_work_limit: 0x1e0377ae
|
|
27
27
|
dns_seeds:
|
|
28
28
|
- "seed.signet.bitcoin.sprovoost.nl"
|
|
29
29
|
- "seed.signet.achownodes.xyz"
|