fixed FPoW consensus bug
This commit is contained in:
parent
d06b4c83f0
commit
58809af07d
|
|
@ -80,12 +80,22 @@ async fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let wallet_path = prompt_wallet_path().await;
|
// Automated callers can keep credentials out of command arguments by
|
||||||
let encryption_key = prompt_hidden_nonempty(
|
// passing them only to this child process. Normal CLI use still prompts.
|
||||||
"What is your wallet decryption key? ",
|
let wallet_path = match env::var("CONTRACTLESS_WALLET_PATH") {
|
||||||
"Wallet key cannot be empty. Please try again.",
|
Ok(path) if !path.trim().is_empty() => path,
|
||||||
)
|
_ => prompt_wallet_path().await,
|
||||||
.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.
|
// Read the signed transaction JSON exactly as it will be serialized for broadcast.
|
||||||
let json = match tokio::fs::read_to_string(filename).await {
|
let json = match tokio::fs::read_to_string(filename).await {
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,23 @@ async fn main() {
|
||||||
.expect("Please enter a valid amount.");
|
.expect("Please enter a valid amount.");
|
||||||
let value = ((value_f32 as f64) * 100_000_000.0).round() as u64;
|
let value = ((value_f32 as f64) * 100_000_000.0).round() as u64;
|
||||||
|
|
||||||
let wallet_path = prompt_wallet_path().await;
|
// Automated tools such as a locally hosted faucet may provide wallet
|
||||||
|
// credentials through the child-process environment. Interactive use
|
||||||
let decryption_key = prompt_hidden_nonempty(
|
// keeps the existing path completion and hidden key prompts.
|
||||||
"What is your wallet decryption key? ",
|
let wallet_path = match env::var("CONTRACTLESS_WALLET_PATH") {
|
||||||
"Wallet key cannot be empty. Please try again.",
|
Ok(path) if !path.trim().is_empty() => path,
|
||||||
)
|
_ => prompt_wallet_path().await,
|
||||||
.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 {
|
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
|
||||||
Ok(wallet) => wallet,
|
Ok(wallet) => wallet,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use contractless::common::binary_conversions::hex_to_u64;
|
use contractless::common::binary_conversions::hex_to_u64;
|
||||||
use contractless::env;
|
use contractless::env;
|
||||||
use contractless::io;
|
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 contractless::to_string_pretty;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
|
@ -22,8 +23,12 @@ async fn main() -> io::Result<()> {
|
||||||
|
|
||||||
// Header loading uses the active network block path internally.
|
// Header loading uses the active network block path internally.
|
||||||
let header = load_block_header(block_number).await.unwrap();
|
let header = load_block_header(block_number).await.unwrap();
|
||||||
|
let proof = load_block_proof(block_number).await.unwrap();
|
||||||
let hash = header.hash().await;
|
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.
|
// The displayed difficulty is the numeric value derived from the header hash.
|
||||||
let difficulty = hex_to_u64(&hash).await.unwrap();
|
let difficulty = hex_to_u64(&hash).await.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
use contractless::blocks::block::{BLOCK_HEADER_BYTES, VRF_BLOCK_BYTES};
|
||||||
use contractless::common::binary_conversions::hex_to_u64;
|
use contractless::common::binary_conversions::hex_to_u64;
|
||||||
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
|
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
|
||||||
use contractless::common::network_paths_and_settings::block_extension_and_paths;
|
use contractless::common::network_paths_and_settings::block_extension_and_paths;
|
||||||
|
|
@ -84,15 +85,17 @@ async fn main() {
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
});
|
});
|
||||||
block_file.read_to_end(&mut block_data).await.unwrap();
|
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);
|
let info_hash_computed = skein_128_hash_bytes(&block_data);
|
||||||
|
|
||||||
// Rebuild the unmined-block hash using the same canonical path as mining
|
// The miner signs the deterministic VRF header hash only after it wins.
|
||||||
// and live block verification.
|
|
||||||
let unmined_hash = header.unmined_block.hash().await;
|
|
||||||
let signature_ok =
|
let signature_ok =
|
||||||
Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex)
|
Wallet::verify_transaction_with_public_key(&block_hash, &proof, &miner_pubkey_hex).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let passed = "[PASSED]".green();
|
let passed = "[PASSED]".green();
|
||||||
let failed = "[FAILED]".red();
|
let failed = "[FAILED]".red();
|
||||||
|
|
@ -140,13 +143,7 @@ async fn main() {
|
||||||
println!("block header hash check: {:>82}", format!("{failed}"));
|
println!("block header hash check: {:>82}", format!("{failed}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let vrf_ok = Wallet::vrf_verify_with_public_key(
|
let vrf_ok = header.unmined_block.deterministic_vrf().await == header.vrf;
|
||||||
header.vrf,
|
|
||||||
&unmined_hash,
|
|
||||||
&miner_pubkey_hex,
|
|
||||||
&header.proof,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if vrf_ok {
|
if vrf_ok {
|
||||||
println!("VRF validation check: {:>85}", format!("{passed}"));
|
println!("VRF validation check: {:>85}", format!("{passed}"));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,10 @@ pub const NONCE_OFFSET: usize = DIFFICULTY_OFFSET + 8;
|
||||||
pub const VRF_OFFSET: usize = NONCE_OFFSET + 1;
|
pub const VRF_OFFSET: usize = NONCE_OFFSET + 1;
|
||||||
pub const PROOF_OFFSET: usize = VRF_OFFSET + 16;
|
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 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
|
// UnminedBlock is the deterministic header data used to derive the VRF.
|
||||||
// the miner wallet adds the VRF proof.
|
|
||||||
#[derive(Debug, Serialize, Clone)] // 67 bytes
|
#[derive(Debug, Serialize, Clone)] // 67 bytes
|
||||||
pub struct UnminedBlock {
|
pub struct UnminedBlock {
|
||||||
pub timestamp: u32, // 4 bytes block timestamp
|
pub timestamp: u32, // 4 bytes block timestamp
|
||||||
|
|
@ -37,18 +37,18 @@ pub struct UnminedBlock {
|
||||||
pub nonce: u8, // 1 byte nonce searched by mining workers
|
pub nonce: u8, // 1 byte nonce searched by mining workers
|
||||||
}
|
}
|
||||||
|
|
||||||
// VrfBlock adds the miner's signed proof and derived VRF number to the header.
|
// VrfBlock adds the deterministic VRF number to the unsigned header.
|
||||||
#[derive(Debug, Serialize, Clone)] // 749 bytes
|
#[derive(Debug, Serialize, Clone)] // 83 bytes
|
||||||
pub struct VrfBlock {
|
pub struct VrfBlock {
|
||||||
pub unmined_block: UnminedBlock, // 67 bytes unsigned block header fields
|
pub unmined_block: UnminedBlock, // 67 bytes unsigned block header fields
|
||||||
pub vrf: u128, // 16 bytes random number derived from proof
|
pub vrf: u128, // 16 bytes deterministic mining value
|
||||||
pub proof: String, // 666 bytes miner signature proof
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
#[derive(Debug, Serialize)] // header is 749 bytes plus transactions
|
||||||
pub struct Block {
|
pub struct Block {
|
||||||
pub vrf_block: VrfBlock,
|
pub vrf_block: VrfBlock,
|
||||||
|
pub proof: String, // 666 bytes miner signature over the VRF header hash
|
||||||
pub transactions: Vec<Transaction>,
|
pub transactions: Vec<Transaction>,
|
||||||
// Exact transaction slices are retained for downloaded/stored blocks so
|
// Exact transaction slices are retained for downloaded/stored blocks so
|
||||||
// mempool reuse can compare bytes without serializing or hashing again.
|
// 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 {
|
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.
|
// into one u128 value by XORing four 16-byte chunks.
|
||||||
let hash = skein_512_hash_data(input);
|
let hash = skein_512_hash_data(input);
|
||||||
let hash_bytes = decode(&hash).expect("Failed to decode hash");
|
let hash_bytes = decode(&hash).expect("Failed to decode hash");
|
||||||
|
|
@ -108,23 +108,25 @@ impl UnminedBlock {
|
||||||
a ^ b ^ c ^ d
|
a ^ b ^ c ^ d
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn vrf_generate(self, private_key: &str) -> VrfBlock {
|
pub async fn vrf_generate(self) -> VrfBlock {
|
||||||
// Sign the unmined header hash with the miner wallet and derive
|
// Derive the VRF from the unsigned header alone so every node
|
||||||
// the VRF number from that signature.
|
// calculates one result for each timestamp/miner/parent/nonce tuple.
|
||||||
let hash = self.hash().await;
|
let vrf = self.deterministic_vrf().await;
|
||||||
let proof = Wallet::sign_transaction(&hash, private_key).await;
|
|
||||||
let vrf = Self::generate_random_number(&proof).await;
|
|
||||||
VrfBlock {
|
VrfBlock {
|
||||||
unmined_block: self,
|
unmined_block: self,
|
||||||
vrf,
|
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 {
|
pub async fn hash(&self) -> String {
|
||||||
let serialized = to_string(self).unwrap();
|
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 {
|
fn asert_target(anchor_target: u64, height_delta: u32, time_delta: i128) -> u64 {
|
||||||
|
|
@ -208,7 +210,7 @@ impl UnminedBlock {
|
||||||
|
|
||||||
impl VrfBlock {
|
impl VrfBlock {
|
||||||
pub async fn hash(&self) -> String {
|
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();
|
let serialized = to_string(self).unwrap();
|
||||||
skein_256_hash_data(&serialized)
|
skein_256_hash_data(&serialized)
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +235,6 @@ impl VrfBlock {
|
||||||
.write_all(&self.unmined_block.nonce.to_le_bytes())
|
.write_all(&self.unmined_block.nonce.to_le_bytes())
|
||||||
.await?;
|
.await?;
|
||||||
cursor.write_all(&self.vrf.to_le_bytes()).await?;
|
cursor.write_all(&self.vrf.to_le_bytes()).await?;
|
||||||
cursor.write_all(&decode(&self.proof).unwrap()).await?;
|
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +255,7 @@ impl VrfBlock {
|
||||||
let miner = Wallet::bytes_to_short_address(&miner_bytes)
|
let miner = Wallet::bytes_to_short_address(&miner_bytes)
|
||||||
.ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?;
|
.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];
|
let mut prev_hash_bytes = vec![0; 32];
|
||||||
cursor.read_exact(&mut prev_hash_bytes).await?;
|
cursor.read_exact(&mut prev_hash_bytes).await?;
|
||||||
let previous_hash = encode(&prev_hash_bytes);
|
let previous_hash = encode(&prev_hash_bytes);
|
||||||
|
|
@ -265,10 +266,6 @@ impl VrfBlock {
|
||||||
cursor.read_exact(&mut vrf_bytes).await?;
|
cursor.read_exact(&mut vrf_bytes).await?;
|
||||||
let vrf = u128::from_le_bytes(vrf_bytes);
|
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 {
|
let unmined_block = UnminedBlock {
|
||||||
timestamp,
|
timestamp,
|
||||||
miner,
|
miner,
|
||||||
|
|
@ -276,11 +273,7 @@ impl VrfBlock {
|
||||||
next_block_difficulty,
|
next_block_difficulty,
|
||||||
nonce,
|
nonce,
|
||||||
};
|
};
|
||||||
Ok(VrfBlock {
|
Ok(VrfBlock { unmined_block, vrf })
|
||||||
unmined_block,
|
|
||||||
vrf,
|
|
||||||
proof,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,6 +284,12 @@ impl Block {
|
||||||
// Serialize the fixed-width VRF header before any transactions.
|
// Serialize the fixed-width VRF header before any transactions.
|
||||||
let vrf_bytes = self.vrf_block.to_bytes().await?;
|
let vrf_bytes = self.vrf_block.to_bytes().await?;
|
||||||
buffer.extend_from_slice(&vrf_bytes);
|
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.
|
// Append each transaction in block order using its own fixed layout.
|
||||||
for transaction in &self.transactions {
|
for transaction in &self.transactions {
|
||||||
|
|
@ -420,3 +419,53 @@ impl Block {
|
||||||
Ok(buffer)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::blocks::block::{Block, UnminedBlock};
|
use crate::blocks::block::{Block, UnminedBlock};
|
||||||
use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction};
|
use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction};
|
||||||
|
use crate::common::binary_conversions::hex_to_u64;
|
||||||
use crate::common::check_genesis::genesis_checkup;
|
use crate::common::check_genesis::genesis_checkup;
|
||||||
use crate::common::types::{Transaction, GENESIS_BLOCK_HASH};
|
use crate::common::types::{Transaction, GENESIS_BLOCK_HASH};
|
||||||
use crate::log::{error, info};
|
use crate::log::{error, info};
|
||||||
|
|
@ -124,14 +125,19 @@ async fn create_genesis_block(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// The VRF binds the candidate header to the mining wallet.
|
// Derive and test the deterministic genesis candidate before signing it.
|
||||||
let vrf_block = UnminedBlock::vrf_generate(block_struct, &wallet.saved.private_key).await;
|
let vrf_block = UnminedBlock::vrf_generate(block_struct).await;
|
||||||
|
|
||||||
let header_hash = vrf_block.hash().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.
|
// The genesis block contains exactly one genesis transaction.
|
||||||
let block = Block {
|
let block = Block {
|
||||||
vrf_block,
|
vrf_block,
|
||||||
|
proof,
|
||||||
transactions: vec![Transaction::Genesis(signed_genesis_transaction.clone())],
|
transactions: vec![Transaction::Genesis(signed_genesis_transaction.clone())],
|
||||||
original_transactions: Vec::new(),
|
original_transactions: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::blocks::block::{Block, UnminedBlock};
|
use crate::blocks::block::{Block, UnminedBlock};
|
||||||
|
use crate::common::binary_conversions::hex_to_u64;
|
||||||
use crate::common::types::Transaction;
|
use crate::common::types::Transaction;
|
||||||
use crate::log::{error, info};
|
use crate::log::{error, info};
|
||||||
use crate::miner::block_rewards::{create_rewards_transaction, reward_value_for_miner_at_height};
|
use crate::miner::block_rewards::{create_rewards_transaction, reward_value_for_miner_at_height};
|
||||||
|
|
@ -116,7 +117,7 @@ pub async fn mine_block(
|
||||||
continue;
|
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.
|
// Each nonce round searches all 256 nonces for a single timestamp.
|
||||||
let winning_block = run_nonce_round(attempt_context).await;
|
let winning_block = run_nonce_round(attempt_context).await;
|
||||||
|
|
@ -235,6 +236,7 @@ async fn build_attempt_context(
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
miner_short,
|
miner_short,
|
||||||
wallet,
|
wallet,
|
||||||
|
timestamp: Utc::now().timestamp() as u32,
|
||||||
current_block_number,
|
current_block_number,
|
||||||
previous_hash,
|
previous_hash,
|
||||||
previous_difficulty,
|
previous_difficulty,
|
||||||
|
|
@ -258,7 +260,7 @@ pub async fn mine_block_internal(
|
||||||
if !is_normal_mode() || is_mining_stop_requested() {
|
if !is_normal_mode() || is_mining_stop_requested() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let timestamp = Utc::now().timestamp() as u32;
|
let timestamp = ctx.timestamp;
|
||||||
|
|
||||||
// Difficulty is calculated from the previous block's difficulty
|
// Difficulty is calculated from the previous block's difficulty
|
||||||
// and the new candidate timestamp.
|
// and the new candidate timestamp.
|
||||||
|
|
@ -276,14 +278,20 @@ pub async fn mine_block_internal(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Add the wallet VRF proof before hashing and verifying the candidate.
|
// Derive the deterministic VRF and candidate hash before performing
|
||||||
let vrf_block = UnminedBlock::vrf_generate(unmined_block, &ctx.wallet.saved.private_key).await;
|
// the comparatively expensive Falcon signature operation.
|
||||||
|
let vrf_block = UnminedBlock::vrf_generate(unmined_block).await;
|
||||||
let block_hash = vrf_block.hash().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.
|
// Every mined block begins with a consensus-created reward transaction.
|
||||||
let rewards_transaction = create_rewards_transaction(timestamp, ctx.reward_value).await;
|
let rewards_transaction = create_rewards_transaction(timestamp, ctx.reward_value).await;
|
||||||
let new_block = Block {
|
let new_block = Block {
|
||||||
vrf_block,
|
vrf_block,
|
||||||
|
proof,
|
||||||
transactions: vec![Transaction::Rewards(rewards_transaction)],
|
transactions: vec![Transaction::Rewards(rewards_transaction)],
|
||||||
original_transactions: Vec::new(),
|
original_transactions: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub struct MiningAttemptContext {
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
pub miner_short: String,
|
pub miner_short: String,
|
||||||
pub wallet: Arc<Wallet>,
|
pub wallet: Arc<Wallet>,
|
||||||
|
pub timestamp: u32,
|
||||||
pub current_block_number: u32,
|
pub current_block_number: u32,
|
||||||
pub previous_hash: String,
|
pub previous_hash: String,
|
||||||
pub previous_difficulty: u64,
|
pub previous_difficulty: u64,
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,12 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mut binary_data = header_bytes.clone();
|
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 previous_hash = &block.vrf_block.unmined_block.previous_hash;
|
||||||
let miner = &block.vrf_block.unmined_block.miner;
|
let miner = &block.vrf_block.unmined_block.miner;
|
||||||
|
|
|
||||||
|
|
@ -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::burn::BurnTransaction;
|
||||||
use crate::blocks::collateral::CollateralClaimTransaction;
|
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||||
use crate::blocks::delete_key::DeleteKey;
|
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> {
|
pub async fn load_block_from_binary(binary_data: &[u8]) -> Result<Block, String> {
|
||||||
// Binary block parsing mirrors the on-disk format so torrent-downloaded
|
// Binary block parsing mirrors the on-disk format so torrent-downloaded
|
||||||
// blocks can be verified before they are written into the local chain.
|
// 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());
|
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])
|
let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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 transactions: Vec<Transaction> = Vec::new();
|
||||||
let mut original_transactions = 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 {
|
let block = Block {
|
||||||
vrf_block,
|
vrf_block,
|
||||||
|
proof,
|
||||||
transactions,
|
transactions,
|
||||||
original_transactions,
|
original_transactions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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::burn::BurnTransaction;
|
||||||
use crate::blocks::collateral::CollateralClaimTransaction;
|
use crate::blocks::collateral::CollateralClaimTransaction;
|
||||||
use crate::blocks::delete_key::DeleteKey;
|
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());
|
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])
|
let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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 transactions: Vec<Transaction> = Vec::new();
|
||||||
let mut original_transactions = 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 {
|
let block = Block {
|
||||||
vrf_block,
|
vrf_block,
|
||||||
|
proof,
|
||||||
transactions,
|
transactions,
|
||||||
original_transactions,
|
original_transactions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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::common::network_paths_and_settings::block_extension_and_paths;
|
||||||
use crate::AsyncReadExt;
|
use crate::AsyncReadExt;
|
||||||
use crate::File;
|
use crate::File;
|
||||||
use crate::PathBuf;
|
use crate::PathBuf;
|
||||||
|
|
||||||
pub async fn load_block_header(block_number: u32) -> Result<VrfBlock, String> {
|
pub async fn load_block_header(block_number: u32) -> Result<VrfBlock, String> {
|
||||||
// Header-only loads avoid reading the full block when only chain metadata
|
// Header-only loads avoid reading the full block when only chain metadata
|
||||||
// is needed.
|
// is needed.
|
||||||
let (
|
let (
|
||||||
|
|
@ -49,4 +49,38 @@ pub async fn load_block_header(block_number: u32) -> Result<VrfBlock, String> {
|
||||||
Ok(block) => Ok(block),
|
Ok(block) => Ok(block),
|
||||||
Err(err) => Err(format!("Error parsing block: {err:?}")),
|
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],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::binary_conversions::binary_to_string;
|
||||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||||
use crate::io;
|
use crate::io;
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::File;
|
||||||
use crate::PathBuf;
|
use crate::PathBuf;
|
||||||
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
|
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 {
|
pub async fn request_transaction_by_txid(db: &Db, txid: Vec<u8>) -> RpcResponse {
|
||||||
// Resolve the saved transaction bytes directly from the txid lookup
|
// Resolve the saved transaction bytes directly from the txid lookup
|
||||||
|
|
|
||||||
|
|
@ -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::binary_conversions::binary_to_string;
|
||||||
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
use crate::common::network_paths_and_settings::block_extension_and_paths;
|
||||||
use crate::common::types::{GENESIS_TYPE, REWARDS_TYPE, VANITY_ADDRESS_TYPE};
|
use crate::common::types::{GENESIS_TYPE, REWARDS_TYPE, VANITY_ADDRESS_TYPE};
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::sled::Db;
|
||||||
use crate::PathBuf;
|
use crate::PathBuf;
|
||||||
use crate::{AsyncReadExt, AsyncSeekExt, File, SeekFrom};
|
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> {
|
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
|
// The txid tree stores `block:index`, which is enough to locate the
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ async fn verify_transaction(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Transaction::Lender(loan_creation_tx) = &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) => {
|
Ok(value) => {
|
||||||
reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool)
|
reserve_verified_transaction(db, &transaction, balance_tracker, already_in_mempool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -61,19 +61,15 @@ impl Block {
|
||||||
let previous_hash = header.unmined_block.previous_hash.clone();
|
let previous_hash = header.unmined_block.previous_hash.clone();
|
||||||
let difficulty = header.unmined_block.next_block_difficulty;
|
let difficulty = header.unmined_block.next_block_difficulty;
|
||||||
let vrf = header.vrf;
|
let vrf = header.vrf;
|
||||||
let proof = &header.proof;
|
let proof = &self.proof;
|
||||||
|
|
||||||
// hash the header to validate the difficulty
|
// The VRF is derived entirely from the unsigned header and must
|
||||||
let unmined_header_hash = header.unmined_block.hash().await;
|
// therefore be identical on every validating node.
|
||||||
|
if header.unmined_block.deterministic_vrf().await != vrf {
|
||||||
// validate vrf number
|
|
||||||
if !Wallet::vrf_verify_with_public_key(vrf, &unmined_header_hash, &miner_pubkey_hex, proof)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Err("Invalid vrf.".to_string());
|
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;
|
let header_hash = header.hash().await;
|
||||||
|
|
||||||
// get u64 value
|
// get u64 value
|
||||||
|
|
@ -82,6 +78,13 @@ impl Block {
|
||||||
// validate hash
|
// validate hash
|
||||||
Self::validate_hash_difficulty(db, hash).await?;
|
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,
|
// Allow the same three-second clock skew tolerated during handshake,
|
||||||
// while the parent timestamp check below still enforces block spacing.
|
// while the parent timestamp check below still enforces block spacing.
|
||||||
if timestamp > current_timestamp.saturating_add(ALLOWED_FUTURE_BLOCK_SECONDS) {
|
if timestamp > current_timestamp.saturating_add(ALLOWED_FUTURE_BLOCK_SECONDS) {
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,28 @@ use crate::records::wallet_registry::{
|
||||||
};
|
};
|
||||||
use crate::sled::Db;
|
use crate::sled::Db;
|
||||||
use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
|
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::{
|
use crate::verifications::async_funcs::checks::verify_db::{
|
||||||
db_bytes_verification, db_hex_verification,
|
db_bytes_verification, db_hex_verification,
|
||||||
};
|
};
|
||||||
use crate::wallets::structures::Wallet;
|
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 {
|
impl LoanContractTransaction {
|
||||||
pub async fn verify(&self, db: &Db) -> Result<String, String> {
|
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;
|
let calculated_hash = &self.unsigned_loan_contract.hash().await;
|
||||||
|
|
||||||
// Loan contracts require valid lender and borrower wallet addresses.
|
// 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())?;
|
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;
|
let timestamp = self.unsigned_loan_contract.timestamp;
|
||||||
if !is_within_30_days(timestamp).await {
|
if !loan_start_date_is_current_or_future(timestamp, as_of_timestamp) {
|
||||||
return Err("Timestamp is to old. LoanContractTransactions must be broadcast within 30 days of signing.").map_err(|s| s.to_string())?;
|
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();
|
let loan_coin = self.unsigned_loan_contract.loan_coin.clone();
|
||||||
|
|
@ -186,3 +203,40 @@ impl LoanContractTransaction {
|
||||||
Ok(sign.to_string())
|
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),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,8 @@
|
||||||
use crate::blocks::block::UnminedBlock;
|
|
||||||
use crate::decode;
|
use crate::decode;
|
||||||
use crate::wallets::structures::Wallet;
|
use crate::wallets::structures::Wallet;
|
||||||
use fn_dsa::{VerifyingKey, VerifyingKeyStandard, DOMAIN_NONE, HASH_ID_RAW};
|
use fn_dsa::{VerifyingKey, VerifyingKeyStandard, DOMAIN_NONE, HASH_ID_RAW};
|
||||||
|
|
||||||
impl Wallet {
|
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(
|
pub async fn verify_transaction_with_public_key(
|
||||||
message: &str,
|
message: &str,
|
||||||
signature_hex: &str,
|
signature_hex: &str,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue