Rejoignez-Nous sur

Construire une blockchain en rouille et substrat: [A Step-by-Step Guide for Developers]

gWbgVn4bydP2Fjyh2KJ1iFePNI73

News

Construire une blockchain en rouille et substrat: [A Step-by-Step Guide for Developers]

Photo de profil de l'auteur

@nicolezhuNicoleZhu

Renforcer la confidentialité des données @Paritytech. Anciennement @gojekindonesia @IDEO @Stanford. Rusty dev

Un tutoriel convivial pour les débutants sur la façon de construire une infrastructure blockchain hardcore dans Substrate, un framework open source.

Dans ce didacticiel autoguidé, vous allez créer une blockchain sans gaz, de type Bitcoin à partir de zéro. Vous apprendrez qu'une blockchain est beaucoup plus puissante que les contrats intelligents.

N'hésitez pas à réutiliser tout ce contenu pour héberger vos propres ateliers!

Ce que vous apprendrez:

  • Mettre en œuvre le modèle de grand livre UTXO, le mécanisme de comptabilité de Bitcoin
  • Modifier la logique du pool de transactions réseau
  • Configurer un bloc de genèse et écrire des tests
  • Déployez votre chaîne et interagissez avec le nœud en direct à l'aide d'un client Web

Exigences:

  • AUCUNE connaissance de la blockchain et de la rouille nécessaire
  • Expérience de programmation de base
  • Durée estimée: 3 heures

Pour une procédure pas à pas plus détaillée où j'explique en détail pourquoi nous faisons chaque étape et où nous prenons le temps de discuter des traits de rouille, consultez cet accompagnement du didacticiel vidéo:

Commençons!

Installation

1. Obtenez la dernière version stable des outils Rust & WebAssembly. Dans votre terminal, exécutez les commandes:

# On Windows, download and run rustup-init.exe from https://rustup.rs instead

# On Macs:
curl https://sh.rustup.rs -sSf | sh
rustup update nightly
rustup target add wasm32-unknown-unknown —toolchain nightly
rustup update stable
cargo install —git https://github.com/alexcrichton/wasm-gc

2. Dans un nouveau terminal, clonez également votre copie du code passe-partout de ce didacticiel:

git clone https://github.com/substrate-developer-hub/utxo-workshop.git
git fetch origin workshop:workshop
git checkout workshop
# (Optional) Once step 1 installations are completed
# Run the following commands to shorten future build time
cd /project_base_directory
cargo test -p utxo-runtime
Ce dépôt contient également une implémentation Bitcoin mise à jour et complète dans le

master

branche (comme une triche), alors assurez-vous de vérifier la

workshop

branche pour repartir de zéro!

Selon votre processeur, les premières installations de Rust peuvent prendre jusqu'à 10 à 20 minutes.

Utilisons cette fois maintenant pour un cours intensif sur le fonctionnement de Bitcoin, et explorons ce SDK développeur que nous utilisons!

Introduction rapide sur le modèle de grand livre UTXO

Si vous possédez un compte bancaire, vous connaissez déjà le modèle de grand livre «basé sur les comptes». C'est là que votre compte bancaire a un solde total qui est crédité ou débité par transaction.

Bitcoin propose un modèle de grand livre fondamentalement différent appelé UTXO, ou sorties de transaction non dépensées.

UTXO fonctionne comme des chèques de voyage en ce sens que:

  1. Celui qui a physiquement le chèque peut dépenser l'argent. En d'autres termes, l'autorisation de dépenser est liée à l'argent et non à un numéro de compte.
  2. Vous ne pouvez pas déchirer un chèque et dépenser ses pièces. Vous devez dépenser la totalité du chèque et recevoir toute modification dans un nouveau chèque.

Dans l'exemple suivant, Bob a un UTXO d'une valeur de 50 $. Il veut donner à Alice 0,5 $, alors il détruit ses 50 UTXO et crée deux nouveaux UTXO de valeurs 0,5 $ (pour Alice) et 49,5 $ (en tant que modification).

Source de l'image: https://freedomnode.com/

La cryptographie est le mécanisme sous-jacent qui permet uniquement à Bob, et à personne d'autre, de dépenser ses UTXO.

Ces informations sont stockées dans l'un des 3 champs de chaque UTXO:

  1. Un hachage de référence de l'endroit où l'UTXO existe, un peu comme un pointeur d'adresse dans une base de données
  2. La valeur monétaire de l'UTXO, par ex. 50 $
  3. La clé publique de son «propriétaire»

La clé publique correspond à une «clé privée» secrète, que seul le propriétaire aurait. Donc, pour dépenser l'UTXO, le propriétaire doit «signer» cryptographiquement une transaction avec sa clé privée correspondante. La signature peut ensuite être vérifiée par rapport à la "clé publique" pour vérifier sa validité.

Par exemple, quand Alice passe son UTXO, cela ressemblerait à ceci:

Elle crée une nouvelle transaction (fond gris), fournit son UTXO comme entrée à dépenser, et dans le

sigscript

domaine, Alice fournit sa signature.

Remarque: Alice "signe" les détails de l'ensemble de la transaction. Cela a l'avantage de verrouiller les détails de sortie de transaction, pour éviter toute falsification au niveau du réseau. Plus tard, la blockchain vérifiera qu'Alice a bien autorisé tous les détails de cette transaction.

Nous couvrirons les implications de sécurité plus en détail dans la partie 2, lorsque vous sécurisez votre blockchain contre les attaques malveillantes.

Introduction rapide au cadre de substrat

Vous utilisez un framework blockchain open source appelé Substrat. Le substrat est À base de rouille et compile dans un format d'instruction binaire appelé WebAssembly (WAsm).

Prêt à l'emploi, vous obtenez les principaux composants de la blockchain comme une base de données distribuée, une couche de réseau peer-to-peer et divers mécanismes de consensus parmi lesquels nous pouvons choisir.

Dans ce didacticiel, nous allons nous familiariser avec les couches de file d'attente de transactions et de module d'exécution

Commençons le codage!

Partie 1: Construire la couche logique de la blockchain

1. Dans le terminal, effectuez une vérification rapide du compilateur Rust pour vous assurer que tout est correctement téléchargé:

cargo check -p utxo-runtime
# Don’t worry if this takes a while, just let it run!
# You should see a few warnings but no breaking errors

2. Ouvrez le projet dans votre IDE compatible Rust préféré. Je recommande IntelliJ ou VSCode pour leur coloration syntaxique Rust.

3. Ouvrez le

Runtime

sous-répertoire, qui héberge le runtime blockchain. Ensuite, ouvrez

utxo.rs

fichier, qui est l'endroit où vous allez construire la plupart de votre logique Bitcoin UTXO.

Vous verrez un modèle de démarrage de substrat typique, avec des commentaires en ligne qui expliquent comment utiliser le SDK. Plus bas, vous devriez également voir où vous pouvez écrire des tests unitaires.

4. Juste après les lignes d'importation de dépendance, créez les structures de données nécessaires pour représenter les UTXO et une transaction UTXO.

/// Single transaction to be dispatched
#(cfg_attr(feature = "std", derive(Serialize, Deserialize)))
#(derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug))
pub struct Transaction {
    /// UTXOs to be used as inputs for current transaction
    pub inputs: Vec,
    
    /// UTXOs to be created as a result of current transaction dispatch
    pub outputs: Vec,
}

/// Single transaction input that refers to one UTXO
#(cfg_attr(feature = "std", derive(Serialize, Deserialize)))
#(derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug))
pub struct TransactionInput {
    /// Reference to an UTXO to be spent
    pub outpoint: H256,
    
    /// Proof that transaction owner is authorized to spend referred UTXO & 
    /// that the entire transaction is untampered
    pub sigscript: H512,
}

/// Single transaction output to create upon transaction dispatch
#(cfg_attr(feature = "std", derive(Serialize, Deserialize)))
#(derive(PartialEq, Eq, PartialOrd, Ord, Default, Clone, Encode, Decode, Hash, Debug))
pub struct TransactionOutput {
    /// Value associated with this output
    pub value: Value,

    /// Public key associated with this output. In order to spend this output
	/// owner must provide a proof by hashing the whole `Transaction` and
	/// signing it with a corresponding private key.
    pub pubkey: H256,
}
5. Désignez ce qui est stocké dans l'état de chaîne de la chaîne de blocs. Cela se fait à l'intérieur d'une macro Rust appelée

decl_storage

. Vous stockerez une table de hachage de pointeurs 256 bits vers UTXO comme clé, et la structure UTXO elle-même comme valeur. Implémentez les éléments suivants:

