Optimism Rollup是目前最流行的以太坊L2解决方案。本文将解释Optimism Rollup每个设计决策背后的动机, 剖析Optimism的系统实现,并提供指向每个分析组件的相应代码的链接,适用于希望了解Optimism解决方案的 工作原理并评估所提议系统的性能和安全性的开发人员。

1、软件重用原则在Optimism Rollup中的重要性

以太坊已经围绕其开发者生态系统发展了护城河。开发人员的技术栈包括:

  • Solidity/ Vyper:这是两种最流行的智能合约编程语言,有很多工具链围绕它们构建,例如 Ethers、Hardhat、 dapp、slither等。
  • 以太坊虚拟机:最流行的区块链虚拟机,其内部设计比任何其他区块链VM都要好得多。
  • Go-ethereum:主流以太坊协议实现,采用率 > 75%,经过了广泛的测试。

由于Optimism Rollup将以太坊作为其第1层,因此如果我们可以无需修改即可重用现有工具,那就太好了。 这将改善开发人员的体验,因为开发人员无需学习新技术。虽然已经多次提出,但是我想强调软件重用 的另一个含义:安全性。

2、Optimistic虚拟机

Optimism Rollup依赖于使用欺诈证明来防止发生无效的状态转换。这需要在以太坊上执行Optimsim交易。简而言之, 如果交易结果存在争议,例如修改了Alice的ETH余额,Alice将尝试在以太坊上重放该确切的交易,以证明那里的 结果是正确的。但是,如果某些EVM操作码依赖于系统范围内的参数,这些参数可能随时都会改变,例如加载或存储状态或 获取当前时间戳,则它们在L1和L2上的行为将不同。

因此,Optimsim的第一个技术,就是处理L1上的L2争端的机制,该机制保证可以重现在L1上执行L2事务时存在的 任何“上下文”,并且在理想情况下不引入太多开销。

目标是实现一个沙盒环境,可确保在L1和L2之间确定性地执行智能合约。

Optimism的解决方案是Optimistic虚拟机。OVM是通过将上下文相关的EVM操作码替换为其对应的OVM操作码来实现的。

一个简单的例子是:

  • L2交易调用TIMESTAMP操作码,例如返回1610889676
  • 一个小时后,由于某种原因,交易都必须在以太坊L1上重放
  • 如果要在EVM中正常执行该交易,则TIMESTAMP操作码将返回1610889676 +3600。这不是我们希望的,因为这将导致交易执行上下文的变化。
  • 在OVM中,在L2上执行交易时,TIMESTAMP操作码将替换为ovmTIMESTAMP,因此将显示正确值的操作码。

所有与上下文相关的EVM操作码在OVM核心合约在ExecutionManager中都有一个对应的ovm{OPCODE}。合约的执行是从EM的 入口点run函数开始的。这些操作码也已修改为可以与可插拔状态数据库交互,其作用我们将在“欺诈证明”部分中进行介绍。

某些在OVM中“无意义”的操作码会通过Optimism的SafetyChecker合约禁用,Optimism合约采用静态分析技术,可以有效地 判断合约是否OVM安全并返回1或0。

请查阅附录部分以了解每个被修改/禁用的EVM操作码。

Optimism Rollup看起来像这样:

上图中问号标注的组件将在下面的欺诈证明部分说明,但在此之前,我们需要进一步解释一些基础知识。

3、Optimisitic Solidity编译器

现在我们有了OVM沙箱,接下来要做的就是将智能合约编译为OVM字节码。下面是一些可选的方案:

  • 发明一种新的可以编译为OVM的智能合约语言:这个思路很容易被放弃,因为它需要从头开始重新做所有事情,而且 我们已经就这一点达成一致,即尽可能重用已有的技术栈。
  • 将EVM字节码转换为OVM字节码:已尝试但由于复杂性而被放弃。
  • 修改Solidity和Vyper编译器以生成OVM字节码。

Optimism当前使用的方法是第三种,Optimsim更改了socl大约500行代码。

