Skip to content

taker.podle

taker.podle

Proof of Discrete Log Equivalence (PoDLE) generation for takers.

This module re-exports PoDLE generation functions from jmcore and provides taker-specific utilities for UTXO selection and commitment generation.

PoDLE is used to prevent sybil attacks in JoinMarket by requiring takers to prove ownership of a UTXO without revealing which UTXO until after the maker commits to participate.

Protocol flow: 1. Taker generates commitment \(C = H(P_2)\) where \(P_2 = k \cdot J\) (\(k\) = private key, \(J\) = NUMS point) 2. Taker sends commitment \(C\) to maker 3. Maker accepts and sends pubkey 4. Taker reveals \(P\), \(P_2\), \(\text{sig}\), \(e\) as the "revelation" 5. Maker verifies: \(P = k \cdot G\) and \(P_2 = k \cdot J\) (same \(k\))

Reference: https://gist.github.com/AdamISZ/9cbba5e9408d23813ca8

Attributes

__all__ = ['ExtendedPoDLECommitment', 'PoDLECommitment', 'PoDLEError', 'generate_podle', 'get_eligible_podle_utxos', 'select_podle_utxo', 'serialize_revelation'] module-attribute

Classes

ExtendedPoDLECommitment

PoDLE commitment with extended UTXO metadata for neutrino_compat feature.

This extends the base PoDLECommitment with scriptpubkey and blockheight for Neutrino-compatible UTXO verification.

Source code in taker/src/taker/podle.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@dataclass
class ExtendedPoDLECommitment:
    """
    PoDLE commitment with extended UTXO metadata for neutrino_compat feature.

    This extends the base PoDLECommitment with scriptpubkey and blockheight
    for Neutrino-compatible UTXO verification.
    """

    commitment: PoDLECommitment
    scriptpubkey: str | None = None  # Hex-encoded scriptPubKey
    blockheight: int | None = None  # Block height where UTXO was confirmed

    # Expose underlying commitment properties for compatibility
    @property
    def p(self) -> bytes:
        """Public key P = k*G"""
        return self.commitment.p

    @property
    def p2(self) -> bytes:
        """Commitment point P2 = k*J"""
        return self.commitment.p2

    @property
    def sig(self) -> bytes:
        """Schnorr signature s"""
        return self.commitment.sig

    @property
    def e(self) -> bytes:
        """Challenge e"""
        return self.commitment.e

    @property
    def utxo(self) -> str:
        """UTXO reference txid:vout"""
        return self.commitment.utxo

    @property
    def index(self) -> int:
        """NUMS point index used"""
        return self.commitment.index

    def to_revelation(self, extended: bool = False) -> dict[str, str]:
        """
        Convert to revelation format for sending to maker.

        Args:
            extended: If True, include scriptpubkey:blockheight in utxo string
        """
        rev = self.commitment.to_revelation()
        if extended and self.scriptpubkey and self.blockheight is not None:
            # Replace utxo with extended format: txid:vout:scriptpubkey:blockheight
            txid, vout = self.commitment.utxo.split(":")
            rev["utxo"] = f"{txid}:{vout}:{self.scriptpubkey}:{self.blockheight}"
        return rev

    def to_commitment_str(self) -> str:
        """Get commitment as hex string."""
        return self.commitment.to_commitment_str()

    def has_neutrino_metadata(self) -> bool:
        """Check if we have metadata for Neutrino-compatible verification."""
        return self.scriptpubkey is not None and self.blockheight is not None
Attributes
blockheight: int | None = None class-attribute instance-attribute
commitment: PoDLECommitment instance-attribute
e: bytes property

Challenge e

index: int property

NUMS point index used

p: bytes property

Public key P = k*G

p2: bytes property

Commitment point P2 = k*J

scriptpubkey: str | None = None class-attribute instance-attribute
sig: bytes property

Schnorr signature s

utxo: str property

UTXO reference txid:vout

Methods:
has_neutrino_metadata() -> bool

Check if we have metadata for Neutrino-compatible verification.

Source code in taker/src/taker/podle.py
111
112
113
def has_neutrino_metadata(self) -> bool:
    """Check if we have metadata for Neutrino-compatible verification."""
    return self.scriptpubkey is not None and self.blockheight is not None
to_commitment_str() -> str

Get commitment as hex string.

Source code in taker/src/taker/podle.py
107
108
109
def to_commitment_str(self) -> str:
    """Get commitment as hex string."""
    return self.commitment.to_commitment_str()
to_revelation(extended: bool = False) -> dict[str, str]

Convert to revelation format for sending to maker.

Args: extended: If True, include scriptpubkey:blockheight in utxo string