decl_storage! {
    trait Store for Module as Utxo {
        /// All valid unspent transaction outputs are stored in this map.
        /// Initial set of UTXO is populated from the list stored in genesis.
        UtxoStore build(|config: &GenesisConfig| {
            config.genesis_utxos
                .iter()
                .cloned()
                .map(|u| (BlakeTwo256::hash_of(&u), u))
                .collect::<Vec<_>>()
        }): map H256 => Option;

        /// Total reward value to be redistributed among authorities.
        /// It is accumulated from transactions during block execution
        /// and then dispersed to validators on block finalization.
        pub RewardTotal get(reward_total): Value;
    }

    add_extra_genesis {
        config(genesis_utxos): Vec;
    }
}

Remarquez, en plus de la configuration du stockage, nous avons également configuré comment * ce stockage sera rempli au niveau du bloc genesis. Au bloc 0, vous pourrez semer votre blockchain avec un vecteur existant d'UTXO à dépenser!

6. Créez les signatures de transaction qui permettront aux utilisateurs de votre blockchain Bitcoin de dépenser des UTXO. Implémentons le

spend

une fonction:

// External functions: callable by the end user
decl_module! {
    pub struct Module for enum Call where origin: T::Origin {
        fn deposit_event() = default;

        /// Dispatch a single transaction and update UTXO set accordingly
        pub fn spend(_origin, transaction: Transaction) -> DispatchResult {
                                    // TransactionValidity{}
            let transaction_validity = Self::validate_transaction(&transaction)?;
            
            Self::update_storage(&transaction, transaction_validity.priority as u128)?;

            Self::deposit_event(Event::TransactionSuccess(transaction));

            Ok(())
        }
}

Remarque, nous allons bientôt mettre en œuvre quelques fonctions d'assistance pour cette logique de transaction de dépenses.

7. Les chaînes de blocs peuvent également émettre des événements chaque fois qu'il y a des transactions en chaîne. Configurez votre blockchain pour reconnaître un

TransactionSuccess

type d'événement.

decl_event!(
    pub enum Event {
        /// Transaction was executed successfully
        TransactionSuccess(Transaction),
    }
);

Notez qu'à l'étape 2, vous émettez déjà cet événement après chaque transaction de dépense réussie.

Partie 2: sécuriser la chaîne contre les attaques malveillantes

Dans la partie 1, nous avons échafaudé le bâtiment de base des UTXO. Dans cette section, nous allons aborder la sécurité cryptographique de notre chaîne et implémenter la logique de transaction UTXO.

En fait, il existe de nombreuses vulnérabilités contre lesquelles la blockchain Bitcoin se défend.

  • Les entrées et sorties ne sont pas vides
  • Chaque entrée existe et est utilisée exactement une fois
  • Chaque sortie est définie exactement une fois et a une valeur différente de zéro
  • La valeur de sortie totale ne doit pas dépasser la valeur d'entrée totale
  • Les nouvelles sorties n'entrent pas en collision avec celles existantes
  • Les attaques par rejeu ne sont pas possibles
  • Les signatures d'entrée fournies sont valides
  • L'entrée UTXO est en effet signée par le propriétaire
  • Les transactions sont inviolables
1. Implémentons maintenant le

validate_transaction

pour assurer ces contrôles de sécurité.

// "Internal" functions, callable by code.
impl Module {
    pub fn validate_transaction(transaction: &Transaction) -> Result'static str> {
        // Check basic requirements
        ensure!(!transaction.inputs.is_empty(), "no inputs");
        ensure!(!transaction.outputs.is_empty(), "no outputs");

        {
            let input_set: BTreeMap<_, ()> =transaction.inputs.iter().map(|input| (input, ())).collect();
            ensure!(input_set.len() == transaction.inputs.len(), "each input must only be used once");
        }
        {
            let output_set: BTreeMap<_, ()> = transaction.outputs.iter().map(|output| (output, ())).collect();
            ensure!(output_set.len() == transaction.outputs.len(), "each output must be defined only once");
        }

        let mut total_input: Value = 0;
        let mut total_output: Value = 0;
        let mut output_index: u64 = 0;
        let simple_transaction = Self::get_simple_transaction(transaction);

        // Variables sent to transaction pool
        let mut missing_utxos = Vec::new();
        let mut new_utxos = Vec::new();
        let mut reward = 0;

        // Check that inputs are valid
        for input in transaction.inputs.iter() {
            if let Some(input_utxo) = ::get(&input.outpoint) {
                ensure!(sp_io::crypto::sr25519_verify(
                    &Signature::from_raw(*input.sigscript.as_fixed_bytes()),
                    &simple_transaction,
                    &Public::from_h256(input_utxo.pubkey)
                ), "signature must be valid" );
                total_input = total_input.checked_add(input_utxo.value).ok_or("input value overflow")?;
            } else {
                missing_utxos.push(input.outpoint.clone().as_fixed_bytes().to_vec());
            }
        }

        // Check that outputs are valid
        for output in transaction.outputs.iter() {
            ensure!(output.value > 0, "output value must be nonzero");
            let hash = BlakeTwo256::hash_of(&(&transaction.encode(), output_index));
            output_index = output_index.checked_add(1).ok_or("output index overflow")?;
            ensure!(!::exists(hash), "output already exists");
            total_output = total_output.checked_add(output.value).ok_or("output value overflow")?;
            new_utxos.push(hash.as_fixed_bytes().to_vec());
        }
        
        // If no race condition, check the math
        if missing_utxos.is_empty() {
            ensure!( total_input >= total_output, "output value must not exceed input value");
            reward = total_input.checked_sub(total_output).ok_or("reward underflow")?;
        }

        // Returns transaction details
        Ok(ValidTransaction {
            requires: missing_utxos,
            provides: new_utxos,
            priority: reward as u64,
            longevity: TransactionLongevity::max_value(),
            propagate: true,
        })
    }
}
2. Dans la partie 1, nous avons supposé utiliser quelques fonctions auxiliaires internes. À savoir l'étape où nous mettons réellement à jour le stockage de la blockchain, lorsque nos transactions sont validées. Dans le même

