DrawDB 是一款功能强大且用户友好的数据库实体关系 (DBER) 编辑器,直接在您的浏览器中使用。只需单击几下即可构建图表、导出 SQL 脚本、自定义编辑器等,无需创建帐户。在此处查看全套功能。
github: https://github.com/drawdb-io/drawdb
在线地址:https://www.drawdb.app/editor
DrawDB 是一款功能强大且用户友好的数据库实体关系 (DBER) 编辑器,直接在您的浏览器中使用。只需单击几下即可构建图表、导出 SQL 脚本、自定义编辑器等,无需创建帐户。在此处查看全套功能。
github: https://github.com/drawdb-io/drawdb
在线地址:https://www.drawdb.app/editor
gpu.cpp 是一个轻量级库,可以简化使用 C++ 的便携式 GPU 计算。
它专注于通用的原生 GPU 计算,利用 WebGPU 规范作为可移植的低级 GPU 接口。这意味着我们可以在 C++ 项目中插入 GPU 代码,并使其在 Nvidia、Intel、AMD 和其他 GPU 上运行。相同的 C++ 代码可以在各种笔记本电脑、工作站、移动设备或几乎任何支持 Vulkan、Metal 或 DirectX 的硬件上运行。
官网:https://gpucpp.answer.ai/
GitHub:https://github.com/AnswerDotAI/gpu.cpp
在Clique共识下,查看block
eth.getBlockByNumber(1000);
返回数据中miner为0x地址
miner: "0x0000000000000000000000000000000000000000",
对于Clique共识查看某个区块的miner,通过getSignersAtHash
查看
clique.getSignersAtHash("0xec6058d9364569e92a7e7c19889fa4daf86111ddf29dfcd6993be697b1cdd553")
["0x0eb7365be3c1fa53fe6ad8080c723def2a4cbb9f", "0x45d959f955e6ba85c03d1131e8e6efbf7061b6ec", "0x48f58ba03e2bfe0f84813184ba605c530ce333d5", "0x57fa14bde7440f47f3a9ddc854c1281d1b7a2fc5", "0x5cd8f88f97ead6f84957cf5a2c80db33da53a8c7"]
对于blockscout浏览器显示0x问题,需要首次启动前,修改docker-compose/envs/common-blockscout.env
BLOCK_TRANSFORMER=clique
https://github.com/blockscout/blockscout/issues/1990
https://github.com/ethereum/go-ethereum/issues/15651
Ethereum的EIP-4844对layer2来说是一次巨大的变革,它显著降低了layer2在使用L1作为DA(数据可用性)的费用。本文不详细解析EIP-4844的具体内容,只简要介绍,作为我们了解OP-Stack更新的背景。
TL;DR:
EIP-4844引入了一种称为“blob”的数据格式,这种格式的数据不参与EVM执行,而是存储在共识层,每个数据块的生命周期为4096个epoch(约18天)。blob存在于l1主网上,由新的type3 transaction携带,每个区块最多能容纳6个blob,每个transaction最多可以携带6个blob。
欲了解更多详情,请参考以下文章:
Ethereum Evolved: Dencun Upgrade Part 5, EIP-4844
EIP-4844, Blobs, and Blob Gas: What you need to know
OP-Stack在采用BLOB替换之前的CALLDATA作为rollup的数据存储方式后,费率直线下降
在OP-Stack的此次更新中,主要的业务逻辑变更涉及将原先通过calldata发送的数据转换为blob格式,并通过blob类型的交易发送到L1。此外,还涉及到从L1获取发送到rollup的数据时对blob的解析,以下是参与此次升级的主要组件:
⚠️⚠️⚠️请注意,本文中所有涉及的代码均基于最初的PR设计,可能与实际生产环境中运行的代码存在差异。
Pull Request(8767) encoding & decoding
BlobSize = 4096 * 32
type Blob [BlobSize]byte
official specs about blob encoding
需要注意的是,此specs对应的是最新版本的代码,而下方PR截取的代码则为最初的简化版本。主要区别在于:Blob类型被分为4096个字段元素,每个字段元素的最大大小受限于特定模的大小,即math.log2(BLS_MODULUS) = 254.8570894...,这意味着每个字段元素的大小不会超过254位,即31.75字节。最初的演示代码只使用了31字节,放弃了0.75字节的空间。而在最新版本的代码中,通过四个字段元素的联合作用,充分利用了每个字段元素的0.75字节空间,从而提高了数据的使用效率。
以下为Pull Request(8767)的部分截取代码
通过4096次循环,它读取总共31*4096字节的数据,这些数据随后被加入到blob中。
func (b *Blob) FromData(data Data) error {
if len(data) > MaxBlobDataSize {
return fmt.Errorf("data is too large for blob. len=%v", len(data))
}
b.Clear()
// encode 4-byte little-endian length value into topmost 4 bytes (out of 31) of first field
// element
binary.LittleEndian.PutUint32(b[1:5], uint32(len(data)))
// encode first 27 bytes of input data into remaining bytes of first field element
offset := copy(b[5:32], data)
// encode (up to) 31 bytes of remaining input data at a time into the subsequent field element
for i := 1; i < 4096; i++ {
offset += copy(b[i*32+1:i*32+32], data[offset:])
if offset == len(data) {
break
}
}
if offset < len(data) {
return fmt.Errorf("failed to fit all data into blob. bytes remaining: %v", len(data)-offset)
}
return nil
}
blob数据的解码,原理同上述的数据编码
func (b *Blob) ToData() (Data, error) {
data := make(Data, 4096*32)
for i := 0; i < 4096; i++ {
if b[i*32] != 0 {
return nil, fmt.Errorf("invalid blob, found non-zero high order byte %x of field element %d", b[i*32], i)
}
copy(data[i*31:i*31+31], b[i*32+1:i*32+32])
}
// extract the length prefix & trim the output accordingly
dataLen := binary.LittleEndian.Uint32(data[:4])
data = data[4:]
if dataLen > uint32(len(data)) {
return nil, fmt.Errorf("invalid blob, length prefix out of range: %d", dataLen)
}
data = data[:dataLen]
return data, nil
}
switch c.DataAvailabilityType {
case flags.CalldataType:
case flags.BlobsType:
default:
return fmt.Errorf("unknown data availability type: %v", c.DataAvailabilityType)
}
BatchSubmitter的功能从之前仅发送calldata数据扩展为根据情况发送calldata或blob类型的数据。Blob类型的数据通过之前提到的FromData(blob-encode)函数在blobTxCandidate内部进行编码
func (l *BatchSubmitter) sendTransaction(txdata txData, queue *txmgr.Queue[txData], receiptsCh chan txmgr.TxReceipt[txData]) error {
// Do the gas estimation offline. A value of 0 will cause the [txmgr] to estimate the gas limit.
data := txdata.Bytes()
var candidate *txmgr.TxCandidate
if l.Config.UseBlobs {
var err error
if candidate, err = l.blobTxCandidate(data); err != nil {
// We could potentially fall through and try a calldata tx instead, but this would
// likely result in the chain spending more in gas fees than it is tuned for, so best
// to just fail. We do not expect this error to trigger unless there is a serious bug
// or configuration issue.
return fmt.Errorf("could not create blob tx candidate: %w", err)
}
} else {
candidate = l.calldataTxCandidate(data)
}
intrinsicGas, err := core.IntrinsicGas(candidate.TxData, nil, false, true, true, false)
if err != nil {
// we log instead of return an error here because txmgr can do its own gas estimation
l.Log.Error("Failed to calculate intrinsic gas", "err", err)
} else {
candidate.GasLimit = intrinsicGas
}
queue.Send(txdata, *candidate, receiptsCh)
return nil
}
func (l *BatchSubmitter) blobTxCandidate(data []byte) (*txmgr.TxCandidate, error) {
var b eth.Blob
if err := b.FromData(data); err != nil {
return nil, fmt.Errorf("data could not be converted to blob: %w", err)
}
return &txmgr.TxCandidate{
To: &l.RollupConfig.BatchInboxAddress,
Blobs: []*eth.Blob{&b},
}, nil
}
GetBlob负责获取blob数据,其主要逻辑包括利用4096个字段元素构建完整的blob,并通过commitment验证构建的blob的正确性。
同时,GetBlob也参与了上层L1Retrieval中的逻辑流程。
func (p *PreimageOracle) GetBlob(ref eth.L1BlockRef, blobHash eth.IndexedBlobHash) *eth.Blob {
// Send a hint for the blob commitment & blob field elements.
blobReqMeta := make([]byte, 16)
binary.BigEndian.PutUint64(blobReqMeta[0:8], blobHash.Index)
binary.BigEndian.PutUint64(blobReqMeta[8:16], ref.Time)
p.hint.Hint(BlobHint(append(blobHash.Hash[:], blobReqMeta...)))
commitment := p.oracle.Get(preimage.Sha256Key(blobHash.Hash))
// Reconstruct the full blob from the 4096 field elements.
blob := eth.Blob{}
fieldElemKey := make([]byte, 80)
copy(fieldElemKey[:48], commitment)
for i := 0; i < params.BlobTxFieldElementsPerBlob; i++ {
binary.BigEndian.PutUint64(fieldElemKey[72:], uint64(i))
fieldElement := p.oracle.Get(preimage.BlobKey(crypto.Keccak256(fieldElemKey)))
copy(blob[i<<5:(i+1)<<5], fieldElement[:])
}
blobCommitment, err := blob.ComputeKZGCommitment()
if err != nil || !bytes.Equal(blobCommitment[:], commitment[:]) {
panic(fmt.Errorf("invalid blob commitment: %w", err))
}
return &blob
}
除了以上几个主要模块外,还包含例如负责签署type3类型transaction的client sign模块,和fault proof相关涉及的模块,fault proof会在下一章节进行详细描述,这里就不过多赘述了
Pull Request(5452), fault proof相关
Pull Request(9182), client sign相关
转载自:https://github.com/joohhnnn/Understanding-Optimism-Codebase-CN
在这一章节中,我们将探讨到底什么是op-proposer
首先分享下来自官方specs中的资源(source)
一句话概括性的描述proposer
的作用:定期的将来自layer2上的状态根(state root)发送到layer1上,以便可以无需信任地直接在layer1层面上执行一些来自layer2的交易,如提款或者message通信。
在本章节中,将会以处理来自layer2的一笔layer1的提现交易为例子,讲解op-proposer
在整个流程中的作用。
在Optimism中,提现是从L2(如 OP Mainnet, OP Goerli)到L1(如 Ethereum mainnet, Goerli)的交易,可能附带或不附带资产。可以粗略的分为四笔交易:
proposer
将L2中的state root
通过发送交易的方式上传到L1当中,以供接下来步骤中用户中L1中使用Merkle Patricia Trie
,证明提现的合法性;具体详情可以查看官方对于这部分的描述(source)
proposer
是服务于在L1中需要用到L2部分数据时的连通器,通过proposer
将这一部分L2的数据(state root)发送到L1的合约当中。L1的合约就可以通过合约调用的方式直接使用了。
注意⚠️:很多人认为proposer
发送的state root
后才代表这些设计的区块是finalized
。这种理解是错误
的。safe
的区块在L1中经过两个epoch(64个区块)
后即可认定为finalized
。
proposer
是将finalized
的区块数据上传,而不是上传后才finalized
。
在之前我们讲解了batcher
部分,batcher
也是负责把L2的数据发送到L1中。你可能会有疑问,batcher
不都已经把数据搬运到L1当中了,为什么还需要一个proposer
再进行一次搬运呢?
batcher
发送数据时,区块的状态还是unsafe
状态,不能直接使用,且无法根据batcher
的交易来判断区块的状态何时变成finalized
状态。
proposer
发送数据时,代表了相关区块已经达到了finalized
阶段,可以最大程度的去相信并使用相关数据。
batcher
是将几乎完整的交易信息,包括gasprice,data
等详细信息存储在layer1
当中。
proposer
只是将区块的state root
发送到l1当中。state root
后续配合merkle-tree的设计使用
batcher
传递的数据是巨量,proposer
是少量的。因此batcher
的数据更适合放置在calldate
中,便宜,但是不能直接被合约使用。proposer
的数据存储在合约的storage
当中,数据量少,成本不会很高,并且可以在合约交互中使用。
在以太坊中,calldata
和storage
的区别主要有三方面:
持久性:
storage
:持久存储,数据永久保存。calldata
:临时存储,函数执行完毕后数据消失。成本:
storage
:较贵,需永久存储数据。calldata
:较便宜,临时存储。可访问性:
storage
:多个函数或事务中可访问。calldata
:仅当前函数执行期间可访问。在这部分我们会从代码层来进行深度的机制和实现原理的讲解
op-proposer/proposer/l2_output_submitter.go
通过调用Start
函数来启动loop
循环,在loop
的循环中,主要通过函数FetchNextOutputInfo
负责查看下一个区块是否该发送proposal
交易,如果需要发送,则直接调用sendTransaction
函数发送到L1当作,如不需要发送,则进行下一次循环。
func (l *L2OutputSubmitter) loop() {
defer l.wg.Done()
ctx := l.ctx
ticker := time.NewTicker(l.pollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
output, shouldPropose, err := l.FetchNextOutputInfo(ctx)
if err != nil {
break
}
if !shouldPropose {
break
}
cCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
if err := l.sendTransaction(cCtx, output); err != nil {
l.log.Error("Failed to send proposal transaction",
"err", err,
"l1blocknum", output.Status.CurrentL1.Number,
"l1blockhash", output.Status.CurrentL1.Hash,
"l1head", output.Status.HeadL1.Number)
cancel()
break
}
l.metr.RecordL2BlocksProposed(output.BlockRef)
cancel()
case <-l.done:
return
}
}
}
op-proposer/proposer/l2_output_submitter.go
FetchNextOutputInfo
函数通过调用l2ooContract
合约来获取下一次该发送proposal
的区块数,再将该区块块号和当前L2度区块块号进行比较,来判断是否应该发送proposal
交易。如果需要发送,则调用fetchOutput
函数来生成output
func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) {
cCtx, cancel := context.WithTimeout(ctx, l.networkTimeout)
defer cancel()
callOpts := &bind.CallOpts{
From: l.txMgr.From(),
Context: cCtx,
}
nextCheckpointBlock, err := l.l2ooContract.NextBlockNumber(callOpts)
if err != nil {
l.log.Error("proposer unable to get next block number", "err", err)
return nil, false, err
}
// Fetch the current L2 heads
cCtx, cancel = context.WithTimeout(ctx, l.networkTimeout)
defer cancel()
status, err := l.rollupClient.SyncStatus(cCtx)
if err != nil {
l.log.Error("proposer unable to get sync status", "err", err)
return nil, false, err
}
// Use either the finalized or safe head depending on the config. Finalized head is default & safer.
var currentBlockNumber *big.Int
if l.allowNonFinalized {
currentBlockNumber = new(big.Int).SetUint64(status.SafeL2.Number)
} else {
currentBlockNumber = new(big.Int).SetUint64(status.FinalizedL2.Number)
}
// Ensure that we do not submit a block in the future
if currentBlockNumber.Cmp(nextCheckpointBlock) < 0 {
l.log.Debug("proposer submission interval has not elapsed", "currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextCheckpointBlock)
return nil, false, nil
}
return l.fetchOutput(ctx, nextCheckpointBlock)
}
fetchOutput
函数在内部间接通过OutputV0AtBlock
函数来获取并处理output
返回体
op-service/sources/l2_client.go
OutputV0AtBlock
函数获取之前检索出来需要传递proposal
的区块哈希来拿到区块头,再根据这个区块头派生OutputV0
所需要的数据。其中通过GetProof
函数获取的的proof
中的StorageHash(withdrawal_storage_root)
的作用是,如果只需要L2ToL1MessagePasserAddr
相关的state
的数据的话,withdrawal_storage_root
可以大幅度减小整个默克尔树证明过程的大小。
func (s *L2Client) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) {
head, err := s.InfoByHash(ctx, blockHash)
if err != nil {
return nil, fmt.Errorf("failed to get L2 block by hash: %w", err)
}
if head == nil {
return nil, ethereum.NotFound
}
proof, err := s.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []common.Hash{}, blockHash.String())
if err != nil {
return nil, fmt.Errorf("failed to get contract proof at block %s: %w", blockHash, err)
}
if proof == nil {
return nil, fmt.Errorf("proof %w", ethereum.NotFound)
}
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root
if err := proof.Verify(head.Root()); err != nil {
return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err)
}
stateRoot := head.Root()
return ð.OutputV0{
StateRoot: eth.Bytes32(stateRoot),
MessagePasserStorageRoot: eth.Bytes32(proof.StorageHash),
BlockHash: blockHash,
}, nil
}
op-proposer/proposer/l2_output_submitter.go
在sendTransaction
函数中会间接调用proposeL2OutputTxData
函数去使用L1链上合约的ABI
来将我们的output
与合约函数的入参格式
进行匹配。随后sendTransaction
函数将包装好的数据发送到L1上,与L2OutputOracle合约
交互。
func proposeL2OutputTxData(abi *abi.ABI, output *eth.OutputResponse) ([]byte, error) {
return abi.Pack(
"proposeL2Output",
output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number),
output.Status.CurrentL1.Hash,
new(big.Int).SetUint64(output.Status.CurrentL1.Number))
}
packages/contracts-bedrock/src/L1/L2OutputOracle.sol
L2OutputOracle合约
通过将此来自L2区块的state root
进行校验,并存入合约的storage
当中。
/// @notice Accepts an outputRoot and the timestamp of the corresponding L2 block.
/// The timestamp must be equal to the current value returned by `nextTimestamp()` in
/// order to be accepted. This function may only be called by the Proposer.
/// @param _outputRoot The L2 output of the checkpoint block.
/// @param _l2BlockNumber The L2 block number that resulted in _outputRoot.
/// @param _l1BlockHash A block hash which must be included in the current chain.
/// @param _l1BlockNumber The block number with the specified block hash.
function proposeL2Output(
bytes32 _outputRoot,
uint256 _l2BlockNumber,
bytes32 _l1BlockHash,
uint256 _l1BlockNumber
)
external
payable
{
require(msg.sender == proposer, "L2OutputOracle: only the proposer address can propose new outputs");
require(
_l2BlockNumber == nextBlockNumber(),
"L2OutputOracle: block number must be equal to next expected block number"
);
require(
computeL2Timestamp(_l2BlockNumber) < block.timestamp,
"L2OutputOracle: cannot propose L2 output in the future"
);
require(_outputRoot != bytes32(0), "L2OutputOracle: L2 output proposal cannot be the zero hash");
if (_l1BlockHash != bytes32(0)) {
// This check allows the proposer to propose an output based on a given L1 block,
// without fear that it will be reorged out.
// It will also revert if the blockheight provided is more than 256 blocks behind the
// chain tip (as the hash will return as zero). This does open the door to a griefing
// attack in which the proposer's submission is censored until the block is no longer
// retrievable, if the proposer is experiencing this attack it can simply leave out the
// blockhash value, and delay submission until it is confident that the L1 block is
// finalized.
require(
blockhash(_l1BlockNumber) == _l1BlockHash,
"L2OutputOracle: block hash does not match the hash at the expected height"
);
}
emit OutputProposed(_outputRoot, nextOutputIndex(), _l2BlockNumber, block.timestamp);
l2Outputs.push(
Types.OutputProposal({
outputRoot: _outputRoot,
timestamp: uint128(block.timestamp),
l2BlockNumber: uint128(_l2BlockNumber)
})
);
}
proposer
的总体实现思路与逻辑相对简单,即定期循环从L1中读取下次需要发送proposal
的L2区块并与本地L2区块比较,并负责将数据处理并发送到L1当中。其他在提款过程中的其他交易流程大部分由SDK
负责,可以详细阅读我们之前推送的官方对于提款过程部分的描述(source)。
如果想要查看在主网中proposer
的实际行为,可以查看此proposer address