|
@@ -6,20 +6,145 @@ use std::collections::HashSet;
|
|
use base64::{engine::general_purpose, Engine as _};
|
|
use base64::{engine::general_purpose, Engine as _};
|
|
use bitcoin::hashes::sha256::Hash as Sha256;
|
|
use bitcoin::hashes::sha256::Hash as Sha256;
|
|
use bitcoin::hashes::Hash;
|
|
use bitcoin::hashes::Hash;
|
|
|
|
+use itertools::Itertools;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
use super::nut01::Keys;
|
|
use super::nut01::Keys;
|
|
|
|
|
|
|
|
+#[derive(Debug, PartialEq, Eq)]
|
|
|
|
+pub enum Error {
|
|
|
|
+ Base64(base64::DecodeError),
|
|
|
|
+ Length,
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/// A keyset ID is an identifier for a specific keyset. It can be derived by
|
|
|
|
+/// anyone who knows the set of public keys of a mint. The keyset ID **CAN**
|
|
|
|
+/// be stored in a Cashu token such that the token can be used to identify
|
|
|
|
+/// which mint or keyset it was generated from.
|
|
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
+pub struct Id([u8; Id::BYTES]);
|
|
|
|
+
|
|
|
|
+impl Id {
|
|
|
|
+ const BYTES: usize = 9;
|
|
|
|
+ const STRLEN: usize = 12;
|
|
|
|
+
|
|
|
|
+ pub fn try_from_base64(b64: &str) -> Result<Self, Error> {
|
|
|
|
+ use base64::{
|
|
|
|
+ engine::general_purpose::{STANDARD, URL_SAFE},
|
|
|
|
+ Engine as _,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if b64.len() != Self::STRLEN {
|
|
|
|
+ return Err(Error::Length);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if let Ok(bytes) = URL_SAFE.decode(b64) {
|
|
|
|
+ if bytes.len() == Self::BYTES {
|
|
|
|
+ return Ok(Self(
|
|
|
|
+ <[u8; Self::BYTES]>::try_from(bytes.as_slice()).unwrap(),
|
|
|
|
+ ));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ match STANDARD.decode(b64) {
|
|
|
|
+ Ok(bytes) if bytes.len() == Self::BYTES => Ok(Self(
|
|
|
|
+ <[u8; Self::BYTES]>::try_from(bytes.as_slice()).unwrap(),
|
|
|
|
+ )),
|
|
|
|
+ Ok(_) => Err(Error::Length),
|
|
|
|
+ Err(e) => Err(Error::Base64(e)),
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl std::fmt::Display for Id {
|
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
+ let mut output = String::with_capacity(Self::STRLEN);
|
|
|
|
+ general_purpose::STANDARD.encode_string(&self.0.as_slice(), &mut output);
|
|
|
|
+ f.write_str(&output)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl std::convert::TryFrom<String> for Id {
|
|
|
|
+ type Error = Error;
|
|
|
|
+ fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
|
|
+ Id::try_from_base64(&value)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl serde::ser::Serialize for Id {
|
|
|
|
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
|
+ where
|
|
|
|
+ S: serde::Serializer,
|
|
|
|
+ {
|
|
|
|
+ serializer.serialize_str(&self.to_string())
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl<'de> serde::de::Deserialize<'de> for Id {
|
|
|
|
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
+ where
|
|
|
|
+ D: serde::Deserializer<'de>,
|
|
|
|
+ {
|
|
|
|
+ struct IdVisitor;
|
|
|
|
+
|
|
|
|
+ impl<'de> serde::de::Visitor<'de> for IdVisitor {
|
|
|
|
+ type Value = Id;
|
|
|
|
+
|
|
|
|
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
+ formatter.write_str("a 12-character Base64 string")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
|
|
+ where
|
|
|
|
+ E: serde::de::Error,
|
|
|
|
+ {
|
|
|
|
+ Id::try_from_base64(v).map_err(|e| match e {
|
|
|
|
+ Error::Length => E::custom(format!(
|
|
|
|
+ "Invalid Length: Expected {}, got {}",
|
|
|
|
+ Id::STRLEN,
|
|
|
|
+ v.len()
|
|
|
|
+ )),
|
|
|
|
+ Error::Base64(e) => E::custom(e),
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ deserializer.deserialize_str(IdVisitor)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl From<&Keys> for Id {
|
|
|
|
+ fn from(map: &Keys) -> Self {
|
|
|
|
+ /* NUT-02 § 2.2.2
|
|
|
|
+ 1 - sort keyset by amount
|
|
|
|
+ 2 - concatenate all (sorted) public keys to one string
|
|
|
|
+ 3 - HASH_SHA256 the concatenated public keys
|
|
|
|
+ 4 - take the first 12 characters of the base64-encoded hash
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ let pubkeys_concat = map
|
|
|
|
+ .iter()
|
|
|
|
+ .sorted_by(|(amt_a, _), (amt_b, _)| amt_a.cmp(amt_b))
|
|
|
|
+ .map(|(_, pubkey)| pubkey)
|
|
|
|
+ .join("");
|
|
|
|
+
|
|
|
|
+ let hash = Sha256::hash(pubkeys_concat.as_bytes());
|
|
|
|
+ let bytes = hash.to_byte_array();
|
|
|
|
+ // First 9 bytes of hash will encode as the first 12 Base64 characters later
|
|
|
|
+ Self(<[u8; Self::BYTES]>::try_from(&bytes[0..Self::BYTES]).unwrap())
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
/// Mint Keysets [NUT-02]
|
|
/// Mint Keysets [NUT-02]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Response {
|
|
pub struct Response {
|
|
/// set of public keys that the mint generates
|
|
/// set of public keys that the mint generates
|
|
- pub keysets: HashSet<String>,
|
|
|
|
|
|
+ pub keysets: HashSet<Id>,
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
|
pub struct KeySet {
|
|
pub struct KeySet {
|
|
- pub id: String,
|
|
|
|
|
|
+ pub id: Id,
|
|
pub keys: Keys,
|
|
pub keys: Keys,
|
|
}
|
|
}
|
|
|
|
|
|
@@ -32,43 +157,23 @@ impl From<mint::KeySet> for KeySet {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-impl Keys {
|
|
|
|
- pub fn id(&self) -> String {
|
|
|
|
- /* 1 - sort keyset by amount
|
|
|
|
- * 2 - concatenate all (sorted) public keys to one string
|
|
|
|
- * 3 - HASH_SHA256 the concatenated public keys
|
|
|
|
- * 4 - take the first 12 characters of the hash
|
|
|
|
- */
|
|
|
|
-
|
|
|
|
- let pubkeys_concat = self
|
|
|
|
- .keys()
|
|
|
|
- .values()
|
|
|
|
- .map(|pubkey| hex::encode(k256::PublicKey::from(pubkey).to_sec1_bytes()))
|
|
|
|
- .collect::<Vec<String>>()
|
|
|
|
- .join("");
|
|
|
|
-
|
|
|
|
- let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
|
|
|
|
-
|
|
|
|
- hash[0..12].to_string()
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
pub mod mint {
|
|
pub mod mint {
|
|
use std::collections::BTreeMap;
|
|
use std::collections::BTreeMap;
|
|
|
|
|
|
- use base64::{engine::general_purpose, Engine as _};
|
|
|
|
use bitcoin::hashes::sha256::Hash as Sha256;
|
|
use bitcoin::hashes::sha256::Hash as Sha256;
|
|
use bitcoin_hashes::Hash;
|
|
use bitcoin_hashes::Hash;
|
|
use bitcoin_hashes::HashEngine;
|
|
use bitcoin_hashes::HashEngine;
|
|
|
|
+ use itertools::Itertools;
|
|
use k256::SecretKey;
|
|
use k256::SecretKey;
|
|
- use serde::Deserialize;
|
|
|
|
use serde::Serialize;
|
|
use serde::Serialize;
|
|
|
|
|
|
|
|
+ use super::Id;
|
|
|
|
+
|
|
use crate::nuts::nut01::mint::{KeyPair, Keys};
|
|
use crate::nuts::nut01::mint::{KeyPair, Keys};
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
pub struct KeySet {
|
|
pub struct KeySet {
|
|
- pub id: String,
|
|
|
|
|
|
+ pub id: Id,
|
|
pub keys: Keys,
|
|
pub keys: Keys,
|
|
}
|
|
}
|
|
|
|
|
|
@@ -104,30 +209,44 @@ pub mod mint {
|
|
map.insert(amount, keypair);
|
|
map.insert(amount, keypair);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ let keys = Keys(map);
|
|
|
|
+
|
|
Self {
|
|
Self {
|
|
- id: Self::id(&map),
|
|
|
|
- keys: Keys(map),
|
|
|
|
|
|
+ id: (&keys).into(),
|
|
|
|
+ keys,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ }
|
|
|
|
|
|
- fn id(map: &BTreeMap<u64, KeyPair>) -> String {
|
|
|
|
- /* 1 - sort keyset by amount
|
|
|
|
- * 2 - concatenate all (sorted) public keys to one string
|
|
|
|
- * 3 - HASH_SHA256 the concatenated public keys
|
|
|
|
- * 4 - take the first 12 characters of the hash
|
|
|
|
- */
|
|
|
|
-
|
|
|
|
- let pubkeys_concat = map
|
|
|
|
- .values()
|
|
|
|
- .map(|keypair| {
|
|
|
|
- hex::encode(k256::PublicKey::from(&keypair.public_key).to_sec1_bytes())
|
|
|
|
- })
|
|
|
|
- .collect::<Vec<String>>()
|
|
|
|
- .join("");
|
|
|
|
|
|
+ impl From<KeySet> for Id {
|
|
|
|
+ fn from(keyset: KeySet) -> Id {
|
|
|
|
+ let keys: super::KeySet = keyset.into();
|
|
|
|
|
|
- let hash = general_purpose::STANDARD.encode(Sha256::hash(pubkeys_concat.as_bytes()));
|
|
|
|
|
|
+ Id::from(&keys.keys)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
- hash[0..12].to_string()
|
|
|
|
|
|
+ impl From<&Keys> for Id {
|
|
|
|
+ fn from(map: &Keys) -> Self {
|
|
|
|
+ /* NUT-02 § 2.2.2
|
|
|
|
+ 1 - sort keyset by amount
|
|
|
|
+ 2 - concatenate all (sorted) public keys to one string
|
|
|
|
+ 3 - HASH_SHA256 the concatenated public keys
|
|
|
|
+ 4 - take the first 12 characters of the base64-encoded hash
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ let keys: super::Keys = map.clone().into();
|
|
|
|
+
|
|
|
|
+ let pubkeys_concat = keys
|
|
|
|
+ .iter()
|
|
|
|
+ .sorted_by(|(amt_a, _), (amt_b, _)| amt_a.cmp(amt_b))
|
|
|
|
+ .map(|(_, pubkey)| pubkey)
|
|
|
|
+ .join("");
|
|
|
|
+
|
|
|
|
+ let hash = Sha256::hash(pubkeys_concat.as_bytes());
|
|
|
|
+ let bytes = hash.to_byte_array();
|
|
|
|
+ // First 9 bytes of hash will encode as the first 12 Base64 characters later
|
|
|
|
+ Self(<[u8; Self::BYTES]>::try_from(&bytes[0..Self::BYTES]).unwrap())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -135,6 +254,8 @@ pub mod mint {
|
|
#[cfg(test)]
|
|
#[cfg(test)]
|
|
mod test {
|
|
mod test {
|
|
|
|
|
|
|
|
+ use crate::nuts::nut02::Id;
|
|
|
|
+
|
|
use super::Keys;
|
|
use super::Keys;
|
|
|
|
|
|
const SHORT_KEYSET_ID: &str = "esom3oyNLLit";
|
|
const SHORT_KEYSET_ID: &str = "esom3oyNLLit";
|
|
@@ -221,14 +342,14 @@ mod test {
|
|
fn deserialization_and_id_generation() {
|
|
fn deserialization_and_id_generation() {
|
|
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
|
|
let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
|
|
|
|
|
|
- let id = keys.id();
|
|
|
|
|
|
+ let id: Id = (&keys).into();
|
|
|
|
|
|
- assert_eq!(id, SHORT_KEYSET_ID);
|
|
|
|
|
|
+ assert_eq!(id, Id::try_from_base64(SHORT_KEYSET_ID).unwrap());
|
|
|
|
|
|
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
|
let keys: Keys = serde_json::from_str(KEYSET).unwrap();
|
|
|
|
|
|
- let id = keys.id();
|
|
|
|
|
|
+ let id: Id = (&keys).into();
|
|
|
|
|
|
- assert_eq!(id, KEYSET_ID);
|
|
|
|
|
|
+ assert_eq!(id, Id::try_from_base64(KEYSET_ID).unwrap());
|
|
}
|
|
}
|
|
}
|
|
}
|