diff --git a/crates/engine-gpu/src/end_to_end_tests.rs b/crates/engine-gpu/src/end_to_end_tests.rs new file mode 100644 index 0000000..43c5dac --- /dev/null +++ b/crates/engine-gpu/src/end_to_end_tests.rs @@ -0,0 +1,196 @@ +use pow_core::{JobContext, hash_from_nonce}; +use primitive_types::U512; +use wgpu::util::DeviceExt; +use rand::Rng; +use rand::SeedableRng; + +pub async fn test_end_to_end_mining( + device: &wgpu::Device, + queue: &wgpu::Queue, +) -> Result<(), Box> { + println!("Running End-to-End Mining Test..."); + + // 1. Setup a job context + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(12345); + let mut header = [0u8; 32]; + rng.fill(&mut header); + + // Use difficulty 1 so target is MAX. Any hash should pass. + let difficulty = U512::from(1u64); + let ctx = JobContext::new(header, difficulty); + + // 2. Pick a nonce + let nonce_val = U512::from(123456789u64); + + // 3. Compute expected hash using CPU (pow-core) + let expected_hash = hash_from_nonce(&ctx, nonce_val); + println!("CPU Expected Hash: {:x}", expected_hash); + + // 4. Run GPU Mining for this specific nonce + + // Header Buffer + let mut header_u32s = [0u32; 8]; + for i in 0..8 { + let chunk = &ctx.header[i * 4..(i + 1) * 4]; + header_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + let header_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Header Buffer"), + contents: bytemuck::cast_slice(&header_u32s), + usage: wgpu::BufferUsages::STORAGE, + }); + + // Target Buffer + let target_bytes = ctx.target.to_little_endian(); + let mut target_u32s = [0u32; 16]; + for i in 0..16 { + let chunk = &target_bytes[i * 4..(i + 1) * 4]; + target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + let target_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Target Buffer"), + contents: bytemuck::cast_slice(&target_u32s), + usage: wgpu::BufferUsages::STORAGE, + }); + + // Start Nonce Buffer + // We set start_nonce = nonce_val. Thread 0 will check start_nonce + 0 = nonce_val. + let start_nonce_bytes = nonce_val.to_little_endian(); + let mut start_nonce_u32s = [0u32; 16]; + for i in 0..16 { + let chunk = &start_nonce_bytes[i * 4..(i + 1) * 4]; + start_nonce_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + let start_nonce_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Start Nonce Buffer"), + contents: bytemuck::cast_slice(&start_nonce_u32s), + usage: wgpu::BufferUsages::STORAGE, + }); + + // Results Buffer + let results_size = (1 + 16 + 16) * 4; + let results_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Results Buffer"), + size: results_size as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Clear results buffer + let zeros = vec![0u8; results_size]; + queue.write_buffer(&results_buffer, 0, &zeros); + + // Load Shader + let shader_source = include_str!("mining.wgsl"); + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Mining Shader"), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), + }); + + let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("Mining Pipeline"), + layout: None, + module: &shader, + entry_point: Some("mining_main"), + compilation_options: Default::default(), + cache: None, + }); + + let bind_group_layout = pipeline.get_bind_group_layout(0); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Mining Bind Group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: results_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: header_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: start_nonce_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: target_buffer.as_entire_binding(), + }, + ], + }); + + // Run Compute Pass + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Mining Encoder"), + }); + + { + let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("Mining Compute Pass"), + timestamp_writes: None, + }); + cpass.set_pipeline(&pipeline); + cpass.set_bind_group(0, &bind_group, &[]); + cpass.dispatch_workgroups(1, 1, 1); // 1 workgroup, 256 threads. Thread 0 will check nonce_val. + } + + // Read Results + let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Staging Buffer"), + size: results_size as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + encoder.copy_buffer_to_buffer(&results_buffer, 0, &staging_buffer, 0, results_size as u64); + queue.submit(Some(encoder.finish())); + + let buffer_slice = staging_buffer.slice(..); + let (sender, receiver) = futures::channel::oneshot::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap()); + + device.poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }).unwrap(); + + if let Ok(Ok(())) = receiver.await { + let data = buffer_slice.get_mapped_range(); + let result_u32s: &[u32] = bytemuck::cast_slice(&data); + + if result_u32s[0] != 0 { + println!("GPU found solution!"); + + // Parse nonce + let mut nonce_bytes = [0u8; 64]; + for i in 0..16 { + let bytes = result_u32s[1 + i].to_le_bytes(); + nonce_bytes[i * 4..(i + 1) * 4].copy_from_slice(&bytes); + } + let found_nonce = U512::from_little_endian(&nonce_bytes); + println!("GPU Nonce: {}", found_nonce); + + assert_eq!(found_nonce, nonce_val, "Nonce mismatch!"); + + // Parse hash + let mut hash_bytes = [0u8; 64]; + for i in 0..16 { + let bytes = result_u32s[17 + i].to_le_bytes(); + hash_bytes[i * 4..(i + 1) * 4].copy_from_slice(&bytes); + } + let found_hash = U512::from_little_endian(&hash_bytes); + println!("GPU Hash: {:x}", found_hash); + + assert_eq!(found_hash, expected_hash, "Hash mismatch!"); + println!("✅ End-to-End Test Passed!"); + } else { + println!("❌ GPU did not find solution (should have passed with MAX target)"); + return Err("GPU did not find solution".into()); + } + } else { + return Err("Failed to map buffer".into()); + } + + Ok(()) +} diff --git a/crates/engine-gpu/src/main.rs b/crates/engine-gpu/src/main.rs index 3577a0b..64cf107 100644 --- a/crates/engine-gpu/src/main.rs +++ b/crates/engine-gpu/src/main.rs @@ -6,6 +6,7 @@ use qp_plonky2_field::types::{Field, PrimeField64}; use wgpu::{self, util::DeviceExt}; mod tests; +mod end_to_end_tests; fn u8_array_to_u32_array(bytes: &[u8; 96]) -> [u32; 24] { let mut result = [0u32; 24]; @@ -121,6 +122,15 @@ async fn run() -> Result<(), Box> { eprintln!("❌ Double hash tests failed: {}", e); } + if let Err(e) = end_to_end_tests::test_end_to_end_mining(&device, &queue).await { + eprintln!("❌ End-to-end mining test failed: {}", e); + } + + println!("\nAll tests completed!"); + if let Err(e) = end_to_end_tests::test_end_to_end_mining(&device, &queue).await { + eprintln!("❌ End-to-end mining test failed: {}", e); + } + println!("\nAll tests completed!"); // generate_correct_wgsl_constants(); diff --git a/crates/engine-gpu/src/mining.wgsl b/crates/engine-gpu/src/mining.wgsl index 68aca8c..95761cf 100644 --- a/crates/engine-gpu/src/mining.wgsl +++ b/crates/engine-gpu/src/mining.wgsl @@ -973,18 +973,29 @@ fn mining_main(@builtin(global_invocation_id) global_id: vec3) { input[8u + i] = rev; } - // Hash - let hash = double_hash(input); + // Hash (Big Endian) + let hash_be = double_hash(input); + + // Convert to Little Endian for difficulty check and storage + var hash_le: array; + for (var i = 0u; i < 16u; i++) { + let val = hash_be[15u - i]; + // Reverse bytes in u32 + hash_le[i] = ((val & 0xFFu) << 24u) | + ((val & 0xFF00u) << 8u) | + ((val & 0xFF0000u) >> 8u) | + ((val & 0xFF000000u) >> 24u); + } // Check target - if (is_below_target(hash, difficulty_target)) { + if (is_below_target(hash_le, difficulty_target)) { // Try to claim the solution if (atomicExchange(&results[0], 1u) == 0u) { // We won! Write nonce and hash // results layout: [0]=found, [1..16]=nonce, [17..32]=hash for (var i = 0u; i < 16u; i++) { atomicStore(&results[1u + i], current_nonce[i]); - atomicStore(&results[17u + i], hash[i]); + atomicStore(&results[17u + i], hash_le[i]); } } }