Skip to content
Snippets Groups Projects
gtest_genesis.rs 18.3 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::constants::*;
use common_runtime::entities::IdtyData;
use common_runtime::*;
use gtest_runtime::{
    opaque::SessionKeys, parameters, AccountConfig, AccountId, AuthorityMembersConfig, BabeConfig,
    CertConfig, GenesisConfig, IdentityConfig, MembershipConfig, SessionConfig, SmithCertConfig,
    SmithMembershipConfig, SudoConfig, SystemConfig, TechnicalCommitteeConfig,
    UniversalDividendConfig,
};
use num_format::{Locale, ToFormattedString};
use serde::Deserialize;
use sp_core::{blake2_256, Decode, Encode, H256};
use std::collections::{BTreeMap, HashMap};

type MembershipData = sp_membership::MembershipData<u32>;

// get values of parameters
static EXISTENTIAL_DEPOSIT: u64 = parameters::ExistentialDeposit::get();
static SMITH_MEMBERSHIP_EXPIRE_ON: u32 = parameters::SmithMembershipPeriod::get();
static SMITH_CERTS_EXPIRE_ON: u32 = parameters::SmithValidityPeriod::get();
static MIN_CERT: u32 = parameters::WotMinCertForMembership::get();
static SMITH_MIN_CERT: u32 = parameters::SmithWotMinCertForMembership::get();

// define structure of json
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GenesisJson {
    /// identity-related data
    // (pseudo, membership, certifications, account balance)
    identities: HashMap<String, Identity>,
    /// smith-related data
    // (smith memberships, smith certifications, session keys of bootstrapper)
    smiths: HashMap<String, Smith>,
    /// time of the first ud reeval
    first_ud_reeval: u64,
    /// time of the first ud
    /// value of the first ud
    ud: u64,
    /// initial monetary mass (must match what is available on accounts)
    initial_monetary_mass: u64,
    /// amount on the accounts (must be above existential deposit)
    wallets: HashMap<AccountId, u64>, // u128
    sudo_key: Option<AccountId>,
    /// list of names of people in initial technical committee
    technical_committee: Vec<String>,
}

/// identities
#[derive(Clone, Deserialize)]
struct Identity {
    /// indentity index matching the order of appearance in the Ǧ1v1 blockchain
    index: u32,
    /// ss58 address in gtest network
    owner_key: AccountId,
    /// optional ss58 address in the Ğ1v1
    old_owner_key: Option<AccountId>,
    /// block at which the membership is set to expire (0 for expired members)
    membership_expire_on: u32,
    /// block at which the next cert can be emitted
    next_cert_issuable_on: u32,
    /// balance of the account of this identity
    balance: u64, // u128
    /// certs received with their expiration block
    certs_received: HashMap<String, u32>,
}

/// smith members
#[derive(Clone, Deserialize)]
struct Smith {
    /// optional pre-set session keys (at least for the smith bootstraping the blockchain)
    session_keys: Option<String>,
    /// smith certification received
    certs_received: Vec<String>,
}

// copied from duniter primitives
fn validate_idty_name(idty_name: &str) -> bool {
    idty_name.len() >= 3
        && idty_name.len() <= 42 // length smaller than 42
        // all characters are alphanumeric or - or _
        && idty_name
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}

