bitcoinrb 1.8.1 → 1.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6e5ff6fa694353e122cd0028354edff2aa877debdc36d368503c25f6277c77f
4
- data.tar.gz: a6ef9fb214582173d6681dcad6547208b6c606f9d4b82f1c2b3423f388866db1
3
+ metadata.gz: 9e9112a5bf75bc7840748693cf849b887cfafd82bbb40dafb8d6eb47e4e0c1e2
4
+ data.tar.gz: b712757ad33c4283ac0b048d527ae0d0d4c597b2b089a94baf4655b70f3441a3
5
5
  SHA512:
6
- metadata.gz: b4b858790060163880d19b8d771383b406facaa755386e7919e8c6d1caa6b7f362eeb611846f13f40b406114cae7d3e100cf4f5cb46adef01e37d40a43c35de6
7
- data.tar.gz: a4712b529713ad39bebd6f32d9124d2f07d3c77c22482297df550f384b6f3ebf0ee6000958494684a95cc70354f95a22d8361dd686241c2d67c645fcee20ea5c
6
+ metadata.gz: 0adb44f570489b3e577fe5f7e42c922f315976ed90e0ba286a727e941b70f6ca5d16fb090f6f014bb4b0f36907ba87c65e3881f31b40c706bf8d79958890536d
7
+ data.tar.gz: 90d803c95d478211ec5054012f96eac13d5ce44872df43e2ce483ed3460343e6051c33d8a7a4e6679d824f922cc99a547855aacd8a857cd3d3ece7c496107171
data/README.md CHANGED
@@ -83,6 +83,8 @@ This parameter is described in https://github.com/chaintope/bitcoinrb/blob/maste
83
83
 
84
84
  ```ruby
85
85
  Bitcoin.chain_params = :testnet
86
+ # or
87
+ Bitcoin.chain_params = :testnet4
86
88
  ```
87
89
 
88
90
  This parameter is described in https://github.com/chaintope/bitcoinrb/blob/master/lib/bitcoin/chainparams/testnet.yml.
data/bitcoinrb.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.1'
24
24
  spec.add_runtime_dependency 'eventmachine'
25
25
  spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
26
- spec.add_runtime_dependency 'bech32', '>= 1.3.0'
26
+ spec.add_runtime_dependency 'bech32', '>= 1.5.0'
27
27
  spec.add_runtime_dependency 'daemon-spawn'
28
28
  spec.add_runtime_dependency 'thor'
29
29
  spec.add_runtime_dependency 'leb128', '~> 1.0.0'
@@ -36,4 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.add_runtime_dependency 'observer', '~> 0.1.2'
37
37
  spec.add_runtime_dependency 'secp256k1rb', '0.1.1'
38
38
  spec.add_runtime_dependency 'logger'
39
+ spec.add_runtime_dependency 'merkle', '0.3.0'
39
40
  end
data/lib/bitcoin/block.rb CHANGED
@@ -28,7 +28,7 @@ module Bitcoin
28
28
  header = BlockHeader.new(
29
29
  version,
30
30
  '00' * 32,
31
- MerkleTree.build_from_leaf([coinbase.txid]).merkle_root.rhex,
31
+ Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: [coinbase.txid]).compute_root.rhex,
32
32
  time,
33
33
  bits,
34
34
  nonce
@@ -72,7 +72,8 @@ module Bitcoin
72
72
 
73
73
  # calculate merkle root from tx list.
74
74
  def calculate_merkle_root
75
- Bitcoin::MerkleTree.build_from_leaf(transactions.map(&:tx_hash)).merkle_root
75
+ tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: transactions.map(&:tx_hash))
76
+ tree.compute_root
76
77
  end
77
78
 
78
79
  # check the witness commitment in coinbase tx matches witness commitment calculated from tx list.
@@ -85,7 +86,8 @@ module Bitcoin
85
86
  witness_hashes = [COINBASE_WTXID]
86
87
  witness_hashes += (transactions[1..-1].map(&:witness_hash))
87
88
  reserved_value = transactions[0].inputs[0].script_witness.stack.map(&:bth).join
88
- root_hash = Bitcoin::MerkleTree.build_from_leaf(witness_hashes).merkle_root
89
+ tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: witness_hashes)
90
+ root_hash = tree.compute_root
89
91
  Bitcoin.double_sha256([root_hash + reserved_value].pack('H*')).bth
90
92
  end
91
93
 
@@ -7,6 +7,10 @@ module Bitcoin
7
7
  :combo
8
8
  end
9
9
 
10
+ def to_hex
11
+ raise ArgumentError, 'musig() is not allowed in top-level combo().' if musig?
12
+ end
13
+
10
14
  def to_scripts
11
15
  candidates = [Pk.new(key), Pkh.new(key)]
12
16
  pubkey = extracted_key
@@ -98,6 +98,8 @@ module Bitcoin
98
98
  is_private = key.is_a?(Bitcoin::ExtKey)
99
99
  paths.each do |path|
100
100
  raise ArgumentError, 'xpub can not derive hardened key.' if !is_private && path.end_with?("'")
101
+ raise ArgumentError, 'Key ranges are not supported.' if path.include?("*")
102
+ raise ArgumentError, 'Key multipath are not supported.' if path.include?("<")
101
103
  if is_private
102
104
  hardened = path.end_with?("'")
103
105
  path = hardened ? path[0..-2] : path
@@ -6,9 +6,9 @@ module Bitcoin
6
6
  # Constructor
7
7
  # @raise [ArgumentError] If +key+ is invalid.
8
8
  def initialize(key)
9
- raise ArgumentError, "Key must be string." unless key.is_a? String
10
- extract_pubkey(key)
9
+ raise ArgumentError, "Key must be string or MuSig." unless key.is_a?(String) || key.is_a?(MuSig)
11
10
  @key = key
11
+ extracted_key
12
12
  end
13
13
 
14
14
  def args
@@ -22,9 +22,14 @@ module Bitcoin
22
22
  # Get extracted key.
23
23
  # @return [Bitcoin::Key] Extracted key.
24
24
  def extracted_key
25
- extract_pubkey(key)
25
+ extract_pubkey(musig? ? key.to_hex : key)
26
26
  end
27
27
 
28
+ # Key is musig or not?
29
+ # @return [Boolean]
30
+ def musig?
31
+ key.is_a?(MuSig)
32
+ end
28
33
  end
29
34
  end
30
35
  end
@@ -0,0 +1,75 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+
4
+ # musig() descriptor class.
5
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0390.mediawiki
6
+ class MuSig < Expression
7
+
8
+ # SHA256 of `MuSig2MuSig2MuSig2`
9
+ # https://github.com/bitcoin/bips/blob/master/bip-0328.mediawiki
10
+ CHAINCODE = '868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965'.htb
11
+
12
+ attr_reader :keys
13
+ attr_reader :path
14
+
15
+ # Constructor.
16
+ # @param [Array] keys An array of key strings.
17
+ # @param [String] path (Optional) derivation path.
18
+ # @return [Bitcoin::Descriptor::MuSig]
19
+ def initialize(keys, path = nil)
20
+ raise ArgumentError, "keys must be an array." unless keys.is_a?(Array)
21
+ unless path.nil?
22
+ raise ArgumentError, "path must be String." unless path.is_a?(String)
23
+ raise ArgumentError, "path must be start with /." unless path.start_with?("/")
24
+ end
25
+ validate_keys!(keys, path)
26
+ @keys = keys
27
+ @path = path
28
+ end
29
+
30
+ def type
31
+ :musig
32
+ end
33
+
34
+ def top_level?
35
+ false
36
+ end
37
+
38
+ # Convert to single key with hex format.
39
+ # @return [String]
40
+ def to_hex
41
+ sorted_key = Schnorr::MuSig2.sort_pubkeys(keys.map{|k| extract_pubkey(k).pubkey})
42
+ agg_key = Schnorr::MuSig2.aggregate(sorted_key)
43
+ if path.nil?
44
+ agg_key.x_only_pubkey
45
+ else
46
+ ext_key = Bitcoin::ExtPubkey.new
47
+ ext_key.pubkey = agg_key.q.to_hex
48
+ ext_key.depth = 0
49
+ ext_key.chain_code = CHAINCODE
50
+ _, *paths = path.split('/')
51
+ derived_key = derive_path(ext_key, paths)
52
+ derived_key.key.xonly_pubkey
53
+ end
54
+ end
55
+
56
+ def to_s(checksum: nil)
57
+ desc = "#{type.to_s}(#{keys.join(',')})"
58
+ desc << path if path
59
+ checksum ? Checksum.descsum_create(desc) : desc
60
+ end
61
+
62
+ private
63
+
64
+ def validate_keys!(keys, path)
65
+ raise ArgumentError, 'musig() cannot have hardened child derivation.' if path && path.include?('h')
66
+ keys.each do |k|
67
+ if path
68
+ raise ArgumentError, 'Ranged musig() requires all participants to be xpubs.' unless k.start_with?('xpub')
69
+ end
70
+ extract_pubkey(k)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -15,6 +15,11 @@ module Bitcoin
15
15
  :pk
16
16
  end
17
17
 
18
+ def to_hex
19
+ raise ArgumentError, 'musig() is not allowed in top-level pk().' if musig?
20
+ super
21
+ end
22
+
18
23
  # Convert to bitcoin script.
19
24
  # @return [Bitcoin::Script]
20
25
  def to_script
@@ -7,6 +7,11 @@ module Bitcoin
7
7
  :pkh
8
8
  end
9
9
 
10
+ def to_hex
11
+ raise ArgumentError, 'musig() is not allowed in top-level pkh().' if musig?
12
+ super
13
+ end
14
+
10
15
  def to_script
11
16
  Script.to_p2pkh(extracted_key.hash160)
12
17
  end
@@ -1,9 +1,17 @@
1
1
  module Bitcoin
2
2
  module Descriptor
3
- # rawtr() expression
3
+ # rawtr() descriptor
4
+ # @see
4
5
  class RawTr < KeyExpression
5
6
  include Bitcoin::Opcodes
6
7
 
8
+ # Constructor
9
+ # @raise [ArgumentError] If +key+ is invalid.
10
+ def initialize(key)
11
+ key = key.to_hex if key.is_a?(MuSig)
12
+ super(key)
13
+ end
14
+
7
15
  def type
8
16
  :rawtr
9
17
  end
@@ -16,6 +16,7 @@ module Bitcoin
16
16
  private
17
17
 
18
18
  def validate!(script)
19
+ raise ArgumentError, "musig() is not allowed in #{type.to_s}()." if script.is_a?(MuSig) || (script.is_a?(KeyExpression) && script.musig?)
19
20
  raise ArgumentError, "Can only have #{script.type.to_s}() at top level." if script.is_a?(Expression) && script.top_level?
20
21
  raise ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().' if script.is_a?(MultiA) || script.is_a?(SortedMultiA)
21
22
  end
@@ -9,6 +9,7 @@ module Bitcoin
9
9
 
10
10
  # Constructor.
11
11
  def initialize(key, tree = nil)
12
+ key = key.to_hex if key.is_a?(MuSig)
12
13
  raise ArgumentError, "Key must be string." unless key.is_a?(String)
13
14
  k = extract_pubkey(key)
14
15
  raise ArgumentError, "Uncompressed key are not allowed." unless k.compressed?
@@ -3,6 +3,7 @@ module Bitcoin
3
3
  # wpkh() expression
4
4
  class Wpkh < KeyExpression
5
5
  def initialize(key)
6
+ raise ArgumentError, 'musig() is not allowed in wpkh().' if key.is_a?(MuSig)
6
7
  super(key)
7
8
  raise ArgumentError, "Uncompressed key are not allowed." unless extract_pubkey(key).compressed?
8
9
  end
@@ -21,45 +21,46 @@ module Bitcoin
21
21
  autoload :SortedMultiA, 'bitcoin/descriptor/sorted_multi_a'
22
22
  autoload :RawTr, 'bitcoin/descriptor/raw_tr'
23
23
  autoload :Checksum, 'bitcoin/descriptor/checksum'
24
+ autoload :MuSig, 'bitcoin/descriptor/musig'
24
25
 
25
26
  module_function
26
27
 
27
- # Generate P2PK output for the given public key.
28
+ # Generate pk() descriptor.
28
29
  # @param [String] key private key or public key with hex format
29
30
  # @return [Bitcoin::Descriptor::Pk]
30
31
  def pk(key)
31
32
  Pk.new(key)
32
33
  end
33
34
 
34
- # Generate P2PKH output for the given public key.
35
+ # Generate pkh() descriptor.
35
36
  # @param [String] key private key or public key with hex format.
36
37
  # @return [Bitcoin::Descriptor::Pkh]
37
38
  def pkh(key)
38
39
  Pkh.new(key)
39
40
  end
40
41
 
41
- # Generate P2PKH output for the given public key.
42
+ # Generate wpkh() descriptor.
42
43
  # @param [String] key private key or public key with hex format.
43
44
  # @return [Bitcoin::Descriptor::Wpkh]
44
45
  def wpkh(key)
45
46
  Wpkh.new(key)
46
47
  end
47
48
 
48
- # Generate P2SH embed the argument.
49
+ # Generate sh() descriptor.
49
50
  # @param [Bitcoin::Descriptor::Base] exp script expression to be embed.
50
51
  # @return [Bitcoin::Descriptor::Sh]
51
52
  def sh(exp)
52
53
  Sh.new(exp)
53
54
  end
54
55
 
55
- # Generate P2WSH embed the argument.
56
+ # Generate wsh() descriptor.
56
57
  # @param [Bitcoin::Descriptor::Expression] exp script expression to be embed.
57
58
  # @return [Bitcoin::Descriptor::Wsh]
58
59
  def wsh(exp)
59
60
  Wsh.new(exp)
60
61
  end
61
62
 
62
- # An alias for the collection of `pk(KEY)` and `pkh(KEY)`.
63
+ # Generate combo() descriptor.
63
64
  # If the key is compressed, it also includes `wpkh(KEY)` and `sh(wpkh(KEY))`.
64
65
  # @param [String] key private key or public key with hex format.
65
66
  # @return [Bitcoin::Descriptor::Combo]
@@ -67,7 +68,7 @@ module Bitcoin
67
68
  Combo.new(key)
68
69
  end
69
70
 
70
- # Generate multisig output for given keys.
71
+ # Generate multi() descriptor.
71
72
  # @param [Integer] threshold the threshold of multisig.
72
73
  # @param [Array[String]] keys an array of keys.
73
74
  # @return [Bitcoin::Descriptor::Multi] multisig script.
@@ -75,7 +76,7 @@ module Bitcoin
75
76
  Multi.new(threshold, keys)
76
77
  end
77
78
 
78
- # Generate sorted multisig output for given keys.
79
+ # Generate sortedmulti() descriptor.
79
80
  # @param [Integer] threshold the threshold of multisig.
80
81
  # @param [Array[String]] keys an array of keys.
81
82
  # @return [Bitcoin::Descriptor::SortedMulti]
@@ -83,21 +84,21 @@ module Bitcoin
83
84
  SortedMulti.new(threshold, keys)
84
85
  end
85
86
 
86
- # Generate raw output script about +hex+.
87
+ # Generate raw() descriptor.
87
88
  # @param [String] hex Hex string of bitcoin script.
88
89
  # @return [Bitcoin::Descriptor::Raw]
89
90
  def raw(hex)
90
91
  Raw.new(hex)
91
92
  end
92
93
 
93
- # Generate raw output script about +hex+.
94
+ # Generate addr() descriptor.
94
95
  # @param [String] addr Bitcoin address.
95
96
  # @return [Bitcoin::Descriptor::Addr]
96
97
  def addr(addr)
97
98
  Addr.new(addr)
98
99
  end
99
100
 
100
- # Generate taproot output script descriptor.
101
+ # Generate tr() descriptor.
101
102
  # @param [String] key
102
103
  # @param [String] tree
103
104
  # @return [Bitcoin::Descriptor::Tr]
@@ -105,14 +106,14 @@ module Bitcoin
105
106
  Tr.new(key, tree)
106
107
  end
107
108
 
108
- # Generate taproot output script descriptor.
109
+ # Generate rawtr() descriptor.
109
110
  # @param [String] key
110
111
  # @return [Bitcoin::Descriptor::RawTr]
111
112
  def rawtr(key)
112
113
  RawTr.new(key)
113
114
  end
114
115
 
115
- # Generate tapscript multisig output for given keys.
116
+ # Generate multi_a() descriptor.
116
117
  # @param [Integer] threshold the threshold of multisig.
117
118
  # @param [Array[String]] keys an array of keys.
118
119
  # @return [Bitcoin::Descriptor::MultiA] multisig script.
@@ -120,7 +121,7 @@ module Bitcoin
120
121
  MultiA.new(threshold, keys)
121
122
  end
122
123
 
123
- # Generate tapscript sorted multisig output for given keys.
124
+ # Generate sortedmulti_a() descriptor.
124
125
  # @param [Integer] threshold the threshold of multisig.
125
126
  # @param [Array[String]] keys an array of keys.
126
127
  # @return [Bitcoin::Descriptor::SortedMulti]
@@ -128,16 +129,30 @@ module Bitcoin
128
129
  SortedMultiA.new(threshold, keys)
129
130
  end
130
131
 
132
+ # Generate musig() descriptor.
133
+ # @param [Array] keys An array fo keys.
134
+ # @param [String] path
135
+ # @return [Bitcoin::Descriptor::MuSig]
136
+ def musig(*keys, path: nil)
137
+ MuSig.new(keys, path)
138
+ end
139
+
131
140
  # Parse descriptor string.
132
141
  # @param [String] string Descriptor string.
133
142
  # @return [Bitcoin::Descriptor::Expression]
134
143
  def parse(string, top_level = true)
135
144
  validate_checksum!(string)
136
145
  content, _ = string.split('#')
137
- exp, args_str = content.match(/(\w+)\((.+)\)/).captures
146
+ exp, args_str, path = content.match(/(\w+)\((.+)\)(.*)/).captures # split "tag(10, 20)/0/1"
147
+ raise ArgumentError, 'Invalid format.' if exp != 'musig' && !path.empty?
148
+
138
149
  case exp
139
150
  when 'pk'
140
- pk(args_str)
151
+ if args_str.include?('(')
152
+ pk(parse(args_str))
153
+ else
154
+ pk(args_str)
155
+ end
141
156
  when 'pkh'
142
157
  pkh(args_str)
143
158
  when 'wpkh'
@@ -169,16 +184,24 @@ module Bitcoin
169
184
  when 'addr'
170
185
  addr(args_str)
171
186
  when 'tr'
172
- key, rest = args_str.split(',', 2)
173
- if rest.nil?
187
+ key, tree = split_two(args_str)
188
+ key = parse(key) if key.include?('(')
189
+ if tree.nil?
174
190
  tr(key)
175
- elsif rest.start_with?('{')
176
- tr(key, parse_nested_string(rest))
191
+ elsif tree.start_with?('{')
192
+ tr(key, parse_nested_string(tree))
177
193
  else
178
- tr(key, parse(rest, false))
194
+ tr(key, parse(tree, false))
179
195
  end
180
196
  when 'rawtr'
181
- rawtr(args_str)
197
+ if args_str.include?('(')
198
+ rawtr(parse(args_str, false))
199
+ else
200
+ rawtr(args_str)
201
+ end
202
+ when 'musig'
203
+ keys = args_str.split(',')
204
+ path.empty? ? musig(*keys) : musig(*keys, path: path)
182
205
  else
183
206
  raise ArgumentError, "Parse failed: #{string}"
184
207
  end
@@ -229,5 +252,23 @@ module Bitcoin
229
252
  current << parse(buffer, false) unless buffer.empty?
230
253
  current.first
231
254
  end
255
+
256
+ def split_two(content)
257
+ paren_depth = 0
258
+ split_pos = content.chars.find_index do |char|
259
+ case char
260
+ when '(' then paren_depth += 1; false
261
+ when ')' then paren_depth -= 1; false
262
+ when ',' then paren_depth == 0
263
+ else false
264
+ end
265
+ end
266
+
267
+ if split_pos
268
+ [content[0...split_pos], content[split_pos+1..-1]]
269
+ else
270
+ [content, nil]
271
+ end
272
+ end
232
273
  end
233
274
  end
data/lib/bitcoin/key.rb CHANGED
@@ -34,6 +34,9 @@ module Bitcoin
34
34
  @key_type = key_type
35
35
  compressed = @key_type != TYPES[:uncompressed]
36
36
  else
37
+ if pubkey && compressed && pubkey.length != COMPRESSED_PUBLIC_KEY_SIZE * 2
38
+ raise ArgumentError, "Invalid compressed pubkey length."
39
+ end
37
40
  @key_type = compressed ? TYPES[:compressed] : TYPES[:uncompressed]
38
41
  end
39
42
  @secp256k1_module = Bitcoin.secp_impl
@@ -2,7 +2,7 @@ module Bitcoin
2
2
  module Message
3
3
 
4
4
  # merckleblock message
5
- # https://bitcoin.org/en/developer-reference#merkleblock
5
+ # https://developer.bitcoin.org/reference/p2p_networking.html#merkleblock
6
6
  class MerkleBlock < Base
7
7
 
8
8
  COMMAND = 'merkleblock'
@@ -36,6 +36,12 @@ module Bitcoin
36
36
  hashes.map(&:htb).join << Bitcoin.pack_var_int(flags.htb.bytesize) << flags.htb
37
37
  end
38
38
 
39
+ # Generate partial tree.
40
+ # @return [Bitcoin::PartialTree]
41
+ def partial_tree
42
+ Bitcoin::PartialTree.build(tx_count, hashes, Bitcoin.byte_to_bit(flags.htb))
43
+ end
44
+
39
45
  end
40
46
 
41
47
  end
@@ -1,7 +1,8 @@
1
1
  module Bitcoin
2
2
 
3
- # merkle tree
4
- class MerkleTree
3
+ # A class for recovering partial from merkleblock message.
4
+ # For a complete Merkle tree implementation, migrate to the merkle gem.
5
+ class PartialTree
5
6
 
6
7
  attr_accessor :root
7
8
 
@@ -13,21 +14,8 @@ module Bitcoin
13
14
  root.value
14
15
  end
15
16
 
16
- def self.build_from_leaf(txids)
17
- if txids.size == 1
18
- nodes = [Node.new(txids.first)]
19
- else
20
- nodes = txids.each_slice(2).map{ |m|
21
- left = Node.new(m[0])
22
- right = Node.new(m[1] ? m[1] : m[0])
23
- [left, right]
24
- }.flatten
25
- end
26
- new(build_initial_tree(nodes))
27
- end
28
-
29
17
  # https://bitcoin.org/en/developer-reference#creating-a-merkleblock-message
30
- def self.build_partial(tx_count, hashes, flags)
18
+ def self.build(tx_count, hashes, flags)
31
19
  flags = flags.each_char.map(&:to_i)
32
20
  root = build_initial_tree( Array.new(tx_count) { Node.new })
33
21
  current_node = root
@@ -1,5 +1,92 @@
1
1
  module Bitcoin
2
2
  module SilentPayment
3
- autoload :Addr, 'bitcoin/sp/addr'
3
+
4
+
5
+ # Derive payment
6
+ # @param [Array] prevouts An array of previous output script(Bitcoin::Script).
7
+ # @param [Array] private_keys An array of private key corresponding to each public key in prevouts.
8
+ # @param [Array] recipients
9
+ # @return [Array]
10
+ # @raise [ArgumentError]
11
+ def derive_payment_points(prevouts, private_keys, recipients)
12
+ raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
13
+ raise ArgumentError, "private_keys must be Array." unless private_keys.is_a? Array
14
+ raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
15
+ raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array
16
+
17
+ outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
18
+
19
+ input_pub_keys = []
20
+ field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
21
+ sum_priv_keys = 0
22
+ prevouts.each_with_index do |prevout, index|
23
+ k = Bitcoin::Key.new(priv_key: private_keys[index].to_s(16))
24
+ public_key = extract_public_key(prevout, inputs[index])
25
+ next if public_key.nil?
26
+ private_key = if public_key.p2tr? && k.to_point.y.odd?
27
+ field.mod(-private_keys[index])
28
+ else
29
+ private_keys[index]
30
+ end
31
+ input_pub_keys << public_key
32
+ sum_priv_keys = field.mod(sum_priv_keys + private_key)
33
+ end
34
+ agg_pubkey = (Bitcoin::Secp256k1::GROUP.generator.to_jacobian * sum_priv_keys).to_affine
35
+ return [] if agg_pubkey.infinity?
36
+
37
+ input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + agg_pubkey.to_hex.htb).bth
38
+
39
+ destinations = {}
40
+ recipients.each do |sp_addr|
41
+ raise ArgumentError, "recipients element must be Bech32::SilentPaymentAddr." unless sp_addr.is_a? Bech32::SilentPaymentAddr
42
+ destinations[sp_addr.scan_key] = [] unless destinations.has_key?(sp_addr.scan_key)
43
+ destinations[sp_addr.scan_key] << sp_addr.spend_key
44
+ end
45
+ outputs = []
46
+ destinations.each do |scan_key, spends|
47
+ scan_key = Bitcoin::Key.new(pubkey: scan_key).to_point.to_jacobian
48
+ ecdh_shared_secret = (scan_key * field.mod(input_hash.to_i(16) * sum_priv_keys)).to_affine.to_hex.htb
49
+ spends.each.with_index do |spend, i|
50
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [i].pack('N'))
51
+ spend_key = Bitcoin::Key.new(pubkey: spend).to_point.to_jacobian
52
+ outputs << (spend_key + Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bth.to_i(16)).to_affine
53
+ end
54
+ end
55
+ outputs
56
+ end
57
+
58
+ # Extract public keys from +prevout+ and input.
59
+ def extract_public_key(prevout, input)
60
+ if prevout.p2pkh?
61
+ spk_hash = prevout.chunks[2].pushed_data.bth
62
+ input.script_sig.chunks.reverse.each do |chunk|
63
+ next unless chunk.pushdata?
64
+ pubkey = chunk.pushed_data.bth
65
+ if Bitcoin.hash160(pubkey) == spk_hash
66
+ return Bitcoin::Key.new(pubkey: pubkey) if pubkey.htb.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
67
+ end
68
+ end
69
+ elsif prevout.p2sh?
70
+ redeem_script = Bitcoin::Script.parse_from_payload(input.script_sig.chunks.last.pushed_data)
71
+ if redeem_script.p2wpkh?
72
+ pk = input.script_witness.stack.last
73
+ return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
74
+ end
75
+ elsif prevout.p2wpkh?
76
+ pk = input.script_witness.stack.last
77
+ return Bitcoin::Key.new(pubkey: pk.bth) if pk.bytesize == Bitcoin::Key::COMPRESSED_PUBLIC_KEY_SIZE
78
+ elsif prevout.p2tr?
79
+ witness_stack = input.script_witness.stack.dup
80
+ witness_stack.pop if witness_stack.last.bth.start_with?("50")
81
+ if witness_stack.length > 1
82
+ # script-path
83
+ cb = Bitcoin::Taproot::ControlBlock.parse_from_payload(witness_stack.last)
84
+ return nil if cb.internal_key == Bitcoin::Taproot::NUMS_H
85
+ end
86
+ pubkey = Bitcoin::Key.from_xonly_pubkey(prevout.chunks[1].pushed_data.bth)
87
+ return pubkey if pubkey.compressed?
88
+ end
89
+ nil
90
+ end
4
91
  end
