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
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,311 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::core::MockCoreRPCLike;
use crate::test::helpers::chain_lock_quorums::{
chain_lock_test_platform, sign_chain_lock, QuorumMinedAfterCoreHeight, TestQuorum,
};
use crate::test::helpers::setup::TestPlatformBuilder;
use dpp::dashcore::hashes::Hash;
use dpp::dashcore::{BlockHash, ChainLock};
use dpp::version::PlatformVersion;
use dpp::dashcore_rpc::Error as DashCoreRpcError;
use dpp::version::{PlatformVersion, PLATFORM_VERSIONS};

#[test]
fn should_accept_through_core_chain_lock_signed_by_quorum_mined_after_last_committed_core_height(
) {
// Core mined quorum n at 1114 and signed the proposed chain lock with it. Platform does
// not hold n, so it hands the chain lock to Core, which verifies its signature and
// already has it.
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let chain_lock = scenario.chain_lock_signed_by_new_quorum();
let chain_lock_height = chain_lock.block_height;

let mut core_rpc = MockCoreRPCLike::new();
let asked = chain_lock.clone();
core_rpc
.expect_verify_chain_lock()
.withf(move |chain_lock| *chain_lock == asked)
.times(1)
.returning(|_| Ok(true));
core_rpc
.expect_submit_chain_lock()
.times(1)
.returning(move |_| Ok(chain_lock_height));
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock_v0(0, &state, &chain_lock, true, PlatformVersion::latest())
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(result.found_valid_locally, None);
assert_eq!(result.found_valid_by_core, None);
assert_eq!(result.core_is_synced, Some(true));
}

#[test]
fn should_reject_forged_chain_lock_without_asking_core_when_signing_height_quorums_are_known() {
// The signing height is within what Platform holds the exact quorums for, so a chain
// lock none of them signed is rejected locally; Core's answer, which skips the
// signature check for heights it already has a chain lock for, is never asked.
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let chain_lock = sign_chain_lock(&TestQuorum::new(9, 0), 1113 + 8);

let mut core_rpc = MockCoreRPCLike::new();
core_rpc.expect_verify_chain_lock().times(0);
core_rpc.expect_submit_chain_lock().times(0);
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock_v0(0, &state, &chain_lock, true, PlatformVersion::latest())
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(result.found_valid_locally, Some(false));
assert_eq!(result.found_valid_by_core, None);
assert_eq!(result.core_is_synced, None);
}

#[test]
fn should_reject_forged_chain_lock_left_to_core_when_core_already_has_a_chain_lock_at_its_height(
) {
// Platform cannot check these chain locks itself: at 1122 a quorum mined at 1114 may
// have signed it, at 1136 the signing height is past the DKG interval of Platform's core
// height 1106. Core already has a chain lock at 1140, so submitchainlock returns that
// height without checking the signature; verifychainlock does check it and fails it.
// The rejection is Core's verdict, so the block proposal reports the chain lock as
// invalid based on a core request, not on platform state.
const CORE_BEST_CHAIN_LOCK_HEIGHT: u32 = 1140;
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());

for chain_lock_height in [1114 + 8, 1128 + 8] {
let forged = sign_chain_lock(&TestQuorum::new(9, 0), chain_lock_height);

let mut core_rpc = MockCoreRPCLike::new();
let asked = forged.clone();
core_rpc
.expect_verify_chain_lock()
.withf(move |chain_lock| *chain_lock == asked)
.times(1)
.returning(|_| Ok(false));
core_rpc
.expect_submit_chain_lock()
.times(0)
.returning(|_| Ok(CORE_BEST_CHAIN_LOCK_HEIGHT));
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock_v0(0, &state, &forged, true, PlatformVersion::latest())
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(
result.found_valid_by_core,
Some(false),
"a forged chain lock at {chain_lock_height} must be rejected by Core's verdict, \
not accepted through Core's early return (core is synced: {:?})",
result.core_is_synced
);
assert_eq!(
result.found_valid_locally, None,
"Platform could not check the chain lock against the quorums it holds"
);
assert_eq!(
result.core_is_synced, None,
"submitchainlock must not be asked"
);
}
}

/// Whether the block proposal accepts a chain lock with this verification result, following
/// the checks `run_block_proposal` makes on it
fn accepted_by_block_proposal(result: &VerifyChainLockResult) -> bool {
result.chain_lock_signature_is_deserializable
&& result.found_valid_locally != Some(false)
&& result.found_valid_by_core != Some(false)
&& result.core_is_synced != Some(false)
}

