跳到主要内容

Layer 2 扩展方案

随着以太坊生态的发展,网络拥堵和高昂的 Gas 费用成为制约其大规模应用的主要瓶颈。Layer 2(L2)扩展方案通过在链下处理交易,同时继承以太坊主网的安全性,成为解决可扩展性问题的关键技术。本章将深入介绍 L2 的技术原理和开发实践。

为什么需要 Layer 2

以太坊的可扩展性挑战

以太坊主网(Layer 1)每秒只能处理约 15-45 笔交易(TPS),远低于传统支付系统(如 Visa 约 24,000 TPS)。当网络需求旺盛时,会导致:

高 Gas 费用:用户需要支付高额费用才能让交易被打包,有时单笔交易费用超过 50 美元。

交易延迟:网络拥堵时,交易确认时间大幅延长,影响用户体验。

准入门槛:高昂的使用成本阻碍了普通用户和小额交易场景的参与。

扩展性三难困境

区块链面临著名的"不可能三角":

          去中心化

/|\
/ | \
/ | \
/ | \
/ | \
/ | \
┌──────┼──────┐
│ │ │
安全性 ────┼──── 可扩展性

去中心化:网络由大量节点维护,无需许可参与。

安全性:抵抗攻击的能力,数据不可篡改。

可扩展性:处理交易的能力(TPS)。

Layer 2 的核心思路是在不牺牲去中心化和安全性的前提下,通过链下处理来提升可扩展性。

Layer 2 技术分类

Rollups

Rollup 是目前最主流的 L2 方案,它将数百笔交易打包(Rollup)在一起,在链下执行计算,但将交易数据发布到 L1。

┌─────────────────────────────────────────────────────────────┐
│ Layer 1 (以太坊主网) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Rollup 合约 │ │
│ │ - 存储状态根 │ │
│ │ - 处理存款/取款 │ │
│ │ - 验证欺诈证明/有效性证明 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

│ 提交状态根和交易数据

┌─────────────────────────────────────────────────────────────┐
│ Layer 2 (Rollup 链) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 排序器 (Sequencer) │ │
│ │ - 收集交易 │ │
│ │ - 执行交易 │ │
│ │ - 生成新状态根 │ │
│ │ - 提交到 L1 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

乐观汇总(Optimistic Rollups)

乐观汇总假设交易默认有效,通过挑战期来检测欺诈。

工作原理

  1. 排序器收集交易并执行,生成新状态根
  2. 将状态根和交易数据提交到 L1
  3. 进入挑战期(通常 7 天),任何人都可以提交欺诈证明
  4. 如果挑战期内没有有效挑战,状态被最终确认

代表项目

  • Arbitrum:由 Offchain Labs 开发,采用多轮交互式欺诈证明
  • Optimism:采用单轮欺诈证明,与 EVM 高度兼容
  • Base:由 Coinbase 开发,基于 OP Stack 构建

优势

  • 与 EVM 兼容性好,易于迁移现有合约
  • 交易成本低,约为 L1 的 1/10
  • 成熟的生态系统和工具链

劣势

  • 提款需要等待挑战期(约 7 天)
  • 依赖排序器的活性

零知识汇总(ZK Rollups)

ZK Rollup 使用零知识证明验证交易的有效性,实现即时最终性。

工作原理

  1. 排序器收集交易并执行
  2. 生成零知识证明(ZK Proof),证明状态转换的正确性
  3. 将证明和新状态根提交到 L1
  4. L1 合约验证证明,立即确认新状态

代表项目

  • zkSync:由 Matter Labs 开发,支持原生账户抽象
  • StarkNet:使用 Cairo 语言,高性能通用 ZK-Rollup
  • Polygon zkEVM:与 EVM 字节码兼容的 ZK-Rollup
  • Scroll:基于 zkEVM 的以太坊等效 L2

优势

  • 即时最终性,无需等待挑战期
  • 更高的数据压缩效率
  • 更强的安全性保证

