bitcoinrb 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 900806837ee6cd6b011a50d6a58c37ea233079e8b86a594c6a5031c338f9a490
4
- data.tar.gz: 34b437fee384d630d9b7f1a47faa8175692d9ebe21408d4720828140461b152c
3
+ metadata.gz: f2697b9fbcca175453c80fc67d70466845c7141e6b9e9f193b5825d34bd7ffa3
4
+ data.tar.gz: 61a5c26a7cfaf7abcc7c692386ff08091ea0249c9e8ea9ccb4f3231049319f47
5
5
  SHA512:
6
- metadata.gz: 87bcfdb59403593475f90b49f9f67299bee3bfd8cfae7089080e95ab8e1ffddcdc21ebbb72f8ca49f1caea9d8df2ea5ca613eb97b698ddab3fb3d3c617611165
7
- data.tar.gz: 55dcbb9ce3f10a5dd822e98605eb00fa74f3034a8852cdcd714b4dff5cd347cbdc55bffd49ae2f74cc4aeaad2bf86d01d635e7146207ccb6557f55ae0849ff0e
6
+ metadata.gz: 40574931606fc1ff074f638bda7042f86c0225a5661f37622971e0b9502e039c5010d31663c34846c80f0bff2d25a02122eb4773e30a6fadad72bb170260b114
7
+ data.tar.gz: aaefa2672459d1d133191aecf8cbe0e8391210647e109c6e5205e349e264ad2303b25c36d4812daf5f5f0e88fb15cc4b0bfc54653ba0a2b4a8bac9fd5fd9b5d6
@@ -22,7 +22,7 @@ jobs:
22
22
  ruby-version: ['3.0', '3.1', '3.2', '3.3']
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Install leveldb
27
27
  run: sudo apt-get install libleveldb-dev
28
28
  - name: Set up Ruby
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Bitcoinrb [![Build Status](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml) [![Gem Version](https://badge.fury.io/rb/bitcoinrb.svg)](https://badge.fury.io/rb/bitcoinrb) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) <img src="http://segwit.co/static/public/images/logo.png" width="100">
2
2
 
3
-
4
3
  Bitcoinrb is a Ruby implementation of Bitcoin Protocol.
5
4
 
6
5
  NOTE: Bitcoinrb work in progress, and there is a possibility of incompatible change.
@@ -18,10 +17,9 @@ Bitcoinrb supports following feature:
18
17
  * bech32([BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)) and bech32m([BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)) address support
19
18
  * [BIP-174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki) PSBT(Partially Signed Bitcoin Transaction) support
20
19
  * [BIP-85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) Deterministic Entropy From BIP32 Keychains support by `Bitcoin::BIP85Entropy` class.
21
- * Schnorr signature([BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
22
- * Taproot consensus([BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and [BIP-342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki))
23
- * [WIP] SPV node
24
- * [WIP] 0ff-chain protocol
20
+ * Schnorr signature([BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
21
+ * Taproot consensus([BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and [BIP-342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki))
22
+ * [Output script descriptor](https://github.com/chaintope/bitcoinrb/wiki/Output-Script-Descriptor) ([BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki), [BIP-381](https://github.com/bitcoin/bips/blob/master/bip-0381.mediawiki), [BIP-382](https://github.com/bitcoin/bips/blob/master/bip-0382.mediawiki), [BIP-383](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki), [BIP-384](https://github.com/bitcoin/bips/blob/master/bip-0384.mediawiki), [BIP-385](https://github.com/bitcoin/bips/blob/master/bip-0385.mediawiki), [BIP-386](https://github.com/bitcoin/bips/blob/master/bip-0386.mediawiki), [BIP-387](https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki))
25
23
 
26
24
  ## Requirements
27
25
 
@@ -112,6 +110,10 @@ Therefore, some tests require this library. In a Linux environment, `spec/lib/li
112
110
  so there is no need to do anything. If you want to test in another environment,
113
111
  please set the library path in the environment variable `TEST_LIBSECP256K1_PATH`.
114
112
 
113
+ In case the supplied linux `spec/lib/libsecp256k1.so` is not working (architecture), you might have to compile it yourself.
114
+ Since if available in the repository, it might not be compiled using the `./configure --enable-module-recovery` option.
115
+ Then `TEST_LIBSECP256K1_PATH=/path/to/secp256k1/.libs/libsecp256k1.so rspec` can be used.
116
+
115
117
  The libsecp256k1 library currently tested for operation with this library is `v0.4.0`.
116
118
 
117
119
  ## Contributing
@@ -0,0 +1,31 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ class Addr < Expression
4
+ include Bitcoin::Util
5
+
6
+ attr_reader :addr
7
+
8
+ def initialize(addr)
9
+ raise ArgumentError, "Address must be string." unless addr.is_a?(String)
10
+ raise ArgumentError, "Address is not valid." unless valid_address?(addr)
11
+ @addr = addr
12
+ end
13
+
14
+ def type
15
+ :addr
16
+ end
17
+
18
+ def to_script
19
+ Bitcoin::Script.parse_from_addr(addr)
20
+ end
21
+
22
+ def top_level?
23
+ true
24
+ end
25
+
26
+ def args
27
+ addr
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,74 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # Descriptor checksum.
4
+ # https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#checksum
5
+ module Checksum
6
+
7
+ INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
8
+ CHECKSUM_CHARSET = Bech32::CHARSET
9
+ GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD]
10
+
11
+ module_function
12
+
13
+ # Verify that the checksum is correct in a descriptor
14
+ # @param [String] s Descriptor string.
15
+ # @return [Boolean]
16
+ def descsum_check(s)
17
+ return false unless s[-9] == '#'
18
+ s[-8..-1].each_char do |c|
19
+ return false unless CHECKSUM_CHARSET.include?(c)
20
+ end
21
+ symbols = descsum_expand(s[0...-9]) + s[-8..-1].each_char.map{|c|CHECKSUM_CHARSET.index(c)}
22
+ descsum_polymod(symbols) == 1
23
+ end
24
+
25
+ # Add a checksum to a descriptor without
26
+ # @param [String] s Descriptor string without checksum.
27
+ # @return [String] Descriptor string with checksum.
28
+ def descsum_create(s)
29
+ symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0]
30
+ checksum = descsum_polymod(symbols) ^ 1
31
+ result = 8.times.map do |i|
32
+ CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31]
33
+ end.join
34
+ "#{s}##{result}"
35
+ end
36
+
37
+ # Internal function that does the character to symbol expansion.
38
+ # @param [String] s Descriptor string without checksum.
39
+ # @return [Array] symbols. An array of integer.
40
+ def descsum_expand(s)
41
+ groups = []
42
+ symbols = []
43
+ s.each_char do |c|
44
+ return nil unless INPUT_CHARSET.include?(c)
45
+ v = INPUT_CHARSET.index(c)
46
+ symbols << (v & 31)
47
+ groups << (v >> 5)
48
+ if groups.length == 3
49
+ symbols << (groups[0] * 9 + groups[1] * 3 + groups[2])
50
+ groups = []
51
+ end
52
+ end
53
+ symbols << groups[0] if groups.length == 1
54
+ symbols << (groups[0] * 3 + groups[1]) if groups.length == 2
55
+ symbols
56
+ end
57
+
58
+ # Internal function that computes the descriptor checksum.
59
+ # @param [Array] symbols
60
+ # @return [Integer]
61
+ def descsum_polymod(symbols)
62
+ chk = 1
63
+ symbols.each do |value|
64
+ top = chk >> 35
65
+ chk = (chk & 0x7FFFFFFFF) << 5 ^ value
66
+ 5.times do |i|
67
+ chk ^= GENERATOR[i] if ((top >> i) & 1) == 1
68
+ end
69
+ end
70
+ chk
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # combo() expression
4
+ class Combo < KeyExpression
5
+
6
+ def type
7
+ :combo
8
+ end
9
+
10
+ def to_scripts
11
+ candidates = [Pk.new(key), Pkh.new(key)]
12
+ pubkey = extract_pubkey(key)
13
+ if pubkey.compressed?
14
+ candidates << Wpkh.new(pubkey.pubkey)
15
+ candidates << Sh.new(candidates.last)
16
+ end
17
+ candidates.map(&:to_script)
18
+ end
19
+
20
+ def ==(other)
21
+ return false unless other.is_a?(Combo)
22
+ type == other.type && to_scripts == other.to_scripts
23
+ end
24
+
25
+ def top_level?
26
+ true
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,122 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # Expression for descriptor.
4
+ class Expression
5
+
6
+ # Get expression type.
7
+ # @return [Symbol]
8
+ def type
9
+ raise NotImplementedError
10
+ end
11
+
12
+ # Convert to bitcoin script
13
+ # @return [Bitcoin::Script]
14
+ def to_script
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Whether this is top level or not.
19
+ # @return [Boolean]
20
+ def top_level?
21
+ raise NotImplementedError
22
+ end
23
+
24
+ # Get args for this expression.
25
+ # @return [String] args
26
+ def args
27
+ raise NotImplementedError
28
+ end
29
+
30
+ # Get descriptor string.
31
+ # @param [Boolean] checksum If true, append checksum.
32
+ # @return [String] Descriptor string.
33
+ def to_s(checksum: false)
34
+ desc = "#{type.to_s}(#{args})"
35
+ checksum ? Checksum.descsum_create(desc) : desc
36
+ end
37
+
38
+ # Convert to bitcoin script as hex string.
39
+ # @return [String]
40
+ def to_hex
41
+ to_script.to_hex
42
+ end
43
+
44
+ # Check whether +key+ is compressed public key or not.
45
+ # @return [Boolean]
46
+ def compressed_key?(key)
47
+ %w(02 03).include?(key[0..1]) && [key].pack("H*").bytesize == 33
48
+ end
49
+
50
+ # Extract public key from KEY format.
51
+ # @param [String] key KEY string.
52
+ # @return [Bitcoin::Key] public key.
53
+ def extract_pubkey(key)
54
+ if key.start_with?('[') # BIP32 fingerprint
55
+ raise ArgumentError, "Multiple ']' characters found for a single pubkey." if key.count('[') > 1 || key.count(']') > 1
56
+ info = key[1...key.index(']')]
57
+ fingerprint, *paths = info.split('/')
58
+ raise ArgumentError, "Fingerprint '#{fingerprint}' is not hex." unless fingerprint.valid_hex?
59
+ raise ArgumentError, "Fingerprint '#{fingerprint}' is not 4 bytes." unless fingerprint.size == 8
60
+ key = key[(key.index(']') + 1)..-1]
61
+ else
62
+ raise ArgumentError, 'Invalid key origin.' if key.include?(']')
63
+ end
64
+
65
+ # check BIP32 derivation path
66
+ key, *paths = key.split('/')
67
+
68
+ raise ArgumentError, "No key provided." unless key
69
+
70
+ if key.start_with?('xprv')
71
+ key = Bitcoin::ExtKey.from_base58(key)
72
+ key = derive_path(key, paths) if paths
73
+ elsif key.start_with?('xpub')
74
+ key = Bitcoin::ExtPubkey.from_base58(key)
75
+ key = derive_path(key, paths) if paths
76
+ else
77
+ begin
78
+ key = Bitcoin::Key.from_wif(key)
79
+ rescue ArgumentError
80
+ key = if key.length == 64
81
+ Bitcoin::Key.from_xonly_pubkey(key)
82
+ else
83
+ key_type = compressed_key?(key) ? Bitcoin::Key::TYPES[:compressed] : Bitcoin::Key::TYPES[:uncompressed]
84
+ Bitcoin::Key.new(pubkey: key, key_type: key_type)
85
+ end
86
+ end
87
+ end
88
+ key = key.is_a?(Bitcoin::Key) ? key : key.key
89
+ raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless key.fully_valid_pubkey?
90
+ key
91
+ end
92
+
93
+ # Derive key using +paths+.
94
+ # @param [Bitcoin::ExtKey or Bitcoin::ExtPubkey] key
95
+ # @param [String] paths derivation path.
96
+ # @return [Bitcoin::Key]
97
+ def derive_path(key, paths)
98
+ is_private = key.is_a?(Bitcoin::ExtKey)
99
+ paths.each do |path|
100
+ raise ArgumentError, 'xpub can not derive hardened key.' if !is_private && path.end_with?("'")
101
+ if is_private
102
+ hardened = path.end_with?("'")
103
+ path = hardened ? path[0..-2] : path
104
+ raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
105
+ raise ArgumentError, 'Key path value is out of range.' if !hardened && path.to_i >= Bitcoin::HARDENED_THRESHOLD
106
+ key = key.derive(path.to_i, hardened)
107
+ else
108
+ raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
109
+ raise ArgumentError, 'Key path value is out of range.' if path.to_i >= Bitcoin::HARDENED_THRESHOLD
110
+ key = key.derive(path.to_i)
111
+ end
112
+ end
113
+ key
114
+ end
115
+
116
+ def ==(other)
117
+ return false unless other.is_a?(Expression)
118
+ type == other.type && to_script == other.to_script
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,23 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ class KeyExpression < Expression
4
+ attr_reader :key
5
+
6
+ # Constructor
7
+ # @raise [ArgumentError] If +key+ is invalid.
8
+ def initialize(key)
9
+ raise ArgumentError, "Key must be string." unless key.is_a? String
10
+ extract_pubkey(key)
11
+ @key = key
12
+ end
13
+
14
+ def args
15
+ key
16
+ end
17
+
18
+ def top_level?
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # multi() expression
4
+ class Multi < Expression
5
+
6
+ attr_reader :threshold
7
+ attr_reader :keys
8
+
9
+ def initialize(threshold, keys)
10
+ validate!(threshold, keys)
11
+ @threshold = threshold
12
+ @keys = keys
13
+ end
14
+
15
+ def type
16
+ :multi
17
+ end
18
+
19
+ def to_script
20
+ Script.to_multisig_script(threshold, keys.map{|key| extract_pubkey(key).pubkey }, sort: false)
21
+ end
22
+
23
+ def to_hex
24
+ result = to_script
25
+ pubkey_count = result.get_pubkeys.length
26
+ raise RuntimeError, "Cannot have #{pubkey_count} pubkeys in bare multisig; only at most 3 pubkeys." if pubkey_count > 3
27
+ result.to_hex
28
+ end
29
+
30
+ def args
31
+ "#{threshold},#{keys.join(',')}"
32
+ end
33
+
34
+ def top_level?
35
+ false
36
+ end
37
+
38
+ private
39
+
40
+ def validate!(threshold, keys)
41
+ raise ArgumentError, "Multisig threshold '#{threshold}' is not valid." unless threshold.is_a?(Integer)
42
+ raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
43
+ raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
44
+ raise ArgumentError, "Multisig must have between 1 and #{Bitcoin::MAX_PUBKEYS_PER_MULTISIG} keys, inclusive." if keys.size > Bitcoin::MAX_PUBKEYS_PER_MULTISIG
45
+ keys.each{|key| extract_pubkey(key) }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # multi_a() expression
4
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki
5
+ class MultiA < Multi
6
+ include Bitcoin::Opcodes
7
+
8
+ def type
9
+ :multi_a
10
+ end
11
+
12
+ def to_hex
13
+ raise RuntimeError, "Can only have multi_a/sortedmulti_a inside tr()."
14
+ end
15
+
16
+ def to_script
17
+ multisig_script(keys.map{|k| extract_pubkey(k).xonly_pubkey})
18
+ end
19
+
20
+ private
21
+
22
+ def multisig_script(keys)
23
+ script = Bitcoin::Script.new
24
+ keys.each.with_index do |k, i|
25
+ script << k
26
+ script << (i == 0 ? OP_CHECKSIG : OP_CHECKSIGADD)
27
+ end
28
+ script << threshold << OP_NUMEQUAL
29
+ end
30
+
31
+ def validate!(threshold, keys)
32
+ raise ArgumentError, "Multisig threshold '#{threshold}' is not valid." unless threshold.is_a?(Integer)
33
+ raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
34
+ raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
35
+ raise ArgumentError, "Multisig must have between 1 and 999 keys, inclusive." if keys.size > 999
36
+ keys.each do |key|
37
+ k = extract_pubkey(key)
38
+ raise ArgumentError, "Uncompressed key are not allowed." unless k.compressed?
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # pk() expression
4
+ class Pk < KeyExpression
5
+ include Bitcoin::Opcodes
6
+
7
+ attr_accessor :xonly
8
+
9
+ def initialize(key)
10
+ super(key)
11
+ @xonly = false
12
+ end
13
+
14
+ def type
15
+ :pk
16
+ end
17
+
18
+ # Convert to bitcoin script.
19
+ # @return [Bitcoin::Script]
20
+ def to_script
21
+ k = extract_pubkey(key)
22
+ target_key = xonly ? k.xonly_pubkey : k.pubkey
23
+ Bitcoin::Script.new << target_key << OP_CHECKSIG
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # pkh() expression
4
+ class Pkh < KeyExpression
5
+
6
+ def type
7
+ :pkh
8
+ end
9
+
10
+ def to_script
11
+ Script.to_p2pkh(extract_pubkey(key).hash160)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ class Raw < Expression
4
+
5
+ attr_reader :hex
6
+
7
+ # Constructor
8
+ # @param [String] hex
9
+ def initialize(hex)
10
+ raise ArgumentError, "Raw script must be string." unless hex.is_a?(String)
11
+ raise ArgumentError, "Raw script is not hex." unless hex.valid_hex?
12
+ @hex = hex
13
+ end
14
+
15
+ def type
16
+ :raw
17
+ end
18
+
19
+ def to_script
20
+ Bitcoin::Script.parse_from_payload(hex.htb)
21
+ end
22
+
23
+ def args
24
+ hex
25
+ end
26
+
27
+ def top_level?
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ class ScriptExpression < Expression
4
+
5
+ attr_reader :script
6
+
7
+ def initialize(script)
8
+ validate!(script)
9
+ @script = script
10
+ end
11
+
12
+ def args
13
+ script.to_s
14
+ end
15
+
16
+ private
17
+
18
+ def validate!(script)
19
+ raise ArgumentError, "Can only have #{script.type.to_s}() at top level." if script.is_a?(Expression) && script.top_level?
20
+ raise ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().' if script.is_a?(MultiA) || script.is_a?(SortedMultiA)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # sh expression
4
+ class Sh < ScriptExpression
5
+
6
+ def type
7
+ :sh
8
+ end
9
+
10
+ def to_script
11
+ script.to_script.to_p2sh
12
+ end
13
+
14
+ def top_level?
15
+ true
16
+ end
17
+
18
+ private
19
+
20
+ def validate!(script)
21
+ super(script)
22
+ raise ArgumentError, 'A function is needed within P2SH.' unless script.is_a?(Expression)
23
+ script_size = script.to_script.size
24
+ if script_size > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
25
+ raise ArgumentError,
26
+ "P2SH script is too large, #{script_size} bytes is larger than #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE} bytes."
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # sortedmulti() expression
4
+ class SortedMulti < Multi
5
+
6
+ def type
7
+ :sortedmulti
8
+ end
9
+
10
+ def to_script
11
+ Script.to_multisig_script(threshold, keys.map{|key| extract_pubkey(key).pubkey }, sort: true)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # sortedmulti_a expression
4
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki
5
+ class SortedMultiA < MultiA
6
+ def type
7
+ :sortedmulti_a
8
+ end
9
+
10
+ def to_script
11
+ multisig_script( keys.map{|k| extract_pubkey(k).xonly_pubkey}.sort)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,91 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # tr() expression.
4
+ # @see https://github.com/bitcoin/bips/blob/master/bip-0386.mediawiki
5
+ class Tr < Expression
6
+
7
+ attr_reader :key
8
+ attr_reader :tree
9
+
10
+ # Constructor.
11
+ def initialize(key, tree = nil)
12
+ raise ArgumentError, "Key must be string." unless key.is_a?(String)
13
+ k = extract_pubkey(key)
14
+ raise ArgumentError, "Uncompressed key are not allowed." unless k.compressed?
15
+ validate_tree!(tree)
16
+ @key = key
17
+ @tree = tree
18
+ end
19
+
20
+ def type
21
+ :tr
22
+ end
23
+
24
+ def top_level?
25
+ true
26
+ end
27
+
28
+ def args
29
+ if tree.nil?
30
+ key
31
+ else
32
+ tree.is_a?(Array) ? "#{key},#{tree_string(tree)}" : "#{key},#{tree}"
33
+ end
34
+ end
35
+
36
+ def to_script
37
+ builder = build_tree_scripts
38
+ builder.build
39
+ end
40
+
41
+ private
42
+
43
+ def build_tree_scripts
44
+ internal_key = extract_pubkey(key)
45
+ return Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey) if tree.nil?
46
+ if tree.is_a?(Expression)
47
+ tree.xonly = true if tree.respond_to?(:xonly)
48
+ Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey, [Bitcoin::Taproot::LeafNode.new(tree.to_script)])
49
+ elsif tree.is_a?(Array)
50
+ Bitcoin::Taproot::CustomDepthBuilder.new(internal_key.xonly_pubkey, parse_tree_items(tree))
51
+ end
52
+ end
53
+
54
+ def parse_tree_items(arry)
55
+ items = []
56
+ arry.each do |item|
57
+ if item.is_a?(Array)
58
+ items << parse_tree_items(item)
59
+ elsif item.is_a?(Expression)
60
+ item.xonly = true
61
+ items << Bitcoin::Taproot::LeafNode.new(item.to_script)
62
+ else
63
+ raise RuntimeError, "Unsupported item #{item}"
64
+ end
65
+ end
66
+ items
67
+ end
68
+
69
+ def validate_tree!(tree)
70
+ return if tree.nil? || tree.is_a?(Expression)
71
+ if tree.is_a?(Array)
72
+ tree.each do |item|
73
+ validate_tree!(item)
74
+ end
75
+ else
76
+ raise ArgumentError, "tree must be a expression or array of expression."
77
+ end
78
+ end
79
+
80
+ def tree_string(tree)
81
+ buffer = '{'
82
+ left, right = tree
83
+ buffer << (left.is_a?(Array) ? tree_string(left) : left.to_s)
84
+ buffer << ","
85
+ buffer << (right.is_a?(Array) ? tree_string(right) : right.to_s)
86
+ buffer << '}'
87
+ buffer
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # wpkh() expression
4
+ class Wpkh < KeyExpression
5
+ def initialize(key)
6
+ super(key)
7
+ raise ArgumentError, "Uncompressed key are not allowed." unless extract_pubkey(key).compressed?
8
+ end
9
+
10
+ def type
11
+ :wpkh
12
+ end
13
+
14
+ def to_script
15
+ Script.to_p2wpkh(extract_pubkey(key).hash160)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ module Bitcoin
2
+ module Descriptor
3
+ # wsh() expression
4
+ class Wsh < ScriptExpression
5
+
6
+ def type
7
+ :wsh
8
+ end
9
+
10
+ def to_script
11
+ Script.to_p2wsh(script.to_script)
12
+ end
13
+
14
+ def top_level?
15
+ false
16
+ end
17
+
18
+ def validate!(script)
19
+ super(script)
20
+ raise ArgumentError, 'A function is needed within P2WSH.' unless script.is_a?(Expression)
21
+ if script.is_a?(Wpkh) || script.is_a?(Wsh)
22
+ raise ArgumentError, "Can only have #{script.type}() at top level or inside sh()."
23
+ end
24
+ if script.to_script.get_pubkeys.any?{|p|!compressed_key?(p)}
25
+ raise ArgumentError, "Uncompressed key are not allowed."
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,145 +3,221 @@ module Bitcoin
3
3
  module Descriptor
4
4
 
5
5
  include Bitcoin::Opcodes
6
-
7
- # generate P2PK output for the given public key.
6
+ autoload :Expression, 'bitcoin/descriptor/expression'
7
+ autoload :KeyExpression, 'bitcoin/descriptor/key_expression'
8
+ autoload :ScriptExpression, 'bitcoin/descriptor/script_expression'
9
+ autoload :Pk, 'bitcoin/descriptor/pk'
10
+ autoload :Pkh, 'bitcoin/descriptor/pkh'
11
+ autoload :Wpkh, 'bitcoin/descriptor/wpkh'
12
+ autoload :Sh, 'bitcoin/descriptor/sh'
13
+ autoload :Wsh, 'bitcoin/descriptor/wsh'
14
+ autoload :Combo, 'bitcoin/descriptor/combo'
15
+ autoload :Multi, 'bitcoin/descriptor/multi'
16
+ autoload :SortedMulti, 'bitcoin/descriptor/sorted_multi'
17
+ autoload :Raw, 'bitcoin/descriptor/raw'
18
+ autoload :Addr, 'bitcoin/descriptor/addr'
19
+ autoload :Tr, 'bitcoin/descriptor/tr'
20
+ autoload :MultiA, 'bitcoin/descriptor/multi_a'
21
+ autoload :SortedMultiA, 'bitcoin/descriptor/sorted_multi_a'
22
+ autoload :Checksum, 'bitcoin/descriptor/checksum'
23
+
24
+ module_function
25
+
26
+ # Generate P2PK output for the given public key.
8
27
  # @param [String] key private key or public key with hex format
9
- # @return [Bitcoin::Script] P2PK script.
28
+ # @return [Bitcoin::Descriptor::Pk]
10
29
  def pk(key)
11
- Bitcoin::Script.new << extract_pubkey(key) << OP_CHECKSIG
30
+ Pk.new(key)
12
31
  end
13
32
 
14
- # generate P2PKH output for the given public key.
33
+ # Generate P2PKH output for the given public key.
15
34
  # @param [String] key private key or public key with hex format.
16
- # @return [Bitcoin::Script] P2PKH script.
35
+ # @return [Bitcoin::Descriptor::Pkh]
17
36
  def pkh(key)
18
- Bitcoin::Script.to_p2pkh(Bitcoin.hash160(extract_pubkey(key)))
37
+ Pkh.new(key)
19
38
  end
20
39
 
21
- # generate P2PKH output for the given public key.
40
+ # Generate P2PKH output for the given public key.
22
41
  # @param [String] key private key or public key with hex format.
23
- # @return [Bitcoin::Script] P2WPKH script.
42
+ # @return [Bitcoin::Descriptor::Wpkh]
24
43
  def wpkh(key)
25
- pubkey = extract_pubkey(key)
26
- raise ArgumentError, "Uncompressed key are not allowed." unless compressed_key?(pubkey)
27
- Bitcoin::Script.to_p2wpkh(Bitcoin.hash160(pubkey))
44
+ Wpkh.new(key)
28
45
  end
29
46
 
30
- # generate P2SH embed the argument.
31
- # @param [String or Script] script script to be embed.
32
- # @return [Bitcoin::Script] P2SH script.
33
- def sh(script)
34
- script = script.to_hex if script.is_a?(Bitcoin::Script)
35
- raise ArgumentError, "P2SH script is too large, 547 bytes is larger than #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE} bytes." if script.htb.bytesize > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
36
- Bitcoin::Script.to_p2sh(Bitcoin.hash160(script))
47
+ # Generate P2SH embed the argument.
48
+ # @param [Bitcoin::Descriptor::Base] exp script expression to be embed.
49
+ # @return [Bitcoin::Descriptor::Sh]
50
+ def sh(exp)
51
+ Sh.new(exp)
37
52
  end
38
53
 
39
- # generate P2WSH embed the argument.
40
- # @param [String or Script] script script to be embed.
41
- # @return [Bitcoin::Script] P2WSH script.
42
- def wsh(script)
43
- script = Bitcoin::Script(script.htb) if script.is_a?(String)
44
- raise ArgumentError, "P2SH script is too large, 547 bytes is larger than #{Bitcoin::MAX_SCRIPT_ELEMENT_SIZE} bytes." if script.to_payload.bytesize > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
45
- raise ArgumentError, "Uncompressed key are not allowed." if script.get_pubkeys.any?{|p|!compressed_key?(p)}
46
- Bitcoin::Script.to_p2wsh(script)
54
+ # Generate P2WSH embed the argument.
55
+ # @param [Bitcoin::Descriptor::Expression] exp script expression to be embed.
56
+ # @return [Bitcoin::Descriptor::Wsh]
57
+ def wsh(exp)
58
+ Wsh.new(exp)
47
59
  end
48
60
 
49
- # an alias for the collection of `pk(KEY)` and `pkh(KEY)`.
61
+ # An alias for the collection of `pk(KEY)` and `pkh(KEY)`.
50
62
  # If the key is compressed, it also includes `wpkh(KEY)` and `sh(wpkh(KEY))`.
51
63
  # @param [String] key private key or public key with hex format.
52
- # @return [Array[Bitcoin::Script]]
64
+ # @return [Bitcoin::Descriptor::Combo]
53
65
  def combo(key)
54
- result = [pk(key), pkh(key)]
55
- pubkey = extract_pubkey(key)
56
- if compressed_key?(pubkey)
57
- result << wpkh(key)
58
- result << sh(result.last)
59
- end
60
- result
66
+ Combo.new(key)
61
67
  end
62
68
 
63
- # generate multisig output for given keys.
69
+ # Generate multisig output for given keys.
64
70
  # @param [Integer] threshold the threshold of multisig.
65
71
  # @param [Array[String]] keys an array of keys.
66
- # @return [Bitcoin::Script] multisig script.
67
- def multi(threshold, *keys, sort: false)
68
- raise ArgumentError, 'Multisig threshold is not valid.' unless threshold.is_a?(Integer)
69
- raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
70
- raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
71
- raise ArgumentError, 'Multisig must have between 1 and 16 keys, inclusive.' if keys.size > 16
72
- pubkeys = keys.map{|key| extract_pubkey(key) }
73
- Bitcoin::Script.to_multisig_script(threshold, pubkeys, sort: sort)
72
+ # @return [Bitcoin::Descriptor::Multi] multisig script.
73
+ def multi(threshold, *keys)
74
+ Multi.new(threshold, keys)
74
75
  end
75
76
 
76
- # generate sorted multisig output for given keys.
77
+ # Generate sorted multisig output for given keys.
77
78
  # @param [Integer] threshold the threshold of multisig.
78
79
  # @param [Array[String]] keys an array of keys.
79
- # @return [Bitcoin::Script] multisig script.
80
+ # @return [Bitcoin::Descriptor::SortedMulti]
80
81
  def sortedmulti(threshold, *keys)
81
- multi(threshold, *keys, sort: true)
82
- end
83
-
84
- private
85
-
86
- # extract public key from KEY format.
87
- # @param [String] key KEY string.
88
- # @return [String] public key.
89
- def extract_pubkey(key)
90
- if key.start_with?('[') # BIP32 fingerprint
91
- raise ArgumentError, 'Invalid key origin.' if key.count('[') > 1 || key.count(']') > 1
92
- info = key[1...key.index(']')] # TODO
93
- fingerprint, *paths = info.split('/')
94
- raise ArgumentError, 'Fingerprint is not hex.' unless fingerprint.valid_hex?
95
- raise ArgumentError, 'Fingerprint is not 4 bytes.' unless fingerprint.size == 8
96
- key = key[(key.index(']') + 1)..-1]
97
- else
98
- raise ArgumentError, 'Invalid key origin.' if key.include?(']')
99
- end
82
+ SortedMulti.new(threshold, keys)
83
+ end
100
84
 
101
- # check BIP32 derivation path
102
- key, *paths = key.split('/')
85
+ # Generate raw output script about +hex+.
86
+ # @param [String] hex Hex string of bitcoin script.
87
+ # @return [Bitcoin::Descriptor::Raw]
88
+ def raw(hex)
89
+ Raw.new(hex)
90
+ end
103
91
 
104
- if key.start_with?('xprv')
105
- key = Bitcoin::ExtKey.from_base58(key)
106
- key = derive_path(key, paths, true) if paths
107
- elsif key.start_with?('xpub')
108
- key = Bitcoin::ExtPubkey.from_base58(key)
109
- key = derive_path(key, paths, false) if paths
110
- else
111
- begin
112
- key = Bitcoin::Key.from_wif(key)
113
- rescue ArgumentError
114
- key_type = compressed_key?(key) ? Bitcoin::Key::TYPES[:compressed] : Bitcoin::Key::TYPES[:uncompressed]
115
- key = Bitcoin::Key.new(pubkey: key, key_type: key_type)
92
+ # Generate raw output script about +hex+.
93
+ # @param [String] addr Bitcoin address.
94
+ # @return [Bitcoin::Descriptor::Addr]
95
+ def addr(addr)
96
+ Addr.new(addr)
97
+ end
98
+
99
+ # Generate taproot output script descriptor.
100
+ # @param [String] key
101
+ # @param [String] tree
102
+ # @return [Bitcoin::Descriptor::Tr]
103
+ def tr(key, tree = nil)
104
+ Tr.new(key, tree)
105
+ end
106
+
107
+ # Generate tapscript multisig output for given keys.
108
+ # @param [Integer] threshold the threshold of multisig.
109
+ # @param [Array[String]] keys an array of keys.
110
+ # @return [Bitcoin::Descriptor::MultiA] multisig script.
111
+ def multi_a(threshold, *keys)
112
+ MultiA.new(threshold, keys)
113
+ end
114
+
115
+ # Generate tapscript sorted multisig output for given keys.
116
+ # @param [Integer] threshold the threshold of multisig.
117
+ # @param [Array[String]] keys an array of keys.
118
+ # @return [Bitcoin::Descriptor::SortedMulti]
119
+ def sortedmulti_a(threshold, *keys)
120
+ SortedMultiA.new(threshold, keys)
121
+ end
122
+
123
+ # Parse descriptor string.
124
+ # @param [String] string Descriptor string.
125
+ # @return [Bitcoin::Descriptor::Expression]
126
+ def parse(string, top_level = true)
127
+ validate_checksum!(string)
128
+ content, _ = string.split('#')
129
+ exp, args_str = content.match(/(\w+)\((.+)\)/).captures
130
+ case exp
131
+ when 'pk'
132
+ pk(args_str)
133
+ when 'pkh'
134
+ pkh(args_str)
135
+ when 'wpkh'
136
+ wpkh(args_str)
137
+ when 'sh'
138
+ sh(parse(args_str, false))
139
+ when 'wsh'
140
+ wsh(parse(args_str, false))
141
+ when 'combo'
142
+ combo(args_str)
143
+ when 'multi', 'sortedmulti', 'multi_a', 'sortedmulti_a'
144
+ args = args_str.split(',')
145
+ threshold = args[0].to_i
146
+ keys = args[1..-1]
147
+ case exp
148
+ when 'multi'
149
+ multi(threshold, *keys)
150
+ when 'sortedmulti'
151
+ sortedmulti(threshold, *keys)
152
+ when 'multi_a'
153
+ raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
154
+ multi_a(threshold, *keys)
155
+ when 'sortedmulti_a'
156
+ raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
157
+ sortedmulti_a(threshold, *keys)
116
158
  end
159
+ when 'raw'
160
+ raw(args_str)
161
+ when 'addr'
162
+ addr(args_str)
163
+ when 'tr'
164
+ key, rest = args_str.split(',', 2)
165
+ if rest.nil?
166
+ tr(key)
167
+ elsif rest.start_with?('{')
168
+ tr(key, parse_nested_string(rest))
169
+ else
170
+ tr(key, parse(rest, false))
171
+ end
172
+ else
173
+ raise ArgumentError, "Parse failed: #{string}"
117
174
  end
118
- key = key.is_a?(Bitcoin::Key) ? key : key.key
119
- raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless key.fully_valid_pubkey?
120
- key.pubkey
121
175
  end
122
176
 
123
- def compressed_key?(key)
124
- %w(02 03).include?(key[0..1]) && [key].pack("H*").bytesize == 33
177
+ # Validate descriptor checksum.
178
+ # @raise [ArgumentError] If +descriptor+ has invalid checksum.
179
+ def validate_checksum!(descriptor)
180
+ return unless descriptor.include?("#")
181
+ content, *checksums = descriptor.split("#")
182
+ raise ArgumentError, "Multiple '#' symbols." if checksums.length > 1
183
+ checksum = checksums.first
184
+ len = checksum.nil? ? 0 : checksum.length
185
+ raise ArgumentError, "Expected 8 character checksum, not #{len} characters." unless len == 8
186
+ _, calc_checksum = Checksum.descsum_create(content).split('#')
187
+ unless calc_checksum == checksum
188
+ raise ArgumentError, "Provided checksum '#{checksum}' does not match computed checksum '#{calc_checksum}'."
189
+ end
125
190
  end
126
191
 
127
- def derive_path(key, paths, is_private)
128
- paths.each do |path|
129
- raise ArgumentError, 'xpub can not derive hardened key.' if !is_private && path.end_with?("'")
130
- if is_private
131
- hardened = path.end_with?("'")
132
- path = hardened ? path[0..-2] : path
133
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
134
- raise ArgumentError, 'Key path value is out of range.' if !hardened && path.to_i >= Bitcoin::HARDENED_THRESHOLD
135
- key = key.derive(path.to_i, hardened)
192
+ def parse_nested_string(string)
193
+ return nil if string.nil?
194
+ stack = []
195
+ current = []
196
+ buffer = ""
197
+ string.each_char do |c|
198
+ case c
199
+ when '{'
200
+ stack << current
201
+ current = []
202
+ when '}'
203
+ unless buffer.empty?
204
+ current << parse(buffer, false)
205
+ buffer = ""
206
+ end
207
+ nested = current
208
+ current = stack.pop
209
+ current << nested
210
+ when ','
211
+ unless buffer.empty?
212
+ current << parse(buffer, false)
213
+ buffer = ""
214
+ end
136
215
  else
137
- raise ArgumentError, 'Key path value is not a valid value.' unless path =~ /^[0-9]+$/
138
- raise ArgumentError, 'Key path value is out of range.' if path.to_i >= Bitcoin::HARDENED_THRESHOLD
139
- key = key.derive(path.to_i)
216
+ buffer << c
140
217
  end
141
218
  end
142
- key
219
+ current << parse(buffer, false) unless buffer.empty?
220
+ current.first
143
221
  end
144
-
145
222
  end
146
-
147
223
  end
data/lib/bitcoin/key.rb CHANGED
@@ -83,7 +83,7 @@ module Bitcoin
83
83
  # @return [Bitcoin::Key] key object has public key.
84
84
  def self.from_xonly_pubkey(xonly_pubkey)
85
85
  raise ArgumentError, "xonly_pubkey must be #{X_ONLY_PUBKEY_SIZE} bytes" unless xonly_pubkey.htb.bytesize == X_ONLY_PUBKEY_SIZE
86
- Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:compressed])
86
+ Bitcoin::Key.new(pubkey: "02#{xonly_pubkey}", key_type: TYPES[:p2tr])
87
87
  end
88
88
 
89
89
  # Generate from public key point.
@@ -55,7 +55,9 @@ module Bitcoin
55
55
 
56
56
  # generate p2sh script with this as a redeem script
57
57
  # @return [Script] P2SH script
58
+ # @raise [RuntimeError] If the script size exceeds 520 bytes
58
59
  def to_p2sh
60
+ raise RuntimeError, "P2SH redeem script must be 520 bytes or less." if size > Bitcoin::MAX_SCRIPT_ELEMENT_SIZE
59
61
  Script.to_p2sh(to_hash160)
60
62
  end
61
63
 
@@ -74,9 +76,11 @@ module Bitcoin
74
76
  end
75
77
 
76
78
  # generate p2wsh script for +redeem_script+
77
- # @param [Script] redeem_script target redeem script
78
- # @param [Script] p2wsh script
79
+ # @param [Bitcoin::Script] redeem_script target redeem script
80
+ # @return [Bitcoin::Script] p2wsh script
81
+ # @raise [ArgumentError] If the script size exceeds 10,000 bytes
79
82
  def self.to_p2wsh(redeem_script)
83
+ raise ArgumentError, 'P2WSH witness script must be 10,000 bytes or less.' if redeem_script.size > Bitcoin::MAX_SCRIPT_SIZE
80
84
  new << WITNESS_VERSION_V0 << redeem_script.to_sha256
81
85
  end
82
86
 
@@ -146,7 +150,7 @@ module Bitcoin
146
150
  end
147
151
  if buf.eof?
148
152
  s.chunks << [len].pack('C')
149
- else buf.eof?
153
+ else
150
154
  chunk = (packed_size ? (opcode + packed_size) : (opcode)) + buf.read(len)
151
155
  s.chunks << chunk
152
156
  end
@@ -555,6 +559,7 @@ module Bitcoin
555
559
  return 'multisig' if multisig?
556
560
  return 'witness_v0_keyhash' if p2wpkh?
557
561
  return 'witness_v0_scripthash' if p2wsh?
562
+ return 'witness_v1_taproot' if p2tr?
558
563
  'nonstandard'
559
564
  end
560
565
 
@@ -7,7 +7,7 @@ module Bitcoin
7
7
  # binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
8
8
  # tag: v0.4.0
9
9
  # this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
10
- # for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so'
10
+ # for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' or '/usr/lib64/libsecp256k1.so'
11
11
  # for mac,
12
12
  module Native
13
13
  include ::FFI::Library
@@ -0,0 +1,64 @@
1
+ module Bitcoin
2
+ module Taproot
3
+ # A class that takes the script tree configuration as a nested array and constructs the Taproot output.
4
+ # TODO WIP
5
+ class CustomDepthBuilder < SimpleBuilder
6
+
7
+ attr_reader :tree
8
+
9
+ # Constructor
10
+ # @param [String] internal_key Internal public key with hex format.
11
+ # @param [Array] tree Script tree configuration as a nested array.
12
+ # @return [Bitcoin::Taproot::CustomDepthBuilder]
13
+ def initialize(internal_key, tree)
14
+ super(internal_key, [])
15
+ raise ArgumentError, "tree must be an array." unless tree.is_a?(Array)
16
+ raise ArgumentError, "tree must be binary tree." unless tree.length == 2
17
+ tree.each do |item|
18
+ unless item.is_a?(Array) || item.is_a?(Bitcoin::Taproot::LeafNode)
19
+ raise ArgumentError, "tree must consist of either an array or LeafNode."
20
+ end
21
+ raise ArgumentError, "tree must be binary tree." if item.is_a?(Array) && item.length != 2
22
+ end
23
+ @tree = tree
24
+ end
25
+
26
+ def add_leaf(leaf)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def add_branch(leaf1, leaf2 = nil)
31
+ raise NotImplementedError
32
+ end
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
+ private
43
+
44
+ def merkle_root
45
+ build_tree(tree).bth
46
+ end
47
+
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])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -16,6 +16,7 @@ module Bitcoin
16
16
  # @raise [Bitcoin::Taproot::Builder] +internal_pubkey+ dose not xonly public key or leaf in +leaves+ does not instance of Bitcoin::Taproot::LeafNode.
17
17
  # @return [Bitcoin::Taproot::SimpleBuilder]
18
18
  def initialize(internal_key, leaves = [])
19
+ raise ArgumentError, "Internal public key must be string." unless internal_key.is_a?(String)
19
20
  raise Error, "Internal public key must be #{X_ONLY_PUBKEY_SIZE} bytes" unless internal_key.htb.bytesize == X_ONLY_PUBKEY_SIZE
20
21
  raise Error, 'leaf must be Bitcoin::Taproot::LeafNode object' if leaves.find{ |leaf| !leaf.is_a?(Bitcoin::Taproot::LeafNode)}
21
22
 
@@ -113,12 +114,6 @@ module Bitcoin
113
114
 
114
115
  private
115
116
 
116
- # Compute tweak from script tree.
117
- # @return [String] tweak with binary format.
118
- def tweak
119
- Taproot.tweak(Bitcoin::Key.from_xonly_pubkey(internal_key), merkle_root)
120
- end
121
-
122
117
  # Calculate merkle root from branches.
123
118
  # @return [String] merkle root with hex format.
124
119
  def merkle_root
@@ -6,6 +6,7 @@ module Bitcoin
6
6
  autoload :LeafNode, 'bitcoin/taproot/leaf_node'
7
7
  autoload :ControlBlock, 'bitcoin/taproot/control_block'
8
8
  autoload :SimpleBuilder, 'bitcoin/taproot/simple_builder'
9
+ autoload :CustomDepthBuilder, 'bitcoin/taproot/custom_depth_builder'
9
10
 
10
11
  module_function
11
12
 
data/lib/bitcoin/tx.rb CHANGED
@@ -111,7 +111,7 @@ module Bitcoin
111
111
  end
112
112
 
113
113
  def witness?
114
- !inputs.find { |i| !i.script_witness.empty? }.nil?
114
+ inputs.any?(&:has_witness?)
115
115
  end
116
116
 
117
117
  def ==(other)
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitcoinrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-14 00:00:00.000000000 Z
11
+ date: 2024-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ecdsa_ext
@@ -350,6 +350,23 @@ files:
350
350
  - lib/bitcoin/chainparams/testnet.yml
351
351
  - lib/bitcoin/constants.rb
352
352
  - lib/bitcoin/descriptor.rb
353
+ - lib/bitcoin/descriptor/addr.rb
354
+ - lib/bitcoin/descriptor/checksum.rb
355
+ - lib/bitcoin/descriptor/combo.rb
356
+ - lib/bitcoin/descriptor/expression.rb
357
+ - lib/bitcoin/descriptor/key_expression.rb
358
+ - lib/bitcoin/descriptor/multi.rb
359
+ - lib/bitcoin/descriptor/multi_a.rb
360
+ - lib/bitcoin/descriptor/pk.rb
361
+ - lib/bitcoin/descriptor/pkh.rb
362
+ - lib/bitcoin/descriptor/raw.rb
363
+ - lib/bitcoin/descriptor/script_expression.rb
364
+ - lib/bitcoin/descriptor/sh.rb
365
+ - lib/bitcoin/descriptor/sorted_multi.rb
366
+ - lib/bitcoin/descriptor/sorted_multi_a.rb
367
+ - lib/bitcoin/descriptor/tr.rb
368
+ - lib/bitcoin/descriptor/wpkh.rb
369
+ - lib/bitcoin/descriptor/wsh.rb
353
370
  - lib/bitcoin/errors.rb
354
371
  - lib/bitcoin/ext.rb
355
372
  - lib/bitcoin/ext/array_ext.rb
@@ -464,6 +481,7 @@ files:
464
481
  - lib/bitcoin/store/utxo_db.rb
465
482
  - lib/bitcoin/taproot.rb
466
483
  - lib/bitcoin/taproot/control_block.rb
484
+ - lib/bitcoin/taproot/custom_depth_builder.rb
467
485
  - lib/bitcoin/taproot/leaf_node.rb
468
486
  - lib/bitcoin/taproot/simple_builder.rb
469
487
  - lib/bitcoin/tx.rb