Skip to content

Commit 49d0fc6

Browse files
committed
tests: Add dashd_sync.rs with test_wallet_sync
Add integration test infrastructure for SPV sync against a local `dashd` regtest node using a pre-generated blockchain which is currently stored in https://github.com/xdustinface/regtest-blockchain. This currently adds just one test which still fails at the moment on `v0.41-dev`. The PRs to fix it will follow _soon_.
1 parent 5d182ab commit 49d0fc6

File tree

5 files changed

+785
-0
lines changed

5 files changed

+785
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: SPV Integration Tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [master, 'v**-dev']
7+
pull_request:
8+
9+
env:
10+
DASHVERSION: "23.0.2"
11+
TEST_DATA_REPO: "xdustinface/regtest-blockchain"
12+
TEST_DATA_VERSION: "v0.0.1"
13+
CACHE_DIR: ${{ github.workspace }}/.rust-dashcore-test
14+
15+
jobs:
16+
spv-integration-test:
17+
name: "SPV sync with dashd"
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: dtolnay/rust-toolchain@stable
24+
25+
- uses: Swatinem/rust-cache@v2
26+
with:
27+
shared-key: "spv-integration"
28+
29+
- name: Cache test dependencies
30+
uses: actions/cache@v4
31+
with:
32+
path: .rust-dashcore-test
33+
key: rust-dashcore-test-${{ env.DASHVERSION }}-${{ env.TEST_DATA_VERSION }}
34+
35+
- name: Run integration test
36+
env:
37+
CACHE_DIR: ${{ github.workspace }}/.rust-dashcore-test
38+
TEST_DATA_REPO: ${{ env.TEST_DATA_REPO }}
39+
TEST_DATA_VERSION: ${{ env.TEST_DATA_VERSION }}
40+
DASHVERSION: ${{ env.DASHVERSION }}
41+
run: |
42+
chmod +x ./contrib/setup-dashd.sh
43+
source ./contrib/setup-dashd.sh
44+
cargo test -p dash-spv --test dashd_sync -- --nocapture

