bitcoinrb 0.3.2 → 0.8.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.
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