diff --git a/Cargo.lock b/Cargo.lock index fbb91ae..3644171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,7 +328,9 @@ dependencies = [ "base64 0.13.1", "bdk", "bdk-reserves", + "lazy_static", "log", + "prometheus", "serde", "serde_json", ] @@ -691,9 +693,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -796,6 +798,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.151" @@ -1001,6 +1009,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot 0.12.1", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "quote" version = "1.0.33" @@ -1330,6 +1359,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.40", +] + [[package]] name = "time" version = "0.3.30" @@ -1672,18 +1721,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zerocopy" -version = "0.7.30" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.30" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c409dea..38e8810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ bdk-reserves = "0.28" #env_logger = "0.10" log = "0.4" base64 = "0.13" +prometheus = "0.13" +lazy_static = "1.4" [dev-dependencies] #actix-rt = "2" diff --git a/Dockerfile b/Dockerfile index 028f099..2011552 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,30 @@ FROM rust:1.72-alpine3.18 as builder + +ARG ALPINE_REPO +ARG MITM_CA +ARG http_proxy +ENV http_proxy=$http_proxy +ENV https_proxy=$http_proxy +ENV HTTP_PROXY=$http_proxy +ENV HTTPS_PROXY=$http_proxy + +# allowing custom package repositories +RUN printf "${ALPINE_REPO}/main\n${ALPINE_REPO}/community\n" > /etc/apk/repositories +RUN cat /etc/apk/repositories + +# allowing MITM attacks (requirement for some build systems) +RUN echo "$MITM_CA" > /root/mitm-ca.crt +RUN cat /root/mitm-ca.crt >> /etc/ssl/certs/ca-certificates.crt +RUN apk --no-cache add ca-certificates \ + && rm -rf /var/cache/apk/* +RUN echo "$MITM_CA" > /usr/local/share/ca-certificates/mitm-ca.crt +RUN update-ca-certificates + RUN apk add --no-cache build-base -USER bin WORKDIR /app COPY . . +RUN mkdir target && chown bin target && mkdir dist && chown bin dist +USER bin RUN cargo test RUN cargo build --release RUN install -D target/release/bdk-reserves-web dist/bin/bdk-reserves-web @@ -16,7 +38,7 @@ RUN cargo fmt -- --check RUN cargo clippy RUN cargo audit -FROM scratch +FROM alpine COPY --from=builder /app/dist / USER 65534 -ENTRYPOINT ["/bin/bdk-reserves-web"] \ No newline at end of file +ENTRYPOINT ["/bin/bdk-reserves-web"] diff --git a/Makefile b/Makefile index b9be5e5..ac7dc1a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ TAG := bdk-reserves-web +ALPINE_REPO := http://dl-cdn.alpinelinux.org/alpine/v3.18 +MITM_CA := "" run: builder docker run --rm --tty --env PORT=8888 --publish 8888:8888 ${TAG} builder: - docker build --tag ${TAG} . + docker build --tag ${TAG} --build-arg "ALPINE_REPO=${ALPINE_REPO}" --build-arg "MITM_CA=${MITM_CA}" . diff --git a/src/main.rs b/src/main.rs index d9e5e46..cb6f197 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, OutPoint, TxOut}; use bdk::electrum_client::{Client, ElectrumApi}; use bdk_reserves::reserves::verify_proof; +use lazy_static::lazy_static; +use prometheus::{self, register_int_counter, Encoder, IntCounter, TextEncoder}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::{env, io, str::FromStr}; @@ -15,6 +17,16 @@ struct ProofOfReserves { proof_psbt: String, } +lazy_static! { + static ref POR_SUCCESS_COUNTER: IntCounter = + register_int_counter!("POR_success", "Successfully validated proof of reserves").unwrap(); +} + +lazy_static! { + static ref POR_INVALID_COUNTER: IntCounter = + register_int_counter!("POR_invalid", "Invalid proof of reserves").unwrap(); +} + #[actix_web::main] async fn main() -> io::Result<()> { let address = env::var("BIND_ADDRESS").unwrap_or_else(|_err| match env::var("PORT") { @@ -25,12 +37,15 @@ async fn main() -> io::Result<()> { println!("Starting HTTP server at http://{}.", address); println!("You can choose a different address through the BIND_ADDRESS env var."); println!("You can choose a different port through the PORT env var."); + POR_INVALID_COUNTER.reset(); + POR_SUCCESS_COUNTER.reset(); HttpServer::new(|| { App::new() .wrap(middleware::Logger::default()) // <- enable logger .app_data(web::JsonConfig::default().limit(40960)) // <- limit size of the payload (global configuration) .service(web::resource("/proof").route(web::post().to(check_proof))) + .service(web::resource("/prometheus").route(web::get().to(prometheus))) .service(index) }) .bind(address)? @@ -44,6 +59,17 @@ async fn index() -> impl Responder { HttpResponse::Ok().content_type("text/html").body(html) } +async fn prometheus() -> HttpResponse { + let metric_families = prometheus::gather(); + let mut buffer = Vec::new(); + let encoder = TextEncoder::new(); + encoder.encode(&metric_families, &mut buffer).unwrap(); + + let output = String::from_utf8(buffer.clone()).unwrap(); + //println!("************************\nprometheus stats:\n{}", output); + HttpResponse::Ok().content_type("text/plain").body(output) +} + async fn check_proof(item: web::Json, req: HttpRequest) -> HttpResponse { println!("request: {:?}", req); println!("model: {:?}", item); @@ -52,8 +78,14 @@ async fn check_proof(item: web::Json, req: HttpRequest) -> Http handle_ext_reserves(&item.message, &item.proof_psbt, 3, item.addresses.clone()); let answer = match proof_result { - Err(e) => json!({ "error": e }), - Ok(res) => res, + Err(e) => { + POR_INVALID_COUNTER.inc(); + json!({ "error": e }) + } + Ok(res) => { + POR_SUCCESS_COUNTER.inc(); + res + } } .to_string(); HttpResponse::Ok().content_type("text/json").body(answer) @@ -147,7 +179,9 @@ mod tests { #[actix_web::test] async fn test_index() -> Result<(), Error> { - let app = App::new().route("/proof", web::post().to(check_proof)); + let app = App::new() + .route("/proof", web::post().to(check_proof)) + .route("/prometheus", web::get().to(prometheus)); let app = test::init_service(app).await; let req = test::TestRequest::post().uri("/proof") @@ -164,7 +198,15 @@ mod tests { let response_body = resp.into_body(); let resp = r#"{"error":"NonSpendableInput(1)"}"#; assert_eq!(to_bytes(response_body).await?, resp); - //assert_eq!(to_bytes(response_body).await?, r##"Hello world!"##); + + let req = test::TestRequest::get().uri("/prometheus").to_request(); + let resp = app.call(req).await?; + + assert_eq!(resp.status(), http::StatusCode::OK); + + let response_body = resp.into_body(); + let resp = "# HELP POR_invalid Invalid proof of reserves\n# TYPE POR_invalid counter\nPOR_invalid 1\n"; + assert_eq!(to_bytes(response_body).await?, resp); Ok(()) }