contrib/setup-dashd.sh

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env bash
2+
# Setup and download script for dashd and test blockchain data for integration tests.
3+
#
4+
# Usage:
5+
# ./contrib/setup-dashd.sh
6+
#
7+
# Environment variables:
8+
# DASHVERSION - Dash Core version (default: 23.0.2)
9+
# TEST_DATA_VERSION - Test data release version (default: v0.0.1)
10+
# TEST_DATA_REPO - GitHub repo for test data (default: xdustinface/regtest-blockchain)
11+
# CACHE_DIR - Cache directory (default: ~/.cache/rust-dashcore-test)
12+
13+
set -euo pipefail
14+
15+
DASHVERSION="${DASHVERSION:-23.0.2}"
16+
TEST_DATA_VERSION="${TEST_DATA_VERSION:-v0.0.1}"
17+
TEST_DATA_REPO="${TEST_DATA_REPO:-xdustinface/regtest-blockchain}"
18+
19+
CACHE_DIR="${CACHE_DIR:-$HOME/.rust-dashcore-test}"
20+
21+
# Detect platform and set asset name
22+
case "$(uname -s)" in
23+
Linux*)
24+
DASHD_ASSET="dashcore-${DASHVERSION}-x86_64-linux-gnu.tar.gz"
25+
;;
26+
Darwin*)
27+
case "$(uname -m)" in
28+
arm64) DASHD_ASSET="dashcore-${DASHVERSION}-arm64-apple-darwin.tar.gz" ;;
29+
*) DASHD_ASSET="dashcore-${DASHVERSION}-x86_64-apple-darwin.tar.gz" ;;
30+
esac
31+
;;
32+
*)
33+
echo "Unsupported platform: $(uname -s)"
34+
exit 1
35+
;;
36+
esac
37+
38+
mkdir -p "$CACHE_DIR"
39+
40+
# Download dashd if not cached
41+
DASHD_DIR="$CACHE_DIR/dashcore-${DASHVERSION}"
42+
DASHD_BIN="$DASHD_DIR/bin/dashd"
43+
if [ -x "$DASHD_BIN" ]; then
44+
echo "dashd ${DASHVERSION} already available"
45+
else
46+
echo "Downloading dashd ${DASHVERSION}..."
47+
curl -L "https://github.com/dashpay/dash/releases/download/v${DASHVERSION}/${DASHD_ASSET}" \
48+
-o "$CACHE_DIR/${DASHD_ASSET}"
49+
tar -xzf "$CACHE_DIR/${DASHD_ASSET}" -C "$CACHE_DIR"
50+
rm "$CACHE_DIR/${DASHD_ASSET}"
51+
echo "Downloaded dashd to $DASHD_DIR"
52+
fi
53+
54+
# Download test data if not cached
55+
TEST_DATA_DIR="$CACHE_DIR/regtest-blockchain-${TEST_DATA_VERSION}/regtest-1000"
56+
if [ -d "$TEST_DATA_DIR/regtest/blocks" ]; then
57+
echo "Test blockchain data ${TEST_DATA_VERSION} already available"
58+
else
59+
echo "Downloading test blockchain data ${TEST_DATA_VERSION}..."
60+
mkdir -p "$CACHE_DIR/regtest-blockchain-${TEST_DATA_VERSION}"
61+
curl -L "https://github.com/${TEST_DATA_REPO}/releases/download/${TEST_DATA_VERSION}/regtest-1000.tar.gz" \
62+
-o "$CACHE_DIR/regtest-1000.tar.gz"
63+
tar -xzf "$CACHE_DIR/regtest-1000.tar.gz" -C "$CACHE_DIR/regtest-blockchain-${TEST_DATA_VERSION}"
64+
rm "$CACHE_DIR/regtest-1000.tar.gz"
65+
echo "Downloaded test data to $TEST_DATA_DIR"
66+
fi
67+
68+
# Set environment variables
69+
export DASHD_PATH="$DASHD_DIR/bin/dashd"
70+
export DASHD_DATADIR="$TEST_DATA_DIR"
71+
72+
echo ""
73+
echo "Environment configured:"
74+
echo " DASHD_PATH=$DASHD_PATH"
75+
echo " DASHD_DATADIR=$DASHD_DATADIR"
76+
echo ""
77+
78+
# Reset strict mode (important when sourcing)
79+
set +euo pipefail

dash-spv/tests/common/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Test utilities for dash-spv integration testing.
2+
pub mod node;
3+
4+
pub use node::{is_dashd_available, DashCoreNode};

