Cómo construir una cadena de bloques en la programación de Rust

Aprenda a crear una aplicación blockchain con un esquema de minería básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

Cuando pensamos en la tecnología P2P y para qué se usa hoy en día, es imposible no pensar inmediatamente en la tecnología blockchain. Pocos temas de TI han sido tan publicitados o controvertidos durante la última década como la tecnología blockchain y las criptomonedas.

Y si bien el amplio interés en la tecnología blockchain ha variado bastante, lo que, naturalmente, se debe al potencial monetario detrás de algunas de las criptomonedas más conocidas y utilizadas, una cosa está clara: sigue siendo relevante y no parece serlo. ir a cualquier parte.

En un artículo anterior , cubrimos cómo construir una aplicación peer-to-peer muy básica y funcional (aunque bastante ineficiente) en Rust. En este tutorial, demostraremos cómo crear una aplicación blockchain con un esquema de minería básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

Cubriremos lo siguiente en detalle:

  • Por qué blockchain es emocionante
  • Escribir una aplicación blockchain en Rust
  • Configurando nuestra aplicación Rust
  • Conceptos básicos de blockchain
  • Bloques, bloques, bloques
  • ¿Qué cadena usar?
  • Minería
  • Conceptos básicos de igual a igual
  • Manejo de mensajes entrantes
  • Poniendolo todo junto
  • Probando nuestra cadena de bloques Rust

Por qué blockchain es emocionante

Si bien personalmente no estoy particularmente interesado en las criptomonedas o los juegos de azar financieros en general, encuentro muy atractiva la idea de descentralizar partes de nuestra infraestructura existente. Hay muchos grandes proyectos basados ​​en blockchain que tienen como objetivo abordar problemas sociales como el cambio climático, la desigualdad social, la privacidad y la transparencia gubernamental.

El potencial de la tecnología basada en la idea de un libro de contabilidad descentralizado, seguro y totalmente transparente que permite a los actores interactuar sin tener que establecer la confianza primero es tan revolucionario como parece. Será emocionante ver cuál de las ambiciosas ideas antes mencionadas despegará, ganará tracción y tendrá éxito en el futuro.

En resumen, la tecnología blockchain es emocionante, no solo por su potencial de cambio mundial, sino también desde una perspectiva técnica. Desde la criptografía, pasando por las redes de igual a igual, hasta los sofisticados algoritmos de consenso , el campo tiene bastantes temas fascinantes en los que sumergirse.

Escribir una aplicación blockchain en Rust

En esta guía, crearemos una aplicación blockchain muy simple desde cero usando Rust. Nuestra aplicación no será particularmente eficiente, segura o robusta, pero lo ayudará a comprender cómo algunos de los conceptos fundamentales detrás de los sistemas blockchain ampliamente conocidos se pueden implementar de una manera simple, explicando algunas de las ideas detrás de ellos.

No entraremos en todos los detalles de cada concepto, y la implementación tendrá algunas deficiencias graves. No querrá usar este proyecto para nada dentro de millas de un caso de uso de producción, pero el objetivo es construir algo con lo que pueda jugar, aplicar a sus propias ideas y examinar para familiarizarse más con la tecnología de Rust y blockchain. en general.

La atención se centrará en la parte técnica, es decir, cómo implementar algunos de los conceptos y cómo se combinan. No explicaremos qué es una cadena de bloques, ni tocaremos la minería, el consenso y cosas por el estilo más allá de lo necesario para este tutorial. Lo que más nos preocupará será cómo poner estas ideas, en una versión simplificada, en el código de Rust.

 

Además, no construiremos una criptomoneda o un sistema similar. Nuestro diseño es mucho más simple: cada nodo en la red puede agregar datos (cadenas) al libro mayor descentralizado (la cadena de bloques) extrayendo un bloque válido localmente y luego transmitiendo ese bloque.

Siempre que sea un bloque válido (veremos más adelante lo que esto significa), cada nodo agregará el bloque a su cadena y nuestro dato se convertirá en parte de un bloque descentralizado, a prueba de manipulaciones, indestructible (excepto que todas las notas se cierran en nuestro caso) red!

Obviamente, este es un diseño bastante simplificado y algo artificial que se toparía con problemas de eficiencia y robustez con bastante rapidez al escalar. Pero como solo estamos haciendo este ejercicio para aprender, está bien. Si llega al final y tiene algo de motivación, puede extenderlo en la dirección que desee y tal vez construir la próxima gran cosa desde nuestros miserables comienzos aquí, ¡nunca se sabe!

Configurando nuestra aplicación Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust .

Primero, cree un nuevo proyecto de Rust:

cargo new rust-blockchain-example cd rust-blockchain-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará.

[dependencies]
chrono = "0.4"
sha2 = "0.9.8"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
libp2p = { version = "0.39", features = ["tcp-tokio", "mdns"] }
tokio = { version = "1.0", features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread", "sync", "time"] }
hex = "0.4"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Estamos usando libp2p como nuestra capa de red peer-to-peer y Tokio como nuestro tiempo de ejecución subyacente.

Usaremos la sha2biblioteca para nuestro hash sha256 y la hexcaja para transformar los hash binarios en hexadecimales legibles y transferibles.

Además de eso, en realidad solo hay utilidades como serdeJSON log, y pretty_env_loggerpara el registro, once_cellpara la inicialización estática y chronopara las marcas de tiempo.

Con la configuración fuera del camino, comencemos implementando los conceptos básicos de blockchain primero y luego, más adelante, poniéndolo todo en un contexto de red P2P.

Conceptos básicos de blockchain

Primero definamos nuestras estructuras de datos para nuestra cadena de bloques real:

pub struct App {
    pub blocks: Vec,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Block {
    pub id: u64,
    pub hash: String,
    pub previous_hash: String,
    pub timestamp: i64,
    pub data: String,
    pub nonce: u64,
}

Eso es todo, no hay mucho detrás, en realidad. Nuestra Appestructura esencialmente contiene nuestro estado de aplicación. No conservaremos la cadena de bloques en este ejemplo, por lo que desaparecerá una vez que detengamos la aplicación.

Este estado es simplemente una lista de Blocks. Agregaremos nuevos bloques al final de esta lista y esta será nuestra estructura de datos de blockchain.

La lógica real hará que esta lista de bloques sea una cadena de bloques, donde cada bloque hace referencia al hash del bloque anterior se implementará en nuestra lógica de aplicación. Sería posible construir una estructura de datos que ya sea compatible con la validación que necesitamos, pero este enfoque parece más simple y definitivamente apuntamos a la simplicidad aquí.

En Blocknuestro caso, A consistirá en an id, que es un índice que comienza en 0 contando hacia arriba. Luego, un hash sha256 (cuyo cálculo veremos más adelante), el hash del bloque anterior, una marca de tiempo, los datos contenidos en el bloque y un nonce, que también cubriremos cuando hablemos miningdel bloque.

Antes de comenzar con la minería, primero implementemos algunas de las funciones de validación que necesitamos para mantener nuestro estado consistente y algunos de los consensos básicos necesarios, para que cada cliente sepa qué blockchain es el correcto, en caso de que haya varios en conflicto.

Comenzamos implementando nuestra Appestructura:

impl App {
    fn new() -> Self {
        Self { blocks: vec![] }
    }

    fn genesis(&mut self) {
        let genesis_block = Block {
            id: 0,
            timestamp: Utc::now().timestamp(),
            previous_hash: String::from("genesis"),
            data: String::from("genesis!"),
            nonce: 2836,
            hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43".to_string(),
        };
        self.blocks.push(genesis_block);
    }
...
}

Inicializamos nuestra aplicación con una cadena vacía. Más adelante, implementaremos algo de lógica. Preguntamos a otros nodos al inicio por su cadena y, si es más larga que la nuestra, usamos la suya. Este es nuestro criterio de consenso simplista.

El genesismétodo crea el primer bloque codificado en nuestra cadena de bloques. Este es un bloque "especial" en el sentido de que realmente no se adhiere a las mismas reglas que el resto de los bloques. Por ejemplo, no tiene una validez previous_hash, ya que simplemente no había ningún bloque antes.

Necesitamos esto para “arrancar” nuestro nodo o, en realidad, toda la red cuando se inicia el primer nodo. La cadena tiene que empezar en alguna parte, y eso es todo.

Bloques, bloques, bloques

A continuación, agreguemos alguna funcionalidad que nos permita agregar nuevos bloques a la cadena.

impl App {
...
    fn try_add_block(&mut self, block: Block) {
        let latest_block = self.blocks.last().expect("there is at least one block");
        if self.is_block_valid(&block, latest_block) {
            self.blocks.push(block);
        } else {
            error!("could not add block - invalid");
        }
    }
...
}

Aquí, buscamos el último bloque de la cadena, nuestro previous block, y luego validamos si el bloque que nos gustaría agregar es realmente válido. Si no, simplemente registramos un error.

En nuestra sencilla aplicación, no implementaremos ningún manejo de errores reales. Como verá más adelante, si tenemos problemas con las condiciones de carrera entre los nodos y tenemos un estado no válido, nuestro nodo está básicamente roto.

Mencionaré algunas posibles soluciones a estos problemas, pero no las implementaremos aquí; Tenemos bastante terreno que cubrir incluso sin tener que preocuparnos por estos molestos problemas del mundo real.

Veamos a is_block_validcontinuación, una pieza central de nuestra lógica.

const DIFFICULTY_PREFIX: &str = "00";

fn hash_to_binary_representation(hash: &[u8]) -> String {
    let mut res: String = String::default();
    for c in hash {
        res.push_str(&format!("{:b}", c));
    }
    res
}

impl App {
...
    fn is_block_valid(&self, block: &Block, previous_block: &Block) -> bool {
        if block.previous_hash != previous_block.hash {
            warn!("block with id: {} has wrong previous hash", block.id);
            return false;
        } else if !hash_to_binary_representation(
            &hex::decode(&block.hash).expect("can decode from hex"),
        )
        .starts_with(DIFFICULTY_PREFIX)
        {
            warn!("block with id: {} has invalid difficulty", block.id);
            return false;
        } else if block.id != previous_block.id + 1 {
            warn!(
                "block with id: {} is not the next block after the latest: {}",
                block.id, previous_block.id
            );
            return false;
        } else if hex::encode(calculate_hash(
            block.id,
            block.timestamp,
            &block.previous_hash,
            &block.data,
            block.nonce,
        )) != block.hash
        {
            warn!("block with id: {} has invalid hash", block.id);
            return false;
        }
        true
    }
...
}

Primero definimos una constante DIFFICULTY_PREFIX. Esta es la base de nuestro esquema de minería muy simplista. Básicamente, cuando se extrae un bloque, la persona que realiza la extracción tiene que hacer un hash de los datos del bloque (con SHA256, en nuestro caso) y encontrar un hash, que, en binario, comienza con 00(dos ceros). Esto también denota nuestra "dificultad" en la red.

Como puede imaginar, el tiempo para encontrar un hash adecuado aumenta bastante si queremos tres, cuatro, cinco o incluso 20 ceros a la izquierda. En un sistema de cadena de bloques "real", esta dificultad sería un atributo de red, que se acuerda entre los nodos en función de un algoritmo de consenso y en función de la potencia de hash de la red, por lo que la red puede garantizar la producción de un nuevo bloque en una determinada cantidad. de tiempo.

No nos ocuparemos de esto aquí. En aras de la simplicidad, simplemente lo codificaremos a dos ceros iniciales. Esto no toma mucho tiempo para computar en hardware normal, por lo que no tenemos que preocuparnos por esperar demasiado durante la prueba.

A continuación, tenemos una función auxiliar, que es simplemente la representación binaria de una matriz de bytes dada en forma de a String. Esto se utiliza para comprobar cómodamente si un hash se ajusta a nuestra DIFFICULTY_PREFIXcondición. Obviamente, hay formas mucho más elegantes y rápidas de hacer esto, pero esto es simple y funciona para nuestro caso.

Ahora a la lógica de validar un Block. Esto es importante porque garantiza que nuestra cadena de bloques se adhiera a su propiedad de cadena y sea difícil de manipular. La dificultad de cambiar algo aumenta con cada bloque, ya que tendría que volver a calcular (es decir, volver a extraer) el resto de la cadena para obtener una cadena válida nuevamente. Esto sería lo suficientemente caro como para desincentivarlo en un sistema blockchain real)

Hay algunas reglas generales que debe seguir:

  1. Las previous_hashnecesidades de hacer coincidir realmente el hash del último bloque de la cadena.
  2. Las hashnecesidades para comenzar con nuestro DIFFICULTY_PREFIX(es decir, dos ceros), lo que indica que se extrajo correctamente
  3. Las idnecesidades para ser el último ID incrementa en 1
  4. El hash debe ser realmente correcto; hash de los datos del bloque debe darnos el hash del bloque (de lo contrario, también podría crear un hash aleatorio comenzando con 001)

Si pensamos en esto como un sistema distribuido, es posible que observe que es posible tener problemas aquí. ¿Qué sucede si dos nodos extraen un bloque al mismo tiempo en función de la ID del bloque 5? Ambos crearían ID de bloque 6con el bloque anterior apuntando a ID de bloque 5.

Entonces nos enviarían ambos bloques. Los validaríamos y agregaríamos el primero que ingrese, pero el segundo se descartaría durante la validación ya que ya tenemos un bloque con ID 6.

Este es un problema inherente en un sistema como este y la razón por la que debe haber un algoritmo de consenso entre los nodos para decidir qué bloques (es decir, qué cadena) acordar y usar.

De manera óptima, si el bloque que extrajo no se agrega a la cadena acordada, tendrá que extraerlo nuevamente y esperar que funcione mejor la próxima vez. En nuestro caso simple aquí, este mecanismo de reintento no se implementará; si ocurre tal carrera, ese nodo está esencialmente fuera del juego.

Hay enfoques más sofisticados para solucionar esto en el espacio de la cadena de bloques, por supuesto. Por ejemplo, si enviáramos nuestros datos como "transacciones" a otros nodos y los nodos minarían bloques con un conjunto de transacciones, esto se mitigaría un poco. Pero entonces todos minarían todo el tiempo y el más rápido gana. Entonces, como puede ver, esto generaría problemas adicionales, pero menos graves, que tendríamos que solucionar.

De todos modos, nuestro enfoque simple funcionará para nuestra red de prueba local.

¿Qué cadena usar?

Ahora que podemos validar un bloque, implementemos la lógica para validar una cadena completa:

impl App {
...
    fn is_chain_valid(&self, chain: &[Block]) -> bool {
        for i in 0..chain.len() {
            if i == 0 {
                continue;
            }
            let first = chain.get(i - 1).expect("has to exist");
            let second = chain.get(i).expect("has to exist");
            if !self.is_block_valid(second, first) {
                return false;
            }
        }
        true
    }
...
}

Ignorando el bloque de génesis, básicamente revisamos todos los bloques y los validamos. Si un bloque falla en la validación, fallamos en toda la cadena.

Queda un método más Appque nos ayudará a elegir qué cadena usar:

impl App {
...
    // We always choose the longest valid chain
    fn choose_chain(&mut self, local: Vec, remote: Vec) -> Vec {
        let is_local_valid = self.is_chain_valid(&local);
        let is_remote_valid = self.is_chain_valid(&remote);

        if is_local_valid && is_remote_valid {
            if local.len() >= remote.len() {
                local
            } else {
                remote
            }
        } else if is_remote_valid && !is_local_valid {
            remote
        } else if !is_remote_valid && is_local_valid {
            local
        } else {
            panic!("local and remote chains are both invalid");
        }
    }
}

Esto sucede si le pedimos a otro nodo su cadena para determinar si es "mejor" (según nuestro algoritmo de consenso) que la local.

Nuestro criterio es simplemente la longitud de la cadena. En los sistemas reales, suele haber más factores, como la dificultad que se tiene en cuenta y muchas otras posibilidades. Para el propósito de este ejercicio, si una cadena (válida) es más larga que la otra, tomamos esa.

Validamos tanto nuestra cadena local como la remota y tomamos la más larga. También podremos utilizar esta funcionalidad durante el inicio cuando solicitemos a otros nodos su cadena y. Dado que el nuestro solo incluye un bloque de génesis, inmediatamente nos pondremos al día con la cadena "acordada".

Minería

Para terminar nuestra lógica relacionada con blockchain, implementemos nuestro esquema de minería básico.

impl Block {
    pub fn new(id: u64, previous_hash: String, data: String) -> Self {
        let now = Utc::now();
        let (nonce, hash) = mine_block(id, now.timestamp(), &previous_hash, &data);
        Self {
            id,
            hash,
            timestamp: now.timestamp(),
            previous_hash,
            data,
            nonce,
        }
    }
}

Cuando se crea un nuevo bloque, llamamos mine_block, que devolverá ay noncea hash. Luego podemos crear el bloque con su marca de tiempo, los datos dados, ID, hash anterior y el nuevo hash y nonce.

Hablamos de todos los campos anteriores, excepto del nonce. Para explicar qué es esto, veamos la mine_blockfunción:

fn mine_block(id: u64, timestamp: i64, previous_hash: &str, data: &str) -> (u64, String) {
    info!("mining block...");
    let mut nonce = 0;

    loop {
        if nonce % 100000 == 0 {
            info!("nonce: {}", nonce);
        }
        let hash = calculate_hash(id, timestamp, previous_hash, data, nonce);
        let binary_hash = hash_to_binary_representation(&hash);
        if binary_hash.starts_with(DIFFICULTY_PREFIX) {
            info!(
                "mined! nonce: {}, hash: {}, binary hash: {}",
                nonce,
                hex::encode(&hash),
                binary_hash
            );
            return (nonce, hex::encode(hash));
        }
        nonce += 1;
    }
}

Después de anunciar que estamos a punto de extraer un bloque, establecemos el valor nonceen 0.

Luego, comenzamos un ciclo sin fin, que incrementa el nonceen cada paso. Dentro del ciclo, además de registrar cada iteración de 100000 para tener un indicador de progreso aproximado, calculamos un hash sobre los datos del bloque usando calculate_hash, que veremos a continuación.

Luego, usamos nuestro hash_to_binary_representationayudante y verificamos si el hash calculado se adhiere a nuestro criterio de dificultad de comenzar con dos ceros.

Si es así, lo registramos y devolvemos el nonce, el entero creciente, dónde sucedió, y el hash (codificado en hexadecimal). De lo contrario, lo incrementamos noncey volvemos a ir.

Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el noncey un número, que, junto con nuestros datos de bloque con hash usando SHA256, nos dará un hash que comienza con dos ceros.

Necesitamos registrar esto nonceen nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonceestá hash junto con los datos del bloque. Por ejemplo, si nos tomaría 52,342 iteraciones calcular un hash de ajuste (comenzando con dos ceros), noncesería 52341(1 menos, ya que comienza en 0).

Veamos también la utilidad para crear realmente el hash SHA256.

fn calculate_hash(id: u64, timestamp: i64, previous_hash: &str, data: &str, nonce: u64) -> Vec<u8> {
    let data = serde_json::json!({
        "id": id,
        "previous_hash": previous_hash,
        "data": data,
        "timestamp": timestamp,
        "nonce": nonce
    });
    let mut hasher = Sha256::new();
    hasher.update(data.to_string().as_bytes());
    hasher.finalize().as_slice().to_owned()
}

Este es bastante sencillo. Creamos una representación JSON de nuestros datos de bloque usando el nonce actual y lo sha2pasamos por el hash SHA256, devolviendo un Vec<u8>.

Eso es esencialmente toda nuestra lógica de blockchain implementada. Tenemos una estructura de datos blockchain: una lista de bloques. Tenemos bloques, que apuntan al bloque anterior. Estos deben tener un número de identificación creciente y un hash que se adhiera a nuestras reglas de minería.

Si pedimos obtener nuevos bloques de otros nodos, los validamos y, si están bien, los agregamos a la cadena. Si obtenemos una cadena de bloques completa de otro nodo, también la validamos y, si es más larga que la nuestra (es decir, tiene más bloques), reemplazamos nuestra propia cadena con ella.

Como puede imaginar, dado que cada nodo implementa esta lógica exacta, los bloques y las cadenas acordadas pueden propagarse a través de la red rápidamente y la red converge al mismo estado (como con las limitaciones de manejo de errores antes mencionadas en nuestro caso simple).

Conceptos básicos de igual a igual

A continuación, implementaremos la pila de red basada en P2P .

Comience por crear un p2p.rsarchivo, que contendrá la mayor parte de la lógica peer-to-peer que usaremos en nuestra aplicación.

Allí, nuevamente, definimos algunas estructuras de datos básicas y constantes que necesitaremos:

pub static KEYS: Lazy = Lazy::new(identity::Keypair::generate_ed25519);
pub static PEER_ID: Lazy = Lazy::new(|| PeerId::from(KEYS.public()));
pub static CHAIN_TOPIC: Lazy = Lazy::new(|| Topic::new("chains"));
pub static BLOCK_TOPIC: Lazy = Lazy::new(|| Topic::new("blocks"));

#[derive(Debug, Serialize, Deserialize)]
pub struct ChainResponse {
    pub blocks: Vec,
    pub receiver: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LocalChainRequest {
    pub from_peer_id: String,
}

pub enum EventType {
    LocalChainResponse(ChainResponse),
    Input(String),
    Init,
}

#[derive(NetworkBehaviour)]
pub struct AppBehaviour {
    pub floodsub: Floodsub,
    pub mdns: Mdns,
    #[behaviour(ignore)]
    pub response_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub init_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub app: App,
}

Comenzando desde arriba, definimos un par de claves y un ID de par derivado. Esos son simplemente los elementos intrínsecos de libp2p para identificar un cliente en la red.

Luego, definimos dos de los llamados topics: chainsy blocks. Usaremos el FloodSubprotocolo, un protocolo simple de publicación / suscripción, para la comunicación entre los nodos.

Esto tiene la ventaja de que es muy sencillo de configurar y usar, pero tiene la desventaja de que necesitamos transmitir cada pieza de información. Entonces, incluso si solo queremos responder a la “solicitud de nuestra cadena” de un cliente, ese cliente enviará esta solicitud a todos los nodos a los que están conectados en la red y también enviaremos nuestra respuesta a todos ellos.

Esto no es un problema en términos de corrección, pero en términos de eficiencia, es obviamente horrendo. Esto podría manejarse mediante un modelo simple de solicitud / respuesta punto a punto, que es algo que libp2p admite, pero esto simplemente agregaría aún más complejidad a este ejemplo ya complejo. Si está interesado, puede consultar los documentos de libp2p .

También podríamos usar el más eficiente en GossipSublugar de FloodSub. Pero, nuevamente, no es tan conveniente de configurar y realmente no estamos particularmente interesados ​​en el rendimiento en este momento. La interfaz es muy similar. Nuevamente, si está interesado en jugar con esto, consulte los documentos oficiales .

De todos modos, los temas son básicamente "canales" a los que suscribirse. Podemos suscribirnos a "cadenas" y usarlas para enviar nuestra cadena de bloques local a otros nodos y recibir la suya. Lo mismo ocurre con los "bloques", que usaremos para transmitir y recibir nuevos bloques.

A continuación, tenemos el concepto de ChainResponsetener una lista de bloques y un receptor. Esta es una estructura, que esperaremos si alguien nos envía su cadena de bloques local y la usamos para enviarle nuestra cadena local.

El LocalChainRequestes lo que desencadena esta interacción. Si enviamos un LocalChainRequestcon el peer_idde otro nodo en el sistema, esto activará que nos envíen su cadena de regreso, como veremos más adelante.

Para manejar mensajes entrantes, inicialización diferida y entrada de teclado por parte del usuario del cliente, definimos la EventTypeenumeración, que nos ayudará a enviar eventos a través de la aplicación para mantener el estado de nuestra aplicación sincronizado con el tráfico de red entrante y saliente.

Finalmente, el núcleo de la funcionalidad P2P es nuestro AppBehaviour, que implementa NetworkBehaviourel concepto de libp2p para implementar una pila de red descentralizada.

No entraremos en el meollo de la cuestión aquí, pero mi tutorial completo de libp2p entra en más detalles sobre esto.

El AppBehavioursostiene nuestra instancia FloodSub para pub / sub comunicación y e instancia de acuses de recibo, lo que nos permitirá encontrar de forma automática otros nodos de nuestra red local (pero no fuera de ella).

También agregamos nuestra cadena Appde bloques a este comportamiento, así como canales para enviar eventos tanto para la inicialización como para la comunicación de solicitud / respuesta entre partes de la aplicación. Veremos esto en acción más adelante.

Inicializar AppBehaviourtambién es bastante sencillo:

impl AppBehaviour {
    pub async fn new(
        app: App,
        response_sender: mpsc::UnboundedSender,
        init_sender: mpsc::UnboundedSender,
    ) -> Self {
        let mut behaviour = Self {
            app,
            floodsub: Floodsub::new(*PEER_ID),
            mdns: Mdns::new(Default::default())
                .await
                .expect("can create mdns"),
            response_sender,
            init_sender,
        };
        behaviour.floodsub.subscribe(CHAIN_TOPIC.clone());
        behaviour.floodsub.subscribe(BLOCK_TOPIC.clone());

        behaviour
    }
}

Manejo de mensajes entrantes

Primero, implementamos los controladores para los datos que provienen de otros nodos.

Comenzaremos con los eventos de Mdns, ya que son básicamente un texto estándar:

impl NetworkBehaviourEventProcess<MdnsEvent> for AppBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

Si se descubre un nuevo nodo, lo agregamos a nuestra lista de nodos FloodSub para que podamos comunicarnos. Una vez que caduque, lo volvemos a quitar.

Más interesante es la implementación de NetworkBehaviourpara nuestro protocolo de comunicación FloodSub.

// incoming event handler
impl NetworkBehaviourEventProcess for AppBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        if let FloodsubEvent::Message(msg) = event {
            if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                if resp.receiver == PEER_ID.to_string() {
                    info!("Response from {}:", msg.source);
                    resp.blocks.iter().for_each(|r| info!("{:?}", r));

                    self.app.blocks = self.app.choose_chain(self.app.blocks.clone(), resp.blocks);
                }
            } else if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                info!("sending local chain to {}", msg.source.to_string());
                let peer_id = resp.from_peer_id;
                if PEER_ID.to_string() == peer_id {
                    if let Err(e) = self.response_sender.send(ChainResponse {
                        blocks: self.app.blocks.clone(),
                        receiver: msg.source.to_string(),
                    }) {
                        error!("error sending response via channel, {}", e);
                    }
                }
            } else if let Ok(block) = serde_json::from_slice::(&msg.data) {
                info!("received new block from {}", msg.source.to_string());
                self.app.try_add_block(block);
            }
        }
    }
}

