optimism 标准桥 提供两个网络之间移动资产,比如L1存入100美元C,来换取L2上100美元C,
需要授权给标准桥来存入ERC20
标准网桥只能与在Optimism上正确配置了ERC-20版本的令牌一起使用。如果您将任何其他类型的令牌直接发送到标准网桥(不使用用户界面或API),它就会被卡住,从而失去该值。token查看地址,有
optimismBridgeAddress才可以使用此标准桥在令牌列表中存在可以选择使用optimism标准网桥或者使用其他不同的网桥,提现在extensions.optimismBridgeAddress值不同。
IL1ERC20Bridge
// 获取对应的L2网桥合约的地址
function l2TokenBridge() external returns (address);
// 将ERC20的一部分存入caller在L2上的余额
function depositERC20(
address _l1Token, // 我们存放的L1 ERC20的地址
address _l2Token, // L1各自的L2 ERC20的地址
uint256 _amount, // ERC20存款金额
uint32 _l2Gas, // 完成 L2上的存款所需的气体限额
bytes calldata _data // 要转发到L2的可选数据 🚩 外部调用传递的什么数据
) external;
// 将一笔 ERC20存入接受者在 L2上的余额
function depositERC20To(
address _l1Token,
address _l2Token,
address _to, // 将取款记入贷方的L2地址
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) external;
// 完成从L2到L1的取款,并将资金记入L1 ERC20代币的接收者余额
function finalizeERC20Withdrawal(
address _l1Token, // 要终结的L1令牌的地址
address _l2Token, // 开始提款的 L2令牌地址
address _from, // 启动传输的L2地址
address _to, // 将取款记入贷方的L1地址。
uint256 _amount, // 要存入的ERC20金额
bytes calldata _data // 发送方在L2上提供的数据
) external;L1StandardBridge
标准桥的L1部分。负责完成L2的取款,并向ETH和符合ERC20的L2发起存款,支持ETH和ETH上的ERC20
从L1存入金额到L2
存入金额调用的是depositETH,这里的calldata🚩, 在存入的时候L1桥合约会记录L1地址映射到L2地址所存入的金额.
QA: 为什么需要地址别名
- https://community.optimism.io/docs/developers/build/differences/#using-eth-in-contracts
- https://community.optimism.io/docs/developers/build/differences/#accessing-the-latest-l1-block-number
QA : l2gas 是如何获取L2上的gas的
QA : finalizeDeposit传输零地址的意思
// 将L1令牌映射到L2令牌,以存储L1令牌的余额
mapping(address => mapping(address => uint256)) public deposits;
function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
_initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
}
function _initiateETHDeposit(
address _from,
address _to,
uint32 _l2Gas,
bytes memory _data
) internal {
// 存入ETH和存入ERC20是不一样的参数传递, 存入ETH, _l2Token 永远是OVM_ETH地址
// _l1Token 永远是 address(0)
bytes memory message = abi.encodeWithSelector(
IL2ERC20Bridge.finalizeDeposit.selector,
address(0),
Lib_PredeployAddresses.OVM_ETH,
_from, // 从L1提取存款的帐户 msg.sender
_to, // L2上的存款账户 msg.sender 🚩这个账户地址难道不会和L1一样,但是属于不同人创建的? 除非这条链不支持创建账户,不然这钱不就有第二个人知道了?
msg.value,
_data
);
// Send calldata into L2
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
// slither-disable-next-line reentrancy-events
emit ETHDepositInitiated(_from, _to, msg.value, _data);
}
IL2ERC20Bridge.finalizeDeposit.selector 设置了一个L2会调用的函数 finalizeDeposit:
finalizeDeposit 完成从L1到L2的存款,并将资金记入该L2代币的接收者余额。如果该调用不是来自L1StandardTokenBridge中的相应存款,则该调用将失败。
这个只能 跨链账户去调用,将相同数量的金额存入到L2的账户
function finalizeDeposit(
address _l1Token, // 用于调用的l1令牌的地址
address _l2Token, // 用于调用的l2令牌的地址
address _from, // 从L2提取存款的帐户。
address _to, // 接收取款的地址
uint256 _amount,
bytes calldata _data
) external;到了这里实际是L1的合约跨链调用L2 的合约, sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);,调用的CrossDomainEnabled(专门做跨链消息传送的合约)
// L1StandardBridge
// Send calldata into L2
sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
function sendCrossDomainMessage(
address _crossDomainTarget, // l2TokenBridge地址
uint32 _gasLimit,
bytes memory _message
) internal {
getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);
}其实到这里,组装了一个调用消息,是让L2去调用自己的方法,包括L2的合约地址,L2gas和L2的ABI信息(传入了L1的参数),
最终会把组装的调用消息交给跨链合约。
跨链合约主要做了以下事情:
- 将CTC队列目前长度作为nonce
- 构造跨链calldata(编码的跨链da ta中的m s g.sender应该是L1桥的地址)
- **发送跨链消息(最终出口)**给CanonicalTransactionChain(L1上的合约) 合约, 同时会将此消息存储在CanonicalTransactionChain合约上通过enqueue,这样L1上的工作完成,L1保存了这个消息,同时触发事件SentMessage(L1CrossDomainMessanger.sol), 谁会去做监听????
- enqueue 会将整个L1存入token的一连串的数据包括发送者,L2目标token合约,交易data keccak256作为一笔交易以及时间戳和当前区块打包成Elements存入CanonicalTransactionChain,并触发TransactionEnqueued事件(此事件由DTL监听)
- ovmCanonicalTransactionChain(预先部署) 合约地址:0x4200000000000000000000000000000000000007
// L1CrossDomainMessenger.sol
function sendMessage(
address _target,
bytes memory _message,
uint32 _gasLimit
) public {
address ovmCanonicalTransactionChain = resolve("CanonicalTransactionChain");
// Use the CTC queue length as nonce
uint40 nonce = ICanonicalTransactionChain(ovmCanonicalTransactionChain).getQueueLength();
bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
_target, // L2tokenBridge
msg.sender, // ?是否是DomainMessenger
_message, // 存款消息
nonce
);
_sendXDomainMessage(ovmCanonicalTransactionChain, xDomainCalldata, _gasLimit);
emit SentMessage(_target, msg.sender, _message, nonce, _gasLimit);
} function _sendXDomainMessage(
address _canonicalTransactionChain,
bytes memory _message,
uint256 _gasLimit
) internal {
ICanonicalTransactionChain(_canonicalTransactionChain).enqueue(
Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER, // 将交易发送到的目标L2合同
_gasLimit,
_message
);
}将一笔交易添加到队列:
- calldata的数据不要大于50000字节
- L2 tx gas 相关最大100000
- 触发 TransactionEnqueued事件
function enqueue(
address _target,
uint256 _gasLimit,
bytes memory _data
) external {
require(
_data.length <= MAX_ROLLUP_TX_SIZE,
"Transaction data size exceeds maximum for rollup transaction."
);
require(
_gasLimit <= maxTransactionGasLimit,
"Transaction gas limit exceeds maximum for rollup transaction."
);
require(_gasLimit >= MIN_ROLLUP_TX_GAS, "Transaction gas limit too low to enqueue.");
if (_gasLimit > enqueueL2GasPrepaid) {
uint256 gasToConsume = (_gasLimit - enqueueL2GasPrepaid) / l2GasDiscountDivisor;
uint256 startingGas = gasleft();
require(startingGas > gasToConsume, "Insufficient gas for L2 rate limiting burn.");
uint256 i;
while (startingGas - gasleft() < gasToConsume) {
i++;
}
}
address sender;
if (msg.sender == tx.origin) {
sender = msg.sender;
} else {
sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
}
bytes32 transactionHash = keccak256(abi.encode(sender, _target, _gasLimit, _data));
queueElements.push(
Lib_OVMCodec.QueueElement({
transactionHash: transactionHash,
timestamp: uint40(block.timestamp),
blockNumber: uint40(block.number)
})
);
uint256 queueIndex = queueElements.length - 1;
emit TransactionEnqueued(sender, _target, _gasLimit, _data, queueIndex, block.timestamp);
}
DTL组件是由typescript写的,TransactionEnqueued事件的处理如下,会解析事件并查出calldata,写入数据库
export const handleEventsTransactionEnqueued: EventHandlerSet<
TransactionEnqueuedEvent,
null,
EnqueueEntry
> = {
getExtraData: async () => {
return null
},
parseEvent: (event) => {
return {
index: event.args._queueIndex.toNumber(),
target: event.args._target,
data: event.args._data,
gasLimit: event.args._gasLimit.toString(),
origin: event.args._l1TxOrigin,
blockNumber: BigNumber.from(event.blockNumber).toNumber(),
timestamp: event.args._timestamp.toNumber(),
ctcIndex: null,
}
},
storeEvent: async (entry, db) => {
...
await db.putEnqueueEntries([entry])
},
}接着就是L2geth(sequencer)从DTL同步TransactionEnqueued 事件,转为交易并执行,这部分在L2geth/rollup/sync_service下面,专门由SequencerLoop执行
L2geth 里面存储着一个rollupclient,用来对DTL进行Http请求的。请求注册的路由在DTL/service.ts
//SequencerLoop 是在 sequencer 模式下运行的轮询循环。它排序
//交易,然后更新 EthContext。
func (s *SyncService) SequencerLoop() {
...
s.sequence();
...
}执行交易主要由s.applyTransaction(tx)实现,会调用applyIndexedTransaction,交易的来源是指L1 batch,或者是sequencer同步DTL中的Transactionenqueued事件的交易。
func (s *SyncService) syncQueueTransactionRange(start, end uint64) error {
log.Info("Syncing enqueue transactions range", "start", start, "end", end)
for i := start; i <= end; i++ {
tx, err := s.client.GetEnqueue(i)
if err != nil {
return fmt.Errorf("Canot get enqueue transaction; %w", err)
}
if err := s.applyTransaction(tx); err != nil {
return fmt.Errorf("Cannot apply transaction: %w", err)
}
}
return nil
}applyTransaction 最终会将交易发到ch:
s.txFeed.Send(core.NewTxsEvent{
Txs: txs,
ErrCh: errCh,
})case ev := <-w.rollupCh:
...
if err := w.commitNewTx(tx); err == nil {
...commitNewTx(提交单个交易DTL扫的交易)→applyTransaction→applyMessage→evm执行→writeBlockWithState ,sequencer是通过POA共识的。最终挖出一个L2的区块并写入数据库。同时移除了w.chainHeadCh提交挖矿任务,这样只能通过执行同步服务从DTL拉过来的交易和用户发给sequencer的交易来执行生成L2 block .同时注意到把TransactionMeta也记录到state db去了。
🚩这里关于L1到L2的消息如何转换成l2交易的具体过程,是需要详细解释的,这才是比较关键的一步
batch-submitter 监听L2区块,会打包txBatch 提交到L1合约,首先会一直判断是否有L2block更新:
start, end, err := s.cfg.Driver.GetBatchBlockRange(s.ctx)接着会通过CraftBatchTx使用给定的nonce将开始和结束之间的L2块转换为批处理交易。在生成的交易中使用虚拟天然气价格,以用于规模估计。
tx, err := s.cfg.Driver.CraftBatchTx(
s.ctx, start, end, nonce,
)批处理交易转换完成了之后还是会调用,batch-submitter会使用L1的客户端去发送这笔交易:
tx, err := d.rawCtcContract.RawTransact(opts, calldata)
func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
var err error
// Ensure a valid value field and resolve the account nonce
value := opts.Value
if value == nil {
value = new(big.Int)
}
var nonce uint64
if opts.Nonce == nil {
nonce, err = c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From)
if err != nil {
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err)
}
} else {
nonce = opts.Nonce.Uint64()
}
// Figure out reasonable gas price values
if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) {
return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
}
head, err := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil)
if err != nil {
return nil, err
}
if head.BaseFee != nil && opts.GasPrice == nil {
if opts.GasTipCap == nil {
tip, err := c.transactor.SuggestGasTipCap(ensureContext(opts.Context))
if err != nil {
return nil, err
}
opts.GasTipCap = tip
}
if opts.GasFeeCap == nil {
gasFeeCap := new(big.Int).Add(
opts.GasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
)
opts.GasFeeCap = gasFeeCap
}
if opts.GasFeeCap.Cmp(opts.GasTipCap) < 0 {
return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", opts.GasFeeCap, opts.GasTipCap)
}
} else {
if opts.GasFeeCap != nil || opts.GasTipCap != nil {
return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet")
}
if opts.GasPrice == nil {
price, err := c.transactor.SuggestGasPrice(ensureContext(opts.Context))
if err != nil {
return nil, err
}
opts.GasPrice = price
}
}
gasLimit := opts.GasLimit
if gasLimit == 0 {
// Gas estimation cannot succeed without code for method invocations
if contract != nil {
if code, err := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.address); err != nil {
return nil, err
} else if len(code) == 0 {
return nil, ErrNoCode
}
}
// If the contract surely has code (or code is not needed), estimate the transaction
msg := ethereum.CallMsg{From: opts.From, To: contract, GasPrice: opts.GasPrice, GasTipCap: opts.GasTipCap, GasFeeCap: opts.GasFeeCap, Value: value, Data: input}
gasLimit, err = c.transactor.EstimateGas(ensureContext(opts.Context), msg)
if err != nil {
return nil, fmt.Errorf("failed to estimate gas needed: %v", err)
}
}
// Create the transaction, sign it and schedule it for execution
var rawTx *types.Transaction
if opts.GasFeeCap == nil {
baseTx := &types.LegacyTx{
Nonce: nonce,
GasPrice: opts.GasPrice,
Gas: gasLimit,
Value: value,
Data: input,
}
if contract != nil {
baseTx.To = &c.address
}
rawTx = types.NewTx(baseTx)
} else {
baseTx := &types.DynamicFeeTx{
Nonce: nonce,
GasFeeCap: opts.GasFeeCap,
GasTipCap: opts.GasTipCap,
Gas: gasLimit,
Value: value,
Data: input,
}
if contract != nil {
baseTx.To = &c.address
}
rawTx = types.NewTx(baseTx)
}
if opts.Signer == nil {
return nil, errors.New("no signer to authorize the transaction with")
}
signedTx, err := opts.Signer(opts.From, rawTx)
if err != nil {
return nil, err
}
if opts.NoSend {
return signedTx, nil
}
if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil {
return nil, err
}
return signedTx, nil
}实际就是通过连接的L1客户端去调用绑定的CTC合约(CanonicalTransactionChain.appendSequencerBatch())这个函数,然后触发TransactionBatchSubmitter事件,DTL再监听这个事件并存储。appendStateBatch() 也是一样(但是这个都是实现的CraftBatchTx接口,到底是调哪一个,还是都调用),这两个函数分别对应ChainStorageContainer和StateCommitmentChain合约
从L1存款ERC20到L2
当L1上开始存款时,L1桥将资金转移到自己(L1标准桥合约中),同时合约会记录L1 → L2金额的映射,以备将来取款
不管是存ETH还是存ERC20,都会触发TransactionEnqueued事件,DTL层会定期扫L1的区块,获取这个事件并存储到levelDB