Skip to content

Commit 048325e

Browse files
committed
update bindings and __init__ definitions
1 parent 5173b91 commit 048325e

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

bittensor_drand/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
decrypt_with_signature as _decrypt_with_signature,
1010
get_signature_for_round as _get_signature_for_round,
1111
get_latest_round as _get_latest_round,
12+
encrypt_mlkem768 as _encrypt_mlkem768,
13+
mlkem_kdf_id as _mlkem_kdf_id,
1214
)
1315

1416

@@ -175,3 +177,49 @@ def get_latest_round() -> int:
175177
ValueError: If fetching the latest round fails.
176178
"""
177179
return _get_latest_round()
180+
181+
182+
def encrypt_mlkem768(pk_bytes: bytes, plaintext: bytes) -> bytes:
183+
"""Encrypts data using ML-KEM-768 + XChaCha20Poly1305.
184+
185+
This function encrypts plaintext using ML-KEM-768 key encapsulation followed by
186+
XChaCha20Poly1305 authenticated encryption. The public key is rotated every block
187+
and can be queried from the NextKey storage item.
188+
189+
Blob format: [u16 kem_len LE][kem_ct][nonce24][aead_ct]
190+
191+
Arguments:
192+
pk_bytes: ML-KEM-768 public key bytes (from NextKey storage)
193+
plaintext: Data to encrypt
194+
195+
Returns:
196+
bytes: Encrypted blob
197+
198+
Raises:
199+
ValueError: If encryption fails
200+
201+
Example:
202+
```python
203+
from bittensor_drand import encrypt_mlkem768
204+
205+
# Get public key from NextKey storage
206+
pk_bytes = get_next_key_from_storage(substrate, mev_pallet)
207+
208+
# Encrypt payload
209+
payload = signer + nonce.to_bytes(4, "little") + scale_call
210+
plaintext = payload + b"\\x01" + cold.sign(b"mev-shield:v1" + genesis + payload)
211+
ciphertext = encrypt_mlkem768(pk_bytes, plaintext)
212+
```
213+
"""
214+
return _encrypt_mlkem768(pk_bytes, plaintext)
215+
216+
217+
def mlkem_kdf_id() -> bytes:
218+
"""Returns the KDF identifier used by ML-KEM encryption.
219+
220+
Returns "v1" indicating direct use of shared secret (no HKDF).
221+
222+
Returns:
223+
bytes: KDF identifier (b"v1")
224+
"""
225+
return _mlkem_kdf_id()

src/python_bindings.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,80 @@ fn get_signature_for_round(reveal_round: u64) -> PyResult<String> {
281281
.ok_or_else(|| PyValueError::new_err("Signature not available"))
282282
}
283283

284+
/// Encrypts data using ML-KEM-768 + XChaCha20Poly1305
285+
///
286+
/// This function encrypts plaintext using ML-KEM-768 key encapsulation followed by
287+
/// XChaCha20Poly1305 authenticated encryption. The public key is rotated every block
288+
/// and can be queried from the NextKey storage item.
289+
///
290+
/// Blob format: [u16 kem_len LE][kem_ct][nonce24][aead_ct]
291+
///
292+
/// Args:
293+
/// pk_bytes (bytes): ML-KEM-768 public key bytes (from NextKey storage)
294+
/// plaintext (bytes): Data to encrypt
295+
///
296+
/// Returns:
297+
/// bytes: Encrypted blob
298+
///
299+
/// Raises:
300+
/// ValueError: If encryption fails
301+
#[pyfunction]
302+
fn encrypt_mlkem768(
303+
py: Python,
304+
pk_bytes: &[u8],
305+
plaintext: &[u8],
306+
) -> PyResult<Py<PyBytes>> {
307+
// Estimate max output size: kem_ct (~1500 bytes) + nonce (24) + aead_ct (plaintext + overhead)
308+
let max_output_size = 2048 + plaintext.len() + 64; // Safe estimate
309+
let mut output = vec![0u8; max_output_size];
310+
let mut written = 0usize;
311+
312+
let result = crate::ffi::mlkem768_seal_blob(
313+
pk_bytes.as_ptr(),
314+
pk_bytes.len(),
315+
plaintext.as_ptr(),
316+
plaintext.len(),
317+
output.as_mut_ptr(),
318+
output.len(),
319+
&mut written,
320+
);
321+
322+
match result {
323+
0 => {
324+
output.truncate(written);
325+
Ok(PyBytes::new(py, &output).into())
326+
}
327+
-1 => Err(PyValueError::new_err("Null pointer provided")),
328+
-2 => Err(PyValueError::new_err("Failed to decode public key")),
329+
-3 => Err(PyValueError::new_err("Encapsulation failed")),
330+
-4 => Err(PyValueError::new_err("KEM ciphertext too long")),
331+
-5 => Err(PyValueError::new_err("Invalid shared secret length")),
332+
-6 => Err(PyValueError::new_err("AEAD encryption failed")),
333+
-7 => Err(PyValueError::new_err("Output buffer too small")),
334+
code => Err(PyValueError::new_err(format!("Unknown error code: {}", code))),
335+
}
336+
}
337+
338+
/// Returns the KDF identifier used by ML-KEM encryption
339+
///
340+
/// Returns "v1" indicating direct use of shared secret (no HKDF)
341+
///
342+
/// Returns:
343+
/// bytes: KDF identifier (b"v1")
344+
#[pyfunction]
345+
fn mlkem_kdf_id(py: Python) -> PyResult<Py<PyBytes>> {
346+
let mut buf = vec![0u8; 10];
347+
let result = crate::ffi::mlkemffi_kdf_id(buf.as_mut_ptr(), buf.len());
348+
349+
match result {
350+
n if n > 0 => {
351+
buf.truncate(n as usize);
352+
Ok(PyBytes::new(py, &buf).into())
353+
}
354+
_ => Err(PyValueError::new_err("Failed to get KDF ID")),
355+
}
356+
}
357+
284358
#[pymodule]
285359
fn bittensor_drand(m: &Bound<'_, PyModule>) -> PyResult<()> {
286360
m.add_function(wrap_pyfunction!(get_encrypted_commit, m)?)?;
@@ -291,5 +365,8 @@ fn bittensor_drand(m: &Bound<'_, PyModule>) -> PyResult<()> {
291365
m.add_function(wrap_pyfunction!(decrypt_with_signature, m)?)?;
292366
m.add_function(wrap_pyfunction!(get_signature_for_round, m)?)?;
293367
m.add_function(wrap_pyfunction!(get_latest_round_py, m)?)?;
368+
// ML-KEM functions
369+
m.add_function(wrap_pyfunction!(encrypt_mlkem768, m)?)?;
370+
m.add_function(wrap_pyfunction!(mlkem_kdf_id, m)?)?;
294371
Ok(())
295372
}

0 commit comments

Comments
 (0)