Skip to content

master_fingerprint() docs promise 0x00000000 for no-origin keys, but PSBT helpers export synthetic fingerprints instead #998

Description

@Nuhiat-Arefin

Summary

The public docs for master_fingerprint() say no-origin descriptor keys should use:

0x00000000 if none

However, for raw descriptor public keys with no origin information, the current implementation computes a nonzero synthetic fingerprint from the key material. The public PSBT descriptor-update helpers then serialize that synthetic (fingerprint, empty_path) pair into PSBT origin metadata maps:

  • bip32_derivation
  • tap_key_origins

The first minimal repro below shows the non-Taproot bip32_derivation path. A Taproot companion variant further below shows the same synthetic-origin behavior in tap_key_origins.

Verified locally on commit:

6131095c8ee99ba9a91ee004b1d387dd8418766a

Doc / implementation contradiction

The contradiction appears to be literal.

The public docs for master_fingerprint() say the fingerprint should be:

0x00000000 if none

That wording appears on both the DescriptorPublicKey and DefiniteDescriptorKey wrappers.

But for raw single keys without origin information, the current implementation instead derives a nonzero fingerprint from the pubkey bytes and then exports that value with an empty derivation path.

Minimal repro

use std::str::FromStr;

use miniscript::bitcoin::psbt;
use miniscript::descriptor::DefiniteDescriptorKey;
use miniscript::{Descriptor, PsbtInputExt};

fn main() {
    let desc = Descriptor::<DefiniteDescriptorKey>::from_str(
        "wpkh(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
    )
    .unwrap();

    let mut input = psbt::Input::default();
    input.update_with_descriptor_unchecked(&desc).unwrap();

    println!("{:?}", input.bip32_derivation);
}

Taproot companion variant

Also locally confirmed:

use std::str::FromStr;

use miniscript::bitcoin::psbt;
use miniscript::descriptor::DefiniteDescriptorKey;
use miniscript::{Descriptor, PsbtOutputExt};

fn main() {
    let desc = Descriptor::<DefiniteDescriptorKey>::from_str(
        "tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)",
    )
    .unwrap();

    let mut output = psbt::Output::default();
    output.update_with_descriptor_unchecked(&desc).unwrap();

    println!("{:?}", output.tap_key_origins);
}

Observed behavior

Locally:

  • the no-origin case does not use the documented 00000000 if none behavior
  • bip32_derivation is populated even though the descriptor key had no origin
  • the exported fingerprint is synthetic and nonzero rather than the documented 00000000 if none

Concrete local output from the existing repro family:

wpkh_bip32_len=1
wpkh_fp=751e76e8
wpkh_path= (empty path)

I also confirmed the same family on Taproot helpers:

tr_tap_key_origins_len=1
tr_fp=f678d9b7
tr_path= (empty path)

So both non-Taproot and Taproot PSBT helper paths export nonzero synthetic fingerprints with an empty derivation path for raw no-origin descriptor keys.

Why this seems to happen

The current raw-key path appears to:

  1. treat a raw DescriptorPublicKey::Single as a definite key
  2. derive a synthetic master_fingerprint() from the raw pubkey bytes
  3. pair it with an empty derivation path
  4. emit that pair into PSBT key-origin metadata maps

Why this matters

Applications using the public PSBT update helpers can emit invented origin/provenance metadata for raw descriptor keys whose descriptor supplied no origin at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions