Skip to content

Commit bfc33f0

Browse files
fix: add new crypto rust library methods
1 parent 09b864b commit bfc33f0

File tree

3 files changed

+123
-0
lines changed

3 files changed

+123
-0
lines changed

hathor-ct-crypto/src/ecdh.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! ECDH key exchange and nonce derivation for shielded output recovery.
2+
3+
use secp256k1_zkp::{PublicKey, SecretKey, SECP256K1};
4+
use sha2::{Digest, Sha256};
5+
6+
use crate::error::HathorCtError;
7+
8+
/// Domain separator for nonce derivation, preventing cross-protocol reuse.
9+
const NONCE_DOMAIN_SEPARATOR: &[u8] = b"Hathor_CT_nonce_v1";
10+
11+
/// Derive ECDH shared secret: `SHA256(privkey * pubkey)`.
12+
pub fn derive_ecdh_shared_secret(
13+
privkey: &[u8; 32],
14+
pubkey: &[u8; 33],
15+
) -> Result<[u8; 32], HathorCtError> {
16+
let sk = SecretKey::from_slice(privkey)
17+
.map_err(|e| HathorCtError::Secp256k1Error(e.to_string()))?;
18+
let pk = PublicKey::from_slice(pubkey)
19+
.map_err(|e| HathorCtError::Secp256k1Error(e.to_string()))?;
20+
21+
let shared_point = pk
22+
.mul_tweak(SECP256K1, &sk.into())
23+
.map_err(|e| HathorCtError::Secp256k1Error(e.to_string()))?;
24+
25+
let mut hasher = Sha256::new();
26+
hasher.update(shared_point.serialize());
27+
let result: [u8; 32] = hasher.finalize().into();
28+
Ok(result)
29+
}
30+
31+
/// Derive the rewind nonce from a shared secret.
32+
pub fn derive_rewind_nonce(shared_secret: &[u8; 32]) -> [u8; 32] {
33+
let mut hasher = Sha256::new();
34+
hasher.update(NONCE_DOMAIN_SEPARATOR);
35+
hasher.update(shared_secret);
36+
hasher.finalize().into()
37+
}

hathor-ct-crypto/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod balance;
2+
pub mod ecdh;
23
pub mod error;
34
pub mod generators;
45
pub mod pedersen;

hathor-ct-crypto/src/napi_bindings.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,88 @@ pub fn get_generator_size() -> u32 {
400400
pub fn get_zero_tweak() -> Buffer {
401401
Buffer::from(ZERO_TWEAK.as_ref().to_vec())
402402
}
403+
404+
#[napi(object)]
405+
pub struct CreatedShieldedOutput {
406+
pub ephemeral_pubkey: Buffer,
407+
pub commitment: Buffer,
408+
pub range_proof: Buffer,
409+
pub blinding_factor: Buffer,
410+
pub asset_commitment: Option<Buffer>,
411+
pub asset_blinding_factor: Option<Buffer>,
412+
}
413+
414+
/// Create a FullShielded output with both value blinding factor and asset blinding factor
415+
/// provided externally. This is needed for the last output in a FullShielded transaction
416+
/// where the balance equation requires pre-computing the vbf using a known abf.
417+
#[napi]
418+
pub fn create_shielded_output_with_both_blindings(
419+
value: i64,
420+
recipient_pubkey: Buffer,
421+
token_uid: Buffer,
422+
value_blinding_factor: Buffer,
423+
asset_blinding_factor: Buffer,
424+
) -> napi::Result<CreatedShieldedOutput> {
425+
use secp256k1_zkp::SECP256K1 as SECP;
426+
427+
if value < 0 {
428+
return Err(napi::Error::from_reason("value must be non-negative"));
429+
}
430+
431+
let pubkey: [u8; 33] = recipient_pubkey
432+
.as_ref()
433+
.try_into()
434+
.map_err(|_| napi::Error::from_reason("recipient_pubkey must be 33 bytes"))?;
435+
let tuid: [u8; 32] = token_uid
436+
.as_ref()
437+
.try_into()
438+
.map_err(|_| napi::Error::from_reason("token_uid must be 32 bytes"))?;
439+
let vbf: [u8; 32] = value_blinding_factor
440+
.as_ref()
441+
.try_into()
442+
.map_err(|_| napi::Error::from_reason("value_blinding_factor must be 32 bytes"))?;
443+
let abf: [u8; 32] = asset_blinding_factor
444+
.as_ref()
445+
.try_into()
446+
.map_err(|_| napi::Error::from_reason("asset_blinding_factor must be 32 bytes"))?;
447+
448+
// 1. Generate ephemeral keypair
449+
let (eph_sk, eph_pk) = SECP.generate_keypair(&mut rand::thread_rng());
450+
451+
// 2. ECDH shared secret
452+
let shared_secret = crate::ecdh::derive_ecdh_shared_secret(
453+
&eph_sk.secret_bytes(),
454+
&pubkey,
455+
)
456+
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
457+
458+
// 3. Derive rewind nonce
459+
let nonce = crate::ecdh::derive_rewind_nonce(&shared_secret);
460+
461+
// 4. Create blinded asset commitment using provided abf
462+
let tag = crate::generators::derive_tag(&tuid).map_err(to_napi_err)?;
463+
let abf_tweak = parse_tweak(&abf)?;
464+
let asset_comm = crate::generators::create_asset_commitment(&tag, &abf_tweak)
465+
.map_err(to_napi_err)?;
466+
let ac_bytes = asset_comm.serialize();
467+
468+
// 5. Create commitment and range proof with provided vbf
469+
let nonce_sk = secp256k1_zkp::SecretKey::from_slice(&nonce)
470+
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
471+
let bf_tweak = parse_tweak(&vbf)?;
472+
let comm = crate::pedersen::create_commitment(value as u64, &bf_tweak, &asset_comm)
473+
.map_err(to_napi_err)?;
474+
let proof = crate::rangeproof::create_range_proof(
475+
value as u64, &bf_tweak, &comm, &asset_comm, None, Some(&nonce_sk),
476+
)
477+
.map_err(to_napi_err)?;
478+
479+
Ok(CreatedShieldedOutput {
480+
ephemeral_pubkey: Buffer::from(eph_pk.serialize().to_vec()),
481+
commitment: Buffer::from(comm.serialize().to_vec()),
482+
range_proof: Buffer::from(proof.serialize()),
483+
blinding_factor: Buffer::from(vbf.to_vec()),
484+
asset_commitment: Some(Buffer::from(ac_bytes.to_vec())),
485+
asset_blinding_factor: Some(Buffer::from(abf.to_vec())),
486+
})
487+
}

0 commit comments

Comments
 (0)