Solidity编译器的工作原理是将Solidity转换为Yul,然后转换为EVM指令,最后转换为字节码。Optimism所做的更改 既简单又优雅:对于每个操作码,在编译为EVM汇编后,如有必要,尝试以ovm变体“重写”它(如果被禁止则抛出错误)。

解释起来有点复杂,下面让我们比较一个简单合约的EVM和OVM字节码:

用solc编译一下:

1
2
$ solc C.sol --bin-runtime --optimize --optimize-runs 200
6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60336035565b005b60008054600101905556fea264697066735822122001fa42ea2b3ac80487c9556a210c5bbbbc1b849ea597dd6c99fafbc988e2a9a164736f6c634300060c0033

我们可以反汇编此代码看一下得到的汇编代码,括号内表示Program Counter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
[025] 35 CALLDATALOAD
...
[030] 63 PUSH4 0xc2985578 // id("foo()")
[035] 14 EQ
[036] 60 PUSH1 0x2d // int: 45
[038] 57 JUMPI // jump to PC 45
...
[045] 60 PUSH1 0x33
[047] 60 PUSH1 0x35 // int: 53
[049] 56 JUMP // jump  to PC 53
...
[053] 60 PUSH1 0x00
[055] 80 DUP1
[056] 54 SLOAD // load the 0th storage slot
[057] 60 PUSH1 0x01
[059] 01 ADD // add 1 to it
[060] 90 SWAP1
[061] 55 SSTORE // store it back
[062] 56 JUMP
...

上述汇编代码的意思是,如果calldata匹配函数foo()的选择器,则使用SLOAD操作码载入0x00处的存储变量, 加上0x01,最后将结果使用SSTORE操作码存回去。听起来不错!

在OVM中看起来如何?首先用修改后的solc编译:

1
2
$ osolc C.sol --bin-runtime --optimize --optimize-runs 200
60806040523480156100195760008061001661006e565b50505b50600436106100345760003560e01c8063c298557814610042575b60008061003f61006e565b50505b61004a61004c565b005b6001600080828261005b6100d9565b019250508190610069610134565b505050565b632a2a7adb598160e01b8152600481016020815285602082015260005b868110156100a657808601518282016040015260200161008b565b506020828760640184336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c52505050565b6303daa959598160e01b8152836004820152602081602483336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c528051935060005b60408110156100695760008282015260200161011d565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c5260008152602061011d56

得到的字节码更长了,让我们再次反汇编一下,看看有什么变化:

1
2
3
4
5
6
7
8
9
10
11
...
[036] 35 CALLDATALOAD
...
[041] 63 PUSH4 0xc2985578 // id("foo()")
[046] 14 EQ
[047] 61 PUSH2 0x0042
[050] 57 JUMPI // jump to PC 66
...
[066] 61 PUSH2 0x004a
[069] 61 PUSH2 0x004c // int: 76
[072] 56 JUMP // jump to PC 76