劣势

  • 生成 ZK 证明需要大量计算资源
  • 对 EVM 兼容性有一定限制
  • 技术复杂度较高

其他 L2 方案

状态通道(State Channels)

状态通道允许参与者在链下进行多次交易,只在开启和关闭时与链上交互。

适用场景

  • 支付通道(如比特币闪电网络)
  • 游戏等需要高频交互的应用
  • 参与者数量有限的场景

优势:即时确认,几乎零成本 劣势:需要参与者在线,资金锁定

侧链(Sidechains)

侧链是独立于主链运行的区块链,通过双向桥接与主链连接。

代表项目:Polygon PoS

优势:高吞吐量,灵活的共识机制 劣势:安全性独立于主链,依赖自身验证者

Validium

Validium 类似 ZK Rollup,但数据不存储在 L1,而是存储在链下数据可用性委员会。

优势:更高的吞吐量 劣势:数据可用性依赖委员会

Optimistic Rollup 实现

以下是简化的 Optimistic Rollup 核心合约实现:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract OptimisticRollup {
struct StateRoot {
bytes32 root;
uint256 blockNumber;
uint256 timestamp;
address proposer;
bool finalized;
}

struct Challenge {
bytes32 stateRoot;
address challenger;
uint256 deposit;
uint256 deadline;
bool resolved;
}

StateRoot[] public stateRoots;
mapping(bytes32 => Challenge) public challenges;

uint256 public constant CHALLENGE_PERIOD = 7 days;
uint256 public constant CHALLENGE_DEPOSIT = 1 ether;

address public admin;
bytes32 public currentStateRoot;

event StateRootProposed(bytes32 indexed root, address proposer, uint256 index);
event ChallengeInitiated(bytes32 indexed root, address challenger);
event ChallengeResolved(bytes32 indexed root, bool valid);
event StateFinalized(bytes32 indexed root, uint256 index);

modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}

constructor() {
admin = msg.sender;
}

function proposeStateRoot(bytes32 _stateRoot) external {
require(_stateRoot != bytes32(0), "Invalid state root");

StateRoot memory newRoot = StateRoot({
root: _stateRoot,
blockNumber: block.number,
timestamp: block.timestamp,
proposer: msg.sender,
finalized: false
});

stateRoots.push(newRoot);
currentStateRoot = _stateRoot;

emit StateRootProposed(_stateRoot, msg.sender, stateRoots.length - 1);
}

function challengeStateRoot(uint256 _index) external payable {
require(_index < stateRoots.length, "Invalid index");
require(msg.value >= CHALLENGE_DEPOSIT, "Insufficient deposit");

StateRoot storage root = stateRoots[_index];
require(!root.finalized, "Already finalized");
require(
block.timestamp <= root.timestamp + CHALLENGE_PERIOD,
"Challenge period expired"
);

bytes32 rootHash = root.root;
require(challenges[rootHash].challenger == address(0), "Already challenged");

challenges[rootHash] = Challenge({
stateRoot: rootHash,
challenger: msg.sender,
deposit: msg.value,
deadline: block.timestamp + 1 days,
resolved: false
});

emit ChallengeInitiated(rootHash, msg.sender);
}

function resolveChallenge(
uint256 _index,
bytes calldata _fraudProof,
bool _isValid
) external {
require(_index < stateRoots.length, "Invalid index");

StateRoot storage root = stateRoots[_index];
bytes32 rootHash = root.root;

Challenge storage challenge = challenges[rootHash];
require(challenge.challenger != address(0), "No challenge exists");
require(!challenge.resolved, "Already resolved");
require(block.timestamp <= challenge.deadline, "Challenge expired");

bool proofValid = _verifyFraudProof(rootHash, _fraudProof);

if (proofValid && !_isValid) {
payable(challenge.challenger).transfer(challenge.deposit * 2);
_removeStateRoot(_index);
} else {
payable(root.proposer).transfer(challenge.deposit);
}

challenge.resolved = true;
emit ChallengeResolved(rootHash, _isValid);
}

