From e27caf0fae69507404133b14ec0bd0631fe2f662 Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Wed, 12 Mar 2025 14:52:05 +0100 Subject: [PATCH 1/3] Send content-type header in integration tests While the directory's OHTTP gateway does not require the content type header to be set to `message/ohttp-req`, the OHTTP relay does. --- payjoin/tests/integration.rs | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index 0ba602fd6..9e60ce2eb 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -206,7 +206,13 @@ mod integration { let mut bad_initializer = Receiver::new(mock_address, directory, bad_ohttp_keys, None)?; let (req, _ctx) = bad_initializer.extract_req(&mock_ohttp_relay)?; - agent.post(req.url).body(req.body).send().await.map_err(|e| e.into()) + agent + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await + .map_err(|e| e.into()) } Ok(()) @@ -292,7 +298,12 @@ mod integration { // Poll receive request let mock_ohttp_relay = services.ohttp_gateway_url(); let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; - let response = agent.post(req.url).body(req.body).send().await?; + let response = agent + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; assert!(response.status().is_success(), "error response: {}", response.status()); let response_body = session.process_res(response.bytes().await?.to_vec().as_slice(), ctx)?; @@ -328,7 +339,12 @@ mod integration { // GET fallback psbt let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; - let response = agent.post(req.url).body(req.body).send().await?; + let response = agent + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; // POST payjoin let proposal = session .process_res(response.bytes().await?.to_vec().as_slice(), ctx)? @@ -488,7 +504,12 @@ mod integration { let agent_clone = agent_clone.clone(); let proposal = loop { let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; - let response = agent_clone.post(req.url).body(req.body).send().await?; + let response = agent_clone + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; if response.status() == 200 { if let Some(proposal) = session @@ -512,7 +533,12 @@ mod integration { // Respond with payjoin psbt within the time window the sender is willing to wait // this response would be returned as http response to the sender let (req, ctx) = payjoin_proposal.extract_v2_req(&mock_ohttp_relay)?; - let response = agent_clone.post(req.url).body(req.body).send().await?; + let response = agent_clone + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; payjoin_proposal .process_res(&response.bytes().await?, ctx) .map_err(|e| e.to_string())?; @@ -747,7 +773,12 @@ mod integration { for sender_sesssion in inner_sender_test_sessions.iter() { let mut receiver_session = sender_sesssion.receiver_session.clone(); let (req, reciever_ctx) = receiver_session.extract_req(&directory)?; - let response = agent.post(req.url).body(req.body).send().await?; + let response = agent + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; assert!(response.status().is_success()); let res = response.bytes().await?.to_vec(); let proposal = receiver_session @@ -816,7 +847,12 @@ mod integration { for sender_sesssion in inner_sender_test_sessions.iter() { let mut receiver_session = sender_sesssion.receiver_session.clone(); let (req, reciever_ctx) = receiver_session.extract_req(&directory)?; - let response = agent.post(req.url).body(req.body).send().await?; + let response = agent + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + .await?; assert!(response.status().is_success()); let finalized_response = receiver_session From 4debe00d253d19e0da7c6376fb4b7bda7f141078 Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Fri, 14 Mar 2025 02:39:09 +0100 Subject: [PATCH 2/3] Update ohttp-relay, use RootCertStore The certificate generated by TestServices is self signed, add it to a new RootCertStore and give it to the relay for when it acts as an HTTP client as part of the updated relay API that supports this. Also use GatewayUri instead of Uri, which has also changed upstream. --- Cargo-minimal.lock | 9 ++++---- Cargo-recent.lock | 9 ++++---- payjoin-test-utils/Cargo.toml | 3 ++- payjoin-test-utils/src/lib.rs | 39 +++++++++++++++++++++-------------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index bc7319ca1..ca59ec80f 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -408,9 +408,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" @@ -1473,8 +1473,7 @@ dependencies = [ [[package]] name = "ohttp-relay" version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f8e8aef13b8327b680aaaca807aa11ba5979fc5858203e7b77c68128ede61a2" +source = "git+https://github.com/payjoin/ohttp-relay.git?rev=b8153e5c290560be58e1395a8e50ea610e146f6d#b8153e5c290560be58e1395a8e50ea610e146f6d" dependencies = [ "futures", "http", @@ -1483,7 +1482,6 @@ dependencies = [ "hyper-rustls", "hyper-tungstenite", "hyper-util", - "once_cell", "rustls 0.22.4", "tokio", "tokio-tungstenite", @@ -1686,6 +1684,7 @@ dependencies = [ "payjoin-directory", "rcgen", "reqwest", + "rustls 0.22.4", "testcontainers", "testcontainers-modules", "tokio", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index bc7319ca1..ca59ec80f 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -408,9 +408,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" @@ -1473,8 +1473,7 @@ dependencies = [ [[package]] name = "ohttp-relay" version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f8e8aef13b8327b680aaaca807aa11ba5979fc5858203e7b77c68128ede61a2" +source = "git+https://github.com/payjoin/ohttp-relay.git?rev=b8153e5c290560be58e1395a8e50ea610e146f6d#b8153e5c290560be58e1395a8e50ea610e146f6d" dependencies = [ "futures", "http", @@ -1483,7 +1482,6 @@ dependencies = [ "hyper-rustls", "hyper-tungstenite", "hyper-util", - "once_cell", "rustls 0.22.4", "tokio", "tokio-tungstenite", @@ -1686,6 +1684,7 @@ dependencies = [ "payjoin-directory", "rcgen", "reqwest", + "rustls 0.22.4", "testcontainers", "testcontainers-modules", "tokio", diff --git a/payjoin-test-utils/Cargo.toml b/payjoin-test-utils/Cargo.toml index a70c80bf3..2dc543233 100644 --- a/payjoin-test-utils/Cargo.toml +++ b/payjoin-test-utils/Cargo.toml @@ -13,11 +13,12 @@ bitcoind = { version = "0.36.0", features = ["0_21_2"] } http = "1" log = "0.4.7" ohttp = { package = "bitcoin-ohttp", version = "0.6.0" } -ohttp-relay = { version = "0.0.9", features = ["_test-util"] } +ohttp-relay = { git = "https://github.com/payjoin/ohttp-relay.git", rev = "b8153e5c290560be58e1395a8e50ea610e146f6d", version = "0.0.9", features = ["_test-util"] } once_cell = "1" payjoin = { path = "../payjoin", features = ["io", "_danger-local-https"] } payjoin-directory = { path = "../payjoin-directory", features = ["_danger-local-https"] } rcgen = "0.11" +rustls = "0.22" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } testcontainers = "0.15.0" testcontainers-modules = { version = "0.3.7", features = ["redis"] } diff --git a/payjoin-test-utils/src/lib.rs b/payjoin-test-utils/src/lib.rs index 1608213ac..c392ff2b8 100644 --- a/payjoin-test-utils/src/lib.rs +++ b/payjoin-test-utils/src/lib.rs @@ -7,14 +7,17 @@ use std::time::Duration; use bitcoin::{Amount, Psbt}; use bitcoind::bitcoincore_rpc::json::AddressType; use bitcoind::bitcoincore_rpc::{self, RpcApi}; -use http::{StatusCode, Uri}; +use http::StatusCode; use log::{log_enabled, Level}; use ohttp::hpke::{Aead, Kdf, Kem}; use ohttp::{KeyId, SymmetricSuite}; use once_cell::sync::{Lazy, OnceCell}; use payjoin::io::{fetch_ohttp_keys_with_cert, Error as IOError}; use payjoin::OhttpKeys; +use rcgen::Certificate; use reqwest::{Client, ClientBuilder}; +use rustls::pki_types::CertificateDer; +use rustls::RootCertStore; use testcontainers::{clients, Container}; use testcontainers_modules::redis::{Redis, REDIS_PORT}; use tokio::task::JoinHandle; @@ -39,7 +42,7 @@ pub fn init_tracing() { } pub struct TestServices { - cert_key: (Vec, Vec), + cert: Certificate, /// redis is an implicit dependency of the directory service #[allow(dead_code)] redis: (u16, Container<'static, Redis>), @@ -50,15 +53,25 @@ pub struct TestServices { impl TestServices { pub async fn initialize() -> Result { - let cert_key = local_cert_key(); + // TODO add a UUID, and cleanup guard to delete after on successful run + let cert = local_cert_key(); + let cert_der = cert.serialize_der().expect("Failed to serialize cert"); + let key_der = cert.serialize_private_key_der(); + let cert_key = (cert_der.clone(), key_der); + + let mut root_store = RootCertStore::empty(); + root_store.add(CertificateDer::from(cert.serialize_der().unwrap())).unwrap(); + let redis = init_redis(); let db_host = format!("127.0.0.1:{}", redis.0); let directory = init_directory(db_host, cert_key.clone()).await?; - let gateway_origin = Uri::from_str(&format!("https://localhost:{}", directory.0))?; - let ohttp_relay = ohttp_relay::listen_tcp_on_free_port(gateway_origin).await?; - let http_agent: Arc = Arc::new(http_agent(cert_key.0.clone())?); + let gateway_origin = + ohttp_relay::GatewayUri::from_str(&format!("https://localhost:{}", directory.0))?; + let ohttp_relay = ohttp_relay::listen_tcp_on_free_port(gateway_origin, root_store).await?; + let http_agent: Arc = Arc::new(http_agent(cert_der)?); + Ok(Self { - cert_key, + cert, redis, directory: (directory.0, Some(directory.1)), ohttp_relay: (ohttp_relay.0, Some(ohttp_relay.1)), @@ -66,7 +79,7 @@ impl TestServices { }) } - pub fn cert(&self) -> Vec { self.cert_key.0.clone() } + pub fn cert(&self) -> Vec { self.cert.serialize_der().expect("Failed to serialize cert") } pub fn directory_url(&self) -> Url { Url::parse(&format!("https://localhost:{}", self.directory.0)).expect("invalid URL") @@ -122,13 +135,9 @@ pub async fn init_directory( } /// generate or get a DER encoded localhost cert and key. -pub fn local_cert_key() -> (Vec, Vec) { - let cert = - rcgen::generate_simple_self_signed(vec!["0.0.0.0".to_string(), "localhost".to_string()]) - .expect("Failed to generate cert"); - let cert_der = cert.serialize_der().expect("Failed to serialize cert"); - let key_der = cert.serialize_private_key_der(); - (cert_der, key_der) +pub fn local_cert_key() -> rcgen::Certificate { + rcgen::generate_simple_self_signed(vec!["0.0.0.0".to_string(), "localhost".to_string()]) + .expect("Failed to generate cert") } pub fn init_bitcoind() -> Result { From 8649acf344be99b5020b4f1ff6e4b6bb4447ed57 Mon Sep 17 00:00:00 2001 From: Yuval Kogman Date: Wed, 12 Mar 2025 15:05:09 +0100 Subject: [PATCH 3/3] Use OHTTP relay in integration tests Instead of sending OHTTP requests directly to the gateway, send them through the relay set up by TestServices. --- payjoin-cli/tests/e2e.rs | 11 +++++------ payjoin/tests/integration.rs | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/payjoin-cli/tests/e2e.rs b/payjoin-cli/tests/e2e.rs index 22b308f70..33ed3d771 100644 --- a/payjoin-cli/tests/e2e.rs +++ b/payjoin-cli/tests/e2e.rs @@ -201,8 +201,7 @@ mod e2e { let payjoin_cli = env!("CARGO_BIN_EXE_payjoin-cli"); let directory = &services.directory_url().to_string(); - // Mock ohttp_relay since the ohttp_relay's http client doesn't have the certificate for the directory - let mock_ohttp_relay = &services.ohttp_gateway_url().to_string(); + let ohttp_relay = &services.ohttp_relay_url().to_string(); let cli_receive_initiator = Command::new(payjoin_cli) .arg("--rpchost") @@ -212,7 +211,7 @@ mod e2e { .arg("--db-path") .arg(&receiver_db_path) .arg("--ohttp-relay") - .arg(mock_ohttp_relay) + .arg(ohttp_relay) .arg("receive") .arg(RECEIVE_SATS) .arg("--pj-directory") @@ -232,7 +231,7 @@ mod e2e { .arg("--db-path") .arg(&sender_db_path) .arg("--ohttp-relay") - .arg(mock_ohttp_relay) + .arg(ohttp_relay) .arg("send") .arg(&bip21) .arg("--fee-rate") @@ -251,7 +250,7 @@ mod e2e { .arg("--db-path") .arg(&receiver_db_path) .arg("--ohttp-relay") - .arg(mock_ohttp_relay) + .arg(ohttp_relay) .arg("resume") .stdout(Stdio::piped()) .stderr(Stdio::inherit()) @@ -267,7 +266,7 @@ mod e2e { .arg("--db-path") .arg(&sender_db_path) .arg("--ohttp-relay") - .arg(mock_ohttp_relay) + .arg(ohttp_relay) .arg("send") .arg(&bip21) .arg("--fee-rate") diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index 9e60ce2eb..646907059 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -200,12 +200,12 @@ mod integration { let agent = services.http_agent(); services.wait_for_services_ready().await?; let directory = services.directory_url(); - let mock_ohttp_relay = services.ohttp_gateway_url(); + let ohttp_relay = services.ohttp_relay_url(); let mock_address = Address::from_str("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4")? .assume_checked(); let mut bad_initializer = Receiver::new(mock_address, directory, bad_ohttp_keys, None)?; - let (req, _ctx) = bad_initializer.extract_req(&mock_ohttp_relay)?; + let (req, _ctx) = bad_initializer.extract_req(&ohttp_relay)?; agent .post(req.url) .header("Content-Type", req.content_type) @@ -234,7 +234,7 @@ mod integration { let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver(None, None)?; services.wait_for_services_ready().await?; let directory = services.directory_url(); - let ohttp_relay = services.ohttp_gateway_url(); + let ohttp_relay = services.ohttp_relay_url(); let ohttp_keys = services.fetch_ohttp_keys().await?; // ********************** // Inside the Receiver: @@ -296,8 +296,8 @@ mod integration { Receiver::new(address.clone(), directory.clone(), ohttp_keys.clone(), None)?; println!("session: {:#?}", &session); // Poll receive request - let mock_ohttp_relay = services.ohttp_gateway_url(); - let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; + let ohttp_relay = services.ohttp_relay_url(); + let (req, ctx) = session.extract_req(&ohttp_relay)?; let response = agent .post(req.url) .header("Content-Type", req.content_type) @@ -322,7 +322,7 @@ mod integration { let req_ctx = SenderBuilder::new(psbt.clone(), pj_uri.clone()) .build_recommended(FeeRate::BROADCAST_MIN)?; let (Request { url, body, content_type, .. }, send_ctx) = - req_ctx.extract_v2(mock_ohttp_relay.to_owned())?; + req_ctx.extract_v2(ohttp_relay.to_owned())?; let response = agent .post(url.clone()) .header("Content-Type", content_type) @@ -338,7 +338,7 @@ mod integration { // Inside the Receiver: // GET fallback psbt - let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; + let (req, ctx) = session.extract_req(&ohttp_relay)?; let response = agent .post(req.url) .header("Content-Type", req.content_type) @@ -351,7 +351,7 @@ mod integration { .expect("proposal should exist"); let mut payjoin_proposal = handle_directory_proposal(&receiver, proposal, None)?; assert!(!payjoin_proposal.is_output_substitution_disabled()); - let (req, ctx) = payjoin_proposal.extract_v2_req(&mock_ohttp_relay)?; + let (req, ctx) = payjoin_proposal.extract_v2_req(&ohttp_relay)?; let response = agent .post(req.url) .header("Content-Type", req.content_type) @@ -365,7 +365,7 @@ mod integration { // Sender checks, signs, finalizes, extracts, and broadcasts // Replay post fallback to get the response let (Request { url, body, content_type, .. }, ohttp_ctx) = - send_ctx.extract_req(mock_ohttp_relay.to_owned())?; + send_ctx.extract_req(ohttp_relay.to_owned())?; let response = agent .post(url.clone()) .header("Content-Type", content_type) @@ -499,11 +499,11 @@ mod integration { let agent_clone: Arc = agent.clone(); let receiver: Arc = Arc::new(receiver); let receiver_clone = receiver.clone(); - let mock_ohttp_relay = services.ohttp_gateway_url(); + let ohttp_relay = services.ohttp_relay_url(); let receiver_loop = tokio::task::spawn(async move { let agent_clone = agent_clone.clone(); let proposal = loop { - let (req, ctx) = session.extract_req(&mock_ohttp_relay)?; + let (req, ctx) = session.extract_req(&ohttp_relay)?; let response = agent_clone .post(req.url) .header("Content-Type", req.content_type) @@ -532,7 +532,7 @@ mod integration { assert!(payjoin_proposal.is_output_substitution_disabled()); // Respond with payjoin psbt within the time window the sender is willing to wait // this response would be returned as http response to the sender - let (req, ctx) = payjoin_proposal.extract_v2_req(&mock_ohttp_relay)?; + let (req, ctx) = payjoin_proposal.extract_v2_req(&ohttp_relay)?; let response = agent_clone .post(req.url) .header("Content-Type", req.content_type)