Skip to content
Snippets Groups Projects
gen_genesis_data.rs 13.9 KiB
Newer Older
// Copyright 2021 Axiom-Team
//
// This file is part of Duniter-v2S.
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.

use common_runtime::*;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sp_core::{blake2_256, Decode, Encode, H256};

type MembershipData = sp_membership::MembershipData<u32>;

const EXISTENTIAL_DEPOSIT: u64 = 200;
#[derive(Clone)]
pub struct GenesisData<Parameters: DeserializeOwned, SessionKeys: Decode> {
    pub accounts: BTreeMap<AccountId, GenesisAccountData<u64>>,
    pub certs_by_receiver: BTreeMap<u32, BTreeMap<u32, Option<u32>>>,
    pub first_ud: Option<u64>,
    pub first_ud_reeval: Option<u64>,
    pub identities: Vec<(String, AccountId)>,
    pub initial_authorities: BTreeMap<u32, (AccountId, bool)>,
    pub initial_monetary_mass: u64,
    pub memberships: BTreeMap<u32, MembershipData>,
    pub parameters: Parameters,
    pub session_keys_map: BTreeMap<AccountId, SessionKeys>,
    pub smith_certs_by_receiver: BTreeMap<u32, BTreeMap<u32, Option<u32>>>,
    pub smith_memberships: BTreeMap<u32, MembershipData>,
    pub sudo_key: Option<AccountId>,
    pub technical_committee_members: Vec<AccountId>,
    pub ud: u64,
pub struct ParamsAppliedAtGenesis {
Éloïs's avatar
Éloïs committed
    pub genesis_certs_min_received: u32,
    pub genesis_memberships_expire_on: u32,
Éloïs's avatar
Éloïs committed
    pub genesis_smith_certs_min_received: u32,
    pub genesis_smith_memberships_expire_on: u32,
}

#[derive(Deserialize, Serialize)]
struct GenesisConfig<Parameters> {
    first_ud: Option<u64>,
    first_ud_reeval: Option<u64>,
    identities: BTreeMap<String, Idty>,
    #[serde(default)]
    parameters: Parameters,
    #[serde(rename = "smiths")]
    smith_identities: BTreeMap<String, SmithData>,
    sudo_key: Option<AccountId>,
    technical_committee: Vec<String>,
    ud: u64,
    #[serde(default)]
    wallets: BTreeMap<AccountId, u64>,
}

#[derive(Clone, Deserialize, Serialize)]
struct Idty {
    #[serde(default)]
    balance: u64,
    #[serde(default)]
    #[serde(rename = "expire_on")]
    membership_expire_on: Option<u64>,
    pubkey: AccountId,
}

#[derive(Clone, Deserialize, Serialize)]
struct SmithData {
    #[serde(default)]
    certs: Vec<Cert>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(untagged)]
enum Cert {
    Issuer(String),
    IssuerAndExpireOn(String, u32),
}
impl From<Cert> for (String, Option<u32>) {
    fn from(cert: Cert) -> Self {
        match cert {
            Cert::Issuer(issuer) => (issuer, None),
            Cert::IssuerAndExpireOn(issuer, expire_on) => (issuer, Some(expire_on)),
        }
    }
/// generate genesis data from a json file
/// takes DUNITER_GENESIS_CONFIG env var if present or duniter-gen-conf.json by default
// this function is targeting dev chainspecs, do not use in production network
pub fn generate_genesis_data<CS, P, SK, F>(
    f: F,
    maybe_force_authority: Option<Vec<u8>>,
) -> Result<CS, String>
where
    P: Default + DeserializeOwned,
    SK: Decode,
    F: Fn(GenesisData<P, SK>) -> CS,
{
    let genesis_timestamp: u64 =
        if let Ok(genesis_timestamp) = std::env::var("DUNITER_GENESIS_TIMESTAMP") {
            genesis_timestamp
                .parse()
                .map_err(|_| "DUNITER_GENESIS_TIMESTAMP must be a number".to_owned())?
        } else {
            use std::time::SystemTime;
            SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)
                .expect("SystemTime before UNIX EPOCH!")
                .as_secs()
        };

    let json_file_path = std::env::var("DUNITER_GENESIS_CONFIG")
        .unwrap_or_else(|_| "duniter-gen-conf.json".to_owned());

    // We mmap the file into memory first, as this is *a lot* faster than using
    // `serde_json::from_reader`. See https://github.com/serde-rs/json/issues/160
    let file = std::fs::File::open(&json_file_path)
        .map_err(|e| format!("Error opening gen conf file `{}`: {}", json_file_path, e))?;

    // SAFETY: `mmap` is fundamentally unsafe since technically the file can change
    //         underneath us while it is mapped; in practice it's unlikely to be a problem
    let bytes = unsafe {
        memmap2::Mmap::map(&file)
            .map_err(|e| format!("Error mmaping gen conf file `{}`: {}", json_file_path, e))?
    };

    let genesis_config = serde_json::from_slice(&bytes)
        .map_err(|e| format!("Error parsing gen conf file: {}", e))?;
    let GenesisConfig {
        sudo_key,
        first_ud,
        genesis_parameters:
            ParamsAppliedAtGenesis {
                genesis_certs_min_received,
                genesis_memberships_expire_on,
                genesis_smith_certs_min_received,
                genesis_smith_memberships_expire_on,
            },
        parameters,
        identities,
        smith_identities,
    } = genesis_config;

    // MONEY AND WOT //

    let mut accounts = BTreeMap::new();
    let mut identities_ = Vec::with_capacity(identities.len());
    let mut idty_index: u32 = 1;
    let mut idty_index_of = BTreeMap::new();
    let mut initial_monetary_mass = 0;
    let mut memberships = BTreeMap::new();
    let mut technical_committee_members = Vec::with_capacity(technical_committee.len());

    // SIMPLE WALLETS //

    let mut wallet_index: u32 = 0;
    for (pubkey, balance) in wallets {
        wallet_index += 1;
        accounts.insert(
            pubkey.clone(),
            GenesisAccountData {
                random_id: H256(blake2_256(&(wallet_index, &pubkey).encode())),
                balance,
                is_identity: false,
            },
        );
    }

    // Technical Comittee //

    for idty_name in technical_committee {
        if let Some(identity) = identities.get(&idty_name) {
            technical_committee_members.push(identity.pubkey.clone());
        } else {
            return Err(format!("Identity '{}' not exist", idty_name));
        }
    }

    // IDENTITIES //

    for (idty_name, identity) in &identities {
        if !validate_idty_name(idty_name) {
            return Err(format!("Identity name '{}' is invalid", &idty_name));
        }

        // Money
        let balance = if identity.balance >= EXISTENTIAL_DEPOSIT {
            //total_dust += identity.balance;
            0
        };
        accounts.insert(
            identity.pubkey.clone(),
            GenesisAccountData {
                random_id: H256(blake2_256(&(idty_index, &identity.pubkey).encode())),
                balance,
                is_identity: true,
            },
        );

        // We must count the money under the existential deposit because what we count is
        // the monetary mass created (for the revaluation of the DU)
        initial_monetary_mass += identity.balance;

        // Wot
        identities_.push((idty_name.clone(), identity.pubkey.clone()));
        memberships.insert(
            idty_index,
            MembershipData {
                expire_on: identity
                    .membership_expire_on
                    .map_or(genesis_memberships_expire_on, |expire_on| {
                        to_bn(genesis_timestamp, expire_on)
                    }),
            },
        );

        // Identity index
        idty_index_of.insert(idty_name, idty_index);
        idty_index += 1;
    }

    // CERTIFICATIONS //
    let mut certs_by_receiver = BTreeMap::new();
    for (idty_name, identity) in &identities {
        let issuer_index = idty_index_of
            .get(&idty_name)
            .ok_or(format!("Identity '{}' not exist", &idty_name))?;
        let mut receiver_certs = BTreeMap::new();
        for cert in &identity.certs {
            let (issuer, maybe_expire_on) = cert.clone().into();
            let issuer_index = idty_index_of
                .ok_or(format!("Identity '{}' not exist", issuer))?;
            receiver_certs.insert(*issuer_index, maybe_expire_on);
        certs_by_receiver.insert(*issuer_index, receiver_certs);
Éloïs's avatar
Éloïs committed
    // Verify certifications coherence
    for (idty_index, receiver_certs) in &certs_by_receiver {
        if receiver_certs.len() < genesis_certs_min_received as usize {
            return Err(format!(
                "Identity n°{} has received only {}/{} certifications)",
                idty_index,
                receiver_certs.len(),
                genesis_certs_min_received
            ));
        }
    }

    // SMITHS SUB-WOT //

    let mut initial_authorities = BTreeMap::new();
    let mut online_authorities_counter = 0;
    let mut session_keys_map = BTreeMap::new();
    let mut smith_memberships = BTreeMap::new();
    let mut smith_certs_by_receiver = BTreeMap::new();
    for (idty_name, smith_data) in smith_identities {
        let idty_index = idty_index_of
            .get(&idty_name)
            .ok_or(format!("Identity '{}' not exist", &idty_name))?;
        let identity = identities
            .get(&idty_name)
            .ok_or(format!("Identity '{}' not exist", &idty_name))?;

        if identity.balance < EXISTENTIAL_DEPOSIT {
            return Err(format!(
                "Identity '{}' have balance '{}' < EXISTENTIAL_DEPOSIT",
                idty_name, identity.balance,
            ));
        }

        // Initial authorities
        if maybe_force_authority.is_some() {
            if smith_data.session_keys.is_some() {
Éloïs's avatar
Éloïs committed
                return Err("session_keys field forbidden".to_owned());
                initial_authorities.insert(1, (identity.pubkey.clone(), true));
            } else {
                // authority but offline
                initial_authorities.insert(*idty_index, (identity.pubkey.clone(), false));
            }
        } else {
            initial_authorities.insert(
                *idty_index,
                (identity.pubkey.clone(), smith_data.session_keys.is_some()),
            );
        }
        let session_keys_bytes = if let Some(ref session_keys) = smith_data.session_keys {
            online_authorities_counter += 1;
            hex::decode(&session_keys[2..])
                .map_err(|_| format!("invalid session keys for idty {}", &idty_name))?
        } else if let (1, Some(ref session_keys_bytes)) = (*idty_index, &maybe_force_authority) {
            session_keys_bytes.clone()
        } else {
            // Create fake session keys (must be unique and deterministic)
            let mut fake_session_keys_bytes = Vec::with_capacity(128);
            for _ in 0..4 {
                fake_session_keys_bytes.extend_from_slice(identity.pubkey.as_ref())
            }
            fake_session_keys_bytes
            //vec![initial_authorities.len() as u8; std::mem::size_of::<SK>()]
        };
        let session_keys = SK::decode(&mut &session_keys_bytes[..])
            .map_err(|_| format!("invalid session keys for idty {}", &idty_name))?;
        session_keys_map.insert(identity.pubkey.clone(), session_keys);
        let mut receiver_certs = BTreeMap::new();
        for cert in &smith_data.certs {
            let (issuer, maybe_expire_on) = cert.clone().into();
            let issuer_index = idty_index_of
                .get(&issuer)
                .ok_or(format!("Identity '{}' not exist", issuer))?;
            receiver_certs.insert(*issuer_index, maybe_expire_on);
        smith_certs_by_receiver.insert(*idty_index, receiver_certs);
        smith_memberships.insert(
            *idty_index,
            MembershipData {
                expire_on: genesis_smith_memberships_expire_on,
            },
        );
    }

    // Verify smith certifications coherence
    if smith_certs_by_receiver.len() < smith_memberships.len() {
Éloïs's avatar
Éloïs committed
        return Err(format!(
            "{} smith identities have not received any smith certification",
            smith_memberships.len() - smith_certs_by_receiver.len()
Éloïs's avatar
Éloïs committed
        ));
    }
    for (idty_index, receiver_certs) in &smith_certs_by_receiver {
Éloïs's avatar
Éloïs committed
        if receiver_certs.len() < genesis_smith_certs_min_received as usize {
            return Err(format!(
                "Identity n°{} has received only {}/{} smith certifications)",
Éloïs's avatar
Éloïs committed
                idty_index,
                receiver_certs.len(),
                genesis_smith_certs_min_received
            ));
        }
    }

    if maybe_force_authority.is_none() && online_authorities_counter == 0 {
Éloïs's avatar
Éloïs committed
        return Err("The session_keys field must be filled in for at least one smith.".to_owned());
    let genesis_data = GenesisData {
        identities: identities_,
        initial_authorities,
        initial_monetary_mass,
        memberships,
        parameters,
        session_keys_map,
        smith_certs_by_receiver,
        smith_memberships,
    };

    Ok(f(genesis_data))
}

// Timestamp to block number
fn to_bn(genesis_timestamp: u64, timestamp: u64) -> u32 {
    let duration_in_secs = timestamp.saturating_sub(genesis_timestamp);
    (duration_in_secs / 6) as u32
}

fn validate_idty_name(name: &str) -> bool {
    name.len() <= 64
}