Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions crates/bitcell-node/src/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl Blockchain {
e.into_inner()
}).get(&height).cloned()
}

/// Get transaction by hash using the O(1) hash index
///
/// Returns the transaction and its location (block height, index) if found.
Expand All @@ -146,7 +146,7 @@ impl Blockchain {
});
index.get(tx_hash).cloned()
};

// Then retrieve the actual transaction from the block
if let Some(loc) = location {
if let Some(block) = self.get_block(loc.block_height) {
Expand All @@ -155,10 +155,10 @@ impl Blockchain {
}
}
}

None
}

/// Get state manager (read-only access)
pub fn state(&self) -> Arc<RwLock<StateManager>> {
Arc::clone(&self.state)
Expand Down Expand Up @@ -196,7 +196,7 @@ impl Blockchain {
});
state.state_root
};

// Generate VRF output and proof using proper VRF chaining
// For genesis block (height 1), use previous hash as input
// For all other blocks, use the previous block's VRF output for chaining
Expand All @@ -216,15 +216,15 @@ impl Blockchain {
tracing::error!("Lock poisoned in produce_block() - prior panic detected: {}", e);
e.into_inner()
});

let vrf_input = if let Some(prev_block) = blocks.get(&current_height) {
prev_block.header.vrf_output.to_vec()
} else {
// Fallback if previous block not found (shouldn't happen in normal operation)
tracing::warn!("Previous block {} not found for VRF chaining, using hash fallback", current_height);
prev_hash.as_bytes().to_vec()
};

// Generate VRF proof while still holding the read lock to prevent race conditions
let (vrf_output, vrf_proof) = self.secret_key.vrf_prove(&vrf_input);
(vrf_output, bincode::serialize(&vrf_proof).unwrap_or_default())
Expand Down Expand Up @@ -282,11 +282,11 @@ impl Blockchain {
if block.signature.verify(&block.header.proposer, header_hash.as_bytes()).is_err() {
return Err(crate::Error::Node("Invalid block signature".to_string()));
}

// Verify VRF proof using proper VRF chaining
let vrf_proof: bitcell_crypto::VrfProof = bincode::deserialize(&block.header.vrf_proof)
.map_err(|_| crate::Error::Node("Invalid VRF proof format".to_string()))?;

// Reconstruct VRF input using the same chaining logic as produce_block
let vrf_input = if block.header.height == 1 {
// First block after genesis uses genesis hash as VRF input
Expand Down
6 changes: 4 additions & 2 deletions crates/bitcell-node/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ impl NetworkManager {
tracing::info!("DHT enabled");
Ok(())
}



/// Start the network listener
///
Expand Down Expand Up @@ -588,7 +590,7 @@ impl NetworkManager {
let guard = self.dht.read();
guard.clone()
};

if let Some(dht) = dht_opt {
if let Err(e) = dht.broadcast_block(block).await {
tracing::error!("Failed to broadcast block via DHT: {}", e);
Expand Down Expand Up @@ -622,7 +624,7 @@ impl NetworkManager {
let guard = self.dht.read();
guard.clone()
};

if let Some(dht) = dht_opt {
if let Err(e) = dht.broadcast_transaction(tx).await {
tracing::error!("Failed to broadcast transaction via DHT: {}", e);
Expand Down
33 changes: 17 additions & 16 deletions crates/bitcell-node/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,10 @@ async fn eth_get_block_by_number(state: &RpcState, params: Option<Value>) -> Res
.collect();
json!(tx_hashes)
};

// Calculate actual block size
let block_size = bincode::serialized_size(&block).unwrap_or(0);

Ok(json!({
"number": format!("0x{:x}", block.header.height),
"hash": format!("0x{}", hex::encode(block.hash().as_bytes())),
Expand Down Expand Up @@ -305,7 +305,7 @@ async fn eth_get_transaction_by_hash(state: &RpcState, params: Option<Value>) ->
let mut hash = [0u8; 32];
hash.copy_from_slice(&tx_hash_bytes);
let target_hash = bitcell_crypto::Hash256::from(hash);

// Use efficient O(1) lookup via transaction hash index
if let Some((tx, location)) = state.blockchain.get_transaction_by_hash(&target_hash) {
// Get the block to include block hash in response
Expand All @@ -325,7 +325,7 @@ async fn eth_get_transaction_by_hash(state: &RpcState, params: Option<Value>) ->
}));
}
}

Ok(Value::Null)
}

Expand Down Expand Up @@ -387,7 +387,8 @@ async fn eth_get_balance(state: &RpcState, params: Option<Value>) -> Result<Valu
.map(|account| account.balance)
.unwrap_or(0)
};



// Return balance as hex string
Ok(json!(format!("0x{:x}", balance)))
}
Expand All @@ -405,7 +406,7 @@ async fn eth_get_transaction_count(state: &RpcState, params: Option<Value>) -> R
message: "Params must be an array".to_string(),
data: None,
})?;

