From 31e8c97a0c2e6a71bfec169aa6ea31e32281b05e Mon Sep 17 00:00:00 2001 From: crivasr Date: Mon, 20 Oct 2025 12:26:55 -0300 Subject: [PATCH 01/22] refactor challenge types --- bitcoin-script-riscv/src/riscv/challenges.rs | 216 +++++++++++-------- definitions/src/challenge.rs | 101 ++++++--- emulator/src/decision/challenge.rs | 170 ++++++++------- 3 files changed, 289 insertions(+), 198 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 19d1b5d..4e47836 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -603,7 +603,7 @@ pub fn future_read_challenge(stack: &mut StackTracker) { let [read_step] = get_selected_vars(stack, [read_step_1], [read_step_2], read_selector); - // read_step != LAST_STEP_INIT + // read_step != LAST_STEP_INIT let init = stack.number_u64(LAST_STEP_INIT); stack.equality(read_step, false, init, true, false, true); @@ -618,35 +618,51 @@ pub fn future_read_challenge(stack: &mut StackTracker) { pub fn execute_challenge(challege_type: &ChallengeType) -> bool { let mut stack = StackTracker::new(); match challege_type { - ChallengeType::TraceHash(pre_hash, trace_step, hash) => { - stack.hexstr_as_nibbles(pre_hash); - stack.number_u32(trace_step.get_write().address); - stack.number_u32(trace_step.get_write().value); - stack.number_u32(trace_step.get_pc().get_address()); - stack.byte(trace_step.get_pc().get_micro()); - stack.hexstr_as_nibbles(hash); + ChallengeType::TraceHash { + prover_step_hash, + prover_trace, + prover_next_hash, + } => { + stack.hexstr_as_nibbles(prover_step_hash); + stack.number_u32(prover_trace.get_write().address); + stack.number_u32(prover_trace.get_write().value); + stack.number_u32(prover_trace.get_pc().get_address()); + stack.byte(prover_trace.get_pc().get_micro()); + stack.hexstr_as_nibbles(prover_next_hash); trace_hash_challenge(&mut stack); } - ChallengeType::TraceHashZero(trace_step, hash) => { - stack.number_u32(trace_step.get_write().address); - stack.number_u32(trace_step.get_write().value); - stack.number_u32(trace_step.get_pc().get_address()); - stack.byte(trace_step.get_pc().get_micro()); - stack.hexstr_as_nibbles(hash); + ChallengeType::TraceHashZero { + prover_trace, + prover_next_hash, + } => { + stack.number_u32(prover_trace.get_write().address); + stack.number_u32(prover_trace.get_write().value); + stack.number_u32(prover_trace.get_pc().get_address()); + stack.byte(prover_trace.get_pc().get_micro()); + stack.hexstr_as_nibbles(prover_next_hash); trace_hash_zero_challenge(&mut stack); } - ChallengeType::EntryPoint(read_pc, step, real_entry_point) => { - stack.number_u32(read_pc.pc.get_address()); - stack.byte(read_pc.pc.get_micro()); - stack.number_u64(*step); + ChallengeType::EntryPoint { + prover_read_pc, + prover_trace_step, + real_entry_point, + } => { + stack.number_u32(prover_read_pc.pc.get_address()); + stack.byte(prover_read_pc.pc.get_micro()); + stack.number_u64(*prover_trace_step); entry_point_challenge(&mut stack, real_entry_point.unwrap()); } - ChallengeType::ProgramCounter(pre_pre_hash, pre_step, prover_step_hash, prover_pc_read) => { - stack.hexstr_as_nibbles(pre_pre_hash); - stack.number_u32(pre_step.get_write().address); - stack.number_u32(pre_step.get_write().value); - stack.number_u32(pre_step.get_pc().get_address()); - stack.byte(pre_step.get_pc().get_micro()); + ChallengeType::ProgramCounter { + pre_hash, + trace, + prover_step_hash, + prover_pc_read, + } => { + stack.hexstr_as_nibbles(pre_hash); + stack.number_u32(trace.get_write().address); + stack.number_u32(trace.get_write().value); + stack.number_u32(trace.get_pc().get_address()); + stack.byte(trace.get_pc().get_micro()); stack.number_u32(prover_pc_read.pc.get_address()); stack.byte(prover_pc_read.pc.get_micro()); @@ -655,74 +671,99 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { program_counter_challenge(&mut stack); } - ChallengeType::Opcode(pc_read, _, chunk) => { - stack.number_u32(pc_read.pc.get_address()); - stack.number_u32(pc_read.opcode); + ChallengeType::Opcode { + prover_pc_read, + chunk_index: _, + chunk, + } => { + stack.number_u32(prover_pc_read.pc.get_address()); + stack.number_u32(prover_pc_read.opcode); opcode_challenge(&mut stack, chunk.as_ref().unwrap()); } - ChallengeType::InputData(read_1, read_2, address, input_for_address) => { + ChallengeType::InputData { + prover_read_1, + prover_read_2, + address, + input_for_address, + } => { stack.number_u32(*input_for_address); //TODO: this should make input_wots[address] - stack.number_u32(read_1.address); - stack.number_u32(read_1.value); - stack.number_u64(read_1.last_step); + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_1.value); + stack.number_u64(prover_read_1.last_step); - stack.number_u32(read_2.address); - stack.number_u32(read_2.value); - stack.number_u64(read_2.last_step); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_read_2.value); + stack.number_u64(prover_read_2.last_step); input_challenge(&mut stack, *address); } - ChallengeType::InitializedData(read_1, read_2, read_selector, _, chunk) => { - stack.number_u32(read_1.address); - stack.number_u32(read_1.value); - stack.number_u64(read_1.last_step); + ChallengeType::InitializedData { + prover_read_1, + prover_read_2, + read_selector, + chunk_index: _, + chunk, + } => { + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_1.value); + stack.number_u64(prover_read_1.last_step); - stack.number_u32(read_2.address); - stack.number_u32(read_2.value); - stack.number_u64(read_2.last_step); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_read_2.value); + stack.number_u64(prover_read_2.last_step); stack.number(*read_selector); initialized_challenge(&mut stack, chunk.as_ref().unwrap()); } - ChallengeType::UninitializedData(read_1, read_2, read_selector, uninitialized_sections) => { - stack.number_u32(read_1.address); - stack.number_u32(read_1.value); - stack.number_u64(read_1.last_step); + ChallengeType::UninitializedData { + prover_read_1, + prover_read_2, + read_selector, + sections, + } => { + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_1.value); + stack.number_u64(prover_read_1.last_step); - stack.number_u32(read_2.address); - stack.number_u32(read_2.value); - stack.number_u64(read_2.last_step); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_read_2.value); + stack.number_u64(prover_read_2.last_step); stack.number(*read_selector); - uninitialized_challenge(&mut stack, uninitialized_sections.as_ref().unwrap()); + uninitialized_challenge(&mut stack, sections.as_ref().unwrap()); } - ChallengeType::RomData(read_1, read_2, address, input_for_address) => { - stack.number_u32(read_1.address); - stack.number_u32(read_1.value); - stack.number_u64(read_1.last_step); - stack.number_u32(read_2.address); - stack.number_u32(read_2.value); - stack.number_u64(read_2.last_step); + ChallengeType::RomData { + prover_read_1, + prover_read_2, + address, + input_for_address, + } => { + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_1.value); + stack.number_u64(prover_read_1.last_step); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_read_2.value); + stack.number_u64(prover_read_2.last_step); rom_challenge(&mut stack, *address, *input_for_address); } - ChallengeType::AddressesSections( - read_1, - read_2, - write, - memory_witness, - pc, + ChallengeType::AddressesSections { + prover_read_1, + prover_read_2, + prover_write, + prover_witness, + prover_pc, read_write_sections, read_only_sections, register_sections, code_sections, - ) => { - stack.number_u32(read_1.address); - stack.number_u32(read_2.address); - stack.number_u32(write.address); - stack.byte(memory_witness.byte()); - stack.number_u32(pc.get_address()); + } => { + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_write.address); + stack.byte(prover_witness.byte()); + stack.number_u32(prover_pc.get_address()); addresses_sections_challenge( &mut stack, @@ -734,45 +775,45 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { } ChallengeType::FutureRead { step, - read_step_1, - read_step_2, + prover_read_step_1, + prover_read_step_2, read_selector, } => { stack.number_u64(*step); - stack.number_u64(*read_step_1); - stack.number_u64(*read_step_2); + stack.number_u64(*prover_read_step_1); + stack.number_u64(*prover_read_step_2); stack.number(*read_selector); future_read_challenge(&mut stack); } ChallengeType::ReadValue { - read_1, - read_2, + prover_read_1, + prover_read_2, read_selector, - step_hash, + prover_hash, trace, - next_hash, + prover_next_hash, write_step, conflict_step, } => { - stack.number_u32(read_1.address); - stack.number_u32(read_1.value); - stack.number_u64(read_1.last_step); + stack.number_u32(prover_read_1.address); + stack.number_u32(prover_read_1.value); + stack.number_u64(prover_read_1.last_step); - stack.number_u32(read_2.address); - stack.number_u32(read_2.value); - stack.number_u64(read_2.last_step); + stack.number_u32(prover_read_2.address); + stack.number_u32(prover_read_2.value); + stack.number_u64(prover_read_2.last_step); stack.number(*read_selector); - stack.hexstr_as_nibbles(step_hash); + stack.hexstr_as_nibbles(prover_hash); stack.number_u32(trace.get_write().address); stack.number_u32(trace.get_write().value); stack.number_u32(trace.get_pc().get_address()); stack.byte(trace.get_pc().get_micro()); - stack.hexstr_as_nibbles(next_hash); + stack.hexstr_as_nibbles(prover_next_hash); stack.number_u64(*write_step); stack.number_u64(*conflict_step); @@ -780,10 +821,10 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { read_value_challenge(&mut stack); } ChallengeType::CorrectHash { - prover_hash, + prover_step_hash: prover_hash, verifier_hash, trace, - next_hash, + prover_next_hash, } => { stack.hexstr_as_nibbles(prover_hash); stack.hexstr_as_nibbles(verifier_hash); @@ -793,7 +834,7 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { stack.number_u32(trace.get_pc().get_address()); stack.byte(trace.get_pc().get_micro()); - stack.hexstr_as_nibbles(next_hash); + stack.hexstr_as_nibbles(prover_next_hash); correct_hash_challenge(&mut stack); } @@ -2017,7 +2058,6 @@ mod tests { write_step, conflict_step )); - } mod coin_tests { diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index 2ebb3d0..ea1f0d2 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -8,48 +8,91 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChallengeType { - TraceHash(String, TraceStep, String), // PROVER_PREV_HASH, PROVER_TRACE_STEP, PROVER_STEP_HASH - TraceHashZero(TraceStep, String), // PROVER_TRACE_STEP, PROVER_STEP_HASH - EntryPoint(TraceReadPC, u64, Option), // (PROVER_READ_PC, PROVER_READ_MICRO), PROVER_TRACE_STEP, ENTRYPOINT (only used on test) - ProgramCounter(String, TraceStep, String, TraceReadPC), - Opcode(TraceReadPC, u32, Option), // (PROVER_PC, PROVER_OPCODE), CHUNK_INDEX, CHUNK_BASE_ADDRESS, OPCODES_CHUNK - InputData(TraceRead, TraceRead, u32, u32), - InitializedData(TraceRead, TraceRead, u32, u32, Option), - UninitializedData(TraceRead, TraceRead, u32, Option), - RomData(TraceRead, TraceRead, u32, u32), - AddressesSections( - TraceRead, - TraceRead, - TraceWrite, - MemoryWitness, - ProgramCounter, - Option, // read write sections - Option, // read only sections - Option, // register sections - Option, // code sections - ), + TraceHash { + prover_step_hash: String, + prover_trace: TraceStep, + prover_next_hash: String, + }, + TraceHashZero { + prover_trace: TraceStep, + prover_next_hash: String, + }, + EntryPoint { + prover_read_pc: TraceReadPC, + prover_trace_step: u64, + real_entry_point: Option, + }, + ProgramCounter { + pre_hash: String, + trace: TraceStep, + prover_step_hash: String, + prover_pc_read: TraceReadPC, + }, + Opcode { + prover_pc_read: TraceReadPC, + chunk_index: u32, + chunk: Option, + }, + InputData { + prover_read_1: TraceRead, + prover_read_2: TraceRead, + address: u32, + input_for_address: u32, + }, + InitializedData { + prover_read_1: TraceRead, + prover_read_2: TraceRead, + read_selector: u32, + chunk_index: u32, + chunk: Option, + }, + UninitializedData { + prover_read_1: TraceRead, + prover_read_2: TraceRead, + read_selector: u32, + sections: Option, + }, + RomData { + prover_read_1: TraceRead, + prover_read_2: TraceRead, + address: u32, + input_for_address: u32, + }, + AddressesSections { + prover_read_1: TraceRead, + prover_read_2: TraceRead, + prover_write: TraceWrite, + prover_witness: MemoryWitness, + prover_pc: ProgramCounter, + read_write_sections: Option, + read_only_sections: Option, + register_sections: Option, + code_sections: Option, + }, FutureRead { step: u64, - read_step_1: u64, - read_step_2: u64, + prover_read_step_1: u64, + prover_read_step_2: u64, read_selector: u32, }, - ReadValueNArySearch(u32), + ReadValueNArySearch { + bits: u32, + }, ReadValue { - read_1: TraceRead, - read_2: TraceRead, + prover_read_1: TraceRead, + prover_read_2: TraceRead, read_selector: u32, - step_hash: String, + prover_hash: String, trace: TraceStep, - next_hash: String, + prover_next_hash: String, write_step: u64, conflict_step: u64, }, CorrectHash { - prover_hash: String, + prover_step_hash: String, verifier_hash: String, trace: TraceStep, - next_hash: String, + prover_next_hash: String, }, No, } diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index 0383787..d7a51e4 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -309,29 +309,32 @@ pub fn verifier_choose_challenge( false, )?; - let (step_hash, next_hash) = get_hashes( + let (prover_step_hash, prover_next_hash) = get_hashes( &nary_def.step_mapping(&conflict_step_log.verifier_decisions), &conflict_step_log.prover_hash_rounds, conflict_step_log.step_to_challenge, ); // check trace_hash - if (!validate_step_hash(&step_hash, &trace.trace_step, &next_hash) + if (!validate_step_hash(&prover_step_hash, &trace.trace_step, &prover_next_hash) && force == ForceChallenge::No) || force == ForceChallenge::TraceHash || force == ForceChallenge::TraceHashZero { if trace.step_number == 1 { info!("Verifier choose to challenge TRACE_HASH_ZERO"); - return Ok(ChallengeType::TraceHashZero(trace.trace_step, next_hash)); + return Ok(ChallengeType::TraceHashZero { + prover_trace: trace.trace_step, + prover_next_hash, + }); } info!("Verifier choose to challenge TRACE_HASH"); - return Ok(ChallengeType::TraceHash( - step_hash, - trace.trace_step, - next_hash, - )); + return Ok(ChallengeType::TraceHash { + prover_step_hash, + prover_trace: trace.trace_step, + prover_next_hash, + }); } let step = conflict_step_log.step_to_challenge; @@ -362,10 +365,13 @@ pub fn verifier_choose_challenge( let register_sections = program.register_sections.clone(); let code_sections = program.code_sections.clone(); + let prover_read_1 = trace.read_1; + let prover_read_2 = trace.read_2; + let is_valid_read_1 = - program.is_valid_mem(trace.mem_witness.read_1(), trace.read_1.address, true); + program.is_valid_mem(trace.mem_witness.read_1(), prover_read_1.address, true); let is_valid_read_2 = - program.is_valid_mem(trace.mem_witness.read_2(), trace.read_2.address, true); + program.is_valid_mem(trace.mem_witness.read_2(), prover_read_2.address, true); let is_valid_write = program.is_valid_mem( trace.mem_witness.write(), @@ -380,17 +386,17 @@ pub fn verifier_choose_challenge( || force == ForceChallenge::AddressesSections { info!("Verifier choose to challenge invalid ADDRESS_SECTION"); - return Ok(ChallengeType::AddressesSections( - trace.read_1, - trace.read_2, - trace.trace_step.write_1, - trace.mem_witness, - trace.read_pc.pc, - return_script_parameters.then_some(read_write_sections), - return_script_parameters.then_some(read_only_sections), - return_script_parameters.then_some(register_sections), - return_script_parameters.then_some(code_sections), - )); + return Ok(ChallengeType::AddressesSections { + prover_read_1, + prover_read_2, + prover_write: trace.trace_step.write_1, + prover_witness: trace.mem_witness, + prover_pc: trace.read_pc.pc, + read_write_sections: return_script_parameters.then_some(read_write_sections), + read_only_sections: return_script_parameters.then_some(read_only_sections), + register_sections: return_script_parameters.then_some(register_sections), + code_sections: return_script_parameters.then_some(code_sections), + }); } // check entrypoint @@ -403,22 +409,22 @@ pub fn verifier_choose_challenge( { if trace.step_number == 1 { info!("Verifier choose to challenge ENTRYPOINT"); - return Ok(ChallengeType::EntryPoint( - trace.read_pc, - trace.step_number, - return_script_parameters.then_some(program.pc.get_address()), //this parameter is only used for the test - )); + return Ok(ChallengeType::EntryPoint { + prover_read_pc: trace.read_pc, + prover_trace_step: trace.step_number, + real_entry_point: return_script_parameters.then_some(program.pc.get_address()), //this parameter is only used for the test + }); } else { info!("Verifier choose to challenge PROGRAM_COUNTER"); - let pre_pre_hash = my_execution[0].1.clone(); + let pre_hash = my_execution[0].1.clone(); let pre_step = my_execution[1].0.clone(); - return Ok(ChallengeType::ProgramCounter( - pre_pre_hash, - pre_step.trace_step, - step_hash, - trace.read_pc, - )); + return Ok(ChallengeType::ProgramCounter { + pre_hash, + trace: pre_step.trace_step, + prover_step_hash, + prover_pc_read: trace.read_pc, + }); } } @@ -428,20 +434,20 @@ pub fn verifier_choose_challenge( info!("Verifier choose to challenge invalid OPCODE"); let pc = trace.read_pc.pc.get_address(); let code_chunks = program.get_code_chunks(CHUNK_SIZE); - let chunk_index = find_chunk_index(&code_chunks, pc).unwrap(); + let chunk_index = find_chunk_index(&code_chunks, pc).unwrap() as u32; - return Ok(ChallengeType::Opcode( - trace.read_pc, - chunk_index as u32, - return_script_parameters.then_some(code_chunks[chunk_index].clone()), - )); + return Ok(ChallengeType::Opcode { + prover_pc_read: trace.read_pc, + chunk_index, + chunk: return_script_parameters.then_some(code_chunks[chunk_index as usize].clone()), + }); } - let read_step_1 = trace.read_1.last_step; - let read_step_2 = trace.read_2.last_step; + let prover_read_step_1 = prover_read_1.last_step; + let prover_read_step_2 = prover_read_2.last_step; - let is_read_1_future = read_step_1 > step && read_step_1 != LAST_STEP_INIT; - let is_read_2_future = read_step_2 > step && read_step_2 != LAST_STEP_INIT; + let is_read_1_future = prover_read_step_1 > step && prover_read_step_1 != LAST_STEP_INIT; + let is_read_2_future = prover_read_step_2 > step && prover_read_step_2 != LAST_STEP_INIT; if ((is_read_1_future || is_read_2_future) && force == ForceChallenge::No) || force == ForceChallenge::FutureRead @@ -450,15 +456,15 @@ pub fn verifier_choose_challenge( return Ok(ChallengeType::FutureRead { step: step + 1, - read_step_1, - read_step_2, + prover_read_step_1, + prover_read_step_2, read_selector, }); } // check const read value - let is_read_1_conflict = trace.read_1.value != my_trace.read_1.value; - let is_read_2_conflict = trace.read_2.value != my_trace.read_2.value; + let is_read_1_conflict = prover_read_1.value != my_trace.read_1.value; + let is_read_2_conflict = prover_read_2.value != my_trace.read_2.value; if ((is_read_1_conflict || is_read_2_conflict) && force == ForceChallenge::No) || force == ForceChallenge::InputData @@ -467,9 +473,9 @@ pub fn verifier_choose_challenge( || force == ForceChallenge::ReadValueNArySearch { let (conflict_read, my_conflict_read, read_selector) = if is_read_1_conflict { - (trace.read_1.clone(), my_trace.read_1.clone(), 1) + (prover_read_1.clone(), my_trace.read_1.clone(), 1) } else { - (trace.read_2.clone(), my_trace.read_2.clone(), 2) + (prover_read_2.clone(), my_trace.read_2.clone(), 2) }; let conflict_address = conflict_read.address; @@ -499,38 +505,40 @@ pub fn verifier_choose_challenge( info!("Verifier choose to challenge invalid INPUT DATA"); let value = program.read_mem(conflict_address, false)?; - return Ok(ChallengeType::InputData( - trace.read_1, - trace.read_2, - conflict_address, - value, - )); + return Ok(ChallengeType::InputData { + prover_read_1: prover_read_1, + prover_read_2: prover_read_2, + address: conflict_address, + input_for_address: value, + }); } else if (section.initialized && force == ForceChallenge::No) || force == ForceChallenge::InitializedData { info!("Verifier choose to challenge invalid INITIALIZED DATA"); let initialized_chunks = program.get_initialized_chunks(CHUNK_SIZE); - let chunk_index = find_chunk_index(&initialized_chunks, conflict_address).unwrap(); + let chunk_index = + find_chunk_index(&initialized_chunks, conflict_address).unwrap() as u32; - return Ok(ChallengeType::InitializedData( - trace.read_1, - trace.read_2, + return Ok(ChallengeType::InitializedData { + prover_read_1, + prover_read_2, read_selector, - chunk_index as u32, - return_script_parameters.then_some(initialized_chunks[chunk_index].clone()), - )); + chunk_index, + chunk: return_script_parameters + .then_some(initialized_chunks[chunk_index as usize].clone()), + }); } else if (!section.initialized && force == ForceChallenge::No) || force == ForceChallenge::UninitializedData { info!("Verifier choose to challenge invalid UNINITIALIZED DATA"); - let uninitilized_ranges = program.get_uninitialized_ranges(&program_def); + let uninitilized_sections = program.get_uninitialized_ranges(&program_def); - return Ok(ChallengeType::UninitializedData( - trace.read_1, - trace.read_2, + return Ok(ChallengeType::UninitializedData { + prover_read_1, + prover_read_2, read_selector, - return_script_parameters.then_some(uninitilized_ranges), - )); + sections: return_script_parameters.then_some(uninitilized_sections), + }); } } else { let step_to_challenge = if conflict_last_step == LAST_STEP_INIT { @@ -560,7 +568,7 @@ pub fn verifier_choose_challenge( verifier_log.read_selector = read_selector; verifier_log.save(checkpoint_path)?; - return Ok(ChallengeType::ReadValueNArySearch(bits)); + return Ok(ChallengeType::ReadValueNArySearch { bits }); } } verifier_log.save(checkpoint_path)?; @@ -581,7 +589,7 @@ pub fn verifier_choose_challenge_for_read_challenge( let conflict_step = verifier_log.conflict_step_log.step_to_challenge; let challenge_step = read_challenge_log.step_to_challenge; - let (step_hash, next_hash) = get_hashes( + let (prover_step_hash, prover_next_hash) = get_hashes( &nary_def.step_mapping(&read_challenge_log.verifier_decisions), &read_challenge_log.prover_hash_rounds, challenge_step, @@ -593,7 +601,7 @@ pub fn verifier_choose_challenge_for_read_challenge( challenge_step, ); - assert_eq!(next_hash, my_next_hash); + assert_eq!(prover_next_hash, my_next_hash); let my_execution = program_def .execute_helper( @@ -607,14 +615,14 @@ pub fn verifier_choose_challenge_for_read_challenge( info!("execution: {:?}", my_execution); let my_trace = my_execution[0].0.clone(); - if (step_hash != my_step_hash && force == ForceChallenge::No) + if (prover_step_hash != my_step_hash && force == ForceChallenge::No) || force == ForceChallenge::CorrectHash { return Ok(ChallengeType::CorrectHash { - prover_hash: step_hash, + prover_step_hash, verifier_hash: my_step_hash, trace: my_trace.trace_step, - next_hash, + prover_next_hash, }); } @@ -623,17 +631,17 @@ pub fn verifier_choose_challenge_for_read_challenge( || force == ForceChallenge::ReadValue { let conflict_step_trace = verifier_log.conflict_step_log.final_trace; - let read_1 = conflict_step_trace.read_1; - let read_2 = conflict_step_trace.read_2; + let prover_read_1 = conflict_step_trace.read_1; + let prover_read_2 = conflict_step_trace.read_2; let read_selector = verifier_log.read_selector; return Ok(ChallengeType::ReadValue { - read_1, - read_2, + prover_read_1, + prover_read_2, read_selector, - step_hash, + prover_hash: prover_step_hash, trace: my_trace.trace_step, - next_hash, + prover_next_hash, write_step: challenge_step + 1, conflict_step: conflict_step + 1, }); @@ -845,7 +853,7 @@ mod tests { .unwrap(); let challenge = match &challenge { - ChallengeType::ReadValueNArySearch(bits) => { + ChallengeType::ReadValueNArySearch { bits } => { let mut v_decision = *bits; for round in 2..nary_def.total_rounds() + 1 { let hashes = prover_get_hashes_for_round( From 8f76917bea4ac7b8cc35d6240367ea99175f249e Mon Sep 17 00:00:00 2001 From: crivasr Date: Tue, 4 Nov 2025 13:20:56 -0300 Subject: [PATCH 02/22] add bitcoin scripts for translation keys --- bitcoin-script-riscv/src/riscv/challenges.rs | 322 ++++++++++++++++-- .../src/riscv/instructions_load.rs | 11 +- .../src/riscv/memory_alignment.rs | 5 +- .../src/riscv/script_utils.rs | 248 +++++++++++++- 4 files changed, 549 insertions(+), 37 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 4e47836..868097a 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -1,7 +1,7 @@ use bitcoin_script_functions::hash::blake3; use bitcoin_script_stack::stack::{StackTracker, StackVariable}; use bitvmx_cpu_definitions::{ - challenge::ChallengeType, + challenge::{ChallengeType, EquivocationKind}, constants::LAST_STEP_INIT, memory::{Chunk, MemoryAccessType, SectionDefinition}, trace::{generate_initial_step_hash, hashvec_to_string}, @@ -11,7 +11,8 @@ use crate::riscv::{ memory_alignment::{is_aligned, load_lower_half_nibble_table, load_upper_half_nibble_table}, script_utils::{ address_in_sections, address_not_in_sections, get_selected_vars, is_lower_than, - verify_wrong_chunk_value, witness_equals, StackTables, + next_decision, var_to_decisions_in_stack, verify_wrong_chunk_value, witness_equals, + StackTables, }, }; @@ -493,7 +494,7 @@ pub fn uninitialized_challenge( stack.drop(read_addr); } -pub fn read_value_challenge(stack: &mut StackTracker) { +pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary_last_round: u8) { stack.clear_definitions(); let read_addr_1 = stack.define(8, "prover_read_addr_1"); @@ -515,23 +516,42 @@ pub fn read_value_challenge(stack: &mut StackTracker) { let next_hash = stack.define(40, "next_hash"); - let write_step = stack.define(16, "write_step"); - let conflict_step = stack.define(16, "conflict_step"); + let write_bits = stack.define(rounds as u32, "write_bits"); + let conflict_bits = stack.define(rounds as u32, "conflict_bits"); - let write_step_copy = stack.copy_var(write_step); - is_lower_than(stack, write_step_copy, conflict_step, true); + let write_bits_copy = stack.copy_var(write_bits); + is_lower_than(stack, write_bits_copy, conflict_bits, true); stack.op_verify(); + let max_nary = nary - 1; + let max_last_round = if nary_last_round == 0 { + max_nary + } else { + nary_last_round - 1 + }; + + stack.explode(write_bits); + next_decision(stack, rounds, max_last_round, max_nary); + let write_bits = stack.from_altstack_joined(rounds as u32, "next_write_bits"); + let [read_addr, read_value, read_step] = get_selected_vars( stack, [read_addr_1, read_value_1, read_step_1], [read_addr_2, read_value_2, read_step_2], read_selector, ); + stack.rename(read_addr, "read_addr"); - // if read_step == write_step -> write_addr != read_addr || write_value != read_value - stack.equality(read_step, false, write_step, false, true, false); + let read_step_copy = stack.copy_var(read_step); + + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + var_to_decisions_in_stack(stack, tables, read_step, nary_last_round, nary, rounds); + tables.drop(stack); + let read_step_bits = stack.from_altstack_joined(rounds as u32, "read_step"); + // if read_step == write_step -> write_addr != read_addr || write_value != read_value + stack.equality(read_step_bits, false, write_bits, false, true, false); + stack.set_breakpoint("MY AWESOME BREAK POINT"); stack.equality(write_addr, false, read_addr, false, false, false); stack.equality(write_value, false, read_value, true, false, false); stack.op_boolor(); @@ -540,8 +560,8 @@ pub fn read_value_challenge(stack: &mut StackTracker) { let init = stack.number_u64(LAST_STEP_INIT); // if read_step == INIT || read_step < write_step -> write_addr == read_addr - stack.equality(read_step, false, init, true, true, false); - is_lower_than(stack, read_step, write_step, true); + stack.equality(read_step_copy, true, init, true, true, false); + is_lower_than(stack, read_step_bits, write_bits, true); stack.op_boolor(); stack.equality(write_addr, false, read_addr, true, true, false); @@ -594,8 +614,10 @@ pub fn correct_hash_challenge(stack: &mut StackTracker) { stack.equals(result, true, next_hash, true); } -pub fn future_read_challenge(stack: &mut StackTracker) { - let step = stack.define(16, "prover_step"); +pub fn future_read_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary_last_round: u8) { + stack.clear_definitions(); + + let step = stack.define(rounds as u32, "prover_step"); let read_step_1 = stack.define(16, "prover_read_step_1"); let read_step_2 = stack.define(16, "prover_read_step_2"); @@ -607,11 +629,87 @@ pub fn future_read_challenge(stack: &mut StackTracker) { let init = stack.number_u64(LAST_STEP_INIT); stack.equality(read_step, false, init, true, false, true); + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + var_to_decisions_in_stack(stack, tables, read_step, nary_last_round, nary, rounds); + tables.drop(stack); + let read_step = stack.from_altstack_joined(rounds as u32, "read_step"); + is_lower_than(stack, read_step, step, true); stack.op_not(); stack.op_verify(); } +fn get_round_and_index(stack: &mut StackTracker, decisions_bits: StackVariable, round: u8) { + if round == 0 { + stack.number(0); + stack.number(0); + } else { + stack.copy_var_sub_n(decisions_bits, round as u32 - 1); + stack.number(0); + stack.op_equal(); + + let (mut if_true, mut if_false) = stack.open_if_with_debug(); + + if_false.number(round as u32); + if_false.copy_var_sub_n(decisions_bits, round as u32 - 1); + + get_round_and_index(&mut if_true, decisions_bits, round - 1); + + stack.end_if( + if_true, + if_false, + 0, + vec![ + (1, "calculated_round".to_string()), + (1, "calculated_index".to_string()), + ], + 0, + ); + } +} + +pub fn equivocation_hash_challenge( + stack: &mut StackTracker, + rounds: u8, + kind: EquivocationKind, + expected_round: u8, + expected_index: u8, + nary: u8, + nary_last_round: u8, +) { + stack.clear_definitions(); + + let true_hash = stack.define(40, "prover_true_hash"); + let wrong_hash = stack.define(40, "prover_wrong_hash"); + let mut decisions_bits = stack.define(rounds as u32, "decisions_bits"); + + if kind == EquivocationKind::NextHash { + stack.explode(decisions_bits); + let max_nary = nary - 1; + let max_last_round = if nary_last_round == 0 { + max_nary + } else { + nary_last_round - 1 + }; + + next_decision(stack, rounds, max_last_round, max_nary); + decisions_bits = stack.from_altstack_joined(rounds as u32, "next_decisions_bits"); + } + + get_round_and_index(stack, decisions_bits, rounds); + + stack.number(expected_index as u32); + stack.op_equalverify(); + + stack.number(expected_round as u32); + stack.op_equalverify(); + + stack.equality(true_hash, true, wrong_hash, true, false, true); + + stack.drop(decisions_bits); +} + //TODO: memory section challenge //TODO: program crash challenge - this might be more about finding the right place to challenge that a challenge itself @@ -774,17 +872,27 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { ); } ChallengeType::FutureRead { - step, + cosigned_decisions_bits, prover_read_step_1, prover_read_step_2, read_selector, + nary, + nary_last_round, } => { - stack.number_u64(*step); + for bit in cosigned_decisions_bits { + stack.number(*bit); + } + stack.number_u64(*prover_read_step_1); stack.number_u64(*prover_read_step_2); stack.number(*read_selector); - future_read_challenge(&mut stack); + future_read_challenge( + &mut stack, + cosigned_decisions_bits.len() as u8, + nary.unwrap(), + nary_last_round.unwrap(), + ); } ChallengeType::ReadValue { prover_read_1, @@ -793,8 +901,10 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { prover_hash, trace, prover_next_hash, - write_step, - conflict_step, + cosigned_read_bits, + cosigned_conflict_bits, + nary, + nary_last_round, } => { stack.number_u32(prover_read_1.address); stack.number_u32(prover_read_1.value); @@ -815,18 +925,28 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { stack.hexstr_as_nibbles(prover_next_hash); - stack.number_u64(*write_step); - stack.number_u64(*conflict_step); + for bits in cosigned_read_bits { + stack.number(*bits); + } + + for bits in cosigned_conflict_bits { + stack.number(*bits); + } - read_value_challenge(&mut stack); + read_value_challenge( + &mut stack, + cosigned_read_bits.len() as u8, + nary.unwrap(), + nary_last_round.unwrap(), + ); } ChallengeType::CorrectHash { - prover_step_hash: prover_hash, + prover_step_hash, verifier_hash, trace, prover_next_hash, } => { - stack.hexstr_as_nibbles(prover_hash); + stack.hexstr_as_nibbles(prover_step_hash); stack.hexstr_as_nibbles(verifier_hash); stack.number_u32(trace.get_write().address); @@ -838,6 +958,33 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { correct_hash_challenge(&mut stack); } + ChallengeType::EquivocationHash { + prover_true_hash, + prover_wrong_hash, + cosigned_decisions_bits, + kind, + round, + index, + nary, + nary_last_round, + } => { + stack.hexstr_as_nibbles(prover_true_hash); + stack.hexstr_as_nibbles(prover_wrong_hash); + + for bits in cosigned_decisions_bits { + stack.number(*bits); + } + + equivocation_hash_challenge( + &mut stack, + cosigned_decisions_bits.len() as u8, + kind.clone(), + *round, + *index, + nary.unwrap(), + nary_last_round.unwrap(), + ); + } _ => { return false; } @@ -1878,12 +2025,23 @@ mod tests { fn test_future_read_aux(step: u64, read_step: u64) -> bool { let stack = &mut StackTracker::new(); - stack.number_u64(step); + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + let rounds = 4; + let nary = 8; + let nary_last_round = 4; + + let step = stack.number_u64(step); + var_to_decisions_in_stack(stack, tables, step, nary_last_round, nary, rounds); + tables.drop(stack); + + stack.from_altstack_joined(rounds as u32, "step"); + stack.number_u64(read_step); // read_1_step stack.number_u64(read_step); // read_2_step stack.number(1); // read_selector - future_read_challenge(stack); + future_read_challenge(stack, rounds, nary, nary_last_round); stack.op_true(); stack.run().success @@ -1934,10 +2092,24 @@ mod tests { stack.hexstr_as_nibbles(&next_hash); - stack.number_u64(write_step); - stack.number_u64(conflict_step); + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + let write_step = stack.number_u64(write_step - 1); + let conflict_step = stack.number_u64(conflict_step - 1); + + let rounds = 4; + let nary = 8; + let nary_last_round = 4; + + var_to_decisions_in_stack(stack, tables, conflict_step, nary_last_round, nary, rounds); + var_to_decisions_in_stack(stack, tables, write_step, nary_last_round, nary, rounds); + + tables.drop(stack); - read_value_challenge(stack); + stack.from_altstack_joined(rounds as u32, "write_bits"); + stack.from_altstack_joined(rounds as u32, "conflict_bits"); + + read_value_challenge(stack, rounds, nary, nary_last_round); stack.op_true(); stack.run().success @@ -2060,6 +2232,100 @@ mod tests { )); } + fn test_equivocation_hash_aux( + hash1: &String, + hash2: &String, + decisions_bits: &Vec, + kind: EquivocationKind, + round: u8, + index: u8, + nary: u8, + nary_last_round: u8, + ) -> bool { + let stack = &mut StackTracker::new(); + stack.hexstr_as_nibbles(hash1); + stack.hexstr_as_nibbles(hash2); + + for decision in decisions_bits { + stack.number(*decision); + } + + equivocation_hash_challenge( + stack, + decisions_bits.len() as u8, + kind, + round, + index, + nary, + nary_last_round, + ); + + stack.op_true(); + + stack.run().success + } + + #[test] + pub fn test_equivocation_hash() { + let true_hash = &"e2f115006467b4b1b2b27612bbfd40ed3bc8299b".to_string(); + let wrong_hash = &"345721506e79c53d2549fc63d02ba8fc3b17efa4".to_string(); + + let nary = 8; + let nary_last_round = 4; + + // this decisions bits selects the hash at round 4 and index 3 + // the next hash was given at round 2 index 1 + let decisions_bits = &vec![4, 0, 7, 3]; + + // can't challenge true hash + assert!(!test_equivocation_hash_aux( + true_hash, + true_hash, + decisions_bits, + EquivocationKind::StepHash, + 4, + 3, + nary, + nary_last_round, + )); + + // can't challenge with other step hash + // this hash was at round 2 index 3 + assert!(!test_equivocation_hash_aux( + true_hash, + wrong_hash, + decisions_bits, + EquivocationKind::StepHash, + 2, // round + 3, // index + nary, + nary_last_round, + )); + + // can challenge if wrong hash and correct round and index + assert!(test_equivocation_hash_aux( + true_hash, + wrong_hash, + decisions_bits, + EquivocationKind::StepHash, + 4, + 3, + nary, + nary_last_round, + )); + + // can challenge if wrong hash and correct round and index for next hash + assert!(test_equivocation_hash_aux( + true_hash, + wrong_hash, + decisions_bits, + EquivocationKind::NextHash, + 2, + 1, + nary, + nary_last_round, + )); + } mod coin_tests { use super::*; use ::blake3::Hasher; diff --git a/bitcoin-script-riscv/src/riscv/instructions_load.rs b/bitcoin-script-riscv/src/riscv/instructions_load.rs index f57847d..4d746d5 100644 --- a/bitcoin-script-riscv/src/riscv/instructions_load.rs +++ b/bitcoin-script-riscv/src/riscv/instructions_load.rs @@ -1,4 +1,4 @@ -use bitcoin_script_stack::{stack::{StackTracker, StackVariable}}; +use bitcoin_script_stack::stack::{StackTracker, StackVariable}; use bitvmx_cpu_definitions::memory::{MemoryAccessType, MemoryWitness}; use riscv_decode::Instruction::{self, *}; @@ -130,7 +130,7 @@ pub fn op_load_micro_0_missaligned( MemoryAccessType::Register, ), ); - + let (nibs, max_extra, prepad, unsigned) = match instruction { Lb(_) => (2, 0, 6, false), Lh(_) => (4, 2, 4, false), @@ -240,7 +240,7 @@ pub fn op_load_micro_0_aligned( let write_addr = number_u32_partial(&mut if_false, base_register_address, 6); if_false.move_var(rd); if_false.join(write_addr); - + //calculate the byte to be stored (with proper bit extension) if_false.move_var(trace_read.read_2_value); if_false.move_var(alignment); //restore alignment @@ -259,10 +259,7 @@ pub fn op_load_micro_0_aligned( if_is_register_zero, if_false, 4, - vec![ - (8, "write_add".to_string()), - (8, "write_value".to_string()), - ], + vec![(8, "write_add".to_string()), (8, "write_value".to_string())], 0, ); diff --git a/bitcoin-script-riscv/src/riscv/memory_alignment.rs b/bitcoin-script-riscv/src/riscv/memory_alignment.rs index 3163dba..d5e5f23 100644 --- a/bitcoin-script-riscv/src/riscv/memory_alignment.rs +++ b/bitcoin-script-riscv/src/riscv/memory_alignment.rs @@ -61,7 +61,10 @@ pub fn verify_alignment(stack: &mut StackTracker, mem_address: StackVariable) { stack.drop(lower_half_nibble_table); } -pub fn clear_least_significant_bit(stack: &mut StackTracker, mem_address: StackVariable) -> StackVariable { +pub fn clear_least_significant_bit( + stack: &mut StackTracker, + mem_address: StackVariable, +) -> StackVariable { let parts = stack.explode(mem_address); let table = load_clear_lsb_table(stack); diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index ce71125..a1f6b7d 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1498,7 +1498,7 @@ pub fn verify_wrong_chunk_value( ) { address_in_range(stack, &chunk.range(), &address); stack.op_verify(); - + let chunk_table = WordTable::new(stack, chunk.data.clone()); let base_addr = stack.number_u32(chunk.base_addr); @@ -1613,6 +1613,160 @@ pub fn var_to_number(stack: &mut StackTracker, var: StackVariable) -> StackVaria result } +pub fn shift(stack: &mut StackTracker, tables: &StackShiftTables, amount: u8) { + if amount == 0 { + return; + } + + let tables = [tables.shift_1, tables.shift_2, tables.shift_3]; + + if amount > 3 { + stack.op_drop(); + stack.number(0); + } else { + stack.get_value_from_table(tables[amount as usize - 1], None); + } +} + +const BITS_NIBBLE: u8 = 4; + +pub fn split(stack: &mut StackTracker, tables: &StackTables, right_size: u8) { + stack.op_dup(); + + shift(stack, &tables.rshift, right_size); + stack.op_swap(); + shift(stack, &tables.lshift, BITS_NIBBLE - right_size); + shift(stack, &tables.rshift, BITS_NIBBLE - right_size); +} + +pub fn var_to_decisions_in_stack( + stack: &mut StackTracker, + tables: &StackTables, + var: StackVariable, + nary_last_round: u8, + nary: u8, + rounds: u8, +) { + let bits_nary_round = f64::log2(nary as f64) as u8; + let mut start_position = 0; + let mut remaining_bits = if nary_last_round == 0 { + bits_nary_round + } else { + f64::log2(nary_last_round as f64) as u8 + }; + + stack.move_var(var); + stack.explode(var); + stack.number(0); + let nibbles_needed = + (remaining_bits + bits_nary_round * (rounds - 1) + BITS_NIBBLE - 1) / BITS_NIBBLE; + + for _ in 0..nibbles_needed { + if remaining_bits > BITS_NIBBLE { + stack.op_swap(); + shift(stack, &tables.lshift, start_position); + stack.op_add(); + start_position += BITS_NIBBLE; + remaining_bits -= BITS_NIBBLE; + } else if remaining_bits == BITS_NIBBLE { + stack.op_swap(); + shift(stack, &tables.lshift, start_position); + stack.op_add(); + start_position = 0; + remaining_bits = bits_nary_round; + stack.to_altstack(); + stack.number(0); + } else { + let times_needed = + 1 + (BITS_NIBBLE - remaining_bits + bits_nary_round - 1) / bits_nary_round; + let mut current_bits = BITS_NIBBLE; + for _ in 0..times_needed { + stack.op_swap(); + split(stack, tables, remaining_bits); + shift(stack, &tables.lshift, start_position); + stack.op_rot(); + stack.op_add(); + + if remaining_bits <= current_bits { + current_bits -= remaining_bits; + remaining_bits = bits_nary_round; + start_position = 0; + stack.to_altstack(); + stack.number(0); + } else { + remaining_bits -= current_bits; + start_position += current_bits; + } + } + stack.op_swap(); + stack.number(0); + stack.op_equalverify(); + } + } + + stack.number(0); + stack.op_equalverify(); + + for _ in 0..(16 - nibbles_needed) { + stack.number(0); + stack.op_equalverify(); + } +} + +pub fn next_decision(stack: &mut StackTracker, rounds: u8, max_last_round: u8, max_nary: u8) { + stack.op_dup(); + stack.number(max_last_round as u32); + stack.op_equal(); + + let (mut overflow, mut no_overflow) = stack.open_if(); + no_overflow.op_1add(); + no_overflow.to_altstack(); + no_overflow.number(0); + no_overflow.to_altstack(); + + overflow.op_drop(); + overflow.number(0); + overflow.to_altstack(); + overflow.number(1); + overflow.to_altstack(); + + stack.end_if(overflow, no_overflow, 1, vec![], 2); + + for _ in 1..rounds - 1 { + stack.from_altstack(); + stack.number(1); + stack.op_equal(); + + let (mut inc, mut no_inc) = stack.open_if(); + no_inc.to_altstack(); + no_inc.number(0); + no_inc.to_altstack(); + + inc.op_dup(); + inc.number(max_nary as u32); + inc.op_equal(); + + let (mut overflow, mut no_overflow) = inc.open_if(); + no_overflow.op_1add(); + no_overflow.to_altstack(); + no_overflow.number(0); + no_overflow.to_altstack(); + + overflow.op_drop(); + overflow.number(0); + overflow.to_altstack(); + overflow.number(1); + overflow.to_altstack(); + + inc.end_if(overflow, no_overflow, 1, vec![], 2); + stack.end_if(inc, no_inc, 1, vec![], 2); + } + + stack.from_altstack(); + stack.op_add(); + stack.to_altstack(); +} + #[cfg(test)] mod tests { use bitvmx_cpu_definitions::memory::MemoryWitness; @@ -2477,6 +2631,98 @@ mod tests { )); } + fn test_var_to_decisions_in_stack_aux( + decisions: &[u32], + step: u64, + nary_last_round: u8, + nary: u8, + ) { + let stack = &mut StackTracker::new(); + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + for decision in decisions.iter().rev() { + stack.number(*decision); + } + let var = stack.number_u64(step); + var_to_decisions_in_stack( + stack, + tables, + var, + nary_last_round, + nary, + decisions.len() as u8, + ); + + for _ in 0..decisions.len() { + stack.from_altstack(); + stack.op_equalverify(); + } + + tables.drop(stack); + stack.op_true(); + assert!(stack.run().success); + } + + #[test] + fn test_var_to_decisions_in_stack() { + test_var_to_decisions_in_stack_aux(&[4, 2, 4, 0], 1104, 4, 8); + test_var_to_decisions_in_stack_aux(&[4, 2, 4, 1], 1105, 4, 8); + test_var_to_decisions_in_stack_aux(&[4, 2, 4, 2], 1106, 4, 8); + test_var_to_decisions_in_stack_aux(&[4, 2, 4, 3], 1107, 4, 8); + + test_var_to_decisions_in_stack_aux(&[0, 3, 0, 3], 99, 4, 8); + test_var_to_decisions_in_stack_aux(&[1, 3, 0, 3], 355, 4, 8); + } + + fn test_next_decision_aux(decision: u64, rounds: u8, nary: u8, nary_last_round: u8) { + let stack = &mut StackTracker::new(); + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + let max_nary = nary - 1; + let max_last_round = if nary_last_round == 0 { + max_nary + } else { + nary_last_round - 1 + }; + + let decision_var = stack.number_u64(decision); + let next_decision_var = stack.number_u64(decision + 1); + + var_to_decisions_in_stack( + stack, + tables, + next_decision_var, + nary_last_round, + nary, + rounds, + ); + + var_to_decisions_in_stack(stack, tables, decision_var, nary_last_round, nary, rounds); + + let decision = stack.from_altstack_joined(rounds as u32, "decision_bits"); + + stack.explode(decision); + next_decision(stack, rounds, max_last_round, max_nary); + let next_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); + + let expected_next_decision_bits = + stack.from_altstack_joined(rounds as u32, "expected_decision_bits"); + + stack.equals(next_decision_bits, true, expected_next_decision_bits, true); + + tables.drop(stack); + stack.op_true(); + + assert!(stack.run().success); + } + #[test] + fn test_next_decision() { + test_next_decision_aux(100, 4, 8, 4); + test_next_decision_aux(302, 8, 8, 2); + test_next_decision_aux(38, 8, 2, 2); + test_next_decision_aux(892, 4, 8, 0); + } + mod fuzz_tests { use super::*; use rand::Rng; From fb5320396a0d01fbdf693826896b68c25d8eba23 Mon Sep 17 00:00:00 2001 From: crivasr Date: Tue, 4 Nov 2025 13:22:03 -0300 Subject: [PATCH 03/22] add key translation --- definitions/Cargo.toml | 2 + definitions/src/challenge.rs | 71 ++++- emulator/src/decision/challenge.rs | 330 +++++++++++++++++++-- emulator/src/decision/nary_search.rs | 25 ++ emulator/src/executor/fetcher.rs | 9 +- emulator/src/executor/utils.rs | 7 + emulator/src/main.rs | 104 ++++++- emulator/tests/test_r_type_instructions.rs | 1 - emulator/tests/test_s_type_instructions.rs | 10 +- 9 files changed, 518 insertions(+), 41 deletions(-) diff --git a/definitions/Cargo.toml b/definitions/Cargo.toml index 897d169..f4dc613 100644 --- a/definitions/Cargo.toml +++ b/definitions/Cargo.toml @@ -10,3 +10,5 @@ thiserror = "2.0.12" hex = "0.4.3" blake3 = "1.6.1" serde_json = "1.0.104" +strum = "0.27" +strum_macros = "0.27" diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index ea1f0d2..6ef2459 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use strum_macros::{EnumIter, EnumString}; use thiserror::Error; use crate::{ @@ -70,10 +71,12 @@ pub enum ChallengeType { code_sections: Option, }, FutureRead { - step: u64, + cosigned_decisions_bits: Vec, prover_read_step_1: u64, prover_read_step_2: u64, read_selector: u32, + nary: Option, + nary_last_round: Option, }, ReadValueNArySearch { bits: u32, @@ -85,8 +88,10 @@ pub enum ChallengeType { prover_hash: String, trace: TraceStep, prover_next_hash: String, - write_step: u64, - conflict_step: u64, + cosigned_read_bits: Vec, + cosigned_conflict_bits: Vec, + nary: Option, + nary_last_round: Option, }, CorrectHash { prover_step_hash: String, @@ -94,9 +99,26 @@ pub enum ChallengeType { trace: TraceStep, prover_next_hash: String, }, + EquivocationHash { + prover_true_hash: String, + prover_wrong_hash: String, + cosigned_decisions_bits: Vec, + kind: EquivocationKind, + round: u8, + index: u8, + nary: Option, + nary_last_round: Option, + }, No, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, EnumIter, Default)] +pub enum EquivocationKind { + #[default] + StepHash, + NextHash, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "data")] pub enum EmulatorResultType { @@ -119,6 +141,14 @@ pub enum EmulatorResultType { }, ProverFinalTraceResult { final_trace: TraceRWStep, + resigned_step_hash: String, + resigned_next_hash: String, + cosigned_decision_bits: Vec, + }, + ProverGetCosignedBitsAndHashesResult { + resigned_step_hash: String, + resigned_next_hash: String, + cosigned_decision_bits: Vec, }, VerifierChooseChallengeResult { challenge: ChallengeType, @@ -188,15 +218,46 @@ impl EmulatorResultType { } } - pub fn as_final_trace(&self) -> Result { + pub fn as_final_trace( + &self, + ) -> Result<(TraceRWStep, String, String, Vec), EmulatorResultError> { match self { - EmulatorResultType::ProverFinalTraceResult { final_trace } => Ok(final_trace.clone()), + EmulatorResultType::ProverFinalTraceResult { + final_trace, + resigned_step_hash, + resigned_next_hash, + cosigned_decision_bits, + } => Ok(( + final_trace.clone(), + resigned_step_hash.to_string(), + resigned_next_hash.to_string(), + cosigned_decision_bits.clone(), + )), _ => Err(EmulatorResultError::GenericError( "Expected ProverFinalTraceResult".to_string(), )), } } + pub fn as_cosigned_bits_and_hashes( + &self, + ) -> Result<(String, String, Vec), EmulatorResultError> { + match self { + EmulatorResultType::ProverGetCosignedBitsAndHashesResult { + resigned_step_hash, + resigned_next_hash, + cosigned_decision_bits, + } => Ok(( + resigned_step_hash.to_string(), + resigned_next_hash.to_string(), + cosigned_decision_bits.clone(), + )), + _ => Err(EmulatorResultError::GenericError( + "Expected ProverGetCosignedBitsAndHashesResult".to_string(), + )), + } + } + pub fn as_challenge(&self) -> Result { match self { EmulatorResultType::VerifierChooseChallengeResult { challenge } => { diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index d7a51e4..8327e11 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -1,5 +1,5 @@ use bitvmx_cpu_definitions::{ - challenge::ChallengeType, + challenge::{ChallengeType, EquivocationKind}, constants::{CHUNK_SIZE, LAST_STEP_INIT}, memory::Chunk, trace::{generate_initial_step_hash, hashvec_to_string, validate_step_hash, TraceRWStep}, @@ -89,7 +89,17 @@ pub fn prover_get_hashes_for_round( fail_config, )?; nary_log.hash_rounds.push(hashes.clone()); - nary_log.verifier_decisions.push(verifier_decision); + // at the first round the verifier hasn't decided anything yet + if round > 1 { + nary_log.verifier_decisions.push(verifier_decision) + }; + // the hashes of the first round would be reused in the next nary-search + if round == 1 && nary_type == NArySearchType::ConflictStep { + challenge_log + .read_challenge_log + .hash_rounds + .push(hashes.clone()); + } challenge_log.save(checkpoint_path)?; Ok(hashes) } @@ -206,7 +216,7 @@ pub fn prover_final_trace( checkpoint_path: &str, final_bits: u32, fail_config: Option, -) -> Result { +) -> Result<(TraceRWStep, String, String, Vec), EmulatorError> { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; let input = challenge_log.input.clone(); let nary_log = challenge_log.get_nary_log(NArySearchType::ConflictStep); @@ -218,14 +228,65 @@ pub fn prover_final_trace( let final_step = nary_def.step_from_base_and_bits(total_rounds, nary_log.base_step, final_bits); nary_log.base_step = final_step; - nary_log.verifier_decisions.push(final_bits); + nary_log.verifier_decisions.push(final_bits - 1); info!("The prover needs to provide the full trace for the selected step {final_step}"); let final_trace = - program_def.get_trace_step(checkpoint_path, input, final_step, fail_config)?; + program_def.get_trace_step(checkpoint_path, input, final_step, fail_config.clone())?; nary_log.final_trace = final_trace.clone(); challenge_log.save(checkpoint_path)?; - Ok(final_trace) + + let (step_hash, next_hash, cosigned_bits) = prover_get_cosigned_bits_and_hashes( + program_definition_file, + checkpoint_path, + NArySearchType::ConflictStep, + None, + fail_config, + )?; + + Ok((final_trace, step_hash, next_hash, cosigned_bits)) +} + +pub fn prover_get_cosigned_bits_and_hashes( + program_definition_file: &str, + checkpoint_path: &str, + nary_type: NArySearchType, + final_bits: Option, + fail_config: Option, +) -> Result<(String, String, Vec), EmulatorError> { + let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; + let nary_log = challenge_log.get_nary_log(nary_type); + + let program_def = ProgramDefinition::from_config(program_definition_file)?; + let nary_def = program_def.nary_def(); + + let total_rounds = nary_def.total_rounds(); + + let final_step = match nary_type { + NArySearchType::ConflictStep => nary_log.base_step - 1, + _ => { + let final_bits = final_bits.unwrap(); + nary_log.verifier_decisions.push(final_bits); + nary_def.step_from_base_and_bits(total_rounds, nary_log.base_step, final_bits) + } + }; + + let (mut step_hash, mut next_hash) = get_hashes( + &nary_def.step_mapping(&nary_log.verifier_decisions), + &nary_log.hash_rounds, + final_step, + ); + + if let Some(step) = fail_config.unwrap_or_default().fail_resign_hash { + if step == final_step { + step_hash = next_hash.clone(); + } else if step == final_step + 1 { + next_hash = step_hash.clone(); + } + } + + let cosigned_bits = nary_log.verifier_decisions.clone(); + Ok((step_hash, next_hash, cosigned_bits)) } pub fn get_hashes( @@ -255,6 +316,7 @@ pub fn get_hashes( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ForceChallenge { + EquivocationHash(EquivocationKind), CorrectHash, TraceHash, TraceHashZero, @@ -291,6 +353,8 @@ pub fn verifier_choose_challenge( program_definition_file: &str, checkpoint_path: &str, trace: TraceRWStep, + resigned_step_hash: &str, + resigned_next_hash: &str, force: ForceChallenge, fail_config: Option, return_script_parameters: bool, @@ -309,11 +373,48 @@ pub fn verifier_choose_challenge( false, )?; - let (prover_step_hash, prover_next_hash) = get_hashes( - &nary_def.step_mapping(&conflict_step_log.verifier_decisions), - &conflict_step_log.prover_hash_rounds, - conflict_step_log.step_to_challenge, - ); + let mapping = &nary_def.step_mapping(&conflict_step_log.verifier_decisions); + let step = conflict_step_log.step_to_challenge; + + let (prover_step_hash, prover_next_hash) = + get_hashes(mapping, &conflict_step_log.prover_hash_rounds, step); + + let nary = return_script_parameters.then_some(nary_def.nary); + let nary_last_round = return_script_parameters.then_some(nary_def.nary_last_round); + + let cosigned_decisions_bits = conflict_step_log.verifier_decisions.clone(); + if (prover_step_hash != resigned_step_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationHash(EquivocationKind::StepHash) + { + let (round, index) = *mapping.get(&step).unwrap(); + + return Ok(ChallengeType::EquivocationHash { + prover_true_hash: prover_step_hash, + prover_wrong_hash: resigned_step_hash.to_string(), + cosigned_decisions_bits, + kind: EquivocationKind::StepHash, + round, + index: index + 1, + nary, + nary_last_round, + }); + } + + if (prover_next_hash != resigned_next_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationHash(EquivocationKind::NextHash) + { + let (round, index) = *mapping.get(&(step + 1)).unwrap(); + return Ok(ChallengeType::EquivocationHash { + prover_true_hash: prover_next_hash, + prover_wrong_hash: resigned_next_hash.to_string(), + cosigned_decisions_bits, + kind: EquivocationKind::NextHash, + round, + index: index+1, + nary, + nary_last_round, + }); + } // check trace_hash if (!validate_step_hash(&prover_step_hash, &trace.trace_step, &prover_next_hash) @@ -337,7 +438,6 @@ pub fn verifier_choose_challenge( }); } - let step = conflict_step_log.step_to_challenge; let mut steps = vec![step, step + 1]; let mut my_trace_idx = 1; if step > 0 { @@ -455,10 +555,12 @@ pub fn verifier_choose_challenge( let read_selector = if is_read_1_future { 1 } else { 2 }; return Ok(ChallengeType::FutureRead { - step: step + 1, + cosigned_decisions_bits, prover_read_step_1, prover_read_step_2, read_selector, + nary, + nary_last_round, }); } @@ -578,29 +680,66 @@ pub fn verifier_choose_challenge( pub fn verifier_choose_challenge_for_read_challenge( program_definition_file: &str, checkpoint_path: &str, + resigned_step_hash: &str, + resigned_next_hash: &str, fail_config: Option, force: ForceChallenge, + return_script_parameters: bool, ) -> Result { let program_def = ProgramDefinition::from_config(program_definition_file)?; let nary_def = program_def.nary_def(); let verifier_log = VerifierChallengeLog::load(checkpoint_path)?; let read_challenge_log = verifier_log.read_challenge_log; - let conflict_step = verifier_log.conflict_step_log.step_to_challenge; + let conflict_step_log = verifier_log.conflict_step_log; let challenge_step = read_challenge_log.step_to_challenge; + let mapping = &nary_def.step_mapping(&read_challenge_log.verifier_decisions); let (prover_step_hash, prover_next_hash) = get_hashes( - &nary_def.step_mapping(&read_challenge_log.verifier_decisions), + mapping, &read_challenge_log.prover_hash_rounds, challenge_step, ); let (my_step_hash, my_next_hash) = get_hashes( - &nary_def.step_mapping(&read_challenge_log.verifier_decisions), + mapping, &read_challenge_log.verifier_hash_rounds, challenge_step, ); + let nary = return_script_parameters.then_some(nary_def.nary); + let nary_last_round = return_script_parameters.then_some(nary_def.nary_last_round); + + let cosigned_decisions_bits = read_challenge_log.verifier_decisions; + if prover_step_hash != resigned_step_hash { + let (round, index) = *mapping.get(&challenge_step).unwrap(); + + return Ok(ChallengeType::EquivocationHash { + prover_true_hash: prover_step_hash, + prover_wrong_hash: resigned_step_hash.to_string(), + cosigned_decisions_bits, + kind: EquivocationKind::StepHash, + round, + index: index + 1, + nary, + nary_last_round, + }); + } + + if prover_next_hash != resigned_next_hash { + let (round, index) = *mapping.get(&(challenge_step + 1)).unwrap(); + return Ok(ChallengeType::EquivocationHash { + prover_true_hash: prover_next_hash, + prover_wrong_hash: resigned_next_hash.to_string(), + cosigned_decisions_bits, + kind: EquivocationKind::NextHash, + round, + index: index + 1, + nary, + nary_last_round, + }); + } + assert_eq!(prover_next_hash, my_next_hash); let my_execution = program_def @@ -630,11 +769,13 @@ pub fn verifier_choose_challenge_for_read_challenge( if (read_step == challenge_step && force == ForceChallenge::No) || force == ForceChallenge::ReadValue { - let conflict_step_trace = verifier_log.conflict_step_log.final_trace; + let conflict_step_trace = conflict_step_log.final_trace; let prover_read_1 = conflict_step_trace.read_1; let prover_read_2 = conflict_step_trace.read_2; let read_selector = verifier_log.read_selector; + let cosigned_conflict_bits = conflict_step_log.verifier_decisions.clone(); + return Ok(ChallengeType::ReadValue { prover_read_1, prover_read_2, @@ -642,8 +783,10 @@ pub fn verifier_choose_challenge_for_read_challenge( prover_hash: prover_step_hash, trace: my_trace.trace_step, prover_next_hash, - write_step: challenge_step + 1, - conflict_step: conflict_step + 1, + cosigned_read_bits: cosigned_decisions_bits, + cosigned_conflict_bits, + nary, + nary_last_round, }); } @@ -827,7 +970,7 @@ mod tests { //PROVER PROVIDES EXECUTE STEP (and reveals full_trace) //Use v_desision + 1 as v_decision defines the last agreed step - let final_trace = + let (final_trace, step_hash, next_hash, _) = prover_final_trace(pdf, chk_prover_path, v_decision + 1, fail_config_prover).unwrap(); info!("Prover final trace: {:?}", final_trace.to_csv()); @@ -846,6 +989,8 @@ mod tests { pdf, chk_verifier_path, final_trace, + step_hash.as_str(), + next_hash.as_str(), force, fail_config_verifier, true, @@ -879,11 +1024,24 @@ mod tests { info!("{:?}", v_decision); } + let (resigned_step_hash, resigned_next_hash, _) = + prover_get_cosigned_bits_and_hashes( + pdf, + &chk_prover_path, + NArySearchType::ReadValueChallenge, + Some(v_decision), + fail_config_prover_read_challenge, + ) + .unwrap(); + verifier_choose_challenge_for_read_challenge( pdf, chk_verifier_path, + resigned_step_hash.as_str(), + resigned_next_hash.as_str(), fail_config_verifier_read_challenge, force_read_challenge, + true, ) .unwrap() } @@ -1944,6 +2102,138 @@ mod tests { ); } + #[test] + fn test_challenge_read_same_step() { + init_trace(); + let fail_read_args = vec!["1106", "0xf000003c", "0xaa000004", "0xf000003c", "1106"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let fail_read_1 = Some(FailConfiguration::new_fail_reads(FailReads::new( + Some(&fail_read_args), + None, + ))); + + test_challenge_aux( + "45", + "hello-world.yaml", + 17, + false, + fail_read_1.clone(), + None, + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "46", + "hello-world.yaml", + 17, + false, + None, + None, + fail_read_1, + None, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::FutureRead, + ForceChallenge::No, + ); + } + + #[test] + fn test_challenge_equivocation_step_hash() { + init_trace(); + let fail_read_args = vec!["1106", "0xf000003c", "0xaa000004", "0xf000003c", "1100"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let mut fail_config = + FailConfiguration::new_fail_reads(FailReads::new(Some(&fail_read_args), None)); + fail_config.fail_resign_hash = Some(1105); + let fail_config = Some(fail_config); + + test_challenge_aux( + "47", + "hello-world.yaml", + 17, + false, + fail_config.clone(), + None, + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "48", + "hello-world.yaml", + 17, + false, + None, + None, + fail_config, + None, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::EquivocationHash(EquivocationKind::StepHash), + ForceChallenge::No, + ); + } + + #[test] + fn test_challenge_equivocation_next_hash() { + init_trace(); + let fail_read_args = vec!["1106", "0xf000003c", "0xaa000004", "0xf000003c", "1100"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let mut fail_config = + FailConfiguration::new_fail_reads(FailReads::new(Some(&fail_read_args), None)); + fail_config.fail_resign_hash = Some(1106); + let fail_config = Some(fail_config); + + test_challenge_aux( + "49", + "hello-world.yaml", + 17, + false, + fail_config.clone(), + None, + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "50", + "hello-world.yaml", + 17, + false, + None, + None, + fail_config, + None, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::EquivocationHash(EquivocationKind::NextHash), + ForceChallenge::No, + ); + } + #[test] fn test_challenge_pc_read_from_non_code() { init_trace(); diff --git a/emulator/src/decision/nary_search.rs b/emulator/src/decision/nary_search.rs index 36d4245..a174026 100644 --- a/emulator/src/decision/nary_search.rs +++ b/emulator/src/decision/nary_search.rs @@ -145,6 +145,15 @@ impl NArySearchDefinition { info!("Mapping: {:?}", mapping); mapping } + + pub fn step_from_decision_bits(&self, decision_bits: &Vec) -> u64 { + decision_bits + .iter() + .enumerate() + .fold(0, |res, (round, bits)| { + (res << self.bits_for_round(round as u8 + 1)) + *bits as u64 + }) + } } #[derive(Debug, Clone)] @@ -422,4 +431,20 @@ mod tests { test_selection_aux(8, 128, 0, 9, 2, Some(1), 1, 2, 3); test_selection_aux(8, 128, 2, 3, 3, Some(0), 0, 2, 2); } + + fn test_step_from_all_bits_aux(max_steps: u64, nary: u8, bits: &Vec, expected_step: u64) { + let nary_search = NArySearchDefinition::new(max_steps, nary); + assert_eq!(bits.len(), nary_search.total_rounds() as usize); + assert_eq!(nary_search.step_from_decision_bits(bits), expected_step); + } + + #[test] + fn test_step_from_all_bits() { + test_step_from_all_bits_aux(128, 8, &vec![0, 0, 0], 0); + test_step_from_all_bits_aux(128, 8, &vec![0, 0, 1], 1); + test_step_from_all_bits_aux(2000, 8, &vec![4, 2, 4, 0], 1104); + test_step_from_all_bits_aux(2000, 8, &vec![4, 2, 4, 1], 1105); + test_step_from_all_bits_aux(2000, 8, &vec![4, 2, 4, 2], 1106); + test_step_from_all_bits_aux(2000, 8, &vec![4, 2, 4, 3], 1107); + } } diff --git a/emulator/src/executor/fetcher.rs b/emulator/src/executor/fetcher.rs index cc362a2..3dc628d 100644 --- a/emulator/src/executor/fetcher.rs +++ b/emulator/src/executor/fetcher.rs @@ -301,9 +301,12 @@ pub fn execute_step( Slli(x) | Srli(x) | Srai(x) => op_shift_imm(&instruction, &x, program), Slti(x) | Sltiu(x) => op_sl_imm(&instruction, &x, program), Sb(x) | Sh(x) | Sw(x) => op_store(&instruction, &x, program)?, - Lbu(x) | Lb(x) | Lh(x) | Lhu(x) | Lw(x) => { - op_load(&instruction, &x, program, fail_config.fail_execute_only_protection)? - } + Lbu(x) | Lb(x) | Lh(x) | Lhu(x) | Lw(x) => op_load( + &instruction, + &x, + program, + fail_config.fail_execute_only_protection, + )?, Auipc(x) | Lui(x) => op_upper(&instruction, &x, program), Beq(x) | Bne(x) | Blt(x) | Bge(x) | Bltu(x) | Bgeu(x) => { op_conditional(&instruction, &x, program) diff --git a/emulator/src/executor/utils.rs b/emulator/src/executor/utils.rs index c37b8ff..03a055c 100644 --- a/emulator/src/executor/utils.rs +++ b/emulator/src/executor/utils.rs @@ -219,6 +219,7 @@ impl FailOpcode { #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct FailConfiguration { pub fail_hash: Option, + pub fail_resign_hash: Option, pub fail_hash_until: Option, pub fail_execute: Option, pub fail_reads: Option, @@ -236,6 +237,12 @@ impl FailConfiguration { ..Default::default() } } + pub fn new_fail_resign_hash(fail_resign_hash: u64) -> Self { + Self { + fail_resign_hash: Some(fail_resign_hash), + ..Default::default() + } + } pub fn new_fail_hash_until(step: u64) -> Self { Self { fail_hash_until: Some(step), diff --git a/emulator/src/main.rs b/emulator/src/main.rs index 46c4952..e75557d 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -5,8 +5,8 @@ use emulator::{ constants::REGISTERS_BASE_ADDRESS, decision::{ challenge::{ - prover_execute, prover_final_trace, prover_get_hashes_for_round, - verifier_check_execution, verifier_choose_challenge, + prover_execute, prover_final_trace, prover_get_cosigned_bits_and_hashes, + prover_get_hashes_for_round, verifier_check_execution, verifier_choose_challenge, verifier_choose_challenge_for_read_challenge, verifier_choose_segment, ForceChallenge, ForceCondition, }, @@ -183,6 +183,27 @@ enum Commands { command_file: String, }, + ProverGetCosignedBitsAndHashes { + #[arg(short, long, value_name = "FILE")] + pdf: String, + + /// Checkpoint prover path + #[arg(short, long, value_name = "CHECKPOINT_PROVER_PATH")] + checkpoint_prover_path: String, + + /// Verifier decision + #[arg(short, long, value_name = "VERIFIER_DECISION")] + v_decision: u32, + + /// Command File to write the result + #[arg(short, long, value_name = "COMMAND_PATH")] + command_file: String, + + // Fail Configuration + #[arg(short, long, value_name = "FailConfigProver")] + fail_config_prover: Option, + }, + VerifierChooseChallenge { /// Yaml file to load #[arg(short, long, value_name = "FILE")] @@ -207,6 +228,12 @@ enum Commands { /// Command File to write the result #[arg(short, long, value_name = "COMMAND_PATH")] command_file: String, + + #[arg(short, long, value_name = "RESIGNED_STEP_HASH")] + resigned_step_hash: String, + + #[arg(short, long, value_name = "RESIGNED_NEXT_HASH")] + resigned_next_hash: String, }, VerifierChooseChallengeForReadChallenge { @@ -229,6 +256,12 @@ enum Commands { /// Command File to write the result #[arg(short, long, value_name = "COMMAND_PATH")] command_file: String, + + #[arg(short, long, value_name = "RESIGNED_STEP_HASH")] + resigned_step_hash: String, + + #[arg(short, long, value_name = "RESIGNED_NEXT_HASH")] + resigned_next_hash: String, }, ///Generate the instruction mapping @@ -342,6 +375,10 @@ enum Commands { #[arg(long, value_names = &["step", "opcode"], num_args = 2)] fail_opcode: Option>, + /// Fail resign hash at a given step + #[arg(long)] + fail_resign_hash: Option, + /// Memory dump at given step #[arg(short, long)] dump_mem: Option, @@ -400,6 +437,7 @@ fn main() -> Result<(), EmulatorError> { fail_read_2: fail_read_2_args, fail_write: fail_write_args, fail_opcode: fail_opcode_args, + fail_resign_hash, dump_mem, fail_pc, save_non_checkpoint_steps, @@ -472,6 +510,7 @@ fn main() -> Result<(), EmulatorError> { fail_opcode, fail_memory_protection: false, fail_execute_only_protection: false, + fail_resign_hash: *fail_resign_hash, }; let result = execute_program( &mut program, @@ -623,16 +662,26 @@ fn main() -> Result<(), EmulatorError> { fail_config_prover, command_file, }) => { - let result: TraceRWStep = prover_final_trace( - pdf, - checkpoint_prover_path, - *v_decision, - fail_config_prover.clone(), - )?; - info!("Prover final trace: {:?}", result); + let (final_trace, resigned_step_hash, resigned_next_hash, cosigned_decision_bits) = + prover_final_trace( + pdf, + checkpoint_prover_path, + *v_decision, + fail_config_prover.clone(), + )?; + info!("Prover final trace: {:?}", final_trace); + info!("Prover resigned step hash: {:?}", resigned_step_hash); + info!("Prover resigned next hash: {:?}", resigned_next_hash); + info!( + "Prover cosigned decision bits: {:?}", + cosigned_decision_bits + ); let result = EmulatorResultType::ProverFinalTraceResult { - final_trace: result.clone(), + final_trace, + resigned_step_hash, + resigned_next_hash, + cosigned_decision_bits, } .to_value()?; let mut file = create_or_open_file(command_file); @@ -643,6 +692,8 @@ fn main() -> Result<(), EmulatorError> { pdf, checkpoint_verifier_path, prover_final_trace, + resigned_step_hash, + resigned_next_hash, force, fail_config_verifier, command_file, @@ -651,6 +702,8 @@ fn main() -> Result<(), EmulatorError> { pdf, checkpoint_verifier_path, prover_final_trace.clone(), + resigned_step_hash, + resigned_next_hash, force.clone(), fail_config_verifier.clone(), false, @@ -669,14 +722,19 @@ fn main() -> Result<(), EmulatorError> { pdf, checkpoint_verifier_path, fail_config_verifier, + resigned_step_hash, + resigned_next_hash, force, command_file, }) => { let result = verifier_choose_challenge_for_read_challenge( pdf, checkpoint_verifier_path, + resigned_step_hash, + resigned_next_hash, fail_config_verifier.clone(), force.clone(), + false, )?; info!("Verifier choose challenge: {:?}", result); @@ -688,6 +746,32 @@ fn main() -> Result<(), EmulatorError> { file.write_all(result.to_string().as_bytes()) .expect("Failed to write JSON to file"); } + Some(Commands::ProverGetCosignedBitsAndHashes { + pdf, + checkpoint_prover_path, + v_decision, + command_file, + fail_config_prover, + }) => { + let (resigned_step_hash, resigned_next_hash, cosigned_decision_bits) = + prover_get_cosigned_bits_and_hashes( + pdf, + &checkpoint_prover_path, + NArySearchType::ReadValueChallenge, + Some(*v_decision), + fail_config_prover.clone(), + )?; + + let result = EmulatorResultType::ProverGetCosignedBitsAndHashesResult { + resigned_step_hash, + resigned_next_hash, + cosigned_decision_bits, + } + .to_value()?; + let mut file = create_or_open_file(command_file); + file.write_all(result.to_string().as_bytes()) + .expect("Failed to write JSON to file"); + } None => { error!("No command specified"); } diff --git a/emulator/tests/test_r_type_instructions.rs b/emulator/tests/test_r_type_instructions.rs index 490926d..48f37a8 100644 --- a/emulator/tests/test_r_type_instructions.rs +++ b/emulator/tests/test_r_type_instructions.rs @@ -217,7 +217,6 @@ fn test_division_aux(dividend: i32, divisor: i32, quotioent: i32, remainder: i32 assert_eq!(program.registers.get(1), quotioent as u32); assert_eq!(witness.unwrap(), remainder as u32); - let (_, witness) = op_arithmetic(&rem, &x, &mut program); assert_eq!(program.registers.get(1), remainder as u32); assert_eq!(witness.unwrap(), quotioent as u32); diff --git a/emulator/tests/test_s_type_instructions.rs b/emulator/tests/test_s_type_instructions.rs index 80ce0ce..b802a55 100644 --- a/emulator/tests/test_s_type_instructions.rs +++ b/emulator/tests/test_s_type_instructions.rs @@ -116,7 +116,10 @@ fn test_store_word( ); assert_eq!( program - .read_mem(start_address + imm_value - imm_value % 4 + mem_aux_2_byte_offset, false) + .read_mem( + start_address + imm_value - imm_value % 4 + mem_aux_2_byte_offset, + false + ) .unwrap(), expected_reg_aux_2 ); @@ -168,7 +171,10 @@ fn test_store_half_word( ); assert_eq!( program - .read_mem(start_address + imm_value - imm_value % 4 + mem_aux_2_byte_offset, false) + .read_mem( + start_address + imm_value - imm_value % 4 + mem_aux_2_byte_offset, + false + ) .unwrap(), expected_reg_aux_2 ); From 58514f34091416fa004284dff031a26b94c4ceab Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 19 Nov 2025 12:24:18 -0300 Subject: [PATCH 04/22] save trace when limit step is reached --- emulator/src/executor/fetcher.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/emulator/src/executor/fetcher.rs b/emulator/src/executor/fetcher.rs index 3dc628d..74ef978 100644 --- a/emulator/src/executor/fetcher.rs +++ b/emulator/src/executor/fetcher.rs @@ -155,7 +155,9 @@ pub fn execute_program( } } - if print_trace || trace.is_err() || program.halt { + let limit_step_reached = limit_step.is_some_and(|limit_step| limit_step == program.step); + + if print_trace || trace.is_err() || program.halt || limit_step_reached { if trace_set.is_none() || trace_set.as_ref().unwrap().contains(&program.step) { let hash_hex = hash_to_string(&program.hash); traces.push(( @@ -203,10 +205,8 @@ pub fn execute_program( break ExecutionResult::Halt(program.registers.get(REGISTER_A0 as u32), program.step); } - if let Some(limit_step) = limit_step { - if limit_step == program.step { - break ExecutionResult::LimitStepReached(limit_step); - } + if limit_step_reached { + break ExecutionResult::LimitStepReached(program.step); } }; From 13f61ab79996eb77bb84607123919e9b78f9489f Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 19 Nov 2025 12:45:46 -0300 Subject: [PATCH 05/22] simplify selection and bound it by the conflict step --- emulator/src/decision/challenge.rs | 6 ++++++ emulator/src/decision/nary_search.rs | 31 +++++++++++++--------------- emulator/tests/challenge.rs | 1 + 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index 8327e11..e6d446e 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -176,6 +176,11 @@ pub fn verifier_choose_segment( let mut challenge_log = VerifierChallengeLog::load(checkpoint_path)?; let input = challenge_log.input.clone(); + let conflict_step = match nary_type { + NArySearchType::ConflictStep => None, + _ => Some(challenge_log.conflict_step_log.step_to_challenge), + }; + let nary_log = challenge_log.get_nary_log(nary_type); let program_def = ProgramDefinition::from_config(program_definition_file)?; @@ -198,6 +203,7 @@ pub fn verifier_choose_segment( &claim_hashes, &my_hashes, nary_type, + conflict_step, ); nary_log.base_step = base; nary_log.step_to_challenge = new_selected; diff --git a/emulator/src/decision/nary_search.rs b/emulator/src/decision/nary_search.rs index a174026..7a30231 100644 --- a/emulator/src/decision/nary_search.rs +++ b/emulator/src/decision/nary_search.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, str::FromStr}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; use tracing::{error, info}; +use std::cmp::{max, min}; #[derive(Clone, Copy, PartialEq, ValueEnum, Serialize, Deserialize, Debug)] pub enum NArySearchType { @@ -194,6 +195,7 @@ pub fn choose_segment( prover_hashes: &ExecutionHashes, my_hashes: &ExecutionHashes, nary_type: NArySearchType, + conflict_step: Option, ) -> (u32, u64, u64) { if prover_hashes.hashes.len() != my_hashes.hashes.len() { error!("Prover and my hashes should have the same length"); @@ -218,25 +220,19 @@ pub fn choose_segment( // first mismatch step //println!("Selection: {}", selection); - let mismatch_step = nary_defs.step_from_base_and_bits(round, base_step, selection as u32) - 1; + let mismatch_step = + nary_defs.step_from_base_and_bits(round, base_step, selection as u32); //println!("Mismatch step: {}", mismatch_step); - let (lower_limit_bits, choice) = if selected_step < mismatch_step { - if nary_type == NArySearchType::ConflictStep { - ( - nary_defs.step_bits_for_round(round, selected_step), - selected_step, - ) - } else { - (selection as u32, mismatch_step + 1) + let (lower_limit_bits, choice) = match nary_type { + NArySearchType::ConflictStep => { + let choice = min(selected_step, mismatch_step - 1); + (nary_defs.step_bits_for_round(round, choice), choice) } - } else { - if nary_type == NArySearchType::ConflictStep { - (selection as u32 - 1, mismatch_step) - } else { - ( - nary_defs.step_bits_for_round(round, selected_step), - selected_step, - ) + _ => { + let conflict = conflict_step.unwrap(); + let base = max(selected_step, mismatch_step); + let choice = min(base, conflict); + (nary_defs.step_bits_for_round(round, choice), choice) } }; @@ -388,6 +384,7 @@ mod tests { &prover_hashes.into(), &my_hashes.into(), NArySearchType::ConflictStep, + None, ); assert_eq!(bits, exp_bits); assert_eq!(base, exp_step); diff --git a/emulator/tests/challenge.rs b/emulator/tests/challenge.rs index 388590c..8c993f0 100644 --- a/emulator/tests/challenge.rs +++ b/emulator/tests/challenge.rs @@ -49,6 +49,7 @@ fn test_nary_search_trace_aux(input: u8, expect_err: bool, checkpoint_path: &str &claim_hashes, &my_hashes, NArySearchType::ConflictStep, + None, ); base = new_base; selected = new_selected; From 5730d40eda15ca67cb0c866dbdc84def05409d67 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 19 Nov 2025 12:46:40 -0300 Subject: [PATCH 06/22] fix selection when no mismatch is found in ReadValueChallenge --- emulator/src/decision/nary_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emulator/src/decision/nary_search.rs b/emulator/src/decision/nary_search.rs index 7a30231..b4f099d 100644 --- a/emulator/src/decision/nary_search.rs +++ b/emulator/src/decision/nary_search.rs @@ -205,7 +205,7 @@ pub fn choose_segment( let mut selection = if nary_type == NArySearchType::ConflictStep { prover_hashes.hashes.len() + 1 } else { - 1 + 0 }; for i in 0..prover_hashes.hashes.len() { let prover_hash = &prover_hashes.hashes[i]; From a945652acd64f5cd672b46346b4da44dfbd54382 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 19 Nov 2025 12:49:36 -0300 Subject: [PATCH 07/22] make challenges sizes constant --- bitcoin-script-riscv/src/riscv/challenges.rs | 306 +++++++++--------- bitcoin-script-riscv/src/riscv/operations.rs | 13 +- .../src/riscv/script_utils.rs | 14 +- definitions/src/challenge.rs | 42 +-- emulator/src/decision/challenge.rs | 214 ++++++++---- emulator/src/decision/nary_search.rs | 5 +- emulator/src/main.rs | 28 +- 7 files changed, 364 insertions(+), 258 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 868097a..2f6b94a 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -9,10 +9,11 @@ use bitvmx_cpu_definitions::{ use crate::riscv::{ memory_alignment::{is_aligned, load_lower_half_nibble_table, load_upper_half_nibble_table}, + operations::add_with_bit_extension, script_utils::{ address_in_sections, address_not_in_sections, get_selected_vars, is_lower_than, - next_decision, var_to_decisions_in_stack, verify_wrong_chunk_value, witness_equals, - StackTables, + next_decision_in_stack, var_to_decisions_in_stack, verify_wrong_chunk_value, + witness_equals, StackTables, }, }; @@ -238,6 +239,10 @@ pub fn trace_hash_zero_challenge(stack: &mut StackTracker) { let write_micro = stack.define(2, "write_micro"); let hash = stack.define(40, "hash"); + let step = stack.define(16, "prover_conflict_step_tk"); + + let zero = stack.number_u64(0); + stack.equals(step, true, zero, true); //save the hash to compare stack.to_altstack(); @@ -494,7 +499,7 @@ pub fn uninitialized_challenge( stack.drop(read_addr); } -pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary_last_round: u8) { +pub fn read_value_challenge(stack: &mut StackTracker) { stack.clear_definitions(); let read_addr_1 = stack.define(8, "prover_read_addr_1"); @@ -516,23 +521,18 @@ pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary let next_hash = stack.define(40, "next_hash"); - let write_bits = stack.define(rounds as u32, "write_bits"); - let conflict_bits = stack.define(rounds as u32, "conflict_bits"); + let write_step = stack.define(16, "prover_write_step_tk"); + let conflict_step = stack.define(16, "prover_conflict_step_tk"); - let write_bits_copy = stack.copy_var(write_bits); - is_lower_than(stack, write_bits_copy, conflict_bits, true); + let write_step_copy = stack.copy_var(write_step); + is_lower_than(stack, write_step_copy, conflict_step, true); stack.op_verify(); - let max_nary = nary - 1; - let max_last_round = if nary_last_round == 0 { - max_nary - } else { - nary_last_round - 1 - }; - - stack.explode(write_bits); - next_decision(stack, rounds, max_last_round, max_nary); - let write_bits = stack.from_altstack_joined(rounds as u32, "next_write_bits"); + // the nary search ends up pointing to the previous step of the write, so we have to increment it + let tables = &StackTables::new(stack, true, true, 0, 0, 0); + let one = stack.number(1); + let no_bit_extension = stack.number(0); + let write_step = add_with_bit_extension(stack, tables, write_step, one, no_bit_extension); let [read_addr, read_value, read_step] = get_selected_vars( stack, @@ -542,16 +542,8 @@ pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary ); stack.rename(read_addr, "read_addr"); - let read_step_copy = stack.copy_var(read_step); - - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - var_to_decisions_in_stack(stack, tables, read_step, nary_last_round, nary, rounds); - tables.drop(stack); - let read_step_bits = stack.from_altstack_joined(rounds as u32, "read_step"); - // if read_step == write_step -> write_addr != read_addr || write_value != read_value - stack.equality(read_step_bits, false, write_bits, false, true, false); - stack.set_breakpoint("MY AWESOME BREAK POINT"); + stack.equality(read_step, false, write_step, false, true, false); stack.equality(write_addr, false, read_addr, false, false, false); stack.equality(write_value, false, read_value, true, false, false); stack.op_boolor(); @@ -560,8 +552,8 @@ pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary let init = stack.number_u64(LAST_STEP_INIT); // if read_step == INIT || read_step < write_step -> write_addr == read_addr - stack.equality(read_step_copy, true, init, true, true, false); - is_lower_than(stack, read_step_bits, write_bits, true); + stack.equality(read_step, false, init, true, true, false); + is_lower_than(stack, read_step, write_step, true); stack.op_boolor(); stack.equality(write_addr, false, read_addr, true, true, false); @@ -571,6 +563,7 @@ pub fn read_value_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary stack.op_verify(); + tables.drop(stack); //save the hash to compare stack.to_altstack(); @@ -614,10 +607,10 @@ pub fn correct_hash_challenge(stack: &mut StackTracker) { stack.equals(result, true, next_hash, true); } -pub fn future_read_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nary_last_round: u8) { +pub fn future_read_challenge(stack: &mut StackTracker) { stack.clear_definitions(); - let step = stack.define(rounds as u32, "prover_step"); + let step = stack.define(16, "prover_conflict_step_tk"); let read_step_1 = stack.define(16, "prover_read_step_1"); let read_step_2 = stack.define(16, "prover_read_step_2"); @@ -629,14 +622,11 @@ pub fn future_read_challenge(stack: &mut StackTracker, rounds: u8, nary: u8, nar let init = stack.number_u64(LAST_STEP_INIT); stack.equality(read_step, false, init, true, false, true); - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - - var_to_decisions_in_stack(stack, tables, read_step, nary_last_round, nary, rounds); - tables.drop(stack); - let read_step = stack.from_altstack_joined(rounds as u32, "read_step"); - - is_lower_than(stack, read_step, step, true); - stack.op_not(); + // The first step corresponds to the step selected by the decision bits, and the second step + // is the step of the given trace, that trace corresponds to the next step selected so + // if both steps are the same that means that the trace did a read to a value writen in the previous step. + // so step == read_step is valid and we shouldn't be able to challenge it, only if step < read_step ≡ trace_step <= read_step + is_lower_than(stack, step, read_step, true); stack.op_verify(); } @@ -669,23 +659,31 @@ fn get_round_and_index(stack: &mut StackTracker, decisions_bits: StackVariable, } } -pub fn equivocation_hash_challenge( +pub fn equivocation_resign_challenge( stack: &mut StackTracker, - rounds: u8, kind: EquivocationKind, expected_round: u8, expected_index: u8, + rounds: u8, nary: u8, nary_last_round: u8, ) { stack.clear_definitions(); - let true_hash = stack.define(40, "prover_true_hash"); - let wrong_hash = stack.define(40, "prover_wrong_hash"); - let mut decisions_bits = stack.define(rounds as u32, "decisions_bits"); + let true_hash = stack.define( + 40, + format!("prover_hash_{}_{}", expected_round, expected_index).as_str(), + ); + let wrong_hash = stack.define(40, format!("prover_{:?}_hash_tk", kind).as_str()); + let step = stack.define(16, "prover_challenge_step_tk"); + + // TODO: Optimize this... + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + var_to_decisions_in_stack(stack, tables, step, nary_last_round, nary, rounds); + tables.drop(stack); + let mut decisions_bits = stack.from_altstack_joined(rounds as u32, "decisions_bits"); if kind == EquivocationKind::NextHash { - stack.explode(decisions_bits); let max_nary = nary - 1; let max_last_round = if nary_last_round == 0 { max_nary @@ -693,7 +691,7 @@ pub fn equivocation_hash_challenge( nary_last_round - 1 }; - next_decision(stack, rounds, max_last_round, max_nary); + next_decision_in_stack(stack, decisions_bits, rounds, max_last_round, max_nary); decisions_bits = stack.from_altstack_joined(rounds as u32, "next_decisions_bits"); } @@ -710,6 +708,16 @@ pub fn equivocation_hash_challenge( stack.drop(decisions_bits); } +pub fn equivocation_hash_challenge(stack: &mut StackTracker) { + let step_hash1 = stack.define(40, "prover_step_hash1"); + let step_hash2 = stack.define(40, "prover_step_hash2"); + let write_step = stack.define(16, "prover_write_step_tk"); + let conflict_step = stack.define(16, "prover_conflict_step_tk"); + + stack.equality(step_hash1, true, step_hash2, true, false, true); + stack.equals(write_step, true, conflict_step, true); +} + //TODO: memory section challenge //TODO: program crash challenge - this might be more about finding the right place to challenge that a challenge itself @@ -732,12 +740,15 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { ChallengeType::TraceHashZero { prover_trace, prover_next_hash, + prover_conflict_step_tk, } => { stack.number_u32(prover_trace.get_write().address); stack.number_u32(prover_trace.get_write().value); stack.number_u32(prover_trace.get_pc().get_address()); stack.byte(prover_trace.get_pc().get_micro()); stack.hexstr_as_nibbles(prover_next_hash); + stack.number_u64(*prover_conflict_step_tk); + trace_hash_zero_challenge(&mut stack); } ChallengeType::EntryPoint { @@ -872,27 +883,17 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { ); } ChallengeType::FutureRead { - cosigned_decisions_bits, + prover_conflict_step_tk, prover_read_step_1, prover_read_step_2, read_selector, - nary, - nary_last_round, } => { - for bit in cosigned_decisions_bits { - stack.number(*bit); - } - + stack.number_u64(*prover_conflict_step_tk); stack.number_u64(*prover_read_step_1); stack.number_u64(*prover_read_step_2); stack.number(*read_selector); - future_read_challenge( - &mut stack, - cosigned_decisions_bits.len() as u8, - nary.unwrap(), - nary_last_round.unwrap(), - ); + future_read_challenge(&mut stack); } ChallengeType::ReadValue { prover_read_1, @@ -901,10 +902,8 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { prover_hash, trace, prover_next_hash, - cosigned_read_bits, - cosigned_conflict_bits, - nary, - nary_last_round, + prover_write_step_tk, + prover_conflict_step_tk, } => { stack.number_u32(prover_read_1.address); stack.number_u32(prover_read_1.value); @@ -925,20 +924,10 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { stack.hexstr_as_nibbles(prover_next_hash); - for bits in cosigned_read_bits { - stack.number(*bits); - } - - for bits in cosigned_conflict_bits { - stack.number(*bits); - } + stack.number_u64(*prover_write_step_tk); + stack.number_u64(*prover_conflict_step_tk); - read_value_challenge( - &mut stack, - cosigned_read_bits.len() as u8, - nary.unwrap(), - nary_last_round.unwrap(), - ); + read_value_challenge(&mut stack); } ChallengeType::CorrectHash { prover_step_hash, @@ -958,33 +947,46 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { correct_hash_challenge(&mut stack); } - ChallengeType::EquivocationHash { + ChallengeType::EquivocationResign { prover_true_hash, prover_wrong_hash, - cosigned_decisions_bits, + prover_challenge_step_tk, kind, - round, - index, + expected_round, + expected_index, + rounds, nary, nary_last_round, } => { stack.hexstr_as_nibbles(prover_true_hash); stack.hexstr_as_nibbles(prover_wrong_hash); - for bits in cosigned_decisions_bits { - stack.number(*bits); - } + stack.number_u64(*prover_challenge_step_tk); - equivocation_hash_challenge( + equivocation_resign_challenge( &mut stack, - cosigned_decisions_bits.len() as u8, kind.clone(), - *round, - *index, + *expected_round, + *expected_index, + rounds.unwrap(), nary.unwrap(), nary_last_round.unwrap(), ); } + ChallengeType::EquivocationHash { + prover_step_hash1, + prover_step_hash2, + prover_write_step_tk, + prover_conflict_step_tk, + } => { + stack.hexstr_as_nibbles(prover_step_hash1); + stack.hexstr_as_nibbles(prover_step_hash2); + + stack.number_u64(*prover_write_step_tk); + stack.number_u64(*prover_conflict_step_tk); + + equivocation_hash_challenge(&mut stack) + } _ => { return false; } @@ -1136,6 +1138,7 @@ mod tests { pc: u32, micro: u8, hash: &str, + step: u64, ) -> bool { let mut stack = StackTracker::new(); @@ -1146,6 +1149,8 @@ mod tests { stack.hexstr_as_nibbles(hash); + stack.number_u64(step); + trace_hash_zero_challenge(&mut stack); stack.op_true(); @@ -1158,12 +1163,17 @@ mod tests { //prover provided valid hash, verifier loses assert!(!test_trace_hash_zero_aux( - 0xf0000028, 0x00000000, 0x80000100, 0x00, hash + 0xf0000028, 0x00000000, 0x80000100, 0x00, hash, 0 + )); + + //not initial step selected, verifier loses + assert!(!test_trace_hash_zero_aux( + 0xf0000028, 0x00000000, 0x80000100, 0x01, hash, 1 )); //prover provided invalid hash, verifier wins assert!(test_trace_hash_zero_aux( - 0xf0000028, 0x00000000, 0x80000100, 0x01, hash + 0xf0000028, 0x00000000, 0x80000100, 0x01, hash, 0 )); } @@ -2025,23 +2035,12 @@ mod tests { fn test_future_read_aux(step: u64, read_step: u64) -> bool { let stack = &mut StackTracker::new(); - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - - let rounds = 4; - let nary = 8; - let nary_last_round = 4; - - let step = stack.number_u64(step); - var_to_decisions_in_stack(stack, tables, step, nary_last_round, nary, rounds); - tables.drop(stack); - - stack.from_altstack_joined(rounds as u32, "step"); - + stack.number_u64(step); stack.number_u64(read_step); // read_1_step stack.number_u64(read_step); // read_2_step stack.number(1); // read_selector - future_read_challenge(stack, rounds, nary, nary_last_round); + future_read_challenge(stack); stack.op_true(); stack.run().success @@ -2056,8 +2055,8 @@ mod tests { // can't challenge initial value read assert!(!test_future_read_aux(step, LAST_STEP_INIT)); - // challenge is valid if read from same step - assert!(test_future_read_aux(step, step)); + // can't challenge same step, see comment in future_read_challenge + assert!(!test_future_read_aux(step, step)); // challenge is valid if read after current step assert!(test_future_read_aux(step, step + 1)); @@ -2092,24 +2091,12 @@ mod tests { stack.hexstr_as_nibbles(&next_hash); - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - - let write_step = stack.number_u64(write_step - 1); - let conflict_step = stack.number_u64(conflict_step - 1); - - let rounds = 4; - let nary = 8; - let nary_last_round = 4; - - var_to_decisions_in_stack(stack, tables, conflict_step, nary_last_round, nary, rounds); - var_to_decisions_in_stack(stack, tables, write_step, nary_last_round, nary, rounds); - - tables.drop(stack); + // the nary search ends up pointing to the previous step of the write, + // so we have to decrement it because the challenge expects it to be one less + stack.number_u64(write_step - 1); + stack.number_u64(conflict_step); - stack.from_altstack_joined(rounds as u32, "write_bits"); - stack.from_altstack_joined(rounds as u32, "conflict_bits"); - - read_value_challenge(stack, rounds, nary, nary_last_round); + read_value_challenge(stack); stack.op_true(); stack.run().success @@ -2232,96 +2219,92 @@ mod tests { )); } - fn test_equivocation_hash_aux( + fn test_equivocation_resign_aux( hash1: &String, hash2: &String, - decisions_bits: &Vec, + step: u64, kind: EquivocationKind, round: u8, index: u8, + rounds: u8, nary: u8, nary_last_round: u8, ) -> bool { let stack = &mut StackTracker::new(); stack.hexstr_as_nibbles(hash1); stack.hexstr_as_nibbles(hash2); + stack.number_u64(step); - for decision in decisions_bits { - stack.number(*decision); - } - - equivocation_hash_challenge( - stack, - decisions_bits.len() as u8, - kind, - round, - index, - nary, - nary_last_round, - ); + equivocation_resign_challenge(stack, kind, round, index, rounds, nary, nary_last_round); stack.op_true(); + // interactive(&stack); stack.run().success } #[test] - pub fn test_equivocation_hash() { + pub fn test_equivocation_resign() { let true_hash = &"e2f115006467b4b1b2b27612bbfd40ed3bc8299b".to_string(); let wrong_hash = &"345721506e79c53d2549fc63d02ba8fc3b17efa4".to_string(); + let rounds = 4; let nary = 8; let nary_last_round = 4; // this decisions bits selects the hash at round 4 and index 3 // the next hash was given at round 2 index 1 - let decisions_bits = &vec![4, 0, 7, 3]; - + // let decisions_bits = &vec![4, 0, 7, 3]; + let step = 1055; // can't challenge true hash - assert!(!test_equivocation_hash_aux( + assert!(!test_equivocation_resign_aux( true_hash, true_hash, - decisions_bits, + step, EquivocationKind::StepHash, 4, 3, + rounds, nary, nary_last_round, )); // can't challenge with other step hash // this hash was at round 2 index 3 - assert!(!test_equivocation_hash_aux( + assert!(!test_equivocation_resign_aux( true_hash, wrong_hash, - decisions_bits, + step, EquivocationKind::StepHash, 2, // round 3, // index + rounds, nary, nary_last_round, )); // can challenge if wrong hash and correct round and index - assert!(test_equivocation_hash_aux( + assert!(test_equivocation_resign_aux( true_hash, wrong_hash, - decisions_bits, + step, EquivocationKind::StepHash, 4, 3, + rounds, nary, nary_last_round, )); // can challenge if wrong hash and correct round and index for next hash - assert!(test_equivocation_hash_aux( + assert!(test_equivocation_resign_aux( true_hash, wrong_hash, - decisions_bits, + step, EquivocationKind::NextHash, 2, 1, + rounds, nary, nary_last_round, )); @@ -2535,6 +2518,7 @@ mod tests { micro: u8, hash: [u8; 20], expected_to_succeed: bool, + step: u64, ) -> bool { let mut stack = StackTracker::new(); stack.number_u32(write_add); @@ -2542,6 +2526,7 @@ mod tests { stack.number_u32(pc); stack.byte(micro); stack.hexstr_as_nibbles(&hex::encode(hash)); + stack.number_u64(step); trace_hash_zero_challenge(&mut stack); stack.op_true(); @@ -2977,24 +2962,30 @@ mod tests { "trace_hash_zero_challenge", |rng| { let should_succeed = rng.random_bool(0.5); + let wrong_step = rng.random_bool(0.5); let write_add: u32 = rng.random(); let write_value: u32 = rng.random(); let pc: u32 = rng.random(); let micro: u8 = rng.random(); + let mut step: u64 = 0; let correct_hash = compute_state_hash_zero_oracle(write_add, write_value, pc, micro); let mut prover_hash: [u8; 20]; - if should_succeed { - loop { - prover_hash = rng.random(); - if prover_hash != correct_hash { - break; - } + loop { + prover_hash = rng.random(); + if prover_hash != correct_hash { + break; + } + } + + if !should_succeed { + if wrong_step { + step = rng.random::() + 1; + } else { + prover_hash = correct_hash; } - } else { - prover_hash = correct_hash; } ( @@ -3004,10 +2995,12 @@ mod tests { micro, prover_hash, should_succeed, + step, ) }, |input| { - let (write_add, write_value, pc, micro, hash, expected_to_succeed) = input; + let (write_add, write_value, pc, micro, hash, expected_to_succeed, step) = + input; trace_hash_zero_challenge_aux( write_add, write_value, @@ -3015,6 +3008,7 @@ mod tests { micro, hash, expected_to_succeed, + step, ) }, ); @@ -3827,6 +3821,7 @@ mod tests { case.micro, incorrect_hash, true, + 0, ); assert!( success_result, @@ -3842,6 +3837,7 @@ mod tests { case.micro, correct_hash, false, + 0, ); assert!( failure_result, diff --git a/bitcoin-script-riscv/src/riscv/operations.rs b/bitcoin-script-riscv/src/riscv/operations.rs index 932b4cd..4589023 100644 --- a/bitcoin-script-riscv/src/riscv/operations.rs +++ b/bitcoin-script-riscv/src/riscv/operations.rs @@ -48,20 +48,21 @@ pub fn add_with_bit_extension( ) -> StackVariable { stack.set_breakpoint("add_with_bit_extension"); + let value_size = stack.get_size(value); //move the value and split the nibbles stack.move_var(value); stack.explode(value); let add_size = stack.get_size(to_add); let mut last = StackVariable::null(); - for i in 0..8 { + for i in 0..value_size { if i > 0 { stack.op_add(); } if i < add_size { stack.move_var_sub_n(to_add, add_size - i - 1); - } else if i < 7 { + } else if i < value_size - 1 { stack.copy_var(bit_extension); } else { stack.move_var(bit_extension); @@ -69,24 +70,24 @@ pub fn add_with_bit_extension( stack.op_add(); - if i < 7 { + if i < value_size - 1 { stack.op_dup(); } last = stack.get_value_from_table(tables.modulo, None); - if i < 7 { + if i < value_size - 1 { stack.to_altstack(); stack.get_value_from_table(tables.quotient, None); } } - for _ in 0..7 { + for _ in 0..value_size - 1 { stack.from_altstack(); } stack.rename(last, "add_bit_ext"); - stack.join_count(last, 7) + stack.join_count(last, value_size - 1) } pub fn sub( diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index a1f6b7d..0aff975 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1713,7 +1713,16 @@ pub fn var_to_decisions_in_stack( } } -pub fn next_decision(stack: &mut StackTracker, rounds: u8, max_last_round: u8, max_nary: u8) { +pub fn next_decision_in_stack( + stack: &mut StackTracker, + decisions_bits: StackVariable, + rounds: u8, + max_last_round: u8, + max_nary: u8, +) { + stack.move_var(decisions_bits); + stack.explode(decisions_bits); + stack.op_dup(); stack.number(max_last_round as u32); stack.op_equal(); @@ -2701,8 +2710,7 @@ mod tests { let decision = stack.from_altstack_joined(rounds as u32, "decision_bits"); - stack.explode(decision); - next_decision(stack, rounds, max_last_round, max_nary); + next_decision_in_stack(stack, decision, rounds, max_last_round, max_nary); let next_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); let expected_next_decision_bits = diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index 6ef2459..3773c56 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -17,6 +17,7 @@ pub enum ChallengeType { TraceHashZero { prover_trace: TraceStep, prover_next_hash: String, + prover_conflict_step_tk: u64, }, EntryPoint { prover_read_pc: TraceReadPC, @@ -71,12 +72,10 @@ pub enum ChallengeType { code_sections: Option, }, FutureRead { - cosigned_decisions_bits: Vec, + prover_conflict_step_tk: u64, prover_read_step_1: u64, prover_read_step_2: u64, read_selector: u32, - nary: Option, - nary_last_round: Option, }, ReadValueNArySearch { bits: u32, @@ -88,10 +87,8 @@ pub enum ChallengeType { prover_hash: String, trace: TraceStep, prover_next_hash: String, - cosigned_read_bits: Vec, - cosigned_conflict_bits: Vec, - nary: Option, - nary_last_round: Option, + prover_write_step_tk: u64, + prover_conflict_step_tk: u64, }, CorrectHash { prover_step_hash: String, @@ -99,16 +96,23 @@ pub enum ChallengeType { trace: TraceStep, prover_next_hash: String, }, - EquivocationHash { + EquivocationResign { prover_true_hash: String, prover_wrong_hash: String, - cosigned_decisions_bits: Vec, + prover_challenge_step_tk: u64, kind: EquivocationKind, - round: u8, - index: u8, + expected_round: u8, + expected_index: u8, + rounds: Option, nary: Option, nary_last_round: Option, }, + EquivocationHash { + prover_step_hash1: String, + prover_step_hash2: String, + prover_write_step_tk: u64, + prover_conflict_step_tk: u64, + }, No, } @@ -143,12 +147,12 @@ pub enum EmulatorResultType { final_trace: TraceRWStep, resigned_step_hash: String, resigned_next_hash: String, - cosigned_decision_bits: Vec, + conflict_step: u64, }, ProverGetCosignedBitsAndHashesResult { resigned_step_hash: String, resigned_next_hash: String, - cosigned_decision_bits: Vec, + write_step: u64, }, VerifierChooseChallengeResult { challenge: ChallengeType, @@ -220,18 +224,18 @@ impl EmulatorResultType { pub fn as_final_trace( &self, - ) -> Result<(TraceRWStep, String, String, Vec), EmulatorResultError> { + ) -> Result<(TraceRWStep, String, String, u64), EmulatorResultError> { match self { EmulatorResultType::ProverFinalTraceResult { final_trace, resigned_step_hash, resigned_next_hash, - cosigned_decision_bits, + conflict_step, } => Ok(( final_trace.clone(), resigned_step_hash.to_string(), resigned_next_hash.to_string(), - cosigned_decision_bits.clone(), + *conflict_step, )), _ => Err(EmulatorResultError::GenericError( "Expected ProverFinalTraceResult".to_string(), @@ -241,16 +245,16 @@ impl EmulatorResultType { pub fn as_cosigned_bits_and_hashes( &self, - ) -> Result<(String, String, Vec), EmulatorResultError> { + ) -> Result<(String, String, u64), EmulatorResultError> { match self { EmulatorResultType::ProverGetCosignedBitsAndHashesResult { resigned_step_hash, resigned_next_hash, - cosigned_decision_bits, + write_step, } => Ok(( resigned_step_hash.to_string(), resigned_next_hash.to_string(), - cosigned_decision_bits.clone(), + *write_step, )), _ => Err(EmulatorResultError::GenericError( "Expected ProverGetCosignedBitsAndHashesResult".to_string(), diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index e6d446e..eb52111 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -222,7 +222,7 @@ pub fn prover_final_trace( checkpoint_path: &str, final_bits: u32, fail_config: Option, -) -> Result<(TraceRWStep, String, String, Vec), EmulatorError> { +) -> Result<(TraceRWStep, String, String, u64), EmulatorError> { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; let input = challenge_log.input.clone(); let nary_log = challenge_log.get_nary_log(NArySearchType::ConflictStep); @@ -242,7 +242,7 @@ pub fn prover_final_trace( nary_log.final_trace = final_trace.clone(); challenge_log.save(checkpoint_path)?; - let (step_hash, next_hash, cosigned_bits) = prover_get_cosigned_bits_and_hashes( + let (step_hash, next_hash, conflict_step) = prover_get_hashes_and_step( program_definition_file, checkpoint_path, NArySearchType::ConflictStep, @@ -250,16 +250,16 @@ pub fn prover_final_trace( fail_config, )?; - Ok((final_trace, step_hash, next_hash, cosigned_bits)) + Ok((final_trace, step_hash, next_hash, conflict_step)) } -pub fn prover_get_cosigned_bits_and_hashes( +pub fn prover_get_hashes_and_step( program_definition_file: &str, checkpoint_path: &str, nary_type: NArySearchType, final_bits: Option, fail_config: Option, -) -> Result<(String, String, Vec), EmulatorError> { +) -> Result<(String, String, u64), EmulatorError> { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; let nary_log = challenge_log.get_nary_log(nary_type); @@ -291,8 +291,7 @@ pub fn prover_get_cosigned_bits_and_hashes( } } - let cosigned_bits = nary_log.verifier_decisions.clone(); - Ok((step_hash, next_hash, cosigned_bits)) + Ok((step_hash, next_hash, final_step)) } pub fn get_hashes( @@ -322,7 +321,8 @@ pub fn get_hashes( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ForceChallenge { - EquivocationHash(EquivocationKind), + EquivocationHash, + EquivocationResign(EquivocationKind), CorrectHash, TraceHash, TraceHashZero, @@ -387,38 +387,40 @@ pub fn verifier_choose_challenge( let nary = return_script_parameters.then_some(nary_def.nary); let nary_last_round = return_script_parameters.then_some(nary_def.nary_last_round); + let rounds = return_script_parameters.then_some(nary_def.total_rounds()); - let cosigned_decisions_bits = conflict_step_log.verifier_decisions.clone(); if (prover_step_hash != resigned_step_hash && force == ForceChallenge::No) - || force == ForceChallenge::EquivocationHash(EquivocationKind::StepHash) + || force == ForceChallenge::EquivocationResign(EquivocationKind::StepHash) { let (round, index) = *mapping.get(&step).unwrap(); - return Ok(ChallengeType::EquivocationHash { + return Ok(ChallengeType::EquivocationResign { prover_true_hash: prover_step_hash, prover_wrong_hash: resigned_step_hash.to_string(), - cosigned_decisions_bits, + prover_challenge_step_tk: step, kind: EquivocationKind::StepHash, - round, - index: index + 1, + expected_round: round, + expected_index: index + 1, nary, nary_last_round, + rounds, }); } if (prover_next_hash != resigned_next_hash && force == ForceChallenge::No) - || force == ForceChallenge::EquivocationHash(EquivocationKind::NextHash) + || force == ForceChallenge::EquivocationResign(EquivocationKind::NextHash) { let (round, index) = *mapping.get(&(step + 1)).unwrap(); - return Ok(ChallengeType::EquivocationHash { + return Ok(ChallengeType::EquivocationResign { prover_true_hash: prover_next_hash, prover_wrong_hash: resigned_next_hash.to_string(), - cosigned_decisions_bits, + prover_challenge_step_tk: step, kind: EquivocationKind::NextHash, - round, - index: index+1, + expected_round: round, + expected_index: index + 1, nary, nary_last_round, + rounds, }); } @@ -433,6 +435,7 @@ pub fn verifier_choose_challenge( return Ok(ChallengeType::TraceHashZero { prover_trace: trace.trace_step, prover_next_hash, + prover_conflict_step_tk: step, }); } @@ -561,12 +564,10 @@ pub fn verifier_choose_challenge( let read_selector = if is_read_1_future { 1 } else { 2 }; return Ok(ChallengeType::FutureRead { - cosigned_decisions_bits, + prover_conflict_step_tk: step, prover_read_step_1, prover_read_step_2, read_selector, - nary, - nary_last_round, }); } @@ -698,6 +699,7 @@ pub fn verifier_choose_challenge_for_read_challenge( let read_challenge_log = verifier_log.read_challenge_log; let conflict_step_log = verifier_log.conflict_step_log; + let conflict_step = conflict_step_log.step_to_challenge; let challenge_step = read_challenge_log.step_to_challenge; let mapping = &nary_def.step_mapping(&read_challenge_log.verifier_decisions); @@ -707,7 +709,7 @@ pub fn verifier_choose_challenge_for_read_challenge( challenge_step, ); - let (my_step_hash, my_next_hash) = get_hashes( + let (my_step_hash, _) = get_hashes( mapping, &read_challenge_log.verifier_hash_rounds, challenge_step, @@ -715,38 +717,62 @@ pub fn verifier_choose_challenge_for_read_challenge( let nary = return_script_parameters.then_some(nary_def.nary); let nary_last_round = return_script_parameters.then_some(nary_def.nary_last_round); + let rounds = return_script_parameters.then_some(nary_def.total_rounds()); - let cosigned_decisions_bits = read_challenge_log.verifier_decisions; - if prover_step_hash != resigned_step_hash { + if (prover_step_hash != resigned_step_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationResign(EquivocationKind::StepHash) + { let (round, index) = *mapping.get(&challenge_step).unwrap(); - return Ok(ChallengeType::EquivocationHash { + return Ok(ChallengeType::EquivocationResign { prover_true_hash: prover_step_hash, prover_wrong_hash: resigned_step_hash.to_string(), - cosigned_decisions_bits, + prover_challenge_step_tk: challenge_step, kind: EquivocationKind::StepHash, - round, - index: index + 1, + expected_round: round, + expected_index: index + 1, nary, nary_last_round, + rounds, }); } - if prover_next_hash != resigned_next_hash { + if (prover_next_hash != resigned_next_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationResign(EquivocationKind::NextHash) + { let (round, index) = *mapping.get(&(challenge_step + 1)).unwrap(); - return Ok(ChallengeType::EquivocationHash { + return Ok(ChallengeType::EquivocationResign { prover_true_hash: prover_next_hash, prover_wrong_hash: resigned_next_hash.to_string(), - cosigned_decisions_bits, + prover_challenge_step_tk: challenge_step, kind: EquivocationKind::NextHash, - round, - index: index + 1, + expected_round: round, + expected_index: index + 1, nary, nary_last_round, + rounds, }); } - assert_eq!(prover_next_hash, my_next_hash); + if (prover_step_hash != my_step_hash + && conflict_step == challenge_step + && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationHash + { + let mapping = &nary_def.step_mapping(&conflict_step_log.verifier_decisions); + let (prover_step_hash1, _) = get_hashes( + mapping, + &conflict_step_log.prover_hash_rounds, + challenge_step, + ); + + return Ok(ChallengeType::EquivocationHash { + prover_step_hash1, + prover_step_hash2: prover_step_hash, + prover_write_step_tk: challenge_step, + prover_conflict_step_tk: conflict_step_log.step_to_challenge, + }); + } let my_execution = program_def .execute_helper( @@ -780,8 +806,6 @@ pub fn verifier_choose_challenge_for_read_challenge( let prover_read_2 = conflict_step_trace.read_2; let read_selector = verifier_log.read_selector; - let cosigned_conflict_bits = conflict_step_log.verifier_decisions.clone(); - return Ok(ChallengeType::ReadValue { prover_read_1, prover_read_2, @@ -789,10 +813,8 @@ pub fn verifier_choose_challenge_for_read_challenge( prover_hash: prover_step_hash, trace: my_trace.trace_step, prover_next_hash, - cosigned_read_bits: cosigned_decisions_bits, - cosigned_conflict_bits, - nary, - nary_last_round, + prover_write_step_tk: challenge_step, + prover_conflict_step_tk: conflict_step_log.step_to_challenge, }); } @@ -1030,15 +1052,14 @@ mod tests { info!("{:?}", v_decision); } - let (resigned_step_hash, resigned_next_hash, _) = - prover_get_cosigned_bits_and_hashes( - pdf, - &chk_prover_path, - NArySearchType::ReadValueChallenge, - Some(v_decision), - fail_config_prover_read_challenge, - ) - .unwrap(); + let (resigned_step_hash, resigned_next_hash, _) = prover_get_hashes_and_step( + pdf, + &chk_prover_path, + NArySearchType::ReadValueChallenge, + Some(v_decision), + fail_config_prover_read_challenge, + ) + .unwrap(); verifier_choose_challenge_for_read_challenge( pdf, @@ -1927,7 +1948,7 @@ mod tests { false, ForceCondition::ValidInputWrongStepOrHash, ForceChallenge::ReadValueNArySearch, - ForceChallenge::TraceHash, + ForceChallenge::CorrectHash, ); } #[test] @@ -1972,7 +1993,7 @@ mod tests { false, ForceCondition::ValidInputWrongStepOrHash, ForceChallenge::ReadValueNArySearch, - ForceChallenge::TraceHash, + ForceChallenge::CorrectHash, ); } @@ -2153,7 +2174,7 @@ mod tests { } #[test] - fn test_challenge_equivocation_step_hash() { + fn test_challenge_equivocation_resign_step_hash() { init_trace(); let fail_read_args = vec!["1106", "0xf000003c", "0xaa000004", "0xf000003c", "1100"] .iter() @@ -2191,13 +2212,13 @@ mod tests { None, false, ForceCondition::ValidInputWrongStepOrHash, - ForceChallenge::EquivocationHash(EquivocationKind::StepHash), + ForceChallenge::EquivocationResign(EquivocationKind::StepHash), ForceChallenge::No, ); } #[test] - fn test_challenge_equivocation_next_hash() { + fn test_challenge_equivocation_resign_next_hash() { init_trace(); let fail_read_args = vec!["1106", "0xf000003c", "0xaa000004", "0xf000003c", "1100"] .iter() @@ -2235,11 +2256,63 @@ mod tests { None, false, ForceCondition::ValidInputWrongStepOrHash, - ForceChallenge::EquivocationHash(EquivocationKind::NextHash), + ForceChallenge::EquivocationResign(EquivocationKind::NextHash), ForceChallenge::No, ); } + #[test] + fn test_challenge_equivocation_hash() { + init_trace(); + let fail_read_args = vec!["1106", "0xaa000000", "0x11111100", "0xaa000000", "1100"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let fail_read_2 = Some(FailConfiguration::new_fail_reads(FailReads::new( + None, + Some(&fail_read_args), + ))); + + let fail_write_args = vec!["1100", "0xaa000000", "0x11111100", "0xaa000000"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + let fail_write = Some(FailConfiguration::new_fail_write(FailWrite::new( + &fail_write_args, + ))); + + test_challenge_aux( + "51", + "hello-world.yaml", + 17, + false, + fail_read_2.clone(), + fail_write.clone(), + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "52", + "hello-world.yaml", + 17, + false, + None, + None, + fail_read_2, + fail_write, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::ReadValueNArySearch, + ForceChallenge::EquivocationHash, + ); + } + #[test] fn test_challenge_pc_read_from_non_code() { init_trace(); @@ -2386,4 +2459,33 @@ mod tests { ForceChallenge::No, ); } + + #[test] + fn test_pc_limit() { + init_trace(); + + // executes a NOP in the step that should jump to the infinite loop, causing the program to wrongfuly halt + let fail_args = vec!["2", "0x100073"] // Ebreak (NOP) + .iter() + .map(|x| x.to_string()) + .collect::>(); + let fail_opcode = Some(FailConfiguration::new_fail_opcode(FailOpcode::new( + &fail_args, + ))); + + test_challenge_aux( + "pc_limit", + "pc_limit.yaml", + 0, + false, + fail_opcode, + None, + None, + None, + true, + ForceCondition::No, + ForceChallenge::No, + ForceChallenge::No, + ); + } } diff --git a/emulator/src/decision/nary_search.rs b/emulator/src/decision/nary_search.rs index b4f099d..51085e7 100644 --- a/emulator/src/decision/nary_search.rs +++ b/emulator/src/decision/nary_search.rs @@ -2,8 +2,8 @@ use std::{collections::HashMap, str::FromStr}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; -use tracing::{error, info}; use std::cmp::{max, min}; +use tracing::{error, info}; #[derive(Clone, Copy, PartialEq, ValueEnum, Serialize, Deserialize, Debug)] pub enum NArySearchType { @@ -220,8 +220,7 @@ pub fn choose_segment( // first mismatch step //println!("Selection: {}", selection); - let mismatch_step = - nary_defs.step_from_base_and_bits(round, base_step, selection as u32); + let mismatch_step = nary_defs.step_from_base_and_bits(round, base_step, selection as u32); //println!("Mismatch step: {}", mismatch_step); let (lower_limit_bits, choice) = match nary_type { NArySearchType::ConflictStep => { diff --git a/emulator/src/main.rs b/emulator/src/main.rs index e75557d..ca1887f 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -5,7 +5,7 @@ use emulator::{ constants::REGISTERS_BASE_ADDRESS, decision::{ challenge::{ - prover_execute, prover_final_trace, prover_get_cosigned_bits_and_hashes, + prover_execute, prover_final_trace, prover_get_hashes_and_step, prover_get_hashes_for_round, verifier_check_execution, verifier_choose_challenge, verifier_choose_challenge_for_read_challenge, verifier_choose_segment, ForceChallenge, ForceCondition, @@ -662,7 +662,7 @@ fn main() -> Result<(), EmulatorError> { fail_config_prover, command_file, }) => { - let (final_trace, resigned_step_hash, resigned_next_hash, cosigned_decision_bits) = + let (final_trace, resigned_step_hash, resigned_next_hash, conflict_step) = prover_final_trace( pdf, checkpoint_prover_path, @@ -672,16 +672,13 @@ fn main() -> Result<(), EmulatorError> { info!("Prover final trace: {:?}", final_trace); info!("Prover resigned step hash: {:?}", resigned_step_hash); info!("Prover resigned next hash: {:?}", resigned_next_hash); - info!( - "Prover cosigned decision bits: {:?}", - cosigned_decision_bits - ); + info!("Prover conflict step: {:?}", conflict_step); let result = EmulatorResultType::ProverFinalTraceResult { final_trace, resigned_step_hash, resigned_next_hash, - cosigned_decision_bits, + conflict_step, } .to_value()?; let mut file = create_or_open_file(command_file); @@ -753,19 +750,18 @@ fn main() -> Result<(), EmulatorError> { command_file, fail_config_prover, }) => { - let (resigned_step_hash, resigned_next_hash, cosigned_decision_bits) = - prover_get_cosigned_bits_and_hashes( - pdf, - &checkpoint_prover_path, - NArySearchType::ReadValueChallenge, - Some(*v_decision), - fail_config_prover.clone(), - )?; + let (resigned_step_hash, resigned_next_hash, write_step) = prover_get_hashes_and_step( + pdf, + &checkpoint_prover_path, + NArySearchType::ReadValueChallenge, + Some(*v_decision), + fail_config_prover.clone(), + )?; let result = EmulatorResultType::ProverGetCosignedBitsAndHashesResult { resigned_step_hash, resigned_next_hash, - cosigned_decision_bits, + write_step, } .to_value()?; let mut file = create_or_open_file(command_file); From 44c1fd2f4478162a2c32ebe1c72aa66db3bb68ed Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 20 Nov 2025 11:26:17 -0300 Subject: [PATCH 08/22] rename command --- definitions/src/challenge.rs | 8 +++----- emulator/src/main.rs | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index 3773c56..bb9d7a9 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -149,7 +149,7 @@ pub enum EmulatorResultType { resigned_next_hash: String, conflict_step: u64, }, - ProverGetCosignedBitsAndHashesResult { + ProverGetHashesAndStepResult { resigned_step_hash: String, resigned_next_hash: String, write_step: u64, @@ -243,11 +243,9 @@ impl EmulatorResultType { } } - pub fn as_cosigned_bits_and_hashes( - &self, - ) -> Result<(String, String, u64), EmulatorResultError> { + pub fn as_hashes_and_step(&self) -> Result<(String, String, u64), EmulatorResultError> { match self { - EmulatorResultType::ProverGetCosignedBitsAndHashesResult { + EmulatorResultType::ProverGetHashesAndStepResult { resigned_step_hash, resigned_next_hash, write_step, diff --git a/emulator/src/main.rs b/emulator/src/main.rs index ca1887f..249c361 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -183,7 +183,7 @@ enum Commands { command_file: String, }, - ProverGetCosignedBitsAndHashes { + ProverGetHashesAndStep { #[arg(short, long, value_name = "FILE")] pdf: String, @@ -743,7 +743,7 @@ fn main() -> Result<(), EmulatorError> { file.write_all(result.to_string().as_bytes()) .expect("Failed to write JSON to file"); } - Some(Commands::ProverGetCosignedBitsAndHashes { + Some(Commands::ProverGetHashesAndStep { pdf, checkpoint_prover_path, v_decision, @@ -758,7 +758,7 @@ fn main() -> Result<(), EmulatorError> { fail_config_prover.clone(), )?; - let result = EmulatorResultType::ProverGetCosignedBitsAndHashesResult { + let result = EmulatorResultType::ProverGetHashesAndStepResult { resigned_step_hash, resigned_next_hash, write_step, From 6e1d25247191d82091d13cfca60d4bc8158bc400 Mon Sep 17 00:00:00 2001 From: crivasr Date: Tue, 25 Nov 2025 10:22:57 -0300 Subject: [PATCH 09/22] change docker commit --- docker-riscv32 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-riscv32 b/docker-riscv32 index c3dd537..4357dc1 160000 --- a/docker-riscv32 +++ b/docker-riscv32 @@ -1 +1 @@ -Subproject commit c3dd537a47a7232ad87aaaf3b5177fd39dc300be +Subproject commit 4357dc103c270a7b373cdbe27767ed864795bb93 From 7b628eb6003a1da6d518d83457fe551c2500a5a9 Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 27 Nov 2025 15:01:40 -0300 Subject: [PATCH 10/22] add halt challenge --- bitcoin-script-riscv/src/riscv/challenges.rs | 273 ++++++++++++++---- .../src/riscv/script_utils.rs | 60 ++-- definitions/src/challenge.rs | 7 + emulator/src/decision/challenge.rs | 120 +++++++- emulator/src/decision/execution_log.rs | 2 +- emulator/src/executor/utils.rs | 14 + emulator/src/main.rs | 10 + 7 files changed, 399 insertions(+), 87 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 2f6b94a..94e9419 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -9,11 +9,10 @@ use bitvmx_cpu_definitions::{ use crate::riscv::{ memory_alignment::{is_aligned, load_lower_half_nibble_table, load_upper_half_nibble_table}, - operations::add_with_bit_extension, script_utils::{ - address_in_sections, address_not_in_sections, get_selected_vars, is_lower_than, - next_decision_in_stack, var_to_decisions_in_stack, verify_wrong_chunk_value, - witness_equals, StackTables, + address_in_sections, address_not_in_sections, get_selected_vars, increment_var, + is_lower_than, next_decision_in_altstack, var_to_decisions_in_altstack, + verify_wrong_chunk_value, witness_equals, StackTables, }, }; @@ -184,22 +183,33 @@ pub fn program_counter_challenge(stack: &mut StackTracker) { // WOTS_PROVER_FINAL_STEP:16 == WOTS_PROVER_TRACE_STEP:16 && ( WOTS_PROVER_READ_VALUE_1:8 | WOTS_PROVER_READ_VALUE_2:8 | WOTS_PROVER_OPCODE:8 != 93 | 0 | 115 ) pub fn halt_challenge(stack: &mut StackTracker) { stack.clear_definitions(); - let final_step = stack.define(16, "final_step"); - let trace_step = stack.define(16, "trace_step"); - let provided = stack.define(8, "read_value_1"); - stack.define(8, "read_value_2"); - stack.define(8, "opcode"); - stack.join_count(provided, 2); - - let expected = stack.number_u32(93); - stack.number_u32(0); - stack.number_u32(115); - stack.join_count(expected, 2); - - stack.not_equal(provided, true, expected, true); + let last_step = stack.define(16, "prover_last_step"); + let conflict_step = stack.define(16, "prover_conflict_step_tk"); + let read_1 = stack.define(8, "prover_read_1_value"); + // technically we don't need to check the status code since execute_step will fail if we call a halt without success + let read_2 = stack.define(8, "prover_read_2_value"); + let opcode = stack.define(8, "prover_read_pc_opcode"); + let next_hash = stack.define(40, "prover_next_hash_tk"); + let last_hash = stack.define(40, "prover_last_hash"); + + // the neary search is off by one + let incremented_conflict_step = increment_var(stack, conflict_step); + stack.equals(last_step, true, incremented_conflict_step, true); + + let halt_syscall = stack.number_u32(0x5d); + let sucess_exit_code = stack.number_u32(0); + let syscall_opcode = stack.number_u32(0x73); + + stack.equality(read_1, true, halt_syscall, true, false, false); + stack.equality(read_2, true, sucess_exit_code, true, false, false); + stack.equality(opcode, true, syscall_opcode, true, false, false); + stack.equality(next_hash, true, last_hash, true, false, false); - stack.equals(final_step, true, trace_step, true); + stack.op_boolor(); + stack.op_boolor(); + stack.op_boolor(); + stack.op_verify(); } // When the prover expands the trace it could happen that HASH( hash_prev_step | trace_write ) != hash_step @@ -529,10 +539,7 @@ pub fn read_value_challenge(stack: &mut StackTracker) { stack.op_verify(); // the nary search ends up pointing to the previous step of the write, so we have to increment it - let tables = &StackTables::new(stack, true, true, 0, 0, 0); - let one = stack.number(1); - let no_bit_extension = stack.number(0); - let write_step = add_with_bit_extension(stack, tables, write_step, one, no_bit_extension); + let write_step = increment_var(stack, write_step); let [read_addr, read_value, read_step] = get_selected_vars( stack, @@ -563,7 +570,6 @@ pub fn read_value_challenge(stack: &mut StackTracker) { stack.op_verify(); - tables.drop(stack); //save the hash to compare stack.to_altstack(); @@ -679,7 +685,7 @@ pub fn equivocation_resign_challenge( // TODO: Optimize this... let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - var_to_decisions_in_stack(stack, tables, step, nary_last_round, nary, rounds); + var_to_decisions_in_altstack(stack, tables, step, nary_last_round, nary, rounds); tables.drop(stack); let mut decisions_bits = stack.from_altstack_joined(rounds as u32, "decisions_bits"); @@ -691,7 +697,7 @@ pub fn equivocation_resign_challenge( nary_last_round - 1 }; - next_decision_in_stack(stack, decisions_bits, rounds, max_last_round, max_nary); + next_decision_in_altstack(stack, decisions_bits, rounds, max_last_round, max_nary); decisions_bits = stack.from_altstack_joined(rounds as u32, "next_decisions_bits"); } @@ -724,6 +730,25 @@ pub fn equivocation_hash_challenge(stack: &mut StackTracker) { pub fn execute_challenge(challege_type: &ChallengeType) -> bool { let mut stack = StackTracker::new(); match challege_type { + ChallengeType::Halt { + prover_last_step, + prover_conflict_step_tk, + prover_trace, + prover_next_hash, + prover_last_hash, + } => { + stack.number_u64(*prover_last_step); + stack.number_u64(*prover_conflict_step_tk); + + stack.number_u32(prover_trace.read_1.value); + stack.number_u32(prover_trace.read_2.value); + stack.number_u32(prover_trace.read_pc.opcode); + + stack.hexstr_as_nibbles(prover_next_hash); + stack.hexstr_as_nibbles(prover_last_hash); + + halt_challenge(&mut stack); + } ChallengeType::TraceHash { prover_step_hash, prover_trace, @@ -1104,32 +1129,108 @@ mod tests { } fn test_halt_challenge_aux( - final_step: u64, - trace_step: u64, - read_value_1: u32, - read_value_2: u32, + last_step: u64, + conflict_step: u64, + read_1: u32, + read_2: u32, opcode: u32, + next_hash: &String, + last_hash: &String, ) -> bool { - let mut stack = StackTracker::new(); + let stack = &mut StackTracker::new(); - stack.number_u64(final_step); - stack.number_u64(trace_step); - stack.number_u32(read_value_1); - stack.number_u32(read_value_2); + stack.number_u64(last_step); + stack.number_u64(conflict_step); + + stack.number_u32(read_1); + stack.number_u32(read_2); stack.number_u32(opcode); - halt_challenge(&mut stack); + stack.hexstr_as_nibbles(next_hash); + stack.hexstr_as_nibbles(last_hash); + + halt_challenge(stack); + stack.op_true(); stack.run().success } #[test] - fn test_halt_challenge() { - assert!(test_halt_challenge_aux(0x0, 0x0, 93, 1, 115)); - assert!(test_halt_challenge_aux(0x0, 0x0, 92, 0, 115)); - assert!(test_halt_challenge_aux(0x0, 0x0, 93, 0, 114)); - assert!(!test_halt_challenge_aux(0x0, 0x0, 93, 0, 115)); - assert!(!test_halt_challenge_aux(0x0, 0x1, 93, 0, 114)); + pub fn test_halt_challenge() { + let last_step = 100; + let conflict_step = last_step - 1; + let last_hash = &"e2f115006467b4b1b2b27612bbfd40ed3bc8299b".to_string(); + let wrong_hash = &"345721506e79c53d2549fc63d02ba8fc3b17efa4".to_string(); + + let halt_syscall = 0x5d; + let success_exit_code = 0; + let syscall_opcode = 0x73; + + // can't challenge at wrong step + assert!(!test_halt_challenge_aux( + last_step, + conflict_step + 1, + 0xDEAD, + 0xDEAD, + 0xDEAD, + last_hash, + wrong_hash + )); + + // can't challenge if everything is correct + assert!(!test_halt_challenge_aux( + last_step, + conflict_step, + halt_syscall, + success_exit_code, + syscall_opcode, + last_hash, + last_hash + )); + + // can challenge if not halt syscall + assert!(test_halt_challenge_aux( + last_step, + conflict_step, + 0xDEAD, + success_exit_code, + syscall_opcode, + last_hash, + last_hash + )); + + // can challenge if not success exit code + assert!(test_halt_challenge_aux( + last_step, + conflict_step, + halt_syscall, + 0xDEAD, + syscall_opcode, + last_hash, + last_hash + )); + + // can challenge if not syscall opcode + assert!(test_halt_challenge_aux( + last_step, + conflict_step, + halt_syscall, + success_exit_code, + 0xDEAD, + last_hash, + last_hash + )); + + // can challenge if wrong hash + assert!(test_halt_challenge_aux( + last_step, + conflict_step, + halt_syscall, + success_exit_code, + syscall_opcode, + last_hash, + wrong_hash + )); } fn test_trace_hash_zero_aux( @@ -2382,19 +2483,24 @@ mod tests { } fn halt_challenge_aux( - final_step: u64, + last_step: u64, trace_step: u64, read_value_1: u32, read_value_2: u32, opcode: u32, + hash: &str, + last_hash: &str, expected_to_succeed: bool, ) -> bool { let mut stack = StackTracker::new(); - stack.number_u64(final_step); - stack.number_u64(trace_step); + let conflict_step = trace_step - 1; + stack.number_u64(last_step); + stack.number_u64(conflict_step); stack.number_u32(read_value_1); stack.number_u32(read_value_2); stack.number_u32(opcode); + stack.hexstr_as_nibbles(hash); + stack.hexstr_as_nibbles(last_hash); halt_challenge(&mut stack); stack.op_true(); @@ -2820,68 +2926,80 @@ mod tests { const SUCCESS_ECALL_VAL2: u32 = 0; const SUCCESS_ECALL_OPCODE: u32 = 115; - let final_step: u64 = rng.random(); + let last_step: u64 = rng.random(); + let last_hash: [u8; 20] = rng.random(); let trace_step: u64; let mut read_value_1: u32; let mut read_value_2: u32; let mut opcode: u32; + let mut hash: [u8; 20]; if should_succeed { // To succeed, steps must match AND the instruction must NOT be the success ecall. - trace_step = final_step; + trace_step = last_step; loop { read_value_1 = rng.random(); read_value_2 = rng.random(); opcode = rng.random(); + hash = rng.random(); if !(read_value_1 == SUCCESS_ECALL_VAL1 && read_value_2 == SUCCESS_ECALL_VAL2 - && opcode == SUCCESS_ECALL_OPCODE) + && opcode == SUCCESS_ECALL_OPCODE + && hash == last_hash) { break; } } } else { - // To fail, either the steps mismatch, OR the instruction is the success ecall. + // To fail, either the steps mismatch, OR the instruction is the success , OR the hash is different. if rng.random_bool(0.5) { // Scenario: Steps mismatch. Instruction can be anything. - trace_step = final_step.wrapping_add(rng.random_range(1..u64::MAX)); + trace_step = last_step.wrapping_add(rng.random_range(1..u64::MAX)); read_value_1 = rng.random(); read_value_2 = rng.random(); opcode = rng.random(); + hash = rng.random(); } else { // Scenario: Steps match, but it's the valid success ecall. - trace_step = final_step; + trace_step = last_step; read_value_1 = SUCCESS_ECALL_VAL1; read_value_2 = SUCCESS_ECALL_VAL2; opcode = SUCCESS_ECALL_OPCODE; + hash = last_hash.clone(); } } ( - final_step, + last_step, trace_step, read_value_1, read_value_2, opcode, should_succeed, + hash, + last_hash, ) }, |input| { let ( - final_step, + last_step, trace_step, read_value_1, read_value_2, opcode, expected_to_succeed, + hash, + last_hash, ) = input; halt_challenge_aux( - final_step, + last_step, trace_step, read_value_1, read_value_2, opcode, + &hex::encode(hash), + &hex::encode(last_hash), expected_to_succeed, ) }, @@ -3562,6 +3680,9 @@ mod tests { const SUCCESS_ECALL_VAL2: u32 = 0; const SUCCESS_ECALL_OPCODE: u32 = 115; + let last_hash = &"e2f115006467b4b1b2b27612bbfd40ed3bc8299b".to_string(); + let wrong_hash = &"345721506e79c53d2549fc63d02ba8fc3b17efa4".to_string(); + #[derive(Debug)] struct TestCase { description: &'static str, @@ -3570,6 +3691,8 @@ mod tests { val1: u32, val2: u32, opcode: u32, + hash: String, + last_hash: String, expected_to_succeed: bool, } @@ -3581,9 +3704,11 @@ mod tests { prover_step: 100, verifier_step: 100, val1: 94, - val2: 0, - opcode: 115, + val2: SUCCESS_ECALL_VAL2, + opcode: SUCCESS_ECALL_OPCODE, expected_to_succeed: true, + hash: last_hash.clone(), + last_hash: last_hash.clone(), }, TestCase { description: "Success: val2 is different.", @@ -3592,6 +3717,8 @@ mod tests { val1: SUCCESS_ECALL_VAL1, val2: 1, opcode: SUCCESS_ECALL_OPCODE, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, TestCase { @@ -3601,15 +3728,31 @@ mod tests { val1: SUCCESS_ECALL_VAL1, val2: SUCCESS_ECALL_VAL2, opcode: 116, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, TestCase { - description: "Border case: Step is 0.", - prover_step: 0, - verifier_step: 0, + description: "Success: hash is different.", + prover_step: 100, + verifier_step: 100, + val1: SUCCESS_ECALL_VAL1, + val2: SUCCESS_ECALL_VAL2, + opcode: SUCCESS_ECALL_OPCODE, + hash: wrong_hash.clone(), + last_hash: last_hash.clone(), + expected_to_succeed: true, + }, + // this used to be "Step is 0." but we can't select the trace for that step. + TestCase { + description: "Border case: Step is 1.", + prover_step: 1, + verifier_step: 1, val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, TestCase { @@ -3619,6 +3762,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, TestCase { @@ -3628,6 +3773,8 @@ mod tests { val1: u32::MAX, val2: u32::MAX, opcode: u32::MAX, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, // --- Failure Scenarios (Challenge should fail) --- @@ -3638,6 +3785,8 @@ mod tests { val1: SUCCESS_ECALL_VAL1, val2: SUCCESS_ECALL_VAL2, opcode: SUCCESS_ECALL_OPCODE, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, TestCase { @@ -3647,6 +3796,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, TestCase { @@ -3656,6 +3807,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, TestCase { @@ -3665,6 +3818,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, ]; @@ -3676,6 +3831,8 @@ mod tests { case.val1, case.val2, case.opcode, + &case.hash, + &case.last_hash, case.expected_to_succeed, ); assert!( diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index 0aff975..8fb20b8 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1639,7 +1639,7 @@ pub fn split(stack: &mut StackTracker, tables: &StackTables, right_size: u8) { shift(stack, &tables.rshift, BITS_NIBBLE - right_size); } -pub fn var_to_decisions_in_stack( +pub fn var_to_decisions_in_altstack( stack: &mut StackTracker, tables: &StackTables, var: StackVariable, @@ -1713,7 +1713,7 @@ pub fn var_to_decisions_in_stack( } } -pub fn next_decision_in_stack( +pub fn next_decision_in_altstack( stack: &mut StackTracker, decisions_bits: StackVariable, rounds: u8, @@ -1741,7 +1741,7 @@ pub fn next_decision_in_stack( stack.end_if(overflow, no_overflow, 1, vec![], 2); - for _ in 1..rounds - 1 { + for _ in 1..rounds { stack.from_altstack(); stack.number(1); stack.op_equal(); @@ -1772,8 +1772,13 @@ pub fn next_decision_in_stack( } stack.from_altstack(); - stack.op_add(); - stack.to_altstack(); + stack.op_drop(); +} + +pub fn increment_var(stack: &mut StackTracker, var: StackVariable) -> StackVariable { + let nibbles = stack.get_size(var); + next_decision_in_altstack(stack, var, nibbles as u8, 15, 15); + stack.from_altstack_joined(nibbles, "inc") } #[cfg(test)] @@ -2640,7 +2645,7 @@ mod tests { )); } - fn test_var_to_decisions_in_stack_aux( + fn test_var_to_decisions_in_altstack_aux( decisions: &[u32], step: u64, nary_last_round: u8, @@ -2653,7 +2658,7 @@ mod tests { stack.number(*decision); } let var = stack.number_u64(step); - var_to_decisions_in_stack( + var_to_decisions_in_altstack( stack, tables, var, @@ -2673,17 +2678,17 @@ mod tests { } #[test] - fn test_var_to_decisions_in_stack() { - test_var_to_decisions_in_stack_aux(&[4, 2, 4, 0], 1104, 4, 8); - test_var_to_decisions_in_stack_aux(&[4, 2, 4, 1], 1105, 4, 8); - test_var_to_decisions_in_stack_aux(&[4, 2, 4, 2], 1106, 4, 8); - test_var_to_decisions_in_stack_aux(&[4, 2, 4, 3], 1107, 4, 8); + fn test_var_to_decisions_in_altstack() { + test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 0], 1104, 4, 8); + test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 1], 1105, 4, 8); + test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 2], 1106, 4, 8); + test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 3], 1107, 4, 8); - test_var_to_decisions_in_stack_aux(&[0, 3, 0, 3], 99, 4, 8); - test_var_to_decisions_in_stack_aux(&[1, 3, 0, 3], 355, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0, 3, 0, 3], 99, 4, 8); + test_var_to_decisions_in_altstack_aux(&[1, 3, 0, 3], 355, 4, 8); } - fn test_next_decision_aux(decision: u64, rounds: u8, nary: u8, nary_last_round: u8) { + fn test_increment_decision_aux(decision: u64, rounds: u8, nary: u8, nary_last_round: u8) { let stack = &mut StackTracker::new(); let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); @@ -2695,9 +2700,9 @@ mod tests { }; let decision_var = stack.number_u64(decision); - let next_decision_var = stack.number_u64(decision + 1); + let next_decision_var = stack.number_u64(decision.wrapping_add(1)); - var_to_decisions_in_stack( + var_to_decisions_in_altstack( stack, tables, next_decision_var, @@ -2706,17 +2711,22 @@ mod tests { rounds, ); - var_to_decisions_in_stack(stack, tables, decision_var, nary_last_round, nary, rounds); + var_to_decisions_in_altstack(stack, tables, decision_var, nary_last_round, nary, rounds); let decision = stack.from_altstack_joined(rounds as u32, "decision_bits"); - next_decision_in_stack(stack, decision, rounds, max_last_round, max_nary); - let next_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); + next_decision_in_altstack(stack, decision, rounds, max_last_round, max_nary); + let incremented_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); let expected_next_decision_bits = stack.from_altstack_joined(rounds as u32, "expected_decision_bits"); - stack.equals(next_decision_bits, true, expected_next_decision_bits, true); + stack.equals( + incremented_decision_bits, + true, + expected_next_decision_bits, + true, + ); tables.drop(stack); stack.op_true(); @@ -2725,10 +2735,10 @@ mod tests { } #[test] fn test_next_decision() { - test_next_decision_aux(100, 4, 8, 4); - test_next_decision_aux(302, 8, 8, 2); - test_next_decision_aux(38, 8, 2, 2); - test_next_decision_aux(892, 4, 8, 0); + test_increment_decision_aux(100, 4, 8, 4); + test_increment_decision_aux(302, 8, 8, 2); + test_increment_decision_aux(38, 8, 2, 2); + test_increment_decision_aux(892, 4, 8, 0); } mod fuzz_tests { diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index bb9d7a9..4124fdf 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -9,6 +9,13 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChallengeType { + Halt { + prover_last_step: u64, + prover_conflict_step_tk: u64, + prover_trace: TraceRWStep, + prover_next_hash: String, + prover_last_hash: String, + }, TraceHash { prover_step_hash: String, prover_trace: TraceStep, diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index eb52111..e3f6607 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -30,8 +30,15 @@ pub fn prover_execute( fail_config: Option, save_non_checkpoint_steps: bool, ) -> Result<(ExecutionResult, u64, String), EmulatorError> { + let fail_last_step = fail_config + .as_ref() + .and_then(|fail| fail.fail_commitment_step); + let fail_last_hash = fail_config + .as_ref() + .is_some_and(|fail| fail.fail_commitment_hash); + let program_def = ProgramDefinition::from_config(program_definition_file)?; - let (result, last_step, last_hash) = program_def.get_execution_result( + let (result, mut last_step, mut last_hash) = program_def.get_execution_result( input.clone(), checkpoint_path, fail_config, @@ -49,6 +56,12 @@ pub fn prover_execute( error!("Execution with force. The claim will be commited on-chain."); } + last_step = fail_last_step.unwrap_or(last_step); + + if fail_last_hash { + last_hash = last_hash.chars().rev().collect(); + } + ProverChallengeLog::new( ExecutionLog::new(result.clone(), last_step, last_hash.clone()), input, @@ -321,6 +334,7 @@ pub fn get_hashes( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ForceChallenge { + Halt, EquivocationHash, EquivocationResign(EquivocationKind), CorrectHash, @@ -430,7 +444,7 @@ pub fn verifier_choose_challenge( || force == ForceChallenge::TraceHash || force == ForceChallenge::TraceHashZero { - if trace.step_number == 1 { + if step == 0 { info!("Verifier choose to challenge TRACE_HASH_ZERO"); return Ok(ChallengeType::TraceHashZero { prover_trace: trace.trace_step, @@ -447,6 +461,32 @@ pub fn verifier_choose_challenge( }); } + let ExecutionLog { + last_hash: prover_claim_last_hash, + last_step: prover_claim_last_step, + result: _, + } = verifier_log.prover_claim_execution.clone(); + + if (step + 1 == prover_claim_last_step + && (prover_next_hash != prover_claim_last_hash + // halt opcode + || trace.read_1.value != 93 + // exit code + || trace.read_2.value != 0 + // syscall opcode + || trace.read_pc.opcode != 0x73) + && force == ForceChallenge::No) + || force == ForceChallenge::Halt + { + return Ok(ChallengeType::Halt { + prover_last_step: prover_claim_last_step, + prover_conflict_step_tk: step, + prover_trace: trace, + prover_next_hash, + prover_last_hash: prover_claim_last_hash, + }); + } + let mut steps = vec![step, step + 1]; let mut my_trace_idx = 1; if step > 0 { @@ -516,7 +556,7 @@ pub fn verifier_choose_challenge( || force == ForceChallenge::EntryPoint || force == ForceChallenge::ProgramCounter { - if trace.step_number == 1 { + if step == 0 { info!("Verifier choose to challenge ENTRYPOINT"); return Ok(ChallengeType::EntryPoint { prover_read_pc: trace.read_pc, @@ -2313,6 +2353,80 @@ mod tests { ); } + #[test] + fn test_challenge_halt_step() { + init_trace(); + + let fail_commitment_step = Some(FailConfiguration::new_fail_commitment_step(1498)); + + test_challenge_aux( + "53", + "hello-world.yaml", + 17, + false, + fail_commitment_step.clone(), + None, + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "54", + "hello-world.yaml", + 17, + false, + None, + None, + fail_commitment_step, + None, + false, + ForceCondition::Always, + ForceChallenge::Halt, + ForceChallenge::No, + ); + } + + #[test] + fn test_challenge_halt_hash() { + init_trace(); + + let fail_commitment_hash = Some(FailConfiguration::new_fail_commitment_hash()); + + test_challenge_aux( + "55", + "hello-world.yaml", + 17, + false, + fail_commitment_hash.clone(), + None, + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "56", + "hello-world.yaml", + 17, + false, + None, + None, + fail_commitment_hash, + None, + false, + ForceCondition::Always, + ForceChallenge::Halt, + ForceChallenge::No, + ); + } + #[test] fn test_challenge_pc_read_from_non_code() { init_trace(); diff --git a/emulator/src/decision/execution_log.rs b/emulator/src/decision/execution_log.rs index d93cd8a..8110487 100644 --- a/emulator/src/decision/execution_log.rs +++ b/emulator/src/decision/execution_log.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{decision::nary_search::NArySearchType, EmulatorError, ExecutionResult}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionLog { pub result: ExecutionResult, pub last_step: u64, diff --git a/emulator/src/executor/utils.rs b/emulator/src/executor/utils.rs index 03a055c..8667164 100644 --- a/emulator/src/executor/utils.rs +++ b/emulator/src/executor/utils.rs @@ -228,6 +228,8 @@ pub struct FailConfiguration { pub fail_opcode: Option, pub fail_memory_protection: bool, pub fail_execute_only_protection: bool, + pub fail_commitment_step: Option, + pub fail_commitment_hash: bool, } impl FailConfiguration { @@ -237,6 +239,18 @@ impl FailConfiguration { ..Default::default() } } + pub fn new_fail_commitment_step(last_step: u64) -> Self { + Self { + fail_commitment_step: Some(last_step), + ..Default::default() + } + } + pub fn new_fail_commitment_hash() -> Self { + Self { + fail_commitment_hash: true, + ..Default::default() + } + } pub fn new_fail_resign_hash(fail_resign_hash: u64) -> Self { Self { fail_resign_hash: Some(fail_resign_hash), diff --git a/emulator/src/main.rs b/emulator/src/main.rs index 249c361..4b001f0 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -379,6 +379,12 @@ enum Commands { #[arg(long)] fail_resign_hash: Option, + #[arg(long)] + fail_commitment_step: Option, + + #[arg(long)] + fail_commitment_hash: bool, + /// Memory dump at given step #[arg(short, long)] dump_mem: Option, @@ -438,6 +444,8 @@ fn main() -> Result<(), EmulatorError> { fail_write: fail_write_args, fail_opcode: fail_opcode_args, fail_resign_hash, + fail_commitment_step, + fail_commitment_hash, dump_mem, fail_pc, save_non_checkpoint_steps, @@ -511,6 +519,8 @@ fn main() -> Result<(), EmulatorError> { fail_memory_protection: false, fail_execute_only_protection: false, fail_resign_hash: *fail_resign_hash, + fail_commitment_step: *fail_commitment_step, + fail_commitment_hash: *fail_commitment_hash, }; let result = execute_program( &mut program, From c511babafa41ee6266c1403c95f990812de6d095 Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 27 Nov 2025 15:04:42 -0300 Subject: [PATCH 11/22] use conflict_step instead of trace step --- bitcoin-script-riscv/src/riscv/challenges.rs | 53 +++++++++----------- definitions/src/challenge.rs | 2 +- emulator/src/decision/challenge.rs | 2 +- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 94e9419..3b95287 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -113,10 +113,11 @@ pub fn entry_point_challenge(stack: &mut StackTracker, entry_point: u32) { stack.clear_definitions(); let provided_pc = stack.define(8, "provided_pc"); let _provided_micro = stack.define(2, "provided_micro"); - let provided_step = stack.define(16, "provided_step"); + let conflict_step = stack.define(16, "prover_conflict_step_tk"); - let step_high = stack.number_u64(1); - stack.equals(provided_step, true, step_high, true); + // conflict_step is off by one of the trace step + let zero = stack.number_u64(0); + stack.equals(conflict_step, true, zero, true); let real = stack.number_u32(entry_point); let _micro = stack.byte(0); @@ -778,12 +779,12 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { } ChallengeType::EntryPoint { prover_read_pc, - prover_trace_step, + prover_conflict_step_tk, real_entry_point, } => { stack.number_u32(prover_read_pc.pc.get_address()); stack.byte(prover_read_pc.pc.get_micro()); - stack.number_u64(*prover_trace_step); + stack.number_u64(*prover_conflict_step_tk); entry_point_challenge(&mut stack, real_entry_point.unwrap()); } ChallengeType::ProgramCounter { @@ -1052,10 +1053,10 @@ mod tests { #[test] fn test_entry_point_challenge() { - assert!(test_entry_point_challenge_aux(0x1234, 0, 1, 0x2222)); - assert!(test_entry_point_challenge_aux(0x2222, 1, 1, 0x2222)); - assert!(!test_entry_point_challenge_aux(0x1234, 0, 1, 0x1234)); - assert!(!test_entry_point_challenge_aux(0x1234, 0, 2, 0x2222)); + assert!(test_entry_point_challenge_aux(0x1234, 0, 0, 0x2222)); + assert!(test_entry_point_challenge_aux(0x2222, 1, 0, 0x2222)); + assert!(!test_entry_point_challenge_aux(0x1234, 0, 0, 0x1234)); + assert!(!test_entry_point_challenge_aux(0x1234, 0, 1, 0x2222)); } fn test_program_counter_challenge_aux( @@ -2478,7 +2479,7 @@ mod tests { entry_point_challenge(&mut stack, entry_point_real); stack.op_true(); - let expected_to_succeed = wots_step == 1 && wots_prover_pc != entry_point_real; + let expected_to_succeed = wots_step == 0 && wots_prover_pc != entry_point_real; stack.run().success == expected_to_succeed } @@ -2805,9 +2806,9 @@ mod tests { let wots_prover_pc: u32 = rng.random(); let wots_prover_micro: u8 = rng.random(); let wots_step: u64 = if rng.random_bool(0.5) { - 1 + 0 } else { - rng.random_range(0..100) + rng.random_range(1..100) }; let entry_point_real: u32 = rng.random(); @@ -3494,65 +3495,59 @@ mod tests { TestCase { description: "Standard case: Prover's PC is simply wrong.", prover_pc: 0x80000004, - step: 1, + step: 0, entry_point: 0x80000000, }, TestCase { description: "Border case: Prover's PC is 0, but the entry point is not.", prover_pc: 0, - step: 1, + step: 0, entry_point: 0x80000000, }, TestCase { description: "Border case: Prover's PC is u32::MAX, but the entry point is not.", prover_pc: u32::MAX, - step: 1, + step: 0, entry_point: 0x80000000, }, TestCase { description: "Border case: Entry point is 0, but the prover's PC is not.", prover_pc: 0x80000000, - step: 1, + step: 0, entry_point: 0, }, TestCase { description: "Border case: Entry point is u32::MAX, but the prover's PC is not.", prover_pc: 0, - step: 1, + step: 0, entry_point: u32::MAX, }, // --- Failure Scenarios (Challenge should fail: Prover is correct or conditions not met) --- TestCase { description: - "Failure Case: Prover is correct (step is 1, PC matches entry point).", + "Failure Case: Prover is correct (step is 0, PC matches entry point).", prover_pc: 0x80000000, - step: 1, + step: 0, entry_point: 0x80000000, }, TestCase { description: "Border case: Prover is correct at entry point 0.", prover_pc: 0, - step: 1, + step: 0, entry_point: 0, }, TestCase { description: "Border case: Prover is correct at entry point u32::MAX.", prover_pc: u32::MAX, - step: 1, - entry_point: u32::MAX, - }, - TestCase { - description: "Failure Case: The step number is 0, PC check is irrelevant.", - prover_pc: 0x80000004, step: 0, - entry_point: 0x80000000, + entry_point: u32::MAX, }, TestCase { - description: "Failure Case: The step number is 2, PC check is irrelevant.", + description: "Failure Case: The step number is 1, PC check is irrelevant.", prover_pc: 0x80000004, - step: 2, + step: 1, entry_point: 0x80000000, }, TestCase { diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index 4124fdf..4d4e247 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -28,7 +28,7 @@ pub enum ChallengeType { }, EntryPoint { prover_read_pc: TraceReadPC, - prover_trace_step: u64, + prover_conflict_step_tk: u64, real_entry_point: Option, }, ProgramCounter { diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index e3f6607..0fa9a34 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -560,7 +560,7 @@ pub fn verifier_choose_challenge( info!("Verifier choose to challenge ENTRYPOINT"); return Ok(ChallengeType::EntryPoint { prover_read_pc: trace.read_pc, - prover_trace_step: trace.step_number, + prover_conflict_step_tk: step, real_entry_point: return_script_parameters.then_some(program.pc.get_address()), //this parameter is only used for the test }); } else { From 8ce577ee58e4eab8aac2e924399f1ab2efddf0c3 Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 27 Nov 2025 15:53:00 -0300 Subject: [PATCH 12/22] add verify_challenge_step --- .../src/riscv/script_utils.rs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index 8fb20b8..3780ebe 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1781,6 +1781,21 @@ pub fn increment_var(stack: &mut StackTracker, var: StackVariable) -> StackVaria stack.from_altstack_joined(nibbles, "inc") } +pub fn verify_challenge_step( + stack: &mut StackTracker, + step: StackVariable, + decisions: StackVariable, + nary: u8, + nary_last_round: u8, + rounds: u8, +) { + let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + var_to_decisions_in_altstack(stack, tables, step, nary_last_round, nary, rounds); + let converted_step = stack.from_altstack_joined(rounds as u32, "converted_step"); + stack.equals(decisions, true, converted_step, true); + tables.drop(stack); +} + #[cfg(test)] mod tests { use bitvmx_cpu_definitions::memory::MemoryWitness; @@ -2651,29 +2666,20 @@ mod tests { nary_last_round: u8, nary: u8, ) { + let rounds = decisions.len(); let stack = &mut StackTracker::new(); - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - for decision in decisions.iter().rev() { + for decision in decisions.iter() { stack.number(*decision); } - let var = stack.number_u64(step); - var_to_decisions_in_altstack( - stack, - tables, - var, - nary_last_round, - nary, - decisions.len() as u8, - ); - for _ in 0..decisions.len() { - stack.from_altstack(); - stack.op_equalverify(); - } + let decisions = stack.join_in_stack(rounds as u32, None, Some("decisions_bits")); + let step = stack.number_u64(step); + + verify_challenge_step(stack, step, decisions, nary, nary_last_round, rounds as u8); - tables.drop(stack); stack.op_true(); + assert!(stack.run().success); } From b9b446f10df9aeb069fa35fe6509c292d3e1114e Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 27 Nov 2025 16:09:25 -0300 Subject: [PATCH 13/22] revert add_with_bit_extension changes --- bitcoin-script-riscv/src/riscv/operations.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/operations.rs b/bitcoin-script-riscv/src/riscv/operations.rs index 4589023..932b4cd 100644 --- a/bitcoin-script-riscv/src/riscv/operations.rs +++ b/bitcoin-script-riscv/src/riscv/operations.rs @@ -48,21 +48,20 @@ pub fn add_with_bit_extension( ) -> StackVariable { stack.set_breakpoint("add_with_bit_extension"); - let value_size = stack.get_size(value); //move the value and split the nibbles stack.move_var(value); stack.explode(value); let add_size = stack.get_size(to_add); let mut last = StackVariable::null(); - for i in 0..value_size { + for i in 0..8 { if i > 0 { stack.op_add(); } if i < add_size { stack.move_var_sub_n(to_add, add_size - i - 1); - } else if i < value_size - 1 { + } else if i < 7 { stack.copy_var(bit_extension); } else { stack.move_var(bit_extension); @@ -70,24 +69,24 @@ pub fn add_with_bit_extension( stack.op_add(); - if i < value_size - 1 { + if i < 7 { stack.op_dup(); } last = stack.get_value_from_table(tables.modulo, None); - if i < value_size - 1 { + if i < 7 { stack.to_altstack(); stack.get_value_from_table(tables.quotient, None); } } - for _ in 0..value_size - 1 { + for _ in 0..7 { stack.from_altstack(); } stack.rename(last, "add_bit_ext"); - stack.join_count(last, value_size - 1) + stack.join_count(last, 7) } pub fn sub( From 6abac93b7210c8e3998fafbe672477f7160fc054 Mon Sep 17 00:00:00 2001 From: crivasr Date: Thu, 27 Nov 2025 16:55:16 -0300 Subject: [PATCH 14/22] fix comment --- bitcoin-script-riscv/src/riscv/challenges.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 3b95287..dd46898 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -2953,7 +2953,7 @@ mod tests { } } } else { - // To fail, either the steps mismatch, OR the instruction is the success , OR the hash is different. + // To fail, either the steps mismatch, OR the instruction is the success ecall, AND the hash is correct. if rng.random_bool(0.5) { // Scenario: Steps mismatch. Instruction can be anything. trace_step = last_step.wrapping_add(rng.random_range(1..u64::MAX)); @@ -2962,7 +2962,7 @@ mod tests { opcode = rng.random(); hash = rng.random(); } else { - // Scenario: Steps match, but it's the valid success ecall. + // Scenario: Steps match, but it's the valid success ecall and correct hash. trace_step = last_step; read_value_1 = SUCCESS_ECALL_VAL1; read_value_2 = SUCCESS_ECALL_VAL2; From 7e5218f699d97a2144ff2b1d3003b888fe84ed2d Mon Sep 17 00:00:00 2001 From: Martin Jonas Date: Wed, 10 Dec 2025 14:41:57 -0300 Subject: [PATCH 15/22] update submodule --- docker-riscv32 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-riscv32 b/docker-riscv32 index 4357dc1..85701a7 160000 --- a/docker-riscv32 +++ b/docker-riscv32 @@ -1 +1 @@ -Subproject commit 4357dc103c270a7b373cdbe27767ed864795bb93 +Subproject commit 85701a7d2fc019e9af3ddae33ff6d091819409f6 From 0e26c9099028a732176940b2c321832081e7d295 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 15:31:02 -0300 Subject: [PATCH 16/22] add test cases and comments for var_to_decisions_in_altstack --- .../src/riscv/script_utils.rs | 98 +++++++++++++++---- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index 3780ebe..3e8aa6c 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1639,6 +1639,13 @@ pub fn split(stack: &mut StackTracker, tables: &StackTables, right_size: u8) { shift(stack, &tables.rshift, BITS_NIBBLE - right_size); } +// Convert a number_u64 into a sequence of decision bits, each containing +// either 'nary_last_round' bits for the last one or 'nary' bits for the rest. +// +// It accumulates bits into a temporary variable, and once enough bits are +// collected for one output decision, the decision is pushed to the altstack. +// 'remaining_bits': how many bits are still needed for the current decision. +// 'start_position': where in the accumulator the next nibble fragment lands. pub fn var_to_decisions_in_altstack( stack: &mut StackTracker, tables: &StackTables, @@ -1657,57 +1664,92 @@ pub fn var_to_decisions_in_altstack( stack.move_var(var); stack.explode(var); - stack.number(0); - let nibbles_needed = - (remaining_bits + bits_nary_round * (rounds - 1) + BITS_NIBBLE - 1) / BITS_NIBBLE; + stack.number(0); // set accumulator to 0 + + let mut nibbles_used = 0; + let mut remaining_rounds = rounds; - for _ in 0..nibbles_needed { + while remaining_rounds > 0 { + nibbles_used += 1; + // Case 1: nibble has less bits than we need if remaining_bits > BITS_NIBBLE { + // Directly add full nibble into accumulator at start_position stack.op_swap(); shift(stack, &tables.lshift, start_position); stack.op_add(); + + // Since we added the current nibble of 'BITS_NIBBLE' bits + // we should update the variables accordingly start_position += BITS_NIBBLE; remaining_bits -= BITS_NIBBLE; - } else if remaining_bits == BITS_NIBBLE { + } + // Case 2: nibble exactly fills the remaining bits + else if remaining_bits == BITS_NIBBLE { + // Directly add full nibble into accumulator at start_position stack.op_swap(); shift(stack, &tables.lshift, start_position); stack.op_add(); + + // Accumulator is now complete we should push it to altstack + // and reset the variables start_position = 0; remaining_bits = bits_nary_round; + remaining_rounds -= 1; stack.to_altstack(); + // Reset accumulator stack.number(0); - } else { - let times_needed = - 1 + (BITS_NIBBLE - remaining_bits + bits_nary_round - 1) / bits_nary_round; + } + // Case 3: nibble contains more bits than we need, this nibble will span at least 2 rounds + else { + // How many bits of the current nibble are still unprocessed let mut current_bits = BITS_NIBBLE; - for _ in 0..times_needed { + while current_bits > 0 && remaining_rounds > 0 { stack.op_swap(); + + // Split nibble into high and low fragments: + // low = remaining_bits; high = rest + // high fragment is used in the next iteration and will be splited again split(stack, tables, remaining_bits); + + // Add low fragment at correct position into accumulator shift(stack, &tables.lshift, start_position); stack.op_rot(); stack.op_add(); if remaining_bits <= current_bits { + // We used remaining_bits of our current_bits and compleated a round current_bits -= remaining_bits; remaining_bits = bits_nary_round; start_position = 0; + + // Push completed accumulator to altstack + remaining_rounds -= 1; stack.to_altstack(); + // Reset accumulator stack.number(0); } else { + // We used remaining_bits of our current_bits but it wasn't enough to complete a round + // we update the tracking variables so the next nibble completes the round remaining_bits -= current_bits; start_position += current_bits; + current_bits = 0; } } + + // the last high fragment is still in the stack and it should be 0 since + // we used all the bits of the nibble or all rounds have beed completed stack.op_swap(); stack.number(0); stack.op_equalverify(); } } + // last unfilled accumulator is still in the stack and it should be 0 stack.number(0); stack.op_equalverify(); - for _ in 0..(16 - nibbles_needed) { + // the rest of the unused nibbles should be 0 too + for _ in 0..(16 - nibbles_used) { stack.number(0); stack.op_equalverify(); } @@ -1791,7 +1833,11 @@ pub fn verify_challenge_step( ) { let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); var_to_decisions_in_altstack(stack, tables, step, nary_last_round, nary, rounds); - let converted_step = stack.from_altstack_joined(rounds as u32, "converted_step"); + let converted_step = if rounds == 1 { + stack.from_altstack() + } else { + stack.from_altstack_joined(rounds as u32, "converted_step") + }; stack.equals(decisions, true, converted_step, true); tables.drop(stack); } @@ -2685,13 +2731,29 @@ mod tests { #[test] fn test_var_to_decisions_in_altstack() { - test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 0], 1104, 4, 8); - test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 1], 1105, 4, 8); - test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 2], 1106, 4, 8); - test_var_to_decisions_in_altstack_aux(&[4, 2, 4, 3], 1107, 4, 8); - - test_var_to_decisions_in_altstack_aux(&[0, 3, 0, 3], 99, 4, 8); - test_var_to_decisions_in_altstack_aux(&[1, 3, 0, 3], 355, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b00], 0b100_010_100_00, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b01], 0b100_010_100_01, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b10], 0b100_010_100_10, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b11], 0b100_010_100_11, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b010, 0b001, 0b111, 0b01], 0b010_001_111_01, 4, 8); + + test_var_to_decisions_in_altstack_aux(&[0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0], 0b011010010110, 0, 2); + + test_var_to_decisions_in_altstack_aux(&[0], 0, 0, 2); + test_var_to_decisions_in_altstack_aux(&[1], 1, 0, 2); + + test_var_to_decisions_in_altstack_aux(&[0b1010, 0b1101, 0b0010, 0b0101], 0b1010_1101_0010_0101, 0, 16); + + test_var_to_decisions_in_altstack_aux(&[0b11, 0b11, 0b11, 0b1], 0b11_11_11_1, 2, 4); + test_var_to_decisions_in_altstack_aux(&[0, 0, 0, 0, 0, 0, 0, 0], 0, 2, 16); + test_var_to_decisions_in_altstack_aux( + &[ + 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, + ], + 0xffff_ffff_ffff_ffff, + 0, + 16, + ); } fn test_increment_decision_aux(decision: u64, rounds: u8, nary: u8, nary_last_round: u8) { From b7ba413e01d3d866f1a9dc7355616b69d3bfe14e Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 15:31:39 -0300 Subject: [PATCH 17/22] add tests for second nary resign challenge --- emulator/src/decision/challenge.rs | 93 ++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index 0fa9a34..0d014c2 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -2427,6 +2427,99 @@ mod tests { ); } + #[test] + fn test_challenge_equivocation_resign_step_hash_second_nary() { + init_trace(); + let fail_read_args = vec!["1106", "0xaa000000", "0x11111100", "0xaa000000", "769"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let fail_config = Some(FailConfiguration::new_fail_reads(FailReads::new( + None, + Some(&fail_read_args), + ))); + + let fail_resign = Some(FailConfiguration::new_fail_resign_hash(768)); + + test_challenge_aux( + "57", + "hello-world.yaml", + 17, + false, + fail_config.clone(), + fail_resign.clone(), + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "58", + "hello-world.yaml", + 17, + false, + None, + None, + fail_config, + fail_resign, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::EquivocationResign(EquivocationKind::StepHash), + ); + } + + #[test] + fn test_challenge_equivocation_resign_next_hash_second_nary() { + init_trace(); + let fail_read_args = vec!["1106", "0xaa000000", "0x11111100", "0xaa000000", "769"] + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let fail_config = Some(FailConfiguration::new_fail_reads(FailReads::new( + None, + Some(&fail_read_args), + ))); + + let fail_resign = Some(FailConfiguration::new_fail_resign_hash(769)); + + test_challenge_aux( + "57", + "hello-world.yaml", + 17, + false, + fail_config.clone(), + fail_resign.clone(), + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "58", + "hello-world.yaml", + 17, + false, + None, + None, + fail_config, + fail_resign, + false, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::EquivocationResign(EquivocationKind::NextHash), + ); + } + + #[test] fn test_challenge_pc_read_from_non_code() { init_trace(); From 53e8ba08c830abf48002b421e4ad4e0631491134 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 15:32:20 -0300 Subject: [PATCH 18/22] change comment for halt_challenge --- bitcoin-script-riscv/src/riscv/challenges.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index dd46898..6650031 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -179,9 +179,8 @@ pub fn program_counter_challenge(stack: &mut StackTracker) { stack.equals(result, true, prover_prev_hash, true); } -// When the prover commits the final step as halt but it's is not a halt-success instruction the verifier can challenge it. -// TODO CHECK: The opcode needs to be provided and compared with the static opcode given by the PC on the proper leaf when expanding the trace -// WOTS_PROVER_FINAL_STEP:16 == WOTS_PROVER_TRACE_STEP:16 && ( WOTS_PROVER_READ_VALUE_1:8 | WOTS_PROVER_READ_VALUE_2:8 | WOTS_PROVER_OPCODE:8 != 93 | 0 | 115 ) +// When the prover commits the final step as halt but it's is not a halt-success instruction or the hash is different, the verifier can challenge it. +// WOTS_PROVER_FINAL_STEP:16 == WOTS_PROVER_TRACE_STEP:16 && ( WOTS_PROVER_READ_VALUE_1:8 | WOTS_PROVER_READ_VALUE_2:8 | WOTS_PROVER_OPCODE:8 | WOTS_PROVER_NEXT_HASH_TK != 93 | 0 | 115 | WOTS_PROVER_FINAL_HASH ) pub fn halt_challenge(stack: &mut StackTracker) { stack.clear_definitions(); From 5232a5799e12c4cbeffe06a7ec61052f238e2fa0 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 17:18:00 -0300 Subject: [PATCH 19/22] maintain consistent nary / nary_last_round parameter order across functions --- bitcoin-script-riscv/src/riscv/challenges.rs | 4 +- .../src/riscv/script_utils.rs | 52 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 6650031..c16433f 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -685,7 +685,7 @@ pub fn equivocation_resign_challenge( // TODO: Optimize this... let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - var_to_decisions_in_altstack(stack, tables, step, nary_last_round, nary, rounds); + var_to_decisions_in_altstack(stack, tables, step, nary, nary_last_round, rounds); tables.drop(stack); let mut decisions_bits = stack.from_altstack_joined(rounds as u32, "decisions_bits"); @@ -697,7 +697,7 @@ pub fn equivocation_resign_challenge( nary_last_round - 1 }; - next_decision_in_altstack(stack, decisions_bits, rounds, max_last_round, max_nary); + next_decision_in_altstack(stack, decisions_bits, rounds, max_nary, max_last_round); decisions_bits = stack.from_altstack_joined(rounds as u32, "next_decisions_bits"); } diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index 3e8aa6c..741a091 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1650,8 +1650,8 @@ pub fn var_to_decisions_in_altstack( stack: &mut StackTracker, tables: &StackTables, var: StackVariable, - nary_last_round: u8, nary: u8, + nary_last_round: u8, rounds: u8, ) { let bits_nary_round = f64::log2(nary as f64) as u8; @@ -1736,7 +1736,7 @@ pub fn var_to_decisions_in_altstack( } } - // the last high fragment is still in the stack and it should be 0 since + // the last high fragment is still in the stack and it should be 0 since // we used all the bits of the nibble or all rounds have beed completed stack.op_swap(); stack.number(0); @@ -1759,8 +1759,8 @@ pub fn next_decision_in_altstack( stack: &mut StackTracker, decisions_bits: StackVariable, rounds: u8, - max_last_round: u8, max_nary: u8, + max_last_round: u8, ) { stack.move_var(decisions_bits); stack.explode(decisions_bits); @@ -1832,7 +1832,7 @@ pub fn verify_challenge_step( rounds: u8, ) { let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); - var_to_decisions_in_altstack(stack, tables, step, nary_last_round, nary, rounds); + var_to_decisions_in_altstack(stack, tables, step, nary, nary_last_round, rounds); let converted_step = if rounds == 1 { stack.from_altstack() } else { @@ -2709,8 +2709,8 @@ mod tests { fn test_var_to_decisions_in_altstack_aux( decisions: &[u32], step: u64, - nary_last_round: u8, nary: u8, + nary_last_round: u8, ) { let rounds = decisions.len(); let stack = &mut StackTracker::new(); @@ -2731,28 +2731,38 @@ mod tests { #[test] fn test_var_to_decisions_in_altstack() { - test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b00], 0b100_010_100_00, 4, 8); - test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b01], 0b100_010_100_01, 4, 8); - test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b10], 0b100_010_100_10, 4, 8); - test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b11], 0b100_010_100_11, 4, 8); - test_var_to_decisions_in_altstack_aux(&[0b010, 0b001, 0b111, 0b01], 0b010_001_111_01, 4, 8); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b00], 0b100_010_100_00, 8, 4); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b01], 0b100_010_100_01, 8, 4); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b10], 0b100_010_100_10, 8, 4); + test_var_to_decisions_in_altstack_aux(&[0b100, 0b010, 0b100, 0b11], 0b100_010_100_11, 8, 4); + test_var_to_decisions_in_altstack_aux(&[0b010, 0b001, 0b111, 0b01], 0b010_001_111_01, 8, 4); - test_var_to_decisions_in_altstack_aux(&[0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0], 0b011010010110, 0, 2); + test_var_to_decisions_in_altstack_aux( + &[0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0], + 0b011010010110, + 2, + 0, + ); - test_var_to_decisions_in_altstack_aux(&[0], 0, 0, 2); - test_var_to_decisions_in_altstack_aux(&[1], 1, 0, 2); + test_var_to_decisions_in_altstack_aux(&[0], 0, 2, 0); + test_var_to_decisions_in_altstack_aux(&[1], 1, 2, 0); - test_var_to_decisions_in_altstack_aux(&[0b1010, 0b1101, 0b0010, 0b0101], 0b1010_1101_0010_0101, 0, 16); + test_var_to_decisions_in_altstack_aux( + &[0b1010, 0b1101, 0b0010, 0b0101], + 0b1010_1101_0010_0101, + 16, + 0, + ); - test_var_to_decisions_in_altstack_aux(&[0b11, 0b11, 0b11, 0b1], 0b11_11_11_1, 2, 4); - test_var_to_decisions_in_altstack_aux(&[0, 0, 0, 0, 0, 0, 0, 0], 0, 2, 16); + test_var_to_decisions_in_altstack_aux(&[0b11, 0b11, 0b11, 0b1], 0b11_11_11_1, 4, 2); + test_var_to_decisions_in_altstack_aux(&[0, 0, 0, 0, 0, 0, 0, 0], 0, 16, 2); test_var_to_decisions_in_altstack_aux( &[ 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, 0xf, ], 0xffff_ffff_ffff_ffff, - 0, 16, + 0, ); } @@ -2768,22 +2778,22 @@ mod tests { }; let decision_var = stack.number_u64(decision); - let next_decision_var = stack.number_u64(decision.wrapping_add(1)); + let next_decision_var = stack.number_u64(decision + 1); var_to_decisions_in_altstack( stack, tables, next_decision_var, - nary_last_round, nary, + nary_last_round, rounds, ); - var_to_decisions_in_altstack(stack, tables, decision_var, nary_last_round, nary, rounds); + var_to_decisions_in_altstack(stack, tables, decision_var, nary, nary_last_round, rounds); let decision = stack.from_altstack_joined(rounds as u32, "decision_bits"); - next_decision_in_altstack(stack, decision, rounds, max_last_round, max_nary); + next_decision_in_altstack(stack, decision, rounds, max_nary, max_last_round); let incremented_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); let expected_next_decision_bits = From 29e14b0f4576c9e5a158baf136d5a2f241b3252f Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 18:51:39 -0300 Subject: [PATCH 20/22] rename increment_decisions_in_altstack function, improve comments and tests --- bitcoin-script-riscv/src/riscv/challenges.rs | 4 +- .../src/riscv/script_utils.rs | 128 ++++++++++-------- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index c16433f..dc2fbac 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -11,7 +11,7 @@ use crate::riscv::{ memory_alignment::{is_aligned, load_lower_half_nibble_table, load_upper_half_nibble_table}, script_utils::{ address_in_sections, address_not_in_sections, get_selected_vars, increment_var, - is_lower_than, next_decision_in_altstack, var_to_decisions_in_altstack, + is_lower_than, increment_decisions_in_altstack, var_to_decisions_in_altstack, verify_wrong_chunk_value, witness_equals, StackTables, }, }; @@ -697,7 +697,7 @@ pub fn equivocation_resign_challenge( nary_last_round - 1 }; - next_decision_in_altstack(stack, decisions_bits, rounds, max_nary, max_last_round); + increment_decisions_in_altstack(stack, decisions_bits, rounds, max_nary, max_last_round); decisions_bits = stack.from_altstack_joined(rounds as u32, "next_decisions_bits"); } diff --git a/bitcoin-script-riscv/src/riscv/script_utils.rs b/bitcoin-script-riscv/src/riscv/script_utils.rs index 741a091..0328204 100644 --- a/bitcoin-script-riscv/src/riscv/script_utils.rs +++ b/bitcoin-script-riscv/src/riscv/script_utils.rs @@ -1755,7 +1755,12 @@ pub fn var_to_decisions_in_altstack( } } -pub fn next_decision_in_altstack( +// Convert a sequence of decision bits selecting a step into the sequence of +// decision bits selecting the next step, each containing numbers in the ranges +// [0, max_last_round] for the last one or [0, max_nary] for the rest. +// If max_last_round == max_nary, this is equivalent to incrementing a number +// encoded in base 2^(max_nary + 1). +pub fn increment_decisions_in_altstack( stack: &mut StackTracker, decisions_bits: StackVariable, rounds: u8, @@ -1765,61 +1770,64 @@ pub fn next_decision_in_altstack( stack.move_var(decisions_bits); stack.explode(decisions_bits); - stack.op_dup(); - stack.number(max_last_round as u32); - stack.op_equal(); - - let (mut overflow, mut no_overflow) = stack.open_if(); - no_overflow.op_1add(); - no_overflow.to_altstack(); - no_overflow.number(0); - no_overflow.to_altstack(); - - overflow.op_drop(); - overflow.number(0); - overflow.to_altstack(); - overflow.number(1); - overflow.to_altstack(); - - stack.end_if(overflow, no_overflow, 1, vec![], 2); + // We carry arround the number we want to add in the top of the altstack + stack.number(1); + stack.to_altstack(); - for _ in 1..rounds { + for round in (1..=rounds).rev() { + // Bring back the carry stack.from_altstack(); stack.number(1); stack.op_equal(); let (mut inc, mut no_inc) = stack.open_if(); + // If we won't increment, then we can just push the current number to the altstack no_inc.to_altstack(); - no_inc.number(0); + // And neither won't add to the next numbers + no_inc.number(0); // carry = 0 no_inc.to_altstack(); + // If we increment, we have to check if it will overflow inc.op_dup(); - inc.number(max_nary as u32); + // It will overflow if the current number is equal to the maximum value allowed for this round + inc.number(if round == rounds { + max_last_round + } else { + max_nary + } as u32); inc.op_equal(); let (mut overflow, mut no_overflow) = inc.open_if(); + // No overflow, we can safely increment the number no_overflow.op_1add(); no_overflow.to_altstack(); + + // No overflow, carry = 0 no_overflow.number(0); no_overflow.to_altstack(); - overflow.op_drop(); - overflow.number(0); + // Overflow, the number wraps to 0 + overflow.op_drop(); // first drop the number + overflow.number(0); // and replace it with zero overflow.to_altstack(); + + // Overflow, carry = 1 overflow.number(1); overflow.to_altstack(); + // In all branches we just consume the number in the top of the stack, and push 2 to the altstack (the incremented number and the carry) inc.end_if(overflow, no_overflow, 1, vec![], 2); stack.end_if(inc, no_inc, 1, vec![], 2); } + // At the end, there is still the carry in the altstack, we have to drop it stack.from_altstack(); stack.op_drop(); } pub fn increment_var(stack: &mut StackTracker, var: StackVariable) -> StackVariable { let nibbles = stack.get_size(var); - next_decision_in_altstack(stack, var, nibbles as u8, 15, 15); + increment_decisions_in_altstack(stack, var, nibbles as u8, 15, 15); stack.from_altstack_joined(nibbles, "inc") } @@ -2766,9 +2774,22 @@ mod tests { ); } - fn test_increment_decision_aux(decision: u64, rounds: u8, nary: u8, nary_last_round: u8) { + fn test_increment_decisions_aux( + decisions: &[u32], + expected_incremented_decisions: &[u32], + nary: u8, + nary_last_round: u8, + ) { + assert!(decisions.len() == expected_incremented_decisions.len()); + + let rounds = decisions.len() as u32; let stack = &mut StackTracker::new(); - let tables = &StackTables::new(stack, false, false, 0b111, 0b111, 0); + + for decision in decisions.iter() { + stack.number(*decision); + } + + let decisions = stack.join_in_stack(rounds, None, Some("decisions")); let max_nary = nary - 1; let max_last_round = if nary_last_round == 0 { @@ -2777,46 +2798,47 @@ mod tests { nary_last_round - 1 }; - let decision_var = stack.number_u64(decision); - let next_decision_var = stack.number_u64(decision + 1); - - var_to_decisions_in_altstack( - stack, - tables, - next_decision_var, - nary, - nary_last_round, - rounds, - ); - - var_to_decisions_in_altstack(stack, tables, decision_var, nary, nary_last_round, rounds); + increment_decisions_in_altstack(stack, decisions, rounds as u8, max_nary, max_last_round); + let incremented_decisions = + stack.from_altstack_joined(rounds as u32, "incremented_decisions"); - let decision = stack.from_altstack_joined(rounds as u32, "decision_bits"); - - next_decision_in_altstack(stack, decision, rounds, max_nary, max_last_round); - let incremented_decision_bits = stack.from_altstack_joined(rounds as u32, "decision_bits"); + for decision in expected_incremented_decisions.iter() { + stack.number(*decision); + } - let expected_next_decision_bits = - stack.from_altstack_joined(rounds as u32, "expected_decision_bits"); + let expected_incremented_decisions = + stack.join_in_stack(rounds, None, Some("expected_incremented_decisions")); stack.equals( - incremented_decision_bits, + incremented_decisions, true, - expected_next_decision_bits, + expected_incremented_decisions, true, ); - tables.drop(stack); stack.op_true(); assert!(stack.run().success); } #[test] - fn test_next_decision() { - test_increment_decision_aux(100, 4, 8, 4); - test_increment_decision_aux(302, 8, 8, 2); - test_increment_decision_aux(38, 8, 2, 2); - test_increment_decision_aux(892, 4, 8, 0); + fn test_increment_decisions() { + test_increment_decisions_aux(&[0, 0, 0, 0], &[0, 0, 0, 1], 16, 8); + test_increment_decisions_aux(&[0, 0, 0, 4], &[0, 0, 0, 5], 16, 8); + + test_increment_decisions_aux(&[0, 0, 0, 7], &[0, 0, 1, 0], 16, 8); + test_increment_decisions_aux(&[0, 0, 2, 7], &[0, 0, 3, 0], 16, 8); + + test_increment_decisions_aux(&[0, 15, 15, 7], &[1, 0, 0, 0], 16, 8); + test_increment_decisions_aux(&[1, 15, 15, 7], &[2, 0, 0, 0], 16, 8); + + test_increment_decisions_aux(&[1, 15, 15, 15], &[2, 0, 0, 0], 16, 0); + + test_increment_decisions_aux(&[2, 1, 3, 1], &[2, 2, 0, 0], 4, 2); + + // This happens if conflict_step == max_step and it is not allowed since conflict_step is one less than the actual + // step the verifier asks the trace for. So it is actually selectiong the 'max_step+1' step, and the prover can + // challenge this since it will also be bigger than his last_step + test_increment_decisions_aux(&[15, 15, 15, 15], &[0, 0, 0, 0], 16, 0); } mod fuzz_tests { From c374b1e7a5daf4e43db2985de720e50737212ab2 Mon Sep 17 00:00:00 2001 From: crivasr Date: Wed, 10 Dec 2025 19:24:09 -0300 Subject: [PATCH 21/22] explain get_round_and_index function --- bitcoin-script-riscv/src/riscv/challenges.rs | 37 +++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index dc2fbac..a6e535b 100644 --- a/bitcoin-script-riscv/src/riscv/challenges.rs +++ b/bitcoin-script-riscv/src/riscv/challenges.rs @@ -636,25 +636,44 @@ pub fn future_read_challenge(stack: &mut StackTracker) { stack.op_verify(); } -fn get_round_and_index(stack: &mut StackTracker, decisions_bits: StackVariable, round: u8) { - if round == 0 { +// We can get the round the selected hash was provided by counting from the end +// how many consecutive zeros we have. +// The first non zero is the index counting from 1. +// Example: +// [4,0,2,0,0]: the hash was provided in the third round, in the second index. +// [4,0,2,0,1]: the hash was provided in the fifth round, in the first index. +// Edge case: +// [0,0,0,0,0]: here our algorithm will say it was in the 0 round 0 index which +// doesn't exist (we start rounds and indices from 1), this points to the +// initial step hash which was never provided, it is already known and we can't +// challenge it in the resign challenge, we should use trace_hash_zero for that +// +// The 'rounds' parameter is the number of rounds in the nary search, but it will +// also be used to call the function recursively and track which round we are +// currently looking at. +fn get_round_and_index(stack: &mut StackTracker, decisions_bits: StackVariable, rounds: u8) { + if rounds == 0 { + // If we reach the round 0 then we are at the invalid edge case stack.number(0); stack.number(0); } else { - stack.copy_var_sub_n(decisions_bits, round as u32 - 1); + stack.copy_var_sub_n(decisions_bits, rounds as u32 - 1); stack.number(0); stack.op_equal(); - let (mut if_true, mut if_false) = stack.open_if_with_debug(); + let (mut is_zero, mut is_not_zero) = stack.open_if_with_debug(); - if_false.number(round as u32); - if_false.copy_var_sub_n(decisions_bits, round as u32 - 1); + // If the current bits aren't zero then this is where the hash was provided + // (assuming that we got here because all the previous one were zero) + is_not_zero.number(rounds as u32); + is_not_zero.copy_var_sub_n(decisions_bits, rounds as u32 - 1); - get_round_and_index(&mut if_true, decisions_bits, round - 1); + // If they are zero, then we call recursively for the previous round. + get_round_and_index(&mut is_zero, decisions_bits, rounds - 1); stack.end_if( - if_true, - if_false, + is_zero, + is_not_zero, 0, vec![ (1, "calculated_round".to_string()), From a3399f9038b0939c141ca5e35310482e7188b35e Mon Sep 17 00:00:00 2001 From: crivasr Date: Fri, 19 Dec 2025 11:10:20 -0300 Subject: [PATCH 22/22] allow prover to challenge the selected step --- definitions/src/challenge.rs | 116 ++++++++++++++++------ emulator/src/decision/challenge.rs | 113 ++++++++++++++++----- emulator/src/executor/utils.rs | 29 ++++++ emulator/src/loader/program_definition.rs | 11 +- emulator/src/main.rs | 40 ++++---- emulator/tests/challenge.rs | 2 +- 6 files changed, 228 insertions(+), 83 deletions(-) diff --git a/definitions/src/challenge.rs b/definitions/src/challenge.rs index 4d4e247..3c65921 100644 --- a/definitions/src/challenge.rs +++ b/definitions/src/challenge.rs @@ -130,6 +130,78 @@ pub enum EquivocationKind { NextHash, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ProverFinalTraceType { + FinalTraceWithHashesAndStep { + trace: TraceRWStep, + step_hash: String, + next_hash: String, + step: u64, + }, + ChallengeStep, +} + +impl ProverFinalTraceType { + pub fn as_final_trace_with_hashes_and_step( + &self, + ) -> Result<(TraceRWStep, String, String, u64), EmulatorResultError> { + match self { + Self::FinalTraceWithHashesAndStep { + trace, + step_hash, + next_hash, + step, + } => Ok((trace.clone(), step_hash.clone(), next_hash.clone(), *step)), + _ => Err(EmulatorResultError::GenericError( + "Expected FinalTraceWithHashesAndStep".to_string(), + )), + } + } + + pub fn as_challenge_step(&self) -> Result<(), EmulatorResultError> { + match self { + Self::ChallengeStep => Ok(()), + _ => Err(EmulatorResultError::GenericError( + "Expected ChallengeStep".to_string(), + )), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ProverHashesAndStepType { + HashesAndStep { + step_hash: String, + next_hash: String, + step: u64, + }, + ChallengeStep, +} + +impl ProverHashesAndStepType { + pub fn as_hashes_with_step(&self) -> Result<(String, String, u64), EmulatorResultError> { + match self { + Self::HashesAndStep { + step_hash, + next_hash, + step, + } => Ok((step_hash.clone(), next_hash.clone(), *step)), + _ => Err(EmulatorResultError::GenericError( + "Expected HashesAndStep".to_string(), + )), + } + } + + pub fn as_challenge_step(&self) -> Result<(), EmulatorResultError> { + match self { + Self::ChallengeStep => Ok(()), + _ => Err(EmulatorResultError::GenericError( + "Expected ChallengeStep".to_string(), + )), + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "data")] pub enum EmulatorResultType { @@ -151,15 +223,10 @@ pub enum EmulatorResultType { round: u8, }, ProverFinalTraceResult { - final_trace: TraceRWStep, - resigned_step_hash: String, - resigned_next_hash: String, - conflict_step: u64, + prover_final_trace: ProverFinalTraceType, }, ProverGetHashesAndStepResult { - resigned_step_hash: String, - resigned_next_hash: String, - write_step: u64, + prover_hashes_and_step: ProverHashesAndStepType, }, VerifierChooseChallengeResult { challenge: ChallengeType, @@ -174,8 +241,9 @@ pub enum EmulatorResultError { impl EmulatorResultType { pub fn from_value(value: serde_json::Value) -> Result { - serde_json::from_value(value) - .map_err(|e| EmulatorResultError::GenericError(format!("Failed to deserialize: {}", e))) + serde_json::from_value(value.clone()).map_err(|e| { + EmulatorResultError::GenericError(format!("Failed to deserialize: {} | {:?}", e, value)) + }) } pub fn to_value(&self) -> Result { @@ -229,38 +297,22 @@ impl EmulatorResultType { } } - pub fn as_final_trace( - &self, - ) -> Result<(TraceRWStep, String, String, u64), EmulatorResultError> { + pub fn as_final_trace(&self) -> Result { match self { - EmulatorResultType::ProverFinalTraceResult { - final_trace, - resigned_step_hash, - resigned_next_hash, - conflict_step, - } => Ok(( - final_trace.clone(), - resigned_step_hash.to_string(), - resigned_next_hash.to_string(), - *conflict_step, - )), + EmulatorResultType::ProverFinalTraceResult { prover_final_trace } => { + Ok(prover_final_trace.clone()) + } _ => Err(EmulatorResultError::GenericError( "Expected ProverFinalTraceResult".to_string(), )), } } - pub fn as_hashes_and_step(&self) -> Result<(String, String, u64), EmulatorResultError> { + pub fn as_hashes_and_step(&self) -> Result { match self { EmulatorResultType::ProverGetHashesAndStepResult { - resigned_step_hash, - resigned_next_hash, - write_step, - } => Ok(( - resigned_step_hash.to_string(), - resigned_next_hash.to_string(), - *write_step, - )), + prover_hashes_and_step, + } => Ok(prover_hashes_and_step.clone()), _ => Err(EmulatorResultError::GenericError( "Expected ProverGetCosignedBitsAndHashesResult".to_string(), )), diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index 0d014c2..03d9358 100644 --- a/emulator/src/decision/challenge.rs +++ b/emulator/src/decision/challenge.rs @@ -1,5 +1,5 @@ use bitvmx_cpu_definitions::{ - challenge::{ChallengeType, EquivocationKind}, + challenge::{ChallengeType, EquivocationKind, ProverFinalTraceType, ProverHashesAndStepType}, constants::{CHUNK_SIZE, LAST_STEP_INIT}, memory::Chunk, trace::{generate_initial_step_hash, hashvec_to_string, validate_step_hash, TraceRWStep}, @@ -15,7 +15,7 @@ use crate::{ execution_log::VerifierChallengeLog, nary_search::{choose_segment, ExecutionHashes, NArySearchType}, }, - executor::utils::FailConfiguration, + executor::utils::{FailConfiguration, FailSelectionBits}, loader::program_definition::ProgramDefinition, EmulatorError, ExecutionResult, }; @@ -80,6 +80,7 @@ pub fn prover_get_hashes_for_round( nary_type: NArySearchType, ) -> Result, EmulatorError> { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; + let last_step = challenge_log.execution.last_step; let input = challenge_log.input.clone(); let nary_log = challenge_log.get_nary_log(nary_type); let program_def = ProgramDefinition::from_config(program_definition_file)?; @@ -100,6 +101,7 @@ pub fn prover_get_hashes_for_round( round, nary_log.base_step, fail_config, + Some(last_step), )?; nary_log.hash_rounds.push(hashes.clone()); // at the first round the verifier hasn't decided anything yet @@ -202,7 +204,8 @@ pub fn verifier_choose_segment( input, round, nary_log.base_step, - fail_config, + fail_config.clone(), + None, )?; let claim_hashes = ExecutionHashes::from_hexstr(&prover_last_hashes); @@ -227,6 +230,21 @@ pub fn verifier_choose_segment( info!("Verifier selects bits: {bits} base: {base} selection: {new_selected}"); + let fail_selection = fail_config.and_then(|fail_config| { + fail_config + .fail_selection_bits + .and_then(|fail_selection_bits| match fail_selection_bits { + FailSelectionBits::Round { round, bits } => Some((round, bits)), + _ => None, + }) + }); + + if let Some((fail_round, fail_bits)) = fail_selection { + if fail_round == round { + return Ok(fail_bits as u32); + } + } + Ok(bits) } @@ -235,7 +253,7 @@ pub fn prover_final_trace( checkpoint_path: &str, final_bits: u32, fail_config: Option, -) -> Result<(TraceRWStep, String, String, u64), EmulatorError> { +) -> Result { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; let input = challenge_log.input.clone(); let nary_log = challenge_log.get_nary_log(NArySearchType::ConflictStep); @@ -248,22 +266,35 @@ pub fn prover_final_trace( nary_log.base_step = final_step; nary_log.verifier_decisions.push(final_bits - 1); - - info!("The prover needs to provide the full trace for the selected step {final_step}"); - let final_trace = - program_def.get_trace_step(checkpoint_path, input, final_step, fail_config.clone())?; - nary_log.final_trace = final_trace.clone(); challenge_log.save(checkpoint_path)?; - let (step_hash, next_hash, conflict_step) = prover_get_hashes_and_step( + if let ProverHashesAndStepType::HashesAndStep { + step_hash, + next_hash, + step, + } = prover_get_hashes_and_step( program_definition_file, checkpoint_path, NArySearchType::ConflictStep, None, - fail_config, - )?; - - Ok((final_trace, step_hash, next_hash, conflict_step)) + fail_config.clone(), + )? { + info!("The prover needs to provide the full trace for the selected step {final_step}"); + let final_trace = + program_def.get_trace_step(checkpoint_path, input, final_step, fail_config)?; + let nary_log = challenge_log.get_nary_log(NArySearchType::ConflictStep); + nary_log.final_trace = final_trace.clone(); + challenge_log.save(checkpoint_path)?; + + Ok(ProverFinalTraceType::FinalTraceWithHashesAndStep { + trace: final_trace, + step_hash, + next_hash, + step, + }) + } else { + Ok(ProverFinalTraceType::ChallengeStep) + } } pub fn prover_get_hashes_and_step( @@ -272,8 +303,10 @@ pub fn prover_get_hashes_and_step( nary_type: NArySearchType, final_bits: Option, fail_config: Option, -) -> Result<(String, String, u64), EmulatorError> { +) -> Result { let mut challenge_log = ProverChallengeLog::load(checkpoint_path)?; + let last_step = challenge_log.execution.last_step; + let conflict_step = challenge_log.conflict_step_log.base_step - 1; let nary_log = challenge_log.get_nary_log(nary_type); let program_def = ProgramDefinition::from_config(program_definition_file)?; @@ -281,15 +314,24 @@ pub fn prover_get_hashes_and_step( let total_rounds = nary_def.total_rounds(); - let final_step = match nary_type { - NArySearchType::ConflictStep => nary_log.base_step - 1, + let (final_step, max_step) = match nary_type { + NArySearchType::ConflictStep => (conflict_step, last_step - 1), _ => { let final_bits = final_bits.unwrap(); nary_log.verifier_decisions.push(final_bits); - nary_def.step_from_base_and_bits(total_rounds, nary_log.base_step, final_bits) + let final_step = + nary_def.step_from_base_and_bits(total_rounds, nary_log.base_step, final_bits); + (final_step, conflict_step) } }; + if final_step > max_step + || fail_config + .as_ref() + .is_some_and(|fail_config| fail_config.fail_prover_challenge_step) + { + return Ok(ProverHashesAndStepType::ChallengeStep); + } let (mut step_hash, mut next_hash) = get_hashes( &nary_def.step_mapping(&nary_log.verifier_decisions), &nary_log.hash_rounds, @@ -304,7 +346,11 @@ pub fn prover_get_hashes_and_step( } } - Ok((step_hash, next_hash, final_step)) + Ok(ProverHashesAndStepType::HashesAndStep { + step_hash, + next_hash, + step: final_step, + }) } pub fn get_hashes( @@ -500,7 +546,7 @@ pub fn verifier_choose_challenge( checkpoint_path, verifier_log.input.clone(), Some(steps), - fail_config, + fail_config.clone(), false, )? .1; @@ -698,7 +744,7 @@ pub fn verifier_choose_challenge( conflict_last_step.max(my_conflict_last_step) }; - let bits = nary_def.step_bits_for_round(1, step_to_challenge - 1); + let mut bits = nary_def.step_bits_for_round(1, step_to_challenge - 1); let read_challenge_log = &mut verifier_log.read_challenge_log; read_challenge_log.step_to_challenge = step_to_challenge - 1; @@ -717,6 +763,19 @@ pub fn verifier_choose_challenge( verifier_log.read_selector = read_selector; verifier_log.save(checkpoint_path)?; + let fail_selection = fail_config.and_then(|fail_config| { + fail_config + .fail_selection_bits + .and_then(|fail_selection_bits| match fail_selection_bits { + FailSelectionBits::Challenge { bits } => Some(bits), + _ => None, + }) + }); + + if let Some(fail_bits) = fail_selection { + bits = fail_bits as u32; + } + return Ok(ChallengeType::ReadValueNArySearch { bits }); } } @@ -1039,7 +1098,10 @@ mod tests { //PROVER PROVIDES EXECUTE STEP (and reveals full_trace) //Use v_desision + 1 as v_decision defines the last agreed step let (final_trace, step_hash, next_hash, _) = - prover_final_trace(pdf, chk_prover_path, v_decision + 1, fail_config_prover).unwrap(); + prover_final_trace(pdf, chk_prover_path, v_decision + 1, fail_config_prover) + .unwrap() + .as_final_trace_with_hashes_and_step() + .unwrap(); info!("Prover final trace: {:?}", final_trace.to_csv()); let result = verify_script(&final_trace, REGISTERS_BASE_ADDRESS, &None); @@ -1099,6 +1161,8 @@ mod tests { Some(v_decision), fail_config_prover_read_challenge, ) + .unwrap() + .as_hashes_with_step() .unwrap(); verifier_choose_challenge_for_read_challenge( @@ -2489,7 +2553,7 @@ mod tests { let fail_resign = Some(FailConfiguration::new_fail_resign_hash(769)); test_challenge_aux( - "57", + "59", "hello-world.yaml", 17, false, @@ -2504,7 +2568,7 @@ mod tests { ); test_challenge_aux( - "58", + "60", "hello-world.yaml", 17, false, @@ -2519,7 +2583,6 @@ mod tests { ); } - #[test] fn test_challenge_pc_read_from_non_code() { init_trace(); diff --git a/emulator/src/executor/utils.rs b/emulator/src/executor/utils.rs index 8667164..6273a85 100644 --- a/emulator/src/executor/utils.rs +++ b/emulator/src/executor/utils.rs @@ -216,6 +216,21 @@ impl FailOpcode { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum FailSelectionBits { + Round { round: u8, bits: u8 }, + Challenge { bits: u8 }, +} + +impl FailSelectionBits { + pub fn new(round: Option, bits: u8) -> Self { + round.map_or_else( + || Self::Challenge { bits }, + |round| Self::Round { round, bits }, + ) + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct FailConfiguration { pub fail_hash: Option, @@ -230,6 +245,8 @@ pub struct FailConfiguration { pub fail_execute_only_protection: bool, pub fail_commitment_step: Option, pub fail_commitment_hash: bool, + pub fail_selection_bits: Option, + pub fail_prover_challenge_step: bool, } impl FailConfiguration { @@ -239,12 +256,24 @@ impl FailConfiguration { ..Default::default() } } + pub fn new_fail_selection_bits(round: Option, bits: u8) -> Self { + Self { + fail_selection_bits: Some(FailSelectionBits::new(round, bits)), + ..Default::default() + } + } pub fn new_fail_commitment_step(last_step: u64) -> Self { Self { fail_commitment_step: Some(last_step), ..Default::default() } } + pub fn new_fail_prover_challenge_step() -> Self { + Self { + fail_prover_challenge_step: true, + ..Default::default() + } + } pub fn new_fail_commitment_hash() -> Self { Self { fail_commitment_hash: true, diff --git a/emulator/src/loader/program_definition.rs b/emulator/src/loader/program_definition.rs index eaa79ff..f2ab47a 100644 --- a/emulator/src/loader/program_definition.rs +++ b/emulator/src/loader/program_definition.rs @@ -1,6 +1,7 @@ use bitvmx_cpu_definitions::trace::{hash_to_string, TraceRWStep}; use config::Config; use serde::Deserialize; +use std::cmp::min; use thiserror::Error; use tracing::info; @@ -146,8 +147,9 @@ impl ProgramDefinition { Ok((result, last_step, last_hash)) } - //TODO: Check that the base is not higher that the reported finish step - //it might be necessary to enforce this in bitcoin script + // If the base is higher that the reported last step, we cap it to the last step + // so the prover can still generate hashes. + // After the nary search finishes, the prover can challenge this. pub fn get_round_hashes( &self, checkpoint_path: &str, @@ -155,7 +157,12 @@ impl ProgramDefinition { round: u8, base: u64, fail_config: Option, + last_step: Option, ) -> Result, EmulatorError> { + let base = match last_step { + Some(last_step) => min(last_step, base), + None => base, + }; let mut steps = self.nary_def().required_steps(round, base); info!( "Getting hashes for round: {} with steps: {:?}", diff --git a/emulator/src/main.rs b/emulator/src/main.rs index 4b001f0..5c37eaf 100644 --- a/emulator/src/main.rs +++ b/emulator/src/main.rs @@ -379,9 +379,11 @@ enum Commands { #[arg(long)] fail_resign_hash: Option, + /// Fail last_step commitment #[arg(long)] fail_commitment_step: Option, + /// Fail hash commitment #[arg(long)] fail_commitment_hash: bool, @@ -521,6 +523,8 @@ fn main() -> Result<(), EmulatorError> { fail_resign_hash: *fail_resign_hash, fail_commitment_step: *fail_commitment_step, fail_commitment_hash: *fail_commitment_hash, + fail_selection_bits: None, + fail_prover_challenge_step: false, }; let result = execute_program( &mut program, @@ -672,25 +676,17 @@ fn main() -> Result<(), EmulatorError> { fail_config_prover, command_file, }) => { - let (final_trace, resigned_step_hash, resigned_next_hash, conflict_step) = - prover_final_trace( - pdf, - checkpoint_prover_path, - *v_decision, - fail_config_prover.clone(), - )?; - info!("Prover final trace: {:?}", final_trace); - info!("Prover resigned step hash: {:?}", resigned_step_hash); - info!("Prover resigned next hash: {:?}", resigned_next_hash); - info!("Prover conflict step: {:?}", conflict_step); - - let result = EmulatorResultType::ProverFinalTraceResult { - final_trace, - resigned_step_hash, - resigned_next_hash, - conflict_step, - } - .to_value()?; + let prover_final_trace = prover_final_trace( + pdf, + checkpoint_prover_path, + *v_decision, + fail_config_prover.clone(), + )?; + info!("Prover final trace: {:?}", prover_final_trace); + + let result = + EmulatorResultType::ProverFinalTraceResult { prover_final_trace }.to_value()?; + let mut file = create_or_open_file(command_file); file.write_all(result.to_string().as_bytes()) .expect("Failed to write JSON to file"); @@ -760,7 +756,7 @@ fn main() -> Result<(), EmulatorError> { command_file, fail_config_prover, }) => { - let (resigned_step_hash, resigned_next_hash, write_step) = prover_get_hashes_and_step( + let prover_hashes_and_step = prover_get_hashes_and_step( pdf, &checkpoint_prover_path, NArySearchType::ReadValueChallenge, @@ -769,9 +765,7 @@ fn main() -> Result<(), EmulatorError> { )?; let result = EmulatorResultType::ProverGetHashesAndStepResult { - resigned_step_hash, - resigned_next_hash, - write_step, + prover_hashes_and_step, } .to_value()?; let mut file = create_or_open_file(command_file); diff --git a/emulator/tests/challenge.rs b/emulator/tests/challenge.rs index 8c993f0..3f90ab8 100644 --- a/emulator/tests/challenge.rs +++ b/emulator/tests/challenge.rs @@ -34,7 +34,7 @@ fn test_nary_search_trace_aux(input: u8, expect_err: bool, checkpoint_path: &str for round in 1..defs.total_rounds() + 1 { info!("Prover gets the steps required by the n-ary search round: {round}"); let reply_hashes = program_def - .get_round_hashes(checkpoint_path, input.clone(), round, base, None) + .get_round_hashes(checkpoint_path, input.clone(), round, base, None, None) .unwrap(); //get_hashes(&bad_trace, &steps); info!("Hashes: {:?}", reply_hashes);