• 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: 为什么需要地址别名

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扫的交易)applyTransactionapplyMessageevm执行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

从L2提款到L1

参考