if args.is_empty() {
return Err(JsonRpcError {
code: -32602,
Expand All @@ -427,18 +428,18 @@ async fn eth_get_transaction_count(state: &RpcState, params: Option<Value>) -> R
message: "Invalid address format".to_string(),
data: None,
})?;

if address_bytes.len() != 33 {
return Err(JsonRpcError {
code: -32602,
message: "Address must be 33 bytes (compressed public key)".to_string(),
data: None,
});
}

let mut address = [0u8; 33];
address.copy_from_slice(&address_bytes);

// Fetch nonce from blockchain state
let nonce = {
let state_lock = state.blockchain.state();
Expand All @@ -451,7 +452,7 @@ async fn eth_get_transaction_count(state: &RpcState, params: Option<Value>) -> R
.map(|account| account.nonce)
.unwrap_or(0)
};

// Return nonce as hex string
Ok(json!(format!("0x{:x}", nonce)))
}
Expand All @@ -460,7 +461,7 @@ async fn eth_get_transaction_count(state: &RpcState, params: Option<Value>) -> R
const DEFAULT_GAS_PRICE: u64 = 1_000_000_000;

/// Get current gas price
///
///
/// Returns the current gas price. In production, this should be
/// dynamically calculated based on network congestion and mempool state.
async fn eth_gas_price(_state: &RpcState) -> Result<Value, JsonRpcError> {
Expand Down Expand Up @@ -565,36 +566,36 @@ async fn eth_send_raw_transaction(state: &RpcState, params: Option<Value>) -> Re
data: None,
});
}

// Validate gas parameters to prevent spam and overflow attacks
// Gas price and limit must be non-zero and within reasonable bounds
const MAX_GAS_PRICE: u64 = 10_000_000_000_000; // 10,000 Gwei max
const MAX_GAS_LIMIT: u64 = 30_000_000; // 30M gas max (similar to Ethereum block limit)

if tx.gas_price == 0 || tx.gas_limit == 0 {
return Err(JsonRpcError {
code: -32602,
message: "Transactions from new accounts require non-zero gas price and limit to prevent DoS attacks".to_string(),
data: None,
});
}

if tx.gas_price > MAX_GAS_PRICE {
return Err(JsonRpcError {
code: -32602,
message: format!("Gas price {} exceeds maximum allowed {}", tx.gas_price, MAX_GAS_PRICE),
data: None,
});
}

if tx.gas_limit > MAX_GAS_LIMIT {
return Err(JsonRpcError {
code: -32602,
message: format!("Gas limit {} exceeds maximum allowed {}", tx.gas_limit, MAX_GAS_LIMIT),
data: None,
});
}