Para los eventos entrantes, que son FloodsubEvent::Message, verificamos si la carga útil se ajusta a alguna de nuestras estructuras de datos esperadas.

Si es un ChainResponse, significa que otro nodo nos envió una cadena de bloques local.

Verificamos si en realidad somos el receptor de dicho dato y, de ser así, registramos la cadena de bloques entrante e intentamos ejecutar nuestro consenso. Si es válido y más largo que nuestra cadena, reemplazamos nuestra cadena con él. De lo contrario, mantenemos nuestra propia cadena.

Si los datos entrantes son a LocalChainRequest, verificamos si somos de quienes quieren la cadena, marcando el from_peer_id. Si es así, simplemente les enviamos una versión JSON de nuestra cadena de bloques local. La parte de envío real está en otra parte del código, pero por ahora, simplemente la enviamos a través de nuestro canal de eventos para obtener respuestas.

Finalmente, si Blockes entrante, significa que alguien más extrajo un bloque y quiere que lo agreguemos a nuestra cadena local. Comprobamos si el bloque es válido y, si lo es, lo añadimos.

Poniendolo todo junto

¡Excelente! Ahora conectemos todo esto y agreguemos algunos comandos para que los usuarios interactúen con la aplicación.

De vuelta main.rs, es hora de implementar la mainfunción.

