bitcoinrb 0.3.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +37 -0
  3. data/.rspec_parallel +2 -0
  4. data/.ruby-version +1 -1
  5. data/README.md +17 -6
  6. data/bitcoinrb.gemspec +9 -8
  7. data/exe/bitcoinrbd +5 -0
  8. data/lib/bitcoin.rb +37 -19
  9. data/lib/bitcoin/bip85_entropy.rb +111 -0
  10. data/lib/bitcoin/block_filter.rb +14 -0
  11. data/lib/bitcoin/block_header.rb +2 -0
  12. data/lib/bitcoin/chain_params.rb +9 -8
  13. data/lib/bitcoin/chainparams/regtest.yml +1 -1
  14. data/lib/bitcoin/chainparams/signet.yml +39 -0
  15. data/lib/bitcoin/chainparams/testnet.yml +1 -1
  16. data/lib/bitcoin/constants.rb +44 -10
  17. data/lib/bitcoin/descriptor.rb +1 -1
  18. data/lib/bitcoin/errors.rb +19 -0
  19. data/lib/bitcoin/ext.rb +6 -0
  20. data/lib/bitcoin/ext/array_ext.rb +22 -0
  21. data/lib/bitcoin/ext/ecdsa.rb +36 -0
  22. data/lib/bitcoin/ext/json_parser.rb +46 -0
  23. data/lib/bitcoin/ext_key.rb +51 -20
  24. data/lib/bitcoin/key.rb +89 -30
  25. data/lib/bitcoin/key_path.rb +12 -5
  26. data/lib/bitcoin/message.rb +79 -0
  27. data/lib/bitcoin/message/addr_v2.rb +34 -0
  28. data/lib/bitcoin/message/base.rb +17 -0
  29. data/lib/bitcoin/message/cf_parser.rb +16 -0
  30. data/lib/bitcoin/message/cfcheckpt.rb +36 -0
  31. data/lib/bitcoin/message/cfheaders.rb +40 -0
  32. data/lib/bitcoin/message/cfilter.rb +35 -0
  33. data/lib/bitcoin/message/fee_filter.rb +1 -1
  34. data/lib/bitcoin/message/filter_load.rb +3 -3
  35. data/lib/bitcoin/message/get_cfcheckpt.rb +29 -0
  36. data/lib/bitcoin/message/get_cfheaders.rb +24 -0
  37. data/lib/bitcoin/message/get_cfilters.rb +25 -0
  38. data/lib/bitcoin/message/header_and_short_ids.rb +1 -1
  39. data/lib/bitcoin/message/inventory.rb +1 -1
  40. data/lib/bitcoin/message/merkle_block.rb +1 -1
  41. data/lib/bitcoin/message/network_addr.rb +141 -18
  42. data/lib/bitcoin/message/ping.rb +1 -1
  43. data/lib/bitcoin/message/pong.rb +1 -1
  44. data/lib/bitcoin/message/send_addr_v2.rb +13 -0
  45. data/lib/bitcoin/message/send_cmpct.rb +2 -2
  46. data/lib/bitcoin/message/tx.rb +1 -1
  47. data/lib/bitcoin/message/version.rb +7 -0
  48. data/lib/bitcoin/message_sign.rb +47 -0
  49. data/lib/bitcoin/mnemonic.rb +7 -7
  50. data/lib/bitcoin/network/peer.rb +9 -4
  51. data/lib/bitcoin/network/peer_discovery.rb +1 -1
  52. data/lib/bitcoin/node/cli.rb +14 -10
  53. data/lib/bitcoin/node/configuration.rb +3 -1
  54. data/lib/bitcoin/node/spv.rb +9 -1
  55. data/lib/bitcoin/opcodes.rb +14 -1
  56. data/lib/bitcoin/out_point.rb +2 -0
  57. data/lib/bitcoin/payment_code.rb +92 -0
  58. data/lib/bitcoin/payments/payment.pb.rb +1 -1
  59. data/lib/bitcoin/psbt/hd_key_path.rb +1 -1
  60. data/lib/bitcoin/psbt/input.rb +9 -18
  61. data/lib/bitcoin/psbt/output.rb +1 -1
  62. data/lib/bitcoin/psbt/tx.rb +12 -17
  63. data/lib/bitcoin/rpc/bitcoin_core_client.rb +22 -12
  64. data/lib/bitcoin/rpc/request_handler.rb +5 -5
  65. data/lib/bitcoin/script/script.rb +96 -39
  66. data/lib/bitcoin/script/script_error.rb +27 -1
  67. data/lib/bitcoin/script/script_interpreter.rb +166 -66
  68. data/lib/bitcoin/script/tx_checker.rb +62 -14
  69. data/lib/bitcoin/secp256k1.rb +1 -0
  70. data/lib/bitcoin/secp256k1/native.rb +184 -17
  71. data/lib/bitcoin/secp256k1/rfc6979.rb +43 -0
  72. data/lib/bitcoin/secp256k1/ruby.rb +112 -56
  73. data/lib/bitcoin/sighash_generator.rb +156 -0
  74. data/lib/bitcoin/store.rb +1 -0
  75. data/lib/bitcoin/store/chain_entry.rb +1 -0
  76. data/lib/bitcoin/store/utxo_db.rb +226 -0
  77. data/lib/bitcoin/taproot.rb +9 -0
  78. data/lib/bitcoin/taproot/leaf_node.rb +23 -0
  79. data/lib/bitcoin/taproot/simple_builder.rb +139 -0
  80. data/lib/bitcoin/tx.rb +34 -104
  81. data/lib/bitcoin/tx_in.rb +4 -5
  82. data/lib/bitcoin/tx_out.rb +2 -3
  83. data/lib/bitcoin/util.rb +22 -6
  84. data/lib/bitcoin/version.rb +1 -1
  85. data/lib/bitcoin/wallet.rb +1 -0
  86. data/lib/bitcoin/wallet/account.rb +2 -1
  87. data/lib/bitcoin/wallet/base.rb +2 -2
  88. data/lib/bitcoin/wallet/master_key.rb +1 -0
  89. data/lib/bitcoin/wallet/utxo.rb +37 -0
  90. metadata +86 -32
  91. data/.travis.yml +0 -11
