Contractless/src/torrent/create_metadata.rs

172 lines
6.4 KiB
Rust

use crate::blocks::block::{NONCE_OFFSET, VRF_OFFSET};
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::skein::skein_128_hash_bytes;
use crate::log::error;
use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::rpc::command_maps::RPC_SUBMIT_TORRENT;
use crate::rpc::responses::RpcResponse;
use crate::torrent::structs::{Info, Torrent};
use crate::torrent::torrenting_system::get_nodes::get_torrent_broadcast_nodes_from_memory;
use crate::torrent::torrenting_system::torrent_cache::should_broadcast_torrent;
use crate::Arc;
use crate::File;
use crate::HashMap;
use crate::Mutex;
use crate::{AsyncReadExt, AsyncWriteExt};
pub async fn metadata_from_file(
file_path: &str,
block_height: u32,
difficulty: u64,
timestamp: u32,
block_hash: &str,
previous_hash: &str,
miner_wallet: String,
) -> Result<Vec<u8>, String> {
// The torrent is built from the mined block file and saved under the network torrent path.
let (
_network_name,
_padded_base_coin,
_block_ext,
out_path,
_wallet_path,
_block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
let mut file = File::open(file_path)
.await
.map_err(|err| format!("Failed to open saved block file for torrent metadata: {err}"))?;
let mut content = Vec::new();
if let Err(err) = file.read_to_end(&mut content).await {
error!("Failed to read file content: {err}");
return Err(format!(
"Failed to read saved block file for torrent metadata: {err}"
));
}
// Pick larger piece sizes for larger blocks so torrents do not exceed the u8 piece index limit.
let mut piece_length: u32;
if !content.is_empty() && content.len() < 1000 {
piece_length = 500_u32;
} else if content.len() >= 1000 && content.len() < 10000 {
piece_length = 1000_u32;
} else if content.len() >= 10000 && content.len() < 100000 {
piece_length = 10000_u32;
} else if content.len() >= 100000 && content.len() < 1000000 {
piece_length = 100000_u32;
} else if content.len() >= 1000000 && content.len() < 10000000 {
piece_length = 1000000_u32;
} else {
piece_length = 2000000_u32;
}
while !content.is_empty() && content.chunks(piece_length as usize).len() > u8::MAX as usize {
// If the rough size bucket still creates too many pieces, keep doubling until it fits.
piece_length = piece_length.saturating_mul(2);
}
// The info hash is the 128-bit hash of the full block bytes.
let block_hashed = skein_128_hash_bytes(&content);
// Nonce and VRF are copied from the serialized block header at fixed offsets.
let nonce = content[NONCE_OFFSET];
let vrf_bytes = &content[VRF_OFFSET..VRF_OFFSET + 16];
let vrf = u128::from_le_bytes(
vrf_bytes
.try_into()
.expect("Slice must be exactly 16 bytes"),
);
// Hash each piece separately so peers can verify downloaded chunks before assembly.
let mut piece_hashes: Vec<HashMap<u8, String>> = Vec::new();
for (index, piece) in content.chunks(piece_length as usize).enumerate() {
let hash = skein_128_hash_bytes(piece);
let piece_number = u8::try_from(index + 1)
.map_err(|_| "Piece count exceeds u8 limit while creating torrent metadata")?;
let mut map = HashMap::new();
map.insert(piece_number, hash);
piece_hashes.push(map);
}
// Torrent info mirrors the data needed to verify the downloaded block later.
let info = Info {
length: content.len() as u64,
this_block_difficulty: difficulty,
timestamp,
nonce,
vrf,
block_hash: block_hash.to_string(),
previous_hash: previous_hash.to_string(),
piece_length,
info_hash: block_hashed,
pieces: piece_hashes,
};
let torrent = Torrent {
info,
mined_by: miner_wallet,
};
// Save the compact binary torrent metadata so the save path can
// broadcast it only after the block height has committed.
let torrent_bytes = torrent.to_bytes().await;
create_torrent_file(out_path, block_height, &torrent_bytes)
.await
.map_err(|err| format!("Failed to create torrent metadata file: {err}"))?;
Ok(torrent_bytes)
}
async fn create_torrent_file(
out_path: String,
block_height: u32,
metadata: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
// The torrent filename follows the committed block height.
let torrent_file_path = crate::Path::new(&out_path).join(format!("{block_height}.torrent"));
// Write the canonical local .torrent file through a temporary path
// so peers never see partial metadata for a committed block.
let temp_torrent_file_path = torrent_file_path.with_extension("torrent.tmp");
let mut torrent_file = File::create(&temp_torrent_file_path).await?;
torrent_file.write_all(metadata).await?;
torrent_file.flush().await?;
crate::fs::rename(&temp_torrent_file_path, &torrent_file_path)?;
Ok(())
}
pub async fn broadcast_new_torrent_to_peers(
block_height: u32,
torrent_bytes: &[u8],
map: Arc<Mutex<Command>>,
) {
// Command byte for "submit torrent".
let command: u8 = RPC_SUBMIT_TORRENT;
// The cache suppresses repeated broadcasts of identical torrent bytes.
let torrent_hash = skein_128_hash_bytes(torrent_bytes);
if !should_broadcast_torrent(&torrent_hash, block_height).await {
return;
}
let torrent_len = 4 + torrent_bytes.len() as u32;
// Send the torrent to all currently connected miner peers.
let peers = get_torrent_broadcast_nodes_from_memory().await;
for (connections_key, stream) in peers {
// Each peer needs its own reply mapping entry and UID.
let (uid_bytes, _tx, _rx) = reserve_entry(map.clone()).await;
let mut message = Vec::with_capacity(1 + 3 + 4 + 4 + torrent_bytes.len());
message.push(command); // Command byte
message.extend_from_slice(&uid_bytes); // UID
message.extend_from_slice(&torrent_len.to_le_bytes()); // Torrent byte size
message.extend_from_slice(&block_height.to_le_bytes()); // Block height
message.extend_from_slice(torrent_bytes); // Torrent file contents
RpcResponse::send_raw(&stream, Some(&connections_key), &message).await;
}
}