bitcoinrb 1.5.0 → 1.6.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: 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