diff --git a/src/bin/broadcast_transaction.rs b/src/bin/broadcast_transaction.rs index 0bce75f..837c48c 100644 --- a/src/bin/broadcast_transaction.rs +++ b/src/bin/broadcast_transaction.rs @@ -80,12 +80,22 @@ async fn main() { } }; - let wallet_path = prompt_wallet_path().await; - let encryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; + // Automated callers can keep credentials out of command arguments by + // passing them only to this child process. Normal CLI use still prompts. + let wallet_path = match env::var("CONTRACTLESS_WALLET_PATH") { + Ok(path) if !path.trim().is_empty() => path, + _ => prompt_wallet_path().await, + }; + let encryption_key = match env::var("CONTRACTLESS_WALLET_KEY") { + Ok(key) if !key.trim().is_empty() => key, + _ => { + prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await + } + }; // Read the signed transaction JSON exactly as it will be serialized for broadcast. let json = match tokio::fs::read_to_string(filename).await { diff --git a/src/bin/create_transfer_tx.rs b/src/bin/create_transfer_tx.rs index 99e6d8e..7a563b4 100644 --- a/src/bin/create_transfer_tx.rs +++ b/src/bin/create_transfer_tx.rs @@ -68,13 +68,23 @@ async fn main() { .expect("Please enter a valid amount."); let value = ((value_f32 as f64) * 100_000_000.0).round() as u64; - let wallet_path = prompt_wallet_path().await; - - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; + // Automated tools such as a locally hosted faucet may provide wallet + // credentials through the child-process environment. Interactive use + // keeps the existing path completion and hidden key prompts. + let wallet_path = match env::var("CONTRACTLESS_WALLET_PATH") { + Ok(path) if !path.trim().is_empty() => path, + _ => prompt_wallet_path().await, + }; + let decryption_key = match env::var("CONTRACTLESS_WALLET_KEY") { + Ok(key) if !key.trim().is_empty() => key, + _ => { + prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await + } + }; let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, diff --git a/src/bin/unpack_block_header.rs b/src/bin/unpack_block_header.rs index 2893120..857d665 100644 --- a/src/bin/unpack_block_header.rs +++ b/src/bin/unpack_block_header.rs @@ -1,8 +1,9 @@ use contractless::common::binary_conversions::hex_to_u64; use contractless::env; use contractless::io; -use contractless::records::unpack_block::unpack_header::load_block_header; +use contractless::records::unpack_block::unpack_header::{load_block_header, load_block_proof}; use contractless::to_string_pretty; +use serde_json::json; #[tokio::main] async fn main() -> io::Result<()> { @@ -22,8 +23,12 @@ async fn main() -> io::Result<()> { // Header loading uses the active network block path internally. let header = load_block_header(block_number).await.unwrap(); + let proof = load_block_proof(block_number).await.unwrap(); let hash = header.hash().await; - let json_pretty = to_string_pretty(&header)?; + let json_pretty = to_string_pretty(&json!({ + "vrf_block": header, + "proof": proof + }))?; // The displayed difficulty is the numeric value derived from the header hash. let difficulty = hex_to_u64(&hash).await.unwrap(); diff --git a/src/bin/validate_torrent_and_block_headers.rs b/src/bin/validate_torrent_and_block_headers.rs index 8fd7451..d9a070f 100644 --- a/src/bin/validate_torrent_and_block_headers.rs +++ b/src/bin/validate_torrent_and_block_headers.rs @@ -1,4 +1,5 @@ use colored::*; +use contractless::blocks::block::{BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES}; use contractless::common::binary_conversions::hex_to_u64; use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use contractless::common::network_paths_and_settings::block_extension_and_paths; @@ -84,15 +85,17 @@ async fn main() { process::exit(1); }); block_file.read_to_end(&mut block_data).await.unwrap(); + if block_data.len() < BLOCK_HEADER_BYTES { + eprintln!("Error: block file is shorter than the complete header."); + process::exit(1); + } + let proof = encode(&block_data[VRF_BLOCK_BYTES..BLOCK_HEADER_BYTES]); let info_hash_computed = skein_128_hash_bytes(&block_data); - // Rebuild the unmined-block hash using the same canonical path as mining - // and live block verification. - let unmined_hash = header.unmined_block.hash().await; + // The miner signs the deterministic VRF header hash only after it wins. let signature_ok = - Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex) - .await; + Wallet::verify_transaction_with_public_key(&block_hash, &proof, &miner_pubkey_hex).await; let passed = "[PASSED]".green(); let failed = "[FAILED]".red(); @@ -140,13 +143,7 @@ async fn main() { println!("block header hash check: {:>82}", format!("{failed}")); } - let vrf_ok = Wallet::vrf_verify_with_public_key( - header.vrf, - &unmined_hash, - &miner_pubkey_hex, - &header.proof, - ) - .await; + let vrf_ok = header.unmined_block.deterministic_vrf().await == header.vrf; if vrf_ok { println!("VRF validation check: {:>85}", format!("{passed}")); } else { diff --git a/src/blocks/block.rs b/src/blocks/block.rs index 300a39c..b77b056 100644 --- a/src/blocks/block.rs +++ b/src/blocks/block.rs @@ -24,10 +24,10 @@ pub const NONCE_OFFSET: usize = DIFFICULTY_OFFSET + 8; pub const VRF_OFFSET: usize = NONCE_OFFSET + 1; pub const PROOF_OFFSET: usize = VRF_OFFSET + 16; pub const UNMINED_BLOCK_BYTES: usize = 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 32 + 8 + 1; -pub const VRF_BLOCK_BYTES: usize = UNMINED_BLOCK_BYTES + 16 + Wallet::SIGNATURE_LENGTH; +pub const VRF_BLOCK_BYTES: usize = UNMINED_BLOCK_BYTES + 16; +pub const BLOCK_HEADER_BYTES: usize = VRF_BLOCK_BYTES + Wallet::SIGNATURE_LENGTH; -// UnminedBlock is the deterministic header data that exists before -// the miner wallet adds the VRF proof. +// UnminedBlock is the deterministic header data used to derive the VRF. #[derive(Debug, Serialize, Clone)] // 67 bytes pub struct UnminedBlock { pub timestamp: u32, // 4 bytes block timestamp @@ -37,18 +37,18 @@ pub struct UnminedBlock { pub nonce: u8, // 1 byte nonce searched by mining workers } -// VrfBlock adds the miner's signed proof and derived VRF number to the header. -#[derive(Debug, Serialize, Clone)] // 749 bytes +// VrfBlock adds the deterministic VRF number to the unsigned header. +#[derive(Debug, Serialize, Clone)] // 83 bytes pub struct VrfBlock { pub unmined_block: UnminedBlock, // 67 bytes unsigned block header fields - pub vrf: u128, // 16 bytes random number derived from proof - pub proof: String, // 666 bytes miner signature proof + pub vrf: u128, // 16 bytes deterministic mining value } -// Block stores the VRF header plus the ordered transaction list. +// Block stores the deterministic VRF header, miner proof, and transactions. #[derive(Debug, Serialize)] // header is 749 bytes plus transactions pub struct Block { pub vrf_block: VrfBlock, + pub proof: String, // 666 bytes miner signature over the VRF header hash pub transactions: Vec, // Exact transaction slices are retained for downloaded/stored blocks so // mempool reuse can compare bytes without serializing or hashing again. @@ -75,7 +75,7 @@ impl UnminedBlock { } pub async fn generate_random_number(input: &str) -> u128 { - // Hash the proof with Skein512, then fold the 64-byte result + // Hash the supplied deterministic seed with Skein512, then fold the 64-byte result // into one u128 value by XORing four 16-byte chunks. let hash = skein_512_hash_data(input); let hash_bytes = decode(&hash).expect("Failed to decode hash"); @@ -108,23 +108,25 @@ impl UnminedBlock { a ^ b ^ c ^ d } - pub async fn vrf_generate(self, private_key: &str) -> VrfBlock { - // Sign the unmined header hash with the miner wallet and derive - // the VRF number from that signature. - let hash = self.hash().await; - let proof = Wallet::sign_transaction(&hash, private_key).await; - let vrf = Self::generate_random_number(&proof).await; + pub async fn vrf_generate(self) -> VrfBlock { + // Derive the VRF from the unsigned header alone so every node + // calculates one result for each timestamp/miner/parent/nonce tuple. + let vrf = self.deterministic_vrf().await; VrfBlock { unmined_block: self, vrf, - proof, } } - // Hash the unmined block header for VRF signing. + pub async fn deterministic_vrf(&self) -> u128 { + let hash = self.hash().await; + Self::generate_random_number(&hash).await + } + + // Hash the serialized unsigned header to create the deterministic VRF seed. pub async fn hash(&self) -> String { let serialized = to_string(self).unwrap(); - skein_256_hash_data(&serialized) + skein_512_hash_data(&serialized) } fn asert_target(anchor_target: u64, height_delta: u32, time_delta: i128) -> u64 { @@ -208,7 +210,7 @@ impl UnminedBlock { impl VrfBlock { pub async fn hash(&self) -> String { - // Hash the full VRF header for indexing and validation. + // Hash only deterministic header data for mining and chain identity. let serialized = to_string(self).unwrap(); skein_256_hash_data(&serialized) } @@ -233,7 +235,6 @@ impl VrfBlock { .write_all(&self.unmined_block.nonce.to_le_bytes()) .await?; cursor.write_all(&self.vrf.to_le_bytes()).await?; - cursor.write_all(&decode(&self.proof).unwrap()).await?; Ok(buffer) } @@ -254,7 +255,7 @@ impl VrfBlock { let miner = Wallet::bytes_to_short_address(&miner_bytes) .ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?; - // Decode parent hash, difficulty, nonce, VRF number, and proof. + // Decode parent hash, difficulty, nonce, and deterministic VRF number. let mut prev_hash_bytes = vec![0; 32]; cursor.read_exact(&mut prev_hash_bytes).await?; let previous_hash = encode(&prev_hash_bytes); @@ -265,10 +266,6 @@ impl VrfBlock { cursor.read_exact(&mut vrf_bytes).await?; let vrf = u128::from_le_bytes(vrf_bytes); - let mut proof_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; - cursor.read_exact(&mut proof_bytes).await?; - let proof = encode(&proof_bytes); - let unmined_block = UnminedBlock { timestamp, miner, @@ -276,11 +273,7 @@ impl VrfBlock { next_block_difficulty, nonce, }; - Ok(VrfBlock { - unmined_block, - vrf, - proof, - }) + Ok(VrfBlock { unmined_block, vrf }) } } @@ -291,6 +284,12 @@ impl Block { // Serialize the fixed-width VRF header before any transactions. let vrf_bytes = self.vrf_block.to_bytes().await?; buffer.extend_from_slice(&vrf_bytes); + let proof_bytes = decode(&self.proof) + .map_err(|_| tokio::io::Error::other("Invalid miner proof encoding"))?; + if proof_bytes.len() != Wallet::SIGNATURE_LENGTH { + return Err(tokio::io::Error::other("Invalid miner proof length")); + } + buffer.extend_from_slice(&proof_bytes); // Append each transaction in block order using its own fixed layout. for transaction in &self.transactions { @@ -420,3 +419,53 @@ impl Block { Ok(buffer) } } + +#[cfg(test)] +mod tests { + use super::{Block, UnminedBlock, BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES}; + + fn candidate(nonce: u8) -> UnminedBlock { + UnminedBlock { + timestamp: 1_800_000_000, + miner: "1111111111111111111111111111111111111111.cltc".to_string(), + previous_hash: "22".repeat(32), + next_block_difficulty: 2_000_000_000_000_000, + nonce, + } + } + + #[tokio::test] + async fn identical_unsigned_headers_produce_identical_vrf_headers() { + let first = candidate(7).vrf_generate().await; + let second = candidate(7).vrf_generate().await; + + assert_eq!(first.vrf, second.vrf); + assert_eq!(first.hash().await, second.hash().await); + } + + #[tokio::test] + async fn changing_nonce_changes_the_vrf_header() { + let first = candidate(7).vrf_generate().await; + let second = candidate(8).vrf_generate().await; + + assert_ne!(first.vrf, second.vrf); + assert_ne!(first.hash().await, second.hash().await); + } + + #[tokio::test] + async fn proof_is_stored_after_the_deterministic_header() { + let vrf_block = candidate(7).vrf_generate().await; + let deterministic_bytes = vrf_block.to_bytes().await.unwrap(); + let block = Block { + vrf_block, + proof: "33".repeat(crate::wallets::structures::Wallet::SIGNATURE_LENGTH), + transactions: Vec::new(), + original_transactions: Vec::new(), + }; + let block_bytes = block.to_bytes().await.unwrap(); + + assert_eq!(deterministic_bytes.len(), VRF_BLOCK_BYTES); + assert_eq!(block_bytes.len(), BLOCK_HEADER_BYTES); + assert_eq!(&block_bytes[..VRF_BLOCK_BYTES], deterministic_bytes); + } +} diff --git a/src/miner/genesis.rs b/src/miner/genesis.rs index 46b23d8..0174f8e 100644 --- a/src/miner/genesis.rs +++ b/src/miner/genesis.rs @@ -1,5 +1,6 @@ use crate::blocks::block::{Block, UnminedBlock}; use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction}; +use crate::common::binary_conversions::hex_to_u64; use crate::common::check_genesis::genesis_checkup; use crate::common::types::{Transaction, GENESIS_BLOCK_HASH}; use crate::log::{error, info}; @@ -124,14 +125,19 @@ async fn create_genesis_block( ) .await; - // The VRF binds the candidate header to the mining wallet. - let vrf_block = UnminedBlock::vrf_generate(block_struct, &wallet.saved.private_key).await; - + // Derive and test the deterministic genesis candidate before signing it. + let vrf_block = UnminedBlock::vrf_generate(block_struct).await; let header_hash = vrf_block.hash().await; + if hex_to_u64(&header_hash).await? > next_block_difficulty { + nonce = nonce.wrapping_add(1); + continue; + } + let proof = Wallet::sign_transaction(&header_hash, &wallet.saved.private_key).await; // The genesis block contains exactly one genesis transaction. let block = Block { vrf_block, + proof, transactions: vec![Transaction::Genesis(signed_genesis_transaction.clone())], original_transactions: Vec::new(), }; diff --git a/src/miner/mining.rs b/src/miner/mining.rs index 63ff9fe..8855fdc 100644 --- a/src/miner/mining.rs +++ b/src/miner/mining.rs @@ -1,4 +1,5 @@ use crate::blocks::block::{Block, UnminedBlock}; +use crate::common::binary_conversions::hex_to_u64; use crate::common::types::Transaction; use crate::log::{error, info}; use crate::miner::block_rewards::{create_rewards_transaction, reward_value_for_miner_at_height}; @@ -116,7 +117,7 @@ pub async fn mine_block( continue; } }; - let round_second = Utc::now().timestamp() as u32; + let round_second = attempt_context.timestamp; // Each nonce round searches all 256 nonces for a single timestamp. let winning_block = run_nonce_round(attempt_context).await; @@ -235,6 +236,7 @@ async fn build_attempt_context( db: db.clone(), miner_short, wallet, + timestamp: Utc::now().timestamp() as u32, current_block_number, previous_hash, previous_difficulty, @@ -258,7 +260,7 @@ pub async fn mine_block_internal( if !is_normal_mode() || is_mining_stop_requested() { return Ok(None); } - let timestamp = Utc::now().timestamp() as u32; + let timestamp = ctx.timestamp; // Difficulty is calculated from the previous block's difficulty // and the new candidate timestamp. @@ -276,14 +278,20 @@ pub async fn mine_block_internal( ) .await; - // Add the wallet VRF proof before hashing and verifying the candidate. - let vrf_block = UnminedBlock::vrf_generate(unmined_block, &ctx.wallet.saved.private_key).await; + // Derive the deterministic VRF and candidate hash before performing + // the comparatively expensive Falcon signature operation. + let vrf_block = UnminedBlock::vrf_generate(unmined_block).await; let block_hash = vrf_block.hash().await; + if hex_to_u64(&block_hash).await? >= ctx.previous_difficulty { + return Ok(None); + } + let proof = Wallet::sign_transaction(&block_hash, &ctx.wallet.saved.private_key).await; // Every mined block begins with a consensus-created reward transaction. let rewards_transaction = create_rewards_transaction(timestamp, ctx.reward_value).await; let new_block = Block { vrf_block, + proof, transactions: vec![Transaction::Rewards(rewards_transaction)], original_transactions: Vec::new(), }; diff --git a/src/miner/structs.rs b/src/miner/structs.rs index e8d0051..15a148e 100644 --- a/src/miner/structs.rs +++ b/src/miner/structs.rs @@ -10,6 +10,7 @@ pub struct MiningAttemptContext { pub db: Db, pub miner_short: String, pub wallet: Arc, + pub timestamp: u32, pub current_block_number: u32, pub previous_hash: String, pub previous_difficulty: u64, diff --git a/src/records/record_chain/save.rs b/src/records/record_chain/save.rs index f6ebb41..11da93b 100644 --- a/src/records/record_chain/save.rs +++ b/src/records/record_chain/save.rs @@ -81,6 +81,12 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> { .await .map_err(|e| e.to_string())?; let mut binary_data = header_bytes.clone(); + let proof_bytes = crate::decode(&block.proof) + .map_err(|_| "Invalid miner proof encoding while saving block".to_string())?; + if proof_bytes.len() != crate::wallets::structures::Wallet::SIGNATURE_LENGTH { + return Err("Invalid miner proof length while saving block".to_string()); + } + binary_data.extend_from_slice(&proof_bytes); let previous_hash = &block.vrf_block.unmined_block.previous_hash; let miner = &block.vrf_block.unmined_block.miner; diff --git a/src/records/unpack_block/load_by_binary_data.rs b/src/records/unpack_block/load_by_binary_data.rs index 4f32c1a..26b3a60 100644 --- a/src/records/unpack_block/load_by_binary_data.rs +++ b/src/records/unpack_block/load_by_binary_data.rs @@ -1,4 +1,4 @@ -use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; +use crate::blocks::block::{Block, VrfBlock, BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES}; use crate::blocks::burn::BurnTransaction; use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::delete_key::DeleteKey; @@ -57,14 +57,15 @@ fn transaction_body_slice( pub async fn load_block_from_binary(binary_data: &[u8]) -> Result { // Binary block parsing mirrors the on-disk format so torrent-downloaded // blocks can be verified before they are written into the local chain. - if binary_data.len() < VRF_BLOCK_BYTES { + if binary_data.len() < BLOCK_HEADER_BYTES { return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); } let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) .await .map_err(|e| e.to_string())?; - let mut i = VRF_BLOCK_BYTES; + let proof = crate::encode(&binary_data[VRF_BLOCK_BYTES..BLOCK_HEADER_BYTES]); + let mut i = BLOCK_HEADER_BYTES; let mut transactions: Vec = Vec::new(); let mut original_transactions = Vec::new(); @@ -368,6 +369,7 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result let block = Block { vrf_block, + proof, transactions, original_transactions, }; diff --git a/src/records/unpack_block/load_by_block_number.rs b/src/records/unpack_block/load_by_block_number.rs index 40b5faf..4ed8fd4 100644 --- a/src/records/unpack_block/load_by_block_number.rs +++ b/src/records/unpack_block/load_by_block_number.rs @@ -1,4 +1,4 @@ -use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; +use crate::blocks::block::{Block, VrfBlock, BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES}; use crate::blocks::burn::BurnTransaction; use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::delete_key::DeleteKey; @@ -85,14 +85,15 @@ pub async fn load_block(block_number: u32) -> Result { } }; - if binary_data.len() < VRF_BLOCK_BYTES { + if binary_data.len() < BLOCK_HEADER_BYTES { return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); } let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) .await .map_err(|e| e.to_string())?; - let mut i = VRF_BLOCK_BYTES; + let proof = crate::encode(&binary_data[VRF_BLOCK_BYTES..BLOCK_HEADER_BYTES]); + let mut i = BLOCK_HEADER_BYTES; let mut transactions: Vec = Vec::new(); let mut original_transactions = Vec::new(); @@ -385,6 +386,7 @@ pub async fn load_block(block_number: u32) -> Result { let block = Block { vrf_block, + proof, transactions, original_transactions, }; diff --git a/src/records/unpack_block/unpack_header.rs b/src/records/unpack_block/unpack_header.rs index 8aa2f35..977f93e 100644 --- a/src/records/unpack_block/unpack_header.rs +++ b/src/records/unpack_block/unpack_header.rs @@ -1,10 +1,10 @@ -use crate::blocks::block::{VrfBlock, VRF_BLOCK_BYTES}; +use crate::blocks::block::{VrfBlock, BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES}; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::AsyncReadExt; use crate::File; use crate::PathBuf; -pub async fn load_block_header(block_number: u32) -> Result { +pub async fn load_block_header(block_number: u32) -> Result { // Header-only loads avoid reading the full block when only chain metadata // is needed. let ( @@ -49,4 +49,38 @@ pub async fn load_block_header(block_number: u32) -> Result { Ok(block) => Ok(block), Err(err) => Err(format!("Error parsing block: {err:?}")), } -} +} + +pub async fn load_block_proof(block_number: u32) -> Result { + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_name = PathBuf::from(block_path) + .join(format!("{block_number}.{block_ext}")) + .to_string_lossy() + .into_owned(); + let file = File::open(&file_name) + .await + .map_err(|err| format!("Error opening block file for height {block_number}: {err:?}"))?; + let mut binary_data = Vec::with_capacity(BLOCK_HEADER_BYTES); + file.take(BLOCK_HEADER_BYTES as u64) + .read_to_end(&mut binary_data) + .await + .map_err(|err| format!("Error reading block proof for height {block_number}: {err:?}"))?; + if binary_data.len() != BLOCK_HEADER_BYTES { + return Err(format!( + "Block {block_number} does not contain a complete {BLOCK_HEADER_BYTES}-byte header" + )); + } + Ok(crate::encode( + &binary_data[VRF_BLOCK_BYTES..BLOCK_HEADER_BYTES], + )) +} diff --git a/src/rpc/commands/transaction_by_txid.rs b/src/rpc/commands/transaction_by_txid.rs index 55df69e..5573bd1 100644 --- a/src/rpc/commands/transaction_by_txid.rs +++ b/src/rpc/commands/transaction_by_txid.rs @@ -1,4 +1,4 @@ -use crate::blocks::block::VRF_BLOCK_BYTES; +use crate::blocks::block::BLOCK_HEADER_BYTES; use crate::common::binary_conversions::binary_to_string; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::io; @@ -10,7 +10,7 @@ use crate::File; use crate::PathBuf; use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; -const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; +const HEADER_SIZE: u64 = BLOCK_HEADER_BYTES as u64; pub async fn request_transaction_by_txid(db: &Db, txid: Vec) -> RpcResponse { // Resolve the saved transaction bytes directly from the txid lookup diff --git a/src/rpc/commands/tx_count.rs b/src/rpc/commands/tx_count.rs index ed89f94..9981b19 100644 --- a/src/rpc/commands/tx_count.rs +++ b/src/rpc/commands/tx_count.rs @@ -1,4 +1,4 @@ -use crate::blocks::block::VRF_BLOCK_BYTES; +use crate::blocks::block::BLOCK_HEADER_BYTES; use crate::common::binary_conversions::binary_to_string; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::types::{GENESIS_TYPE, REWARDS_TYPE, VANITY_ADDRESS_TYPE}; @@ -8,7 +8,7 @@ use crate::sled::Db; use crate::PathBuf; use crate::{AsyncReadExt, AsyncSeekExt, File, SeekFrom}; -const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; +const HEADER_SIZE: u64 = BLOCK_HEADER_BYTES as u64; async fn lookup_transaction_location(db: &Db, txid: Vec) -> Result<(u64, u32, String), String> { // The txid tree stores `block:index`, which is enough to locate the diff --git a/src/verifications/async_funcs/transactions.rs b/src/verifications/async_funcs/transactions.rs index 3d11ba3..757f134 100644 --- a/src/verifications/async_funcs/transactions.rs +++ b/src/verifications/async_funcs/transactions.rs @@ -241,7 +241,7 @@ async fn verify_transaction( } } if let Transaction::Lender(loan_creation_tx) = &transaction { - match loan_creation_tx.verify(db).await { + match loan_creation_tx.verify_at(db, block_timestamp).await { Ok(value) => { reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool) .await?; diff --git a/src/verifications/async_funcs/verify_block.rs b/src/verifications/async_funcs/verify_block.rs index acb3c63..d363ccc 100644 --- a/src/verifications/async_funcs/verify_block.rs +++ b/src/verifications/async_funcs/verify_block.rs @@ -61,19 +61,15 @@ impl Block { let previous_hash = header.unmined_block.previous_hash.clone(); let difficulty = header.unmined_block.next_block_difficulty; let vrf = header.vrf; - let proof = &header.proof; + let proof = &self.proof; - // hash the header to validate the difficulty - let unmined_header_hash = header.unmined_block.hash().await; - - // validate vrf number - if !Wallet::vrf_verify_with_public_key(vrf, &unmined_header_hash, &miner_pubkey_hex, proof) - .await - { + // The VRF is derived entirely from the unsigned header and must + // therefore be identical on every validating node. + if header.unmined_block.deterministic_vrf().await != vrf { return Err("Invalid vrf.".to_string()); } - // hash the header to validate the difficulty + // The deterministic VRF header controls both difficulty and block identity. let header_hash = header.hash().await; // get u64 value @@ -82,6 +78,13 @@ impl Block { // validate hash Self::validate_hash_difficulty(db, hash).await?; + // Falcon authenticates the winning deterministic header but does + // not contribute any additional mining attempts. + if !Wallet::verify_transaction_with_public_key(&header_hash, proof, &miner_pubkey_hex).await + { + return Err("Invalid miner proof.".to_string()); + } + // Allow the same three-second clock skew tolerated during handshake, // while the parent timestamp check below still enforces block spacing. if timestamp > current_timestamp.saturating_add(ALLOWED_FUTURE_BLOCK_SECONDS) { diff --git a/src/verifications/async_funcs/verify_lender.rs b/src/verifications/async_funcs/verify_lender.rs index daf9728..31a8c36 100644 --- a/src/verifications/async_funcs/verify_lender.rs +++ b/src/verifications/async_funcs/verify_lender.rs @@ -12,14 +12,28 @@ use crate::records::wallet_registry::{ }; use crate::sled::Db; use crate::verifications::async_funcs::checks::balance_check::balance_checkup; -use crate::verifications::async_funcs::checks::time_checks::is_within_30_days; use crate::verifications::async_funcs::checks::verify_db::{ db_bytes_verification, db_hex_verification, }; use crate::wallets::structures::Wallet; +use crate::{DateTime, Utc}; + +fn loan_start_date_is_current_or_future(start_timestamp: u32, as_of_timestamp: u32) -> bool { + let Some(start) = DateTime::::from_timestamp(start_timestamp as i64, 0) else { + return false; + }; + let Some(as_of) = DateTime::::from_timestamp(as_of_timestamp as i64, 0) else { + return false; + }; + start.date_naive() >= as_of.date_naive() +} impl LoanContractTransaction { pub async fn verify(&self, db: &Db) -> Result { + self.verify_at(db, Utc::now().timestamp() as u32).await + } + + pub async fn verify_at(&self, db: &Db, as_of_timestamp: u32) -> Result { let calculated_hash = &self.unsigned_loan_contract.hash().await; // Loan contracts require valid lender and borrower wallet addresses. @@ -60,10 +74,13 @@ impl LoanContractTransaction { return Err("Invalid signature2 the RewardsTransaction.").map_err(|s| s.to_string())?; } - // Loan offers expire if they are broadcast too long after signing. + // A loan cannot enter the chain after its repayment schedule has begun. + // Compare UTC calendar dates because loan due dates are calendar based. let timestamp = self.unsigned_loan_contract.timestamp; - if !is_within_30_days(timestamp).await { - return Err("Timestamp is to old. LoanContractTransactions must be broadcast within 30 days of signing.").map_err(|s| s.to_string())?; + if !loan_start_date_is_current_or_future(timestamp, as_of_timestamp) { + return Err( + "Loan contracts must be broadcast no later than their UTC start date.".to_string(), + ); } let loan_coin = self.unsigned_loan_contract.loan_coin.clone(); @@ -186,3 +203,40 @@ impl LoanContractTransaction { Ok(sign.to_string()) } } + +#[cfg(test)] +mod tests { + use super::loan_start_date_is_current_or_future; + use chrono::{TimeZone, Utc}; + + fn timestamp(year: i32, month: u32, day: u32, hour: u32) -> u32 { + Utc.with_ymd_and_hms(year, month, day, hour, 0, 0) + .single() + .unwrap() + .timestamp() as u32 + } + + #[test] + fn loan_may_be_broadcast_during_its_utc_start_date() { + assert!(loan_start_date_is_current_or_future( + timestamp(2026, 7, 31, 0), + timestamp(2026, 7, 31, 23), + )); + } + + #[test] + fn loan_may_be_broadcast_before_its_utc_start_date() { + assert!(loan_start_date_is_current_or_future( + timestamp(2026, 8, 1, 0), + timestamp(2026, 7, 31, 23), + )); + } + + #[test] + fn loan_cannot_be_broadcast_after_its_utc_start_date() { + assert!(!loan_start_date_is_current_or_future( + timestamp(2026, 7, 31, 0), + timestamp(2026, 8, 1, 0), + )); + } +} diff --git a/src/wallets/verifications.rs b/src/wallets/verifications.rs index 90cca28..6776a75 100644 --- a/src/wallets/verifications.rs +++ b/src/wallets/verifications.rs @@ -1,27 +1,8 @@ -use crate::blocks::block::UnminedBlock; use crate::decode; use crate::wallets::structures::Wallet; use fn_dsa::{VerifyingKey, VerifyingKeyStandard, DOMAIN_NONE, HASH_ID_RAW}; impl Wallet { - pub async fn vrf_verify_with_public_key( - number: u128, - hash: &str, - public_key_hex: &str, - signature: &str, - ) -> bool { - // Derive the VRF number from the submitted signature. - let calculated_number = UnminedBlock::generate_random_number(signature).await; - - // Verify directly against a public key when a long wallet address is not available. - if Self::verify_transaction_with_public_key(hash, signature, public_key_hex).await - && calculated_number == number - { - return true; - } - false - } - pub async fn verify_transaction_with_public_key( message: &str, signature_hex: &str,