optimism-rollup 做為目前最流行的以太坊L2解決方案梧喷,最近研究了下,寫個筆記。
另外,layer2并不是側(cè)鏈柳畔,而是以太坊的擴展,layer1到layer2的交易郭赐,并不是跨鏈交易薪韩,而是跨域交易.
optimism 的項目源碼在 https://github.com/ethereum-optimism/optimism。
雖然是一個項目捌锭,但是分了很多層俘陷。為了方便理解,把每層的作用先記錄下.
一.代碼結(jié)構(gòu)
項目分了5層:
- l2geth
- contracts
- data-transport-layer
- batch-submitter
- message-relayer
上面的5層共同構(gòu)成了optimism-rollup 這個系統(tǒng)观谦。下來分別了解下:
l2geth:
這是fork的以太坊的1.9.10版本拉盾,里面增加了個rollup包,實現(xiàn)了layer2上的兩種角色:Sequncer,Verifier.
Sequencer 用于偵聽layer1的跨域消息豁状,并且將交易改為OVMMessage到 虛擬機(OVM)中運行捉偏,
Verifier 用于驗證layer2上的Sequencer提交的交易的正確性。
以下是兩種角色的啟動代碼:
func (s *SyncService) Start() error {
if !s.enable {
return nil
}
log.Info("Initializing Sync Service", "eth1-chainid", s.eth1ChainId)
// When a sequencer, be sure to sync to the tip of the ctc before allowing
// user transactions.
if !s.verifier {
err := s.syncTransactionsToTip()
if err != nil {
return fmt.Errorf("Cannot sync transactions to the tip: %w", err)
}
// TODO: This should also sync the enqueue'd transactions that have not
// been synced yet
s.setSyncStatus(false)
}
if s.verifier {
go s.VerifierLoop()
} else {
go s.SequencerLoop()
}
return nil
}
packages包下有幾個文件夾泻红,不過主要的模塊是: batch-submitter,contracts,data-transport-layer,message-relayer. 我們分別說明下先了解下這些結(jié)構(gòu)所伴演的角色:
batch-submitter
向layer1的CTC chain和 SCC chain分別提交layer2的交易和交易的狀態(tài)根夭禽。里面分別實現(xiàn)了兩個typescript文件,state-batch-submitter.ts 和 tx-batch-submitter.ts 這兩個文件就是通過
分別向 scc chain 和 ctc chain提交狀態(tài)和交易的兩個文件谊路。另外讹躯,在CTC chain中,區(qū)塊叫batch,也是交易的集合缠劝。 batch-sumitter.ts就是每隔一段時間潮梯,從layer2中,從當前ctc的index開始惨恭,獲取一批交易.組成一個batch, 提交到ctc chain的 appendSequencerBatch 中去秉馏。代碼如下:
public async _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt> {
// Do not submit batch if gas price above threshold
const gasPriceInGwei = parseInt(
ethers.utils.formatUnits(await this.signer.getGasPrice(), 'gwei'),
10
)
if (gasPriceInGwei > this.gasThresholdInGwei) {
this.log.warn(
'Gas price is higher than gas price threshold; aborting batch submission',
{
gasPriceInGwei,
gasThresholdInGwei: this.gasThresholdInGwei,
}
)
return
}
const [
batchParams,
wasBatchTruncated,
] = await this._generateSequencerBatchParams(startBlock, endBlock)
const batchSizeInBytes = encodeAppendSequencerBatch(batchParams).length / 2
this.log.debug('Sequencer batch generated', {
batchSizeInBytes,
})
// Only submit batch if one of the following is true:
// 1. it was truncated
// 2. it is large enough
// 3. enough time has passed since last submission
if (!wasBatchTruncated && !this._shouldSubmitBatch(batchSizeInBytes)) {
return
}
this.log.debug('Submitting batch.', {
calldata: batchParams,
})
const nonce = await this.signer.getTransactionCount()
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
const tx = await this.chainContract.appendSequencerBatch(batchParams, {
nonce,
gasPrice,
})
this.log.info('Submitted appendSequencerBatch transaction', {
nonce,
txHash: tx.hash,
contractAddr: this.chainContract.address,
from: tx.from,
data: tx.data,
})
return this.signer.provider.waitForTransaction(
tx.hash,
this.numConfirmations
)
}
return this._submitAndLogTx(contractFunction, 'Submitted batch!')
}
state-batch-submitter.ts 過程和tx-batch-submitter一樣,不過state-batch-submitter提交的是區(qū)塊的狀態(tài)根(state root),方法是_generateStateCommitmentBatch(startBlock:number,endBlock: number);
調(diào)用的是scc chain的 appendStateBatch方法.代碼如果下:
public async _submitBatch(
startBlock: number,
endBlock: number
): Promise<TransactionReceipt> {
const batch = await this._generateStateCommitmentBatch(startBlock, endBlock)
const tx = this.chainContract.interface.encodeFunctionData(
'appendStateBatch',
[batch, startBlock]
)
const batchSizeInBytes = remove0x(tx).length / 2
this.log.debug('State batch generated', {
batchSizeInBytes,
tx,
})
if (!this._shouldSubmitBatch(batchSizeInBytes)) {
return
}
const offsetStartsAtIndex = startBlock - BLOCK_OFFSET // TODO: Remove BLOCK_OFFSET by adding a tx to Geth's genesis
this.log.debug('Submitting batch.', { tx })
const nonce = await this.signer.getTransactionCount()
const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
const contractTx = await this.chainContract.appendStateBatch(
batch,
offsetStartsAtIndex,
{ nonce, gasPrice }
)
this.log.info('Submitted appendStateBatch transaction', {
nonce,
txHash: contractTx.hash,
contractAddr: this.chainContract.address,
from: contractTx.from,
data: contractTx.data,
})
return this.signer.provider.waitForTransaction(
contractTx.hash,
this.numConfirmations
)
}
return this._submitAndLogTx(contractFunction, 'Submitted state root batch!')
}
contracts
Layer2系統(tǒng)中使用的各種智能合約脱羡,不過有些需要在layer1上布署萝究,有些要在layer2上布署.需要注意的是: 這些合約要用 optimistic-solc 編譯器進行編譯母廷,目的是為了保證無論何時,在執(zhí)行同一個交易的時候糊肤,輸出結(jié)果都是一樣的。 因為 OVM_ExecutionManager.sol 中對于一些動態(tài)的opcode進行了重寫氓鄙, 比如: timestamp 在evm中獲取的是當前區(qū)塊的時間戳馆揉,而ovm中是按交易來的,執(zhí)行哪個交易抖拦,是哪個交易的時間戳升酣。
除了實現(xiàn)了ovm外,還包括一些賬戶态罪,跨域橋噩茄,layer1上的驗證者,預編譯合約和ctc,scc chain 复颈。這些都是optimism系統(tǒng)的核心绩聘。 所有的跨域消息都是通過調(diào)用這些合約和偵聽合約的事件進行工作的。
data-transport-layer
數(shù)據(jù)傳輸層耗啦,其實這層就是個事件索引器凿菩,通過rpc訪問layer1的rpc接口,索引layer1的合約事件帜讲,比如:
CTC chain的 TransactionEnqueued事件和SequencerBatchAppended事件衅谷,另外還有SCC chain的StateBatchAppended事件,索引到這些事件后似将,就會存在本地數(shù)據(jù)庫下获黔。然后再提供個rpc接口,供layer2也就是l2geth 來獲取這些事件在验。當然這層也會提供相當?shù)膔pc接口玷氏,也就是實現(xiàn)了個client專門供layer2來獲取數(shù)據(jù)。
**TransactionEnqueued 事件就是CTC chain的enqueue方法執(zhí)行完畢腋舌,將一個交易提交到了CTC chain 的queue隊列.SequencerBatchAppended 就是squencer提交了個batch到CTC chain中预茄。是 appendSequencerBatch 這個接口的事件。StateBatchAppended 當然就是 交易的狀態(tài)根提交到了SCC chain中 是方法 _appendBatch 的執(zhí)行事件侦厚。
相應的代碼如下:
protected async _start(): Promise<void> {
// This is our main function. It's basically just an infinite loop that attempts to stay in
// sync with events coming from Ethereum. Loops as quickly as it can until it approaches the
// tip of the chain, after which it starts waiting for a few seconds between each loop to avoid
// unnecessary spam.
while (this.running) {
try {
const highestSyncedL1Block =
(await this.state.db.getHighestSyncedL1Block()) ||
this.state.startingL1BlockNumber
const currentL1Block = await this.state.l1RpcProvider.getBlockNumber()
const targetL1Block = Math.min(
highestSyncedL1Block + this.options.logsPerPollingInterval,
currentL1Block - this.options.confirmations
)
// We're already at the head, so no point in attempting to sync.
if (highestSyncedL1Block === targetL1Block) {
await sleep(this.options.pollingInterval)
continue
}
this.logger.info('Synchronizing events from Layer 1 (Ethereum)', {
highestSyncedL1Block,
targetL1Block,
})
// I prefer to do this in serial to avoid non-determinism. We could have a discussion about
// using Promise.all if necessary, but I don't see a good reason to do so unless parsing is
// really, really slow for all event types.
await this._syncEvents(
'OVM_CanonicalTransactionChain',
'TransactionEnqueued',
highestSyncedL1Block,
targetL1Block,
handleEventsTransactionEnqueued
)
await this._syncEvents(
'OVM_CanonicalTransactionChain',
'SequencerBatchAppended',
highestSyncedL1Block,
targetL1Block,
handleEventsSequencerBatchAppended
)
await this._syncEvents(
'OVM_StateCommitmentChain',
'StateBatchAppended',
highestSyncedL1Block,
targetL1Block,
handleEventsStateBatchAppended
)
await this.state.db.setHighestSyncedL1Block(targetL1Block)
if (
currentL1Block - highestSyncedL1Block <
this.options.logsPerPollingInterval
) {
await sleep(this.options.pollingInterval)
}
} catch (err) {
if (!this.running || this.options.dangerouslyCatchAllErrors) {
this.logger.error('Caught an unhandled error', { err })
await sleep(this.options.pollingInterval)
} else {
// TODO: Is this the best thing to do here?
throw err
}
}
}
}
_syncEvents就是通過某個合約的某個事件耻陕,然后通過相應的handle的存儲在本地的數(shù)據(jù)庫中。
到這里刨沦,有了batch-submitter和data-transport-layer就可以把layer1和layer2上的交易形成循環(huán)诗宣。
如果有人在layer2上執(zhí)行了交易,交易在打包后想诅,會通過batch-submitter 提交到layer1的CTC chain,然后data-transport-layer偵聽到事件后召庞,會存在本地數(shù)據(jù)庫岛心,這時l2geth可以通過rpc獲取 data-transport-layer存的數(shù)據(jù)。然后再到 layer2上嘗試執(zhí)行篮灼,拿結(jié)果和layer2已經(jīng)確定的交易進行比較忘古,如果一樣,說明layer1上的交易是正確的诅诱,如果不一樣髓堪,則需要layer1上的驗證者去做驗證。這是verifier的功能娘荡。
l2geth是另一個角 色是Sequencer,他是把data-transport-layer中偵聽到的quence的交易干旁,提交到layer2中打包。
然后batch-submitter獲取區(qū)塊的stateroot再提交到layer1的SCC chain中炮沐。**
這塊邏輯有點繞争群。需要慢慢理解。大年。
message-relayer
這是一個 中繼服務换薄,是將layer2中的提現(xiàn)交易中繼到layer1上。其實現(xiàn)過程翔试,就是利用rpc接口偵聽layer2的SentMessages事件专控,這個事件就是跨域轉(zhuǎn)賬或其他跨域消息。然后,relayer偵聽到這個事件后遏餐,會根據(jù)事件的參數(shù)伦腐。在layer1上調(diào)用OVM_L1CrossDomainMessenger的relayMessage方法,進行relay.然后就會到相應的合約上執(zhí)行相應的方法失都。以達到跨域轉(zhuǎn)賬的目的.
我們先介紹這幾個主要的模塊代碼柏蘑。希望以大家理解有幫助。