@@ -0,0 +1,156 @@
1
+ module Bitcoin
2
+
3
+ module SigHashGenerator
4
+
5
+ def self.load(sig_ver)
6
+ case sig_ver
7
+ when :base
8
+ LegacySigHashGenerator.new
9
+ when :witness_v0
10
+ SegwitSigHashGenerator.new
11
+ when :taproot, :tapscript
12
+ SchnorrSigHashGenerator.new
13
+ else
14
+ raise ArgumentError, "Unsupported sig version specified. #{sig_ver}"
15
+ end
16
+ end
17
+
18
+ # Legacy SigHash Generator
19
+ class LegacySigHashGenerator
20
+
21
+ def generate(tx, input_index, hash_type, opts)
22
+ output_script = opts[:script_code]
23
+ ins = tx.inputs.map.with_index do |i, idx|
24
+ if idx == input_index
25
+ i.to_payload(output_script.delete_opcode(Bitcoin::Opcodes::OP_CODESEPARATOR))
26
+ else
27
+ case hash_type & 0x1f
28
+ when SIGHASH_TYPE[:none], SIGHASH_TYPE[:single]
29
+ i.to_payload(Bitcoin::Script.new, 0)
30
+ else
31
+ i.to_payload(Bitcoin::Script.new)
32
+ end
33
+ end
34
+ end
35
+
36
+ outs = tx.outputs.map(&:to_payload)
37
+ out_size = Bitcoin.pack_var_int(tx.outputs.size)
38
+
39
+ case hash_type & 0x1f
40
+ when SIGHASH_TYPE[:none]
41
+ outs = ''
42
+ out_size = Bitcoin.pack_var_int(0)
43
+ when SIGHASH_TYPE[:single]
44
+ return "\x01".ljust(32, "\x00") if input_index >= tx.outputs.size
45
+ outs = tx.outputs[0...(input_index + 1)].map.with_index { |o, idx| (idx == input_index) ? o.to_payload : o.to_empty_payload }.join
46
+ out_size = Bitcoin.pack_var_int(input_index + 1)
47
+ end
48
+
49
+ ins = [ins[input_index]] unless hash_type & SIGHASH_TYPE[:anyonecanpay] == 0
50
+
51
+ buf = [[tx.version].pack('V'), Bitcoin.pack_var_int(ins.size),
52
+ ins, out_size, outs, [tx.lock_time, hash_type].pack('VV')].join
53
+
54
+ Bitcoin.double_sha256(buf)
55
+ end
56
+
57
+ end
58
+
59
+ # V0 witness sighash generator.
60
+ # see: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
61
+ class SegwitSigHashGenerator
62
+
63
+ def generate(tx, input_index, hash_type, opts)
64
+ amount = opts[:amount]
65
+ output_script = opts[:script_code]
66
+ skip_separator_index = opts[:skip_separator_index]
67
+ hash_prevouts = Bitcoin.double_sha256(tx.inputs.map{|i|i.out_point.to_payload}.join)
68
+ hash_sequence = Bitcoin.double_sha256(tx.inputs.map{|i|[i.sequence].pack('V')}.join)
69
+ outpoint = tx.inputs[input_index].out_point.to_payload
70
+ amount = [amount].pack('Q')
71
+ nsequence = [tx.inputs[input_index].sequence].pack('V')
72
+ hash_outputs = Bitcoin.double_sha256(tx.outputs.map{|o|o.to_payload}.join)
73
+
74
+ script_code = output_script.to_script_code(skip_separator_index)
75
+
76
+ case (hash_type & 0x1f)
77
+ when SIGHASH_TYPE[:single]
78
+ hash_outputs = input_index >= tx.outputs.size ? "\x00".ljust(32, "\x00") : Bitcoin.double_sha256(tx.outputs[input_index].to_payload)
79
+ hash_sequence = "\x00".ljust(32, "\x00")
80
+ when SIGHASH_TYPE[:none]
81
+ hash_sequence = hash_outputs = "\x00".ljust(32, "\x00")
82
+ end
83
+
84
+ unless (hash_type & SIGHASH_TYPE[:anyonecanpay]) == 0
85
+ hash_prevouts = hash_sequence ="\x00".ljust(32, "\x00")
86
+ end
87
+
88
+ buf = [ [tx.version].pack('V'), hash_prevouts, hash_sequence, outpoint,
89
+ script_code ,amount, nsequence, hash_outputs, [tx.lock_time, hash_type].pack('VV')].join
90
+ Bitcoin.double_sha256(buf)
91
+ end
92
+
93
+ end
94
+
95
+ # v1 witness sighash generator
96
+ # see: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
97
+ class SchnorrSigHashGenerator
98
+
99
+ # generate signature hash for taproot and tapscript
100
+ # @param [Hash] opts some data using signature. This class requires following key params:
101
+ # - sig_version: sig version. :taproot or :tapscript
102
+ # - prevouts: array of all prevout[Txout]
103
+ # - annex: annex value with binary format if annex exist.
104
+ # - leaf_hash: leaf hash with binary format if sig_version is :tapscript, it required
105
+ # - last_code_separator_pos: the position of last code separator
106
+ # @return [String] signature hash with binary format.
107
+ def generate(tx, input_index, hash_type, opts)
108
+ raise ArgumentError, 'Invalid sig_version was specified.' unless [:taproot, :tapscript].include?(opts[:sig_version])
109
+
110
+ ext_flag = opts[:sig_version] == :taproot ? 0 : 1
111
+ key_version = 0
112
+ output_ype = hash_type == SIGHASH_TYPE[:default] ? SIGHASH_TYPE[:all] : (hash_type & 0x03)
113
+ input_type = hash_type & 0x80
114
+ epoc = '00'.htb
115
+
116
+ buf = epoc # EPOC
117
+ buf << [hash_type, tx.version, tx.lock_time].pack('CVV')
118
+ unless input_type == SIGHASH_TYPE[:anyonecanpay]
119
+ buf << Bitcoin.sha256(tx.in.map{|i|i.out_point.to_payload}.join) # sha_prevouts
120
+ buf << Bitcoin.sha256(opts[:prevouts].map(&:value).pack('Q*'))# sha_amounts
121
+ buf << Bitcoin.sha256(opts[:prevouts].map{|o|o.script_pubkey.to_payload(true)}.join) # sha_scriptpubkeys
122
+ buf << Bitcoin.sha256(tx.in.map(&:sequence).pack('V*')) # sha_sequences
123
+ end
124
+
125
+ buf << Bitcoin.sha256(tx.out.map(&:to_payload).join) if output_ype == SIGHASH_TYPE[:all]
126
+
127
+ spend_type = (ext_flag << 1) + (opts[:annex] ? 1 : 0)
128
+ buf << [spend_type].pack('C')
129
+ if input_type == SIGHASH_TYPE[:anyonecanpay]
130
+ buf << tx.in[input_index].out_point.to_payload
131
+ buf << opts[:prevouts][input_index].to_payload
132
+ buf << [tx.in[input_index].sequence].pack('V')
133
+ else
134
+ buf << [input_index].pack('V')
135
+ end
136
+
137
+ buf << Bitcoin.sha256(Bitcoin.pack_var_string(opts[:annex])) if opts[:annex]
138
+
139
+ if output_ype == SIGHASH_TYPE[:single]
140
+ raise ArgumentError, "Tx does not have #{input_index} th output." if input_index >= tx.out.size
141
+ buf << Bitcoin.sha256(tx.out[input_index].to_payload)
142
+ end
143
+
144
+ if opts[:sig_version] == :tapscript
145
+ buf << opts[:leaf_hash]
146
+ buf << [key_version, opts[:last_code_separator_pos]].pack("CV")
147
+ end
148
+
149
+ Bitcoin.tagged_hash('TapSighash', buf)
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end
data/lib/bitcoin/store.rb CHANGED
@@ -6,6 +6,7 @@ module Bitcoin
6
6
  autoload :DB, 'bitcoin/store/db'
