diff --git a/bitcoin-script-riscv/src/riscv/challenges.rs b/bitcoin-script-riscv/src/riscv/challenges.rs index 19d1b5d..a6e535b 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}, @@ -10,7 +10,8 @@ use bitvmx_cpu_definitions::{ 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, + address_in_sections, address_not_in_sections, get_selected_vars, increment_var, + is_lower_than, increment_decisions_in_altstack, var_to_decisions_in_altstack, verify_wrong_chunk_value, witness_equals, StackTables, }, }; @@ -112,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); @@ -177,27 +179,37 @@ 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(); - 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 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"); - let expected = stack.number_u32(93); - stack.number_u32(0); - stack.number_u32(115); - stack.join_count(expected, 2); + // 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); - stack.not_equal(provided, true, expected, true); + let halt_syscall = stack.number_u32(0x5d); + let sucess_exit_code = stack.number_u32(0); + let syscall_opcode = stack.number_u32(0x73); - stack.equals(final_step, true, trace_step, true); + 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.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 @@ -237,6 +249,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(); @@ -515,23 +531,26 @@ 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_step = stack.define(16, "prover_write_step_tk"); + let conflict_step = stack.define(16, "prover_conflict_step_tk"); let write_step_copy = stack.copy_var(write_step); is_lower_than(stack, write_step_copy, conflict_step, true); stack.op_verify(); + // the nary search ends up pointing to the previous step of the write, so we have to increment it + let write_step = increment_var(stack, write_step); + 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); - stack.equality(write_addr, false, read_addr, false, false, false); stack.equality(write_value, false, read_value, true, false, false); stack.op_boolor(); @@ -595,7 +614,9 @@ pub fn correct_hash_challenge(stack: &mut StackTracker) { } pub fn future_read_challenge(stack: &mut StackTracker) { - let step = stack.define(16, "prover_step"); + stack.clear_definitions(); + + 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"); @@ -603,50 +624,198 @@ 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); - 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(); } +// 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, rounds as u32 - 1); + stack.number(0); + stack.op_equal(); + + let (mut is_zero, mut is_not_zero) = stack.open_if_with_debug(); + + // 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); + + // 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( + is_zero, + is_not_zero, + 0, + vec![ + (1, "calculated_round".to_string()), + (1, "calculated_index".to_string()), + ], + 0, + ); + } +} + +pub fn equivocation_resign_challenge( + stack: &mut StackTracker, + 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, + 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_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"); + + if kind == EquivocationKind::NextHash { + let max_nary = nary - 1; + let max_last_round = if nary_last_round == 0 { + max_nary + } else { + nary_last_round - 1 + }; + + 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"); + } + + 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); +} + +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 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::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, + 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, + 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(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_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_conflict_step_tk); 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 +824,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, @@ -733,59 +927,59 @@ pub fn execute_challenge(challege_type: &ChallengeType) -> bool { ); } ChallengeType::FutureRead { - step, - read_step_1, - read_step_2, + prover_conflict_step_tk, + 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_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); } ChallengeType::ReadValue { - read_1, - read_2, + prover_read_1, + prover_read_2, read_selector, - step_hash, + prover_hash, trace, - next_hash, - write_step, - conflict_step, + prover_next_hash, + prover_write_step_tk, + prover_conflict_step_tk, } => { - 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); + stack.number_u64(*prover_write_step_tk); + stack.number_u64(*prover_conflict_step_tk); read_value_challenge(&mut stack); } ChallengeType::CorrectHash { - prover_hash, + prover_step_hash, verifier_hash, trace, - next_hash, + 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); @@ -793,10 +987,50 @@ 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); } + ChallengeType::EquivocationResign { + prover_true_hash, + prover_wrong_hash, + prover_challenge_step_tk, + kind, + expected_round, + expected_index, + rounds, + nary, + nary_last_round, + } => { + stack.hexstr_as_nibbles(prover_true_hash); + stack.hexstr_as_nibbles(prover_wrong_hash); + + stack.number_u64(*prover_challenge_step_tk); + + equivocation_resign_challenge( + &mut stack, + kind.clone(), + *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; } @@ -837,10 +1071,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( @@ -914,32 +1148,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(last_step); + stack.number_u64(conflict_step); - stack.number_u64(final_step); - stack.number_u64(trace_step); - stack.number_u32(read_value_1); - stack.number_u32(read_value_2); + 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( @@ -948,6 +1258,7 @@ mod tests { pc: u32, micro: u8, hash: &str, + step: u64, ) -> bool { let mut stack = StackTracker::new(); @@ -958,6 +1269,8 @@ mod tests { stack.hexstr_as_nibbles(hash); + stack.number_u64(step); + trace_hash_zero_challenge(&mut stack); stack.op_true(); @@ -970,12 +1283,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 )); } @@ -1857,8 +2175,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)); @@ -1893,7 +2211,9 @@ mod tests { stack.hexstr_as_nibbles(&next_hash); - stack.number_u64(write_step); + // 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); read_value_challenge(stack); @@ -2017,9 +2337,98 @@ mod tests { write_step, conflict_step )); + } + + fn test_equivocation_resign_aux( + hash1: &String, + hash2: &String, + 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); + 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_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 step = 1055; + // can't challenge true hash + assert!(!test_equivocation_resign_aux( + true_hash, + true_hash, + 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_resign_aux( + true_hash, + wrong_hash, + 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_resign_aux( + true_hash, + wrong_hash, + 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_resign_aux( + true_hash, + wrong_hash, + step, + EquivocationKind::NextHash, + 2, + 1, + rounds, + nary, + nary_last_round, + )); + } mod coin_tests { use super::*; use ::blake3::Hasher; @@ -2088,24 +2497,29 @@ 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 } 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(); @@ -2229,6 +2643,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); @@ -2236,6 +2651,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(); @@ -2408,9 +2824,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(); @@ -2529,68 +2945,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 ecall, AND the hash is correct. 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; + // 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; 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, ) }, @@ -2671,24 +3099,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; } ( @@ -2698,10 +3132,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, @@ -2709,6 +3145,7 @@ mod tests { micro, hash, expected_to_succeed, + step, ) }, ); @@ -3076,65 +3513,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 { @@ -3262,6 +3693,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, @@ -3270,6 +3704,8 @@ mod tests { val1: u32, val2: u32, opcode: u32, + hash: String, + last_hash: String, expected_to_succeed: bool, } @@ -3281,9 +3717,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.", @@ -3292,6 +3730,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 { @@ -3301,15 +3741,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 { @@ -3319,6 +3775,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: true, }, TestCase { @@ -3328,6 +3786,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) --- @@ -3338,6 +3798,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 { @@ -3347,6 +3809,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, TestCase { @@ -3356,6 +3820,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, TestCase { @@ -3365,6 +3831,8 @@ mod tests { val1: 94, val2: 0, opcode: 115, + hash: last_hash.clone(), + last_hash: last_hash.clone(), expected_to_succeed: false, }, ]; @@ -3376,6 +3844,8 @@ mod tests { case.val1, case.val2, case.opcode, + &case.hash, + &case.last_hash, case.expected_to_succeed, ); assert!( @@ -3521,6 +3991,7 @@ mod tests { case.micro, incorrect_hash, true, + 0, ); assert!( success_result, @@ -3536,6 +4007,7 @@ mod tests { case.micro, correct_hash, false, + 0, ); assert!( failure_result, 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..0328204 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,243 @@ 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); +} + +// 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, + var: StackVariable, + nary: u8, + nary_last_round: 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); // set accumulator to 0 + + let mut nibbles_used = 0; + let mut remaining_rounds = rounds; + + 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; + } + // 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); + } + // 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; + 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(); + + // the rest of the unused nibbles should be 0 too + for _ in 0..(16 - nibbles_used) { + stack.number(0); + stack.op_equalverify(); + } +} + +// 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, + max_nary: u8, + max_last_round: u8, +) { + stack.move_var(decisions_bits); + stack.explode(decisions_bits); + + // We carry arround the number we want to add in the top of the altstack + stack.number(1); + stack.to_altstack(); + + 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(); + // 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(); + // 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, 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); + increment_decisions_in_altstack(stack, var, nibbles as u8, 15, 15); + 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, nary_last_round, rounds); + 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); +} + #[cfg(test)] mod tests { use bitvmx_cpu_definitions::memory::MemoryWitness; @@ -2477,6 +2714,133 @@ mod tests { )); } + fn test_var_to_decisions_in_altstack_aux( + decisions: &[u32], + step: u64, + nary: u8, + nary_last_round: u8, + ) { + let rounds = decisions.len(); + let stack = &mut StackTracker::new(); + + for decision in decisions.iter() { + stack.number(*decision); + } + + 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); + + stack.op_true(); + + assert!(stack.run().success); + } + + #[test] + fn test_var_to_decisions_in_altstack() { + 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, + 2, + 0, + ); + + 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, + 16, + 0, + ); + + 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, + 16, + 0, + ); + } + + 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(); + + 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 { + max_nary + } else { + nary_last_round - 1 + }; + + 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"); + + for decision in expected_incremented_decisions.iter() { + stack.number(*decision); + } + + let expected_incremented_decisions = + stack.join_in_stack(rounds, None, Some("expected_incremented_decisions")); + + stack.equals( + incremented_decisions, + true, + expected_incremented_decisions, + true, + ); + + stack.op_true(); + + assert!(stack.run().success); + } + #[test] + 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 { use super::*; use rand::Rng; 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 2ebb3d0..3c65921 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::{ @@ -8,52 +9,199 @@ 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 - ), + 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, + prover_next_hash: String, + }, + TraceHashZero { + prover_trace: TraceStep, + prover_next_hash: String, + prover_conflict_step_tk: u64, + }, + EntryPoint { + prover_read_pc: TraceReadPC, + prover_conflict_step_tk: 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_conflict_step_tk: 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, - write_step: u64, - conflict_step: u64, + prover_next_hash: String, + prover_write_step_tk: u64, + prover_conflict_step_tk: u64, }, CorrectHash { - prover_hash: String, + prover_step_hash: String, verifier_hash: String, trace: TraceStep, - next_hash: String, + prover_next_hash: String, + }, + EquivocationResign { + prover_true_hash: String, + prover_wrong_hash: String, + prover_challenge_step_tk: u64, + kind: EquivocationKind, + 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, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, EnumIter, Default)] +pub enum EquivocationKind { + #[default] + StepHash, + 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 { @@ -75,7 +223,10 @@ pub enum EmulatorResultType { round: u8, }, ProverFinalTraceResult { - final_trace: TraceRWStep, + prover_final_trace: ProverFinalTraceType, + }, + ProverGetHashesAndStepResult { + prover_hashes_and_step: ProverHashesAndStepType, }, VerifierChooseChallengeResult { challenge: ChallengeType, @@ -90,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 { @@ -145,15 +297,28 @@ impl EmulatorResultType { } } - pub fn as_final_trace(&self) -> Result { + pub fn as_final_trace(&self) -> Result { match self { - EmulatorResultType::ProverFinalTraceResult { final_trace } => Ok(final_trace.clone()), + 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 { + match self { + EmulatorResultType::ProverGetHashesAndStepResult { + prover_hashes_and_step, + } => Ok(prover_hashes_and_step.clone()), + _ => Err(EmulatorResultError::GenericError( + "Expected ProverGetCosignedBitsAndHashesResult".to_string(), + )), + } + } + pub fn as_challenge(&self) -> Result { match self { EmulatorResultType::VerifierChooseChallengeResult { challenge } => { diff --git a/docker-riscv32 b/docker-riscv32 index c3dd537..85701a7 160000 --- a/docker-riscv32 +++ b/docker-riscv32 @@ -1 +1 @@ -Subproject commit c3dd537a47a7232ad87aaaf3b5177fd39dc300be +Subproject commit 85701a7d2fc019e9af3ddae33ff6d091819409f6 diff --git a/emulator/src/decision/challenge.rs b/emulator/src/decision/challenge.rs index 0383787..03d9358 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, 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, }; @@ -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, @@ -67,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)?; @@ -87,9 +101,20 @@ pub fn prover_get_hashes_for_round( round, nary_log.base_step, fail_config, + Some(last_step), )?; 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) } @@ -166,6 +191,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)?; @@ -174,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); @@ -188,6 +219,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; @@ -198,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) } @@ -206,7 +253,7 @@ pub fn prover_final_trace( checkpoint_path: &str, final_bits: u32, fail_config: Option, -) -> Result { +) -> 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); @@ -218,14 +265,92 @@ 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); - - 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)?; - nary_log.final_trace = final_trace.clone(); + nary_log.verifier_decisions.push(final_bits - 1); challenge_log.save(checkpoint_path)?; - Ok(final_trace) + + if let ProverHashesAndStepType::HashesAndStep { + step_hash, + next_hash, + step, + } = prover_get_hashes_and_step( + program_definition_file, + checkpoint_path, + NArySearchType::ConflictStep, + None, + 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( + program_definition_file: &str, + checkpoint_path: &str, + nary_type: NArySearchType, + final_bits: Option, + fail_config: Option, +) -> 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)?; + let nary_def = program_def.nary_def(); + + let total_rounds = nary_def.total_rounds(); + + 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); + 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, + 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(); + } + } + + Ok(ProverHashesAndStepType::HashesAndStep { + step_hash, + next_hash, + step: final_step, + }) } pub fn get_hashes( @@ -255,6 +380,9 @@ 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, TraceHash, TraceHashZero, @@ -291,6 +419,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,32 +439,100 @@ pub fn verifier_choose_challenge( false, )?; - let (step_hash, 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 rounds = return_script_parameters.then_some(nary_def.total_rounds()); + + if (prover_step_hash != resigned_step_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationResign(EquivocationKind::StepHash) + { + let (round, index) = *mapping.get(&step).unwrap(); + + return Ok(ChallengeType::EquivocationResign { + prover_true_hash: prover_step_hash, + prover_wrong_hash: resigned_step_hash.to_string(), + prover_challenge_step_tk: step, + kind: EquivocationKind::StepHash, + expected_round: round, + expected_index: index + 1, + nary, + nary_last_round, + rounds, + }); + } + + if (prover_next_hash != resigned_next_hash && force == ForceChallenge::No) + || force == ForceChallenge::EquivocationResign(EquivocationKind::NextHash) + { + let (round, index) = *mapping.get(&(step + 1)).unwrap(); + return Ok(ChallengeType::EquivocationResign { + prover_true_hash: prover_next_hash, + prover_wrong_hash: resigned_next_hash.to_string(), + prover_challenge_step_tk: step, + kind: EquivocationKind::NextHash, + expected_round: round, + expected_index: index + 1, + nary, + nary_last_round, + rounds, + }); + } // 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 { + if step == 0 { 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, + prover_conflict_step_tk: step, + }); } 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 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 step = conflict_step_log.step_to_challenge; let mut steps = vec![step, step + 1]; let mut my_trace_idx = 1; if step > 0 { @@ -348,7 +546,7 @@ pub fn verifier_choose_challenge( checkpoint_path, verifier_log.input.clone(), Some(steps), - fail_config, + fail_config.clone(), false, )? .1; @@ -362,10 +560,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 +581,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 @@ -401,24 +602,24 @@ 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( - 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_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 { 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 +629,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 @@ -449,16 +650,16 @@ pub fn verifier_choose_challenge( let read_selector = if is_read_1_future { 1 } else { 2 }; return Ok(ChallengeType::FutureRead { - step: step + 1, - read_step_1, - read_step_2, + prover_conflict_step_tk: step, + 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 +668,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 +700,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 { @@ -541,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; @@ -560,7 +763,20 @@ pub fn verifier_choose_challenge( verifier_log.read_selector = read_selector; verifier_log.save(checkpoint_path)?; - return Ok(ChallengeType::ReadValueNArySearch(bits)); + 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 }); } } verifier_log.save(checkpoint_path)?; @@ -570,30 +786,92 @@ 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 conflict_step = conflict_step_log.step_to_challenge; let challenge_step = read_challenge_log.step_to_challenge; - let (step_hash, next_hash) = get_hashes( - &nary_def.step_mapping(&read_challenge_log.verifier_decisions), + let mapping = &nary_def.step_mapping(&read_challenge_log.verifier_decisions); + let (prover_step_hash, prover_next_hash) = get_hashes( + 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), + let (my_step_hash, _) = get_hashes( + mapping, &read_challenge_log.verifier_hash_rounds, challenge_step, ); - assert_eq!(next_hash, my_next_hash); + 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()); + + 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::EquivocationResign { + prover_true_hash: prover_step_hash, + prover_wrong_hash: resigned_step_hash.to_string(), + prover_challenge_step_tk: challenge_step, + kind: EquivocationKind::StepHash, + expected_round: round, + expected_index: index + 1, + nary, + nary_last_round, + rounds, + }); + } + + 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::EquivocationResign { + prover_true_hash: prover_next_hash, + prover_wrong_hash: resigned_next_hash.to_string(), + prover_challenge_step_tk: challenge_step, + kind: EquivocationKind::NextHash, + expected_round: round, + expected_index: index + 1, + nary, + nary_last_round, + rounds, + }); + } + + 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( @@ -607,14 +885,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, }); } @@ -622,20 +900,20 @@ 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 read_1 = conflict_step_trace.read_1; - let read_2 = conflict_step_trace.read_2; + 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; 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, - write_step: challenge_step + 1, - conflict_step: conflict_step + 1, + prover_next_hash, + prover_write_step_tk: challenge_step, + prover_conflict_step_tk: conflict_step_log.step_to_challenge, }); } @@ -819,8 +1097,11 @@ 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 = - prover_final_trace(pdf, chk_prover_path, v_decision + 1, fail_config_prover).unwrap(); + let (final_trace, step_hash, next_hash, _) = + 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); @@ -838,6 +1119,8 @@ mod tests { pdf, chk_verifier_path, final_trace, + step_hash.as_str(), + next_hash.as_str(), force, fail_config_verifier, true, @@ -845,7 +1128,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( @@ -871,11 +1154,25 @@ mod tests { info!("{:?}", v_decision); } + 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() + .as_hashes_with_step() + .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() } @@ -1755,7 +2052,7 @@ mod tests { false, ForceCondition::ValidInputWrongStepOrHash, ForceChallenge::ReadValueNArySearch, - ForceChallenge::TraceHash, + ForceChallenge::CorrectHash, ); } #[test] @@ -1800,7 +2097,7 @@ mod tests { false, ForceCondition::ValidInputWrongStepOrHash, ForceChallenge::ReadValueNArySearch, - ForceChallenge::TraceHash, + ForceChallenge::CorrectHash, ); } @@ -1936,6 +2233,356 @@ 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_resign_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::EquivocationResign(EquivocationKind::StepHash), + ForceChallenge::No, + ); + } + + #[test] + fn test_challenge_equivocation_resign_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::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_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_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( + "59", + "hello-world.yaml", + 17, + false, + fail_config.clone(), + fail_resign.clone(), + None, + None, + true, + ForceCondition::ValidInputWrongStepOrHash, + ForceChallenge::No, + ForceChallenge::No, + ); + + test_challenge_aux( + "60", + "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(); @@ -2082,4 +2729,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/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/decision/nary_search.rs b/emulator/src/decision/nary_search.rs index 36d4245..51085e7 100644 --- a/emulator/src/decision/nary_search.rs +++ b/emulator/src/decision/nary_search.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, str::FromStr}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; +use std::cmp::{max, min}; use tracing::{error, info}; #[derive(Clone, Copy, PartialEq, ValueEnum, Serialize, Deserialize, Debug)] @@ -145,6 +146,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)] @@ -185,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"); @@ -194,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]; @@ -209,25 +220,18 @@ 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) } }; @@ -379,6 +383,7 @@ mod tests { &prover_hashes.into(), &my_hashes.into(), NArySearchType::ConflictStep, + None, ); assert_eq!(bits, exp_bits); assert_eq!(base, exp_step); @@ -422,4 +427,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..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); } }; @@ -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..6273a85 100644 --- a/emulator/src/executor/utils.rs +++ b/emulator/src/executor/utils.rs @@ -216,9 +216,25 @@ 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, + pub fail_resign_hash: Option, pub fail_hash_until: Option, pub fail_execute: Option, pub fail_reads: Option, @@ -227,6 +243,10 @@ 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, + pub fail_selection_bits: Option, + pub fail_prover_challenge_step: bool, } impl FailConfiguration { @@ -236,6 +256,36 @@ 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, + ..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/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 46c4952..5c37eaf 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_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, }, @@ -183,6 +183,27 @@ enum Commands { command_file: String, }, + ProverGetHashesAndStep { + #[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,18 @@ 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, + + /// Fail last_step commitment + #[arg(long)] + fail_commitment_step: Option, + + /// Fail hash commitment + #[arg(long)] + fail_commitment_hash: bool, + /// Memory dump at given step #[arg(short, long)] dump_mem: Option, @@ -400,6 +445,9 @@ fn main() -> Result<(), EmulatorError> { fail_read_2: fail_read_2_args, 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, @@ -472,6 +520,11 @@ fn main() -> Result<(), EmulatorError> { fail_opcode, 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, + fail_selection_bits: None, + fail_prover_challenge_step: false, }; let result = execute_program( &mut program, @@ -623,18 +676,17 @@ fn main() -> Result<(), EmulatorError> { fail_config_prover, command_file, }) => { - let result: TraceRWStep = prover_final_trace( + let prover_final_trace = prover_final_trace( pdf, checkpoint_prover_path, *v_decision, fail_config_prover.clone(), )?; - info!("Prover final trace: {:?}", result); + info!("Prover final trace: {:?}", prover_final_trace); + + let result = + EmulatorResultType::ProverFinalTraceResult { prover_final_trace }.to_value()?; - let result = EmulatorResultType::ProverFinalTraceResult { - final_trace: result.clone(), - } - .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"); @@ -643,6 +695,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 +705,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 +725,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 +749,29 @@ fn main() -> Result<(), EmulatorError> { file.write_all(result.to_string().as_bytes()) .expect("Failed to write JSON to file"); } + Some(Commands::ProverGetHashesAndStep { + pdf, + checkpoint_prover_path, + v_decision, + command_file, + fail_config_prover, + }) => { + let prover_hashes_and_step = prover_get_hashes_and_step( + pdf, + &checkpoint_prover_path, + NArySearchType::ReadValueChallenge, + Some(*v_decision), + fail_config_prover.clone(), + )?; + + let result = EmulatorResultType::ProverGetHashesAndStepResult { + prover_hashes_and_step, + } + .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/challenge.rs b/emulator/tests/challenge.rs index 388590c..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); @@ -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; 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 );