github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/internal/era/builder.go (about)

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