impl Module

portée, procédez comme suit:

/// Update storage to reflect changes made by transaction
    /// Where each utxo key is a hash of the entire transaction and its order in the TransactionOutputs vector
    fn update_storage(transaction: &Transaction, reward: Value) -> DispatchResult {
        // Calculate new reward total
        let new_total = ::get()
            .checked_add(reward)
            .ok_or("Reward overflow")?;
        ::put(new_total);

        // Removing spent UTXOs
        for input in &transaction.inputs {
            ::remove(input.outpoint);
        }

        let mut index: u64 = 0;
        for output in &transaction.outputs {
            let hash = BlakeTwo256::hash_of(&(&transaction.encode(), index));
            index = index.checked_add(1).ok_or("output index overflow")?;
            ::insert(hash, output);
        }

        Ok(())
    }
Aussi bien que

get_simple_transaction

:

    // Strips a transaction of its Signature fields by replacing value with ZERO-initialized fixed hash.
    pub fn get_simple_transaction(transaction: &Transaction) -> Vec<u8> {//&'a (u8) {
        let mut trx = transaction.clone();
        for input in trx.inputs.iter_mut() {
            input.sigscript = H512::zero();
        }

        trx.encode()
    }

Partie 3: Testez votre code

Dans cette courte partie, vous apprendrez à construire un environnement de test de blockchain, à créer un état initial avant chaque test, ainsi qu'à utiliser des fonctions d'assistance pratiques qui sont disponibles pour les tests dans Substrate / Rust.

1. Construisez votre environnement de test:

// This function basically just builds a genesis storage key/value store according to our desired mockup.
    // We start each test by giving Alice 100 utxo to start with.
    fn new_test_ext() -> sp_io::TestExternalities {
    
        let keystore = KeyStore::new(); // a key storage to store new key pairs during testing
        let alice_pub_key = keystore.write().sr25519_generate_new(SR25519, Some(ALICE_PHRASE)).unwrap();

        let mut t = system::GenesisConfig::default()
            .build_storage::()
            .unwrap();

        t.top.extend(
            GenesisConfig {
                genesis_utxos: vec!(
                    TransactionOutput {
                        value: 100,
                        pubkey: H256::from(alice_pub_key),
                    }
                ),
                ..Default::default()
            }
            .build_storage()
            .unwrap()
            .top,
        );
        
        // Print the values to get GENESIS_UTXO
        let mut ext = sp_io::TestExternalities::from(t);
        ext.register_extension(KeystoreExt(keystore));
        ext
    }
Cette fonction crée une clé de stockage / valeur de stockage genesis selon le code que nous avons écrit à l'étape 1 au cours

decl_storage

