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
111 changes: 111 additions & 0 deletions packages/rs-drive-abci/src/abci/handler/check_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,114 @@ where
})
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::core::MockCoreRPCLike;
use crate::test::helpers::setup::TestPlatformBuilder;

/// Exercises the early-return path in `check_tx` where `r#type.try_into()?`
/// propagates a `BadRequest` error before the validation result pipeline is
/// reached. Unlike the `.or_else` branch which converts errors into responses,
/// this error path propagates out of the handler entirely.
#[test]
fn check_tx_invalid_check_tx_type_propagates_bad_request_error() {
let platform = TestPlatformBuilder::new()
.with_latest_protocol_version()
.build_with_mock_rpc()
.set_initial_state_structure();
let core_rpc = MockCoreRPCLike::new();

let request = proto::RequestCheckTx {
tx: vec![1, 2, 3],
// Only 0 (New) and 1 (Recheck) are valid. 2 is rejected by TryFrom.
r#type: 2,
};

let result = check_tx(&platform.platform, &core_rpc, request);

assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("CheckTxLevel") || err_str.contains("2"),
"expected BadRequest about CheckTxLevel, got: {}",
err_str
);
}

/// Exercises the error-path of the main `and_then` branch where the
/// `check_tx_v0` code produces a `ValidationResult` with a consensus error
/// (`InvalidEncoding` from `decode_raw_state_transitions`) for garbage bytes.
/// Covers the code/info propagation via `response_info_for_version`.
#[test]
fn check_tx_garbage_bytes_returns_nonzero_consensus_code() {
let platform = TestPlatformBuilder::new()
.with_latest_protocol_version()
.build_with_mock_rpc()
.set_initial_state_structure();
let core_rpc = MockCoreRPCLike::new();

// Random garbage is guaranteed to fail state transition deserialization.
let request = proto::RequestCheckTx {
tx: vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA],
r#type: 0,
};

let response = check_tx(&platform.platform, &core_rpc, request)
.expect("handler should return Ok with a consensus error code in the response");

// Rejected, non-zero code and non-empty info (base64-encoded consensus info).
assert_ne!(response.code, 0);
assert!(
!response.info.is_empty(),
"expected non-empty response info for consensus error"
);
assert_eq!(response.gas_wanted, 0);
}

/// Recheck mode (type = 1) should take the same code paths as new, but with
/// the different label; here we confirm the handler also returns a response
/// (not an error) for garbage bytes in Recheck mode.
#[test]
fn check_tx_garbage_bytes_recheck_returns_response() {
let platform = TestPlatformBuilder::new()
.with_latest_protocol_version()
.build_with_mock_rpc()
.set_initial_state_structure();
let core_rpc = MockCoreRPCLike::new();

let request = proto::RequestCheckTx {
tx: vec![0x00, 0x01, 0x02, 0x03],
r#type: 1, // Recheck
};

let response = check_tx(&platform.platform, &core_rpc, request)
.expect("recheck with garbage bytes should not produce an Err");

assert_ne!(response.code, 0);
}