Source code in taker/src/taker/podle.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def to_revelation(self, extended: bool = False) -> dict[str, str]:
    """
    Convert to revelation format for sending to maker.

    Args:
        extended: If True, include scriptpubkey:blockheight in utxo string
    """
    rev = self.commitment.to_revelation()
    if extended and self.scriptpubkey and self.blockheight is not None:
        # Replace utxo with extended format: txid:vout:scriptpubkey:blockheight
        txid, vout = self.commitment.utxo.split(":")
        rev["utxo"] = f"{txid}:{vout}:{self.scriptpubkey}:{self.blockheight}"
    return rev

PoDLECommitment

PoDLE commitment data generated by taker.

Source code in jmcore/src/jmcore/podle.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@dataclass
class PoDLECommitment:
    """PoDLE commitment data generated by taker."""

    commitment: bytes  # H(P2) - 32 bytes
    p: bytes  # Public key P = k*G - 33 bytes compressed
    p2: bytes  # Commitment point P2 = k*J - 33 bytes compressed
    sig: bytes  # Schnorr signature s - 32 bytes
    e: bytes  # Challenge e - 32 bytes
    utxo: str  # UTXO reference "txid:vout"
    index: int  # NUMS point index used

    def to_revelation(self) -> dict[str, str]:
        """Convert to revelation format for sending to maker."""
        return {
            "P": self.p.hex(),
            "P2": self.p2.hex(),
            "sig": self.sig.hex(),
            "e": self.e.hex(),
            "utxo": self.utxo,
        }

    def to_commitment_str(self) -> str:
        """
        Get commitment as string with type prefix.

        JoinMarket requires a commitment type prefix to allow future
        commitment schemes. "P" indicates a standard PoDLE commitment.
        Format: "P" + hex(commitment)
        """
        return "P" + self.commitment.hex()
Attributes
commitment: bytes instance-attribute
e: bytes instance-attribute
index: int instance-attribute
p: bytes instance-attribute
p2: bytes instance-attribute
sig: bytes instance-attribute
utxo: str instance-attribute
Methods:
to_commitment_str() -> str

Get commitment as string with type prefix.

JoinMarket requires a commitment type prefix to allow future commitment schemes. "P" indicates a standard PoDLE commitment. Format: "P" + hex(commitment)

Source code in jmcore/src/jmcore/podle.py
105
106
107
108
109
110
111
112
113
def to_commitment_str(self) -> str:
    """
    Get commitment as string with type prefix.

    JoinMarket requires a commitment type prefix to allow future
    commitment schemes. "P" indicates a standard PoDLE commitment.
    Format: "P" + hex(commitment)
    """
    return "P" + self.commitment.hex()
to_revelation() -> dict[str, str]

Convert to revelation format for sending to maker.

Source code in jmcore/src/jmcore/podle.py
 95
 96
 97
 98
 99
100
101
102
103
def to_revelation(self) -> dict[str, str]:
    """Convert to revelation format for sending to maker."""
    return {
        "P": self.p.hex(),
        "P2": self.p2.hex(),
        "sig": self.sig.hex(),
        "e": self.e.hex(),
        "utxo": self.utxo,
    }

PoDLEError

Bases: Exception

PoDLE generation or verification error.

Source code in jmcore/src/jmcore/podle.py
73
74
75
76
class PoDLEError(Exception):
    """PoDLE generation or verification error."""

    pass

Functions:

generate_podle(private_key_bytes: bytes, utxo_str: str, index: int = 0) -> PoDLECommitment

Generate a PoDLE commitment for a UTXO.

The PoDLE proves that the taker owns the UTXO without revealing the private key. It creates a zero-knowledge proof that: P = kG and P2 = kJ have the same discrete log k.

Args: private_key_bytes: 32-byte private key utxo_str: UTXO reference as "txid:vout" index: NUMS point index (0-255)

Returns: PoDLECommitment with all proof data

