use assert_matches::assert_matches;
use exonum::{
blockchain::{
config::GenesisConfigBuilder, BlockParams, Blockchain, BlockchainBuilder, BlockchainMut,
ConsensusConfig, IndexProof,
},
crypto::PublicKey,
helpers::{Height, ValidatorId},
merkledb::{
access::{Access, CopyAccessExt, FromAccess, Prefixed},
Entry, HashTag, ProofMapIndex, Snapshot,
},
runtime::{
versioning::{ArtifactReq, ArtifactReqError, RequireArtifact},
BlockchainData, InstanceDescriptor, SnapshotExt,
},
};
use exonum_derive::{FromAccess, ServiceDispatcher, ServiceFactory};
use futures::channel::mpsc;
use exonum_rust_runtime::{DefaultInstance, RustRuntimeBuilder, Service, ServiceFactory};
#[derive(Debug, FromAccess)]
struct Schema<T: Access> {
pub wallets: ProofMapIndex<T::Base, PublicKey, u64>,
}
impl<T: Access> RequireArtifact for Schema<T> {
fn required_artifact() -> ArtifactReq {
"exonum.Token@^1.3.0".parse().unwrap()
}
}
#[derive(Debug, FromAccess)]
struct SchemaImpl<T: Access> {
#[from_access(flatten)]
public: Schema<T>,
private: Entry<T::Base, String>,
}
#[derive(Debug, ServiceDispatcher, ServiceFactory)]
#[service_factory(artifact_name = "exonum.Token", artifact_version = "1.4.0")]
struct TokenService;
impl Service for TokenService {}
impl DefaultInstance for TokenService {
const INSTANCE_ID: u32 = 100;
const INSTANCE_NAME: &'static str = "token";
}
#[derive(Debug, ServiceDispatcher, ServiceFactory)]
#[service_factory(artifact_name = "exonum.Token", artifact_version = "1.0.0")]
struct OldService;
impl Service for OldService {}
impl DefaultInstance for OldService {
const INSTANCE_ID: u32 = 101;
const INSTANCE_NAME: &'static str = "old-token";
}
#[derive(Debug, ServiceDispatcher, ServiceFactory)]
#[service_factory(artifact_name = "exonum.OtherService", artifact_version = "1.3.5")]
struct OtherService;
impl Service for OtherService {}
impl DefaultInstance for OtherService {
const INSTANCE_ID: u32 = 102;
const INSTANCE_NAME: &'static str = "other";
}
fn create_blockchain() -> BlockchainMut {
let (config, _) = ConsensusConfig::for_tests(1);
let genesis_config = GenesisConfigBuilder::with_consensus_config(config)
.with_artifact(TokenService.artifact_id())
.with_instance(TokenService.default_instance())
.with_artifact(OldService.artifact_id())
.with_instance(OldService.default_instance())
.with_artifact(OtherService.artifact_id())
.with_instance(OtherService.default_instance())
.build();
let runtime = RustRuntimeBuilder::new()
.with_factory(TokenService)
.with_factory(OldService)
.with_factory(OtherService)
.build(mpsc::channel(1).0);
BlockchainBuilder::new(Blockchain::build_for_tests())
.with_genesis_config(genesis_config)
.with_runtime(runtime)
.build()
}
fn setup_blockchain_for_index_proofs() -> Box<dyn Snapshot> {
let mut blockchain = create_blockchain();
let fork = blockchain.fork();
fork.get_proof_list("test.list").push(1_u32);
fork.get_proof_entry(("test.entry", &0_u8))
.set("!".to_owned());
fork.get_value_set("test.set").insert(2_u64);
blockchain.merge(fork.into_patch()).unwrap();
let block_params = BlockParams::new(ValidatorId(0), Height(1), &[]);
let patch = blockchain.create_patch(block_params, &());
blockchain.commit(patch, vec![]).unwrap();
blockchain.snapshot()
}
fn check_list_proof(proof: &IndexProof) {
let block = &proof.block_proof.block;
assert_eq!(block.height, Height(1));
let checked_proof = proof
.index_proof
.check_against_hash(block.state_hash)
.unwrap();
let entries: Vec<_> = checked_proof
.entries()
.map(|(name, hash)| (name.as_str(), *hash))
.collect();
assert_eq!(entries, vec![("test.list", HashTag::hash_list(&[1_u32]))]);
}
#[test]
fn proof_for_index_in_snapshot() {
let snapshot = setup_blockchain_for_index_proofs();
let proof = snapshot.proof_for_index("test.list").unwrap();
check_list_proof(&proof);
assert!(snapshot.proof_for_index("test.entry").is_none());
assert!(snapshot.proof_for_index("test.set").is_none());
}
#[test]
fn proof_for_service_index() {
let snapshot = setup_blockchain_for_index_proofs();
let instance = InstanceDescriptor::new(100, "test");
let data = BlockchainData::new(snapshot.as_ref(), &instance.name);
let proof = data.proof_for_service_index("list").unwrap();
check_list_proof(&proof);
assert!(data.proof_for_service_index("entry").is_none());
assert!(data.proof_for_service_index("set").is_none());
}
#[test]
fn access_to_service_schema() {
let mut blockchain = create_blockchain();
let fork = blockchain.fork();
{
let mut schema = SchemaImpl::from_root(Prefixed::new("token", &fork)).unwrap();
schema.public.wallets.put(&PublicKey::new([0; 32]), 100);
schema.public.wallets.put(&PublicKey::new([1; 32]), 200);
schema.private.set("Some value".to_owned());
}
let instance = InstanceDescriptor::new(0, "who");
let data = BlockchainData::new(&fork, &instance.name);
{
let schema: Schema<_> = data.service_schema("token").unwrap();
assert_eq!(schema.wallets.values().sum::<u64>(), 300);
}
let err = data
.service_schema::<Schema<_>, _>("what")
.expect_err("Retrieving schema for non-existing service should fail");
assert_matches!(err, ArtifactReqError::NoService);
let err = data
.service_schema::<Schema<_>, _>("old-token")
.expect_err("Retrieving schema for old service should fail");
assert_matches!(err, ArtifactReqError::IncompatibleVersion { .. });
let err = data
.service_schema::<Schema<_>, _>("other")
.expect_err("Retrieving schema for unrelated service should fail");
assert_matches!(
err,
ArtifactReqError::UnexpectedName { ref actual, .. } if actual == "exonum.OtherService"
);
blockchain.merge(fork.into_patch()).unwrap();
let snapshot = blockchain.snapshot();
let schema: Schema<_> = snapshot.service_schema("token").unwrap();
assert_eq!(schema.wallets.values().sum::<u64>(), 300);
}