├── src ├── wallet │ ├── contract2.rs │ ├── mod.rs │ ├── error.rs │ └── storage.rs ├── maker │ ├── rpc │ │ ├── mod.rs │ │ ├── messages.rs │ │ └── server.rs │ ├── mod.rs │ └── error.rs ├── watch_tower │ ├── mod.rs │ ├── constants.rs │ ├── service.rs │ ├── zmq_backend.rs │ ├── watcher_error.rs │ ├── rpc_backend.rs │ └── watcher.rs ├── protocol │ ├── mod.rs │ ├── musig_interface.rs │ ├── musig2.rs │ ├── error.rs │ └── messages2.rs ├── lib.rs ├── taker │ ├── mod.rs │ ├── error.rs │ ├── config.rs │ └── offers.rs ├── error.rs └── bin │ ├── makerd.rs │ └── maker-cli.rs ├── rustfmt.toml ├── rust-toolchain.toml ├── assets └── logo.png ├── torrc ├── .gitignore ├── codecov.yaml ├── docker ├── bitcoin-mutinynet.version ├── Dockerfile └── Dockerfile.bitcoin-mutinynet ├── .github ├── bitcoin.conf └── workflows │ ├── docker.yaml │ ├── test.yaml │ ├── build.yaml │ ├── lint.yaml │ ├── docker-publish.yaml │ └── docker-publish-bitcoin.yaml ├── .dockerignore ├── LICENSE ├── LICENSE-MIT ├── docs ├── bitcoin.conf ├── tor.md ├── workshop.md ├── demo-v2.md └── docker.md ├── git_hooks └── pre-commit ├── Cargo.toml ├── docker-compose.yml └── tests ├── wallet_backup.rs ├── multi-taker.rs ├── malice1.rs ├── taker_cli.rs ├── funding_dynamic_splits.rs ├── standard_swap.rs ├── abort3_case3.rs ├── abort1.rs ├── abort2_case2.rs └── abort3_case2.rs /src/wallet/contract2.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citadel-tech/coinswap/HEAD/assets/logo.png -------------------------------------------------------------------------------- /src/maker/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | mod messages; 2 | pub mod server; 3 | 4 | pub use messages::{RpcMsgReq, RpcMsgResp}; 5 | pub(crate) use server::start_rpc_server; 6 | -------------------------------------------------------------------------------- /torrc: -------------------------------------------------------------------------------- 1 | SocksPort 0.0.0.0:19050 2 | ControlPort 0.0.0.0:19051 3 | HashedControlPassword 16:16E946B27E1A526E60A2B64061EC3F140ACA991F1B705ACD35374D6730 4 | DataDirectory /var/lib/tor -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.swp 3 | /docs/fidelity_bonds.md 4 | Cargo.lock 5 | .vscode 6 | .idea 7 | .ra-target 8 | bin 9 | /coverage/reports 10 | 11 | docker-compose.generated.yml 12 | torrc.generated 13 | .docker-config -------------------------------------------------------------------------------- /src/watch_tower/mod.rs: -------------------------------------------------------------------------------- 1 | //! Watch tower implementation for coinswap. 2 | 3 | pub mod constants; 4 | pub mod registry_storage; 5 | pub mod rpc_backend; 6 | pub mod service; 7 | pub mod utils; 8 | pub mod watcher; 9 | pub mod watcher_error; 10 | pub mod zmq_backend; 11 | -------------------------------------------------------------------------------- /src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines the Contract Transaction and Protocol Messages. 2 | 3 | pub(crate) mod contract; 4 | pub mod contract2; 5 | pub mod error; 6 | pub mod messages; 7 | pub mod messages2; 8 | pub mod musig2; 9 | pub mod musig_interface; 10 | 11 | pub(crate) use contract::Hash160; 12 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 10% 7 | base: auto 8 | informational: false 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 100% 13 | base: auto 14 | github_checks: 15 | annotations: false -------------------------------------------------------------------------------- /docker/bitcoin-mutinynet.version: -------------------------------------------------------------------------------- 1 | TAG=mutinynet-inq-29 2 | FILENAME_AMD64=bitcoin-38351585048e-x86_64-linux-gnu.tar.gz 3 | SHA256_AMD64=0c61faf45e2cf44cf6d51b9f279aa7c67cb16bb385b471d2c36bd852817c6f03 4 | FILENAME_ARM64=bitcoin-38351585048e-aarch64-linux-gnu.tar.gz 5 | SHA256_ARM64=e79d214d2a265e64ebe6ef1ed617bece3f611df2350645c28c7e6fd9bed46a31 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![deny(missing_docs)] 3 | pub extern crate bitcoin; 4 | pub extern crate bitcoind; 5 | 6 | pub mod error; 7 | pub mod fee_estimation; 8 | pub mod maker; 9 | pub mod protocol; 10 | pub mod security; 11 | pub mod taker; 12 | pub mod utill; 13 | pub mod wallet; 14 | pub mod watch_tower; 15 | -------------------------------------------------------------------------------- /src/watch_tower/constants.rs: -------------------------------------------------------------------------------- 1 | //! Default block height where the watchtower begins its discovery scan. 2 | 3 | /// Bitcoin mainnet. 4 | pub const BITCOIN: u64 = 925022; 5 | 6 | /// Signet. 7 | pub const SIGNET: u64 = 2646224 + 19000; 8 | 9 | /// Regtest. 10 | pub const REGTEST: u64 = 1; 11 | 12 | /// Testnet (v3). 13 | pub const TESTNET: u64 = 4740671; 14 | 15 | /// Testnet4. 16 | pub const TESTNET4: u64 = 111218; 17 | -------------------------------------------------------------------------------- /.github/bitcoin.conf: -------------------------------------------------------------------------------- 1 | # bitcoind configuration 2 | 3 | # remove the following line to enable Bitcoin mainnet 4 | regtest=1 5 | fallbackfee=0.0001 6 | # Bitcoind options 7 | server=1 8 | 9 | 10 | # Connection settings 11 | rpcuser=regtestrpcuser 12 | rpcpassword=regtestrpcpass 13 | # blockfilterindex=1 14 | # peerblockfilters=1 15 | 16 | [regtest] 17 | rpcbind=0.0.0.0 18 | rpcallowip=0.0.0.0/0 19 | 20 | zmqpubrawblock=tcp://127.0.0.1:28332 21 | zmqpubrawtx=tcp://127.0.0.1:28333 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore target directory (except for copying specific files) 2 | target/ 3 | !target/release/ 4 | 5 | # Ignore git and development files 6 | .git 7 | .gitignore 8 | # Keep README.md as it's needed by src/lib.rs 9 | # *.md 10 | docs/ 11 | !docs/bitcoin.conf 12 | assets/ 13 | 14 | # Keep test files for running tests in container 15 | # tests/ 16 | 17 | # Ignore IDE files 18 | .vscode/ 19 | .idea/ 20 | 21 | # Ignore temporary files 22 | *.tmp 23 | *.temp 24 | *~ 25 | 26 | # Ignore OS files 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Ignore Docker files (keep the one we're using) 31 | Dockerfile.* 32 | !Dockerfile 33 | 34 | # Ignore other build artifacts 35 | *.log 36 | .env 37 | .env.local 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under [Apache 2.0](LICENSE-APACHE) or 2 | [MIT](LICENSE-MIT), at your option. 3 | 4 | Some files retain their own copyright notice, however, for full authorship 5 | information, see version control history. 6 | 7 | Except as otherwise noted in individual files, all files in this repository are 8 | licensed under the Apache License, Version 2.0 or the MIT license , at your option. 11 | 12 | You may not use, copy, modify, merge, publish, distribute, sublicense, and/or 13 | sell copies of this software or any files in this repository except in 14 | accordance with one or both of these licenses. 15 | -------------------------------------------------------------------------------- /src/wallet/mod.rs: -------------------------------------------------------------------------------- 1 | //! The Coinswap Wallet (unsecured). Used by both the Taker and Maker. 2 | 3 | mod api; 4 | mod backup; 5 | mod error; 6 | pub mod ffi; 7 | mod fidelity; 8 | mod funding; 9 | mod rpc; 10 | mod spend; 11 | mod split_utxos; 12 | mod storage; 13 | mod swapcoin; 14 | mod swapcoin2; 15 | 16 | pub use api::{Balances, UTXOSpendInfo, Wallet}; 17 | pub use backup::WalletBackup; 18 | pub use error::WalletError; 19 | pub use fidelity::FidelityBond; 20 | pub(crate) use fidelity::{fidelity_redeemscript, FidelityError}; 21 | pub use rpc::RPCConfig; 22 | pub use spend::Destination; 23 | pub(crate) use swapcoin::{ 24 | IncomingSwapCoin, OutgoingSwapCoin, SwapCoin, WalletSwapCoin, WatchOnlySwapCoin, 25 | }; 26 | pub(crate) use swapcoin2::{IncomingSwapCoinV2, OutgoingSwapCoinV2}; 27 | -------------------------------------------------------------------------------- /src/taker/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines a Coinswap Taker Client. 2 | //! 3 | //! This also contains the entire swap workflow as major decision makings are involved for the Taker. Makers are 4 | //! simple request-response servers. The Taker handles all the necessary communications between one or many makers to route the swap across various makers. Description of 5 | //! protocol workflow is described in the [protocol between takers and makers](https://github.com/citadel-tech/Coinswap-Protocol-Specification/blob/main/v1/3_protocol-flow.md) 6 | 7 | pub mod api; 8 | /// Taker API 2.0 - Taproot-based coinswap implementation 9 | pub mod api2; 10 | mod config; 11 | pub mod error; 12 | pub mod offers; 13 | mod routines; 14 | 15 | pub use self::api::TakerBehavior; 16 | pub use api::{SwapParams, Taker}; 17 | pub use api2::Taker as TaprootTaker; 18 | pub use config::TakerConfig; 19 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Build Docker images 17 | run: ./docker-setup build 18 | 19 | - name: Test makerd 20 | run: docker run --rm coinswap/coinswap:latest makerd --help 21 | 22 | - name: Test taker 23 | run: docker run --rm coinswap/coinswap:latest taker --help 24 | 25 | - name: Start Coinswap stack 26 | run: ./docker-setup start --default 27 | 28 | - name: Wait for services to be healthy 29 | run: | 30 | echo "Waiting for services to start..." 31 | sleep 30 32 | 33 | - name: Check service status 34 | run: ./docker-setup status 35 | 36 | - name: Stop Coinswap stack 37 | run: ./docker-setup stop -------------------------------------------------------------------------------- /src/maker/mod.rs: -------------------------------------------------------------------------------- 1 | //! The Coinswap Maker. 2 | //! 3 | //! A Maker server that acts as a swap service provider. 4 | //! It can be run in an unix/mac system with local access to Bitcoin Core RPC. 5 | //! 6 | //! Maker server responds to RPC requests via `maker-cli` app, which is used as an 7 | //! operating tool for the server. 8 | //! 9 | //! Default Ports: 10 | //! 6102: Client connection for swaps. 11 | //! 6103: RPC Connection for operations. 12 | 13 | mod api; 14 | mod api2; 15 | mod config; 16 | mod error; 17 | mod handlers; 18 | mod handlers2; 19 | mod rpc; 20 | mod server; 21 | mod server2; 22 | 23 | pub use api::{Maker, MakerBehavior}; 24 | pub use error::MakerError; 25 | pub use rpc::{RpcMsgReq, RpcMsgResp}; 26 | pub use server::start_maker_server; 27 | 28 | // Taproot protocol exports 29 | pub use api2::Maker as TaprootMaker; 30 | #[cfg(feature = "integration-test")] 31 | pub use api2::MakerBehavior as TaprootMakerBehavior; 32 | pub use server2::start_maker_server_taproot; 33 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: test 8 | 9 | jobs: 10 | test_with_codecov: 11 | name: Run tests with coverage reporting 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install Rust toolchain 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | components: llvm-tools-preview 21 | 22 | - name: Install cargo-llvm-cov 23 | uses: taiki-e/install-action@cargo-llvm-cov 24 | 25 | - name: Install cargo-nextest 26 | uses: taiki-e/install-action@nextest 27 | 28 | - name: Run cargo-llvm-cov 29 | run: | 30 | mkdir -p ./coverage/reports/ 31 | cargo llvm-cov nextest --lib --bins --tests --fail-fast --features integration-test --no-capture 32 | cargo llvm-cov report --cobertura --output-path ./coverage/reports/cobertura.xml 33 | 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | file: ./coverage/reports/cobertura.xml 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: build 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | rust: [stable, nightly] 16 | features: [default, integration-test] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Generate cache key 22 | run: echo "${{ runner.os }}-${{ matrix.rust }}-${{ matrix.features }}" | tee .cache_key 23 | 24 | - name: cache 25 | uses: actions/cache@v4 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | target 31 | key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 32 | 33 | - name: Set default toolchain 34 | run: rustup default ${{ matrix.rust }} 35 | 36 | - name: Set profile 37 | run: rustup set profile minimal 38 | 39 | - name: Update toolchain 40 | run: rustup update 41 | 42 | - name: Build for OS 43 | run: cargo build --features=${{ matrix.features }} 44 | -------------------------------------------------------------------------------- /docs/bitcoin.conf: -------------------------------------------------------------------------------- 1 | [signet] 2 | # Signet network configuration for running Coinswap Taker and Maker 3 | # Mutinynet default signet parameters 4 | signetchallenge=512102f7561d208dd9ae99bf497273e16f389bdbd6c4742ddb8e6b216e64fa2928ad8f51ae 5 | addnode=45.79.52.207:38333 6 | dnsseed=0 7 | signetblocktime=30 8 | 9 | # RPC Configurations for Coinswap operations 10 | server=1 11 | rpcuser=user 12 | rpcpassword=password 13 | rpcport=38332 14 | rpcbind=127.0.0.1 15 | rpcallowip=127.0.0.1 16 | 17 | # ZMQ Configurations for real-time transaction and block notifications 18 | # Needed for the Watchers. 19 | zmqpubrawblock=tcp://127.0.0.1:28332 20 | zmqpubrawtx=tcp://127.0.0.1:28332 21 | 22 | # Required indexes for faster wallet sync 23 | txindex=1 24 | blockfilterindex=1 25 | 26 | [regtest] 27 | # Regtest network configurations for running Coinswap Taker and Maker 28 | fallbackfee=0.00001000 29 | 30 | # RPC Configurations for Coinswap operations 31 | server=1 32 | rpcuser=user 33 | rpcpassword=password 34 | rpcport=18442 35 | rpcbind=127.0.0.1 36 | rpcallowip=127.0.0.1 37 | 38 | # ZMQ Configurations for real-time transaction and block notifications 39 | # Needed for the Watchers. 40 | zmqpubrawblock=tcp://127.0.0.1:28332 41 | zmqpubrawtx=tcp://127.0.0.1:28332 42 | 43 | # Required indexes for faster wallet sync 44 | txindex=1 45 | blockfilterindex=1 46 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Coinswap Dockerfile 2 | # Contains makerd, maker-cli, and taker 3 | 4 | ## --- Builder Stage --- 5 | FROM rust:1.90-alpine3.20 AS builder 6 | 7 | RUN apk add --no-cache \ 8 | build-base \ 9 | cmake \ 10 | git \ 11 | curl \ 12 | pkgconfig \ 13 | openssl-dev \ 14 | sqlite-dev \ 15 | sqlite-static \ 16 | zeromq-dev 17 | 18 | WORKDIR /usr/src/coinswap 19 | COPY . . 20 | RUN --mount=type=cache,target=/root/.cargo/registry \ 21 | --mount=type=cache,target=/root/.cargo/git \ 22 | cargo build --release 23 | 24 | ## --- Runtime Stage --- 25 | FROM alpine:3.20 26 | 27 | RUN --mount=type=cache,target=/var/cache/apk \ 28 | apk add --no-cache \ 29 | ca-certificates \ 30 | openssl \ 31 | libgcc \ 32 | sqlite \ 33 | zeromq 34 | 35 | RUN adduser -D -u 1001 coinswap 36 | 37 | RUN mkdir -p /app/bin /home/coinswap/.coinswap && \ 38 | chown -R coinswap:coinswap /app /home/coinswap 39 | 40 | COPY --from=builder --chown=coinswap:coinswap /usr/src/coinswap/target/release/makerd /app/bin/ 41 | COPY --from=builder --chown=coinswap:coinswap /usr/src/coinswap/target/release/maker-cli /app/bin/ 42 | COPY --from=builder --chown=coinswap:coinswap /usr/src/coinswap/target/release/taker /app/bin/ 43 | 44 | RUN chmod +x /app/bin/* 45 | 46 | USER coinswap 47 | WORKDIR /app 48 | 49 | ENV PATH="/app/bin:$PATH" 50 | 51 | ENV COINSWAP_DATA_DIR="/home/coinswap/.coinswap" 52 | 53 | 54 | 55 | VOLUME ["/home/coinswap/.coinswap"] 56 | 57 | CMD ["makerd"] 58 | -------------------------------------------------------------------------------- /git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Check Rust formatting and automatically correct it 4 | echo "Auto-correcting code style with rustfmt..." 5 | rustup override set nightly 6 | cargo fmt --all -- --check 7 | 8 | # Check Clippy lints 9 | echo "Checking code quality with Clippy..." 10 | if ! cargo clippy --all-features --lib --bins --tests -- -D warnings || ! cargo clippy --examples -- -D warnings; then 11 | echo "Clippy issues detected." 12 | echo "Please fix the Clippy issues before committing." 13 | exit 1 14 | fi 15 | # Check if cargo-hack is installed 16 | if ! command -v cargo-hack >/dev/null 2>&1; then 17 | echo "cargo-hack is not installed. Installing..." 18 | cargo install cargo-hack 19 | fi 20 | # Check for feature combinations 21 | echo "Checking feature combinations with cargo-hack..." 22 | if ! cargo hack --feature-powerset check; then 23 | echo "Feature combination issues detected" 24 | echo "Please fix the issues before committing." 25 | exit 1 26 | fi 27 | 28 | # Check for documentations 29 | echo "Checking doc nits..." 30 | if ! RUSTDOCFLAGS="-D warnings" cargo +nightly doc --all-features --document-private-items --no-deps; then 31 | echo "Documentation issues detected" 32 | echo "Please fix the issues before committing." 33 | exit 1 34 | fi 35 | 36 | # Check for unit tests 37 | echo "Checking Unit Tests..." 38 | if ! cargo test -q; then 39 | echo "unit test failed" 40 | echo "Please fix the issues before committing." 41 | exit 1 42 | fi 43 | 44 | 45 | exit 0 -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: lint 8 | 9 | jobs: 10 | fmt: 11 | name: rust fmt 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Set profile 18 | run: rustup set profile minimal 19 | 20 | - name: Install nightly toolchain 21 | run: rustup toolchain install nightly 22 | 23 | - name: Install stable toolchain 24 | run: rustup toolchain install stable 25 | 26 | - name: Add rustfmt 27 | run: rustup component add rustfmt --toolchain nightly 28 | 29 | - name: Add clippy 30 | run: rustup component add clippy --toolchain stable 31 | 32 | - name: Update toolchain 33 | run: rustup update 34 | 35 | - name: Check fmt 36 | run: cargo +nightly fmt --all -- --check 37 | 38 | - name: Clippy (main codebase with all features) 39 | run: cargo +stable clippy --all-features --lib --bins --tests -- -D warnings 40 | 41 | - name: Clippy (examples with default features only) 42 | run: cargo +stable clippy --examples -- -D warnings 43 | 44 | # - name: cargo install cargo-hack 45 | # uses: taiki-e/install-action@cargo-hack 46 | 47 | # - name: cargo hack 48 | # run: cargo hack --feature-powerset check 49 | 50 | - name: Check docs 51 | env: 52 | RUSTDOCFLAGS: "-D warnings" 53 | run: cargo +nightly doc --all-features --document-private-items --no-deps -------------------------------------------------------------------------------- /docker/Dockerfile.bitcoin-mutinynet: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | ARG TARGETARCH 4 | ARG TAG 5 | ARG FILENAME_AMD64 6 | ARG SHA256_AMD64 7 | ARG FILENAME_ARM64 8 | ARG SHA256_ARM64 9 | 10 | # Install dependencies 11 | RUN apt-get update && \ 12 | apt-get install -y curl ca-certificates && \ 13 | rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /tmp 16 | 17 | # Download and verify binary based on architecture 18 | RUN if [ "$TARGETARCH" = "amd64" ]; then \ 19 | FILENAME=$FILENAME_AMD64; \ 20 | SHA256=$SHA256_AMD64; \ 21 | elif [ "$TARGETARCH" = "arm64" ]; then \ 22 | FILENAME=$FILENAME_ARM64; \ 23 | SHA256=$SHA256_ARM64; \ 24 | else \ 25 | echo "Unsupported architecture: $TARGETARCH"; exit 1; \ 26 | fi && \ 27 | echo "Downloading $FILENAME..." && \ 28 | curl -L -O "https://github.com/benthecarman/bitcoin/releases/download/${TAG}/${FILENAME}" && \ 29 | echo "${SHA256} ${FILENAME}" | sha256sum -c - && \ 30 | tar -xzvf "$FILENAME" && \ 31 | # Find and move binaries (directory name changes with commit hash) 32 | find . -name bitcoind -type f -exec cp {} /usr/local/bin/ \; && \ 33 | find . -name bitcoin-cli -type f -exec cp {} /usr/local/bin/ \; && \ 34 | rm -rf * 35 | 36 | # Create user 37 | RUN useradd -m -u 1000 bitcoin 38 | 39 | # Setup directories 40 | RUN mkdir -p /home/bitcoin/.bitcoin && \ 41 | chown -R bitcoin:bitcoin /home/bitcoin/.bitcoin 42 | 43 | # Switch to user 44 | USER bitcoin 45 | WORKDIR /home/bitcoin 46 | 47 | # Expose ports (Signet, Regtest, ZMQ) 48 | EXPOSE 38332 38333 18442 18444 28332 49 | 50 | CMD ["bitcoind"] 51 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Publish 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | release: 7 | types: [ published ] 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | # set up QEMU for multi-platform builds (linux/amd64, linux/arm64) 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3 26 | 27 | - name: Log into Docker Hub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Extract Docker metadata 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: ${{ secrets.DOCKERHUB_USERNAME }}/coinswap 38 | tags: | 39 | # Tag for master branch 40 | type=ref,event=branch 41 | # Tag for releases (e.g. v1.0.0 -> 1.0.0) 42 | type=semver,pattern={{version}} 43 | # Tag 'latest' only on release 44 | type=raw,value=latest,enable=${{ github.event_name == 'release' }} 45 | 46 | - name: Build and push Docker image 47 | uses: docker/build-push-action@v5 48 | with: 49 | context: . 50 | file: docker/Dockerfile 51 | platforms: linux/amd64,linux/arm64 52 | push: true 53 | tags: ${{ steps.meta.outputs.tags }} 54 | labels: ${{ steps.meta.outputs.labels }} 55 | cache-from: type=gha 56 | cache-to: type=gha,mode=max 57 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-bitcoin.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Bitcoin Mutinynet 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docker/bitcoin-mutinynet.version' 7 | - 'docker/Dockerfile.bitcoin-mutinynet' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-and-push: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Read version info 18 | id: version 19 | run: | 20 | # Read the properties file 21 | while IFS='=' read -r key value; do 22 | echo "$key=$value" >> $GITHUB_OUTPUT 23 | done < docker/bitcoin-mutinynet.version 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Log into Docker Hub 32 | uses: docker/login-action@v3 33 | with: 34 | username: ${{ secrets.DOCKERHUB_USERNAME }} 35 | password: ${{ secrets.DOCKERHUB_TOKEN }} 36 | 37 | - name: Build and push 38 | uses: docker/build-push-action@v5 39 | with: 40 | context: . 41 | file: docker/Dockerfile.bitcoin-mutinynet 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: | 45 | ${{ secrets.DOCKERHUB_USERNAME }}/bitcoin-mutinynet:${{ steps.version.outputs.TAG }} 46 | ${{ secrets.DOCKERHUB_USERNAME }}/bitcoin-mutinynet:latest 47 | build-args: | 48 | TAG=${{ steps.version.outputs.TAG }} 49 | FILENAME_AMD64=${{ steps.version.outputs.FILENAME_AMD64 }} 50 | SHA256_AMD64=${{ steps.version.outputs.SHA256_AMD64 }} 51 | FILENAME_ARM64=${{ steps.version.outputs.FILENAME_ARM64 }} 52 | SHA256_ARM64=${{ steps.version.outputs.SHA256_ARM64 }} 53 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coinswap" 3 | version = "0.1.3" 4 | authors = ["Developers at Citadel-Tech"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | description = "Functioning, minimal-viable binaries and libraries to perform a trustless, p2p Maxwell-Belcher Coinswap Protocol" 10 | license = "MIT OR Apache-2.0" 11 | documentation = "https://docs.rs/coinswap" 12 | homepage = "https://github.com/citadel-tech/coinswap" 13 | repository = "https://github.com/citadel-tech/coinswap" 14 | categories = ["Bitcoin", "Atomic Swap", "HTLC"] 15 | keywords = ["bitcoin", "HTLC", "coinswap"] 16 | 17 | [dependencies] 18 | bip39 = { version = "2.1.0", features = ["rand"] } 19 | bitcoin = "0.32" 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_json = "1.0" 22 | serde_cbor = "0.11.2" 23 | log = "^0.4" 24 | dirs = "3.0.1" 25 | socks = "0.3.4" 26 | clap = { version = "3.2.22", features = ["derive"] } 27 | bitcoind = "0.36" 28 | log4rs = "1.3.0" 29 | chrono = { version = "0.4.40", features = ["serde"] } 30 | secp256k1 = {git = "https://github.com/jlest01/rust-secp256k1.git", branch = "musig2-module", features = ["rand", "global-context"]} 31 | minreq = { version = "2.12.0", features = ["https"] } 32 | pbkdf2 = { version = "0.12", features = ["simple"] } 33 | aes-gcm = "0.10.3" 34 | sha2 = "0.10.9" 35 | rust-coinselect = "0.1.6" 36 | crossterm = "0.29.0" 37 | zmq = "0.10.0" 38 | 39 | [dev-dependencies] 40 | flate2 = {version = "1.0.35"} 41 | tar = {version = "0.4.43"} 42 | 43 | #Empty default feature set, (helpful to generalise in github actions) 44 | [features] 45 | default = [] 46 | # The following feature set is in response to the issue described at https://github.com/rust-lang/rust/issues/45599 47 | # Only used for running the integration tests 48 | integration-test = [] 49 | 50 | [patch.crates-io] 51 | secp256k1 = { git = "https://github.com/jlest01/rust-secp256k1.git", branch = "musig2-module" } 52 | -------------------------------------------------------------------------------- /src/watch_tower/service.rs: -------------------------------------------------------------------------------- 1 | //! Public watchtower service for sending commands to and receiving events from the watcher. 2 | 3 | use bitcoin::OutPoint; 4 | use std::sync::{ 5 | mpsc::{Receiver, Sender}, 6 | Arc, Mutex, 7 | }; 8 | 9 | use crate::watch_tower::watcher::{WatcherCommand, WatcherEvent}; 10 | 11 | /// Client-facing service for sending watcher commands and receiving events. 12 | pub struct WatchService { 13 | tx: Sender, 14 | rx: Arc>>, 15 | } 16 | 17 | impl WatchService { 18 | /// Creates a new service from the given command sender and event receiver. 19 | pub fn new(tx: Sender, rx: Receiver) -> Self { 20 | Self { 21 | tx, 22 | rx: Arc::new(Mutex::new(rx)), 23 | } 24 | } 25 | 26 | /// Registers an outpoint to be monitored for future spends. 27 | pub fn register_watch_request(&self, outpoint: OutPoint) { 28 | let _ = self 29 | .tx 30 | .send(WatcherCommand::RegisterWatchRequest { outpoint }); 31 | } 32 | 33 | /// Queries whether a previously registered outpoint has been spent. 34 | pub fn watch_request(&self, outpoint: OutPoint) { 35 | let _ = self.tx.send(WatcherCommand::WatchRequest { outpoint }); 36 | } 37 | 38 | /// Stops monitoring an outpoint by removing its watch entry from the registry. 39 | pub fn unwatch(&self, outpoint: OutPoint) { 40 | let _ = self.tx.send(WatcherCommand::Unwatch { outpoint }); 41 | } 42 | 43 | /// Attempts a non-blocking receive; returns `None` if no event is pending. 44 | pub fn poll_event(&self) -> Option { 45 | self.rx.lock().ok()?.try_recv().ok() 46 | } 47 | 48 | /// Blocks until the next watcher event arrives. 49 | pub fn wait_for_event(&self) -> Option { 50 | self.rx.lock().ok()?.recv().ok() 51 | } 52 | 53 | /// Requests the list of maker addresses. 54 | pub fn request_maker_address(&self) { 55 | _ = self.tx.send(WatcherCommand::MakerAddress); 56 | } 57 | 58 | /// Signals the watcher to shut down gracefully. 59 | pub fn shutdown(&self) { 60 | let _ = self.tx.send(WatcherCommand::Shutdown); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/watch_tower/zmq_backend.rs: -------------------------------------------------------------------------------- 1 | //! ZMQ subscriber for Bitcoin node notifications. 2 | 3 | /// Reference to a block received via ZMQ. 4 | #[derive(Debug, Clone)] 5 | pub struct BlockRef { 6 | /// Height of the block if known / 0 when unavailable. 7 | pub height: u64, 8 | /// Raw block hash. 9 | pub hash: Vec, 10 | } 11 | 12 | /// Events generated from the ZMQ subscriber. 13 | #[derive(Debug, Clone)] 14 | pub enum BackendEvent { 15 | /// Notifies when a transaction is seen (mempool or block) and includes the raw bytes. 16 | TxSeen { 17 | /// Raw transaction bytes. 18 | raw_tx: Vec, 19 | }, 20 | /// Notifies when chain tip is updated with rawblock data. 21 | BlockConnected(BlockRef), 22 | } 23 | 24 | /// ZMQ backend used by the watcher to subscribe to node notifications. 25 | pub struct ZmqBackend { 26 | socket: zmq::Socket, 27 | } 28 | 29 | impl ZmqBackend { 30 | /// Connects to a ZMQ endpoint and subscribes to rawtx and rawblock topics. 31 | pub fn new(endpoint: &str) -> Self { 32 | let ctx = zmq::Context::new(); 33 | let socket = ctx.socket(zmq::SUB).expect("socket"); 34 | 35 | socket.connect(endpoint).expect("connect"); 36 | 37 | // Subscribe to both topics 38 | socket.set_subscribe(b"rawtx").expect("subscribe rawtx"); 39 | socket 40 | .set_subscribe(b"rawblock") 41 | .expect("subscribe rawblock"); 42 | 43 | Self { socket } 44 | } 45 | 46 | fn recv_event(&self) -> Option<(String, Vec)> { 47 | let msg = self.socket.recv_multipart(zmq::DONTWAIT).ok()?; 48 | if msg.len() < 2 { 49 | return None; 50 | } 51 | 52 | let topic = String::from_utf8_lossy(&msg[0]).to_string(); 53 | let payload = msg[1].clone(); 54 | Some((topic, payload)) 55 | } 56 | } 57 | 58 | impl ZmqBackend { 59 | /// Non-blocking poll for the next backend event. 60 | pub fn poll(&mut self) -> Option { 61 | let (topic, payload) = self.recv_event()?; 62 | 63 | match topic.as_str() { 64 | "rawtx" => Some(BackendEvent::TxSeen { raw_tx: payload }), 65 | "rawblock" => Some(BackendEvent::BlockConnected(BlockRef { 66 | height: 0, 67 | hash: payload, 68 | })), 69 | _ => None, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | tor: 4 | image: osminogin/tor-simple 5 | container_name: tor 6 | restart: unless-stopped 7 | ports: 8 | - "${TOR_SOCKS_PORT:-19050}:${TOR_SOCKS_PORT:-19050}" 9 | - "${TOR_CONTROL_PORT:-19051}:${TOR_CONTROL_PORT:-19051}" 10 | - "${MAKER_RPC_PORT:-6113}:${MAKER_RPC_PORT:-6113}" 11 | volumes: 12 | - ./torrc:/etc/tor/torrc:ro 13 | - tor-data:/var/lib/tor 14 | 15 | makerd: 16 | image: coinswap/coinswap:master 17 | container_name: coinswap-makerd 18 | network_mode: "service:tor" 19 | command: 20 | - sh 21 | - -c 22 | - | 23 | sleep 15 24 | mkdir -p /home/coinswap/.coinswap/maker 25 | printf '%s\n' \ 26 | "network_port = ${MAKER_NETWORK_PORT:-6112}" \ 27 | "rpc_port = ${MAKER_RPC_PORT:-6113}" \ 28 | "socks_port = ${TOR_SOCKS_PORT:-19050}" \ 29 | "control_port = ${TOR_CONTROL_PORT:-19051}" \ 30 | "tor_auth_password = ${TOR_AUTH_PASSWORD:-coinswap}" \ 31 | "min_swap_amount = ${MIN_SWAP_AMOUNT:-10000}" \ 32 | "fidelity_amount = ${FIDELITY_AMOUNT:-50000}" \ 33 | "fidelity_timelock = ${FIDELITY_TIMELOCK:-13104}" \ 34 | "connection_type = TOR" \ 35 | "base_fee = ${BASE_FEE:-100}" \ 36 | "amount_relative_fee_ppt = ${AMOUNT_RELATIVE_FEE_PPT:-1000}" \ 37 | > /home/coinswap/.coinswap/maker/config.toml 38 | makerd -w ${WALLET_NAME} -r ${BITCOIN_RPC_HOST:-172.81.178.3:48332} -a ${BITCOIN_RPC_AUTH:-user:password} --taproot 39 | volumes: 40 | - maker-data:/home/coinswap/.coinswap 41 | environment: 42 | - RUST_LOG=${RUST_LOG:-info} 43 | - TOR_SOCKS_PORT=${TOR_SOCKS_PORT:-19050} 44 | - TOR_CONTROL_PORT=${TOR_CONTROL_PORT:-19051} 45 | - TOR_AUTH_PASSWORD=${TOR_AUTH_PASSWORD:-coinswap} 46 | - MAKER_NETWORK_PORT=${MAKER_NETWORK_PORT:-6112} 47 | - MAKER_RPC_PORT=${MAKER_RPC_PORT:-6113} 48 | - MIN_SWAP_AMOUNT=${MIN_SWAP_AMOUNT:-10000} 49 | - FIDELITY_AMOUNT=${FIDELITY_AMOUNT:-50000} 50 | - FIDELITY_TIMELOCK=${FIDELITY_TIMELOCK:-13104} 51 | - BASE_FEE=${BASE_FEE:-100} 52 | - AMOUNT_RELATIVE_FEE_PPT=${AMOUNT_RELATIVE_FEE_PPT:-1000} 53 | - BITCOIN_RPC_HOST=${BITCOIN_RPC_HOST:-172.81.178.3:48332} 54 | - BITCOIN_RPC_AUTH=${BITCOIN_RPC_AUTH:-user:password} 55 | restart: unless-stopped 56 | 57 | volumes: 58 | tor-data: 59 | driver: local 60 | maker-data: 61 | driver: local 62 | -------------------------------------------------------------------------------- /docs/tor.md: -------------------------------------------------------------------------------- 1 | # **Tor Setup & Configuration Guide** 2 | 3 | This guide covers: 4 | - Installing Tor 5 | - Configuring Tor settings 6 | - Setting up a Hidden Service 7 | - Configuring the Control Port (with/without password) 8 | - Setting the SOCKS Port 9 | 10 | --- 11 | 12 | ## **1. Installing Tor** 13 | ### **Linux (Debian/Ubuntu)** 14 | ```bash 15 | sudo apt update 16 | sudo apt install tor -y 17 | ``` 18 | 19 | ### MacOS 20 | ```bash 21 | brew install tor 22 | ``` 23 | 24 | --- 25 | 26 | ## **2. Configuring Tor (`torrc` File)** 27 | ### **Locate & Edit `torrc`** 28 | ### Linux 29 | ```bash 30 | sudo nano /etc/tor/torrc 31 | ``` 32 | ### MacOS 33 | ```bash 34 | nano /opt/homebrew/etc/tor/torrc 35 | ``` 36 | 37 | --- 38 | 39 | ## **3. Configuring the SOCKS Proxy** 40 | Tor acts as a **SOCKS5 Proxy** for anonymous traffic. 41 | 42 | Add this to `torrc`: 43 | ```ini 44 | SOCKSPort 9050 45 | ``` 46 | Now, you can route applications through `127.0.0.1:9050`. 47 | 48 | To test it: 49 | ```bash 50 | sudo systemctl start tor 51 | curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/ 52 | ``` 53 | --- 54 | 55 | ## **4. Configuring Control Port** 56 | The **Control Port** allows applications to talk to Tor. 57 | ```ini 58 | ControlPort 9051 59 | ``` 60 | 61 | ### **Option 1: No Authentication (Not Recommended for Production)** 62 | ```ini 63 | CookieAuthentication 0 64 | ``` 65 | This allows unrestricted access—use it **only for testing**. 66 | 67 | ### **Option 2: Password Authentication (Recommended)** 68 | 1. Generate a hashed password: 69 | ```bash 70 | tor --hash-password "yourpassword" 71 | ``` 72 | Example output: 73 | ``` 74 | 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C 75 | ``` 76 | 2. Add it to `torrc`: 77 | ```ini 78 | HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C 79 | ``` 80 | 81 | ### **Option 3: Cookie Authentication** 82 | 1. Enable cookie authentication in `torrc`: 83 | ```ini 84 | ControlPort 9051 85 | CookieAuthentication 1 86 | CookieAuthFileGroupReadable 1 87 | DataDirectoryGroupReadable 1 88 | ``` 89 | 2. Restart Tor: 90 | ```bash 91 | sudo systemctl restart tor 92 | ``` 93 | 3. The **cookie file** is usually located at: 94 | ```bash 95 | /var/lib/tor/control_auth_cookie 96 | ``` 97 | 4. Use it in your applications for authentication. 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/watch_tower/watcher_error.rs: -------------------------------------------------------------------------------- 1 | //! Watchtower-related error types. 2 | 3 | /// Errors that can occur within the watchtower components. 4 | #[derive(Debug)] 5 | pub enum WatcherError { 6 | /// Internal server failure. 7 | ServerError, 8 | /// Failure in the mempool indexer. 9 | MempoolIndexerError, 10 | /// Graceful shutdown requested. 11 | Shutdown, 12 | /// Transaction or block parsing failed. 13 | ParsingError, 14 | /// Channel send failed. 15 | SendError, 16 | /// I/O error surfaced from filesystem operations. 17 | IOError(std::io::Error), 18 | /// RPC error from bitcoind. 19 | RPCError(bitcoind::bitcoincore_rpc::Error), 20 | /// Serialization/deserialization error for CBOR. 21 | SerdeCbor(serde_cbor::Error), 22 | /// Represents a general error with a descriptive message. 23 | General(String), 24 | } 25 | 26 | impl From for WatcherError { 27 | fn from(value: std::io::Error) -> Self { 28 | WatcherError::IOError(value) 29 | } 30 | } 31 | 32 | impl From for WatcherError { 33 | fn from(value: bitcoind::bitcoincore_rpc::Error) -> Self { 34 | WatcherError::RPCError(value) 35 | } 36 | } 37 | 38 | impl std::fmt::Display for WatcherError { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!(f, "{self:?}") 41 | } 42 | } 43 | 44 | impl From for WatcherError { 45 | fn from(value: serde_cbor::Error) -> Self { 46 | Self::SerdeCbor(value) 47 | } 48 | } 49 | 50 | impl WatcherError { 51 | /// Returns the underlying `ErrorKind` if the error wraps an I/O failure. 52 | pub fn io_error_kind(&self) -> Option { 53 | match self { 54 | WatcherError::IOError(e) => Some(e.kind()), 55 | _ => None, 56 | } 57 | } 58 | 59 | /// Returns a stable string identifier for the error variant. 60 | pub fn kind(&self) -> &'static str { 61 | match self { 62 | WatcherError::ServerError => "ServerError", 63 | WatcherError::MempoolIndexerError => "MempoolIndexerError", 64 | WatcherError::Shutdown => "Shutdown", 65 | WatcherError::ParsingError => "ParsingError", 66 | WatcherError::SendError => "SendError", 67 | WatcherError::IOError(_) => "IOError", 68 | WatcherError::RPCError(_) => "RPCError", 69 | WatcherError::SerdeCbor(_) => "SerdeCbor", 70 | WatcherError::General(_) => "General", 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! High-level network and protocol errors. 2 | 3 | use std::error::Error; 4 | 5 | /// Represents all possible network-related errors. 6 | #[derive(Debug)] 7 | pub enum NetError { 8 | /// Error originating from standard I/O operations. 9 | /// 10 | /// This variant wraps a [`std::io::Error`] to provide details about I/O failures. 11 | IO(std::io::Error), 12 | 13 | /// Error indicating the end of a file was reached unexpectedly. 14 | ReachedEOF, 15 | 16 | /// Error indicating that a connection attempt timed out. 17 | ConnectionTimedOut, 18 | 19 | /// Error caused by an invalid network address. 20 | InvalidNetworkAddress, 21 | 22 | /// Error related to CBOR (Concise Binary Object Representation) serialization or deserialization. 23 | /// 24 | /// This variant wraps a [`serde_cbor::Error`] to provide details about the issue. 25 | Cbor(serde_cbor::Error), 26 | 27 | /// Error indicating an invalid CLI application network. 28 | InvalidAppNetwork, 29 | } 30 | 31 | impl std::fmt::Display for NetError { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | write!(f, "{self:?}") 34 | } 35 | } 36 | 37 | impl Error for NetError { 38 | fn source(&self) -> Option<&(dyn Error + 'static)> { 39 | None 40 | } 41 | } 42 | 43 | impl From for NetError { 44 | fn from(value: std::io::Error) -> Self { 45 | Self::IO(value) 46 | } 47 | } 48 | 49 | impl From for NetError { 50 | fn from(value: serde_cbor::Error) -> Self { 51 | Self::Cbor(value) 52 | } 53 | } 54 | 55 | /// Represents various errors that can occur while doing Fee Estimation 56 | #[derive(Debug)] 57 | pub enum FeeEstimatorError { 58 | /// Error from Bitcoin Core RPC 59 | BitcoinRpc(bitcoind::bitcoincore_rpc::Error), 60 | /// Error while receiving or parsing an HTTP Response 61 | HttpError(minreq::Error), 62 | /// Missing expected data in API response 63 | MissingData(String), 64 | /// No wallet configured for Bitcoin Core estimates 65 | NoWallet, 66 | /// No sources available or all sources failed 67 | NoFeeSources, 68 | /// A scoped thread panicked 69 | ThreadError, 70 | } 71 | 72 | impl From for FeeEstimatorError { 73 | fn from(err: bitcoind::bitcoincore_rpc::Error) -> Self { 74 | FeeEstimatorError::BitcoinRpc(err) 75 | } 76 | } 77 | 78 | impl From for FeeEstimatorError { 79 | fn from(err: minreq::Error) -> Self { 80 | FeeEstimatorError::HttpError(err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/taker/error.rs: -------------------------------------------------------------------------------- 1 | //! All Taker-related errors. 2 | use crate::{ 3 | error::NetError, protocol::error::ProtocolError, utill::TorError, wallet::WalletError, 4 | watch_tower::watcher_error::WatcherError, 5 | }; 6 | use bitcoin::address::ParseError; 7 | 8 | /// Represents errors that can occur during Taker operations. 9 | /// 10 | /// This enum covers a range of errors related to I/O, wallet operations, network communication, 11 | /// and other Taker-specific scenarios. 12 | #[derive(Debug)] 13 | pub enum TakerError { 14 | /// Standard input/output error. 15 | IO(std::io::Error), 16 | /// Error indicating contracts were broadcasted prematurely. 17 | /// Contains a list of the transaction IDs of the broadcasted contracts. 18 | ContractsBroadcasted(Vec), 19 | /// Error indicating there are not enough makers available in the offer book. 20 | NotEnoughMakersInOfferBook, 21 | /// Error related to wallet operations. 22 | Wallet(WalletError), 23 | /// Error related to network operations. 24 | Net(NetError), 25 | /// Error indicating the send amount was not set for a transaction. 26 | SendAmountNotSet, 27 | /// Error indicating a timeout while waiting for the funding transaction. 28 | FundingTxWaitTimeOut, 29 | /// Error deserializing data, typically related to CBOR-encoded data. 30 | Deserialize(String), 31 | /// Error indicating an MPSC channel failure. 32 | /// 33 | /// This error occurs during internal thread communication. 34 | MPSC(String), 35 | /// Tor error 36 | TorError(TorError), 37 | /// Error relating to Bitcoin Address Parsing. 38 | AddressParseError(ParseError), 39 | /// General error with a custom message 40 | General(String), 41 | /// Watcher Service Error 42 | Watcher(WatcherError), 43 | } 44 | 45 | impl From for TakerError { 46 | fn from(value: TorError) -> Self { 47 | Self::TorError(value) 48 | } 49 | } 50 | 51 | impl From for TakerError { 52 | fn from(value: serde_cbor::Error) -> Self { 53 | Self::Deserialize(value.to_string()) 54 | } 55 | } 56 | 57 | impl From for TakerError { 58 | fn from(value: serde_json::Error) -> Self { 59 | Self::Deserialize(value.to_string()) 60 | } 61 | } 62 | 63 | impl From for TakerError { 64 | fn from(value: WalletError) -> Self { 65 | Self::Wallet(value) 66 | } 67 | } 68 | 69 | impl From for TakerError { 70 | fn from(value: std::io::Error) -> Self { 71 | Self::IO(value) 72 | } 73 | } 74 | 75 | impl From for TakerError { 76 | fn from(value: NetError) -> Self { 77 | Self::Net(value) 78 | } 79 | } 80 | 81 | impl From for TakerError { 82 | fn from(value: ProtocolError) -> Self { 83 | Self::Wallet(value.into()) 84 | } 85 | } 86 | 87 | impl From for TakerError { 88 | fn from(value: std::sync::mpsc::RecvError) -> Self { 89 | Self::MPSC(value.to_string()) 90 | } 91 | } 92 | 93 | impl From> for TakerError { 94 | fn from(value: std::sync::mpsc::SendError) -> Self { 95 | Self::MPSC(value.to_string()) 96 | } 97 | } 98 | 99 | impl From for TakerError { 100 | fn from(value: ParseError) -> Self { 101 | Self::AddressParseError(value) 102 | } 103 | } 104 | 105 | impl From for TakerError { 106 | fn from(value: WatcherError) -> Self { 107 | Self::Watcher(value) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/maker/error.rs: -------------------------------------------------------------------------------- 1 | //! All Maker related errors. 2 | 3 | use std::sync::{MutexGuard, PoisonError, RwLockReadGuard, RwLockWriteGuard}; 4 | 5 | use bitcoin::secp256k1; 6 | 7 | use crate::{ 8 | error::NetError, protocol::error::ProtocolError, utill::TorError, wallet::WalletError, 9 | watch_tower::watcher_error::WatcherError, 10 | }; 11 | 12 | use super::MakerBehavior; 13 | 14 | /// Enum to handle Maker-related errors. 15 | /// 16 | /// This enum encapsulates different types of errors that can occur while interacting 17 | /// with the maker. Each variant represents a specific category of error and provides 18 | /// relevant details to help diagnose issues. 19 | #[derive(Debug)] 20 | pub enum MakerError { 21 | /// Represents a standard IO error. 22 | IO(std::io::Error), 23 | /// Represents an unexpected message received during communication. 24 | UnexpectedMessage { 25 | /// The expected message. 26 | expected: String, 27 | /// The received message. 28 | got: String, 29 | }, 30 | /// Represents a general error with a static message. 31 | General(&'static str), 32 | /// Represents a mutex poisoning error. 33 | MutexPossion, 34 | /// Represents an error related to secp256k1 operations. 35 | Secp(secp256k1::Error), 36 | /// Represents an error related to wallet operations. 37 | Wallet(WalletError), 38 | /// Represents a network-related error. 39 | Net(NetError), 40 | /// Represents an error triggered by special maker behavior. 41 | SpecialBehaviour(MakerBehavior), 42 | /// Represents a protocol-related error. 43 | Protocol(ProtocolError), 44 | /// Tor Error. 45 | TorError(TorError), 46 | /// Watcher Service Error 47 | Watcher(WatcherError), 48 | } 49 | 50 | impl From for MakerError { 51 | fn from(value: TorError) -> Self { 52 | Self::TorError(value) 53 | } 54 | } 55 | 56 | impl From for MakerError { 57 | fn from(value: std::io::Error) -> Self { 58 | Self::IO(value) 59 | } 60 | } 61 | 62 | impl From for MakerError { 63 | fn from(value: serde_cbor::Error) -> Self { 64 | Self::Net(NetError::Cbor(value)) 65 | } 66 | } 67 | 68 | impl<'a, T> From>> for MakerError { 69 | fn from(_: PoisonError>) -> Self { 70 | Self::MutexPossion 71 | } 72 | } 73 | 74 | impl<'a, T> From>> for MakerError { 75 | fn from(_: PoisonError>) -> Self { 76 | Self::MutexPossion 77 | } 78 | } 79 | 80 | impl<'a, T> From>> for MakerError { 81 | fn from(_: PoisonError>) -> Self { 82 | Self::MutexPossion 83 | } 84 | } 85 | 86 | impl From for MakerError { 87 | fn from(value: secp256k1::Error) -> Self { 88 | Self::Secp(value) 89 | } 90 | } 91 | 92 | impl From for MakerError { 93 | fn from(value: ProtocolError) -> Self { 94 | Self::Protocol(value) 95 | } 96 | } 97 | 98 | impl From for MakerError { 99 | fn from(value: WalletError) -> Self { 100 | Self::Wallet(value) 101 | } 102 | } 103 | 104 | impl From for MakerError { 105 | fn from(value: MakerBehavior) -> Self { 106 | Self::SpecialBehaviour(value) 107 | } 108 | } 109 | 110 | impl From for MakerError { 111 | fn from(value: NetError) -> Self { 112 | Self::Net(value) 113 | } 114 | } 115 | 116 | impl From for MakerError { 117 | fn from(value: WatcherError) -> Self { 118 | Self::Watcher(value) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/protocol/musig_interface.rs: -------------------------------------------------------------------------------- 1 | //! This module provides an interface for the musig2 protocol. 2 | use crate::protocol::error::ProtocolError; 3 | 4 | use super::{ 5 | musig2::{aggregate_partial_signatures, generate_partial_signature}, 6 | *, 7 | }; 8 | use musig2::{generate_new_nonce_pair, get_aggregated_pubkey}; 9 | 10 | //TODOS 11 | //- Use macro for converting between secp256k1 and bitcoin::secp256k1 12 | //- Use named imports for secp256k1 and bitcoin::secp256k1 13 | 14 | /// Aggregates the public keys 15 | pub fn get_aggregated_pubkey_compat( 16 | pubkey1: bitcoin::secp256k1::PublicKey, 17 | pubkey2: bitcoin::secp256k1::PublicKey, 18 | ) -> Result { 19 | let s_pubkey1 = pubkey1.serialize(); 20 | let s_pubkey2 = pubkey2.serialize(); 21 | let pubkey1 = secp256k1::PublicKey::from_slice(&s_pubkey1)?; 22 | let pubkey2 = secp256k1::PublicKey::from_slice(&s_pubkey2)?; 23 | let agg_pubkey = get_aggregated_pubkey(&pubkey1, &pubkey2); 24 | let s_agg_pubkey = agg_pubkey.serialize(); 25 | 26 | Ok(bitcoin::secp256k1::XOnlyPublicKey::from_slice( 27 | &s_agg_pubkey, 28 | )?) 29 | } 30 | 31 | /// Generates a new nonce pair 32 | pub fn generate_new_nonce_pair_compat( 33 | nonce_pubkey: bitcoin::secp256k1::PublicKey, 34 | ) -> Result<(secp256k1::musig::SecretNonce, secp256k1::musig::PublicNonce), ProtocolError> { 35 | let nonce_pubkey = nonce_pubkey.serialize(); 36 | let nonce_pubkey = secp256k1::PublicKey::from_slice(&nonce_pubkey)?; 37 | // Convert bitcoin::secp256k1::Message to secp256k1::Message directly from bytes 38 | Ok(generate_new_nonce_pair(nonce_pubkey)) 39 | } 40 | 41 | /// get aggregated nonce 42 | pub fn get_aggregated_nonce_compat( 43 | nonces: &[&secp256k1::musig::PublicNonce], 44 | ) -> secp256k1::musig::AggregatedNonce { 45 | let secp = secp256k1::Secp256k1::new(); 46 | secp256k1::musig::AggregatedNonce::new(&secp, nonces) 47 | } 48 | 49 | /// Generates a partial signature 50 | pub fn generate_partial_signature_compat( 51 | message: bitcoin::secp256k1::Message, 52 | agg_nonce: &secp256k1::musig::AggregatedNonce, 53 | sec_nonce: secp256k1::musig::SecretNonce, 54 | keypair: bitcoin::secp256k1::Keypair, 55 | tap_tweak: bitcoin::secp256k1::Scalar, 56 | pubkey1: bitcoin::secp256k1::PublicKey, 57 | pubkey2: bitcoin::secp256k1::PublicKey, 58 | ) -> Result { 59 | let secp = secp256k1::Secp256k1::new(); 60 | let tap_tweak = tap_tweak.to_be_bytes(); 61 | let tap_tweak = secp256k1::Scalar::from_be_bytes(tap_tweak)?; 62 | let pubkey1 = pubkey1.serialize(); 63 | let pubkey2 = pubkey2.serialize(); 64 | let pubkey1 = secp256k1::PublicKey::from_slice(&pubkey1)?; 65 | let pubkey2 = secp256k1::PublicKey::from_slice(&pubkey2)?; 66 | let pubkeys = [&pubkey1, &pubkey2]; 67 | // Convert bitcoin::secp256k1::Message to secp256k1::Message directly from bytes 68 | let message_bytes = message.as_ref(); 69 | let message = secp256k1::Message::from_digest(*message_bytes); 70 | let keypair_secret = keypair.secret_bytes(); 71 | let secret_key = secp256k1::SecretKey::from_slice(&keypair_secret)?; 72 | let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key); 73 | generate_partial_signature(message, agg_nonce, sec_nonce, keypair, tap_tweak, &pubkeys) 74 | } 75 | 76 | /// Aggregates the partial signatures 77 | pub fn aggregate_partial_signatures_compat( 78 | message: bitcoin::secp256k1::Message, 79 | agg_nonce: secp256k1::musig::AggregatedNonce, 80 | tap_tweak: bitcoin::secp256k1::Scalar, 81 | partial_sigs: Vec<&secp256k1::musig::PartialSignature>, 82 | pubkey_1: bitcoin::secp256k1::PublicKey, 83 | pubkey2: bitcoin::secp256k1::PublicKey, 84 | ) -> Result { 85 | let tap_tweak = tap_tweak.to_be_bytes(); 86 | let tap_tweak = secp256k1::Scalar::from_be_bytes(tap_tweak)?; 87 | // Convert bitcoin::secp256k1::Message to secp256k1::Message directly from bytes 88 | let message_bytes = message.as_ref(); 89 | let message = secp256k1::Message::from_digest(*message_bytes); 90 | let pubkey1 = pubkey_1.serialize(); 91 | let pubkey2 = pubkey2.serialize(); 92 | let pubkey1 = secp256k1::PublicKey::from_slice(&pubkey1)?; 93 | let pubkey2 = secp256k1::PublicKey::from_slice(&pubkey2)?; 94 | aggregate_partial_signatures( 95 | message, 96 | agg_nonce, 97 | tap_tweak, 98 | &partial_sigs, 99 | &[&pubkey1, &pubkey2], 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/bin/makerd.rs: -------------------------------------------------------------------------------- 1 | use bitcoind::bitcoincore_rpc::Auth; 2 | use clap::Parser; 3 | use coinswap::{ 4 | maker::{ 5 | start_maker_server, start_maker_server_taproot, Maker, MakerBehavior, MakerError, 6 | TaprootMaker, 7 | }, 8 | utill::{parse_proxy_auth, setup_maker_logger}, 9 | wallet::RPCConfig, 10 | }; 11 | use std::{path::PathBuf, sync::Arc}; 12 | /// Coinswap Maker Server 13 | /// 14 | /// The server requires a Bitcoin Core RPC connection running in Testnet4. It requires some starting balance, around 50,000 sats for Fidelity + Swap Liquidity (suggested 50,000 sats). 15 | /// So topup with at least 0.001 BTC to start all the node processses. Suggested [faucet here] 16 | /// 17 | /// All server processes will start after the fidelity bond transaction is confirmed. This may take some time. Approx: 10 mins. 18 | /// Once the bond is confirmed, the server starts listening for incoming swap requests. As it performs swaps for clients, it keeps earning fees. 19 | /// 20 | /// The server is operated with the maker-cli app, for all basic wallet related operations. 21 | /// 22 | /// For more detailed usage information, please refer the [Maker Doc] 23 | /// 24 | /// This is early beta, and there are known and unknown bugs. Please report issues in the [Project Issue Board] 25 | #[derive(Parser, Debug)] 26 | #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), 27 | author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] 28 | struct Cli { 29 | /// Optional DNS data directory. Default value: "~/.coinswap/maker" 30 | #[clap(long, short = 'd')] 31 | data_directory: Option, 32 | /// Bitcoin Core RPC network address. 33 | #[clap( 34 | name = "ADDRESS:PORT", 35 | long, 36 | short = 'r', 37 | default_value = "127.0.0.1:38332" 38 | )] 39 | pub rpc: String, 40 | /// Bitcoin Core ZMQ address:port value 41 | #[clap( 42 | name = "ZMQ", 43 | long, 44 | short = 'z', 45 | default_value = "tcp://127.0.0.1:28332" 46 | )] 47 | pub zmq: String, 48 | /// Bitcoin Core RPC authentication string (username, password). 49 | #[clap( 50 | name = "USER:PASSWORD", 51 | short = 'a', 52 | long, 53 | value_parser = parse_proxy_auth, 54 | default_value = "user:password", 55 | )] 56 | pub auth: (String, String), 57 | #[clap(long, short = 't')] 58 | pub tor_auth: Option, 59 | /// Optional wallet name. If the wallet exists, load the wallet, else create a new wallet with the given name. Default: maker-wallet 60 | #[clap(name = "WALLET", long, short = 'w')] 61 | pub(crate) wallet_name: Option, 62 | /// Use experimental Taproot-based coinswap protocol 63 | #[clap(long)] 64 | pub taproot: bool, 65 | /// Optional Password for the encryption of the wallet. 66 | #[clap(name = "PASSWORD", long, short = 'p')] 67 | pub password: Option, 68 | } 69 | 70 | fn main() -> Result<(), MakerError> { 71 | let args = Cli::parse(); 72 | setup_maker_logger(log::LevelFilter::Info, args.data_directory.clone()); 73 | 74 | let rpc_config = RPCConfig { 75 | url: args.rpc, 76 | auth: Auth::UserPass(args.auth.0, args.auth.1), 77 | wallet_name: "random".to_string(), // we can put anything here as it will get updated in the init. 78 | }; 79 | 80 | if args.taproot { 81 | log::warn!("Using experimental Taproot-based coinswap protocol"); 82 | let maker = Arc::new(TaprootMaker::init( 83 | args.data_directory, 84 | args.wallet_name, 85 | Some(rpc_config), 86 | None, 87 | None, 88 | None, 89 | args.tor_auth.clone(), 90 | None, 91 | args.zmq, 92 | args.password, 93 | #[cfg(feature = "integration-test")] 94 | None, 95 | )?); 96 | 97 | start_maker_server_taproot(maker)?; 98 | } else { 99 | let maker = Arc::new(Maker::init( 100 | args.data_directory, 101 | args.wallet_name, 102 | Some(rpc_config), 103 | None, 104 | None, 105 | None, 106 | args.tor_auth, 107 | None, 108 | MakerBehavior::Normal, 109 | args.zmq, 110 | args.password, 111 | )?); 112 | 113 | start_maker_server(maker)?; 114 | } 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /tests/wallet_backup.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | mod test_framework; 3 | 4 | use std::{fs, path::PathBuf}; 5 | 6 | use bip39::rand; 7 | use bitcoin::{Address, Amount}; 8 | use bitcoind::{ 9 | bitcoincore_rpc::{self, Auth}, 10 | BitcoinD, 11 | }; 12 | use log::info; 13 | 14 | use coinswap::wallet::{RPCConfig, Wallet, WalletBackup}; 15 | 16 | use coinswap::security::{load_sensitive_struct_interactive, KeyMaterial, SerdeJson}; 17 | 18 | use test_framework::init_bitcoind; 19 | 20 | use crate::test_framework::{generate_blocks, send_to_address}; 21 | 22 | fn setup(test_name: String) -> (PathBuf, RPCConfig, PathBuf, BitcoinD, PathBuf) { 23 | let temp_dir = std::env::temp_dir() 24 | .join("coinswap") 25 | .join("wallet-tests") 26 | .join(test_name); 27 | let wallets_dir = temp_dir.join(""); 28 | 29 | let original_wallet_name = "original-wallet".to_string(); 30 | let original_wallet = wallets_dir.join(&original_wallet_name); 31 | let wallet_backup_file = wallets_dir.join("wallet-backup.json"); 32 | let restored_wallet_name = "restored-wallet".to_string(); 33 | let restored_wallet_file = wallets_dir.join(&restored_wallet_name); 34 | if temp_dir.exists() { 35 | fs::remove_dir_all(&temp_dir).unwrap(); 36 | } 37 | 38 | let port_zmq = 28332 + rand::random::() % 1000; 39 | 40 | let zmq_addr = format!("tcp://127.0.0.1:{port_zmq}"); 41 | 42 | let bitcoind = init_bitcoind(&temp_dir, zmq_addr); 43 | 44 | let url = bitcoind.rpc_url().split_at(7).1.to_string(); 45 | let auth = Auth::CookieFile(bitcoind.params.cookie_file.clone()); 46 | 47 | let rpc_config = RPCConfig { 48 | url, 49 | auth, 50 | wallet_name: original_wallet_name.clone(), 51 | }; 52 | ( 53 | original_wallet, 54 | rpc_config, 55 | wallet_backup_file, 56 | bitcoind, 57 | restored_wallet_file, 58 | ) 59 | } 60 | 61 | fn cleanup(bitcoind: &mut BitcoinD) { 62 | bitcoind.stop().unwrap(); 63 | } 64 | 65 | fn send_and_mine( 66 | bitcoind: &mut BitcoinD, 67 | address: &Address, 68 | btc_amount: f64, 69 | blocks_to_generate: u64, 70 | ) -> Result<(), bitcoincore_rpc::Error> { 71 | send_to_address(bitcoind, address, Amount::from_btc(btc_amount)?); 72 | generate_blocks(bitcoind, blocks_to_generate); 73 | Ok(()) 74 | } 75 | 76 | #[test] 77 | fn plainwallet_plainbackup_plainrestore() { 78 | info!("Running Test: Creating Wallet file, backing it up, then recieve a payment, and restore backup"); 79 | 80 | let (original_wallet, rpc_config, wallet_backup_file, mut bitcoind, restored_wallet_file) = 81 | setup("plain_wallet_plainbackup_plain_restore".to_string()); 82 | 83 | let mut wallet = coinswap::wallet::Wallet::init(&original_wallet, &rpc_config, None).unwrap(); 84 | 85 | let addr = wallet.get_next_external_address().unwrap(); 86 | send_and_mine(&mut bitcoind, &addr, 0.05, 1).unwrap(); 87 | 88 | let _ = wallet.backup(&wallet_backup_file, None); 89 | 90 | let addr = wallet.get_next_external_address().unwrap(); 91 | send_and_mine(&mut bitcoind, &addr, 0.05, 1).unwrap(); 92 | 93 | wallet.sync().unwrap(); 94 | 95 | let (backup, _) = 96 | load_sensitive_struct_interactive::(&wallet_backup_file); 97 | 98 | let restored_wallet = 99 | Wallet::restore(&backup, &restored_wallet_file, &rpc_config, None).unwrap(); 100 | 101 | assert_eq!(wallet, restored_wallet); // only compares .store! 102 | 103 | //cleanup(&mut bitcoind); 104 | cleanup(&mut bitcoind); 105 | 106 | info!("🎉 Wallet Backup and Restore after tx test ran succefully!"); 107 | } 108 | 109 | #[test] 110 | fn encwallet_encbackup_encrestore() { 111 | let (original_wallet, rpc_config, wallet_backup_file, mut bitcoind, restored_wallet_file) = 112 | setup("encwallet_encbackup_encrestore".to_string()); 113 | 114 | let km = KeyMaterial::new_interactive(None); 115 | 116 | let mut wallet = 117 | coinswap::wallet::Wallet::init(&original_wallet, &rpc_config, km.clone()).unwrap(); 118 | 119 | let addr = wallet.get_next_external_address().unwrap(); 120 | send_and_mine(&mut bitcoind, &addr, 0.05, 1).unwrap(); 121 | 122 | let _ = wallet.backup(&wallet_backup_file, km.clone()); 123 | 124 | let addr = wallet.get_next_external_address().unwrap(); 125 | send_and_mine(&mut bitcoind, &addr, 0.05, 1).unwrap(); 126 | 127 | wallet.sync().unwrap(); 128 | 129 | let (backup, _) = 130 | load_sensitive_struct_interactive::(&wallet_backup_file); 131 | 132 | let restored_wallet = 133 | Wallet::restore(&backup, &restored_wallet_file, &rpc_config, km.clone()).unwrap(); 134 | 135 | assert_eq!(wallet, restored_wallet); // only compares .store! 136 | 137 | cleanup(&mut bitcoind); 138 | } 139 | -------------------------------------------------------------------------------- /src/watch_tower/rpc_backend.rs: -------------------------------------------------------------------------------- 1 | //! Watchtower RPC backend: querying bitcoind, scanning mempool, and running discovery. 2 | 3 | use bitcoin::Network; 4 | use bitcoincore_rpc::{ 5 | bitcoin::{Block, BlockHash, Transaction, Txid}, 6 | json::GetBlockchainInfoResult, 7 | Client, RpcApi, 8 | }; 9 | use bitcoind::bitcoincore_rpc; 10 | 11 | use crate::{ 12 | wallet::RPCConfig, 13 | watch_tower::{ 14 | constants::{BITCOIN, REGTEST, SIGNET, TESTNET, TESTNET4}, 15 | registry_storage::FileRegistry, 16 | utils::{process_fidelity, process_transaction}, 17 | watcher_error::WatcherError, 18 | }, 19 | }; 20 | 21 | /// Lightweight wrapper around bitcoind RPC calls used by the watchtower. 22 | pub struct BitcoinRpc { 23 | client: Client, 24 | } 25 | 26 | impl BitcoinRpc { 27 | /// Constructs a new RPC wrapper using the provided configuration. 28 | pub fn new(rpc_config: RPCConfig) -> Result { 29 | let client = Client::new(&rpc_config.url, rpc_config.auth)?; 30 | Ok(Self { client }) 31 | } 32 | 33 | /// Wraps an existing bitcoind RPC client. 34 | pub fn new_client(client: Client) -> Self { 35 | Self { client } 36 | } 37 | 38 | /// Get txids of all transactions in the mempool. 39 | pub fn get_raw_mempool(&self) -> Result, WatcherError> { 40 | let raw_mempool = self.client.get_raw_mempool()?; 41 | Ok(raw_mempool) 42 | } 43 | 44 | /// Fetches a full transaction by txid. 45 | pub fn get_raw_tx(&self, txid: &Txid) -> Result { 46 | let tx = self.client.get_raw_transaction(txid, None)?; 47 | Ok(tx) 48 | } 49 | 50 | /// Returns chain metadata. 51 | pub fn get_blockchain_info(&self) -> Result { 52 | let blockchain_info = self.client.get_blockchain_info()?; 53 | Ok(blockchain_info) 54 | } 55 | 56 | /// Retrieves the block hash at a given height. 57 | pub fn get_block_hash(&self, height: u64) -> Result { 58 | let block_hash = self.client.get_block_hash(height)?; 59 | Ok(block_hash) 60 | } 61 | 62 | /// Fetches a block by hash. 63 | pub fn get_block(&self, hash: BlockHash) -> Result { 64 | let block = self.client.get_block(&hash)?; 65 | Ok(block) 66 | } 67 | 68 | /// Processes transactions in the mempool and updates registry. 69 | pub fn process_mempool(&mut self, registry: &mut FileRegistry) -> Result<(), WatcherError> { 70 | let txids = self.get_raw_mempool()?; 71 | for txid in &txids { 72 | let tx = self.get_raw_tx(txid)?; 73 | process_transaction(&tx, registry, false); 74 | } 75 | Ok(()) 76 | } 77 | 78 | /// Discovers maker fidelity bonds by scanning historical blocks. 79 | pub fn run_discovery(self, mut registry: FileRegistry) -> Result<(), WatcherError> { 80 | log::info!("Starting with market discovery"); 81 | let blockchain_info = self.get_blockchain_info()?; 82 | let coinswap_height = match blockchain_info.chain { 83 | Network::Bitcoin => BITCOIN, 84 | Network::Regtest => REGTEST, 85 | Network::Signet => SIGNET, 86 | Network::Testnet => TESTNET, 87 | Network::Testnet4 => TESTNET4, 88 | }; 89 | let last_tip = registry 90 | .load_checkpoint() 91 | .map(|checkpoint| checkpoint.height) 92 | .unwrap_or(coinswap_height); 93 | let tip_height = blockchain_info.blocks + 1; 94 | let total_blocks = tip_height.saturating_sub(last_tip); 95 | log::info!( 96 | "Scanning {} blocks for fidelity bonds (height {} to {})", 97 | total_blocks, 98 | last_tip, 99 | tip_height.saturating_sub(1) 100 | ); 101 | let mut makers_found = 0; 102 | for (i, height) in (last_tip..tip_height).enumerate() { 103 | if total_blocks > 100 { 104 | log::info!( 105 | "Discovery progress: {}/{} blocks scanned ({:.1}%)", 106 | i + 1, 107 | total_blocks, 108 | ((i + 1) as f64 / total_blocks as f64) * 100.0 109 | ); 110 | } 111 | let block_hash = self.get_block_hash(height)?; 112 | let block = self.get_block(block_hash)?; 113 | for tx in block.txdata { 114 | let onion_address = process_fidelity(&tx); 115 | if let Some(onion_address) = onion_address { 116 | makers_found += 1; 117 | log::info!("Maker found in the market: {:?}", onion_address); 118 | registry.insert_fidelity(tx.compute_txid(), onion_address); 119 | } 120 | } 121 | } 122 | log::info!( 123 | "Market discovery completed: scanned {} blocks, found {} makers", 124 | total_blocks, 125 | makers_found 126 | ); 127 | Ok(()) 128 | } 129 | } 130 | 131 | impl From for BitcoinRpc { 132 | fn from(value: Client) -> Self { 133 | BitcoinRpc { client: value } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/maker/rpc/messages.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::utill::UTXO; 4 | use bitcoin::Txid; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::{json, to_string_pretty}; 7 | use std::path::PathBuf; 8 | 9 | use crate::wallet::Balances; 10 | 11 | /// Enum representing RPC message requests. 12 | /// 13 | /// These messages are used for various operations in the Maker-rpc communication. 14 | /// Each variant corresponds to a specific action or query in the RPC protocol. 15 | #[derive(Serialize, Deserialize, Debug)] 16 | pub enum RpcMsgReq { 17 | /// Ping request to check connectivity. 18 | Ping, 19 | /// Request to fetch all utxos in the wallet. 20 | Utxo, 21 | /// Request to fetch only swap utxos in the wallet. 22 | SwapUtxo, 23 | /// Request to fetch UTXOs in the contract pool. 24 | ContractUtxo, 25 | /// Request to fetch UTXOs in the fidelity pool. 26 | FidelityUtxo, 27 | /// Request to retrieve the total wallet balances of different categories. 28 | Balances, 29 | /// Request for generating a new wallet address. 30 | NewAddress, 31 | /// Request to send funds to a specific address. 32 | SendToAddress { 33 | /// The recipient's address. 34 | address: String, 35 | /// The amount to send. 36 | amount: u64, 37 | /// The transaction fee to include. 38 | feerate: f64, 39 | }, 40 | /// Request to retrieve the Tor address of the Maker. 41 | GetTorAddress, 42 | /// Request to retrieve the data directory path. 43 | GetDataDir, 44 | /// Request to stop the Maker server. 45 | Stop, 46 | /// Request to list all active and past fidelity bonds. 47 | ListFidelity, 48 | /// Request to sync the internal wallet with blockchain. 49 | SyncWallet, 50 | } 51 | 52 | /// Enum representing RPC message responses. 53 | /// 54 | /// These messages are sent in response to RPC requests and carry the results 55 | /// of the corresponding actions or queries. 56 | #[derive(Serialize, Deserialize, Debug)] 57 | pub enum RpcMsgResp { 58 | /// Response to a Ping request. 59 | Pong, 60 | /// Response containing all spendable UTXOs 61 | UtxoResp { 62 | /// List of spendable UTXOs in the wallet. 63 | utxos: Vec, 64 | }, 65 | /// Response containing UTXOs in the swap pool. 66 | SwapUtxoResp { 67 | /// List of UTXOs in the swap pool. 68 | utxos: Vec, 69 | }, 70 | /// Response containing UTXOs in the fidelity pool. 71 | FidelityUtxoResp { 72 | /// List of UTXOs in the fidelity pool. 73 | utxos: Vec, 74 | }, 75 | /// Response containing UTXOs in the contract pool. 76 | ContractUtxoResp { 77 | /// List of UTXOs in the contract pool. 78 | utxos: Vec, 79 | }, 80 | /// Response containing the total wallet balances of different categories. 81 | TotalBalanceResp(Balances), 82 | /// Response containing a newly generated wallet address. 83 | NewAddressResp(String), 84 | /// Response to a send-to-address request. 85 | SendToAddressResp(String), 86 | /// Response containing the Tor address of the Maker. 87 | GetTorAddressResp(String), 88 | /// Response containing the path to the data directory. 89 | GetDataDirResp(PathBuf), 90 | /// Response indicating the server has been shut down. 91 | Shutdown, 92 | /// Response with the fidelity spending txid. 93 | FidelitySpend(Txid), 94 | /// Response with the internal server error. 95 | ServerError(String), 96 | /// Response listing all current and past fidelity bonds. 97 | ListBonds(String), 98 | } 99 | 100 | impl Display for RpcMsgResp { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | match self { 103 | Self::Pong => write!(f, "Pong"), 104 | Self::NewAddressResp(addr) => write!(f, "{addr}"), 105 | Self::TotalBalanceResp(balances) => { 106 | write!( 107 | f, 108 | "{}", 109 | to_string_pretty(&json!({ 110 | "regular": balances.regular.to_sat(), 111 | "swap": balances.swap.to_sat(), 112 | "contract": balances.contract.to_sat(), 113 | "fidelity": balances.fidelity.to_sat(), 114 | "spendable": balances.spendable.to_sat(), 115 | })) 116 | .unwrap() 117 | ) 118 | } 119 | Self::UtxoResp { utxos } 120 | | Self::SwapUtxoResp { utxos } 121 | | Self::FidelityUtxoResp { utxos } 122 | | Self::ContractUtxoResp { utxos } => { 123 | write!( 124 | f, 125 | "{}", 126 | serde_json::to_string_pretty(utxos).expect("UTXO JSON serialization failed") 127 | ) 128 | } 129 | Self::SendToAddressResp(tx_hex) => write!(f, "{tx_hex}"), 130 | Self::GetTorAddressResp(addr) => write!(f, "{addr}"), 131 | Self::GetDataDirResp(path) => write!(f, "{}", path.display()), 132 | Self::Shutdown => write!(f, "Shutdown Initiated"), 133 | Self::FidelitySpend(txid) => write!(f, "{txid}"), 134 | Self::ServerError(e) => write!(f, "{e}"), 135 | Self::ListBonds(v) => write!(f, "{v}"), 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/protocol/musig2.rs: -------------------------------------------------------------------------------- 1 | //! The musig2 APIs 2 | //! 3 | //! This module includes most of the fundamental functions needed for Taproot and MuSig2. 4 | 5 | use secp256k1::{ 6 | musig::{ 7 | new_nonce_pair, AggregatedNonce, AggregatedSignature, KeyAggCache, PartialSignature, 8 | PublicNonce, SecretNonce, Session, SessionSecretRand, 9 | }, 10 | rand, Keypair, Message, PublicKey, Scalar, Secp256k1, XOnlyPublicKey, 11 | }; 12 | 13 | use crate::protocol::error::ProtocolError; 14 | 15 | /// get aggregated public key from two public keys 16 | pub fn get_aggregated_pubkey(pubkey1: &PublicKey, pubkey2: &PublicKey) -> XOnlyPublicKey { 17 | let secp = Secp256k1::new(); 18 | let mut pubkeys = [pubkey1, pubkey2]; 19 | // Sort pubkeys lexicographically (manual implementation) 20 | pubkeys.sort_by_key(|a| a.serialize()); 21 | let agg_cache = KeyAggCache::new(&secp, &pubkeys); 22 | agg_cache.agg_pk() 23 | } 24 | 25 | /// Generates a new nonce pair 26 | pub fn generate_new_nonce_pair(pubkey: PublicKey) -> (SecretNonce, PublicNonce) { 27 | let secp = Secp256k1::new(); 28 | let musig_session_sec_rand = SessionSecretRand::from_rng(&mut rand::thread_rng()); 29 | new_nonce_pair( 30 | &secp, 31 | musig_session_sec_rand, 32 | None, 33 | None, 34 | pubkey, 35 | None, 36 | None, 37 | ) 38 | } 39 | 40 | /// Generates a partial signature 41 | pub fn generate_partial_signature( 42 | message: Message, 43 | agg_nonce: &AggregatedNonce, 44 | sec_nonce: SecretNonce, 45 | keypair: Keypair, 46 | tap_tweak: Scalar, 47 | pubkeys: &[&PublicKey], 48 | ) -> Result { 49 | let secp = Secp256k1::new(); 50 | let mut musig_key_agg_cache = KeyAggCache::new(&secp, pubkeys); 51 | musig_key_agg_cache.pubkey_xonly_tweak_add(&secp, &tap_tweak)?; 52 | let session = Session::new(&secp, &musig_key_agg_cache, *agg_nonce, message); 53 | Ok(session.partial_sign(&secp, sec_nonce, &keypair, &musig_key_agg_cache)) 54 | } 55 | 56 | /// Aggregates the partial signatures 57 | pub fn aggregate_partial_signatures( 58 | message: Message, 59 | agg_nonce: AggregatedNonce, 60 | tap_tweak: Scalar, 61 | partial_sigs: &[&PartialSignature], 62 | pubkeys: &[&PublicKey], 63 | ) -> Result { 64 | let secp = Secp256k1::new(); 65 | let mut musig_key_agg_cache = KeyAggCache::new(&secp, pubkeys); 66 | musig_key_agg_cache.pubkey_xonly_tweak_add(&secp, &tap_tweak)?; 67 | let session = Session::new(&secp, &musig_key_agg_cache, agg_nonce, message); 68 | Ok(session.partial_sig_agg(partial_sigs)) 69 | } 70 | #[cfg(test)] 71 | mod tests { 72 | use std::convert::TryInto; 73 | 74 | use bitcoin::hex::FromHex; 75 | use secp256k1::Scalar; 76 | 77 | use super::*; 78 | 79 | #[test] 80 | fn test_taproot() { 81 | let secp = Secp256k1::new(); 82 | let seckey1_bytes = [ 83 | 53, 126, 153, 168, 20, 2, 57, 61, 57, 192, 65, 188, 170, 70, 195, 245, 0, 137, 135, 59, 84 | 128, 104, 181, 90, 187, 118, 160, 138, 217, 172, 220, 56, 85 | ]; 86 | let keypair1 = Keypair::from_seckey_slice(&secp, &seckey1_bytes).unwrap(); 87 | let seckey2_bytes = [ 88 | 87, 32, 109, 105, 102, 136, 254, 135, 248, 148, 13, 5, 127, 89, 5, 64, 49, 245, 51, 89 | 224, 211, 94, 101, 150, 225, 7, 68, 134, 79, 188, 167, 235, 90 | ]; 91 | let keypair2 = Keypair::from_seckey_slice(&secp, &seckey2_bytes).unwrap(); 92 | 93 | let pubkey1 = keypair1.public_key(); 94 | let pubkey2 = keypair2.public_key(); 95 | 96 | let pubkeys = vec![&pubkey1, &pubkey2]; 97 | 98 | let agg_pubkey = get_aggregated_pubkey(&pubkey1, &pubkey2); 99 | println!("Aggregated public key: {:?}", agg_pubkey); 100 | 101 | let mut musig_key_agg_cache = KeyAggCache::new(&secp, pubkeys.as_slice()); 102 | let tweak = Scalar::from_be_bytes( 103 | Vec::::from_hex("712d48c5f50912f30ea973aa8a713e9009960db11d1d896f40996eac1524c8be") 104 | .unwrap() 105 | .try_into() 106 | .unwrap(), 107 | ) 108 | .unwrap(); 109 | let _ = musig_key_agg_cache.pubkey_xonly_tweak_add(&secp, &tweak); 110 | let message = Message::from_digest( 111 | Vec::::from_hex("d977c6fd2a9a9e43ef9d66171536a0af5e022f76eae397ab69291a3b1f3b52ea") 112 | .unwrap() 113 | .try_into() 114 | .unwrap(), 115 | ); 116 | 117 | let (sec_nonce1, pub_nonce1) = generate_new_nonce_pair(pubkey1); 118 | let (sec_nonce2, pub_nonce2) = generate_new_nonce_pair(pubkey2); 119 | println!("Generated nonce pairs."); 120 | 121 | let agg_nonce = AggregatedNonce::new(&secp, &[&pub_nonce1, &pub_nonce2]); 122 | println!("Aggregated nonce: {:?}", agg_nonce); 123 | 124 | println!("Session created."); 125 | 126 | let partial_sig1 = 127 | generate_partial_signature(message, &agg_nonce, sec_nonce1, keypair1, tweak, &pubkeys); 128 | let partial_sig2 = 129 | generate_partial_signature(message, &agg_nonce, sec_nonce2, keypair2, tweak, &pubkeys); 130 | println!("Generated partial signatures."); 131 | 132 | let partial_sigs = vec![&partial_sig1, &partial_sig2]; 133 | println!("Partial signatures: {:?}", partial_sigs); 134 | // let agg_sig = aggregate_partial_signatures(&partial_sigs, &session); 135 | // println!("Aggregated signature: {:?}", agg_sig); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/bin/maker-cli.rs: -------------------------------------------------------------------------------- 1 | use std::{net::TcpStream, time::Duration}; 2 | 3 | use clap::Parser; 4 | use coinswap::{ 5 | maker::{MakerError, RpcMsgReq, RpcMsgResp}, 6 | utill::{read_message, send_message, MIN_FEE_RATE}, 7 | }; 8 | 9 | /// A simple command line app to operate the makerd server. 10 | /// 11 | /// The app works as an RPC client for makerd, useful to access the server, retrieve information, and manage server operations. 12 | /// 13 | /// For more detailed usage information, please refer: 14 | /// 15 | /// This is early beta, and there are known and unknown bugs. Please report issues at: 16 | #[derive(Parser, Debug)] 17 | #[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), 18 | author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] 19 | struct App { 20 | /// Sets the rpc-port of Makerd 21 | #[clap(long, short = 'p', default_value = "127.0.0.1:6103")] 22 | rpc_port: String, 23 | /// The command to execute 24 | #[clap(subcommand)] 25 | command: Commands, 26 | } 27 | 28 | #[derive(Parser, Debug)] 29 | enum Commands { 30 | /// Sends a ping to makerd. Will return a pong. 31 | SendPing, 32 | /// Lists all utxos in the wallet. Including fidelity bonds. 33 | ListUtxo, 34 | /// Lists utxos received from incoming swaps. 35 | ListUtxoSwap, 36 | /// Lists HTLC contract utxos. 37 | ListUtxoContract, 38 | /// Lists fidelity bond utxos. 39 | ListUtxoFidelity, 40 | /// Get total wallet balances of different categories. 41 | /// regular: All single signature regular wallet coins (seed balance). 42 | /// swap: All 2of2 multisig coins received in swaps. 43 | /// contract: All live contract transaction balance locked in timelocks. If you see value in this field, you have unfinished or malfinished swaps. You can claim them back with the recover command. 44 | /// fidelity: All coins locked in fidelity bonds. 45 | /// spendable: Spendable amount in wallet (regular + swap balance). 46 | GetBalances, 47 | /// Gets a new bitcoin receiving address 48 | GetNewAddress, 49 | /// Send Bitcoin to an external address and return the txid. 50 | SendToAddress { 51 | /// Recipient's address. 52 | #[clap(long, short = 't')] 53 | address: String, 54 | /// Amount to send in sats 55 | #[clap(long, short = 'a')] 56 | amount: u64, 57 | /// Feerate in sats/vByte. Defaults to 2 sats/vByte 58 | #[clap(long, short = 'f')] 59 | feerate: Option, 60 | }, 61 | /// Show the server tor address 62 | ShowTorAddress, 63 | /// Show the data directory path 64 | ShowDataDir, 65 | /// Shutdown the makerd server 66 | Stop, 67 | /// Show all the fidelity bonds, current and previous, with an (index, {bond_proof, is_spent}) tuple. 68 | ShowFidelity, 69 | /// Sync the maker wallet with the current blockchain state. 70 | SyncWallet, 71 | } 72 | 73 | fn main() -> Result<(), MakerError> { 74 | let cli = App::parse(); 75 | 76 | let stream = TcpStream::connect(cli.rpc_port)?; 77 | 78 | match cli.command { 79 | Commands::SendPing => { 80 | send_rpc_req(stream, RpcMsgReq::Ping)?; 81 | } 82 | Commands::ListUtxoContract => { 83 | send_rpc_req(stream, RpcMsgReq::ContractUtxo)?; 84 | } 85 | Commands::ListUtxoFidelity => { 86 | send_rpc_req(stream, RpcMsgReq::FidelityUtxo)?; 87 | } 88 | Commands::GetBalances => { 89 | send_rpc_req(stream, RpcMsgReq::Balances)?; 90 | } 91 | Commands::ListUtxo => { 92 | send_rpc_req(stream, RpcMsgReq::Utxo)?; 93 | } 94 | Commands::ListUtxoSwap => { 95 | send_rpc_req(stream, RpcMsgReq::SwapUtxo)?; 96 | } 97 | Commands::GetNewAddress => { 98 | send_rpc_req(stream, RpcMsgReq::NewAddress)?; 99 | } 100 | Commands::SendToAddress { 101 | address, 102 | amount, 103 | feerate, 104 | } => { 105 | send_rpc_req( 106 | stream, 107 | RpcMsgReq::SendToAddress { 108 | address, 109 | amount, 110 | feerate: feerate.unwrap_or(MIN_FEE_RATE), 111 | }, 112 | )?; 113 | } 114 | Commands::ShowTorAddress => { 115 | send_rpc_req(stream, RpcMsgReq::GetTorAddress)?; 116 | } 117 | Commands::ShowDataDir => { 118 | send_rpc_req(stream, RpcMsgReq::GetDataDir)?; 119 | } 120 | Commands::Stop => { 121 | send_rpc_req(stream, RpcMsgReq::Stop)?; 122 | } 123 | Commands::ShowFidelity => { 124 | send_rpc_req(stream, RpcMsgReq::ListFidelity)?; 125 | } 126 | Commands::SyncWallet => { 127 | send_rpc_req(stream, RpcMsgReq::SyncWallet)?; 128 | } 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | fn send_rpc_req(mut stream: TcpStream, req: RpcMsgReq) -> Result<(), MakerError> { 135 | // stream.set_read_timeout(Some(Duration::from_secs(20)))?; 136 | stream.set_write_timeout(Some(Duration::from_secs(20)))?; 137 | 138 | send_message(&mut stream, &req)?; 139 | 140 | let response_bytes = read_message(&mut stream)?; 141 | let response: RpcMsgResp = serde_cbor::from_slice(&response_bytes)?; 142 | 143 | if matches!(response, RpcMsgResp::Pong) { 144 | println!("success"); 145 | } else { 146 | println!("{response}"); 147 | } 148 | 149 | Ok(()) 150 | } 151 | -------------------------------------------------------------------------------- /docs/workshop.md: -------------------------------------------------------------------------------- 1 | ## What Is Coinswap? 2 | 3 | Coinswap is a decentralized atomic swaps protocol designed to operate over a peer-to-peer network using Tor. The protocol provides a mechanism for peers to swap their UTXOs with other peers in the network, resulting in the transfer of ownership between the two UTXOs without leaving an on-chain footprint. 4 | 5 | The protocol includes a peer-to-peer messaging system, similar to the Lightning Network, enabling peers to perform swaps without trusting each other by utilizing the well-established HTLC (Hashed Time-Locked Contract) script constructions. The protocol supports *composable swaps*, allowing for the creation of swap chains such as `Alice --> Bob --> Carol --> Alice`. At the end of the swap, Alice ends up with Carol's UTXO, Carol ends up with Bob's, and Bob ends up with Alice's. 6 | 7 | In this scenario, Alice acts as the client, while Bob and Carol act as service providers. The client is responsible for paying all the fees, which consist of two components: 8 | - swap transaction fees 9 | - service provider fees. 10 | 11 | For each swap, the service providers earn fees, incentivizing node operators to *lock liquidity* and *earn yield*. Unlike the Lightning Network, swap service software does not require active node management. It is a plug-and-play system, making it much easier to integrate swap services into existing node modules. 12 | 13 | The service providers are invisible to each other and only relay messages via the client. The client acts as the relay and handles the majority of protocol validations, while the service providers act as simple daemons that respond to client messages. The protocol follows the `smart-client-dumb-server` design philosophy, making the servers lightweight and capable of running in constrained environments. 14 | 15 | At any point during a swap, if any party misbehaves, the other parties can recover their funds from the swap using the HTLC's time-lock path. 16 | 17 | The protocol also includes a marketplace with dynamic offer data attached to a **Fidelity Bond** and a Tor address. The Fidelity Bond is a time-locked Bitcoin UTXO that service providers must display in the marketplace to be accepted for swaps. If a provider misbehaves, clients in the marketplace can punish them by refusing to swap with the same Fidelity Bond in the future. Fidelity Bonds thus serve as an identity mechanism, providing **provable costliness** to create Sybil resistance in the decentralized marketplace. 18 | 19 | The protocol is in its early stages and has several open questions and potential vulnerabilities. However, it is a promising approach to decentralized atomic swaps, and we are excited to see where it goes. 20 | 21 | For more details, please refer to the project [README](../README.md) and check out the [App Demos](./). 22 | 23 | A more detailed [protocol specification](https://github.com/citadel-tech/Coinswap-Protocol-Specification) is also available. 24 | 25 | --- 26 | 27 | ## Session Timeline 28 | 29 | | **Topic** | **Duration (mins)** | **Format** | **Host** | 30 | |------------------------|---------------------|-----------------|-----------| 31 | | Intro to Coinswap | 15 | Presentation | Rishabh | 32 | | Coinswap Live Demo | 30 | Workshop | Raj | 33 | | Problem Statement | 15 | Presentation | Raj | 34 | | Brainstorming Session | 30 | Discussion | Raj | 35 | 36 | --- 37 | 38 | ## Prerequisites 39 | 40 | To actively engage in the session, please prepare with the following: 41 | 42 | - **Read up on Coinswap**: start with the project [README](../../README.md). 43 | - **Set up your environment**: Follow the [demo documentation](./demo.md) to set up your system. You will need: 44 | - A running `bitcoind` node on your local machine, synced on Testnet4. 45 | - At least 511,000 sats (500,000 sats for the Fidelity Bond + 1000 sats for fidelity tx fee + 10,000 sats as minimum swap liquidity) of balance in your wallet if you are running maker. 46 | - Instructions for setting up `bitcoind`, connecting the apps, and running the entire Coinswap process are provided in the demo documentation. 47 | 48 | --- 49 | 50 | ## The Session 51 | 52 | ### **Introduction** 53 | We will begin with a basic introduction to Coinswap and lay the foundation for the session. Participants are encouraged to ask questions to clarify any doubts before moving to the next section. 54 | 55 | --- 56 | 57 | ### **Demo** 58 | In this segment, we will set up a complete swap marketplace with multiple makers and takers on our systems. Participants will role-play as takers and makers, performing a multi-hop swap with each other. We will monitor the live system logs on our laptops to observe the progress of the swap in real-time. 59 | 60 | If time permits, we will conduct another swap round with a malicious maker who drops out during the swap. This will demonstrate the recovery process and highlight any potential traces left on the blockchain. 61 | 62 | --- 63 | 64 | ### **Problem Statement** 65 | The demo will reveal a centralization vector in the marketplace: the DNS server. Without the DNS, clients cannot discover servers, and taking the DNS offline would disrupt the entire marketplace. 66 | 67 | --- 68 | 69 | ### **Brainstorming** 70 | We will explore the design space of decentralized DNS systems and gossip protocols to address this centralization issue. Participants will discuss the pros and cons of various designs, and through a collective brainstorming session, we will aim to propose a suitable design for Coinswap. 71 | 72 | --- 73 | 74 | ### **Learnings** 75 | By the end of the session, participants can expect to gain a solid understanding of concepts such as atomic swaps, HTLCs, decentralized marketplaces, gossip protocols, and more. -------------------------------------------------------------------------------- /tests/multi-taker.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use std::{ 3 | sync::{atomic::Ordering::Relaxed, Arc}, 4 | thread, 5 | time::Duration, 6 | }; 7 | 8 | use bitcoin::Amount; 9 | use coinswap::{ 10 | maker::{start_maker_server, MakerBehavior}, 11 | taker::{SwapParams, TakerBehavior}, 12 | }; 13 | 14 | use log::{info, warn}; 15 | 16 | mod test_framework; 17 | use test_framework::*; 18 | 19 | /// Multiple Takers with Different Behaviors 20 | /// This test demonstrates a scenario where a single Maker is connected to two Takers 21 | /// exhibiting different behaviors: 22 | /// - Taker1: Normal 23 | /// - Taker2: Drops connection after full setup 24 | /// 25 | /// The test verifies that the Maker can properly manage multiple concurrent swaps with 26 | /// different taker behaviors and recover appropriately in each case if required. 27 | #[test] 28 | fn multi_taker_single_maker_swap() { 29 | let makers_config_map = [ 30 | ((6102, None), MakerBehavior::Normal), 31 | ((16102, None), MakerBehavior::Normal), 32 | ]; 33 | 34 | let taker_behavior = vec![ 35 | TakerBehavior::Normal, 36 | TakerBehavior::Normal, // TODO: Making a taker misbehave, makes the behavior of makers unpredictable. Fix It. 37 | ]; 38 | // Initiate test framework, Makers. 39 | // Taker has normal behavior. 40 | let (test_framework, mut takers, makers, block_generation_handle) = 41 | TestFramework::init(makers_config_map.into(), taker_behavior); 42 | 43 | warn!("🧪 Running Test: Multiple Takers with Different Behaviors"); 44 | 45 | info!("💰 Funding multiple takers with UTXOs"); 46 | // Fund the Takers with 3 utxos of 0.05 btc each and do basic checks on the balance 47 | for taker in takers.iter_mut() { 48 | fund_and_verify_taker( 49 | taker, 50 | &test_framework.bitcoind, 51 | 3, 52 | Amount::from_btc(0.05).unwrap(), 53 | ); 54 | } 55 | 56 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 57 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 58 | fund_and_verify_maker( 59 | makers_ref, 60 | &test_framework.bitcoind, 61 | 6, 62 | Amount::from_btc(0.05).unwrap(), 63 | ); 64 | 65 | // Start the Maker Server threads 66 | info!("🚀 Initiating Maker servers"); 67 | let maker_threads = makers 68 | .iter() 69 | .map(|maker| { 70 | let maker_clone = maker.clone(); 71 | thread::spawn(move || { 72 | start_maker_server(maker_clone).unwrap(); 73 | }) 74 | }) 75 | .collect::>(); 76 | 77 | // Makers take time to fully setup. 78 | let org_maker_spend_balances = makers 79 | .iter() 80 | .map(|maker| { 81 | while !maker.is_setup_complete.load(Relaxed) { 82 | info!("⏳ Waiting for maker setup completion"); 83 | // Introduce a delay of 10 seconds to prevent write lock starvation. 84 | thread::sleep(Duration::from_secs(10)); 85 | continue; 86 | } 87 | 88 | // Check balance after setting up maker server. 89 | let wallet = maker.wallet.read().unwrap(); 90 | 91 | let balances = wallet.get_balances().unwrap(); 92 | 93 | verify_maker_pre_swap_balances(&balances, 24999508); 94 | 95 | balances.spendable 96 | }) 97 | .collect::>(); 98 | 99 | // Initiate Coinswap for both Takers concurrently 100 | info!("🔄 Initiating coinswap protocol for multiple takers"); 101 | 102 | // Spawn threads for each taker to initiate coinswap concurrently 103 | thread::scope(|s| { 104 | for taker in &mut takers { 105 | let swap_params = SwapParams { 106 | send_amount: Amount::from_sat(500000), 107 | maker_count: 2, 108 | manually_selected_outpoints: None, 109 | }; 110 | s.spawn(move || { 111 | taker.do_coinswap(swap_params).unwrap(); 112 | }); 113 | std::thread::sleep(Duration::from_secs(10)); 114 | } 115 | }); 116 | 117 | maker_threads 118 | .into_iter() 119 | .for_each(|thread| thread.join().unwrap()); 120 | 121 | info!("🎯 All coinswaps processed. Transactions complete."); 122 | 123 | thread::sleep(Duration::from_secs(10)); 124 | 125 | info!("📊 Verifying Maker balances"); 126 | // Verify spendable balances for makers. 127 | // TODO - Add more assertions / checks for balances. 128 | for _ in takers.iter() { 129 | makers.iter().zip(org_maker_spend_balances.iter()).for_each( 130 | |(maker, org_spend_balance)| { 131 | let wallet = maker.get_wallet().read().unwrap(); 132 | let balances = wallet.get_balances().unwrap(); 133 | assert!( 134 | balances.spendable == balances.regular + balances.swap, 135 | "Maker balances mismatch" 136 | ); 137 | let balance_diff = balances.spendable.to_sat() - org_spend_balance.to_sat(); 138 | println!("🔍 DEBUG: Multi-taker balance diff: {balance_diff} sats"); 139 | assert!( 140 | (20000..=70000).contains(&balance_diff), 141 | "Expected balance diff between 40000-70000 sats, got {}", 142 | balance_diff 143 | ); 144 | }, 145 | ); 146 | } 147 | 148 | info!("🎉 All checks successful. Terminating integration test case"); 149 | 150 | test_framework.stop(); 151 | block_generation_handle.join().unwrap(); 152 | } 153 | -------------------------------------------------------------------------------- /docs/demo-v2.md: -------------------------------------------------------------------------------- 1 | # Coinswap V2: Live Demo Setup Instructions 2 | 3 | Instructions for the full Coinswap setup to be used for live demo. 4 | 5 | ## Setup The Backend 6 | 7 | ### Backend Stack 8 | 9 | The Coinswap backend stack consists of the following components: 10 | - bitcoind: Used for wallet syncing and market discovery. 11 | - tor: Used for all network communications. 12 | - makerd/maker-cli: The maker server to earn fees from swaps. 13 | 14 | ### Get The Core Lib 15 | 16 | ```shell 17 | git clone https://github.com/citadel-tech/coinswap.git 18 | ``` 19 | 20 | ### Docker 21 | 22 | The backend stack is compiled into an easily usable Docker image. 23 | The containers can be spawned with either a configurable shell script: 24 | ```shell 25 | ./docker-setup configure 26 | ./docker-setup start 27 | ``` 28 | Or 29 | by using the prebuilt [docker-compose.yml](./../docker-compose.yml) file: 30 | ```shell 31 | docker compose up 32 | ``` 33 | 34 | Both will create a backend server stack with Tor and Maker server connected to each other. 35 | 36 | For Bitcoind we will use Mutinynet's Signet. For the purpose of the demo, a pre-synced Signet node is 37 | available for use with RPC and ZMQ access open to the public. The docker compose will automatically connect the maker 38 | server to the pre-synced Mutinynet node. 39 | 40 | ### Funding The Maker 41 | Once the docker container is up, check out the makerd logs with the following command: 42 | ```bash 43 | ./docker-setup logs makerd 44 | ``` 45 | The log will ask for funds to be sent to a specific address. The log will look something like below: 46 | ```shell 47 | coinswap-makerd | 2025-12-01T14:30:26.655863622+00:00 INFO coinswap::wallet::api - Saving wallet to disk: 0 incoming_v2, 0 outgoing_v2 swapcoins 48 | coinswap-makerd | 2025-12-01T14:30:26.656215409+00:00 INFO coinswap::wallet::rpc - Completed wallet sync and save for "maker-wallet-1764598870" 49 | coinswap-makerd | 2025-12-01T14:30:27.171543413+00:00 ERROR coinswap::wallet::api - No spendable UTXOs available 50 | coinswap-makerd | 2025-12-01T14:30:27.172360695+00:00 WARN coinswap::maker::server2 - [6012] Insufficient funds to create fidelity bond. 51 | coinswap-makerd | 2025-12-01T14:30:27.213539195+00:00 INFO coinswap::watch_tower::rpc_backend - Discovery progress: 684/817 blocks scanned (83.7%) 52 | coinswap-makerd | 2025-12-01T14:30:27.617607128+00:00 INFO coinswap::wallet::api - Saving wallet to disk: 0 incoming_v2, 0 outgoing_v2 swapcoins 53 | coinswap-makerd | 2025-12-01T14:30:27.617987039+00:00 INFO coinswap::maker::server2 - [6012] Send at least 0.00050324 BTC to tb1qty2ypwf9mllzvsjvwuag3zx3syss2sra82zz9w | If you send extra, that will be added to your wallet balance 54 | ``` 55 | 56 | Use the [Mutinynet Faucet](https://faucet.mutinynet.com/) to send at least 100K sats to the address mentioned in your log. 57 | 58 | Once the funds are confirmed, the maker will automatically set up its fidelity bond. Once everything is set up, the maker log will look like below: 59 | ```shell 60 | coinswap-makerd | 2025-12-01T14:35:23.932951431+00:00 INFO coinswap::maker::server2 - [6012] Taproot maker initialized - Address: 3l7nmodhgplgejj3vb42fc5vlfi2zf6hxgbdeepombdlw7ivt5okjrid.onion:6012 61 | coinswap-makerd | 2025-12-01T14:35:23.937119351+00:00 INFO coinswap::wallet::api - Searching for unfinished swapcoins: 0 incoming, 0 outgoing in store 62 | coinswap-makerd | 2025-12-01T14:35:23.938856172+00:00 INFO coinswap::maker::server2 - [6012] Taproot maker setup completed 63 | coinswap-makerd | 2025-12-01T14:35:23.939783495+00:00 INFO coinswap::maker::server2 - [6012] Taproot maker server listening on port 6012 64 | coinswap-makerd | 2025-12-01T14:35:23.940607635+00:00 INFO coinswap::wallet::api - Searching for unfinished swapcoins: 0 incoming, 0 outgoing in store 65 | coinswap-makerd | 2025-12-01T14:35:23.941120981+00:00 INFO coinswap::maker::server2 - [6012] Taproot swap liquidity: 49540 sats, 0 ongoing swaps 66 | coinswap-makerd | 2025-12-01T14:35:23.941453528+00:00 INFO coinswap::maker::rpc::server - [6012] RPC socket binding successful at 127.0.0.1:6013 67 | coinswap-makerd | 2025-12-01T14:40:23.957061609+00:00 INFO coinswap::wallet::api - Searching for unfinished swapcoins: 0 incoming, 0 outgoing in store 68 | coinswap-makerd | 2025-12-01T14:40:23.957581168+00:00 INFO coinswap::maker::server2 - [6012] Taproot swap liquidity: 49540 sats, 0 ongoing swaps 69 | ``` 70 | 71 | ## Setup The Frontend 72 | 73 | Compiling the frontend requires npm and node at the latest version. Use the following commands to install node using nvm, if you don't have it already. 74 | 75 | ### Get nvm Latest 76 | ```shell 77 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash 78 | ``` 79 | ### Install npm and node latest 80 | ```shell 81 | nvm install node 82 | ``` 83 | ### Check Installation 84 | ```shell 85 | node -v 86 | npm -v 87 | ``` 88 | 89 | ### Build the Taker App 90 | ```shell 91 | git clone https://github.com/citadel-tech/taker-app.git 92 | cd taker-app 93 | npm install 94 | npm run dev 95 | ``` 96 | ## Connecting The Taker App 97 | 98 | The Taker app, once started, will try to connect to a bitcoind and Tor network. You can put any value there as per your local node and Tor configuration. 99 | 100 | For the purpose of the demo, we will use the following values: 101 | 102 | ### bitcoind: 103 | - RPC Host: `172.81.178.3` 104 | - RPC Port: `48332` 105 | - RPC User: `user` 106 | - RPC Password: `password` 107 | - ZMQ (for both block and tx): `tcp://172.81.178.3:58332` 108 | 109 | After inputting the values, click on `Test Connection`. It should return "connection successful". 110 | 111 | ### Tor: 112 | - Socks Port: `9050` 113 | - Control Port: `9051` 114 | - Tor Auth Password: `coinswap` 115 | 116 | ### Create A New Wallet 117 | After setting all the configurations, click on "Create New Wallet". This will start a fresh new Coinswap wallet, make all the connections, and start the UI dashboard. 118 | 119 | The rest of the demo will follow the app layout and perform a coinswap across multiple makers. 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/protocol/error.rs: -------------------------------------------------------------------------------- 1 | //! All Contract related errors. 2 | 3 | use bitcoin::Amount; 4 | 5 | /// Represents errors encountered during protocol operations. 6 | /// 7 | /// This enum encapsulates various errors that can occur during protocol execution, 8 | /// including cryptographic errors, mismatches in expected values, and general protocol violations. 9 | #[derive(Debug)] 10 | pub enum ProtocolError { 11 | /// Error related to Secp256k1 cryptographic operations. 12 | Secp(bitcoin::secp256k1::Error), 13 | /// Error in Bitcoin script handling. 14 | Script(bitcoin::blockdata::script::Error), 15 | /// Error converting from a byte slice to a hash type. 16 | Hash(bitcoin::hashes::FromSliceError), 17 | /// Error converting from a byte slice to a key type. 18 | Key(bitcoin::key::FromSliceError), 19 | /// Error related to calculating or validating Sighashes. 20 | Sighash(bitcoin::transaction::InputsIndexError), 21 | /// Error when an unexpected message is received. 22 | WrongMessage { 23 | /// The expected message type. 24 | expected: String, 25 | /// The received message type. 26 | received: String, 27 | }, 28 | /// Error when the number of signatures does not match the expected count. 29 | WrongNumOfSigs { 30 | /// The expected number of signatures. 31 | expected: usize, 32 | /// The received number of signatures. 33 | received: usize, 34 | }, 35 | /// Error when the number of contract transactions is incorrect. 36 | WrongNumOfContractTxs { 37 | /// The expected number of contract transactions. 38 | expected: usize, 39 | /// The received number of contract transactions. 40 | received: usize, 41 | }, 42 | /// Error when the number of private keys is incorrect. 43 | WrongNumOfPrivkeys { 44 | /// The expected number of private keys. 45 | expected: usize, 46 | /// The received number of private keys. 47 | received: usize, 48 | }, 49 | /// Error when the funding amount does not match the expected value. 50 | IncorrectFundingAmount { 51 | /// The expected funding amount. 52 | expected: Amount, 53 | /// The actual funding amount. 54 | found: Amount, 55 | }, 56 | /// Error encountered when a non-segwit `script_pubkey` is used. 57 | /// 58 | /// The protocol only supports `V0_Segwit` transactions. 59 | ScriptPubkey(bitcoin::script::witness_program::Error), 60 | /// Represents an error with secp256k1::scalar conversion. 61 | ScalarOutOfRange(secp256k1::scalar::OutOfRangeError), 62 | /// General error not covered by other variants. 63 | General(&'static str), 64 | /// Represents an error related to Secp256k1 cryptographic operations. 65 | TaprootSecp(secp256k1::Error), 66 | ///Repesents an error in Taproot Script handling 67 | TaprootScript(bitcoin::taproot::TaprootBuilderError), 68 | /// Represents an error in Building a Taproot Tree 69 | TaprootBuilder(bitcoin::taproot::TaprootBuilder), 70 | /// Represents an error with Tweaking a PublicKey. 71 | MusigTweak(secp256k1::musig::InvalidTweakErr), 72 | /// Represents an error with computing a Taproot Signature from a slice. 73 | /// 74 | /// Generally occuring when computing Taproot Signature from a byte array of aggregated signatures. 75 | TaprootSigSlice(bitcoin::taproot::SigFromSliceError), 76 | /// Error related to calculating or validating Signature hash 77 | TaprootSighash(bitcoin::sighash::TaprootError), 78 | } 79 | 80 | impl From for ProtocolError { 81 | fn from(value: bitcoin::script::witness_program::Error) -> Self { 82 | Self::ScriptPubkey(value) 83 | } 84 | } 85 | 86 | impl From for ProtocolError { 87 | fn from(value: bitcoin::secp256k1::Error) -> Self { 88 | Self::Secp(value) 89 | } 90 | } 91 | 92 | impl From for ProtocolError { 93 | fn from(value: secp256k1::Error) -> Self { 94 | Self::TaprootSecp(value) 95 | } 96 | } 97 | 98 | impl From for ProtocolError { 99 | fn from(value: bitcoin::blockdata::script::Error) -> Self { 100 | Self::Script(value) 101 | } 102 | } 103 | 104 | impl From for ProtocolError { 105 | fn from(value: bitcoin::hashes::FromSliceError) -> Self { 106 | Self::Hash(value) 107 | } 108 | } 109 | 110 | impl From for ProtocolError { 111 | fn from(value: bitcoin::key::FromSliceError) -> Self { 112 | Self::Key(value) 113 | } 114 | } 115 | 116 | impl From for ProtocolError { 117 | fn from(value: bitcoin::transaction::InputsIndexError) -> Self { 118 | Self::Sighash(value) 119 | } 120 | } 121 | 122 | impl From for ProtocolError { 123 | fn from(value: bitcoin::taproot::TaprootBuilderError) -> Self { 124 | Self::TaprootScript(value) 125 | } 126 | } 127 | impl From for ProtocolError { 128 | fn from(value: bitcoin::taproot::TaprootBuilder) -> Self { 129 | Self::TaprootBuilder(value) 130 | } 131 | } 132 | 133 | impl From for ProtocolError { 134 | fn from(value: bitcoin::sighash::TaprootError) -> Self { 135 | Self::TaprootSighash(value) 136 | } 137 | } 138 | impl From for ProtocolError { 139 | fn from(value: secp256k1::scalar::OutOfRangeError) -> Self { 140 | Self::ScalarOutOfRange(value) 141 | } 142 | } 143 | impl From for ProtocolError { 144 | fn from(value: secp256k1::musig::InvalidTweakErr) -> Self { 145 | Self::MusigTweak(value) 146 | } 147 | } 148 | 149 | impl From for ProtocolError { 150 | fn from(value: bitcoin::taproot::SigFromSliceError) -> Self { 151 | Self::TaprootSigSlice(value) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/wallet/error.rs: -------------------------------------------------------------------------------- 1 | //! All Wallet-related errors. 2 | 3 | use crate::protocol::error::ProtocolError; 4 | 5 | use super::fidelity::FidelityError; 6 | 7 | /// Represents various errors that can occur within a wallet. 8 | /// 9 | /// This enum consolidates errors from multiple sources such as I/O, CBOR parsing, 10 | /// and custom application logic. 11 | #[derive(Debug)] 12 | pub enum WalletError { 13 | /// Represents a standard I/O error. 14 | /// 15 | /// Typically occurs during file or network operations. 16 | IO(std::io::Error), 17 | 18 | /// Represents an error during CBOR (Concise Binary Object Representation) serialization or deserialization. 19 | /// 20 | /// This is used for encoding/decoding data structures. 21 | Cbor(serde_cbor::Error), 22 | 23 | /// Represents an error during JSON serialization or deserialization. 24 | /// 25 | /// This is used for encoding/decoding the wallet backup. 26 | Json(serde_json::Error), 27 | 28 | /// Represents an error returned by the Bitcoin Core RPC client. 29 | /// 30 | /// Typically occurs during communication with a Bitcoin node. 31 | Rpc(bitcoind::bitcoincore_rpc::Error), 32 | 33 | /// Represents an error related to BIP32 (Hierarchical Deterministic Wallets). 34 | /// 35 | /// This may occur during key derivation or wallet operations involving BIP32 paths. 36 | BIP32(bitcoin::bip32::Error), 37 | 38 | /// Represents an error related to BIP39 (Mnemonic Codes for Generating Deterministic Keys). 39 | /// 40 | /// Typically occurs when handling mnemonic phrases for seed generation. 41 | BIP39(bip39::Error), 42 | 43 | /// Represents a general error with a descriptive message. 44 | /// 45 | /// Use this variant for errors that do not fall under any specific category. 46 | General(String), 47 | 48 | /// Represents an error related to protocol violations or unexpected protocol behavior. 49 | Protocol(ProtocolError), 50 | 51 | /// Represents an error related to fidelity operations. 52 | /// 53 | /// Typically specific to application-defined fidelity processes. 54 | Fidelity(FidelityError), 55 | 56 | /// Represents an error with Bitcoin's locktime conversion. 57 | /// 58 | /// This occurs when converting between absolute and relative locktime representations. 59 | Locktime(bitcoin::blockdata::locktime::absolute::ConversionError), 60 | 61 | /// Represents an error from the Secp256k1 cryptographic library. 62 | /// 63 | /// Typically occurs during signature generation or verification. 64 | Secp(bitcoin::secp256k1::Error), 65 | 66 | /// Represents an error related to Bitcoin consensus rules. 67 | /// 68 | /// Use this variant to indicate issues related to transaction or block validation. 69 | Consensus(String), 70 | 71 | /// Represents an error when the wallet has insufficient funds to complete an operation. 72 | /// 73 | /// - `available`: The amount of funds available in the wallet. 74 | /// - `required`: The amount of funds needed to complete the operation. 75 | InsufficientFund { 76 | /// The amount of funds available in the wallet. 77 | available: u64, 78 | /// The amount of funds needed to complete the operation. 79 | required: u64, 80 | }, 81 | 82 | /// Represents an error from the rust-coinselect library. 83 | /// 84 | /// Typically occurs during fee calculation or coin selection operations. 85 | Selection(rust_coinselect::types::SelectionError), 86 | } 87 | 88 | impl From for WalletError { 89 | fn from(e: std::io::Error) -> Self { 90 | Self::IO(e) 91 | } 92 | } 93 | 94 | impl From for WalletError { 95 | fn from(value: bitcoind::bitcoincore_rpc::Error) -> Self { 96 | Self::Rpc(value) 97 | } 98 | } 99 | 100 | impl From for WalletError { 101 | fn from(value: bitcoin::bip32::Error) -> Self { 102 | Self::BIP32(value) 103 | } 104 | } 105 | 106 | impl From for WalletError { 107 | fn from(value: bip39::Error) -> Self { 108 | Self::BIP39(value) 109 | } 110 | } 111 | 112 | impl From for WalletError { 113 | fn from(value: ProtocolError) -> Self { 114 | Self::Protocol(value) 115 | } 116 | } 117 | impl From for WalletError { 118 | fn from(value: serde_cbor::Error) -> Self { 119 | Self::Cbor(value) 120 | } 121 | } 122 | 123 | impl From for WalletError { 124 | fn from(value: serde_json::Error) -> Self { 125 | Self::Json(value) 126 | } 127 | } 128 | 129 | impl From for WalletError { 130 | fn from(value: FidelityError) -> Self { 131 | Self::Fidelity(value) 132 | } 133 | } 134 | 135 | impl From for WalletError { 136 | fn from(value: bitcoin::blockdata::locktime::absolute::ConversionError) -> Self { 137 | Self::Locktime(value) 138 | } 139 | } 140 | 141 | impl From for WalletError { 142 | fn from(value: bitcoin::secp256k1::Error) -> Self { 143 | Self::Secp(value) 144 | } 145 | } 146 | 147 | impl From for WalletError { 148 | fn from(value: bitcoin::sighash::P2wpkhError) -> Self { 149 | Self::Consensus(value.to_string()) 150 | } 151 | } 152 | 153 | impl From for WalletError { 154 | fn from(value: bitcoin::key::UncompressedPublicKeyError) -> Self { 155 | Self::Consensus(value.to_string()) 156 | } 157 | } 158 | 159 | impl From for WalletError { 160 | fn from(value: bitcoin::transaction::InputsIndexError) -> Self { 161 | Self::Consensus(value.to_string()) 162 | } 163 | } 164 | 165 | impl From for WalletError { 166 | fn from(value: bitcoin::consensus::encode::Error) -> Self { 167 | Self::Consensus(value.to_string()) 168 | } 169 | } 170 | 171 | impl From for WalletError { 172 | fn from(value: rust_coinselect::types::SelectionError) -> Self { 173 | Self::Selection(value) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/malice1.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | }; 7 | use std::sync::Arc; 8 | 9 | mod test_framework; 10 | use test_framework::*; 11 | 12 | use log::{info, warn}; 13 | use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; 14 | 15 | /// Malice 1: Taker Broadcasts contract transactions prematurely. 16 | /// 17 | /// The Makers identify the situation and get their money back via contract txs. This is 18 | /// a potential DOS on Makers. But Taker would lose money too for doing this. 19 | #[test] 20 | fn malice1_taker_broadcast_contract_prematurely() { 21 | // ---- Setup ---- 22 | 23 | let makers_config_map = [ 24 | ((6102, None), MakerBehavior::Normal), 25 | ((16102, None), MakerBehavior::Normal), 26 | ]; 27 | 28 | let taker_behavior = vec![TakerBehavior::BroadcastContractAfterFullSetup]; 29 | // Initiate test framework, Makers. 30 | // Taker has normal behavior. 31 | let (test_framework, mut takers, makers, block_generation_handle) = 32 | TestFramework::init(makers_config_map.into(), taker_behavior); 33 | 34 | warn!("🧪 Running Test: Malice 1 - Taker broadcasts contract transaction prematurely"); 35 | 36 | info!("💰 Funding taker and makers"); 37 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 38 | let taker = &mut takers[0]; 39 | let org_taker_spend_balance = fund_and_verify_taker( 40 | taker, 41 | &test_framework.bitcoind, 42 | 3, 43 | Amount::from_btc(0.05).unwrap(), 44 | ); 45 | 46 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 47 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 48 | fund_and_verify_maker( 49 | makers_ref, 50 | &test_framework.bitcoind, 51 | 4, 52 | Amount::from_btc(0.05).unwrap(), 53 | ); 54 | 55 | // Start the Maker Server threads 56 | info!("🚀 Initiating Maker servers"); 57 | 58 | let maker_threads = makers 59 | .iter() 60 | .map(|maker| { 61 | let maker_clone = maker.clone(); 62 | thread::spawn(move || { 63 | start_maker_server(maker_clone).unwrap(); 64 | }) 65 | }) 66 | .collect::>(); 67 | 68 | // Makers take time to fully setup. 69 | let org_maker_spend_balances = makers 70 | .iter() 71 | .map(|maker| { 72 | while !maker.is_setup_complete.load(Relaxed) { 73 | info!("⏳ Waiting for maker setup completion"); 74 | // Introduce a delay of 10 seconds to prevent write lock starvation. 75 | thread::sleep(Duration::from_secs(10)); 76 | continue; 77 | } 78 | 79 | // Check balance after setting up maker server. 80 | let wallet = maker.wallet.read().unwrap(); 81 | 82 | let balances = wallet.get_balances().unwrap(); 83 | 84 | verify_maker_pre_swap_balances(&balances, 14999508); 85 | 86 | balances.spendable 87 | }) 88 | .collect::>(); 89 | 90 | // Initiate Coinswap 91 | info!("🔄 Initiating coinswap protocol"); 92 | 93 | // Swap params for coinswap. 94 | let swap_params = SwapParams { 95 | send_amount: Amount::from_sat(500000), 96 | maker_count: 2, 97 | manually_selected_outpoints: None, 98 | }; 99 | taker.do_coinswap(swap_params).unwrap(); 100 | 101 | // After Swap is done, wait for maker threads to conclude. 102 | makers 103 | .iter() 104 | .for_each(|maker| maker.shutdown.store(true, Relaxed)); 105 | 106 | maker_threads 107 | .into_iter() 108 | .for_each(|thread| thread.join().unwrap()); 109 | 110 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 111 | 112 | thread::sleep(Duration::from_secs(10)); 113 | 114 | /////////////////// 115 | let taker_wallet = taker.get_wallet_mut(); 116 | taker_wallet.sync_and_save().unwrap(); 117 | 118 | // Synchronize each maker's wallet. 119 | for maker in makers.iter() { 120 | let mut wallet = maker.get_wallet().write().unwrap(); 121 | wallet.sync_and_save().unwrap(); 122 | } 123 | /////////////// 124 | 125 | //-------- Fee Tracking and Workflow:------------ 126 | // 127 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 128 | // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| 129 | // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | 130 | // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | 131 | // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | 132 | // 133 | // **Taker** => BroadcastContractAfterFullSetup 134 | // 135 | // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** 136 | // due to mining fees (recovery + initial transaction fees). 137 | // 138 | // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | 139 | // |----------------|------------------------------------|---------------------|--------------------|----------------------------| 140 | // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | 141 | // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | 142 | // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | 143 | 144 | info!("📊 Verifying malicious taker scenario results"); 145 | // After Swap checks: 146 | verify_swap_results( 147 | taker, 148 | &makers, 149 | org_taker_spend_balance, 150 | org_maker_spend_balances, 151 | ); 152 | 153 | info!("🎉 All checks successful. Terminating integration test case"); 154 | 155 | test_framework.stop(); 156 | block_generation_handle.join().unwrap(); 157 | } 158 | -------------------------------------------------------------------------------- /tests/taker_cli.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bip39::rand; 3 | use bitcoin::{address::NetworkChecked, Address, Amount}; 4 | use bitcoind::{bitcoincore_rpc::RpcApi, tempfile::env::temp_dir, BitcoinD}; 5 | 6 | use serde_json::Value; 7 | use std::{fs, path::PathBuf, process::Command, str::FromStr}; 8 | mod test_framework; 9 | use test_framework::{generate_blocks, init_bitcoind, send_to_address}; 10 | 11 | use log::info; 12 | 13 | /// The taker-cli command struct 14 | struct TakerCli { 15 | data_dir: PathBuf, 16 | bitcoind: BitcoinD, 17 | zmq_addr: String, 18 | } 19 | 20 | impl TakerCli { 21 | /// Construct a new [`TakerCli`] struct that also include initiating bitcoind. 22 | fn new() -> TakerCli { 23 | // Initiate the bitcoind backend. 24 | 25 | let temp_dir = temp_dir().join("coinswap"); 26 | 27 | // Remove if previously existing 28 | if temp_dir.exists() { 29 | fs::remove_dir_all(&temp_dir).unwrap(); 30 | } 31 | 32 | let port_zmq = 28332 + rand::random::() % 1000; 33 | 34 | let zmq_addr = format!("tcp://127.0.0.1:{port_zmq}"); 35 | 36 | let bitcoind = init_bitcoind(&temp_dir, zmq_addr.clone()); 37 | let data_dir = temp_dir.join("taker"); 38 | 39 | TakerCli { 40 | data_dir, 41 | bitcoind, 42 | zmq_addr, 43 | } 44 | } 45 | 46 | // Execute a cli-command 47 | fn execute(&self, cmd: &[&str]) -> String { 48 | let mut args = vec!["--data-directory", self.data_dir.to_str().unwrap()]; 49 | 50 | // RPC authentication (user:password) from the cookie file 51 | let cookie_file_path = &self.bitcoind.params.cookie_file; 52 | let rpc_auth = fs::read_to_string(cookie_file_path).expect("failed to read from file"); 53 | args.push("--USER:PASSWORD"); 54 | args.push(&rpc_auth); 55 | 56 | // Full node address for RPC connection 57 | let rpc_address = self.bitcoind.params.rpc_socket.to_string(); 58 | args.push("--ADDRESS:PORT"); 59 | args.push(&rpc_address); 60 | 61 | args.push("--WALLET"); 62 | args.push("test_wallet"); 63 | 64 | args.push("--ZMQ"); 65 | args.push(&self.zmq_addr); 66 | 67 | for arg in cmd { 68 | args.push(arg); 69 | } 70 | 71 | let output = Command::new(env!("CARGO_BIN_EXE_taker")) 72 | .args(args) 73 | .output() 74 | .expect("Failed to execute taker"); 75 | 76 | // Capture the standard output and error from the command execution 77 | let mut value = output.stdout; 78 | let error = output.stderr; 79 | 80 | // Panic if there is any error output 81 | if !error.is_empty() { 82 | panic!("Error: {:?}", String::from_utf8(error).unwrap()); 83 | } 84 | 85 | // Remove the `\n` at the end of the output 86 | value.pop(); 87 | 88 | // Convert the output bytes to a UTF-8 string 89 | let output_string = std::str::from_utf8(&value).unwrap().to_string(); 90 | 91 | output_string 92 | } 93 | } 94 | 95 | #[test] 96 | fn test_taker_cli() { 97 | info!("🧪 Running Test: Taker CLI functionality and wallet operations"); 98 | 99 | let taker_cli = TakerCli::new(); 100 | info!("🚀 TakerCli initialized successfully"); 101 | 102 | let bitcoind = &taker_cli.bitcoind; 103 | 104 | info!("💰 Funding taker with 3 UTXOs of 1 BTC each"); 105 | // Fund the taker with 3 utxos of 1 BTC each. 106 | for _ in 0..3 { 107 | let taker_address = taker_cli.execute(&["get-new-address"]); 108 | 109 | let taker_address: Address = 110 | Address::from_str(&taker_address).unwrap().assume_checked(); 111 | 112 | send_to_address(bitcoind, &taker_address, Amount::ONE_BTC); 113 | } 114 | 115 | // confirm balance 116 | generate_blocks(bitcoind, 10); 117 | 118 | info!("📊 Verifying initial balance - expecting 3 BTC"); 119 | // Assert that total_balance & seed_balance must be 3 BTC 120 | let balances = taker_cli.execute(&["get-balances"]); 121 | let balances = serde_json::from_str::(&balances).unwrap(); 122 | 123 | assert_eq!("300000000", balances["regular"].to_string()); 124 | assert_eq!("0", balances["swap"].to_string()); 125 | assert_eq!("0", balances["contract"].to_string()); 126 | assert_eq!("300000000", balances["spendable"].to_string()); 127 | 128 | info!("🔍 Checking UTXO count - expecting 3 UTXOs"); 129 | // Assert that total no of seed-utxos are 3. 130 | let all_utxos = taker_cli.execute(&["list-utxo"]); 131 | 132 | let no_of_seed_utxos = all_utxos.matches("addr").count(); 133 | assert_eq!(3, no_of_seed_utxos); 134 | info!("✅ Initial setup verification successful"); 135 | 136 | info!("💸 Testing internal transfer - 100,000 sats with 1,000 sats fee"); 137 | // Send 100,000 sats to a new address within the wallet, with a fee of 1,000 sats. 138 | 139 | // get new external address 140 | let new_address = taker_cli.execute(&["get-new-address"]); 141 | 142 | let _ = taker_cli.execute(&[ 143 | "send-to-address", 144 | "-t", 145 | &new_address, 146 | "-a", 147 | "100000", 148 | "-f", 149 | "2", 150 | ]); 151 | 152 | generate_blocks(bitcoind, 10); 153 | 154 | info!("📊 Verifying balance after transfer - expecting only fee deduction"); 155 | // Assert the total_amount & seed_amount must be initial (balance -fee) 156 | let balances = taker_cli.execute(&["get-balances"]); 157 | let balances = serde_json::from_str::(&balances).unwrap(); 158 | 159 | // Since the amount is sent back to our wallet, the transaction fee is deducted from the balance. 160 | assert_eq!("299999720", balances["regular"].to_string()); 161 | assert_eq!("0", balances["swap"].to_string()); 162 | assert_eq!("0", balances["contract"].to_string()); 163 | assert_eq!("299999720", balances["spendable"].to_string()); 164 | 165 | info!("🔍 Checking final UTXO count - expecting 4 UTXOs"); 166 | // Assert that no of seed utxos are 4 167 | let all_utxos = taker_cli.execute(&["list-utxo"]); 168 | 169 | let no_of_seed_utxos = all_utxos.matches("addr").count(); 170 | assert_eq!(4, no_of_seed_utxos); 171 | info!("✅ Transfer verification successful"); 172 | 173 | info!("🔧 Shutting down bitcoind"); 174 | bitcoind.client.stop().unwrap(); 175 | 176 | // Wait for some time for successful shutdown of bitcoind. 177 | std::thread::sleep(std::time::Duration::from_secs(3)); 178 | 179 | info!("🎉 Taker CLI test completed successfully!"); 180 | } 181 | -------------------------------------------------------------------------------- /tests/funding_dynamic_splits.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | mod test_framework; 3 | use bitcoin::{Address, Amount}; 4 | use coinswap::{taker::TakerBehavior, utill::MIN_FEE_RATE}; 5 | use test_framework::*; 6 | 7 | const UTXO_SETS: &[&[u64]] = &[ 8 | &[ 9 | 107_831, 91_379, 712_971, 432_441, 301_909, 38_012, 298_092, 9_091, 10 | ], 11 | &[109_831, 3_919], 12 | &[1_946_436], 13 | &[1_000_000, 1_992_436], 14 | &[70_000, 800_000, 900_000, 100_000], 15 | &[46_824, 53_245, 65_658, 35_892], 16 | ]; 17 | 18 | // Test data structure: (target amount, expected selected inputs, expected number of outputs) 19 | #[rustfmt::skip] 20 | const TEST_CASES: &[(u64, &[u64], u64)] = &[ 21 | (54_082, &[53245, 46824, 9091], 4), // CASE A : Threshold -> 2 Targets, 2 Changes 22 | (102_980, &[35892, 70000, 65658, 38012], 4), // CASE B : Threshold -> 2 Targets, 2 Changes 23 | (708_742, &[107831, 301909, 38012, 35892, 9091, 65658, 53245, 100000, 712971], 4), // CASE C.1 : Threshold -> 2 Targets, 2 Changes 24 | (500_000, &[91379, 3919, 107831, 35892, 46824, 9091, 38012, 70000, 100000, 65658, 298092, 53245, 109831], 4), // CASE C.2 : Deterministic -> 2 Targets, 2 Changes 25 | (654_321, &[301909, 46824, 70000, 38012, 9091, 100000, 91379, 53245, 65658, 432441, 107831], 4), // Edge Case A for the UTXO set 26 | (90_000, &[53245, 35892, 3919, 91379], 4), // Edge Case B 27 | (10_000, &[9091, 3919], 4), // Gradual scaling targets 28 | // (100_000, &[38012, 65658, 46824, 53245], 4), // OR &[38012, 65658, 100000] Edge Case C. Will investigate 29 | (1_000_000, &[91379, 100000, 9091, 65658, 109831, 46824, 107831, 35892, 3919, 432441, 38012, 70000, 900000], 4), 30 | (7_777_777, &[46824, 1992436, 1000000, 712971, 9091, 91379, 38012, 900000, 800000, 1946436, 65658, 70000, 107831], 3), // Odd, lopsided amounts 31 | (888_888, &[3919, 9091, 35892, 46824, 53245, 65658, 70000, 91379, 107831, 109831, 298092, 100000, 800000], 4), 32 | (999_999, &[432441, 109831, 107831, 100000, 91379, 65658, 46824, 35892, 9091, 3919, 900000, 38012, 70000], 4), // Near-round numbers 33 | (123_456, &[38012, 53245, 35892, 46824, 9091, 3919, 65658], 4), // Non-round numbers 34 | (250_000, &[9091, 53245, 107831, 46824, 35892, 109831, 100000, 38012, 3919], 4), // Large, uneven splits 35 | (7_500_000, &[1946436, 1000000, 900000, 3919, 9091, 38012, 100000, 712971, 800000, 1992436], 3), 36 | (500, &[3919], 3), // Small, uneven splits 37 | (1_500, &[9091], 6), 38 | (2_500, &[9091], 7), 39 | ]; 40 | 41 | #[test] 42 | fn test_create_funding_txn_with_varied_distributions() { 43 | println!( 44 | "Sum of the Entire UTXO set: {}\n", 45 | UTXO_SETS.iter().flat_map(|x| x.iter()).sum::() 46 | ); 47 | 48 | // Initialize the test framework with a single taker with Normal behavior 49 | let (test_framework, mut takers, _, _) = 50 | TestFramework::init(vec![], vec![TakerBehavior::Normal]); 51 | 52 | let bitcoind = &test_framework.bitcoind; 53 | let taker = &mut takers[0]; 54 | 55 | // Fund the taker with the UTXO sets 56 | for individual_utxo in UTXO_SETS.iter().flat_map(|x| x.iter()) { 57 | fund_and_verify_taker(taker, bitcoind, 1, Amount::from_sat(*individual_utxo)); 58 | } 59 | 60 | // Generate 5 random addresses from the taker's wallet 61 | let mut destinations: Vec
= Vec::with_capacity(5); 62 | for _ in 0..5 { 63 | let addr = taker.get_wallet_mut().get_next_external_address().unwrap(); 64 | destinations.push(addr); 65 | } 66 | 67 | for (i, (target_amount, expected_inputs, expected_outputs)) in TEST_CASES.iter().enumerate() { 68 | let target = Amount::from_sat(*target_amount); 69 | 70 | // Call `create_funding_txes` with Normie Flag turned off, i.e Destination::MultiDynamic 71 | let result = taker 72 | .get_wallet_mut() 73 | .create_funding_txes_regular_swaps( 74 | false, 75 | target, 76 | destinations.clone(), 77 | Amount::from_sat(MIN_FEE_RATE as u64), 78 | None, 79 | ) 80 | .unwrap(); 81 | 82 | let tx = &result.funding_txes[0]; 83 | let selected_inputs = tx 84 | .input 85 | .iter() 86 | .map(|txin| { 87 | taker 88 | .get_wallet() 89 | .list_all_utxo_spend_info() 90 | .iter() 91 | .find(|(utxo, _)| { 92 | txin.previous_output.txid == utxo.txid 93 | && txin.previous_output.vout == utxo.vout 94 | }) 95 | .map(|(u, _)| u.amount) 96 | .expect("should find utxo") 97 | }) 98 | .collect::>(); 99 | 100 | let outputs = tx.output.iter().map(|o| o.value).collect::>(); 101 | let sum_of_inputs = selected_inputs.iter().map(|a| a.to_sat()).sum::(); 102 | let sum_of_outputs = tx.output.iter().map(|o| o.value.to_sat()).sum::(); 103 | let actual_fee = sum_of_inputs - sum_of_outputs; 104 | let tx_size = tx.weight().to_vbytes_ceil(); 105 | let actual_feerate = actual_fee as f64 / tx_size as f64; 106 | 107 | println!("\nTarget = {}", Amount::to_sat(target)); 108 | println!("Sum of Inputs: {sum_of_inputs:?}"); 109 | println!("Inputs : {selected_inputs:?}"); 110 | println!("Outputs: {outputs:?}"); 111 | println!("Actual fee rate: {actual_feerate}"); 112 | 113 | // Assert no duplicate inputs. 114 | assert!(selected_inputs.iter().all(|&x| selected_inputs 115 | .iter() 116 | .filter(|&&y| y == x) 117 | .count() 118 | == 1),); 119 | 120 | // Assert the Output UTXOs matches the expected outputs. 121 | for &utxo in *expected_inputs { 122 | assert!( 123 | selected_inputs.contains(&Amount::from_sat(utxo)), 124 | "Missing UTXO input: {} in test case {}", 125 | utxo, 126 | i 127 | ); 128 | } 129 | 130 | // Assert the number of Outputs matches the expected number. 131 | assert_eq!( 132 | outputs.len(), 133 | *expected_outputs as usize, 134 | "Expected {} outputs, got {}", 135 | expected_outputs, 136 | outputs.len() 137 | ); 138 | 139 | // Assert Fee is less than 98% of the expected Fee Rate or equal to MIN_FEE_RATE. 140 | assert!( 141 | actual_feerate > MIN_FEE_RATE * 0.98 || actual_fee == MIN_FEE_RATE as u64, 142 | "Fee rate ({}) is not less than 98% of MIN_FEE_RATE ({}) or fee is not equal to MIN_FEE_RATE", 143 | actual_feerate, 144 | MIN_FEE_RATE 145 | ); 146 | } 147 | test_framework.stop(); 148 | println!("\nTest completed successfully."); 149 | } 150 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # Coinswap Docker 2 | 3 | A dockerized version of the **Complete Coinswap Backend**. 4 | 5 | The Docker spawns a multiple containers, with Mutinynet, Tor, makerd, maker-cli, and taker, with the right configurations and connections among each other. 6 | 7 | Various subset of the stack can be used for different application needs and environments. 8 | 9 | ## Overview 10 | 11 | The Docker setup provides a complete environment for running any Coinswap applications: 12 | 13 | - **Single unified Dockerfile** containing all Coinswap components 14 | - **Alpine Linux 3.20** base image for minimal size 15 | - **Rust 1.90.0** for building the applications 16 | - **Custom Bitcoin Mutinynet image** for Signet testing 17 | - **External Tor image** (`leplusorg/tor`) 18 | - **Interactive configuration** with automatic service detection 19 | - **Coinswap binaries:** `makerd`, `maker-cli`, `taker` 20 | 21 | ## Architecture 22 | 23 | This is an overview for the docker stack with default settings for all nodes: 24 | 25 | ```mermaid 26 | graph TD 27 | bitcoind["Bitcoind
Bitcoin Node
RPC Port (Default: 18332)
ZMQ Port (Default: 28332)"] 28 | tor["Tor
Tor Proxy
SOCKS Port (Default: 9050)
Control Port (Default: 9051)"] 29 | makerd["Makerd
Network Port (Default: 6102)
RPC Port (Default: 6103)"] 30 | 31 | bitcoin_vol["bitcoin-data"] 32 | tor_vol["tor-data"] 33 | maker_vol["maker-data"] 34 | 35 | network["coinswap-network"] 36 | 37 | makerd <-->|"RPC calls
(on bitcoind rpc-port)"| bitcoind 38 | makerd -->|SOCKS proxy| tor 39 | makerd -->|Control commands| tor 40 | 41 | bitcoind --> bitcoin_vol 42 | tor --> tor_vol 43 | makerd --> maker_vol 44 | 45 | bitcoind --> network 46 | tor --> network 47 | makerd --> network 48 | 49 | style bitcoind fill:#f9a825 50 | style tor fill:#7d4698 51 | style makerd fill:#3498db 52 | style network fill:#e8f4f 53 | style bitcoin_vol fill:#ecf0f 54 | style tor_vol fill:#ecf0f 55 | style maker_vol fill:#fcf0f 56 | ``` 57 | 58 | The Docker setup uses: 59 | 60 | - `docker/Dockerfile` - Unified image containing `makerd`, `maker-cli`, and `taker` 61 | - `docker/Dockerfile.bitcoin-mutinynet` - Custom Bitcoin Core image for Mutinynet 62 | - External images: `leplusorg/tor` for Tor 63 | 64 | ## Quick Start 65 | 66 | ### Using the Setup Script (Recommended) 67 | 68 | The `docker-setup` script provides an interactive way to configure and run Coinswap: 69 | 70 | ```bash 71 | git clone https://github.com/citadel-tech/coinswap.git 72 | cd coinswap 73 | 74 | # Interactive configuration 75 | ./docker-setup configure 76 | 77 | # Build the Docker image 78 | ./docker-setup build 79 | 80 | # (Optional) Build the Bitcoin Mutinynet image if needed 81 | ./docker-setup build-bitcoin 82 | 83 | # Start the complete stack 84 | ./docker-setup start 85 | 86 | # Check status 87 | ./docker-setup status 88 | 89 | # View logs 90 | ./docker-setup logs makerd 91 | ``` 92 | 93 | ### Configuration Options 94 | 95 | The setup script will prompt for: 96 | 97 | 1. **Bitcoin Core Configuration**: 98 | - Data directory path 99 | - Network selection (regtest/signet/testnet/mainnet) 100 | - Use existing Bitcoin Core instance or spawn new one 101 | - Custom RPC and ZMQ ports 102 | 103 | 2. **Tor Configuration**: 104 | - Detect existing Tor instance 105 | - Use external Tor or spawn containerized version 106 | - Custom SOCKS and control ports 107 | 108 | 3. **Service Ports**: 109 | - Makerd network port (default: 6102) 110 | - Makerd RPC port (default: 6103) 111 | 112 | Configuration is saved to `.docker-config` and reused on subsequent runs. 113 | 114 | ## Building Docker Images 115 | 116 | The build process creates a single image containing all binaries: 117 | 118 | ```bash 119 | git clone https://github.com/citadel-tech/coinswap.git 120 | cd coinswap 121 | 122 | # Build using the setup script 123 | ./docker-setup build 124 | 125 | # Build Bitcoin Mutinynet image 126 | ./docker-setup build-bitcoin 127 | 128 | # Or build manually 129 | docker build -f docker/Dockerfile -t coinswap:latest . 130 | ``` 131 | 132 | ### Available Images 133 | 134 | - **coinswap**: Contains `makerd`, `maker-cli`, and `taker` 135 | - **bitcoin-mutinynet**: Custom Bitcoin Core node for Mutinynet network 136 | 137 | ## Running Applications 138 | 139 | ### Makerd (Maker Daemon) 140 | 141 | Run the maker daemon with persistent data storage: 142 | 143 | ```bash 144 | # Using the setup script (recommended) 145 | ./docker-setup start 146 | 147 | # Or manually with specific image 148 | docker run -d \ 149 | --name coinswap-makerd \ 150 | -p 6102:6102 \ 151 | -p 6103:6103 \ 152 | -v coinswap-maker-data:/home/coinswap/.coinswap \ 153 | --network coinswap-network \ 154 | coinswap:latest makerd 155 | ``` 156 | 157 | **Port mappings:** 158 | - `6102`: Maker network port for coinswap protocol 159 | - `6103`: Maker RPC port for `maker-cli` commands 160 | 161 | ### Maker CLI 162 | 163 | Control the maker daemon: 164 | 165 | ```bash 166 | # Using the setup script 167 | ./docker-setup maker-cli ping 168 | ./docker-setup maker-cli wallet-balance 169 | ./docker-setup maker-cli stop 170 | 171 | # Or manually 172 | docker run --rm --network coinswap-network coinswap:latest maker-cli ping 173 | docker run --rm --network coinswap-network coinswap:latest maker-cli wallet-balance 174 | docker run --rm --network coinswap-network coinswap:latest maker-cli stop 175 | ``` 176 | 177 | ### Taker 178 | 179 | Run taker operations: 180 | 181 | ```bash 182 | # Using the setup script 183 | ./docker-setup taker --help 184 | 185 | # Or manually 186 | docker run --rm -it \ 187 | -v coinswap-taker-data:/home/coinswap/.coinswap \ 188 | --network coinswap-network \ 189 | coinswap:latest taker --help 190 | ``` 191 | 192 | ## Docker Compose Setup 193 | 194 | The setup script automatically generates `docker-compose.generated.yml` based on your configuration. For a complete setup with all services: 195 | 196 | ```bash 197 | # Start all services (Bitcoin Core, Tor, Makerd) 198 | ./docker-setup start 199 | 200 | # Or use docker-compose directly 201 | docker compose -f docker-compose.generated.yml up -d 202 | 203 | # Check status 204 | ./docker-setup status 205 | 206 | # View logs 207 | ./docker-setup logs makerd 208 | ``` 209 | 210 | ## Data Persistence 211 | 212 | All application data is stored in Docker volumes: 213 | 214 | - `bitcoin-data`: Bitcoin blockchain data 215 | - `tor-data`: Tor configuration and data 216 | - `maker-data`: Maker configuration and wallet data 217 | 218 | ## Troubleshooting 219 | 220 | ### Check logs 221 | 222 | ```bash 223 | # using setup script 224 | ./docker-setup logs makerd 225 | 226 | # or directly with docker-compose 227 | docker compose -f docker-compose.generated.yml logs -f makerd 228 | ``` 229 | 230 | ### Interactive debugging 231 | 232 | ```bash 233 | # enter container for debugging 234 | ./docker-setup shell 235 | 236 | # or manually 237 | docker run --rm -it coinswap:latest sh 238 | ``` 239 | -------------------------------------------------------------------------------- /tests/standard_swap.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | utill::MIN_FEE_RATE, 7 | wallet::Destination, 8 | }; 9 | use std::sync::Arc; 10 | 11 | use bitcoind::bitcoincore_rpc::RpcApi; 12 | 13 | mod test_framework; 14 | use test_framework::*; 15 | 16 | use log::{info, warn}; 17 | use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; 18 | 19 | /// This test demonstrates a standard coinswap round between a Taker and 2 Makers. Nothing goes wrong 20 | /// and the coinswap completes successfully. 21 | #[test] 22 | fn test_standard_coinswap() { 23 | // ---- Setup ---- 24 | 25 | // 2 Makers with Normal behavior. 26 | let makers_config_map = [ 27 | ((6102, Some(19051)), MakerBehavior::Normal), 28 | ((16102, Some(19052)), MakerBehavior::Normal), 29 | ]; 30 | 31 | let taker_behavior = vec![TakerBehavior::Normal]; 32 | 33 | // Initiate test framework, Makers and a Taker with default behavior. 34 | let (test_framework, mut takers, makers, block_generation_handle) = 35 | TestFramework::init(makers_config_map.into(), taker_behavior); 36 | 37 | warn!("🧪 Running Test: Standard Coinswap Procedure"); 38 | let bitcoind = &test_framework.bitcoind; 39 | 40 | info!("💰 Funding taker and makers"); 41 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 42 | let taker = &mut takers[0]; 43 | let org_taker_spend_balance = 44 | fund_and_verify_taker(taker, bitcoind, 3, Amount::from_btc(0.05).unwrap()); 45 | 46 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 47 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 48 | fund_and_verify_maker(makers_ref, bitcoind, 4, Amount::from_btc(0.05).unwrap()); 49 | 50 | // Start the Maker Server threads 51 | info!("🚀 Initiating Maker servers"); 52 | 53 | let maker_threads = makers 54 | .iter() 55 | .map(|maker| { 56 | let maker_clone = maker.clone(); 57 | thread::spawn(move || { 58 | start_maker_server(maker_clone).unwrap(); 59 | }) 60 | }) 61 | .collect::>(); 62 | 63 | // Makers take time to fully setup. 64 | let org_maker_spend_balances = makers 65 | .iter() 66 | .map(|maker| { 67 | while !maker.is_setup_complete.load(Relaxed) { 68 | info!("⏳ Waiting for maker setup completion"); 69 | // Introduce a delay of 10 seconds to prevent write lock starvation. 70 | thread::sleep(Duration::from_secs(10)); 71 | continue; 72 | } 73 | 74 | // Check balance after setting up maker server. 75 | let wallet = maker.wallet.read().unwrap(); 76 | 77 | let balances = wallet.get_balances().unwrap(); 78 | 79 | verify_maker_pre_swap_balances(&balances, 14999508); 80 | 81 | balances.spendable 82 | }) 83 | .collect::>(); 84 | 85 | // Initiate Coinswap 86 | info!("🔄 Initiating coinswap protocol"); 87 | 88 | // Swap params for coinswap. 89 | let swap_params = SwapParams { 90 | send_amount: Amount::from_sat(500000), 91 | maker_count: 2, 92 | manually_selected_outpoints: None, 93 | }; 94 | taker.do_coinswap(swap_params).unwrap(); 95 | 96 | // After Swap is done, wait for maker threads to conclude. 97 | makers 98 | .iter() 99 | .for_each(|maker| maker.shutdown.store(true, Relaxed)); 100 | 101 | maker_threads 102 | .into_iter() 103 | .for_each(|thread| thread.join().unwrap()); 104 | 105 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 106 | 107 | thread::sleep(Duration::from_secs(10)); 108 | 109 | //-------- Fee Tracking and Workflow:------------ 110 | // 111 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 112 | // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| 113 | // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | 114 | // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | 115 | // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | 116 | // 117 | // ## 3. Final Outcome for Taker (Successful Coinswap): 118 | // 119 | // | Participant | Coinswap Outcome (Sats) | 120 | // |---------------|---------------------------------------------------------------------------| 121 | // | **Taker** | 438,642= 500,000 - (Total Fees for Maker16102 + Total Fees for Maker6102) | 122 | // 123 | // ## 4. Final Outcome for Makers: 124 | // 125 | // | Participant | Coinswap Outcome (Sats) | 126 | // |----------------|-------------------------------------------------------------------| 127 | // | **Maker16102** | 500,000 - 463,500 - 3,000 = +33,500 | 128 | // | **Maker6102** | 465,384 - 438,642 - 3,000 = +21,858 | 129 | 130 | info!("📊 Verifying swap results"); 131 | // After Swap Asserts 132 | verify_swap_results( 133 | taker, 134 | &makers, 135 | org_taker_spend_balance, 136 | org_maker_spend_balances, 137 | ); 138 | 139 | info!("✅ Balance check successful"); 140 | 141 | // Check spending from swapcoins. 142 | info!("💸 Checking spend from swapcoins"); 143 | 144 | let taker_wallet_mut = taker.get_wallet_mut(); 145 | let swap_coins = taker_wallet_mut.list_swept_incoming_swap_utxos(); 146 | 147 | let addr = taker_wallet_mut.get_next_internal_addresses(1).unwrap()[0].to_owned(); 148 | 149 | let tx = taker_wallet_mut 150 | .spend_from_wallet(MIN_FEE_RATE, Destination::Sweep(addr), &swap_coins) 151 | .unwrap(); 152 | 153 | assert_eq!( 154 | tx.input.len(), 155 | 1, 156 | "Not all swap coin utxos got included in the spend transaction" 157 | ); 158 | 159 | bitcoind.client.send_raw_transaction(&tx).unwrap(); 160 | generate_blocks(bitcoind, 1); 161 | 162 | taker_wallet_mut.sync_and_save().unwrap(); 163 | 164 | let balances = taker_wallet_mut.get_balances().unwrap(); 165 | 166 | assert_in_range!(balances.swap.to_sat(), [443415], "Swap Balance Mismatch"); 167 | assert_in_range!( 168 | balances.regular.to_sat(), 169 | [14499696], 170 | "Taker regular balance mismatch" 171 | ); 172 | 173 | info!("🎉 All checks successful. Terminating integration test case"); 174 | 175 | test_framework.stop(); 176 | block_generation_handle.join().unwrap(); 177 | } 178 | -------------------------------------------------------------------------------- /src/taker/config.rs: -------------------------------------------------------------------------------- 1 | //! Taker configuration. Controlling various behaviors. 2 | //! 3 | //! This module defines the configuration options for the Taker module, controlling various aspects 4 | //! of the taker's behavior including network settings, connection preferences, and security settings. 5 | 6 | use crate::utill::{get_taker_dir, parse_field, parse_toml}; 7 | use std::{io, io::Write, path::Path}; 8 | 9 | /// Taker configuration 10 | /// 11 | /// This struct defines all configurable parameters for the Taker app, including all network ports and marketplace settings 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct TakerConfig { 14 | /// Control port for Tor interface (default: 9051) 15 | pub control_port: u16, 16 | /// Socks port for Tor proxy (default: 9050) 17 | pub socks_port: u16, 18 | /// Authentication password for Tor interface 19 | pub tor_auth_password: String, 20 | } 21 | 22 | impl Default for TakerConfig { 23 | fn default() -> Self { 24 | Self { 25 | control_port: 9051, 26 | socks_port: 9050, 27 | tor_auth_password: "".to_string(), 28 | } 29 | } 30 | } 31 | 32 | impl TakerConfig { 33 | /// Constructs a [`TakerConfig`] from a specified data directory. Or create default configs and load them. 34 | /// 35 | /// The maker(/taker).toml file should exist at the provided data-dir location. 36 | /// Or else, a new default-config will be loaded and created at the given data-dir location. 37 | /// If no data-dir is provided, a default config will be created at the default data-dir location. 38 | /// 39 | /// For reference of default config checkout `./taker.toml` in repo folder. 40 | /// 41 | /// Default data-dir for linux: `~/.coinswap/taker` 42 | /// Default config locations: `~/.coinswap/taker/config.toml`. 43 | pub(crate) fn new(config_path: Option<&Path>) -> io::Result { 44 | let default_config_path = get_taker_dir().join("config.toml"); 45 | 46 | let config_path = config_path.unwrap_or(&default_config_path); 47 | 48 | let default_config = Self::default(); 49 | 50 | if !config_path.exists() || std::fs::metadata(config_path)?.len() == 0 { 51 | log::warn!( 52 | "Taker config file not found, creating default config file at path: {}", 53 | config_path.display() 54 | ); 55 | default_config.write_to_file(config_path)?; 56 | } 57 | 58 | let config_map = parse_toml(config_path)?; 59 | 60 | log::info!( 61 | "Successfully loaded config file from : {}", 62 | config_path.display() 63 | ); 64 | 65 | Ok(TakerConfig { 66 | control_port: parse_field(config_map.get("control_port"), default_config.control_port), 67 | socks_port: parse_field(config_map.get("socks_port"), default_config.socks_port), 68 | tor_auth_password: parse_field( 69 | config_map.get("tor_auth_password"), 70 | default_config.tor_auth_password, 71 | ), 72 | }) 73 | } 74 | 75 | /// This method serializes the TakerConfig into a TOML format and writes it to disk. 76 | /// It creates the parent directory if it doesn't exist. 77 | pub(crate) fn write_to_file(&self, path: &Path) -> std::io::Result<()> { 78 | let toml_data = format!( 79 | "# Taker Configuration File 80 | # Control port for Tor control interface 81 | control_port = {} 82 | # Socks port for Tor proxy 83 | socks_port = {} 84 | # Authentication password for Tor control interface 85 | tor_auth_password = {}", 86 | self.control_port, self.socks_port, self.tor_auth_password, 87 | ); 88 | 89 | std::fs::create_dir_all(path.parent().expect("Path should NOT be root!"))?; 90 | let mut file = std::fs::File::create(path)?; 91 | file.write_all(toml_data.as_bytes())?; 92 | file.flush()?; 93 | Ok(()) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | 100 | use crate::taker::api::REFUND_LOCKTIME; 101 | 102 | use super::*; 103 | use std::{ 104 | fs::{self, File}, 105 | io::Write, 106 | path::PathBuf, 107 | }; 108 | 109 | fn create_temp_config(contents: &str, file_name: &str) -> PathBuf { 110 | let file_path = PathBuf::from(file_name); 111 | let mut file = File::create(&file_path).unwrap(); 112 | writeln!(file, "{contents}").unwrap(); 113 | file_path 114 | } 115 | 116 | fn remove_temp_config(path: &Path) { 117 | fs::remove_file(path).unwrap(); 118 | } 119 | 120 | #[test] 121 | fn test_valid_config() { 122 | let contents = r#" 123 | control_port = 9051 124 | socks_port = 9050 125 | connection_type = "TOR" 126 | rpc_port = 8081 127 | "#; 128 | let config_path = create_temp_config(contents, "valid_taker_config.toml"); 129 | let config = TakerConfig::new(Some(&config_path)).unwrap(); 130 | remove_temp_config(&config_path); 131 | 132 | let default_config = TakerConfig::default(); 133 | assert_eq!(config, default_config); 134 | } 135 | 136 | #[test] 137 | fn test_missing_fields() { 138 | let contents = r#" 139 | [taker_config] 140 | refund_locktime = 48 141 | "#; 142 | let config_path = create_temp_config(contents, "missing_fields_taker_config.toml"); 143 | let config = TakerConfig::new(Some(&config_path)).unwrap(); 144 | remove_temp_config(&config_path); 145 | 146 | assert_eq!(REFUND_LOCKTIME, 20); 147 | assert_eq!(config, TakerConfig::default()); 148 | } 149 | 150 | #[test] 151 | fn test_incorrect_data_type() { 152 | let contents = r#" 153 | [taker_config] 154 | refund_locktime = "not_a_number" 155 | "#; 156 | let config_path = create_temp_config(contents, "incorrect_type_taker_config.toml"); 157 | let config = TakerConfig::new(Some(&config_path)).unwrap(); 158 | remove_temp_config(&config_path); 159 | 160 | assert_eq!(config, TakerConfig::default()); 161 | } 162 | 163 | #[test] 164 | fn test_different_data() { 165 | let contents = r#" 166 | [taker_config] 167 | socks_port = 9050 168 | "#; 169 | let config_path = create_temp_config(contents, "different_data_taker_config.toml"); 170 | let config = TakerConfig::new(Some(&config_path)).unwrap(); 171 | remove_temp_config(&config_path); 172 | assert_eq!(REFUND_LOCKTIME, 20); 173 | assert_eq!( 174 | TakerConfig { 175 | socks_port: 9050, // Configurable via TOML. 176 | ..TakerConfig::default() // Use default for other values. 177 | }, 178 | config 179 | ); 180 | } 181 | 182 | #[test] 183 | fn test_missing_file() { 184 | let config_path = get_taker_dir().join("taker.toml"); 185 | let config = TakerConfig::new(Some(&config_path)).unwrap(); 186 | remove_temp_config(&config_path); 187 | assert_eq!(config, TakerConfig::default()); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tests/abort3_case3.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | }; 7 | use std::sync::Arc; 8 | 9 | mod test_framework; 10 | use test_framework::*; 11 | 12 | use log::{info, warn}; 13 | use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; 14 | 15 | /// ABORT 3: Maker Drops After Setup 16 | /// Case 3: CloseAtHashPreimage 17 | /// 18 | /// Maker closes the connection at hash preimage handling. Funding txs are already broadcasted. 19 | /// The Maker will lose contract txs fees in that case, so it's not malice. 20 | /// Taker waits for the response until timeout. Aborts if the Maker doesn't show up. 21 | #[test] 22 | fn abort3_case3_close_at_hash_preimage_handover() { 23 | // ---- Setup ---- 24 | 25 | // 6102 is naughty. And theres not enough makers. 26 | let makers_config_map = [ 27 | ((6102, None), MakerBehavior::CloseAtHashPreimage), 28 | ((16102, None), MakerBehavior::Normal), 29 | ]; 30 | 31 | let taker_behavior = vec![TakerBehavior::Normal]; 32 | // Initiate test framework, Makers. 33 | // Taker has normal behavior. 34 | let (test_framework, mut takers, makers, block_generation_handle) = 35 | TestFramework::init(makers_config_map.into(), taker_behavior); 36 | 37 | warn!("🧪 Running Test: Maker closes connection at hash preimage handling"); 38 | 39 | info!("💰 Funding taker and makers"); 40 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 41 | let taker = &mut takers[0]; 42 | let org_taker_spend_balance = fund_and_verify_taker( 43 | taker, 44 | &test_framework.bitcoind, 45 | 3, 46 | Amount::from_btc(0.05).unwrap(), 47 | ); 48 | 49 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 50 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 51 | fund_and_verify_maker( 52 | makers_ref, 53 | &test_framework.bitcoind, 54 | 4, 55 | Amount::from_btc(0.05).unwrap(), 56 | ); 57 | 58 | // Start the Maker Server threads 59 | info!("🚀 Initiating Maker servers"); 60 | 61 | let maker_threads = makers 62 | .iter() 63 | .map(|maker| { 64 | let maker_clone = maker.clone(); 65 | thread::spawn(move || { 66 | start_maker_server(maker_clone).unwrap(); 67 | }) 68 | }) 69 | .collect::>(); 70 | 71 | // Makers take time to fully setup. 72 | let org_maker_spend_balances = makers 73 | .iter() 74 | .map(|maker| { 75 | while !maker.is_setup_complete.load(Relaxed) { 76 | info!("⏳ Waiting for maker setup completion"); 77 | // Introduce a delay of 10 seconds to prevent write lock starvation. 78 | thread::sleep(Duration::from_secs(10)); 79 | continue; 80 | } 81 | 82 | // Check balance after setting up maker server. 83 | let wallet = maker.wallet.read().unwrap(); 84 | 85 | let balances = wallet.get_balances().unwrap(); 86 | 87 | verify_maker_pre_swap_balances(&balances, 14999508); 88 | 89 | balances.spendable 90 | }) 91 | .collect::>(); 92 | 93 | // Initiate Coinswap 94 | info!("🔄 Initiating coinswap protocol"); 95 | 96 | // Swap params for coinswap. 97 | let swap_params = SwapParams { 98 | send_amount: Amount::from_sat(500000), 99 | maker_count: 2, 100 | manually_selected_outpoints: None, 101 | }; 102 | taker.do_coinswap(swap_params).unwrap(); 103 | 104 | maker_threads 105 | .into_iter() 106 | .for_each(|thread| thread.join().unwrap()); 107 | 108 | //TODO: Start the faulty maker again, and validate its recovery. 109 | // Start the bad maker again. 110 | // Assert logs to check that it has recovered from its own swap. 111 | 112 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 113 | 114 | thread::sleep(Duration::from_secs(10)); 115 | 116 | /////////////////// 117 | let taker_wallet = taker.get_wallet_mut(); 118 | taker_wallet.sync_and_save().unwrap(); 119 | 120 | // Synchronize each maker's wallet. 121 | for maker in makers.iter() { 122 | let mut wallet = maker.get_wallet().write().unwrap(); 123 | wallet.sync_and_save().unwrap(); 124 | } 125 | /////////////// 126 | 127 | //-------- Fee Tracking and Workflow:-------------------------------------------------------------------------- 128 | // 129 | // This fee scenario would occur in both cases whether Maker6102 is the first or last maker. 130 | 131 | // Case 1: Maker6102 is the first maker 132 | // Workflow: Taker -> Maker6102(CloseAtHashPreimage) -> Maker16102 133 | // 134 | // 135 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 136 | // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| 137 | // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | 138 | // | **Maker16102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | 139 | // | **Maker6102** | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | 140 | // 141 | // Maker6102 => DropConnectionAfterFullSetup 142 | // 143 | // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** 144 | // due to mining fees (recovery + initial transaction fees). 145 | // 146 | // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | 147 | // |----------------|------------------------------------|---------------------|--------------------|----------------------------| 148 | // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | 149 | // | **Maker16102** | 3,000 | 768 | 3,000 | 6,768 | 150 | // | **Maker6102** | 3,000 | 768 | 3,000 | 6,768 | 151 | // 152 | // Case 2: Maker16102 is the last maker. 153 | // Workflow: Taker -> Maker16102 -> Maker16102(CloseAtHashPreimage) 154 | // 155 | // Same as Case 1. 156 | //----------------------------------------------------------------------------------------------------------------------------------------------- 157 | 158 | info!("📊 Verifying swap results after connection close"); 159 | // After Swap checks: 160 | verify_swap_results( 161 | taker, 162 | &makers, 163 | org_taker_spend_balance, 164 | org_maker_spend_balances, 165 | ); 166 | 167 | info!("🎉 All checks successful. Terminating integration test case"); 168 | 169 | test_framework.stop(); 170 | block_generation_handle.join().unwrap(); 171 | } 172 | -------------------------------------------------------------------------------- /src/watch_tower/watcher.rs: -------------------------------------------------------------------------------- 1 | //! Watchtower watcher module. 2 | //! 3 | //! Runs the core event loop, processes watcher commands, reacts to ZMQ backend events, 4 | //! spawns optional RPC-based discovery and updates the on-disk registry of watches and fidelity records. 5 | 6 | use std::{ 7 | marker::PhantomData, 8 | sync::mpsc::{Receiver, Sender, TryRecvError}, 9 | }; 10 | 11 | use bitcoin::{consensus::deserialize, Block, OutPoint, Transaction}; 12 | 13 | use crate::watch_tower::{ 14 | registry_storage::{Checkpoint, FileRegistry, WatchRequest}, 15 | rpc_backend::BitcoinRpc, 16 | utils::{process_block, process_transaction}, 17 | watcher_error::WatcherError, 18 | zmq_backend::{BackendEvent, ZmqBackend}, 19 | }; 20 | 21 | /// Describes watcher behavior. 22 | pub trait Role { 23 | /// Enables or disables discovery. 24 | const RUN_DISCOVERY: bool; 25 | } 26 | 27 | /// Drives the watchtower event loop, coordinating backend events and client commands. 28 | pub struct Watcher { 29 | backend: ZmqBackend, 30 | registry: FileRegistry, 31 | rx_requests: Receiver, 32 | tx_events: Sender, 33 | _role: PhantomData, 34 | } 35 | 36 | /// Events emitted by the watcher to its clients. 37 | #[derive(Debug, Clone)] 38 | pub enum WatcherEvent { 39 | /// Indicates that a watched outpoint was spent. 40 | UtxoSpent { 41 | /// Monitored outpoint. 42 | outpoint: OutPoint, 43 | /// Transaction that spent the outpoint, if known. 44 | spending_tx: Option, 45 | }, 46 | /// Maker addresses. 47 | MakerAddresses { 48 | /// All maker addresses currently recorded in the registry. 49 | maker_addresses: Vec, 50 | }, 51 | /// Returned when a queried outpoint is not being watched. 52 | NoOutpoint, 53 | } 54 | 55 | /// Commands accepted by the watcher from clients. 56 | #[derive(Debug, Clone)] 57 | pub enum WatcherCommand { 58 | /// Store a new watch request. 59 | RegisterWatchRequest { 60 | /// Outpoint to begin tracking. 61 | outpoint: OutPoint, 62 | }, 63 | /// Query whether an outpoint has been spent. 64 | WatchRequest { 65 | /// Outpoint being queried. 66 | outpoint: OutPoint, 67 | }, 68 | /// Remove an existing watch. 69 | Unwatch { 70 | /// Outpoint to stop tracking. 71 | outpoint: OutPoint, 72 | }, 73 | /// Ask for the current maker address list. 74 | MakerAddress, 75 | /// Terminate the watcher loop. 76 | Shutdown, 77 | } 78 | 79 | impl Watcher { 80 | /// Creates a watcher with its backend, registry, and communication channels. 81 | pub fn new( 82 | backend: ZmqBackend, 83 | registry: FileRegistry, 84 | rx_requests: Receiver, 85 | tx_events: Sender, 86 | ) -> Self { 87 | Self { 88 | backend, 89 | registry, 90 | rx_requests, 91 | tx_events, 92 | _role: PhantomData, 93 | } 94 | } 95 | 96 | /// Runs the watcher loop: handles ZMQ events and commands, optionally spawning discovery. 97 | pub fn run(&mut self, rpc_backend: BitcoinRpc) -> Result<(), WatcherError> { 98 | log::info!("Watcher initiated"); 99 | let registry = self.registry.clone(); 100 | std::thread::scope(move |s| { 101 | if R::RUN_DISCOVERY { 102 | s.spawn(move || { 103 | if let Err(e) = rpc_backend.run_discovery(registry) { 104 | log::error!("Discovery thread failed: {:?}", e); 105 | } 106 | }); 107 | } 108 | loop { 109 | match self.rx_requests.try_recv() { 110 | Ok(cmd) => { 111 | if !self.handle_command(cmd) { 112 | break; 113 | } 114 | } 115 | Err(TryRecvError::Disconnected) => break, 116 | Err(TryRecvError::Empty) => {} 117 | } 118 | 119 | if let Some(event) = self.backend.poll() { 120 | self.handle_event(event); 121 | } 122 | } 123 | }); 124 | Ok(()) 125 | } 126 | 127 | fn handle_command(&mut self, cmd: WatcherCommand) -> bool { 128 | match cmd { 129 | WatcherCommand::RegisterWatchRequest { outpoint } => { 130 | log::info!("Intercepted register watch request: {outpoint}"); 131 | let req = WatchRequest { 132 | outpoint, 133 | in_block: false, 134 | spent_tx: None, 135 | }; 136 | self.registry.upsert_watch(&req); 137 | } 138 | WatcherCommand::WatchRequest { outpoint } => { 139 | log::info!("Intercepted watch request: {outpoint}"); 140 | let watches = self.registry.list_watches(); 141 | let mut spent = false; 142 | for watch in watches { 143 | if watch.outpoint == outpoint { 144 | spent = true; 145 | _ = self.tx_events.send(WatcherEvent::UtxoSpent { 146 | outpoint: watch.outpoint, 147 | spending_tx: watch.spent_tx, 148 | }); 149 | } 150 | } 151 | if !spent { 152 | _ = self.tx_events.send(WatcherEvent::NoOutpoint); 153 | } 154 | } 155 | WatcherCommand::Unwatch { outpoint } => { 156 | log::info!("Intercepted unwatch : {outpoint}"); 157 | self.registry.remove_watch(outpoint); 158 | } 159 | WatcherCommand::MakerAddress => { 160 | log::info!("Intercepted maker address"); 161 | let maker_addresses: Vec = self 162 | .registry 163 | .list_fidelity() 164 | .into_iter() 165 | .map(|fidelity| fidelity.onion_address) 166 | .collect(); 167 | _ = self 168 | .tx_events 169 | .send(WatcherEvent::MakerAddresses { maker_addresses }); 170 | } 171 | WatcherCommand::Shutdown => return false, 172 | } 173 | true 174 | } 175 | 176 | /// Handles a backend event, updating registry state and checkpoints. 177 | pub fn handle_event(&mut self, ev: BackendEvent) { 178 | match ev { 179 | BackendEvent::TxSeen { raw_tx } => { 180 | if let Ok(tx) = deserialize::(&raw_tx) { 181 | process_transaction(&tx, &mut self.registry, false); 182 | } 183 | } 184 | BackendEvent::BlockConnected(b) => { 185 | if let Ok(block) = deserialize::(&b.hash) { 186 | self.registry.save_checkpoint(Checkpoint { 187 | height: block.bip34_block_height().unwrap(), 188 | hash: block.block_hash(), 189 | }); 190 | process_block(block, &mut self.registry); 191 | } 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/wallet/storage.rs: -------------------------------------------------------------------------------- 1 | //! The Wallet Storage Interface. 2 | //! 3 | //! Wallet data is currently written in unencrypted CBOR files which are not directly human readable. 4 | 5 | use crate::{ 6 | security::{encrypt_struct, load_sensitive_struct_from_value, KeyMaterial, SerdeCbor}, 7 | wallet::UTXOSpendInfo, 8 | }; 9 | 10 | use super::{error::WalletError, fidelity::FidelityBond}; 11 | 12 | use bitcoin::{bip32::Xpriv, Network, OutPoint, ScriptBuf}; 13 | use serde::{Deserialize, Serialize}; 14 | use std::{ 15 | collections::HashMap, 16 | fs::{self, File}, 17 | io::BufWriter, 18 | path::Path, 19 | }; 20 | 21 | use super::{ 22 | swapcoin::{IncomingSwapCoin, OutgoingSwapCoin}, 23 | swapcoin2::{IncomingSwapCoinV2, OutgoingSwapCoinV2}, 24 | }; 25 | 26 | use bitcoind::bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry; 27 | 28 | /// Represents the internal data store for a Bitcoin wallet. 29 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 30 | pub(crate) struct WalletStore { 31 | /// The file name associated with the wallet store. 32 | pub(crate) file_name: String, 33 | /// Network the wallet operates on. 34 | pub(crate) network: Network, 35 | /// The master key for the wallet. 36 | pub(super) master_key: Xpriv, 37 | /// The external index for the wallet. 38 | pub(super) external_index: u32, 39 | /// The maximum size for an offer in the wallet. 40 | pub(crate) offer_maxsize: u64, 41 | /// Map of multisig redeemscript to incoming swapcoins. 42 | pub(super) incoming_swapcoins: HashMap, 43 | /// Map of multisig redeemscript to outgoing swapcoins. 44 | pub(super) outgoing_swapcoins: HashMap, 45 | /// Map of taproot contract txid to incoming taproot swapcoins. 46 | pub(super) incoming_swapcoins_v2: HashMap, 47 | /// Map of taproot contract txid to outgoing taproot swapcoins. 48 | pub(super) outgoing_swapcoins_v2: HashMap, 49 | /// Map of prevout to contract redeemscript. 50 | pub(super) prevout_to_contract_map: HashMap, 51 | /// Map of swept incoming swap coins to prevent mixing with regular UTXOs 52 | /// Key: ScriptPubKey of swept UTXO, Value: Original multisig redeemscript 53 | pub(crate) swept_incoming_swapcoins: HashMap, 54 | /// Map of swept incoming taproot swap coins (V2) to track swap balance 55 | /// Key: ScriptPubKey of swept UTXO, Value: Original contract txid 56 | pub(super) swept_incoming_swapcoins_v2: HashMap, 57 | /// Map for all the fidelity bond information. 58 | pub(crate) fidelity_bond: HashMap, 59 | pub(super) last_synced_height: Option, 60 | 61 | pub(super) wallet_birthday: Option, 62 | 63 | /// Maps transaction outpoints to their associated UTXO and spend information. 64 | #[serde(default)] // Ensures deserialization works if `utxo_cache` is missing 65 | pub(super) utxo_cache: HashMap, 66 | } 67 | 68 | impl WalletStore { 69 | /// Initialize a store at a path (if path already exists, it will overwrite it). 70 | pub(crate) fn init( 71 | file_name: String, 72 | path: &Path, 73 | network: Network, 74 | master_key: Xpriv, 75 | wallet_birthday: Option, 76 | store_enc_material: &Option, 77 | ) -> Result { 78 | let store = Self { 79 | file_name, 80 | network, 81 | master_key, 82 | external_index: 0, 83 | offer_maxsize: 0, 84 | incoming_swapcoins: HashMap::new(), 85 | outgoing_swapcoins: HashMap::new(), 86 | incoming_swapcoins_v2: HashMap::new(), 87 | outgoing_swapcoins_v2: HashMap::new(), 88 | prevout_to_contract_map: HashMap::new(), 89 | swept_incoming_swapcoins: HashMap::new(), 90 | swept_incoming_swapcoins_v2: HashMap::new(), 91 | fidelity_bond: HashMap::new(), 92 | last_synced_height: None, 93 | wallet_birthday, 94 | utxo_cache: HashMap::new(), 95 | }; 96 | 97 | std::fs::create_dir_all(path.parent().expect("Path should NOT be root!"))?; 98 | // write: overwrites existing file. 99 | // create: creates new file if doesn't exist. 100 | File::create(path)?; 101 | 102 | store.write_to_disk(path, store_enc_material)?; 103 | 104 | Ok(store) 105 | } 106 | 107 | /// Load existing file, updates it, writes it back (errors if path doesn't exist). 108 | pub(crate) fn write_to_disk( 109 | &self, 110 | path: &Path, 111 | store_enc_material: &Option, 112 | ) -> Result<(), WalletError> { 113 | let wallet_file = fs::OpenOptions::new() 114 | .write(true) 115 | .truncate(true) 116 | .open(path)?; 117 | let writer = BufWriter::new(wallet_file); 118 | 119 | match store_enc_material { 120 | Some(material) => { 121 | // Encryption branch: encrypt the serialized wallet before writing. 122 | 123 | let encrypted = encrypt_struct(self, material).unwrap(); 124 | 125 | // Write encrypted wallet data to disk. 126 | serde_cbor::to_writer(writer, &encrypted)?; 127 | } 128 | None => { 129 | // No encryption: serialize and write the wallet directly. 130 | serde_cbor::to_writer(writer, &self)?; 131 | } 132 | } 133 | Ok(()) 134 | } 135 | 136 | /// Reads from a path (errors if path doesn't exist). 137 | /// If `store_enc_material` is provided, attempts to decrypt the file using the 138 | /// provided key. Returns the deserialized `WalletStore` and the nonce. 139 | pub(crate) fn read_from_disk( 140 | backup_file_path: &Path, 141 | password: String, 142 | ) -> Result<(Self, Option), WalletError> { 143 | let (wallet_store, store_enc_material) = 144 | load_sensitive_struct_from_value::(backup_file_path, password); 145 | 146 | Ok((wallet_store, store_enc_material)) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | use bip39::rand::{thread_rng, Rng}; 154 | use bitcoind::tempfile::tempdir; 155 | 156 | #[test] 157 | fn test_write_and_read_wallet_to_disk() { 158 | let temp_dir = tempdir().unwrap(); 159 | let file_path = temp_dir.path().join("test_wallet.cbor"); 160 | 161 | let master_key = { 162 | let seed: [u8; 16] = thread_rng().gen(); 163 | Xpriv::new_master(Network::Bitcoin, &seed).unwrap() 164 | }; 165 | 166 | let original_wallet_store = WalletStore::init( 167 | "test_wallet".to_string(), 168 | &file_path, 169 | Network::Bitcoin, 170 | master_key, 171 | None, 172 | &None, 173 | ) 174 | .unwrap(); 175 | 176 | original_wallet_store 177 | .write_to_disk(&file_path, &None) 178 | .unwrap(); 179 | 180 | let (read_wallet, _nonce) = WalletStore::read_from_disk(&file_path, String::new()).unwrap(); 181 | assert_eq!(original_wallet_store, read_wallet); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/taker/offers.rs: -------------------------------------------------------------------------------- 1 | //! Download, process and store Maker offers from the directory-server. 2 | //! 3 | //! It defines structures like [`OfferAndAddress`] and [`MakerAddress`] for representing maker offers and addresses. 4 | //! The [`OfferBook`] struct keeps track of good and bad makers, and it provides methods for managing offers. 5 | //! The module handles the syncing of the offer book with addresses obtained from directory servers and local configurations. 6 | //! It uses asynchronous channels for concurrent processing of maker offers. 7 | 8 | use std::{ 9 | convert::TryFrom, fmt, io::BufWriter, net::TcpStream, path::Path, sync::mpsc, thread::Builder, 10 | }; 11 | 12 | use chrono::{DateTime, Utc}; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | use crate::protocol::messages::Offer; 16 | 17 | use super::{config::TakerConfig, error::TakerError, routines::download_maker_offer}; 18 | 19 | /// Represents an offer along with the corresponding maker address. 20 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 21 | pub struct OfferAndAddress { 22 | /// Details for Maker Offer 23 | pub offer: Offer, 24 | /// Maker Address: onion_addr:port 25 | pub address: MakerAddress, 26 | /// When this offer had been downloaded and cached locally 27 | pub timestamp: DateTime, 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] 31 | struct OnionAddress { 32 | port: String, 33 | onion_addr: String, 34 | } 35 | 36 | /// Enum representing maker addresses. 37 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] 38 | pub struct MakerAddress(OnionAddress); 39 | 40 | impl fmt::Display for MakerAddress { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | write!(f, "{}:{}", self.0.onion_addr, self.0.port) 43 | } 44 | } 45 | 46 | impl TryFrom<&mut TcpStream> for MakerAddress { 47 | type Error = std::io::Error; 48 | fn try_from(value: &mut TcpStream) -> Result { 49 | let socket_addr = value.peer_addr()?; 50 | Ok(MakerAddress(OnionAddress { 51 | port: socket_addr.port().to_string(), 52 | onion_addr: socket_addr.ip().to_string(), 53 | })) 54 | } 55 | } 56 | 57 | /// An ephemeral Offerbook tracking good and bad makers. Currently, Offerbook is initiated 58 | /// at the start of every swap. So good and bad maker list will not be persisted. 59 | #[derive(Debug, Default, Serialize, Deserialize)] 60 | pub struct OfferBook { 61 | pub(super) all_makers: Vec, 62 | pub(super) bad_makers: Vec, 63 | } 64 | 65 | impl OfferBook { 66 | /// Gets all "not-bad" offers. 67 | pub fn all_good_makers(&self) -> Vec<&OfferAndAddress> { 68 | self.all_makers 69 | .iter() 70 | .filter(|offer| !self.bad_makers.contains(offer)) 71 | .collect() 72 | } 73 | /// Gets all offers. 74 | pub fn all_makers(&self) -> Vec<&OfferAndAddress> { 75 | self.all_makers.iter().collect() 76 | } 77 | 78 | /// Adds a new offer to the offer book. 79 | pub fn add_new_offer(&mut self, offer: &OfferAndAddress) -> bool { 80 | let timestamped = OfferAndAddress { 81 | offer: offer.offer.clone(), 82 | address: offer.address.clone(), 83 | timestamp: Utc::now(), 84 | }; 85 | if !self.all_makers.iter().any(|to| to.offer == offer.offer) { 86 | self.all_makers.push(timestamped); 87 | true 88 | } else { 89 | false 90 | } 91 | } 92 | 93 | /// Gets the list of addresses which were last update less than 30 minutes ago. 94 | /// We exclude these addresses from the next round of downloading offers. 95 | pub fn get_fresh_addrs(&self) -> Vec<&OfferAndAddress> { 96 | let now = Utc::now(); 97 | self.all_makers 98 | .iter() 99 | .filter(|to| (now - to.timestamp).num_minutes() < 30) 100 | .collect() 101 | } 102 | 103 | /// Adds a bad maker to the offer book. 104 | pub(crate) fn add_bad_maker(&mut self, bad_maker: &OfferAndAddress) -> bool { 105 | if !self.bad_makers.contains(bad_maker) { 106 | self.bad_makers.push(bad_maker.clone()); 107 | true 108 | } else { 109 | false 110 | } 111 | } 112 | 113 | /// Gets the list of bad makers. 114 | pub(crate) fn get_bad_makers(&self) -> Vec<&OfferAndAddress> { 115 | self.bad_makers.iter().collect() 116 | } 117 | 118 | /// Load existing file, updates it, writes it back (errors if path doesn't exist). 119 | pub fn write_to_disk(&self, path: &Path) -> Result<(), TakerError> { 120 | let offerdata_file = std::fs::OpenOptions::new().write(true).open(path)?; 121 | let writer = BufWriter::new(offerdata_file); 122 | Ok(serde_json::to_writer_pretty(writer, &self)?) 123 | } 124 | 125 | /// Reads from a path (errors if path doesn't exist). 126 | pub fn read_from_disk(path: &Path) -> Result { 127 | let content = std::fs::read_to_string(path)?; 128 | Ok(serde_json::from_str(&content)?) 129 | } 130 | } 131 | 132 | /// Synchronizes the offer book with specific maker addresses. 133 | pub(crate) fn fetch_offer_from_makers( 134 | maker_addresses: Vec, 135 | config: &TakerConfig, 136 | ) -> Result, TakerError> { 137 | let (offers_writer, offers_reader) = mpsc::channel::>(); 138 | // Thread pool for all connections to fetch maker offers. 139 | let mut thread_pool = Vec::new(); 140 | let maker_addresses_len = maker_addresses.len(); 141 | for addr in maker_addresses { 142 | let offers_writer = offers_writer.clone(); 143 | let taker_config = config.clone(); 144 | let thread = Builder::new() 145 | .name(format!("maker_offer_fetch_thread_{addr}")) 146 | .spawn(move || -> Result<(), TakerError> { 147 | let offer = download_maker_offer(addr, taker_config); 148 | Ok(offers_writer.send(offer)?) 149 | })?; 150 | 151 | thread_pool.push(thread); 152 | } 153 | let mut result = Vec::new(); 154 | for _ in 0..maker_addresses_len { 155 | if let Some(offer_addr) = offers_reader.recv()? { 156 | result.push(offer_addr); 157 | } 158 | } 159 | 160 | for thread in thread_pool { 161 | let join_result = thread.join(); 162 | 163 | if let Err(e) = join_result { 164 | log::error!("Error while joining thread: {e:?}"); 165 | } 166 | } 167 | Ok(result) 168 | } 169 | 170 | impl TryFrom for OnionAddress { 171 | type Error = &'static str; 172 | 173 | fn try_from(value: String) -> Result { 174 | let mut parts = value.splitn(2, ':'); 175 | let onion_addr = parts.next().ok_or("Missing onion address")?.to_string(); 176 | let port = parts.next().ok_or("Missing port")?.to_string(); 177 | 178 | if onion_addr.is_empty() || port.is_empty() { 179 | return Err("Empty onion address or port"); 180 | } 181 | 182 | Ok(OnionAddress { onion_addr, port }) 183 | } 184 | } 185 | 186 | impl TryFrom for MakerAddress { 187 | type Error = &'static str; 188 | 189 | fn try_from(value: String) -> Result { 190 | let onion = OnionAddress::try_from(value)?; 191 | Ok(MakerAddress(onion)) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/maker/rpc/server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::ErrorKind, 3 | net::{TcpListener, TcpStream}, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering::Relaxed}, 6 | Arc, 7 | }, 8 | thread::sleep, 9 | time::Duration, 10 | }; 11 | 12 | use bitcoin::{Address, Amount}; 13 | 14 | use super::messages::RpcMsgReq; 15 | use crate::{ 16 | maker::{config::MakerConfig, error::MakerError, rpc::messages::RpcMsgResp}, 17 | utill::{get_tor_hostname, read_message, send_message, HEART_BEAT_INTERVAL, UTXO}, 18 | wallet::{Destination, Wallet}, 19 | }; 20 | use std::{path::Path, str::FromStr, sync::RwLock}; 21 | 22 | pub trait MakerRpc { 23 | fn wallet(&self) -> &RwLock; 24 | fn data_dir(&self) -> &Path; 25 | fn config(&self) -> &MakerConfig; 26 | fn shutdown(&self) -> &AtomicBool; 27 | } 28 | 29 | fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), MakerError> { 30 | let msg_bytes = read_message(socket)?; 31 | let rpc_request: RpcMsgReq = serde_cbor::from_slice(&msg_bytes)?; 32 | log::info!("RPC request received: {rpc_request:?}"); 33 | 34 | let resp = match rpc_request { 35 | RpcMsgReq::Ping => RpcMsgResp::Pong, 36 | RpcMsgReq::ContractUtxo => { 37 | let utxos = maker 38 | .wallet() 39 | .read()? 40 | .list_live_timelock_contract_spend_info() 41 | .into_iter() 42 | .map(UTXO::from_utxo_data) 43 | .collect(); 44 | RpcMsgResp::ContractUtxoResp { utxos } 45 | } 46 | RpcMsgReq::FidelityUtxo => { 47 | let utxos = maker 48 | .wallet() 49 | .read()? 50 | .list_fidelity_spend_info() 51 | .into_iter() 52 | .map(UTXO::from_utxo_data) 53 | .collect(); 54 | RpcMsgResp::FidelityUtxoResp { utxos } 55 | } 56 | RpcMsgReq::Utxo => { 57 | let utxos = maker 58 | .wallet() 59 | .read()? 60 | .list_all_utxo_spend_info() 61 | .into_iter() 62 | .map(UTXO::from_utxo_data) 63 | .collect(); 64 | RpcMsgResp::UtxoResp { utxos } 65 | } 66 | RpcMsgReq::SwapUtxo => { 67 | let utxos = maker 68 | .wallet() 69 | .read()? 70 | .list_incoming_swap_coin_utxo_spend_info() 71 | .into_iter() 72 | .map(UTXO::from_utxo_data) 73 | .collect(); 74 | RpcMsgResp::SwapUtxoResp { utxos } 75 | } 76 | RpcMsgReq::Balances => { 77 | let balances = maker.wallet().read()?.get_balances()?; 78 | RpcMsgResp::TotalBalanceResp(balances) 79 | } 80 | RpcMsgReq::NewAddress => { 81 | let new_address = maker.wallet().write()?.get_next_external_address()?; 82 | RpcMsgResp::NewAddressResp(new_address.to_string()) 83 | } 84 | RpcMsgReq::SendToAddress { 85 | address, 86 | amount, 87 | feerate, 88 | } => { 89 | let amount = Amount::from_sat(amount); 90 | let outputs = vec![( 91 | Address::from_str(&address).unwrap().assume_checked(), 92 | amount, 93 | )]; 94 | let destination = Destination::Multi { 95 | outputs, 96 | op_return_data: None, 97 | }; 98 | 99 | let coins_to_send = maker.wallet().read()?.coin_select(amount, feerate, None)?; 100 | let tx = 101 | maker 102 | .wallet() 103 | .write()? 104 | .spend_from_wallet(feerate, destination, &coins_to_send)?; 105 | 106 | let txid = maker.wallet().read()?.send_tx(&tx)?; 107 | 108 | maker.wallet().write()?.sync_no_fail(); 109 | 110 | RpcMsgResp::SendToAddressResp(txid.to_string()) 111 | } 112 | RpcMsgReq::GetDataDir => RpcMsgResp::GetDataDirResp(maker.data_dir().to_path_buf()), 113 | RpcMsgReq::GetTorAddress => { 114 | if cfg!(feature = "integration-test") { 115 | RpcMsgResp::GetTorAddressResp("Maker is not running on TOR".to_string()) 116 | } else { 117 | let hostname = get_tor_hostname( 118 | maker.data_dir(), 119 | maker.config().control_port, 120 | maker.config().network_port, 121 | &maker.config().tor_auth_password, 122 | )?; 123 | let address = format!("{}:{}", hostname, maker.config().network_port); 124 | RpcMsgResp::GetTorAddressResp(address) 125 | } 126 | } 127 | RpcMsgReq::Stop => { 128 | maker.shutdown().store(true, Relaxed); 129 | RpcMsgResp::Shutdown 130 | } 131 | 132 | RpcMsgReq::ListFidelity => { 133 | let list = maker.wallet().read()?.display_fidelity_bonds()?; 134 | RpcMsgResp::ListBonds(list) 135 | } 136 | RpcMsgReq::SyncWallet => { 137 | log::info!("Initializing wallet sync"); 138 | let mut wallet = maker.wallet().write()?; 139 | if let Err(e) = wallet.sync() { 140 | RpcMsgResp::ServerError(format!("{e:?}")) 141 | } else { 142 | log::info!("Completed wallet sync"); 143 | wallet.save_to_disk()?; 144 | RpcMsgResp::Pong 145 | } 146 | } 147 | }; 148 | 149 | if let Err(e) = send_message(socket, &resp) { 150 | log::error!("Error sending RPC response {e:?}"); 151 | } 152 | 153 | Ok(()) 154 | } 155 | 156 | pub(crate) fn start_rpc_server(maker: Arc) -> Result<(), MakerError> { 157 | let rpc_port = maker.config().rpc_port; 158 | let rpc_socket = format!("127.0.0.1:{rpc_port}"); 159 | let listener = Arc::new(TcpListener::bind(&rpc_socket)?); 160 | log::info!( 161 | "[{}] RPC socket binding successful at {}", 162 | maker.config().network_port, 163 | rpc_socket 164 | ); 165 | 166 | listener.set_nonblocking(true)?; 167 | 168 | while !maker.shutdown().load(Relaxed) { 169 | match listener.accept() { 170 | Ok((mut stream, addr)) => { 171 | log::info!("Got RPC request from: {addr}"); 172 | stream.set_read_timeout(Some(Duration::from_secs(20)))?; 173 | stream.set_write_timeout(Some(Duration::from_secs(20)))?; 174 | // Do not cause hard error if a rpc request fails 175 | if let Err(e) = handle_request(&maker, &mut stream) { 176 | log::error!("Error processing RPC Request: {e:?}"); 177 | // Send the error back to client. 178 | if let Err(e) = 179 | send_message(&mut stream, &RpcMsgResp::ServerError(format!("{e:?}"))) 180 | { 181 | log::error!("Error sending RPC response {e:?}"); 182 | }; 183 | } 184 | } 185 | 186 | Err(e) => { 187 | if e.kind() == ErrorKind::WouldBlock { 188 | // do nothing 189 | } else { 190 | log::error!("Error accepting RPC connection: {e:?}"); 191 | } 192 | } 193 | } 194 | 195 | sleep(HEART_BEAT_INTERVAL); 196 | } 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /tests/abort1.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | }; 7 | mod test_framework; 8 | use log::{info, warn}; 9 | use std::{ 10 | sync::{atomic::Ordering::Relaxed, Arc}, 11 | thread, 12 | time::Duration, 13 | }; 14 | use test_framework::*; 15 | 16 | /// Abort 1: TAKER Drops After Full Setup. 17 | /// This test demonstrates the situation where the Taker drops the connection after broadcasting all the 18 | /// funding transactions. The Makers identify this and wait for a timeout (5mins in prod, 30 secs in test) 19 | /// for the Taker to come back. If the Taker doesn't come back within timeout, the Makers broadcast the contract 20 | /// transactions and reclaims their funds via timelock. 21 | /// 22 | /// The Taker after coming live again will see unfinished coinswaps in his wallet. He can reclaim his funds via 23 | /// broadcasting his contract transactions and claiming via timelock. 24 | #[test] 25 | fn test_stop_taker_after_setup() { 26 | // ---- Setup ---- 27 | 28 | // 2 Makers with Normal behavior. 29 | let makers_config_map = [ 30 | ((6102, None), MakerBehavior::Normal), 31 | ((16102, None), MakerBehavior::Normal), 32 | ]; 33 | 34 | let taker_behavior = vec![TakerBehavior::DropConnectionAfterFullSetup]; 35 | 36 | // Initiate test framework, Makers. 37 | // Taker has a special behavior DropConnectionAfterFullSetup. 38 | let (test_framework, mut takers, makers, block_generation_handle) = 39 | TestFramework::init(makers_config_map.into(), taker_behavior); 40 | 41 | warn!("🧪 Running Test: Taker cheats on everybody"); 42 | let taker = &mut takers[0]; 43 | 44 | info!("💰 Funding taker and makers"); 45 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 46 | let org_taker_spend_balance = fund_and_verify_taker( 47 | taker, 48 | &test_framework.bitcoind, 49 | 3, 50 | Amount::from_btc(0.05).unwrap(), 51 | ); 52 | 53 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 54 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 55 | fund_and_verify_maker( 56 | makers_ref, 57 | &test_framework.bitcoind, 58 | 4, 59 | Amount::from_btc(0.05).unwrap(), 60 | ); 61 | 62 | // Start the Maker Server threads 63 | info!("🚀 Initiating Maker servers"); 64 | 65 | let maker_threads = makers 66 | .iter() 67 | .map(|maker| { 68 | let maker_clone = maker.clone(); 69 | thread::spawn(move || { 70 | start_maker_server(maker_clone).unwrap(); 71 | }) 72 | }) 73 | .collect::>(); 74 | 75 | // Makers take time to fully setup. 76 | let org_maker_spend_balances = makers 77 | .iter() 78 | .map(|maker| { 79 | while !maker.is_setup_complete.load(Relaxed) { 80 | info!("⏳ Waiting for maker setup completion"); 81 | // Introduce a delay of 10 seconds to prevent write lock starvation. 82 | thread::sleep(Duration::from_secs(10)); 83 | continue; 84 | } 85 | 86 | // Check balance after setting up maker server. 87 | let wallet = maker.wallet.read().unwrap(); 88 | 89 | let balances = wallet.get_balances().unwrap(); 90 | 91 | verify_maker_pre_swap_balances(&balances, 14999508); 92 | 93 | balances.spendable 94 | }) 95 | .collect::>(); 96 | 97 | // Initiate Coinswap 98 | info!("🔄 Initiating coinswap protocol"); 99 | 100 | // Swap params for coinswap. 101 | let swap_params = SwapParams { 102 | send_amount: Amount::from_sat(500000), 103 | maker_count: 2, 104 | manually_selected_outpoints: None, 105 | }; 106 | taker.do_coinswap(swap_params).unwrap(); 107 | 108 | // After Swap is done, wait for maker threads to conclude. 109 | maker_threads 110 | .into_iter() 111 | .for_each(|thread| thread.join().unwrap()); 112 | 113 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 114 | 115 | thread::sleep(Duration::from_secs(10)); 116 | 117 | /////////////////// 118 | let taker_wallet = taker.get_wallet_mut(); 119 | taker_wallet.sync_and_save().unwrap(); 120 | 121 | // Synchronize each maker's wallet. 122 | for maker in makers.iter() { 123 | let mut wallet = maker.get_wallet().write().unwrap(); 124 | wallet.sync_and_save().unwrap(); 125 | } 126 | /////////////// 127 | 128 | //Run Recovery script 129 | warn!("🔧 Starting Taker recovery process"); 130 | taker.recover_from_swap().unwrap(); 131 | 132 | // ## Fee Tracking and Workflow: 133 | // 134 | // ### Fee Breakdown: 135 | // 136 | // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ 137 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 138 | // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ 139 | // | Taker | _ | 500,000 | _ | 3,000 | 3,000 | 140 | // | Maker16102 | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | 141 | // | Maker6102 | 463,500 | 438,642 | 21,858 | 3,000 | 24,858 | 142 | // +------------------+-------------------------+--------------------------+------------+----------------------------+-------------------+ 143 | // 144 | // 145 | // **Taker** => DropConnectionAfterFullSetup 146 | // 147 | // Participants regain their initial funding amounts but incur a total loss of **6,768 sats** 148 | // due to mining fees (recovery + initial transaction fees). 149 | // 150 | // ### Recovery Fees Breakdown: 151 | // 152 | // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ 153 | // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | 154 | // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ 155 | // | Taker | 3,000 | 768 | 3,000 | 6,768 | 156 | // | Maker16102 | 3,000 | 768 | 3,000 | 6,768 | 157 | // | Maker6102 | 3,000 | 768 | 3,000 | 6,768 | 158 | // +------------------+------------------------------------+---------------------+--------------------+----------------------------+ 159 | // 160 | 161 | info!("📊 Verifying swap results after taker recovery"); 162 | verify_swap_results( 163 | taker, 164 | &makers, 165 | org_taker_spend_balance, 166 | org_maker_spend_balances, 167 | ); 168 | 169 | info!("🎉 All checks successful. Terminating integration test case"); 170 | 171 | test_framework.stop(); 172 | 173 | block_generation_handle.join().unwrap(); 174 | } 175 | -------------------------------------------------------------------------------- /src/protocol/messages2.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the messages communicated between the parties(Taker, Maker) 2 | use crate::wallet::FidelityBond; 3 | use bitcoin::{hashes::sha256::Hash, Amount, PublicKey, ScriptBuf, Txid}; 4 | use secp256k1::musig::{PartialSignature, PublicNonce}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::{convert::TryInto, fmt::Display}; 7 | 8 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 9 | /// Serializable wrapper for secp256k1 PublicNonce 10 | pub struct SerializablePublicNonce(pub Vec); 11 | 12 | /// Defines the length of the Preimage. 13 | pub(crate) const PREIMAGE_LEN: usize = 32; 14 | 15 | /// Type for Preimage. 16 | pub(crate) type Preimage = [u8; PREIMAGE_LEN]; 17 | 18 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 19 | /// Serializable wrapper for secp256k1 Scalar 20 | pub struct SerializableScalar(pub Vec); 21 | 22 | impl From for SerializablePublicNonce { 23 | fn from(nonce: PublicNonce) -> Self { 24 | SerializablePublicNonce(nonce.serialize().to_vec()) 25 | } 26 | } 27 | 28 | impl From for PublicNonce { 29 | fn from(nonce: SerializablePublicNonce) -> Self { 30 | let bytes: [u8; 66] = nonce.0.try_into().expect("invalid nonce value"); 31 | PublicNonce::from_byte_array(&bytes).expect("invalid nonce value") 32 | } 33 | } 34 | 35 | impl From for SerializableScalar { 36 | fn from(scalar: bitcoin::secp256k1::Scalar) -> Self { 37 | SerializableScalar(scalar.to_be_bytes().to_vec()) 38 | } 39 | } 40 | 41 | impl From for bitcoin::secp256k1::Scalar { 42 | fn from(scalar: SerializableScalar) -> Self { 43 | let bytes: [u8; 32] = scalar.0.try_into().expect("invalid scalar length"); 44 | bitcoin::secp256k1::Scalar::from_be_bytes(bytes).expect("invalid scalar value") 45 | } 46 | } 47 | 48 | // Note: Nonces should be generated using proper MuSig2 procedures with transaction context, 49 | // not converted from arbitrary secret keys 50 | 51 | impl Display for TakerToMakerMessage { 52 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 53 | match self { 54 | Self::GetOffer(_) => write!(f, "GetOffer"), 55 | Self::SwapDetails(_) => write!(f, "SwapDetails"), 56 | Self::SendersContract(_) => write!(f, "SendersContract"), 57 | Self::PrivateKeyHandover(_) => write!(f, "PrivateKeyHandover"), 58 | } 59 | } 60 | } 61 | 62 | impl Display for MakerToTakerMessage { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | match self { 65 | Self::RespOffer(_) => write!(f, "RespOffer"), 66 | Self::AckResponse(_) => write!(f, "AckResponse"), 67 | Self::SenderContractFromMaker(_) => write!(f, "SenderContractFromMaker"), 68 | Self::PrivateKeyHandover(_) => write!(f, "PrivateKeyHandover"), 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 74 | /// Serializable wrapper for secp256k1 PartialSignature 75 | pub struct SerializablePartialSignature(pub Vec); 76 | 77 | impl From for SerializablePartialSignature { 78 | fn from(sig: PartialSignature) -> Self { 79 | SerializablePartialSignature(sig.serialize().to_vec()) 80 | } 81 | } 82 | 83 | impl From for PartialSignature { 84 | fn from(sig: SerializablePartialSignature) -> Self { 85 | let bytes: [u8; 32] = sig.0.try_into().expect("invalid signature value"); 86 | PartialSignature::from_byte_array(&bytes).expect("invalid signature value") 87 | } 88 | } 89 | 90 | /// Represents a fidelity proof in the Coinswap protocol 91 | #[derive(Debug, Serialize, Deserialize, Clone)] 92 | pub struct FidelityProof { 93 | pub(crate) bond: FidelityBond, 94 | pub(crate) cert_hash: Hash, 95 | pub(crate) cert_sig: bitcoin::secp256k1::ecdsa::Signature, 96 | } 97 | 98 | /// Private key handover message exchanged during taproot coinswap sweeps 99 | /// 100 | /// After contract transactions are established and broadcasted, parties exchange 101 | /// their outgoing contract private keys to enable independent sweeping without 102 | /// requiring MuSig2 coordination. 103 | #[derive(Debug, Serialize, Deserialize, Clone)] 104 | pub struct PrivateKeyHandover { 105 | /// The outgoing contract private key 106 | pub(crate) secret_key: bitcoin::secp256k1::SecretKey, 107 | } 108 | 109 | #[derive(Debug, Serialize, Deserialize)] 110 | #[allow(clippy::large_enum_variant)] 111 | pub(crate) enum TakerToMakerMessage { 112 | GetOffer(GetOffer), 113 | SwapDetails(SwapDetails), 114 | SendersContract(SendersContract), 115 | PrivateKeyHandover(PrivateKeyHandover), 116 | } 117 | 118 | #[derive(Debug, Serialize, Deserialize)] 119 | pub(crate) enum MakerToTakerMessage { 120 | RespOffer(Box), 121 | AckResponse(AckResponse), 122 | SenderContractFromMaker(SenderContractFromMaker), 123 | PrivateKeyHandover(PrivateKeyHandover), 124 | } 125 | 126 | #[derive(Debug, Serialize, Deserialize)] 127 | pub(crate) struct GetOffer { 128 | pub(crate) protocol_version_min: u32, 129 | pub(crate) protocol_version_max: u32, 130 | pub(crate) number_of_transactions: u32, 131 | } 132 | 133 | /// An offer from a maker to participate in a coinswap 134 | #[derive(Debug, Serialize, Deserialize, Clone)] 135 | pub struct Offer { 136 | /// The tweakable public key for the maker 137 | pub tweakable_point: PublicKey, 138 | /// Base fee charged by the maker (in satoshis) 139 | pub base_fee: u64, 140 | /// Fee as a percentage relative to the swap amount 141 | pub amount_relative_fee: f64, 142 | /// Fee as a percentage relative to the time lock duration 143 | pub time_relative_fee: f64, 144 | /// Minimum time lock duration required by the maker 145 | pub minimum_locktime: u16, 146 | /// Fidelity proof demonstrating the maker's commitment 147 | pub fidelity: FidelityProof, 148 | /// Minimum swap amount the maker will accept (in satoshis) 149 | pub min_size: u64, 150 | /// Maximum swap amount the maker can handle (in satoshis) 151 | pub max_size: u64, 152 | } 153 | 154 | #[derive(Debug, Serialize, Deserialize)] 155 | pub(crate) struct SwapDetails { 156 | pub(crate) amount: Amount, 157 | pub(crate) no_of_tx: u8, 158 | pub(crate) timelock: u16, 159 | } 160 | 161 | #[derive(Debug, Serialize, Deserialize)] 162 | pub(crate) enum AckResponse { 163 | Ack, 164 | Nack, 165 | } 166 | 167 | #[derive(Debug, Serialize, Deserialize, Clone)] 168 | pub(crate) struct SendersContract { 169 | pub(crate) contract_txs: Vec, 170 | // Below data is used to verify transaction 171 | pub(crate) pubkeys_a: Vec, 172 | pub(crate) hashlock_scripts: Vec, 173 | pub(crate) timelock_scripts: Vec, 174 | // Tweakable point for allowing maker to create next contract 175 | pub(crate) next_party_tweakable_point: bitcoin::PublicKey, 176 | // MuSig2 data for cooperative spending 177 | pub(crate) internal_key: Option, 178 | pub(crate) tap_tweak: Option, 179 | } 180 | 181 | #[derive(Debug, Serialize, Deserialize)] 182 | pub(crate) struct SenderContractFromMaker { 183 | pub(crate) contract_txs: Vec, 184 | // Below data is used to verify transaction 185 | pub(crate) pubkeys_a: Vec, 186 | pub(crate) hashlock_scripts: Vec, 187 | pub(crate) timelock_scripts: Vec, 188 | // MuSig2 data for cooperative spending 189 | pub(crate) internal_key: Option, 190 | pub(crate) tap_tweak: Option, 191 | } 192 | 193 | /// Mempool transaction 194 | #[derive(Serialize, Deserialize, Debug)] 195 | pub struct MempoolTx { 196 | /// Txid of the transaction spending the utxo 197 | pub txid: String, 198 | /// Hex encoded raw transaction 199 | pub rawtx: String, 200 | } 201 | -------------------------------------------------------------------------------- /tests/abort2_case2.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | }; 7 | use std::sync::Arc; 8 | mod test_framework; 9 | use test_framework::*; 10 | 11 | use log::{info, warn}; 12 | use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; 13 | 14 | /// ABORT 2: Maker Drops Before Setup 15 | /// This test demonstrates the situation where a Maker prematurely drops connections after doing 16 | /// initial protocol handshake. This should not necessarily disrupt the round, the Taker will try to find 17 | /// more makers in his address book and carry on as usual. The Taker will mark this Maker as "bad" and will 18 | /// not swap this maker again. 19 | /// 20 | /// CASE 2: Maker Drops Before Sending Sender's Signature, and Taker cannot find a new Maker, recovers from Swap. 21 | #[test] 22 | fn test_abort_case_2_recover_if_no_makers_found() { 23 | // ---- Setup ---- 24 | 25 | // 6102 is naughty. And theres not enough makers. 26 | let naughty = 6102; 27 | let makers_config_map = [ 28 | ( 29 | (naughty, None), 30 | MakerBehavior::CloseAtReqContractSigsForSender, 31 | ), 32 | ((16102, None), MakerBehavior::Normal), 33 | ]; 34 | 35 | let taker_behavior = vec![TakerBehavior::Normal]; 36 | 37 | warn!( 38 | "Running test: Maker {naughty} Closes before sending sender's sigs. Taker recovers. Or Swap cancels" 39 | ); 40 | 41 | // Initiate test framework, Makers. 42 | // Taker has normal behavior. 43 | let (test_framework, mut takers, makers, block_generation_handle) = 44 | TestFramework::init(makers_config_map.into(), taker_behavior); 45 | 46 | info!("💰 Funding taker and makers"); 47 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 48 | let taker = &mut takers[0]; 49 | let org_taker_spend_balance = fund_and_verify_taker( 50 | taker, 51 | &test_framework.bitcoind, 52 | 3, 53 | Amount::from_btc(0.05).unwrap(), 54 | ); 55 | 56 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 57 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 58 | fund_and_verify_maker( 59 | makers_ref, 60 | &test_framework.bitcoind, 61 | 4, 62 | Amount::from_btc(0.05).unwrap(), 63 | ); 64 | 65 | // Start the Maker Server threads 66 | info!("🚀 Initiating Maker servers"); 67 | 68 | let maker_threads = makers 69 | .iter() 70 | .map(|maker| { 71 | let maker_clone = maker.clone(); 72 | thread::spawn(move || { 73 | start_maker_server(maker_clone).unwrap(); 74 | }) 75 | }) 76 | .collect::>(); 77 | 78 | // Makers take time to fully setup. 79 | let org_maker_spend_balances = makers 80 | .iter() 81 | .map(|maker| { 82 | while !maker.is_setup_complete.load(Relaxed) { 83 | info!("⏳ Waiting for maker setup completion"); 84 | // Introduce a delay of 10 seconds to prevent write lock starvation. 85 | thread::sleep(Duration::from_secs(10)); 86 | continue; 87 | } 88 | 89 | // Check balance after setting up maker server. 90 | let wallet = maker.wallet.read().unwrap(); 91 | 92 | let balances = wallet.get_balances().unwrap(); 93 | 94 | verify_maker_pre_swap_balances(&balances, 14999508); 95 | 96 | balances.spendable 97 | }) 98 | .collect::>(); 99 | 100 | // Initiate Coinswap 101 | info!("🔄 Initiating coinswap protocol"); 102 | 103 | // Swap params for coinswap. 104 | let swap_params = SwapParams { 105 | send_amount: Amount::from_sat(500000), 106 | maker_count: 2, 107 | manually_selected_outpoints: None, 108 | }; 109 | 110 | taker.do_coinswap(swap_params).unwrap(); 111 | 112 | // After Swap is done, wait for maker threads to conclude. 113 | makers 114 | .iter() 115 | .for_each(|maker| maker.shutdown.store(true, Relaxed)); 116 | 117 | maker_threads 118 | .into_iter() 119 | .for_each(|thread| thread.join().unwrap()); 120 | 121 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 122 | 123 | thread::sleep(Duration::from_secs(10)); 124 | 125 | /////////////////// 126 | let taker_wallet = taker.get_wallet_mut(); 127 | taker_wallet.sync_and_save().unwrap(); 128 | 129 | // Synchronize each maker's wallet. 130 | for maker in makers.iter() { 131 | let mut wallet = maker.get_wallet().write().unwrap(); 132 | wallet.sync_and_save().unwrap(); 133 | } 134 | /////////////// 135 | 136 | // -------- Fee Tracking and Workflow -------- 137 | // 138 | // Case 1: Maker6102 is the second maker, and the Taker recovers from an initiated swap. 139 | // Workflow: Taker -> Maker16102 -> Maker6102 (CloseAtReqContractSigsForSender) 140 | // 141 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 142 | // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| 143 | // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | 144 | // 145 | // - Taker sends [`ProofOfFunding`] to Maker16102. 146 | // - Maker16102 responds with [`ReqContractSigsAsRecvrAndSender`] to the Taker. 147 | // - Taker forwards [`ReqContractSigsForSender`] to Maker6102, but Maker6102 does not respond, and the Taker recovers from the swap. 148 | // 149 | // Final Outcome for Taker (Recover from Swap): 150 | // 151 | // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | 152 | // |----------------|------------------------------------|---------------------|--------------------|----------------------------| 153 | // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | 154 | // 155 | // - The Taker regains their initial funding amounts but incurs a total loss of **6,768 sats** due to mining fees. 156 | // 157 | // Case 2: Maker6102 is the first maker. 158 | // Workflow: Taker -> Maker6102 (CloseAtReqContractSigsForSender) 159 | // 160 | // - Taker creates unbroadcasted funding transactions and sends [`ReqContractSigsForSender`] to Maker6102. 161 | // - Maker6102 does not respond, and the swap fails. 162 | // 163 | // Final Outcome for Taker: 164 | // 165 | // | Participant | Coinswap Outcome (Sats) | 166 | // |----------------|--------------------------| 167 | // | **Taker** | 0 | 168 | // 169 | // Final Outcome for Makers (In both cases): 170 | // 171 | // | Participant | Coinswap Outcome (Sats) | 172 | // |----------------|------------------------------------------| 173 | // | **Maker6102** | 0 (Marked as a bad maker by the Taker) | 174 | // | **Maker16102** | 0 | 175 | 176 | info!("🚫 Verifying naughty maker gets banned"); 177 | // Maker gets banned for being naughty. 178 | assert_eq!( 179 | format!("127.0.0.1:{naughty}"), 180 | taker.get_bad_makers()[0].address.to_string() 181 | ); 182 | 183 | info!("📊 Verifying swap results after maker drops connection"); 184 | // After Swap checks: 185 | verify_swap_results( 186 | taker, 187 | &makers, 188 | org_taker_spend_balance, 189 | org_maker_spend_balances, 190 | ); 191 | 192 | info!("🎉 All checks successful. Terminating integration test case"); 193 | 194 | test_framework.stop(); 195 | block_generation_handle.join().unwrap(); 196 | } 197 | -------------------------------------------------------------------------------- /tests/abort3_case2.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "integration-test")] 2 | use bitcoin::Amount; 3 | use coinswap::{ 4 | maker::{start_maker_server, MakerBehavior}, 5 | taker::{SwapParams, TakerBehavior}, 6 | }; 7 | use std::sync::Arc; 8 | 9 | mod test_framework; 10 | use test_framework::*; 11 | 12 | use log::{info, warn}; 13 | use std::{sync::atomic::Ordering::Relaxed, thread, time::Duration}; 14 | 15 | /// ABORT 3: Maker Drops After Setup 16 | /// Case 2: CloseAtContractSigsForRecvr 17 | /// 18 | /// Maker closes the connection after sending a `ContractSigsForRecvr`. Funding txs to the faulty maker are already broadcasted. 19 | /// The other makers will lose contract txs fees in that case, so it's not malice. 20 | /// Taker waits for the response until timeout. Aborts if the Maker doesn't show up. 21 | #[test] 22 | fn abort3_case2_close_at_contract_sigs_for_recvr() { 23 | // ---- Setup ---- 24 | 25 | // 6102 is naughty. And theres not enough makers. 26 | let naughty = 6102; 27 | let makers_config_map = [ 28 | ((naughty, None), MakerBehavior::CloseAtContractSigsForRecvr), 29 | ((16102, None), MakerBehavior::Normal), 30 | ]; 31 | 32 | let taker_behavior = vec![TakerBehavior::Normal]; 33 | // Initiate test framework, Makers. 34 | // Taker has normal behavior. 35 | let (test_framework, mut takers, makers, block_generation_handle) = 36 | TestFramework::init(makers_config_map.into(), taker_behavior); 37 | 38 | warn!("🧪 Running Test: Maker closes connection after sending a ContractSigsForRecvr"); 39 | 40 | info!("💰 Funding taker and makers"); 41 | // Fund the Taker with 3 utxos of 0.05 btc each and do basic checks on the balance 42 | let taker = &mut takers[0]; 43 | let org_taker_spend_balance = fund_and_verify_taker( 44 | taker, 45 | &test_framework.bitcoind, 46 | 3, 47 | Amount::from_btc(0.05).unwrap(), 48 | ); 49 | 50 | // Fund the Maker with 4 utxos of 0.05 btc each and do basic checks on the balance. 51 | let makers_ref = makers.iter().map(Arc::as_ref).collect::>(); 52 | fund_and_verify_maker( 53 | makers_ref, 54 | &test_framework.bitcoind, 55 | 4, 56 | Amount::from_btc(0.05).unwrap(), 57 | ); 58 | 59 | // Start the Maker Server threads 60 | info!("🚀 Initiating Maker servers"); 61 | 62 | let maker_threads = makers 63 | .iter() 64 | .map(|maker| { 65 | let maker_clone = maker.clone(); 66 | thread::spawn(move || { 67 | start_maker_server(maker_clone).unwrap(); 68 | }) 69 | }) 70 | .collect::>(); 71 | 72 | // Makers take time to fully setup. 73 | let org_maker_spend_balances = makers 74 | .iter() 75 | .map(|maker| { 76 | while !maker.is_setup_complete.load(Relaxed) { 77 | info!("⏳ Waiting for maker setup completion"); 78 | // Introduce a delay of 10 seconds to prevent write lock starvation. 79 | thread::sleep(Duration::from_secs(10)); 80 | continue; 81 | } 82 | 83 | // Check balance after setting up maker server. 84 | let wallet = maker.wallet.read().unwrap(); 85 | 86 | let balances = wallet.get_balances().unwrap(); 87 | 88 | verify_maker_pre_swap_balances(&balances, 14999508); 89 | 90 | balances.spendable 91 | }) 92 | .collect::>(); 93 | 94 | // Initiate Coinswap 95 | info!("🔄 Initiating coinswap protocol"); 96 | 97 | // Swap params for coinswap. 98 | let swap_params = SwapParams { 99 | send_amount: Amount::from_sat(500000), 100 | maker_count: 2, 101 | manually_selected_outpoints: None, 102 | }; 103 | taker.do_coinswap(swap_params).unwrap(); 104 | 105 | // After Swap is done, wait for maker threads to conclude. 106 | makers 107 | .iter() 108 | .for_each(|maker| maker.shutdown.store(true, Relaxed)); 109 | 110 | maker_threads 111 | .into_iter() 112 | .for_each(|thread| thread.join().unwrap()); 113 | 114 | info!("🎯 All coinswaps processed successfully. Transaction complete."); 115 | 116 | thread::sleep(Duration::from_secs(10)); 117 | 118 | /////////////////// 119 | let taker_wallet = taker.get_wallet_mut(); 120 | taker_wallet.sync_and_save().unwrap(); 121 | // Synchronize each maker's wallet. 122 | for maker in makers.iter() { 123 | let mut wallet = maker.get_wallet().write().unwrap(); 124 | wallet.sync_and_save().unwrap(); 125 | } 126 | /////////////// 127 | 128 | // -------- Fee Tracking and Workflow -------- 129 | // 130 | // Case 1: Maker6102 is the First Maker. 131 | // Workflow: Taker -> Maker6102 (CloseAtContractSigsForRecvr) -----> Maker16102 132 | // 133 | // | Participant | Amount Received (Sats) | Amount Forwarded (Sats) | Fee (Sats) | Funding Mining Fees (Sats) | Total Fees (Sats) | 134 | // |----------------|------------------------|-------------------------|------------|----------------------------|-------------------| 135 | // | **Taker** | _ | 500,000 | _ | 3,000 | 3,000 | 136 | // | **Maker6102** | 500,000 | 463,500 | 33,500 | 3,000 | 36,500 | 137 | // 138 | // - Taker sends [`ProofOfFunding`] of Maker6102 to Maker16102, who replies with [`ReqContractSigsForRecvrAndSender`]. 139 | // - Taker forwards [`ReqContractSigsForRecvr`] to Maker6102, but Maker6102 doesn't respond. 140 | // - After a timeout, both Taker and Maker6102 recover from the swap, incurring losses. 141 | // 142 | // Final Outcome for Taker & Maker6102 (Recover from Swap): 143 | // 144 | // | Participant | Mining Fee for Contract txes (Sats) | Timelock Fee (Sats) | Funding Fee (Sats) | Total Recovery Fees (Sats) | 145 | // |-----------------------------------------------------|------------------------------------|---------------------|--------------------|----------------------------| 146 | // | **Taker** | 3,000 | 768 | 3,000 | 6,768 | 147 | // | **Maker6102** (Marked as a bad maker by the Taker) | 3,000 | 768 | 3,000 | 6,768 | 148 | // 149 | // - Both **Taker** and **Maker6102** regain their initial funding amounts but incur a total loss of **6,768 sats** due to mining fees. 150 | // 151 | // Final Outcome for Maker16102: 152 | // 153 | // | Participant | Coinswap Outcome (Sats) | 154 | // |----------------|------------------------------------------| 155 | // | **Maker16102** | 0 | 156 | // 157 | // ------------------------------------------------------------------------------------------------------------------------ 158 | // 159 | // Case 2: Maker6102 is the Second Maker. 160 | // Workflow: Taker -> Maker16102 -> Maker6102 (CloseAtContractSigsForRecvr) 161 | // 162 | // In this case, the Coinswap completes successfully since Maker6102, being the last maker, does not receive [`ReqContractSigsForRecvr`] from the Taker. 163 | // 164 | // The Fee balance would look like `standard_swap` IT for this case. 165 | 166 | info!("🚫 Verifying naughty maker gets banned"); 167 | // Maker gets banned for being naughty. 168 | assert_eq!( 169 | format!("127.0.0.1:{naughty}"), 170 | taker.get_bad_makers()[0].address.to_string() 171 | ); 172 | 173 | info!("📊 Verifying swap results after connection close"); 174 | // After Swap checks: 175 | verify_swap_results( 176 | taker, 177 | &makers, 178 | org_taker_spend_balance, 179 | org_maker_spend_balances, 180 | ); 181 | 182 | info!("🎉 All checks successful. Terminating integration test case"); 183 | 184 | test_framework.stop(); 185 | block_generation_handle.join().unwrap(); 186 | } 187 | --------------------------------------------------------------------------------