Skip to content

Conversation

@suhailsaqan
Copy link

Description

This PR adds support for Bitcoin Taproot (tr) output descriptors, extending the current functionality to include Segwit v1 addresses. The implementation follows the Bitcoin Core descriptor specification and supports key path spending for Taproot addresses.

Features Added

  • Support for parsing tr() descriptors
  • Script generation for Taproot outputs
  • Address generation for both mainnet and testnet networks
  • Comprehensive test coverage

Implementation Details

  • Added parsing logic for tr() descriptors
  • Implemented script generation using x-only public keys
  • Added address generation support
  • Added test cases to verify functionality

Current Limitations

  • Only supports key path spending
  • Script path spending will be implemented in a future PR

Testing

All tests pass, including the new Taproot descriptor tests that verify:

  • Parsing tr descriptors with public keys
  • Generating correct scripts and addresses
  • Supporting both mainnet and testnet addresses
  • Proper rejection handling for unsupported features

This makes the library more complete by supporting all current Bitcoin address types, including the latest Taproot addresses, which enable more efficient and private Bitcoin transactions.

@SachinMeier SachinMeier self-requested a review April 23, 2025 20:43
case parse_closing_bracket(remaining) do
{:ok, rest_with_checksum} ->
checksum = extract_checksum(rest_with_checksum)
type = if sorted?, do: :sortedmulti, else: :multi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we verify the sortedness of the keys if sorted?

Comment on lines +409 to +410
defp extract_checksum(""), do: nil
defp extract_checksum("#" <> checksum), do: checksum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate the checksum

Converts an output descriptor to a script.
"""
@spec to_script(t()) :: Script.t()
def to_script(%__MODULE__{type: :pk, key: key}) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think in each of these, you will need to add a when key != nil otherwise key: key will always match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, there's no handling of cases where an xpub is provided, meaning this function would crash. we should add sth like def to_script(_), do: {:error, "..."} or, even better, allow the caller to pass in an opts which contains a derivation path, or other necessary args to derive a valid script.

Comment on lines +489 to +491
serialized = Point.sec(key)
# Remove the first byte (compression marker) to get the x-only key
binary_part(serialized, 1, 32)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a function called Point.x_bytes which does this.


binary when is_binary(binary) and byte_size(binary) == 33 ->
# Remove first byte from compressed pubkey
binary_part(binary, 1, 32)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should validate this is a valid pubkey.

end

# Create a pay-to-taproot script: OP_1 <32-byte x-only pubkey>
{:ok, script} = Script.create_p2tr(xonly_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the create_p2tr function does take a Point fwiw.

Converts an output descriptor to an address.
"""
@spec to_address(t(), atom()) :: {:ok, String.t()} | {:error, String.t()}
def to_address(descriptor, network \\ :mainnet) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just my opinion, but i'd prefer avoiding a default value for network, like in Script.to_address

Comment on lines +515 to +516
script = to_script(descriptor)
Script.to_address(script, network)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (feel free to ignore): pipe?

# Test script generation
script = OutputDescriptor.to_script(descriptor)
assert Script.is_multi?(script)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests of parsing and serializing the examples provided here: https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md

@@ -0,0 +1,140 @@
defmodule Bitcoinex.OutputDescriptorTest do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test files should be named ..._test.exs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants