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 +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/README.md +7 -5
- data/lib/bitcoin/descriptor/addr.rb +31 -0
- data/lib/bitcoin/descriptor/checksum.rb +74 -0
- data/lib/bitcoin/descriptor/combo.rb +30 -0
- data/lib/bitcoin/descriptor/expression.rb +122 -0
- data/lib/bitcoin/descriptor/key_expression.rb +23 -0
- data/lib/bitcoin/descriptor/multi.rb +49 -0
- data/lib/bitcoin/descriptor/multi_a.rb +43 -0
- data/lib/bitcoin/descriptor/pk.rb +27 -0
- data/lib/bitcoin/descriptor/pkh.rb +15 -0
- data/lib/bitcoin/descriptor/raw.rb +32 -0
- data/lib/bitcoin/descriptor/script_expression.rb +24 -0
- data/lib/bitcoin/descriptor/sh.rb +31 -0
- data/lib/bitcoin/descriptor/sorted_multi.rb +15 -0
- data/lib/bitcoin/descriptor/sorted_multi_a.rb +15 -0
- data/lib/bitcoin/descriptor/tr.rb +91 -0
- data/lib/bitcoin/descriptor/wpkh.rb +19 -0
- data/lib/bitcoin/descriptor/wsh.rb +30 -0
- data/lib/bitcoin/descriptor.rb +176 -100
- data/lib/bitcoin/key.rb +1 -1
- data/lib/bitcoin/script/script.rb +8 -3
- data/lib/bitcoin/secp256k1/native.rb +1 -1
- data/lib/bitcoin/taproot/custom_depth_builder.rb +64 -0
- data/lib/bitcoin/taproot/simple_builder.rb +1 -6
- data/lib/bitcoin/taproot.rb +1 -0
- data/lib/bitcoin/tx.rb +1 -1
- data/lib/bitcoin/version.rb +1 -1
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2697b9fbcca175453c80fc67d70466845c7141e6b9e9f193b5825d34bd7ffa3
|
4
|
+
data.tar.gz: 61a5c26a7cfaf7abcc7c692386ff08091ea0249c9e8ea9ccb4f3231049319f47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40574931606fc1ff074f638bda7042f86c0225a5661f37622971e0b9502e039c5010d31663c34846c80f0bff2d25a02122eb4773e30a6fadad72bb170260b114
|
7
|
+
data.tar.gz: aaefa2672459d1d133191aecf8cbe0e8391210647e109c6e5205e349e264ad2303b25c36d4812daf5f5f0e88fb15cc4b0bfc54653ba0a2b4a8bac9fd5fd9b5d6
|
data/.github/workflows/ruby.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# Bitcoinrb [](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml) [](https://badge.fury.io/rb/bitcoinrb) [](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
|
-
* [
|
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,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
|
data/lib/bitcoin/descriptor.rb
CHANGED
@@ -3,145 +3,221 @@ module Bitcoin
|
|
3
3
|
module Descriptor
|
4
4
|
|
5
5
|
include Bitcoin::Opcodes
|
6
|
-
|
7
|
-
|
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::
|
28
|
+
# @return [Bitcoin::Descriptor::Pk]
|
10
29
|
def pk(key)
|
11
|
-
|
30
|
+
Pk.new(key)
|
12
31
|
end
|
13
32
|
|
14
|
-
#
|
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::
|
35
|
+
# @return [Bitcoin::Descriptor::Pkh]
|
17
36
|
def pkh(key)
|
18
|
-
|
37
|
+
Pkh.new(key)
|
19
38
|
end
|
20
39
|
|
21
|
-
#
|
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::
|
42
|
+
# @return [Bitcoin::Descriptor::Wpkh]
|
24
43
|
def wpkh(key)
|
25
|
-
|
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
|
-
#
|
31
|
-
# @param [
|
32
|
-
# @return [Bitcoin::
|
33
|
-
def sh(
|
34
|
-
|
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
|
-
#
|
40
|
-
# @param [
|
41
|
-
# @return [Bitcoin::
|
42
|
-
def wsh(
|
43
|
-
|
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
|
-
#
|
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 [
|
64
|
+
# @return [Bitcoin::Descriptor::Combo]
|
53
65
|
def combo(key)
|
54
|
-
|
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
|
-
#
|
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::
|
67
|
-
def multi(threshold, *keys
|
68
|
-
|
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
|
-
#
|
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::
|
80
|
+
# @return [Bitcoin::Descriptor::SortedMulti]
|
80
81
|
def sortedmulti(threshold, *keys)
|
81
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
124
|
-
|
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
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
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
|
-
|
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[:
|
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
|
-
# @
|
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
|
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
|
data/lib/bitcoin/taproot.rb
CHANGED
@@ -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
data/lib/bitcoin/version.rb
CHANGED
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.
|
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-
|
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
|