Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qr optimizations #417

Merged
merged 6 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ dependencies = [

[[package]]
name = "bitcoin"
version = "0.32.4"
version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb"
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
dependencies = [
"base58ck",
"base64 0.21.7",
Expand Down
4 changes: 2 additions & 2 deletions Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ dependencies = [

[[package]]
name = "bitcoin"
version = "0.32.4"
version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb"
checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026"
dependencies = [
"base58ck",
"base64 0.21.7",
Expand Down
49 changes: 20 additions & 29 deletions payjoin-directory/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@ use tracing::debug;
const DEFAULT_COLUMN: &str = "";
const PJ_V1_COLUMN: &str = "pjv1";

// TODO move to payjoin crate as pub?
// TODO impl From<HpkePublicKey> for ShortId
// TODO impl Display for ShortId (Base64)
// TODO impl TryFrom<&str> for ShortId (Base64)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ShortId(pub [u8; 8]);

impl ShortId {
pub fn column_key(&self, column: &str) -> Vec<u8> {
self.0.iter().chain(column.as_bytes()).copied().collect()
}
}

#[derive(Debug, Clone)]
pub(crate) struct DbPool {
client: Client,
Expand All @@ -32,48 +19,48 @@ impl DbPool {
Ok(Self { client, timeout })
}

pub async fn push_default(&self, pubkey_id: &ShortId, data: Vec<u8>) -> RedisResult<()> {
self.push(pubkey_id, DEFAULT_COLUMN, data).await
pub async fn push_default(&self, subdirectory_id: &str, data: Vec<u8>) -> RedisResult<()> {
self.push(subdirectory_id, DEFAULT_COLUMN, data).await
}

pub async fn peek_default(&self, pubkey_id: &ShortId) -> Option<RedisResult<Vec<u8>>> {
self.peek_with_timeout(pubkey_id, DEFAULT_COLUMN).await
pub async fn peek_default(&self, subdirectory_id: &str) -> Option<RedisResult<Vec<u8>>> {
self.peek_with_timeout(subdirectory_id, DEFAULT_COLUMN).await
}

pub async fn push_v1(&self, pubkey_id: &ShortId, data: Vec<u8>) -> RedisResult<()> {
self.push(pubkey_id, PJ_V1_COLUMN, data).await
pub async fn push_v1(&self, subdirectory_id: &str, data: Vec<u8>) -> RedisResult<()> {
self.push(subdirectory_id, PJ_V1_COLUMN, data).await
}

pub async fn peek_v1(&self, pubkey_id: &ShortId) -> Option<RedisResult<Vec<u8>>> {
self.peek_with_timeout(pubkey_id, PJ_V1_COLUMN).await
pub async fn peek_v1(&self, subdirectory_id: &str) -> Option<RedisResult<Vec<u8>>> {
self.peek_with_timeout(subdirectory_id, PJ_V1_COLUMN).await
}

async fn push(
&self,
pubkey_id: &ShortId,
subdirectory_id: &str,
channel_type: &str,
data: Vec<u8>,
) -> RedisResult<()> {
let mut conn = self.client.get_async_connection().await?;
let key = pubkey_id.column_key(channel_type);
let key = channel_name(subdirectory_id, channel_type);
() = conn.set(&key, data.clone()).await?;
() = conn.publish(&key, "updated").await?;
Ok(())
}

async fn peek_with_timeout(
&self,
pubkey_id: &ShortId,
subdirectory_id: &str,
channel_type: &str,
) -> Option<RedisResult<Vec<u8>>> {
tokio::time::timeout(self.timeout, self.peek(pubkey_id, channel_type)).await.ok()
tokio::time::timeout(self.timeout, self.peek(subdirectory_id, channel_type)).await.ok()
}

async fn peek(&self, pubkey_id: &ShortId, channel_type: &str) -> RedisResult<Vec<u8>> {
async fn peek(&self, subdirectory_id: &str, channel_type: &str) -> RedisResult<Vec<u8>> {
let mut conn = self.client.get_async_connection().await?;
let key = pubkey_id.column_key(channel_type);
let key = channel_name(subdirectory_id, channel_type);

// Attempt to fetch existing content for the given pubkey_id and channel_type
// Attempt to fetch existing content for the given subdirectory_id and channel_type
if let Ok(data) = conn.get::<_, Vec<u8>>(&key).await {
if !data.is_empty() {
return Ok(data);
Expand All @@ -83,7 +70,7 @@ impl DbPool {

// Set up a temporary listener for changes
let mut pubsub_conn = self.client.get_async_connection().await?.into_pubsub();
let channel_name = pubkey_id.column_key(channel_type);
let channel_name = channel_name(subdirectory_id, channel_type);
pubsub_conn.subscribe(&channel_name).await?;

// Use a block to limit the scope of the mutable borrow
Expand Down Expand Up @@ -116,3 +103,7 @@ impl DbPool {
Ok(data)
}
}

fn channel_name(subdirectory_id: &str, channel_type: &str) -> Vec<u8> {
(subdirectory_id.to_owned() + channel_type).into_bytes()
}
46 changes: 23 additions & 23 deletions payjoin-directory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use bitcoin::base64::prelude::BASE64_URL_SAFE_NO_PAD;
use bitcoin::base64::Engine;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Empty, Full};
use hyper::body::{Body, Bytes, Incoming};
Expand All @@ -17,8 +15,6 @@ use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tracing::{debug, error, info, trace};

use crate::db::ShortId;

pub const DEFAULT_DIR_PORT: u16 = 8080;
pub const DEFAULT_DB_HOST: &str = "localhost:6379";
pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
Expand All @@ -34,6 +30,9 @@ const V1_REJECT_RES_JSON: &str =
r#"{{"errorCode": "original-psbt-rejected ", "message": "Body is not a string"}}"#;
const V1_UNAVAILABLE_RES_JSON: &str = r#"{{"errorCode": "unavailable", "message": "V2 receiver offline. V1 sends require synchronous communications."}}"#;

// 8 bytes as bech32 is 12.8 characters
const ID_LENGTH: usize = 13;

mod db;
use crate::db::DbPool;

Expand Down Expand Up @@ -306,11 +305,11 @@ async fn post_fallback_v1(
};

let v2_compat_body = format!("{}\n{}", body_str, query);
let id = decode_short_id(id)?;
pool.push_default(&id, v2_compat_body.into())
let id = check_id_length(id)?;
pool.push_default(id, v2_compat_body.into())
.await
.map_err(|e| HandlerError::BadRequest(e.into()))?;
match pool.peek_v1(&id).await {
match pool.peek_v1(id).await {
Some(result) => match result {
Ok(buffered_req) => Ok(Response::new(full(buffered_req))),
Err(e) => Err(HandlerError::BadRequest(e.into())),
Expand All @@ -327,19 +326,29 @@ async fn put_payjoin_v1(
trace!("Put_payjoin_v1");
let ok_response = Response::builder().status(StatusCode::OK).body(empty())?;

let id = decode_short_id(id)?;
let id = check_id_length(id)?;
let req =
body.collect().await.map_err(|e| HandlerError::InternalServerError(e.into()))?.to_bytes();
if req.len() > V1_MAX_BUFFER_SIZE {
return Err(HandlerError::PayloadTooLarge);
}

match pool.push_v1(&id, req.into()).await {
match pool.push_v1(id, req.into()).await {
Ok(_) => Ok(ok_response),
Err(e) => Err(HandlerError::BadRequest(e.into())),
}
}

fn check_id_length(id: &str) -> Result<&str, HandlerError> {
if id.len() != ID_LENGTH {
return Err(HandlerError::BadRequest(anyhow::anyhow!(
"subdirectory ID must be 13 bech32 characters",
)));
}

Ok(id)
}

async fn post_subdir(
id: &str,
body: BoxBody<Bytes, hyper::Error>,
Expand All @@ -348,14 +357,15 @@ async fn post_subdir(
let none_response = Response::builder().status(StatusCode::OK).body(empty())?;
trace!("post_subdir");

let id = decode_short_id(id)?;
let id = check_id_length(id)?;

let req =
body.collect().await.map_err(|e| HandlerError::InternalServerError(e.into()))?.to_bytes();
if req.len() > V1_MAX_BUFFER_SIZE {
return Err(HandlerError::PayloadTooLarge);
}

match pool.push_default(&id, req.into()).await {
match pool.push_default(id, req.into()).await {
Ok(_) => Ok(none_response),
Err(e) => Err(HandlerError::BadRequest(e.into())),
}
Expand All @@ -366,8 +376,8 @@ async fn get_subdir(
pool: DbPool,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, HandlerError> {
trace!("get_subdir");
let id = decode_short_id(id)?;
match pool.peek_default(&id).await {
let id = check_id_length(id)?;
match pool.peek_default(id).await {
Some(result) => match result {
Ok(buffered_req) => Ok(Response::new(full(buffered_req))),
Err(e) => Err(HandlerError::BadRequest(e.into())),
Expand Down Expand Up @@ -396,16 +406,6 @@ async fn get_ohttp_keys(
Ok(res)
}

fn decode_short_id(input: &str) -> Result<ShortId, HandlerError> {
let decoded =
BASE64_URL_SAFE_NO_PAD.decode(input).map_err(|e| HandlerError::BadRequest(e.into()))?;

decoded[..8]
.try_into()
.map_err(|_| HandlerError::BadRequest(anyhow::anyhow!("Invalid subdirectory ID")))
.map(ShortId)
}

fn empty() -> BoxBody<Bytes, hyper::Error> {
Empty::<Bytes>::new().map_err(|never| match never {}).boxed()
}
Expand Down
4 changes: 2 additions & 2 deletions payjoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ exclude = ["tests"]
send = []
receive = ["bitcoin/rand"]
base64 = ["bitcoin/base64"]
v2 = ["bitcoin/rand", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "url/serde"]
v2 = ["bitcoin/rand", "bitcoin/serde", "hpke", "dep:http", "bhttp", "ohttp", "serde", "url/serde" ]
io = ["reqwest/rustls-tls"]
danger-local-https = ["io", "reqwest/rustls-tls", "rustls"]

[dependencies]
bitcoin = { version = "0.32.4", features = ["base64"] }
bitcoin = { version = "0.32.5", features = ["base64"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update is missing from payjoin-directory, but I don't think it matters since the specified [^]0.32.4 allows for 0.32.5

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, i missed that. i think this should be addressed in #418

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in fact, cargo udeps points out payjoin-directory doesn't even use its bitcoin dep

bip21 = "0.5.0"
hpke = { package = "bitcoin-hpke", version = "0.13.0", optional = true }
log = { version = "0.4.14"}
Expand Down
49 changes: 49 additions & 0 deletions payjoin/src/bech32.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::fmt;

use bitcoin::bech32::primitives::decode::{CheckedHrpstring, CheckedHrpstringError};
use bitcoin::bech32::{self, EncodeError, Hrp, NoChecksum};

pub mod nochecksum {
use super::*;

pub fn decode(encoded: &str) -> Result<(Hrp, Vec<u8>), CheckedHrpstringError> {
let hrp_string = CheckedHrpstring::new::<NoChecksum>(encoded)?;
Ok((hrp_string.hrp(), hrp_string.byte_iter().collect::<Vec<u8>>()))
}

pub fn encode(hrp: Hrp, data: &[u8]) -> Result<String, EncodeError> {
bech32::encode_upper::<NoChecksum>(hrp, data)
}

pub fn encode_to_fmt(f: &mut fmt::Formatter, hrp: Hrp, data: &[u8]) -> Result<(), EncodeError> {
bech32::encode_upper_to_fmt::<NoChecksum, fmt::Formatter>(f, hrp, data)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn bech32_for_qr() {
let bytes = vec![0u8, 1, 2, 3, 31, 32, 33, 95, 0, 96, 127, 128, 129, 254, 255, 0];
let hrp = Hrp::parse("STUFF").unwrap();
let encoded = nochecksum::encode(hrp, &bytes).unwrap();
let decoded = nochecksum::decode(&encoded).unwrap();
assert_eq!(decoded, (hrp, bytes.to_vec()));

// no checksum
assert_eq!(
encoded.len() as f32,
(hrp.as_str().len() + 1) as f32 + (bytes.len() as f32 * 8.0 / 5.0).ceil()
);

// TODO assert uppercase

// should not error
let corrupted = encoded + "QQPP";
let decoded = nochecksum::decode(&corrupted).unwrap();
assert_eq!(decoded.0, hrp);
assert_ne!(decoded, (hrp, bytes.to_vec()));
}
}
2 changes: 2 additions & 0 deletions payjoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub use crate::hpke::{HpkeKeyPair, HpkePublicKey};
pub(crate) mod ohttp;
#[cfg(feature = "v2")]
pub use crate::ohttp::OhttpKeys;
#[cfg(feature = "v2")]
pub(crate) mod bech32;

#[cfg(feature = "io")]
pub mod io;
Expand Down
Loading
Loading