diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 4f82cda89..c5bd2e46a 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.74.1 + - uses: dtolnay/rust-toolchain@1.90.0 with: components: rustfmt, clippy diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 3873162bd..d85628460 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -7,8 +7,11 @@ on: jobs: functional: - name: Functional + name: Functional ${{ matrix.type }} runs-on: ubuntu-latest + strategy: + matrix: + type: [python, rust] steps: - uses: actions/checkout@v4 @@ -16,11 +19,13 @@ jobs: # These are the minimal deps defined by bitcoin core at # https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md - name: Prepare bitcoin-core deps + if: matrix.type == 'python' run: sudo apt install build-essential cmake pkgconf python3 libevent-dev libboost-dev # see more at # https://docs.astral.sh/uv/guides/integration/github/ - name: Install uv + if: matrix.type == 'python' uses: astral-sh/setup-uv@v5 with: python-version: "3.12" @@ -28,18 +33,22 @@ jobs: cache-dependency-glob: "uv.lock" - name: Prepare environment + if: matrix.type == 'python' run: uv sync --all-extras --dev - name: Run black formatting + if: matrix.type == 'python' run: uv run black --check --verbose ./tests - name: Run pylint linter + if: matrix.type == 'python' run: uv run pylint --verbose ./tests - name: Cache Rust uses: Swatinem/rust-cache@v2 - - name: Run functional tests tasks + - name: Prepare and run functional tests in python + if: matrix.type == 'python' run: | tests/prepare.sh tests/run.sh @@ -57,3 +66,16 @@ jobs: cat "$logfile" || echo "Failed to read $logfile" echo "::endgroup::" done + + - name: Prepare Bitcoind and Utreexod binaries + if: matrix.type == 'rust' + run: | + source ./test-rust/scripts/prepare.sh + echo "BITCOIND_EXE=$BITCOIND_EXE" >> "$GITHUB_ENV" + echo "UTREEXOD_EXE=$UTREEXOD_EXE" >> "$GITHUB_ENV" + + - name: Build and test Rust + if: matrix.type == 'rust' + run: | + cargo build --bin florestad --release + cargo test --features=functional-tests -p test-functional -- --test-threads=1 \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 597c4b93f..55c345b6f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.74.1 # The version in our `rust-toolchain.toml` + - uses: dtolnay/rust-toolchain@1.90.0 # The version in our `rust-toolchain.toml` with: components: rustfmt, clippy - uses: taiki-e/install-action@cargo-hack diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6c1fcd7a..0bb7d8253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ When adding a new feature ensure that it is covered by functional tests where po When refactoring, structure your PR to make it easy to review and don't hesitate to split it into multiple small, focused PRs. -The Minimum Supported Rust Version is **1.74.1** (enforced by our CI). +The Minimum Supported Rust Version is **1.90.0** (enforced by our CI). Commits should cover both the issue fixed and the solution's rationale. diff --git a/Cargo.lock b/Cargo.lock index 59c364e75..9d320494f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -335,7 +335,7 @@ dependencies = [ "rustc-hash", "shlex", "syn", - "which", + "which 4.4.2", ] [[package]] @@ -372,6 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" dependencies = [ "base58ck", + "base64 0.21.7", "bech32 0.11.0", "bitcoin-internals 0.3.0", "bitcoin-io 0.1.3", @@ -518,6 +519,26 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cast" version = "0.3.0" @@ -577,7 +598,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -741,12 +762,66 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corepc-client" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ce205b817339b55d93bdb41d66704cfc5299f89576ab24107bd2abf4c29c1e" +dependencies = [ + "bitcoin 0.32.7", + "corepc-types", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "corepc-node" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76025e0755bc411fda75e0912a0a0e511d13b54988bf05b90d7681f0c7570b67" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.0", + "corepc-client", + "flate2", + "log", + "minreq", + "serde_json", + "tar", + "tempfile", + "which 3.1.1", + "zip", +] + +[[package]] +name = "corepc-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0231e773ddcfebb8eb8627a16553de56ab064a03843d847f409107ad661f25" +dependencies = [ + "bitcoin 0.32.7", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1011,6 +1086,44 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "electrsd" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e2d112cb8b9fa56e95632eedfc9ca7ace67910c28764367b1415b8148ec062" +dependencies = [ + "bitcoin_hashes 0.14.0", + "corepc-client", + "corepc-node", + "electrum-client", + "log", + "minreq", + "nix", + "which 4.4.2", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7b07e2578a6df0093b101915c79dca0119d7f7810099ad9eef11341d2ae57" +dependencies = [ + "bitcoin 0.32.7", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1033,6 +1146,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.1.2" @@ -1225,7 +1350,7 @@ dependencies = [ "metrics", "oneshot", "rand", - "rustls", + "rustls 0.23.27", "rustreexo", "serde", "serde_json", @@ -1261,6 +1386,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1293,6 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1301,6 +1442,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.31" @@ -1320,9 +1467,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1568,6 +1719,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.27", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1581,12 +1748,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1594,12 +1778,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1762,6 +1950,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -1904,6 +2108,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", + "redox_syscall 0.5.12", ] [[package]] @@ -1985,6 +2190,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "metrics" version = "0.2.0" @@ -2044,8 +2258,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d2aaba477837b46ec1289588180fabfccf0c3b1d1a0c6b1866240cd6cd5ce9" dependencies = [ "log", + "rustls 0.21.12", + "rustls-webpki 0.101.7", "serde", "serde_json", + "webpki-roots 0.25.4", ] [[package]] @@ -2059,6 +2276,37 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.3" @@ -2135,6 +2383,50 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2509,6 +2801,48 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2561,6 +2895,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.27" @@ -2571,7 +2917,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -2594,6 +2940,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.3" @@ -2636,12 +2992,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.28.2" @@ -2681,6 +3056,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -2884,6 +3282,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2910,6 +3311,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -2923,6 +3345,17 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -2942,6 +3375,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test-functional" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitcoin 0.32.7", + "electrsd", + "floresta-rpc", + "once_cell", + "rand", + "rcgen", + "reqwest", + "serde_json", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3052,13 +3500,23 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.27", "tokio", ] @@ -3210,8 +3668,12 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", + "futures-util", "http", + "http-body", + "iri-string", "pin-project-lite", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -3337,7 +3799,7 @@ dependencies = [ "cookie_store", "log", "percent-encoding", - "rustls", + "rustls 0.23.27", "rustls-pemfile", "rustls-pki-types", "serde", @@ -3395,6 +3857,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -3467,6 +3935,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -3509,6 +3990,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.11" @@ -3527,6 +4014,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "which" version = "4.4.2" @@ -3600,9 +4096,9 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -3655,6 +4151,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3670,7 +4183,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", ] [[package]] @@ -3679,7 +4201,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -3709,6 +4231,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3918,6 +4449,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.0.7", +] + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -4053,6 +4594,19 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zmq" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index b7c328b1a..5d62e38ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] resolver = "2" +exclude = ["test-rust"] members = [ # Libraries "crates/floresta", @@ -19,6 +20,7 @@ members = [ # Tests and monitoring "metrics", "fuzz", + "test-rust", ] default-members = [ diff --git a/Dockerfile b/Dockerfile index e3fb9ece3..432532d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="/root/.cargo/bin:${PATH}" -RUN rustup default 1.74.0 +RUN rustup default 1.90.0 WORKDIR /opt/app @@ -20,6 +20,7 @@ COPY Cargo.* ./ COPY bin/ bin/ COPY crates/ crates/ COPY fuzz/ fuzz/ +COPY test-rust/ test-rust/ COPY metrics/ metrics/ COPY doc/ doc/ RUN --mount=type=cache,target=/usr/local/cargo/registry \ diff --git a/contrib/nix/build_floresta.nix b/contrib/nix/build_floresta.nix index 5a645336b..37574994c 100644 --- a/contrib/nix/build_floresta.nix +++ b/contrib/nix/build_floresta.nix @@ -20,7 +20,7 @@ let else basicDeps; - # This is the 1.74.1 rustup (and its components) toolchain from our `./rust-toolchain.toml` + # This is the 1.90.0 rustup (and its components) toolchain from our `./rust-toolchain.toml` florestaRust = pkgs.rust-bin.fromRustupToolchainFile "${src}/rust-toolchain.toml"; # This sets the rustc and cargo to the ones from the florestaRust. diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index ffc49463b..2ba11b620 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -234,11 +234,18 @@ impl RpcImpl { block.header.time }; + let witness_block_size = block + .txdata + .iter() + .map(|tx| tx.total_size() - tx.base_size()) + .sum::(); + let strippedsize = block.total_size() - witness_block_size; + let block = GetBlockResVerbose { - bits: serialize_hex(&block.header.bits), - chainwork: block.header.work().to_string(), + bits: serialize_hex(&block.header.bits.to_consensus().to_be()), + chainwork: serialize_hex(&block.header.work().to_be_bytes()), confirmations: (tip - height) + 1, - difficulty: block.header.difficulty(self.chain.get_params()), + difficulty: block.header.difficulty_float(), hash: block.header.block_hash().to_string(), height, merkleroot: block.header.merkle_root.to_string(), @@ -252,7 +259,7 @@ impl RpcImpl { .map(|tx| tx.compute_txid().to_string()) .collect(), version: block.header.version.to_consensus(), - version_hex: serialize_hex(&block.header.version), + version_hex: serialize_hex(&(block.header.version.to_consensus() as u32).to_be()), weight: block.weight().to_wu() as usize, mediantime: median_time_past, n_tx: block.txdata.len(), @@ -261,7 +268,8 @@ impl RpcImpl { .get_block_hash(height + 1) .ok() .map(|h| h.to_string()), - strippedsize: block.total_size(), + strippedsize, + target: serialize_hex(&block.header.target().to_be_bytes()), }; Ok(block) @@ -306,7 +314,7 @@ impl RpcImpl { root_count, root_hashes, chain: self.network.to_string(), - difficulty: latest_header.difficulty(self.chain.get_params()) as u64, + difficulty: latest_header.difficulty_float(), progress: validated_blocks as f32 / height as f32, }) } diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index bd0ab9f5f..b406734b4 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -17,7 +17,7 @@ pub struct GetBlockchainInfoRes { pub root_hashes: Vec, pub chain: String, pub progress: f32, - pub difficulty: u64, + pub difficulty: f64, } /// A confidence enum to auxiliate rescan timestamp values. @@ -219,7 +219,7 @@ pub struct GetBlockResVerbose { /// difficulty is a multiple of the smallest possible difficulty. So to find the actual /// difficulty you have to multiply this by the min_diff. /// For mainnet, mindiff is 2 ^ 32 - pub difficulty: u128, + pub difficulty: f64, /// Commullative work in this network /// @@ -235,6 +235,11 @@ pub struct GetBlockResVerbose { #[serde(skip_serializing_if = "Option::is_none")] /// The hash of the block coming after this one, if any pub nextblockhash: Option, + + /// Represents the current proof-of-work target as a 256-bit number in string format. + /// A block's SHA-256 hash must be less than or equal to this value to be accepted by the network. + /// Lower values indicate higher mining difficulty. + pub target: String, } #[derive(Debug)] diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 4392ccded..c15b3ee9c 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -26,6 +26,8 @@ pub trait FlorestaRPC { /// the current height, the best block hash, the difficulty, and whether we are /// currently in IBD (Initial Block Download) mode. fn get_blockchain_info(&self) -> Result; + /// Returns the hash of the best (most recent) block in the chain + fn get_best_block_hash(&self) -> Result; /// Returns the hash of the block at the given height /// /// This method returns the hash of the block at the given height. If the height is @@ -98,7 +100,7 @@ pub trait FlorestaRPC { /// This method returns a cached transaction output. If the output is not in the cache, /// or is spent, an empty object is returned. If you want to find a utxo that's not in /// the cache, you can use the findtxout method. - fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result; + fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result; /// Stops the florestad process /// /// This can be used to gracefully stop the florestad process. @@ -254,14 +256,18 @@ impl FlorestaRPC for T { self.call("getblockcount", &[]) } - fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { - self.call( + fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { + let result: serde_json::Value = self.call( "gettxout", &[ Value::String(tx_id.to_string()), Value::Number(Number::from(outpoint)), ], - ) + )?; + if result.is_null() { + return Err(Error::TxOutNotFound); + } + serde_json::from_value(result).map_err(Error::Serde) } fn get_txout_proof(&self, txids: Vec, blockhash: Option) -> Option { @@ -284,6 +290,10 @@ impl FlorestaRPC for T { self.call("getpeerinfo", &[]) } + fn get_best_block_hash(&self) -> Result { + self.call("getbestblockhash", &[]) + } + fn get_block_hash(&self, height: u32) -> Result { self.call("getblockhash", &[Value::Number(Number::from(height))]) } diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 3333793cd..8fdf8d04d 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use bitcoin::BlockHash; use serde::Deserialize; use serde::Serialize; @@ -49,7 +50,7 @@ pub struct GetBlockchainInfoRes { /// /// On average, miners needs to make `difficulty` hashes before finding one that /// solves a block's PoW - pub difficulty: u64, + pub difficulty: f64, } /// The information returned by a get_raw_tx @@ -258,7 +259,7 @@ pub struct GetBlockResVerbose { /// difficulty is a multiple of the smallest possible difficulty. So to find the actual /// difficulty you have to multiply this by the min_diff. /// For mainnet, mindiff is 2 ^ 32 - pub difficulty: u128, + pub difficulty: f64, /// Commullative work in this network /// /// This is a estimate of how many hashes the network has ever made to produce this chain @@ -270,6 +271,51 @@ pub struct GetBlockResVerbose { #[serde(skip_serializing_if = "Option::is_none")] /// The hash of the block coming after this one, if any pub nextblockhash: Option, + /// Represents the current proof-of-work target as a 256-bit number in string format. + /// A block's SHA-256 hash must be less than or equal to this value to be accepted by the network. + /// Lower values indicate higher mining difficulty. + pub target: String, +} + +/// The information by UTXO +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTxOut { + /// The hash of the block at the tip of the chain + pub bestblock: BlockHash, + + /// The number of confirmations + pub confirmations: u32, + + /// The transaction value in BTC + pub value: f64, + + #[serde(rename = "scriptPubKey")] + /// Script Public Key struct + pub script_pubkey: ScriptPubkeyDescription, + + /// Coinbase or not + pub coinbase: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +/// Struct helper for RpcGetTxOut +pub struct ScriptPubkeyDescription { + /// Disassembly of the output script + pub asm: String, + + /// Inferred descriptor for the output + pub desc: String, + + /// The raw output script bytes, hex-encoded + pub hex: String, + + /// The type, eg pubkeyhash + #[serde(rename = "type")] + pub type_field: String, + + /// The Bitcoin address (only if a well-defined address exists) + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, } /// A confidence enum to auxiliate rescan timestamp values. @@ -318,6 +364,9 @@ pub enum Error { /// The user requested a rescan based on invalid values. InvalidRescanVal, + + /// The requested transaction output was not found + TxOutNotFound, } impl From for Error { @@ -343,32 +392,33 @@ impl Display for Error { Error::EmptyResponse => write!(f, "got an empty response from server"), Error::InvalidVerbosity => write!(f, "invalid verbosity level"), Error::InvalidRescanVal => write!(f, "Invalid rescan values"), + Error::TxOutNotFound => write!(f, "Transaction output was not found"), } } } #[derive(Debug, Default, Serialize, Deserialize)] pub struct GetMemInfoStats { - locked: MemInfoLocked, + pub locked: MemInfoLocked, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct MemInfoLocked { /// Memory currently in use, in bytes - used: u64, + pub used: u64, /// Memory currently free, in bytes - free: u64, + pub free: u64, /// Total memory allocated, in bytes - total: u64, + pub total: u64, /// Total memory locked, in bytes /// /// If total is less than total, then some pages may be on swap or not philysically allocated /// yet - locked: u64, + pub locked: u64, /// How many chunks are currently in use - chunks_used: u64, + pub chunks_used: u64, /// How many chunks are currently free - chunks_free: u64, + pub chunks_free: u64, } #[derive(Debug, Serialize, Deserialize)] @@ -380,14 +430,14 @@ pub enum GetMemInfoRes { #[derive(Debug, Serialize, Deserialize)] pub struct ActiveCommand { - method: String, - duration: u64, + pub method: String, + pub duration: u64, } #[derive(Debug, Serialize, Deserialize)] pub struct GetRpcInfoRes { - active_commands: Vec, - logpath: String, + pub active_commands: Vec, + pub logpath: String, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/floresta-wire/README.md b/crates/floresta-wire/README.md index 9a390d9cf..970005af0 100644 --- a/crates/floresta-wire/README.md +++ b/crates/floresta-wire/README.md @@ -66,7 +66,7 @@ Example `peers.json`: ## Minimum Supported Rust Version (MSRV) -This library should compile with any combination of features on **Rust 1.74.1**. +This library should compile with any combination of features on **Rust 1.90.0**. ## License diff --git a/rust-toolchain.toml b/rust-toolchain.toml index cb0fe3064..5e1781e54 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.74.1" +channel = "1.90.0" components = ["rustfmt", "clippy"] profile = "default" diff --git a/test-rust/Cargo.toml b/test-rust/Cargo.toml new file mode 100644 index 000000000..731c6183f --- /dev/null +++ b/test-rust/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "test-functional" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = """ + Functional tests for Floresta, the tests use the Utreexod and Bitcoind in the tests. +""" +repository = "https://github.com/vinteumorg/Floresta" +readme.workspace = true # Floresta/README.md + +keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] +categories = ["cryptography::cryptocurrencies", "command-line-utilities"] + +[dependencies] + + +[dev-dependencies] +floresta-rpc = { path = "../crates/floresta-rpc", features = ["clap"] } +rand = "0.8.5" +rcgen = "0.13" +once_cell = "1.19" +serde_json = "1.0" +reqwest = { version = "0.12", features = ["json", "blocking"] } +anyhow = "1.0" +bitcoin = "0.32.7" +electrsd = { version = "0.36.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_29_0"] } + +[features] +functional-tests = [] \ No newline at end of file diff --git a/test-rust/scripts/bitcoind.sh b/test-rust/scripts/bitcoind.sh new file mode 100755 index 000000000..ac4fbfe6c --- /dev/null +++ b/test-rust/scripts/bitcoind.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -eox pipefail + +# Our tests require `bitcoind` binary. Here, we download the binary, validate it, and export its +# location via `BITCOIND_EXE` which will be used by the `bitcoind` crate in our tests. + +HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" +BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" +BITCOIND_VERSION="29.0" +if [[ "$HOST_PLATFORM" == *linux* ]]; then + BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz + BITCOIND_DL_HASH="a681e4f6ce524c338a105f214613605bac6c33d58c31dc5135bbc02bc458bb6c" +elif [[ "$HOST_PLATFORM" == *darwin* ]]; then + BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz + BITCOIND_DL_HASH="5bb824fc86a15318d6a83a1b821ff4cd4b3d3d0e1ec3d162b805ccf7cae6fca8" +else + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." + exit 1 +fi + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +BITCOIND_DL_URL="$BITCOIND_DL_ENDPOINT"/bitcoin-core-"$BITCOIND_VERSION"/"$BITCOIND_DL_FILE_NAME" +curl -L -o "$BITCOIND_DL_FILE_NAME" "$BITCOIND_DL_URL" +echo "$BITCOIND_DL_HASH $BITCOIND_DL_FILE_NAME"|shasum -a 256 -c +tar xzf "$BITCOIND_DL_FILE_NAME" +export BITCOIND_EXE="$DL_TMP_DIR"/bitcoin-"$BITCOIND_VERSION"/bin/bitcoind +chmod +x "$BITCOIND_EXE" +popd \ No newline at end of file diff --git a/test-rust/scripts/prepare.sh b/test-rust/scripts/prepare.sh new file mode 100755 index 000000000..c65047c5b --- /dev/null +++ b/test-rust/scripts/prepare.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -eox pipefail + +check_installed() { + if ! command -v "$1" &>/dev/null; then + echo "You must have $1 installed to run those tests!" + exit 1 + fi +} + +check_installed git +check_installed go +check_installed cargo + +# Script to run bitcoind.sh and utreexod.sh, then move the binaries to a persistent location. + +# Set FLORESTA_TEMP_DIR if not set +export FLORESTA_TEMP_DIR="/tmp/floresta-func-tests" +mkdir -p "$FLORESTA_TEMP_DIR/binaries" + +# Get the directory where this script is located (use BASH_SOURCE for sourced scripts) +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Define target paths +BITCOIND_TARGET="$FLORESTA_TEMP_DIR/binaries/bitcoind" +UTREEXOD_TARGET="$FLORESTA_TEMP_DIR/binaries/utreexod" + +# Check if bitcoind is already in the correct location +if [ -f "$BITCOIND_TARGET" ]; then + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind already available in $BITCOIND_EXE" +else + # Check if BITCOIND_EXE is already set and the file exists; if so, copy it directly + if [ -n "$BITCOIND_EXE" ] && [ -f "$BITCOIND_EXE" ]; then + cp "$BITCOIND_EXE" "$BITCOIND_TARGET" + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind already available, copied to $BITCOIND_EXE" + else + # Run the script to download/build bitcoind + . "$SCRIPT_DIR/bitcoind.sh" + # After sourcing, copy the binary + if [ -n "$BITCOIND_EXE" ] && [ -f "$BITCOIND_EXE" ]; then + cp "$BITCOIND_EXE" "$BITCOIND_TARGET" + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind moved to $BITCOIND_EXE" + else + echo "BITCOIND_EXE not found or invalid after running script" + exit 1 + fi + fi +fi + +# Check if utreexod is already in the correct location +if [ -f "$UTREEXOD_TARGET" ]; then + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod already available in $UTREEXOD_EXE" +else + # Check if UTREEXOD_EXE is already set and the file exists; if so, copy it directly + if [ -n "$UTREEXOD_EXE" ] && [ -f "$UTREEXOD_EXE" ]; then + cp "$UTREEXOD_EXE" "$UTREEXOD_TARGET" + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod already available, copied to $UTREEXOD_EXE" + else + # Run the script to download/build utreexod + . "$SCRIPT_DIR/utreexod.sh" + # After sourcing, copy the binary + if [ -n "$UTREEXOD_EXE" ] && [ -f "$UTREEXOD_EXE" ]; then + cp "$UTREEXOD_EXE" "$UTREEXOD_TARGET" + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod moved to $UTREEXOD_EXE" + else + echo "UTREEXOD_EXE not found or invalid after running script" + exit 1 + fi + fi +fi + +echo "All binaries pushed to $FLORESTA_TEMP_DIR/binaries" \ No newline at end of file diff --git a/test-rust/scripts/utreexod.sh b/test-rust/scripts/utreexod.sh new file mode 100755 index 000000000..bcd2b7a5a --- /dev/null +++ b/test-rust/scripts/utreexod.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -eox pipefail + +# Our tests require `utreexod` binary. Here, we clone the repository, build it, and export its +# location via `UTREEXOD_EXE` which will be used by the `utreexod` crate in our tests. + +UTREEXOD_REPO="https://github.com/utreexo/utreexod" +UTREEXOD_REVISION="${UTREEXO_REVISION:-}" # Optional: set to a specific tag or commit if needed + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +echo "Cloning and building utreexod..." +git clone "$UTREEXOD_REPO" utreexod +cd utreexod + +# Checkout specific revision if provided +if [ -n "$UTREEXOD_REVISION" ]; then + git checkout "$UTREEXOD_REVISION" +fi + +# Build the binary +go build -o utreexod . + +# Export the executable path +export UTREEXOD_EXE="$DL_TMP_DIR/utreexod/utreexod" +chmod +x "$UTREEXOD_EXE" + +popd \ No newline at end of file diff --git a/test-rust/src/main.rs b/test-rust/src/main.rs new file mode 100644 index 000000000..78c5bbbec --- /dev/null +++ b/test-rust/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + // This project is a test in rust of floresta +} diff --git a/test-rust/tests/common/bitcoind.rs b/test-rust/tests/common/bitcoind.rs new file mode 100644 index 000000000..b4d712d66 --- /dev/null +++ b/test-rust/tests/common/bitcoind.rs @@ -0,0 +1,98 @@ +#![allow(dead_code)] + +use std::env; + +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::corepc_node::Node; + +use crate::common::run_node; +use crate::common::wait_for_condition; +use crate::common::DESCRIPTOR_EXTERNAL; +use crate::common::DESCRIPTOR_INTERNAL; + +const VIEW_LOGS: bool = false; +const WALLET_NAME: &str = "floresta_test"; + +pub(crate) fn setup_bitcoind(v2_transport: bool) -> Node { + let bitcoind_exe = env::var("BITCOIND_EXE") + .ok() + .or_else(|| corepc_node::downloaded_exe_path().ok()) + .expect( + "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", + ); + + let mut bitcoind_conf = corepc_node::Conf::default(); + bitcoind_conf.network = "regtest"; + bitcoind_conf.args.push("-rest"); + bitcoind_conf.p2p = corepc_node::P2P::Yes; + if !v2_transport { + bitcoind_conf.args.push("-v2transport=0"); + } + bitcoind_conf.view_stdout = VIEW_LOGS; + bitcoind_conf.wallet = Some(WALLET_NAME.to_string()); + let bitcoind = Node::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); + + let args = vec![ + serde_json::json!({ + "desc": DESCRIPTOR_INTERNAL, + "timestamp": 1455191478, + "label": "address internal", + "internal": true, + "active": true + }), + serde_json::json!({ + "desc": DESCRIPTOR_EXTERNAL, + "label": "address receive", + "timestamp": 1455191480, + "active": true + }), + ]; + let _: serde_json::Value = bitcoind + .client + .call("importdescriptors", &[serde_json::Value::Array(args)]) + .expect("importdescriptors failed"); + + bitcoind +} + +pub(crate) fn setup_bitcoind_by_bitcoind(bitcoind: &mut Node, v2_transport: bool) { + let bitcoind_exe = env::var("BITCOIND_EXE") + .ok() + .or_else(|| corepc_node::downloaded_exe_path().ok()) + .expect( + "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", + ); + + let args_p2p_port = format!("-bind={}", bitcoind.params.p2p_socket.unwrap()); + let rpcport_arg = format!("-rpcport={}", bitcoind.params.rpc_socket.port()); + let data_dir_arg = format!("-datadir={}", bitcoind.workdir().display()); + let rpc = "bitcoind".to_string(); + let rpc_user_arg = format!("-rpcuser={}", rpc); + let rpc_password_arg = format!("-rpcpassword={}", rpc); + let v2_tranpost_arg = if v2_transport { + "-v2transport=1" + } else { + "-v2transport=0" + }; + + let args = vec![ + "-regtest", + "-rpcthreads=1", + &data_dir_arg, + &args_p2p_port, + &rpcport_arg, + &rpc_user_arg, + &rpc_password_arg, + &v2_tranpost_arg, + ]; + + let _ = run_node(bitcoind_exe, args, VIEW_LOGS); + + let rpc_url = bitcoind.rpc_url_with_wallet(WALLET_NAME); + bitcoind.client = + BitcoindClient::new_with_auth(&rpc_url, Auth::UserPass(rpc.clone(), rpc)).unwrap(); + + wait_for_condition(|| bitcoind.client.get_rpc_info().is_ok()).unwrap(); +} diff --git a/test-rust/tests/common/florestad.rs b/test-rust/tests/common/florestad.rs new file mode 100644 index 000000000..75bd1f819 --- /dev/null +++ b/test-rust/tests/common/florestad.rs @@ -0,0 +1,85 @@ +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Child; + +use electrsd::corepc_node::get_available_port; +use floresta_rpc::jsonrpc_client::Client; +use floresta_rpc::rpc::FlorestaRPC; +use rcgen::generate_simple_self_signed; +use rcgen::CertifiedKey; + +use crate::common::run_node; +use crate::common::wait_for_condition_node_started; +use crate::common::XPUB_STR; + +const VIEW_LOGS: bool = false; + +pub(crate) struct Florestad { + process: Child, + pub(crate) client: Client, + pub(crate) directory: String, +} + +pub(crate) fn setup_florestad() -> Florestad { + // CARGO_MANIFEST_DIR is always floresta-cli's directory; PWD changes based on where the + // command is executed. + let root = format!("{}/..", env!("CARGO_MANIFEST_DIR")); + let release_path = format!("{root}/target/release/florestad"); + let debug_path = format!("{root}/target/debug/florestad"); + + let release_found = Path::new(&release_path).try_exists().unwrap(); + // If release target not found, default to the debug path + let florestad_path: String = match release_found { + true => release_path, + false => debug_path, + }; + + // Makes a temporary directory to store the chain db, TLS certificate, logs, etc. + let test_code = rand::random::(); + let dirname = format!("{root}/tmp/floresta.{test_code}"); + fs::DirBuilder::new() + .recursive(true) + .create(&dirname) + .unwrap(); + + // Generate TLS private key and certificate using rcgen + let CertifiedKey { cert, key_pair } = + generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_pem = cert.pem(); + let key_pem = key_pair.serialize_pem(); + fs::create_dir_all(format!("{dirname}/regtest/tls")).unwrap(); + fs::write(format!("{dirname}/regtest/tls/cert.pem"), cert_pem).unwrap(); + fs::write(format!("{dirname}/regtest/tls/key.pem"), key_pem).unwrap(); + + let rpc_port = get_available_port().unwrap(); + let electrum_port = get_available_port().unwrap(); + let data_dir_arg = format!("--data-dir={}", dirname.clone()); + let wallet_xpub_arg = format!("--wallet-xpub={}", XPUB_STR); + let rpc_address_arg = format!("--rpc-address=127.0.0.1:{}", rpc_port); + let electrum_address_arg = format!("--electrum-address=127.0.0.1:{}", electrum_port); + + let args = vec![ + "--network=regtest", + // "--debug", + // "--no-assume-utreexo", + &electrum_address_arg, + &data_dir_arg, + &wallet_xpub_arg, + &rpc_address_arg, + ]; + + let process = run_node(florestad_path, args, VIEW_LOGS); + + let client = Client::new(format!("http://127.0.0.1:{rpc_port}")); + + wait_for_condition_node_started(|| client.get_blockchain_info().is_ok()).unwrap(); + + Florestad { + process, + client, + directory: dirname, + } +} diff --git a/test-rust/tests/common/mod.rs b/test-rust/tests/common/mod.rs new file mode 100644 index 000000000..92b5ec2f4 --- /dev/null +++ b/test-rust/tests/common/mod.rs @@ -0,0 +1,381 @@ +#![allow(dead_code)] + +pub(crate) mod bitcoind; +pub(crate) mod florestad; +pub(crate) mod utreexod; + +use std::process::Command; +use std::process::Stdio; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; + +use bitcoind::setup_bitcoind; +use electrsd::corepc_node; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::corepc_node::Node as BitcoinD; +use floresta_rpc::rpc::FlorestaRPC; +use florestad::setup_florestad; +use florestad::Florestad; +use once_cell::sync::Lazy; +use rand::Rng; +use utreexod::Utreexod; + +const MAX_RETRY_DURATION_SECONDS_NODE_START: u64 = 500; +const MAX_RETRY_DURATION_SECONDS: u64 = 40; +const DELAYED_RETRY_SECONDS: u64 = 20; +const SLEEP_SECONDS: u64 = 3; + +//menemonicos = useless ritual arm slow mention dog force almost sudden pulp rude eager +pub(crate) const DESCRIPTOR_INTERNAL: &str = "wpkh(tprv8hCwaWbnCTeqSXMmEgtYqC3tjCHQTKphfXBG5MfWgcA6pif3fAUqCuqwphSyXmVFhd8b5ep5krkRxF6YkuQfxSAhHMTGeRA8rKPzQd9BMre/1/*)#v08p3aj4"; +pub(crate) const DESCRIPTOR_EXTERNAL: &str = "wpkh(tprv8hCwaWbnCTeqSXMmEgtYqC3tjCHQTKphfXBG5MfWgcA6pif3fAUqCuqwphSyXmVFhd8b5ep5krkRxF6YkuQfxSAhHMTGeRA8rKPzQd9BMre/0/*)#amzqvgzd"; +pub(crate) const XPUB_STR: &str = "vpub5ZrpbMUWLCJ6MbpU1RzocWBddAQnk2XYry9JSXrtzxSqoicei28CzqUhiN2HJ8z2VjY6rsUNf4qxjym43ydhAFQJ7BDDcC2bK6et6x9hc4D"; +pub(crate) const ADDRESS_STR: &str = "bcrt1q427ze5mrzqupzyfmqsx9gxh7xav538yk2j4cft"; + +// Shared instance of Florestad, BitcoinD, Utreexod, and generated blocks height, initialized once. +// Purpose: Optimize tests needing a chain with blocks by avoiding repeated setup and generation. +static SHARED_FLORESTAD_BITCOIND_UTREEXOD_WITH_BLOCKS: Lazy> = + Lazy::new(|| { + let (florestad, bitcoind, utreexod) = setup_florestad_bitcoind_utreexod(true); + let _height = generate_random_blocks_by_utreexod(&florestad, &bitcoind, &utreexod); + Arc::new((florestad, bitcoind, utreexod)) + }); + +// Returns a shared reference to Florestad, BitcoinD, and Utreexod. +// Purpose: Provide access to the shared setup for tests that don't modify the chain. +pub(crate) fn get_shared_florestad_bitcoind_utreexod_with_blocks( +) -> &'static (Florestad, BitcoinD, Utreexod) { + &*SHARED_FLORESTAD_BITCOIND_UTREEXOD_WITH_BLOCKS +} + +// Returns a shared reference to Florestad and BitcoinD. +// Purpose: Provide access to the shared setup for tests that don't modify the chain. +pub(crate) fn get_shared_florestad_bitcoind_with_blocks() -> (&'static Florestad, &'static BitcoinD) +{ + let (florestad, bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + (florestad, bitcoind) +} + +pub(crate) fn setup_florestad_bitcoind_utreexod( + v2_transport: bool, +) -> (Florestad, BitcoinD, Utreexod) { + let florestad = setup_florestad(); + let bitcoind = setup_bitcoind(v2_transport); + let utreexod = Utreexod::setup(); + + // It is necessary to generate some blocks so that utreexod and bitcoind can sync + let initial_height = 10; + utreexod.client.generate(initial_height).unwrap(); + + // florestad_connected_to_bitcoind(&florestad, &bitcoind, v2_transport); + florestad_connected_to_utreexod(&florestad, &utreexod); + assert!(florestad.client.get_peer_info().unwrap().len() == 1); + + // Add utreexod as peer to bitcoind + bitcoind + .client + .add_node( + &utreexod.p2p.to_string(), + electrsd::corepc_node::AddNodeCommand::Add, + ) + .unwrap(); + + // Check if bitcoind connected to utreexod + wait_for_condition(|| { + let info = bitcoind.client.get_peer_info().unwrap().0; + if info.iter().any(|p| p.address == utreexod.p2p.to_string()) { + assert!(info.len() == 1); + return true; + } + false + }) + .unwrap(); + + // check if utreexod connected to bitcoind + wait_for_condition(|| { + let resp = utreexod.client.get_peer_info().unwrap(); + let peers = resp.as_array().unwrap(); + if peers + .iter() + .any(|p| p["subver"].as_str().unwrap().contains("Satoshi")) + { + assert!(peers.len() == 2); + return true; + } + false + }) + .unwrap(); + + // Wait for all nodes to reach the same block height + wait_for_condition(|| florestad.client.get_block_count().unwrap() == initial_height).unwrap(); + wait_for_condition(|| bitcoind.client.get_block_count().unwrap().0 as u32 == initial_height) + .unwrap(); + + (florestad, bitcoind, utreexod) +} + +pub(crate) fn setup_florestad_bitcoind(v2_transport: bool) -> (Florestad, BitcoinD) { + let florestad = setup_florestad(); + let bitcoind = setup_bitcoind(v2_transport); + + florestad_connected_to_bitcoind(&florestad, &bitcoind, v2_transport); + + (florestad, bitcoind) +} + +pub(crate) fn setup_florestad_utreexod() -> (Florestad, Utreexod) { + let florestad = setup_florestad(); + let utreexod = Utreexod::setup(); + + florestad_connected_to_utreexod(&florestad, &utreexod); + + (florestad, utreexod) +} + +pub(crate) fn florestad_connected_to_bitcoind( + florestad: &Florestad, + bitcoind: &BitcoinD, + v2_transport: bool, +) { + florestad + .client + .add_node( + bitcoind.params.p2p_socket.as_ref().unwrap().to_string(), + floresta_rpc::rpc_types::AddNodeCommand::Add, + v2_transport, + ) + .unwrap(); + + // Wait for florestad to connect to bitcoind + wait_for_condition(|| { + let peers = florestad.client.get_peer_info().unwrap(); + peers.iter().any(|p| { + Some(p.address.clone()) == bitcoind.params.p2p_socket.as_ref().map(|s| s.to_string()) + }) + }) + .unwrap(); + + // Wait for bitcoind to connect to florestad + wait_for_condition(|| { + let peers = bitcoind.client.get_peer_info().unwrap().0; + peers.iter().any(|p| p.subversion.contains("Floresta")) + }) + .unwrap(); +} + +pub(crate) fn florestad_connected_to_utreexod(florestad: &Florestad, utreexod: &Utreexod) { + florestad + .client + .add_node( + utreexod.p2p.to_string(), + floresta_rpc::rpc_types::AddNodeCommand::Add, + false, + ) + .unwrap(); + + // Wait for florestad to connect to utreexod + wait_for_condition(|| { + if let Ok(peers) = florestad.client.get_peer_info() { + return peers.iter().any(|p| p.address == utreexod.p2p.to_string()); + } + false + }) + .unwrap(); + + // Wait for utreexod to connect to florestad + wait_for_condition(|| { + let resp = utreexod.client.get_peer_info().unwrap(); + let peers = resp.as_array().unwrap(); + if peers + .iter() + .any(|p| p["subver"].as_str().unwrap().contains("Floresta")) + { + return true; + } + false + }) + .unwrap(); +} + +pub(crate) fn wait_for_condition_node_started(condition: F) -> Result<(), String> +where + F: Fn() -> bool, +{ + wait_for_condition_inner(condition, MAX_RETRY_DURATION_SECONDS_NODE_START) +} + +// Helper function to wait for a condition with timeout +pub(crate) fn wait_for_condition(condition: F) -> Result<(), String> +where + F: Fn() -> bool, +{ + wait_for_condition_inner(condition, MAX_RETRY_DURATION_SECONDS) +} + +fn wait_for_condition_inner(condition: F, max_retries: u64) -> Result<(), String> +where + F: Fn() -> bool, +{ + let start = Instant::now(); + loop { + if condition() { + return Ok(()); + } + if start.elapsed() > Duration::from_secs(max_retries) { + return Err("Timeout waiting for condition".into()); + } + if start.elapsed() > Duration::from_secs(DELAYED_RETRY_SECONDS) { + std::thread::sleep(Duration::from_secs(SLEEP_SECONDS)); + } + } +} + +/// Asserts that a u32 value equals an i64 value, allowing the i64 to be 1 more due to conversion errors. +/// Panics if the values do not match (with or without the +1 offset). +pub(crate) fn assert_u32_i64_equal_with_offset(u32_val: u32, i64_val: i64) -> Result<(), String> { + let converted = u32_val as i64; + if !(converted == i64_val || converted + 1 == i64_val || converted == i64_val + 1) { + return Err(format!( + "Time comparison failed: {} does not match {} (allowing +1 offset)", + u32_val, i64_val + )); + } + Ok(()) +} + +pub(crate) fn check_peers_florestad_bitcoind( + florestad: &Florestad, + bitcoind: &BitcoinD, + v2_transport: bool, + peer_len: usize, +) { + assert!(florestad.client.ping().is_ok()); + assert!(bitcoind.client.ping().is_ok()); + let peers = florestad.client.get_peer_info().unwrap(); + assert!(peers.len() == peer_len); + + let peer = peers + .iter() + .find(|p| { + Some(p.address.clone()) == bitcoind.params.p2p_socket.as_ref().map(|s| s.to_string()) + }) + .unwrap(); + assert!(peer.state == "Ready"); + assert!(peer.address == bitcoind.params.p2p_socket.unwrap().to_string()); + assert!(peer.kind == "regular"); + assert!(peer.initial_height == 0); + let transporte_protocol = if v2_transport { "V2" } else { "V1" }; + assert!(peer.transport_protocol == transporte_protocol); + + let bitcoind_network = bitcoind.client.get_network_info().unwrap(); + assert!(peer.user_agent == bitcoind_network.subversion); + let service = bitcoind_network.local_services_names.join("|"); + assert!(peer.services.contains(&service)); + + let bitcoind_peers = bitcoind.client.get_peer_info().unwrap(); + assert!(bitcoind_peers.0.len() == 1); + let bitcoind_peer = &bitcoind_peers.0[0]; + assert!(bitcoind_peer.starting_height.unwrap() == 0); + assert!(bitcoind_peer.services == "0000000001000009"); + assert!(bitcoind_peer.subversion.contains("Floresta")); + assert!(bitcoind_peer.transport_protocol_type == transporte_protocol.to_lowercase()); +} + +pub(crate) fn generate_random_blocks_by_utreexod( + florestad: &Florestad, + bitcoind: &BitcoinD, + utreexod: &Utreexod, +) -> usize { + let height_florestad = florestad.client.get_block_count().unwrap() as usize; + + let number_blocks = rand::thread_rng().gen_range(21..50); + utreexod.client.generate(number_blocks as u32).unwrap(); + let expect_height_blocks = height_florestad + number_blocks; + println!( + "Generated {} blocks via utreexod, new height: {}", + number_blocks, expect_height_blocks + ); + + wait_for_condition(|| { + florestad.client.get_block_count().unwrap() == expect_height_blocks as u32 + }) + .unwrap(); + + wait_for_condition(|| { + bitcoind.client.get_block_count().unwrap().0 == expect_height_blocks as u64 + }) + .unwrap(); + + wait_for_condition(|| !florestad.client.get_blockchain_info().unwrap().ibd).unwrap(); + + expect_height_blocks +} + +pub(crate) fn generate_random_blocks(florestad: &Florestad, bitcoind: &BitcoinD) -> usize { + let number_blocks = rand::thread_rng().gen_range(21..50); + let height_bitcoind = bitcoind.client.get_block_count().unwrap().0 as usize; + let height_florestad = florestad.client.get_block_count().unwrap() as usize; + assert!(height_bitcoind == height_florestad); + + generate_blocks_bitcoind(&bitcoind.client, number_blocks); + let expect_height_blocks = height_bitcoind + number_blocks; + + wait_for_condition(|| { + florestad.client.get_block_count().unwrap() == expect_height_blocks as u32 + }) + .unwrap(); + + wait_for_condition(|| { + bitcoind.client.get_block_count().unwrap().0 == expect_height_blocks as u64 + }) + .unwrap(); + + expect_height_blocks +} + +pub(crate) fn generate_blocks_bitcoind(bitcoind: &BitcoindClient, num: usize) { + let address_unchecked = bitcoin::Address::from_str(&ADDRESS_STR).expect("invalid address"); + let address = address_unchecked.assume_checked(); + + print!("Generating {} blocks...", num); + bitcoind.generate_to_address(num, &address).unwrap(); + + print!(" Done!"); + println!("\n"); +} + +pub(crate) fn run_node(path: String, args: Vec<&str>, view_log: bool) -> std::process::Child { + let view_stdout = if view_log { + Stdio::inherit() + } else { + Stdio::null() + }; + let view_stderr = if view_log { + Stdio::inherit() + } else { + Stdio::null() + }; + + let mut process = corepc_node::anyhow::Context::with_context( + Command::new(&path) + .args(&args) + .stdout(view_stdout) + .stderr(view_stderr) + .spawn(), + || format!("Error while executing {:?}", path), + ) + .unwrap(); + + match process.try_wait() { + Ok(Some(_)) | Err(_) => { + let _ = process.kill(); + panic!("process exited prematurely or failed to start"); + } + Ok(None) => { + // Process is still running, proceed + } + }; + + process +} diff --git a/test-rust/tests/common/utreexod.rs b/test-rust/tests/common/utreexod.rs new file mode 100644 index 000000000..793f8d059 --- /dev/null +++ b/test-rust/tests/common/utreexod.rs @@ -0,0 +1,149 @@ +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::net::SocketAddrV4; +use std::process::Child; + +use electrsd::corepc_node::get_available_port; +use floresta_rpc::jsonrpc_client::Client; +use floresta_rpc::jsonrpc_client::JsonRPCConfig; +use floresta_rpc::rpc_types::Error; +use serde_json::json; +use serde_json::Value; + +use crate::common::run_node; +use crate::common::wait_for_condition_node_started; +use crate::common::ADDRESS_STR; + +const VIEW_LOGS: bool = false; + +pub struct Utreexod { + process: Child, + pub(crate) client: RpcUtreexod, + pub(crate) p2p: SocketAddrV4, +} + +impl Utreexod { + pub(crate) fn setup() -> Self { + let utreexod_exe = env::var("UTREEXOD_EXE") + .ok() + .or_else(|| Some("/tmp/floresta-func-tests/binaries/utreexod".to_string())) + .expect("you need to provide an env var UTREEXOD_EXE or specify a utreexod path"); + + // Data directory to avoid write errors + let test_code = rand::random::(); + let data_dir = format!("/tmp/utreexod-data-{}", test_code); + fs::create_dir_all(&data_dir).expect("Failed to create data directory"); + + // Ports for RPC and P2P + let rpc_port = get_available_port().expect("Failed to get available RPC port"); + let p2p_port = get_available_port().expect("Failed to get available P2P port"); + + // Arguments for utreexod (based on btcd) + let bind_arg = format!("--listen=0.0.0.0:{}", p2p_port); + let rpcport_arg = format!("--rpclisten=127.0.0.1:{}", rpc_port); + let datadir_arg = format!("--datadir={}", data_dir); + let mining_addr_arg = format!("--miningaddr={}", ADDRESS_STR); + let args = vec![ + "--regtest", + "--debuglevel=debug", + "--prune=0", + &bind_arg, + &rpcport_arg, + &mining_addr_arg, + "--rpcuser=floresta", + "--rpcpass=floresta", + "--notls", + "--utreexoproofindex", + &datadir_arg, + ]; + + let process = run_node(utreexod_exe, args, VIEW_LOGS); + + // Create the RPC client + let url = format!("http://127.0.0.1:{}", rpc_port); + let client = RpcUtreexod::new(url, Some("floresta".into()), Some("floresta".into())); + + wait_for_condition_node_started(|| client.get_blockchain_info().is_ok()).unwrap(); + + let p2p = format!("127.0.0.1:{}", p2p_port); + + Utreexod { + process, + client, + p2p: p2p.parse().unwrap(), + } + } +} + +pub struct RpcUtreexod { + client: Client, +} + +impl RpcUtreexod { + fn new(url: String, user: Option, pass: Option) -> Self { + let config = JsonRPCConfig { url, user, pass }; + let client = Client::new_with_config(config); + Self { client } + } + + pub fn call(&self, method: &str, args: &[Value]) -> Result { + self.client.rpc_call(method, args) + } + + pub fn get_blockchain_info(&self) -> Result { + self.call("getblockchaininfo", &[]) + } + + pub fn stop(&self) -> Result { + self.call("stop", &[]) + } + + pub fn get_new_address(&self) -> Result { + self.call("getnewaddress", &[]) + } + + pub fn generate(&self, blocks: u32) -> Result { + self.call("generate", &[json!(blocks)]) + } + + pub fn get_utreexo_roots(&self, block_hash: &str) -> Result { + self.call("getutreexoroots", &[json!(block_hash)]) + } + + pub fn send_to_address(&self, address: &str, amount: f64) -> Result { + self.call("sendtoaddress", &[json!(address), json!(amount)]) + } + + pub fn get_balance(&self) -> Result { + self.call("getbalance", &[]) + } + + pub fn get_peer_info(&self) -> Result { + self.call("getpeerinfo", &[]) + } + + pub fn invalidate_block(&self, blockhash: &str) -> Result { + self.call("invalidateblock", &[json!(blockhash)]) + } + + pub fn get_blockhash(&self, height: u32) -> Result { + self.call("getblockhash", &[json!(height)]) + } + + pub fn addnode(&self, node: &str, command: &str) -> Result { + self.call("addnode", &[json!(node), json!(command)]) + } + + pub fn get_block_count(&self) -> Result { + self.call("getblockcount", &[]) + } + + pub fn get_txout(&self, txid: &str, vout: u32, include_mempool: bool) -> Result { + self.call( + "gettxout", + &[json!(txid), json!(vout), json!(include_mempool)], + ) + } +} diff --git a/test-rust/tests/floresta-rpc.rs b/test-rust/tests/floresta-rpc.rs new file mode 100644 index 000000000..d8f25c691 --- /dev/null +++ b/test-rust/tests/floresta-rpc.rs @@ -0,0 +1,537 @@ +#![allow(dead_code)] +#![cfg(feature = "functional-tests")] + +use bitcoin::BlockHash; +use common::bitcoind::setup_bitcoind_by_bitcoind; +use common::check_peers_florestad_bitcoind; +use common::florestad::Florestad; +use common::generate_random_blocks; +use common::setup_florestad_bitcoind; +use common::wait_for_condition; +use electrsd::corepc_node::Node as BitcoinD; +use floresta_rpc::rpc::FlorestaRPC; +use floresta_rpc::rpc_types::AddNodeCommand; +use floresta_rpc::rpc_types::GetBlockRes; +use floresta_rpc::rpc_types::GetMemInfoRes; + +use crate::common::assert_u32_i64_equal_with_offset; +use crate::common::florestad::setup_florestad; +use crate::common::get_shared_florestad_bitcoind_utreexod_with_blocks; +use crate::common::get_shared_florestad_bitcoind_with_blocks; + +mod common; + +#[test] +fn test_add_node_v1() { + do_test_add_node(false); +} +#[test] +fn test_add_node_v2() { + do_test_add_node(true); +} + +fn do_test_add_node(v2_transport: bool) { + //Helper function to restart bitcoind + fn restart_bitcoind(bitcoind: &mut BitcoinD, v2_transport: bool) { + bitcoind.client.stop().unwrap(); + wait_for_condition(|| bitcoind.client.ping().is_err()).unwrap(); + setup_bitcoind_by_bitcoind(bitcoind, v2_transport); + } + + println!("=== Setting up florestad and bitcoind ==="); + let (florestad, mut bitcoind) = setup_florestad_bitcoind(v2_transport); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Stopping bitcoind and waiting for disconnection ==="); + bitcoind.client.stop().unwrap(); + wait_for_condition(|| bitcoind.client.ping().is_err()).unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().is_empty()).unwrap(); + + println!("=== Restarting bitcoind and waiting for reconnection ==="); + setup_bitcoind_by_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| { + florestad.client.get_peer_info().unwrap().len() == 1 && florestad.client.ping().is_ok() + }) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + let node = bitcoind.params.p2p_socket.unwrap().to_string(); + + println!("=== Not adding peer again ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Add, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + florestad + .client + .add_node(node.clone(), AddNodeCommand::Add, !v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + florestad + .client + .add_node(node.clone(), AddNodeCommand::Onetry, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + + println!("=== Removing node from florestad ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Remove, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Waiting for florestad to disconnect from bitcoind ==="); + restart_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| { + florestad.client.get_peer_info().unwrap().is_empty() && florestad.client.ping().is_ok() + }) + .unwrap(); + + println!("=== Adding node with Onetry command ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Onetry, v2_transport) + .unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().len() == 1).unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Final checks for florestad and bitcoind ==="); + restart_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| florestad.client.ping().is_ok()).unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().is_empty()).unwrap(); +} + +#[test] +fn test_get_best_block_hash() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + let floresta_best = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_best = bitcoind.client.get_best_block_hash().unwrap(); + assert_eq!(floresta_best.to_string(), bitcoind_best.0); + + generate_random_blocks(&florestad, &bitcoind); + + let floresta_best = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_best = bitcoind.client.get_best_block_hash().unwrap(); + assert_eq!(floresta_best.to_string(), bitcoind_best.0); +} + +#[test] +fn test_get_block() { + fn check_block( + florestad: &Florestad, + bitcoind: &BitcoinD, + block_hash: BlockHash, + verbosity: Option, + ) { + let floresta_block = florestad.client.get_block(block_hash, verbosity).unwrap(); + match floresta_block { + GetBlockRes::Serialized(floresta_block_hex) => { + assert!(verbosity.is_none()); + let bitcoin_block = bitcoind.client.get_block_verbose_zero(block_hash).unwrap(); + assert_eq!(floresta_block_hex, bitcoin_block.0); + } + GetBlockRes::Verbose(floresta_res) => { + assert!(verbosity == Some(1)); + let bitcoin_res = bitcoind.client.get_block_verbose_one(block_hash).unwrap(); + assert_eq!(floresta_res.hash, bitcoin_res.hash); + assert_u32_i64_equal_with_offset( + floresta_res.confirmations, + bitcoin_res.confirmations, + ) + .unwrap(); + assert_eq!( + floresta_res.strippedsize, + bitcoin_res.stripped_size.unwrap() as usize + ); + assert_eq!(floresta_res.size, bitcoin_res.size as usize); + assert_eq!(floresta_res.weight, bitcoin_res.weight as usize); + assert_u32_i64_equal_with_offset(floresta_res.height, bitcoin_res.height).unwrap(); + assert_eq!(floresta_res.version, bitcoin_res.version); + assert_eq!(floresta_res.version_hex, bitcoin_res.version_hex); + assert_eq!(floresta_res.merkleroot, bitcoin_res.merkle_root); + assert_eq!(floresta_res.tx, bitcoin_res.tx); + assert_u32_i64_equal_with_offset(floresta_res.time, bitcoin_res.time).unwrap(); + assert_u32_i64_equal_with_offset( + floresta_res.mediantime, + bitcoin_res.median_time.unwrap(), + ) + .unwrap(); + assert_u32_i64_equal_with_offset(floresta_res.nonce, bitcoin_res.nonce).unwrap(); + assert_eq!(floresta_res.bits, bitcoin_res.bits); + assert_eq!(floresta_res.difficulty, bitcoin_res.difficulty); + // Not comparing chainwork, because the floresta no accumula chainwork yet. + // assert_eq!(floresta_res.chainwork, bitcoin_res.chain_work); + assert_eq!(floresta_res.n_tx, bitcoin_res.n_tx as usize); + assert_eq!( + floresta_res.previousblockhash, + bitcoin_res.previous_block_hash.unwrap_or( + "0000000000000000000000000000000000000000000000000000000000000000" + .to_string() + ) + ); + assert_eq!(floresta_res.nextblockhash, bitcoin_res.next_block_hash); + assert_eq!(floresta_res.target, bitcoin_res.target); + } + } + } + + fn get_blocks_and_check(florestad: &Florestad, bitcoind: &BitcoinD, block_hash: BlockHash) { + let mut verbosity = None; + check_block(florestad, bitcoind, block_hash, verbosity); + + verbosity = Some(1); + check_block(florestad, bitcoind, block_hash, verbosity); + } + + // Initial block (genesis) + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + let block_hash = florestad.client.get_block_hash(0).unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check last block + let block_hash = florestad.client.get_best_block_hash().unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check a random block floresta + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let block_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check a random block bitcoind + let random_height = rand::thread_rng().gen_range(0..height); + let block_hash = bitcoind + .client + .get_block_hash(random_height as u64) + .unwrap() + .block_hash() + .unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); +} + +#[test] +fn test_block_chain_info() { + fn check(florestad: &Florestad, bitcoind: &BitcoinD) { + let floresta_info = florestad.client.get_blockchain_info().unwrap(); + let bitcoind_info = bitcoind.client.get_blockchain_info().unwrap(); + + assert_eq!(floresta_info.chain, bitcoind_info.chain); + assert_eq!(floresta_info.height, bitcoind_info.blocks as u32); + assert_eq!(floresta_info.best_block, bitcoind_info.best_block_hash); + assert_eq!(floresta_info.difficulty, bitcoind_info.difficulty); + assert_eq!(floresta_info.latest_block_time, bitcoind_info.time as u32); + } + + // Initial block (genesis) + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + check(&florestad, &bitcoind); + + // Generate more blocks + generate_random_blocks(&florestad, &bitcoind); + + // Check last block + check(&florestad, &bitcoind); +} + +#[test] +fn test_get_block_count() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + // Check genesis block height + let floresta_height = florestad.client.get_block_count().unwrap(); + let bitcoind_height = bitcoind.client.get_block_count().unwrap().0; + assert_eq!(floresta_height as u64, bitcoind_height); + + // Generate more blocks + let new_height = generate_random_blocks(&florestad, &bitcoind); + + // Check latest block height + let floresta_height = florestad.client.get_block_count().unwrap(); + let bitcoind_height = bitcoind.client.get_block_count().unwrap().0; + assert_eq!(floresta_height as u64, bitcoind_height); + assert_eq!(floresta_height as usize, new_height); +} + +#[test] +fn test_get_block_hash() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Check genesis block hash + let floresta_hash = florestad.client.get_block_hash(0).unwrap(); + let bitcoind_hash = bitcoind + .client + .get_block_hash(0) + .unwrap() + .block_hash() + .unwrap(); + assert_eq!(floresta_hash, bitcoind_hash); + + // Check latest block hash + let floresta_hash = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_hash = bitcoind + .client + .get_best_block_hash() + .unwrap() + .block_hash() + .unwrap(); + + assert_eq!(floresta_hash, bitcoind_hash); + + // Check a random block hash + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let floresta_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let bitcoind_hash = bitcoind + .client + .get_block_hash(random_height as u64) + .unwrap() + .block_hash() + .unwrap(); + assert_eq!(floresta_hash, bitcoind_hash); +} + +#[test] +fn test_get_block_header() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Check genesis block header + let floresta_hash = florestad.client.get_block_hash(0).unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); + + // Check latest block header + let floresta_hash = florestad.client.get_best_block_hash().unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); + + // Check a random block header + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let floresta_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); +} + +#[test] +fn test_get_memory_info() { + let (florestad, _bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Test mode "stats" (only on Linux) + #[cfg(target_os = "linux")] + { + let result = florestad + .client + .get_memory_info("stats".to_string()) + .unwrap(); + match result { + GetMemInfoRes::Stats(res) => { + // Check basic invariants: total should be at least the used/free parts, + assert!(res.locked.total >= res.locked.used); + assert!(res.locked.total >= res.locked.free); + assert!(res.locked.locked > 0); + + assert!(res.locked.chunks_used > 0); + assert!(res.locked.chunks_free > 0); + } + _ => panic!("Expected GetMemInfoRes::Stats"), + } + } + #[cfg(not(target_os = "linux"))] + { + // Skip on non-Linux + println!("Skipping 'getmemoryinfo stats': not implemented for this OS"); + } + + // Test mode "mallocinfo" (only on Linux) + #[cfg(target_os = "linux")] + { + let result = florestad + .client + .get_memory_info("mallocinfo".to_string()) + .unwrap(); + match result { + GetMemInfoRes::MallocInfo(xml) => { + // Just checking if we got some XML content + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + } + _ => panic!("Expected GetMemInfoRes::MallocInfo"), + } + } + #[cfg(not(target_os = "linux"))] + { + // Skip on non-Linux + println!("Skipping 'getmemoryinfo mallocinfo': not implemented for this OS"); + } +} + +#[test] +fn test_get_peer_info() { + let v2_transport = true; + let (florestad, bitcoind) = setup_florestad_bitcoind(v2_transport); + + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); +} + +#[test] +fn test_get_roots() { + let (florestad, _bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + + let floresta_roots = florestad.client.get_roots().unwrap(); + + assert!(!floresta_roots.is_empty()); +} + +#[test] +fn test_get_rpc_info() { + let florestad = setup_florestad(); + let rpc_info = florestad.client.get_rpc_info().unwrap(); + assert_eq!(rpc_info.active_commands.len(), 1); + assert_eq!(rpc_info.active_commands[0].method, "getrpcinfo"); + assert!(rpc_info.active_commands[0].duration > 0); + assert_eq!(rpc_info.logpath, florestad.directory + "/regtest/debug.log"); +} + +#[test] +fn test_get_tx_out() { + let (florestad, bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + + let height = florestad.client.get_block_count().unwrap() as usize; + + use rand::Rng; + let random_height = rand::thread_rng().gen_range(1..height); + let block_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let block = bitcoind.client.get_block(block_hash).unwrap(); + let txid = block.txdata[0].compute_txid(); + let vout = 0; + + let txout_floresta = florestad.client.get_tx_out(txid, vout).unwrap(); + let txout_bitcoind = bitcoind.client.get_tx_out(txid, vout.into()).unwrap(); + assert_eq!( + txout_floresta.bestblock.to_string(), + txout_bitcoind.best_block + ); + assert_eq!(txout_floresta.confirmations, txout_bitcoind.confirmations); + assert_eq!(txout_floresta.value, txout_bitcoind.value); + assert_eq!(txout_floresta.coinbase, txout_bitcoind.coinbase); + assert_eq!( + txout_floresta.script_pubkey.hex, + txout_bitcoind.script_pubkey.hex + ); + assert_eq!( + txout_floresta.script_pubkey.type_field, + txout_bitcoind.script_pubkey.type_ + ); + assert_eq!( + txout_floresta.script_pubkey.asm, + txout_bitcoind.script_pubkey.asm + ); + assert_eq!( + txout_floresta.script_pubkey.desc, + txout_bitcoind + .script_pubkey + .descriptor + .unwrap_or("".to_string()) + ); + assert_eq!( + txout_floresta.script_pubkey.address, + txout_bitcoind.script_pubkey.address + ); +} + +#[test] +fn test_ping() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + // Check initial state (no ping_time) + let bitcoin_res = bitcoind.client.get_peer_info().unwrap(); + let peer_bitcoin = bitcoin_res.0.first().unwrap(); + assert!(peer_bitcoin.ping_time.is_none()); + + // Send ping from florestad + florestad.client.ping().unwrap(); + + // Wait for ping_time to be set in bitcoind + wait_for_condition(|| { + let bitcoin_res = bitcoind.client.get_peer_info().unwrap(); + let peer_bitcoin = bitcoin_res.0.first().unwrap(); + peer_bitcoin.ping_time.is_some() + }) + .unwrap(); +} + +#[test] +fn test_stop() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + let stop_res = florestad.client.stop().unwrap(); + assert_eq!(stop_res.as_str(), "Floresta stopping"); + wait_for_condition(|| florestad.client.ping().is_err()).unwrap(); + + wait_for_condition(|| bitcoind.client.ping().is_ok()).unwrap(); + assert!(bitcoind.client.get_peer_info().unwrap().0.is_empty()) +} + +#[test] +fn test_uptime() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + std::thread::sleep(std::time::Duration::from_secs(2)); + + let floresta_uptime = florestad.client.uptime().unwrap(); + let bitcoin_uptime = bitcoind.client.uptime().unwrap(); + + assert!(floresta_uptime > 0); + assert!(bitcoin_uptime > 0); + + use rand::Rng; + let random_sleep = rand::thread_rng().gen_range(1..15); + std::thread::sleep(std::time::Duration::from_secs(random_sleep)); + + let floresta_new_uptime = florestad.client.uptime().unwrap(); + assert!(floresta_new_uptime >= floresta_uptime + random_sleep as u32); + let bitcoin_new_uptime = bitcoind.client.uptime().unwrap(); + assert!(bitcoin_new_uptime >= bitcoin_uptime + random_sleep as u32); +}