Celo系列博客有:
- Celo中的随机数
- Chorus One:bridge between Cosmos and Celo
- Celo生态图
前序博客有:
- Optics Bridge:Celo <-> 以太坊
Optics Bridge开源代码见:
- https://github.com/celo-org/optics-monorepo(Rust & Solidity等)
Optics中:
- 以Solidity语言实现了链上合约
- 以Rust语言实现了链下系统agents
未来将针对NEAR和Solana实现Rust版本的链上合约。
Optics Bridge当前已部署在Celo、以太坊和Polygon主网上。 Celo、以太坊、Polygon主网上的Home及其它核心合约地址见:
- https://github.com/celo-org/optics-monorepo/tree/main/rust/config/mainnet【V1】
- https://github.com/celo-org/optics-monorepo/blob/main/rust/config/production-community/【V2】
Optics V2中,向以太坊上各Replica合约提交proveAndProcess
请求的费用需由用户承担,而不再由Optics社区运营的processor来承担:【Notice: Optics will no longer cover processing fees when returning to Ethereum.】【Cleo、Polygon上的prove/process
操作由相应的processor承担】
- https://etherscan.io/tx/0xe67f9b18d761a8cf61b0b83686e98beaf34562a572695eb632f5a0e539f568e5【以太坊】
- https://polygonscan.com/tx/0x81bc053af25dd5adc9f3876494f36f3a028479480234853b35ab1162d7baca08【Polygon】
当前各链分配了唯一的Domian ID:
- Celo:Domain ID为1667591279
- 以太坊:Domain ID为6648936
- Polygon:Domain ID为1886350457
- Avalanche:Domain ID为1635148152
目前,Home
合约中只能配置一个Updater,仅能由UpdaterManager合约来设置:【并不是随机选择updater来签名。且目前对Updater的bond/slash功能并未实现。】
/**
* @notice Set a new Updater
* @param _updater the new Updater
*/
function setUpdater(address _updater) external onlyUpdaterManager {
_setUpdater(_updater);
}
/**
* @notice Set the address of a new updater
* @dev only callable by trusted owner
* @param _updaterAddress The address of the new updater
*/
function setUpdater(address _updaterAddress) external onlyOwner {
_updater = _updaterAddress;
Home(home).setUpdater(_updaterAddress);
}
Optics会在链之间建立communication channels,但是这些channels由xApp(又名“cross-chain applications”) developers来使用。 在 https://github.com/celo-org/optics-monorepo 提供了标准的模式来集成Optics channels,来确保communication是安全的。
集成过程中,需要以下关键要素:
- 1)已部署在链上的一个Home合约和任意多个Replica合约。这些合约管理Optics communication channels,可由xApp用于send and receive messages。
- 2)XAppConnectionManager合约:将xApp连接到Optics,允许xApp admin 注册新的Home和Replica合约。对channel进行注册和取消注册是确保xApp正确处理消息的主要方法。xApp可部署自己的connection manager,或者与其它xApps共享connection manager。
/**
* @title XAppConnectionManager
* @author Celo Labs Inc.
* @notice Manages a registry of local Replica contracts
* for remote Home domains. Accepts Watcher signatures
* to un-enroll Replicas attached to fraudulent remote Homes
*/
contract XAppConnectionManager is Ownable {
// ============ Public Storage ============
// Home contract
Home public home;
// local Replica address => remote Home domain
mapping(address => uint32) public replicaToDomain;
// remote Home domain => local Replica address
mapping(uint32 => address) public domainToReplica;
// watcher address => replica remote domain => has/doesn't have permission
mapping(address => mapping(uint32 => bool)) private watcherPermissions;
// ============ Events ============
/**
* @notice Emitted when a new Replica is enrolled / added
* @param domain the remote domain of the Home contract for the Replica
* @param replica the address of the Replica
*/
event ReplicaEnrolled(uint32 indexed domain, address replica);
/**
* @notice Emitted when a new Replica is un-enrolled / removed
* @param domain the remote domain of the Home contract for the Replica
* @param replica the address of the Replica
*/
event ReplicaUnenrolled(uint32 indexed domain, address replica);
/**
* @notice Emitted when Watcher permissions are changed
* @param domain the remote domain of the Home contract for the Replica
* @param watcher the address of the Watcher
* @param access TRUE if the Watcher was given permissions, FALSE if permissions were removed
*/
event WatcherPermissionSet(
uint32 indexed domain,
address watcher,
bool access
);
// ============ Modifiers ============
modifier onlyReplica() {
require(isReplica(msg.sender), "!replica");
_;
}
.........
}
- 3)Message库:Optics在链之间发送的是raw byte arrays。xApp必须定义一种message specification,使得发送时可serialized,在remote chain上进行处理时可deserialized。
- 4)Router合约:Router合约会在Optics cross-chain message format 与 本地链合约调用 之间进行转换。Router合约中同时实现了xApp的business logic。Router合约:
- 公开了面向用户的接口
- 处理来自其它链的messages
- 派发将要送到其它链的messages
BridgeRouter合约,若 liquidity provider为终端用户提供快速流动性preFill
,可获得0.05%的手续费:
// 5 bps (0.05%) hardcoded fast liquidity fee. Can be changed by contract upgrade
uint256 public constant PRE_FILL_FEE_NUMERATOR = 9995;
uint256 public constant PRE_FILL_FEE_DENOMINATOR = 10000;
/**
* @notice Allows a liquidity provider to give an
* end user fast liquidity by pre-filling an
* incoming transfer message.
* Transfers tokens from the liquidity provider to the end recipient, minus the LP fee;
* Records the liquidity provider, who receives
* the full token amount when the transfer message is handled.
* @dev fast liquidity can only be provided for ONE token transfer
* with the same (recipient, amount) at a time.
* in the case that multiple token transfers with the same (recipient, amount)
* @param _message The incoming transfer message to pre-fill
*/
function preFill(bytes calldata _message) external {
// parse tokenId and action from message
bytes29 _msg = _message.ref(0).mustBeMessage();
bytes29 _tokenId = _msg.tokenId().mustBeTokenId();
bytes29 _action = _msg.action().mustBeTransfer();
// calculate prefill ID
bytes32 _id = _preFillId(_tokenId, _action);
// require that transfer has not already been pre-filled
require(liquidityProvider[_id] == address(0), "!unfilled");
// record liquidity provider
liquidityProvider[_id] = msg.sender;
// transfer tokens from liquidity provider to token recipient
IERC20 _token = _mustHaveToken(_tokenId);
_token.safeTransferFrom(
msg.sender,
_action.evmRecipient(),
_applyPreFillFee(_action.amnt())
);
}
Solidity开发者若有兴趣实现自己的Message库和Router合约,可参看optics-xapps中的例子。
当前测试网的部署配置可参看rust/config/
目录。
强烈建议xApp admins运行一个watcher
进程来 维护其XAppConnectionManager合约 并 guard from fraud。
GovernanceRouter合约:
- https://etherscan.io/address/0xdfb2a95900d6b7c8aa95f2e46563a5fcfb5505a1/advanced【V1】
- https://etherscan.io/address/0xe552861e90a42dddc66b508a18a85bceabfcb835/advanced【V2】:对应的合约owner为https://etherscan.io/address/0x52864fcb88d0862fba14508280f9e2dcbad9281f
Optics系统sketch为:
- 1)A “home” chain commits messages in a merkle tree
- 2)A bonded “updater” attests to the commitment
- 3)The home chain ensures the attestation is accurate, and slashes if not
- 4)Attested updates are relayed on any number of “replica” chains, after a time delay
结果通常为二者之一:
- 1)All replicas have a valid commitment to messages from the home chain。
- 2)Failure was published before processing, and the updater can be slashed on the home chain。
尽管Optics的安全保证要弱于header-chain validation,但是,可满足大多数应用场景的要求。
Optics为一种新策略,可在不validate header的情况下,进行跨链通讯。 Optics的目标是:
- 创建a single short piece of state(为32字节hash),并定期更新该state。
该hash值对应为a merkle tree root,在该merkle tree中包含了a set of cross-chain messages being sent by a single chain(在Optics系统中对应为“home” chain)。home chain上的合约可提交messages,这些messages会被放入a merkle tree(可称为“message tree”)。该message tree的root可传送到任意数量的"replica" chains。
相比于对该commitment进行validity prove,Optics选择了put a delay on message receipt,以保证failures are publicly visible。这样可保证协议的参与者有机会在产生实际伤害之前,对failure进行响应。也就是说,相比于阻止the inclusion of bad messages,Optics可确保message recipients可感知该inclusion,并由机会拒绝对bad message进行处理。
为此,home chain中会指定a single “updater”。该updater需质押bond以确保其good behavior。该updater负责为new message tree root生成signed attestation,以确保其为a previous attestation的extend,同时包含了a valid new root of the message set。这些signed attestation会被发送到每个replica。
Replica:会accept an update attestation signed by the updater,并将其放入pending state。当挑战期过后,Replica会接收该attestation中的update,并存储a new local root。由于该root中包含了a commitment of all messages sent by the home chain,因此,这些messages可be proven (using the replica’s root),然后派发到replica chain上的合约。
为new update设置挑战期主要有2个目的:
- 1)可保证updater的任何misbehavior都可在message被处理之前公开。This guarantees that data necessary for home chain slashing is available for all faults.
- 2)使得message recipients可选择不对update中的message进行处理。 If an incorrect update is published, recipients always have the information necessary to take defensive measures before any messages can be processed.
Optics 以raw bytes的形式,将messages由one chain发送到another chain。因此,对于希望使用Optics的跨链应用xApp,需要根据其应用场景定义发送和接收messages的规则。 目前,在Home合约中限制的message最长为2KB:
// Maximum bytes per message = 2 KiB
// (somewhat arbitrarily set to begin)
uint256 public constant MAX_MESSAGE_BODY_BYTES = 2 * 2**10;
每个跨链应用必须实现其自己的messaging protocol。通常,将实现了messaging protocol的合约称为该xApp的Router contract。 在Router contract中,必须:
- 1)维护a permissioned set of the contract(s) on remote chains,表示其可通过Optics接收这些远程链上合约的messages。它可为a single owner of the application on one chain,也可为a registry of other applications implementing the same rules on various chains。
- 2)以标准格式对messages编码,使得目标链上的Router contract可对其进行解码。
- 3)处理来自于remote Router contracts的messages。
- 4)将messages派发到remote Router contracts。
通过在Router contract上实现以上功能,并部署在多条链上,从而可以通用语言和规则创建一个可用的跨链应用。这种跨链应用可将Optics作为跨链信使来相互发送和接收messages。
Optics团队 xApp Template 提供了xApp开发的模板,开发者可基于此实现自己的应用逻辑,并利用an Optics channel for cross-chain communication。
不同链上xApps之间Message Flow流程为:
- 1)xApp Router A receives a command on chain A
- 2)xApp Router A encodes (formats) the information into a message
- 3)xApp Router A sends the message to xApp Router B on chain B via Optics
- 4)xApp Router B receives the message via Optics
- 5)xApp Router B decodes (gets) the information from the message and acts on it
为了实现a xApp,需定义跨链所需执行的actions,对于每个action类型:
- 1)在 xApp Router合约 中:
- 1.1)实现一个类似doTypeA的函数,来initiate the action from one domain to another (可附加自己的参数和逻辑)。
- 1.2)在remote domain上,实现相应的_handle函数来接收、解析、执行该类型的message。
- 1.3)在handle函数中,添加逻辑来route相应类型的incoming message到合适的_handle函数。
- 2)在Message library合约 中:
- 2.1)Formatter函数:将information作为Solidity参数,将其编码为a byte vector in a defined format,产生message。
- 2.2)Identifier函数:输入为a byte vector,若该vector与其他的message类型格式一致,则返回TRUE。
- 2.3)Getter(s)函数:对message中的information进行解析,以Solidity 参数的形式返回。
PingPong xApp 仅供参考,实际请勿部署。
PingPong xApp可initiating PingPong “matches” between two chains. A match consists of “volleys” sent back-and-forth between the two chains via Optics.
The first volley in a match is always a Ping volley.
- When a Router receives a Ping volley, it returns a Pong.
- When a Router receives a Pong volley, it returns a Ping.
The Routers keep track of the number of volleys in a given match, and emit events for each Sent and Received volley so that spectators can watch.
library PingPongMessage {
using TypedMemView for bytes;
using TypedMemView for bytes29;
/// @dev Each message is encoded as a 1-byte type distinguisher, a 4-byte
/// match id, and a 32-byte volley counter. The messages are therefore all
/// 37 bytes
enum Types {
Invalid, // 0
Ping, // 1
Pong // 2
}
// ============ Formatters ============
/**
* @notice Format a Ping volley
* @param _count The number of volleys in this match
* @return The encoded bytes message
*/
function formatPing(uint32 _match, uint256 _count)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(uint8(Types.Ping), _match, _count);
}
/**
* @notice Format a Pong volley
* @param _count The number of volleys in this match
* @return The encoded bytes message
*/
function formatPong(uint32 _match, uint256 _count)
internal
pure
returns (bytes memory)
{
return abi.encodePacked(uint8(Types.Pong), _match, _count);
}
// ============ Identifiers ============
/**
* @notice Get the type that the TypedMemView is cast to
* @param _view The message
* @return _type The type of the message (either Ping or Pong)
*/
function messageType(bytes29 _view) internal pure returns (Types _type) {
_type = Types(uint8(_view.typeOf()));
}
/**
* @notice Determine whether the message contains a Ping volley
* @param _view The message
* @return True if the volley is Ping
*/
function isPing(bytes29 _view) internal pure returns (bool) {
return messageType(_view) == Types.Ping;
}
/**
* @notice Determine whether the message contains a Pong volley
* @param _view The message
* @return True if the volley is Pong
*/
function isPong(bytes29 _view) internal pure returns (bool) {
return messageType(_view) == Types.Pong;
}
// ============ Getters ============
/**
* @notice Parse the match ID sent within a Ping or Pong message
* @dev The number is encoded as a uint32 at index 1
* @param _view The message
* @return The match id encoded in the message
*/
function matchId(bytes29 _view) internal pure returns (uint32) {
// At index 1, read 4 bytes as a uint, and cast to a uint32
return uint32(_view.indexUint(1, 4));
}
/**
* @notice Parse the volley count sent within a Ping or Pong message
* @dev The number is encoded as a uint256 at index 1
* @param _view The message
* @return The count encoded in the message
*/
function count(bytes29 _view) internal pure returns (uint256) {
// At index 1, read 32 bytes as a uint
return _view.indexUint(1, 32);
}
}
contract PingPongRouter is Router {
// ============ Libraries ============
using TypedMemView for bytes;
using TypedMemView for bytes29;
using PingPongMessage for bytes29;
// ============ Mutable State ============
uint32 nextMatch;
// ============ Events ============
event Received(
uint32 indexed domain,
uint32 indexed matchId,
uint256 count,
bool isPing
);
event Sent(
uint32 indexed domain,
uint32 indexed matchId,
uint256 count,
bool isPing
);
// ============ Constructor ============
constructor(address _xAppConnectionManager) {
require(false, "example xApp, do not deploy");
__XAppConnectionClient_initialize(_xAppConnectionManager);
}
// ============ Handle message functions ============
/**
* @notice Handle "volleys" sent via Optics from other remote PingPong Routers
* @param _origin The domain the message is coming from
* @param _sender The address the message is coming from
* @param _message The message in the form of raw bytes
*/
function handle(
uint32 _origin,
bytes32 _sender,
bytes memory _message
) external override onlyReplica onlyRemoteRouter(_origin, _sender) {
bytes29 _msg = _message.ref(0);
if (_msg.isPing()) {
_handlePing(_origin, _msg);
} else if (_msg.isPong()) {
_handlePong(_origin, _msg);
} else {
// if _message doesn't match any valid actions, revert
require(false, "!valid action");
}
}
/**
* @notice Handle a Ping volley
* @param _origin The domain that sent the volley
* @param _message The message in the form of raw bytes
*/
function _handlePing(uint32 _origin, bytes29 _message) internal {
bool _isPing = true;
_handle(_origin, _isPing, _message);
}
/**
* @notice Handle a Pong volley
* @param _origin The domain that sent the volley
* @param _message The message in the form of raw bytes
*/
function _handlePong(uint32 _origin, bytes29 _message) internal {
bool _isPing = false;
_handle(_origin, _isPing, _message);
}
/**
* @notice Upon receiving a volley, emit an event, increment the count and return a the opposite volley
* @param _origin The domain that sent the volley
* @param _isPing True if the volley received is a Ping, false if it is a Pong
* @param _message The message in the form of raw bytes
*/
function _handle(
uint32 _origin,
bool _isPing,
bytes29 _message
) internal {
// get the volley count for this game
uint256 _count = _message.count();
uint32 _match = _message.matchId();
// emit a Received event
emit Received(_origin, _match, _count, _isPing);
// send the opposite volley back
_send(_origin, !_isPing, _match, _count + 1);
}
// ============ Dispatch message functions ============
/**
* @notice Initiate a PingPong match with the destination domain
* by sending the first Ping volley.
* @param _destinationDomain The domain to initiate the match with
*/
function initiatePingPongMatch(uint32 _destinationDomain) external {
// the PingPong match always begins with a Ping volley
bool _isPing = true;
// increment match counter
uint32 _match = nextMatch;
nextMatch = _match + 1;
// send the first volley to the destination domain
_send(_destinationDomain, _isPing, _match, 0);
}
/**
* @notice Send a Ping or Pong volley to the destination domain
* @param _destinationDomain The domain to send the volley to
* @param _isPing True if the volley to send is a Ping, false if it is a Pong
* @param _count The number of volleys in this match
*/
function _send(
uint32 _destinationDomain,
bool _isPing,
uint32 _match,
uint256 _count
) internal {
// get the xApp Router at the destinationDomain
bytes32 _remoteRouterAddress = _mustHaveRemote(_destinationDomain);
// format the ping message
bytes memory _message = _isPing
? PingPongMessage.formatPing(_match, _count)
: PingPongMessage.formatPong(_match, _count);
// send the message to the xApp Router
(_home()).dispatch(_destinationDomain, _remoteRouterAddress, _message);
// emit a Sent event
emit Sent(_destinationDomain, _match, _count, _isPing);
}
}
2.3 Token Bridge xApp示例
Optics Token Bridge为a xApp,为Optics生态应用的一种,用于链之间的token transfer。 Token Bridge的主要特征为:保证token在多条链之间的流通总量保持不变。
部署在Celo、以太坊、Polygon主网上的Token Bridge合约地址见:
- https://github.com/celo-org/optics-monorepo/tree/main/rust/config/mainnet/bridge/1631143085018【V1】
- https://github.com/celo-org/optics-monorepo/commit/b90a38a2c9f81d508f9acfffb05b0c592f433c54【V2】
BridgeRouter、Home等合约地址有2套是因为,2021年11月26日,Celo团队对Optics协议进行了升级。
- Bridge Router V1合约:https://etherscan.io/address/0x67364232a8f8da6f22df3be3408ef9872132f2a6/advanced#internaltx
- Bridge Router V2合约:https://etherscan.io/address/0x688a54c4b1c5b917154ea2f61b8a4a4cbdff4738/advanced#internaltx
- Home V1合约:https://etherscan.io/address/0xfac41463ef1e01546f2130f92184a053a0e3fa14/advanced#internaltx
- Home V2合约:https://etherscan.io/address/0xfc6e146384b5c65f372d5b20537f3e8727ad3723/advanced#internaltx
部署在以太坊上的XAppConnectionManager合约地址为:
- https://etherscan.io/address/0xcec158a719d11005bd9339865965bed938beafa3/advanced#readContract【v1,部署于2021年9月8日。近期仍有交易。】
- https://etherscan.io/address/0x8a926ce79f83a5a4c234bee93feafcc85b1e40cd/advanced#internaltx【v2,部署于2021年11月26日,近期交易活跃。】
部署在以太坊上的Replica合约有多个:
- https://etherscan.io/address/0xfc4060e4fd5979f848b8edc8505d2f89d83b9e04#readContract【v1,2021年9月8日部署。近期仍有内部交易】
- https://etherscan.io/address/0xcbe8b8c4fe6590bb59d1507de7f252af3e621e58#readContract【v2,2021年11月26日部署。最新的交易截止到2022年2月2日。】
- https://etherscan.io/address/0x8f6b6adb49cdca3b9f6947f61a1201242c3d827f/advanced#readContract【v2,2022年2月1日部署。近期交易活跃。】
以太坊上wrapped ether合约为:
- https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2/advanced#internaltx
转账时的tokenID为该token所属源链domain_id+该token address:
uint256 private constant TOKEN_ID_LEN = 36; // 4 bytes domain + 32 bytes id
/**
* @notice Formats the Token ID
* @param _domain The domain
* @param _id The ID
* @return The formatted Token ID
*/
function formatTokenId(uint32 _domain, bytes32 _id)
internal
pure
returns (bytes29)
{
return mustBeTokenId(abi.encodePacked(_domain, _id).ref(0));
}
若某token未在目标链上发布,当前BridgeRouter合约会部署相应的token合约?待细看?【Wormhole V2将部署wrapped token合约的费用拆分出来了,不再由官方合约承担。】
/**
* @notice Deploy and initialize a new token contract
* @dev Each token contract is a proxy which
* points to the token upgrade beacon
* @return _token the address of the token contract
*/
function _deployToken(bytes29 _tokenId) internal returns (address _token) {
// deploy and initialize the token contract
_token = address(new UpgradeBeaconProxy(tokenBeacon, ""));
// initialize the token separately from the
IBridgeToken(_token).initialize();
// set the default token name & symbol
string memory _name;
string memory _symbol;
(_name, _symbol) = _defaultDetails(_tokenId);
IBridgeToken(_token).setDetails(_name, _symbol, 18);
// store token in mappings
representationToCanonical[_token].domain = _tokenId.domain();
representationToCanonical[_token].id = _tokenId.id();
canonicalToRepresentation[_tokenId.keccak()] = _token;
// emit event upon deploying new token
emit TokenDeployed(_tokenId.domain(), _tokenId.id(), _token);
}
Optics部署采用可升级配置,由proxy contracts指向implementation contracts,使得可在无需迁移contract state的情况下,通过governance对contract implementation进行升级。
详细的跨链token transfer流程可参看:
- 2021年9月博客 Optics: How to Ape ERC-20 Tokens With Etherscan
message处理规则为:
- 1)BridgeRouter合约仅接收messages from other remote BridgeRouter合约,每个BridgeRouter合约需注册其支持的remote BridgeRouter合约,使得:
- 所接收到的message符合local BridgeRouter合约的规则。
- 消息中的任何sent token都是有效的,因为发送该消息的remote BridgeRouter在发送消息之前,在其本地就应强化确认用户已托管了相应的token。
/**
* @notice Register the address of a Router contract for the same xApp on a remote chain
* @param _domain The domain of the remote xApp Router
* @param _router The address of the remote xApp Router
*/
function enrollRemoteRouter(uint32 _domain, bytes32 _router)
external
onlyOwner
{
remotes[_domain] = _router;
}
- 2)来自于remote BridgeRouter合约的messages必须通过Optics发送,经由local Replica合约派发。Replica地址与Domain映射关系可通过XAppConnectionManager合约注册。 BridgeRouter依赖XAppConnectionManager合约for a valid registry of local Replicas。
onlyReplica
表示仅可由Replica合约调用。
modifier onlyReplica() {
require(isReplica(msg.sender), "!replica");
_;
}
/**
* @notice Check whether _replica is enrolled
* @param _replica the replica to check for enrollment
* @return TRUE iff _replica is enrolled
*/
function isReplica(address _replica) public view returns (bool) {
return replicaToDomain[_replica] != 0;
}
/**
* @notice Handles an incoming message
* @param _origin The origin domain
* @param _sender The sender address
* @param _message The message
*/
function handle(
uint32 _origin,
bytes32 _sender,
bytes memory _message
) external override onlyReplica onlyRemoteRouter(_origin, _sender) {
// parse tokenId and action from message
bytes29 _msg = _message.ref(0).mustBeMessage();
bytes29 _tokenId = _msg.tokenId();
bytes29 _action = _msg.action();
// handle message based on the intended action
if (_action.isTransfer()) {
_handleTransfer(_tokenId, _action);
} else if (_action.isDetails()) {
_handleDetails(_tokenId, _action);
} else if (_action.isRequestDetails()) {
_handleRequestDetails(_origin, _sender, _tokenId);
} else {
require(false, "!valid action");
}
}
- 3)若从源链发来的token为目标链上的native token,则会从Router合约的托管池中向目标链上的收款方发送相应数额的token。
- 4)若从源链发来的token不是目标链上的native token,则:
- 4.1)可检查目标链上,Router合约是否已部署a representation token contract。若没有,则部署一个representation token contract,然后在token registry中添加该地址。
- 4.2)在目标链上mint 相应数量的representation tokens,并将其发送到目标链的收款方。
Message派发规则为:
- 1)TODO:规则为——用户必须approve token to Router on local chain (if it’s a native token) proving they have ownership over that token and can send to the native chain。【ETH native token目前是借助ETHHelper合约的
sendTo
接口。先存入ETHHelper合约,只需对ETHHelper合约进行一次无限额的approve,而不需要每个用户都approve。不过不限额有风险,未来还是应修改为由用户来approve】
constructor(address _weth, address _bridge) {
weth = IWeth(_weth);
bridge = BridgeRouter(_bridge);
IWeth(_weth).approve(_bridge, uint256(-1));
}
/**
* @notice Sends ETH over the Optics Bridge. Sends to a specified EVM
* address on the other side.
* @dev This function should only be used when sending TO an EVM-like
* domain. As with all bridges, improper use may result in loss of funds
* @param _domain The domain to send funds to.
* @param _to The EVM address of the recipient
*/
function sendToEVMLike(uint32 _domain, address _to) external payable {
sendTo(_domain, TypeCasts.addressToBytes32(_to));
}
/**
* @notice Sends ETH over the Optics Bridge. Sends to a full-width Optics
* identifer on the other side.
* @dev As with all bridges, improper use may result in loss of funds.
* @param _domain The domain to send funds to.
* @param _to The 32-byte identifier of the recipient
*/
function sendTo(uint32 _domain, bytes32 _to) public payable {
weth.deposit{value: msg.value}();
bridge.send(address(weth), msg.value, _domain, _to);
}
- 2)sending tokens:【ERC20 token可直接调用BridgeRouter合约的
send
函数接口。】- 2.1)用户使用ERC-20的
approve
来grant allowance for the tokens being sent to the local BridgeRouter contract。 - 2.2)用户调用local BridgeRouter合约中的
send
函数来transfer the tokens to a remote。
/** * @notice Sends ETH over the Optics Bridge. Sends to a full-width Optics * identifer on the other side. * @dev As with all bridges, improper use may result in loss of funds. * @param _domain The domain to send funds to. * @param _to The 32-byte identifier of the recipient */ function sendTo(uint32 _domain, bytes32 _to) public payable { weth.deposit{value: msg.value}(); bridge.send(address(weth), msg.value, _domain, _to); } /** * @notice Send tokens to a recipient on a remote chain * @param _token The token address * @param _amount The token amount * @param _destination The destination domain * @param _recipient The recipient address */ function send( address _token, uint256 _amount, uint32 _destination, bytes32 _recipient ) external { require(_amount > 0, "!amnt"); require(_recipient != bytes32(0), "!recip"); // get remote BridgeRouter address; revert if not found bytes32 _remote = _mustHaveRemote(_destination); // remove tokens from circulation on this chain IERC20 _bridgeToken = IERC20(_token); if (_isLocalOrigin(_bridgeToken)) { // if the token originates on this chain, hold the tokens in escrow // in the Router _bridgeToken.safeTransferFrom(msg.sender, address(this), _amount); } else { // if the token originates on a remote chain, burn the // representation tokens on this chain _downcast(_bridgeToken).burn(msg.sender, _amount); } // format Transfer Tokens action bytes29 _action = BridgeMessage.formatTransfer(_recipient, _amount); // send message to remote chain via Optics Home(xAppConnectionManager.home()).dispatch( _destination, _remote, BridgeMessage.formatMessage(_formatTokenId(_token), _action) ); // emit Send event to record token sender emit Send( address(_bridgeToken), msg.sender, _destination, _recipient, _amount ); }
- 2.1)用户使用ERC-20的
- 3)若收到的token为源链上的native token,则会将相应数量存入源链BridgeRouter合约资金池中。
- 4)若收到的token不是源链上的native token,则第一次时,源链上的BridgeRouter合约会部署一个相应的representation token contract。BridgeRouter合约在发送token之前,会burn相应数额的representation token。
Message格式:
- 表示的是针对此应用的消息编码格式。
主要包括3大合约内容:
- 1)BridgeRouter合约
- 2)TokenRegistry abstract合约:BridgeRouter合约会继承该合约。
- 3)BridgeMessage library
具体为:
-
1)BridgeRouter合约:
- 1.1)负责从本链的
Replica
合约中接收其他链发来的sending token messages。仅可由注册的Replica合约调用其handle
接口。
/** * @notice Given formatted message, attempts to dispatch * message payload to end recipient. * @dev Recipient must implement a `handle` method (refer to IMessageRecipient.sol) * Reverts if formatted message's destination domain is not the Replica's domain, * if message has not been proven, * or if not enough gas is provided for the dispatch transaction. * @param _message Formatted message * @return _success TRUE iff dispatch transaction succeeded */ function process(bytes memory _message) public returns (bool _success) { bytes29 _m = _message.ref(0); // ensure message was meant for this domain require(_m.destination() == localDomain, "!destination"); // ensure message has been proven bytes32 _messageHash = _m.keccak(); require(messages[_messageHash] == MessageStatus.Proven, "!proven"); // check re-entrancy guard require(entered == 1, "!reentrant"); entered = 0; // update message status as processed messages[_messageHash] = MessageStatus.Processed; // A call running out of gas TYPICALLY errors the whole tx. We want to // a) ensure the call has a sufficient amount of gas to make a // meaningful state change. // b) ensure that if the subcall runs out of gas, that the tx as a whole // does not revert (i.e. we still mark the message processed) // To do this, we require that we have enough gas to process // and still return. We then delegate only the minimum processing gas. require(gasleft() >= PROCESS_GAS + RESERVE_GAS, "!gas"); // get the message recipient address _recipient = _m.recipientAddress(); // set up for assembly call uint256 _toCopy; uint256 _maxCopy = 256; uint256 _gas = PROCESS_GAS; // allocate memory for returndata bytes memory _returnData = new bytes(_maxCopy); bytes memory _calldata = abi.encodeWithSignature( "handle(uint32,bytes32,bytes)", _m.origin(), _m.sender(), _m.body().clone() ); // dispatch message to recipient // by assembly calling "handle" function // we call via assembly to avoid memcopying a very large returndata // returned by a malicious contract assembly { _success := call( _gas, // gas _recipient, // recipient 0, // ether value add(_calldata, 0x20), // inloc mload(_calldata), // inlen 0, // outloc 0 // outlen ) // limit our copy to 256 bytes _toCopy := returndatasize() if gt(_toCopy, _maxCopy) { _toCopy := _maxCopy } // Store the length of the copied bytes mstore(_returnData, _toCopy) // copy the bytes from returndata[0:_toCopy] returndatacopy(add(_returnData, 0x20), 0, _toCopy) } // emit process results emit Process(_messageHash, _success, _returnData); // reset re-entrancy guard entered = 1; }
- 1.2)将由本链发起的向其它链的sending token message 派发到本链的
Home
合约中。BridgeRouter合约会调用Home
合约的dispatch
接口。
/** * @notice Dispatch the message it to the destination domain & recipient * @dev Format the message, insert its hash into Merkle tree, * enqueue the new Merkle root, and emit `Dispatch` event with message information. * @param _destinationDomain Domain of destination chain * @param _recipientAddress Address of recipient on destination chain as bytes32 * @param _messageBody Raw bytes content of message */ function dispatch( uint32 _destinationDomain, bytes32 _recipientAddress, bytes memory _messageBody ) external notFailed { require(_messageBody.length token ID mapping(address => TokenId) public representationToCanonical; // hash of the tightly-packed TokenId => local representation token address // If the token is of local origin, this MUST map to address(0). mapping(bytes32 => address) public canonicalToRepresentation; // Tokens are identified by a TokenId: // domain - 4 byte chain ID of the chain from which the token originates // id - 32 byte identifier of the token address on the origin chain, in that chain's address format struct TokenId { uint32 domain; bytes32 id; }
- 1.4)维护remote
BridgeRouter
合约registry,使得:- 1.4.1)本链仅接收来自已注册认证的remote
BridgeRouter
合约消息。 - 1.4.2)可正确处理发送到remote
BridgeRouter
合约的消息
- 1.4.1)本链仅接收来自已注册认证的remote
- 1.1)负责从本链的
-
2)TokenRegistry合约:
- 2.1)负责在本链部署和跟踪representation ERC-20 token合约。
/** * @notice Deploy and initialize a new token contract * @dev Each token contract is a proxy which * points to the token upgrade beacon * @return _token the address of the token contract */ function _deployToken(bytes29 _tokenId) internal returns (address _token) { // deploy and initialize the token contract _token = address(new UpgradeBeaconProxy(tokenBeacon, "")); // initialize the token separately from the IBridgeToken(_token).initialize(); // set the default token name & symbol string memory _name; string memory _symbol; (_name, _symbol) = _defaultDetails(_tokenId); IBridgeToken(_token).setDetails(_name, _symbol, 18); // store token in mappings representationToCanonical[_token].domain = _tokenId.domain(); representationToCanonical[_token].id = _tokenId.id(); canonicalToRepresentation[_tokenId.keccak()] = _token; // emit event upon deploying new token emit TokenDeployed(_tokenId.domain(), _tokenId.id(), _token); }
- 2.2)当transfer新的token时,会在本链相应部署一个新的representation token合约,并存储源链上的token address以及本链上的representation合约地址 map信息。具体为:
// store token in mappings representationToCanonical[_token].domain = _tokenId.domain(); representationToCanonical[_token].id = _tokenId.id(); canonicalToRepresentation[_tokenId.keccak()] = _token;
- 2.3)BridgeRouter合约集成了TokenRegistry合约。BridgeRouter可确保在mint/burn之前,相应的token representation在本链确实存在。
contract BridgeRouter is Version0, Router, TokenRegistry {.....} /** * @notice Get the local token address * for the canonical token represented by tokenID * Returns address(0) if canonical token is of remote origin * and no representation token has been deployed locally * @param _tokenId the token id of the canonical token * @return _local the local token address */ function _getTokenAddress(bytes29 _tokenId) internal view returns (address _local) { if (_tokenId.domain() == _localDomain()) { // Token is of local origin _local = _tokenId.evmId(); } else { // Token is a representation of a token of remote origin _local = canonicalToRepresentation[_tokenId.keccak()]; } } function _ensureToken(bytes29 _tokenId) internal returns (IERC20) { address _local = _getTokenAddress(_tokenId); if (_local == address(0)) { // Representation does not exist yet; // deploy representation contract _local = _deployToken(_tokenId); // message the origin domain // to request the token details _requestDetails(_tokenId); } return IERC20(_local); } /** * @notice Handles an incoming Transfer message. * * If the token is of local origin, the amount is sent from escrow. * Otherwise, a representation token is minted. * * @param _tokenId The token ID * @param _action The action */ function _handleTransfer(bytes29 _tokenId, bytes29 _action) internal { // get the token contract for the given tokenId on this chain; // (if the token is of remote origin and there is // no existing representation token contract, the TokenRegistry will // deploy a new one) IERC20 _token = _ensureToken(_tokenId); ........ } /** * @notice Enroll a custom token. This allows projects to work with * governance to specify a custom representation. * @dev This is done by inserting the custom representation into the token * lookup tables. It is permissioned to the owner (governance) and can * potentially break token representations. It must be used with extreme * caution. * After the token is inserted, new mint instructions will be sent to the * custom token. The default representation (and old custom representations) * may still be burnt. Until all users have explicitly called migrate, both * representations will continue to exist. * The custom representation MUST be trusted, and MUST allow the router to * both mint AND burn tokens at will. * @param _id the canonical ID of the Token to enroll, as a byte vector * @param _custom the address of the custom implementation to use. */ function enrollCustom( uint32 _domain, bytes32 _id, address _custom ) external onlyOwner { // Sanity check. Ensures that human error doesn't cause an // unpermissioned contract to be enrolled. IBridgeToken(_custom).mint(address(this), 1); IBridgeToken(_custom).burn(address(this), 1); // update mappings with custom token bytes29 _tokenId = BridgeMessage.formatTokenId(_domain, _id); representationToCanonical[_custom].domain = _tokenId.domain(); representationToCanonical[_custom].id = _tokenId.id(); bytes32 _idHash = _tokenId.keccak(); canonicalToRepresentation[_idHash] = _custom; }
-
3)BridgeMessage library:用于以标准化方式处理所有编码/解码信息的库,以便通过Optics发送。
由链A将token发送到链B的message flow流程,主要参与方有:
- 1)链A端
- 2)链下服务:Updater、Relayer、Processor、Watcher
- 3)链B端
详细流程为:
-
1)链A端
- 1.1)用户想要将其在链A的token发送到链B。
- 若为native token,则用户必须首先
approve
相应数量的token到本链的BridgeRouter-A
合约。
- 若为native token,则用户必须首先
- 1.2)用户调用本链的
BridgeRouter-A
合约。- 1.2.1)若为native token,则会将相应数量的token由用户钱包转入到
BridgeRouter-A
合约的资金池。 - 1.2.2)若为non-native token,则
BridgeRouter-A
合约会从用户钱包中burn相应数量的token。- 注意:
BridgeRouter-A
token可burn non-native tokens,是因为相应的representation合约是由BridgeRouter-A
部署的,BridgeRouter-A
合约具有相应representation合约的administrator权限。
- 注意:
- 1.2.1)若为native token,则会将相应数量的token由用户钱包转入到
- 1.3)
BridgeRouter-A
为BridgeRouter-B
构建相应的messages。BridgeRouter-A
合约中会维护其它链上的BridgeRouter
合约map,使得其知道应往哪发送链B的message。
// get remote BridgeRouter address; revert if not found bytes32 _remote = _mustHaveRemote(_destination); // format Transfer Tokens action bytes29 _action = BridgeMessage.formatTransfer(_recipient, _amount); // send message to remote chain via Optics Home(xAppConnectionManager.home()).dispatch( _destination, _remote, BridgeMessage.formatMessage(_formatTokenId(_token), _action) ); // get the next nonce for the destination domain, then increment it uint32 _nonce = nonces[_destinationDomain]; nonces[_destinationDomain] = _nonce + 1; // format the message into packed bytes bytes memory _message = Message.formatMessage( localDomain, bytes32(uint256(uint160(msg.sender))), _nonce, _destinationDomain, _recipientAddress, _messageBody ); // insert the hashed message into the Merkle tree bytes32 _messageHash = keccak256(_message); tree.insert(_messageHash); // enqueue the new Merkle root after inserting the message queue.enqueue(root());
- 1.4)
BridgeRouter-A
合约会调用Home-A
合约的enqueue
接口来将消息发送到链B。
// insert the hashed message into the Merkle tree bytes32 _messageHash = keccak256(_message); tree.insert(_messageHash); // enqueue the new Merkle root after inserting the message queue.enqueue(root()); // Emit Dispatch event with message information // note: leafIndex is count() - 1 since new leaf has already been inserted emit Dispatch( _messageHash, count() - 1, _destinationAndNonce(_destinationDomain, _nonce), committedRoot, _message );
- 1.1)用户想要将其在链A的token发送到链B。
-
2)链下服务:标准的流程为Updater->Relayer->Processor。
- 2.1)Updater链下服务:会确定当前链A的Home合约中配置的Updater为其自身,然后会定期查询Home合约的
suggestUpdate
来获取最新待处理的消息。在UpdateProducer
线程中,会进行判断,符合条件会对[committedRoot, new]进行签名,并存储在Updater本地数据库中。UpdateSubmitter
线程中,会定期从本地数据库中获取已签名的update,调用Home合约的update
接口提交。
/** * @notice Suggest an update for the Updater to sign and submit. * @dev If queue is empty, null bytes returned for both * (No update is necessary because no messages have been dispatched since the last update) * @return _committedRoot Latest root signed by the Updater * @return _new Latest enqueued Merkle root */ function suggestUpdate() external view returns (bytes32 _committedRoot, bytes32 _new) { if (queue.length() != 0) { _committedRoot = committedRoot; _new = queue.lastItem(); } }
// If the suggested matches our local view, sign an update // and store it as locally produced let signed = suggested.sign_with(self.signer.as_ref()).await?; self.db.store_produced_update(&signed)?;
pub struct Update { /// The home chain pub home_domain: u32, /// The previous root pub previous_root: H256, /// The new root pub new_root: H256, } fn signing_hash(&self) -> H256 { // sign: // domain(home_domain) || previous_root || new_root H256::from_slice( Keccak256::new() .chain(home_domain_hash(self.home_domain)) .chain(self.previous_root) .chain(self.new_root) .finalize() .as_slice(), ) } /// Sign an update using the specified signer pub async fn sign_with(self, signer: &S) -> Result { let signature = signer .sign_message_without_eip_155(self.signing_hash()) .await?; Ok(SignedUpdate { update: self, signature, }) }
- 2.2)Relayer链下服务:读取链B的replica合约的
committedRoot
,基于该committedRoot,从本地数据库中读取【???跟Updater共用一个数据库???而不是监听Home合约的Update(localDomain, _committedRoot, _newRoot, _signature)
事件??】已签名的update,调用Replica合约的update
接口。 - 2.3)Processor链下服务:按本domain的nonce顺序,从数据库中获取相应待处理的已签名update消息(
self.db.message_by_nonce(destination, nonce)
),并从数据库中获取相应的merkle tree proof(self.db.proof_by_leaf_index(message.leaf_index)
),以及leaf index信息。【代码中有设置deny list 黑名单。】调用链B的Replica合约的acceptableRoot
接口,若已过挑战期,则返回true。若已过挑战期,Processor会将根据消息类型,调用链B的Replica合约的prove_and_process
或process
接口:【Processor会在本地维护merkle tree with all leaves。】
// The basic structure of this loop is as follows: // 1. Get the last processed index // 2. Check if the Home knows of a message above that index // - If not, wait and poll again // 3. Check if we have a proof for that message // - If not, wait and poll again // 4. Check if the proof is valid under the replica // 5. Submit the proof to the replica /// Dispatch a message for processing. If the message is already proven, process only. async fn process(&self, message: CommittedMessage, proof: Proof) -> Result { use optics_core::Replica; let status = self.replica.message_status(message.to_leaf()).await?; match status { MessageStatus::None => { self.replica .prove_and_process(message.as_ref(), &proof) .await?; } MessageStatus::Proven => { self.replica.process(message.as_ref()).await?; } MessageStatus::Processed => { info!( domain = message.message.destination, nonce = message.message.nonce, leaf_index = message.leaf_index, leaf = ?message.message.to_leaf(), "Message {}:{} already processed", message.message.destination, message.message.nonce ); return Ok(()); } } info!( domain = message.message.destination, nonce = message.message.nonce, leaf_index = message.leaf_index, leaf = ?message.message.to_leaf(), "Processed message. Destination: {}. Nonce: {}. Leaf index: {}.", message.message.destination, message.message.nonce, message.leaf_index, ); Ok(()) }
- 2.1)Updater链下服务:会确定当前链A的Home合约中配置的Updater为其自身,然后会定期查询Home合约的
-
3)链B端:
- 3.1)挑战期过后,链B的replica合约会处理消息,并dispatch到链B的BridgeRouter合约。
// dispatch message to recipient // by assembly calling "handle" function // we call via assembly to avoid memcopying a very large returndata // returned by a malicious contract assembly { _success := call( _gas, // gas _recipient, // recipient 0, // ether value add(_calldata, 0x20), // inloc mload(_calldata), // inlen 0, // outloc 0 // outlen ) // limit our copy to 256 bytes _toCopy := returndatasize() if gt(_toCopy, _maxCopy) { _toCopy := _maxCopy } // Store the length of the copied bytes mstore(_returnData, _toCopy) // copy the bytes from returndata[0:_toCopy] returndatacopy(add(_returnData, 0x20), 0, _toCopy) }
- 3.2)链B的BridgeRouter合约中维护了其在本链信任的Replica合约mapping,以此来接收证实来自于链A的message。
onlyReplica
。 - 3.3)链B的BridgeRouter合约中维护了其它链的BridgeRouter合约mapping,以此来接收证实来自于链A BridgeRouter的message。
onlyRemoteRouter(_origin, _sender)
。 - 3.4)链B的BridgeRouter合约会查找其registry中相应的ERC-20 token合约,若不存在,会部署一个新的representative one。
function _ensureToken(bytes29 _tokenId) internal returns (IERC20) { address _local = _getTokenAddress(_tokenId); if (_local == address(0)) { // Representation does not exist yet; // deploy representation contract _local = _deployToken(_tokenId); // message the origin domain // to request the token details _requestDetails(_tokenId); } return IERC20(_local); }
- 3.5)链B的BridgeRouter将token发送到相应的接收方。若为native token,则从BridgeRouter资金池中将金额转给收款方;若为non-native token,则BridgeRouter合约给收款方mint相应数量的non-native tokens。BridgeRouter可mint是因为相应的non-native token合约是其部署的,其具有相应合约的管理员权限。
/** * @notice Handles an incoming message * @param _origin The origin domain * @param _sender The sender address * @param _message The message */ function handle( uint32 _origin, bytes32 _sender, bytes memory _message ) external override onlyReplica onlyRemoteRouter(_origin, _sender) { // parse tokenId and action from message bytes29 _msg = _message.ref(0).mustBeMessage(); bytes29 _tokenId = _msg.tokenId(); bytes29 _action = _msg.action(); // handle message based on the intended action if (_action.isTransfer()) { _handleTransfer(_tokenId, _action); } else if (_action.isDetails()) { _handleDetails(_tokenId, _action); } else if (_action.isRequestDetails()) { _handleRequestDetails(_origin, _sender, _tokenId); } else { require(false, "!valid action"); } } /** * @notice Handles an incoming Transfer message. * * If the token is of local origin, the amount is sent from escrow. * Otherwise, a representation token is minted. * * @param _tokenId The token ID * @param _action The action */ function _handleTransfer(bytes29 _tokenId, bytes29 _action) internal { // get the token contract for the given tokenId on this chain; // (if the token is of remote origin and there is // no existing representation token contract, the TokenRegistry will // deploy a new one) IERC20 _token = _ensureToken(_tokenId); address _recipient = _action.evmRecipient(); // If an LP has prefilled this token transfer, // send the tokens to the LP instead of the recipient bytes32 _id = _preFillId(_tokenId, _action); address _lp = liquidityProvider[_id]; if (_lp != address(0)) { _recipient = _lp; delete liquidityProvider[_id]; } // send the tokens into circulation on this chain if (_isLocalOrigin(_token)) { // if the token is of local origin, the tokens have been held in // escrow in this contract // while they have been circulating on remote chains; // transfer the tokens to the recipient _token.safeTransfer(_recipient, _action.amnt()); } else { // if the token is of remote origin, mint the tokens to the // recipient on this chain _downcast(_token).mint(_recipient, _action.amnt()); } }
Optics当前仍在迭代开发中。 Optics会将messages进行batch,仅发送tree roots,因此,一旦message被传入到Home合约,就无法在链上跟踪各个单独的message。可开发一个agent-querying工具,向链下agents query单独的每笔交易,当前并没有相应的工具存在。
因此,这意味着在send和receipt之间,存在a state of unknown,可将其看成是snail mail,尽管无法跟踪,但是由delivery confirmation。在链上可确认的事情仅有:
- 1)A transaction was sent on chain A to the BridgeRouter合约。
- 2)链B的收款方收到了相应的token。
详细的message伪追踪流程为:
- 1)查看生产环境的各合约地址。
- 2)验证在源链上的某笔交易被发送到BridgeRouter合约。等待时间:取决于源链的区块确认时间。
- 3)验证有交易发送到Home合约。等待时间:取决于源链的区块确认,当交易发送到BridgeRouter合约之后,应很快就能看到。此时没有办法来查询某笔特殊的交易,可交互检查BridgeRouter交易的时间戳。
- 4)挑战期过后,验证有交易发送到目标链的Replica合约。可交叉检查时间戳。等待时间为挑战期,当前约为30分钟。
- 5)验证有交易发送到目标链的BridgeRouter合约。等待时间为挑战期+区块确认时间。
- 6)验证目标链的收款方收到了a token mint。等待时间为:源链的区块确认时间 + 挑战期 + 目标链的区块确认时间。
以以太坊为源链,发起token transfer:
- 1)Updater向以太坊上的Home合约提交签名committedRoot的交易费约为53K,对应约为$7。【提交间隔可控制,无需每笔交易都提交一次。支持batch。】
- 2)Relayer会向 除以太坊之外的其他各链(如Celo/Polygon等)的"以太坊Replica"合约转发相应已签名committedRoot。由于其他链交易费较低,可忽略。
- 3)processor会向目标链上对应源链的Replica合约提交proveAndProcess请求,从而目标链上的接收方可收到相应的token。【目前,仅对于目标链为以太坊的情况,提交proveAndProcess请求的手续费由用户发起并承担。】
以 非以太坊为源链,发起token transfer:
- 1)Updater 向非以太坊源链上的Home合约 提交签名CommittedRoot 的交易费可忽略。【提交间隔可控制,无需每笔交易都提交一次。支持batch。】
- 2)Relayer 会向其他各链的“源链Replica”合约转发相应已签名committedRoot。此时,重点考虑向 以太坊上“源链Replica”合约提交 的gas为74K,对应约$9。【其他任何链发起转账请求,只要Updater提交了签名,都需要在以太坊上更新,会产生相应的成本。】
- 3)以太坊上由用户自身发起proveAndProcess请求 的gas为178K,对应约$18。目标链为 非以太坊链 的话,由processor承担发起proveAndProcess请求相应的费用。
[1] 2021年9月博客 Optics: How to Ape ERC-20 Tokens With Etherscan [2] Optics Architecture [3] Optics Token Bridge xApp [4] Optics Developing Cross-Chain Applications [5] Optics v2 deployment complete! Please help verify the deployment [6] Optics v2 is live