Skip to content

Commit 0881f5f

Browse files
committed
feat: add dashd integration tests for SPV sync
Adds comprehensive integration tests that verify SPV sync against a real `dashd` instance with pre-generated regtest blockchain: [dashpay/regtest-blockchain](https://github.com/dashpay/regtest-blockchain) ### Includes: - CI infrastructure: `contrib/setup-dashd.py` for cross-platform dashd/test-data setup, GitHub Actions caching and log retention on test failure - Shared test utilities in `dash-spv/src/test_utils/`: `DashCoreNode` (`dashd` process management, RPC), `DashdTestContext` (common setup), filesystem helpers - FFI test utilities in `dash-spv-ffi/src/test_utils/`: `CallbackTracker` (callback verification), `FFITestContext` (FFI client wrapper lifecycle management) - SPV tests: basic sync, empty wallet, multi-wallet, restart consistency, restart with fresh wallet, multiple restarts, random restarts, peer disconnection (exclusive and non-exclusive mode), incremental transactions (single block, across blocks) - FFI tests: wallet sync, incremental sync, restart consistency, all-callbacks verification, post-sync transaction and disconnect callbacks
1 parent aa85b87 commit 0881f5f

22 files changed

Lines changed: 3537 additions & 2 deletions

.github/workflows/build-and-test.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ on:
1313
permissions:
1414
contents: read
1515

16+
# Keep these defaults in sync with contrib/setup-dashd.py
17+
env:
18+
DASHVERSION: "23.1.0"
19+
TEST_DATA_REPO: "dashpay/regtest-blockchain"
20+
TEST_DATA_VERSION: "v0.0.2"
21+
1622
jobs:
1723
test:
1824
name: ${{ matrix.group }}
@@ -30,5 +36,32 @@ jobs:
3036
with:
3137
shared-key: "test-${{ inputs.os }}-${{ matrix.group }}"
3238
- run: pip install pyyaml
39+
40+
# Set up dashd and test data for groups that need it
41+
- name: Cache dashd and test data
42+
if: matrix.group == 'spv' || matrix.group == 'ffi'
43+
uses: actions/cache@v4
44+
with:
45+
path: .rust-dashcore-test
46+
key: rust-dashcore-test-${{ inputs.os }}-${{ env.DASHVERSION }}-${{ env.TEST_DATA_VERSION }}
47+
48+
- name: Setup dashd for integration tests
49+
if: matrix.group == 'spv' || matrix.group == 'ffi'
50+
env:
51+
CACHE_DIR: ${{ github.workspace }}/.rust-dashcore-test
52+
shell: bash
53+
run: python contrib/setup-dashd.py >> "$GITHUB_ENV"
54+
3355
- name: Run tests
56+
env:
57+
DASHD_TEST_RETAIN_DIR: ${{ (matrix.group == 'spv' || matrix.group == 'ffi') && '/tmp/dashd-test-logs' || '' }}
3458
run: python .github/scripts/ci_config.py run-group ${{ matrix.group }} --os ${{ inputs.os }}
59+
60+
- name: Upload failed dashd test logs
61+
if: failure() && (matrix.group == 'spv' || matrix.group == 'ffi')
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: ${{ matrix.group }}-test-logs-${{ inputs.os }}
65+
path: /tmp/dashd-test-logs/
66+
retention-days: 7
67+
if-no-files-found: ignore

.github/workflows/sanitizer.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
RUSTFLAGS: "-Zsanitizer=address -Cdebuginfo=2 -Cforce-frame-pointers=yes"
4141
ASAN_OPTIONS: "symbolize=1:allow_addr2line=1"
4242
LSAN_OPTIONS: "fast_unwind_on_malloc=0"
43+
SKIP_DASHD_TESTS: 1
4344
run: |
4445
# FFI crates (C interop)
4546
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu \
@@ -63,6 +64,7 @@ jobs:
6364
RUST_BACKTRACE: 1
6465
RUSTFLAGS: "-Zsanitizer=thread -Cdebuginfo=2"
6566
TSAN_OPTIONS: "second_deadlock_stack=1"
67+
SKIP_DASHD_TESTS: 1
6668
run: |
6769
# Async crate with concurrent code
6870
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu \

contrib/setup-dashd.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
"""Cross-platform setup script for dashd and test blockchain data.
3+
4+
Downloads the Dash Core binary and regtest test data for integration tests.
5+
Outputs DASHD_PATH and DASHD_DATADIR lines suitable for appending to GITHUB_ENV
6+
or evaluating in a shell.
7+
8+
Environment variables:
9+
DASHVERSION - Dash Core version (default: 23.1.0)
10+
TEST_DATA_VERSION - Test data release version (default: v0.0.2)
11+
TEST_DATA_REPO - GitHub repo for test data (default: dashpay/regtest-blockchain)
12+
CACHE_DIR - Cache directory (default: ~/.rust-dashcore-test)
13+
"""
14+
15+
import os
16+
import platform
17+
import sys
18+
import tarfile
19+
import time
20+
import urllib.request
21+
import zipfile
22+
23+
# Keep these defaults in sync with .github/workflows/build-and-test.yml
24+
DASHVERSION = os.environ.get("DASHVERSION", "23.1.0")
25+
TEST_DATA_VERSION = os.environ.get("TEST_DATA_VERSION", "v0.0.2")
26+
TEST_DATA_REPO = os.environ.get("TEST_DATA_REPO", "dashpay/regtest-blockchain")
27+
28+
29+
def get_cache_dir():
30+
if "CACHE_DIR" in os.environ:
31+
return os.environ["CACHE_DIR"]
32+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
33+
if not home:
34+
sys.exit("Cannot determine home directory: neither HOME nor USERPROFILE is set")
35+
return os.path.join(home, ".rust-dashcore-test")
36+
37+
38+
def get_asset_info():
39+
"""Return the asset filename for the current platform."""
40+
system = platform.system()
41+
machine = platform.machine()
42+
43+
if system == "Linux":
44+
linux_archs = {"aarch64": "aarch64", "arm64": "aarch64", "x86_64": "x86_64", "amd64": "x86_64"}
45+
arch = linux_archs.get(machine)
46+
if not arch:
47+
sys.exit(f"Unsupported Linux architecture: {machine}")
48+
asset = f"dashcore-{DASHVERSION}-{arch}-linux-gnu.tar.gz"
49+
elif system == "Darwin":
50+
darwin_archs = {"arm64": "arm64", "x86_64": "x86_64"}
51+
arch = darwin_archs.get(machine)
52+
if not arch:
53+
sys.exit(f"Unsupported macOS architecture: {machine}")
54+
asset = f"dashcore-{DASHVERSION}-{arch}-apple-darwin.tar.gz"
55+
elif system == "Windows":
56+
asset = f"dashcore-{DASHVERSION}-win64.zip"
57+
else:
58+
sys.exit(f"Unsupported platform: {system}")
59+
60+
return asset
61+
62+
63+
def log(msg):
64+
print(msg, file=sys.stderr)
65+
66+
67+
def download(url, dest, timeout=300, retries=3):
68+
for attempt in range(1, retries + 1):
69+
try:
70+
log(f"Downloading {url} (attempt {attempt}/{retries})...")
71+
with urllib.request.urlopen(url, timeout=timeout) as response:
72+
with open(dest, "wb") as f:
73+
while chunk := response.read(8192):
74+
f.write(chunk)
75+
return
76+
except Exception as e:
77+
log(f"Download failed: {e}")
78+
if attempt == retries:
79+
sys.exit(f"Failed to download {url} after {retries} attempts")
80+
time.sleep(5 * attempt)
81+
82+
83+
def extract(archive_path, dest_dir):
84+
if archive_path.endswith(".zip"):
85+
with zipfile.ZipFile(archive_path, "r") as zf:
86+
zf.extractall(dest_dir)
87+
else:
88+
with tarfile.open(archive_path, "r:gz") as tf:
89+
tf.extractall(dest_dir, filter="data")
90+
91+
92+
def setup_dashd(cache_dir):
93+
"""Download and extract dashd binary. Returns the path to the dashd binary."""
94+
asset = get_asset_info()
95+
dashd_dir = os.path.join(cache_dir, f"dashcore-{DASHVERSION}")
96+
97+
ext = ".exe" if platform.system() == "Windows" else ""
98+
dashd_bin = os.path.join(dashd_dir, "bin", f"dashd{ext}")
99+
100+
if os.path.isfile(dashd_bin):
101+
log(f"dashd {DASHVERSION} already available")
102+
return dashd_bin
103+
104+
log(f"Downloading dashd {DASHVERSION}...")
105+
archive_path = os.path.join(cache_dir, asset)
106+
url = f"https://github.com/dashpay/dash/releases/download/v{DASHVERSION}/{asset}"
107+
download(url, archive_path)
108+
extract(archive_path, cache_dir)
109+
os.remove(archive_path)
110+
log(f"Downloaded dashd to {dashd_dir}")
111+
112+
if not os.path.isfile(dashd_bin):
113+
sys.exit(f"Expected binary not found after extraction: {dashd_bin}")
114+
115+
return dashd_bin
116+
117+
118+
def setup_test_data(cache_dir):
119+
"""Download and extract test blockchain data. Returns the datadir path."""
120+
test_data_dir = os.path.join(
121+
cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}", "regtest-40000"
122+
)
123+
blocks_dir = os.path.join(test_data_dir, "regtest", "blocks")
124+
125+
if os.path.isdir(blocks_dir):
126+
log(f"Test blockchain data {TEST_DATA_VERSION} already available")
127+
return test_data_dir
128+
129+
log(f"Downloading test blockchain data {TEST_DATA_VERSION}...")
130+
parent_dir = os.path.join(cache_dir, f"regtest-blockchain-{TEST_DATA_VERSION}")
131+
os.makedirs(parent_dir, exist_ok=True)
132+
133+
archive_path = os.path.join(cache_dir, "regtest-40000.tar.gz")
134+
url = f"https://github.com/{TEST_DATA_REPO}/releases/download/{TEST_DATA_VERSION}/regtest-40000.tar.gz"
135+
download(url, archive_path)
136+
extract(archive_path, parent_dir)
137+
os.remove(archive_path)
138+
139+
if not os.path.isdir(blocks_dir):
140+
sys.exit(f"Expected blocks directory not found after extraction: {blocks_dir}")
141+
142+
log(f"Downloaded test data to {test_data_dir}")
143+
144+
return test_data_dir
145+
146+
147+
def main():
148+
cache_dir = get_cache_dir()
149+
os.makedirs(cache_dir, exist_ok=True)
150+
151+
dashd_path = setup_dashd(cache_dir)
152+
datadir = setup_test_data(cache_dir)
153+
154+
# Output lines for GITHUB_ENV or shell eval
155+
print(f"DASHD_PATH={dashd_path}")
156+
print(f"DASHD_DATADIR={datadir}")
157+
158+
159+
if __name__ == "__main__":
160+
main()

dash-spv-ffi/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@ key-wallet = { path = "../key-wallet" }
3131
key-wallet-manager = { path = "../key-wallet-manager" }
3232
rand = "0.8"
3333
clap = { version = "4.5", features = ["derive"] }
34+
tempfile = { version = "3.8", optional = true }
35+
36+
[features]
37+
test-utils = ["dep:tempfile", "dash-spv/test-utils"]
3438

3539
[dev-dependencies]
36-
tempfile = "3.8"
40+
dash-spv = { path = "../dash-spv", features = ["test-utils"] }
41+
dash-spv-ffi = { path = ".", features = ["test-utils"] }
3742
serial_test = "3.0"
3843
env_logger = "0.10"
3944

dash-spv-ffi/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub use platform_integration::*;
1414
pub use types::*;
1515
pub use utils::*;
1616

17+
#[cfg(any(test, feature = "test-utils"))]
18+
pub mod test_utils;
19+
1720
// FFINetwork is now defined in types.rs for cbindgen compatibility
1821
// It must match the definition in key_wallet_ffi
1922

0 commit comments

Comments
 (0)