forked from ethereum/go-ethereum
-
Notifications
You must be signed in to change notification settings - Fork 277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: blockhash mismatch #913
Draft
jonastheis
wants to merge
13
commits into
feat/sync-directly-from-da
Choose a base branch
from
jt/fix-blockhash-mismatch
base: feat/sync-directly-from-da
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 2 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
eb69db0
feat: implement missing header fields manager to provide missing head…
jonastheis e36fead
fix: fixed bug where reader.Read would only return first 10 bytes ins…
jonastheis c7ac706
feat: add missing block header fields manager to DA syncing pipeline …
jonastheis cf1eadf
refactor: compose DA types for more cohesion, maintainability and cod…
jonastheis 097ae5d
feat: add exponential backoff utility
jonastheis e6d5555
feat: add exponential backoff to pipeline
jonastheis fa25018
feat: run pipeline and L1 message sync in parallel
jonastheis 0adb079
Merge branch 'feat/sync-directly-from-da' into jt/fix-blockhash-mismatch
jonastheis 20204ab
feat: execute blocks only once
jonastheis 2d2691a
refactor: introduce partial header and partial block for data from DA…
jonastheis 43d9c8c
minor cleanup
jonastheis 291be77
Merge branch 'jt/execute-blocks-only-once' into jt/fix-blockhash-mism…
jonastheis 135eda0
feat: add missing header manager to source generation
jonastheis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package missing_header_fields | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/sha256" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"os" | ||
"time" | ||
|
||
"github.com/scroll-tech/go-ethereum/log" | ||
) | ||
|
||
const timeoutDownload = 10 * time.Minute | ||
|
||
// Manager is responsible for managing the missing header fields file. | ||
// It lazily downloads the file if it doesn't exist, verifies its checksum and provides the missing header fields. | ||
type Manager struct { | ||
ctx context.Context | ||
filePath string | ||
downloadURL string | ||
checksum [sha256.Size]byte | ||
|
||
reader *Reader | ||
} | ||
|
||
func NewManager(ctx context.Context, filePath string, downloadURL string, checksum [sha256.Size]byte) *Manager { | ||
return &Manager{ | ||
ctx: ctx, | ||
filePath: filePath, | ||
downloadURL: downloadURL, | ||
checksum: checksum, | ||
} | ||
} | ||
|
||
func (m *Manager) GetMissingHeaderFields(headerNum uint64) (difficulty uint64, extraData []byte, err error) { | ||
// lazy initialization: if the reader is not initialized this is the first time we read from the file | ||
if m.reader == nil { | ||
if err = m.initialize(); err != nil { | ||
return 0, nil, fmt.Errorf("failed to initialize missing header reader: %v", err) | ||
} | ||
} | ||
|
||
return m.reader.Read(headerNum) | ||
} | ||
|
||
func (m *Manager) initialize() error { | ||
// if the file doesn't exist, download it | ||
if _, err := os.Stat(m.filePath); errors.Is(err, os.ErrNotExist) { | ||
if err = m.downloadFile(); err != nil { | ||
return fmt.Errorf("failed to download file: %v", err) | ||
} | ||
} | ||
|
||
// verify the checksum | ||
f, err := os.Open(m.filePath) | ||
if err != nil { | ||
return fmt.Errorf("failed to open file: %v", err) | ||
} | ||
|
||
h := sha256.New() | ||
if _, err = io.Copy(h, f); err != nil { | ||
return fmt.Errorf("failed to copy file: %v", err) | ||
} | ||
if err = f.Close(); err != nil { | ||
return fmt.Errorf("failed to close file: %v", err) | ||
} | ||
|
||
if !bytes.Equal(h.Sum(nil), m.checksum[:]) { | ||
return fmt.Errorf("checksum mismatch") | ||
} | ||
|
||
// finally initialize the reader | ||
reader, err := NewReader(m.filePath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
m.reader = reader | ||
return nil | ||
} | ||
func (m *Manager) Close() error { | ||
if m.reader != nil { | ||
return m.reader.Close() | ||
} | ||
return nil | ||
} | ||
|
||
func (m *Manager) downloadFile() error { | ||
log.Info("Downloading missing header fields. This might take a while...", "url", m.downloadURL) | ||
|
||
downloadCtx, downloadCtxCancel := context.WithTimeout(m.ctx, timeoutDownload) | ||
defer downloadCtxCancel() | ||
|
||
req, err := http.NewRequestWithContext(downloadCtx, http.MethodGet, m.downloadURL, nil) | ||
if err != nil { | ||
return fmt.Errorf("failed to create download request: %v", err) | ||
} | ||
|
||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("failed to download file: %v", err) | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("server returned status code %d", resp.StatusCode) | ||
} | ||
|
||
// create a temporary file | ||
tmpFilePath := m.filePath + ".tmp" // append .tmp to the file path | ||
tmpFile, err := os.Create(tmpFilePath) | ||
if err != nil { | ||
return fmt.Errorf("failed to create temporary file: %v", err) | ||
} | ||
var ok bool | ||
defer func() { | ||
if !ok { | ||
_ = os.Remove(tmpFilePath) | ||
} | ||
}() | ||
|
||
// copy the response body to the temporary file and print progress | ||
writeCounter := NewWriteCounter(m.ctx, uint64(resp.ContentLength)) | ||
if _, err = io.Copy(tmpFile, io.TeeReader(resp.Body, writeCounter)); err != nil { | ||
return fmt.Errorf("failed to copy response body: %v", err) | ||
} | ||
|
||
if err = tmpFile.Close(); err != nil { | ||
return fmt.Errorf("failed to close temporary file: %v", err) | ||
} | ||
|
||
// rename the temporary file to the final file path | ||
if err = os.Rename(tmpFilePath, m.filePath); err != nil { | ||
return fmt.Errorf("failed to rename temporary file: %v", err) | ||
} | ||
|
||
ok = true | ||
return nil | ||
} | ||
|
||
type WriteCounter struct { | ||
ctx context.Context | ||
total uint64 | ||
written uint64 | ||
lastProgressPrinted time.Time | ||
} | ||
|
||
func NewWriteCounter(ctx context.Context, total uint64) *WriteCounter { | ||
return &WriteCounter{ | ||
ctx: ctx, | ||
total: total, | ||
} | ||
} | ||
|
||
func (wc *WriteCounter) Write(p []byte) (int, error) { | ||
n := len(p) | ||
wc.written += uint64(n) | ||
|
||
// check if the context is done and return early | ||
select { | ||
case <-wc.ctx.Done(): | ||
return n, wc.ctx.Err() | ||
default: | ||
} | ||
|
||
wc.printProgress() | ||
|
||
return n, nil | ||
} | ||
|
||
func (wc *WriteCounter) printProgress() { | ||
if time.Since(wc.lastProgressPrinted) < 5*time.Second { | ||
return | ||
} | ||
wc.lastProgressPrinted = time.Now() | ||
|
||
log.Info(fmt.Sprintf("Downloading missing header fields... %d MB / %d MB", toMB(wc.written), toMB(wc.total))) | ||
} | ||
|
||
func toMB(bytes uint64) uint64 { | ||
return bytes / 1024 / 1024 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package missing_header_fields | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/scroll-tech/go-ethereum/common" | ||
"github.com/scroll-tech/go-ethereum/log" | ||
) | ||
|
||
func TestManagerDownload(t *testing.T) { | ||
t.Skip("skipping test due to long runtime/downloading file") | ||
log.Root().SetHandler(log.StdoutHandler) | ||
|
||
// TODO: replace with actual sha256 hash and downloadURL | ||
sha256 := [32]byte(common.FromHex("0x575858a53b8cdde8d63a2cc1a5b90f1bbf0c2243b292a66a1ab2931d571eb260")) | ||
downloadURL := "https://ftp.halifax.rwth-aachen.de/ubuntu-releases/24.04/ubuntu-24.04-netboot-amd64.tar.gz" | ||
filePath := filepath.Join(t.TempDir(), "test_file_path") | ||
manager := NewManager(context.Background(), filePath, downloadURL, sha256) | ||
|
||
_, _, err := manager.GetMissingHeaderFields(0) | ||
require.NoError(t, err) | ||
|
||
// Check if the file was downloaded and tmp file was removed | ||
_, err = os.Stat(filePath) | ||
require.NoError(t, err) | ||
_, err = os.Stat(filePath + ".tmp") | ||
require.Error(t, err) | ||
} | ||
|
||
func TestManagerChecksum(t *testing.T) { | ||
// Checksum doesn't match | ||
{ | ||
sha256 := [32]byte(common.FromHex("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) | ||
downloadURL := "" // since the file exists we don't need to download it | ||
filePath := "testdata/missing_headers_1.dedup" | ||
manager := NewManager(context.Background(), filePath, downloadURL, sha256) | ||
|
||
_, _, err := manager.GetMissingHeaderFields(0) | ||
require.ErrorContains(t, err, "checksum mismatch") | ||
} | ||
|
||
// Checksum matches | ||
{ | ||
sha256 := [32]byte(common.FromHex("0x5dee238e74c350c7116868bfe6c5218d440be3613f47f8c052bd5cef46f4ae04")) | ||
downloadURL := "" // since the file exists we don't need to download it | ||
filePath := "testdata/missing_headers_1.dedup" | ||
manager := NewManager(context.Background(), filePath, downloadURL, sha256) | ||
|
||
difficulty, extra, err := manager.GetMissingHeaderFields(0) | ||
require.NoError(t, err) | ||
require.Equal(t, expectedMissingHeaders1[0].difficulty, difficulty) | ||
require.Equal(t, expectedMissingHeaders1[0].extra, extra) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package missing_header_fields | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"os" | ||
) | ||
|
||
type missingHeader struct { | ||
headerNum uint64 | ||
difficulty uint64 | ||
extraData []byte | ||
} | ||
|
||
type Reader struct { | ||
file *os.File | ||
reader *bufio.Reader | ||
sortedVanities map[int][32]byte | ||
lastReadHeader *missingHeader | ||
} | ||
|
||
func NewReader(filePath string) (*Reader, error) { | ||
f, err := os.Open(filePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to open file: %v", err) | ||
} | ||
|
||
r := &Reader{ | ||
file: f, | ||
reader: bufio.NewReader(f), | ||
} | ||
|
||
// read the count of unique vanities | ||
vanityCount, err := r.reader.ReadByte() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// read the unique vanities | ||
r.sortedVanities = make(map[int][32]byte) | ||
for i := uint8(0); i < vanityCount; i++ { | ||
var vanity [32]byte | ||
if _, err = r.reader.Read(vanity[:]); err != nil { | ||
return nil, err | ||
} | ||
r.sortedVanities[int(i)] = vanity | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func (r *Reader) Read(headerNum uint64) (difficulty uint64, extraData []byte, err error) { | ||
if r.lastReadHeader == nil { | ||
if _, _, err = r.ReadNext(); err != nil { | ||
return 0, nil, err | ||
} | ||
} | ||
|
||
if headerNum > r.lastReadHeader.headerNum { | ||
// skip the headers until the requested header number | ||
for i := r.lastReadHeader.headerNum; i < headerNum; i++ { | ||
if _, _, err = r.ReadNext(); err != nil { | ||
return 0, nil, err | ||
} | ||
} | ||
} | ||
|
||
if headerNum == r.lastReadHeader.headerNum { | ||
return r.lastReadHeader.difficulty, r.lastReadHeader.extraData, nil | ||
} | ||
|
||
// headerNum < r.lastReadHeader.headerNum is not supported | ||
return 0, nil, fmt.Errorf("requested header %d below last read header number %d", headerNum, r.lastReadHeader.headerNum) | ||
} | ||
|
||
func (r *Reader) ReadNext() (difficulty uint64, extraData []byte, err error) { | ||
// read the bitmask | ||
bitmask, err := r.reader.ReadByte() | ||
if err != nil { | ||
return 0, nil, fmt.Errorf("failed to read bitmask: %v", err) | ||
} | ||
|
||
bits := newBitMask(bitmask) | ||
|
||
seal := make([]byte, bits.sealLen()) | ||
if _, err = io.ReadFull(r.reader, seal); err != nil { | ||
return 0, nil, fmt.Errorf("failed to read seal: %v", err) | ||
} | ||
|
||
// construct the extraData field | ||
vanity := r.sortedVanities[bits.vanityIndex()] | ||
var b bytes.Buffer | ||
b.Write(vanity[:]) | ||
b.Write(seal) | ||
|
||
// we don't have the header number, so we'll just increment the last read header number | ||
// we assume that the headers are written in order, starting from 0 | ||
if r.lastReadHeader == nil { | ||
r.lastReadHeader = &missingHeader{ | ||
headerNum: 0, | ||
difficulty: uint64(bits.difficulty()), | ||
extraData: b.Bytes(), | ||
} | ||
} else { | ||
r.lastReadHeader.headerNum++ | ||
r.lastReadHeader.difficulty = uint64(bits.difficulty()) | ||
r.lastReadHeader.extraData = b.Bytes() | ||
} | ||
|
||
return difficulty, b.Bytes(), nil | ||
} | ||
|
||
func (r *Reader) Close() error { | ||
return r.file.Close() | ||
} | ||
|
||
// bitMask is a bitmask that encodes the following information: | ||
// | ||
// bit 0-5: index of the vanity in the sorted vanities list | ||
// bit 6: 0 if difficulty is 2, 1 if difficulty is 1 | ||
// bit 7: 0 if seal length is 65, 1 if seal length is 85 | ||
type bitMask struct { | ||
b uint8 | ||
} | ||
|
||
func newBitMask(b uint8) bitMask { | ||
return bitMask{b} | ||
} | ||
|
||
func (b bitMask) vanityIndex() int { | ||
return int(b.b & 0b00111111) | ||
} | ||
|
||
func (b bitMask) difficulty() int { | ||
val := (b.b >> 6) & 0x01 | ||
if val == 0 { | ||
return 2 | ||
} else { | ||
return 1 | ||
} | ||
} | ||
|
||
func (b bitMask) sealLen() int { | ||
val := (b.b >> 7) & 0x01 | ||
if val == 0 { | ||
return 65 | ||
} else { | ||
return 85 | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Risk: Affected versions of golang.org/x/net, golang.org/x/net/http2, and net/http are vulnerable to Uncontrolled Resource Consumption. An attacker may cause an HTTP/2 endpoint to read arbitrary amounts of header data by sending an excessive number of CONTINUATION frames.
Fix: Upgrade this library to at least version 0.23.0 at go-ethereum/go.mod:103.
Reference(s): GHSA-4v7x-pqxf-cx7m, CVE-2023-45288
Ignore this finding from ssc-46663897-ab0c-04dc-126b-07fe2ce42fb2.