1637910399
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:
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.
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!
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.toml
archivo 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 sha2
biblioteca para nuestro hash sha256 y la hex
caja para transformar los hash binarios en hexadecimales legibles y transferibles.
Además de eso, en realidad solo hay utilidades como serde
JSON log
, y pretty_env_logger
para el registro, once_cell
para la inicialización estática y chrono
para 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.
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 App
estructura 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 Block
nuestro 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 mining
del 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 App
estructura:
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 genesis
mé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.
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_valid
continuació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_PREFIX
condició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:
previous_hash
necesidades de hacer coincidir realmente el hash del último bloque de la cadena.hash
necesidades para comenzar con nuestro DIFFICULTY_PREFIX
(es decir, dos ceros), lo que indica que se extrajo correctamenteid
necesidades para ser el último ID incrementa en 1001
)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 6
con 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.
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 App
que 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".
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 nonce
a 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_block
funció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 nonce
en 0.
Luego, comenzamos un ciclo sin fin, que incrementa el nonce
en 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_representation
ayudante 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 nonce
y volvemos a ir.
Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el nonce
y 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 nonce
en nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonce
está 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), nonce
serí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 sha2
pasamos 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).
A continuación, implementaremos la pila de red basada en P2P .
Comience por crear un p2p.rs
archivo, 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
: chains
y blocks
. Usaremos el FloodSub
protocolo, 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 GossipSub
lugar 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 ChainResponse
tener 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 LocalChainRequest
es lo que desencadena esta interacción. Si enviamos un LocalChainRequest
con el peer_id
de 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 EventType
enumeració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 NetworkBehaviour
el 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 AppBehaviour
sostiene 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 App
de 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 AppBehaviour
tambié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
}
}
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 NetworkBehaviour
para 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 Block
es 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.
¡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 main
funció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 stdin
para 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 main
es 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::Input
con 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 Init
evento, invocamos genesis()
nuestra aplicación, creando nuestro bloque de génesis. Si estamos conectados a nodos, activamos LocalChainRequest
a 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 LocalChainResponse
evento, 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ñerosls c
imprime la cadena de bloques localcreate b $data
crea un nuevo bloque con $data
su contenido de cadenaCada 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::new
para 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!
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 p
de 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 c
para 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 init
evento, solicitamos la cadena del segundo nodo y la obtuvimos.
Llamar ls c
aquí 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 .
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
1637910399
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:
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.
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!
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.toml
archivo 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 sha2
biblioteca para nuestro hash sha256 y la hex
caja para transformar los hash binarios en hexadecimales legibles y transferibles.
Además de eso, en realidad solo hay utilidades como serde
JSON log
, y pretty_env_logger
para el registro, once_cell
para la inicialización estática y chrono
para 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.
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 App
estructura 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 Block
nuestro 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 mining
del 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 App
estructura:
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 genesis
mé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.
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_valid
continuació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_PREFIX
condició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:
previous_hash
necesidades de hacer coincidir realmente el hash del último bloque de la cadena.hash
necesidades para comenzar con nuestro DIFFICULTY_PREFIX
(es decir, dos ceros), lo que indica que se extrajo correctamenteid
necesidades para ser el último ID incrementa en 1001
)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 6
con 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.
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 App
que 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".
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 nonce
a 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_block
funció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 nonce
en 0.
Luego, comenzamos un ciclo sin fin, que incrementa el nonce
en 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_representation
ayudante 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 nonce
y volvemos a ir.
Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el nonce
y 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 nonce
en nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonce
está 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), nonce
serí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 sha2
pasamos 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).
A continuación, implementaremos la pila de red basada en P2P .
Comience por crear un p2p.rs
archivo, 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
: chains
y blocks
. Usaremos el FloodSub
protocolo, 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 GossipSub
lugar 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 ChainResponse
tener 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 LocalChainRequest
es lo que desencadena esta interacción. Si enviamos un LocalChainRequest
con el peer_id
de 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 EventType
enumeració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 NetworkBehaviour
el 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 AppBehaviour
sostiene 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 App
de 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 AppBehaviour
tambié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
}
}
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 NetworkBehaviour
para 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 Block
es 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.
¡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 main
funció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 stdin
para 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 main
es 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::Input
con 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 Init
evento, invocamos genesis()
nuestra aplicación, creando nuestro bloque de génesis. Si estamos conectados a nodos, activamos LocalChainRequest
a 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 LocalChainResponse
evento, 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ñerosls c
imprime la cadena de bloques localcreate b $data
crea un nuevo bloque con $data
su contenido de cadenaCada 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::new
para 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!
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 p
de 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 c
para 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 init
evento, solicitamos la cadena del segundo nodo y la obtuvimos.
Llamar ls c
aquí 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 .
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
1641220320
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.
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.
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!
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.toml
archivo 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 sha2
biblioteca para nuestro hash sha256 y la hex
caja para transformar los hash binarios en hexadecimales legibles y transferibles.
Además de eso, en realidad solo hay utilidades como serde
JSON log
, y pretty_env_logger
para el registro, once_cell
para la inicialización estática y chrono
para 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.
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 App
estructura 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 Block
nuestro 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 mining
del 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 App
estructura:
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 genesis
mé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.
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_valid
continuació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_PREFIX
condició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:
previous_hash
necesidades de hacer coincidir realmente el hash del último bloque de la cadena.hash
necesidades para comenzar con nuestro DIFFICULTY_PREFIX
(es decir, dos ceros), lo que indica que se extrajo correctamenteid
necesidades para ser el último ID incrementa en 1001
)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 6
con 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.
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 App
que 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".
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 nonce
a 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_block
funció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 nonce
en 0.
Luego, comenzamos un ciclo sin fin, que incrementa el nonce
en 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_representation
ayudante 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
número entero creciente, dónde sucedió y el hash (codificado en hexadecimal). De lo contrario, lo incrementamos nonce
y vamos de nuevo.
Esencialmente, estamos tratando desesperadamente de encontrar un dato; en este caso, el nonce
y 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 nonce
en nuestro bloque para que otros nodos puedan verificar nuestro hash, ya que nonce
está 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), nonce
serí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 sha2
pasamos 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).
A continuación, implementaremos la pila de red basada en P2P .
Comience por crear un p2p.rs
archivo, 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
: chains
y blocks
. Usaremos el FloodSub
protocolo, 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 GossipSub
lugar 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 ChainResponse
tener 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 LocalChainRequest
es lo que desencadena esta interacción. Si enviamos un LocalChainRequest
con el peer_id
de 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 EventType
enumeració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 NetworkBehaviour
el 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 AppBehaviour
sostiene 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 App
de 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 AppBehaviour
tambié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
}
}
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 NetworkBehaviour
para 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 Block
es 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.
¡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 main
funció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 stdin
para 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 main
es 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::Input
con 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 Init
evento, invocamos genesis()
nuestra aplicación, creando nuestro bloque génesis. Si estamos conectados a nodos, activamos LocalChainRequest
a 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 LocalChainResponse
evento, 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ñerosls c
imprime la cadena de bloques localcreate b $data
crea un nuevo bloque con $data
su contenido de cadenaCada 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::new
para 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!
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 p
en 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 c
para 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 init
evento, solicitamos la cadena del segundo nodo y la obtuvimos.
Llamar ls c
aquí 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.
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/
1643176207
Serde
*Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.*
You may be looking for:
#[derive(Serialize, Deserialize)]
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);
}
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
1617255938
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.
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:
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:
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
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
1617257581
¿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.
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.
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.
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.
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