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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +37 -0
- data/.rspec_parallel +2 -0
- data/.ruby-version +1 -1
- data/README.md +17 -6
- data/bitcoinrb.gemspec +9 -8
- data/exe/bitcoinrbd +5 -0
- data/lib/bitcoin.rb +37 -19
- data/lib/bitcoin/bip85_entropy.rb +111 -0
- data/lib/bitcoin/block_filter.rb +14 -0
- data/lib/bitcoin/block_header.rb +2 -0
- data/lib/bitcoin/chain_params.rb +9 -8
- data/lib/bitcoin/chainparams/regtest.yml +1 -1
- data/lib/bitcoin/chainparams/signet.yml +39 -0
- data/lib/bitcoin/chainparams/testnet.yml +1 -1
- data/lib/bitcoin/constants.rb +44 -10
- data/lib/bitcoin/descriptor.rb +1 -1
- data/lib/bitcoin/errors.rb +19 -0
- data/lib/bitcoin/ext.rb +6 -0
- data/lib/bitcoin/ext/array_ext.rb +22 -0
- data/lib/bitcoin/ext/ecdsa.rb +36 -0
- data/lib/bitcoin/ext/json_parser.rb +46 -0
- data/lib/bitcoin/ext_key.rb +51 -20
- data/lib/bitcoin/key.rb +89 -30
- data/lib/bitcoin/key_path.rb +12 -5
- data/lib/bitcoin/message.rb +79 -0
- data/lib/bitcoin/message/addr_v2.rb +34 -0
- data/lib/bitcoin/message/base.rb +17 -0
- data/lib/bitcoin/message/cf_parser.rb +16 -0
- data/lib/bitcoin/message/cfcheckpt.rb +36 -0
- data/lib/bitcoin/message/cfheaders.rb +40 -0
- data/lib/bitcoin/message/cfilter.rb +35 -0
- data/lib/bitcoin/message/fee_filter.rb +1 -1
- data/lib/bitcoin/message/filter_load.rb +3 -3
- data/lib/bitcoin/message/get_cfcheckpt.rb +29 -0
- data/lib/bitcoin/message/get_cfheaders.rb +24 -0
- data/lib/bitcoin/message/get_cfilters.rb +25 -0
- data/lib/bitcoin/message/header_and_short_ids.rb +1 -1
- data/lib/bitcoin/message/inventory.rb +1 -1
- data/lib/bitcoin/message/merkle_block.rb +1 -1
- data/lib/bitcoin/message/network_addr.rb +141 -18
- data/lib/bitcoin/message/ping.rb +1 -1
- data/lib/bitcoin/message/pong.rb +1 -1
- data/lib/bitcoin/message/send_addr_v2.rb +13 -0
- data/lib/bitcoin/message/send_cmpct.rb +2 -2
- data/lib/bitcoin/message/tx.rb +1 -1
- data/lib/bitcoin/message/version.rb +7 -0
- data/lib/bitcoin/message_sign.rb +47 -0
- data/lib/bitcoin/mnemonic.rb +7 -7
- data/lib/bitcoin/network/peer.rb +9 -4
- data/lib/bitcoin/network/peer_discovery.rb +1 -1
- data/lib/bitcoin/node/cli.rb +14 -10
- data/lib/bitcoin/node/configuration.rb +3 -1
- data/lib/bitcoin/node/spv.rb +9 -1
- data/lib/bitcoin/opcodes.rb +14 -1
- data/lib/bitcoin/out_point.rb +2 -0
- data/lib/bitcoin/payment_code.rb +92 -0
- data/lib/bitcoin/payments/payment.pb.rb +1 -1
- data/lib/bitcoin/psbt/hd_key_path.rb +1 -1
- data/lib/bitcoin/psbt/input.rb +9 -18
- data/lib/bitcoin/psbt/output.rb +1 -1
- data/lib/bitcoin/psbt/tx.rb +12 -17
- data/lib/bitcoin/rpc/bitcoin_core_client.rb +22 -12
- data/lib/bitcoin/rpc/request_handler.rb +5 -5
- data/lib/bitcoin/script/script.rb +96 -39
- data/lib/bitcoin/script/script_error.rb +27 -1
- data/lib/bitcoin/script/script_interpreter.rb +166 -66
- data/lib/bitcoin/script/tx_checker.rb +62 -14
- data/lib/bitcoin/secp256k1.rb +1 -0
- data/lib/bitcoin/secp256k1/native.rb +184 -17
- data/lib/bitcoin/secp256k1/rfc6979.rb +43 -0
- data/lib/bitcoin/secp256k1/ruby.rb +112 -56
- data/lib/bitcoin/sighash_generator.rb +156 -0
- data/lib/bitcoin/store.rb +1 -0
- data/lib/bitcoin/store/chain_entry.rb +1 -0
- data/lib/bitcoin/store/utxo_db.rb +226 -0
- data/lib/bitcoin/taproot.rb +9 -0
- data/lib/bitcoin/taproot/leaf_node.rb +23 -0
- data/lib/bitcoin/taproot/simple_builder.rb +139 -0
- data/lib/bitcoin/tx.rb +34 -104
- data/lib/bitcoin/tx_in.rb +4 -5
- data/lib/bitcoin/tx_out.rb +2 -3
- data/lib/bitcoin/util.rb +22 -6
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin/wallet.rb +1 -0
- data/lib/bitcoin/wallet/account.rb +2 -1
- data/lib/bitcoin/wallet/base.rb +2 -2
- data/lib/bitcoin/wallet/master_key.rb +1 -0
- data/lib/bitcoin/wallet/utxo.rb +37 -0
- metadata +86 -32
- 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
@@ -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,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
|