这一部分还是检查是否匹配指定的函数选择器,让我们看看之后会发生什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
...
[076] 60 PUSH1 0x01 // Push 1 to the stack (to be used for the addition later)
[078] 60 PUSH1 0x00
[080] 80 DUP1
[081] 82 DUP3
[082] 82 DUP3
[083] 61 PUSH2 0x005b
[086] 61 PUSH2 0x00d9 (int: 217)
[089] 56 JUMP // jump to PC 217
...
[217] 63 PUSH4 0x03daa959       // <---|  id("ovmSLOAD(bytes32)")
[222] 59 MSIZE                  //     |
[223] 81 DUP2                   //     |
[224] 60 PUSH1 0xe0             //     |
[226] 1b SHL                    //     |
[227] 81 DUP2                   //     |
[228] 52 MSTORE                 //     |
[229] 83 DUP4                   //     |
[230] 60 PUSH1 0x04             //     | CALL to the CALLER's ovmSLOAD
[232] 82 DUP3                   //     |
[233] 01 ADD                    //     |
[234] 52 MSTORE                 //     |
[235] 60 PUSH1 0x20             //     |
[237] 81 DUP2                   //     |
[238] 60 PUSH1 0x24             //     |
[240] 83 DUP4                   //     |
[241] 33 CALLER                 //     |
[242] 60 PUSH1 0x00             //     |
[244] 90 SWAP1                  //     |
[245] 5a GAS                    //     |
[246] f1 CALL                   // <---|[247] 58 PC                     // <---|
[248] 60 PUSH1 0x1d             //     |
[250] 01 ADD                    //     |
[251] 57 JUMPI                  //     |
[252] 3d RETURNDATASIZE         //     |
[253] 60 PUSH1 0x01             //     |
[255] 14 EQ                     //     |
[256] 58 PC                     //     |
[257] 60 PUSH1 0x0c             //     |
[259] 01 ADD                    //     |
[260] 57 JUMPI                  //     |  Handle the returned data
[261] 3d RETURNDATASIZE         //     |
[262] 60 PUSH1 0x00             //     |
[264] 80 DUP1                   //     |
[265] 3e RETURNDATACOPY         //     |
[266] 3d RETURNDATASIZE         //     |
[267] 62 PUSH3 0x123456         //     |
[271] 52 MSTORE                 //     |
[272] 60 PUSH1 0xea             //     |
[274] 61 PUSH2 0x109c           //     |
[277] 52 MSTORE                 // <---|

上面代码包含很多操作,要点在于这里不是使用SLOAD操作码,而是构造一个栈以便执行CALL操作码。 调用的接收者通过CALLER操作码被压入栈。每一个调用都是来自EM,因此实际上CALLER是调用EM的有效方法。 调用的数据以ovmSLOAD(bytes32)函数的选择器开头,接下来是参数(在这个示例中,就是占用32字节的字)。 之后,将处理返回的数据并将其添加到内存中。

让我们继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
[297] 82 DUP3
[298] 01 ADD // Adds the 3rd item on the stack to the ovmSLOAD value
[299] 52 MSTORE
[308] 63 PUSH4 0x22bd64c0  // <---| id("ovmSSTORE(bytes32,bytes32)")
[313] 59 MSIZE             //     |
[314] 81 DUP2              //     |
[315] 60 PUSH1 0xe0        //     |
[317] 1b SHL               //     |
[318] 81 DUP2              //     |
[319] 52 MSTORE            //     |
[320] 83 DUP4              //     |
[321] 60 PUSH1 0x04        //     |
[323] 82 DUP3              //     |
[324] 01 ADD               //     |  CALL to the CALLER's ovmSSTORE
[325] 52 MSTORE            //     |  (RETURNDATA handling is omited
[326] 84 DUP5              //     |   because it is identical to ovmSSLOAD)
[327] 60 PUSH1 0x24        //     |
[329] 82 DUP3              //     |
[330] 01 ADD               //     |
[331] 52 MSTORE            //     |
[332] 60 PUSH1 0x00        //     |
[334] 81 DUP2              //     |
[335] 60 PUSH1 0x44        //     |
[337] 83 DUP4              //     |
[338] 33 CALLER            //     |
[339] 60 PUSH1 0x00        //     |
[341] 90 SWAP1             //     |
[342] 5a GAS               //     |
[343] f1 CALL              // <---|
...

类似于将SLOAD调整到外部调用ovmSLOAD,SSTORE也调整到外部调用ovmSSTORE。调用的数据不同,因为ovmSSTORE 需要两个参数,即存储插槽和要存储的值。下面是两者的比较:

实际上,我们先调用Execution Manager的ovmSLOAD方法,然后再调用其ovmSTORE方法,而不是SLOAD和SSTORE。

通过比较EVM与OVM的执行(我们仅显示执行的SLOAD一部分),我们可以看到通过Execution Manager进行的虚拟化:

这种虚拟化技术有一个“陷阱”:

