这篇文章将详细讲解EVM Circuit各个Column的设计,每种Opcode如何约束以及多个Opcode之间是如何约束以及组合。
zkEVM是零知识证明相对复杂的零知识证明应用,源代码值得反复阅读和学习。
https://github.com/appliedzkp/zkevm-circuits.git
本文中采用的源代码对应的最后一个提交信息如下:
commit 1ec38f207f150733a90081d3825b4de9c3a0a724 (HEAD -> main)
Author: z2trillion <z2trillion@users.noreply.github.com>
Date: Thu Mar 24 15:42:09 2022 -0400
zkEVM的电路主要由两部分电路组成:EVM Circuit和State Circuit。本文先讲解EVM Circuit的电路相关的源代码。其他部分在后续的文章中介绍。这篇文章将详细讲解EVM Circuit各个Column的设计,每种Opcode如何约束以及多个Opcode之间是如何约束以及组合。
EVM Circuit的整体的电路结构如下图所示:
从Column的角度来看,EVM Circuit分为三部分:1/ Step选择子(包括当前Step,第一个Step,最后一个Step等等)2/ Step电路 3/ Fixed Table(固定的查找表)。Step电路逻辑是核心部分。所谓的Step是指从电路约束角度的执行的一步。这一部分又分为两部分:a/ execution state (Step状态选择子) b/ 各种执行状态的约束逻辑。图中用虚线画出了大体的约束逻辑模块。
理解EVM Circuit从Configure和Assign入手。
EVM Circuit电路实现在zkevm-circuits/src/evm_circuit.rs。从它的configure函数看起:
pub fn configure<TxTable, RwTable, BytecodeTable, BlockTable>(
meta: &mut ConstraintSystem<F>,
power_of_randomness: [Expression<F>; 31],
tx_table: TxTable,
rw_table: RwTable,
bytecode_table: BytecodeTable,
block_table: BlockTable,
) -> Self
where
TxTable: LookupTable<F, 4>,
RwTable: LookupTable<F, 11>,
BytecodeTable: LookupTable<F, 4>,
BlockTable: LookupTable<F, 3>,
EVM Circuit的电路约束由两部分组成:1/ fixed_table 2/ Execution 部分。fixed_table是一些固定的表信息,占用4个column,分别对应tag/value1/value2/结果。
tag的种类为10种,定义在zkevm-circuits/src/evm_circuit/table.rs:
pub enum FixedTableTag {
Range5 = 1,
Range16,
Range32,
Range256,
Range512,
SignByte,
BitwiseAnd,
BitwiseOr,
BitwiseXor,
ResponsibleOpcode,
}
接着查看Execution的部分。ExecutionConfig的configure函数定义了电路的其他约束:
pub(crate) fn configure<TxTable, RwTable, BytecodeTable, BlockTable>(
meta: &mut ConstraintSystem<F>,
power_of_randomness: [Expression<F>; 31],
fixed_table: [Column<Fixed>; 4],
tx_table: TxTable,
rw_table: RwTable,
bytecode_table: BytecodeTable,
block_table: BlockTable,
) -> Self
where
TxTable: LookupTable<F, 4>,
RwTable: LookupTable<F, 11>,
BytecodeTable: LookupTable<F, 4>,
BlockTable: LookupTable<F, 3>,
{
q_step - Step的选择子(selector)
q_step_first - 第一个Step的选择子
q_step_last - 最后一个Step的选择子
qs_byte_lookup - byte范围检查的选择子
对于一个Step,采用32个Column的数据进行约束。
let q_step = meta.complex_selector();
let q_step_first = meta.complex_selector();
let q_step_last = meta.complex_selector();
let qs_byte_lookup = meta.advice_column();
let advices = [(); STEP_WIDTH].map(|_| meta.advice_column());
从功能角度看,Step是“一步”执行。从电路角度看,Step是一个由32列16行的电路构成。Step的参数信息定义在zkevm-circuits/src/evm_circuit/param.rs:
const STEP_WIDTH: usize = 32;
const STEP_HEIGHT: usize = 16;
Step定义在zkevm-circuits/src/evm_circuit/step.rs中:
pub(crate) struct Step<F> {
pub(crate) state: StepState<F>,
pub(crate) rows: Vec<StepRow<F>>,
}
Step由StepState和16个StepRow组成。先说StepRow,Step Row包含了一个Step中某个Row涉及的所有信息:
pub(crate) struct StepRow<F> {
pub(crate) qs_byte_lookup: Cell<F>,
pub(crate) cells: [Cell<F>; STEP_WIDTH],
}
对于一个Step中的某个Row来说,除了cell的数据外(cells),还有该Row对应的qs_byte_lookup,是否该Row的Cell中的数据被Range256进行约束(qs_byte_lookup后续会详细讲解)。
StepState数据结构包括了一个Step对应的状态信息:
pub(crate) struct StepState<F> {
/// The execution state for the step
pub(crate) execution_state: Vec<Cell<F>>,
/// The Read/Write counter
pub(crate) rw_counter: Cell<F>,
/// The unique identifier of call in the whole proof, using the
/// `rw_counter` at the call step.
pub(crate) call_id: Cell<F>,
/// Whether the call is root call
pub(crate) is_root: Cell<F>,
/// Whether the call is a create call
pub(crate) is_create: Cell<F>,
// This is the identifier of current executed bytecode, which is used to
// lookup current executed code and used to do code copy. In most time,
// it would be bytecode_hash, but when it comes to root creation call, the
// executed bytecode is actually from transaction calldata, so it might be
// tx_id if we decide to lookup different table.
// However, how to handle root creation call is yet to be determined, see
// issue https://github.com/appliedzkp/zkevm-specs/issues/73 for more
// discussion.
pub(crate) code_source: Cell<F>,
/// The program counter
pub(crate) program_counter: Cell<F>,
/// The stack pointer
pub(crate) stack_pointer: Cell<F>,
/// The amount of gas left
pub(crate) gas_left: Cell<F>,
/// Memory size in words (32 bytes)
pub(crate) memory_word_size: Cell<F>,
/// The counter for state writes
pub(crate) state_write_counter: Cell<F>,
}
先详细介绍一下StepState的各个变量的含义。
pub enum ExecutionState {
// Internal state
BeginTx,
EndTx,
EndBlock,
CopyToMemory,
// Opcode successful cases
STOP,
ADD, // ADD, SUB
...
// Error cases
ErrorInvalidOpcode,
ErrorStackOverflow,
ErrorStackUnderflow,
...
}
一个Step的执行状态,包括了内部的状态(一个区块中的交易,通过BeginTx, EndTx隔离),Opcode的成功执行状态以及错误状态。有关Opcode的成功执行状态,表示的是一个约束能表示的情况下的执行状态。也就是说,多个Opcode,如果是采用的同一个约束,可以用一个执行状态表示。举个例子,ADD/SUB Opcode采用的是一个约束,对于这两个Opcode,可以采用同一个执行状态进行约束。
仔细查看Step的创建函数(new),发现在创建Step的时候分别创建StepState和StepRow。
pub(crate) fn new(
meta: &mut ConstraintSystem<F>,
qs_byte_lookup: Column<Advice>,
advices: [Column<Advice>; STEP_WIDTH],
is_next_step: bool,
) -> Self {
创建StepState的逻辑如下:
let num_state_cells = ExecutionState::amount() + N_CELLS_STEP_STATE;
let mut cells = VecDeque::with_capacity(num_state_cells);
meta.create_gate("Query state for step", |meta| {
for idx in 0..num_state_cells {
let column_idx = idx % STEP_WIDTH;
let rotation = idx / STEP_WIDTH + if is_next_step { STEP_HEIGHT } else { 0
};
cells.push_back(Cell::new(meta, advices[column_idx], rotation));
}
vec![0.expr()]
});
StepState {
execution_state: cells.drain(..ExecutionState::amount()).collect(),
rw_counter: cells.pop_front().unwrap(),
call_id: cells.pop_front().unwrap(),
is_root: cells.pop_front().unwrap(),
is_create: cells.pop_front().unwrap(),
code_source: cells.pop_front().unwrap(),
program_counter: cells.pop_front().unwrap(),
stack_pointer: cells.pop_front().unwrap(),
gas_left: cells.pop_front().unwrap(),
memory_word_size: cells.pop_front().unwrap(),
state_write_counter: cells.pop_front().unwrap(),
}
N_CELLS_STEP_STATE=10是StepState中除了execution_state外的其他Cell的个数。
按照16行*32列,创建出对应于advices这些Column的Cell。重点要理解好Cell的表示,在同一个Column中的不同的Cell,采用Rotation(偏移)进行区分。在编写电路业务的时候,特别注意对Cell的抽象和描述,这些模块化的电路可能会应用多个实例。再观察这些StepState对应的Cell,这些Cell再配合上q_step相应的选择子,就可以精确的描述一个Step的电路约束逻辑。简单的说,q_step选择子约束行,Step中的Cell采用Rotation(相对偏移)定位。
创建StepRow的逻辑如下:
let mut rows = Vec::with_capacity(STEP_HEIGHT - rotation_offset);
meta.create_gate("Query rows for step", |meta| {
for rotation in rotation_offset..STEP_HEIGHT {
let rotation = rotation + if is_next_step { STEP_HEIGHT } else { 0 };
rows.push(StepRow {
qs_byte_lookup: Cell::new(meta, qs_byte_lookup, rotation),
cells: advices.map(|column| Cell::new(meta, column, rotation)),
});
}
vec![0.expr()]
});
从StepState下面的第一个行开始是一个个的StepRow。
Custom Gate的约束相对来说最复杂。Custom Gate的约束包括:
a. 每一个Step中的execution_state只有一个有效状态:
let sum_to_one = (
"Only one of execution_state should be enabled",
step_curr
.state
.execution_state
.iter()
.fold(1u64.expr(), |acc, cell| acc - cell.expr()), // expression -> 1 - sum(state's cells)
);
实现的方法是将所有的状态的Cell的数值累加起来。sum_to_one是1-sum的表达式(expression)。
b. 每一个Step中execution_state是布尔值:
let bool_checks = step_curr.state.execution_state.iter().map(|cell| {
(
"Representation for execution_state should be bool",
cell.expr() * (1u64.expr() - cell.expr()),
)
});
检查的方法就是采用x*(1-x)。
c. 相邻的两个Step中的ExecuteState满足约定的条件:比如说,EndTx状态后,只能是BeginTx或者EndBlock。
[
(
"EndTx can only transit to BeginTx or EndBlock",
ExecutionState::EndTx,
vec![ExecutionState::BeginTx, ExecutionState::EndBlock],
),
(
"EndBlock can only transit to EndBlock",
ExecutionState::EndBlock,
vec![ExecutionState::EndBlock],
),
]
.map(|(name, from, to)| {
(
name,
step_curr.execution_state_selector([from])
* (1.expr() - step_next.execution_state_selector(to)),
)
}),
注意,这些相邻的约束,对于最后一个Step不需要满足:
.map(move |(name, poly)| (name, (1.expr() - q_step_last.clone()) * poly))
d. 第一个Step的状态必须是BeginTx,最后一个Step的状态必须是EndBlock:
let _first_step_check = {
let begin_tx_selector =
step_curr.execution_state_selector([ExecutionState::BeginTx]);
iter::once((
"First step should be BeginTx",
q_step_first * (1.expr() - begin_tx_selector),
))
};
let _last_step_check = {
let end_block_selector =
step_curr.execution_state_selector([ExecutionState::EndBlock]);
iter::once((
"Last step should be EndBlock",
q_step_last * (1.expr() - end_block_selector),
))
};
特别注意的是,如上的这些custom gate的约束都是相对于某个Step而已的,所以所有的约束必须加上q_step的限制:
iter::once(sum_to_one)
.chain(bool_checks)
.chain(execution_state_transition)
.map(move |(name, poly)| (name, q_step.clone() * poly))
// TODO: Enable these after test of CALLDATACOPY is complete.
// .chain(first_step_check)
// .chain(last_step_check)
当前的代码不是最终的代码,first_step_check以及last_step_check都被注释掉了。从TODO可以看出,这些约束在CALLDATACOPY的约束实现测试完成后会加上。
约束每个advice都是byte(也就是Range256)的范围内。因为范围(Range)的约束实现在fixed_table中,由4个fixed column组成。所以Range256的范围约束由tag/value等四部分对应。
for advice in advices {
meta.lookup_any("Qs byte", |meta| {
let advice = meta.query_advice(advice, Rotation::cur());
let qs_byte_lookup = meta.query_advice(qs_byte_lookup, Rotation::cur());
vec![
qs_byte_lookup.clone() * FixedTableTag::Range256.expr(), //tag约束
qs_byte_lookup * advice, //值约束
0u64.expr(), //ignore
0u64.expr(), //ignore
]
.into_iter()
.zip(fixed_table.table_exprs(meta).to_vec().into_iter())
.collect::<Vec<_>>()
});
}
到目前为止,整个电路的大致的样子有了,划分了Column的类型,定义了每个Step的电路范围。并且约束了每个Step中的状态以及相邻两个Step的状态关系。
对于每一种类型的操作(Opcode),创建对应的Gadget约束。具体的逻辑实现在configure_gadget函数中:
fn configure_gadget<G: ExecutionGadget<F>>(
meta: &mut ConstraintSystem<F>,
q_step: Selector,
q_step_first: Selector,
power_of_randomness: &[Expression<F>; 31],
step_curr: &Step<F>,
step_next: &Step<F>,
independent_lookups: &mut Vec<Vec<Lookup<F>>>,
presets_map: &mut HashMap<ExecutionState, Vec<Preset<F>>>,
) -> G {
对于Gadget的约束,抽象出ConstraintBuilder:
let mut cb = ConstraintBuilder::new(
step_curr,
step_next,
power_of_randomness,
G::EXECUTION_STATE,
);
let gadget = G::configure(&mut cb);
let (constraints, constraints_first_step, lookups, presets) = cb.build();
debug_assert!(
presets_map.insert(G::EXECUTION_STATE, presets).is_none(),
"execution state already configured"
);
通过ConstraintBuilder::new创建ConstraintBuilder。对于每一种Gadget,调用相应的configure函数。最终调用ConstraintBuilder的build函数完成约束配置。
先从ConstraintBuilder的定义讲起。ConstraintBuilder定义在zkevm-circuits/src/evm_circuit/util/constraint_builder.rs中:
pub(crate) struct ConstraintBuilder<'a, F> {
pub(crate) curr: &'a Step<F>, //current Step
pub(crate) next: &'a Step<F>, //next Step
power_of_randomness: &'a [Expression<F>; 31],
execution_state: ExecutionState, //execution state
cb: BaseConstraintBuilder<F>, //base builder
constraints_first_step: Vec<(&'static str, Expression<F>)>,
lookups: Vec<(&'static str, Lookup<F>)>,
curr_row_usages: Vec<StepRowUsage>, //当前StepRow的使用情况
next_row_usages: Vec<StepRowUsage>, //下一个StepRow的使用情况
rw_counter_offset: Expression<F>, //
program_counter_offset: usize, //PC
stack_pointer_offset: i32, //栈指针
in_next_step: bool, //
condition: Option<Expression<F>>,
}
a. ConstraintBuilder::new
创建了ConstraintBuilder对象。
b. G::configure
所有的Opcode相关的Gadget相关的代码在zkevm-circuits/src/evm_circuit/execution/目录下。目前已经支持三十多种Gadget,也就是说,支持三十多种execution_state。以比较简单的AddGadget介绍相关的约束实现:
pub(crate) struct AddGadget<F> {
same_context: SameContextGadget<F>,
add_words: AddWordsGadget<F, 2, false>,
is_sub: PairSelectGadget<F>,
}
AddGadget依赖于其他三个Gadget: SameContextGadget (约束Context),AddWordsGadget(Words相加)和PairSelectGadget(选边,选择a或者b)。通过AddGadget的configure函数可以查看相关的约束。
1/ 约定opcode,a/b/c(操作字)的Cell(通过cb.query_cell)
let opcode = cb.query_cell();
let a = cb.query_word();
let b = cb.query_word();
let c = cb.query_word();
2/ 构建加法约束(减法也转化为加法)
let add_words = AddWordsGadget::construct(cb, [a.clone(), b.clone()], c.clone());
3/ 确定是加法还是减法操作,查看opcode是ADD还是SUB
let is_sub = PairSelectGadget::construct(
cb,
opcode.expr(),
OpcodeId::SUB.expr(),
OpcodeId::ADD.expr(),
);
4/ 约束对应的Stack的变化
cb.stack_pop(select::expr(is_sub.expr().0, c.expr(), a.expr()));
cb.stack_pop(b.expr());
cb.stack_push(select::expr(is_sub.expr().0, a.expr(), c.expr()));
5/ 约束Context的变化
let step_state_transition = StepStateTransition {
rw_counter: Delta(3.expr()),
program_counter: Delta(1.expr()),
stack_pointer: Delta(1.expr()),
gas_left: Delta(-OpcodeId::ADD.constant_gas_cost().expr()),
..StepStateTransition::default()
};
let same_context = SameContextGadget::construct(cb, opcode, step_state_transition);
注意所有的约束最后都是保存在cb.constraints变量。
c. cb.build
cb.build就是对于每个Gadget的约束进行修正和补充。对于当前Step的所有的Row:
1/ 如果没有用到的Cell,需要限制为0
2/ 如果需要检查范围的,检查qs_byte_lookup
for (row, usage) in self.curr.rows.iter().zip(self.curr_row_usages.iter()) {
if usage.is_byte_lookup_enabled {
constraints.push(("Enable byte lookup", row.qs_byte_lookup.expr() - 1.expr()));
}
presets.extend(
row.cells[usage.next_idx..]
.iter()
.map(|cell| (cell.clone(), F::zero())),
);
presets.push((
row.qs_byte_lookup.clone(),
if usage.is_byte_lookup_enabled {
F::one()
} else {
F::zero()
},
));
}
对于当前Step的约束或者查找表,加上execution_state的选择子。
let execution_state_selector = self.curr.execution_state_selector([self.execution_state]);
(
constraints
.into_iter()
.map(|(name, constraint)| (name, execution_state_selector.clone() * constraint))
.collect(),
self.constraints_first_step
.into_iter()
.map(|(name, constraint)| (name, execution_state_selector.clone() * constraint))
.collect(),
self.lookups
.into_iter()
.map(|(name, lookup)| (name, lookup.conditional(execution_state_selector.clone())))
.collect(),
presets,
)
到此,Step的约束框架浮现出来了。ExecuteState是可能的执行状态,相对于qs_step来说,execute_state的相对位置固定。并且execute_state的每个Cell的值必须是布尔类型,有且只有一个执行状态有效。针对每个执行状态,存在相应的Gadget电路。所有的Gadget电路在cb.build的阶段都必须加上execution_state_selector选择子。也就是说,整个EVM Circuit电路逻辑上包括了所有执行状态的约束。
在讲最后一部分,其他Lookup约束之前,介绍一下Stack约束和Context约束。
回头再看Add电路约束,利用cb.stack_pop和cb.stack_push函数实现相应的Stack的约束。
pub(crate) fn stack_pop(&mut self, value: Expression<F>) {
self.stack_lookup(false.expr(), self.stack_pointer_offset.expr(), value);
self.stack_pointer_offset += 1;
}
pub(crate) fn stack_push(&mut self, value: Expression<F>) {
self.stack_pointer_offset -= 1;
self.stack_lookup(true.expr(), self.stack_pointer_offset.expr(), value);
}
stack_pop和stack_push的实现类似,都是调整stack_pointer_offset偏移,并通过stack_lookup约束Stack上stack_pointer_offset对应的值。
pub(crate) fn stack_lookup(
&mut self,
is_write: Expression<F>, //是否是写操作?
stack_pointer_offset: Expression<F>, //Stack偏移
value: Expression<F>, //Stack的值
) {
self.rw_lookup(
"Stack lookup",
is_write,
RwTableTag::Stack,
[
self.curr.state.call_id.expr(),
0.expr(),
self.curr.state.stack_pointer.expr() + stack_pointer_offset,
0.expr(),
value,
0.expr(),
0.expr(),
0.expr(),
],
);
}
核心逻辑是rw_loopup,rw_loopup实现又是基于rw_lookup_with_counter。Stack的访问细化为call_id, Stack的偏移以及rw的次数。在同一个Step State中,对同一个Stack的偏移存在多次读写的可能性,这些可能性的约束需要加上读写的次数。
rw_loopup_with_counter的实现就是将lookup(查找表)通过add_lookup记录在ConstraintBuilder的lookups变量中,为后面的查找表的Configure做准备。
fn rw_lookup_with_counter(
&mut self,
name: &'static str,
counter: Expression<F>,
is_write: Expression<F>,
tag: RwTableTag,
values: [Expression<F>; 8],
) {
self.add_lookup(
name,
Lookup::Rw {
counter,
is_write,
tag: tag.expr(),
values,
},
);
}
除了RW(可读可写的)查找表外,目前还支持其他一些查找表类型。Lookup枚举类型定义在zkevm-circuits/src/evm_circuit/table.rs:
pub(crate) enum Lookup<F> {
/// Lookup to fixed table, which contains serveral pre-built tables such as
/// range tables or bitwise tables.
Fixed {
...
},
Tx {
...
},
Rw {
...
},
Bytecode {
...
},
Block {
...
},
Conditional {
...
}
Tx查找表约束交易数据,RW查找表约束可读可写的数据访问(Stack/Memory),Bytecode约束执行的程序,Block约束的是区块相关的数据。
再重头看一下Stack的约束实现。对于每一种Step,可能存在Stack或者Memory的操作。为了约束这些操作,除了申请和这些操作数据相关的Cell外,还需要指定Stack Pointer。在Stack Pointer锚定的情况下,某个Step内部的Stack的操作可以独立约束。并且,Stack Pointer对应的Cell在Step中的位置可以通过q_step进行锚定。也就是说,从单个Step的角度来说,约束可以独立表达和实现。当然,Stack的约束除了单个Step的约束外,多个Step之间也存在Stack数据一致性和正确性的约束。这些约束由后续“Lookup约束”进行约束(请查看Lookup约束章节)。
之前的重点是单个Execution State的约束实现。SameContextGadget约束的是多个Execution State之间约束实现。
pub(crate) struct SameContextGadget<F> {
opcode: Cell<F>,
sufficient_gas_left: RangeCheckGadget<F, N_BYTES_GAS>,
}
具体的约束逻辑实现在contruct函数中:
pub(crate) fn construct(
cb: &mut ConstraintBuilder<F>,
opcode: Cell<F>,
step_state_transition: StepStateTransition<F>,
) -> Self {
cb.opcode_lookup(opcode.expr(), 1.expr());
cb.add_lookup(
"Responsible opcode lookup",
Lookup::Fixed {
tag: FixedTableTag::ResponsibleOpcode.expr(),
values: [
cb.execution_state().as_u64().expr(),
opcode.expr(),
0.expr(),
],
},
);
// Check gas_left is sufficient
let sufficient_gas_left = RangeCheckGadget::construct(cb, cb.next.state.gas_left.expr());
// State transition
cb.require_step_state_transition(step_state_transition);
Self {
opcode,
sufficient_gas_left,
}
}
逻辑简单清晰,实现了如下的约束:
1/ opcode_lookup - 约束PC对应的op code一致
2/ add_lookup - 约束opcode和Step中的execution state的一致性
3/ 查看gas是否足够
4/ require_step_state_transition - 约束状态的转换是否一致(涉及到两个Step之间的约束)。比如说PC的关系,Stack Pointer的关系,Call id的关系等等。
具体的代码,感兴趣的小伙伴可以自行查看。
在每个Execution State的约束准备就绪后,开始处理所有的Lookup的约束。
Self::configure_lookup(
meta,
q_step,
fixed_table,
tx_table,
rw_table,
bytecode_table,
block_table,
independent_lookups,
);
实现的逻辑相对简单,每个Lookup的约束需要额外加上q_step约束外,建立不同的表的约束关系。其中independent_lookups就是某个Step约束中除去常见表外的查找表约束。
EVM Circuit通过assign_block对一个Block中的所有的交易进行证明。
pub fn assign_block(
&self,
layouter: &mut impl Layouter<F>,
block: &Block<F>,
) -> Result<(), Error> {
self.execution.assign_block(layouter, block)
}
ExecutionState的assign_block,除了设置q_step_first以及q_step_last外,对Block中的每一笔交易Tx进行约束。
self.q_step_first.enable(&mut region, offset)?;
for transaction in &block.txs {
for step in &transaction.steps {
let call = &transaction.calls[step.call_index];
self.q_step.enable(&mut region, offset)?;
self.assign_exec_step(&mut region, offset, block, transaction, call, step)?;
offset += STEP_HEIGHT;
}
}
self.q_step_last.enable(&mut region, offset - STEP_HEIGHT)?;
一笔交易Tx的一个个步骤(step)存储在steps变量中。针对每个Step,调用assign_exec_step进行assign。在理解Configure的逻辑的基础上,这些逻辑相对简单。感兴趣的小伙伴可以自行深入查看相应源代码。
EVM Circuit的电路实现先介绍到这里,后续的文章接着介绍State Circuit和Bytecode Circuit。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!