-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d9eee3a
commit 697d61f
Showing
15 changed files
with
991 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// Copyright 2025 The Tari Project | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
|
||
//! A commitment is like a sealed envelope. You put some information inside the envelope, and then seal (commit) it. | ||
//! You can't change what you've said, but also, no-one knows what you've said until you're ready to open (open) the | ||
//! envelope and reveal its contents. Also it's a special envelope that can only be opened by a special opener that | ||
//! you keep safe in your drawer. | ||
use core::{ | ||
cmp::Ordering, | ||
hash::{Hash, Hasher}, | ||
}; | ||
|
||
use tari_utilities::{ByteArray, ByteArrayError}; | ||
|
||
use crate::{commitment::HomomorphicCommitment, compressed_key::CompressedKey, keys::PublicKey}; | ||
|
||
/// There are also different types of commitments that vary in their security guarantees, but all of them are | ||
/// represented by binary data; so [HomomorphicCommitment](trait.HomomorphicCommitment.html) implements | ||
/// [ByteArray](trait.ByteArray.html). | ||
/// | ||
/// The Homomorphic part means, more or less, that commitments follow some of the standard rules of | ||
/// arithmetic. Adding two commitments is the same as committing to the sum of their parts: | ||
/// $$ \begin{aligned} | ||
/// C_1 &= v_1.H + k_1.G \\\\ | ||
/// C_2 &= v_2.H + k_2.G \\\\ | ||
/// \therefore C_1 + C_2 &= (v_1 + v_2)H + (k_1 + k_2)G | ||
/// \end{aligned} $$ | ||
#[derive(Debug, Clone)] | ||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | ||
pub struct CompressedCommitment<P>(pub(crate) CompressedKey<P>); | ||
|
||
impl<P: Default + ByteArray + PublicKey> Default for CompressedCommitment<P> { | ||
fn default() -> Self { | ||
Self(CompressedKey::default()) | ||
} | ||
} | ||
|
||
#[cfg(feature = "borsh")] | ||
impl<P: borsh::BorshDeserialize> borsh::BorshDeserialize for CompressedCommitment<P> { | ||
fn deserialize_reader<R>(reader: &mut R) -> Result<Self, borsh::io::Error> | ||
where R: borsh::io::Read { | ||
Ok(Self(CompressedKey::deserialize_reader(reader)?)) | ||
} | ||
} | ||
|
||
#[cfg(feature = "borsh")] | ||
impl<P: borsh::BorshSerialize> borsh::BorshSerialize for CompressedCommitment<P> { | ||
fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> { | ||
self.0.serialize(writer) | ||
} | ||
} | ||
|
||
impl<P> CompressedCommitment<P> | ||
where P: PublicKey | ||
{ | ||
/// Get this commitment as a public key point | ||
pub fn to_public_key(&self) -> Result<P, ByteArrayError> { | ||
self.0.to_public_key() | ||
} | ||
|
||
/// Get this compressed commitment as a homomorphic commitment | ||
pub fn to_commitment(&self) -> Result<HomomorphicCommitment<P>, ByteArrayError> { | ||
Ok(HomomorphicCommitment(self.to_public_key()?)) | ||
} | ||
|
||
/// Get this compressed commitment as a compressed key | ||
pub fn to_compressed_key(&self) -> CompressedKey<P> { | ||
self.0.clone() | ||
} | ||
|
||
/// Converts a public key into a commitment | ||
pub fn from_public_key(p: P) -> CompressedCommitment<P> { | ||
let compressed_key = CompressedKey::new_from_pk(p); | ||
CompressedCommitment(compressed_key) | ||
} | ||
|
||
/// Converts a compressed key into a commitment | ||
pub fn from_compressed_key(compressed_key: CompressedKey<P>) -> Self { | ||
Self(compressed_key) | ||
} | ||
|
||
/// Converts a commitment into a compressed commitment | ||
pub fn from_commitment(commitment: HomomorphicCommitment<P>) -> Self { | ||
Self(CompressedKey::new_from_pk(commitment.0)) | ||
} | ||
} | ||
|
||
impl<P> ByteArray for CompressedCommitment<P> | ||
where P: PublicKey | ||
{ | ||
fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ByteArrayError> { | ||
let key = CompressedKey::from_canonical_bytes(bytes)?; | ||
Ok(Self(key)) | ||
} | ||
|
||
fn as_bytes(&self) -> &[u8] { | ||
self.0.as_bytes() | ||
} | ||
} | ||
|
||
impl<P> PartialOrd for CompressedCommitment<P> | ||
where P: PublicKey | ||
{ | ||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
impl<P> Ord for CompressedCommitment<P> | ||
where P: PublicKey | ||
{ | ||
fn cmp(&self, other: &Self) -> Ordering { | ||
self.0.cmp(&other.0) | ||
} | ||
} | ||
|
||
impl<P: PublicKey> Hash for CompressedCommitment<P> { | ||
fn hash<H: Hasher>(&self, state: &mut H) { | ||
state.write(self.as_bytes()) | ||
} | ||
} | ||
|
||
impl<P: PublicKey> PartialEq for CompressedCommitment<P> { | ||
fn eq(&self, other: &Self) -> bool { | ||
self.0 == other.0 | ||
} | ||
} | ||
|
||
impl<P: PublicKey> Eq for CompressedCommitment<P> {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
// Copyright 2025. The Tari Project | ||
// SPDX-License-Identifier: BSD-3-Clause | ||
|
||
//! This stores a public key in compressed form, keeping it in compressed form until the point is needed, only then | ||
//! decompressing it back down to a public key | ||
use alloc::vec::Vec; | ||
use core::fmt; | ||
use std::{ | ||
cmp::Ordering, | ||
hash::{Hash, Hasher}, | ||
marker::PhantomData, | ||
prelude::rust_2015::{String, ToString}, | ||
sync::OnceLock, | ||
}; | ||
|
||
use blake2::Blake2b; | ||
use digest::{consts::U64, Digest}; | ||
use rand_core::{CryptoRng, RngCore}; | ||
#[cfg(feature = "serde")] | ||
use serde::de::Visitor; | ||
#[cfg(feature = "serde")] | ||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; | ||
use subtle::ConstantTimeEq; | ||
use tari_utilities::{hex::Hex, ByteArray, ByteArrayError, Hashable}; | ||
use zeroize::Zeroize; | ||
|
||
use crate::keys::{PublicKey, SecretKey}; | ||
|
||
/// This stores a public key in compressed form, keeping it in compressed form until the point is needed, only then | ||
/// decompressing it back down to a public key | ||
#[derive(Clone)] | ||
pub struct CompressedKey<T> { | ||
key: Vec<u8>, | ||
public_key: OnceLock<T>, | ||
} | ||
|
||
impl<T: PublicKey> CompressedKey<T> { | ||
/// Create a new compressed key from a public key | ||
pub fn new_from_pk(pk: T) -> Self { | ||
Self { | ||
key: pk.as_bytes().to_vec(), | ||
public_key: pk.into(), | ||
} | ||
} | ||
|
||
/// Return the public key | ||
pub fn to_public_key(&self) -> Result<T, ByteArrayError> { | ||
match self.public_key.get() { | ||
Some(pk) => Ok(pk.clone()), | ||
None => { | ||
let pk = T::from_canonical_bytes(&self.key)?; | ||
let _unused = self.public_key.set(pk.clone()); | ||
Ok(pk) | ||
}, | ||
} | ||
} | ||
|
||
/// Create a new compressed key from a secret key | ||
pub fn from_secret_key(sk: &T::K) -> Self { | ||
let pk = T::from_secret_key(sk); | ||
Self { | ||
key: pk.as_bytes().to_vec(), | ||
public_key: pk.into(), | ||
} | ||
} | ||
|
||
/// Create a new cnew random compressed key and secret key | ||
pub fn random_keypair<R: RngCore + CryptoRng>(rng: &mut R) -> (T::K, Self) { | ||
let k = T::K::random(rng); | ||
let pk = Self::from_secret_key(&k); | ||
(k, pk) | ||
} | ||
|
||
/// returns the length of the key | ||
pub fn key_length() -> usize { | ||
T::KEY_LEN | ||
} | ||
} | ||
|
||
impl<T> CompressedKey<T> { | ||
/// Create a new compressed key | ||
pub fn new(key: &[u8]) -> CompressedKey<T> { | ||
Self { | ||
key: key.to_vec(), | ||
public_key: OnceLock::new(), | ||
} | ||
} | ||
|
||
// Formats a 64 char hex string to a given width. | ||
// If w >= 64, we pad the result. | ||
// If 7 <= w < 64, we replace the middle of the string with "..." | ||
// If w <= 6, we return the first w chars of the string | ||
fn fmt_case(&self, f: &mut fmt::Formatter, uppercase: bool) -> fmt::Result { | ||
let mut hex = self.to_hex(); | ||
if uppercase { | ||
hex = hex.to_uppercase(); | ||
} | ||
if f.alternate() { | ||
hex = format!("0x{hex}"); | ||
} | ||
match f.width() { | ||
None => f.write_str(hex.as_str()), | ||
Some(w @ 1..=6) => f.write_str(&hex[..w]), | ||
Some(w @ 7..=63) => { | ||
let left = (w - 3) / 2; | ||
let right = hex.len() - (w - left - 3); | ||
f.write_str(format!("{}...{}", &hex[..left], &hex[right..]).as_str()) | ||
}, | ||
_ => core::fmt::Display::fmt(&hex, f), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "borsh")] | ||
impl<T: borsh::BorshSerialize> borsh::BorshSerialize for CompressedKey<T> { | ||
fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> { | ||
borsh::BorshSerialize::serialize(&self.as_bytes(), writer) | ||
} | ||
} | ||
|
||
#[cfg(feature = "borsh")] | ||
impl<T: borsh::BorshDeserialize> borsh::BorshDeserialize for CompressedKey<T> { | ||
fn deserialize_reader<R>(reader: &mut R) -> Result<Self, borsh::io::Error> | ||
where R: borsh::io::Read { | ||
let bytes: Vec<u8> = borsh::BorshDeserialize::deserialize_reader(reader)?; | ||
Self::from_canonical_bytes(bytes.as_slice()) | ||
.map_err(|e| borsh::io::Error::new(borsh::io::ErrorKind::InvalidInput, e.to_string())) | ||
} | ||
} | ||
|
||
impl<T> Hashable for CompressedKey<T> { | ||
fn hash(&self) -> Vec<u8> { | ||
Blake2b::<U64>::digest(self.as_bytes()).to_vec() | ||
} | ||
} | ||
|
||
impl<T> Hash for CompressedKey<T> { | ||
/// Require the implementation of the Hash trait for Hashmaps | ||
fn hash<H: Hasher>(&self, state: &mut H) { | ||
self.as_bytes().hash(state); | ||
} | ||
} | ||
|
||
impl<T: Default + ByteArray + PublicKey> Default for CompressedKey<T> { | ||
fn default() -> Self { | ||
let key = T::default(); | ||
Self::new_from_pk(key) | ||
} | ||
} | ||
|
||
impl<T> fmt::Display for CompressedKey<T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
self.fmt_case(f, false) | ||
} | ||
} | ||
|
||
impl<T> ConstantTimeEq for CompressedKey<T> { | ||
fn ct_eq(&self, other: &Self) -> subtle::Choice { | ||
self.key.ct_eq(&other.key) | ||
} | ||
} | ||
|
||
impl<T> fmt::LowerHex for CompressedKey<T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
self.fmt_case(f, false) | ||
} | ||
} | ||
|
||
impl<T> fmt::UpperHex for CompressedKey<T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
self.fmt_case(f, true) | ||
} | ||
} | ||
|
||
impl<T> fmt::Debug for CompressedKey<T> { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
write!(f, "{}", self.to_hex()) | ||
} | ||
} | ||
|
||
impl<T> PartialEq for CompressedKey<T> { | ||
fn eq(&self, other: &CompressedKey<T>) -> bool { | ||
self.ct_eq(other).into() | ||
} | ||
} | ||
|
||
impl<T> Eq for CompressedKey<T> {} | ||
|
||
impl<T> PartialOrd for CompressedKey<T> { | ||
fn partial_cmp(&self, other: &CompressedKey<T>) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
impl<T> Ord for CompressedKey<T> { | ||
fn cmp(&self, other: &Self) -> Ordering { | ||
self.key.cmp(&other.key) | ||
} | ||
} | ||
|
||
impl<T> ByteArray for CompressedKey<T> { | ||
/// Create a new `RistrettoPublicKey` instance form the given byte array. The constructor returns errors under | ||
/// the following circumstances: | ||
/// * The byte array is not exactly 32 bytes | ||
/// * The byte array does not represent a valid (compressed) point on the ristretto255 curve | ||
fn from_canonical_bytes(bytes: &[u8]) -> Result<CompressedKey<T>, ByteArrayError> | ||
where Self: Sized { | ||
// Check the length here, because The Ristretto constructor panics rather than returning an error | ||
if bytes.len() != 32 { | ||
return Err(ByteArrayError::IncorrectLength {}); | ||
} | ||
Ok(Self::new(bytes)) | ||
} | ||
|
||
/// Return the little-endian byte array representation of the compressed public key | ||
fn as_bytes(&self) -> &[u8] { | ||
&self.key | ||
} | ||
} | ||
|
||
impl<T> Zeroize for CompressedKey<T> { | ||
/// Zeroizes both the point and (if it exists) the compressed point | ||
fn zeroize(&mut self) { | ||
self.key.zeroize(); | ||
} | ||
} | ||
|
||
#[cfg(feature = "serde")] | ||
impl<'de, T> Deserialize<'de> for CompressedKey<T> { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where D: Deserializer<'de> { | ||
struct CompressedKeyVisitor<T> { | ||
phantom: PhantomData<T>, | ||
} | ||
|
||
impl<T> Visitor<'_> for CompressedKeyVisitor<T> { | ||
type Value = CompressedKey<T>; | ||
|
||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { | ||
formatter.write_str("a public key in binary format") | ||
} | ||
|
||
fn visit_bytes<E>(self, v: &[u8]) -> Result<CompressedKey<T>, E> | ||
where E: de::Error { | ||
CompressedKey::from_canonical_bytes(v).map_err(E::custom) | ||
} | ||
} | ||
|
||
if deserializer.is_human_readable() { | ||
let s = String::deserialize(deserializer)?; | ||
CompressedKey::from_hex(&s).map_err(de::Error::custom) | ||
} else { | ||
deserializer.deserialize_bytes(CompressedKeyVisitor { phantom: PhantomData }) | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "serde")] | ||
impl<T> Serialize for CompressedKey<T> { | ||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||
where S: Serializer { | ||
if serializer.is_human_readable() { | ||
self.to_hex().serialize(serializer) | ||
} else { | ||
serializer.serialize_bytes(self.as_bytes()) | ||
} | ||
} | ||
} |
Oops, something went wrong.