Skip to content

Commit

Permalink
Implements raw() and hex() descriptor specified by BIP-385
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Jul 4, 2024
1 parent 9dbc76b commit 4bac156
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 18 deletions.
34 changes: 27 additions & 7 deletions lib/bitcoin/descriptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,41 @@ module Descriptor
autoload :Combo, 'bitcoin/descriptor/combo'
autoload :Multi, 'bitcoin/descriptor/multi'
autoload :SortedMulti, 'bitcoin/descriptor/sorted_multi'
autoload :Raw, 'bitcoin/descriptor/raw'
autoload :Addr, 'bitcoin/descriptor/addr'
autoload :Checksum, 'bitcoin/descriptor/checksum'

module_function

# generate P2PK output for the given public key.
# Generate P2PK output for the given public key.
# @param [String] key private key or public key with hex format
# @return [Bitcoin::Descriptor::Pk]
def pk(key)
Pk.new(key)
end

# generate P2PKH output for the given public key.
# Generate P2PKH output for the given public key.
# @param [String] key private key or public key with hex format.
# @return [Bitcoin::Descriptor::Pkh]
def pkh(key)
Pkh.new(key)
end

# generate P2PKH output for the given public key.
# Generate P2PKH output for the given public key.
# @param [String] key private key or public key with hex format.
# @return [Bitcoin::Descriptor::Wpkh]
def wpkh(key)
Wpkh.new(key)
end

# generate P2SH embed the argument.
# Generate P2SH embed the argument.
# @param [Bitcoin::Descriptor::Base] exp script expression to be embed.
# @return [Bitcoin::Descriptor::Sh]
def sh(exp)
Sh.new(exp)
end

# generate P2WSH embed the argument.
# Generate P2WSH embed the argument.
# @param [Bitcoin::Descriptor::Expression] exp script expression to be embed.
# @return [Bitcoin::Descriptor::Wsh]
def wsh(exp)
Expand All @@ -61,22 +63,36 @@ def combo(key)
Combo.new(key)
end

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

# generate sorted multisig output for given keys.
# Generate 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(threshold, *keys)
SortedMulti.new(threshold, keys)
end

# Generate raw output script about +hex+.
# @param [String] hex Hex string of bitcoin script.
# @return [Bitcoin::Descriptor::Raw]
def raw(hex)
Raw.new(hex)
end

# Generate raw output script about +hex+.
# @param [String] addr Bitcoin address.
# @return [Bitcoin::Descriptor::Addr]
def addr(addr)
Addr.new(addr)
end

# Parse descriptor string.
# @param [String] string Descriptor string.
# @return [Bitcoin::Descriptor::Expression]
Expand All @@ -102,6 +118,10 @@ def parse(string)
threshold = args[0].to_i
keys = args[1..-1]
exp == 'multi' ? multi(threshold, *keys) : sortedmulti(threshold, *keys)
when 'raw'
raw(args_str)
when 'addr'
addr(args_str)
else
raise ArgumentError, "Parse failed: #{string}"
end
Expand Down
31 changes: 31 additions & 0 deletions lib/bitcoin/descriptor/addr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Bitcoin
module Descriptor
class Addr < Expression
include Bitcoin::Util

attr_reader :addr

def initialize(addr)
raise ArgumentError, "Address must be string." unless addr.is_a?(String)
raise ArgumentError, "Address is not valid." unless valid_address?(addr)
@addr = addr
end

def type
:addr
end

def to_script
Bitcoin::Script.parse_from_addr(addr)
end

def top_level?
true
end

def args
addr
end
end
end
end
4 changes: 4 additions & 0 deletions lib/bitcoin/descriptor/combo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def ==(other)
return false unless other.is_a?(Combo)
type == other.type && to_scripts == other.to_scripts
end

def top_level?
true
end
end
end
end
20 changes: 20 additions & 0 deletions lib/bitcoin/descriptor/expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ def to_script
raise NotImplementedError
end

# Whether this is top level or not.
# @return [Boolean]
def top_level?
raise NotImplementedError
end

# Get args for this expression.
# @return [String] args
def args
raise NotImplementedError
end

# Get descriptor string.
# @param [Boolean] checksum If true, append checksum.
# @return [String] Descriptor string.
def to_s(checksum: false)
desc = "#{type.to_s}(#{args})"
checksum ? Checksum.descsum_create(desc) : desc
end