/// An empty body is a boundary case: depending on decoding, it produces an
/// `InvalidEncoding` error. The handler should still return Ok with a
/// rejection code - not propagate an error.
#[test]
fn check_tx_empty_tx_body_returns_rejection_code() {
let platform = TestPlatformBuilder::new()
.with_latest_protocol_version()
.build_with_mock_rpc()
.set_initial_state_structure();
let core_rpc = MockCoreRPCLike::new();

let request = proto::RequestCheckTx {
tx: vec![],
r#type: 0,
};

let response = check_tx(&platform.platform, &core_rpc, request)
.expect("empty tx should not propagate an Err");

assert_ne!(response.code, 0);
}
}
265 changes: 265 additions & 0 deletions packages/rs-drive-abci/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,10 @@ impl Default for PlatformTestConfig {
#[cfg(test)]
mod tests {
use super::FromEnv;
use crate::config::{
ChainLockConfig, ConsensusCoreRpcConfig, CoreConfig, InstantLockConfig, PlatformConfig,
QuorumLikeConfig, ValidatorSetConfig,
};
use crate::logging::LogDestination;
use dpp::dashcore::Network;
use dpp::dashcore_rpc::dashcore_rpc_json::QuorumType;
Expand Down Expand Up @@ -969,4 +973,265 @@ mod tests {
assert_eq!(config.network, config.drive.network);
assert_eq!(config.network, Network::Testnet);
}

// --- `from_str_to_network_with_aliases`: valid aliases and error path ---

/// Ensures every network alias accepted by the deserializer maps to the
/// expected `Network`, case-insensitively. This exercises each match arm in
/// `from_str_to_network_with_aliases`, which is otherwise only called at
/// most once per integration-test setup.
#[test]
fn network_aliases_deserialize_all_variants_case_insensitively() {
let cases = &[
("\"dash\"", Network::Mainnet),
("\"DASH\"", Network::Mainnet),
("\"MainNet\"", Network::Mainnet),
("\"main\"", Network::Mainnet),
("\"local\"", Network::Regtest),
("\"regtest\"", Network::Regtest),
("\"testnet\"", Network::Testnet),
("\"TEST\"", Network::Testnet),
("\"devnet\"", Network::Devnet),
("\"Dev\"", Network::Devnet),
];

for (input, expected) in cases {
let mut de = serde_json::Deserializer::from_str(input);
let network = super::from_str_to_network_with_aliases(&mut de)
.expect("alias should parse successfully");
assert_eq!(
&network, expected,
"input {} should map to {:?}",
input, expected
);
}
}

/// Verifies that an unknown network alias produces a clear deserializer error.
/// This covers the final arm of `from_str_to_network_with_aliases`.
#[test]
fn network_alias_unknown_value_returns_custom_error() {
let mut de = serde_json::Deserializer::from_str("\"nonsense_network\"");
let err = super::from_str_to_network_with_aliases(&mut de)
.expect_err("unknown network should fail");
let msg = err.to_string();
assert!(
msg.contains("unknown network") && msg.contains("nonsense_network"),
"error message should name the offending value, got: {}",
msg
);
}

// --- `deserialize_quorum_type`: numeric/string/UNKNOWN error path ---

/// Covers the string-name branch of `deserialize_quorum_type`.
#[test]
fn deserialize_quorum_type_accepts_string_names() {
// All numeric fields use `from_str_or_number`, which requires a JSON
// string (even for numbers).
let json = r#"{
"validator_set_quorum_type": "llmq_25_67",
"validator_set_quorum_size": "25",
"validator_set_quorum_window": "24",
"validator_set_quorum_active_signers": "24",
"validator_set_quorum_rotation": "false"
}"#;
let cfg: ValidatorSetConfig =
serde_json::from_str(json).expect("quorum type string should deserialize");
assert_eq!(cfg.quorum_type, QuorumType::Llmq25_67);
}

/// Covers the numeric (u32) branch of `deserialize_quorum_type` (a number
/// serialized as a JSON string still goes through the `parse::<u32>()` path).
#[test]
fn deserialize_quorum_type_accepts_numeric_string() {
let json = r#"{
"validator_set_quorum_type": "6",
"validator_set_quorum_size": "25",
"validator_set_quorum_window": "24",
"validator_set_quorum_active_signers": "24",
"validator_set_quorum_rotation": "false"
}"#;
let cfg: ValidatorSetConfig =
serde_json::from_str(json).expect("numeric quorum type should deserialize");
// QuorumType::from(6) should be a known variant; we only assert it is not UNKNOWN.
assert_ne!(cfg.quorum_type, QuorumType::UNKNOWN);
}

/// Covers the `UNKNOWN` rejection branch of `deserialize_quorum_type`.
#[test]
fn deserialize_quorum_type_rejects_unknown_names() {
let json = r#"{
"validator_set_quorum_type": "this_is_not_a_quorum",
"validator_set_quorum_size": "25",
"validator_set_quorum_window": "24",
"validator_set_quorum_active_signers": "24",
"validator_set_quorum_rotation": "false"
}"#;
let err = serde_json::from_str::<ValidatorSetConfig>(json)
.expect_err("unknown quorum type name should fail");
let msg = err.to_string();
assert!(
msg.contains("unsupported") && msg.contains("QUORUM_TYPE"),
"expected unsupported QUORUM_TYPE error, got: {}",
msg
);
}

// --- Core RPC URL composition and defaults ---

/// Exercises `ConsensusCoreRpcConfig::url` formatting.
#[test]
fn consensus_core_rpc_url_composes_host_and_port() {
let cfg = ConsensusCoreRpcConfig {
host: "node.example".to_string(),
port: 9998,
username: "u".to_string(),
password: "p".to_string(),
};
assert_eq!(cfg.url(), "node.example:9998");
}

/// Exercises `CheckTxCoreRpcConfig::url` formatting.
#[test]
fn check_tx_core_rpc_url_composes_host_and_port() {
let cfg = super::CheckTxCoreRpcConfig {
host: "127.0.0.1".to_string(),
port: 1,
username: String::new(),
password: String::new(),
};
assert_eq!(cfg.url(), "127.0.0.1:1");
}

/// `CoreConfig::default()` should be empty strings and zero port (covers
/// the auto-derived `Default` with both flattened members).
#[test]
fn core_config_default_is_empty() {
let cfg = CoreConfig::default();
assert_eq!(cfg.consensus_rpc.host, "");
assert_eq!(cfg.consensus_rpc.port, 0);
assert_eq!(cfg.check_tx_rpc.host, "");
assert_eq!(cfg.check_tx_rpc.port, 0);
}

// --- `default_for_network` dispatch, including the catch-all ---

/// Exercises the `Network::Mainnet`, `Network::Testnet`, `Network::Devnet`,
/// `Network::Regtest` arms of `default_for_network`.
#[test]
fn default_for_network_dispatches_all_known_networks() {
let mainnet = PlatformConfig::default_for_network(Network::Mainnet);
assert_eq!(mainnet.network, Network::Mainnet);
assert_eq!(mainnet.validator_set.quorum_type, QuorumType::Llmq100_67);

let testnet = PlatformConfig::default_for_network(Network::Testnet);
assert_eq!(testnet.network, Network::Testnet);
assert_eq!(testnet.validator_set.quorum_type, QuorumType::Llmq25_67);

let devnet = PlatformConfig::default_for_network(Network::Devnet);
// default_devnet currently sets network to Regtest (see impl).
assert_eq!(devnet.network, Network::Regtest);

let regtest = PlatformConfig::default_for_network(Network::Regtest);
assert_eq!(regtest.network, Network::Regtest);
}

// --- `ValidatorSetConfig::default_100_67` and QuorumLikeConfig getters ---

/// Exercises `default_100_67` + every getter in `QuorumLikeConfig` for
/// `ValidatorSetConfig` - these getters are pure accessors that would
/// otherwise be uncovered outside the actual consensus path.
#[test]
fn validator_set_default_100_67_accessors() {
let cfg = ValidatorSetConfig::default_100_67();
assert_eq!(cfg.quorum_type(), QuorumType::Llmq100_67);
assert_eq!(cfg.quorum_size(), 100);
assert_eq!(cfg.quorum_window(), 24);
assert_eq!(cfg.quorum_active_signers(), 24);
assert!(!cfg.quorum_rotation());
}

/// Same but for `ChainLockConfig::default_100_67`.
#[test]
fn chain_lock_default_100_67_accessors() {
let cfg = ChainLockConfig::default_100_67();
assert_eq!(cfg.quorum_type(), QuorumType::Llmq100_67);
assert_eq!(cfg.quorum_size(), 100);
assert_eq!(cfg.quorum_window(), 24);
assert_eq!(cfg.quorum_active_signers(), 24);
assert!(!cfg.quorum_rotation());
}

/// `ChainLockConfig::default()` is Mainnet LLMQ400_60 - cover all getters.
#[test]
fn chain_lock_default_is_llmq_400_60() {
let cfg = ChainLockConfig::default();
assert_eq!(cfg.quorum_type(), QuorumType::Llmq400_60);
assert_eq!(cfg.quorum_size(), 400);
assert_eq!(cfg.quorum_window(), 24 * 12);
assert_eq!(cfg.quorum_active_signers(), 4);
assert!(!cfg.quorum_rotation());
}

/// `InstantLockConfig::default()` is DIP24 rotated LLMQ60_75.
#[test]
fn instant_lock_default_is_llmq_60_75_rotated() {
let cfg = InstantLockConfig::default();
assert_eq!(cfg.quorum_type(), QuorumType::Llmq60_75);
assert_eq!(cfg.quorum_size(), 60);
assert_eq!(cfg.quorum_window(), 24 * 12);
assert_eq!(cfg.quorum_active_signers(), 32);
assert!(cfg.quorum_rotation());
}

/// `InstantLockConfig::default_100_67` is the classic LLMQ variant.
#[test]
fn instant_lock_default_100_67_accessors() {
let cfg = InstantLockConfig::default_100_67();
assert_eq!(cfg.quorum_type(), QuorumType::Llmq100_67);
assert_eq!(cfg.quorum_size(), 100);
assert_eq!(cfg.quorum_window(), 24);
assert_eq!(cfg.quorum_active_signers(), 24);
assert!(!cfg.quorum_rotation());
}

/// Exercises `PlatformConfig::default()` which delegates to `default_mainnet`.
#[test]
fn platform_config_default_is_mainnet() {
let cfg = PlatformConfig::default();
assert_eq!(cfg.network, Network::Mainnet);
assert_eq!(cfg.validator_set.quorum_type, QuorumType::Llmq100_67);
assert_eq!(cfg.chain_lock.quorum_type, QuorumType::Llmq400_60);
assert_eq!(cfg.instant_lock.quorum_type, QuorumType::Llmq60_75);
assert_eq!(cfg.block_spacing_ms, 5000);
}

// --- `ExecutionConfig` default values ---

/// Exercises all four default-provider functions on `ExecutionConfig`.
#[test]
fn execution_config_defaults() {
let cfg = super::ExecutionConfig::default();
assert!(cfg.verify_sum_trees);
assert!(cfg.verify_token_sum_trees);
assert!(cfg.use_document_triggers);
assert_eq!(cfg.epoch_time_length_s, 788400);
}

// --- `serialize_quorum_type` round-trip via Serialize ---

/// Covers the `serialize_quorum_type` codepath (symmetric pair of the
/// deserialize tests above).
#[test]
fn validator_set_config_serializes_quorum_type_as_string() {
let cfg = ValidatorSetConfig::default_100_67();
let json = serde_json::to_string(&cfg).expect("valid serialize");
// The serialized representation must use the textual form.
assert!(
json.contains("llmq_100_67") || json.contains("Llmq100_67"),
"expected textual quorum type in JSON, got: {}",
json
);
}
}
Loading
Loading