exonum-supervisor 1.0.0

Exonum supervisor service.
// Copyright 2020 The Exonum Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! This file contains tests for the Supervisor in the simple mode.
//! Since simple Supervisor only difference from decentralized one is
//! decision-making algorithm, the tests affect only this aspect.

use exonum::{
    crypto::Hash,
    helpers::{Height, ValidatorId},
    merkledb::access::AccessExt,
    messages::{AnyTx, Verified},
    runtime::{
        CommonError, ErrorMatch, ExecutionContext, ExecutionError, InstanceId, SnapshotExt,
        SUPERVISOR_INSTANCE_ID,
    },
};
use exonum_derive::*;
use exonum_rust_runtime::{
    spec::{JustFactory, Spec},
    DefaultInstance, Service, ServiceFactory as _,
};
use exonum_testkit::{ApiKind, TestKit, TestKitBuilder};

use exonum_supervisor::{
    ConfigPropose, Configure, DeployRequest, Schema, Supervisor, SupervisorInterface,
};

pub fn sign_config_propose_transaction(
    testkit: &TestKit,
    config: ConfigPropose,
    initiator_id: ValidatorId,
) -> Verified<AnyTx> {
    let keypair = testkit.validator(initiator_id).service_keypair();
    keypair.propose_config_change(SUPERVISOR_INSTANCE_ID, config)
}

pub fn sign_config_propose_transaction_by_us(
    testkit: &TestKit,
    config: ConfigPropose,
) -> Verified<AnyTx> {
    let initiator_id = testkit.us().validator_id().unwrap();
    sign_config_propose_transaction(&testkit, config, initiator_id)
}

#[derive(Debug, ServiceDispatcher, ServiceFactory)]
#[service_factory(artifact_name = "deployable-test-service", artifact_version = "0.1.0")]
pub struct DeployableService;

impl Service for DeployableService {}

#[derive(Debug, ServiceDispatcher, ServiceFactory)]
#[service_dispatcher(implements(raw = "Configure<Params = String>"))]
#[service_factory(artifact_name = "config-change-test-service")]
pub struct ConfigChangeService;

impl DefaultInstance for ConfigChangeService {
    const INSTANCE_ID: InstanceId = 119;
    const INSTANCE_NAME: &'static str = "config-change";
}

impl Service for ConfigChangeService {}

impl Configure for ConfigChangeService {
    type Params = String;

    fn verify_config(
        &self,
        context: ExecutionContext<'_>,
        params: Self::Params,
    ) -> Result<(), ExecutionError> {
        context
            .caller()
            .as_supervisor()
            .ok_or(CommonError::UnauthorizedCaller)?;

        match params.as_str() {
            "error" => Err(CommonError::malformed_arguments("Error!")),
            "panic" => panic!("Aaaa!"),
            _ => Ok(()),
        }
    }

    fn apply_config(
        &self,
        context: ExecutionContext<'_>,
        params: Self::Params,
    ) -> Result<(), ExecutionError> {
        context
            .caller()
            .as_supervisor()
            .ok_or(CommonError::UnauthorizedCaller)?;

        context
            .service_data()
            .get_entry("params")
            .set(params.clone());

        match params.as_str() {
            "apply_error" => Err(CommonError::malformed_arguments("Error!")),
            "apply_panic" => panic!("Aaaa!"),
            _ => Ok(()),
        }
    }
}

fn assert_config_change_is_applied(testkit: &TestKit) {
    let snapshot = testkit.snapshot();
    let schema: Schema<_> = snapshot.service_schema(Supervisor::NAME).unwrap();
    assert!(!schema.pending_proposal.exists());
}

/// Attempts to change consensus config with only one confirmation.
#[test]
fn change_consensus_config_with_one_confirmation() {
    let initial_validator_count = 4;
    let expected_new_validator_number = initial_validator_count;

    let mut testkit = TestKitBuilder::auditor()
        .with_validators(initial_validator_count)
        .with(Supervisor::simple())
        .build();

    let cfg_change_height = Height(5);
    let new_consensus_config = {
        let mut cfg = testkit.consensus_config();
        // Add us node.
        cfg.validator_keys.push(testkit.us().public_keys());
        // Add new node.
        cfg.validator_keys
            .push(testkit.network_mut().add_node().public_keys());
        cfg
    };

    // Sign request by validator (we're an auditor yet).
    let initiator_id = testkit.network().validators()[0].validator_id().unwrap();
    let config_propose =
        ConfigPropose::new(0, cfg_change_height).consensus_config(new_consensus_config.clone());

    testkit.create_block_with_transaction(sign_config_propose_transaction(
        &testkit,
        config_propose,
        initiator_id,
    ));

    testkit.create_blocks_until(cfg_change_height.previous());
    assert_eq!(testkit.network().us().validator_id(), None);
    testkit.create_block();

    // We did not send (2/3+1) confirmations, but expect config to be applied.
    assert_config_change_is_applied(&testkit);
    assert_eq!(
        testkit.network().us().validator_id(),
        Some(ValidatorId(expected_new_validator_number))
    );
    assert_eq!(
        &testkit.network().validators()[expected_new_validator_number as usize],
        testkit.network().us()
    );
    assert_eq!(testkit.consensus_config(), new_consensus_config);
}

/// Attempts to change service config with only one confirmation.
#[test]
fn service_config_change() {
    let mut testkit = TestKitBuilder::validator()
        .with_validators(2)
        .with(Supervisor::simple())
        .with(Spec::new(ConfigChangeService).with_default_instance())
        .build();

    let cfg_change_height = Height(5);
    let params = "I am a new parameter".to_owned();

    let config_propose = ConfigPropose::new(0, cfg_change_height)
        .service_config(ConfigChangeService::INSTANCE_ID, params.clone());

    testkit.create_block_with_transaction(sign_config_propose_transaction_by_us(
        &testkit,
        config_propose,
    ));
    testkit.create_blocks_until(cfg_change_height);

    let actual_params: String = testkit
        .snapshot()
        .for_service(ConfigChangeService::INSTANCE_NAME)
        .unwrap()
        .get_entry("params")
        .get()
        .unwrap();

    assert_eq!(actual_params, params);
}

#[test]
fn incorrect_actual_from_field() {
    let mut testkit = TestKitBuilder::validator()
        .with_validators(2)
        .with(Supervisor::simple())
        .with(Spec::new(ConfigChangeService).with_default_instance())
        .build();

    let cfg_change_height = Height(5);
    let params = "I am a new parameter".to_owned();

    testkit.create_blocks_until(cfg_change_height);

    let config_propose = ConfigPropose::new(0, cfg_change_height)
        .service_config(ConfigChangeService::INSTANCE_ID, params);

    testkit
        .create_block_with_transaction(sign_config_propose_transaction_by_us(
            &testkit,
            config_propose,
        ))
        .transactions[0]
        .status()
        .unwrap_err();
}

/// Checks that config propose signed by auditor is discarded.
#[test]
fn discard_config_propose_from_auditor() {
    let mut testkit = TestKitBuilder::auditor()
        .with_validators(2)
        .with(Supervisor::simple())
        .build();

    let cfg_change_height = Height(5);
    let old_consensus_config = testkit.consensus_config();
    // Attempt to add ourselves into validators list.
    let new_consensus_config = {
        let mut cfg = testkit.consensus_config();
        cfg.validator_keys.push(testkit.us().public_keys());
        cfg
    };

    let old_validators = testkit.network().validators();

    // Sign request by an auditor.
    let propose = ConfigPropose::new(0, cfg_change_height).consensus_config(new_consensus_config);
    let keys = testkit.us().service_keypair();
    let propose = keys.propose_config_change(SUPERVISOR_INSTANCE_ID, propose);
    let block = testkit.create_block_with_transaction(propose);
    // Verify that transaction failed.
    let expected_err =
        ErrorMatch::from_fail(&CommonError::UnauthorizedCaller).for_service(SUPERVISOR_INSTANCE_ID);
    assert_eq!(*block[0].status().unwrap_err(), expected_err);

    testkit.create_blocks_until(cfg_change_height);
    // Verify that no changes have been applied.
    let new_validators = testkit.network().validators();

    assert_eq!(testkit.consensus_config(), old_consensus_config);
    assert_eq!(testkit.network().us().validator_id(), None);
    assert_eq!(old_validators, new_validators);
}

/// Checks that config proposal sent through api is executed correctly.
#[tokio::test]
async fn test_send_proposal_with_api() {
    let mut testkit = TestKitBuilder::validator()
        .with_validators(2)
        .with(Supervisor::simple())
        .build();

    let old_validators = testkit.network().validators();

    let cfg_change_height = Height(5);
    let new_consensus_config = {
        let mut cfg = testkit.consensus_config();
        // Remove us from validators
        cfg.validator_keys.remove(0);
        cfg
    };

    let config_propose =
        ConfigPropose::new(0, cfg_change_height).consensus_config(new_consensus_config.clone());

    // Create proposal
    let hash: Hash = testkit
        .api()
        .private(ApiKind::Service("supervisor"))
        .query(&config_propose)
        .post("propose-config")
        .await
        .unwrap();
    let block = testkit.create_block();
    block[hash].status().unwrap();

    // Assert that config is now pending.
    let snapshot = testkit.snapshot();
    let schema: Schema<_> = snapshot.service_schema(Supervisor::NAME).unwrap();
    assert_eq!(
        schema.pending_proposal.get().unwrap().config_propose,
        config_propose
    );

    testkit.create_blocks_until(cfg_change_height);

    // Assert that config sent through the api is applied.
    assert_config_change_is_applied(&testkit);
    assert_eq!(testkit.consensus_config(), new_consensus_config);
    assert_eq!(testkit.network().us().validator_id(), None);
    assert_ne!(old_validators, testkit.network().validators());
}

/// Tests that deploy request with only one approval (initial) is executed successfully.
#[tokio::test]
async fn deploy_service() {
    let mut testkit = TestKitBuilder::validator()
        .with(Supervisor::simple())
        .with(JustFactory::new(DeployableService))
        .build();

    let deadline_height = Height(5);
    let artifact = DeployableService.artifact_id();
    let deploy_request = DeployRequest::new(artifact.clone(), deadline_height);

    // Create deploy request
    let hash: Hash = testkit
        .api()
        .private(ApiKind::Service("supervisor"))
        .query(&deploy_request)
        .post("deploy-artifact")
        .await
        .unwrap();
    let block = testkit.create_block();
    // Check that request was executed.
    block[hash].status().unwrap();

    testkit.create_blocks_until(deadline_height);
    // Verify that after reaching the deadline height artifact is deployed.
    let snapshot = testkit.snapshot();
    assert!(snapshot
        .for_dispatcher()
        .service_artifacts()
        .contains(&artifact));
}

/// Attempts to change config without `actual_from` height set.
/// When `actual_from` is not set, it is expected to be treated as the next height.
#[tokio::test]
async fn actual_from_is_zero() {
    let initial_validator_count = 4;
    let expected_new_validator_number = initial_validator_count;

    let mut testkit = TestKitBuilder::auditor()
        .with_validators(initial_validator_count)
        .with(Supervisor::simple())
        .build();

    // Change height set to 0
    let cfg_change_height = Height(0);

    // Sample config change, we don't actually care about what se change.
    let new_consensus_config = {
        let mut cfg = testkit.consensus_config();
        cfg.validator_keys.push(testkit.us().public_keys());
        cfg.validator_keys
            .push(testkit.network_mut().add_node().public_keys());
        cfg
    };

    let initiator_id = testkit.network().validators()[0].validator_id().unwrap();
    let config_propose =
        ConfigPropose::new(0, cfg_change_height).consensus_config(new_consensus_config.clone());

    testkit.create_block_with_transaction(sign_config_propose_transaction(
        &testkit,
        config_propose,
        initiator_id,
    ));

    // Create one block.
    testkit.create_block();

    // Check that config is applied.
    assert_config_change_is_applied(&testkit);
    assert_eq!(
        testkit.network().us().validator_id(),
        Some(ValidatorId(expected_new_validator_number))
    );
    assert_eq!(
        &testkit.network().validators()[expected_new_validator_number as usize],
        testkit.network().us()
    );
    assert_eq!(testkit.consensus_config(), new_consensus_config);
}