Source code in jmcore/src/jmcore/podle.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def generate_podle(
    private_key_bytes: bytes,
    utxo_str: str,
    index: int = 0,
) -> PoDLECommitment:
    """
    Generate a PoDLE commitment for a UTXO.

    The PoDLE proves that the taker owns the UTXO without revealing
    the private key. It creates a zero-knowledge proof that:
    P = k*G and P2 = k*J have the same discrete log k.

    Args:
        private_key_bytes: 32-byte private key
        utxo_str: UTXO reference as "txid:vout"
        index: NUMS point index (0-255)

    Returns:
        PoDLECommitment with all proof data
    """
    if len(private_key_bytes) != 32:
        raise PoDLEError(f"Invalid private key length: {len(private_key_bytes)}")

    if not 0 <= index <= 255:
        raise PoDLEError(f"Invalid NUMS index: {index} (must be 0-255)")

    try:
        private_key = PrivateKey(private_key_bytes)
    except ValueError as exc:
        raise PoDLEError("Invalid private key value") from exc

    # Calculate P = k*G (standard public key)
    p_point = private_key.public_key
    p_bytes = point_to_bytes(p_point)

    # Get NUMS point J
    j_point = get_nums_point(index)

    # Calculate P2 = k*J
    p2_point = j_point.multiply(private_key_bytes)
    p2_bytes = point_to_bytes(p2_point)

    # Generate commitment C = H(P2)
    commitment = hashlib.sha256(p2_bytes).digest()

    # Derive the nonce from the secret key and proof transcript. This removes
    # runtime RNG failures from a Schnorr-style operation where nonce reuse
    # across two proofs would reveal the UTXO private key.
    s_bytes: bytes | None = None
    for k_proof in _podle_nonce_candidates(
        private_key_bytes,
        utxo_str,
        index,
        p_bytes,
        p2_bytes,
    ):
        kg_bytes = point_to_bytes(scalar_mult_g(k_proof))
        kj_bytes = point_to_bytes(point_mult(k_proof, j_point))
        e_bytes = hashlib.sha256(kg_bytes + kj_bytes + p_bytes + p2_bytes).digest()
        s_bytes = _compute_podle_response(private_key, k_proof, e_bytes)
        if s_bytes is not None:
            break
    if s_bytes is None:
        raise PoDLEError("PoDLE nonce candidates exhausted")

    logger.debug(
        f"Generated PoDLE for {utxo_str} using NUMS index {index}, "
        f"commitment={commitment.hex()[:16]}..."
    )

    return PoDLECommitment(
        commitment=commitment,
        p=p_bytes,
        p2=p2_bytes,
        sig=s_bytes,
        e=e_bytes,
        utxo=utxo_str,
        index=index,
    )

get_eligible_podle_utxos(utxos: list[UTXOInfo], cj_amount: int, min_confirmations: int = 5, min_percent: int = 20) -> list[UTXOInfo]

Get all eligible UTXOs for PoDLE commitment, sorted by preference.

Criteria: - Must have at least min_confirmations - Must be at least min_percent of cj_amount

Returns: List of eligible UTXOs sorted by (confirmations, value) descending

Source code in taker/src/taker/podle.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def get_eligible_podle_utxos(
    utxos: list[UTXOInfo],
    cj_amount: int,
    min_confirmations: int = 5,
    min_percent: int = 20,
) -> list[UTXOInfo]:
    """
    Get all eligible UTXOs for PoDLE commitment, sorted by preference.

    Criteria:
    - Must have at least min_confirmations
    - Must be at least min_percent of cj_amount

    Returns:
        List of eligible UTXOs sorted by (confirmations, value) descending
    """
    min_value = int(cj_amount * min_percent / 100)

    eligible = [u for u in utxos if u.confirmations >= min_confirmations and u.value >= min_value]

    # Prefer older UTXOs with more value
    eligible.sort(key=lambda u: (u.confirmations, u.value), reverse=True)
    return eligible

select_podle_utxo(utxos: list[UTXOInfo], cj_amount: int, min_confirmations: int = 5, min_percent: int = 20) -> UTXOInfo | None

Select the best UTXO for PoDLE commitment.

Args: utxos: Available UTXOs cj_amount: CoinJoin amount min_confirmations: Minimum confirmations required min_percent: Minimum value as percentage of cj_amount

Returns: Best UTXO for PoDLE or None if no suitable UTXO

Source code in taker/src/taker/podle.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def select_podle_utxo(
    utxos: list[UTXOInfo],
    cj_amount: int,
    min_confirmations: int = 5,
    min_percent: int = 20,
) -> UTXOInfo | None:
    """
    Select the best UTXO for PoDLE commitment.

    Args:
        utxos: Available UTXOs
        cj_amount: CoinJoin amount
        min_confirmations: Minimum confirmations required
        min_percent: Minimum value as percentage of cj_amount

    Returns:
        Best UTXO for PoDLE or None if no suitable UTXO
    """
    eligible = get_eligible_podle_utxos(utxos, cj_amount, min_confirmations, min_percent)

    if not eligible:
        min_value = int(cj_amount * min_percent / 100)
        logger.warning(
            f"No suitable UTXOs for PoDLE: need {min_confirmations}+ confirmations "
            f"and value >= {min_value} sats ({min_percent}% of {cj_amount})"
        )
        return None

    selected = eligible[0]
    logger.info(
        f"Selected UTXO for PoDLE: {selected.txid}:{selected.vout} "
        f"(value={selected.value}, confs={selected.confirmations})"
    )

    return selected

serialize_revelation(commitment: PoDLECommitment) -> str

Serialize PoDLE revelation to wire format.

Format: utxo|P|P2|sig|e (pipe-separated strings)

Source code in jmcore/src/jmcore/podle.py
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
def serialize_revelation(commitment: PoDLECommitment) -> str:
    """
    Serialize PoDLE revelation to wire format.

    Format: utxo|P|P2|sig|e (pipe-separated strings)
    """
    return "|".join(
        [
            commitment.utxo,
            commitment.p.hex(),
            commitment.p2.hex(),
            commitment.sig.hex(),
            commitment.e.hex(),
        ]
    )