Skip to content

Commit

Permalink
Implements multi_a and sortedmulti_a
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Jul 9, 2024
1 parent ad34f28 commit 099fc15
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 16 deletions.
53 changes: 44 additions & 9 deletions lib/bitcoin/descriptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module Descriptor
autoload :Raw, 'bitcoin/descriptor/raw'
autoload :Addr, 'bitcoin/descriptor/addr'
autoload :Tr, 'bitcoin/descriptor/tr'
autoload :MultiA, 'bitcoin/descriptor/multi_a'
autoload :SortedMultiA, 'bitcoin/descriptor/sorted_multi_a'
autoload :Checksum, 'bitcoin/descriptor/checksum'

module_function
Expand Down Expand Up @@ -102,10 +104,26 @@ def tr(key, tree = nil)
Tr.new(key, tree)
end

# Generate tapscript multisig output for given keys.
# @param [Integer] threshold the threshold of multisig.
# @param [Array[String]] keys an array of keys.
# @return [Bitcoin::Descriptor::MultiA] multisig script.
def multi_a(threshold, *keys)
MultiA.new(threshold, keys)
end

# Generate tapscript sorted multisig output for given keys.
# @param [Integer] threshold the threshold of multisig.
# @param [Array[String]] keys an array of keys.
# @return [Bitcoin::Descriptor::SortedMulti]
def sortedmulti_a(threshold, *keys)
SortedMultiA.new(threshold, keys)
end

# Parse descriptor string.
# @param [String] string Descriptor string.
# @return [Bitcoin::Descriptor::Expression]
def parse(string)
def parse(string, top_level = true)
validate_checksum!(string)
content, _ = string.split('#')
exp, args_str = content.match(/(\w+)\((.+)\)/).captures
Expand All @@ -117,23 +135,40 @@ def parse(string)
when 'wpkh'
wpkh(args_str)
when 'sh'
sh(parse(args_str))
sh(parse(args_str, false))
when 'wsh'
wsh(parse(args_str))
wsh(parse(args_str, false))
when 'combo'
combo(args_str)
when 'multi', 'sortedmulti'
when 'multi', 'sortedmulti', 'multi_a', 'sortedmulti_a'
args = args_str.split(',')
threshold = args[0].to_i
keys = args[1..-1]
exp == 'multi' ? multi(threshold, *keys) : sortedmulti(threshold, *keys)
case exp
when 'multi'
multi(threshold, *keys)
when 'sortedmulti'
sortedmulti(threshold, *keys)
when 'multi_a'
raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
multi_a(threshold, *keys)
when 'sortedmulti_a'
raise ArgumentError, "Can only have multi_a/sortedmulti_a inside tr()." if top_level
sortedmulti_a(threshold, *keys)
end
when 'raw'
raw(args_str)
when 'addr'
addr(args_str)
when 'tr'
key, rest = args_str.split(',', 2)
tr(key, parse_nested_string(rest))
if rest.nil?
tr(key)
elsif rest.start_with?('{')
tr(key, parse_nested_string(rest))
else
tr(key, parse(rest, false))
end
else
raise ArgumentError, "Parse failed: #{string}"
end
Expand Down Expand Up @@ -166,22 +201,22 @@ def parse_nested_string(string)
current = []
when '}'
unless buffer.empty?
current << parse(buffer)
current << parse(buffer, false)
buffer = ""
end
nested = current
current = stack.pop
current << nested
when ','
unless buffer.empty?
current << parse(buffer)
current << parse(buffer, false)
buffer = ""
end
else
buffer << c
end
end
current << parse(buffer) unless buffer.empty?
current << parse(buffer, false) unless buffer.empty?
current.first
end
end
Expand Down
8 changes: 3 additions & 5 deletions lib/bitcoin/descriptor/multi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ def to_script

def to_hex
result = to_script
if result.multisig?
pubkey_count = result.get_pubkeys.length
raise RuntimeError, "Cannot have #{pubkey_count} pubkeys in bare multisig; only at most 3 pubkeys." if pubkey_count > 3
end
super
pubkey_count = result.get_pubkeys.length
raise RuntimeError, "Cannot have #{pubkey_count} pubkeys in bare multisig; only at most 3 pubkeys." if pubkey_count > 3
result.to_hex
end

def args
Expand Down
43 changes: 43 additions & 0 deletions lib/bitcoin/descriptor/multi_a.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Bitcoin
module Descriptor
# multi_a() expression
# @see https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki
class MultiA < Multi
include Bitcoin::Opcodes

def type
:multi_a
end

def to_hex
raise RuntimeError, "Can only have multi_a/sortedmulti_a inside tr()."
end

def to_script
multisig_script(keys.map{|k| extract_pubkey(k).xonly_pubkey})
end

private

def multisig_script(keys)
script = Bitcoin::Script.new
keys.each.with_index do |k, i|
script << k
script << (i == 0 ? OP_CHECKSIG : OP_CHECKSIGADD)
end
script << threshold << OP_NUMEQUAL
end

