Skip to content

Commit

Permalink
Hermes client
Browse files Browse the repository at this point in the history
  • Loading branch information
TonyGiorgio committed Mar 21, 2024
1 parent e921910 commit eb680b1
Show file tree
Hide file tree
Showing 6 changed files with 371 additions and 39 deletions.
38 changes: 28 additions & 10 deletions mutiny-core/src/blindauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ impl<S: MutinyStorage> BlindAuthClient<S> {

Ok(())
}

pub fn get_unblinded_info_from_token(
&self,
token: &SignedToken,
) -> (fedimint_mint_client::Nonce, BlindingKey) {
generate_nonce(&self.secret, token.service_id, token.plan_id, token.counter)
}
}

async fn get_available_tokens(
Expand Down Expand Up @@ -301,6 +308,25 @@ async fn derive_blind_token(
plan_id: u32,
counter: u32,
) -> Result<UnsignedToken, MutinyError> {
let (nonce, blinding_key) = generate_nonce(secret, service_id, plan_id, counter);
let blinded_message = blind_message(nonce.to_message(), blinding_key);

let unsigned_token = UnsignedToken {
counter,
service_id,
plan_id,
blinded_message,
};

Ok(unsigned_token)
}

fn generate_nonce(
secret: &DerivableSecret,
service_id: u32,
plan_id: u32,
counter: u32,
) -> (fedimint_mint_client::Nonce, BlindingKey) {
let child_secret = secret
.child_key(SERVICE_REGISTRATION_CHILD_ID)
.child_key(ChildId(service_id.into()))
Expand All @@ -312,21 +338,13 @@ async fn derive_blind_token(
.to_secp_key(fedimint_ln_common::bitcoin::secp256k1::SECP256K1);

let nonce = fedimint_mint_client::Nonce(spend_key.public_key());

let blinding_key = BlindingKey(
child_secret
.child_key(BLINDING_KEY_CHILD_ID)
.to_bls12_381_key(),
);
let blinded_message = blind_message(nonce.to_message(), blinding_key);

let signed_token = UnsignedToken {
counter,
service_id,
plan_id,
blinded_message,
};

Ok(signed_token)
(nonce, blinding_key)
}

// Creates the root derivation secret for the blind auth client:
Expand Down
3 changes: 3 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ pub enum MutinyError {
/// Token already spent.
#[error("Token has been already spent.")]
TokenAlreadySpent,
/// Federation required.
#[error("A federation is required")]
FederationRequired,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Expand Down
304 changes: 304 additions & 0 deletions mutiny-core/src/hermes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};

use async_lock::RwLock;
use bitcoin::{bip32::ExtendedPrivKey, secp256k1::Secp256k1};
use fedimint_core::config::FederationId;
use futures::{pin_mut, select, FutureExt};
use lightning::util::logger::Logger;
use lightning::{log_error, log_warn};
use nostr::{nips::nip04::decrypt, Keys};
use nostr::{Filter, Kind, Timestamp};
use nostr_sdk::{Client, NostrSigner, RelayPoolNotification};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tbs::unblind_signature;
use url::Url;

use crate::{
blindauth::{BlindAuthClient, SignedToken},
error::MutinyError,
federation::{FederationClient, FederationIdentity},
logging::MutinyLogger,
nostr::{derive_nostr_key, HERMES_CHAIN_INDEX, SERVICE_ACCOUNT_INDEX},
storage::MutinyStorage,
utils,
};

const HERMES_SERVICE_ID: u32 = 1;
const HERMES_FREE_PLAN_ID: u32 = 1;
const HERMES_PAID_PLAN_ID: u32 = 2;

#[derive(Deserialize, Serialize)]
pub struct RegisterRequest {
pub name: Option<String>,
pub pubkey: String,
pub federation_id: FederationId,
pub federation_invite_code: String,
pub msg: tbs::Message,
pub sig: tbs::Signature,
}

#[derive(Deserialize, Serialize)]
pub struct RegisterResponse {
pub name: String,
}

pub struct HermesClient<S: MutinyStorage> {
pub(crate) primary_key: Keys,
pub public_key: nostr::PublicKey,
pub client: Client,
http_client: reqwest::Client,
pub(crate) federations: Arc<RwLock<HashMap<FederationId, Arc<FederationClient<S>>>>>,
blind_auth: BlindAuthClient<S>,
base_url: String,
storage: S,
pub logger: Arc<MutinyLogger>,
pub stop: Arc<AtomicBool>,
}

impl<S: MutinyStorage> HermesClient<S> {
pub async fn new(
xprivkey: ExtendedPrivKey,
base_url: String,
federations: Arc<RwLock<HashMap<FederationId, Arc<FederationClient<S>>>>>,
blind_auth: BlindAuthClient<S>,
storage: &S,
logger: Arc<MutinyLogger>,
stop: Arc<AtomicBool>,
) -> Result<Self, MutinyError> {
let keys = derive_nostr_key(
&Secp256k1::new(),
xprivkey,
SERVICE_ACCOUNT_INDEX,
Some(HERMES_CHAIN_INDEX),
None,
)?;
let public_key = keys.public_key();
let client = Client::new(&keys);

let relays: Vec<String> = vec![
"wss://relay.primal.net".to_string(),
"wss://relay.damus.io".to_string(),
"wss://nostr.mutinywallet.com".to_string(),
"wss://relay.mutinywallet.com".to_string(),
];
client
.add_relays(relays)
.await
.expect("Failed to add relays");

// TODO need to store the fact that we have a LNURL or not...

Ok(Self {
primary_key: keys,
public_key,
client,
http_client: reqwest::Client::new(),
base_url,
federations,
blind_auth,
storage: storage.clone(),
logger,
stop,
})
}

pub fn start(&self) -> Result<(), MutinyError> {
let logger = self.logger.clone();
let stop = self.stop.clone();
let client = self.client.clone();
let public_key = self.public_key.clone();
let storage = self.storage.clone();
let primary_key = self.primary_key.clone();

// if we haven't synced before, use now and save to storage
// TODO FIXME this won't be very correct
// I guess make a new dm sync time?
let last_sync_time = storage.get_dm_sync_time()?;
let time_stamp = match last_sync_time {
None => {
let now = Timestamp::now();
storage.set_dm_sync_time(now.as_u64())?;
now
}
Some(time) => Timestamp::from(time + 1), // add one so we get only new events
};

utils::spawn(async move {
loop {
if stop.load(Ordering::Relaxed) {
break;
};

let received_dm_filter = Filter::new()
.kind(Kind::EncryptedDirectMessage)
.pubkey(public_key)
.since(time_stamp);

client.connect().await;

client.subscribe(vec![received_dm_filter]).await;

let mut notifications = client.notifications();

loop {
let read_fut = notifications.recv().fuse();
let delay_fut = Box::pin(utils::sleep(1_000)).fuse();

pin_mut!(read_fut, delay_fut);
select! {
notification = read_fut => {
match notification {
Ok(RelayPoolNotification::Event { event, .. }) => {
if event.verify().is_ok() {
match event.kind {
Kind::EncryptedDirectMessage => {
match decrypt_dm(primary_key.clone(), public_key, &event.content).await {
Ok(_) => {
// TODO we need to parse and redeem ecash
},
Err(e) => {
log_error!(logger, "Error decrypting DM: {e}");
}
}
}
kind => log_warn!(logger, "Received unexpected note of kind {kind}")
}
}
},
Ok(RelayPoolNotification::Message { .. }) => {}, // ignore messages
Ok(RelayPoolNotification::Shutdown) => break, // if we disconnect, we restart to reconnect
Ok(RelayPoolNotification::Stop) => {}, // Currently unused
Ok(RelayPoolNotification::RelayStatus { .. }) => {}, // Currently unused
Err(_) => break, // if we are erroring we should reconnect
}
}
_ = delay_fut => {
if stop.load(Ordering::Relaxed) {
break;
}
}
}
}

if let Err(e) = client.disconnect().await {
log_warn!(logger, "Error disconnecting from relays: {e}");
}
}
});

Ok(())
}

pub async fn check_available_name(&self, name: String) -> Result<bool, MutinyError> {
check_name_request(&self.http_client, &self.base_url, name).await
}

pub async fn reserve_name(&self, name: String) -> Result<(), MutinyError> {
// check that we have a name token available
let available_tokens = self.blind_auth.available_tokens().await;
let available_paid_token =
match find_hermes_token(&available_tokens, HERMES_SERVICE_ID, HERMES_PAID_PLAN_ID) {
Some(t) => t,
None => return Err(MutinyError::NotFound),
};

// check that we have a federation added and get it's id/invite code
let federation_identity = match self.get_first_federation().await {
Some(f) => f,
None => return Err(MutinyError::FederationRequired),
};

// do the unblinding
let (nonce, blinding_key) = self
.blind_auth
.get_unblinded_info_from_token(available_paid_token);
let unblinded_sig = unblind_signature(blinding_key, available_paid_token.blind_sig);

// send the register request
let req = RegisterRequest {
name: Some(name),
pubkey: self.public_key.to_string(),
federation_id: federation_identity.federation_id,
federation_invite_code: federation_identity.invite_code.to_string(),
msg: nonce.to_message(),
sig: unblinded_sig,
};
register_name(&self.http_client.clone(), &self.base_url, req).await?;

Ok(())
}

pub async fn get_first_federation(&self) -> Option<FederationIdentity> {
let federations = self.federations.read().await;
match federations.iter().next() {
Some((_, n)) => Some(n.get_mutiny_federation_identity().await),
None => None,
}
}

// TODO need a way to change the federation if the user's federation changes
}

fn find_hermes_token(
tokens: &Vec<SignedToken>,
service_id: u32,
plan_id: u32,
) -> Option<&SignedToken> {
tokens
.iter()
.find(|token| token.service_id == service_id && token.plan_id == plan_id)
}

async fn check_name_request(
http_client: &reqwest::Client,
base_url: &str,
name: String,
) -> Result<bool, MutinyError> {
let url = Url::parse(&format!("{}/v1/check-username/{name}", base_url))
.map_err(|_| MutinyError::ConnectionFailed)?;
let request = http_client.request(Method::GET, url);

let res = utils::fetch_with_timeout(http_client, request.build().expect("should build req"))
.await?
.json::<bool>()
.await
.map_err(|_| MutinyError::ConnectionFailed)?;

Ok(res)
}

async fn register_name(
http_client: &reqwest::Client,
base_url: &str,
req: RegisterRequest,
) -> Result<RegisterResponse, MutinyError> {
let url = Url::parse(&format!("{}/v1/register", base_url))
.map_err(|_| MutinyError::ConnectionFailed)?;
let request = http_client.request(Method::POST, url).json(&req);

let res = utils::fetch_with_timeout(http_client, request.build().expect("should build req"))
.await?
.json::<RegisterResponse>()
.await
.map_err(|_| MutinyError::ConnectionFailed)?;

Ok(res)
}

/// Decrypts a DM using the primary key
pub async fn decrypt_dm(
primary_key: Keys,
pubkey: nostr::PublicKey,
message: &str,
) -> Result<String, MutinyError> {
let secret = primary_key.secret_key().expect("must have");
let decrypted = decrypt(secret, &pubkey, message)?;
Ok(decrypted)
}
1 change: 1 addition & 0 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod event;
pub mod federation;
mod fees;
mod gossip;
mod hermes;
mod key;
mod keymanager;
pub mod labels;
Expand Down
Loading

0 comments on commit eb680b1

Please sign in to comment.