. Nous commençons simplement chaque test en donnant à Alice un UTXO de valeur 100 pour commencer à dépenser.

2. Écrivez un test unitaire simple, testez une transaction simple

    #(test)
    fn test_simple_transaction() {
        new_test_ext().execute_with(|| {
            let alice_pub_key = sp_io::crypto::sr25519_public_keys(SR25519)(0);

            // Alice wants to send herself a new utxo of value 50.
            let mut transaction = Transaction {
                inputs: vec!(TransactionInput {
                    outpoint: H256::from(GENESIS_UTXO),
                    sigscript: H512::zero(),
                }),
                outputs: vec!(TransactionOutput {
                    value: 50,
                    pubkey: H256::from(alice_pub_key),
                }),
            };
            
            let alice_signature = sp_io::crypto::sr25519_sign(SR25519, &alice_pub_key, &transaction.encode()).unwrap();
            transaction.inputs(0).sigscript = H512::from(alice_signature);
            let new_utxo_hash = BlakeTwo256::hash_of(&(&transaction.encode(), 0 as u64)); 
            
            assert_ok!(Utxo::spend(Origin::signed(0), transaction));
            assert!(!UtxoStore::exists(H256::from(GENESIS_UTXO)));
            assert!(UtxoStore::exists(new_utxo_hash));
            assert_eq!(50, UtxoStore::get(new_utxo_hash).unwrap().value);
        });
    }

3. N'oubliez pas les constantes pratiques!

    // need to manually import this crate since its no include by default
    use hex_literal::hex;

    const ALICE_PHRASE: &str = "news slush supreme milk chapter athlete soap sausage put clutch what kitten";
    const GENESIS_UTXO: (u8; 32) = hex!("79eabcbd5ef6e958c6a7851b36da07691c19bda1835a08f875aa286911800999");
    
4. Exécutez votre test dans la console avec

cargo test -p utxo-runtime

Et votre test devrait réussir, ce qui signifie que votre blockchain UTXO de base est terminée!

Coincé à ce stade? Check-out ce repo à quoi pourrait ressembler votre implémentation actuelle.

Partie 4: Configurer la logique du pool de transactions

Dans cette section, vous allez modifier la façon dont votre réseau priorise les transactions entrantes et gère une condition de concurrence UTXO ennuyeuse que Bitcoin rencontre. Plus précisément, vous apprendrez à changer la logique de mise en file d'attente des transactions de la blockchain sans trop de code.

Considérez la condition de concurrence suivante, où Alice envoie à Bob son UTXO A, créant un nouveau UTXO B appartenant à Bob.

Ce qui se passe dans les coulisses, c'est que la transaction d'Alice commence à se propager sur les nœuds du réseau:

Immédiatement, Bob décide de dépenser cet UTXO B, créant un UTXO C.

Ainsi, sa transaction commence à se propager à travers le réseau vers des nœuds qui n'ont pas encore entendu parler d'Alice! Cela peut être courant, en raison de la latence régulière du réseau et d'autres contraintes du monde réel.

Les nœuds qui ont entendu parler de Bob mais pas encore d'Alice, rejetteront sa transaction car UTXO B n'existe pas encore dans leur état blockchain. Mais la transaction de Bob EST valide, donc cette erreur due à cette condition de concurrence n'est pas idéale.

Idéalement, nous pouvons mettre en file d'attente des transactions valides dans un pool de réseaux et attendre que les conditions préalables soient remplies.

1. Heureusement, Substrate permet à un seul appel d'API de modifier la logique de commande des transactions. Configurer le

runtime_api::TaggedTransactionQueue

trait comme suit:

    impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime {
        fn validate_transaction(tx: as BlockT>::Extrinsic) -> TransactionValidity {            
            // Extrinsics representing UTXO transaction need some special handling
            if let Some(&utxo::Call::spend(ref transaction)) = IsSubType::, Runtime>::is_sub_type(&tx.function) {
                match >::validate_transaction(&transaction) {
                    // Transaction verification failed
                    Err(e) => {
                        sp_runtime::print(e);
                        return Err(TransactionValidityError::Invalid(InvalidTransaction::Custom(1)));
                    }
                    // Race condition, or Transaction is good to go
                    Ok(tv) => { return Ok(tv); }
                }                
            }

            // Fall back to default logic for non UTXO::execute extrinsics
            Executive::validate_transaction(tx)
        }
    }

