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 +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 [![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
|
-
* [
|
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
|