use crate::block::{block_digest, Block, Transaction};
use crate::pqc::{probe_id, verify_hex, Probe};
pub const BIG_BANG: &str = "BIGBANG";
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ChainError {
#[error("block {0}: index out of sequence")]
BadIndex(u64),
#[error("block {0}: prev_hash does not link to the previous block")]
BrokenLink(u64),
#[error("block {0}: hash does not match recomputed digest")]
BadHash(u64),
#[error("block {0}: proposer_id does not match proposer_pubkey")]
ForgedProposer(u64),
#[error("block {0}: post-quantum signature is invalid")]
BadSignature(u64),
#[error("chain is empty")]
Empty,
}
#[derive(Debug, Default, Clone)]
pub struct Chain {
pub blocks: Vec<Block>,
}
impl Chain {
pub fn genesis(founder: &Probe, timestamp: u64, beacon: impl Into<String>) -> Self {
let mut chain = Chain { blocks: Vec::new() };
chain.propose(founder, timestamp, beacon, Vec::new());
chain
}
pub fn draft(
&self,
proposer: &Probe,
timestamp: u64,
beacon: impl Into<String>,
transactions: Vec<Transaction>,
) -> Block {
let index = self.blocks.len() as u64;
let prev_hash = self
.blocks
.last()
.map(|b| b.hash.clone())
.unwrap_or_else(|| BIG_BANG.to_string());
let beacon = beacon.into();
let proposer_id = proposer.id();
let proposer_pubkey = proposer.pubkey_hex();
let digest = block_digest(
index,
timestamp,
&prev_hash,
&beacon,
&transactions,
&proposer_id,
);
let signature = proposer.sign_hex(&digest);
Block {
index,
timestamp,
prev_hash,
beacon,
transactions,
proposer_id,
proposer_pubkey,
hash: hex::encode(digest),
signature,
}
}
pub fn try_append(&mut self, block: Block) -> Result<(), ChainError> {
let expected = self.blocks.len() as u64;
let prev = self
.blocks
.last()
.map(|b| b.hash.as_str())
.unwrap_or(BIG_BANG);
if block.index != expected {
return Err(ChainError::BadIndex(expected));
}
if block.prev_hash != prev {
return Err(ChainError::BrokenLink(expected));
}
if probe_id(&block.proposer_pubkey) != block.proposer_id {
return Err(ChainError::ForgedProposer(expected));
}
let digest = block_digest(
block.index,
block.timestamp,
&block.prev_hash,
&block.beacon,
&block.transactions,
&block.proposer_id,
);
if hex::encode(digest) != block.hash {
return Err(ChainError::BadHash(expected));
}
if !verify_hex(&block.proposer_pubkey, &digest, &block.signature) {
return Err(ChainError::BadSignature(expected));
}
self.blocks.push(block);
Ok(())
}
pub fn propose(
&mut self,
proposer: &Probe,
timestamp: u64,
beacon: impl Into<String>,
transactions: Vec<Transaction>,
) -> u64 {
let block = self.draft(proposer, timestamp, beacon, transactions);
let index = block.index;
self.try_append(block)
.expect("a self-drafted block is always valid");
index
}
pub fn verify(&self) -> Result<(), ChainError> {
if self.blocks.is_empty() {
return Err(ChainError::Empty);
}
let mut prev = BIG_BANG.to_string();
for (i, b) in self.blocks.iter().enumerate() {
let i = i as u64;
if b.index != i {
return Err(ChainError::BadIndex(i));
}
if b.prev_hash != prev {
return Err(ChainError::BrokenLink(i));
}
if probe_id(&b.proposer_pubkey) != b.proposer_id {
return Err(ChainError::ForgedProposer(i));
}
let digest = block_digest(
b.index,
b.timestamp,
&b.prev_hash,
&b.beacon,
&b.transactions,
&b.proposer_id,
);
if hex::encode(digest) != b.hash {
return Err(ChainError::BadHash(i));
}
if !verify_hex(&b.proposer_pubkey, &digest, &b.signature) {
return Err(ChainError::BadSignature(i));
}
prev = b.hash.clone();
}
Ok(())
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
pub fn head(&self) -> Option<&Block> {
self.blocks.last()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beacon;
fn sample_chain() -> (Chain, Probe, Probe) {
let founder = Probe::spawn();
let alice = Probe::spawn();
let mut chain = Chain::genesis(&founder, 1_000, beacon::sample(0));
chain.propose(
&alice,
1_001,
beacon::sample(1),
vec![Transaction::new(
alice.id(),
"transfer",
"10 -> PROBE-DEADBEEF",
)],
);
chain.propose(
&founder,
1_002,
beacon::sample(2),
vec![Transaction::new(
"oracle",
"attest",
"cosmic beacon round 2",
)],
);
(chain, founder, alice)
}
#[test]
fn builds_and_verifies() {
let (chain, _, _) = sample_chain();
assert_eq!(chain.len(), 3);
assert_eq!(chain.verify(), Ok(()));
}
#[test]
fn detects_tampered_transaction() {
let (mut chain, _, _) = sample_chain();
chain.blocks[1].transactions[0].payload = "9000 -> PROBE-ATTACKER".into();
assert_eq!(chain.verify(), Err(ChainError::BadHash(1)));
}
#[test]
fn detects_forged_signature() {
let (mut chain, _, _) = sample_chain();
let attacker = Probe::spawn();
let digest = block_digest(
chain.blocks[2].index,
chain.blocks[2].timestamp,
&chain.blocks[2].prev_hash,
&chain.blocks[2].beacon,
&chain.blocks[2].transactions,
&chain.blocks[2].proposer_id,
);
chain.blocks[2].signature = attacker.sign_hex(&digest);
assert_eq!(chain.verify(), Err(ChainError::BadSignature(2)));
}
#[test]
fn detects_broken_link() {
let (mut chain, _, _) = sample_chain();
chain.blocks[1].prev_hash = "0".repeat(64);
assert_eq!(chain.verify(), Err(ChainError::BrokenLink(1)));
}
#[test]
fn try_append_rejects_foreign_block() {
let (mut chain, _, _) = sample_chain();
let rogue = Probe::spawn();
let foreign = Chain::default().draft(&rogue, 5, beacon::sample(9), Vec::new());
assert_eq!(chain.try_append(foreign), Err(ChainError::BadIndex(3)));
}
#[test]
fn serde_round_trip() {
let (chain, _, _) = sample_chain();
let json = serde_json::to_string(&chain.blocks).unwrap();
let back: Vec<Block> = serde_json::from_str(&json).unwrap();
assert_eq!(back.len(), 3);
}
}