5
92
  end
@@ -31,33 +31,22 @@ module Bitcoin
31
31
  raise NotImplementedError
32
32
  end
33
33
 
34
- def control_block(leaf)
35
- raise NotImplementedError # TODO
36
- end
37
-
38
- def inclusion_proof(leaf)
39
- raise NotImplementedError # TODO
40
- end
41
-
42
34
  private
43
35
 
44
36
  def merkle_root
45
- build_tree(tree).bth
37
+ return '' if tree.empty?
38
+ script_tree = Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: extract_leaves(tree))
39
+ script_tree.compute_root
46
40
  end
47
41
 
48
- def build_tree(tree)
49
- left, right = tree
50
- left_hash = if left.is_a?(Array)
51
- build_tree(left)
52
- else
53
- left
54
- end
55
- right_hash = if right.is_a?(Array)
56
- build_tree(right)
57
- else
58
- right
59
- end
60
- combine_hash([left_hash, right_hash])
42
+ def extract_leaves(leaves)
43
+ leaves.map do |leaf|
44
+ if leaf.is_a?(Bitcoin::Taproot::LeafNode)
45
+ leaf.leaf_hash
46
+ elsif leaf.is_a?(Array)
47
+ extract_leaves(leaf)
48
+ end
49
+ end
61
50
  end
62
51
  end
63
52
  end
@@ -20,7 +20,6 @@ module Bitcoin
20
20
  raise Error, "Internal public key must be #{X_ONLY_PUBKEY_SIZE} bytes" unless internal_key.htb.bytesize == X_ONLY_PUBKEY_SIZE
21
21
  raise Error, 'leaf must be Bitcoin::Taproot::LeafNode object' if leaves.find{ |leaf| !leaf.is_a?(Bitcoin::Taproot::LeafNode)}
22
22
 
23
- @leaves = leaves
24
23
  @branches = leaves.each_slice(2).map.to_a
25
24
  @internal_key = internal_key
26
25
  end
@@ -75,74 +74,46 @@ module Bitcoin
75
74
  # @param [Bitcoin::Taproot::LeafNode] leaf Leaf to use for unlocking.
76
75
  # @return [Bitcoin::Taproot::ControlBlock] control block.
77
76
  def control_block(leaf)
78
- path = inclusion_proof(leaf)
77
+ path = inclusion_proof(leaf).siblings
79
78
  parity = tweak_public_key.to_point.has_even_y? ? 0 : 1
80
- ControlBlock.new(parity, leaf.leaf_ver, internal_key, path.map(&:bth))
79
+ ControlBlock.new(parity, leaf.leaf_ver, internal_key, path)
81
80
  end
82
81
 
83
82
  # Generate inclusion proof for +leaf+.
84
83
  # @param [Bitcoin::Taproot::LeafNode] leaf The leaf node in script tree.
85
- # @return [Array[String]] Inclusion proof.
84
+ # @return [Merkle::Proof] Inclusion proof.
86
85
  # @raise [Bitcoin::Taproot::Error] If the specified +leaf+ does not exist
87
86
  def inclusion_proof(leaf)
88
- proofs = []
89
- target_branch = branches.find{|b| b.include?(leaf)}
90
- raise Error 'Specified leaf does not exist' unless target_branch
91
-
92
- # flatten each branch
93
- proofs << hash_value(target_branch.find{|b| b != leaf}) if target_branch.size == 2
94
- parent_hash = combine_hash(target_branch)
95
- parents = branches.map {|pair| combine_hash(pair)}
96
-
97
- until parents.size == 1
98
- parents = parents.each_slice(2).map do |pair|
99
- combined = combine_hash(pair)
100
- unless pair.size == 1
101
- if hash_value(pair[0]) == parent_hash
102
- proofs << hash_value(pair[1])
103
- parent_hash = combined
104
- elsif hash_value(pair[1]) == parent_hash
105
- proofs << hash_value(pair[0])
106
- parent_hash = combined
107
- end
108
- end
109
- combined
87
+ tree = script_tree
88
+ leaf_index = 0
89
+ branches.each.with_index do |branch, i|
90
+ if branch.include?(leaf)
91
+ leaf_index += (branch[0] == leaf ? 0 : 1)
92
+ break
93
+ else
94
+ leaf_index += branch.length
110
95
  end