会导致更快达到合约大小上限 :通常,以太坊合约的字节码最大24KB 。使用Optimistic Solidity Compiler编译的 合约最终比原来大,这意味着必须重构接近24KB限制的合约,以便其OVM大小仍适合24KB限制,因为它们需要在以太坊 主网上执行。

4、Optimistic Geth

以太坊最流行的实现是go-ethereum(即geth)。让我们看看通常如何在Geth中执行交易。

在每个块上,调用状态处理器的Process方法,该方法对每个交易执行ApplyTransaction方法。在内部,交易被转换为 消息,消息被应用于当前状态,最后将新产生的状态存储回数据库中。

此核心数据流在Optimistic Geth上保持不变,但进行了一些修改以保持交易“对OVM友好”:

修改1:通过Sequencer入口点的OVM消息

交易被转换为OVM消息。由于除去了消息的签名,因此消息数据被修改为包括交易签名以及原始交易的其余字段。to字段 将替换为“Sequencer入口点”合约的地址。这样做是为了使交易格式紧凑,因为它将被发布到以太坊,并且我们已经确定, 好紧凑伸缩性就越好。

修改2:通过执行管理器的OVM沙箱

为了通过OVM沙箱运行交易,必须将它们发送到Execution Manager的run 功能。不要求用户仅提交符合该限制的交易, 所有消息都被修改为在内部发送到Execution Manager。这里很简单:消息的to字段被替换为执行管理器的地址,并且消息 的原始数据被打包为参数传入run。

这可能有点不直观,因此我们提供了代码以给出一个具体示例:https : //github.com/gakonst/optimism-tx-format。

修改3:拦截对状态管理器的调用

StateManager是一个特殊的合约,在Optimistic Geth 上并不存在。仅在欺诈证明期间部署它。细心的读者会注意到, 当打包参数以进行run调用时,Optimism的geth还将打包一个硬编码的State Manager地址。这就是最终被用作任何 ovmSSTORE或ovmSLOAD(或类似)调用的最终目的地的原因。在L2上运行时,以State Manager合约为目标的所有消息 都将被拦截,并且它们被连接为直接与Geth的StateDB对话(或不执行任何操作)。

对于寻求整体代码更改的人们来说,最好的方法是搜索UsingOVM并比较geth 1.9.10的差异。

修改4:基于epoch的批次而不是块

OVM没有块,它仅维护交易的有序列表。因此,没有区块gas限制的概念;取而代之的是,根据时间段(称为epoch)限制 总的gas消耗率。在执行交易之前,要检查是否需要启动一个新的epoch,在执行之后,将其gas小号添加到该epoch所使用 的累积gas用量上。对于Equenecer提交的交易和“ L1至L2”交易,每个epoch都有单独的gas限制。任何超过gas限值的交易 将提前返回。这意味着操作员可以在一个链上批次中发布多个具有不同时间戳的交易(时间戳由Sequencer定义,但有一些 限制,我们将在“数据可用性批处理”部分中说明)。

修改5:Rollup同步服务

该同步服务是一个新的进程运行,它与“正常” GETH同时运行。Rollup同步服务负责监视以太坊日志,对其进行处理, 并通过geth的worker注入要在L2状态下应用的相应L2交易。

5、Optimistic Rollup

Optimistic Rollup的主要特性包括:

  • OVM作为其运行时/状态迁移函数
  • 拥有单个Sequencer的Optimistic Geth作为L2客户端
  • 在以太坊上部署的Solidity智能合约用于:
    • 数据可用性
    • 争议解决和欺诈证明,我们将深入研究实现数据可用性层的智能合约,并探索端到端的欺诈证明流程。

数据可用性批次

如前所述,交易数据被压缩,然后发送到L2上的Sequencer Entrypoint合约。然后,Sequencer负责“汇总”这些交易, 并在以太坊上发布数据,提供数据可用性,以便即使Sequencer消失了,也可以启动新的Sequencer以从中断的地方继续。