dash-spv/tests/common/node.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
//! Dash Core node harness for integration testing.
2+
//!
3+
//! This starts a dashd instance using existing regtest data providing full protocol support.
4+
use std::net::{Ipv4Addr, SocketAddr};
5+
use std::path::PathBuf;
6+
use std::time::Duration;
7+
use tokio::io::{AsyncBufReadExt, BufReader};
8+
use tokio::process::{Child, Command};
9+
use tokio::time::{sleep, timeout};
10+
11+
const REGTEST_P2P_PORT: u16 = 19999;
12+
const REGTEST_RPC_PORT: u16 = 19998;
13+
14+
/// Configuration for Dash Core node
15+
pub struct DashCoreConfig {
16+
/// Path to dashd binary
17+
pub dashd_path: PathBuf,
18+
/// Path to existing datadir with blockchain data
19+
pub datadir: PathBuf,
20+
/// Wallet name to load on startup
21+
pub wallet: String,
22+
}
23+
24+
impl Default for DashCoreConfig {
25+
fn default() -> Self {
26+
let dashd_path = std::env::var("DASHD_PATH")
27+
.map(PathBuf::from)
28+
.expect("DASHD_PATH not set. Run: source ./contrib/setup-dashd.sh");
29+
30+
let datadir = std::env::var("DASHD_DATADIR")
31+
.map(PathBuf::from)
32+
.or_else(|_| {
33+
// Fallback to default cache location from setup-dashd.sh
34+
std::env::var("HOME").map(|h| {
35+
PathBuf::from(h)
36+
.join(".rust-dashcore-test/regtest-blockchain-v0.0.1/regtest-1000")
37+
})
38+
})
39+
.expect("Neither DASHD_DATADIR nor HOME is set");
40+
41+
Self {
42+
dashd_path,
43+
datadir,
44+
wallet: "default".to_string(),
45+
}
46+
}
47+
}
48+
49+
/// Harness for managing a Dash Core node
50+
pub struct DashCoreNode {
51+
config: DashCoreConfig,
52+
process: Option<Child>,
53+
}
54+
55+
impl DashCoreNode {
56+
/// Create a new Dash Core node with custom configuration
57+
pub fn with_config(config: DashCoreConfig) -> Result<Self, Box<dyn std::error::Error>> {
58+
if !config.dashd_path.exists() {
59+
return Err(format!("dashd not found at {:?}", config.dashd_path).into());
60+
}
61+
62+
Ok(Self {
63+
config,
64+
process: None,
65+
})
66+
}
67+
68+
/// Start the Dash Core node
69+
pub async fn start(&mut self) -> Result<SocketAddr, Box<dyn std::error::Error>> {
70+
tracing::info!("Starting dashd...");
71+
tracing::info!(" Binary: {:?}", self.config.dashd_path);
72+
tracing::info!(" Datadir: {:?}", self.config.datadir);
73+
tracing::info!(" P2P port: {}", REGTEST_P2P_PORT);
74+
tracing::info!(" RPC port: {}", REGTEST_RPC_PORT);
75+
76+
// Ensure datadir exists
77+
std::fs::create_dir_all(&self.config.datadir)?;
78+
79+
// Build command arguments
80+
let args_vec = vec![
81+
"-regtest".to_string(),
82+
format!("-datadir={}", self.config.datadir.display()),
83+
format!("-port={}", REGTEST_P2P_PORT),
84+
format!("-rpcport={}", REGTEST_RPC_PORT),
85+
"-server=1".to_string(),
86+
"-daemon=0".to_string(),
87+
"-fallbackfee=0.00001".to_string(),
88+
"-rpcbind=127.0.0.1".to_string(),
89+
"-rpcallowip=127.0.0.1".to_string(),
90+
"-listen=1".to_string(),
91+
"-txindex=0".to_string(),
92+
"-addressindex=0".to_string(),
93+
"-spentindex=0".to_string(),
94+
"-timestampindex=0".to_string(),
95+
"-blockfilterindex=1".to_string(),
96+
"-peerblockfilters=1".to_string(),
97+
"-printtoconsole".to_string(),
98+
format!("-wallet={}", self.config.wallet),
99+
];
100+
101+
// Try running through bash with explicit ulimit
102+
// Use launchctl to set file descriptor limit if on macOS
103+
let script = if cfg!(target_os = "macos") {
104+
format!(
105+
"launchctl limit maxfiles 10000 unlimited 2>/dev/null || true; ulimit -Sn 10000 2>/dev/null || ulimit -n 10000; exec {} {}",
106+
self.config.dashd_path.display(),
107+
args_vec.join(" ")
108+
)
109+
} else {
110+
format!(
111+
"ulimit -n 10000; exec {} {}",
112+
self.config.dashd_path.display(),
113+
args_vec.join(" ")
114+
)
115+
};
116+
117+
let mut child = Command::new("bash")
118+
.arg("-c")
119+
.arg(&script)
120+
.stdout(std::process::Stdio::piped())
121+
.stderr(std::process::Stdio::piped())
122+
.spawn()?;
123+
124+
// Spawn task to read stderr for debugging
125+
if let Some(stderr) = child.stderr.take() {
126+
tokio::spawn(async move {
127+
let mut reader = BufReader::new(stderr).lines();
128+
while let Ok(Some(line)) = reader.next_line().await {
129+
tracing::debug!("dashd stderr: {}", line);
130+
}
131+
});
132+
}
133+
134+
self.process = Some(child);
135+
136+
// Wait for node to be ready by checking if port is open
137+
tracing::info!("Waiting for dashd to be ready...");
138+
139+
// First check if process died immediately (e.g., due to lock)
140+
tokio::time::sleep(Duration::from_millis(500)).await;
141+
if let Some(ref mut proc) = self.process {
142+
if let Ok(Some(status)) = proc.try_wait() {
143+
return Err(format!("dashd exited immediately with status: {}", status).into());
144+
}
145+
}
146+
147+
let ready = self.wait_for_ready().await?;
148+
if !ready {
149+
// Try to get exit status if process died
150+
if let Some(ref mut proc) = self.process {
151+
if let Ok(Some(status)) = proc.try_wait() {
152+
return Err(format!("dashd exited with status: {}", status).into());
153+
}
154+
}
155+
return Err("dashd failed to start within timeout".into());
156+
}
157+
158+
// Double-check process is still alive after port check
159+
if let Some(ref mut proc) = self.process {
160+
if let Ok(Some(status)) = proc.try_wait() {
161+
return Err(
162+
format!("dashd died after port became ready, status: {}", status).into()
163+
);
164+
}
165+
}
166+
167+
let addr = SocketAddr::from(([127, 0, 0, 1], REGTEST_P2P_PORT));
168+
tracing::info!("✅ dashd started and ready at {}", addr);
169+
170+
Ok(addr)
171+
}
172+
173+
/// Wait for dashd to be ready by checking if P2P port is accepting connections
174+
async fn wait_for_ready(&self) -> Result<bool, Box<dyn std::error::Error>> {
175+
let max_wait = Duration::from_secs(30);
176+
let check_interval = Duration::from_millis(500);
177+
178+
let result = timeout(max_wait, async {
179+
loop {
180+
let addr = SocketAddr::from((Ipv4Addr::new(127, 0, 0, 1), REGTEST_P2P_PORT));
181+
if tokio::net::TcpStream::connect(addr).await.is_ok() {
182+
tracing::debug!("P2P port is accepting connections");
183+
return true;
184+
}
185+
186+
sleep(check_interval).await;
187+
}
188+
})
189+
.await;
190+
191+
Ok(result.unwrap_or(false))
192+
}
193+
194+
/// Stop the Dash Core node
195+
pub async fn stop(&mut self) {
196+
if let Some(mut process) = self.process.take() {
197+
tracing::info!("Stopping dashd...");
198+
199+
// Try graceful shutdown via RPC if possible
200+
// For now, just kill the process
201+
let _ = process.kill();
202+
let _ = process.wait();
203+
204+
tracing::info!("✅ dashd stopped");
205+
}
206+
}
207+
208+
/// Get block count via RPC
209+
pub async fn get_block_count(&self) -> Result<u32, Box<dyn std::error::Error>> {
210+
// This would use RPC to get block count
211+
// For now, we'll use dash-cli
212+
let dash_cli = self
213+
.config
214+
.dashd_path
215+
.parent()
216+
.map(|p| p.join("dash-cli"))
217+
.ok_or("Could not find dash-cli")?;
218+
219+
let output = std::process::Command::new(dash_cli)
220+
.arg("-regtest")
221+
.arg(format!("-datadir={}", self.config.datadir.display()))
222+
.arg(format!("-rpcport={}", REGTEST_RPC_PORT))
223+
.arg("getblockcount")
224+
.output()?;
225+
226+
if !output.status.success() {
227+
return Err(
228+
format!("dash-cli failed: {}", String::from_utf8_lossy(&output.stderr)).into()
229+
);
230+
}
231+
232+
let count_str = String::from_utf8(output.stdout)?;
233+
let count_str = count_str.trim();
234+
if count_str.is_empty() {
235+
return Err("Empty response from getblockcount".into());
236+
}
237+
let count = count_str.parse::<u32>()?;
238+
Ok(count)
239+
}
240+
}
241+
242+
impl Drop for DashCoreNode {
243+
fn drop(&mut self) {
244+
if let Some(mut process) = self.process.take() {
245+
tracing::info!("Stopping dashd process in Drop...");
246+
247+
if let Err(e) = process.start_kill() {
248+
tracing::warn!("Failed to kill dashd process: {}", e);
249+
} else {
250+
tracing::info!("✅ dashd process stopped");
251+
}
252+
}
253+
}
254+
}
255+
256+
/// Check if dashd is available (DASHD_PATH env var set and file exists)
257+
pub fn is_dashd_available() -> bool {
258+
std::env::var("DASHD_PATH").map(|p| PathBuf::from(p).exists()).unwrap_or(false)
259+
}

0 commit comments

Comments
 (0)