CanonicalTransactionChain (CTC)和 StateCommitmentChain(SCC)
上次说到OVM的作用就是为了在Layer1上重放Layer2上的交易时,保持一致性。也就是为了验证某个交易是否有人作恶。
在这之前我们必须先理解两个合约,分别是CanonicalTransactionChain.sol和StateCommitmentChain.sol。
这两份合约分别实现了两条链,这也是为什么 Optimism 被称为Layer2的原因,因为他是工作在以太坊之上的事务状态。
- CanonicalTransactionChain 维护的是Layer2上的交易信息,是由交易组成的一个列表。通过enqueue 方法入队。然后emit 一个 TransactionEnqueued 事件,由data-transport-layer 侦听并存储。以供l2geth获取并打包进一个区块,把区块的stateroot再通过 batch-submitter提交到StateCommitmentChain.sol
- StateCommitmentChain StateCommitmentChain.sol就是维护交易的状态根的列表。
为了能验证某个交易是否在layer2上打包了。也是利用了Merkle树证明。首先,layer2的所有的交易信息,每隔一段时间都会通过batch-submitter 组成一个Batch, 每个Batch中的交易hash再组成Merkle树。 所以CanonicalTransactionChain 也会存储一个个Batch交易的Merkle树根。。用来判断某个具体的交易是否真的被打包。
同理,StateCommitmentChain也会通过每个交易的状态根也由Batch-submitter组成一个个Batch提到StateCommitmentChain 中,然后,每个Batch中的stateRoot再次组成Merkle树。用来判断某个状态是否在链中。
这些交易信息在CanonicalTransactionChain 和 StateCommitmentChain 是一一对应的。所以验证过程就是从CanonicalTransactionChain 中 拿到一个交易在OVM中运行,将运行的结果和StateCommitmentChain 中相应的状态根进行比较。如果一样,表示验证通过。如果不一样,则表示这个提交Batch的 submitter有作恶。则将StateCommitmentChain中的这个交易开始的后面的状态列表都删除。并扣除质押金。
了解了这些信息,我们就可以从源码角度看下验证过程了。
verification
上面的代码就是组成欺诈验证的所有合约。 这些代码工作在Layer1上。只有在需要验证的时候才需要通过OVM执行判断某个交易执行是否正确。
- Abs_FraudContributor.sol 记录 要验证的交易hash和preStateRoot,及gas消耗
- OVM_BondManager.sol 押金管理器,管理Sequencer的质押金,和提供证明的gas消耗
- OVM_FraudVerifier.sol 欺诈验证逻辑的主文件,整个验证过程都在这里面
- OVM_StateTransitioner.sol 状态转换管理, 控制着 statemanager.sol 和 ExecutionManager.sol,通过 applyTransaction 调用 ExecutionManager 的run 方法执行交易来记录状态根。OVM_FraudVerifier 通过比较提供的stateroot 和 执行的stateroot来比较检查是否有欺诈
- OVM_StateTransitionerFactory.sol 一个工厂类,OVM_FraudVerifier用来创建OVM_StateTransitioner.
证明过程如下:
1. 初始化
function initializeFraudVerification(
bytes32 _preStateRoot,
Lib_OVMCodec.ChainBatchHeader memory _preStateRootBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _preStateRootProof,
Lib_OVMCodec.Transaction memory _transaction,
Lib_OVMCodec.TransactionChainElement memory _txChainElement,
Lib_OVMCodec.ChainBatchHeader memory _transactionBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _transactionProof
)
override
public
contributesToFraudProof(_preStateRoot, Lib_OVMCodec.hashTransaction(_transaction))
{
bytes32 _txHash = Lib_OVMCodec.hashTransaction(_transaction);
if (_hasStateTransitioner(_preStateRoot, _txHash)) {
return;
}
iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
iOVM_CanonicalTransactionChain ovmCanonicalTransactionChain = iOVM_CanonicalTransactionChain(resolve("OVM_CanonicalTransactionChain"));
require(
ovmStateCommitmentChain.verifyStateCommitment(
_preStateRoot,
_preStateRootBatchHeader,
_preStateRootProof
),
"Invalid pre-state root inclusion proof."
);
require(
ovmCanonicalTransactionChain.verifyTransaction(
_transaction,
_txChainElement,
_transactionBatchHeader,
_transactionProof
),
"Invalid transaction inclusion proof."
);
require (
_preStateRootBatchHeader.prevTotalElements + _preStateRootProof.index + 1 == _transactionBatchHeader.prevTotalElements + _transactionProof.index,
"Pre-state root global index must equal to the transaction root global index."
);
_deployTransitioner(_preStateRoot, _txHash, _preStateRootProof.index);
emit FraudProofInitialized(
_preStateRoot,
_preStateRootProof.index,
_txHash,
msg.sender
);
}
用户 调用OVM_FraudVerifier 的initializeFraudVerification方法,提供前置状态根及其在SCC中的证明和要要验证的交易 及其在CTC 中的证明。
上面的代码逻辑分为
- 验证前置状态的有效性,和要验证交易的有效性:就是通过在SCC Chain和CTC Chain中的merkle 树来证明其有效性的。
- 保证交易顺序是一致的
- 通过前置状态根,txHash, 前置状态号初始化一个状态转换器(OVM_StateTransitioner), 就是通过OVM_StateTransitionerFactory创建的,这时状态转换器处于预执行阶段(PRE_EXECUTION)
2 上传所有交易状态
现在虽然创建的状态转换器,但现在还不能执行交易,因为现在Layer1上的stateManager,还没有任何涉及的Layer2上的状态,这时需要调用状态转换器的proveContractState,将Layer2的OVM合约与Layer1的OVM合约进行链接,下面是代码:
function proveContractState(
address _ovmContractAddress,
address _ethContractAddress,
bytes memory _stateTrieWitness
)
override
public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
contributesToFraudProof(preStateRoot, transactionHash)
{
// Exit quickly to avoid unnecessary work.
require(
(
ovmStateManager.hasAccount(_ovmContractAddress) == false
&& ovmStateManager.hasEmptyAccount(_ovmContractAddress) == false
),
"Account state has already been proven."
);
// Function will fail if the proof is not a valid inclusion or exclusion proof.
(
bool exists,
bytes memory encodedAccount
) = Lib_SecureMerkleTrie.get(
abi.encodePacked(_ovmContractAddress),
_stateTrieWitness,
preStateRoot
);
if (exists == true) {
// Account exists, this was an inclusion proof.
Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.decodeEVMAccount(
encodedAccount
);
address ethContractAddress = _ethContractAddress;
if (account.codeHash == EMPTY_ACCOUNT_CODE_HASH) {
// Use a known empty contract to prevent an attack in which a user provides a
// contract address here and then later deploys code to it.
ethContractAddress = 0x0000000000000000000000000000000000000000;
} else {
// Otherwise, make sure that the code at the provided eth address matches the hash
// of the code stored on L2.
require(
Lib_EthUtils.getCodeHash(ethContractAddress) == account.codeHash,
"OVM_StateTransitioner: Provided L1 contract code hash does not match L2 contract code hash."
);
}
ovmStateManager.putAccount(
_ovmContractAddress,
Lib_OVMCodec.Account({
nonce: account.nonce,
balance: account.balance,
storageRoot: account.storageRoot,
codeHash: account.codeHash,
ethAddress: ethContractAddress,
isFresh: false
})
);
} else {
// Account does not exist, this was an exclusion proof.
ovmStateManager.putEmptyAccount(_ovmContractAddress);
}
}
通过上面的代码可以看到执行这个方法前需要当前状态是 PRE_EXECUTION。相应的参数就是要链接的合约在Layer2和在layer1上相应的地址。及账户状态。最后 通过ovmStateManager.putAccount进行相应的链接。
链接后再 调用调用proveStorageSlot,设置执行交易前的账户状态。以方便在执行交易后的账户状态和layer2上的账户状态相比较。
function proveStorageSlot(
address _ovmContractAddress,
bytes32 _key,
bytes memory _storageTrieWitness
)
override
public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
contributesToFraudProof(preStateRoot, transactionHash)
{
// Exit quickly to avoid unnecessary work.
require(
ovmStateManager.hasContractStorage(_ovmContractAddress, _key) == false,
"Storage slot has already been proven."
);
require(
ovmStateManager.hasAccount(_ovmContractAddress) == true,
"Contract must be verified before proving a storage slot."
);
bytes32 storageRoot = ovmStateManager.getAccountStorageRoot(_ovmContractAddress);
bytes32 value;
if (storageRoot == EMPTY_ACCOUNT_STORAGE_ROOT) {
// Storage trie was empty, so the user is always allowed to insert zero-byte values.
value = bytes32(0);
} else {
// Function will fail if the proof is not a valid inclusion or exclusion proof.
(
bool exists,
bytes memory encodedValue
) = Lib_SecureMerkleTrie.get(
abi.encodePacked(_key),
_storageTrieWitness,
storageRoot
);
if (exists == true) {
// Inclusion proof.
// Stored values are RLP encoded, with leading zeros removed.
value = Lib_BytesUtils.toBytes32PadLeft(
Lib_RLPReader.readBytes(encodedValue)
);
} else {
// Exclusion proof, can only be zero bytes.
value = bytes32(0);
}
}
ovmStateManager.putContractStorage(
_ovmContractAddress,
_key,
value
);
}
3.执行交易
做完执行交易前的准备后,就由用户调用OVM_StateTransitioner的 applyTransaction 来执行交易
function applyTransaction(
Lib_OVMCodec.Transaction memory _transaction
)
override
public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
contributesToFraudProof(preStateRoot, transactionHash)
{
require(
Lib_OVMCodec.hashTransaction(_transaction) == transactionHash,
"Invalid transaction provided."
);
// We require gas to complete the logic here in run() before/after execution,
// But must ensure the full _tx.gasLimit can be given to the ovmCALL (determinism)
// This includes 1/64 of the gas getting lost because of EIP-150 (lost twice--first
// going into EM, then going into the code contract).
require(
gasleft() >= 100000 + _transaction.gasLimit * 1032 / 1000, // 1032/1000 = 1.032 = (64/63)^2 rounded up
"Not enough gas to execute transaction deterministically."
);
iOVM_ExecutionManager ovmExecutionManager = iOVM_ExecutionManager(resolve("OVM_ExecutionManager"));
// We call `setExecutionManager` right before `run` (and not earlier) just in case the
// OVM_ExecutionManager address was updated between the time when this contract was created
// and when `applyTransaction` was called.
ovmStateManager.setExecutionManager(address(ovmExecutionManager));
// `run` always succeeds *unless* the user hasn't provided enough gas to `applyTransaction`
// or an INVALID_STATE_ACCESS flag was triggered. Either way, we won't get beyond this line
// if that's the case.
ovmExecutionManager.run(_transaction, address(ovmStateManager));
// Prevent the Execution Manager from calling this SM again.
ovmStateManager.setExecutionManager(address(0));
phase = TransitionPhase.POST_EXECUTION;
}
通过代码可以看到,执行前有状态检查和Abs_FraudContributor中contributesToFraudProof,来记录验证的花费和交易. 然后通过OVM_ExecutionManager来run 这个交易,状态存在ovmStateManager中。最后将状态设置为POST_EXECUTION 我们就叫提交状态吧。
在这个阶段下,由于状态转换器/状态管理器对不知道整个L2状态,因此它们无法自动计算新的后状态根。需要用户调用 commitContractState 和 commitStorageSlot 来提交用户的账户和存储信息。
function commitContractState(
address _ovmContractAddress,
bytes memory _stateTrieWitness
)
override
public
onlyDuringPhase(TransitionPhase.POST_EXECUTION)
contributesToFraudProof(preStateRoot, transactionHash)
{
require(
ovmStateManager.getTotalUncommittedContractStorage() == 0,
"All storage must be committed before committing account states."
);
require (
ovmStateManager.commitAccount(_ovmContractAddress) == true,
"Account state wasn't changed or has already been committed."
);
Lib_OVMCodec.Account memory account = ovmStateManager.getAccount(_ovmContractAddress);
postStateRoot = Lib_SecureMerkleTrie.update(
abi.encodePacked(_ovmContractAddress),
Lib_OVMCodec.encodeEVMAccount(
Lib_OVMCodec.toEVMAccount(account)
),
_stateTrieWitness,
postStateRoot
);
// Emit an event to help clients figure out the proof ordering.
emit AccountCommitted(
_ovmContractAddress
);
}
/**
* Allows a user to commit the final state of a contract storage slot.
* @param _ovmContractAddress Address of the contract on the OVM.
* @param _key Claimed account slot key.
* @param _storageTrieWitness Proof of the storage slot.
*/
function commitStorageSlot(
address _ovmContractAddress,
bytes32 _key,
bytes memory _storageTrieWitness
)
override
public
onlyDuringPhase(TransitionPhase.POST_EXECUTION)
contributesToFraudProof(preStateRoot, transactionHash)
{
require(
ovmStateManager.commitContractStorage(_ovmContractAddress, _key) == true,
"Storage slot value wasn't changed or has already been committed."
);
Lib_OVMCodec.Account memory account = ovmStateManager.getAccount(_ovmContractAddress);
bytes32 value = ovmStateManager.getContractStorage(_ovmContractAddress, _key);
account.storageRoot = Lib_SecureMerkleTrie.update(
abi.encodePacked(_key),
Lib_RLPWriter.writeBytes(
Lib_Bytes32Utils.removeLeadingZeros(value)
),
_storageTrieWitness,
account.storageRoot
);
ovmStateManager.putAccount(_ovmContractAddress, account);
// Emit an event to help clients figure out the proof ordering.
emit ContractStorageCommitted(
_ovmContractAddress,
_key
);
}
提交完所有用户账户数据后,就是completeTransition 转换结束阶段。
function completeTransition()
override
public
onlyDuringPhase(TransitionPhase.POST_EXECUTION)
{
require(
ovmStateManager.getTotalUncommittedAccounts() == 0,
"All accounts must be committed before completing a transition."
);
require(
ovmStateManager.getTotalUncommittedContractStorage() == 0,
"All storage must be committed before completing a transition."
);
phase = TransitionPhase.COMPLETE;
}
这个阶段就是检查目前阶段是否是提交状态(POST_EXECUTION),然后检查未提交的账户是否是0,和未提交的合约存储是否是0.最后设置转换状态为完成状态(COMPLETE).
4.完成验证 finalizeFraudVerification
当状态转换器状态是COMPLETE后,就要执行OVM_FraudVerifier的finalizeFraudVerification方法来对验证结果做处理,来看代码
function finalizeFraudVerification(
bytes32 _preStateRoot,
Lib_OVMCodec.ChainBatchHeader memory _preStateRootBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _preStateRootProof,
bytes32 _txHash,
bytes32 _postStateRoot,
Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _postStateRootProof
)
override
public
contributesToFraudProof(_preStateRoot, _txHash)
{
iOVM_StateTransitioner transitioner = getStateTransitioner(_preStateRoot, _txHash);
iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
require(
transitioner.isComplete() == true,
"State transition process must be completed prior to finalization."
);
require (
_postStateRootBatchHeader.prevTotalElements + _postStateRootProof.index == _preStateRootBatchHeader.prevTotalElements + _preStateRootProof.index + 1,
"Post-state root global index must equal to the pre state root global index plus one."
);
require(
ovmStateCommitmentChain.verifyStateCommitment(
_preStateRoot,
_preStateRootBatchHeader,
_preStateRootProof
),
"Invalid pre-state root inclusion proof."
);
require(
ovmStateCommitmentChain.verifyStateCommitment(
_postStateRoot,
_postStateRootBatchHeader,
_postStateRootProof
),
"Invalid post-state root inclusion proof."
);
// If the post state root did not match, then there was fraud and we should delete the batch
require(
_postStateRoot != transitioner.getPostStateRoot(),
"State transition has not been proven fraudulent."
);
_cancelStateTransition(_postStateRootBatchHeader, _preStateRoot);
// TEMPORARY: Remove the transitioner; for minnet.
transitioners[keccak256(abi.encodePacked(_preStateRoot, _txHash))] = iOVM_StateTransitioner(0x0000000000000000000000000000000000000000);
emit FraudProofFinalized(
_preStateRoot,
_preStateRootProof.index,
_txHash,
msg.sender
);
}
这个方法会检查状态转换器是否是COMPLETE状态(transitioner.isComplete),并且拿_postStateRoot和转换器执行的stateRoot相比较。如果不一样,表示有欺诈.就会调用_cancelStateTransition 来运行处罚程序.
function _cancelStateTransition(
Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
bytes32 _preStateRoot
)
internal
{
iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
iOVM_BondManager ovmBondManager = iOVM_BondManager(resolve("OVM_BondManager"));
// Delete the state batch.
ovmStateCommitmentChain.deleteStateBatch(
_postStateRootBatchHeader
);
// Get the timestamp and publisher for that block.
(uint256 timestamp, address publisher) = abi.decode(_postStateRootBatchHeader.extraData, (uint256, address));
// Slash the bonds at the bond manager.
ovmBondManager.finalize(
_preStateRoot,
publisher,
timestamp
);
}
这里面会调用SCC Chain中的deleteStateBatch来删除(包括)有争议的交易之后的所有状态根Batch.CTC保持不变,因此原始交易将以相同顺序重新执行。来更新删除后的SCC Chain. 最后通过 质押管理器处理质押金和奖励验证者 ovmBondManager.finalize。
好了。到这篇文章后,基本上对Optimism这个系统也有了初步认识,,可能是不对的地方。望纠正。。












网友评论