github.com/ethereum/go-ethereum@v1.16.1/internal/era/builder.go (about)

     1  // Copyright 2024 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package era
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/binary"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"math/big"
    26  
    27  	"github.com/ethereum/go-ethereum/common"
    28  	"github.com/ethereum/go-ethereum/core/types"
    29  	"github.com/ethereum/go-ethereum/internal/era/e2store"
    30  	"github.com/ethereum/go-ethereum/rlp"
    31  	"github.com/golang/snappy"
    32  )
    33  
    34  // Builder is used to create Era1 archives of block data.
    35  //
    36  // Era1 files are themselves e2store files. For more information on this format,
    37  // see https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md.
    38  //
    39  // The overall structure of an Era1 file follows closely the structure of an Era file
    40  // which contains consensus Layer data (and as a byproduct, EL data after the merge).
    41  //
    42  // The structure can be summarized through this definition:
    43  //
    44  //	era1 := Version | block-tuple* | other-entries* | Accumulator | BlockIndex
    45  //	block-tuple :=  CompressedHeader | CompressedBody | CompressedReceipts | TotalDifficulty
    46  //
    47  // Each basic element is its own entry:
    48  //
    49  //	Version            = { type: [0x65, 0x32], data: nil }
    50  //	CompressedHeader   = { type: [0x03, 0x00], data: snappyFramed(rlp(header)) }
    51  //	CompressedBody     = { type: [0x04, 0x00], data: snappyFramed(rlp(body)) }
    52  //	CompressedReceipts = { type: [0x05, 0x00], data: snappyFramed(rlp(receipts)) }
    53  //	TotalDifficulty    = { type: [0x06, 0x00], data: uint256(header.total_difficulty) }
    54  //	AccumulatorRoot    = { type: [0x07, 0x00], data: accumulator-root }
    55  //	BlockIndex         = { type: [0x32, 0x66], data: block-index }
    56  //
    57  // Accumulator is computed by constructing an SSZ list of header-records of length at most
    58  // 8192 and then calculating the hash_tree_root of that list.
    59  //
    60  //	header-record := { block-hash: Bytes32, total-difficulty: Uint256 }
    61  //	accumulator   := hash_tree_root([]header-record, 8192)
    62  //
    63  // BlockIndex stores relative offsets to each compressed block entry. The
    64  // format is:
    65  //
    66  //	block-index := starting-number | index | index | index ... | count
    67  //
    68  // starting-number is the first block number in the archive. Every index is a
    69  // defined relative to beginning of the record. The total number of block
    70  // entries in the file is recorded with count.
    71  //
    72  // Due to the accumulator size limit of 8192, the maximum number of blocks in
    73  // an Era1 batch is also 8192.
    74  type Builder struct {
    75  	w        *e2store.Writer
    76  	startNum *uint64
    77  	startTd  *big.Int
    78  	indexes  []uint64
    79  	hashes   []common.Hash
    80  	tds      []*big.Int
    81  	written  int
    82  
    83  	buf    *bytes.Buffer
    84  	snappy *snappy.Writer
    85  }
    86  
    87  // NewBuilder returns a new Builder instance.
    88  func NewBuilder(w io.Writer) *Builder {
    89  	buf := bytes.NewBuffer(nil)
    90  	return &Builder{
    91  		w:      e2store.NewWriter(w),
    92  		buf:    buf,
    93  		snappy: snappy.NewBufferedWriter(buf),
    94  	}
    95  }
    96  
    97  // Add writes a compressed block entry and compressed receipts entry to the
    98  // underlying e2store file.
    99  func (b *Builder) Add(block *types.Block, receipts types.Receipts, td *big.Int) error {
   100  	eh, err := rlp.EncodeToBytes(block.Header())
   101  	if err != nil {
   102  		return err
   103  	}
   104  	eb, err := rlp.EncodeToBytes(block.Body())
   105  	if err != nil {
   106  		return err
   107  	}
   108  	er, err := rlp.EncodeToBytes(receipts)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	return b.AddRLP(eh, eb, er, block.NumberU64(), block.Hash(), td, block.Difficulty())
   113  }
   114  
   115  // AddRLP writes a compressed block entry and compressed receipts entry to the
   116  // underlying e2store file.
   117  func (b *Builder) AddRLP(header, body, receipts []byte, number uint64, hash common.Hash, td, difficulty *big.Int) error {
   118  	// Write Era1 version entry before first block.
   119  	if b.startNum == nil {
   120  		n, err := b.w.Write(TypeVersion, nil)
   121  		if err != nil {
   122  			return err
   123  		}
   124  		startNum := number
   125  		b.startNum = &startNum
   126  		b.startTd = new(big.Int).Sub(td, difficulty)
   127  		b.written += n
   128  	}
   129  	if len(b.indexes) >= MaxEra1Size {
   130  		return fmt.Errorf("exceeds maximum batch size of %d", MaxEra1Size)
   131  	}
   132  
   133  	b.indexes = append(b.indexes, uint64(b.written))
   134  	b.hashes = append(b.hashes, hash)
   135  	b.tds = append(b.tds, td)
   136  
   137  	// Write block data.
   138  	if err := b.snappyWrite(TypeCompressedHeader, header); err != nil {
   139  		return err
   140  	}
   141  	if err := b.snappyWrite(TypeCompressedBody, body); err != nil {
   142  		return err
   143  	}
   144  	if err := b.snappyWrite(TypeCompressedReceipts, receipts); err != nil {
   145  		return err
   146  	}
   147  
   148  	// Also write total difficulty, but don't snappy encode.
   149  	btd := bigToBytes32(td)
   150  	n, err := b.w.Write(TypeTotalDifficulty, btd[:])
   151  	b.written += n
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  // Finalize computes the accumulator and block index values, then writes the
   160  // corresponding e2store entries.
   161  func (b *Builder) Finalize() (common.Hash, error) {
   162  	if b.startNum == nil {
   163  		return common.Hash{}, errors.New("finalize called on empty builder")
   164  	}
   165  	// Compute accumulator root and write entry.
   166  	root, err := ComputeAccumulator(b.hashes, b.tds)
   167  	if err != nil {
   168  		return common.Hash{}, fmt.Errorf("error calculating accumulator root: %w", err)
   169  	}
   170  	n, err := b.w.Write(TypeAccumulator, root[:])
   171  	b.written += n
   172  	if err != nil {
   173  		return common.Hash{}, fmt.Errorf("error writing accumulator: %w", err)
   174  	}
   175  	// Get beginning of index entry to calculate block relative offset.
   176  	base := int64(b.written)
   177  
   178  	// Construct block index. Detailed format described in Builder
   179  	// documentation, but it is essentially encoded as:
   180  	// "start | index | index | ... | count"
   181  	var (
   182  		count = len(b.indexes)
   183  		index = make([]byte, 16+count*8)
   184  	)
   185  	binary.LittleEndian.PutUint64(index, *b.startNum)
   186  	// Each offset is relative from the position it is encoded in the
   187  	// index. This means that even if the same block was to be included in
   188  	// the index twice (this would be invalid anyways), the relative offset
   189  	// would be different. The idea with this is that after reading a
   190  	// relative offset, the corresponding block can be quickly read by
   191  	// performing a seek relative to the current position.
   192  	for i, offset := range b.indexes {
   193  		relative := int64(offset) - base
   194  		binary.LittleEndian.PutUint64(index[8+i*8:], uint64(relative))
   195  	}
   196  	binary.LittleEndian.PutUint64(index[8+count*8:], uint64(count))
   197  
   198  	// Finally, write the block index entry.
   199  	if _, err := b.w.Write(TypeBlockIndex, index); err != nil {
   200  		return common.Hash{}, fmt.Errorf("unable to write block index: %w", err)
   201  	}
   202  
   203  	return root, nil
   204  }
   205  
   206  // snappyWrite is a small helper to take care snappy encoding and writing an e2store entry.
   207  func (b *Builder) snappyWrite(typ uint16, in []byte) error {
   208  	var (
   209  		buf = b.buf
   210  		s   = b.snappy
   211  	)
   212  	buf.Reset()
   213  	s.Reset(buf)
   214  	if _, err := b.snappy.Write(in); err != nil {
   215  		return fmt.Errorf("error snappy encoding: %w", err)
   216  	}
   217  	if err := s.Flush(); err != nil {
   218  		return fmt.Errorf("error flushing snappy encoding: %w", err)
   219  	}
   220  	n, err := b.w.Write(typ, b.buf.Bytes())
   221  	b.written += n
   222  	if err != nil {
   223  		return fmt.Errorf("error writing e2store entry: %w", err)
   224  	}
   225  	return nil
   226  }