From a050ee8a567ed273f63babf2985cf6e90b6d4a3d Mon Sep 17 00:00:00 2001 From: David Caseria Date: Mon, 12 Aug 2024 14:28:23 -0400 Subject: [PATCH 1/2] Add TransactionId for set of proofs --- crates/cdk/src/nuts/nut00/mod.rs | 56 ++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 4aae0fab..159a4f9b 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -8,6 +8,7 @@ use std::hash::{Hash, Hasher}; use std::str::FromStr; use std::string::FromUtf8Error; +use bitcoin::hashes::{sha256, Hash as _, HashEngine as _}; use serde::{Deserialize, Deserializer, Serialize}; use thiserror::Error; @@ -21,6 +22,7 @@ use crate::nuts::nut12::BlindSignatureDleq; use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness}; use crate::nuts::{Id, ProofDleq}; use crate::secret::Secret; +use crate::util::hex; use crate::Amount; pub mod token; @@ -65,6 +67,9 @@ pub enum Error { /// NUT11 error #[error(transparent)] NUT11(#[from] crate::nuts::nut11::Error), + /// Hex error + #[error(transparent)] + HexError(#[from] hex::Error), } /// Blinded Message (also called `output`) @@ -675,10 +680,51 @@ impl PartialOrd for PreMintSecrets { } } +/// Transaction ID +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct TransactionId([u8; 32]); + +impl TransactionId { + /// Create new [`TransactionId`] + pub fn new(proofs: Proofs) -> Result { + let mut ys = proofs + .iter() + .map(|proof| proof.y()) + .collect::, Error>>()?; + ys.sort(); + let mut hasher = sha256::Hash::engine(); + for y in ys { + hasher.input(&y.to_bytes()); + } + let hash = sha256::Hash::from_engine(hasher); + Ok(Self(hash.to_byte_array())) + } + + /// Get inner value + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for TransactionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl FromStr for TransactionId { + type Err = Error; + fn from_str(value: &str) -> Result { + let bytes = hex::decode(value)?; + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(Self(array)) + } +} + #[cfg(test)] mod tests { - use std::str::FromStr; - use super::*; #[test] @@ -692,6 +738,12 @@ mod tests { ); assert_eq!(proof.len(), 2); + + let transaction_id = TransactionId::new(proof).unwrap(); + assert_eq!( + transaction_id.to_string(), + "dac0748828d855ac4bc0e0a008cbc4b02e7d4238af06d730461cc559a5ae24b1" + ); } #[test] From bd4fdd036caea03e7190a64be9d68e088d317cf2 Mon Sep 17 00:00:00 2001 From: David Caseria Date: Mon, 12 Aug 2024 14:30:03 -0400 Subject: [PATCH 2/2] Better test --- crates/cdk/src/nuts/nut00/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 159a4f9b..9c7fd095 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -738,12 +738,22 @@ mod tests { ); assert_eq!(proof.len(), 2); + } - let transaction_id = TransactionId::new(proof).unwrap(); + #[test] + fn test_transaction_id() { + let proof = "[{\"id\":\"009a1f293253e41e\",\"amount\":2,\"secret\":\"407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837\",\"C\":\"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea\"},{\"id\":\"009a1f293253e41e\",\"amount\":8,\"secret\":\"fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be\",\"C\":\"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059\"}]"; + let mut proofs: Proofs = serde_json::from_str(proof).unwrap(); + + let transaction_id = TransactionId::new(proofs.clone()).unwrap(); assert_eq!( transaction_id.to_string(), "dac0748828d855ac4bc0e0a008cbc4b02e7d4238af06d730461cc559a5ae24b1" ); + + proofs.reverse(); + let rev_transaction_id = TransactionId::new(proofs).unwrap(); + assert_eq!(transaction_id, rev_transaction_id); } #[test]