111
96
  end
112
- proofs
97
+ tree.generate_proof(leaf_index)
113
98
  end
114
99
 
115
100
  private
116
101
 
117
- # Calculate merkle root from branches.
118
- # @return [String] merkle root with hex format.
119
- def merkle_root
120
- parents = branches.map {|pair| combine_hash(pair)}
121
- if parents.empty?
122
- parents = ['']
123
- elsif parents.size == 1
124
- parents = [combine_hash(parents)]
125
- else
126
- parents = parents.each_slice(2).map { |pair| combine_hash(pair) } until parents.size == 1
127
- end
128
- parents.first.bth
129
- end
130
-
131
- def combine_hash(pair)
132
- if pair.size == 1
133
- hash_value(pair[0])
134
- else
135
- hash1 = hash_value(pair[0])
136
- hash2 = hash_value(pair[1])
137
-
138
- # Lexicographically sort a and b's hash, and compute parent hash.
139
- payload = hash1.bth < hash2.bth ? hash1 + hash2 : hash2 + hash1
140
- Bitcoin.tagged_hash('TapBranch', payload)
102
+ def script_tree
103
+ leaves = []
104
+ branches.each do |pair|
105
+ if leaves.empty? || leaves.length == 1
106
+ leaves << pair.map(&:leaf_hash)
107
+ elsif leaves.length == 2
108
+ leaves = [leaves, pair.map(&:leaf_hash)]
109
+ end
141
110
  end
111
+ Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: leaves)
142
112
  end
143
113
 
144
- def hash_value(leaf_or_branch)
145
- leaf_or_branch.is_a?(LeafNode) ? leaf_or_branch.leaf_hash : leaf_or_branch
114
+ def merkle_root
115
+ return '' if branches.empty?
116
+ script_tree.compute_root
146
117
  end
147
118
  end
148
119
  end
@@ -8,6 +8,8 @@ module Bitcoin
8
8
  autoload :SimpleBuilder, 'bitcoin/taproot/simple_builder'
9
9
  autoload :CustomDepthBuilder, 'bitcoin/taproot/custom_depth_builder'
10
10
 
11
+ NUMS_H = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
12
+
11
13
  module_function
12
14
 
13
15
  # Calculate tweak value from +internal_pubkey+ and +merkle_root+.
data/lib/bitcoin/tx.rb CHANGED
@@ -7,6 +7,7 @@ module Bitcoin
7
7
  class Tx
8
8
 
9
9
  include Bitcoin::HexConverter
10
+ include SilentPayment
10
11
 
11
12
  MAX_STANDARD_VERSION = 2
12
13
 
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.8.1"
2
+ VERSION = "1.9.0"
3
3
  end
data/lib/bitcoin.rb CHANGED
@@ -10,6 +10,7 @@ require 'bech32'
10
10
  require 'base64'
11
11
  require 'observer'
12
12
  require 'tmpdir'
13
+ require 'merkle'
13
14
  require_relative 'openassets'
14
15
 
15
16
  module Bitcoin
@@ -31,7 +32,7 @@ module Bitcoin
31
32
  autoload :TxOut, 'bitcoin/tx_out'
32
33
  autoload :OutPoint, 'bitcoin/out_point'
33
34
  autoload :ScriptWitness, 'bitcoin/script_witness'
34
- autoload :MerkleTree, 'bitcoin/merkle_tree'
35
+ autoload :PartialTree, 'bitcoin/partial_tree'
35
36
  autoload :Key, 'bitcoin/key'
36
37
  autoload :ExtKey, 'bitcoin/ext_key'
37
38
  autoload :ExtPubkey, 'bitcoin/ext_key'
@@ -71,9 +72,11 @@ module Bitcoin
71
72
 
72
73
  @chain_param = :mainnet
73
74
 
75
+ AVAILABLE_NETWORKS = %i(mainnet testnet regtest signet testnet4)
76
+
74
77
  # set bitcoin network chain params
75
78
  def self.chain_params=(name)
76
- raise "chain params for #{name} is not defined." unless %i(mainnet testnet regtest signet).include?(name.to_sym)
79
+ raise "chain params for #{name} is not defined." unless AVAILABLE_NETWORKS.include?(name.to_sym)
77
80
  @current_chain = nil
78
81
  @chain_param = name.to_sym
79
82
  end
@@ -90,6 +93,8 @@ module Bitcoin
90
93
  @current_chain = Bitcoin::ChainParams.regtest
91
94
  when :signet
92
95
  @current_chain = Bitcoin::ChainParams.signet
96
+ when :testnet4
97
+ @current_chain = Bitcoin::ChainParams.testnet4
93
98
  end
94
99
  @current_chain
95
100
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitcoinrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ecdsa_ext
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 1.3.0
60
+ version: 1.5.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: 1.3.0
67
+ version: 1.5.0
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: daemon-spawn
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -233,6 +233,20 @@ dependencies:
233
233
  - - ">="
234
234
  - !ruby/object:Gem::Version
235
235
  version: '0'
236
+ - !ruby/object:Gem::Dependency
237
+ name: merkle
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - '='
241
+ - !ruby/object:Gem::Version
242
+ version: 0.3.0
243
+ type: :runtime
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - '='
248
+ - !ruby/object:Gem::Version
249
+ version: 0.3.0
236
250
  description: The implementation of Bitcoin Protocol for Ruby.
237
251
  email:
238
252
  - azuchi@chaintope.com
@@ -287,6 +301,7 @@ files:
287
301
  - lib/bitcoin/descriptor/key_expression.rb
288
302
  - lib/bitcoin/descriptor/multi.rb
289
303
  - lib/bitcoin/descriptor/multi_a.rb
304
+ - lib/bitcoin/descriptor/musig.rb
290
305
  - lib/bitcoin/descriptor/pk.rb
291
306
  - lib/bitcoin/descriptor/pkh.rb
292
307
  - lib/bitcoin/descriptor/raw.rb
@@ -308,7 +323,6 @@ files:
308
323
  - lib/bitcoin/key.rb
309
324
  - lib/bitcoin/key_path.rb
310
325
  - lib/bitcoin/logger.rb
311
- - lib/bitcoin/merkle_tree.rb
312
326
  - lib/bitcoin/message.rb
313
327
  - lib/bitcoin/message/addr.rb
314
328
  - lib/bitcoin/message/addr_v2.rb
@@ -376,6 +390,7 @@ files:
376
390
  - lib/bitcoin/node/spv.rb
377
391
  - lib/bitcoin/opcodes.rb
378
392
  - lib/bitcoin/out_point.rb
393
+ - lib/bitcoin/partial_tree.rb
379
394
  - lib/bitcoin/payment_code.rb
380
395
  - lib/bitcoin/psbt.rb
381
396
  - lib/bitcoin/psbt/hd_key_path.rb
@@ -404,7 +419,6 @@ files:
404
419
  - lib/bitcoin/slip39/share.rb
405
420
  - lib/bitcoin/slip39/sss.rb
406
421
  - lib/bitcoin/slip39/wordlist/english.txt
407
- - lib/bitcoin/sp/addr.rb
408
422
  - lib/bitcoin/store.rb
409
423
  - lib/bitcoin/store/chain_entry.rb
410
424
  - lib/bitcoin/store/db.rb
@@ -450,7 +464,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
450
464
  - !ruby/object:Gem::Version
451
465
  version: '0'
452
466
  requirements: []
453
- rubygems_version: 3.6.3
467
+ rubygems_version: 3.6.9
454
468
  specification_version: 4
455
469
  summary: The implementation of Bitcoin Protocol for Ruby.
456
470
  test_files: []
@@ -1,55 +0,0 @@
1
- module Bitcoin
2
- module SilentPayment
3
- class Addr
4
-
5
- HRP_MAINNET = 'sp'
6
- HRP_TESTNET = 'tsp'
7
- MAX_CHARACTERS = 1023
8
-
9
- attr_reader :version
10
- attr_reader :scan_key
11
- attr_reader :spend_key
12
-
13
- # Constructor.
14
- # @param [Bitcoin::Key] scan_key
15
- # @param [Bitcoin::Key] spend_key
16
- def initialize(version, scan_key:, spend_key:)
17
- raise ArgumentError, "version must be integer." unless version.is_a?(Integer)
18
- raise ArgumentError, "scan_key must be Bitcoin::Key." unless scan_key.is_a?(Bitcoin::Key)
19
- raise ArgumentError, "spend_key must be Bitcoin::Key." unless spend_key.is_a?(Bitcoin::Key)
20
- raise ArgumentError, "version '#{version}' is unsupported." unless version.zero?
21
-
22
- @version = version
23
- @scan_key = scan_key
24
- @spend_key = spend_key
25
- end
26
-
27
- # Parse silent payment address.
28
- # @param [String] A silent payment address.
29
- # @return [Bitcoin::SilentPayment::Addr]
30
- def self.from_string(addr)
31
- raise ArgumentError, "addr must be string." unless addr.is_a?(String)
32
- hrp, data, spec = Bech32.decode(addr, MAX_CHARACTERS)
33
- unless hrp == Bitcoin.chain_params.mainnet? ? HRP_MAINNET : HRP_TESTNET
34
- raise ArgumentError, "The specified hrp is different from the current network HRP."
35
- end
36
- raise ArgumentError, "spec must be bech32m." unless spec == Bech32::Encoding::BECH32M
37
-
38
- ver = data[0]
39
- payload = Bech32.convert_bits(data[1..-1], 5, 8, false).pack("C*")
40
- scan_key = Bitcoin::Key.new(pubkey: payload[0...33].bth, key_type: Bitcoin::Key::TYPES[:compressed])
41
- spend_key = Bitcoin::Key.new(pubkey: payload[33..-1].bth, key_type: Bitcoin::Key::TYPES[:compressed])
42
- Addr.new(ver, scan_key: scan_key, spend_key: spend_key)
43
- end
44
-
45
- # Get silent payment address.
46
- # @return [String]
47
- def to_s
48
- hrp = Bitcoin.chain_params.mainnet? ? HRP_MAINNET : HRP_TESTNET
49
- payload = [scan_key.pubkey + spend_key.pubkey].pack("H*").unpack('C*')
50
- Bech32.encode(hrp, [version] + Bech32.convert_bits(payload, 8, 5), Bech32::Encoding::BECH32M)
51
- end
52
-
53
- end
54
- end
55
- end