Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ members = [

[workspace.dependencies]
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "ea33cbc84179666c25515dfc817ce32210953037" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "ea33cbc84179666c25515dfc817ce32210953037" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "ea33cbc84179666c25515dfc817ce32210953037" }
dash-spv-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "ea33cbc84179666c25515dfc817ce32210953037" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "ea33cbc84179666c25515dfc817ce32210953037" }
Expand Down
80 changes: 2 additions & 78 deletions packages/rs-sdk-ffi/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,84 +380,8 @@ pub unsafe extern "C" fn dash_sdk_create_trusted(config: *const DashSDKConfig) -
info!("dash_sdk_create_trusted: no DAPI addresses provided, using defaults for network");
// Use default addresses for the network
match network {
Network::Testnet => {
// Fixed testnet DAPI addresses (hp-masternodes 1-29)
let default_addresses = [
"https://68.67.122.1:1443",
"https://68.67.122.2:1443",
"https://68.67.122.3:1443",
"https://68.67.122.4:1443",
"https://68.67.122.5:1443",
"https://68.67.122.6:1443",
"https://68.67.122.7:1443",
"https://68.67.122.8:1443",
"https://68.67.122.9:1443",
"https://68.67.122.10:1443",
"https://68.67.122.11:1443",
"https://68.67.122.12:1443",
"https://68.67.122.13:1443",
"https://68.67.122.14:1443",
"https://68.67.122.15:1443",
"https://68.67.122.16:1443",
"https://68.67.122.17:1443",
"https://68.67.122.18:1443",
"https://68.67.122.19:1443",
"https://68.67.122.20:1443",
"https://68.67.122.21:1443",
"https://68.67.122.22:1443",
"https://68.67.122.23:1443",
"https://68.67.122.24:1443",
"https://68.67.122.25:1443",
"https://68.67.122.26:1443",
"https://68.67.122.27:1443",
"https://68.67.122.28:1443",
"https://68.67.122.29:1443",
]
.join(",");

info!(
addresses = default_addresses.as_str(),
"dash_sdk_create_trusted: using default testnet addresses"
);
let address_list = match AddressList::from_str(&default_addresses) {
Ok(list) => list,
Err(e) => {
error!(error = %e, "dash_sdk_create_trusted: failed to parse default addresses");
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InternalError,
format!("Failed to parse default addresses: {}", e),
));
}
};
SdkBuilder::new(address_list).with_network(network)
}
Network::Mainnet => {
// Use mainnet addresses from WASM SDK
let default_addresses = [
"https://149.28.241.190:443",
"https://198.7.115.48:443",
"https://134.255.182.186:443",
"https://93.115.172.39:443",
"https://5.189.164.253:443",
"https://178.215.237.134:443",
"https://157.66.81.162:443",
"https://173.212.232.90:443",
]
.join(",");

info!("dash_sdk_create_trusted: using default mainnet addresses");
let address_list = match AddressList::from_str(&default_addresses) {
Ok(list) => list,
Err(e) => {
error!(error = %e, "dash_sdk_create_trusted: failed to parse default addresses");
return DashSDKResult::error(DashSDKError::new(
DashSDKErrorCode::InternalError,
format!("Failed to parse default addresses: {}", e),
));
}
};
SdkBuilder::new(address_list).with_network(network)
}
Network::Testnet => SdkBuilder::new_testnet(),
Network::Mainnet => SdkBuilder::new_mainnet(),
_ => {
error!(
?network,
Expand Down
1 change: 1 addition & 0 deletions packages/rs-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [
] }
dapi-grpc = { path = "../dapi-grpc", default-features = false }
rs-dapi-client = { path = "../rs-dapi-client", default-features = false }
dash-network-seeds = { workspace = true }
drive = { path = "../rs-drive", default-features = false, features = [
"verify",
] }
Expand Down
126 changes: 118 additions & 8 deletions packages/rs-sdk/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use drive_proof_verifier::FromProof;
pub use http::Uri;
#[cfg(feature = "mocks")]
use rs_dapi_client::mock::MockDapiClient;
use rs_dapi_client::Address;
pub use rs_dapi_client::AddressList;
pub use rs_dapi_client::RequestSettings;
use rs_dapi_client::{
Expand Down Expand Up @@ -63,6 +64,41 @@ const DEFAULT_REQUEST_SETTINGS: RequestSettings = RequestSettings {
max_decoding_message_size: None,
};

/// Build the default DAPI bootstrap address list for `network` from
/// [`dash_network_seeds`].
///
/// The seed lists are single-source-of-truth, weekly-refreshed upstream in
/// `rust-dashcore`. We filter to Evo (HPMN) masternodes — the only ones that
/// run Dash Platform — and build `https://<ip>:<platform_http_port>` URIs.
/// The Core port on `seed.address` is intentionally discarded: DAPI clients
/// need the platform HTTP port, not the Core P2P port.
///
/// Malformed upstream entries are silently skipped rather than panicking;
/// the DAPI client handles retry/rotation across the remaining addresses.
///
/// ## Panics
///
/// Panics on networks other than `Mainnet` and `Testnet` — no upstream
/// seed list exists for devnet/regtest.
fn default_address_list_for_network(network: Network) -> AddressList {
if !matches!(network, Network::Mainnet | Network::Testnet) {
panic!("default address list is only available for mainnet and testnet");
}
let mut list = AddressList::new();
for seed in dash_network_seeds::evo_seeds(network) {
let Some(port) = seed.platform_http_port else {
continue;
};
let url = format!("https://{}:{}", seed.address.ip(), port);
if let Ok(uri) = url.parse::<Uri>() {
if let Ok(address) = Address::try_from(uri) {
list.add(address);
}
}
}
list
}
Comment on lines +83 to +100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find packages/rs-sdk -name "*.rs" -type f | head -20

Repository: dashpay/platform

Length of output: 896


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | sed -n '75,110p'

Repository: dashpay/platform

Length of output: 1622


🏁 Script executed:

cat packages/rs-sdk/Cargo.toml | grep -A 5 -B 5 "dash_network_seeds"

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

rg "struct.*Seed|fn evo_seeds" packages/rs-sdk -A 5 -B 2

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

rg "seed\.address" packages/rs-sdk -B 3 -A 3

Repository: dashpay/platform

Length of output: 1180


🏁 Script executed:

find . -name "dash_network_seeds" -o -name "*dash*network*" -type d | head -10

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

rg "platform_http_port|address\.ip()" packages/rs-sdk -B 5 -A 3

Repository: dashpay/platform

Length of output: 1644


🏁 Script executed:

rg "default_address_list_for_network|new_mainnet|new_testnet" packages/rs-sdk -B 2 -A 5

Repository: dashpay/platform

Length of output: 4253


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | head -120

Repository: dashpay/platform

Length of output: 5481


🏁 Script executed:

rg "use.*Uri|use.*Address" packages/rs-sdk/src/sdk.rs

Repository: dashpay/platform

Length of output: 145


🏁 Script executed:

rg "dash_network_seeds" packages/rs-sdk -B 2 -A 2

Repository: dashpay/platform

Length of output: 691


🌐 Web query:

RFC 3986 IPv6 address URI format brackets requirement

💡 Result:

Yes, RFC 3986 requires square brackets around IPv6 literal addresses in URIs. The specification defines the host component syntax such that an IPv6 address must be enclosed in square brackets to distinguish it from other host formats and resolve colon ambiguities (e.g., with ports). This is the only place in URI syntax where square brackets are permitted. Key syntax from RFC 3986: IP-literal = "[" ( IPv6address / IPvFuture ) "]" host = IP-literal / IPv4address / reg-name Without brackets, an IPv6 address like 2001:db8::1 cannot be unambiguously parsed in a URI like http://2001:db8::1:8080/, as the parser cannot distinguish the address from a port. Correct form: http://[2001:db8::1]:8080/. This requirement originates from RFC 2732 (which introduced brackets for URLs) and is formalized in RFC 3986 Section 3.2.2. Brackets are mandatory; omitting them produces invalid URI syntax per the ABNF grammar.

Citations:


🏁 Script executed:

cat packages/rs-sdk/Cargo.toml | grep -A 2 "dash.network.seeds"

Repository: dashpay/platform

Length of output: 185


🏁 Script executed:

git ls-files | grep -i "dash.*network.*seed" | head -10

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

find . -name "dash_network_seeds" -o -name "dash-network-seeds" -type d 2>/dev/null

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

grep -r "dash_network_seeds" Cargo.toml | head -5

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | sed -n '400,450p'

Repository: dashpay/platform

Length of output: 2167


🏁 Script executed:

rg "bootstrap_counts_reasonable" packages/rs-sdk -B 2 -A 10

Repository: dashpay/platform

Length of output: 970


🏁 Script executed:

find . -path "*/dash-network-seeds" -o -path "*/dash_network_seeds" -type d 2>/dev/null

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

cat Cargo.toml | grep -A 20 "\[workspace\]"

Repository: dashpay/platform

Length of output: 688


🏁 Script executed:

rg "^name.*dash.network.seeds" | head -5

Repository: dashpay/platform

Length of output: 99


🏁 Script executed:

find . -name "Cargo.toml" -type f -exec grep -l "dash-network-seeds\|dash_network_seeds" {} \; | head -10

Repository: dashpay/platform

Length of output: 102


🏁 Script executed:

rg "dash-network-seeds" Cargo.lock -A 10 | head -30

Repository: dashpay/platform

Length of output: 657


🏁 Script executed:

cat packages/rs-sdk/Cargo.toml | head -50

Repository: dashpay/platform

Length of output: 2120


🏁 Script executed:

python3 << 'EOF'
# Check if dash-network-seeds is a crate.io dependency
# by examining Cargo.lock
with open('Cargo.lock', 'r') as f:
    in_dash_seeds = False
    for i, line in enumerate(f):
        if 'dash-network-seeds' in line and 'name =' in line:
            in_dash_seeds = True
            for j in range(20):
                if i+j < len(f.readlines()):
                    pass
        if in_dash_seeds:
            print(line.rstrip())
            if line.strip() == '':
                break
EOF

Repository: dashpay/platform

Length of output: 88


🏁 Script executed:

sed -n '/\[\[package\]\]/,/^$/p' Cargo.lock | grep -A 10 'name = "dash-network-seeds"' | head -20

Repository: dashpay/platform

Length of output: 378


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | sed -n '478,510p'

Repository: dashpay/platform

Length of output: 1749


🏁 Script executed:

rg "bootstrap_counts_reasonable" packages/rs-sdk -A 20

Repository: dashpay/platform

Length of output: 1382


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | sed -n '67,100p'

Repository: dashpay/platform

Length of output: 1709


🏁 Script executed:

rg "ipv6|IPv6|v6" packages/rs-sdk -i

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

cat -n packages/rs-sdk/src/sdk.rs | sed -n '67,82p'

Repository: dashpay/platform

Length of output: 925


🌐 Web query:

http crate Rust Uri IPv6 parsing bracket requirement

💡 Result:

In the Rust http crate (used by Hyper, Reqwest, Axum), the Uri type requires IPv6 literal addresses in URIs to be enclosed in square brackets [] for parsing to succeed, per RFC 3986 and RFC 2732 standards for URI hosts. Example: "http://[::1]/".parse::http::Uri succeeds, while "http://::1/" fails with InvalidUri. Once parsed, uri.host returns the host with brackets for IPv6 literals, e.g., "[::1]" (fixed in 2019 via PR #292). This applies to HTTP request targets and is standard across the ecosystem. For socket connections (e.g., TcpListener::bind), use "[::]:8080"; libraries like hyper-rustls strip brackets internally for TLS. Reqwest accepts url::Url inputs, which also enforce brackets in string serialization (host_str includes them), ensuring compatibility with http::Uri.

Citations:


🏁 Script executed:

git log --oneline packages/rs-sdk/src/sdk.rs 2>/dev/null | head -5

Repository: dashpay/platform

Length of output: 163


🏁 Script executed:

rg "IpAddr::V6|IpAddr::V4" packages/rs-sdk -B 2 -A 2

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

rg "Address.*try_from" packages/rs-sdk -B 3 -A 5

Repository: dashpay/platform

Length of output: 16033


Handle IPv6 addresses in bootstrap URIs.

Per RFC 3986, IPv6 literal addresses in URIs must be enclosed in square brackets. When seed.address.ip() returns IpAddr::V6, the current code produces invalid URIs (e.g., https://2001:db8::1:8080) that fail to parse and are silently dropped, discarding valid IPv6 bootstrap entries.

The suggested assertion on empty lists contradicts the documented design: the function explicitly states "Malformed upstream entries are silently skipped rather than panicking; the DAPI client handles retry/rotation across the remaining addresses." Fail-fast validation should be deferred to the caller if stricter semantics are desired.

🔧 Suggested fix (IPv6 only)
 fn default_address_list_for_network(network: Network) -> AddressList {
     if !matches!(network, Network::Mainnet | Network::Testnet) {
         panic!("default address list is only available for mainnet and testnet");
     }
     let mut list = AddressList::new();
     for seed in dash_network_seeds::evo_seeds(network) {
         let Some(port) = seed.platform_http_port else {
             continue;
         };
-        let url = format!("https://{}:{}", seed.address.ip(), port);
+        let url = match seed.address.ip() {
+            std::net::IpAddr::V4(ip) => format!("https://{}:{}", ip, port),
+            std::net::IpAddr::V6(ip) => format!("https://[{}]:{}", ip, port),
+        };
         if let Ok(uri) = url.parse::<Uri>() {
             if let Ok(address) = Address::try_from(uri) {
                 list.add(address);
             }
         }
     }
     list
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn default_address_list_for_network(network: Network) -> AddressList {
if !matches!(network, Network::Mainnet | Network::Testnet) {
panic!("default address list is only available for mainnet and testnet");
}
let mut list = AddressList::new();
for seed in dash_network_seeds::evo_seeds(network) {
let Some(port) = seed.platform_http_port else {
continue;
};
let url = format!("https://{}:{}", seed.address.ip(), port);
if let Ok(uri) = url.parse::<Uri>() {
if let Ok(address) = Address::try_from(uri) {
list.add(address);
}
}
}
list
}
fn default_address_list_for_network(network: Network) -> AddressList {
if !matches!(network, Network::Mainnet | Network::Testnet) {
panic!("default address list is only available for mainnet and testnet");
}
let mut list = AddressList::new();
for seed in dash_network_seeds::evo_seeds(network) {
let Some(port) = seed.platform_http_port else {
continue;
};
let url = match seed.address.ip() {
std::net::IpAddr::V4(ip) => format!("https://{}:{}", ip, port),
std::net::IpAddr::V6(ip) => format!("https://[{}]:{}", ip, port),
};
if let Ok(uri) = url.parse::<Uri>() {
if let Ok(address) = Address::try_from(uri) {
list.add(address);
}
}
}
list
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk/src/sdk.rs` around lines 83 - 100, The function
default_address_list_for_network is producing invalid URIs for IPv6 because it
formats the host without brackets; update the URL construction in
default_address_list_for_network to detect when seed.address.ip() yields an
IpAddr::V6 and wrap the IP literal in square brackets (e.g., "[...]" around the
ip string) before appending the port so the Uri::parse and Address::try_from can
succeed; keep the current behavior of silently skipping malformed entries (do
not add assertions or panics) and only modify the host formatting logic that
uses seed.address.ip() and the Uri/Address creation paths.


/// Dash Platform SDK
///
/// This is the main entry point for interacting with Dash Platform.
Expand Down Expand Up @@ -747,18 +783,18 @@ impl SdkBuilder {
Self::default()
}

/// Create a new SdkBuilder instance preconfigured for testnet. NOT IMPLEMENTED YET.
/// Create a new SdkBuilder instance preconfigured for testnet.
///
/// This is a helper method that preconfigures [SdkBuilder] for testnet use.
/// Use this method if you want to connect to Dash Platform testnet during development and testing
/// of your solution.
pub fn new_testnet() -> Self {
unimplemented!(
"Testnet address list not implemented yet. Use new() and provide address list."
)
let address_list = default_address_list_for_network(Network::Testnet);

Self::new(address_list).with_network(Network::Testnet)
}

/// Create a new SdkBuilder instance preconfigured for mainnet (production network). NOT IMPLEMENTED YET.
/// Create a new SdkBuilder instance preconfigured for mainnet (production network).
///
/// This is a helper method that preconfigures [SdkBuilder] for production use.
/// Use this method if you want to connect to Dash Platform mainnet with production-ready product.
Expand All @@ -771,9 +807,9 @@ impl SdkBuilder {
///
/// This method is unstable and can be changed in the future.
pub fn new_mainnet() -> Self {
unimplemented!(
"Mainnet address list not implemented yet. Use new() and provide address list."
)
let address_list = default_address_list_for_network(Network::Mainnet);

Self::new(address_list).with_network(Network::Mainnet)
}

/// Configure network type.
Expand Down Expand Up @@ -1102,6 +1138,80 @@ mod test {

use crate::SdkBuilder;

use super::Network;

/// Mainnet Evo masternodes expose the Platform HTTP endpoint on 443.
const MAINNET_PLATFORM_HTTP_PORT: u16 = 443;
/// Testnet Evo masternodes expose the Platform HTTP endpoint on 1443.
const TESTNET_PLATFORM_HTTP_PORT: u16 = 1443;

#[test]
fn new_testnet_sources_bootstrap_from_seeds() {
let builder = SdkBuilder::new_testnet();
let address_list = builder
.addresses
.as_ref()
.expect("testnet builder should configure default addresses");

assert_eq!(builder.network, Network::Testnet);
assert!(
!address_list.is_empty(),
"testnet must have at least one bootstrap address"
);
for address in address_list.get_live_addresses() {
assert_eq!(
address.uri().port_u16(),
Some(TESTNET_PLATFORM_HTTP_PORT),
"testnet bootstrap address must use the platform HTTP port",
);
}
}

#[test]
fn new_mainnet_sources_bootstrap_from_seeds() {
let builder = SdkBuilder::new_mainnet();
let address_list = builder
.addresses
.as_ref()
.expect("mainnet builder should configure default addresses");

assert_eq!(builder.network, Network::Mainnet);
assert!(
!address_list.is_empty(),
"mainnet must have at least one bootstrap address"
);
for address in address_list.get_live_addresses() {
assert_eq!(
address.uri().port_u16(),
Some(MAINNET_PLATFORM_HTTP_PORT),
"mainnet bootstrap address must use the platform HTTP port",
);
}
}

/// Smoke signal: the upstream seed lists are far larger than 10 entries on
/// both networks. If parsing drops most of them we want a loud test
/// failure rather than silently shipping a near-empty bootstrap list.
#[test]
fn bootstrap_counts_reasonable() {
let mainnet = SdkBuilder::new_mainnet()
.addresses
.expect("mainnet builder should configure default addresses");
let testnet = SdkBuilder::new_testnet()
.addresses
.expect("testnet builder should configure default addresses");
assert!(
mainnet.len() >= 10,
"expected >=10 mainnet bootstrap addresses, got {}",
mainnet.len()
);
assert!(
testnet.len() >= 10,
"expected >=10 testnet bootstrap addresses, got {}",
testnet.len()
);
}

#[test_matrix(97..102, 100, 2, false; "valid height")]
#[test_case(103, 100, 2, true; "invalid height")]
fn test_verify_metadata_height(
Expand Down
34 changes: 3 additions & 31 deletions packages/wasm-sdk/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,7 @@ fn parse_addresses(addresses: &'static [&str]) -> Vec<Address> {
})
.collect()
}
// Mainnet addresses from mnowatch.org
fn default_mainnet_addresses() -> Vec<Address> {
parse_addresses(&[
"https://149.28.241.190:443",
"https://198.7.115.48:443",
"https://134.255.182.186:443",
"https://93.115.172.39:443",
"https://5.189.164.253:443",
])
}
// Testnet addresses from https://quorums.testnet.networks.dash.org/masternodes
fn default_testnet_addresses() -> Vec<Address> {
parse_addresses(&[
"https://52.12.176.90:1443",
"https://35.82.197.197:1443",
"https://44.240.98.102:1443",
"https://52.34.144.50:1443",
"https://44.239.39.153:1443",
"https://34.214.48.68:1443",
"https://54.149.33.167:1443",
"https://52.24.124.162:1443",
])
}

fn default_local_addresses() -> Vec<Address> {
parse_addresses(&["https://127.0.0.1:2443"])
}
Expand Down Expand Up @@ -249,10 +227,7 @@ impl WasmSdkBuilder {

#[wasm_bindgen(js_name = "mainnet")]
pub fn new_mainnet() -> Self {
let address_list = dash_sdk::sdk::AddressList::from_iter(default_mainnet_addresses());
let sdk_builder = SdkBuilder::new(address_list)
.with_network(dash_sdk::dpp::dashcore::Network::Mainnet)
.with_context_provider(WasmContext {});
let sdk_builder = SdkBuilder::new_mainnet().with_context_provider(WasmContext {});

Self {
inner: sdk_builder,
Expand All @@ -262,10 +237,7 @@ impl WasmSdkBuilder {

#[wasm_bindgen(js_name = "testnet")]
pub fn new_testnet() -> Self {
let address_list = dash_sdk::sdk::AddressList::from_iter(default_testnet_addresses());
let sdk_builder = SdkBuilder::new(address_list)
.with_network(dash_sdk::dpp::dashcore::Network::Testnet)
.with_context_provider(WasmContext {});
let sdk_builder = SdkBuilder::new_testnet().with_context_provider(WasmContext {});

Self {
inner: sdk_builder,
Expand Down
Loading