依靠以太坊实现该逻辑的智能合约称为权威交易链(CTC:Canonical Transaction Chain)。权威交易链是一个追加型 日志,它代表Rollup链的“正式历史”(所有交易以及其顺序)。交易可以由Sequencer等提交给CTC。为了保留L1的抗审查 能力,任何人都可以将交易提交到此队列,并在一定滞后期之后将其包括在CTC中。

CTC为每批发布的L2交易提供数据可用性。可以通过两种方式创建批处理:

  • 预计每隔几秒钟,Sequencer就会检查接收到的新交易,将它们分批汇总,以及所需的任何其他元数据。然后,他们 利用appendSequencerBatch将该数据发布到以太坊。这是由批处理提交者服务自动完成的。
  • 当Sequencer审查其用户或当用户执行从L1到L2的交易,用户需要调用enqueue和appendQueueBatch,这会强制在CTC中 包含交易

这里的一个极端情况是:如果Sequencer广播了一个批次,则用户可以强制包含涉及与该批次冲突的状态的交易,从而 可能使该批次的某些交易无效。为了避免这种情况,我们引入了时间延迟,在此延迟之后可以由非Sequencer帐户将批处理 追加到队列中。对此进行考虑的另一种方法是,给利用appendeSequencerBatcher添加的交易一个“宽限期”,否则用户 使用appendQueueBatch。

鉴于大多数交易预计将通过Sequencer提交,因此有必要深入研究批处理结构和执行流程。

你可能会注意到,appendSequencerBatch没有任何参数。批次以紧密打包的格式提交,而使用ABI编码和解码则效率要 低得多。它使用内联汇编来对calldata进行切片,并以预期的格式将其解压缩。

一个批次由以下部分组成:

  • 批次头
  • 批处理上下文(> = 1,请注意:此上下文与我们在上面的“ OVM”部分中提到的消息/交易/全局上下文不同)
  • 交易(> = 1)

批次头指定了上下文的数量,因此序列化的批处理看起来像是 [header, context1, context2, …, tx1, tx2, … ]

该函数继续执行以下两项操作:

  • 验证所有与上下文相关的不变量是否适用
  • 根据已发布的交易数据创建默克尔树

如果通过了上下文验证,则该批次将转换为OVM链批次头,然后将其存储在CTC中。

存储的批次头包含该批次的merkle根,这意味着证明已包含交易是提供针对针对CTC中存储的merkle根进行验证的 merkle证明的简单问题。

这里的自然问题是:这似乎太复杂了!为什么需要上下文?

上下文对于Sequencer来说是必要的,以便知道是否应在已排序交易之前或之后执行已排队的交易。让我们来看一个例子:

在时间T1,Sequencer已接收到2个交易,它们将包括在其批次中。在T2(> T1)用户也排队的交易时,将它添加到L1到 L2交易队列(但不将其添加到批次!)。在T2,Sequencer又接收到1个交易,另外2个交易也入队列。换句话说,待处理 交易的批处理看起来像:

1
[(sequencer, T1), (sequencer, T1), (queue, T2), (sequencer, T2), (queue, T3), (queue, T4)]

为了保持时间戳和块号信息,同时又保持序列化格式的紧凑性,我们使用了“上下文”,即Sequencer和排队交易之间的 共享信息集合。上下文必须严格增加块数和时间戳。在上下文中,所有Sequencer交易共享相同的块号和时间戳。 对于“队列交易”,将时间戳和块号设置为调用队列时的值。在这种情况下,该批交易的上下文为:

1
[{ numSequencedTransactions: 2, numSubsequentQueueTransactions: 1, timestamp: T1}, {numSequencedTransactions: 1, numSubsequentQueueTransactions: 2, timestamp: T2}]

状态承诺

在以太坊中,每个交易都会导致对状态以及全局状态根的修改。通过在某个区块提供状态根并通过默克尔证明来 证明某个帐户在某个区块拥有一些ETH,以证明该账户的状态与所声明的值匹配。因为每个块包含多个交易,并且我们 只能访问状态根,所以这意味着我们只能在执行整个块后才声明状态。

一段历史:

在EIP98和Byzantium分叉之前,以太坊交易在每次执行后产生中间状态根,这些根通过交易收据提供给用户 删除中间状态根能够提高性能,虽然有一点小缺陷,因此很快就采用了它。EIP PR658中提供的其他动机解决了该问题: 收据的PostState字段(指示与tx执行后的状态相对应的状态根)被布尔状态字段(指示交易的成功状态)替换。

事实证明,警告并非无关紧要。EIP98写道:

所做的更改确实意味着,如果矿工创建了一个区块,其中一个状态转换的处理不正确,那么就不可能针对该交易 提供欺诈证明;相反,欺诈证明必须包含整个区块。

此更改的含义是,如果一个区块有1000个交易,并且你在第988个交易中检测到欺诈,则在实际执行你感兴趣的交易 之前,需要在前一个区块的状态之上运行987个交易,这会使欺诈证明效率极低。以太坊本身没有欺诈证明,所以没关系!

另一方面,Optimism的欺诈证据是至关重要的。在前面,我们提到Optimism没有区块,那只是个小谎言:Optimism有区块, 但是每个区块只有1个交易,我们称之为“微区块”。由于每个微块包含1个交易,因此每个块的状态根实际上是单个交易 产生的状态根。乌拉!我们已经重新引入了中间状态根,而不必对协议进行任何重大更改。当然,由于微块在技术上 仍然是块并且包含冗余的其他信息,因此当前当然具有恒定的性能开销,但是这种冗余可以在将来删除(例如,使所有 微块都具有0x0作为块哈希,并且仅填充RPC中的修剪字段以便向后兼容)。

现在,我们可以介绍状态承诺链(SCC:State Commitment Chain)。SCC包含状态根列表,在乐观情况下,该列表对应于 针对先前状态在CTC中应用每个交易的结果。如果不是这种情况,则欺诈验证过程将删除无效的状态根,然后删除所有 无效的状态根,以便可以为这些交易提出正确的状态根。

与CTC相反,SCC没有任何酷炫的数据表示形式。它的目的很简单:给定状态根列表,它会对其进行存储并保存批处理中 包含的中间状态根的merkle根,以供以后通过appendStateBatch用作欺诈证明。

欺诈证明

既然我们了解了OVM的基本概念以及将其状态锚定在以太坊上的支持功能,那么让我们深入探讨争端解决程序, 也就是欺诈证明。

Sequencer执行3件事:

  • 接收用户提交的交易
  • 批量汇总这些交易并将其发布在权威交易链中
  • 在状态承诺链中将交易产生的中间状态根发布为状态批。

例如,如果在CTC中发布了8个交易,则对于每个状态从S1到S8的转换,在SCC中都会有8个状态根。

但是,如果Sequencer是恶意的,他们可以在状态Trie中将其帐户余额设置为1000万个ETH,这显然是非法的操作, 从而使状态根及其后面的所有状态根均无效。他们可以通过发布看起来像这样的数据来做到这一点:

我们注定要失败吗?我们必须做点什么!

众所周知,Optimistic Rollup假定存在验证者:对于Sequencer发布的每个交易,验证者负责下载该交易并将其 应用于本地状态。如果一切都匹配,它们什么也不做,但是如果不匹配,那就有问题了!为了解决该问题,他们 将尝试在以太坊上重新执行T4以产生S4。然后,将修剪所有在S4之后发布的状态根,因为无法保证它对应于有效状态:

