L'article précédent a posé les bases conceptuelles pour la création d'une blockchain simplifiée. Nous allons maintenant concrétiser ces idées en développant "Tinychain", notre mini-blockchain, à travers la rédaction de code et des tests fonctionnels de base. Nous explorerons également des scénarios plus complexes, comme la gestion des bifurcations (forks) et l'ajustement de la difficulté de minage.
Architecture Logicielle
Conformément à notre analyse préalable, nous aborderons le développement de la blockchain de manière descendante, de la couche de service la plus élevée aux composants fondamentaux.
1. Le Serveur d'Application
Le point d'entrée de notre système est le serveur. Il est chargé de l'initialisation du service et de l'exécution de la boucle principale. Voici une ébauche de sa configuration :
#include "core_node.h"
#include "network_gateway.h"
#include "logger.h" // Assuming a custom logger
// Configuration du serveur
int main() {
CoreNode blockchain_instance;
NetworkGateway api_server("webroot_path", blockchain_instance); // Path pour les ressources web statiques
// Attachement du service d'écoute sur une interface réseau
auto& http_listener = api_server.attach_listener("0.0.0.0:8080"); // Écoute sur le port 8080
api_server.establish_http_websocket_protocol(&http_listener); // Configuration du protocole HTTP/WebSocket
Logger::info("Application", "Serveur API démarré sur http://0.0.0.0:8080");
// Lancement de la boucle principale du serveur
api_server.start_service();
return 0;
}
Nous instancions un objet CoreNode, qui représente le cœur de notre blockchain, puis nous le fournissons au NetworkGateway. Ce serveur remplit une double fonction : il propose des services aux clients locaux (gestion des commandes via RPC) et traite les nouvelles transactions ou blocs provenant du réseau externe. Il est l'interface principale du nœud.
2. Le Nœud Central (CoreNode)
Le CoreNode est l'entité centrale de la blockchain. Il encapsule la configuration fondamentale de la chaîne, incluant des paramètres pré-définis, comme un identifiant unique (souvent appelé "nombre magique") qui permet de distinguer cette blockchain des autres. Tout message non conforme à cet identifiant serait rejeté.
// Extrait du constructeur de la classe Blockchain
Blockchain(uint16_t chain_id_val = 4200) : chain_identifier_(chain_id_val) {
initialize_genesis_block(); // Création du bloc de genèse
}
Dans cette implémentation de Tinychain, l'identifiant de la chaîne (chain\_identifier\_) est codé en dur pour la simplicité. Un CoreNode intègre au minimum trois modules principaux : le réseau P2P, le cœur de la blockchain et le module de minage.
class CoreNode {
public:
// Interfaces publiques
void initiate_mining(WalletAddress target_address);
Blockchain& retrieve_chain() { return internal_blockchain_; }
PeerToPeerNetwork& get_network_interface() { return p2p_interface_; }
private:
PeerToPeerNetwork p2p_interface_;
Blockchain internal_blockchain_;
MiningModule mining_controller_{internal_blockchain_}; // Le module de minage dépend de la blockchain
};
Ces modules sont agrégés au sein du CoreNode, qui expose également des interfaces simplifiées pour la Blockchain et le MiningModule, facilitant ainsi leur appel par la couche serveur.
3. Le Cœur de la Blockchain (Blockchain)
Une instance de la classe Blockchain gère les éléments suivants :
class Blockchain {
private:
uint16_t identifier_chain_; // ID de la blockchain
Block initial_block_; // Le bloc de genèse
ChainDataStore block_registry_; // Base de données des blocs confirmés
KeyManagementSystem key_store_; // Base de données des paires de clés
TransactionMemoryPool pending_transactions_; // Pool de transactions non confirmées
};
initial_block_: Le bloc de genèse, le premier bloc de la chaîne, souvent pré-généré avec des informations fixes. Dans Tinychain, il peut être généré dynamiquement pour des tests facilités.block_registry_: Représente la base de données des blocs qui ont été validés et ajoutés à la chaîne principale. Bien que la persistance ne soit pas encore entièrement implémentée dans Tinychain, ce module est conçu pour stocker durablement ces données.pending_transactions_: Il s'agit d'un pool de transactions qui n'ont pas encore été confirmées par un bloc. Ces transactions sont sélectionnées lors du processus de minage pour être incluses dans le prochain bloc.
// La pool de transactions est utilisée pour peupler un nouveau bloc
new_block.incorporate_transactions(pending_transactions_);
key_store_: Un système de gestion des clés privées des utilisateurs, offrant des fonctionnalités d'administration des clés.
La classe Blockchain expose également des méthodes pour interagir avec ces composants :
class Blockchain {
public:
// Récupère la hauteur actuelle de la chaîne
uint64_t get_current_height() const { return block_registry_.get_height(); }
// Récupère le dernier bloc ajouté
Block fetch_latest_block();
// Recherche un bloc par son hachage
bool find_block_by_hash(HashType block_id, Block& found_block);
// Recherche une transaction par son hachage
bool find_transaction_by_hash(HashType tx_id, Transaction& found_tx);
// Récupère le solde d'une adresse donnée
bool get_account_balance(WalletAddress address, uint64_t& balance);
// Retourne l'ID de la blockchain
auto get_chain_id() const { return identifier_chain_; }
// Récupère la pool de transactions en attente
TransactionMemoryPool& get_transaction_pool() { return pending_transactions_; }
// Vide la pool de transactions après l'inclusion dans un bloc
void clear_transaction_pool() { pending_transactions_.reset(); }
// Ajoute une transaction non confirmée à la pool
void add_unconfirmed_transaction(Transaction& tx) { pending_transactions_.add_transaction(tx); }
// Gère la fusion ou le remplacement d'une chaîne plus courte par une plus longue
void merge_or_replace_chain(const std::vector<block>& incoming_block_list);
};
</block>
En plus de ces interfaces, la Blockchain est capable de gérer la logique de réorganisation de la chaîne, par exemple, en basculant vers la branche la plus longue du réseau en cas de bifurcation.
4. Le Module Réseau (PeerToPeerNetwork)
Le module PeerToPeerNetwork gère les connexions entre les nœuds. Il maintient une liste d'adresses connues (PeerRegistry) et les connexions actives (ActiveConnections).
class PeerToPeerNetwork {
public:
// Diffuse un nouveau bloc à tous les pairs connectés
void propagate_block(const ChainBlock& new_block_data);
// Diffuse une nouvelle transaction à tous les pairs connectés
void distribute_transaction(const TxPayload& new_transaction);
// Traite les événements réseau entrants
void handle_incoming_event(NetworkEvent event_type, EventProcessor handler_func);
private:
PeerRegistry peer_list_; // Carnet d'adresses des pairs connus
ActiveConnections active_links_; // Connexions réseau établies
};
Le carnet d'adresses est mis à jour dynamiquement et résident en mémoire pendant l'exécution, puis persisté lors de l'arrêt du nœud. Les ActiveConnections représentent les canaux de communication actifs, utilisés par la méthode propagate\_block et distribute\_transaction pour diffuser les nouvelles informations générées localement. Les événements reçus du réseau P2P sont traités par handle\_incoming\_event, qui transmet ensuite les données pertinentes au module Blockchain.
5. Le Module de Consensus (MiningModule)
Le consensus est crucial pour la validation des données. Il s'applique à la fois aux transactions créées localement et aux blocs/transactions reçus du réseau. Les règles de validation sont uniformes dans les deux cas, gérées par des fonctions comme verify\_transaction\_integrity et confirm\_block\_validity.
class ConsensusValidator {
public:
// Vérifie la validité d'une transaction entrante
bool verify_transaction_integrity(const Transaction& submitted_tx);
// Confirme la validité d'un bloc proposé
bool confirm_block_validity(const Block& proposed_block);
};
En plus de la validation, ce module fournit l'infrastructure pour le minage. Qu'il s'agisse de minage individuel (solo) ou en pool, la classe MiningModule est essentielle.
class MiningModule {
public:
// Démarre le processus de minage
void initiate_mining_process(WalletAddress miner_address);
// Effectue une tentative de preuve de travail
bool attempt_proof_of_work(Block& candidate_block, WalletAddress target_recipient);
// Génère la transaction de récompense (coinbase)
Transaction generate_coinbase_reward(WalletAddress recipient_address);
private:
Blockchain& target_chain_; // Référence à la blockchain cible
};
Voici le détail de la fonction attempt_proof_of_work, appelée en boucle par la fonction initiate_mining_process :
bool MiningModule::attempt_proof_of_work(Block& candidate_block, WalletAddress target_recipient) {
auto& transaction_pool = target_chain_.get_transaction_pool();
const Block& previous_block = target_chain_.fetch_latest_block();
// Préparation des métadonnées du nouveau bloc
candidate_block.header.block_height = previous_block.header.block_height + 1;
candidate_block.header.previous_block_hash = previous_block.header.block_hash;
candidate_block.header.timestamp_utc = get_current_timestamp();
candidate_block.header.transaction_count = transaction_pool.get_transaction_count();
// Ajustement dynamique de la difficulté pour maintenir le rythme de création des blocs
uint64_t time_since_last_block = candidate_block.header.timestamp_utc - previous_block.header.timestamp_utc;
// La difficulté est ajustée pour viser environ 10 secondes par bloc.
if (time_since_last_block <= 12u) {
candidate_block.header.mining_difficulty = previous_block.header.mining_difficulty + 9500;
} else {
candidate_block.header.mining_difficulty = previous_block.header.mining_difficulty - 3500;
}
// Calcul de la valeur cible de hachage pour le minage
// La cible est l'intervalle maximal possible divisé par la difficulté actuelle
uint64_t mining_target_value = UINT64_MAX / candidate_block.header.mining_difficulty;
// Génération et ajout de la transaction de récompense (coinbase)
Transaction coinbase_tx = generate_coinbase_reward(target_recipient);
transaction_pool.add_transaction(coinbase_tx);
// Intégration des transactions en attente de la pool dans le bloc
candidate_block.populate_with_transactions(transaction_pool.get_all_transactions());
// Boucle de recherche du nonce (Preuve de Travail)
for (uint64_t nonce_candidate = 0; ; ++nonce_candidate) {
candidate_block.header.proof_of_work_nonce = nonce_candidate;
// Sérialisation du bloc pour le calcul de hachage
auto block_json_representation = candidate_block.serialize_to_json();
std::string block_hash_result = CryptoHashUtil::compute_sha256(block_json_representation);
// Comparaison du préfixe du hachage avec la cible
uint64_t hash_prefix_value = std::stoull(block_hash_result.substr(0, 16), nullptr, 16);
if (hash_prefix_value < mining_target_value) {
candidate_block.header.block_hash = block_hash_result;
Logger::info("Minage", "Nouveau bloc trouvé : " + block_json_representation.toStyledString());
return true;
}
}
}
Les étapes précédant la boucle de recherche du nonce peuvent être considérées comme une interface "getblocktemplate", une fonction RPC qui permet de partager l'état du minage avec d'autres mineurs, leur laissant la tâche de trouver le nonce par force brute.
6. Le Stockage de Données (ChainDataStore & WalletKeyStorage)
Les classes de base de données fournissent des services de bas niveau, notamment la persistance des blocs et des paires de clés, ainsi que des interfaces de recherche pour les transactions et les blocs. block_registry_ et key_store_ de la classe Blockchain dérivent de ces concepts.
WalletKeyStorage
class WalletKeyStorage {
public:
// Génère une nouvelle paire de clés (privée/publique)
KeyPair generate_new_pair();
// Liste toutes les paires de clés stockées
const std::vector<KeyPair>& get_all_keys() const;
private:
std::vector<KeyPair> stored_key_pairs_; // Stockage interne des paires de clés
};
ChainDataStore
class ChainDataStore {
public:
uint64_t get_chain_length(); // Retourne le nombre de blocs dans la chaîne
Block retrieve_latest_block(); // Récupère le bloc le plus récent
bool retrieve_block_by_hash(const HashType& hash_id, Block& output_block); // Recherche un bloc par son hachage
bool retrieve_transaction_by_hash(const HashType& tx_id, Transaction& output_tx); // Recherche une transaction par son hachage
bool add_block_to_chain(const Block& new_block); // Ajoute un bloc validé à la chaîne
bool remove_block_from_chain(const HashType& block_id_to_remove); // Supprime un bloc de la chaîne (pour les réorganisations)
private:
std::map<HashType, Block> blockchain_map_storage_; // Stockage des blocs par leur hachage
};
7. Le Gestionnaire de Commandes (CommandProcessor)
Ce module offre une interface pour les interactions en ligne de commande, permettant aux développeurs d'exécuter des opérations.
class CommandProcessor {
public:
// Exécute une commande et renvoie la sortie au format JSON
bool process_command(Json::Value& response_output);
// Liste statique des commandes reconnues
static const std::vector<std::string> recognized_commands;
private:
std::vector<std::string> arguments_parsed_; // Arguments de la commande
CoreNode& blockchain_system_ref_; // Référence au nœud central pour l'exécution
};
Une liste de commandes est d'abord définie, puis la méthode process\_command est utilisée pour les exécuter, en s'appuyant sur l'objet CoreNode. La complexité de cette classe peut augmenter considérablement avec le nombre de fonctionnalités offertes par le portefeuille.
De plus, les commandes peuvent être invoquées via JSON-RPC, transformant le client en ligne de commande en un client HTTP léger.
#include "http_client.h" // Assuming a custom HTTP client
// Appel de commandes via une requête HTTP
std::string rpc_endpoint = "http://localhost:8080/api"; // Endpoint RPC du serveur
HttpClient rpc_client(rpc_endpoint, 5000 /*ms timeout*/, [](const std::string& reply) {
// Logique de traitement de la réponse du serveur
Logger::info("RPC Client", "Réponse du serveur: " + reply);
});
8. Les Classes de Base
Ces classes sont les unités fondamentales pour la génération de paires de clés, la construction de transactions (Transaction) et la création de blocs (ChainBlock).
AsymmetricKeyPair (Paire de Clés)
class AsymmetricKeyPair {
public:
AsymmetricKeyPair() {
private_key_data_ = CryptoUtils::generate_private_key(); // Génération de la clé privée
public_key_data_ = CryptoUtils::derive_public_key(private_key_data_); // Dérivation de la clé publique
}
std::string get_wallet_address() const; // Adresse du portefeuille dérivée de la clé publique
std::string get_public_key_string() const; // Représentation string de la clé publique
std::string get_private_key_string() const; // Représentation string de la clé privée
// ... interfaces de sérialisation (par exemple, vers JSON)
private:
std::string private_key_data_;
std::string public_key_data_;
};
Transaction (Tx)
class Transaction {
public:
const std::vector<TxInput>& get_transaction_inputs() const { return transaction_inputs_; }
const std::vector<TxOutput>& get_transaction_outputs() const { return transaction_outputs_; }
std::string calculate_transaction_hash() const; // Calcul du hachage de la transaction
private:
std::vector<TxInput> transaction_inputs_;
std::vector<TxOutput> transaction_outputs_;
std::string transaction_id_hash_; // Hachage (ID) de la transaction
};
ChainBlock (Bloc)
class ChainBlock {
public:
using TransactionList = std::vector<Transaction>;
struct BlockMetadata {
uint64_t proof_of_work_nonce{0};
uint64_t block_height{0};
uint64_t creation_timestamp{0};
uint64_t transaction_count{0};
uint64_t mining_difficulty{0};
std::string block_hash;
std::string merkle_tree_root; // Racine de l'arbre de Merkle des transactions
std::string previous_block_hash;
};
// ... autres interfaces et fonctions de sérialisation
std::string serialize_to_string() const {
Json::Value json_rep = to_json_representation();
return json_rep.toStyledString();
}
std::string get_current_block_hash() const { return block_header_data_.block_hash; }
void set_transactions(TransactionList& incoming_transactions) { transaction_payload_.swap(incoming_transactions); }
private:
BlockMetadata block_header_data_;
TransactionList transaction_payload_;
};
Première Exécution et Minage
Après avoir mis en place les classes de base et la structure architecturale, nous pouvons compiler et lancer Tinychain. Une fois compilé, vous devriez trouver l'exécutable principal (par exemple, tinychain) et le client en ligne de commande (par exemple, cli-tinychain).
Le serveur Tinychain inclut également une interface WebSocket visualisable. En plaçant les fichiers web statiques (comme index.html) dans un dossier webroot au même niveau que l'exécutable, vous pouvez accéder à l'interface via votre navigateur à http://127.0.0.1:8080. Cette page est conçue pour surveiller la blockchain et envoyer des commandes.
Lancement Initial
Lancez le programme principal :
./tinychain
Une fois que les logs indiqunet que le nœud et le serveur sont démarrés, vous pouvez commencer à interagir. Les journaux de debug (debug.log) et d'erreur (error.log) sont disponibles dans le même répertoire pour surveiller l'activité.
Premier Minage
Lorsque Tinychain est démarré, le processus de minage peut commencer. Initialement, la découverte de blocs peut être rapide, mais la difficulté s'ajustera pour stabiliser le rythme à environ 10 secondes par bloc. Si aucun bloc n'est trouvé pendant une période prolongée, la difficulté sera automatiquement réduite. L'ajustement dynamique de la difficulté est crucial pour la stabilité de la blockchain.
20231027T100501 INFO [Application] Serveur API démarré sur http://0.0.0.0:8080
20231027T100501 INFO [Node] Nœud blockchain initialisé
20231027T100510 INFO [Minage] Nouveau bloc trouvé :{
"header" :
{
"block_hash" : "",
"block_height" : 1,
"creation_timestamp" : 1698391510,
"merkle_tree_root" : "",
"mining_difficulty" : 9501,
"proof_of_work_nonce" : 0,
"previous_block_hash" : "00b586611d6f2580e1ea0773ec8b684dc4acf231710519e6272ed7d0c61ed43e",
"transaction_count" : 1
},
"transactions" :
[
{
"hash_id" : "cddf6e838eff470d81155cb4c26fd3a7615b94a00e82f99b1fd9f583d7bc0659",
"inputs" :
[
{
"hash_prev_output" : "00000000000000000000000000000000",
"index_prev_output" : 0
}
],
"outputs" :
[
{
"recipient_address" : "122b03d11a622ac3384904948c4d808",
"value" : 1000
}
]
}
]
}
20231027T100510 INFO [Minage] Nouveau bloc trouvé :0de5c36420aab2f7fc9413cfbd21bece697a349106771dc58b25a6a099d6aa86
20231027T100515 INFO [Minage] Nouveau bloc trouvé :{
"header" :
{
"block_hash" : "",
"block_height" : 2,
"creation_timestamp" : 1698391515,
"merkle_tree_root" : "",
"mining_difficulty" : 19001,
"proof_of_work_nonce" : 6048,
"previous_block_hash" : "0de5c36420aab2f7fc9413cfbd21bece697a349106771dc58b25a6a099d6aa86",
"transaction_count" : 1
},
"transactions" :
[
{
"hash_id" : "cddf6e838eff470d81155cb4c26fd3a7615b94a00e82f99b1fd9f583d7bc0659",
"inputs" :
[
{
"hash_prev_output" : "00000000000000000000000000000000",
"index_prev_output" : 0
}
],
"outputs" :
[
{
"recipient_address" : "122b03d11a622ac3384904948c4d808",
"value" : 1000
}
]
}
]
}
Première Transaction
Tout en laissant le minage s'exécuter, nous pouvons créer et envoyer une transaction. Commencez par générer une nouvelle paire de clés et son adresse correspondante via le client en ligne de commande (par exemple, la commande getnewkey). Ensuite, envoyez une transaction depuis une adresse existante (par exemple, l'adresse de la transaction coinbase) vers la nouvelle adresse.
La transaction sera ajoutée au pool de transactions en attente, puis incluse dans un bloc subséquent par le mineur.
Gestion des Forks et des Fusions
Les bifurcations de la blockchain (forks) surviennent lorsque le réseau n'est pas unanime sur la chaîne principale, souvent à cause de comportements incohérents des mineurs, de partitions réseau ou d'incompatibilités de protocole. Dans Tinychain, si vous créez un environnement multi-nœuds (par exemple, via Docker) et simulez une partition réseau, vous pourriez observer l'émergence de plusieurs chaînes. La logique de merge\_or\_replace\_chain permet au nœud de rejoindre la chaîne la plus longue une fois la partition résolue.
Ce projet de mini-blockchain, bien que simplifié, met en évidence la complexité inhérente à la conception d'un système blockchain complet. Il ne s'agit pas seulement d'assembler des composants, mais aussi de gérer leurs interactions et de concevoir des architectures robustes.
Actuellement, Tinychain manque encore d'une implémentation complète du réseau P2P, d'une intégration robuste des paires de clés RSA et de la validation détaillée des transactions et des blocs dans le module de consensus. Ces aspects peuvent être développés ultérieurement.