diff options
| author | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
|---|---|---|
| committer | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
| commit | 9f5d864d533ce86459e654f5d78212933c0269ea (patch) | |
| tree | 4b71e8c7ab4e78da93e5f3164803da4de68c7031 /core | |
| download | onioncoin-0.1.0.tar.gz onioncoin-0.1.0.tar.xz onioncoin-0.1.0.zip | |
OnionCoin is a privacy cryptocurrency that rewards Tor relay operators
through a unique Proof-of-Contribution consensus mechanism.
Core Features:
- Proof-of-Relay: 30% of block rewards go to Tor operators
- Native .onion node identity (no IP exposure)
- Temporal obfuscation protocols
- Dandelion++ over Tor propagation
- Native inheritance system with dead man's switch
Technical Stack:
- Rust workspace with 8 crates
- Ed25519/X25519 cryptography
- arti (Rust Tor client) integration planned
- 10 minute block time, 5-10 TPS design
Status: Prototype
- Consensus logic complete with passing tests (30/33)
- Network layer conceptual design complete
- Tor integration pending
- Testnet launch planned Q3 2026
License: MIT
Author: Gabriele Salati (virebent)
Contact: g48rix@gmail.com
Website: https://www.gabrielesalati.eu
Repository: https://git.virebent.art/virebent/onioncoin
Diffstat (limited to 'core')
| -rw-r--r-- | core/Cargo.toml | 19 | ||||
| -rw-r--r-- | core/src/block.rs | 253 | ||||
| -rw-r--r-- | core/src/lib.rs | 5 | ||||
| -rw-r--r-- | core/src/transaction.rs | 231 |
4 files changed, 508 insertions, 0 deletions
diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..a03e6cd --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "onioncoin-core" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +onioncoin-timing = { path = "../timing" } +onioncoin-crypto = { path = "../crypto" } + +serde.workspace = true +serde_json.workspace = true +bincode.workspace = true +serde_bytes.workspace = true +blake3.workspace = true +ed25519-dalek.workspace = true +chrono.workspace = true +thiserror.workspace = true diff --git a/core/src/block.rs b/core/src/block.rs new file mode 100644 index 0000000..d72f962 --- /dev/null +++ b/core/src/block.rs @@ -0,0 +1,253 @@ +use serde::{Deserialize, Serialize}; +use crate::transaction::Transaction; +use blake3::Hasher; + +/// Block hash +pub type BlockHash = [u8; 32]; + +/// A block in the OnionCoin blockchain +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Block { + /// Block header + pub header: BlockHeader, + + /// Transactions in this block + pub transactions: Vec<Transaction>, + + /// Staking actions (stake/unstake) + pub stake_actions: Vec<StakeAction>, +} + +impl Block { + /// Maximum block size (2 MB for Tor bandwidth constraints) + pub const MAX_SIZE: usize = 2 * 1024 * 1024; + + /// Target block time (10 minutes) + pub const TARGET_BLOCK_TIME_SECS: u64 = 10 * 60; + + /// Calculate block hash + pub fn hash(&self) -> BlockHash { + self.header.hash() + } + + /// Validate block structure + pub fn validate(&self) -> Result<(), BlockError> { + // Check size + let size = self.size(); + if size > Self::MAX_SIZE { + return Err(BlockError::TooLarge(size)); + } + + // Validate all transactions + for tx in &self.transactions { + tx.validate().map_err(|_| BlockError::InvalidTransaction)?; + } + + // Verify merkle root + let calculated_merkle = Self::calculate_merkle_root(&self.transactions); + if calculated_merkle != self.header.merkle_root { + return Err(BlockError::InvalidMerkleRoot); + } + + Ok(()) + } + + /// Get block size in bytes + pub fn size(&self) -> usize { + bincode::serialize(self) + .map(|s| s.len()) + .unwrap_or(0) + } + + /// Calculate merkle root of transactions + fn calculate_merkle_root(transactions: &[Transaction]) -> [u8; 32] { + if transactions.is_empty() { + return [0u8; 32]; + } + + let mut hashes: Vec<[u8; 32]> = transactions + .iter() + .map(|tx| tx.id()) + .collect(); + + // Build merkle tree bottom-up + while hashes.len() > 1 { + let mut next_level = Vec::new(); + + for chunk in hashes.chunks(2) { + let mut hasher = Hasher::new(); + hasher.update(&chunk[0]); + if chunk.len() > 1 { + hasher.update(&chunk[1]); + } else { + hasher.update(&chunk[0]); // Duplicate if odd + } + next_level.push(*hasher.finalize().as_bytes()); + } + + hashes = next_level; + } + + hashes[0] + } +} + +/// Block header +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlockHeader { + /// Protocol version + pub version: u8, + + /// Previous block hash + pub previous_hash: BlockHash, + + /// Merkle root of transactions + pub merkle_root: [u8; 32], + + /// Block timestamp (Unix seconds) + pub timestamp: i64, + + /// Block height + pub height: u64, + + /// Validator's public key (PoS) + pub validator_pubkey: [u8; 32], + + /// Validator's signature + #[serde(with = "serde_bytes")] + pub validator_signature: [u8; 64], +} + +impl BlockHeader { + /// Calculate header hash + pub fn hash(&self) -> BlockHash { + let mut hasher = Hasher::new(); + hasher.update(&[self.version]); + hasher.update(&self.previous_hash); + hasher.update(&self.merkle_root); + hasher.update(&self.timestamp.to_le_bytes()); + hasher.update(&self.height.to_le_bytes()); + hasher.update(&self.validator_pubkey); + // Note: signature not included in hash + *hasher.finalize().as_bytes() + } +} + +/// Staking action (stake or unstake coins) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StakeAction { + /// Action type + pub action_type: StakeActionType, + + /// Validator public key + pub validator_pubkey: [u8; 32], + + /// Amount to stake/unstake + pub amount: u64, + + /// Signature + #[serde(with = "serde_bytes")] + pub signature: [u8; 64], +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum StakeActionType { + Stake, + Unstake, +} + +#[derive(Debug, thiserror::Error)] +pub enum BlockError { + #[error("Block too large: {0} bytes (max: {})", Block::MAX_SIZE)] + TooLarge(usize), + + #[error("Invalid transaction in block")] + InvalidTransaction, + + #[error("Invalid merkle root")] + InvalidMerkleRoot, + + #[error("Invalid validator signature")] + InvalidValidatorSignature, + + #[error("Invalid previous hash")] + InvalidPreviousHash, + + #[error("Block timestamp too far in future")] + TimestampTooFarFuture, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::transaction::*; + use chrono::Utc; + use onioncoin_timing::TimingMetadata; + + fn create_test_block() -> Block { + let seed = [42u8; 32]; + let timing = TimingMetadata::new(Utc::now(), &seed).unwrap(); + + let tx = Transaction { + version: 1, + inputs: vec![TransactionInput { + previous_output: OutPoint { + txid: [1u8; 32], + vout: 0, + }, + key_image: [2u8; 32], + }], + outputs: vec![TransactionOutput { + encrypted_amount: vec![0u8; 32], + stealth_address: [3u8; 32], + commitment: [4u8; 32], + }], + ring_signatures: vec![RingSignature::new(11)], + range_proof: vec![0u8; 64], + encrypted_fee: 1000, + timing, + }; + + let merkle_root = Block::calculate_merkle_root(&[tx.clone()]); + + Block { + header: BlockHeader { + version: 1, + previous_hash: [0u8; 32], + merkle_root, + timestamp: Utc::now().timestamp(), + height: 1, + validator_pubkey: [5u8; 32], + validator_signature: [6u8; 64], + }, + transactions: vec![tx], + stake_actions: vec![], + } + } + + #[test] + fn test_block_hash() { + let block = create_test_block(); + let hash = block.hash(); + assert_ne!(hash, [0u8; 32]); + } + + #[test] + fn test_block_validation() { + let block = create_test_block(); + assert!(block.validate().is_ok()); + } + + #[test] + fn test_merkle_root() { + let block = create_test_block(); + let calculated = Block::calculate_merkle_root(&block.transactions); + assert_eq!(calculated, block.header.merkle_root); + } + + #[test] + fn test_empty_merkle_root() { + let root = Block::calculate_merkle_root(&[]); + assert_eq!(root, [0u8; 32]); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..af9d6d2 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,5 @@ +pub mod transaction; +pub mod block; + +pub use transaction::{Transaction, TransactionInput, TransactionOutput}; +pub use block::{Block, BlockHeader}; diff --git a/core/src/transaction.rs b/core/src/transaction.rs new file mode 100644 index 0000000..d0a181d --- /dev/null +++ b/core/src/transaction.rs @@ -0,0 +1,231 @@ +use serde::{Deserialize, Serialize}; +use onioncoin_timing::TimingMetadata; + +/// Transaction ID (hash of transaction) +pub type TxId = [u8; 32]; + +/// A transaction in the OnionCoin network +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + /// Protocol version + pub version: u8, + + /// Transaction inputs + pub inputs: Vec<TransactionInput>, + + /// Transaction outputs + pub outputs: Vec<TransactionOutput>, + + /// Ring signatures for anonymity (simplified placeholder) + pub ring_signatures: Vec<RingSignature>, + + /// Range proof to hide amounts (placeholder) + pub range_proof: Vec<u8>, + + /// Encrypted transaction fee + pub encrypted_fee: u64, + + /// Timing metadata for temporal privacy + pub timing: TimingMetadata, +} + +impl Transaction { + /// Calculate transaction ID (hash) + pub fn id(&self) -> TxId { + let serialized = bincode::serialize(self).expect("Serialization should not fail"); + let hash = blake3::hash(&serialized); + *hash.as_bytes() + } + + /// Validate transaction structure + pub fn validate(&self) -> Result<(), TransactionError> { + // Must have at least one input and one output + if self.inputs.is_empty() { + return Err(TransactionError::NoInputs); + } + + if self.outputs.is_empty() { + return Err(TransactionError::NoOutputs); + } + + // Ring signatures must match inputs + if self.ring_signatures.len() != self.inputs.len() { + return Err(TransactionError::InvalidRingSignatures); + } + + // Validate timing metadata + if !self.timing.validate(chrono::Utc::now()) { + return Err(TransactionError::InvalidTimingMetadata); + } + + Ok(()) + } + + /// Get size in bytes + pub fn size(&self) -> usize { + bincode::serialize(self) + .map(|s| s.len()) + .unwrap_or(0) + } +} + +/// Transaction input +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TransactionInput { + /// Reference to previous transaction output + pub previous_output: OutPoint, + + /// Key image (prevents double spending in ring signatures) + pub key_image: [u8; 32], +} + +/// Reference to a specific output in a previous transaction +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct OutPoint { + /// Transaction ID + pub txid: TxId, + + /// Output index + pub vout: u32, +} + +/// Transaction output +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TransactionOutput { + /// Encrypted amount (confidential transaction) + pub encrypted_amount: Vec<u8>, + + /// One-time stealth address (public key) + pub stealth_address: [u8; 32], + + /// Commitment to the amount (for verification) + pub commitment: [u8; 32], +} + +/// Ring signature for input anonymity +/// Simplified placeholder - production would use actual cryptographic implementation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RingSignature { + /// Public keys in the ring (decoy outputs + real output) + pub ring: Vec<[u8; 32]>, + + /// Signature components + pub c: [u8; 32], + pub r: Vec<[u8; 32]>, + + /// Ring size (typically 11 for privacy) + pub ring_size: usize, +} + +impl RingSignature { + /// Create a new ring signature with specified ring size + pub fn new(ring_size: usize) -> Self { + Self { + ring: vec![[0u8; 32]; ring_size], + c: [0u8; 32], + r: vec![[0u8; 32]; ring_size], + ring_size, + } + } + + /// Verify ring signature (placeholder) + pub fn verify(&self) -> bool { + // In production: actual ring signature verification + self.ring.len() == self.ring_size && self.r.len() == self.ring_size + } +} + +#[derive(Debug, thiserror::Error)] +pub enum TransactionError { + #[error("Transaction has no inputs")] + NoInputs, + + #[error("Transaction has no outputs")] + NoOutputs, + + #[error("Invalid ring signatures")] + InvalidRingSignatures, + + #[error("Invalid timing metadata")] + InvalidTimingMetadata, + + #[error("Transaction too large")] + TooLarge, + + #[error("Invalid fee")] + InvalidFee, +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + + fn create_test_transaction() -> Transaction { + let seed = [42u8; 32]; + let timing = TimingMetadata::new(Utc::now(), &seed).unwrap(); + + Transaction { + version: 1, + inputs: vec![TransactionInput { + previous_output: OutPoint { + txid: [1u8; 32], + vout: 0, + }, + key_image: [2u8; 32], + }], + outputs: vec![TransactionOutput { + encrypted_amount: vec![0u8; 32], + stealth_address: [3u8; 32], + commitment: [4u8; 32], + }], + ring_signatures: vec![RingSignature::new(11)], + range_proof: vec![0u8; 64], + encrypted_fee: 1000, + timing, + } + } + + #[test] + fn test_transaction_id() { + let tx1 = create_test_transaction(); + let tx2 = create_test_transaction(); + + // Same transaction should have same ID + assert_eq!(tx1.id(), tx2.id()); + } + + #[test] + fn test_transaction_validation() { + let tx = create_test_transaction(); + assert!(tx.validate().is_ok()); + } + + #[test] + fn test_transaction_no_inputs() { + let mut tx = create_test_transaction(); + tx.inputs.clear(); + + assert!(matches!( + tx.validate(), + Err(TransactionError::NoInputs) + )); + } + + #[test] + fn test_transaction_no_outputs() { + let mut tx = create_test_transaction(); + tx.outputs.clear(); + + assert!(matches!( + tx.validate(), + Err(TransactionError::NoOutputs) + )); + } + + #[test] + fn test_ring_signature_verification() { + let ring_sig = RingSignature::new(11); + assert!(ring_sig.verify()); + } +} |
