[POC] wallet: Add Support for BIP-353 DNS-Based Bitcoin Address via External Resolver #33069

pull w0xlt wants to merge 1 commits into bitcoin:master from w0xlt:bip353 changing 3 files +219 −1
  1. w0xlt commented at 7:13 am on July 26, 2025: contributor

    This PR implements BIP-353 support in Bitcoin Core’s wallet, enabling users to send Bitcoin to human-readable usernames instead of traditional addresses.

    What is BIP-353?

    BIP-353 specifies a method for resolving human-friendly payment instructions via DNS. Instead of sharing long Bitcoin addresses, users can share memorable usernames like alice._bitcoin-payment.example.com.

    Key Features

    • New -dnsresolver option: Specify a DNSSEC-validating DNS resolver command to handle BIP-353 lookups
    • Seamless integration: The sendtoaddress RPC now accepts both traditional Bitcoin addresses and BIP-353 usernames
    • Automatic resolution: When a BIP-353 username is provided, it’s automatically resolved to a Bitcoin address before processing the transaction

    How it works

    1. Users provide a username in the format: username._bitcoin-payment.domain.com
    2. The wallet validates the username format
    3. If valid and a DNS resolver is configured, it queries the DNS TXT record
    4. The resolver returns a bitcoin: URI containing the destination address
    5. The transaction proceeds with the resolved address

    Example Usage

    0# Configure DNS resolver
    1bitcoind -dnsresolver="/home/user/dns-bitcoin-resolver"
    2
    3# Send to a BIP-353 username
    4bitcoin-cli sendtoaddress "alice.user._bitcoin-payment.alice.com" 0.01
    

    Implementation Details

    • Validates usernames according to DNS naming rules and BIP-353 format
    • Handles FQDN format (with or without trailing dot)
    • Provides error messages for resolution failures
    • Falls back to traditional address validation if the input isn’t a valid BIP-353 username

    P.S.: There is an example -dnsresolver application here: https://github.com/w0xlt/dns-bitcoin-resolver for testing

    The idea of this PR is to gather feedback on whether this direction makes sense and how we might best implement BIP-353 support if there’s interest. This would be even better if combined with Silent Payment and DNS resolver with DNSSEC validation could also solve some issues related to Payjoin V2 implementation in Core.

  2. DrahtBot commented at 7:13 am on July 26, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33069.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept NACK luke-jr, 1440000bytes
    Concept ACK Sjors

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

  3. w0xlt marked this as a draft on Jul 26, 2025
  4. w0xlt force-pushed on Jul 26, 2025
  5. DrahtBot added the label CI failed on Jul 26, 2025
  6. DrahtBot commented at 7:21 am on July 26, 2025: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/runs/46773849269 LLM reason (✨ experimental): The CI failure is caused by an assertion error in the lint check, specifically due to a missing hidden argument ‘-dnsresolver’.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  7. w0xlt force-pushed on Jul 26, 2025
  8. Add Support for BIP-353 DNS-Based Bitcoin Address via External Resolver b5ad89f036
  9. w0xlt force-pushed on Jul 26, 2025
  10. in src/wallet/rpc/spend.cpp:392 in b5ad89f036
    387+        std::string uri = contents.substr(8); // Remove "bitcoin:" prefix
    388+        size_t question_pos = uri.find('?');
    389+
    390+        if (question_pos == 0) {
    391+            // No address, only parameters (bitcoin:?lno=...)
    392+            return {false, "No Bitcoin address found in the DNS record"};
    


    Sjors commented at 7:52 am on July 26, 2025:
    So after #28201 this would no longer be a failure?
  11. in src/wallet/rpc/spend.cpp:332 in b5ad89f036
    327+    }
    328+
    329+    return true;
    330+}
    331+
    332+struct BIP353Result {
    


    Sjors commented at 7:53 am on July 26, 2025:
    Maybe return an array of strings, because there might be multiple options we can pay to (regular address, silent payment, some future thing).
  12. in src/wallet/rpc/spend.cpp:496 in b5ad89f036
    492@@ -303,7 +493,32 @@ RPCHelpMan sendtoaddress()
    493     EnsureWalletIsUnlocked(*pwallet);
    494 
    495     UniValue address_amounts(UniValue::VOBJ);
    496-    const std::string address = request.params[0].get_str();
    497+    std::string address = request.params[0].get_str();
    


    Sjors commented at 7:55 am on July 26, 2025:
    Can you add this to the send RPC as well? Ideally the code should reusable by the GUI too, so maybe move it to CWallet.
  13. Sjors commented at 8:04 am on July 26, 2025: member

    Concept ACK

    Using RunCommandParseJSON() is similar to how we handle external signers. It’s a good way to add this functionality without adding a lot of code and dependencies.

    It won’t be useful until we have silent payments send support #28201 (we don’t want to encourage address reuse), so this PR could be contingent on that.

    Node in a box applications like Raspiblitz could easily ship and configure a resolver, so this can be used not just by advanced users.

    URI parsing is always a bit dangerous so it would be nice to have fuzzer coverage. In fact we already have some well tested parsing code that you may want to reuse.

  14. in src/wallet/rpc/spend.cpp:314 in b5ad89f036
    309+        if (label != "_bitcoin-payment") {
    310+            if (label.front() == '-' || label.back() == '-') {
    311+                return false;
    312+            }
    313+        }
    314+
    


    TheBlueMatt commented at 5:46 pm on July 26, 2025:
    This should be allowed, I believe.
  15. in src/wallet/rpc/spend.cpp:245 in b5ad89f036
    240+ *   - pay.bob._bitcoin-payment.company.co.uk
    241+ *
    242+ * Invalid examples:
    243+ *   - carol.user.carol.com (missing ._bitcoin-payment.)
    244+ *   - _bitcoin-payment.example.com (missing username part)
    245+ *   - user@example.com (not a valid DNS name format)
    


    TheBlueMatt commented at 5:48 pm on July 26, 2025:
    Why require users to pass in the converted string rather than letting them just pass in the HRN directly and adding the .user._bitcoin-payment ourselves?
  16. in src/wallet/rpc/spend.cpp:371 in b5ad89f036
    366+
    367+    // Look for TXT record with bitcoin: URI
    368+    for (size_t i = 0; i < result.size(); ++i) {
    369+        const UniValue& record = result[i];
    370+
    371+        if (!record.isObject() || !record.exists("type") || !record.exists("contents")) {
    


    TheBlueMatt commented at 5:51 pm on July 26, 2025:
    I’m not quite sure what the anticipated format for this response is, is it documented anywhere? eg there’s the google-proprietary format at https://dns.google/resolve?name=matt.user._bitcoin-payment.mattcorallo.com&type=TXT which seems to be similar to what you want, but uses the integer value for type (also would require checking the AD bit!).
  17. in src/wallet/rpc/spend.cpp:404 in b5ad89f036
    399+            address = uri;
    400+        }
    401+
    402+        // Basic validation - check if address is not empty
    403+        if (address.empty()) {
    404+            return {false, "No Bitcoin address found in the DNS record"};
    


    TheBlueMatt commented at 5:53 pm on July 26, 2025:
    spec also mandates that you fail if there is more than one TXT record that starts with “bitcoin:”
  18. in src/wallet/rpc/spend.cpp:382 in b5ad89f036
    377+        }
    378+
    379+        std::string contents = record["contents"].get_str();
    380+
    381+        // Check if it starts with "bitcoin:"
    382+        if (contents.find("bitcoin:") != 0) {
    


    TheBlueMatt commented at 5:53 pm on July 26, 2025:
    Should be ignoring case.
  19. in src/wallet/rpc/spend.cpp:407 in b5ad89f036
    402+        // Basic validation - check if address is not empty
    403+        if (address.empty()) {
    404+            return {false, "No Bitcoin address found in the DNS record"};
    405+        }
    406+
    407+        // Return the extracted address
    


    TheBlueMatt commented at 5:55 pm on July 26, 2025:
    You also need to accept arguments. As @Sjors noted this will be needed for future things like silent payments ?sp=... but also BIP 321 mandates that you need to recognize any bech32 payment instructions in parameters (eg bitcoin:1Legacy?bc=segwit&BC=TAPROOT). Probably this code should be shared with the existing URI-parsing logic in the GUI, which may or may not need updating to handle 321 correctly.
  20. in src/wallet/init.cpp:85 in b5ad89f036
    81@@ -82,6 +82,8 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
    82 
    83     argsman.AddArg("-walletrejectlongchains", strprintf("Wallet will not create transactions that violate mempool chain limits (default: %u)", DEFAULT_WALLET_REJECT_LONG_CHAINS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
    84     argsman.AddArg("-walletcrosschain", strprintf("Allow reusing wallet files across chains (default: %u)", DEFAULT_WALLETCROSSCHAIN), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::WALLET_DEBUG_TEST);
    85+    argsman.AddArg("-dnsresolver=<cmd>", "DNSSEC-validating resolver for BIP-353 Bitcoin addresses", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
    


    TheBlueMatt commented at 5:57 pm on July 26, 2025:
    What is the plan here? Ship a separate binary (included via the same multi-process release process) to do resolution or is the goal to eventually just embed something that can talk directly to a DNS server over TCP and fetch results (its pretty trivial code, but its a bunch of parsing to do in C/C++)?

    Sjors commented at 8:43 am on July 27, 2025:
    I think initially it would be similar to HWI: separately maintained and released. (but see discussion below)
  21. TheBlueMatt commented at 6:22 pm on July 26, 2025: contributor

    Oh one last comment, it would be nice if the resolver also provided the proof which was stored in the wallet.

    In general on-chain bitcoin users expect txids to, by themselves, be “proof of payment”. That changes with 353 - you now need the txid plus the DNSSEC proof (as the records may change out from under you). Whether the proof are available today or not, ensuring they get stored in the wallet is good future-proofing for PoP.

  22. achow101 commented at 9:04 pm on July 26, 2025: member

    New -dnsresolver option: Specify a DNSSEC-validating DNS resolver command to handle BIP-353 lookups

    I don’t think we should be outsourcing that to another program. Validating DNSSEC signatures is an integral part of BIP 353 and the only way we can guarantee that the proof is valid is for us to receive the prrof and validate it internally, which defeats the purpose of calling another binary.

    Unlike with -signer, I don’t think there’s a good reason to use a separate binary for the DNS lookups. There is no need for minimizing attack surface since we already do DNS lookups via syscalls. I don’t think there’s a situation where the called binary would need to be any different. It’s not like HWI where the binary may support different hardware; rather there’s exactly one thing that it is supposed to do, so I think it would be better for us to (re)write TXT resolving and DNSSEC validation internally.

  23. TheBlueMatt commented at 10:39 pm on July 26, 2025: contributor

    One advantage of doing a separate process is that it sidesteps either dealing with validation integration [1], which will invariably stall progress ~forever. Using a separate binary for now would let the work streams progress independently and figuring out how to in-house validation can come separately.

    [1] I think there’s several ways, so even analyzing them will probably stall things ~forever - Rust integration, trying to write secp256r1/RSA plus DNSSEC validation in C++, or taking on some dependency that does those (though there’s now several not-DNS-resolvers that can do DNSSEC validation, including systemd and dnsmasq, so maybe one has reusable code).

  24. achow101 commented at 1:53 am on July 27, 2025: member

    One advantage of doing a separate process is that it sidesteps either dealing with validation integration

    It introduces a potential security issue though since we can’t validate the output of the separate process. We’re just trusting whatever it spits out, and if it were malicious, it could be making fake addresses which results in funds being sent to an attacker., and the user basically wouldn’t know.

    There’s also the question of how to distribute that binary which I think would result in a significant stalling. We still haven’t resolved the distribution questions around assumeutxo and asmap, and I think those projects probably have more interest from contributors than BIP 353.

    Since we’d only need a DNS client that does TXT and the DNSSEC records, and validation for RSA and secp256r1, I don’t think an internal implementation would actually be a major blocker. These implementations can even be relatively slow and naive as they’re just validation only, as you’ve done in dnssec-prover. I’m planning to dig into that next week.

  25. bitcoin deleted a comment on Jul 28, 2025
  26. w0xlt commented at 8:28 am on July 28, 2025: contributor

    I’ve created a C++ implementation of the dnssec-prover, available at: https://github.com/w0xlt/dnssec-validation. This implementation focuses solely on the DNSSEC validation component, which is the critical aspect for our use case.

    The dnssec-validation binary can also be used as an external dependency in this PR. Functionally, it mirrors dnssec-prover, with the main difference being its C++ codebase.

    If the goal is to eventually integrate DNSSEC validation directly into the Bitcoin Core codebase, most of the required C++ logic is already implemented in the repository above. However, a key challenge lies in the cryptographic validation implemented in validation.cpp (equivalent to validation.rs in the original project), which relies on OpenSSL.

    Below is a breakdown of the OpenSSL functionality currently in use.

    At this point, it’s unclear whether incorporating these OpenSSL-based cryptographic functions directly into the Bitcoin Core codebase is practical or desirable. Further discussion would be needed to evaluate this integration path.


    Hashing via EVP (Envelope) Interface

    • EVP_MD_CTX_new() – Allocate a new message digest context
    • EVP_MD_CTX_free() – Free the context
    • EVP_sha1(), EVP_sha256(), EVP_sha384(), EVP_sha512() – Retrieve specific hash algorithms
    • EVP_DigestInit_ex() – Initialize the digest context
    • EVP_DigestUpdate() – Feed data into the digest
    • EVP_DigestFinal_ex() – Finalize and retrieve the digest
    • EVP_MD_size() – Get the digest size for a given algorithm

    RSA Functions

    • RSA_new(), RSA_free() – Allocate and free RSA structures
    • RSA_set0_key() – Set modulus (n) and exponent (e)
    • RSA_verify() – Verify an RSA signature (PKCS#1 padding)

    BIGNUM Utilities

    • BN_bin2bn() – Convert binary to BIGNUM (used in RSA key construction)
    • BN_free() – Free BIGNUM structures

    Elliptic Curve (EC) Handling

    • EC_KEY_new_by_curve_name() – Create a key for a named curve
    • EC_KEY_set_public_key_affine_coordinates() – Set EC public key coordinates (x, y)
    • EC_KEY_free() – Free the EC_KEY

    ECDSA Operations

    • ECDSA_SIG_new(), ECDSA_SIG_free() – Allocate and free ECDSA signatures
    • ECDSA_SIG_set0() – Set r and s components
    • ECDSA_do_verify() – Verify an ECDSA signature

    OpenSSL Constants Used

    • NID_sha256, NID_sha512 – Identifiers for SHA-2 algorithms
    • NID_X9_62_prime256v1 – Identifier for the P-256 curve
    • NID_secp384r1 – Identifier for the P-384 curve

    Supported Cryptographic Algorithms

    • RSA/SHA-256 (algorithm 8)
    • RSA/SHA-512 (algorithm 10)
    • ECDSA P-256/SHA-256 (algorithm 13)
    • ECDSA P-384/SHA-384 (algorithm 14)
    • ED25519 is referenced but currently not supported (algorithm 15)

    The implementation uses OpenSSL’s EVP interface for hashing (encapsulated within a Hasher class), and leverages direct RSA and ECDSA operations for signature verification, in line with DNSSEC requirements.

  27. furszy commented at 4:57 pm on July 28, 2025: member

    and validation for RSA and secp256r1, I don’t think an internal implementation would actually be a major blocker. These implementations can even be relatively slow and naive as they’re just validation only, as you’ve done in dnssec-prover. I’m planning to dig into that next week.

    Just off the top of my head, a minimalistic approach would be to implement an ASN.1 DER parser. For RSA, we’d extend our arith_uint to support 4096-bit numbers with a mod operator (doubling the modulus size as needed for exponentiation) and implement modular squaring exponentiation. For secp256r1, we could extend arith_uint256 for finite field arithmetic (adding FLT for inverses to keep the code small), and then we’d need to implement the curve’s point addition, doubling, and scalar multiplication. Note: while fun to implement, I’m currently a ~0 on adding this to Core. A naive implementation with no proper review could be as bad as using an external library too.

  28. achow101 commented at 6:34 pm on July 28, 2025: member

    At this point, it’s unclear whether incorporating these OpenSSL-based cryptographic functions directly into the Bitcoin Core codebase is practical or desirable.

    I don’t think it is desirable. OpenSSL is a massive codebase, and we only need a tiny part. We spent a very long time trying to remove OpenSSL as well.

  29. w0xlt commented at 6:48 pm on July 28, 2025: contributor

    I don’t think it is desirable. OpenSSL is a massive codebase, and we only need a tiny part. We spent a very long time trying to remove OpenSSL as well.

    What I meant was reimplementing those OpenSSL functions I mentioned above in our codebase.

    A naive implementation with no proper review could be as bad as using an external library too.

    That’s my point. Adding cryptographic schemes that aren’t directly part of the Bitcoin protocol can be difficult to obtain sufficient review or even consensus for merging.

  30. TheBlueMatt commented at 9:55 pm on July 28, 2025: contributor

    It introduces a potential security issue though since we can’t validate the output of the separate process. We’re just trusting whatever it spits out, and if it were malicious, it could be making fake addresses which results in funds being sent to an attacker., and the user basically wouldn’t know.

    This is why I raised it in the context of multi-process - multi-process already inherently moves to a world where Bitcoin Core trusts output of other processes, doing this in that context wouldn’t change anything.

    There’s also the question of how to distribute that binary which I think would result in a significant stalling. We still haven’t resolved the distribution questions around assumeutxo and asmap, and I think those projects probably have more interest from contributors than BIP 353.

    How is it different than any other multi-process binary?

    Since we’d only need a DNS client that does TXT and the DNSSEC records, and validation for RSA and secp256r1, I don’t think an internal implementation would actually be a major blocker. These implementations can even be relatively slow and naive as they’re just validation only, as you’ve done in dnssec-prover. I’m planning to dig into that next week.

    Okay, if you want to build it all in-house great, I imagine that will take a while, though :)

  31. luke-jr commented at 5:40 am on July 29, 2025: member
    Concept NACK, Bitcoin invoice addresses are single-use, and it doesn’t make sense to put them in DNS.
  32. 1440000bytes commented at 8:53 am on July 29, 2025: none

    Concept NACK

    • Adds unnecessary complexity
    • Adds attack vector
    • It is possible to achieve this functionality without changing bitcoin core

    BIP 353 itself is controversial because it expects an average user to securely manage DNS for bitcoin payments.

  33. achow101 commented at 6:23 pm on July 29, 2025: member

    How is it different than any other multi-process binary?

    The multiprocess binaries are produced by this project, built using the same deterministic build system, using the same code as the monolithic binaries. There are already concrete ways that the binaries can be distributed with minimal changes to our own release process. Users who run the multiprocess binaries don’t have to do any special configuration for them to work. This is different from requiring users to set a configuration option that points to a binary that they may be downloading from somewhere else, and probably has different review standards. Multiprocess would work straight out of the box, whereas at least in the initial releases, this BIP 353 implementation won’t. This extra configuration opens the door to malicious documentation where users who are trying to set this up are directed by attackers to download a malicious binary for use as the external resolver.

  34. TheBlueMatt commented at 1:11 pm on July 30, 2025: contributor

    The multiprocess binaries are produced by this project, built using the same deterministic build system, using the same code as the monolithic binaries. There are already concrete ways that the binaries can be distributed with minimal changes to our own release process. Users who run the multiprocess binaries don’t have to do any special configuration for them to work.

    Right, I was ancitipating that, ultimately, an external-process DNS resolution thing would be built and dstributed the same, even if its in a different language. Whether an initial PR or intermediate PR does that seems unrelated.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-08-12 09:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me