/// ============================================================================================ ///
/// build genesis from json file
pub fn build_genesis(
    // genesis data build from json
    genesis_data: GenesisJson,
    // wasm binary
    wasm_binary: &[u8],
    // useful to enforce Alice authority when developing
    maybe_force_authority: Option<Vec<u8>>,
) -> Result<GenesisConfig, String> {
    // preparatory steps

    // declare variables for building genesis
    // -------------------------------------
    // track if fatal error occured, but let processing continue
    let mut fatal = false;
    // monetary mass for double check
    let mut monetary_mass = 0u64;
    // counter for online authorities at genesis
    let mut counter_online_authorities = 0;
    // track identity index
    let mut identity_index = HashMap::new();
    // counter for certifications
    let mut counter_cert = 0u32;
    // counter for smith certifications
    let mut counter_smith_cert = 0u32;
    // track inactive identities
    let mut inactive_identities = HashMap::<u32, &str>::new();

    // declare variables to fill in genesis
    // -------------------------------------
    // account inserted in genesis
    let mut accounts = BTreeMap::new();
    // members of technical committee
    let mut technical_committee_members = Vec::new();
    // memberships
    let mut memberships = BTreeMap::new();
    // identities
    let mut identities = Vec::new();
    // certifications
    let mut certs_by_receiver = BTreeMap::new();
    // initial authorities
    let mut initial_authorities = BTreeMap::new();
    // session keys
    let mut session_keys_map = BTreeMap::new();
    // smith memberships
    let mut smith_memberships = BTreeMap::new();
    // smith certifications
    let mut smith_certs_by_receiver = BTreeMap::new();

    // SIMPLE WALLETS //
    for (pubkey, balance) in &genesis_data.wallets {
        // check existential deposit
        if balance < &EXISTENTIAL_DEPOSIT {
            log::warn!("wallet {pubkey} has {balance} cǦT which is below {EXISTENTIAL_DEPOSIT}");
            fatal = true;
        }

        // double check the monetary mass
        monetary_mass += balance;

        // json prevents duplicate wallets
        accounts.insert(
            pubkey.clone(),
            GenesisAccountData {
                random_id: H256(blake2_256(&(balance, &pubkey).encode())),
                balance: *balance,
                is_identity: false,
            },
        );
    }

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

        // do not check existential deposit of identities
        // // check existential deposit
        // if identity.balance < EXISTENTIAL_DEPOSIT {
        //     if identity.membership_expire_on == 0 {
        //         log::warn!(
        //             "expired identity {name} has {} cǦT which is below {EXISTENTIAL_DEPOSIT}",
        //             identity.balance
        //         );
        //         fatal = true;
        //     } else {
        //         member identities can still be below existential deposit thanks to sufficient
        //         log::info!(
        //             "identity {name} has {} cǦT which is below {EXISTENTIAL_DEPOSIT}",
        //             identity.balance
        //         );
        //     }
        // }

        // Money
        // check that wallet with same owner_key does not exist
        if accounts.get(&identity.owner_key).is_some() {
            log::warn!(
                "{name} owner_key {} already exists as a simple wallet",
                identity.owner_key
            );
            fatal = true;
        }
        // insert as an account
        accounts.insert(
            identity.owner_key.clone(),
            GenesisAccountData {
                random_id: H256(blake2_256(&(identity.index, &identity.owner_key).encode())),
                balance: identity.balance,
                is_identity: true,
            },
        );

        // double check the monetary mass
        monetary_mass += identity.balance;

        // insert identity
        // check that index does not already exist
        if let Some(other_name) = identity_index.get(&identity.index) {
            log::warn!(
                "{other_name} already has identity index {} of {name}",
                identity.index
            );
            fatal = true;
        }
        identity_index.insert(identity.index, name);

        // only add the identity if not expired
        if identity.membership_expire_on != 0 {
            identities.push(GenesisIdty {
                index: identity.index,
                name: common_runtime::IdtyName::from(name.as_str()),
                value: common_runtime::IdtyValue {
                    data: IdtyData::new(),
                    next_creatable_identity_on: identity.next_cert_issuable_on,
                    // FIXME old owner key expiration
                    old_owner_key: identity.old_owner_key.clone().map(|address| (address, 0)),
                    // old_owner_key: None,
                    owner_key: identity.owner_key.clone(),
                    // TODO remove the removable_on field of identity
                    removable_on: 0,
                    status: IdtyStatus::Validated,
                },
            });
        } else {
            inactive_identities.insert(identity.index, name);
        };

        // insert the membershup data (only if not expired)
        if identity.membership_expire_on != 0 {
            memberships.insert(
                identity.index,
                MembershipData {
                    expire_on: identity.membership_expire_on,
                },
            );
        }
    }
    // sort the identities by index for reproducibility (should have been a vec in json)
    identities.sort_unstable_by(|a, b| (a.index as u32).cmp(&(b.index as u32)));

    // Technical Comittee //
    // NOTE : when changing owner key, the technical committee is not changed
    for name in &genesis_data.technical_committee {
        if let Some(identity) = &genesis_data.identities.get(name) {
            technical_committee_members.push(identity.owner_key.clone());
        } else {
            log::error!("Identity '{}' does not exist", name);
            fatal = true;
        }
    }

    // CERTIFICATIONS //
    for identity in genesis_data.identities.values() {
        let mut certs = BTreeMap::new();
        for (issuer, expire_on) in &identity.certs_received {
            if let Some(issuer) = &genesis_data.identities.get(issuer) {
                certs.insert(issuer.index, Some(*expire_on));
                counter_cert += 1;
            } else {
                log::error!("Identity '{}' does not exist", issuer);
                fatal = true;
            };
        }
        certs_by_receiver.insert(identity.index, certs);
    }

    // SMITHS SUB-WOT //
    for (name, smith_data) in &genesis_data.smiths {
        // check that smith exists
        if let Some(identity) = &genesis_data.identities.get(&name.clone()) {
            // Initial authorities and session keys
            let session_keys_bytes = if let Some(declared_session_keys) = &smith_data.session_keys {
                counter_online_authorities += 1;
                // insert authority as online
                initial_authorities.insert(identity.index, (identity.owner_key.clone(), true));
                // decode session keys or force to given value
                match maybe_force_authority {
                    Some(ref bytes) => bytes.clone(),
                    None => hex::decode(&declared_session_keys[2..])
                        .map_err(|_| format!("invalid session keys for idty {}", &name))?,
                }
            } else {
                // still authority but offline
                initial_authorities.insert(identity.index, (identity.owner_key.clone(), false));
                // fake session keys
                let mut fake_bytes = Vec::with_capacity(128);
                for _ in 0..4 {
                    fake_bytes.extend_from_slice(identity.owner_key.as_ref())
                }
                fake_bytes
            };

            // insert session keys to map
            session_keys_map.insert(
                identity.owner_key.clone(),
                SessionKeys::decode(&mut &session_keys_bytes[..]).unwrap(),
            );

            // smith certifications
            let mut certs = BTreeMap::new();
            for issuer in &smith_data.certs_received {
                let issuer_index = &genesis_data
                    .identities
                    .get(issuer)
                    .ok_or(format!("Identity '{}' does not exist", issuer))?
                    .index;
                certs.insert(*issuer_index, Some(SMITH_CERTS_EXPIRE_ON));
                counter_smith_cert += 1;
            }
            smith_certs_by_receiver.insert(identity.index, certs);

            // smith memberships
            smith_memberships.insert(
                identity.index,
                MembershipData {
                    expire_on: SMITH_MEMBERSHIP_EXPIRE_ON,
                },
            );
        } else {
            log::error!("Smith '{}' does not correspond to exising identity", &name);
            fatal = true;
        }
    }

    // Verify certifications coherence (can be ignored for old users)
    for (idty_index, receiver_certs) in &certs_by_receiver {
        if receiver_certs.len() < MIN_CERT as usize {
            let name = identity_index.get(idty_index).unwrap();
            let identity = genesis_data.identities.get(&(*name).clone()).unwrap();
            if identity.membership_expire_on != 0 {
                log::warn!(
                    "[{}] has received only {}/{} certifications",
                    name,
                    receiver_certs.len(),
                    MIN_CERT
                );
                fatal = true;
            }
        }
    }

    // Verify smith certifications coherence
    for (idty_index, certs) in &smith_certs_by_receiver {
        if certs.len() < SMITH_MIN_CERT as usize {
            log::warn!(
                "[{}] has received only {}/{} smith certifications",
                identity_index.get(idty_index).unwrap(),
                certs.len(),
                SMITH_MIN_CERT
            );
            fatal = true;
        }
    }

    // check number of online authorities
    if counter_online_authorities != 1 {
        log::error!("one and only one smith must be online, not {counter_online_authorities}");
    }

    // check monetary mass
    if monetary_mass != genesis_data.initial_monetary_mass {
        log::warn!(
            "actual monetary_mass ({}) and initial_monetary_mass ({}) do not match",
            monetary_mass.to_formatted_string(&Locale::en),
            genesis_data
                .initial_monetary_mass
                .to_formatted_string(&Locale::en)
        );
        fatal = true;
    }

    // give genesis info
    log::info!(
        "prepared genesis with:
        - {} accounts ({} identities, {} simple wallets)
        - {} total identities ({} active, {} inactive)
        - {} smiths
        - {} initial online authorities
        - {} certifications
        - {} smith certifications
        - {} members in technical committee",
        accounts.len(),
        identity_index.len(),
        &genesis_data.wallets.len(),
        identity_index.len(),
        identities.len(),
        inactive_identities.len(),
        smith_memberships.len(),
        counter_online_authorities,
        counter_cert,
        counter_smith_cert,
        technical_committee_members.len(),
    );

    // some more checks
    assert_eq!(identities.len(), memberships.len());
    assert_eq!(smith_memberships.len(), initial_authorities.len());
    assert_eq!(smith_memberships.len(), session_keys_map.len());
    assert_eq!(
        identity_index.len(),
        identities.len() + inactive_identities.len()
    );
    assert_eq!(
        accounts.len(),
        identity_index.len() + genesis_data.wallets.len()
    );
    // no inactive tech comm
    for tech_com_member in &genesis_data.technical_committee {
        assert!(!inactive_identities.values().any(|&v| v == tech_com_member));
    }
    // no inactive smith
    for smith in genesis_data.smiths.keys() {
        assert!(!inactive_identities.values().any(|&v| v == smith));
    }

    // check the logs to see all the fatal error preventing from starting gtest currency
    if fatal {
        log::error!("some previously logged error prevent from building a sane genesis");
        panic!();
    }

    // return genesis config
    Ok(gtest_runtime::GenesisConfig {
        system: SystemConfig {
            // Add Wasm runtime to storage.
            code: wasm_binary.to_vec(),
        },
        account: AccountConfig { accounts },
        authority_discovery: Default::default(),
        authority_members: AuthorityMembersConfig {
            initial_authorities,
        },
        balances: Default::default(), // FIXME
        babe: BabeConfig {
            authorities: Vec::with_capacity(0),
            epoch_config: Some(BABE_GENESIS_EPOCH_CONFIG),
        },
        grandpa: Default::default(),
        im_online: Default::default(),
        session: SessionConfig {
            keys: session_keys_map
                .into_iter()
                .map(|(account_id, session_keys)| (account_id.clone(), account_id, session_keys))
                .collect::<Vec<_>>(),
        },
        sudo: SudoConfig {
            key: genesis_data.sudo_key,
        },
        technical_committee: TechnicalCommitteeConfig {
            members: technical_committee_members,
            ..Default::default()
        },
        identity: IdentityConfig { identities },
        cert: CertConfig {
            apply_cert_period_at_genesis: false,
            certs_by_receiver,
        },
        membership: MembershipConfig { memberships },
        smith_cert: SmithCertConfig {
            apply_cert_period_at_genesis: true,
            certs_by_receiver: smith_certs_by_receiver,
        },
        smith_membership: SmithMembershipConfig {
            memberships: smith_memberships,
        },
        universal_dividend: UniversalDividendConfig {
            first_reeval: Some(genesis_data.first_ud_reeval),
            first_ud: Some(genesis_data.first_ud),
            ud: genesis_data.ud,
            initial_monetary_mass: genesis_data.initial_monetary_mass,
        },
        treasury: Default::default(),
    })
}