En effet, nous demandons à la file d'attente de transactions d'attendre que le hachage d'un UTXO requis existe, avant de traiter une nouvelle transaction qui dépense cet UTXO. Le pool de transactions conservera les race-UTXO jusqu'à ce que cette condition soit satisfaite pendant un certain temps.

Partie 5: Touche finale et déploiement!

Dans cette dernière section, nous sommes prêts à configurer nos spécifications de déploiement de la chaîne de blocs, à désigner ce qui sera inclus dans le bloc Genesis et à déployer!

1. Entrez dans le

src

sous-répertoire maintenant, et trouvez le

chain_spec.rs

fichier.

2. Dans

testnet_genesis

, ajoutez la configuration suivante pour configurer votre testnet avec les données de départ / UTXO.

// Dev mode genesis setup
fn testnet_genesis(
    initial_authorities: Vec<(AuraId, GrandpaId)>,
    root_key: AccountId,
    endowed_accounts: Vec,
    endowed_utxos: Vec,
    _enable_println: bool) -> GenesisConfig 
{
    GenesisConfig {
      system: Some(SystemConfig {
        code: WASM_BINARY.to_vec(),
        changes_trie_config: Default::default(),
      }),
      ...
      utxo: Some(utxo::GenesisConfig {
        genesis_utxos: endowed_utxos
                      .iter()
                      .map(|x|
                        utxo::TransactionOutput {
                          value: 100 as utxo::Value,
                          pubkey: H256::from_slice(x.as_slice()),
                      })
                      .collect()
      }),
    }
}
3. Dans

fn load

, assurez-vous d'inclure également l'ensemble genesis de clés publiques qui devraient posséder ces UTXO.

// Genesis set of pubkeys that own UTXOs
                vec!(
                    get_from_seed::("Alice"),
                    get_from_seed::("Bob"),
                ),

4. Dans votre terminal, compilez et construisez une version de votre blockchain en mode développeur:

# Initialize your Wasm Build environment:
./scripts/init.sh

# Build Wasm and native code:
cargo build --release

5. Démarrez votre nœud et votre blockchain commencera à produire des blocs:

./target/release/utxo-workshop --dev

# If you already modified state, run this to purge the chain
./target/release/utxo-workshop purge-chain --dev
Pour une explication plus détaillée de la procédure pas à pas et ligne par ligne de ce qui précède, consultez ce dernière partie de la vidéo accompagnement.

Toutes nos félicitations! Vous avez maintenant créé et déployé une blockchain de type Bitcoin à partir de zéro.

Démo rapide de l'interface utilisateur

Dans ce didacticiel, vous avez appris à modifier le modèle de registre sous-jacent et à créer une chaîne Bitcoin sur le substrat. En fait, vous pouvez implémenter n'importe quel modèle de jeton fondamental sur votre chaîne. Vous pouvez modifier la façon dont votre réseau priorise diverses transactions, c'est-à-dire manipuler la couche réseau sans trop de code. Vous pouvez même modifier les structures économiques du validateur en utilisant des valeurs résiduelles pour récompenser vos valideurs. Vous pouvez facilement configurer votre bloc genesis.

J'espère que ce tutoriel vous a convaincu d'essayer les infrastructures de blockchain et de vérifier Substrat!

Des questions?

  • Envoyez-nous un ping sur Riot chat: https://riot.im/app/#/room/#substrate-technical:matrix.org
  • DM moi sur Twitter: https://twitter.com/nczhu
Photo de profil de l'auteur

Lisez mes histoires

Renforcer la confidentialité des données @Paritytech. Anciennement @gojekindonesia @IDEO @Stanford. Rusty dev

Mots clés

La bannière Noonification

Abonnez-vous pour obtenir votre récapitulatif quotidien des meilleures histoires technologiques!





Traduction de l’article de NicoleZhu : Article Original

BlockBlog

Le Meilleur de l'Actualité Blockchain Francophone & Internationale | News, Guides, Avis & Tutoriels pour s'informer et démarrer facilement avec Bitcoin, les Crypto-Monnaies et le Blockchain. En Savoir Plus sur L'Équipe BlockBlog

Commenter cet Article

Commenter cet Article

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Plus dans News

Les Plus Populaires

Acheter des Bitcoin

Acheter des Alt-Coins

Sécuriser vos Cryptos

Vêtements et Produits Dérivés

Top