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

Functions
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
 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
@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
Functions
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
101
102
103
104
105
106
107
108
109
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
91
92
93
94
95
96
97
98
99
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
69
70
71
72
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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)")

    # Get private key as integer
    k = int.from_bytes(private_key_bytes, "big")
    if k == 0 or k >= SECP256K1_N:
        raise PoDLEError("Invalid private key value")

    # Calculate P = k*G (standard public key)
    p_point = scalar_mult_g(k)
    p_bytes = point_to_bytes(p_point)

    # Get NUMS point J
    j_point = get_nums_point(index)

    # Calculate P2 = k*J
    p2_point = point_mult(k, j_point)
    p2_bytes = point_to_bytes(p2_point)

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

    # Generate Schnorr-like proof
    # Choose random nonce k_proof
    k_proof = int.from_bytes(secrets.token_bytes(32), "big") % SECP256K1_N
    if k_proof == 0:
        k_proof = 1

    # Kg = k_proof * G
    kg_point = scalar_mult_g(k_proof)
    kg_bytes = point_to_bytes(kg_point)

    # Kj = k_proof * J
    kj_point = point_mult(k_proof, j_point)
    kj_bytes = point_to_bytes(kj_point)

    # Challenge e = H(Kg || Kj || P || P2)
    e_bytes = hashlib.sha256(kg_bytes + kj_bytes + p_bytes + p2_bytes).digest()
    e = int.from_bytes(e_bytes, "big") % SECP256K1_N

    # Response s = k_proof + e * k (mod n) - JAM compatible
    s = (k_proof + e * k) % SECP256K1_N
    s_bytes = s.to_bytes(32, "big")

    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: P|P2|sig|e|utxo (pipe-separated hex strings)

Source code in jmcore/src/jmcore/podle.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def serialize_revelation(commitment: PoDLECommitment) -> str:
    """
    Serialize PoDLE revelation to wire format.

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