def validate!(threshold, keys)
raise ArgumentError, "Multisig threshold '#{threshold}' is not valid." unless threshold.is_a?(Integer)
raise ArgumentError, 'Multisig threshold cannot be 0, must be at least 1.' unless threshold > 0
raise ArgumentError, 'Multisig threshold cannot be larger than the number of keys.' if threshold > keys.size
raise ArgumentError, "Multisig must have between 1 and 999 keys, inclusive." if keys.size > 999
keys.each do |key|
k = extract_pubkey(key)
raise ArgumentError, "Uncompressed key are not allowed." unless k.compressed?
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/bitcoin/descriptor/script_expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def args

def validate!(script)
raise ArgumentError, "Can only have #{script.type.to_s}() at top level." if script.is_a?(Expression) && script.top_level?
raise ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().' if script.is_a?(MultiA) || script.is_a?(SortedMultiA)
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/bitcoin/descriptor/sorted_multi_a.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Bitcoin
module Descriptor
# sortedmulti_a expression
# @see https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki
class SortedMultiA < MultiA
def type
:sortedmulti_a
end

def to_script
multisig_script( keys.map{|k| extract_pubkey(k).xonly_pubkey}.sort)
end
end
end
end
8 changes: 6 additions & 2 deletions lib/bitcoin/descriptor/tr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ def top_level?
end

def args
tree.nil? ? key : "#{key},#{tree_string(tree)}"
if tree.nil?
key
else
tree.is_a?(Array) ? "#{key},#{tree_string(tree)}" : "#{key},#{tree}"
end
end

def to_script
Expand All @@ -40,7 +44,7 @@ def build_tree_scripts
internal_key = extract_pubkey(key)
return Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey) if tree.nil?
if tree.is_a?(Expression)
tree.xonly = true
tree.xonly = true if tree.respond_to?(:xonly)
Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey, [Bitcoin::Taproot::LeafNode.new(tree.to_script)])
elsif tree.is_a?(Array)
Bitcoin::Taproot::CustomDepthBuilder.new(internal_key.xonly_pubkey, parse_tree_items(tree))
Expand Down
53 changes: 53 additions & 0 deletions spec/bitcoin/descriptor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,57 @@
to raise_error(ArgumentError, "Can only have tr() at top level.")
end
end

describe "BIP387" do
it do
expect(tr('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1',
multi_a(1, 'KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy')).to_hex).
to eq('5120eb5bd3894327d75093891cc3a62506df7d58ec137fcd104cdd285d67816074f3')
expect(tr('a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd',
multi_a(1, '669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0')).to_hex).
to eq('5120eb5bd3894327d75093891cc3a62506df7d58ec137fcd104cdd285d67816074f3')
key = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'
multi_keys = %w[[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0]
expect(tr(key, multi_a(2, *multi_keys)).to_hex).to eq('51202eea93581594a43c0c8423b70dc112e5651df63984d108d4fc8ccd3b63b4eafa')
expect(tr(key, sortedmulti_a(2, *multi_keys)).to_hex).to eq('512016fa6a6ba7e98c54b5bf43b3144912b78a61b60b02f6a74172b8dcb35b12bc30')
expect(tr(key, sortedmulti_a(2,
"xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0",
"xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/0/0")
).to_hex).to eq('5120abd47468515223f58a1a18edfde709a7a2aab2b696d59ecf8c34f0ba274ef772')
desc_multi_a = "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,multi_a(2,xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/0,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/0'))"
multi_a = described_class.parse(desc_multi_a)
expect(multi_a.to_hex).to eq('5120e4c8f2b0a7d3a688ac131cb03248c0d4b0a59bbd4f37211c848cfbd22a981192')
expect(multi_a.to_s).to eq(desc_multi_a)

key = '03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0'
expect{multi_a(1, key).to_hex}.
to raise_error(RuntimeError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{described_class.parse("multi_a(1,#{key})")}.
to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{sortedmulti_a(1, key).to_hex}.
to raise_error(RuntimeError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{described_class.parse("sortedmulti_a(1,#{key})")}.
to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{sh(multi_a(1, key))}.to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{sh(sortedmulti_a(1, key))}.to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{wsh(multi_a(1, key))}.to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
expect{wsh(sortedmulti_a(1, key))}.to raise_error(ArgumentError, 'Can only have multi_a/sortedmulti_a inside tr().')
key = '03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd'
expect{multi_a('a', key)}.to raise_error(ArgumentError, "Multisig threshold 'a' is not valid.")
expect{sortedmulti_a('a', key)}.to raise_error(ArgumentError, "Multisig threshold 'a' is not valid.")
expect{multi_a(0, key)}.to raise_error(ArgumentError, "Multisig threshold cannot be 0, must be at least 1.")
expect{sortedmulti_a(0, key)}.to raise_error(ArgumentError, "Multisig threshold cannot be 0, must be at least 1.")
uncompressed_key = '04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235'
expect{multi_a(1, uncompressed_key)}.to raise_error(ArgumentError, "Uncompressed key are not allowed.")
expect{sortedmulti_a(1, uncompressed_key)}.to raise_error(ArgumentError, "Uncompressed key are not allowed.")
expect{multi_a(3,
'L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1',
'5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss')}
.to raise_error(ArgumentError, "Multisig threshold cannot be larger than the number of keys.")
expect{sortedmulti_a(3,
'L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1',
'5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss')}
.to raise_error(ArgumentError, "Multisig threshold cannot be larger than the number of keys.")
end
end
end

0 comments on commit 099fc15

Please sign in to comment.