OPStack在EIP-4844中的升级

Ethereum的EIP-4844对layer2来说是一次巨大的变革,它显著降低了layer2在使用L1作为DA(数据可用性)的费用。本文不详细解析EIP-4844的具体内容,只简要介绍,作为我们了解OP-Stack更新的背景。

EIP-4844

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

Proto-Danksharding

OP-Stack的应用

OP-Stack在采用BLOB替换之前的CALLDATA作为rollup的数据存储方式后,费率直线下降
image

在OP-Stack的此次更新中,主要的业务逻辑变更涉及将原先通过calldata发送的数据转换为blob格式,并通过blob类型的交易发送到L1。此外,还涉及到从L1获取发送到rollup的数据时对blob的解析,以下是参与此次升级的主要组件:

  1. submitter —— 负责将rollup数据发送到L1的组件
  2. fetcher —— 将L1的数据(旧rollup数据/deposit交易等)同步到L2中
  3. blob相关定义与实现 —— 如何获取和结构blob数据等内容等
  4. 其他相关设计部分 —— 如客户端支持blob类型交易的签名、与fault proof相关的设计等

⚠️⚠️⚠️请注意,本文中所有涉及的代码均基于最初的PR设计,可能与实际生产环境中运行的代码存在差异。


Blob相关定义与编解码实现

Pull Request(8131) blob 定义

Pull Request(8767) encoding & decoding

定义blob

BlobSize        = 4096 * 32

type Blob [BlobSize]byte

blob encoding

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 decoding

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
}

Submiter

Pull Request(8769)

flag配置

switch c.DataAvailabilityType {
case flags.CalldataType:
case flags.BlobsType:
default:
    return fmt.Errorf("unknown data availability type: %v", c.DataAvailabilityType)
}

BatchSubmitter

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
}

Fetcher

Pull Request(9098)

GetBlob

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