bless
This commit is contained in:
commit
f42c4ace56
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
1439
Cargo.lock
generated
Normal file
1439
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "blockchain"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.41"
|
||||
clap = { version = "4.5.45", features = ["derive"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
thiserror = "2.0.16"
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
tokio-tungstenite = "0.27.0"
|
||||
warp = { version = "0.4.2", features = ["server", "websocket"] }
|
||||
wasm-bindgen = "0.2.100"
|
||||
web-sys = { version = "0.3.77", features = ["WebSocket"] }
|
||||
7
database/genesis.json
Normal file
7
database/genesis.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"genesis_time": "2019-03-18T00:00:00.000000000Z",
|
||||
"chain_id": "the-blockchain-bar-ledger",
|
||||
"balances": {
|
||||
"andrej": 1000000
|
||||
}
|
||||
}
|
||||
5
database/state.json
Normal file
5
database/state.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"balances": {
|
||||
"andrej": 998801
|
||||
}
|
||||
}
|
||||
15
database/tx.db
Normal file
15
database/tx.db
Normal file
@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"head": {
|
||||
"previous_hash": "0000",
|
||||
"timestamp": 1234567890,
|
||||
"merkle_root": "abc123",
|
||||
"block_hash": "def456",
|
||||
"nonce": 5
|
||||
},
|
||||
"tx": [
|
||||
{"from": "alice", "to": "bob", "value": 10, "data": ""},
|
||||
{"from": "bob", "to": "charlie", "value": 5, "data": ""}
|
||||
]
|
||||
}
|
||||
]
|
||||
41
src/args.rs
Normal file
41
src/args.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use crate::tx::Tx;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Transaction Options (add, ...)
|
||||
#[command(short_flag = 't')]
|
||||
Tx {
|
||||
#[command(subcommand)]
|
||||
tx_command: TxCmd
|
||||
},
|
||||
/// Show accounts and balances
|
||||
#[command(short_flag = 'l')]
|
||||
List,
|
||||
#[command(short_flag = 's')]
|
||||
Seed
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum TxCmd {
|
||||
/// Add a new transaction to the DB
|
||||
#[command(short_flag = 'a')]
|
||||
Add(Tx)
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn get_commands(&self) -> &Commands {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_args() -> Args {
|
||||
Args::parse()
|
||||
}
|
||||
22
src/block.rs
Normal file
22
src/block.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::tx::Tx;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct BlockHeader {
|
||||
previous_hash: String,
|
||||
timestamp: u64,
|
||||
merkle_root: String,
|
||||
block_hash: String,
|
||||
nonce: u32
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Block {
|
||||
head: BlockHeader,
|
||||
tx: Vec<Tx>
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn get_header(&self) -> &BlockHeader {
|
||||
&self.head
|
||||
}
|
||||
}
|
||||
53
src/error.rs
Normal file
53
src/error.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BlockchainError {
|
||||
#[error("invalid account creation")]
|
||||
InvalidAccountCreation,
|
||||
#[error("Transactional error")]
|
||||
Tx(#[from] TxError)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TxError {
|
||||
#[error("from field is empty")]
|
||||
FromEmpty,
|
||||
#[error("to field is empty")]
|
||||
ToEmpty,
|
||||
#[error("insuffitient fonds")]
|
||||
FromInsuffitientFonds,
|
||||
#[error("0 value transaction")]
|
||||
ValueEmpty,
|
||||
#[error("account {0} not found in database")]
|
||||
UnknownAccount(String)
|
||||
}
|
||||
|
||||
pub fn print_error_chain(err: &dyn std::error::Error) {
|
||||
eprintln!("Error: {}", err);
|
||||
|
||||
let mut source = err.source();
|
||||
let mut level = 1;
|
||||
|
||||
while let Some(err) = source {
|
||||
eprintln!(" {}: {}", level, err);
|
||||
source = err.source();
|
||||
level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_error(err: BlockchainError) {
|
||||
match &err {
|
||||
BlockchainError::Tx(tx) => {
|
||||
match tx {
|
||||
TxError::FromEmpty => { }
|
||||
TxError::ToEmpty => { }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
print_error_chain(&err)
|
||||
}
|
||||
|
||||
115
src/log.rs
Normal file
115
src/log.rs
Normal file
@ -0,0 +1,115 @@
|
||||
pub const INFO: usize = 0;
|
||||
pub const DEBUG: usize = 1;
|
||||
pub const WARNING: usize = 2;
|
||||
pub const ERROR: usize = 3;
|
||||
pub const FATAL: usize = 4;
|
||||
|
||||
pub const LOG_LEVEL: [&str; 5] = [
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"WARNING",
|
||||
"ERROR",
|
||||
"FATAL",
|
||||
];
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
pub fn colored(text: &str, color: &str) -> String {
|
||||
// For terminals: use ANSI escape codes
|
||||
let ansi_code = match color {
|
||||
"black" => "30",
|
||||
"red" => "31",
|
||||
"green" => "32",
|
||||
"yellow" => "33",
|
||||
"blue" => "34",
|
||||
"purple" => "35",
|
||||
"cyan" => "36",
|
||||
"white" => "37",
|
||||
_ => "0", // default no color
|
||||
};
|
||||
format!("\x1b[{ansi_code}m{text}\x1b[0m")
|
||||
}
|
||||
|
||||
pub fn current_timestamp() -> String {
|
||||
Utc::now().format("%Y-%m-%d@%H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
/// You can use this macro to create error messages that will show in the console
|
||||
/// if run on the client or in the terminal if run on the server
|
||||
///
|
||||
/// log_levels are :
|
||||
/// - INFO
|
||||
/// - DEBUG
|
||||
/// - WARNING
|
||||
/// - ERROR
|
||||
/// - FATAL
|
||||
///
|
||||
/// important: FATAL will cause the thread to panic and stop execution
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:expr, $($arg:tt)*) => {{
|
||||
let formatted_msg = format!($($arg)*);
|
||||
match $level {
|
||||
$crate::log::INFO => {
|
||||
println!(
|
||||
"[{}] {} | {}",
|
||||
$crate::log::colored($crate::log::LOG_LEVEL[$level], "green"),
|
||||
$crate::log::current_timestamp(),
|
||||
formatted_msg
|
||||
);
|
||||
},
|
||||
$crate::log::DEBUG => {
|
||||
println!(
|
||||
"[{}] [{}:{}] {} | {}",
|
||||
$crate::log::LOG_LEVEL[$level],
|
||||
file!(),
|
||||
line!(),
|
||||
$crate::log::current_timestamp(),
|
||||
formatted_msg
|
||||
);
|
||||
},
|
||||
$crate::log::WARNING => {
|
||||
println!(
|
||||
"[{}] [{}:{}] {} | {}",
|
||||
$crate::log::colored($crate::log::LOG_LEVEL[$level], "yellow"),
|
||||
file!(),
|
||||
line!(),
|
||||
$crate::log::current_timestamp(),
|
||||
formatted_msg
|
||||
);
|
||||
},
|
||||
$crate::log::ERROR => {
|
||||
println!(
|
||||
"[{}] [{}:{}] {} | {}",
|
||||
$crate::log::colored($crate::log::LOG_LEVEL[$level], "red"),
|
||||
file!(),
|
||||
line!(),
|
||||
$crate::log::current_timestamp(),
|
||||
formatted_msg
|
||||
);
|
||||
},
|
||||
$crate::log::FATAL => {
|
||||
println!(
|
||||
"[{}] [{}:{}] {} | {}",
|
||||
$crate::log::colored($crate::log::LOG_LEVEL[$level], "red"),
|
||||
file!(),
|
||||
line!(),
|
||||
$crate::log::current_timestamp(),
|
||||
formatted_msg
|
||||
);
|
||||
if $level == $crate::log::FATAL {
|
||||
panic!("Fatal error encountered: {}", formatted_msg);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!(
|
||||
"[{}] {} {}",
|
||||
$crate::log::LOG_LEVEL[$crate::log::ERROR],
|
||||
"loggin error: log_level value invalid:",
|
||||
$level
|
||||
);
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
45
src/main.rs
Normal file
45
src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use error::{ BlockchainError, handle_error };
|
||||
|
||||
#[macro_use]
|
||||
pub mod log;
|
||||
pub mod node;
|
||||
pub mod tx;
|
||||
pub mod error;
|
||||
pub mod block;
|
||||
pub mod args;
|
||||
pub mod network_native;
|
||||
pub mod network_browser;
|
||||
|
||||
use crate::tx::Tx;
|
||||
use crate::args::{get_args, TxCmd, Commands};
|
||||
|
||||
fn add_transaction(tx: Tx) -> Result<(), BlockchainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_accounts() {
|
||||
println!("Account Balances:\n-----------------");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
||||
let args = get_args();
|
||||
|
||||
match args.get_commands() {
|
||||
Commands::Tx{ tx_command } => {
|
||||
match tx_command {
|
||||
TxCmd::Add(tx) => {
|
||||
add_transaction(tx.clone()).unwrap_or_else(|e| handle_error(e))
|
||||
}
|
||||
}
|
||||
},
|
||||
Commands::List => {
|
||||
list_accounts()
|
||||
}
|
||||
Commands::Seed => {
|
||||
network_native::NativeNode::run_as_seed().await;
|
||||
}
|
||||
}
|
||||
println!("Hello, world!");
|
||||
}
|
||||
3
src/network_browser.rs
Normal file
3
src/network_browser.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub struct BrowserNetwork {
|
||||
|
||||
}
|
||||
125
src/network_native.rs
Normal file
125
src/network_native.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use crate::node::{ Account, Blockchain, };
|
||||
use crate::block::Block;
|
||||
use warp::Filter;
|
||||
use std::io::{Read, Write};
|
||||
use std::collections::HashMap;
|
||||
use crate::log::*;
|
||||
|
||||
use crate::network_native;
|
||||
|
||||
struct TcpPeer {
|
||||
stream: tokio::net::TcpStream,
|
||||
addr: std::net::SocketAddr
|
||||
}
|
||||
|
||||
pub struct NativeNode {
|
||||
tcp: Vec<TcpPeer>,
|
||||
ws: Vec<web_sys::WebSocket>,
|
||||
chain: crate::node::Blockchain,
|
||||
db_file: std::fs::File
|
||||
}
|
||||
|
||||
async fn handle_browser_connection(websocket: warp::ws::WebSocket) {
|
||||
println!("Browser connected via WebSocket on addr {:?}", websocket);
|
||||
}
|
||||
|
||||
async fn handle_tcp_connections(listner: tokio::net::TcpListener) {
|
||||
loop {
|
||||
match listner.accept().await {
|
||||
Ok((stream, addr)) => {
|
||||
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Failed to accept TCP connection: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl NativeNode{
|
||||
|
||||
pub fn persist(&mut self) {
|
||||
for t in self.chain.blocks() {
|
||||
let json = serde_json::to_string(&t).unwrap();
|
||||
self.db_file.write(json.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(chain: Blockchain, db_file: std::fs::File) -> Self {
|
||||
Self {
|
||||
tcp: Vec::new(),
|
||||
ws: Vec::new(),
|
||||
chain,
|
||||
db_file
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_seed() -> Self {
|
||||
log!(INFO, "Running As Seed Node");
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut genpath = std::path::PathBuf::from(&cwd);
|
||||
genpath.push("database");
|
||||
genpath.push("genesis.json");
|
||||
let mut gen_file = std::fs::File::open(genpath).unwrap();
|
||||
|
||||
let mut buf = String::new();
|
||||
gen_file.read_to_string(&mut buf).unwrap();
|
||||
|
||||
let mut db_file: std::fs::File = {
|
||||
let mut db_path = std::path::PathBuf::from(&cwd);
|
||||
db_path.push("database");
|
||||
db_path.push("tx.db");
|
||||
|
||||
std::fs::OpenOptions::new().read(true).write(true).create(true).open(&db_path).unwrap()
|
||||
};
|
||||
|
||||
let balances: HashMap<Account, u32> = {
|
||||
if let Ok(genesis) = serde_json::from_str::<crate::node::Genesis>(&buf) {
|
||||
genesis.balances.clone()
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
buf.clear();
|
||||
db_file.read_to_string(&mut buf).unwrap();
|
||||
|
||||
let block = serde_json::from_str::<Vec<Block>>(&buf).unwrap();
|
||||
let chain = Blockchain::new(balances, block, vec![]);
|
||||
|
||||
Self::new(chain, db_file)
|
||||
}
|
||||
|
||||
pub async fn connect_to_peer(&mut self, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Attempting to connect to peer: {}", addr);
|
||||
|
||||
let stream = tokio::net::TcpStream::connect(addr).await?;
|
||||
let peer_addr = stream.peer_addr()?;
|
||||
|
||||
println!("Connected to peer: {}", peer_addr);
|
||||
|
||||
let peer = TcpPeer {
|
||||
stream,
|
||||
addr: peer_addr
|
||||
};
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_as_seed() {
|
||||
let node = NativeNode::new_seed();
|
||||
let tcp_listner = tokio::net::TcpListener::bind("0.0.0.0:8333").await.unwrap();
|
||||
let websocket_route = warp::path("ws")
|
||||
.and(warp::ws())
|
||||
.map(|ws: warp::ws::Ws| {
|
||||
ws.on_upgrade(handle_browser_connection)
|
||||
});
|
||||
let web_server = warp::serve(websocket_route).run(([0, 0, 0, 0], 8080));
|
||||
tokio::select! {
|
||||
_ = web_server => {}
|
||||
_ = handle_tcp_connections(tcp_listner) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/node.rs
Normal file
104
src/node.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use crate::tx::Tx;
|
||||
use crate::error::{ BlockchainError, TxError };
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::block::Block;
|
||||
|
||||
pub type Account = String;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Genesis {
|
||||
pub genesis_time: String,
|
||||
pub chain_id: String,
|
||||
pub balances: std::collections::HashMap<Account, u32>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Blockchain {
|
||||
balances: std::collections::HashMap<Account, u32>,
|
||||
blocks: Vec<Block>,
|
||||
tx_mempool: Vec<Tx>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Blockchain {
|
||||
pub fn open_account(&mut self, tx: Tx) -> Result<(), BlockchainError> {
|
||||
if !tx.is_new_account() {
|
||||
Err(BlockchainError::InvalidAccountCreation)
|
||||
} else {
|
||||
self.add(tx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, tx: Tx) -> Result<(), BlockchainError> {
|
||||
self.apply(&tx)?;
|
||||
self.tx_mempool.push(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply(&mut self, tx: &Tx) -> Result<(), BlockchainError> {
|
||||
match tx.validate() {
|
||||
Ok(_) => {},
|
||||
Err(e) => return Err(BlockchainError::Tx(e))
|
||||
}
|
||||
|
||||
if let Some(from_balance) = self.balances.get_mut(tx.get_from()) {
|
||||
if *from_balance > tx.get_value() {
|
||||
*from_balance -= tx.get_value();
|
||||
} else {
|
||||
return Err(BlockchainError::Tx(TxError::FromInsuffitientFonds))
|
||||
}
|
||||
} else {
|
||||
return Err(BlockchainError::Tx(TxError::UnknownAccount(tx.get_from().to_string())))
|
||||
}
|
||||
|
||||
if let Some(to_balance) = self.balances.get_mut(&tx.get_to().to_string()) {
|
||||
*to_balance += tx.get_value()
|
||||
} else {
|
||||
if tx.is_new_account() {
|
||||
self.balances.insert(tx.get_to().to_string(), tx.get_value());
|
||||
} else {
|
||||
return Err(BlockchainError::Tx(TxError::UnknownAccount(tx.get_to().to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new(balances: HashMap<Account, u32>, blocks: Vec<Block>, tx_mempool: Vec<Tx>) -> Blockchain {
|
||||
return Self {
|
||||
balances,
|
||||
blocks,
|
||||
tx_mempool
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum BlockchainEvent {
|
||||
// Connection events
|
||||
// NewTcpPeer(TcpPeer),
|
||||
// NewBrowserPeer(BrowserPeer),
|
||||
PeerDisconnected(String),
|
||||
|
||||
// Message events
|
||||
// MessageReceived { message: NetworkMessage, from: String },
|
||||
|
||||
// User commands
|
||||
ConnectToPeer(String),
|
||||
ListPeers,
|
||||
|
||||
// Blockchain events
|
||||
MineBlock,
|
||||
BlockMined(Block),
|
||||
}
|
||||
|
||||
impl Blockchain {
|
||||
pub fn get_balances(&self) -> &std::collections::HashMap<String, u32> {
|
||||
&self.balances
|
||||
}
|
||||
pub fn blocks(&self) -> &[Block] {
|
||||
&self.blocks
|
||||
}
|
||||
}
|
||||
38
src/tx.rs
Normal file
38
src/tx.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use crate::node::Account;
|
||||
use crate::error::TxError;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug, clap::Args, Clone)]
|
||||
pub struct Tx {
|
||||
from: Account,
|
||||
to: Account,
|
||||
value: u32,
|
||||
data: String
|
||||
}
|
||||
|
||||
impl Tx {
|
||||
pub fn validate(&self) -> Result<(), TxError> {
|
||||
if self.from.is_empty() {
|
||||
return Err(TxError::FromEmpty)
|
||||
} else if self.to.is_empty() {
|
||||
return Err(TxError::ToEmpty)
|
||||
} else if self.value == 0 {
|
||||
return Err(TxError::ValueEmpty)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn is_new_account(&self) -> bool {
|
||||
return self.data == "new_account";
|
||||
}
|
||||
pub fn is_reward(&self) -> bool {
|
||||
return self.data == "reward";
|
||||
}
|
||||
pub fn get_from(&self) -> &str {
|
||||
&self.from
|
||||
}
|
||||
pub fn get_to(&self) -> &str {
|
||||
&self.to
|
||||
}
|
||||
pub fn get_value(&self) -> u32 {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user