Empezamos con la configuración:

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", p2p::PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();
    let (init_sender, mut init_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::::new()
        .into_authentic(&p2p::KEYS)
        .expect("can create auth keys");

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

    let behaviour = p2p::AppBehaviour::new(App::new(), response_sender, init_sender.clone()).await;

    let mut swarm = SwarmBuilder::new(transp, behaviour, *p2p::PEER_ID)
        .executor(Box::new(|fut| {
            spawn(fut);
        }))
        .build();

    let mut stdin = BufReader::new(stdin()).lines();

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

    spawn(async move {
        sleep(Duration::from_secs(1)).await;
        info!("sending init event");
        init_sender.send(true).expect("can send init event");
    });

Eso es mucho código, pero básicamente configura cosas de las que ya hablamos. Inicializamos el registro y nuestros dos canales de eventos para la inicialización y las respuestas.

Luego, inicializamos nuestro par de claves, el transporte, el comportamiento de libp2p y el enjambre de libp2p, que es la entidad que ejecuta nuestra pila de red.

También inicializamos un lector almacenado en búfer stdinpara que podamos leer los comandos entrantes del usuario e iniciar nuestro Swarm.

Finalmente, generamos una corrutina asincrónica, que espera un segundo y luego envía un disparador de inicialización en el canal de inicio.

Esta es la señal que usaremos después de iniciar un nodo para esperar un poco hasta que el nodo esté encendido y conectado. Luego le pedimos a otro nodo su cadena de bloques actual para ponernos al día.

El resto maines la parte interesante: la parte en la que manejamos los eventos de teclado del usuario, los datos entrantes y los datos salientes.

    loop {
        let evt = {
            select! {
                line = stdin.next_line() => Some(p2p::EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                response = response_rcv.recv() => {
                    Some(p2p::EventType::LocalChainResponse(response.expect("response exists")))
                },
                _init = init_rcv.recv() => {
                    Some(p2p::EventType::Init)
                }
                event = swarm.select_next_some() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
            }
        };

        if let Some(event) = evt {
            match event {
                p2p::EventType::Init => {
                    let peers = p2p::get_list_peers(&swarm);
                    swarm.behaviour_mut().app.genesis();

                    info!("connected nodes: {}", peers.len());
                    if !peers.is_empty() {
                        let req = p2p::LocalChainRequest {
                            from_peer_id: peers
                                .iter()
                                .last()
                                .expect("at least one peer")
                                .to_string(),
                        };

                        let json = serde_json::to_string(&req).expect("can jsonify request");
                        swarm
                            .behaviour_mut()
                            .floodsub
                            .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                    }
                }
                p2p::EventType::LocalChainResponse(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm
                        .behaviour_mut()
                        .floodsub
                        .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                }
                p2p::EventType::Input(line) => match line.as_str() {
                    "ls p" => p2p::handle_print_peers(&swarm),
                    cmd if cmd.starts_with("ls c") => p2p::handle_print_chain(&swarm),
                    cmd if cmd.starts_with("create b") => p2p::handle_create_block(cmd, &mut swarm),
                    _ => error!("unknown command"),
                },
            }
        }
    }

Comenzamos un ciclo sin fin y usamos la select!macro de Tokio para competir con múltiples funciones asíncronas.

Esto significa que cualquiera de estos acabados primero se manejará primero y luego comenzaremos de nuevo.

El primer emisor de eventos es nuestro lector en búfer, que nos dará líneas de entrada del usuario. Si obtenemos uno, creamos un EventType::Inputcon la línea.

Luego, escuchamos el canal de respuesta y el canal de inicio, creando sus eventos respectivamente.
Y si los eventos entran en el enjambre en sí, esto significa que son eventos que no son manejados por nuestro comportamiento Mdns ni nuestro comportamiento FloodSub y simplemente los registramos. En su mayoría son ruido, como conexión / desconexión en nuestro caso, pero útiles para la depuración.

Con los eventos correspondientes creados (o ningún evento creado), nos ocupamos de manejarlos.

Para nuestro Initevento, invocamos genesis()nuestra aplicación, creando nuestro bloque de génesis. Si estamos conectados a nodos, activamos LocalChainRequesta al último de la lista.

Obviamente, aquí tendría sentido preguntar a varios nodos, y tal vez varias veces, y seleccionar la mejor (es decir, la más larga) cadena de respuestas que obtenemos. Pero en aras de la simplicidad, solo pedimos uno y aceptamos lo que sea que nos envíen.

Luego, si obtenemos un LocalChainResponseevento, eso significa que se envió algo en el canal de respuesta. Si recuerda lo anterior, eso sucedió en nuestro comportamiento FloodSub cuando enviamos nuestra cadena de bloques local a un nodo solicitante. Aquí, en realidad enviamos el JSON entrante al tema FloodSub correcto, por lo que se transmite a la red.

Finalmente, para la entrada del usuario, tenemos tres comandos:

  • ls p enumera todos los compañeros
  • ls c imprime la cadena de bloques local
  • create b $datacrea un nuevo bloque con $datasu contenido de cadena

Cada comando llama a una de estas funciones auxiliares:

pub fn get_list_peers(swarm: &Swarm) -> Vec {
    info!("Discovered Peers:");
    let nodes = swarm.behaviour().mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().map(|p| p.to_string()).collect()
}

pub fn handle_print_peers(swarm: &Swarm) {
    let peers = get_list_peers(swarm);
    peers.iter().for_each(|p| info!("{}", p));
}

pub fn handle_print_chain(swarm: &Swarm) {
    info!("Local Blockchain:");
    let pretty_json =
        serde_json::to_string_pretty(&swarm.behaviour().app.blocks).expect("can jsonify blocks");
    info!("{}", pretty_json);
}

pub fn handle_create_block(cmd: &str, swarm: &mut Swarm) {
    if let Some(data) = cmd.strip_prefix("create b") {
        let behaviour = swarm.behaviour_mut();
        let latest_block = behaviour
            .app
            .blocks
            .last()
            .expect("there is at least one block");
        let block = Block::new(
            latest_block.id + 1,
            latest_block.hash.clone(),
            data.to_owned(),
        );
        let json = serde_json::to_string(&block).expect("can jsonify request");
        behaviour.app.blocks.push(block);
        info!("broadcasting new block");
        behaviour
            .floodsub
            .publish(BLOCK_TOPIC.clone(), json.as_bytes());
    }
}

Enumerar clientes e imprimir la cadena de bloques es bastante sencillo. Crear un bloque es más interesante.

En ese caso, usamos Block::newpara crear (y extraer) un nuevo bloque. Una vez que eso sucede, lo JSONificamos y lo transmitimos a la red para que otros puedan agregarlo a su cadena.

Aquí es donde pondríamos algo de lógica para r-intentar esto. Por ejemplo, podríamos agregarlo a una cola y ver si, después de un tiempo, nuestro bloque se propaga a la cadena de bloques ampliamente acordada y, de no ser así, obtener una nueva copia de la cadena acordada y extraerla nuevamente para obtenerla. alli. Como se mencionó anteriormente, este diseño ciertamente no se escalará a muchos nodos que extraigan sus bloques todo el tiempo, pero eso está bien para el propósito de este tutorial.

¡Empecemos y veamos si funciona!

Probando nuestra cadena de bloques Rust

Podemos iniciar la aplicación usando RUST_LOG=info cargo run. En realidad, es mejor iniciar varias instancias en diferentes ventanas de terminal.

Por ejemplo, podemos iniciar dos nodos:

INFO  rust_blockchain_example > Peer Id: 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Y:

INFO  rust_blockchain_example > Peer Id: 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

El uso ls pde la segunda aplicación nos muestra la conexión con la primera:

INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example::p2p > 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Luego, podemos usar ls cpara imprimir el bloque de génesis:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  }
]

Hasta aquí todo bien. creemos un bloque:

create b hello
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 62235, hash: 00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922, binary hash: 0010001100111101101000110110101001111110011111000101010101000101111110101010110110010011111110111000010100001011110001111000000110110111101100010111111100001011011110001111110100011111011000101111111001111110101001100010
 INFO  rust_blockchain_example::p2p > broadcasting new block

En el primer nodo, vemos esto:

INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

Y llamando ls c:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

¡El bloque se agregó!

Comencemos con un tercer nodo. Debería obtener automáticamente esta cadena actualizada porque es más larga que la suya (solo el bloque de génesis).

