fixed FPoW consensus bug

This commit is contained in:
viraladmin 2026-08-01 14:36:10 -06:00
parent d06b4c83f0
commit 58809af07d
18 changed files with 278 additions and 110 deletions

View File

@ -80,12 +80,22 @@ async fn main() {
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
// 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;
.await
}
};
// Read the signed transaction JSON exactly as it will be serialized for broadcast.
let json = match tokio::fs::read_to_string(filename).await {

View File

@ -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(
// 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;
.await
}
};
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,

View File

@ -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();

View File

@ -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 {

View File

@ -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<Transaction>,
// 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);
}
}

View File

@ -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(),
};

View File

@ -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(),
};

View File

@ -10,6 +10,7 @@ pub struct MiningAttemptContext {
pub db: Db,
pub miner_short: String,
pub wallet: Arc<Wallet>,
pub timestamp: u32,
pub current_block_number: u32,
pub previous_hash: String,
pub previous_difficulty: u64,

View File

@ -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;

View File

@ -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<Block, String> {
// 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<Transaction> = Vec::new();
let mut original_transactions = Vec::new();
@ -368,6 +369,7 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result<Block, String>
let block = Block {
vrf_block,
proof,
transactions,
original_transactions,
};

View File

@ -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<Block, String> {
}
};
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<Transaction> = Vec::new();
let mut original_transactions = Vec::new();
@ -385,6 +386,7 @@ pub async fn load_block(block_number: u32) -> Result<Block, String> {
let block = Block {
vrf_block,
proof,
transactions,
original_transactions,
};

View File

@ -1,4 +1,4 @@
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;
@ -50,3 +50,37 @@ pub async fn load_block_header(block_number: u32) -> Result<VrfBlock, String> {
Err(err) => Err(format!("Error parsing block: {err:?}")),
}
}
pub async fn load_block_proof(block_number: u32) -> Result<String, String> {
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],
))
}

View File

@ -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<u8>) -> RpcResponse {
// Resolve the saved transaction bytes directly from the txid lookup

View File

@ -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<u8>) -> Result<(u64, u32, String), String> {
// The txid tree stores `block:index`, which is enough to locate the

View File

@ -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?;

View File

@ -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) {

View File

@ -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::<Utc>::from_timestamp(start_timestamp as i64, 0) else {
return false;
};
let Some(as_of) = DateTime::<Utc>::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<String, String> {
self.verify_at(db, Utc::now().timestamp() as u32).await
}
pub async fn verify_at(&self, db: &Db, as_of_timestamp: u32) -> Result<String, String> {
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),
));
}
}

View File

@ -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,