7
7
  autoload :SPVChain, 'bitcoin/store/spv_chain'
8
8
  autoload :ChainEntry, 'bitcoin/store/chain_entry'
9
+ autoload :UtxoDB, 'bitcoin/store/utxo_db'
9
10
 
10
11
  end
11
12
  end
@@ -3,6 +3,7 @@ module Bitcoin
3
3
 
4
4
  # wrap a block header object with extra data.
5
5
  class ChainEntry
6
+ include Bitcoin::HexConverter
6
7
 
7
8
  attr_reader :header
8
9
  attr_reader :height
@@ -0,0 +1,226 @@
1
+ require 'leveldb-native'
2
+
3
+ module Bitcoin
4
+ module Store
5
+ class UtxoDB
6
+
7
+ KEY_PREFIX = {
8
+ out_point: 'o', # key: out_point(tx_hash and index), value: Utxo
9
+ script: 's', # key: script_pubkey and out_point(tx_hash and index), value: Utxo
10
+ height: 'h', # key: block_height and out_point, value: Utxo
11
+ tx_hash: 't', # key: tx_hash of transaction, value: [block_height, tx_index]
12
+ block: 'b', # key: block_height and tx_index, value: tx_hash
13
+ tx_payload: 'p', # key: tx_hash, value: Tx
14
+ }
15
+
16
+ attr_reader :level_db, :logger
17
+
18
+ def initialize(path = "#{Bitcoin.base_dir}/db/utxo")
19
+ FileUtils.mkdir_p(path)
20
+ @level_db = ::LevelDBNative::DB.new(path)
21
+ @logger = Bitcoin::Logger.create(:debug)
22
+ end
23
+
24
+ def close
25
+ level_db.close
26
+ end
27
+
28
+ # Save payload of a transaction into db
29
+ #
30
+ # @param [String] tx_hash
31
+ # @param [String] tx_payload
32
+ def save_tx(tx_hash, tx_payload)
33
+ logger.info("UtxoDB#save_tx:#{[tx_hash, tx_payload]}")
34
+ level_db.batch do
35
+ # tx_hash -> [block_height, tx_index]
36
+ key = KEY_PREFIX[:tx_payload] + tx_hash
37
+ level_db.put(key, tx_payload)
38
+ end
39
+ end
40
+
41
+ # Save tx position (block height and index in the block) into db
42
+ # When node receives `header` message, node should call save_tx_position to store block height and its index.
43
+ #
44
+ # @param [String] tx_hash
45
+ # @param [Integer] block_height
46
+ # @param [Integer] tx_index
47
+ def save_tx_position(tx_hash, block_height, tx_index)
48
+ logger.info("UtxoDB#save_tx_position:#{[tx_hash, block_height, tx_index]}")
49
+ level_db.batch do
50
+ # tx_hash -> [block_height, tx_index]
51
+ key = KEY_PREFIX[:tx_hash] + tx_hash
52
+ level_db.put(key, [block_height, tx_index].pack('N2').bth)
53
+
54
+ # block_hash and tx_index -> tx_hash
55
+ key = KEY_PREFIX[:block] + [block_height, tx_index].pack('N2').bth
56
+ level_db.put(key, tx_hash)
57
+ end
58
+ end
59
+
60
+ # Save utxo into db
61
+ #
62
+ # @param [Bitcoin::OutPoint] out_point
63
+ # @param [Double] value
64
+ # @param [Bitcoin::Script] script_pubkey
65
+ # @param [Integer] block_height
66
+ def save_utxo(out_point, value, script_pubkey, block_height=nil)
67
+ logger.info("UtxoDB#save_utxo:#{[out_point, value, script_pubkey, block_height]}")
68
+ level_db.batch do
69
+ utxo = Bitcoin::Wallet::Utxo.new(out_point.tx_hash, out_point.index, value, script_pubkey, block_height)
70
+ payload = utxo.to_payload
71
+
72
+ # out_point
73
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
74
+ return if level_db.contains?(key)
75
+ level_db.put(key, payload)
76
+
77
+ # script_pubkey
78
+ if script_pubkey
79
+ key = KEY_PREFIX[:script] + script_pubkey.to_hex + out_point.to_hex
80
+ level_db.put(key, payload)
81
+ end
82
+
83
+ # height
84
+ unless block_height.nil?
85
+ key = KEY_PREFIX[:height] + [block_height].pack('N').bth + out_point.to_hex
86
+ level_db.put(key, payload)
87
+ end
88
+
89
+ utxo
90
+ end
91
+ end
92
+
93
+ # Get transaction stored via save_tx and save_tx_position
94
+ #
95
+ # @param [string] tx_hash
96
+ # @return [block_height, tx_index, tx_payload]
97
+ def get_tx(tx_hash)
98
+ key = KEY_PREFIX[:tx_hash] + tx_hash
99
+ return [] unless level_db.contains?(key)
100
+ block_height, tx_index = level_db.get(key).htb.unpack('N2')
101
+ key = KEY_PREFIX[:tx_payload] + tx_hash
102
+ tx_payload = level_db.get(key)
103
+ [block_height, tx_index, tx_payload]
104
+ end
105
+
106
+ # Delete utxo from db
107
+ #
108
+ # @param [Bitcoin::Outpoint] out_point
109
+ # @return [Bitcoin::Wallet::Utxo]
110
+ def delete_utxo(out_point)
111
+ level_db.batch do
112
+ # [:out_point]
113
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
114
+ return unless level_db.contains?(key)
115
+ utxo = Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
116
+ level_db.delete(key)
117
+
118
+ # [:script]
119
+ if utxo.script_pubkey
120
+ key = KEY_PREFIX[:script] + utxo.script_pubkey.to_hex + out_point.to_hex
121
+ level_db.delete(key)
122
+ end
123
+
124
+ if utxo.block_height
125
+ # [:height]
126
+ key = KEY_PREFIX[:height] + [utxo.block_height].pack('N').bth + out_point.to_hex
127
+ level_db.delete(key)
128
+
129
+ # [:block]
130
+ key = KEY_PREFIX[:block] + [utxo.block_height, utxo.index].pack('N2').bth
131
+ level_db.delete(key)
132
+ end
133
+
134
+ # handles both [:tx_hash] and [:tx_payload]
135
+ if utxo.tx_hash
136
+ key = KEY_PREFIX[:tx_hash] + utxo.tx_hash
137
+ level_db.delete(key)
138
+
139
+ key = KEY_PREFIX[:tx_payload] + utxo.tx_hash
140
+ level_db.delete(key)
141
+ end
142
+
143
+ utxo
144
+ end
145
+ end
146
+
147
+ # Get utxo of the specified out point
148
+ #
149
+ # @param [Bitcoin::Outpoint] out_point
150
+ # @return [Bitcoin::Wallet::Utxo]
151
+ def get_utxo(out_point)
152
+ level_db.batch do
153
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
154
+ return unless level_db.contains?(key)
155
+ return Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
156
+ end
157
+ end
158
+
159
+ # return [Bitcoin::Wallet::Utxo ...]
160
+ def list_unspent(current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
161
+ if addresses
162
+ list_unspent_by_addresses(current_block_height, min: min, max: max, addresses: addresses)
163
+ else
164
+ list_unspent_by_block_height(current_block_height, min: min, max: max)
165
+ end
166
+ end
167
+
168
+ # @param [Bitcoin::Wallet::Account]
169
+ # return [Bitcoin::Wallet::Utxo ...]
170
+ def list_unspent_in_account(account, current_block_height: 9999999, min: 0, max: 9999999)
171
+ return [] unless account
172
+
173
+ script_pubkeys = case account.purpose
174
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:legacy]
175
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2pkh(t).to_hex }
176
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:nested_witness]
177
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_p2sh.to_hex }
178
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:native_segwit]
179
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_hex }
180
+ end
181
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
182
+ end
183
+
184
+ # @param [Bitcoin::Wallet::Account]
185
+ # return [Bitcoin::Wallet::Utxo ...]
186
+ def get_balance(account, current_block_height: 9999999, min: 0, max: 9999999)
187
+ list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max).sum { |u| u.value }
188
+ end
189
+
190
+ private
191
+
192
+ def utxos_between(from, to)
193
+ level_db.each(from: from, to: to).map { |k, v| Bitcoin::Wallet::Utxo.parse_from_payload(v) }
194
+ end
195
+
196
+ class ::Array
197
+ def with_height(min, max)
198
+ select { |u| u.block_height >= min && u.block_height <= max }
199
+ end
200
+ end
201
+
202
+ def list_unspent_by_block_height(current_block_height, min: 0, max: 9999999)
203
+ max_height = [current_block_height - min, 0].max
204
+ min_height = [current_block_height - max, 0].max
205
+ from = KEY_PREFIX[:height] + [min_height].pack('N').bth + '000000000000000000000000000000000000000000000000000000000000000000000000'
206
+ to = KEY_PREFIX[:height] + [max_height].pack('N').bth + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
207
+ utxos_between(from, to)
208
+ end
209
+
210
+ def list_unspent_by_addresses(current_block_height, min: 0, max: 9999999, addresses: [])
211
+ script_pubkeys = addresses.map { |a| Bitcoin::Script.parse_from_addr(a).to_hex }
212
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
213
+ end
214
+
215
+ def list_unspent_by_script_pubkeys(current_block_height, min: 0, max: 9999999, script_pubkeys: [])
216
+ max_height = current_block_height - min
217
+ min_height = current_block_height - max
218
+ script_pubkeys.map do |key|
219
+ from = KEY_PREFIX[:script] + key + '000000000000000000000000000000000000000000000000000000000000000000000000'
220
+ to = KEY_PREFIX[:script] + key + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
221
+ utxos_between(from, to).with_height(min_height, max_height)
222
+ end.flatten
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,9 @@
1
+ module Bitcoin
2
+ module Taproot
3
+
4
+ class Error < StandardError; end
5
+
6
+ autoload :LeafNode, 'bitcoin/taproot/leaf_node'
7
+ autoload :SimpleBuilder, 'bitcoin/taproot/simple_builder'
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Bitcoin
2
+ module Taproot
3
+ class LeafNode
4
+
5
+ attr_reader :script, :leaf_ver
6
+
7
+ # Initialize
8
+ # @param [Bitcoin::Script] script Locking script
9
+ # @param [Integer] leaf_ver The leaf version of this script.
10
+ def initialize(script, leaf_ver)
11
+ raise Taproot::Error, 'script must be Bitcoin::Script object' unless script.is_a?(Bitcoin::Script)
12
+ @script = script
13
+ @leaf_ver = leaf_ver
14
+ end
15
+
16
+ # Calculate leaf hash.
17
+ # @return [String] leaf hash.
18
+ def leaf_hash
19
+ @hash_value ||= Bitcoin.tagged_hash('TapLeaf', [leaf_ver].pack('C') + Bitcoin.pack_var_string(script.to_payload))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,139 @@
1
+ module Bitcoin
2
+ module Taproot
3
+
4
+ # Utility class to construct Taproot outputs from internal key and script tree.
5
+ # SimpleBuilder builds a script tree that places all lock scripts, in the order they are added, as leaf nodes.
6
+ # It is not possible to specify the depth of the locking script or to insert any intermediate nodes.
7
+ class SimpleBuilder
8
+ include Bitcoin::Opcodes
9
+
10
+ attr_reader :internal_key # String with hex format
11
+ attr_reader :leaves # Array[LeafNode]
12
+
13
+ # Initialize builder.
14
+ # @param [String] internal_key Internal public key with hex format.
15
+ # @param [Array[Bitcoin::Script]] scripts Scripts for each lock condition.
16
+ # @param [Integer] leaf_ver (Optional) The leaf version of tapscript.
17
+ # @raise [Bitcoin::Taproot::Builder] +internal_pubkey+ dose not xonly public key or script in +scripts+ does not instance of Bitcoin::Script.
18
+ # @return [Bitcoin::Taproot::SimpleBuilder]
19
+ def initialize(internal_key, *scripts, leaf_ver: Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
20
+ raise Error, 'Internal public key must be 32 bytes' unless internal_key.htb.bytesize == 32
21
+ @leaves = scripts.map { |script| LeafNode.new(script, leaf_ver) }
22
+ @internal_key = internal_key
23
+ end
24
+
25
+ # Add lock script to leaf node.
26
+ # @param [Bitcoin::Script] script lock script.
27
+ # @param [Integer] leaf_ver (Optional) The leaf version of tapscript.
28
+ # @raise [Bitcoin::Taproot::Builder] If +script+ does not instance of Bitcoin::Script.
29
+ # @return [Bitcoin::Taproot::SimpleBuilder] self
30
+ def <<(script, leaf_ver: Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
31
+ leaves << LeafNode.new(script, leaf_ver)
32
+ self
33
+ end
34
+
35
+ # Build P2TR script.
36
+ # @return [Bitcoin::Script] P2TR script.
37
+ def build
38
+ q = tweak_public_key
39
+ Bitcoin::Script.new << OP_1 << q.xonly_pubkey
40
+ end
41
+
42
+ # Compute the tweaked public key.
43
+ # @return [Bitcoin::Key] the tweaked public key
44
+ def tweak_public_key
45
+ key = Bitcoin::Key.new(priv_key: tweak.bth, key_type: Key::TYPES[:compressed])
46
+ Bitcoin::Key.from_point(key.to_point + Bitcoin::Key.from_xonly_pubkey(internal_key).to_point)
47
+ end
48
+
49
+ # Compute the secret key for a tweaked public key.
50
+ # @param [Bitcoin::Key] key key object contains private key.
51
+ # @return [Bitcoin::Key] secret key for a tweaked public key
52
+ def tweak_private_key(key)
53
+ raise Error, 'Requires private key' unless key.priv_key
54
+ p = key.to_point
55
+ private_key = p.has_even_y? ? key.priv_key.to_i(16) : ECDSA::Group::Secp256k1.order - key.priv_key.to_i(16)
56
+ Bitcoin::Key.new(priv_key: ((tweak.bti + private_key) % ECDSA::Group::Secp256k1.order).to_even_length_hex)
57
+ end
58
+
59
+ # Generate control block needed to unlock with script-path.
60
+ # @param [Bitcoin::Script] script Script to use for unlocking.
61
+ # @param [Integer] leaf_ver leaf version of script.
62
+ # @return [String] control block with binary format.
63
+ def control_block(script, leaf_ver: Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
64
+ path = inclusion_proof(script, leaf_ver: leaf_ver)
65
+ parity = tweak_public_key.to_point.has_even_y? ? 0 : 1
66
+ [parity + leaf_ver].pack("C") + internal_key.htb + path.join
67
+ end
68
+
69
+ # Generate inclusion proof for +script+.
70
+ # @param [Bitcoin::Script] script The script in script tree.
71
+ # @param [Integer] leaf_ver (Optional) The leaf version of tapscript.
72
+ # @return [Array[String]] Inclusion proof.
73
+ def inclusion_proof(script, leaf_ver: Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
74
+ parents = leaves
75
+ parent_hash = leaf_hash(script, leaf_ver: leaf_ver)
76
+ proofs = []
77
+ until parents.size == 1
78
+ parents = parents.each_slice(2).map do |pair|
79
+ combined = combine_hash(pair)
80
+ unless pair.size == 1
81
+ if hash_value(pair[0]) == parent_hash
82
+ proofs << hash_value(pair[1])
83
+ parent_hash = combined
84
+ elsif hash_value(pair[1]) == parent_hash
85
+ proofs << hash_value(pair[0])
86
+ parent_hash = combined
87
+ end
88
+ end
89
+ combined
90
+ end
91
+ end
92
+ proofs
93
+ end
94
+
95
+ # Computes leaf hash
96
+ # @param [Bitcoin::Script] script
97
+ # @param [Integer] leaf_ver leaf version
98
+ # @@return [String] leaf hash with binary format.
99
+ def leaf_hash(script, leaf_ver: Bitcoin::TAPROOT_LEAF_TAPSCRIPT)
100
+ raise Error, 'script does not exist' unless leaves.find{ |leaf| leaf.script == script}
101
+ LeafNode.new(script, leaf_ver).leaf_hash
102
+ end
103
+
104
+ private
105
+
106
+ # Compute tweak from script tree.
107
+ # @return [String] tweak with binary format.
108
+ def tweak
109
+ parents = leaves
110
+ if parents.empty?
111
+ parents = ['']
112
+ else
113
+ parents = parents.each_slice(2).map { |pair| combine_hash(pair) } until parents.size == 1
114
+ end
115
+ t = Bitcoin.tagged_hash('TapTweak', internal_key.htb + parents.first)
116
+ raise Error, 'tweak value exceeds the curve order' if t.bti >= ECDSA::Group::Secp256k1.order
117
+ t
118
+ end
119
+
120
+ def combine_hash(pair)
121
+ if pair.size == 1
122
+ hash_value(pair[0])
123
+ else
124
+ hash1 = hash_value(pair[0])
125
+ hash2 = hash_value(pair[1])
126
+
127
+ # Lexicographically sort a and b's hash, and compute parent hash.
128
+ payload = hash1.bth < hash2.bth ? hash1 + hash2 : hash2 + hash1
129
+ Bitcoin.tagged_hash('TapBranch', payload)
130
+ end
131
+ end
132
+
133
+ def hash_value(leaf_or_branch)
134
+ leaf_or_branch.is_a?(LeafNode) ? leaf_or_branch.leaf_hash : leaf_or_branch
135
+ end
136
+ end
137
+ end
138
+
139
+ end