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 }