From 00b766df91a2d25cb9d3d4f6a78287989ac1d0f7 Mon Sep 17 00:00:00 2001 From: nickfarrow Date: Fri, 14 Oct 2022 16:51:35 -0400 Subject: [PATCH] Multistage cargo build for x86 and ARM - Conditionally `cargo build` for platforms of x86_64 or ARM. - In the final Docker stage we copy the built binary to alpine, and run with environment: - $LND_HOST, $LND_GRPC_PORT, $TLS_FILE, $MACAROON_FILE" Don't copy everything! Copy static later fix onion endpoint to http --- .github/workflows/test.yml | 25 +++++++++ Dockerfile | 63 ++++++++++++++++++++++ run_loin | 19 +++++++ src/main.rs | 11 ++-- tests/integration.rs | 108 +++++++++++++++++++++++-------------- 5 files changed, 181 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 Dockerfile create mode 100755 run_loin diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d06ef5b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: Test + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: KengoTODA/actions-setup-docker-compose@main + with: + version: '2.10.2' + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose -- --nocapture diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..67b9c3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Multistage Build for Loin +# +# x86_64-unknown-linux-musl +# aarch64-unknown-linux-musl +# +# Conditionally `cargo build` for platforms of x86_64 or ARM. +# Use musl for static linking, producing a standalone executable with no dependencies. +# In the final Docker stage we copy the built binary to alpine, and run with environment: +# $LND_HOST, $LND_GRPC_PORT, $TLS_FILE, $MACAROON_FILE" + +## Initial build Stage +FROM rustlang/rust:nightly AS builder +# Target architecture argument used to change build +ARG TARGETARCH +# Some nicer rust debugging +ENV RUSTFLAGS="-Z macro-backtrace" +ENV RUST_BACKTRACE=1 +# Copy the required build files. In this case, these are all the files that +# are used for both architectures. +WORKDIR /usr/src/loin/ +COPY Cargo.toml Cargo.lock build.rs config_spec.toml ./ +COPY src/ ./src/ +# COPY static/ /usr/share/loin/static/ + +## x86_64 +FROM builder AS branch-version-amd64 +RUN echo "Preparing to cargo build for x86_64 (${TARGETARCH})" +# Install the required dependencies to build for `musl` static linking +RUN apt-get update && apt-get install -y musl-tools musl-dev +# Add our x86 target to rust, then compile and install +RUN rustup target add x86_64-unknown-linux-musl +RUN cargo install --target x86_64-unknown-linux-musl --path . + +# ARM +FROM builder AS branch-version-arm64 +RUN echo "Preparing to cargo build for arm (${TARGETARCH})" +# Install the required dependencies to build for `musl` static linking for arm. +RUN apt-get update && apt-get install musl-tools clang llvm -y +# Add our arm target to rust, some build variables, then compile and install +RUN rustup target add aarch64-unknown-linux-musl +ENV CC_aarch64_unknown_linux_musl=clang +ENV AR_aarch64_unknown_linux_musl=llvm-ar +ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld" +RUN cargo install --target aarch64-unknown-linux-musl --path . + +# We build for either x86_64 or ARM from above options using the docker $TARGETARCH +FROM branch-version-${TARGETARCH} AS chosen_builder +RUN echo "Called build!" + +# Run Loin from a final debian container +FROM debian:buster-slim +# COPY --chown=1000:1000 . . +USER 1000 + +# Copy just the binary from our build stage +COPY --from=chosen_builder /usr/local/cargo/bin/loin /usr/local/bin/loin +COPY run_loin /usr/local/bin/run_loin +COPY static/ /usr/share/loin/static/ + +# Expose any necessary ports +EXPOSE 4444 +# Run +CMD ["run_loin"] diff --git a/run_loin b/run_loin new file mode 100755 index 0000000..97b9cab --- /dev/null +++ b/run_loin @@ -0,0 +1,19 @@ +#!/bin/sh + +# Old: create loin config (we may want some persistent & modifyable config in the future, will need a volume) +# CONF=loin.conf +# if [ ! -f $CONF ] +# then +# touch $CONF +# echo "bind_port=4444" >> $CONF +# echo "lnd_address=\"https://lightning_lnd_1:$LND_GRPC_PORT\"" >> $CONF +# echo "lnd_cert_path=\"$TLS_FILE\"" >> $CONF +# echo "lnd_macaroon_path=\"$MACAROON_FILE\"" >> $CONF +# echo "endpoint=\"https://$APP_HIDDEN_SERVICE\"" >> $CONF +# fi +# cat $CONF +#loin --conf $CONF + +echo "Running loin" +loin --bind-port=4444 --lnd-address=https://lightning_lnd_1:$LND_GRPC_PORT --lnd-cert-path=$TLS_FILE --lnd-macaroon-path=$MACAROON_FILE --endpoint=http://$APP_HIDDEN_SERVICE +echo "STOPPED RUNNING LOIN" diff --git a/src/main.rs b/src/main.rs index 461b545..8ad1086 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,14 +25,13 @@ async fn main() -> Result<(), Box> { if let Some(payjoin) = scheduled_pj { let address = scheduler.schedule_payjoin(&payjoin).await?; - println!("{}", scheduler::format_bip21( - address, - payjoin.total_amount(), - secure_endpoint.clone() - )); + println!( + "{}", + scheduler::format_bip21(address, payjoin.total_amount(), secure_endpoint.clone()) + ); } - let bind_addr = ([127, 0, 0, 1], config.bind_port).into(); + let bind_addr = ([0, 0, 0, 0], config.bind_port).into(); http::serve(scheduler, bind_addr, secure_endpoint).await?; Ok(()) diff --git a/tests/integration.rs b/tests/integration.rs index 0545e31..eaeece7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod integration { + use std::thread::sleep; use std::time::Duration; use std::{ env, @@ -33,43 +34,62 @@ mod integration { let tmp_path = fixture.tmp_path(); // wait for bitcoind to start and for lnd to be fully initialized with secrets - std::thread::sleep(std::time::Duration::from_secs(10)); // sanity check - let bitcoin_rpc = Client::new( - "http://localhost:43782", - Auth::UserPass("ceiwHEbqWI83".to_string(), "DwubwWsoo3".to_string()), - ) - .unwrap(); - assert!(&bitcoin_rpc.get_best_block_hash().is_ok()); - - Command::new("docker") - .arg("cp") - .arg("compose-merchant_lnd-1:/root/.lnd/tls.cert") - .arg(format!("{}/merchant-tls.cert", tmp_path)) - .output() - .expect("failed to copy tls.cert"); - println!("copied merchant-tls.cert"); + let timeout = 6; + let bitcoin_rpc = loop { + if --timeout < 0 { + panic!("can't connect to bitcoin rpc"); + } + sleep(Duration::from_secs(1)); + if let Ok(btcrpc) = Client::new("http://localhost:43782", Auth::UserPass("ceiwHEbqWI83".to_string(), "DwubwWsoo3".to_string())) { + match btcrpc.get_best_block_hash() { + Ok(_) => break btcrpc, + Err(e) => println!("Attempting to contact btcrpc: {}", e), + } + } - Command::new("docker") - .arg("cp") - .arg("compose-merchant_lnd-1:/data/chain/bitcoin/regtest/admin.macaroon") - .arg(format!("{}/merchant-admin.macaroon", &tmp_path)) - .output() - .expect("failed to copy admin.macaroon"); - println!("copied merchant-admin.macaroon"); + }; // merchant lnd loin configuration let address_str = "https://localhost:53281"; let cert_file = format!("{}/merchant-tls.cert", &tmp_path).to_string(); let macaroon_file = format!("{}/merchant-admin.macaroon", &tmp_path).to_string(); - // Connecting to LND requires only address, cert file, and macaroon file - let mut merchant_client = - tonic_lnd::connect(address_str, &cert_file, &macaroon_file).await.unwrap(); + let timeout = 6; + let mut merchant_client = loop { + if --timeout < 0 { + panic!("can't connect to merchant_client"); + } + sleep(Duration::from_secs(1)); + + Command::new("docker") + .arg("cp") + .arg("compose-merchant_lnd-1:/root/.lnd/tls.cert") + .arg(format!("{}/merchant-tls.cert", tmp_path)) + .output() + .expect("failed to copy tls.cert"); + println!("copied merchant-tls.cert"); - // Just test the node rpc - merchant_client.get_info(tonic_lnd::rpc::GetInfoRequest {}).await.unwrap(); + Command::new("docker") + .arg("cp") + .arg("compose-merchant_lnd-1:/data/chain/bitcoin/regtest/admin.macaroon") + .arg(format!("{}/merchant-admin.macaroon", &tmp_path)) + .output() + .expect("failed to copy admin.macaroon"); + println!("copied merchant-admin.macaroon"); + + // Connecting to LND requires only address, cert file, and macaroon file + let client = + tonic_lnd::connect(address_str, &cert_file, &macaroon_file).await; + + if let Ok(mut client) = client { + match client.get_info(tonic_lnd::rpc::GetInfoRequest {}).await { + Ok(_) => break client, + Err(e) => println!("Attempting to connect lnd: {}", e), + } + } + }; // conf to merchant let endpoint: url::Url = "https://localhost:3010".parse().expect("not a valid Url"); @@ -112,7 +132,7 @@ mod integration { let source_address = bitcoin_rpc.get_new_address(None, None).unwrap(); bitcoin_rpc.generate_to_address(101, &source_address).unwrap(); - std::thread::sleep(std::time::Duration::from_secs(5)); + std::thread::sleep(Duration::from_secs(5)); println!("SLEPT"); // connect one to the next let connected = merchant_client @@ -144,11 +164,25 @@ mod integration { let bip21 = scheduler::format_bip21(address, pj.total_amount(), endpoint.clone()); println!("{}", &bip21); + let loop_til_open_channel = tokio::spawn(async move { + let channel_update = peer_client.subscribe_channel_events(tonic_lnd::rpc::ChannelEventSubscription {}); + let mut res = channel_update.await.unwrap().into_inner(); + loop { + if let Ok(Some(channel_event)) = res.message().await { + if channel_event.r#type() == tonic_lnd::rpc::channel_event_update::UpdateType::OpenChannel { + break; + } + } + }; + }); + let bind_addr = ([127, 0, 0, 1], 3000).into(); + let loin_server = http::serve(scheduler, bind_addr, endpoint.clone()); // trigger payjoin-client let payjoin_channel_open = tokio::spawn(async move { // if we don't wait for loin server to run we'll make requests to a closed port - std::thread::sleep(std::time::Duration::from_secs(2)); + std::thread::sleep(Duration::from_secs(2)); + // TODO loop on ping 3000 until it the server is live let link = bip78::Uri::try_from(bip21).expect("bad bip78 uri"); @@ -216,25 +250,21 @@ mod integration { let tx = bitcoin_rpc.finalize_psbt(&psbt, Some(true)).unwrap().hex.expect("incomplete psbt"); bitcoin_rpc.send_raw_transaction(&tx).unwrap(); - // Open channel on newly created payjoin + // Confirm the newly opene transaction in new blocks bitcoin_rpc.generate_to_address(8, &source_address).unwrap(); - std::thread::sleep(Duration::from_secs(1)); }); - let loin_server = http::serve(scheduler, bind_addr, endpoint.clone()); - tokio::select! { _ = payjoin_channel_open => println!("payjoin-client completed first"), _ = loin_server => println!("loin server stopped first. This shouldn't happen"), - _ = tokio::time::sleep(std::time::Duration::from_secs(20)) => println!("payjoin timed out after 20 seconds"), + _ = tokio::time::sleep(Duration::from_secs(20)) => println!("payjoin timed out after 20 seconds"), }; - let bal_res = peer_client.channel_balance(tonic_lnd::rpc::ChannelBalanceRequest::default()).await?; - println!("{:?}",bal_res); - let merchant_side_channel_balance = bal_res.into_inner().remote_balance.unwrap().sat; - println!("{:?}",merchant_side_channel_balance); + tokio::select! { + _ = loop_til_open_channel => println!("Channel opened!"), + _ = tokio::time::sleep(Duration::from_secs(6)) => println!("Channel open upate listener timed out"), + }; - assert!(merchant_side_channel_balance != 0); Ok(()) } struct Fixture {