tracing::debug!(
from = %hex::encode(tx.from.as_bytes()),
"Allowing transaction from new account with nonce 0"
Expand Down
10 changes: 6 additions & 4 deletions crates/bitcell-state/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ pub enum Error {

#[error("Invalid bond")]
InvalidBond,

#[error("Balance overflow")]
BalanceOverflow,

#[error("Storage error: {0}")]
StorageError(String),
}
Expand Down Expand Up @@ -116,7 +116,8 @@ impl StateManager {
/// in memory (eventual consistency model).
pub fn update_account(&mut self, pubkey: [u8; 33], account: Account) {
self.accounts.insert(pubkey, account.clone());



// Persist to storage if available
if let Some(storage) = &self.storage {
if let Err(e) = storage.store_account(&pubkey, &account) {
Expand Down Expand Up @@ -160,7 +161,8 @@ impl StateManager {
/// in memory (eventual consistency model).
pub fn update_bond(&mut self, pubkey: [u8; 33], bond: BondState) {
self.bonds.insert(pubkey, bond.clone());



// Persist to storage if available
if let Some(storage) = &self.storage {
if let Err(e) = storage.store_bond(&pubkey, &bond) {
Expand Down
14 changes: 7 additions & 7 deletions crates/bitcell-zkp/src/battle_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisE
use ark_bn254::Fr;

/// Battle circuit configuration
///
///
/// Proves that a battle between two players resulted in the claimed winner.
/// Winner ID meanings:
/// - 0: Draw (no winner)
Expand Down Expand Up @@ -162,15 +162,15 @@ mod tests {
Fr::one(), // commitment B
Fr::from(1u8), // winner ID
];

assert!(BattleCircuit::verify(&vk, &proof, &public_inputs).unwrap());
}

#[test]
fn test_battle_circuit_all_winner_ids() {
// Test that all valid winner IDs (0, 1, 2) work
let (pk, vk) = BattleCircuit::setup().expect("Circuit setup should succeed");

for winner_id in [0u8, 1u8, 2u8] {
let circuit = BattleCircuit::new(
Fr::one(),
Expand All @@ -179,15 +179,15 @@ mod tests {
100,
200,
);

let proof = circuit.prove(&pk).unwrap_or_else(|_| panic!("Proof should succeed for winner_id {}", winner_id));

let public_inputs = vec![
Fr::one(),
Fr::one(),
Fr::from(winner_id),
];

assert!(
BattleCircuit::verify(&vk, &proof, &public_inputs).unwrap(),
"Verification should succeed for winner_id {}",
Expand Down
23 changes: 12 additions & 11 deletions crates/bitcell-zkp/src/state_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ark_std::rand::thread_rng;
use ark_std::Zero;

/// State transition circuit configuration
///
///
/// This circuit proves that a state transition occurred correctly by verifying:
/// 1. The old and new state roots are different (state changed)
/// 2. The nullifier is properly computed to prevent double-spending
Expand Down Expand Up @@ -42,7 +42,7 @@ impl StateCircuit {
leaf_index: Some(Fr::from(leaf_index)),
}
}

/// Setup the circuit and generate proving/verifying keys
///
/// Returns an error if the circuit setup fails (e.g., due to constraint system issues).
Expand Down Expand Up @@ -91,28 +91,29 @@ impl ConstraintSynthesizer<Fr> for StateCircuit {

// Allocate private witness
let _leaf_index = cs.new_witness_variable(|| self.leaf_index.ok_or(SynthesisError::AssignmentMissing))?;



// Constraint: old_root != new_root (state must change)
// To prove non-equality, we use the following approach:
// 1. Compute diff = new_root - old_root
// 2. Compute inv = inverse(diff) as a witness
// 3. Enforce: diff * inv = 1
// This proves diff != 0, which proves new_root != old_root

// Step 1: Compute diff = new_root - old_root
let diff = cs.new_witness_variable(|| {
let old = self.old_state_root.ok_or(SynthesisError::AssignmentMissing)?;
let new = self.new_state_root.ok_or(SynthesisError::AssignmentMissing)?;
Ok(new - old)
})?;

// Enforce: diff = new_root - old_root
cs.enforce_constraint(
ark_relations::lc!() + new_root - old_root,
ark_relations::lc!() + ark_relations::r1cs::Variable::One,
ark_relations::lc!() + diff,
)?;

// Step 2: Allocate inverse of diff as witness
let inv = cs.new_witness_variable(|| {
let old = self.old_state_root.ok_or(SynthesisError::AssignmentMissing)?;
Expand All @@ -126,20 +127,20 @@ impl ConstraintSynthesizer<Fr> for StateCircuit {
}
diff_val.inverse().ok_or(SynthesisError::Unsatisfiable)
})?;

// Step 3: Enforce diff * inv = 1 (proves diff != 0)
cs.enforce_constraint(
ark_relations::lc!() + diff,
ark_relations::lc!() + inv,
ark_relations::lc!() + ark_relations::r1cs::Variable::One,
)?;

// TODO: Add full Merkle tree verification constraints
// This would include:
// - Verifying the old leaf at leaf_index against old_state_root
// - Verifying the new leaf at leaf_index against new_state_root
// - Ensuring the nullifier is derived from the old leaf

Ok(())
}
}
Expand Down Expand Up @@ -171,10 +172,10 @@ mod tests {
Fr::from(200u64),
Fr::one(),
];

assert!(StateCircuit::verify(&vk, &proof, &public_inputs).unwrap());
}

#[test]
fn test_state_circuit_rejects_same_roots() {
// Setup
Expand Down
Loading