function finalizeStateRoots() external {
uint256 cutoff = block.timestamp - CHALLENGE_PERIOD;

for (uint256 i = 0; i < stateRoots.length; i++) {
if (!stateRoots[i].finalized &&
stateRoots[i].timestamp <= cutoff &&
challenges[stateRoots[i].root].challenger == address(0)) {

stateRoots[i].finalized = true;
emit StateFinalized(stateRoots[i].root, i);
}
}
}

function _verifyFraudProof(bytes32, bytes calldata)
internal
pure
returns (bool)
{
return true;
}

function _removeStateRoot(uint256 _index) internal {
stateRoots[_index] = stateRoots[stateRoots.length - 1];
stateRoots.pop();
}

function getStateRootCount() external view returns (uint256) {
return stateRoots.length;
}
}

ZK Rollup 实现

以下是简化的 ZK Rollup 核心合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IVerifier {
function verifyProof(
uint256[2] calldata pA,
uint256[2][2] calldata pB,
uint256[2] calldata pC,
uint256[2] calldata publicSignals
) external view returns (bool);
}

contract ZKRollup {
struct Block {
bytes32 stateRoot;
bytes32 transactionsHash;
uint256 blockNumber;
uint256 timestamp;
address operator;
}

Block[] public blocks;
mapping(bytes32 => bool) public processedTransactions;

IVerifier public immutable verifier;
bytes32 public currentStateRoot;
uint256 public blockCount;

event BlockCommitted(uint256 indexed blockNumber, bytes32 stateRoot);
event TransactionProcessed(bytes32 indexed txHash);

constructor(IVerifier _verifier, bytes32 _genesisStateRoot) {
verifier = _verifier;
currentStateRoot = _genesisStateRoot;

blocks.push(Block({
stateRoot: _genesisStateRoot,
transactionsHash: bytes32(0),
blockNumber: 0,
timestamp: block.timestamp,
operator: msg.sender
}));
}

function commitBlock(
bytes32 _newStateRoot,
bytes32 _transactionsHash,
uint256[2] calldata _pA,
uint256[2][2] calldata _pB,
uint256[2] calldata _pC,
uint256[2] calldata _publicSignals
) external {
require(
verifier.verifyProof(_pA, _pB, _pC, _publicSignals),
"Invalid proof"
);

require(
bytes32(_publicSignals[0]) == currentStateRoot,
"Invalid previous state root"
);
require(
bytes32(_publicSignals[1]) == _newStateRoot,
"State root mismatch"
);

Block memory newBlock = Block({
stateRoot: _newStateRoot,
transactionsHash: _transactionsHash,
blockNumber: blockCount + 1,
timestamp: block.timestamp,
operator: msg.sender
});

blocks.push(newBlock);
currentStateRoot = _newStateRoot;
blockCount++;

emit BlockCommitted(blockCount, _newStateRoot);
}

function verifyInclusion(
bytes32 _leaf,
bytes32[] calldata _proof,
uint256 _index
) public view returns (bool) {
bytes32 computedHash = _leaf;

for (uint256 i = 0; i < _proof.length; i++) {
bytes32 proofElement = _proof[i];

if (_index % 2 == 0) {
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}

_index = _index / 2;
}

return computedHash == currentStateRoot;
}

function withdraw(
uint256 _amount,
uint256 _nonce,
bytes32[] calldata _merkleProof,
uint256 _merkleIndex
) external {
bytes32 leaf = keccak256(abi.encodePacked(
msg.sender,
_amount,
_nonce,
"withdrawal"
));

require(
verifyInclusion(leaf, _merkleProof, _merkleIndex),
"Invalid merkle proof"
);

require(!processedTransactions[leaf], "Already withdrawn");
processedTransactions[leaf] = true;

payable(msg.sender).transfer(_amount);

emit TransactionProcessed(leaf);
}

function getBlockCount() external view returns (uint256) {
return blockCount;
}
}