# Convert to bitcoin script as hex string.
# @return [String]
def to_hex
Expand Down
9 changes: 6 additions & 3 deletions lib/bitcoin/descriptor/key_expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ def initialize(key)
@key = key
end

def to_s(checksum: false)
desc = "#{type.to_s}(#{key})"
checksum ? Checksum.descsum_create(desc) : desc
def args
key
end

def top_level?
false
end
end
end
Expand Down
9 changes: 6 additions & 3 deletions lib/bitcoin/descriptor/multi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ def to_hex
super
end

def to_s(checksum: false)
desc = "#{type.to_s}(#{threshold},#{keys.join(',')})"
checksum ? Checksum.descsum_create(desc) : desc
def args
"#{threshold},#{keys.join(',')}"
end

def top_level?
false
end

private
Expand Down
32 changes: 32 additions & 0 deletions lib/bitcoin/descriptor/raw.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Bitcoin
module Descriptor
class Raw < Expression

attr_reader :hex

# Constructor
# @param [String] hex
def initialize(hex)
raise ArgumentError, "Raw script must be string." unless hex.is_a?(String)
raise ArgumentError, "Raw script is not hex." unless hex.valid_hex?
@hex = hex
end

def type
:raw
end

def to_script
Bitcoin::Script.parse_from_payload(hex.htb)
end

def args
hex
end

def top_level?
true
end
end
end
end
8 changes: 3 additions & 5 deletions lib/bitcoin/descriptor/script_expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ def initialize(script)
@script = script
end

def to_s(checksum: false)
desc = "#{type.to_s}(#{script.to_s})"
checksum ? Checksum.descsum_create(desc) : desc
def args
script.to_s
end

private

def validate!(script)
raise ArgumentError, 'Can only have combo() at top level.' if script.is_a?(Combo)
raise ArgumentError, 'Can only have sh() at top level.' if script.is_a?(Sh)
raise ArgumentError, "Can only have #{script.type.to_s}() at top level." if script.is_a?(Expression) && script.top_level?
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/bitcoin/descriptor/sh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def to_script
script.to_script.to_p2sh
end

def top_level?
true
end

private

def validate!(script)
Expand Down
4 changes: 4 additions & 0 deletions lib/bitcoin/descriptor/wsh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def to_script
Script.to_p2wsh(script.to_script)
end

def top_level?
false
end

def validate!(script)
super(script)
raise ArgumentError, 'A function is needed within P2WSH.' unless script.is_a?(Expression)
Expand Down
25 changes: 25 additions & 0 deletions spec/bitcoin/descriptor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,29 @@
expect(described_class.parse(desc_combo).to_s(checksum: true)).to eq("#{desc_combo}#p5326pcv")
end
end

describe "BIP-385" do
it do
desc_raw = 'raw(deadbeef)'
raw = described_class.parse(desc_raw)
expect(raw.to_hex).to eq("deadbeef")
expect(raw("deadbeef")).to eq(raw)
expect(raw.to_s(checksum: true)).to eq("#{desc_raw}#89f8spxm")

expect{raw('asdf')}.to raise_error(ArgumentError, "Raw script is not hex.")
expect{sh(raw('deadbeef'))}.to raise_error(ArgumentError, "Can only have raw() at top level.")
expect{wsh(raw('deadbeef'))}.to raise_error(ArgumentError, "Can only have raw() at top level.")

p2sh = "3PUNyaW7M55oKWJ3kDukwk9bsKvryra15j"
desc_addr = "addr(#{p2sh})"
addr = described_class.parse(desc_addr)
expect(addr.to_hex).to eq("a914eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee87")
expect(addr('3PUNyaW7M55oKWJ3kDukwk9bsKvryra15j')).to eq(addr)
expect(addr.to_s(checksum: true)).to eq("#{desc_addr}#6vhk2xgr")

expect{addr('asdf')}.to raise_error(ArgumentError, "Address is not valid.")
expect{sh(addr(p2sh))}.to raise_error(ArgumentError, "Can only have addr() at top level.")
expect{wsh(addr(p2sh))}.to raise_error(ArgumentError, "Can only have addr() at top level.")
end
end
end

0 comments on commit 4bac156

Please sign in to comment.