值得信赖的区块链资讯!
零知识证明 – zkEVM源代码分析(EVM Circuit)
zkEVM是零知识证明相对复杂的零知识证明应用,源代码值得反复阅读和学习。
https://github.com/appliedzkp/zkevm-circuits.git
本文中采用的源代码对应的最后一个提交信息如下:
commit 1ec38f207f150733a90081d3825b4de9c3a0a724 (HEAD -> main)
Author: z2trillion
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 Configure
EVM Circuit电路实现在zkevm-circuits/src/evm_circuit.rs。从它的configure函数看起:
pub fn configure(
meta: &mut ConstraintSystem,
power_of_randomness: [Expression; 31],
tx_table: TxTable,
rw_table: RwTable,
bytecode_table: BytecodeTable,
block_table: BlockTable,
) -> Self
where
TxTable: LookupTable,
RwTable: LookupTable,
BytecodeTable: LookupTable,
BlockTable: LookupTable,
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(
meta: &mut ConstraintSystem,
power_of_randomness: [Expression; 31],
fixed_table: [Column; 4],
tx_table: TxTable,
rw_table: RwTable,
bytecode_table: BytecodeTable,
block_table: BlockTable,
) -> Self
where
TxTable: LookupTable,
RwTable: LookupTable,
BytecodeTable: LookupTable,
BlockTable: LookupTable,
{
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是“一步”执行。从电路角度看,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 {
pub(crate) state: StepState,
pub(crate) rows: Vec<StepRow>,
}
Step由StepState和16个StepRow组成。先说StepRow,Step Row包含了一个Step中某个Row涉及的所有信息:
pub(crate) struct StepRow {
pub(crate) qs_byte_lookup: Cell,
pub(crate) cells: [Cell; STEP_WIDTH],
}
对于一个Step中的某个Row来说,除了cell的数据外(cells),还有该Row对应的qs_byte_lookup,是否该Row的Cell中的数据被Range256进行约束(qs_byte_lookup后续会详细讲解)。
StepState数据结构包括了一个Step对应的状态信息:
pub(crate) struct StepState {
/// The execution state for the step
pub(crate) execution_state: Vec<Cell>,
/// The Read/Write counter
pub(crate) rw_counter: Cell,
/// The unique identifier of call in the whole proof, using the
/// `rw_counter` at the call step.
pub(crate) call_id: Cell,
/// Whether the call is root call
pub(crate) is_root: Cell,
/// Whether the call is a create call
pub(crate) is_create: Cell,
// 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,
/// The program counter
pub(crate) program_counter: Cell,
/// The stack pointer
pub(crate) stack_pointer: Cell,
/// The amount of gas left
pub(crate) gas_left: Cell,
/// Memory size in words (32 bytes)
pub(crate) memory_word_size: Cell,
/// The counter for state writes
pub(crate) state_write_counter: Cell,
}
先详细介绍一下StepState的各个变量的含义。
-
execution_state – 表明当前Step的执行状态。一个Step的执行状态定义在step.rs中:
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,可以采用同一个执行状态进行约束。
-
rw_counter – Stack/Memory访问时采用rw_counter区分开同一个地址的访问
-
call_id – 每一个函数调用赋予一个id,用于区分不同的程序
-
is_root – 是否是根
-
is_create – 是否是create调用
-
code_source – 调用程序的标示(一般是个程序的hash结果)
-
program_counter – PC
-
stack_point – 栈指针
-
gas_left – 剩下的gas数量
-
memory_word_size – memory的大小(以word(32字节)为单位)
-
state_write_counter – 状态写入的计数器
仔细查看Step的创建函数(new),发现在创建Step的时候分别创建StepState和StepRow。
pub(crate) fn new(
meta: &mut ConstraintSystem,
qs_byte_lookup: Column,
advices: [Column; 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的约束相对来说最复杂。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 Column的数据范围约束
约束每个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的状态关系。
Gadget约束
对于每一种类型的操作(Opcode),创建对应的Gadget约束。具体的逻辑实现在configure_gadget函数中:
fn configure_gadget<G: ExecutionGadget>(
meta: &mut ConstraintSystem,
q_step: Selector,
q_step_first: Selector,
power_of_randomness: &[Expression; 31],
step_curr: &Step,
step_next: &Step,
independent_lookups: &mut Vec<Vec<Lookup>>,
presets_map: &mut HashMap<ExecutionState, Vec<Preset>>,
) -> 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 {
pub(crate) curr: &'a Step, //current Step
pub(crate) next: &'a Step, //next Step
power_of_randomness: &'a [Expression; 31],
execution_state: ExecutionState, //execution state
cb: BaseConstraintBuilder, //base builder
constraints_first_step: Vec<(&'static str, Expression)>,
lookups: Vec<(&'static str, Lookup)>,
curr_row_usages: Vec, //当前StepRow的使用情况
next_row_usages: Vec, //下一个StepRow的使用情况
rw_counter_offset: Expression, //
program_counter_offset: usize, //PC
stack_pointer_offset: i32, //栈指针
in_next_step: bool, //
condition: Option<Expression>,
}
a. ConstraintBuilder::new
创建了ConstraintBuilder对象。
b. G::configure
所有的Opcode相关的Gadget相关的代码在zkevm-circuits/src/evm_circuit/execution/目录下。目前已经支持三十多种Gadget,也就是说,支持三十多种execution_state。以比较简单的AddGadget介绍相关的约束实现:
pub(crate) struct AddGadget {
same_context: SameContextGadget,
add_words: AddWordsGadget,
is_sub: PairSelectGadget,
}
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约束。
Stack约束
回头再看Add电路约束,利用cb.stack_pop和cb.stack_push函数实现相应的Stack的约束。
pub(crate) fn stack_pop(&mut self, value: Expression) {
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) {
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, //是否是写操作?
stack_pointer_offset: Expression, //Stack偏移
value: Expression, //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,
is_write: Expression,
tag: RwTableTag,
values: [Expression; 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 {
/// 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约束章节)。
Context约束
之前的重点是单个Execution State的约束实现。SameContextGadget约束的是多个Execution State之间约束实现。
pub(crate) struct SameContextGadget {
opcode: Cell,
sufficient_gas_left: RangeCheckGadget,
}
具体的约束逻辑实现在contruct函数中:
pub(crate) fn construct(
cb: &mut ConstraintBuilder,
opcode: Cell,
step_state_transition: StepStateTransition,
) -> 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的关系等等。
具体的代码,感兴趣的小伙伴可以自行查看。
Lookup约束
在每个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
EVM Circuit通过assign_block对一个Block中的所有的交易进行证明。
pub fn assign_block(
&self,
layouter: &mut impl Layouter,
block: &Block,
) -> Result {
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。
比推快讯
更多 >>- 数据:Hyperliquid 平台鲸鱼当前持仓 64.41 亿美元,多空持仓比为 0.91
- Meme 币memes市值回涨至 1000 万美元,现报价约 0.0104 美元
- 贝佐斯辟谣 Polymarket 官推内容:不知道为什么你们要编造此事
- A 股黄金概念持续拉升,中国黄金、豫光金铅涨停
- 现货白银突破 99 美元关口,新年首月涨超 38%
- Polymarket 官推频发假新闻引争议,Jeff Bezos 亲自辟谣
- 在特朗普任命的 SEC 主席领导下,2025 年加密货币执法行动减少 60%
- RIVER 短线一度逼近 70 USDT,24H 涨幅近 30%
- 比特币充币情绪延续,过去 24 小时 CEX 净流入 5,024.49 枚 BTC
- Morningstar Ventures:并非 Space 的领投方,该项目已承诺退还至少 50%的公募资金
- 特朗普称已锁定下一任美联储主席人选,贝莱德高管成黑马
- 数据:900 枚 BTC 从 Fidelity Custody 转出,经中转后转至另一匿名地址
- 数据:某巨鲸 4 天前高位卖 1 万 ETH 后 OTC 抄底回补
- 重仓做多贵金属巨鲸浮盈突破 47 万美元,代币化黄金合约持仓规模逼近 500 万美元
- 比特币挖矿难度下调 3.28% 至 141.67 T
- 现货白银站上 97 美元,再创历史新高
- 代币化黄金板块总市值升至 51.2 亿美元,XAUT、PAXG 跻身加密货币排行榜前六十
- 美股收盘加密板块跌多涨少,VIX 收跌 7.46%,ETHZ 收涨 4.15%
- 数据:“麻吉大哥”再次加仓以太坊多单,目前仓位价值约 1211 万美元
- TikTok官宣美国方案,两公司共同运营,字节保留算法知识产权
- 某 Pendle 投资人地址将 180 万枚代币转入 CEX,持仓 3 年价值约 383 万美元
- Capital One 斥资 51.5 亿美元收购金融科技初创公司 Brex
- BitGo 纽交所上市首日上涨约 20%,盘中估值最高达 26 亿美元
- SEC 与 CFTC 将于下周举办关于加密货币监管协调的联合活动
- World Liberty Financial 与加密卫星公司 Spacecoin 达成合作并互换代币
- Cap 第二季积分活动 Homestead 将于 1 月 29 日开始,7 月 23 日结束
- 若比特币跌破 8.8 万美元,主流 CEX 累计多单清算强度将达 6.38 亿
- Farcaster 创始人澄清:协议当前运行正常,购房资金来自 Coinbase IPO 收益
- 加密恐慌指数回升至 24,市场仍处于极度恐慌区间
- 特朗普称获格陵兰全面准入,北约推动加强北极安全但协议细节存疑
- 美众议院未通过旨在限制特朗普对委再动武的决议
- 某巨鲸再次增持超 2 万枚 ETH,总持仓量升至 80,115 枚
- 特朗普:将对所有与伊朗进行贸易往来国家加征 25%关税
- SENT 24 小时涨超 170%,市值升至 2.25 亿美元
- 特朗普:到处都是创纪录的数字,我应该争取第四个任期吗?
- 现货黄金上破4950美元
- 美国堪萨斯州提出比特币战略储备法案
- 普华永道:机构级加密采用已越过不可逆转点
- 美国参议院农业委员会公布加密市场结构法案版本,立法进程仍存分歧
- BitGo 登陆纽交所首日大幅震荡,股价上涨 36%后回落
- 美联储 1 月维持利率不变概率达 95%,降息概率仅 5%
- 英特尔盘后股价下跌超 4%
- 美元指数 DXY 日内跌超 0.5%,现报 98.3
- 花旗警告:以太坊活跃度激增或是“地址投毒”诈骗引发的虚假繁荣
- 数据:500 万枚 TON 从 Telegram 转出,价值约 770 万美元
- 数据:若 BTC 突破 93,951 美元,主流 CEX 累计空单清算强度将达 19.57 亿美元
- 现货黄金上涨突破 4910 美元/盎司创历史新高
- 数据:2015 枚 ETH 从 Binance 转出,价值约 595 万美元
- 加密托管商 BitGo 登陆纽交所首日大涨 25%
- 特朗普因“去银行化”起诉摩根大通,索赔50亿美元
比推专栏
更多 >>观点
比推热门文章
- 比特币充币情绪延续,过去 24 小时 CEX 净流入 5,024.49 枚 BTC
- Morningstar Ventures:并非 Space 的领投方,该项目已承诺退还至少 50%的公募资金
- 特朗普称已锁定下一任美联储主席人选,贝莱德高管成黑马
- 数据:900 枚 BTC 从 Fidelity Custody 转出,经中转后转至另一匿名地址
- 数据:某巨鲸 4 天前高位卖 1 万 ETH 后 OTC 抄底回补
- 重仓做多贵金属巨鲸浮盈突破 47 万美元,代币化黄金合约持仓规模逼近 500 万美元
- 比特币挖矿难度下调 3.28% 至 141.67 T
- 现货白银站上 97 美元,再创历史新高
- 代币化黄金板块总市值升至 51.2 亿美元,XAUT、PAXG 跻身加密货币排行榜前六十
- 美股收盘加密板块跌多涨少,VIX 收跌 7.46%,ETHZ 收涨 4.15%
比推 APP