从较高层面来说,欺诈证明是“以S3作为我的开始状态,我想证明在S3上应用T4会导致S4,这与Sequencer发布的内容 不同(

Optimism Rollup原理详解相关推荐

  1. Oracle SQL语句执行流程与顺序原理详解

    以前读的文章,保存到本地了,忘记来源了,分享一下,本地存着怕丢了 Oracle SQL语句执行流程与顺序原理详解 第一步:客户端把语句发给服务器端执行 当我们在客户端执行SQL语句时,客户端会把这条S ...

  2. CRF(条件随机场)与Viterbi(维特比)算法原理详解

    摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...

  3. LVS原理详解(3种工作方式8种调度算法)--老男孩

    一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...

  4. jQuery中getJSON跨域原理详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp28 jQuery中getJSON跨域原理详解 前几天我再开发一个叫 河蟹工 ...

  5. nginx配置文件及工作原理详解

    nginx配置文件及工作原理详解 1 nginx配置文件的结构 2 nginx工作原理 1 nginx配置文件的结构 1)以下是nginx配置文件默认的主要内容: #user nobody; #配置用 ...

  6. EMD算法之Hilbert-Huang Transform原理详解和案例分析

    目录 Hilbert-Huang Transform 希尔伯特-黄变换 Section I 人物简介 Section II Hilbert-Huang的应用领域 Section III Hilbert ...

  7. 图像质量损失函数SSIM Loss的原理详解和代码具体实现

    本文转自微信公众号SIGAI 文章PDF见: http://www.tensorinfinity.com/paper_164.html http://www.360doc.com/content/19 ...

  8. 深入剖析Redis系列(三) - Redis集群模式搭建与原理详解

    前言 在 Redis 3.0 之前,使用 哨兵(sentinel)机制来监控各个节点之间的状态.Redis Cluster 是 Redis 的 分布式解决方案,在 3.0 版本正式推出,有效地解决了 ...

  9. 【Android架构师java原理详解】二;反射原理及动态代理模式

    前言: 本篇为Android架构师java原理专题二:反射原理及动态代理模式 大公司面试都要求我们有扎实的Java语言基础.而很多Android开发朋友这一块并不是很熟练,甚至半路初级底子很薄,这给我 ...

最新文章

  1. iphone11系统输入框的光标位置不正常
  2. 京东JIMI用户未来意图预测技术揭秘
  3. 电饼锅的样式图片价格_2020年三明治机/电饼铛推荐选购指南,电饼档那个牌子好?有哪些好用的三明治机/早餐机/电饼铛?...
  4. windows server如何加强云服务器的安全性
  5. 抓娃娃机爪不动怎么办_黄子韬吃娃娃菜能把临时牙咬断?种植牙到底结实不结实?...
  6. c语言贪吃蛇最简单代码_C语言指针,这可能是史上最干最全的讲解啦(附代码)!!!...
  7. Day5 - 前端高频面试题之计算机网络相关
  8. 原来 Python 还有这些实用的功能和特点!
  9. adb shell settings(系统服务:settings)
  10. 转型实践|产品设计从青铜到王者—青铜篇
  11. Javascript 格式化json字符串
  12. 如何卸载重装Adobe Acrobat
  13. [Realtek sdk-4.4.x ]RTL8198D+RTL8192F+RTL8812F WiFi 信道、频宽、加密方式、SSID设置(WPA/WPA/WPA3加密方式)
  14. _tsplitpath_s(分解路径)
  15. OSPF在NBMA网络中的五种模式
  16. UR机器人(14)-解决故障
  17. RC522RFID读卡寻卡失败原因
  18. 海康工业相机USB相机问题排查思路—Windows 系统
  19. c语言写层次分析法,基于C语言的层次分析法在医院工作质量评价中的设计与实现...
  20. 抽卡计算机在线,《闪耀暖暖》抽卡概率计算器

热门文章

  1. 七日杀服务器进不去显示红字,七日杀各类红字问题解决方案 七日杀红字怎么办...
  2. 手机变板砖?这有专业救砖教程
  3. antd Form.Item的name是数组时如何取值
  4. 步步扎进Java-泛型与集合
  5. 八张图,看懂数据分析如何驱动决策
  6. 领峰:黄金价格走势图的分析你懂吗?
  7. Habit-基于JFinal+vue+element的后台通用模板项目
  8. 中小企业倒闭率_死亡率98%,每分钟2家企业倒闭!中小企业如何避免“死亡规律”?...
  9. conda使用清华源解决pytorch安装过慢问题5分钟安装成功!!!!!!!
  10. 面试初体验:经历七场面试大数据开发工程师岗位总结(一)