L1-L2 消息传递

L2 与 L1 之间的消息传递是跨链交互的核心:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract L1L2MessageBridge {
struct Message {
address sender;
address target;
bytes data;
uint256 value;
uint256 gasLimit;
uint256 nonce;
}

mapping(bytes32 => bool) public sentMessages;
mapping(bytes32 => bool) public relayedMessages;

uint256 public nonce;

event MessageSent(
address indexed sender,
address indexed target,
bytes32 indexed messageHash,
bytes data
);

event MessageRelayed(
bytes32 indexed messageHash,
bool success
);

function sendMessage(
address _target,
bytes calldata _data,
uint256 _gasLimit
) external payable {
require(_gasLimit >= 21000, "Gas limit too low");

Message memory message = Message({
sender: msg.sender,
target: _target,
data: _data,
value: msg.value,
gasLimit: _gasLimit,
nonce: nonce++
});

bytes32 messageHash = hashMessage(message);
sentMessages[messageHash] = true;

emit MessageSent(msg.sender, _target, messageHash, _data);
}

function relayMessage(
address _sender,
address _target,
bytes calldata _data,
uint256 _value,
uint256 _gasLimit,
uint256 _nonce
) external {
Message memory message = Message({
sender: _sender,
target: _target,
data: _data,
value: _value,
gasLimit: _gasLimit,
nonce: _nonce
});

bytes32 messageHash = hashMessage(message);

require(sentMessages[messageHash], "Message not sent");
require(!relayedMessages[messageHash], "Message already relayed");

relayedMessages[messageHash] = true;

(bool success, ) = _target.call{value: _value, gas: _gasLimit}(
abi.encodePacked(_data, _sender)
);

emit MessageRelayed(messageHash, success);

require(success, "Message execution failed");
}

function hashMessage(Message memory _message)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(
_message.sender,
_message.target,
_message.data,
_message.value,
_message.gasLimit,
_message.nonce
));
}
}

主流 L2 对比

特性ArbitrumOptimismzkSyncStarkNet
类型OptimisticOptimisticZKZK
最终性7 天7 天即时即时
EVM 兼容
开发语言SoliditySoliditySolidityCairo
TVL最高
生态成熟度成熟成熟发展中发展中

L2 开发最佳实践

选择合适的 L2

选择 Arbitrum/Optimism 如果

  • 需要完整的 EVM 兼容性
  • 现有合约需要快速迁移
  • 生态成熟度和工具链支持重要

选择 zkSync/StarkNet 如果

  • 需要即时最终性
  • 构建新项目,可以从头设计
  • 需要更高的安全性保证

Gas 优化

contract L2GasOptimized {
function batchOperation(
address[] calldata targets,
bytes[] calldata datas
) external {
require(targets.length == datas.length, "Length mismatch");

for (uint256 i = 0; i < targets.length;) {
(bool success,) = targets[i].call(datas[i]);
require(success, "Call failed");

unchecked { ++i; }
}
}
}

跨链桥接

const { ethers } = require('ethers');

async function bridgeToL2(amount) {
const l1Bridge = new ethers.Contract(L1_BRIDGE_ADDRESS, L1_BRIDGE_ABI, signer);

const tx = await l1Bridge.depositTo(RECIPIENT_ADDRESS, {
value: ethers.parseEther(amount)
});

const receipt = await tx.wait();
console.log('Bridge transaction:', receipt.hash);

return receipt;
}

小结

Layer 2 扩展方案是以太坊实现大规模应用的关键技术。Optimistic Rollup 和 ZK Rollup 各有优势,开发者应根据项目需求选择合适的方案。随着技术成熟,L2 将继续降低用户成本,提升区块链的可用性。

参考资料