Skip to content
This repository was archived by the owner on Jul 5, 2024. It is now read-only.

Commit f05f6cb

Browse files
authored
fix soundness: share txid write lookup across step (#1719)
### Description This PR fix soundness by sharing txid write lookup between current step and next step ### Issue Link #1707 ### Type of change - [x] Bug fix (non-breaking change which fixes an issue) ### Design rationale Before fix, `cur_end_tx -> callcontext(txid, WRITE)` vs `next_begin_tx -> callcontext(txid, WRITE)` are meant to constraint `next_tx_id = cur_tx_id + 1`. However, because 2 callcontext write are different records in rw_table, so actually second record are under-constraint and tx_id can be manipulated. Because so far we do not have negative test framework. So this PR just simply adding a simple positive test to assure there is no consecutive tx_id write with SAME tx_id. If this fix make sense, will update zkevm-specs accordingly https://github.com/privacy-scaling-explorations/zkevm-specs/blob/master/src/zkevm_specs/evm_circuit/execution/end_tx.py#L71-L79
1 parent da454e2 commit f05f6cb

File tree

5 files changed

+94
-18
lines changed

5 files changed

+94
-18
lines changed

Diff for: bus-mapping/src/evm/opcodes/begin_end_tx.rs

-9
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,6 @@ pub(crate) fn end_tx(
343343
// Write the tx receipt
344344
write_tx_receipt(state, exec_step, call.is_persistent)?;
345345

346-
// Write the next transaction id if we're not at the last tx
347-
if !state.tx_ctx.is_last_tx() {
348-
state.call_context_write(
349-
exec_step,
350-
state.block_ctx.rwc.0 + 1,
351-
CallContextField::TxId,
352-
(state.tx_ctx.id() + 1).into(),
353-
)?;
354-
}
355346
Ok(())
356347
}
357348

Diff for: zkevm-circuits/src/evm_circuit/execution/end_tx.rs

+55-2
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,11 @@ impl<F: Field> ExecutionGadget<F> for EndTxGadget<F> {
239239

240240
#[cfg(test)]
241241
mod test {
242-
use crate::test_util::CircuitTestBuilder;
243-
use bus_mapping::circuit_input_builder::FixedCParams;
242+
243+
use crate::{table::CallContextFieldTag, test_util::CircuitTestBuilder};
244+
use bus_mapping::{circuit_input_builder::FixedCParams, operation::Target};
244245
use eth_types::{self, bytecode, Word};
246+
use itertools::Itertools;
245247
use mock::{
246248
eth, gwei, test_ctx::helpers::account_0_code_account_1_no_code, TestContext, MOCK_ACCOUNTS,
247249
};
@@ -420,6 +422,57 @@ mod test {
420422
);
421423
}
422424

425+
#[test]
426+
fn end_tx_consistent_tx_id_write() {
427+
// check there is no consecutive txid write with same txid in rw_table
428+
429+
let block = CircuitTestBuilder::new_from_test_ctx(
430+
TestContext::<2, 3>::new(
431+
None,
432+
account_0_code_account_1_no_code(bytecode! { STOP }),
433+
|mut txs, accs| {
434+
txs[0]
435+
.to(accs[0].address)
436+
.from(accs[1].address)
437+
.value(eth(1));
438+
txs[1]
439+
.to(accs[0].address)
440+
.from(accs[1].address)
441+
.value(eth(1));
442+
txs[2]
443+
.to(accs[0].address)
444+
.from(accs[1].address)
445+
.value(eth(1));
446+
},
447+
|block, _tx| block.number(0xcafeu64),
448+
)
449+
.unwrap(),
450+
)
451+
.params(FixedCParams {
452+
max_txs: 5,
453+
..Default::default()
454+
})
455+
.build_block()
456+
.unwrap();
457+
458+
block.rws.0[&Target::CallContext]
459+
.iter()
460+
.filter(|rw| {
461+
// filter all txid write operation
462+
rw.is_write()
463+
&& rw
464+
.field_tag()
465+
.is_some_and(|tag| tag == CallContextFieldTag::TxId as u64)
466+
})
467+
.sorted_by_key(|a| a.rw_counter())
468+
.tuple_windows()
469+
.for_each(|(a, b)| {
470+
// chech there is no consecutive write with same txid value
471+
assert!(a.rw_counter() != b.rw_counter());
472+
assert!(a.value_assignment() != b.value_assignment());
473+
})
474+
}
475+
423476
#[test]
424477
fn end_tx_gadget_nonexisting_coinbase() {
425478
// Check that the code hash of the coinbase address is correctly set to be the empty code

Diff for: zkevm-circuits/src/evm_circuit/util/constraint_builder.rs

+26-1
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,6 @@ impl<'a, F: Field> EVMConstraintBuilder<'a, F> {
10811081
}
10821082

10831083
// Call context
1084-
10851084
pub(crate) fn call_context(
10861085
&mut self,
10871086
call_id: Option<Expression<F>>,
@@ -1132,6 +1131,32 @@ impl<'a, F: Field> EVMConstraintBuilder<'a, F> {
11321131
);
11331132
}
11341133

1134+
// same as call_context_lookup_write with bypassing external rwc
1135+
// Note: will not bumping internal rwc
1136+
pub(crate) fn call_context_lookup_write_with_counter(
1137+
&mut self,
1138+
rw_counter: Expression<F>,
1139+
call_id: Option<Expression<F>>,
1140+
field_tag: CallContextFieldTag,
1141+
value: Word<Expression<F>>,
1142+
) {
1143+
self.rw_lookup_with_counter(
1144+
"CallContext lookup",
1145+
rw_counter,
1146+
1.expr(),
1147+
Target::CallContext,
1148+
RwValues::new(
1149+
call_id.unwrap_or_else(|| self.curr.state.call_id.expr()),
1150+
0.expr(),
1151+
field_tag.expr(),
1152+
Word::zero(),
1153+
value,
1154+
Word::zero(),
1155+
Word::zero(),
1156+
),
1157+
);
1158+
}
1159+
11351160
pub(crate) fn call_context_lookup_write(
11361161
&mut self,
11371162
call_id: Option<Expression<F>>,

Diff for: zkevm-circuits/src/evm_circuit/util/tx.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,24 @@ impl<F: Field> EndTxHelperGadget<F> {
119119
);
120120

121121
// Transition
122-
let rw_counter = num_rw.expr() - is_first_tx.expr();
122+
let rw_counter_offset = num_rw.expr() - is_first_tx.expr();
123123
cb.condition(
124124
cb.next
125125
.execution_state_selector([ExecutionState::BeginTx, ExecutionState::InvalidTx]),
126126
|cb| {
127-
cb.call_context_lookup_write(
128-
Some(cb.next.state.rw_counter.expr()),
127+
let next_step_rwc = cb.next.state.rw_counter.expr();
128+
// lookup use next step initial rwc, thus lead to same record on rw table
129+
cb.call_context_lookup_write_with_counter(
130+
next_step_rwc.clone(),
131+
Some(next_step_rwc),
129132
CallContextFieldTag::TxId,
133+
// tx_id has been lookup and range_check above
130134
Word::from_lo_unchecked(tx_id.expr() + 1.expr()),
131135
);
136+
// minus 1.expr() because `call_context_lookup_write_with_counter` do not bump
137+
// rwc
132138
cb.require_step_state_transition(StepStateTransition {
133-
rw_counter: Delta(rw_counter.clone()),
139+
rw_counter: Delta(rw_counter_offset.clone() - 1.expr()),
134140
..StepStateTransition::any()
135141
});
136142
},
@@ -139,7 +145,7 @@ impl<F: Field> EndTxHelperGadget<F> {
139145
cb.next.execution_state_selector([ExecutionState::EndBlock]),
140146
|cb| {
141147
cb.require_step_state_transition(StepStateTransition {
142-
rw_counter: Delta(rw_counter.expr() - 1.expr()),
148+
rw_counter: Delta(rw_counter_offset.expr() - 1.expr()),
143149
// We propagate call_id so that EndBlock can get the last tx_id
144150
// in order to count processed txs.
145151
call_id: Same,

Diff for: zkevm-circuits/src/test_util.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ impl<const NACC: usize, const NTX: usize> CircuitTestBuilder<NACC, NTX> {
145145
}
146146

147147
impl<const NACC: usize, const NTX: usize> CircuitTestBuilder<NACC, NTX> {
148-
fn build_block(&self) -> Result<Block<Fr>, CircuitTestError> {
148+
/// build block
149+
pub fn build_block(&self) -> Result<Block<Fr>, CircuitTestError> {
149150
if let Some(block) = &self.block {
150151
// If a block is specified, no need to modify the block
151152
return Ok(block.clone());

0 commit comments

Comments
 (0)