#[test]
fn should_never_accept_forged_chain_lock_signed_where_a_new_quorum_may_have_signed() {
// Protocol versions up to 14 reject this forged chain lock locally, because Platform
// holds the quorums of signing heights up to 1113 and assumes they hold up to the
// interval end. From 15 Platform leaves it to Core, since a quorum mined at 1114 may
// have signed it, so a protocol version must not leave chain locks to Core without
// having verifychainlock check them: Core, already holding a chain lock at 1140, would
// answer submitchainlock with that height without checking the signature.
const CORE_BEST_CHAIN_LOCK_HEIGHT: u32 = 1140;
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let forged = sign_chain_lock(&TestQuorum::new(9, 0), 1114 + 8);

let mut core_rpc = MockCoreRPCLike::new();
core_rpc.expect_verify_chain_lock().returning(|_| Ok(false));
core_rpc
.expect_submit_chain_lock()
.returning(|_| Ok(CORE_BEST_CHAIN_LOCK_HEIGHT));
platform.core_rpc = core_rpc;

for platform_version in PLATFORM_VERSIONS {
let result = platform
.platform
.verify_chain_lock(0, &state, &forged, true, platform_version)
.expect("verification runs");

assert!(
!accepted_by_block_proposal(&result),
"protocol version {} accepted a forged chain lock",
platform_version.protocol_version
);
}
}

#[test]
fn should_not_accept_chain_lock_left_to_core_that_core_is_not_synced_to() {
// Core's verifychainlock accepts the chain lock signed by the quorum mined at 1114, but
// Core's best chain lock is 10 blocks behind it, so submitchainlock does not make Core
// synced to it.
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let chain_lock = scenario.chain_lock_signed_by_new_quorum();
let core_best_chain_lock_height = chain_lock.block_height - 10;

let mut core_rpc = MockCoreRPCLike::new();
core_rpc
.expect_verify_chain_lock()
.times(1)
.returning(|_| Ok(true));
core_rpc
.expect_submit_chain_lock()
.times(1)
.returning(move |_| Ok(core_best_chain_lock_height));
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock(0, &state, &chain_lock, true, PlatformVersion::latest())
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(result.found_valid_locally, None);
assert_eq!(result.found_valid_by_core, Some(true));
assert_eq!(result.core_is_synced, Some(false));
assert!(!accepted_by_block_proposal(&result));
}

#[test]
fn should_ask_verifychainlock_once_about_chain_lock_left_to_core_without_submitting() {
// Without submitting, Core's verifychainlock is the whole verification of a chain lock
// Platform leaves to Core, and it is asked once.
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let chain_lock = scenario.chain_lock_signed_by_new_quorum();

for verdict in [true, false] {
let mut core_rpc = MockCoreRPCLike::new();
let asked = chain_lock.clone();
core_rpc
.expect_verify_chain_lock()
.withf(move |chain_lock| *chain_lock == asked)
.times(1)
.returning(move |_| Ok(verdict));
core_rpc.expect_submit_chain_lock().times(0);
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock_v0(0, &state, &chain_lock, false, PlatformVersion::latest())
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(result.found_valid_locally, None);
assert_eq!(result.found_valid_by_core, Some(verdict));
assert_eq!(result.core_is_synced, None);
}
}

#[test]
fn should_keep_protocol_version_14_verdict_on_chain_lock_left_to_core_when_core_already_has_a_chain_lock_at_its_height(
) {
// Protocol version 14 leaves a chain lock signed past the DKG interval of Platform's
// core height to submitchainlock without a signature check, and Core, already holding a
// chain lock at 1140, answers that height. Blocks committed under protocol version 14
// replay the same way only if it keeps accepting such a chain lock.
const CORE_BEST_CHAIN_LOCK_HEIGHT: u32 = 1140;
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let forged = sign_chain_lock(&TestQuorum::new(9, 0), 1128 + 8);

let mut core_rpc = MockCoreRPCLike::new();
core_rpc.expect_verify_chain_lock().times(0);
core_rpc
.expect_submit_chain_lock()
.times(1)
.returning(|_| Ok(CORE_BEST_CHAIN_LOCK_HEIGHT));
platform.core_rpc = core_rpc;

let result = platform
.platform
.verify_chain_lock_v0(
0,
&state,
&forged,
true,
PlatformVersion::get(14).expect("protocol version 14 exists"),
)
.expect("verification runs");

assert!(result.chain_lock_signature_is_deserializable);
assert_eq!(result.found_valid_locally, None);
assert_eq!(result.found_valid_by_core, None);
assert_eq!(result.core_is_synced, Some(true));
}

#[test]
fn should_fail_verification_of_chain_lock_left_to_core_when_verifychainlock_fails() {
// The block proposal turns this error into an invalid chain lock, as it does when
// submitchainlock fails.
let mut platform = chain_lock_test_platform();
let scenario = QuorumMinedAfterCoreHeight::default();
let state = scenario.platform_state(&platform.platform.state.load());
let chain_lock = scenario.chain_lock_signed_by_new_quorum();

let mut core_rpc = MockCoreRPCLike::new();
core_rpc.expect_verify_chain_lock().times(1).returning(|_| {
Err(DashCoreRpcError::UnexpectedStructure(
"simulated rpc failure".to_string(),
))
});
core_rpc.expect_submit_chain_lock().times(0);
platform.core_rpc = core_rpc;

let result = platform.platform.verify_chain_lock_v0(
0,
&state,
&chain_lock,
true,
PlatformVersion::latest(),
);

assert!(
matches!(result, Err(Error::CoreRpc(_))),
"the verifychainlock failure must reach the block proposal"
);
}

/// Exercises the `Err(Error::BLSError(_))` branch of `verify_chain_lock_v0`:
/// when the inner `verify_chain_lock_locally` returns a BLS deserialization
Expand Down
Loading
Loading