INFO  rust_blockchain_example > Peer Id: 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

 INFO  rust_blockchain_example > sending init event
 INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example      > connected nodes: 2
 INFO  rust_blockchain_example::p2p > Response from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX:
 INFO  rust_blockchain_example::p2p > Block { id: 0, hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", previous_hash: "genesis", timestamp: 1636664658, data: "genesis!", nonce: 2836 }
 INFO  rust_blockchain_example::p2p > Block { id: 1, hash: "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922", previous_hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", timestamp: 1636664772, data: " hello", nonce: 62235 }

Después de enviar el initevento, solicitamos la cadena del segundo nodo y la obtuvimos.

Llamar ls caquí nos muestra la misma cadena:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

La creación de un bloque también funciona:

create b alsoworks
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 34855, hash: 0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd, binary hash: 001110000010111101110111111001110110000011110110100110111010110111001101011100010001110100011011101001011101001101110101010101010100010101101100000110110111101110110010101011010110010100101111011110111000011111110111111101
 INFO  rust_blockchain_example::p2p > broadcasting new block

Nodo 1:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Nodo 2:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q
ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Genial, ¡funciona!

Puede jugar e intentar crear condiciones de carrera (por ejemplo, aumentando la dificultad a tres ceros y comenzando varios bloques en varios nodos. Notará inmediatamente algunos de los defectos de este diseño, pero los conceptos básicos funcionan. Tenemos un par -to-peer blockchain, un libro mayor descentralizado real con robustez básica, construido completamente desde cero en Rust.

Puede encontrar el código de ejemplo completo en GitHub .

Conclusión

En este tutorial, creamos una aplicación blockchain simple, bastante limitada, pero que funciona en Rust. Nuestra aplicación blockchain tiene un esquema de minería muy básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

La mayor parte de esta simplicidad se debe a la fantástica biblioteca libp2p , que hace todo el trabajo pesado en términos de redes. Claramente, como siempre es el caso en los tutoriales de ingeniería de software, para una aplicación de cadena de bloques de grado de producción, hay muchas, muchas más cosas que considerar y hacer bien.

Sin embargo, este ejercicio prepara el escenario para el tema, explicando algunos de los conceptos básicos y mostrándolos en Rust, de modo que podamos continuar este viaje observando cómo desarrollaríamos una aplicación blockchain que realmente podría usarse en la práctica con un marco como Substrate .

Fuente del artículo original en https://blog.logrocket.com

#rust # #blockchain 

What is GEEK

Buddha Community

Cómo construir una cadena de bloques en la programación de Rust

Cómo construir una cadena de bloques en la programación de Rust

Aprenda a crear una aplicación blockchain con un esquema de minería básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

Cuando pensamos en la tecnología P2P y para qué se usa hoy en día, es imposible no pensar inmediatamente en la tecnología blockchain. Pocos temas de TI han sido tan publicitados o controvertidos durante la última década como la tecnología blockchain y las criptomonedas.

Y si bien el amplio interés en la tecnología blockchain ha variado bastante, lo que, naturalmente, se debe al potencial monetario detrás de algunas de las criptomonedas más conocidas y utilizadas, una cosa está clara: sigue siendo relevante y no parece serlo. ir a cualquier parte.

En un artículo anterior , cubrimos cómo construir una aplicación peer-to-peer muy básica y funcional (aunque bastante ineficiente) en Rust. En este tutorial, demostraremos cómo crear una aplicación blockchain con un esquema de minería básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

Cubriremos lo siguiente en detalle:

  • Por qué blockchain es emocionante
  • Escribir una aplicación blockchain en Rust
  • Configurando nuestra aplicación Rust
  • Conceptos básicos de blockchain
  • Bloques, bloques, bloques
  • ¿Qué cadena usar?
  • Minería
  • Conceptos básicos de igual a igual
  • Manejo de mensajes entrantes
  • Poniendolo todo junto
  • Probando nuestra cadena de bloques Rust

Por qué blockchain es emocionante

Si bien personalmente no estoy particularmente interesado en las criptomonedas o los juegos de azar financieros en general, encuentro muy atractiva la idea de descentralizar partes de nuestra infraestructura existente. Hay muchos grandes proyectos basados ​​en blockchain que tienen como objetivo abordar problemas sociales como el cambio climático, la desigualdad social, la privacidad y la transparencia gubernamental.

El potencial de la tecnología basada en la idea de un libro de contabilidad descentralizado, seguro y totalmente transparente que permite a los actores interactuar sin tener que establecer la confianza primero es tan revolucionario como parece. Será emocionante ver cuál de las ambiciosas ideas antes mencionadas despegará, ganará tracción y tendrá éxito en el futuro.

En resumen, la tecnología blockchain es emocionante, no solo por su potencial de cambio mundial, sino también desde una perspectiva técnica. Desde la criptografía, pasando por las redes de igual a igual, hasta los sofisticados algoritmos de consenso , el campo tiene bastantes temas fascinantes en los que sumergirse.

Escribir una aplicación blockchain en Rust

En esta guía, crearemos una aplicación blockchain muy simple desde cero usando Rust. Nuestra aplicación no será particularmente eficiente, segura o robusta, pero lo ayudará a comprender cómo algunos de los conceptos fundamentales detrás de los sistemas blockchain ampliamente conocidos se pueden implementar de una manera simple, explicando algunas de las ideas detrás de ellos.

No entraremos en todos los detalles de cada concepto, y la implementación tendrá algunas deficiencias graves. No querrá usar este proyecto para nada dentro de millas de un caso de uso de producción, pero el objetivo es construir algo con lo que pueda jugar, aplicar a sus propias ideas y examinar para familiarizarse más con la tecnología de Rust y blockchain. en general.

La atención se centrará en la parte técnica, es decir, cómo implementar algunos de los conceptos y cómo se combinan. No explicaremos qué es una cadena de bloques, ni tocaremos la minería, el consenso y cosas por el estilo más allá de lo necesario para este tutorial. Lo que más nos preocupará será cómo poner estas ideas, en una versión simplificada, en el código de Rust.

 

Además, no construiremos una criptomoneda o un sistema similar. Nuestro diseño es mucho más simple: cada nodo en la red puede agregar datos (cadenas) al libro mayor descentralizado (la cadena de bloques) extrayendo un bloque válido localmente y luego transmitiendo ese bloque.

Siempre que sea un bloque válido (veremos más adelante lo que esto significa), cada nodo agregará el bloque a su cadena y nuestro dato se convertirá en parte de un bloque descentralizado, a prueba de manipulaciones, indestructible (excepto que todas las notas se cierran en nuestro caso) red!

Obviamente, este es un diseño bastante simplificado y algo artificial que se toparía con problemas de eficiencia y robustez con bastante rapidez al escalar. Pero como solo estamos haciendo este ejercicio para aprender, está bien. Si llega al final y tiene algo de motivación, puede extenderlo en la dirección que desee y tal vez construir la próxima gran cosa desde nuestros miserables comienzos aquí, ¡nunca se sabe!

Configurando nuestra aplicación Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust .

Primero, cree un nuevo proyecto de Rust:

cargo new rust-blockchain-example cd rust-blockchain-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará.

[dependencies]
chrono = "0.4"
sha2 = "0.9.8"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
libp2p = { version = "0.39", features = ["tcp-tokio", "mdns"] }
tokio = { version = "1.0", features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread", "sync", "time"] }
hex = "0.4"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Estamos usando libp2p como nuestra capa de red peer-to-peer y Tokio como nuestro tiempo de ejecución subyacente.

Usaremos la sha2biblioteca para nuestro hash sha256 y la hexcaja para transformar los hash binarios en hexadecimales legibles y transferibles.

Además de eso, en realidad solo hay utilidades como serdeJSON log, y pretty_env_loggerpara el registro, once_cellpara la inicialización estática y chronopara las marcas de tiempo.

Con la configuración fuera del camino, comencemos implementando los conceptos básicos de blockchain primero y luego, más adelante, poniéndolo todo en un contexto de red P2P.

Conceptos básicos de blockchain

Primero definamos nuestras estructuras de datos para nuestra cadena de bloques real:

pub struct App {
    pub blocks: Vec,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Block {
    pub id: u64,
    pub hash: String,
    pub previous_hash: String,
    pub timestamp: i64,
    pub data: String,
    pub nonce: u64,
}

Eso es todo, no hay mucho detrás, en realidad. Nuestra Appestructura esencialmente contiene nuestro estado de aplicación. No conservaremos la cadena de bloques en este ejemplo, por lo que desaparecerá una vez que detengamos la aplicación.

Este estado es simplemente una lista de Blocks. Agregaremos nuevos bloques al final de esta lista y esta será nuestra estructura de datos de blockchain.

La lógica real hará que esta lista de bloques sea una cadena de bloques, donde cada bloque hace referencia al hash del bloque anterior se implementará en nuestra lógica de aplicación. Sería posible construir una estructura de datos que ya sea compatible con la validación que necesitamos, pero este enfoque parece más simple y definitivamente apuntamos a la simplicidad aquí.

En Blocknuestro caso, A consistirá en an id, que es un índice que comienza en 0 contando hacia arriba. Luego, un hash sha256 (cuyo cálculo veremos más adelante), el hash del bloque anterior, una marca de tiempo, los datos contenidos en el bloque y un nonce, que también cubriremos cuando hablemos miningdel bloque.

Antes de comenzar con la minería, primero implementemos algunas de las funciones de validación que necesitamos para mantener nuestro estado consistente y algunos de los consensos básicos necesarios, para que cada cliente sepa qué blockchain es el correcto, en caso de que haya varios en conflicto.

Comenzamos implementando nuestra Appestructura:

impl App {
    fn new() -> Self {
        Self { blocks: vec![] }
    }

    fn genesis(&mut self) {
        let genesis_block = Block {
            id: 0,
            timestamp: Utc::now().timestamp(),
            previous_hash: String::from("genesis"),
            data: String::from("genesis!"),
            nonce: 2836,
            hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43".to_string(),
        };
        self.blocks.push(genesis_block);
    }
...
}

Inicializamos nuestra aplicación con una cadena vacía. Más adelante, implementaremos algo de lógica. Preguntamos a otros nodos al inicio por su cadena y, si es más larga que la nuestra, usamos la suya. Este es nuestro criterio de consenso simplista.

El genesismétodo crea el primer bloque codificado en nuestra cadena de bloques. Este es un bloque "especial" en el sentido de que realmente no se adhiere a las mismas reglas que el resto de los bloques. Por ejemplo, no tiene una validez previous_hash, ya que simplemente no había ningún bloque antes.

Necesitamos esto para “arrancar” nuestro nodo o, en realidad, toda la red cuando se inicia el primer nodo. La cadena tiene que empezar en alguna parte, y eso es todo.

Bloques, bloques, bloques

A continuación, agreguemos alguna funcionalidad que nos permita agregar nuevos bloques a la cadena.

impl App {
...
    fn try_add_block(&mut self, block: Block) {
        let latest_block = self.blocks.last().expect("there is at least one block");
        if self.is_block_valid(&block, latest_block) {
            self.blocks.push(block);
        } else {
            error!("could not add block - invalid");
        }
    }
...
}

Aquí, buscamos el último bloque de la cadena, nuestro previous block, y luego validamos si el bloque que nos gustaría agregar es realmente válido. Si no, simplemente registramos un error.

En nuestra sencilla aplicación, no implementaremos ningún manejo de errores reales. Como verá más adelante, si tenemos problemas con las condiciones de carrera entre los nodos y tenemos un estado no válido, nuestro nodo está básicamente roto.

Mencionaré algunas posibles soluciones a estos problemas, pero no las implementaremos aquí; Tenemos bastante terreno que cubrir incluso sin tener que preocuparnos por estos molestos problemas del mundo real.

Veamos a is_block_validcontinuación, una pieza central de nuestra lógica.

const DIFFICULTY_PREFIX: &str = "00";

fn hash_to_binary_representation(hash: &[u8]) -> String {
    let mut res: String = String::default();
    for c in hash {
        res.push_str(&format!("{:b}", c));
    }
    res
}

impl App {
...
    fn is_block_valid(&self, block: &Block, previous_block: &Block) -> bool {
        if block.previous_hash != previous_block.hash {
            warn!("block with id: {} has wrong previous hash", block.id);
            return false;
        } else if !hash_to_binary_representation(
            &hex::decode(&block.hash).expect("can decode from hex"),
        )
        .starts_with(DIFFICULTY_PREFIX)
        {
            warn!("block with id: {} has invalid difficulty", block.id);
            return false;
        } else if block.id != previous_block.id + 1 {
            warn!(
                "block with id: {} is not the next block after the latest: {}",
                block.id, previous_block.id
            );
            return false;
        } else if hex::encode(calculate_hash(
            block.id,
            block.timestamp,
            &block.previous_hash,
            &block.data,
            block.nonce,
        )) != block.hash
        {
            warn!("block with id: {} has invalid hash", block.id);
            return false;
        }
        true
    }
...
}

Primero definimos una constante DIFFICULTY_PREFIX. Esta es la base de nuestro esquema de minería muy simplista. Básicamente, cuando se extrae un bloque, la persona que realiza la extracción tiene que hacer un hash de los datos del bloque (con SHA256, en nuestro caso) y encontrar un hash, que, en binario, comienza con 00(dos ceros). Esto también denota nuestra "dificultad" en la red.

Como puede imaginar, el tiempo para encontrar un hash adecuado aumenta bastante si queremos tres, cuatro, cinco o incluso 20 ceros a la izquierda. En un sistema de cadena de bloques "real", esta dificultad sería un atributo de red, que se acuerda entre los nodos en función de un algoritmo de consenso y en función de la potencia de hash de la red, por lo que la red puede garantizar la producción de un nuevo bloque en una determinada cantidad. de tiempo.

No nos ocuparemos de esto aquí. En aras de la simplicidad, simplemente lo codificaremos a dos ceros iniciales. Esto no toma mucho tiempo para computar en hardware normal, por lo que no tenemos que preocuparnos por esperar demasiado durante la prueba.

A continuación, tenemos una función auxiliar, que es simplemente la representación binaria de una matriz de bytes dada en forma de a String. Esto se utiliza para comprobar cómodamente si un hash se ajusta a nuestra DIFFICULTY_PREFIXcondición. Obviamente, hay formas mucho más elegantes y rápidas de hacer esto, pero esto es simple y funciona para nuestro caso.

Ahora a la lógica de validar un Block. Esto es importante porque garantiza que nuestra cadena de bloques se adhiera a su propiedad de cadena y sea difícil de manipular. La dificultad de cambiar algo aumenta con cada bloque, ya que tendría que volver a calcular (es decir, volver a extraer) el resto de la cadena para obtener una cadena válida nuevamente. Esto sería lo suficientemente caro como para desincentivarlo en un sistema blockchain real)

Hay algunas reglas generales que debe seguir:

  1. Las previous_hashnecesidades de hacer coincidir realmente el hash del último bloque de la cadena.
  2. Las hashnecesidades para comenzar con nuestro DIFFICULTY_PREFIX(es decir, dos ceros), lo que indica que se extrajo correctamente
  3. Las idnecesidades para ser el último ID incrementa en 1
  4. El hash debe ser realmente correcto; hash de los datos del bloque debe darnos el hash del bloque (de lo contrario, también podría crear un hash aleatorio comenzando con 001)

Si pensamos en esto como un sistema distribuido, es posible que observe que es posible tener problemas aquí. ¿Qué sucede si dos nodos extraen un bloque al mismo tiempo en función de la ID del bloque 5? Ambos crearían ID de bloque 6con el bloque anterior apuntando a ID de bloque 5.

Entonces nos enviarían ambos bloques. Los validaríamos y agregaríamos el primero que ingrese, pero el segundo se descartaría durante la validación ya que ya tenemos un bloque con ID 6.

Este es un problema inherente en un sistema como este y la razón por la que debe haber un algoritmo de consenso entre los nodos para decidir qué bloques (es decir, qué cadena) acordar y usar.

De manera óptima, si el bloque que extrajo no se agrega a la cadena acordada, tendrá que extraerlo nuevamente y esperar que funcione mejor la próxima vez. En nuestro caso simple aquí, este mecanismo de reintento no se implementará; si ocurre tal carrera, ese nodo está esencialmente fuera del juego.

Hay enfoques más sofisticados para solucionar esto en el espacio de la cadena de bloques, por supuesto. Por ejemplo, si enviáramos nuestros datos como "transacciones" a otros nodos y los nodos minarían bloques con un conjunto de transacciones, esto se mitigaría un poco. Pero entonces todos minarían todo el tiempo y el más rápido gana. Entonces, como puede ver, esto generaría problemas adicionales, pero menos graves, que tendríamos que solucionar.

De todos modos, nuestro enfoque simple funcionará para nuestra red de prueba local.

¿Qué cadena usar?

Ahora que podemos validar un bloque, implementemos la lógica para validar una cadena completa:

impl App {
...
    fn is_chain_valid(&self, chain: &[Block]) -> bool {
        for i in 0..chain.len() {
            if i == 0 {
                continue;
            }
            let first = chain.get(i - 1).expect("has to exist");
            let second = chain.get(i).expect("has to exist");
            if !self.is_block_valid(second, first) {
                return false;
            }
        }
        true
    }
...
}

Ignorando el bloque de génesis, básicamente revisamos todos los bloques y los validamos. Si un bloque falla en la validación, fallamos en toda la cadena.

Queda un método más Appque nos ayudará a elegir qué cadena usar:

impl App {
...
    // We always choose the longest valid chain
    fn choose_chain(&mut self, local: Vec, remote: Vec) -> Vec {
        let is_local_valid = self.is_chain_valid(&local);
        let is_remote_valid = self.is_chain_valid(&remote);

        if is_local_valid && is_remote_valid {
            if local.len() >= remote.len() {
                local
            } else {
                remote
            }
        } else if is_remote_valid && !is_local_valid {
            remote
        } else if !is_remote_valid && is_local_valid {
            local
        } else {
            panic!("local and remote chains are both invalid");
        }
    }
}

Esto sucede si le pedimos a otro nodo su cadena para determinar si es "mejor" (según nuestro algoritmo de consenso) que la local.

Nuestro criterio es simplemente la longitud de la cadena. En los sistemas reales, suele haber más factores, como la dificultad que se tiene en cuenta y muchas otras posibilidades. Para el propósito de este ejercicio, si una cadena (válida) es más larga que la otra, tomamos esa.

Validamos tanto nuestra cadena local como la remota y tomamos la más larga. También podremos utilizar esta funcionalidad durante el inicio cuando solicitemos a otros nodos su cadena y. Dado que el nuestro solo incluye un bloque de génesis, inmediatamente nos pondremos al día con la cadena "acordada".

Minería

Para terminar nuestra lógica relacionada con blockchain, implementemos nuestro esquema de minería básico.

impl Block {
    pub fn new(id: u64, previous_hash: String, data: String) -> Self {
        let now = Utc::now();
        let (nonce, hash) = mine_block(id, now.timestamp(), &previous_hash, &data);
        Self {
            id,
            hash,
            timestamp: now.timestamp(),
            previous_hash,
            data,
            nonce,
        }
    }
}

Cuando se crea un nuevo bloque, llamamos mine_block, que devolverá ay noncea hash. Luego podemos crear el bloque con su marca de tiempo, los datos dados, ID, hash anterior y el nuevo hash y nonce.

Hablamos de todos los campos anteriores, excepto del nonce. Para explicar qué es esto, veamos la mine_blockfunción:

fn mine_block(id: u64, timestamp: i64, previous_hash: &str, data: &str) -> (u64, String) {
    info!("mining block...");
    let mut nonce = 0;

    loop {
        if nonce % 100000 == 0 {
            info!("nonce: {}", nonce);
        }
        let hash = calculate_hash(id, timestamp, previous_hash, data, nonce);
        let binary_hash = hash_to_binary_representation(&hash);
        if binary_hash.starts_with(DIFFICULTY_PREFIX) {
            info!(
                "mined! nonce: {}, hash: {}, binary hash: {}",
                nonce,
                hex::encode(&hash),
                binary_hash
            );
            return (nonce, hex::encode(hash));
        }
        nonce += 1;
    }
}

Después de anunciar que estamos a punto de extraer un bloque, establecemos el valor nonceen 0.

Luego, comenzamos un ciclo sin fin, que incrementa el nonceen cada paso. Dentro del ciclo, además de registrar cada iteración de 100000 para tener un indicador de progreso aproximado, calculamos un hash sobre los datos del bloque usando calculate_hash, que veremos a continuación.

Luego, usamos nuestro hash_to_binary_representationayudante y verificamos si el hash calculado se adhiere a nuestro criterio de dificultad de comenzar con dos ceros.

Si es así, lo registramos y devolvemos el nonce, el entero creciente, dónde sucedió, y el hash (codificado en hexadecimal). De lo contrario, lo incrementamos noncey volvemos a ir.

Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el noncey un número, que, junto con nuestros datos de bloque con hash usando SHA256, nos dará un hash que comienza con dos ceros.

Necesitamos registrar esto nonceen nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonceestá hash junto con los datos del bloque. Por ejemplo, si nos tomaría 52,342 iteraciones calcular un hash de ajuste (comenzando con dos ceros), noncesería 52341(1 menos, ya que comienza en 0).

Veamos también la utilidad para crear realmente el hash SHA256.

fn calculate_hash(id: u64, timestamp: i64, previous_hash: &str, data: &str, nonce: u64) -> Vec<u8> {
    let data = serde_json::json!({
        "id": id,
        "previous_hash": previous_hash,
        "data": data,
        "timestamp": timestamp,
        "nonce": nonce
    });
    let mut hasher = Sha256::new();
    hasher.update(data.to_string().as_bytes());
    hasher.finalize().as_slice().to_owned()
}

Este es bastante sencillo. Creamos una representación JSON de nuestros datos de bloque usando el nonce actual y lo sha2pasamos por el hash SHA256, devolviendo un Vec<u8>.

Eso es esencialmente toda nuestra lógica de blockchain implementada. Tenemos una estructura de datos blockchain: una lista de bloques. Tenemos bloques, que apuntan al bloque anterior. Estos deben tener un número de identificación creciente y un hash que se adhiera a nuestras reglas de minería.

Si pedimos obtener nuevos bloques de otros nodos, los validamos y, si están bien, los agregamos a la cadena. Si obtenemos una cadena de bloques completa de otro nodo, también la validamos y, si es más larga que la nuestra (es decir, tiene más bloques), reemplazamos nuestra propia cadena con ella.

Como puede imaginar, dado que cada nodo implementa esta lógica exacta, los bloques y las cadenas acordadas pueden propagarse a través de la red rápidamente y la red converge al mismo estado (como con las limitaciones de manejo de errores antes mencionadas en nuestro caso simple).

Conceptos básicos de igual a igual

A continuación, implementaremos la pila de red basada en P2P .

Comience por crear un p2p.rsarchivo, que contendrá la mayor parte de la lógica peer-to-peer que usaremos en nuestra aplicación.

Allí, nuevamente, definimos algunas estructuras de datos básicas y constantes que necesitaremos:

pub static KEYS: Lazy = Lazy::new(identity::Keypair::generate_ed25519);
pub static PEER_ID: Lazy = Lazy::new(|| PeerId::from(KEYS.public()));
pub static CHAIN_TOPIC: Lazy = Lazy::new(|| Topic::new("chains"));
pub static BLOCK_TOPIC: Lazy = Lazy::new(|| Topic::new("blocks"));

#[derive(Debug, Serialize, Deserialize)]
pub struct ChainResponse {
    pub blocks: Vec,
    pub receiver: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LocalChainRequest {
    pub from_peer_id: String,
}

pub enum EventType {
    LocalChainResponse(ChainResponse),
    Input(String),
    Init,
}

#[derive(NetworkBehaviour)]
pub struct AppBehaviour {
    pub floodsub: Floodsub,
    pub mdns: Mdns,
    #[behaviour(ignore)]
    pub response_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub init_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub app: App,
}

Comenzando desde arriba, definimos un par de claves y un ID de par derivado. Esos son simplemente los elementos intrínsecos de libp2p para identificar un cliente en la red.

Luego, definimos dos de los llamados topics: chainsy blocks. Usaremos el FloodSubprotocolo, un protocolo simple de publicación / suscripción, para la comunicación entre los nodos.

Esto tiene la ventaja de que es muy sencillo de configurar y usar, pero tiene la desventaja de que necesitamos transmitir cada pieza de información. Entonces, incluso si solo queremos responder a la “solicitud de nuestra cadena” de un cliente, ese cliente enviará esta solicitud a todos los nodos a los que están conectados en la red y también enviaremos nuestra respuesta a todos ellos.

Esto no es un problema en términos de corrección, pero en términos de eficiencia, es obviamente horrendo. Esto podría manejarse mediante un modelo simple de solicitud / respuesta punto a punto, que es algo que libp2p admite, pero esto simplemente agregaría aún más complejidad a este ejemplo ya complejo. Si está interesado, puede consultar los documentos de libp2p .

También podríamos usar el más eficiente en GossipSublugar de FloodSub. Pero, nuevamente, no es tan conveniente de configurar y realmente no estamos particularmente interesados ​​en el rendimiento en este momento. La interfaz es muy similar. Nuevamente, si está interesado en jugar con esto, consulte los documentos oficiales .

De todos modos, los temas son básicamente "canales" a los que suscribirse. Podemos suscribirnos a "cadenas" y usarlas para enviar nuestra cadena de bloques local a otros nodos y recibir la suya. Lo mismo ocurre con los "bloques", que usaremos para transmitir y recibir nuevos bloques.

A continuación, tenemos el concepto de ChainResponsetener una lista de bloques y un receptor. Esta es una estructura, que esperaremos si alguien nos envía su cadena de bloques local y la usamos para enviarle nuestra cadena local.

El LocalChainRequestes lo que desencadena esta interacción. Si enviamos un LocalChainRequestcon el peer_idde otro nodo en el sistema, esto activará que nos envíen su cadena de regreso, como veremos más adelante.

Para manejar mensajes entrantes, inicialización diferida y entrada de teclado por parte del usuario del cliente, definimos la EventTypeenumeración, que nos ayudará a enviar eventos a través de la aplicación para mantener el estado de nuestra aplicación sincronizado con el tráfico de red entrante y saliente.

Finalmente, el núcleo de la funcionalidad P2P es nuestro AppBehaviour, que implementa NetworkBehaviourel concepto de libp2p para implementar una pila de red descentralizada.

No entraremos en el meollo de la cuestión aquí, pero mi tutorial completo de libp2p entra en más detalles sobre esto.

El AppBehavioursostiene nuestra instancia FloodSub para pub / sub comunicación y e instancia de acuses de recibo, lo que nos permitirá encontrar de forma automática otros nodos de nuestra red local (pero no fuera de ella).

También agregamos nuestra cadena Appde bloques a este comportamiento, así como canales para enviar eventos tanto para la inicialización como para la comunicación de solicitud / respuesta entre partes de la aplicación. Veremos esto en acción más adelante.

Inicializar AppBehaviourtambién es bastante sencillo:

impl AppBehaviour {
    pub async fn new(
        app: App,
        response_sender: mpsc::UnboundedSender,
        init_sender: mpsc::UnboundedSender,
    ) -> Self {
        let mut behaviour = Self {
            app,
            floodsub: Floodsub::new(*PEER_ID),
            mdns: Mdns::new(Default::default())
                .await
                .expect("can create mdns"),
            response_sender,
            init_sender,
        };
        behaviour.floodsub.subscribe(CHAIN_TOPIC.clone());
        behaviour.floodsub.subscribe(BLOCK_TOPIC.clone());

        behaviour
    }
}

Manejo de mensajes entrantes

Primero, implementamos los controladores para los datos que provienen de otros nodos.

Comenzaremos con los eventos de Mdns, ya que son básicamente un texto estándar:

impl NetworkBehaviourEventProcess<MdnsEvent> for AppBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

Si se descubre un nuevo nodo, lo agregamos a nuestra lista de nodos FloodSub para que podamos comunicarnos. Una vez que caduque, lo volvemos a quitar.

Más interesante es la implementación de NetworkBehaviourpara nuestro protocolo de comunicación FloodSub.

// incoming event handler
impl NetworkBehaviourEventProcess for AppBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        if let FloodsubEvent::Message(msg) = event {
            if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                if resp.receiver == PEER_ID.to_string() {
                    info!("Response from {}:", msg.source);
                    resp.blocks.iter().for_each(|r| info!("{:?}", r));

                    self.app.blocks = self.app.choose_chain(self.app.blocks.clone(), resp.blocks);
                }
            } else if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                info!("sending local chain to {}", msg.source.to_string());
                let peer_id = resp.from_peer_id;
                if PEER_ID.to_string() == peer_id {
                    if let Err(e) = self.response_sender.send(ChainResponse {
                        blocks: self.app.blocks.clone(),
                        receiver: msg.source.to_string(),
                    }) {
                        error!("error sending response via channel, {}", e);
                    }
                }
            } else if let Ok(block) = serde_json::from_slice::(&msg.data) {
                info!("received new block from {}", msg.source.to_string());
                self.app.try_add_block(block);
            }
        }
    }
}

Para los eventos entrantes, que son FloodsubEvent::Message, verificamos si la carga útil se ajusta a alguna de nuestras estructuras de datos esperadas.

Si es un ChainResponse, significa que otro nodo nos envió una cadena de bloques local.

Verificamos si en realidad somos el receptor de dicho dato y, de ser así, registramos la cadena de bloques entrante e intentamos ejecutar nuestro consenso. Si es válido y más largo que nuestra cadena, reemplazamos nuestra cadena con él. De lo contrario, mantenemos nuestra propia cadena.

Si los datos entrantes son a LocalChainRequest, verificamos si somos de quienes quieren la cadena, marcando el from_peer_id. Si es así, simplemente les enviamos una versión JSON de nuestra cadena de bloques local. La parte de envío real está en otra parte del código, pero por ahora, simplemente la enviamos a través de nuestro canal de eventos para obtener respuestas.

Finalmente, si Blockes entrante, significa que alguien más extrajo un bloque y quiere que lo agreguemos a nuestra cadena local. Comprobamos si el bloque es válido y, si lo es, lo añadimos.

Poniendolo todo junto

¡Excelente! Ahora conectemos todo esto y agreguemos algunos comandos para que los usuarios interactúen con la aplicación.

De vuelta main.rs, es hora de implementar la mainfunción.

Empezamos con la configuración:

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", p2p::PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();
    let (init_sender, mut init_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::::new()
        .into_authentic(&p2p::KEYS)
        .expect("can create auth keys");

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

    let behaviour = p2p::AppBehaviour::new(App::new(), response_sender, init_sender.clone()).await;

    let mut swarm = SwarmBuilder::new(transp, behaviour, *p2p::PEER_ID)
        .executor(Box::new(|fut| {
            spawn(fut);
        }))
        .build();

    let mut stdin = BufReader::new(stdin()).lines();

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

    spawn(async move {
        sleep(Duration::from_secs(1)).await;
        info!("sending init event");
        init_sender.send(true).expect("can send init event");
    });

Eso es mucho código, pero básicamente configura cosas de las que ya hablamos. Inicializamos el registro y nuestros dos canales de eventos para la inicialización y las respuestas.

Luego, inicializamos nuestro par de claves, el transporte, el comportamiento de libp2p y el enjambre de libp2p, que es la entidad que ejecuta nuestra pila de red.

También inicializamos un lector almacenado en búfer stdinpara que podamos leer los comandos entrantes del usuario e iniciar nuestro Swarm.

Finalmente, generamos una corrutina asincrónica, que espera un segundo y luego envía un disparador de inicialización en el canal de inicio.

Esta es la señal que usaremos después de iniciar un nodo para esperar un poco hasta que el nodo esté encendido y conectado. Luego le pedimos a otro nodo su cadena de bloques actual para ponernos al día.

El resto maines la parte interesante: la parte en la que manejamos los eventos de teclado del usuario, los datos entrantes y los datos salientes.

    loop {
        let evt = {
            select! {
                line = stdin.next_line() => Some(p2p::EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                response = response_rcv.recv() => {
                    Some(p2p::EventType::LocalChainResponse(response.expect("response exists")))
                },
                _init = init_rcv.recv() => {
                    Some(p2p::EventType::Init)
                }
                event = swarm.select_next_some() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
            }
        };

        if let Some(event) = evt {
            match event {
                p2p::EventType::Init => {
                    let peers = p2p::get_list_peers(&swarm);
                    swarm.behaviour_mut().app.genesis();

                    info!("connected nodes: {}", peers.len());
                    if !peers.is_empty() {
                        let req = p2p::LocalChainRequest {
                            from_peer_id: peers
                                .iter()
                                .last()
                                .expect("at least one peer")
                                .to_string(),
                        };

                        let json = serde_json::to_string(&req).expect("can jsonify request");
                        swarm
                            .behaviour_mut()
                            .floodsub
                            .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                    }
                }
                p2p::EventType::LocalChainResponse(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm
                        .behaviour_mut()
                        .floodsub
                        .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                }
                p2p::EventType::Input(line) => match line.as_str() {
                    "ls p" => p2p::handle_print_peers(&swarm),
                    cmd if cmd.starts_with("ls c") => p2p::handle_print_chain(&swarm),
                    cmd if cmd.starts_with("create b") => p2p::handle_create_block(cmd, &mut swarm),
                    _ => error!("unknown command"),
                },
            }
        }
    }

Comenzamos un ciclo sin fin y usamos la select!macro de Tokio para competir con múltiples funciones asíncronas.

Esto significa que cualquiera de estos acabados primero se manejará primero y luego comenzaremos de nuevo.

El primer emisor de eventos es nuestro lector en búfer, que nos dará líneas de entrada del usuario. Si obtenemos uno, creamos un EventType::Inputcon la línea.

Luego, escuchamos el canal de respuesta y el canal de inicio, creando sus eventos respectivamente.
Y si los eventos entran en el enjambre en sí, esto significa que son eventos que no son manejados por nuestro comportamiento Mdns ni nuestro comportamiento FloodSub y simplemente los registramos. En su mayoría son ruido, como conexión / desconexión en nuestro caso, pero útiles para la depuración.

Con los eventos correspondientes creados (o ningún evento creado), nos ocupamos de manejarlos.

Para nuestro Initevento, invocamos genesis()nuestra aplicación, creando nuestro bloque de génesis. Si estamos conectados a nodos, activamos LocalChainRequesta al último de la lista.

Obviamente, aquí tendría sentido preguntar a varios nodos, y tal vez varias veces, y seleccionar la mejor (es decir, la más larga) cadena de respuestas que obtenemos. Pero en aras de la simplicidad, solo pedimos uno y aceptamos lo que sea que nos envíen.

Luego, si obtenemos un LocalChainResponseevento, eso significa que se envió algo en el canal de respuesta. Si recuerda lo anterior, eso sucedió en nuestro comportamiento FloodSub cuando enviamos nuestra cadena de bloques local a un nodo solicitante. Aquí, en realidad enviamos el JSON entrante al tema FloodSub correcto, por lo que se transmite a la red.

Finalmente, para la entrada del usuario, tenemos tres comandos:

  • ls p enumera todos los compañeros
  • ls c imprime la cadena de bloques local
  • create b $datacrea un nuevo bloque con $datasu contenido de cadena

Cada comando llama a una de estas funciones auxiliares:

pub fn get_list_peers(swarm: &Swarm) -> Vec {
    info!("Discovered Peers:");
    let nodes = swarm.behaviour().mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().map(|p| p.to_string()).collect()
}

pub fn handle_print_peers(swarm: &Swarm) {
    let peers = get_list_peers(swarm);
    peers.iter().for_each(|p| info!("{}", p));
}

pub fn handle_print_chain(swarm: &Swarm) {
    info!("Local Blockchain:");
    let pretty_json =
        serde_json::to_string_pretty(&swarm.behaviour().app.blocks).expect("can jsonify blocks");
    info!("{}", pretty_json);
}

pub fn handle_create_block(cmd: &str, swarm: &mut Swarm) {
    if let Some(data) = cmd.strip_prefix("create b") {
        let behaviour = swarm.behaviour_mut();
        let latest_block = behaviour
            .app
            .blocks
            .last()
            .expect("there is at least one block");
        let block = Block::new(
            latest_block.id + 1,
            latest_block.hash.clone(),
            data.to_owned(),
        );
        let json = serde_json::to_string(&block).expect("can jsonify request");
        behaviour.app.blocks.push(block);
        info!("broadcasting new block");
        behaviour
            .floodsub
            .publish(BLOCK_TOPIC.clone(), json.as_bytes());
    }
}

Enumerar clientes e imprimir la cadena de bloques es bastante sencillo. Crear un bloque es más interesante.

En ese caso, usamos Block::newpara crear (y extraer) un nuevo bloque. Una vez que eso sucede, lo JSONificamos y lo transmitimos a la red para que otros puedan agregarlo a su cadena.

Aquí es donde pondríamos algo de lógica para r-intentar esto. Por ejemplo, podríamos agregarlo a una cola y ver si, después de un tiempo, nuestro bloque se propaga a la cadena de bloques ampliamente acordada y, de no ser así, obtener una nueva copia de la cadena acordada y extraerla nuevamente para obtenerla. alli. Como se mencionó anteriormente, este diseño ciertamente no se escalará a muchos nodos que extraigan sus bloques todo el tiempo, pero eso está bien para el propósito de este tutorial.

¡Empecemos y veamos si funciona!

Probando nuestra cadena de bloques Rust

Podemos iniciar la aplicación usando RUST_LOG=info cargo run. En realidad, es mejor iniciar varias instancias en diferentes ventanas de terminal.

Por ejemplo, podemos iniciar dos nodos:

INFO  rust_blockchain_example > Peer Id: 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Y:

INFO  rust_blockchain_example > Peer Id: 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

El uso ls pde la segunda aplicación nos muestra la conexión con la primera:

INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example::p2p > 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Luego, podemos usar ls cpara imprimir el bloque de génesis:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  }
]

Hasta aquí todo bien. creemos un bloque:

create b hello
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 62235, hash: 00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922, binary hash: 0010001100111101101000110110101001111110011111000101010101000101111110101010110110010011111110111000010100001011110001111000000110110111101100010111111100001011011110001111110100011111011000101111111001111110101001100010
 INFO  rust_blockchain_example::p2p > broadcasting new block

En el primer nodo, vemos esto:

INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

Y llamando ls c:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

¡El bloque se agregó!

Comencemos con un tercer nodo. Debería obtener automáticamente esta cadena actualizada porque es más larga que la suya (solo el bloque de génesis).

INFO  rust_blockchain_example > Peer Id: 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

 INFO  rust_blockchain_example > sending init event
 INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example      > connected nodes: 2
 INFO  rust_blockchain_example::p2p > Response from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX:
 INFO  rust_blockchain_example::p2p > Block { id: 0, hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", previous_hash: "genesis", timestamp: 1636664658, data: "genesis!", nonce: 2836 }
 INFO  rust_blockchain_example::p2p > Block { id: 1, hash: "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922", previous_hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", timestamp: 1636664772, data: " hello", nonce: 62235 }

Después de enviar el initevento, solicitamos la cadena del segundo nodo y la obtuvimos.

Llamar ls caquí nos muestra la misma cadena:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

La creación de un bloque también funciona:

create b alsoworks
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 34855, hash: 0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd, binary hash: 001110000010111101110111111001110110000011110110100110111010110111001101011100010001110100011011101001011101001101110101010101010100010101101100000110110111101110110010101011010110010100101111011110111000011111110111111101
 INFO  rust_blockchain_example::p2p > broadcasting new block

Nodo 1:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Nodo 2:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q
ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Genial, ¡funciona!

Puede jugar e intentar crear condiciones de carrera (por ejemplo, aumentando la dificultad a tres ceros y comenzando varios bloques en varios nodos. Notará inmediatamente algunos de los defectos de este diseño, pero los conceptos básicos funcionan. Tenemos un par -to-peer blockchain, un libro mayor descentralizado real con robustez básica, construido completamente desde cero en Rust.

Puede encontrar el código de ejemplo completo en GitHub .

Conclusión

En este tutorial, creamos una aplicación blockchain simple, bastante limitada, pero que funciona en Rust. Nuestra aplicación blockchain tiene un esquema de minería muy básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

La mayor parte de esta simplicidad se debe a la fantástica biblioteca libp2p , que hace todo el trabajo pesado en términos de redes. Claramente, como siempre es el caso en los tutoriales de ingeniería de software, para una aplicación de cadena de bloques de grado de producción, hay muchas, muchas más cosas que considerar y hacer bien.

Sin embargo, este ejercicio prepara el escenario para el tema, explicando algunos de los conceptos básicos y mostrándolos en Rust, de modo que podamos continuar este viaje observando cómo desarrollaríamos una aplicación blockchain que realmente podría usarse en la práctica con un marco como Substrate .

Fuente del artículo original en https://blog.logrocket.com

#rust # #blockchain 

Cómo Construir Una Cadena De Bloques En Rust

Cuando pensamos en la tecnología P2P y para qué se usa hoy en día, es imposible no pensar inmediatamente en la tecnología blockchain. Pocos temas de TI han sido tan publicitados o controvertidos durante la última década como la tecnología blockchain y las criptomonedas.

Y aunque el amplio interés en la tecnología blockchain ha variado bastante, lo que se debe, naturalmente, al potencial monetario detrás de algunas de las criptomonedas más conocidas y utilizadas, una cosa está clara: sigue siendo relevante y no parece serlo. ir a cualquier parte.

En un artículo anterior , cubrimos cómo construir una aplicación peer-to-peer muy básica y funcional (aunque bastante ineficiente) en Rust. En este tutorial, demostraremos cómo crear una aplicación blockchain con un esquema de minería básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

Por qué blockchain es emocionante

Si bien personalmente no estoy particularmente interesado en las criptomonedas o los juegos de azar financieros en general, encuentro muy atractiva la idea de descentralizar partes de nuestra infraestructura existente. Hay muchos proyectos excelentes basados ​​en blockchain que tienen como objetivo abordar problemas sociales como el cambio climático, la desigualdad social, la privacidad y la transparencia gubernamental.

El potencial de la tecnología basada en la idea de un libro de contabilidad descentralizado, seguro y totalmente transparente que permita a los actores interactuar sin tener que establecer la confianza primero es tan revolucionario como parece. Será emocionante ver cuál de las ambiciosas ideas antes mencionadas despegará, ganará tracción y tendrá éxito en el futuro.

En resumen, la tecnología blockchain es emocionante, no solo por su potencial de cambio mundial, sino también desde una perspectiva técnica. Desde la criptografía, pasando por las redes de pares hasta los sofisticados algoritmos de consenso , el campo tiene bastantes temas fascinantes en los que sumergirse.

Escribir una aplicación blockchain en Rust

En esta guía, crearemos una aplicación blockchain muy simple desde cero usando Rust. Nuestra aplicación no será particularmente eficiente, segura o robusta, pero lo ayudará a comprender cómo algunos de los conceptos fundamentales detrás de los sistemas blockchain ampliamente conocidos se pueden implementar de una manera simple, explicando algunas de las ideas detrás de ellos.

No entraremos en todos los detalles de cada concepto, y la implementación tendrá algunas deficiencias graves. No querrá usar este proyecto para nada dentro de millas de un caso de uso de producción, pero el objetivo es construir algo con lo que pueda jugar, aplicar a sus propias ideas y examinar para familiarizarse más con la tecnología de Rust y blockchain. en general.

La atención se centrará en la parte técnica, es decir, cómo implementar algunos de los conceptos y cómo se combinan. No explicaremos qué es una cadena de bloques, ni tocaremos la minería, el consenso y cosas por el estilo más allá de lo necesario para este tutorial. Sobre todo nos preocuparemos de cómo poner estas ideas, en una versión simplificada, en el código de Rust.

Además, no construiremos una criptomoneda o un sistema similar. Nuestro diseño es mucho más simple: cada nodo en la red puede agregar datos (cadenas) al libro mayor descentralizado (la cadena de bloques) extrayendo un bloque válido localmente y luego transmitiendo ese bloque.

Siempre que sea un bloque válido (veremos más adelante lo que esto significa), cada nodo agregará el bloque a su cadena y nuestro dato se convertirá en parte de un bloque descentralizado, a prueba de manipulaciones, indestructible (excepto que todas las notas se cierran en nuestro caso) red!

Obviamente, este es un diseño bastante simplificado y algo artificial que se toparía con problemas de eficiencia y robustez con bastante rapidez al escalar. Pero como solo estamos haciendo este ejercicio para aprender, está bien. Si llega al final y tiene algo de motivación, puede extenderlo en la dirección que desee y tal vez construir la próxima gran cosa desde nuestros miserables comienzos aquí, ¡nunca se sabe!

Configurando nuestra aplicación Rust

Para seguir, todo lo que necesita es una instalación reciente de Rust .

Primero, cree un nuevo proyecto de Rust:

cargo new rust-blockchain-example
cd rust-blockchain-example

A continuación, edite el Cargo.tomlarchivo y agregue las dependencias que necesitará.

[dependencies]
chrono = "0.4"
sha2 = "0.9.8"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
libp2p = { version = "0.39", features = ["tcp-tokio", "mdns"] }
tokio = { version = "1.0", features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread", "sync", "time"] }
hex = "0.4"
once_cell = "1.5"
log = "0.4"
pretty_env_logger = "0.4"

Estamos usando libp2p como nuestra capa de red peer-to-peer y Tokio como nuestro tiempo de ejecución subyacente.

Usaremos la sha2biblioteca para nuestro hash sha256 y la hexcaja para transformar los hash binarios en hexadecimales legibles y transferibles.

Además de eso, en realidad solo hay utilidades como serdeJSON log, y pretty_env_loggerpara el registro, once_cellpara la inicialización estática y chronopara las marcas de tiempo.

Con la configuración fuera del camino, comencemos implementando los conceptos básicos de blockchain primero y luego, más adelante, poniéndolo todo en un contexto de red P2P.

Conceptos básicos de blockchain

Primero definamos nuestras estructuras de datos para nuestra cadena de bloques real:

pub struct App {
    pub blocks: Vec,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Block {
    pub id: u64,
    pub hash: String,
    pub previous_hash: String,
    pub timestamp: i64,
    pub data: String,
    pub nonce: u64,
}

Eso es todo, no hay mucho detrás, de verdad. Nuestra Appestructura esencialmente contiene nuestro estado de aplicación. No conservaremos la cadena de bloques en este ejemplo, por lo que desaparecerá una vez que detengamos la aplicación.

Este estado es simplemente una lista de Blocks. Agregaremos nuevos bloques al final de esta lista y esta será nuestra estructura de datos de blockchain.

La lógica real hará que esta lista de bloques sea una cadena de bloques, donde cada bloque hace referencia al hash del bloque anterior se implementará en nuestra lógica de aplicación. Sería posible construir una estructura de datos que ya sea compatible con la validación que necesitamos, pero este enfoque parece más simple y definitivamente apuntamos a la simplicidad aquí.

En Blocknuestro caso, A consistirá en an id, que es un índice que comienza en 0 contando hacia arriba. Luego, un hash sha256 (cuyo cálculo veremos más adelante), el hash del bloque anterior, una marca de tiempo, los datos contenidos en el bloque y un nonce, que también cubriremos cuando hablemos miningdel bloque.

Antes de comenzar con la minería, primero implementemos algunas de las funciones de validación que necesitamos para mantener nuestro estado consistente y algunos de los consensos básicos necesarios, para que cada cliente sepa qué blockchain es el correcto, en caso de que haya varios en conflicto.

Comenzamos implementando nuestra Appestructura:

impl App {
    fn new() -> Self {
        Self { blocks: vec![] }
    }

    fn genesis(&mut self) {
        let genesis_block = Block {
            id: 0,
            timestamp: Utc::now().timestamp(),
            previous_hash: String::from("genesis"),
            data: String::from("genesis!"),
            nonce: 2836,
            hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43".to_string(),
        };
        self.blocks.push(genesis_block);
    }
...
}

Inicializamos nuestra aplicación con una cadena vacía. Más adelante, implementaremos algo de lógica. Preguntamos a otros nodos al inicio por su cadena y, si es más larga que la nuestra, usamos la suya. Este es nuestro criterio de consenso simplista.

El genesismétodo crea el primer bloque codificado en nuestra cadena de bloques. Este es un bloque "especial" en el sentido de que realmente no se adhiere a las mismas reglas que el resto de los bloques. Por ejemplo, no tiene una validez previous_hash, ya que simplemente no había ningún bloque antes.

Necesitamos esto para "arrancar" nuestro nodo o, en realidad, toda la red cuando se inicia el primer nodo. La cadena tiene que empezar en alguna parte, y eso es todo.

Bloques, bloques, bloques

A continuación, agreguemos alguna funcionalidad que nos permita agregar nuevos bloques a la cadena.

impl App {
...
    fn try_add_block(&mut self, block: Block) {
        let latest_block = self.blocks.last().expect("there is at least one block");
        if self.is_block_valid(&block, latest_block) {
            self.blocks.push(block);
        } else {
            error!("could not add block - invalid");
        }
    }
...
}

Aquí, buscamos el último bloque de la cadena, nuestro previous block, y luego validamos si el bloque que nos gustaría agregar es realmente válido. Si no, simplemente registramos un error.

En nuestra sencilla aplicación, no implementaremos ningún manejo de errores reales. Como verá más adelante, si tenemos problemas con las condiciones de carrera entre los nodos y tenemos un estado no válido, nuestro nodo está básicamente roto.

Mencionaré algunas posibles soluciones a estos problemas, pero no las implementaremos aquí; Tenemos bastante terreno que cubrir incluso sin tener que preocuparnos por estos molestos problemas del mundo real.

Veamos a is_block_validcontinuación, una pieza central de nuestra lógica.

const DIFFICULTY_PREFIX: &str = "00";

fn hash_to_binary_representation(hash: &[u8]) -> String {
    let mut res: String = String::default();
    for c in hash {
        res.push_str(&format!("{:b}", c));
    }
    res
}

impl App {
...
    fn is_block_valid(&self, block: &Block, previous_block: &Block) -> bool {
        if block.previous_hash != previous_block.hash {
            warn!("block with id: {} has wrong previous hash", block.id);
            return false;
        } else if !hash_to_binary_representation(
            &hex::decode(&block.hash).expect("can decode from hex"),
        )
        .starts_with(DIFFICULTY_PREFIX)
        {
            warn!("block with id: {} has invalid difficulty", block.id);
            return false;
        } else if block.id != previous_block.id + 1 {
            warn!(
                "block with id: {} is not the next block after the latest: {}",
                block.id, previous_block.id
            );
            return false;
        } else if hex::encode(calculate_hash(
            block.id,
            block.timestamp,
            &block.previous_hash,
            &block.data,
            block.nonce,
        )) != block.hash
        {
            warn!("block with id: {} has invalid hash", block.id);
            return false;
        }
        true
    }
...
}

Primero definimos una constante DIFFICULTY_PREFIX. Esta es la base de nuestro esquema de minería muy simplista. Básicamente, cuando se extrae un bloque, la persona que realiza la extracción tiene que hacer un hash de los datos del bloque (con SHA256, en nuestro caso) y encontrar un hash, que, en binario, comienza con 00(dos ceros). Esto también denota nuestra "dificultad" en la red.

Como puede imaginar, el tiempo para encontrar un hash adecuado aumenta bastante si queremos tres, cuatro, cinco o incluso 20 ceros a la izquierda. En un sistema de cadena de bloques "real", esta dificultad sería un atributo de red, que se acuerda entre los nodos en función de un algoritmo de consenso y en función de la potencia de hash de la red, por lo que la red puede garantizar la producción de un nuevo bloque en una determinada cantidad. de tiempo.

No nos ocuparemos de esto aquí. En aras de la simplicidad, simplemente lo codificaremos a dos ceros iniciales. Esto no toma mucho tiempo para computar en hardware normal, por lo que no tenemos que preocuparnos por esperar demasiado durante la prueba.

A continuación, tenemos una función auxiliar, que es simplemente la representación binaria de una matriz de bytes dada en forma de a String. Esto se utiliza para comprobar cómodamente si un hash se ajusta a nuestra DIFFICULTY_PREFIXcondición. Obviamente, hay formas mucho más elegantes y rápidas de hacer esto, pero esto es simple y funciona para nuestro caso.

Ahora a la lógica de validar un Block. Esto es importante porque garantiza que nuestra cadena de bloques se adhiera a su propiedad de cadena y sea difícil de manipular. La dificultad de cambiar algo aumenta con cada bloque, ya que tendría que volver a calcular (es decir, volver a extraer) el resto de la cadena para obtener una cadena válida nuevamente. Esto sería lo suficientemente caro como para desincentivarlo en un sistema de cadena de bloques real)

Hay algunas reglas generales que debe seguir:

  1. Las previous_hashnecesidades de hacer coincidir realmente el hash del último bloque de la cadena.
  2. Las hashnecesidades para comenzar con nuestro DIFFICULTY_PREFIX(es decir, dos ceros), lo que indica que se extrajo correctamente
  3. Las idnecesidades para ser el último ID incrementa en 1
  4. El hash debe ser realmente correcto; hash de los datos del bloque debe darnos el hash del bloque (de lo contrario, también podría crear un hash aleatorio comenzando con 001)

Si pensamos en esto como un sistema distribuido, es posible que observe que es posible tener problemas aquí. ¿Qué pasa si dos nodos extraen un bloque al mismo tiempo en función de la ID del bloque 5? Ambos crearían ID de bloque 6con el bloque anterior apuntando a ID de bloque 5.

Entonces nos enviarían ambos bloques. Los validaríamos y agregaríamos el primero que ingrese, pero el segundo se descartaría durante la validación ya que ya tenemos un bloque con ID 6.

Este es un problema inherente en un sistema como este y la razón por la que debe haber un algoritmo de consenso entre los nodos para decidir qué bloques (es decir, qué cadena) acordar y usar.

De manera óptima, si el bloque que extrajo no se agrega a la cadena acordada, tendrá que extraerlo nuevamente y esperar que funcione mejor la próxima vez. En nuestro caso simple aquí, este mecanismo de reintento no se implementará; si ocurre una carrera así, ese nodo está esencialmente fuera del juego.

Hay enfoques más sofisticados para solucionar esto en el espacio blockchain, por supuesto. Por ejemplo, si enviáramos nuestros datos como "transacciones" a otros nodos y los nodos minarían bloques con un conjunto de transacciones, esto se mitigaría un poco. Pero entonces todos minarían todo el tiempo y el más rápido gana. Entonces, como puede ver, esto generaría problemas adicionales, pero menos graves, que tendríamos que solucionar.

De todos modos, nuestro enfoque simple funcionará para nuestra red de prueba local.

¿Qué cadena usar?

Ahora que podemos validar un bloque, implementemos la lógica para validar una cadena completa:

impl App {
...
    fn is_chain_valid(&self, chain: &[Block]) -> bool {
        for i in 0..chain.len() {
            if i == 0 {
                continue;
            }
            let first = chain.get(i - 1).expect("has to exist");
            let second = chain.get(i).expect("has to exist");
            if !self.is_block_valid(second, first) {
                return false;
            }
        }
        true
    }
...
}

Ignorando el bloque de génesis, básicamente revisamos todos los bloques y los validamos. Si un bloque falla en la validación, fallamos en toda la cadena.

Queda un método más Appque nos ayudará a elegir qué cadena usar:

impl App {
...
    // We always choose the longest valid chain
    fn choose_chain(&mut self, local: Vec, remote: Vec) -> Vec {
        let is_local_valid = self.is_chain_valid(&local);
        let is_remote_valid = self.is_chain_valid(&remote);

        if is_local_valid && is_remote_valid {
            if local.len() >= remote.len() {
                local
            } else {
                remote
            }
        } else if is_remote_valid && !is_local_valid {
            remote
        } else if !is_remote_valid && is_local_valid {
            local
        } else {
            panic!("local and remote chains are both invalid");
        }
    }
}

Esto sucede si le pedimos a otro nodo su cadena para determinar si es "mejor" (según nuestro algoritmo de consenso) que la local.

Nuestro criterio es simplemente la longitud de la cadena. En los sistemas reales, suele haber más factores, como la dificultad que se tiene en cuenta y muchas otras posibilidades. Para el propósito de este ejercicio, si una cadena (válida) es más larga que la otra, tomamos esa.

Validamos tanto nuestra cadena local como la remota y tomamos la más larga. También podremos utilizar esta funcionalidad durante el inicio cuando solicitemos a otros nodos su cadena y. Dado que el nuestro solo incluye un bloque de génesis, inmediatamente nos pondremos al día con la cadena "acordada".

Minería

Para terminar nuestra lógica relacionada con blockchain, implementemos nuestro esquema de minería básico.

impl Block {
    pub fn new(id: u64, previous_hash: String, data: String) -> Self {
        let now = Utc::now();
        let (nonce, hash) = mine_block(id, now.timestamp(), &previous_hash, &data);
        Self {
            id,
            hash,
            timestamp: now.timestamp(),
            previous_hash,
            data,
            nonce,
        }
    }
}

Cuando se crea un nuevo bloque, llamamos mine_block, que devolverá ay noncea hash. Luego podemos crear el bloque con su marca de tiempo, los datos dados, ID, hash anterior y el nuevo hash y nonce.

Hablamos de todos los campos anteriores, excepto del nonce. Para explicar qué es esto, veamos la mine_blockfunción:

fn mine_block(id: u64, timestamp: i64, previous_hash: &str, data: &str) -> (u64, String) {
    info!("mining block...");
    let mut nonce = 0;

    loop {
        if nonce % 100000 == 0 {
            info!("nonce: {}", nonce);
        }
        let hash = calculate_hash(id, timestamp, previous_hash, data, nonce);
        let binary_hash = hash_to_binary_representation(&hash);
        if binary_hash.starts_with(DIFFICULTY_PREFIX) {
            info!(
                "mined! nonce: {}, hash: {}, binary hash: {}",
                nonce,
                hex::encode(&hash),
                binary_hash
            );
            return (nonce, hex::encode(hash));
        }
        nonce += 1;
    }
}

Después de anunciar que estamos a punto de extraer un bloque, establecemos el valor nonceen 0.

Luego, comenzamos un ciclo sin fin, que incrementa el nonceen cada paso. Dentro del ciclo, además de registrar cada iteración de 100000 para tener un indicador de progreso aproximado, calculamos un hash sobre los datos del bloque usando calculate_hash, que veremos a continuación.

Luego, usamos nuestro hash_to_binary_representationayudante y verificamos si el hash calculado se adhiere a nuestro criterio de dificultad de comenzar con dos ceros.

Si es así, lo registramos y devolvemos el noncenúmero entero creciente, dónde sucedió y el hash (codificado en hexadecimal). De lo contrario, lo incrementamos noncey vamos de nuevo.

Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el noncey un número, que, junto con nuestros datos de bloque con hash usando SHA256, nos dará un hash que comienza con dos ceros.

Necesitamos registrar esto nonceen nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonceestá hash junto con los datos del bloque. Por ejemplo, si nos tomaría 52,342 iteraciones calcular un hash de ajuste (comenzando con dos ceros), noncesería 52341(1 menos, ya que comienza en 0).

Veamos también la utilidad para crear realmente el hash SHA256.

fn calculate_hash(id: u64, timestamp: i64, previous_hash: &str, data: &str, nonce: u64) -> Vec<u8> {
    let data = serde_json::json!({
        "id": id,
        "previous_hash": previous_hash,
        "data": data,
        "timestamp": timestamp,
        "nonce": nonce
    });
    let mut hasher = Sha256::new();
    hasher.update(data.to_string().as_bytes());
    hasher.finalize().as_slice().to_owned()
}

Este es bastante sencillo. Creamos una representación JSON de nuestros datos de bloque usando el nonce actual y lo sha2pasamos por el hash SHA256, devolviendo un Vec<u8>.

Eso es esencialmente toda nuestra lógica de blockchain implementada. Tenemos una estructura de datos blockchain: una lista de bloques. Tenemos bloques, que apuntan al bloque anterior. Estos deben tener un número de identificación creciente y un hash que se adhiera a nuestras reglas de minería.

Si pedimos obtener nuevos bloques de otros nodos, los validamos y, si están bien, los agregamos a la cadena. Si obtenemos una cadena de bloques completa de otro nodo, también la validamos y, si es más larga que la nuestra (es decir, tiene más bloques), reemplazamos nuestra propia cadena con ella.

Como puede imaginar, dado que cada nodo implementa esta lógica exacta, los bloques y las cadenas acordadas pueden propagarse a través de la red rápidamente y la red converge al mismo estado (como con las limitaciones de manejo de errores antes mencionadas en nuestro caso simple).

Conceptos básicos de igual a igual

A continuación, implementaremos la pila de red basada en P2P .

Comience por crear un p2p.rsarchivo, que contendrá la mayor parte de la lógica peer-to-peer que usaremos en nuestra aplicación.

Allí, nuevamente, definimos algunas estructuras de datos básicas y constantes que necesitaremos:

pub static KEYS: Lazy = Lazy::new(identity::Keypair::generate_ed25519);
pub static PEER_ID: Lazy = Lazy::new(|| PeerId::from(KEYS.public()));
pub static CHAIN_TOPIC: Lazy = Lazy::new(|| Topic::new("chains"));
pub static BLOCK_TOPIC: Lazy = Lazy::new(|| Topic::new("blocks"));

#[derive(Debug, Serialize, Deserialize)]
pub struct ChainResponse {
    pub blocks: Vec,
    pub receiver: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LocalChainRequest {
    pub from_peer_id: String,
}

pub enum EventType {
    LocalChainResponse(ChainResponse),
    Input(String),
    Init,
}

#[derive(NetworkBehaviour)]
pub struct AppBehaviour {
    pub floodsub: Floodsub,
    pub mdns: Mdns,
    #[behaviour(ignore)]
    pub response_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub init_sender: mpsc::UnboundedSender,
    #[behaviour(ignore)]
    pub app: App,
}

Comenzando desde arriba, definimos un par de claves y un ID de par derivado. Esos son simplemente los elementos intrínsecos de libp2p para identificar un cliente en la red.

Luego, definimos dos de los llamados topics: chainsy blocks. Usaremos el FloodSubprotocolo, un protocolo simple de publicación / suscripción, para la comunicación entre los nodos.

Esto tiene la ventaja de que es muy simple de configurar y usar, pero tiene la desventaja de que necesitamos transmitir cada pieza de información. Entonces, incluso si solo queremos responder a la "solicitud de nuestra cadena" de un cliente, ese cliente enviará esta solicitud a todos los nodos a los que están conectados en la red y también enviaremos nuestra respuesta a todos ellos.

Esto no es un problema en términos de corrección, pero en términos de eficiencia, obviamente es horrible. Esto podría manejarse mediante un modelo simple de solicitud / respuesta punto a punto, que es algo que libp2p admite, pero esto simplemente agregaría aún más complejidad a este ejemplo ya complejo. Si está interesado, puede consultar los documentos de libp2p .

También podríamos usar el más eficiente en GossipSublugar de FloodSub. Pero, nuevamente, no es tan conveniente de configurar y realmente no estamos particularmente interesados ​​en el rendimiento en este momento. La interfaz es muy similar. Nuevamente, si está interesado en jugar con esto, consulte los documentos oficiales .

De todos modos, los temas son básicamente "canales" a los que suscribirse. Podemos suscribirnos a "cadenas" y usarlas para enviar nuestra cadena de bloques local a otros nodos y recibir la de ellos. Lo mismo ocurre con los "bloques", que usaremos para transmitir y recibir nuevos bloques.

A continuación, tenemos el concepto de ChainResponsetener una lista de bloques y un receptor. Esta es una estructura, que esperaremos si alguien nos envía su cadena de bloques local y la usamos para enviarle nuestra cadena local.

El LocalChainRequestes lo que desencadena esta interacción. Si enviamos un LocalChainRequestcon el peer_idde otro nodo en el sistema, esto activará que nos envíen su cadena de regreso, como veremos más adelante.

Para manejar mensajes entrantes, inicialización diferida y entrada de teclado por parte del usuario del cliente, definimos la EventTypeenumeración, que nos ayudará a enviar eventos a través de la aplicación para mantener el estado de nuestra aplicación sincronizado con el tráfico de red entrante y saliente.

Finalmente, el núcleo de la funcionalidad P2P es nuestro AppBehaviour, que implementa NetworkBehaviourel concepto de libp2p para implementar una pila de red descentralizada.

No entraremos en el meollo de la cuestión aquí, pero mi tutorial completo de libp2p entra en más detalles sobre esto.

El AppBehavioursostiene nuestra instancia FloodSub para pub / sub comunicación y e instancia de acuses de recibo, lo que nos permitirá encontrar de forma automática otros nodos de nuestra red local (pero no fuera de ella).

También agregamos nuestra cadena Appde bloques a este comportamiento, así como canales para enviar eventos tanto para la inicialización como para la comunicación de solicitud / respuesta entre partes de la aplicación. Veremos esto en acción más adelante.

Inicializar AppBehaviourtambién es bastante sencillo:

impl AppBehaviour {
    pub async fn new(
        app: App,
        response_sender: mpsc::UnboundedSender,
        init_sender: mpsc::UnboundedSender,
    ) -> Self {
        let mut behaviour = Self {
            app,
            floodsub: Floodsub::new(*PEER_ID),
            mdns: Mdns::new(Default::default())
                .await
                .expect("can create mdns"),
            response_sender,
            init_sender,
        };
        behaviour.floodsub.subscribe(CHAIN_TOPIC.clone());
        behaviour.floodsub.subscribe(BLOCK_TOPIC.clone());

        behaviour
    }
}

Manejo de mensajes entrantes

Primero, implementamos los controladores para los datos que provienen de otros nodos.

Comenzaremos con los eventos Mdns, ya que son básicamente un texto estándar:

impl NetworkBehaviourEventProcess<MdnsEvent> for AppBehaviour {
    fn inject_event(&mut self, event: MdnsEvent) {
        match event {
            MdnsEvent::Discovered(discovered_list) => {
                for (peer, _addr) in discovered_list {
                    self.floodsub.add_node_to_partial_view(peer);
                }
            }
            MdnsEvent::Expired(expired_list) => {
                for (peer, _addr) in expired_list {
                    if !self.mdns.has_node(&peer) {
                        self.floodsub.remove_node_from_partial_view(&peer);
                    }
                }
            }
        }
    }
}

Si se descubre un nuevo nodo, lo agregamos a nuestra lista de nodos FloodSub para que podamos comunicarnos. Una vez que caduque, lo volvemos a quitar.

Más interesante es la implementación de NetworkBehaviourpara nuestro protocolo de comunicación FloodSub.

// incoming event handler
impl NetworkBehaviourEventProcess for AppBehaviour {
    fn inject_event(&mut self, event: FloodsubEvent) {
        if let FloodsubEvent::Message(msg) = event {
            if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                if resp.receiver == PEER_ID.to_string() {
                    info!("Response from {}:", msg.source);
                    resp.blocks.iter().for_each(|r| info!("{:?}", r));

                    self.app.blocks = self.app.choose_chain(self.app.blocks.clone(), resp.blocks);
                }
            } else if let Ok(resp) = serde_json::from_slice::(&msg.data) {
                info!("sending local chain to {}", msg.source.to_string());
                let peer_id = resp.from_peer_id;
                if PEER_ID.to_string() == peer_id {
                    if let Err(e) = self.response_sender.send(ChainResponse {
                        blocks: self.app.blocks.clone(),
                        receiver: msg.source.to_string(),
                    }) {
                        error!("error sending response via channel, {}", e);
                    }
                }
            } else if let Ok(block) = serde_json::from_slice::(&msg.data) {
                info!("received new block from {}", msg.source.to_string());
                self.app.try_add_block(block);
            }
        }
    }
}

Para los eventos entrantes, que son FloodsubEvent::Message, verificamos si la carga útil se ajusta a alguna de nuestras estructuras de datos esperadas.

Si es un ChainResponse, significa que otro nodo nos envió una cadena de bloques local.

Verificamos si en realidad somos el receptor de dicho dato y, de ser así, registramos la cadena de bloques entrante e intentamos ejecutar nuestro consenso. Si es válido y más largo que nuestra cadena, reemplazamos nuestra cadena con él. De lo contrario, mantenemos nuestra propia cadena.

Si los datos entrantes son a LocalChainRequest, verificamos si somos de quienes quieren la cadena, marcando el from_peer_id. Si es así, simplemente les enviamos una versión JSON de nuestra cadena de bloques local. La parte de envío real está en otra parte del código, pero por ahora, simplemente la enviamos a través de nuestro canal de eventos para obtener respuestas.

Finalmente, si Blockes entrante, significa que alguien más extrajo un bloque y quiere que lo agreguemos a nuestra cadena local. Comprobamos si el bloque es válido y, si lo es, lo añadimos.

Poniendolo todo junto

¡Estupendo! Ahora conectemos todo esto y agreguemos algunos comandos para que los usuarios interactúen con la aplicación.

De vuelta main.rs, es hora de implementar la mainfunción.

Empezamos con la configuración:

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    info!("Peer Id: {}", p2p::PEER_ID.clone());
    let (response_sender, mut response_rcv) = mpsc::unbounded_channel();
    let (init_sender, mut init_rcv) = mpsc::unbounded_channel();

    let auth_keys = Keypair::::new()
        .into_authentic(&p2p::KEYS)
        .expect("can create auth keys");

    let transp = TokioTcpConfig::new()
        .upgrade(upgrade::Version::V1)
        .authenticate(NoiseConfig::xx(auth_keys).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();

    let behaviour = p2p::AppBehaviour::new(App::new(), response_sender, init_sender.clone()).await;

    let mut swarm = SwarmBuilder::new(transp, behaviour, *p2p::PEER_ID)
        .executor(Box::new(|fut| {
            spawn(fut);
        }))
        .build();

    let mut stdin = BufReader::new(stdin()).lines();

    Swarm::listen_on(
        &mut swarm,
        "/ip4/0.0.0.0/tcp/0"
            .parse()
            .expect("can get a local socket"),
    )
    .expect("swarm can be started");

    spawn(async move {
        sleep(Duration::from_secs(1)).await;
        info!("sending init event");
        init_sender.send(true).expect("can send init event");
    });

Eso es mucho código, pero básicamente configura cosas de las que ya hablamos. Inicializamos el registro y nuestros dos canales de eventos para la inicialización y las respuestas.

Luego, inicializamos nuestro par de claves, el transporte, el comportamiento de libp2p y el enjambre de libp2p, que es la entidad que ejecuta nuestra pila de red.

También inicializamos un lector en búfer activado stdinpara que podamos leer los comandos entrantes del usuario e iniciar nuestro Swarm.

Finalmente, generamos una corrutina asincrónica, que espera un segundo y luego envía un disparador de inicialización en el canal de inicio.

Esta es la señal que usaremos después de iniciar un nodo para esperar un poco hasta que el nodo esté encendido y conectado. Luego le pedimos a otro nodo su cadena de bloques actual para ponernos al día.

El resto maines la parte interesante: la parte en la que manejamos los eventos de teclado del usuario, los datos entrantes y los datos salientes.

    loop {
        let evt = {
            select! {
                line = stdin.next_line() => Some(p2p::EventType::Input(line.expect("can get line").expect("can read line from stdin"))),
                response = response_rcv.recv() => {
                    Some(p2p::EventType::LocalChainResponse(response.expect("response exists")))
                },
                _init = init_rcv.recv() => {
                    Some(p2p::EventType::Init)
                }
                event = swarm.select_next_some() => {
                    info!("Unhandled Swarm Event: {:?}", event);
                    None
                },
            }
        };

        if let Some(event) = evt {
            match event {
                p2p::EventType::Init => {
                    let peers = p2p::get_list_peers(&swarm);
                    swarm.behaviour_mut().app.genesis();

                    info!("connected nodes: {}", peers.len());
                    if !peers.is_empty() {
                        let req = p2p::LocalChainRequest {
                            from_peer_id: peers
                                .iter()
                                .last()
                                .expect("at least one peer")
                                .to_string(),
                        };

                        let json = serde_json::to_string(&req).expect("can jsonify request");
                        swarm
                            .behaviour_mut()
                            .floodsub
                            .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                    }
                }
                p2p::EventType::LocalChainResponse(resp) => {
                    let json = serde_json::to_string(&resp).expect("can jsonify response");
                    swarm
                        .behaviour_mut()
                        .floodsub
                        .publish(p2p::CHAIN_TOPIC.clone(), json.as_bytes());
                }
                p2p::EventType::Input(line) => match line.as_str() {
                    "ls p" => p2p::handle_print_peers(&swarm),
                    cmd if cmd.starts_with("ls c") => p2p::handle_print_chain(&swarm),
                    cmd if cmd.starts_with("create b") => p2p::handle_create_block(cmd, &mut swarm),
                    _ => error!("unknown command"),
                },
            }
        }
    }

Comenzamos un ciclo sin fin y usamos la select!macro de Tokio para competir con múltiples funciones asíncronas.

Esto significa que cualquiera de estos acabados primero se manejará primero y luego comenzaremos de nuevo.

El primer emisor de eventos es nuestro lector en búfer, que nos dará líneas de entrada del usuario. Si obtenemos uno, creamos un EventType::Inputcon la línea.

Luego, escuchamos el canal de respuesta y el canal de inicio, creando sus eventos respectivamente.
Y si los eventos entran en el enjambre en sí, esto significa que son eventos que no son manejados por nuestro comportamiento Mdns ni nuestro comportamiento FloodSub y simplemente los registramos. En su mayoría son ruido, como conexión / desconexión en nuestro caso, pero útiles para la depuración.

Con los eventos correspondientes creados (o ningún evento creado), nos ocupamos de manejarlos.

Para nuestro Initevento, invocamos genesis()nuestra aplicación, creando nuestro bloque génesis. Si estamos conectados a nodos, activamos LocalChainRequesta al último de la lista.

Obviamente, aquí tendría sentido preguntar a varios nodos, y tal vez varias veces, y seleccionar la mejor (es decir, la más larga) cadena de respuestas que obtenemos. Pero en aras de la simplicidad, solo pedimos uno y aceptamos lo que sea que nos envíen.

Luego, si obtenemos un LocalChainResponseevento, eso significa que se envió algo en el canal de respuesta. Si recuerda lo anterior, eso sucedió en nuestro comportamiento FloodSub cuando enviamos nuestra cadena de bloques local a un nodo solicitante. Aquí, en realidad enviamos el JSON entrante al tema FloodSub correcto, por lo que se transmite a la red.

Finalmente, para la entrada del usuario, tenemos tres comandos:

  • ls p enumera todos los compañeros
  • ls c imprime la cadena de bloques local
  • create b $datacrea un nuevo bloque con $datasu contenido de cadena

Cada comando llama a una de estas funciones auxiliares:

pub fn get_list_peers(swarm: &Swarm) -> Vec {
    info!("Discovered Peers:");
    let nodes = swarm.behaviour().mdns.discovered_nodes();
    let mut unique_peers = HashSet::new();
    for peer in nodes {
        unique_peers.insert(peer);
    }
    unique_peers.iter().map(|p| p.to_string()).collect()
}

pub fn handle_print_peers(swarm: &Swarm) {
    let peers = get_list_peers(swarm);
    peers.iter().for_each(|p| info!("{}", p));
}

pub fn handle_print_chain(swarm: &Swarm) {
    info!("Local Blockchain:");
    let pretty_json =
        serde_json::to_string_pretty(&swarm.behaviour().app.blocks).expect("can jsonify blocks");
    info!("{}", pretty_json);
}

pub fn handle_create_block(cmd: &str, swarm: &mut Swarm) {
    if let Some(data) = cmd.strip_prefix("create b") {
        let behaviour = swarm.behaviour_mut();
        let latest_block = behaviour
            .app
            .blocks
            .last()
            .expect("there is at least one block");
        let block = Block::new(
            latest_block.id + 1,
            latest_block.hash.clone(),
            data.to_owned(),
        );
        let json = serde_json::to_string(&block).expect("can jsonify request");
        behaviour.app.blocks.push(block);
        info!("broadcasting new block");
        behaviour
            .floodsub
            .publish(BLOCK_TOPIC.clone(), json.as_bytes());
    }
}

Enumerar clientes e imprimir la cadena de bloques es bastante sencillo. Crear un bloque es más interesante.

En ese caso, usamos Block::newpara crear (y extraer) un nuevo bloque. Una vez que eso sucede, lo JSONificamos y lo transmitimos a la red para que otros puedan agregarlo a su cadena.

Aquí es donde pondríamos algo de lógica para r-intentar esto. Por ejemplo, podríamos agregarlo a una cola y ver si, después de un tiempo, nuestro bloque se propaga a la cadena de bloques ampliamente acordada y, si no es así, obtener una nueva copia de la cadena acordada y extraerla nuevamente para obtenerla. alli. Como se mencionó anteriormente, este diseño ciertamente no se escalará a muchos nodos que extraigan sus bloques todo el tiempo, pero eso está bien para el propósito de este tutorial.

¡Empecemos y veamos si funciona!

Probando nuestra cadena de bloques Rust

Podemos iniciar la aplicación usando RUST_LOG=info cargo run. Es mejor iniciar varias instancias en diferentes ventanas de terminal.

Por ejemplo, podemos iniciar dos nodos:

INFO  rust_blockchain_example > Peer Id: 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Y:

INFO  rust_blockchain_example > Peer Id: 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

Usar ls pen la segunda aplicación nos muestra la conexión con la primera:

INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example::p2p > 12D3KooWJWbGzpdakrDroXuCKPRBqmDW8wYc1U3WzWEydVr2qZNv

Luego, podemos usar ls cpara imprimir el bloque de génesis:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  }
]

Hasta ahora tan bueno. creemos un bloque:

create b hello
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 62235, hash: 00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922, binary hash: 0010001100111101101000110110101001111110011111000101010101000101111110101010110110010011111110111000010100001011110001111000000110110111101100010111111100001011011110001111110100011111011000101111111001111110101001100010
 INFO  rust_blockchain_example::p2p > broadcasting new block

En el primer nodo, vemos esto:

INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX

Y llamando ls c:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

¡El bloque se agregó!

Comencemos con un tercer nodo. Debería obtener automáticamente esta cadena actualizada porque es más larga que la suya (solo el bloque de génesis).

INFO  rust_blockchain_example > Peer Id: 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

 INFO  rust_blockchain_example > sending init event
 INFO  rust_blockchain_example::p2p > Discovered Peers:
 INFO  rust_blockchain_example      > connected nodes: 2
 INFO  rust_blockchain_example::p2p > Response from 12D3KooWSXGZJJEnh3tndGEVm6ACQ5pdaPKL34ktmCsUqkqSVTWX:
 INFO  rust_blockchain_example::p2p > Block { id: 0, hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", previous_hash: "genesis", timestamp: 1636664658, data: "genesis!", nonce: 2836 }
 INFO  rust_blockchain_example::p2p > Block { id: 1, hash: "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922", previous_hash: "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43", timestamp: 1636664772, data: " hello", nonce: 62235 }

Después de enviar el initevento, solicitamos la cadena del segundo nodo y la obtuvimos.

Llamar ls caquí nos muestra la misma cadena:

INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  }
]

La creación de un bloque también funciona:

create b alsoworks
 INFO  rust_blockchain_example      > mining block...
 INFO  rust_blockchain_example      > nonce: 0
 INFO  rust_blockchain_example      > mined! nonce: 34855, hash: 0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd, binary hash: 001110000010111101110111111001110110000011110110100110111010110111001101011100010001110100011011101001011101001101110101010101010100010101101100000110110111101110110010101011010110010100101111011110111000011111110111111101
 INFO  rust_blockchain_example::p2p > broadcasting new block

Nodo 1:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q

ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664658,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Nodo 2:

 INFO  rust_blockchain_example::p2p > received new block from 12D3KooWSDyn83pJD4eEg9dvYffceAEcbUkioQvSPY7aCi7J598q
ls c
 INFO  rust_blockchain_example::p2p > Local Blockchain:
 INFO  rust_blockchain_example::p2p > [
  {
    "id": 0,
    "hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "previous_hash": "genesis",
    "timestamp": 1636664655,
    "data": "genesis!",
    "nonce": 2836
  },
  {
    "id": 1,
    "hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "previous_hash": "0000f816a87f806bb0073dcf026a64fb40c946b5abee2573702828694d5b4c43",
    "timestamp": 1636664772,
    "data": " hello",
    "nonce": 62235
  },
  {
    "id": 2,
    "hash": "0000e0bddf4e603da675b92b88e86e25692eaaa8ad20db6ecab5940bdee1fdfd",
    "previous_hash": "00008cf68da9f978aa080b7aad93fb4285e3c0dbd85fc21bc7e83e623f9fa922",
    "timestamp": 1636664920,
    "data": " alsoworks",
    "nonce": 34855
  }
]

Genial, ¡funciona!

Puede jugar e intentar crear condiciones de carrera (por ejemplo, aumentando la dificultad a tres ceros y comenzando múltiples bloques en varios nodos. Notará inmediatamente algunos de los defectos de este diseño, pero los conceptos básicos funcionan. Tenemos un par -to-peer blockchain, un libro mayor descentralizado real con solidez básica, construido completamente desde cero en Rust.

Conclusión

En este tutorial, creamos una aplicación blockchain simple, bastante limitada, pero que funciona en Rust. Nuestra aplicación blockchain tiene un esquema de minería muy básico, consenso y redes de igual a igual en solo 500 líneas de Rust.

La mayor parte de esta simplicidad se debe a la fantástica biblioteca libp2p , que hace todo el trabajo pesado en términos de redes. Claramente, como siempre es el caso en los tutoriales de ingeniería de software, para una aplicación de cadena de bloques de grado de producción, hay muchas, muchas más cosas que considerar y hacer bien.

Sin embargo, este ejercicio prepara el escenario para el tema, explicando algunos de los conceptos básicos y mostrándolos en Rust, de modo que podamos continuar este viaje observando cómo desarrollaríamos una aplicación blockchain que realmente podría usarse en la práctica con un marco como Substrate .

Enlace: https://blog.logrocket.com/how-to-build-a-blockchain-in-rust/

#rust 

Serde Rust: Serialization Framework for Rust

Serde

*Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.*

You may be looking for:

Serde in action

Click to show Cargo.toml. Run this code in the playground.

[dependencies]

# The core APIs, including the Serialize and Deserialize traits. Always
# required when using Serde. The "derive" feature is only required when
# using #[derive(Serialize, Deserialize)] to make Serde work with structs
# and enums defined in your crate.
serde = { version = "1.0", features = ["derive"] }

# Each data format lives in its own crate; the sample code below uses JSON
# but you may be using a different one.
serde_json = "1.0"

 

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    // Convert the Point to a JSON string.
    let serialized = serde_json::to_string(&point).unwrap();

    // Prints serialized = {"x":1,"y":2}
    println!("serialized = {}", serialized);

    // Convert the JSON string back to a Point.
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();

    // Prints deserialized = Point { x: 1, y: 2 }
    println!("deserialized = {:?}", deserialized);
}

Getting help

Serde is one of the most widely used Rust libraries so any place that Rustaceans congregate will be able to help you out. For chat, consider trying the #rust-questions or #rust-beginners channels of the unofficial community Discord (invite: https://discord.gg/rust-lang-community), the #rust-usage or #beginners channels of the official Rust Project Discord (invite: https://discord.gg/rust-lang), or the #general stream in Zulip. For asynchronous, consider the [rust] tag on StackOverflow, the /r/rust subreddit which has a pinned weekly easy questions post, or the Rust Discourse forum. It's acceptable to file a support issue in this repo but they tend not to get as many eyes as any of the above and may get closed without a response after some time.

Download Details:
Author: serde-rs
Source Code: https://github.com/serde-rs/serde
License: View license

#rust  #rustlang 

joe biden

1617255938

¿Cómo migrar los buzones de correo de Exchange a la nube de Office 365?

Si tiene problemas para migrar los buzones de correo de Exchange a Office 365, debe leer este artículo para saber cómo migrar los buzones de correo de Exchange EDB a Office 365. Al migrar a Office 365, los usuarios pueden acceder a sus buzones de correo desde cualquier lugar y desde cualquier dispositivo.

En esta publicación, explicaremos las razones detrás de esta migración y una solución profesional para migrar de Exchange a Office 365.

Razones para migrar Exchange Server a la nube de Office 365

Office 365 apareció por primera vez en 2011 y, dado que se considera la mejor plataforma para aquellas organizaciones que desean administrar todo su sistema de correo electrónico en la nube. Estas son las características clave de Office 365:

  1. Permite trabajar desde cualquier lugar y desde cualquier lugar.
  2. No se preocupe por el spam y el malware.
  3. La seguridad proporcionada por Office 365 es altamente confiable.
  4. Controla el costo total y brinda flexibilidad financiera.
  5. Todas las actualizaciones y mejoras son administradas por Microsoft.

¿Cómo migrar los buzones de correo de Exchange a Office 365?

Hay varias formas manuales de migrar los buzones de correo de Exchange EDB a Office 365, pero para evitar estos complicados y prolongados procedimientos, presentamos una solución de terceros, es decir, la herramienta de migración de Exchange, que es automatizada y directa para la migración de Exchange a Office 365. La herramienta funciona rápidamente y migra todos los elementos del buzón de Exchange Server a Office 365.

La herramienta de migración de Datavare Exchange es demasiado fácil de usar y ofrece pasos sencillos para migrar EDB a Office 365:

  1. Descargue e instale el software en su sistema.
  2. Agregue el archivo EDB de Exchange con el botón Examinar.
  3. Seleccione exportar a buzones de correo de Office 365.
  4. Proporcione los detalles de inicio de sesión de la cuenta de Office 365.
  5. Seleccione la carpeta y presione el botón Finalizar.

Por lo tanto, todos sus buzones de correo de Exchange EDB ahora se migran a Office 365.
Nota: puede usar filtros para migrar los elementos de datos deseados de la cuenta de Exchange a la de Office 365

Líneas finales

Este blog le indica una solución profesional para la migración de buzones de correo de Exchange a la cuenta de Office 365. Dado que las soluciones manuales son complicadas, sugerimos la herramienta de migración de Exchange, que es demasiado simple de usar. Los usuarios no se enfrentan a problemas al operar el programa. La mejor parte de este software es que no necesita habilidades técnicas para realizar la migración. Se puede comprender el funcionamiento del software descargando la versión de demostración que permite la migración de los primeros 50 elementos por carpeta.

Más información:- https://www.datavare.com/software/edb-migration.html

#herramienta de migración de intercambio #migración de intercambio #migrar buzones de correo de exchange

joe biden

1617257581

Software de restauración de Exchange para restaurar sin problemas PST en Exchange Server

¿Quiere restaurar los buzones de correo de PST a Exchange Server? Entonces, estás en la página correcta. Aquí, lo guiaremos sobre cómo puede restaurar fácilmente mensajes y otros elementos de PST a MS Exchange Server.

Muchas veces, los usuarios necesitan restaurar los elementos de datos de PST en Exchange Server, pero debido a la falta de disponibilidad de una solución confiable, los usuarios no pueden obtener la solución. Háganos saber primero sobre el archivo PST y MS Exchange Server.

Conozca PST y Exchange Server

PST es un formato de archivo utilizado por MS Outlook, un cliente de correo electrónico de Windows y muy popular entre los usuarios domésticos y comerciales.

Por otro lado, Exchange Server es un poderoso servidor de correo electrónico donde todos los datos se almacenan en un archivo EDB. Los usuarios generalmente guardan la copia de seguridad de los buzones de correo de Exchange en el archivo PST, pero muchas veces, los usuarios deben restaurar los datos del archivo PST en Exchange. Para resolver este problema, estamos aquí con una solución profesional que discutiremos en la siguiente sección de esta publicación.

Un método profesional para restaurar PST a Exchange Server

No le recomendamos que elija una solución al azar para restaurar los datos de PST en Exchange Server. Por lo tanto, al realizar varias investigaciones, estamos aquí con una solución inteligente y conveniente, es decir, Exchange Restore Software. Es demasiado fácil de manejar por todos los usuarios y restaurar cómodamente todos los datos del archivo PST a Exchange Server.

Funciones principales ofrecidas por Exchange Restore Software

El software es demasiado simple de usar y se puede instalar fácilmente en todas las versiones de Windows. Con unos pocos clics, la herramienta puede restaurar los elementos del buzón de Exchange.

No es necesario que MS Outlook restaure los datos PST en Exchange. Todos los correos electrónicos, contactos, notas, calendarios, etc. se restauran desde el archivo PST a Exchange Server.

Todas las versiones de Outlook son compatibles con la herramienta, como Outlook 2019, 2016, 2013, 2010, 2007, etc. La herramienta proporciona varios filtros mediante los cuales se pueden restaurar los datos deseados desde un archivo PST a Exchange Server. El programa se puede instalar en todas las versiones de Windows como Windows 10, 8.1, 8, 7, XP, Vista, etc.

Descargue la versión de demostración del software de restauración de Exchange y analice el funcionamiento del software restaurando los primeros 50 elementos por carpeta.

Líneas finales

No existe una solución manual para restaurar los buzones de correo de Exchange desde el archivo PST. Por lo tanto, hemos explicado una solución fácil e inteligente para restaurar datos de archivos PST en Exchange Server. Simplemente puede usar este software y restaurar todos los datos de PST a Exchange Server.

Más información:- https://www.datavare.com/software/exchange-restore.html

#intercambio de software de restauración #intercambio de restauración #buzón del servidor de intercambio #herramienta de restauración de intercambio