mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 21:31:37 +00:00
Merge branch 'ethereum:master' into master
This commit is contained in:
commit
ea6da4378c
270 changed files with 11079 additions and 6731 deletions
1
.mailmap
1
.mailmap
|
|
@ -56,7 +56,6 @@ Diederik Loerakker <proto@protolambda.com>
|
|||
Dimitry Khokhlov <winsvega@mail.ru>
|
||||
|
||||
Domino Valdano <dominoplural@gmail.com>
|
||||
Domino Valdano <dominoplural@gmail.com> <jeff@okcupid.com>
|
||||
|
||||
Edgar Aroutiounian <edgar.factorial@gmail.com>
|
||||
|
||||
|
|
|
|||
20
.travis.yml
20
.travis.yml
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
os: linux
|
||||
arch: amd64
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- docker
|
||||
services:
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
os: linux
|
||||
arch: arm64
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- docker
|
||||
services:
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
os: linux
|
||||
dist: bionic
|
||||
sudo: required
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- azure-linux
|
||||
git:
|
||||
|
|
@ -85,7 +85,7 @@ jobs:
|
|||
if: type = push
|
||||
os: osx
|
||||
osx_image: xcode14.2
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- azure-osx
|
||||
git:
|
||||
|
|
@ -101,7 +101,7 @@ jobs:
|
|||
os: linux
|
||||
arch: amd64
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
script:
|
||||
- travis_wait 30 go run build/ci.go test $TEST_PACKAGES
|
||||
|
||||
|
|
@ -110,14 +110,14 @@ jobs:
|
|||
os: linux
|
||||
arch: arm64
|
||||
dist: bionic
|
||||
go: 1.20.x
|
||||
go: 1.21.x
|
||||
script:
|
||||
- travis_wait 30 go run build/ci.go test $TEST_PACKAGES
|
||||
|
||||
- stage: build
|
||||
os: linux
|
||||
dist: bionic
|
||||
go: 1.20.x
|
||||
go: 1.21.x
|
||||
script:
|
||||
- travis_wait 30 go run build/ci.go test $TEST_PACKAGES
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ jobs:
|
|||
if: type = cron || (type = push && tag ~= /^v[0-9]/)
|
||||
os: linux
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- ubuntu-ppa
|
||||
git:
|
||||
|
|
@ -149,7 +149,7 @@ jobs:
|
|||
if: type = cron
|
||||
os: linux
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
env:
|
||||
- azure-purge
|
||||
git:
|
||||
|
|
@ -162,7 +162,7 @@ jobs:
|
|||
if: type = cron
|
||||
os: linux
|
||||
dist: bionic
|
||||
go: 1.21.x
|
||||
go: 1.22.x
|
||||
script:
|
||||
- travis_wait 30 go run build/ci.go test -race $TEST_PACKAGES
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ ARG VERSION=""
|
|||
ARG BUILDNUM=""
|
||||
|
||||
# Build Geth in a stock Go builder container
|
||||
FROM golang:1.21-alpine as builder
|
||||
FROM golang:1.22-alpine as builder
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev linux-headers git
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ COPY --from=builder /go-ethereum/build/bin/geth /usr/local/bin/
|
|||
EXPOSE 8545 8546 30303 30303/udp
|
||||
ENTRYPOINT ["geth"]
|
||||
|
||||
# Add some metadata labels to help programatic image consumption
|
||||
# Add some metadata labels to help programmatic image consumption
|
||||
ARG COMMIT=""
|
||||
ARG VERSION=""
|
||||
ARG BUILDNUM=""
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ ARG VERSION=""
|
|||
ARG BUILDNUM=""
|
||||
|
||||
# Build Geth in a stock Go builder container
|
||||
FROM golang:1.21-alpine as builder
|
||||
FROM golang:1.22-alpine as builder
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev linux-headers git
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ COPY --from=builder /go-ethereum/build/bin/* /usr/local/bin/
|
|||
|
||||
EXPOSE 8545 8546 30303 30303/udp
|
||||
|
||||
# Add some metadata labels to help programatic image consumption
|
||||
# Add some metadata labels to help programmatic image consumption
|
||||
ARG COMMIT=""
|
||||
ARG VERSION=""
|
||||
ARG BUILDNUM=""
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -2,7 +2,7 @@
|
|||
# with Go source code. If you know what GOPATH is then you probably
|
||||
# don't need to bother with make.
|
||||
|
||||
.PHONY: geth android ios evm all test clean
|
||||
.PHONY: geth all test lint clean devtools help
|
||||
|
||||
GOBIN = ./build/bin
|
||||
GO ?= latest
|
||||
|
|
@ -47,4 +47,3 @@ devtools:
|
|||
help: Makefile
|
||||
@echo " Choose a command run in go-ethereum:"
|
||||
@sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /'
|
||||
.PHONY: help
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ archives are published at https://geth.ethereum.org/downloads/.
|
|||
|
||||
For prerequisites and detailed build instructions please read the [Installation Instructions](https://geth.ethereum.org/docs/getting-started/installing-geth).
|
||||
|
||||
Building `geth` requires both a Go (version 1.19 or later) and a C compiler. You can install
|
||||
Building `geth` requires both a Go (version 1.21 or later) and a C compiler. You can install
|
||||
them using your favourite package manager. Once the dependencies are installed, run
|
||||
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -142,10 +142,10 @@ func NewKeyStoreTransactorWithChainID(keystore *keystore.KeyStore, account accou
|
|||
// NewKeyedTransactorWithChainID is a utility method to easily create a transaction signer
|
||||
// from a single private key.
|
||||
func NewKeyedTransactorWithChainID(key *ecdsa.PrivateKey, chainID *big.Int) (*TransactOpts, error) {
|
||||
keyAddr := crypto.PubkeyToAddress(key.PublicKey)
|
||||
if chainID == nil {
|
||||
return nil, ErrNoChainID
|
||||
}
|
||||
keyAddr := crypto.PubkeyToAddress(key.PublicKey)
|
||||
signer := types.LatestSignerForChainID(chainID)
|
||||
return &TransactOpts{
|
||||
From: keyAddr,
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]int
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sub, err := event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
sub := event.NewSubscription(func(quit <-chan struct{}) error {
|
||||
for _, log := range buff {
|
||||
select {
|
||||
case logs <- log:
|
||||
|
|
@ -470,11 +470,8 @@ func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]int
|
|||
}
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return logs, sub, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,9 +179,6 @@ func NewType(t string, internalType string, components []ArgumentMarshaling) (ty
|
|||
return Type{}, errors.New("abi: purely anonymous or underscored field is not supported")
|
||||
}
|
||||
fieldName := ResolveNameConflict(name, func(s string) bool { return used[s] })
|
||||
if err != nil {
|
||||
return Type{}, err
|
||||
}
|
||||
used[fieldName] = true
|
||||
if !isValidFieldName(fieldName) {
|
||||
return Type{}, fmt.Errorf("field %d has invalid name", idx)
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ func TextHash(data []byte) []byte {
|
|||
//
|
||||
// This gives context to the signed message and prevents signing of transactions.
|
||||
func TextAndHash(data []byte) ([]byte, string) {
|
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), string(data))
|
||||
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
|
||||
hasher := sha3.NewLegacyKeccak256()
|
||||
hasher.Write([]byte(msg))
|
||||
return hasher.Sum(nil), msg
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error {
|
|||
func TestWatchNewFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, ks := tmpKeyStore(t, false)
|
||||
dir, ks := tmpKeyStore(t)
|
||||
|
||||
// Ensure the watcher is started before adding any files.
|
||||
ks.Accounts()
|
||||
|
|
|
|||
|
|
@ -87,15 +87,6 @@ func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
|
|||
return ks
|
||||
}
|
||||
|
||||
// NewPlaintextKeyStore creates a keystore for the given directory.
|
||||
// Deprecated: Use NewKeyStore.
|
||||
func NewPlaintextKeyStore(keydir string) *KeyStore {
|
||||
keydir, _ = filepath.Abs(keydir)
|
||||
ks := &KeyStore{storage: &keyStorePlain{keydir}}
|
||||
ks.init(keydir)
|
||||
return ks
|
||||
}
|
||||
|
||||
func (ks *KeyStore) init(keydir string) {
|
||||
// Lock the mutex since the account cache might call back with events
|
||||
ks.mu.Lock()
|
||||
|
|
@ -509,7 +500,5 @@ func (ks *KeyStore) isUpdating() bool {
|
|||
// zeroKey zeroes a private key in memory.
|
||||
func zeroKey(k *ecdsa.PrivateKey) {
|
||||
b := k.D.Bits()
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
clear(b)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ var testSigData = make([]byte, 32)
|
|||
|
||||
func TestKeyStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir, ks := tmpKeyStore(t, true)
|
||||
dir, ks := tmpKeyStore(t)
|
||||
|
||||
a, err := ks.NewAccount("foo")
|
||||
if err != nil {
|
||||
|
|
@ -72,7 +72,7 @@ func TestKeyStore(t *testing.T) {
|
|||
|
||||
func TestSign(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
pass := "" // not used but required by API
|
||||
a1, err := ks.NewAccount(pass)
|
||||
|
|
@ -89,7 +89,7 @@ func TestSign(t *testing.T) {
|
|||
|
||||
func TestSignWithPassphrase(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
pass := "passwd"
|
||||
acc, err := ks.NewAccount(pass)
|
||||
|
|
@ -117,7 +117,7 @@ func TestSignWithPassphrase(t *testing.T) {
|
|||
|
||||
func TestTimedUnlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
|
|
@ -152,7 +152,7 @@ func TestTimedUnlock(t *testing.T) {
|
|||
|
||||
func TestOverrideUnlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, false)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
pass := "foo"
|
||||
a1, err := ks.NewAccount(pass)
|
||||
|
|
@ -193,7 +193,7 @@ func TestOverrideUnlock(t *testing.T) {
|
|||
// This test should fail under -race if signing races the expiration goroutine.
|
||||
func TestSignRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, false)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
// Create a test account.
|
||||
a1, err := ks.NewAccount("")
|
||||
|
|
@ -238,7 +238,7 @@ func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time
|
|||
func TestWalletNotifierLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create a temporary keystore to test with
|
||||
_, ks := tmpKeyStore(t, false)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
// Ensure that the notification updater is not running yet
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
|
@ -284,7 +284,7 @@ type walletEvent struct {
|
|||
// or deleted from the keystore.
|
||||
func TestWalletNotifications(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, false)
|
||||
_, ks := tmpKeyStore(t)
|
||||
|
||||
// Subscribe to the wallet feed and collect events.
|
||||
var (
|
||||
|
|
@ -346,7 +346,7 @@ func TestWalletNotifications(t *testing.T) {
|
|||
// TestImportExport tests the import functionality of a keystore.
|
||||
func TestImportECDSA(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %v", key)
|
||||
|
|
@ -365,7 +365,7 @@ func TestImportECDSA(t *testing.T) {
|
|||
// TestImportECDSA tests the import and export functionality of a keystore.
|
||||
func TestImportExport(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
acc, err := ks.NewAccount("old")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create account: %v", acc)
|
||||
|
|
@ -374,7 +374,7 @@ func TestImportExport(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("failed to export account: %v", acc)
|
||||
}
|
||||
_, ks2 := tmpKeyStore(t, true)
|
||||
_, ks2 := tmpKeyStore(t)
|
||||
if _, err = ks2.Import(json, "old", "old"); err == nil {
|
||||
t.Errorf("importing with invalid password succeeded")
|
||||
}
|
||||
|
|
@ -394,7 +394,7 @@ func TestImportExport(t *testing.T) {
|
|||
// This test should fail under -race if importing races.
|
||||
func TestImportRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ks := tmpKeyStore(t, true)
|
||||
_, ks := tmpKeyStore(t)
|
||||
acc, err := ks.NewAccount("old")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create account: %v", acc)
|
||||
|
|
@ -403,7 +403,7 @@ func TestImportRace(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("failed to export account: %v", acc)
|
||||
}
|
||||
_, ks2 := tmpKeyStore(t, true)
|
||||
_, ks2 := tmpKeyStore(t)
|
||||
var atom atomic.Uint32
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
|
@ -457,11 +457,7 @@ func checkEvents(t *testing.T, want []walletEvent, have []walletEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) {
|
||||
func tmpKeyStore(t *testing.T) (string, *KeyStore) {
|
||||
d := t.TempDir()
|
||||
newKs := NewPlaintextKeyStore
|
||||
if encrypted {
|
||||
newKs = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) }
|
||||
}
|
||||
return d, newKs(d)
|
||||
return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import (
|
|||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
|
|
@ -72,11 +71,11 @@ func NewSecureChannelSession(card *pcsc.Card, keyData []byte) (*SecureChannelSes
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal public key from card: %v", err)
|
||||
}
|
||||
secret, _ := key.Curve.ScalarMult(cardPublic.X, cardPublic.Y, key.D.Bytes())
|
||||
secret, _ := crypto.S256().ScalarMult(cardPublic.X, cardPublic.Y, key.D.Bytes())
|
||||
return &SecureChannelSession{
|
||||
card: card,
|
||||
secret: secret.Bytes(),
|
||||
publicKey: elliptic.Marshal(crypto.S256(), key.PublicKey.X, key.PublicKey.Y),
|
||||
publicKey: crypto.FromECDSAPub(&key.PublicKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/karalabe/usb"
|
||||
"github.com/karalabe/hid"
|
||||
)
|
||||
|
||||
// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
|
||||
|
|
@ -109,7 +109,7 @@ func NewTrezorHubWithWebUSB() (*Hub, error) {
|
|||
|
||||
// newHub creates a new hardware wallet manager for generic USB devices.
|
||||
func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) {
|
||||
if !usb.Supported() {
|
||||
if !hid.Supported() {
|
||||
return nil, errors.New("unsupported platform")
|
||||
}
|
||||
hub := &Hub{
|
||||
|
|
@ -155,7 +155,7 @@ func (hub *Hub) refreshWallets() {
|
|||
return
|
||||
}
|
||||
// Retrieve the current list of USB wallet devices
|
||||
var devices []usb.DeviceInfo
|
||||
var devices []hid.DeviceInfo
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
// hidapi on Linux opens the device during enumeration to retrieve some infos,
|
||||
|
|
@ -170,7 +170,7 @@ func (hub *Hub) refreshWallets() {
|
|||
return
|
||||
}
|
||||
}
|
||||
infos, err := usb.Enumerate(hub.vendorID, 0)
|
||||
infos, err := hid.Enumerate(hub.vendorID, 0)
|
||||
if err != nil {
|
||||
failcount := hub.enumFails.Add(1)
|
||||
if runtime.GOOS == "linux" {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
// This file contains the implementation for interacting with the Ledger hardware
|
||||
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||
// https://github.com/LedgerHQ/app-ethereum/blob/develop/doc/ethapp.adoc
|
||||
|
||||
package usbwallet
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/karalabe/usb"
|
||||
"github.com/karalabe/hid"
|
||||
)
|
||||
|
||||
// Maximum time between wallet health checks to detect USB unplugs.
|
||||
|
|
@ -79,8 +79,8 @@ type wallet struct {
|
|||
driver driver // Hardware implementation of the low level device operations
|
||||
url *accounts.URL // Textual URL uniquely identifying this wallet
|
||||
|
||||
info usb.DeviceInfo // Known USB device infos about the wallet
|
||||
device usb.Device // USB device advertising itself as a hardware wallet
|
||||
info hid.DeviceInfo // Known USB device infos about the wallet
|
||||
device hid.Device // USB device advertising itself as a hardware wallet
|
||||
|
||||
accounts []accounts.Account // List of derive accounts pinned on the hardware wallet
|
||||
paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ for:
|
|||
- go run build/ci.go lint
|
||||
- go run build/ci.go install -dlgo
|
||||
test_script:
|
||||
- go run build/ci.go test -dlgo
|
||||
- go run build/ci.go test -dlgo -short
|
||||
|
||||
# linux/386 is disabled.
|
||||
- matrix:
|
||||
|
|
|
|||
146
beacon/blsync/block_sync.go
Executable file
146
beacon/blsync/block_sync.go
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blsync
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/sync"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/lru"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// beaconBlockSync implements request.Module; it fetches the beacon blocks belonging
|
||||
// to the validated and prefetch heads.
|
||||
type beaconBlockSync struct {
|
||||
recentBlocks *lru.Cache[common.Hash, *types.BeaconBlock]
|
||||
locked map[common.Hash]request.ServerAndID
|
||||
serverHeads map[request.Server]common.Hash
|
||||
headTracker headTracker
|
||||
|
||||
lastHeadInfo types.HeadInfo
|
||||
chainHeadFeed event.FeedOf[types.ChainHeadEvent]
|
||||
}
|
||||
|
||||
type headTracker interface {
|
||||
PrefetchHead() types.HeadInfo
|
||||
ValidatedHead() (types.SignedHeader, bool)
|
||||
ValidatedFinality() (types.FinalityUpdate, bool)
|
||||
}
|
||||
|
||||
// newBeaconBlockSync returns a new beaconBlockSync.
|
||||
func newBeaconBlockSync(headTracker headTracker) *beaconBlockSync {
|
||||
return &beaconBlockSync{
|
||||
headTracker: headTracker,
|
||||
recentBlocks: lru.NewCache[common.Hash, *types.BeaconBlock](10),
|
||||
locked: make(map[common.Hash]request.ServerAndID),
|
||||
serverHeads: make(map[request.Server]common.Hash),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *beaconBlockSync) SubscribeChainHead(ch chan<- types.ChainHeadEvent) event.Subscription {
|
||||
return s.chainHeadFeed.Subscribe(ch)
|
||||
}
|
||||
|
||||
// Process implements request.Module.
|
||||
func (s *beaconBlockSync) Process(requester request.Requester, events []request.Event) {
|
||||
for _, event := range events {
|
||||
switch event.Type {
|
||||
case request.EvResponse, request.EvFail, request.EvTimeout:
|
||||
sid, req, resp := event.RequestInfo()
|
||||
blockRoot := common.Hash(req.(sync.ReqBeaconBlock))
|
||||
if resp != nil {
|
||||
s.recentBlocks.Add(blockRoot, resp.(*types.BeaconBlock))
|
||||
}
|
||||
if s.locked[blockRoot] == sid {
|
||||
delete(s.locked, blockRoot)
|
||||
}
|
||||
case sync.EvNewHead:
|
||||
s.serverHeads[event.Server] = event.Data.(types.HeadInfo).BlockRoot
|
||||
case request.EvUnregistered:
|
||||
delete(s.serverHeads, event.Server)
|
||||
}
|
||||
}
|
||||
s.updateEventFeed()
|
||||
// request validated head block if unavailable and not yet requested
|
||||
if vh, ok := s.headTracker.ValidatedHead(); ok {
|
||||
s.tryRequestBlock(requester, vh.Header.Hash(), false)
|
||||
}
|
||||
// request prefetch head if the given server has announced it
|
||||
if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) {
|
||||
s.tryRequestBlock(requester, prefetchHead, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *beaconBlockSync) tryRequestBlock(requester request.Requester, blockRoot common.Hash, needSameHead bool) {
|
||||
if _, ok := s.recentBlocks.Get(blockRoot); ok {
|
||||
return
|
||||
}
|
||||
if _, ok := s.locked[blockRoot]; ok {
|
||||
return
|
||||
}
|
||||
for _, server := range requester.CanSendTo() {
|
||||
if needSameHead && (s.serverHeads[server] != blockRoot) {
|
||||
continue
|
||||
}
|
||||
id := requester.Send(server, sync.ReqBeaconBlock(blockRoot))
|
||||
s.locked[blockRoot] = request.ServerAndID{Server: server, ID: id}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func blockHeadInfo(block *types.BeaconBlock) types.HeadInfo {
|
||||
if block == nil {
|
||||
return types.HeadInfo{}
|
||||
}
|
||||
return types.HeadInfo{Slot: block.Slot(), BlockRoot: block.Root()}
|
||||
}
|
||||
|
||||
func (s *beaconBlockSync) updateEventFeed() {
|
||||
head, ok := s.headTracker.ValidatedHead()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
finality, ok := s.headTracker.ValidatedFinality() //TODO fetch directly if subscription does not deliver
|
||||
if !ok || head.Header.Epoch() != finality.Attested.Header.Epoch() {
|
||||
return
|
||||
}
|
||||
validatedHead := head.Header.Hash()
|
||||
headBlock, ok := s.recentBlocks.Get(validatedHead)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
headInfo := blockHeadInfo(headBlock)
|
||||
if headInfo == s.lastHeadInfo {
|
||||
return
|
||||
}
|
||||
s.lastHeadInfo = headInfo
|
||||
|
||||
// new head block and finality info available; extract executable data and send event to feed
|
||||
execBlock, err := headBlock.ExecutionPayload()
|
||||
if err != nil {
|
||||
log.Error("Error extracting execution block from validated beacon block", "error", err)
|
||||
return
|
||||
}
|
||||
s.chainHeadFeed.Send(types.ChainHeadEvent{
|
||||
BeaconHead: head.Header,
|
||||
Block: execBlock,
|
||||
Finalized: finality.Finalized.PayloadHeader.BlockHash(),
|
||||
})
|
||||
}
|
||||
150
beacon/blsync/block_sync_test.go
Normal file
150
beacon/blsync/block_sync_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blsync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/sync"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/deneb"
|
||||
)
|
||||
|
||||
var (
|
||||
testServer1 = "testServer1"
|
||||
testServer2 = "testServer2"
|
||||
|
||||
testBlock1 = types.NewBeaconBlock(&deneb.BeaconBlock{
|
||||
Slot: 123,
|
||||
Body: deneb.BeaconBlockBody{
|
||||
ExecutionPayload: deneb.ExecutionPayload{
|
||||
BlockNumber: 456,
|
||||
BlockHash: zrntcommon.Hash32(common.HexToHash("905ac721c4058d9ed40b27b6b9c1bdd10d4333e4f3d9769100bf9dfb80e5d1f6")),
|
||||
},
|
||||
},
|
||||
})
|
||||
testBlock2 = types.NewBeaconBlock(&deneb.BeaconBlock{
|
||||
Slot: 124,
|
||||
Body: deneb.BeaconBlockBody{
|
||||
ExecutionPayload: deneb.ExecutionPayload{
|
||||
BlockNumber: 457,
|
||||
BlockHash: zrntcommon.Hash32(common.HexToHash("011703f39c664efc1c6cf5f49ca09b595581eec572d4dfddd3d6179a9e63e655")),
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
func TestBlockSync(t *testing.T) {
|
||||
ht := &testHeadTracker{}
|
||||
blockSync := newBeaconBlockSync(ht)
|
||||
headCh := make(chan types.ChainHeadEvent, 16)
|
||||
blockSync.SubscribeChainHead(headCh)
|
||||
ts := sync.NewTestScheduler(t, blockSync)
|
||||
ts.AddServer(testServer1, 1)
|
||||
ts.AddServer(testServer2, 1)
|
||||
|
||||
expHeadBlock := func(expHead *types.BeaconBlock) {
|
||||
t.Helper()
|
||||
var expNumber, headNumber uint64
|
||||
if expHead != nil {
|
||||
p, _ := expHead.ExecutionPayload()
|
||||
expNumber = p.NumberU64()
|
||||
}
|
||||
select {
|
||||
case event := <-headCh:
|
||||
headNumber = event.Block.NumberU64()
|
||||
default:
|
||||
}
|
||||
if headNumber != expNumber {
|
||||
t.Errorf("Wrong head block, expected block number %d, got %d)", expNumber, headNumber)
|
||||
}
|
||||
}
|
||||
|
||||
// no block requests expected until head tracker knows about a head
|
||||
ts.Run(1)
|
||||
expHeadBlock(nil)
|
||||
|
||||
// set block 1 as prefetch head, announced by server 2
|
||||
head1 := blockHeadInfo(testBlock1)
|
||||
ht.prefetch = head1
|
||||
ts.ServerEvent(sync.EvNewHead, testServer2, head1)
|
||||
|
||||
// expect request to server 2 which has announced the head
|
||||
ts.Run(2, testServer2, sync.ReqBeaconBlock(head1.BlockRoot))
|
||||
|
||||
// valid response
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testBlock1)
|
||||
ts.AddAllowance(testServer2, 1)
|
||||
ts.Run(3)
|
||||
// head block still not expected as the fetched block is not the validated head yet
|
||||
expHeadBlock(nil)
|
||||
|
||||
// set as validated head, expect no further requests but block 1 set as head block
|
||||
ht.validated.Header = testBlock1.Header()
|
||||
ts.Run(4)
|
||||
expHeadBlock(testBlock1)
|
||||
|
||||
// set block 2 as prefetch head, announced by server 1
|
||||
head2 := blockHeadInfo(testBlock2)
|
||||
ht.prefetch = head2
|
||||
ts.ServerEvent(sync.EvNewHead, testServer1, head2)
|
||||
// expect request to server 1
|
||||
ts.Run(5, testServer1, sync.ReqBeaconBlock(head2.BlockRoot))
|
||||
|
||||
// req2 fails, no further requests expected because server 2 has not announced it
|
||||
ts.RequestEvent(request.EvFail, ts.Request(5, 1), nil)
|
||||
ts.Run(6)
|
||||
|
||||
// set as validated head before retrieving block; now it's assumed to be available from server 2 too
|
||||
ht.validated.Header = testBlock2.Header()
|
||||
// expect req2 retry to server 2
|
||||
ts.Run(7, testServer2, sync.ReqBeaconBlock(head2.BlockRoot))
|
||||
// now head block should be unavailable again
|
||||
expHeadBlock(nil)
|
||||
|
||||
// valid response, now head block should be block 2 immediately as it is already validated
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testBlock2)
|
||||
ts.Run(8)
|
||||
expHeadBlock(testBlock2)
|
||||
}
|
||||
|
||||
type testHeadTracker struct {
|
||||
prefetch types.HeadInfo
|
||||
validated types.SignedHeader
|
||||
}
|
||||
|
||||
func (h *testHeadTracker) PrefetchHead() types.HeadInfo {
|
||||
return h.prefetch
|
||||
}
|
||||
|
||||
func (h *testHeadTracker) ValidatedHead() (types.SignedHeader, bool) {
|
||||
return h.validated, h.validated.Header != (types.Header{})
|
||||
}
|
||||
|
||||
// TODO add test case for finality
|
||||
func (h *testHeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
|
||||
finalized := types.NewExecutionHeader(new(deneb.ExecutionPayloadHeader))
|
||||
return types.FinalityUpdate{
|
||||
Attested: types.HeaderWithExecProof{Header: h.validated.Header},
|
||||
Finalized: types.HeaderWithExecProof{PayloadHeader: finalized},
|
||||
Signature: h.validated.Signature,
|
||||
SignatureSlot: h.validated.SignatureSlot,
|
||||
}, h.validated.Header != (types.Header{})
|
||||
}
|
||||
115
beacon/blsync/client.go
Normal file
115
beacon/blsync/client.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blsync
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/api"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/sync"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
urls []string
|
||||
customHeader map[string]string
|
||||
chainConfig *lightClientConfig
|
||||
scheduler *request.Scheduler
|
||||
blockSync *beaconBlockSync
|
||||
engineRPC *rpc.Client
|
||||
|
||||
chainHeadSub event.Subscription
|
||||
engineClient *engineClient
|
||||
}
|
||||
|
||||
func NewClient(ctx *cli.Context) *Client {
|
||||
if !ctx.IsSet(utils.BeaconApiFlag.Name) {
|
||||
utils.Fatalf("Beacon node light client API URL not specified")
|
||||
}
|
||||
var (
|
||||
chainConfig = makeChainConfig(ctx)
|
||||
customHeader = make(map[string]string)
|
||||
)
|
||||
for _, s := range ctx.StringSlice(utils.BeaconApiHeaderFlag.Name) {
|
||||
kv := strings.Split(s, ":")
|
||||
if len(kv) != 2 {
|
||||
utils.Fatalf("Invalid custom API header entry: %s", s)
|
||||
}
|
||||
customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
||||
}
|
||||
|
||||
// create data structures
|
||||
var (
|
||||
db = memorydb.New()
|
||||
threshold = ctx.Int(utils.BeaconThresholdFlag.Name)
|
||||
committeeChain = light.NewCommitteeChain(db, chainConfig.ChainConfig, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name))
|
||||
headTracker = light.NewHeadTracker(committeeChain, threshold)
|
||||
)
|
||||
headSync := sync.NewHeadSync(headTracker, committeeChain)
|
||||
|
||||
// set up scheduler and sync modules
|
||||
scheduler := request.NewScheduler()
|
||||
checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint)
|
||||
forwardSync := sync.NewForwardUpdateSync(committeeChain)
|
||||
beaconBlockSync := newBeaconBlockSync(headTracker)
|
||||
scheduler.RegisterTarget(headTracker)
|
||||
scheduler.RegisterTarget(committeeChain)
|
||||
scheduler.RegisterModule(checkpointInit, "checkpointInit")
|
||||
scheduler.RegisterModule(forwardSync, "forwardSync")
|
||||
scheduler.RegisterModule(headSync, "headSync")
|
||||
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
|
||||
|
||||
return &Client{
|
||||
scheduler: scheduler,
|
||||
urls: ctx.StringSlice(utils.BeaconApiFlag.Name),
|
||||
customHeader: customHeader,
|
||||
chainConfig: &chainConfig,
|
||||
blockSync: beaconBlockSync,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetEngineRPC(engine *rpc.Client) {
|
||||
c.engineRPC = engine
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
headCh := make(chan types.ChainHeadEvent, 16)
|
||||
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
|
||||
c.engineClient = startEngineClient(c.chainConfig, c.engineRPC, headCh)
|
||||
|
||||
c.scheduler.Start()
|
||||
for _, url := range c.urls {
|
||||
beaconApi := api.NewBeaconLightApi(url, c.customHeader)
|
||||
c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
c.engineClient.stop()
|
||||
c.chainHeadSub.Unsubscribe()
|
||||
c.scheduler.Stop()
|
||||
return nil
|
||||
}
|
||||
116
beacon/blsync/config.go
Normal file
116
beacon/blsync/config.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blsync
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// lightClientConfig contains beacon light client configuration
|
||||
type lightClientConfig struct {
|
||||
*types.ChainConfig
|
||||
Checkpoint common.Hash
|
||||
}
|
||||
|
||||
var (
|
||||
MainnetConfig = lightClientConfig{
|
||||
ChainConfig: (&types.ChainConfig{
|
||||
GenesisValidatorsRoot: common.HexToHash("0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"),
|
||||
GenesisTime: 1606824023,
|
||||
}).
|
||||
AddFork("GENESIS", 0, []byte{0, 0, 0, 0}).
|
||||
AddFork("ALTAIR", 74240, []byte{1, 0, 0, 0}).
|
||||
AddFork("BELLATRIX", 144896, []byte{2, 0, 0, 0}).
|
||||
AddFork("CAPELLA", 194048, []byte{3, 0, 0, 0}).
|
||||
AddFork("DENEB", 269568, []byte{4, 0, 0, 0}),
|
||||
Checkpoint: common.HexToHash("0x388be41594ec7d6a6894f18c73f3469f07e2c19a803de4755d335817ed8e2e5a"),
|
||||
}
|
||||
|
||||
SepoliaConfig = lightClientConfig{
|
||||
ChainConfig: (&types.ChainConfig{
|
||||
GenesisValidatorsRoot: common.HexToHash("0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"),
|
||||
GenesisTime: 1655733600,
|
||||
}).
|
||||
AddFork("GENESIS", 0, []byte{144, 0, 0, 105}).
|
||||
AddFork("ALTAIR", 50, []byte{144, 0, 0, 112}).
|
||||
AddFork("BELLATRIX", 100, []byte{144, 0, 0, 113}).
|
||||
AddFork("CAPELLA", 56832, []byte{144, 0, 0, 114}).
|
||||
AddFork("DENEB", 132608, []byte{144, 0, 0, 115}),
|
||||
Checkpoint: common.HexToHash("0x1005a6d9175e96bfbce4d35b80f468e9bff0b674e1e861d16e09e10005a58e81"),
|
||||
}
|
||||
|
||||
GoerliConfig = lightClientConfig{
|
||||
ChainConfig: (&types.ChainConfig{
|
||||
GenesisValidatorsRoot: common.HexToHash("0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"),
|
||||
GenesisTime: 1614588812,
|
||||
}).
|
||||
AddFork("GENESIS", 0, []byte{0, 0, 16, 32}).
|
||||
AddFork("ALTAIR", 36660, []byte{1, 0, 16, 32}).
|
||||
AddFork("BELLATRIX", 112260, []byte{2, 0, 16, 32}).
|
||||
AddFork("CAPELLA", 162304, []byte{3, 0, 16, 32}).
|
||||
AddFork("DENEB", 231680, []byte{4, 0, 16, 32}),
|
||||
Checkpoint: common.HexToHash("0x53a0f4f0a378e2c4ae0a9ee97407eb69d0d737d8d8cd0a5fb1093f42f7b81c49"),
|
||||
}
|
||||
)
|
||||
|
||||
func makeChainConfig(ctx *cli.Context) lightClientConfig {
|
||||
utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag)
|
||||
customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name) || ctx.IsSet(utils.BeaconGenesisRootFlag.Name) || ctx.IsSet(utils.BeaconGenesisTimeFlag.Name)
|
||||
var config lightClientConfig
|
||||
switch {
|
||||
case ctx.Bool(utils.MainnetFlag.Name):
|
||||
config = MainnetConfig
|
||||
case ctx.Bool(utils.SepoliaFlag.Name):
|
||||
config = SepoliaConfig
|
||||
case ctx.Bool(utils.GoerliFlag.Name):
|
||||
config = GoerliConfig
|
||||
default:
|
||||
if !customConfig {
|
||||
config = MainnetConfig
|
||||
}
|
||||
}
|
||||
if customConfig && config.Forks != nil {
|
||||
utils.Fatalf("Cannot use custom beacon chain config flags in combination with pre-defined network config")
|
||||
}
|
||||
if ctx.IsSet(utils.BeaconGenesisRootFlag.Name) {
|
||||
if c, err := hexutil.Decode(ctx.String(utils.BeaconGenesisRootFlag.Name)); err == nil && len(c) <= 32 {
|
||||
copy(config.GenesisValidatorsRoot[:len(c)], c)
|
||||
} else {
|
||||
utils.Fatalf("Invalid hex string", "beacon.genesis.gvroot", ctx.String(utils.BeaconGenesisRootFlag.Name), "error", err)
|
||||
}
|
||||
}
|
||||
if ctx.IsSet(utils.BeaconGenesisTimeFlag.Name) {
|
||||
config.GenesisTime = ctx.Uint64(utils.BeaconGenesisTimeFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(utils.BeaconConfigFlag.Name) {
|
||||
if err := config.ChainConfig.LoadForks(ctx.String(utils.BeaconConfigFlag.Name)); err != nil {
|
||||
utils.Fatalf("Could not load beacon chain config file", "file name", ctx.String(utils.BeaconConfigFlag.Name), "error", err)
|
||||
}
|
||||
}
|
||||
if ctx.IsSet(utils.BeaconCheckpointFlag.Name) {
|
||||
if c, err := hexutil.Decode(ctx.String(utils.BeaconCheckpointFlag.Name)); err == nil && len(c) <= 32 {
|
||||
copy(config.Checkpoint[:len(c)], c)
|
||||
} else {
|
||||
utils.Fatalf("Invalid hex string", "beacon.checkpoint", ctx.String(utils.BeaconCheckpointFlag.Name), "error", err)
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
147
beacon/blsync/engineclient.go
Normal file
147
beacon/blsync/engineclient.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package blsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/engine"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ctypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
)
|
||||
|
||||
type engineClient struct {
|
||||
config *lightClientConfig
|
||||
rpc *rpc.Client
|
||||
rootCtx context.Context
|
||||
cancelRoot context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func startEngineClient(config *lightClientConfig, rpc *rpc.Client, headCh <-chan types.ChainHeadEvent) *engineClient {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ec := &engineClient{
|
||||
config: config,
|
||||
rpc: rpc,
|
||||
rootCtx: ctx,
|
||||
cancelRoot: cancel,
|
||||
}
|
||||
ec.wg.Add(1)
|
||||
go ec.updateLoop(headCh)
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *engineClient) stop() {
|
||||
ec.cancelRoot()
|
||||
ec.wg.Wait()
|
||||
}
|
||||
|
||||
func (ec *engineClient) updateLoop(headCh <-chan types.ChainHeadEvent) {
|
||||
defer ec.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ec.rootCtx.Done():
|
||||
return
|
||||
|
||||
case event := <-headCh:
|
||||
if ec.rpc == nil { // dry run, no engine API specified
|
||||
log.Info("New execution block retrieved", "number", event.Block.NumberU64(), "hash", event.Block.Hash(), "finalized", event.Finalized)
|
||||
continue
|
||||
}
|
||||
|
||||
fork := ec.config.ForkAtEpoch(event.BeaconHead.Epoch())
|
||||
forkName := strings.ToLower(fork.Name)
|
||||
|
||||
if status, err := ec.callNewPayload(forkName, event); err == nil {
|
||||
log.Info("Successful NewPayload", "number", event.Block.NumberU64(), "hash", event.Block.Hash(), "status", status)
|
||||
} else {
|
||||
log.Error("Failed NewPayload", "number", event.Block.NumberU64(), "hash", event.Block.Hash(), "error", err)
|
||||
}
|
||||
|
||||
if status, err := ec.callForkchoiceUpdated(forkName, event); err == nil {
|
||||
log.Info("Successful ForkchoiceUpdated", "head", event.Block.Hash(), "status", status)
|
||||
} else {
|
||||
log.Error("Failed ForkchoiceUpdated", "head", event.Block.Hash(), "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ec *engineClient) callNewPayload(fork string, event types.ChainHeadEvent) (string, error) {
|
||||
execData := engine.BlockToExecutableData(event.Block, nil, nil).ExecutionPayload
|
||||
|
||||
var (
|
||||
method string
|
||||
params = []any{execData}
|
||||
)
|
||||
switch fork {
|
||||
case "deneb":
|
||||
method = "engine_newPayloadV3"
|
||||
parentBeaconRoot := event.BeaconHead.ParentRoot
|
||||
blobHashes := collectBlobHashes(event.Block)
|
||||
params = append(params, blobHashes, parentBeaconRoot)
|
||||
case "capella":
|
||||
method = "engine_newPayloadV2"
|
||||
default:
|
||||
method = "engine_newPayloadV1"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ec.rootCtx, time.Second*5)
|
||||
defer cancel()
|
||||
var resp engine.PayloadStatusV1
|
||||
err := ec.rpc.CallContext(ctx, &resp, method, params...)
|
||||
return resp.Status, err
|
||||
}
|
||||
|
||||
func collectBlobHashes(b *ctypes.Block) []common.Hash {
|
||||
list := make([]common.Hash, 0)
|
||||
for _, tx := range b.Transactions() {
|
||||
list = append(list, tx.BlobHashes()...)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (ec *engineClient) callForkchoiceUpdated(fork string, event types.ChainHeadEvent) (string, error) {
|
||||
update := engine.ForkchoiceStateV1{
|
||||
HeadBlockHash: event.Block.Hash(),
|
||||
SafeBlockHash: event.Finalized,
|
||||
FinalizedBlockHash: event.Finalized,
|
||||
}
|
||||
|
||||
var method string
|
||||
switch fork {
|
||||
case "deneb":
|
||||
method = "engine_forkchoiceUpdatedV3"
|
||||
case "capella":
|
||||
method = "engine_forkchoiceUpdatedV2"
|
||||
default:
|
||||
method = "engine_forkchoiceUpdatedV1"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ec.rootCtx, time.Second*5)
|
||||
defer cancel()
|
||||
var resp engine.ForkChoiceResponse
|
||||
err := ec.rpc.CallContext(ctx, &resp, method, update, nil)
|
||||
return resp.PayloadStatus.Status, err
|
||||
}
|
||||
103
beacon/light/api/api_server.go
Executable file
103
beacon/light/api/api_server.go
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/sync"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ApiServer is a wrapper around BeaconLightApi that implements request.requestServer.
|
||||
type ApiServer struct {
|
||||
api *BeaconLightApi
|
||||
eventCallback func(event request.Event)
|
||||
unsubscribe func()
|
||||
}
|
||||
|
||||
// NewApiServer creates a new ApiServer.
|
||||
func NewApiServer(api *BeaconLightApi) *ApiServer {
|
||||
return &ApiServer{api: api}
|
||||
}
|
||||
|
||||
// Subscribe implements request.requestServer.
|
||||
func (s *ApiServer) Subscribe(eventCallback func(event request.Event)) {
|
||||
s.eventCallback = eventCallback
|
||||
listener := HeadEventListener{
|
||||
OnNewHead: func(slot uint64, blockRoot common.Hash) {
|
||||
log.Debug("New head received", "slot", slot, "blockRoot", blockRoot)
|
||||
eventCallback(request.Event{Type: sync.EvNewHead, Data: types.HeadInfo{Slot: slot, BlockRoot: blockRoot}})
|
||||
},
|
||||
OnSignedHead: func(head types.SignedHeader) {
|
||||
log.Debug("New signed head received", "slot", head.Header.Slot, "blockRoot", head.Header.Hash(), "signerCount", head.Signature.SignerCount())
|
||||
eventCallback(request.Event{Type: sync.EvNewSignedHead, Data: head})
|
||||
},
|
||||
OnFinality: func(head types.FinalityUpdate) {
|
||||
log.Debug("New finality update received", "slot", head.Attested.Slot, "blockRoot", head.Attested.Hash(), "signerCount", head.Signature.SignerCount())
|
||||
eventCallback(request.Event{Type: sync.EvNewFinalityUpdate, Data: head})
|
||||
},
|
||||
OnError: func(err error) {
|
||||
log.Warn("Head event stream error", "err", err)
|
||||
},
|
||||
}
|
||||
s.unsubscribe = s.api.StartHeadListener(listener)
|
||||
}
|
||||
|
||||
// SendRequest implements request.requestServer.
|
||||
func (s *ApiServer) SendRequest(id request.ID, req request.Request) {
|
||||
go func() {
|
||||
var resp request.Response
|
||||
var err error
|
||||
switch data := req.(type) {
|
||||
case sync.ReqUpdates:
|
||||
log.Debug("Beacon API: requesting light client update", "reqid", id, "period", data.FirstPeriod, "count", data.Count)
|
||||
var r sync.RespUpdates
|
||||
r.Updates, r.Committees, err = s.api.GetBestUpdatesAndCommittees(data.FirstPeriod, data.Count)
|
||||
resp = r
|
||||
case sync.ReqHeader:
|
||||
log.Debug("Beacon API: requesting header", "reqid", id, "hash", common.Hash(data))
|
||||
resp, err = s.api.GetHeader(common.Hash(data))
|
||||
case sync.ReqCheckpointData:
|
||||
log.Debug("Beacon API: requesting checkpoint data", "reqid", id, "hash", common.Hash(data))
|
||||
resp, err = s.api.GetCheckpointData(common.Hash(data))
|
||||
case sync.ReqBeaconBlock:
|
||||
log.Debug("Beacon API: requesting block", "reqid", id, "hash", common.Hash(data))
|
||||
resp, err = s.api.GetBeaconBlock(common.Hash(data))
|
||||
default:
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warn("Beacon API request failed", "type", reflect.TypeOf(req), "reqid", id, "err", err)
|
||||
s.eventCallback(request.Event{Type: request.EvFail, Data: request.RequestResponse{ID: id, Request: req}})
|
||||
} else {
|
||||
s.eventCallback(request.Event{Type: request.EvResponse, Data: request.RequestResponse{ID: id, Request: req, Response: resp}})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Unsubscribe implements request.requestServer.
|
||||
// Note: Unsubscribe should not be called concurrently with Subscribe.
|
||||
func (s *ApiServer) Unsubscribe() {
|
||||
if s.unsubscribe != nil {
|
||||
s.unsubscribe()
|
||||
s.unsubscribe = nil
|
||||
}
|
||||
}
|
||||
507
beacon/light/api/light_api.go
Executable file
507
beacon/light/api/light_api.go
Executable file
|
|
@ -0,0 +1,507 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more detaiapi.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/donovanhide/eventsource"
|
||||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("404 Not Found")
|
||||
ErrInternal = errors.New("500 Internal Server Error")
|
||||
)
|
||||
|
||||
type CommitteeUpdate struct {
|
||||
Version string
|
||||
Update types.LightClientUpdate
|
||||
NextSyncCommittee types.SerializedSyncCommittee
|
||||
}
|
||||
|
||||
// See data structure definition here:
|
||||
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate
|
||||
type committeeUpdateJson struct {
|
||||
Version string `json:"version"`
|
||||
Data committeeUpdateData `json:"data"`
|
||||
}
|
||||
|
||||
type committeeUpdateData struct {
|
||||
Header jsonBeaconHeader `json:"attested_header"`
|
||||
NextSyncCommittee types.SerializedSyncCommittee `json:"next_sync_committee"`
|
||||
NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"`
|
||||
FinalizedHeader *jsonBeaconHeader `json:"finalized_header,omitempty"`
|
||||
FinalityBranch merkle.Values `json:"finality_branch,omitempty"`
|
||||
SyncAggregate types.SyncAggregate `json:"sync_aggregate"`
|
||||
SignatureSlot common.Decimal `json:"signature_slot"`
|
||||
}
|
||||
|
||||
type jsonBeaconHeader struct {
|
||||
Beacon types.Header `json:"beacon"`
|
||||
}
|
||||
|
||||
type jsonHeaderWithExecProof struct {
|
||||
Beacon types.Header `json:"beacon"`
|
||||
Execution json.RawMessage `json:"execution"`
|
||||
ExecutionBranch merkle.Values `json:"execution_branch"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals from JSON.
|
||||
func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error {
|
||||
var dec committeeUpdateJson
|
||||
if err := json.Unmarshal(input, &dec); err != nil {
|
||||
return err
|
||||
}
|
||||
u.Version = dec.Version
|
||||
u.NextSyncCommittee = dec.Data.NextSyncCommittee
|
||||
u.Update = types.LightClientUpdate{
|
||||
AttestedHeader: types.SignedHeader{
|
||||
Header: dec.Data.Header.Beacon,
|
||||
Signature: dec.Data.SyncAggregate,
|
||||
SignatureSlot: uint64(dec.Data.SignatureSlot),
|
||||
},
|
||||
NextSyncCommitteeRoot: u.NextSyncCommittee.Root(),
|
||||
NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch,
|
||||
FinalityBranch: dec.Data.FinalityBranch,
|
||||
}
|
||||
if dec.Data.FinalizedHeader != nil {
|
||||
u.Update.FinalizedHeader = &dec.Data.FinalizedHeader.Beacon
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetcher is an interface useful for debug-harnessing the http api.
|
||||
type fetcher interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// BeaconLightApi requests light client information from a beacon node REST API.
|
||||
// Note: all required API endpoints are currently only implemented by Lodestar.
|
||||
type BeaconLightApi struct {
|
||||
url string
|
||||
client fetcher
|
||||
customHeaders map[string]string
|
||||
}
|
||||
|
||||
func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi {
|
||||
return &BeaconLightApi{
|
||||
url: url,
|
||||
client: &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
},
|
||||
customHeaders: customHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *BeaconLightApi) httpGet(path string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", api.url+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range api.customHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := api.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
return io.ReadAll(resp.Body)
|
||||
case 404:
|
||||
return nil, ErrNotFound
|
||||
case 500:
|
||||
return nil, ErrInternal
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected error from API endpoint \"%s\": status code %d", path, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *BeaconLightApi) httpGetf(format string, params ...any) ([]byte, error) {
|
||||
return api.httpGet(fmt.Sprintf(format, params...))
|
||||
}
|
||||
|
||||
// GetBestUpdateAndCommittee fetches and validates LightClientUpdate for given
|
||||
// period and full serialized committee for the next period (committee root hash
|
||||
// equals update.NextSyncCommitteeRoot).
|
||||
// Note that the results are validated but the update signature should be verified
|
||||
// by the caller as its validity depends on the update chain.
|
||||
func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedSyncCommittee, error) {
|
||||
resp, err := api.httpGetf("/eth/v1/beacon/light_client/updates?start_period=%d&count=%d", firstPeriod, count)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var data []CommitteeUpdate
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(data) != int(count) {
|
||||
return nil, nil, errors.New("invalid number of committee updates")
|
||||
}
|
||||
updates := make([]*types.LightClientUpdate, int(count))
|
||||
committees := make([]*types.SerializedSyncCommittee, int(count))
|
||||
for i, d := range data {
|
||||
if d.Update.AttestedHeader.Header.SyncPeriod() != firstPeriod+uint64(i) {
|
||||
return nil, nil, errors.New("wrong committee update header period")
|
||||
}
|
||||
if err := d.Update.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if d.NextSyncCommittee.Root() != d.Update.NextSyncCommitteeRoot {
|
||||
return nil, nil, errors.New("wrong sync committee root")
|
||||
}
|
||||
updates[i], committees[i] = new(types.LightClientUpdate), new(types.SerializedSyncCommittee)
|
||||
*updates[i], *committees[i] = d.Update, d.NextSyncCommittee
|
||||
}
|
||||
return updates, committees, nil
|
||||
}
|
||||
|
||||
// GetOptimisticHeadUpdate fetches a signed header based on the latest available
|
||||
// optimistic update. Note that the signature should be verified by the caller
|
||||
// as its validity depends on the update chain.
|
||||
//
|
||||
// See data structure definition here:
|
||||
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
|
||||
func (api *BeaconLightApi) GetOptimisticHeadUpdate() (types.SignedHeader, error) {
|
||||
resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update")
|
||||
if err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
return decodeOptimisticHeadUpdate(resp)
|
||||
}
|
||||
|
||||
func decodeOptimisticHeadUpdate(enc []byte) (types.SignedHeader, error) {
|
||||
var data struct {
|
||||
Data struct {
|
||||
Header jsonBeaconHeader `json:"attested_header"`
|
||||
Aggregate types.SyncAggregate `json:"sync_aggregate"`
|
||||
SignatureSlot common.Decimal `json:"signature_slot"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(enc, &data); err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
if data.Data.Header.Beacon.StateRoot == (common.Hash{}) {
|
||||
// workaround for different event encoding format in Lodestar
|
||||
if err := json.Unmarshal(enc, &data.Data); err != nil {
|
||||
return types.SignedHeader{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
|
||||
return types.SignedHeader{}, errors.New("invalid sync_committee_bits length")
|
||||
}
|
||||
if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
|
||||
return types.SignedHeader{}, errors.New("invalid sync_committee_signature length")
|
||||
}
|
||||
return types.SignedHeader{
|
||||
Header: data.Data.Header.Beacon,
|
||||
Signature: data.Data.Aggregate,
|
||||
SignatureSlot: uint64(data.Data.SignatureSlot),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFinalityUpdate fetches the latest available finality update.
|
||||
//
|
||||
// See data structure definition here:
|
||||
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate
|
||||
func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) {
|
||||
resp, err := api.httpGet("/eth/v1/beacon/light_client/finality_update")
|
||||
if err != nil {
|
||||
return types.FinalityUpdate{}, err
|
||||
}
|
||||
return decodeFinalityUpdate(resp)
|
||||
}
|
||||
|
||||
func decodeFinalityUpdate(enc []byte) (types.FinalityUpdate, error) {
|
||||
var data struct {
|
||||
Version string
|
||||
Data struct {
|
||||
Attested jsonHeaderWithExecProof `json:"attested_header"`
|
||||
Finalized jsonHeaderWithExecProof `json:"finalized_header"`
|
||||
FinalityBranch merkle.Values `json:"finality_branch"`
|
||||
Aggregate types.SyncAggregate `json:"sync_aggregate"`
|
||||
SignatureSlot common.Decimal `json:"signature_slot"`
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(enc, &data); err != nil {
|
||||
return types.FinalityUpdate{}, err
|
||||
}
|
||||
// Decode the execution payload headers.
|
||||
attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution)
|
||||
if err != nil {
|
||||
return types.FinalityUpdate{}, fmt.Errorf("invalid attested header: %v", err)
|
||||
}
|
||||
finalizedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Finalized.Execution)
|
||||
if err != nil {
|
||||
return types.FinalityUpdate{}, fmt.Errorf("invalid finalized header: %v", err)
|
||||
}
|
||||
// Perform sanity checks.
|
||||
if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize {
|
||||
return types.FinalityUpdate{}, errors.New("invalid sync_committee_bits length")
|
||||
}
|
||||
if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize {
|
||||
return types.FinalityUpdate{}, errors.New("invalid sync_committee_signature length")
|
||||
}
|
||||
|
||||
return types.FinalityUpdate{
|
||||
Attested: types.HeaderWithExecProof{
|
||||
Header: data.Data.Attested.Beacon,
|
||||
PayloadHeader: attestedExecHeader,
|
||||
PayloadBranch: data.Data.Attested.ExecutionBranch,
|
||||
},
|
||||
Finalized: types.HeaderWithExecProof{
|
||||
Header: data.Data.Finalized.Beacon,
|
||||
PayloadHeader: finalizedExecHeader,
|
||||
PayloadBranch: data.Data.Finalized.ExecutionBranch,
|
||||
},
|
||||
FinalityBranch: data.Data.FinalityBranch,
|
||||
Signature: data.Data.Aggregate,
|
||||
SignatureSlot: uint64(data.Data.SignatureSlot),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetHead fetches and validates the beacon header with the given blockRoot.
|
||||
// If blockRoot is null hash then the latest head header is fetched.
|
||||
func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, error) {
|
||||
var blockId string
|
||||
if blockRoot == (common.Hash{}) {
|
||||
blockId = "head"
|
||||
} else {
|
||||
blockId = blockRoot.Hex()
|
||||
}
|
||||
resp, err := api.httpGetf("/eth/v1/beacon/headers/%s", blockId)
|
||||
if err != nil {
|
||||
return types.Header{}, err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Data struct {
|
||||
Root common.Hash `json:"root"`
|
||||
Canonical bool `json:"canonical"`
|
||||
Header struct {
|
||||
Message types.Header `json:"message"`
|
||||
Signature hexutil.Bytes `json:"signature"`
|
||||
} `json:"header"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return types.Header{}, err
|
||||
}
|
||||
header := data.Data.Header.Message
|
||||
if blockRoot == (common.Hash{}) {
|
||||
blockRoot = data.Data.Root
|
||||
}
|
||||
if header.Hash() != blockRoot {
|
||||
return types.Header{}, errors.New("retrieved beacon header root does not match")
|
||||
}
|
||||
return header, nil
|
||||
}
|
||||
|
||||
// GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint.
|
||||
func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types.BootstrapData, error) {
|
||||
resp, err := api.httpGetf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See data structure definition here:
|
||||
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap
|
||||
type bootstrapData struct {
|
||||
Data struct {
|
||||
Header jsonBeaconHeader `json:"header"`
|
||||
Committee *types.SerializedSyncCommittee `json:"current_sync_committee"`
|
||||
CommitteeBranch merkle.Values `json:"current_sync_committee_branch"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
var data bootstrapData
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.Data.Committee == nil {
|
||||
return nil, errors.New("sync committee is missing")
|
||||
}
|
||||
header := data.Data.Header.Beacon
|
||||
if header.Hash() != checkpointHash {
|
||||
return nil, fmt.Errorf("invalid checkpoint block header, have %v want %v", header.Hash(), checkpointHash)
|
||||
}
|
||||
checkpoint := &types.BootstrapData{
|
||||
Header: header,
|
||||
CommitteeBranch: data.Data.CommitteeBranch,
|
||||
CommitteeRoot: data.Data.Committee.Root(),
|
||||
Committee: data.Data.Committee,
|
||||
}
|
||||
if err := checkpoint.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid checkpoint: %w", err)
|
||||
}
|
||||
if checkpoint.Header.Hash() != checkpointHash {
|
||||
return nil, errors.New("wrong checkpoint hash")
|
||||
}
|
||||
return checkpoint, nil
|
||||
}
|
||||
|
||||
func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconBlock, error) {
|
||||
resp, err := api.httpGetf("/eth/v2/beacon/blocks/0x%x", blockRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var beaconBlockMessage struct {
|
||||
Version string
|
||||
Data struct {
|
||||
Message json.RawMessage `json:"message"`
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil {
|
||||
return nil, fmt.Errorf("invalid block json data: %v", err)
|
||||
}
|
||||
block, err := types.BlockFromJSON(beaconBlockMessage.Version, beaconBlockMessage.Data.Message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
computedRoot := block.Root()
|
||||
if computedRoot != blockRoot {
|
||||
return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot)
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) {
|
||||
var data struct {
|
||||
Slot common.Decimal `json:"slot"`
|
||||
Block common.Hash `json:"block"`
|
||||
}
|
||||
if err := json.Unmarshal(enc, &data); err != nil {
|
||||
return 0, common.Hash{}, err
|
||||
}
|
||||
return uint64(data.Slot), data.Block, nil
|
||||
}
|
||||
|
||||
type HeadEventListener struct {
|
||||
OnNewHead func(slot uint64, blockRoot common.Hash)
|
||||
OnSignedHead func(head types.SignedHeader)
|
||||
OnFinality func(head types.FinalityUpdate)
|
||||
OnError func(err error)
|
||||
}
|
||||
|
||||
// StartHeadListener creates an event subscription for heads and signed (optimistic)
|
||||
// head updates and calls the specified callback functions when they are received.
|
||||
// The callbacks are also called for the current head and optimistic head at startup.
|
||||
// They are never called concurrently.
|
||||
func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func() {
|
||||
closeCh := make(chan struct{}) // initiate closing the stream
|
||||
closedCh := make(chan struct{}) // stream closed (or failed to create)
|
||||
stoppedCh := make(chan struct{}) // sync loop stopped
|
||||
streamCh := make(chan *eventsource.Stream, 1)
|
||||
go func() {
|
||||
defer close(closedCh)
|
||||
// when connected to a Lodestar node the subscription blocks until the
|
||||
// first actual event arrives; therefore we create the subscription in
|
||||
// a separate goroutine while letting the main goroutine sync up to the
|
||||
// current head
|
||||
req, err := http.NewRequest("GET", api.url+
|
||||
"/eth/v1/events?topics=head&topics=light_client_optimistic_update&topics=light_client_finality_update", nil)
|
||||
if err != nil {
|
||||
listener.OnError(fmt.Errorf("error creating event subscription request: %v", err))
|
||||
return
|
||||
}
|
||||
for k, v := range api.customHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
stream, err := eventsource.SubscribeWithRequest("", req)
|
||||
if err != nil {
|
||||
listener.OnError(fmt.Errorf("error creating event subscription: %v", err))
|
||||
close(streamCh)
|
||||
return
|
||||
}
|
||||
streamCh <- stream
|
||||
<-closeCh
|
||||
stream.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer close(stoppedCh)
|
||||
|
||||
if head, err := api.GetHeader(common.Hash{}); err == nil {
|
||||
listener.OnNewHead(head.Slot, head.Hash())
|
||||
}
|
||||
if signedHead, err := api.GetOptimisticHeadUpdate(); err == nil {
|
||||
listener.OnSignedHead(signedHead)
|
||||
}
|
||||
if finalityUpdate, err := api.GetFinalityUpdate(); err == nil {
|
||||
listener.OnFinality(finalityUpdate)
|
||||
}
|
||||
stream := <-streamCh
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-stream.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch event.Event() {
|
||||
case "head":
|
||||
if slot, blockRoot, err := decodeHeadEvent([]byte(event.Data())); err == nil {
|
||||
listener.OnNewHead(slot, blockRoot)
|
||||
} else {
|
||||
listener.OnError(fmt.Errorf("error decoding head event: %v", err))
|
||||
}
|
||||
case "light_client_optimistic_update":
|
||||
if signedHead, err := decodeOptimisticHeadUpdate([]byte(event.Data())); err == nil {
|
||||
listener.OnSignedHead(signedHead)
|
||||
} else {
|
||||
listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err))
|
||||
}
|
||||
case "light_client_finality_update":
|
||||
if finalityUpdate, err := decodeFinalityUpdate([]byte(event.Data())); err == nil {
|
||||
listener.OnFinality(finalityUpdate)
|
||||
} else {
|
||||
listener.OnError(fmt.Errorf("error decoding finality update event: %v", err))
|
||||
}
|
||||
default:
|
||||
listener.OnError(fmt.Errorf("unexpected event: %s", event.Event()))
|
||||
}
|
||||
case err, ok := <-stream.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listener.OnError(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
close(closeCh)
|
||||
<-closedCh
|
||||
<-stoppedCh
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ type CommitteeChain struct {
|
|||
committees *canonicalStore[*types.SerializedSyncCommittee]
|
||||
fixedCommitteeRoots *canonicalStore[common.Hash]
|
||||
committeeCache *lru.Cache[uint64, syncCommittee] // cache deserialized committees
|
||||
changeCounter uint64
|
||||
|
||||
clock mclock.Clock // monotonic clock (simulated clock in tests)
|
||||
unixNano func() int64 // system clock (simulated clock in tests)
|
||||
|
|
@ -86,6 +87,11 @@ func NewCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signer
|
|||
return newCommitteeChain(db, config, signerThreshold, enforceTime, blsVerifier{}, &mclock.System{}, func() int64 { return time.Now().UnixNano() })
|
||||
}
|
||||
|
||||
// NewTestCommitteeChain creates a new CommitteeChain for testing.
|
||||
func NewTestCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, clock *mclock.Simulated) *CommitteeChain {
|
||||
return newCommitteeChain(db, config, signerThreshold, enforceTime, dummyVerifier{}, clock, func() int64 { return int64(clock.Now()) })
|
||||
}
|
||||
|
||||
// newCommitteeChain creates a new CommitteeChain with the option of replacing the
|
||||
// clock source and signature verification for testing purposes.
|
||||
func newCommitteeChain(db ethdb.KeyValueStore, config *types.ChainConfig, signerThreshold int, enforceTime bool, sigVerifier committeeSigVerifier, clock mclock.Clock, unixNano func() int64) *CommitteeChain {
|
||||
|
|
@ -181,20 +187,20 @@ func (s *CommitteeChain) Reset() {
|
|||
if err := s.rollback(0); err != nil {
|
||||
log.Error("Error writing batch into chain database", "error", err)
|
||||
}
|
||||
s.changeCounter++
|
||||
}
|
||||
|
||||
// CheckpointInit initializes a CommitteeChain based on the checkpoint.
|
||||
// CheckpointInit initializes a CommitteeChain based on a checkpoint.
|
||||
// Note: if the chain is already initialized and the committees proven by the
|
||||
// checkpoint do match the existing chain then the chain is retained and the
|
||||
// new checkpoint becomes fixed.
|
||||
func (s *CommitteeChain) CheckpointInit(bootstrap *types.BootstrapData) error {
|
||||
func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error {
|
||||
s.chainmu.Lock()
|
||||
defer s.chainmu.Unlock()
|
||||
|
||||
if err := bootstrap.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
period := bootstrap.Header.SyncPeriod()
|
||||
if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil {
|
||||
s.Reset()
|
||||
|
|
@ -215,6 +221,7 @@ func (s *CommitteeChain) CheckpointInit(bootstrap *types.BootstrapData) error {
|
|||
s.Reset()
|
||||
return err
|
||||
}
|
||||
s.changeCounter++
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +374,7 @@ func (s *CommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommi
|
|||
return ErrWrongCommitteeRoot
|
||||
}
|
||||
}
|
||||
s.changeCounter++
|
||||
if reorg {
|
||||
if err := s.rollback(period + 1); err != nil {
|
||||
return err
|
||||
|
|
@ -405,6 +413,13 @@ func (s *CommitteeChain) NextSyncPeriod() (uint64, bool) {
|
|||
return s.committees.periods.End - 1, true
|
||||
}
|
||||
|
||||
func (s *CommitteeChain) ChangeCounter() uint64 {
|
||||
s.chainmu.RLock()
|
||||
defer s.chainmu.RUnlock()
|
||||
|
||||
return s.changeCounter
|
||||
}
|
||||
|
||||
// rollback removes all committees and fixed roots from the given period and updates
|
||||
// starting from the previous period.
|
||||
func (s *CommitteeChain) rollback(period uint64) error {
|
||||
|
|
@ -452,12 +467,12 @@ func (s *CommitteeChain) getSyncCommittee(period uint64) (syncCommittee, error)
|
|||
if sc, ok := s.committees.get(s.db, period); ok {
|
||||
c, err := s.sigVerifier.deserializeSyncCommittee(sc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sync committee #%d deserialization error: %v", period, err)
|
||||
return nil, fmt.Errorf("sync committee #%d deserialization error: %v", period, err)
|
||||
}
|
||||
s.committeeCache.Add(period, c)
|
||||
return c, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Missing serialized sync committee #%d", period)
|
||||
return nil, fmt.Errorf("missing serialized sync committee #%d", period)
|
||||
}
|
||||
|
||||
// VerifySignedHeader returns true if the given signed header has a valid signature
|
||||
|
|
|
|||
|
|
@ -241,12 +241,12 @@ func newCommitteeChainTest(t *testing.T, config types.ChainConfig, signerThresho
|
|||
signerThreshold: signerThreshold,
|
||||
enforceTime: enforceTime,
|
||||
}
|
||||
c.chain = newCommitteeChain(c.db, &config, signerThreshold, enforceTime, dummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) })
|
||||
c.chain = NewTestCommitteeChain(c.db, &config, signerThreshold, enforceTime, c.clock)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) reloadChain() {
|
||||
c.chain = newCommitteeChain(c.db, &c.config, c.signerThreshold, c.enforceTime, dummyVerifier{}, c.clock, func() int64 { return int64(c.clock.Now()) })
|
||||
c.chain = NewTestCommitteeChain(c.db, &c.config, c.signerThreshold, c.enforceTime, c.clock)
|
||||
}
|
||||
|
||||
func (c *committeeChainTest) setClockPeriod(period float64) {
|
||||
|
|
|
|||
150
beacon/light/head_tracker.go
Normal file
150
beacon/light/head_tracker.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package light
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// HeadTracker keeps track of the latest validated head and the "prefetch" head
|
||||
// which is the (not necessarily validated) head announced by the majority of
|
||||
// servers.
|
||||
type HeadTracker struct {
|
||||
lock sync.RWMutex
|
||||
committeeChain *CommitteeChain
|
||||
minSignerCount int
|
||||
signedHead types.SignedHeader
|
||||
hasSignedHead bool
|
||||
finalityUpdate types.FinalityUpdate
|
||||
hasFinalityUpdate bool
|
||||
prefetchHead types.HeadInfo
|
||||
changeCounter uint64
|
||||
}
|
||||
|
||||
// NewHeadTracker creates a new HeadTracker.
|
||||
func NewHeadTracker(committeeChain *CommitteeChain, minSignerCount int) *HeadTracker {
|
||||
return &HeadTracker{
|
||||
committeeChain: committeeChain,
|
||||
minSignerCount: minSignerCount,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatedHead returns the latest validated head.
|
||||
func (h *HeadTracker) ValidatedHead() (types.SignedHeader, bool) {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
|
||||
return h.signedHead, h.hasSignedHead
|
||||
}
|
||||
|
||||
// ValidatedHead returns the latest validated head.
|
||||
func (h *HeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
|
||||
return h.finalityUpdate, h.hasFinalityUpdate
|
||||
}
|
||||
|
||||
// Validate validates the given signed head. If the head is successfully validated
|
||||
// and it is better than the old validated head (higher slot or same slot and more
|
||||
// signers) then ValidatedHead is updated. The boolean return flag signals if
|
||||
// ValidatedHead has been changed.
|
||||
func (h *HeadTracker) ValidateHead(head types.SignedHeader) (bool, error) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
replace, err := h.validate(head, h.signedHead)
|
||||
if replace {
|
||||
h.signedHead, h.hasSignedHead = head, true
|
||||
h.changeCounter++
|
||||
}
|
||||
return replace, err
|
||||
}
|
||||
|
||||
func (h *HeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
replace, err := h.validate(update.SignedHeader(), h.finalityUpdate.SignedHeader())
|
||||
if replace {
|
||||
h.finalityUpdate, h.hasFinalityUpdate = update, true
|
||||
h.changeCounter++
|
||||
}
|
||||
return replace, err
|
||||
}
|
||||
|
||||
func (h *HeadTracker) validate(head, oldHead types.SignedHeader) (bool, error) {
|
||||
signerCount := head.Signature.SignerCount()
|
||||
if signerCount < h.minSignerCount {
|
||||
return false, errors.New("low signer count")
|
||||
}
|
||||
if head.Header.Slot < oldHead.Header.Slot || (head.Header.Slot == oldHead.Header.Slot && signerCount <= oldHead.Signature.SignerCount()) {
|
||||
return false, nil
|
||||
}
|
||||
sigOk, age, err := h.committeeChain.VerifySignedHeader(head)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if age < 0 {
|
||||
log.Warn("Future signed head received", "age", age)
|
||||
}
|
||||
if age > time.Minute*2 {
|
||||
log.Warn("Old signed head received", "age", age)
|
||||
}
|
||||
if !sigOk {
|
||||
return false, errors.New("invalid header signature")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PrefetchHead returns the latest known prefetch head's head info.
|
||||
// This head can be used to start fetching related data hoping that it will be
|
||||
// validated soon.
|
||||
// Note that the prefetch head cannot be validated cryptographically so it should
|
||||
// only be used as a performance optimization hint.
|
||||
func (h *HeadTracker) PrefetchHead() types.HeadInfo {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
|
||||
return h.prefetchHead
|
||||
}
|
||||
|
||||
// SetPrefetchHead sets the prefetch head info.
|
||||
// Note that HeadTracker does not verify the prefetch head, just acts as a thread
|
||||
// safe bulletin board.
|
||||
func (h *HeadTracker) SetPrefetchHead(head types.HeadInfo) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
if head == h.prefetchHead {
|
||||
return
|
||||
}
|
||||
h.prefetchHead = head
|
||||
h.changeCounter++
|
||||
}
|
||||
|
||||
func (h *HeadTracker) ChangeCounter() uint64 {
|
||||
h.lock.RLock()
|
||||
defer h.lock.RUnlock()
|
||||
|
||||
return h.changeCounter
|
||||
}
|
||||
401
beacon/light/request/scheduler.go
Normal file
401
beacon/light/request/scheduler.go
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// Module represents a mechanism which is typically responsible for downloading
|
||||
// and updating a passive data structure. It does not directly interact with the
|
||||
// servers. It can start requests using the Requester interface, maintain its
|
||||
// internal state by receiving and processing Events and update its target data
|
||||
// structure based on the obtained data.
|
||||
// It is the Scheduler's responsibility to feed events to the modules, call
|
||||
// Process as long as there might be something to process and then generate request
|
||||
// candidates using MakeRequest and start the best possible requests.
|
||||
// Modules are called by Scheduler whenever a global trigger is fired. All events
|
||||
// fire the trigger. Changing a target data structure also triggers a next
|
||||
// processing round as it could make further actions possible either by the same
|
||||
// or another Module.
|
||||
type Module interface {
|
||||
// Process is a non-blocking function responsible for starting requests,
|
||||
// processing events and updating the target data structures(s) and the
|
||||
// internal state of the module. Module state typically consists of information
|
||||
// about pending requests and registered servers.
|
||||
// Process is always called after an event is received or after a target data
|
||||
// structure has been changed.
|
||||
//
|
||||
// Note: Process functions of different modules are never called concurrently;
|
||||
// they are called by Scheduler in the same order of priority as they were
|
||||
// registered in.
|
||||
Process(Requester, []Event)
|
||||
}
|
||||
|
||||
// Requester allows Modules to obtain the list of momentarily available servers,
|
||||
// start new requests and report server failure when a response has been proven
|
||||
// to be invalid in the processing phase.
|
||||
// Note that all Requester functions should be safe to call from Module.Process.
|
||||
type Requester interface {
|
||||
CanSendTo() []Server
|
||||
Send(Server, Request) ID
|
||||
Fail(Server, string)
|
||||
}
|
||||
|
||||
// Scheduler is a modular network data retrieval framework that coordinates multiple
|
||||
// servers and retrieval mechanisms (modules). It implements a trigger mechanism
|
||||
// that calls the Process function of registered modules whenever either the state
|
||||
// of existing data structures or events coming from registered servers could
|
||||
// allow new operations.
|
||||
type Scheduler struct {
|
||||
lock sync.Mutex
|
||||
modules []Module // first has highest priority
|
||||
names map[Module]string
|
||||
servers map[server]struct{}
|
||||
targets map[targetData]uint64
|
||||
|
||||
requesterLock sync.RWMutex
|
||||
serverOrder []server
|
||||
pending map[ServerAndID]pendingRequest
|
||||
|
||||
// eventLock guards access to the events list. Note that eventLock can be
|
||||
// locked either while lock is locked or unlocked but lock cannot be locked
|
||||
// while eventLock is locked.
|
||||
eventLock sync.Mutex
|
||||
events []Event
|
||||
stopCh chan chan struct{}
|
||||
|
||||
triggerCh chan struct{} // restarts waiting sync loop
|
||||
// if trigger has already been fired then send to testWaitCh blocks until
|
||||
// the triggered processing round is finished
|
||||
testWaitCh chan struct{}
|
||||
}
|
||||
|
||||
type (
|
||||
// Server identifies a server without allowing any direct interaction.
|
||||
// Note: server interface is used by Scheduler and Tracker but not used by
|
||||
// the modules that do not interact with them directly.
|
||||
// In order to make module testing easier, Server interface is used in
|
||||
// events and modules.
|
||||
Server any
|
||||
Request any
|
||||
Response any
|
||||
ID uint64
|
||||
ServerAndID struct {
|
||||
Server Server
|
||||
ID ID
|
||||
}
|
||||
)
|
||||
|
||||
// targetData represents a registered target data structure that increases its
|
||||
// ChangeCounter whenever it has been changed.
|
||||
type targetData interface {
|
||||
ChangeCounter() uint64
|
||||
}
|
||||
|
||||
// pendingRequest keeps track of sent and not yet finalized requests and their
|
||||
// sender modules.
|
||||
type pendingRequest struct {
|
||||
request Request
|
||||
module Module
|
||||
}
|
||||
|
||||
// NewScheduler creates a new Scheduler.
|
||||
func NewScheduler() *Scheduler {
|
||||
s := &Scheduler{
|
||||
servers: make(map[server]struct{}),
|
||||
names: make(map[Module]string),
|
||||
pending: make(map[ServerAndID]pendingRequest),
|
||||
targets: make(map[targetData]uint64),
|
||||
stopCh: make(chan chan struct{}),
|
||||
// Note: testWaitCh should not have capacity in order to ensure
|
||||
// that after a trigger happens testWaitCh will block until the resulting
|
||||
// processing round has been finished
|
||||
triggerCh: make(chan struct{}, 1),
|
||||
testWaitCh: make(chan struct{}),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// RegisterTarget registers a target data structure, ensuring that any changes
|
||||
// made to it trigger a new round of Module.Process calls, giving a chance to
|
||||
// modules to react to the changes.
|
||||
func (s *Scheduler) RegisterTarget(t targetData) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.targets[t] = 0
|
||||
}
|
||||
|
||||
// RegisterModule registers a module. Should be called before starting the scheduler.
|
||||
// In each processing round the order of module processing depends on the order of
|
||||
// registration.
|
||||
func (s *Scheduler) RegisterModule(m Module, name string) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.modules = append(s.modules, m)
|
||||
s.names[m] = name
|
||||
}
|
||||
|
||||
// RegisterServer registers a new server.
|
||||
func (s *Scheduler) RegisterServer(server server) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.addEvent(Event{Type: EvRegistered, Server: server})
|
||||
server.subscribe(func(event Event) {
|
||||
event.Server = server
|
||||
s.addEvent(event)
|
||||
})
|
||||
}
|
||||
|
||||
// UnregisterServer removes a registered server.
|
||||
func (s *Scheduler) UnregisterServer(server server) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
server.unsubscribe()
|
||||
s.addEvent(Event{Type: EvUnregistered, Server: server})
|
||||
}
|
||||
|
||||
// Start starts the scheduler. It should be called after registering all modules
|
||||
// and before registering any servers.
|
||||
func (s *Scheduler) Start() {
|
||||
go s.syncLoop()
|
||||
}
|
||||
|
||||
// Stop stops the scheduler.
|
||||
func (s *Scheduler) Stop() {
|
||||
stop := make(chan struct{})
|
||||
s.stopCh <- stop
|
||||
<-stop
|
||||
s.lock.Lock()
|
||||
for server := range s.servers {
|
||||
server.unsubscribe()
|
||||
}
|
||||
s.servers = nil
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
// syncLoop is the main event loop responsible for event/data processing and
|
||||
// sending new requests.
|
||||
// A round of processing starts whenever the global trigger is fired. Triggers
|
||||
// fired during a processing round ensure that there is going to be a next round.
|
||||
func (s *Scheduler) syncLoop() {
|
||||
for {
|
||||
s.lock.Lock()
|
||||
s.processRound()
|
||||
s.lock.Unlock()
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case stop := <-s.stopCh:
|
||||
close(stop)
|
||||
return
|
||||
case <-s.triggerCh:
|
||||
break loop
|
||||
case <-s.testWaitCh:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// targetChanged returns true if a registered target data structure has been
|
||||
// changed since the last call to this function.
|
||||
func (s *Scheduler) targetChanged() (changed bool) {
|
||||
for target, counter := range s.targets {
|
||||
if newCounter := target.ChangeCounter(); newCounter != counter {
|
||||
s.targets[target] = newCounter
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// processRound runs an entire processing round. It calls the Process functions
|
||||
// of all modules, passing all relevant events and repeating Process calls as
|
||||
// long as any changes have been made to the registered target data structures.
|
||||
// Once all events have been processed and a stable state has been achieved,
|
||||
// requests are generated and sent if necessary and possible.
|
||||
func (s *Scheduler) processRound() {
|
||||
for {
|
||||
log.Trace("Processing modules")
|
||||
filteredEvents := s.filterEvents()
|
||||
for _, module := range s.modules {
|
||||
log.Trace("Processing module", "name", s.names[module], "events", len(filteredEvents[module]))
|
||||
module.Process(requester{s, module}, filteredEvents[module])
|
||||
}
|
||||
if !s.targetChanged() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger starts a new processing round. If fired during processing, it ensures
|
||||
// another full round of processing all modules.
|
||||
func (s *Scheduler) Trigger() {
|
||||
select {
|
||||
case s.triggerCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// addEvent adds an event to be processed in the next round. Note that it can be
|
||||
// called regardless of the state of the lock mutex, making it safe for use in
|
||||
// the server event callback.
|
||||
func (s *Scheduler) addEvent(event Event) {
|
||||
s.eventLock.Lock()
|
||||
s.events = append(s.events, event)
|
||||
s.eventLock.Unlock()
|
||||
s.Trigger()
|
||||
}
|
||||
|
||||
// filterEvent sorts each Event either as a request event or a server event,
|
||||
// depending on its type. Request events are also sorted in a map based on the
|
||||
// module that originally initiated the request. It also ensures that no events
|
||||
// related to a server are returned before EvRegistered or after EvUnregistered.
|
||||
// In case of an EvUnregistered server event it also closes all pending requests
|
||||
// to the given server by adding a failed request event (EvFail), ensuring that
|
||||
// all requests get finalized and thereby allowing the module logic to be safe
|
||||
// and simple.
|
||||
func (s *Scheduler) filterEvents() map[Module][]Event {
|
||||
s.eventLock.Lock()
|
||||
events := s.events
|
||||
s.events = nil
|
||||
s.eventLock.Unlock()
|
||||
|
||||
s.requesterLock.Lock()
|
||||
defer s.requesterLock.Unlock()
|
||||
|
||||
filteredEvents := make(map[Module][]Event)
|
||||
for _, event := range events {
|
||||
server := event.Server.(server)
|
||||
if _, ok := s.servers[server]; !ok && event.Type != EvRegistered {
|
||||
continue // before EvRegister or after EvUnregister, discard
|
||||
}
|
||||
|
||||
if event.IsRequestEvent() {
|
||||
sid, _, _ := event.RequestInfo()
|
||||
pending, ok := s.pending[sid]
|
||||
if !ok {
|
||||
continue // request already closed, ignore further events
|
||||
}
|
||||
if event.Type == EvResponse || event.Type == EvFail {
|
||||
delete(s.pending, sid) // final event, close pending request
|
||||
}
|
||||
filteredEvents[pending.module] = append(filteredEvents[pending.module], event)
|
||||
} else {
|
||||
switch event.Type {
|
||||
case EvRegistered:
|
||||
s.servers[server] = struct{}{}
|
||||
s.serverOrder = append(s.serverOrder, nil)
|
||||
copy(s.serverOrder[1:], s.serverOrder[:len(s.serverOrder)-1])
|
||||
s.serverOrder[0] = server
|
||||
case EvUnregistered:
|
||||
s.closePending(event.Server, filteredEvents)
|
||||
delete(s.servers, server)
|
||||
for i, srv := range s.serverOrder {
|
||||
if srv == server {
|
||||
copy(s.serverOrder[i:len(s.serverOrder)-1], s.serverOrder[i+1:])
|
||||
s.serverOrder = s.serverOrder[:len(s.serverOrder)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, module := range s.modules {
|
||||
filteredEvents[module] = append(filteredEvents[module], event)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredEvents
|
||||
}
|
||||
|
||||
// closePending closes all pending requests to the given server and adds an EvFail
|
||||
// event to properly finalize them
|
||||
func (s *Scheduler) closePending(server Server, filteredEvents map[Module][]Event) {
|
||||
for sid, pending := range s.pending {
|
||||
if sid.Server == server {
|
||||
filteredEvents[pending.module] = append(filteredEvents[pending.module], Event{
|
||||
Type: EvFail,
|
||||
Server: server,
|
||||
Data: RequestResponse{
|
||||
ID: sid.ID,
|
||||
Request: pending.request,
|
||||
},
|
||||
})
|
||||
delete(s.pending, sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// requester implements Requester. Note that while requester basically wraps
|
||||
// Scheduler (with the added information of the currently processed Module), all
|
||||
// functions are safe to call from Module.Process which is running while
|
||||
// the Scheduler.lock mutex is held.
|
||||
type requester struct {
|
||||
*Scheduler
|
||||
module Module
|
||||
}
|
||||
|
||||
// CanSendTo returns the list of currently available servers. It also returns
|
||||
// them in an order of least to most recently used, ensuring a round-robin usage
|
||||
// of suitable servers if the module always chooses the first suitable one.
|
||||
func (s requester) CanSendTo() []Server {
|
||||
s.requesterLock.RLock()
|
||||
defer s.requesterLock.RUnlock()
|
||||
|
||||
list := make([]Server, 0, len(s.serverOrder))
|
||||
for _, server := range s.serverOrder {
|
||||
if server.canRequestNow() {
|
||||
list = append(list, server)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Send sends a request and adds an entry to Scheduler.pending map, ensuring that
|
||||
// related request events will be delivered to the sender Module.
|
||||
func (s requester) Send(srv Server, req Request) ID {
|
||||
s.requesterLock.Lock()
|
||||
defer s.requesterLock.Unlock()
|
||||
|
||||
server := srv.(server)
|
||||
id := server.sendRequest(req)
|
||||
sid := ServerAndID{Server: srv, ID: id}
|
||||
s.pending[sid] = pendingRequest{request: req, module: s.module}
|
||||
for i, ss := range s.serverOrder {
|
||||
if ss == server {
|
||||
copy(s.serverOrder[i:len(s.serverOrder)-1], s.serverOrder[i+1:])
|
||||
s.serverOrder[len(s.serverOrder)-1] = server
|
||||
return id
|
||||
}
|
||||
}
|
||||
log.Error("Target server not found in ordered list of registered servers")
|
||||
return id
|
||||
}
|
||||
|
||||
// Fail should be called when a server delivers invalid or useless information.
|
||||
// Calling Fail disables the given server for a period that is initially short
|
||||
// but is exponentially growing if it happens frequently. This results in a
|
||||
// somewhat fault tolerant operation that avoids hammering servers with requests
|
||||
// that they cannot serve but still gives them a chance periodically.
|
||||
func (s requester) Fail(srv Server, desc string) {
|
||||
srv.(server).fail(desc)
|
||||
}
|
||||
122
beacon/light/request/scheduler_test.go
Normal file
122
beacon/light/request/scheduler_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventFilter(t *testing.T) {
|
||||
s := NewScheduler()
|
||||
module1 := &testModule{name: "module1"}
|
||||
module2 := &testModule{name: "module2"}
|
||||
s.RegisterModule(module1, "module1")
|
||||
s.RegisterModule(module2, "module2")
|
||||
s.Start()
|
||||
// startup process round without events
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, nil)
|
||||
module2.expProcess(t, nil)
|
||||
srv := &testServer{}
|
||||
// register server; both modules should receive server event
|
||||
s.RegisterServer(srv)
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, []Event{
|
||||
{Type: EvRegistered, Server: srv},
|
||||
})
|
||||
module2.expProcess(t, []Event{
|
||||
{Type: EvRegistered, Server: srv},
|
||||
})
|
||||
// let module1 send a request
|
||||
srv.canRequest = 1
|
||||
module1.sendReq = testRequest
|
||||
s.Trigger()
|
||||
// in first triggered round module1 sends the request, no events yet
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, nil)
|
||||
module2.expProcess(t, nil)
|
||||
// server emits EvTimeout; only module1 should receive it
|
||||
srv.eventCb(Event{Type: EvTimeout, Data: RequestResponse{ID: 1, Request: testRequest}})
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, []Event{
|
||||
{Type: EvTimeout, Server: srv, Data: RequestResponse{ID: 1, Request: testRequest}},
|
||||
})
|
||||
module2.expProcess(t, nil)
|
||||
// unregister server; both modules should receive server event
|
||||
s.UnregisterServer(srv)
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, []Event{
|
||||
// module1 should also receive EvFail on its pending request
|
||||
{Type: EvFail, Server: srv, Data: RequestResponse{ID: 1, Request: testRequest}},
|
||||
{Type: EvUnregistered, Server: srv},
|
||||
})
|
||||
module2.expProcess(t, []Event{
|
||||
{Type: EvUnregistered, Server: srv},
|
||||
})
|
||||
// response after server unregistered; should be discarded
|
||||
srv.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expProcess(t, nil)
|
||||
module2.expProcess(t, nil)
|
||||
// no more process rounds expected; shut down
|
||||
s.testWaitCh <- struct{}{}
|
||||
module1.expNoMoreProcess(t)
|
||||
module2.expNoMoreProcess(t)
|
||||
s.Stop()
|
||||
}
|
||||
|
||||
type testServer struct {
|
||||
eventCb func(Event)
|
||||
lastID ID
|
||||
canRequest int
|
||||
}
|
||||
|
||||
func (s *testServer) subscribe(eventCb func(Event)) {
|
||||
s.eventCb = eventCb
|
||||
}
|
||||
|
||||
func (s *testServer) canRequestNow() bool {
|
||||
return s.canRequest > 0
|
||||
}
|
||||
|
||||
func (s *testServer) sendRequest(req Request) ID {
|
||||
s.canRequest--
|
||||
s.lastID++
|
||||
return s.lastID
|
||||
}
|
||||
|
||||
func (s *testServer) fail(string) {}
|
||||
func (s *testServer) unsubscribe() {}
|
||||
|
||||
type testModule struct {
|
||||
name string
|
||||
processed [][]Event
|
||||
sendReq Request
|
||||
}
|
||||
|
||||
func (m *testModule) Process(requester Requester, events []Event) {
|
||||
m.processed = append(m.processed, events)
|
||||
if m.sendReq != nil {
|
||||
if cs := requester.CanSendTo(); len(cs) > 0 {
|
||||
requester.Send(cs[0], m.sendReq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testModule) expProcess(t *testing.T, expEvents []Event) {
|
||||
if len(m.processed) == 0 {
|
||||
t.Errorf("Missing call to %s.Process", m.name)
|
||||
return
|
||||
}
|
||||
events := m.processed[0]
|
||||
m.processed = m.processed[1:]
|
||||
if !reflect.DeepEqual(events, expEvents) {
|
||||
t.Errorf("Call to %s.Process with wrong events (expected %v, got %v)", m.name, expEvents, events)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testModule) expNoMoreProcess(t *testing.T) {
|
||||
for len(m.processed) > 0 {
|
||||
t.Errorf("Unexpected call to %s.Process with events %v", m.name, m.processed[0])
|
||||
m.processed = m.processed[1:]
|
||||
}
|
||||
}
|
||||
439
beacon/light/request/server.go
Normal file
439
beacon/light/request/server.go
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// request events
|
||||
EvResponse = &EventType{Name: "response", requestEvent: true} // data: RequestResponse; sent by requestServer
|
||||
EvFail = &EventType{Name: "fail", requestEvent: true} // data: RequestResponse; sent by requestServer
|
||||
EvTimeout = &EventType{Name: "timeout", requestEvent: true} // data: RequestResponse; sent by serverWithTimeout
|
||||
// server events
|
||||
EvRegistered = &EventType{Name: "registered"} // data: nil; sent by Scheduler
|
||||
EvUnregistered = &EventType{Name: "unregistered"} // data: nil; sent by Scheduler
|
||||
EvCanRequestAgain = &EventType{Name: "canRequestAgain"} // data: nil; sent by serverWithLimits
|
||||
)
|
||||
|
||||
const (
|
||||
softRequestTimeout = time.Second // allow resending request to a different server but do not cancel yet
|
||||
hardRequestTimeout = time.Second * 10 // cancel request
|
||||
)
|
||||
|
||||
const (
|
||||
// serverWithLimits parameters
|
||||
parallelAdjustUp = 0.1 // adjust parallelLimit up in case of success under full load
|
||||
parallelAdjustDown = 1 // adjust parallelLimit down in case of timeout/failure
|
||||
minParallelLimit = 1 // parallelLimit lower bound
|
||||
defaultParallelLimit = 3 // parallelLimit initial value
|
||||
minFailureDelay = time.Millisecond * 100 // minimum disable time in case of request failure
|
||||
maxFailureDelay = time.Minute // maximum disable time in case of request failure
|
||||
maxServerEventBuffer = 5 // server event allowance buffer limit
|
||||
maxServerEventRate = time.Second // server event allowance buffer recharge rate
|
||||
)
|
||||
|
||||
// requestServer can send requests in a non-blocking way and feed back events
|
||||
// through the event callback. After each request it should send back either
|
||||
// EvResponse or EvFail. Additionally, it may also send application-defined
|
||||
// events that the Modules can interpret.
|
||||
type requestServer interface {
|
||||
Subscribe(eventCallback func(Event))
|
||||
SendRequest(ID, Request)
|
||||
Unsubscribe()
|
||||
}
|
||||
|
||||
// server is implemented by a requestServer wrapped into serverWithTimeout and
|
||||
// serverWithLimits and is used by Scheduler.
|
||||
// In addition to requestServer functionality, server can also handle timeouts,
|
||||
// limit the number of parallel in-flight requests and temporarily disable
|
||||
// new requests based on timeouts and response failures.
|
||||
type server interface {
|
||||
subscribe(eventCallback func(Event))
|
||||
canRequestNow() bool
|
||||
sendRequest(Request) ID
|
||||
fail(string)
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
// NewServer wraps a requestServer and returns a server
|
||||
func NewServer(rs requestServer, clock mclock.Clock) server {
|
||||
s := &serverWithLimits{}
|
||||
s.parent = rs
|
||||
s.serverWithTimeout.init(clock)
|
||||
s.init()
|
||||
return s
|
||||
}
|
||||
|
||||
// EventType identifies an event type, either related to a request or the server
|
||||
// in general. Server events can also be externally defined.
|
||||
type EventType struct {
|
||||
Name string
|
||||
requestEvent bool // all request events are pre-defined in request package
|
||||
}
|
||||
|
||||
// Event describes an event where the type of Data depends on Type.
|
||||
// Server field is not required when sent through the event callback; it is filled
|
||||
// out when processed by the Scheduler. Note that the Scheduler can also create
|
||||
// and send events (EvRegistered, EvUnregistered) directly.
|
||||
type Event struct {
|
||||
Type *EventType
|
||||
Server Server // filled by Scheduler
|
||||
Data any
|
||||
}
|
||||
|
||||
// IsRequestEvent returns true if the event is a request event
|
||||
func (e *Event) IsRequestEvent() bool {
|
||||
return e.Type.requestEvent
|
||||
}
|
||||
|
||||
// RequestInfo assumes that the event is a request event and returns its contents
|
||||
// in a convenient form.
|
||||
func (e *Event) RequestInfo() (ServerAndID, Request, Response) {
|
||||
data := e.Data.(RequestResponse)
|
||||
return ServerAndID{Server: e.Server, ID: data.ID}, data.Request, data.Response
|
||||
}
|
||||
|
||||
// RequestResponse is the Data type of request events.
|
||||
type RequestResponse struct {
|
||||
ID ID
|
||||
Request Request
|
||||
Response Response
|
||||
}
|
||||
|
||||
// serverWithTimeout wraps a requestServer and introduces timeouts.
|
||||
// The request's lifecycle is concluded if EvResponse or EvFail emitted by the
|
||||
// parent requestServer. If this does not happen until softRequestTimeout then
|
||||
// EvTimeout is emitted, after which the final EvResponse or EvFail is still
|
||||
// guaranteed to follow.
|
||||
// If the parent fails to send this final event for hardRequestTimeout then
|
||||
// serverWithTimeout emits EvFail and discards any further events from the
|
||||
// parent related to the given request.
|
||||
type serverWithTimeout struct {
|
||||
parent requestServer
|
||||
lock sync.Mutex
|
||||
clock mclock.Clock
|
||||
childEventCb func(event Event)
|
||||
timeouts map[ID]mclock.Timer
|
||||
lastID ID
|
||||
}
|
||||
|
||||
// init initializes serverWithTimeout
|
||||
func (s *serverWithTimeout) init(clock mclock.Clock) {
|
||||
s.clock = clock
|
||||
s.timeouts = make(map[ID]mclock.Timer)
|
||||
}
|
||||
|
||||
// subscribe subscribes to events which include parent (requestServer) events
|
||||
// plus EvTimeout.
|
||||
func (s *serverWithTimeout) subscribe(eventCallback func(event Event)) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.childEventCb = eventCallback
|
||||
s.parent.Subscribe(s.eventCallback)
|
||||
}
|
||||
|
||||
// sendRequest generated a new request ID, emits EvRequest, sets up the timeout
|
||||
// timer, then sends the request through the parent (requestServer).
|
||||
func (s *serverWithTimeout) sendRequest(request Request) (reqId ID) {
|
||||
s.lock.Lock()
|
||||
s.lastID++
|
||||
id := s.lastID
|
||||
s.startTimeout(RequestResponse{ID: id, Request: request})
|
||||
s.lock.Unlock()
|
||||
s.parent.SendRequest(id, request)
|
||||
return id
|
||||
}
|
||||
|
||||
// eventCallback is called by parent (requestServer) event subscription.
|
||||
func (s *serverWithTimeout) eventCallback(event Event) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
switch event.Type {
|
||||
case EvResponse, EvFail:
|
||||
id := event.Data.(RequestResponse).ID
|
||||
if timer, ok := s.timeouts[id]; ok {
|
||||
// Note: if stopping the timer is unsuccessful then the resulting AfterFunc
|
||||
// call will just do nothing
|
||||
timer.Stop()
|
||||
delete(s.timeouts, id)
|
||||
s.childEventCb(event)
|
||||
}
|
||||
default:
|
||||
s.childEventCb(event)
|
||||
}
|
||||
}
|
||||
|
||||
// startTimeout starts a timeout timer for the given request.
|
||||
func (s *serverWithTimeout) startTimeout(reqData RequestResponse) {
|
||||
id := reqData.ID
|
||||
s.timeouts[id] = s.clock.AfterFunc(softRequestTimeout, func() {
|
||||
s.lock.Lock()
|
||||
if _, ok := s.timeouts[id]; !ok {
|
||||
s.lock.Unlock()
|
||||
return
|
||||
}
|
||||
s.timeouts[id] = s.clock.AfterFunc(hardRequestTimeout-softRequestTimeout, func() {
|
||||
s.lock.Lock()
|
||||
if _, ok := s.timeouts[id]; !ok {
|
||||
s.lock.Unlock()
|
||||
return
|
||||
}
|
||||
delete(s.timeouts, id)
|
||||
childEventCb := s.childEventCb
|
||||
s.lock.Unlock()
|
||||
childEventCb(Event{Type: EvFail, Data: reqData})
|
||||
})
|
||||
childEventCb := s.childEventCb
|
||||
s.lock.Unlock()
|
||||
childEventCb(Event{Type: EvTimeout, Data: reqData})
|
||||
})
|
||||
}
|
||||
|
||||
// stop stops all goroutines associated with the server.
|
||||
func (s *serverWithTimeout) unsubscribe() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
for _, timer := range s.timeouts {
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
s.childEventCb = nil
|
||||
s.parent.Unsubscribe()
|
||||
}
|
||||
|
||||
// serverWithLimits wraps serverWithTimeout and implements server. It limits the
|
||||
// number of parallel in-flight requests and prevents sending new requests when a
|
||||
// pending one has already timed out. Server events are also rate limited.
|
||||
// It also implements a failure delay mechanism that adds an exponentially growing
|
||||
// delay each time a request fails (wrong answer or hard timeout). This makes the
|
||||
// syncing mechanism less brittle as temporary failures of the server might happen
|
||||
// sometimes, but still avoids hammering a non-functional server with requests.
|
||||
type serverWithLimits struct {
|
||||
serverWithTimeout
|
||||
lock sync.Mutex
|
||||
childEventCb func(event Event)
|
||||
softTimeouts map[ID]struct{}
|
||||
pendingCount, timeoutCount int
|
||||
parallelLimit float32
|
||||
sendEvent bool
|
||||
delayTimer mclock.Timer
|
||||
delayCounter int
|
||||
failureDelayEnd mclock.AbsTime
|
||||
failureDelay float64
|
||||
serverEventBuffer int
|
||||
eventBufferUpdated mclock.AbsTime
|
||||
}
|
||||
|
||||
// init initializes serverWithLimits
|
||||
func (s *serverWithLimits) init() {
|
||||
s.softTimeouts = make(map[ID]struct{})
|
||||
s.parallelLimit = defaultParallelLimit
|
||||
s.serverEventBuffer = maxServerEventBuffer
|
||||
}
|
||||
|
||||
// subscribe subscribes to events which include parent (serverWithTimeout) events
|
||||
// plus EvCanRequestAgain.
|
||||
func (s *serverWithLimits) subscribe(eventCallback func(event Event)) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.childEventCb = eventCallback
|
||||
s.serverWithTimeout.subscribe(s.eventCallback)
|
||||
}
|
||||
|
||||
// eventCallback is called by parent (serverWithTimeout) event subscription.
|
||||
func (s *serverWithLimits) eventCallback(event Event) {
|
||||
s.lock.Lock()
|
||||
var sendCanRequestAgain bool
|
||||
passEvent := true
|
||||
switch event.Type {
|
||||
case EvTimeout:
|
||||
id := event.Data.(RequestResponse).ID
|
||||
s.softTimeouts[id] = struct{}{}
|
||||
s.timeoutCount++
|
||||
s.parallelLimit -= parallelAdjustDown
|
||||
if s.parallelLimit < minParallelLimit {
|
||||
s.parallelLimit = minParallelLimit
|
||||
}
|
||||
log.Debug("Server timeout", "count", s.timeoutCount, "parallelLimit", s.parallelLimit)
|
||||
case EvResponse, EvFail:
|
||||
id := event.Data.(RequestResponse).ID
|
||||
if _, ok := s.softTimeouts[id]; ok {
|
||||
delete(s.softTimeouts, id)
|
||||
s.timeoutCount--
|
||||
log.Debug("Server timeout finalized", "count", s.timeoutCount, "parallelLimit", s.parallelLimit)
|
||||
}
|
||||
if event.Type == EvResponse && s.pendingCount >= int(s.parallelLimit) {
|
||||
s.parallelLimit += parallelAdjustUp
|
||||
}
|
||||
s.pendingCount--
|
||||
if s.canRequest() {
|
||||
sendCanRequestAgain = s.sendEvent
|
||||
s.sendEvent = false
|
||||
}
|
||||
if event.Type == EvFail {
|
||||
s.failLocked("failed request")
|
||||
}
|
||||
default:
|
||||
// server event; check rate limit
|
||||
if s.serverEventBuffer < maxServerEventBuffer {
|
||||
now := s.clock.Now()
|
||||
sinceUpdate := time.Duration(now - s.eventBufferUpdated)
|
||||
if sinceUpdate >= maxServerEventRate*time.Duration(maxServerEventBuffer-s.serverEventBuffer) {
|
||||
s.serverEventBuffer = maxServerEventBuffer
|
||||
s.eventBufferUpdated = now
|
||||
} else {
|
||||
addBuffer := int(sinceUpdate / maxServerEventRate)
|
||||
s.serverEventBuffer += addBuffer
|
||||
s.eventBufferUpdated += mclock.AbsTime(maxServerEventRate * time.Duration(addBuffer))
|
||||
}
|
||||
}
|
||||
if s.serverEventBuffer > 0 {
|
||||
s.serverEventBuffer--
|
||||
} else {
|
||||
passEvent = false
|
||||
}
|
||||
}
|
||||
childEventCb := s.childEventCb
|
||||
s.lock.Unlock()
|
||||
if passEvent {
|
||||
childEventCb(event)
|
||||
}
|
||||
if sendCanRequestAgain {
|
||||
childEventCb(Event{Type: EvCanRequestAgain})
|
||||
}
|
||||
}
|
||||
|
||||
// sendRequest sends a request through the parent (serverWithTimeout).
|
||||
func (s *serverWithLimits) sendRequest(request Request) (reqId ID) {
|
||||
s.lock.Lock()
|
||||
s.pendingCount++
|
||||
s.lock.Unlock()
|
||||
return s.serverWithTimeout.sendRequest(request)
|
||||
}
|
||||
|
||||
// stop stops all goroutines associated with the server.
|
||||
func (s *serverWithLimits) unsubscribe() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if s.delayTimer != nil {
|
||||
s.delayTimer.Stop()
|
||||
s.delayTimer = nil
|
||||
}
|
||||
s.childEventCb = nil
|
||||
s.serverWithTimeout.unsubscribe()
|
||||
}
|
||||
|
||||
// canRequest checks whether a new request can be started.
|
||||
func (s *serverWithLimits) canRequest() bool {
|
||||
if s.delayTimer != nil || s.pendingCount >= int(s.parallelLimit) || s.timeoutCount > 0 {
|
||||
return false
|
||||
}
|
||||
if s.parallelLimit < minParallelLimit {
|
||||
s.parallelLimit = minParallelLimit
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// canRequestNow checks whether a new request can be started, according to the
|
||||
// current in-flight request count and parallelLimit, and also the failure delay
|
||||
// timer.
|
||||
// If it returns false then it is guaranteed that an EvCanRequestAgain will be
|
||||
// sent whenever the server becomes available for requesting again.
|
||||
func (s *serverWithLimits) canRequestNow() bool {
|
||||
var sendCanRequestAgain bool
|
||||
s.lock.Lock()
|
||||
canRequest := s.canRequest()
|
||||
if canRequest {
|
||||
sendCanRequestAgain = s.sendEvent
|
||||
s.sendEvent = false
|
||||
}
|
||||
childEventCb := s.childEventCb
|
||||
s.lock.Unlock()
|
||||
if sendCanRequestAgain {
|
||||
childEventCb(Event{Type: EvCanRequestAgain})
|
||||
}
|
||||
return canRequest
|
||||
}
|
||||
|
||||
// delay sets the delay timer to the given duration, disabling new requests for
|
||||
// the given period.
|
||||
func (s *serverWithLimits) delay(delay time.Duration) {
|
||||
if s.delayTimer != nil {
|
||||
// Note: if stopping the timer is unsuccessful then the resulting AfterFunc
|
||||
// call will just do nothing
|
||||
s.delayTimer.Stop()
|
||||
s.delayTimer = nil
|
||||
}
|
||||
|
||||
s.delayCounter++
|
||||
delayCounter := s.delayCounter
|
||||
log.Debug("Server delay started", "length", delay)
|
||||
s.delayTimer = s.clock.AfterFunc(delay, func() {
|
||||
log.Debug("Server delay ended", "length", delay)
|
||||
var sendCanRequestAgain bool
|
||||
s.lock.Lock()
|
||||
if s.delayTimer != nil && s.delayCounter == delayCounter { // do nothing if there is a new timer now
|
||||
s.delayTimer = nil
|
||||
if s.canRequest() {
|
||||
sendCanRequestAgain = s.sendEvent
|
||||
s.sendEvent = false
|
||||
}
|
||||
}
|
||||
childEventCb := s.childEventCb
|
||||
s.lock.Unlock()
|
||||
if sendCanRequestAgain {
|
||||
childEventCb(Event{Type: EvCanRequestAgain})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fail reports that a response from the server was found invalid by the processing
|
||||
// Module, disabling new requests for a dynamically adjusted time period.
|
||||
func (s *serverWithLimits) fail(desc string) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.failLocked(desc)
|
||||
}
|
||||
|
||||
// failLocked calculates the dynamic failure delay and applies it.
|
||||
func (s *serverWithLimits) failLocked(desc string) {
|
||||
log.Debug("Server error", "description", desc)
|
||||
s.failureDelay *= 2
|
||||
now := s.clock.Now()
|
||||
if now > s.failureDelayEnd {
|
||||
s.failureDelay *= math.Pow(2, -float64(now-s.failureDelayEnd)/float64(maxFailureDelay))
|
||||
}
|
||||
if s.failureDelay < float64(minFailureDelay) {
|
||||
s.failureDelay = float64(minFailureDelay)
|
||||
}
|
||||
s.failureDelayEnd = now + mclock.AbsTime(s.failureDelay)
|
||||
s.delay(time.Duration(s.failureDelay))
|
||||
}
|
||||
158
beacon/light/request/server_test.go
Normal file
158
beacon/light/request/server_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
)
|
||||
|
||||
const (
|
||||
testRequest = "Life, the Universe, and Everything"
|
||||
testResponse = 42
|
||||
)
|
||||
|
||||
var testEventType = &EventType{Name: "testEvent"}
|
||||
|
||||
func TestServerEvents(t *testing.T) {
|
||||
rs := &testRequestServer{}
|
||||
clock := &mclock.Simulated{}
|
||||
srv := NewServer(rs, clock)
|
||||
var lastEventType *EventType
|
||||
srv.subscribe(func(event Event) { lastEventType = event.Type })
|
||||
evTypeName := func(evType *EventType) string {
|
||||
if evType == nil {
|
||||
return "none"
|
||||
}
|
||||
return evType.Name
|
||||
}
|
||||
expEvent := func(expType *EventType) {
|
||||
if lastEventType != expType {
|
||||
t.Errorf("Wrong event type (expected %s, got %s)", evTypeName(expType), evTypeName(lastEventType))
|
||||
}
|
||||
lastEventType = nil
|
||||
}
|
||||
// user events should simply be passed through
|
||||
rs.eventCb(Event{Type: testEventType})
|
||||
expEvent(testEventType)
|
||||
// send request, soft timeout, then valid response
|
||||
srv.sendRequest(testRequest)
|
||||
clock.WaitForTimers(1)
|
||||
clock.Run(softRequestTimeout)
|
||||
expEvent(EvTimeout)
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
|
||||
expEvent(EvResponse)
|
||||
// send request, hard timeout (response after hard timeout should be ignored)
|
||||
srv.sendRequest(testRequest)
|
||||
clock.WaitForTimers(1)
|
||||
clock.Run(softRequestTimeout)
|
||||
expEvent(EvTimeout)
|
||||
clock.WaitForTimers(1)
|
||||
clock.Run(hardRequestTimeout)
|
||||
expEvent(EvFail)
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
|
||||
expEvent(nil)
|
||||
}
|
||||
|
||||
func TestServerParallel(t *testing.T) {
|
||||
rs := &testRequestServer{}
|
||||
srv := NewServer(rs, &mclock.Simulated{})
|
||||
srv.subscribe(func(event Event) {})
|
||||
|
||||
expSend := func(expSent int) {
|
||||
var sent int
|
||||
for sent <= expSent {
|
||||
if !srv.canRequestNow() {
|
||||
break
|
||||
}
|
||||
sent++
|
||||
srv.sendRequest(testRequest)
|
||||
}
|
||||
if sent != expSent {
|
||||
t.Errorf("Wrong number of parallel requests accepted (expected %d, got %d)", expSent, sent)
|
||||
}
|
||||
}
|
||||
// max out parallel allowance
|
||||
expSend(defaultParallelLimit)
|
||||
// 1 answered, should accept 1 more
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
|
||||
expSend(1)
|
||||
// 2 answered, should accept 2 more
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 2, Request: testRequest, Response: testResponse}})
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 3, Request: testRequest, Response: testResponse}})
|
||||
expSend(2)
|
||||
// failed request, should decrease allowance and not accept more
|
||||
rs.eventCb(Event{Type: EvFail, Data: RequestResponse{ID: 4, Request: testRequest}})
|
||||
expSend(0)
|
||||
srv.unsubscribe()
|
||||
}
|
||||
|
||||
func TestServerFail(t *testing.T) {
|
||||
rs := &testRequestServer{}
|
||||
clock := &mclock.Simulated{}
|
||||
srv := NewServer(rs, clock)
|
||||
srv.subscribe(func(event Event) {})
|
||||
expCanRequest := func(expCanRequest bool) {
|
||||
if canRequest := srv.canRequestNow(); canRequest != expCanRequest {
|
||||
t.Errorf("Wrong result for canRequestNow (expected %v, got %v)", expCanRequest, canRequest)
|
||||
}
|
||||
}
|
||||
// timed out request
|
||||
expCanRequest(true)
|
||||
srv.sendRequest(testRequest)
|
||||
clock.WaitForTimers(1)
|
||||
expCanRequest(true)
|
||||
clock.Run(softRequestTimeout)
|
||||
expCanRequest(false) // cannot request when there is a timed out request
|
||||
rs.eventCb(Event{Type: EvResponse, Data: RequestResponse{ID: 1, Request: testRequest, Response: testResponse}})
|
||||
expCanRequest(true)
|
||||
// explicit server.Fail
|
||||
srv.fail("")
|
||||
clock.WaitForTimers(1)
|
||||
expCanRequest(false) // cannot request for a while after a failure
|
||||
clock.Run(minFailureDelay)
|
||||
expCanRequest(true)
|
||||
// request returned with EvFail
|
||||
srv.sendRequest(testRequest)
|
||||
rs.eventCb(Event{Type: EvFail, Data: RequestResponse{ID: 2, Request: testRequest}})
|
||||
clock.WaitForTimers(1)
|
||||
expCanRequest(false) // EvFail should also start failure delay
|
||||
clock.Run(minFailureDelay)
|
||||
expCanRequest(false) // second failure delay is longer, should still be disabled
|
||||
clock.Run(minFailureDelay)
|
||||
expCanRequest(true)
|
||||
srv.unsubscribe()
|
||||
}
|
||||
|
||||
func TestServerEventRateLimit(t *testing.T) {
|
||||
rs := &testRequestServer{}
|
||||
clock := &mclock.Simulated{}
|
||||
srv := NewServer(rs, clock)
|
||||
var eventCount int
|
||||
srv.subscribe(func(event Event) {
|
||||
if !event.IsRequestEvent() {
|
||||
eventCount++
|
||||
}
|
||||
})
|
||||
expEvents := func(send, expAllowed int) {
|
||||
eventCount = 0
|
||||
for sent := 0; sent < send; sent++ {
|
||||
rs.eventCb(Event{Type: testEventType})
|
||||
}
|
||||
if eventCount != expAllowed {
|
||||
t.Errorf("Wrong number of server events passing rate limitation (sent %d, expected %d, got %d)", send, expAllowed, eventCount)
|
||||
}
|
||||
}
|
||||
expEvents(maxServerEventBuffer+5, maxServerEventBuffer)
|
||||
clock.Run(maxServerEventRate)
|
||||
expEvents(5, 1)
|
||||
clock.Run(maxServerEventRate * maxServerEventBuffer * 2)
|
||||
expEvents(maxServerEventBuffer+5, maxServerEventBuffer)
|
||||
}
|
||||
|
||||
type testRequestServer struct {
|
||||
eventCb func(Event)
|
||||
}
|
||||
|
||||
func (rs *testRequestServer) Subscribe(eventCb func(Event)) { rs.eventCb = eventCb }
|
||||
func (rs *testRequestServer) SendRequest(ID, Request) {}
|
||||
func (rs *testRequestServer) Unsubscribe() {}
|
||||
176
beacon/light/sync/head_sync.go
Normal file
176
beacon/light/sync/head_sync.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
)
|
||||
|
||||
type headTracker interface {
|
||||
ValidateHead(head types.SignedHeader) (bool, error)
|
||||
ValidateFinality(head types.FinalityUpdate) (bool, error)
|
||||
SetPrefetchHead(head types.HeadInfo)
|
||||
}
|
||||
|
||||
// HeadSync implements request.Module; it updates the validated and prefetch
|
||||
// heads of HeadTracker based on the EvHead and EvSignedHead events coming from
|
||||
// registered servers.
|
||||
// It can also postpone the validation of the latest announced signed head
|
||||
// until the committee chain is synced up to at least the required period.
|
||||
type HeadSync struct {
|
||||
headTracker headTracker
|
||||
chain committeeChain
|
||||
nextSyncPeriod uint64
|
||||
chainInit bool
|
||||
unvalidatedHeads map[request.Server]types.SignedHeader
|
||||
unvalidatedFinality map[request.Server]types.FinalityUpdate
|
||||
serverHeads map[request.Server]types.HeadInfo
|
||||
headServerCount map[types.HeadInfo]headServerCount
|
||||
headCounter uint64
|
||||
prefetchHead types.HeadInfo
|
||||
}
|
||||
|
||||
// headServerCount is associated with most recently seen head infos; it counts
|
||||
// the number of servers currently having the given head info as their announced
|
||||
// head and a counter signaling how recent that head is.
|
||||
// This data is used for selecting the prefetch head.
|
||||
type headServerCount struct {
|
||||
serverCount int
|
||||
headCounter uint64
|
||||
}
|
||||
|
||||
// NewHeadSync creates a new HeadSync.
|
||||
func NewHeadSync(headTracker headTracker, chain committeeChain) *HeadSync {
|
||||
s := &HeadSync{
|
||||
headTracker: headTracker,
|
||||
chain: chain,
|
||||
unvalidatedHeads: make(map[request.Server]types.SignedHeader),
|
||||
unvalidatedFinality: make(map[request.Server]types.FinalityUpdate),
|
||||
serverHeads: make(map[request.Server]types.HeadInfo),
|
||||
headServerCount: make(map[types.HeadInfo]headServerCount),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Process implements request.Module.
|
||||
func (s *HeadSync) Process(requester request.Requester, events []request.Event) {
|
||||
for _, event := range events {
|
||||
switch event.Type {
|
||||
case EvNewHead:
|
||||
s.setServerHead(event.Server, event.Data.(types.HeadInfo))
|
||||
case EvNewSignedHead:
|
||||
s.newSignedHead(event.Server, event.Data.(types.SignedHeader))
|
||||
case EvNewFinalityUpdate:
|
||||
s.newFinalityUpdate(event.Server, event.Data.(types.FinalityUpdate))
|
||||
case request.EvUnregistered:
|
||||
s.setServerHead(event.Server, types.HeadInfo{})
|
||||
delete(s.serverHeads, event.Server)
|
||||
delete(s.unvalidatedHeads, event.Server)
|
||||
}
|
||||
}
|
||||
|
||||
nextPeriod, chainInit := s.chain.NextSyncPeriod()
|
||||
if nextPeriod != s.nextSyncPeriod || chainInit != s.chainInit {
|
||||
s.nextSyncPeriod, s.chainInit = nextPeriod, chainInit
|
||||
s.processUnvalidated()
|
||||
}
|
||||
}
|
||||
|
||||
// newSignedHead handles received signed head; either validates it if the chain
|
||||
// is properly synced or stores it for further validation.
|
||||
func (s *HeadSync) newSignedHead(server request.Server, signedHead types.SignedHeader) {
|
||||
if !s.chainInit || types.SyncPeriod(signedHead.SignatureSlot) > s.nextSyncPeriod {
|
||||
s.unvalidatedHeads[server] = signedHead
|
||||
return
|
||||
}
|
||||
s.headTracker.ValidateHead(signedHead)
|
||||
}
|
||||
|
||||
// newSignedHead handles received signed head; either validates it if the chain
|
||||
// is properly synced or stores it for further validation.
|
||||
func (s *HeadSync) newFinalityUpdate(server request.Server, finalityUpdate types.FinalityUpdate) {
|
||||
if !s.chainInit || types.SyncPeriod(finalityUpdate.SignatureSlot) > s.nextSyncPeriod {
|
||||
s.unvalidatedFinality[server] = finalityUpdate
|
||||
return
|
||||
}
|
||||
s.headTracker.ValidateFinality(finalityUpdate)
|
||||
}
|
||||
|
||||
// processUnvalidatedHeads iterates the list of unvalidated heads and validates
|
||||
// those which can be validated.
|
||||
func (s *HeadSync) processUnvalidated() {
|
||||
if !s.chainInit {
|
||||
return
|
||||
}
|
||||
for server, signedHead := range s.unvalidatedHeads {
|
||||
if types.SyncPeriod(signedHead.SignatureSlot) <= s.nextSyncPeriod {
|
||||
s.headTracker.ValidateHead(signedHead)
|
||||
delete(s.unvalidatedHeads, server)
|
||||
}
|
||||
}
|
||||
for server, finalityUpdate := range s.unvalidatedFinality {
|
||||
if types.SyncPeriod(finalityUpdate.SignatureSlot) <= s.nextSyncPeriod {
|
||||
s.headTracker.ValidateFinality(finalityUpdate)
|
||||
delete(s.unvalidatedFinality, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setServerHead processes non-validated server head announcements and updates
|
||||
// the prefetch head if necessary.
|
||||
func (s *HeadSync) setServerHead(server request.Server, head types.HeadInfo) bool {
|
||||
if oldHead, ok := s.serverHeads[server]; ok {
|
||||
if head == oldHead {
|
||||
return false
|
||||
}
|
||||
h := s.headServerCount[oldHead]
|
||||
if h.serverCount--; h.serverCount > 0 {
|
||||
s.headServerCount[oldHead] = h
|
||||
} else {
|
||||
delete(s.headServerCount, oldHead)
|
||||
}
|
||||
}
|
||||
if head != (types.HeadInfo{}) {
|
||||
h, ok := s.headServerCount[head]
|
||||
if !ok {
|
||||
s.headCounter++
|
||||
h.headCounter = s.headCounter
|
||||
}
|
||||
h.serverCount++
|
||||
s.headServerCount[head] = h
|
||||
s.serverHeads[server] = head
|
||||
} else {
|
||||
delete(s.serverHeads, server)
|
||||
}
|
||||
var (
|
||||
bestHead types.HeadInfo
|
||||
bestHeadInfo headServerCount
|
||||
)
|
||||
for head, headServerCount := range s.headServerCount {
|
||||
if headServerCount.serverCount > bestHeadInfo.serverCount ||
|
||||
(headServerCount.serverCount == bestHeadInfo.serverCount && headServerCount.headCounter > bestHeadInfo.headCounter) {
|
||||
bestHead, bestHeadInfo = head, headServerCount
|
||||
}
|
||||
}
|
||||
if bestHead == s.prefetchHead {
|
||||
return false
|
||||
}
|
||||
s.prefetchHead = bestHead
|
||||
s.headTracker.SetPrefetchHead(bestHead)
|
||||
return true
|
||||
}
|
||||
151
beacon/light/sync/head_sync_test.go
Normal file
151
beacon/light/sync/head_sync_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var (
|
||||
testServer1 = "testServer1"
|
||||
testServer2 = "testServer2"
|
||||
testServer3 = "testServer3"
|
||||
testServer4 = "testServer4"
|
||||
|
||||
testHead0 = types.HeadInfo{}
|
||||
testHead1 = types.HeadInfo{Slot: 123, BlockRoot: common.Hash{1}}
|
||||
testHead2 = types.HeadInfo{Slot: 124, BlockRoot: common.Hash{2}}
|
||||
testHead3 = types.HeadInfo{Slot: 124, BlockRoot: common.Hash{3}}
|
||||
testHead4 = types.HeadInfo{Slot: 125, BlockRoot: common.Hash{4}}
|
||||
|
||||
testSHead1 = types.SignedHeader{SignatureSlot: 0x0124, Header: types.Header{Slot: 0x0123, StateRoot: common.Hash{1}}}
|
||||
testSHead2 = types.SignedHeader{SignatureSlot: 0x2010, Header: types.Header{Slot: 0x200e, StateRoot: common.Hash{2}}}
|
||||
// testSHead3 is at the end of period 1 but signed in period 2
|
||||
testSHead3 = types.SignedHeader{SignatureSlot: 0x4000, Header: types.Header{Slot: 0x3fff, StateRoot: common.Hash{3}}}
|
||||
testSHead4 = types.SignedHeader{SignatureSlot: 0x6444, Header: types.Header{Slot: 0x6443, StateRoot: common.Hash{4}}}
|
||||
)
|
||||
|
||||
func TestValidatedHead(t *testing.T) {
|
||||
chain := &TestCommitteeChain{}
|
||||
ht := &TestHeadTracker{}
|
||||
headSync := NewHeadSync(ht, chain)
|
||||
ts := NewTestScheduler(t, headSync)
|
||||
|
||||
ht.ExpValidated(t, 0, nil)
|
||||
|
||||
ts.AddServer(testServer1, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead1)
|
||||
ts.Run(1)
|
||||
// announced head should be queued because of uninitialized chain
|
||||
ht.ExpValidated(t, 1, nil)
|
||||
|
||||
chain.SetNextSyncPeriod(0) // initialize chain
|
||||
ts.Run(2)
|
||||
// expect previously queued head to be validated
|
||||
ht.ExpValidated(t, 2, []types.SignedHeader{testSHead1})
|
||||
|
||||
chain.SetNextSyncPeriod(1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead2)
|
||||
ts.AddServer(testServer2, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer2, testSHead2)
|
||||
ts.Run(3)
|
||||
// expect both head announcements to be validated instantly
|
||||
ht.ExpValidated(t, 3, []types.SignedHeader{testSHead2, testSHead2})
|
||||
|
||||
ts.ServerEvent(EvNewSignedHead, testServer1, testSHead3)
|
||||
ts.AddServer(testServer3, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer3, testSHead4)
|
||||
ts.Run(4)
|
||||
// future period announced heads should be queued
|
||||
ht.ExpValidated(t, 4, nil)
|
||||
|
||||
chain.SetNextSyncPeriod(2)
|
||||
ts.Run(5)
|
||||
// testSHead3 can be validated now but not testSHead4
|
||||
ht.ExpValidated(t, 5, []types.SignedHeader{testSHead3})
|
||||
|
||||
// server 3 disconnected without proving period 3, its announced head should be dropped
|
||||
ts.RemoveServer(testServer3)
|
||||
ts.Run(6)
|
||||
ht.ExpValidated(t, 6, nil)
|
||||
|
||||
chain.SetNextSyncPeriod(3)
|
||||
ts.Run(7)
|
||||
// testSHead4 could be validated now but it's not queued by any registered server
|
||||
ht.ExpValidated(t, 7, nil)
|
||||
|
||||
ts.ServerEvent(EvNewSignedHead, testServer2, testSHead4)
|
||||
ts.Run(8)
|
||||
// now testSHead4 should be validated
|
||||
ht.ExpValidated(t, 8, []types.SignedHeader{testSHead4})
|
||||
}
|
||||
|
||||
func TestPrefetchHead(t *testing.T) {
|
||||
chain := &TestCommitteeChain{}
|
||||
ht := &TestHeadTracker{}
|
||||
headSync := NewHeadSync(ht, chain)
|
||||
ts := NewTestScheduler(t, headSync)
|
||||
|
||||
ht.ExpPrefetch(t, 0, testHead0) // no servers registered
|
||||
|
||||
ts.AddServer(testServer1, 1)
|
||||
ts.ServerEvent(EvNewHead, testServer1, testHead1)
|
||||
ts.Run(1)
|
||||
ht.ExpPrefetch(t, 1, testHead1) // s1: h1
|
||||
|
||||
ts.AddServer(testServer2, 1)
|
||||
ts.ServerEvent(EvNewHead, testServer2, testHead2)
|
||||
ts.Run(2)
|
||||
ht.ExpPrefetch(t, 2, testHead2) // s1: h1, s2: h2
|
||||
|
||||
ts.ServerEvent(EvNewHead, testServer1, testHead2)
|
||||
ts.Run(3)
|
||||
ht.ExpPrefetch(t, 3, testHead2) // s1: h2, s2: h2
|
||||
|
||||
ts.AddServer(testServer3, 1)
|
||||
ts.ServerEvent(EvNewHead, testServer3, testHead3)
|
||||
ts.Run(4)
|
||||
ht.ExpPrefetch(t, 4, testHead2) // s1: h2, s2: h2, s3: h3
|
||||
|
||||
ts.AddServer(testServer4, 1)
|
||||
ts.ServerEvent(EvNewHead, testServer4, testHead4)
|
||||
ts.Run(5)
|
||||
ht.ExpPrefetch(t, 5, testHead2) // s1: h2, s2: h2, s3: h3, s4: h4
|
||||
|
||||
ts.ServerEvent(EvNewHead, testServer2, testHead3)
|
||||
ts.Run(6)
|
||||
ht.ExpPrefetch(t, 6, testHead3) // s1: h2, s2: h3, s3: h3, s4: h4
|
||||
|
||||
ts.RemoveServer(testServer3)
|
||||
ts.Run(7)
|
||||
ht.ExpPrefetch(t, 7, testHead4) // s1: h2, s2: h3, s4: h4
|
||||
|
||||
ts.RemoveServer(testServer1)
|
||||
ts.Run(8)
|
||||
ht.ExpPrefetch(t, 8, testHead4) // s2: h3, s4: h4
|
||||
|
||||
ts.RemoveServer(testServer4)
|
||||
ts.Run(9)
|
||||
ht.ExpPrefetch(t, 9, testHead3) // s2: h3
|
||||
|
||||
ts.RemoveServer(testServer2)
|
||||
ts.Run(10)
|
||||
ht.ExpPrefetch(t, 10, testHead0) // no servers registered
|
||||
}
|
||||
254
beacon/light/sync/test_helpers.go
Normal file
254
beacon/light/sync/test_helpers.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
)
|
||||
|
||||
type requestWithID struct {
|
||||
sid request.ServerAndID
|
||||
request request.Request
|
||||
}
|
||||
|
||||
type TestScheduler struct {
|
||||
t *testing.T
|
||||
module request.Module
|
||||
events []request.Event
|
||||
servers []request.Server
|
||||
allowance map[request.Server]int
|
||||
sent map[int][]requestWithID
|
||||
testIndex int
|
||||
expFail map[request.Server]int // expected Server.Fail calls during next Run
|
||||
lastId request.ID
|
||||
}
|
||||
|
||||
func NewTestScheduler(t *testing.T, module request.Module) *TestScheduler {
|
||||
return &TestScheduler{
|
||||
t: t,
|
||||
module: module,
|
||||
allowance: make(map[request.Server]int),
|
||||
expFail: make(map[request.Server]int),
|
||||
sent: make(map[int][]requestWithID),
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) Run(testIndex int, exp ...any) {
|
||||
expReqs := make([]requestWithID, len(exp)/2)
|
||||
id := ts.lastId
|
||||
for i := range expReqs {
|
||||
id++
|
||||
expReqs[i] = requestWithID{
|
||||
sid: request.ServerAndID{Server: exp[i*2].(request.Server), ID: id},
|
||||
request: exp[i*2+1].(request.Request),
|
||||
}
|
||||
}
|
||||
if len(expReqs) == 0 {
|
||||
expReqs = nil
|
||||
}
|
||||
|
||||
ts.testIndex = testIndex
|
||||
ts.module.Process(ts, ts.events)
|
||||
ts.events = nil
|
||||
|
||||
for server, count := range ts.expFail {
|
||||
delete(ts.expFail, server)
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
ts.t.Errorf("Missing %d Server.Fail(s) from server %s in test case #%d", count, server.(string), testIndex)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ts.sent[testIndex], expReqs) {
|
||||
ts.t.Errorf("Wrong sent requests in test case #%d (expected %v, got %v)", testIndex, expReqs, ts.sent[testIndex])
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) CanSendTo() (cs []request.Server) {
|
||||
for _, server := range ts.servers {
|
||||
if ts.allowance[server] > 0 {
|
||||
cs = append(cs, server)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) Send(server request.Server, req request.Request) request.ID {
|
||||
ts.lastId++
|
||||
ts.sent[ts.testIndex] = append(ts.sent[ts.testIndex], requestWithID{
|
||||
sid: request.ServerAndID{Server: server, ID: ts.lastId},
|
||||
request: req,
|
||||
})
|
||||
ts.allowance[server]--
|
||||
return ts.lastId
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) Fail(server request.Server, desc string) {
|
||||
if ts.expFail[server] == 0 {
|
||||
ts.t.Errorf("Unexpected Fail from server %s in test case #%d: %s", server.(string), ts.testIndex, desc)
|
||||
return
|
||||
}
|
||||
ts.expFail[server]--
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) Request(testIndex, reqIndex int) requestWithID {
|
||||
if len(ts.sent[testIndex]) < reqIndex {
|
||||
ts.t.Errorf("Missing request from test case %d index %d", testIndex, reqIndex)
|
||||
return requestWithID{}
|
||||
}
|
||||
return ts.sent[testIndex][reqIndex-1]
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) ServerEvent(evType *request.EventType, server request.Server, data any) {
|
||||
ts.events = append(ts.events, request.Event{
|
||||
Type: evType,
|
||||
Server: server,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) RequestEvent(evType *request.EventType, req requestWithID, resp request.Response) {
|
||||
if req.request == nil {
|
||||
return
|
||||
}
|
||||
ts.events = append(ts.events, request.Event{
|
||||
Type: evType,
|
||||
Server: req.sid.Server,
|
||||
Data: request.RequestResponse{
|
||||
ID: req.sid.ID,
|
||||
Request: req.request,
|
||||
Response: resp,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) AddServer(server request.Server, allowance int) {
|
||||
ts.servers = append(ts.servers, server)
|
||||
ts.allowance[server] = allowance
|
||||
ts.ServerEvent(request.EvRegistered, server, nil)
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) RemoveServer(server request.Server) {
|
||||
ts.servers = append(ts.servers, server)
|
||||
for i, s := range ts.servers {
|
||||
if s == server {
|
||||
copy(ts.servers[i:len(ts.servers)-1], ts.servers[i+1:])
|
||||
ts.servers = ts.servers[:len(ts.servers)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
delete(ts.allowance, server)
|
||||
ts.ServerEvent(request.EvUnregistered, server, nil)
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) AddAllowance(server request.Server, allowance int) {
|
||||
ts.allowance[server] += allowance
|
||||
}
|
||||
|
||||
func (ts *TestScheduler) ExpFail(server request.Server) {
|
||||
ts.expFail[server]++
|
||||
}
|
||||
|
||||
type TestCommitteeChain struct {
|
||||
fsp, nsp uint64
|
||||
init bool
|
||||
}
|
||||
|
||||
func (t *TestCommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error {
|
||||
t.fsp, t.nsp, t.init = bootstrap.Header.SyncPeriod(), bootstrap.Header.SyncPeriod()+2, true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestCommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error {
|
||||
period := update.AttestedHeader.Header.SyncPeriod()
|
||||
if period < t.fsp || period > t.nsp || !t.init {
|
||||
return light.ErrInvalidPeriod
|
||||
}
|
||||
if period == t.nsp {
|
||||
t.nsp++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestCommitteeChain) NextSyncPeriod() (uint64, bool) {
|
||||
return t.nsp, t.init
|
||||
}
|
||||
|
||||
func (tc *TestCommitteeChain) ExpInit(t *testing.T, ExpInit bool) {
|
||||
if tc.init != ExpInit {
|
||||
t.Errorf("Incorrect init flag (expected %v, got %v)", ExpInit, tc.init)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TestCommitteeChain) SetNextSyncPeriod(nsp uint64) {
|
||||
t.init, t.nsp = true, nsp
|
||||
}
|
||||
|
||||
func (tc *TestCommitteeChain) ExpNextSyncPeriod(t *testing.T, expNsp uint64) {
|
||||
tc.ExpInit(t, true)
|
||||
if tc.nsp != expNsp {
|
||||
t.Errorf("Incorrect NextSyncPeriod (expected %d, got %d)", expNsp, tc.nsp)
|
||||
}
|
||||
}
|
||||
|
||||
type TestHeadTracker struct {
|
||||
phead types.HeadInfo
|
||||
validated []types.SignedHeader
|
||||
}
|
||||
|
||||
func (ht *TestHeadTracker) ValidateHead(head types.SignedHeader) (bool, error) {
|
||||
ht.validated = append(ht.validated, head)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TODO add test case for finality
|
||||
func (ht *TestHeadTracker) ValidateFinality(head types.FinalityUpdate) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (ht *TestHeadTracker) ExpValidated(t *testing.T, tci int, expHeads []types.SignedHeader) {
|
||||
for i, expHead := range expHeads {
|
||||
if i >= len(ht.validated) {
|
||||
t.Errorf("Missing validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got none)", tci, i, expHead.Header.Slot, expHead.Header.Hash())
|
||||
continue
|
||||
}
|
||||
if ht.validated[i] != expHead {
|
||||
vhead := ht.validated[i].Header
|
||||
t.Errorf("Wrong validated head in test case #%d index #%d (expected {slot %d blockRoot %x}, got {slot %d blockRoot %x})", tci, i, expHead.Header.Slot, expHead.Header.Hash(), vhead.Slot, vhead.Hash())
|
||||
}
|
||||
}
|
||||
for i := len(expHeads); i < len(ht.validated); i++ {
|
||||
vhead := ht.validated[i].Header
|
||||
t.Errorf("Unexpected validated head in test case #%d index #%d (expected none, got {slot %d blockRoot %x})", tci, i, vhead.Slot, vhead.Hash())
|
||||
}
|
||||
ht.validated = nil
|
||||
}
|
||||
|
||||
func (ht *TestHeadTracker) SetPrefetchHead(head types.HeadInfo) {
|
||||
ht.phead = head
|
||||
}
|
||||
|
||||
func (ht *TestHeadTracker) ExpPrefetch(t *testing.T, tci int, exp types.HeadInfo) {
|
||||
if ht.phead != exp {
|
||||
t.Errorf("Wrong prefetch head in test case #%d (expected {slot %d blockRoot %x}, got {slot %d blockRoot %x})", tci, exp.Slot, exp.BlockRoot, ht.phead.Slot, ht.phead.BlockRoot)
|
||||
}
|
||||
}
|
||||
42
beacon/light/sync/types.go
Normal file
42
beacon/light/sync/types.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
var (
|
||||
EvNewHead = &request.EventType{Name: "newHead"} // data: types.HeadInfo
|
||||
EvNewSignedHead = &request.EventType{Name: "newSignedHead"} // data: types.SignedHeader
|
||||
EvNewFinalityUpdate = &request.EventType{Name: "newFinalityUpdate"} // data: types.FinalityUpdate
|
||||
)
|
||||
|
||||
type (
|
||||
ReqUpdates struct {
|
||||
FirstPeriod, Count uint64
|
||||
}
|
||||
RespUpdates struct {
|
||||
Updates []*types.LightClientUpdate
|
||||
Committees []*types.SerializedSyncCommittee
|
||||
}
|
||||
ReqHeader common.Hash
|
||||
ReqCheckpointData common.Hash
|
||||
ReqBeaconBlock common.Hash
|
||||
)
|
||||
299
beacon/light/sync/update_sync.go
Normal file
299
beacon/light/sync/update_sync.go
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light"
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
const maxUpdateRequest = 8 // maximum number of updates requested in a single request
|
||||
|
||||
type committeeChain interface {
|
||||
CheckpointInit(bootstrap types.BootstrapData) error
|
||||
InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error
|
||||
NextSyncPeriod() (uint64, bool)
|
||||
}
|
||||
|
||||
// CheckpointInit implements request.Module; it fetches the light client bootstrap
|
||||
// data belonging to the given checkpoint hash and initializes the committee chain
|
||||
// if successful.
|
||||
type CheckpointInit struct {
|
||||
chain committeeChain
|
||||
checkpointHash common.Hash
|
||||
locked request.ServerAndID
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// NewCheckpointInit creates a new CheckpointInit.
|
||||
func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit {
|
||||
return &CheckpointInit{
|
||||
chain: chain,
|
||||
checkpointHash: checkpointHash,
|
||||
}
|
||||
}
|
||||
|
||||
// Process implements request.Module.
|
||||
func (s *CheckpointInit) Process(requester request.Requester, events []request.Event) {
|
||||
for _, event := range events {
|
||||
if !event.IsRequestEvent() {
|
||||
continue
|
||||
}
|
||||
sid, req, resp := event.RequestInfo()
|
||||
if s.locked == sid {
|
||||
s.locked = request.ServerAndID{}
|
||||
}
|
||||
if resp != nil {
|
||||
if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) {
|
||||
s.chain.CheckpointInit(*checkpoint)
|
||||
s.initialized = true
|
||||
return
|
||||
}
|
||||
|
||||
requester.Fail(event.Server, "invalid checkpoint data")
|
||||
}
|
||||
}
|
||||
// start a request if possible
|
||||
if s.initialized || s.locked != (request.ServerAndID{}) {
|
||||
return
|
||||
}
|
||||
cs := requester.CanSendTo()
|
||||
if len(cs) == 0 {
|
||||
return
|
||||
}
|
||||
server := cs[0]
|
||||
id := requester.Send(server, ReqCheckpointData(s.checkpointHash))
|
||||
s.locked = request.ServerAndID{Server: server, ID: id}
|
||||
}
|
||||
|
||||
// ForwardUpdateSync implements request.Module; it fetches updates between the
|
||||
// committee chain head and each server's announced head. Updates are fetched
|
||||
// in batches and multiple batches can also be requested in parallel.
|
||||
// Out of order responses are also handled; if a batch of updates cannot be added
|
||||
// to the chain immediately because of a gap then the future updates are
|
||||
// remembered until they can be processed.
|
||||
type ForwardUpdateSync struct {
|
||||
chain committeeChain
|
||||
rangeLock rangeLock
|
||||
lockedIDs map[request.ServerAndID]struct{}
|
||||
processQueue []updateResponse
|
||||
nextSyncPeriod map[request.Server]uint64
|
||||
}
|
||||
|
||||
// NewForwardUpdateSync creates a new ForwardUpdateSync.
|
||||
func NewForwardUpdateSync(chain committeeChain) *ForwardUpdateSync {
|
||||
return &ForwardUpdateSync{
|
||||
chain: chain,
|
||||
rangeLock: make(rangeLock),
|
||||
lockedIDs: make(map[request.ServerAndID]struct{}),
|
||||
nextSyncPeriod: make(map[request.Server]uint64),
|
||||
}
|
||||
}
|
||||
|
||||
// rangeLock allows locking sections of an integer space, preventing the syncing
|
||||
// mechanism from making requests again for sections where a not timed out request
|
||||
// is already pending or where already fetched and unprocessed data is available.
|
||||
type rangeLock map[uint64]int
|
||||
|
||||
// lock locks or unlocks the given section, depending on the sign of the add parameter.
|
||||
func (r rangeLock) lock(first, count uint64, add int) {
|
||||
for i := first; i < first+count; i++ {
|
||||
if v := r[i] + add; v > 0 {
|
||||
r[i] = v
|
||||
} else {
|
||||
delete(r, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// firstUnlocked returns the first unlocked section starting at or after start
|
||||
// and not longer than maxCount.
|
||||
func (r rangeLock) firstUnlocked(start, maxCount uint64) (first, count uint64) {
|
||||
first = start
|
||||
for {
|
||||
if _, ok := r[first]; !ok {
|
||||
break
|
||||
}
|
||||
first++
|
||||
}
|
||||
for {
|
||||
count++
|
||||
if count == maxCount {
|
||||
break
|
||||
}
|
||||
if _, ok := r[first+count]; ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// lockRange locks the range belonging to the given update request, unless the
|
||||
// same request has already been locked
|
||||
func (s *ForwardUpdateSync) lockRange(sid request.ServerAndID, req ReqUpdates) {
|
||||
if _, ok := s.lockedIDs[sid]; ok {
|
||||
return
|
||||
}
|
||||
s.lockedIDs[sid] = struct{}{}
|
||||
s.rangeLock.lock(req.FirstPeriod, req.Count, 1)
|
||||
}
|
||||
|
||||
// unlockRange unlocks the range belonging to the given update request, unless
|
||||
// same request has already been unlocked
|
||||
func (s *ForwardUpdateSync) unlockRange(sid request.ServerAndID, req ReqUpdates) {
|
||||
if _, ok := s.lockedIDs[sid]; !ok {
|
||||
return
|
||||
}
|
||||
delete(s.lockedIDs, sid)
|
||||
s.rangeLock.lock(req.FirstPeriod, req.Count, -1)
|
||||
}
|
||||
|
||||
// verifyRange returns true if the number of updates and the individual update
|
||||
// periods in the response match the requested section.
|
||||
func (s *ForwardUpdateSync) verifyRange(request ReqUpdates, response RespUpdates) bool {
|
||||
if uint64(len(response.Updates)) != request.Count || uint64(len(response.Committees)) != request.Count {
|
||||
return false
|
||||
}
|
||||
for i, update := range response.Updates {
|
||||
if update.AttestedHeader.Header.SyncPeriod() != request.FirstPeriod+uint64(i) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// updateResponse is a response that has passed initial verification and has been
|
||||
// queued for processing. Note that an update response cannot be processed until
|
||||
// the previous updates have also been added to the chain.
|
||||
type updateResponse struct {
|
||||
sid request.ServerAndID
|
||||
request ReqUpdates
|
||||
response RespUpdates
|
||||
}
|
||||
|
||||
// updateResponseList implements sort.Sort and sorts update request/response events by FirstPeriod.
|
||||
type updateResponseList []updateResponse
|
||||
|
||||
func (u updateResponseList) Len() int { return len(u) }
|
||||
func (u updateResponseList) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
func (u updateResponseList) Less(i, j int) bool {
|
||||
return u[i].request.FirstPeriod < u[j].request.FirstPeriod
|
||||
}
|
||||
|
||||
// Process implements request.Module.
|
||||
func (s *ForwardUpdateSync) Process(requester request.Requester, events []request.Event) {
|
||||
for _, event := range events {
|
||||
switch event.Type {
|
||||
case request.EvResponse, request.EvFail, request.EvTimeout:
|
||||
sid, rq, rs := event.RequestInfo()
|
||||
req := rq.(ReqUpdates)
|
||||
var queued bool
|
||||
if event.Type == request.EvResponse {
|
||||
resp := rs.(RespUpdates)
|
||||
if s.verifyRange(req, resp) {
|
||||
// there is a response with a valid format; put it in the process queue
|
||||
s.processQueue = append(s.processQueue, updateResponse{sid: sid, request: req, response: resp})
|
||||
s.lockRange(sid, req)
|
||||
queued = true
|
||||
} else {
|
||||
requester.Fail(event.Server, "invalid update range")
|
||||
}
|
||||
}
|
||||
if !queued {
|
||||
s.unlockRange(sid, req)
|
||||
}
|
||||
case EvNewSignedHead:
|
||||
signedHead := event.Data.(types.SignedHeader)
|
||||
s.nextSyncPeriod[event.Server] = types.SyncPeriod(signedHead.SignatureSlot + 256)
|
||||
case request.EvUnregistered:
|
||||
delete(s.nextSyncPeriod, event.Server)
|
||||
}
|
||||
}
|
||||
|
||||
// try processing ordered list of available responses
|
||||
sort.Sort(updateResponseList(s.processQueue))
|
||||
for s.processQueue != nil {
|
||||
u := s.processQueue[0]
|
||||
if !s.processResponse(requester, u) {
|
||||
break
|
||||
}
|
||||
s.unlockRange(u.sid, u.request)
|
||||
s.processQueue = s.processQueue[1:]
|
||||
if len(s.processQueue) == 0 {
|
||||
s.processQueue = nil
|
||||
}
|
||||
}
|
||||
|
||||
// start new requests if possible
|
||||
startPeriod, chainInit := s.chain.NextSyncPeriod()
|
||||
if !chainInit {
|
||||
return
|
||||
}
|
||||
for {
|
||||
firstPeriod, maxCount := s.rangeLock.firstUnlocked(startPeriod, maxUpdateRequest)
|
||||
var (
|
||||
sendTo request.Server
|
||||
bestCount uint64
|
||||
)
|
||||
for _, server := range requester.CanSendTo() {
|
||||
nextPeriod := s.nextSyncPeriod[server]
|
||||
if nextPeriod <= firstPeriod {
|
||||
continue
|
||||
}
|
||||
count := maxCount
|
||||
if nextPeriod < firstPeriod+maxCount {
|
||||
count = nextPeriod - firstPeriod
|
||||
}
|
||||
if count > bestCount {
|
||||
sendTo, bestCount = server, count
|
||||
}
|
||||
}
|
||||
if sendTo == nil {
|
||||
return
|
||||
}
|
||||
req := ReqUpdates{FirstPeriod: firstPeriod, Count: bestCount}
|
||||
id := requester.Send(sendTo, req)
|
||||
s.lockRange(request.ServerAndID{Server: sendTo, ID: id}, req)
|
||||
}
|
||||
}
|
||||
|
||||
// processResponse adds the fetched updates and committees to the committee chain.
|
||||
// Returns true in case of full or partial success.
|
||||
func (s *ForwardUpdateSync) processResponse(requester request.Requester, u updateResponse) (success bool) {
|
||||
for i, update := range u.response.Updates {
|
||||
if err := s.chain.InsertUpdate(update, u.response.Committees[i]); err != nil {
|
||||
if err == light.ErrInvalidPeriod {
|
||||
// there is a gap in the update periods; stop processing without
|
||||
// failing and try again next time
|
||||
return
|
||||
}
|
||||
if err == light.ErrInvalidUpdate || err == light.ErrWrongCommitteeRoot || err == light.ErrCannotReorg {
|
||||
requester.Fail(u.sid.Server, "invalid update received")
|
||||
} else {
|
||||
log.Error("Unexpected InsertUpdate error", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
success = true
|
||||
}
|
||||
return
|
||||
}
|
||||
219
beacon/light/sync/update_sync_test.go
Normal file
219
beacon/light/sync/update_sync_test.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package sync
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/light/request"
|
||||
"github.com/ethereum/go-ethereum/beacon/types"
|
||||
)
|
||||
|
||||
func TestCheckpointInit(t *testing.T) {
|
||||
chain := &TestCommitteeChain{}
|
||||
checkpoint := &types.BootstrapData{Header: types.Header{Slot: 0x2000*4 + 0x1000}} // period 4
|
||||
checkpointHash := checkpoint.Header.Hash()
|
||||
chkInit := NewCheckpointInit(chain, checkpointHash)
|
||||
ts := NewTestScheduler(t, chkInit)
|
||||
// add 2 servers
|
||||
ts.AddServer(testServer1, 1)
|
||||
ts.AddServer(testServer2, 1)
|
||||
|
||||
// expect bootstrap request to server 1
|
||||
ts.Run(1, testServer1, ReqCheckpointData(checkpointHash))
|
||||
|
||||
// server 1 times out; expect request to server 2
|
||||
ts.RequestEvent(request.EvTimeout, ts.Request(1, 1), nil)
|
||||
ts.Run(2, testServer2, ReqCheckpointData(checkpointHash))
|
||||
|
||||
// invalid response from server 2; expect init state to still be false
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(2, 1), &types.BootstrapData{Header: types.Header{Slot: 123456}})
|
||||
ts.ExpFail(testServer2)
|
||||
ts.Run(3)
|
||||
chain.ExpInit(t, false)
|
||||
|
||||
// server 1 fails (hard timeout)
|
||||
ts.RequestEvent(request.EvFail, ts.Request(1, 1), nil)
|
||||
ts.Run(4)
|
||||
chain.ExpInit(t, false)
|
||||
|
||||
// server 3 is registered; expect bootstrap request to server 3
|
||||
ts.AddServer(testServer3, 1)
|
||||
ts.Run(5, testServer3, ReqCheckpointData(checkpointHash))
|
||||
|
||||
// valid response from server 3; expect chain to be initialized
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(5, 1), checkpoint)
|
||||
ts.Run(6)
|
||||
chain.ExpInit(t, true)
|
||||
}
|
||||
|
||||
func TestUpdateSyncParallel(t *testing.T) {
|
||||
chain := &TestCommitteeChain{}
|
||||
chain.SetNextSyncPeriod(0)
|
||||
updateSync := NewForwardUpdateSync(chain)
|
||||
ts := NewTestScheduler(t, updateSync)
|
||||
// add 2 servers, head at period 100; allow 3-3 parallel requests for each
|
||||
ts.AddServer(testServer1, 3)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer1, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000})
|
||||
ts.AddServer(testServer2, 3)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*100 + 0x1000})
|
||||
|
||||
// expect 6 requests to be sent
|
||||
ts.Run(1,
|
||||
testServer1, ReqUpdates{FirstPeriod: 0, Count: 8},
|
||||
testServer1, ReqUpdates{FirstPeriod: 8, Count: 8},
|
||||
testServer1, ReqUpdates{FirstPeriod: 16, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 24, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 32, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 40, Count: 8})
|
||||
|
||||
// valid response to request 1; expect 8 periods synced and a new request started
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 1), testRespUpdate(ts.Request(1, 1)))
|
||||
ts.AddAllowance(testServer1, 1)
|
||||
ts.Run(2, testServer1, ReqUpdates{FirstPeriod: 48, Count: 8})
|
||||
chain.ExpNextSyncPeriod(t, 8)
|
||||
|
||||
// valid response to requests 4 and 5
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 4), testRespUpdate(ts.Request(1, 4)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 5), testRespUpdate(ts.Request(1, 5)))
|
||||
ts.AddAllowance(testServer2, 2)
|
||||
// expect 2 more requests but no sync progress (responses 4 and 5 cannot be added before 2 and 3)
|
||||
ts.Run(3,
|
||||
testServer2, ReqUpdates{FirstPeriod: 56, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 64, Count: 8})
|
||||
chain.ExpNextSyncPeriod(t, 8)
|
||||
|
||||
// soft timeout for requests 2 and 3 (server 1 is overloaded)
|
||||
ts.RequestEvent(request.EvTimeout, ts.Request(1, 2), nil)
|
||||
ts.RequestEvent(request.EvTimeout, ts.Request(1, 3), nil)
|
||||
// no allowance, no more requests
|
||||
ts.Run(4)
|
||||
|
||||
// valid response to requests 6 and 8 and 9
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 6), testRespUpdate(ts.Request(1, 6)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(3, 1), testRespUpdate(ts.Request(3, 1)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(3, 2), testRespUpdate(ts.Request(3, 2)))
|
||||
ts.AddAllowance(testServer2, 3)
|
||||
// server 2 can now resend requests 2 and 3 (timed out by server 1) and also send a new one
|
||||
ts.Run(5,
|
||||
testServer2, ReqUpdates{FirstPeriod: 8, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 16, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 72, Count: 8})
|
||||
|
||||
// server 1 finally answers timed out request 2
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 2), testRespUpdate(ts.Request(1, 2)))
|
||||
ts.AddAllowance(testServer1, 1)
|
||||
// expect sync progress and one new request
|
||||
ts.Run(6, testServer1, ReqUpdates{FirstPeriod: 80, Count: 8})
|
||||
chain.ExpNextSyncPeriod(t, 16)
|
||||
|
||||
// server 2 answers requests 11 and 12 (resends of requests 2 and 3)
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(5, 1), testRespUpdate(ts.Request(5, 1)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(5, 2), testRespUpdate(ts.Request(5, 2)))
|
||||
ts.AddAllowance(testServer2, 2)
|
||||
ts.Run(7,
|
||||
testServer2, ReqUpdates{FirstPeriod: 88, Count: 8},
|
||||
testServer2, ReqUpdates{FirstPeriod: 96, Count: 4})
|
||||
// finally the gap is filled, update can process responses up to req6
|
||||
chain.ExpNextSyncPeriod(t, 48)
|
||||
|
||||
// all remaining requests are answered
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 3), testRespUpdate(ts.Request(1, 3)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testRespUpdate(ts.Request(2, 1)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(5, 3), testRespUpdate(ts.Request(5, 3)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(6, 1), testRespUpdate(ts.Request(6, 1)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testRespUpdate(ts.Request(7, 1)))
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(7, 2), testRespUpdate(ts.Request(7, 2)))
|
||||
ts.Run(8)
|
||||
// expect chain to be fully synced
|
||||
chain.ExpNextSyncPeriod(t, 100)
|
||||
}
|
||||
|
||||
func TestUpdateSyncDifferentHeads(t *testing.T) {
|
||||
chain := &TestCommitteeChain{}
|
||||
chain.SetNextSyncPeriod(10)
|
||||
updateSync := NewForwardUpdateSync(chain)
|
||||
ts := NewTestScheduler(t, updateSync)
|
||||
// add 3 servers with different announced head periods
|
||||
ts.AddServer(testServer1, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer1, types.SignedHeader{SignatureSlot: 0x2000*15 + 0x1000})
|
||||
ts.AddServer(testServer2, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer2, types.SignedHeader{SignatureSlot: 0x2000*16 + 0x1000})
|
||||
ts.AddServer(testServer3, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer3, types.SignedHeader{SignatureSlot: 0x2000*17 + 0x1000})
|
||||
|
||||
// expect request to the best announced head
|
||||
ts.Run(1, testServer3, ReqUpdates{FirstPeriod: 10, Count: 7})
|
||||
|
||||
// request times out, expect request to the next best head
|
||||
ts.RequestEvent(request.EvTimeout, ts.Request(1, 1), nil)
|
||||
ts.Run(2, testServer2, ReqUpdates{FirstPeriod: 10, Count: 6})
|
||||
|
||||
// request times out, expect request to the last available server
|
||||
ts.RequestEvent(request.EvTimeout, ts.Request(2, 1), nil)
|
||||
ts.Run(3, testServer1, ReqUpdates{FirstPeriod: 10, Count: 5})
|
||||
|
||||
// valid response to request 3, expect chain synced to period 15
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(3, 1), testRespUpdate(ts.Request(3, 1)))
|
||||
ts.AddAllowance(testServer1, 1)
|
||||
ts.Run(4)
|
||||
chain.ExpNextSyncPeriod(t, 15)
|
||||
|
||||
// invalid response to request 1, server can only deliver updates up to period 15 despite announced head
|
||||
truncated := ts.Request(1, 1)
|
||||
truncated.request = ReqUpdates{FirstPeriod: 10, Count: 5}
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(1, 1), testRespUpdate(truncated))
|
||||
ts.ExpFail(testServer3)
|
||||
ts.Run(5)
|
||||
// expect no progress of chain head
|
||||
chain.ExpNextSyncPeriod(t, 15)
|
||||
|
||||
// valid response to request 2, expect chain synced to period 16
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testRespUpdate(ts.Request(2, 1)))
|
||||
ts.AddAllowance(testServer2, 1)
|
||||
ts.Run(6)
|
||||
chain.ExpNextSyncPeriod(t, 16)
|
||||
|
||||
// a new server is registered with announced head period 17
|
||||
ts.AddServer(testServer4, 1)
|
||||
ts.ServerEvent(EvNewSignedHead, testServer4, types.SignedHeader{SignatureSlot: 0x2000*17 + 0x1000})
|
||||
// expect request to sync one more period
|
||||
ts.Run(7, testServer4, ReqUpdates{FirstPeriod: 16, Count: 1})
|
||||
|
||||
// valid response, expect chain synced to period 17
|
||||
ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testRespUpdate(ts.Request(7, 1)))
|
||||
ts.AddAllowance(testServer4, 1)
|
||||
ts.Run(8)
|
||||
chain.ExpNextSyncPeriod(t, 17)
|
||||
}
|
||||
|
||||
func testRespUpdate(request requestWithID) request.Response {
|
||||
var resp RespUpdates
|
||||
if request.request == nil {
|
||||
return resp
|
||||
}
|
||||
req := request.request.(ReqUpdates)
|
||||
resp.Updates = make([]*types.LightClientUpdate, int(req.Count))
|
||||
resp.Committees = make([]*types.SerializedSyncCommittee, int(req.Count))
|
||||
period := req.FirstPeriod
|
||||
for i := range resp.Updates {
|
||||
resp.Updates[i] = &types.LightClientUpdate{AttestedHeader: types.SignedHeader{Header: types.Header{Slot: 0x2000*period + 0x1000}}}
|
||||
resp.Committees[i] = new(types.SerializedSyncCommittee)
|
||||
period++
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
|
@ -41,4 +41,6 @@ const (
|
|||
StateIndexNextSyncCommittee = 55
|
||||
StateIndexExecPayload = 56
|
||||
StateIndexExecHead = 908
|
||||
|
||||
BodyIndexExecPayload = 25
|
||||
)
|
||||
|
|
|
|||
110
beacon/types/beacon_block.go
Normal file
110
beacon/types/beacon_block.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/capella"
|
||||
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/deneb"
|
||||
"github.com/protolambda/zrnt/eth2/configs"
|
||||
"github.com/protolambda/ztyp/tree"
|
||||
)
|
||||
|
||||
type blockObject interface {
|
||||
HashTreeRoot(spec *zrntcommon.Spec, hFn tree.HashFn) zrntcommon.Root
|
||||
Header(spec *zrntcommon.Spec) *zrntcommon.BeaconBlockHeader
|
||||
}
|
||||
|
||||
// BeaconBlock represents a full block in the beacon chain.
|
||||
type BeaconBlock struct {
|
||||
blockObj blockObject
|
||||
}
|
||||
|
||||
// BlockFromJSON decodes a beacon block from JSON.
|
||||
func BlockFromJSON(forkName string, data []byte) (*BeaconBlock, error) {
|
||||
var obj blockObject
|
||||
switch forkName {
|
||||
case "deneb":
|
||||
obj = new(deneb.BeaconBlock)
|
||||
case "capella":
|
||||
obj = new(capella.BeaconBlock)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported fork: " + forkName)
|
||||
}
|
||||
if err := json.Unmarshal(data, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BeaconBlock{obj}, nil
|
||||
}
|
||||
|
||||
// NewBeaconBlock wraps a ZRNT block.
|
||||
func NewBeaconBlock(obj blockObject) *BeaconBlock {
|
||||
switch obj := obj.(type) {
|
||||
case *capella.BeaconBlock:
|
||||
return &BeaconBlock{obj}
|
||||
case *deneb.BeaconBlock:
|
||||
return &BeaconBlock{obj}
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported block type %T", obj))
|
||||
}
|
||||
}
|
||||
|
||||
// Slot returns the slot number of the block.
|
||||
func (b *BeaconBlock) Slot() uint64 {
|
||||
switch obj := b.blockObj.(type) {
|
||||
case *capella.BeaconBlock:
|
||||
return uint64(obj.Slot)
|
||||
case *deneb.BeaconBlock:
|
||||
return uint64(obj.Slot)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported block type %T", b.blockObj))
|
||||
}
|
||||
}
|
||||
|
||||
// ExecutionPayload parses and returns the execution payload of the block.
|
||||
func (b *BeaconBlock) ExecutionPayload() (*types.Block, error) {
|
||||
switch obj := b.blockObj.(type) {
|
||||
case *capella.BeaconBlock:
|
||||
return convertPayload(&obj.Body.ExecutionPayload, &obj.ParentRoot)
|
||||
case *deneb.BeaconBlock:
|
||||
return convertPayload(&obj.Body.ExecutionPayload, &obj.ParentRoot)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported block type %T", b.blockObj))
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns the block's header data.
|
||||
func (b *BeaconBlock) Header() Header {
|
||||
switch obj := b.blockObj.(type) {
|
||||
case *capella.BeaconBlock:
|
||||
return headerFromZRNT(obj.Header(configs.Mainnet))
|
||||
case *deneb.BeaconBlock:
|
||||
return headerFromZRNT(obj.Header(configs.Mainnet))
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported block type %T", b.blockObj))
|
||||
}
|
||||
}
|
||||
|
||||
// Root computes the SSZ root hash of the block.
|
||||
func (b *BeaconBlock) Root() common.Hash {
|
||||
return common.Hash(b.blockObj.HashTreeRoot(configs.Mainnet, tree.GetHashFn()))
|
||||
}
|
||||
77
beacon/types/beacon_block_test.go
Normal file
77
beacon/types/beacon_block_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
func TestBlockFromJSON(t *testing.T) {
|
||||
type blocktest struct {
|
||||
file string
|
||||
version string
|
||||
wantSlot uint64
|
||||
wantBlockNumber uint64
|
||||
wantBlockHash common.Hash
|
||||
}
|
||||
tests := []blocktest{
|
||||
{
|
||||
file: "block_deneb.json",
|
||||
version: "deneb",
|
||||
wantSlot: 8631513,
|
||||
wantBlockNumber: 19431837,
|
||||
wantBlockHash: common.HexToHash("0x4cf7d9108fc01b50023ab7cab9b372a96068fddcadec551630393b65acb1f34c"),
|
||||
},
|
||||
{
|
||||
file: "block_capella.json",
|
||||
version: "capella",
|
||||
wantSlot: 7378495,
|
||||
wantBlockNumber: 18189758,
|
||||
wantBlockHash: common.HexToHash("0x802acf5c350f4252e31d83c431fcb259470250fa0edf49e8391cfee014239820"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("testdata", test.file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beaconBlock, err := BlockFromJSON(test.version, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if beaconBlock.Slot() != test.wantSlot {
|
||||
t.Errorf("wrong slot number %d", beaconBlock.Slot())
|
||||
}
|
||||
execBlock, err := beaconBlock.ExecutionPayload()
|
||||
if err != nil {
|
||||
t.Fatalf("payload extraction failed: %v", err)
|
||||
}
|
||||
if execBlock.NumberU64() != test.wantBlockNumber {
|
||||
t.Errorf("wrong block number: %v", execBlock.NumberU64())
|
||||
}
|
||||
if execBlock.Hash() != test.wantBlockHash {
|
||||
t.Errorf("wrong block hash: %v", execBlock.Hash())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ const syncCommitteeDomain = 7
|
|||
// Fork describes a single beacon chain fork and also stores the calculated
|
||||
// signature domain used after this fork.
|
||||
type Fork struct {
|
||||
// Name of the fork in the chain config (config.yaml) file{
|
||||
// Name of the fork in the chain config (config.yaml) file
|
||||
Name string
|
||||
|
||||
// Epoch when given fork version is activated
|
||||
|
|
@ -110,6 +110,16 @@ type ChainConfig struct {
|
|||
Forks Forks
|
||||
}
|
||||
|
||||
// ForkAtEpoch returns the latest active fork at the given epoch.
|
||||
func (c *ChainConfig) ForkAtEpoch(epoch uint64) Fork {
|
||||
for i := len(c.Forks) - 1; i >= 0; i-- {
|
||||
if c.Forks[i].Epoch <= epoch {
|
||||
return *c.Forks[i]
|
||||
}
|
||||
}
|
||||
return Fork{}
|
||||
}
|
||||
|
||||
// AddFork adds a new item to the list of forks.
|
||||
func (c *ChainConfig) AddFork(name string, epoch uint64, version []byte) *ChainConfig {
|
||||
fork := &Fork{
|
||||
|
|
|
|||
80
beacon/types/exec_header.go
Normal file
80
beacon/types/exec_header.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/capella"
|
||||
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/deneb"
|
||||
"github.com/protolambda/ztyp/tree"
|
||||
)
|
||||
|
||||
type headerObject interface {
|
||||
HashTreeRoot(hFn tree.HashFn) zrntcommon.Root
|
||||
}
|
||||
|
||||
type ExecutionHeader struct {
|
||||
obj headerObject
|
||||
}
|
||||
|
||||
// HeaderFromJSON decodes an execution header from JSON data provided by
|
||||
// the beacon chain API.
|
||||
func ExecutionHeaderFromJSON(forkName string, data []byte) (*ExecutionHeader, error) {
|
||||
var obj headerObject
|
||||
switch forkName {
|
||||
case "capella":
|
||||
obj = new(capella.ExecutionPayloadHeader)
|
||||
case "deneb":
|
||||
obj = new(deneb.ExecutionPayloadHeader)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported fork: " + forkName)
|
||||
}
|
||||
if err := json.Unmarshal(data, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ExecutionHeader{obj: obj}, nil
|
||||
}
|
||||
|
||||
func NewExecutionHeader(obj headerObject) *ExecutionHeader {
|
||||
switch obj.(type) {
|
||||
case *capella.ExecutionPayloadHeader:
|
||||
case *deneb.ExecutionPayloadHeader:
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
|
||||
}
|
||||
return &ExecutionHeader{obj: obj}
|
||||
}
|
||||
|
||||
func (eh *ExecutionHeader) PayloadRoot() merkle.Value {
|
||||
return merkle.Value(eh.obj.HashTreeRoot(tree.GetHashFn()))
|
||||
}
|
||||
|
||||
func (eh *ExecutionHeader) BlockHash() common.Hash {
|
||||
switch obj := eh.obj.(type) {
|
||||
case *capella.ExecutionPayloadHeader:
|
||||
return common.Hash(obj.BlockHash)
|
||||
case *deneb.ExecutionPayloadHeader:
|
||||
return common.Hash(obj.BlockHash)
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
|
||||
}
|
||||
}
|
||||
144
beacon/types/exec_payload.go
Normal file
144
beacon/types/exec_payload.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2024 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/capella"
|
||||
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
|
||||
"github.com/protolambda/zrnt/eth2/beacon/deneb"
|
||||
)
|
||||
|
||||
type payloadType interface {
|
||||
*capella.ExecutionPayload | *deneb.ExecutionPayload
|
||||
}
|
||||
|
||||
// convertPayload converts a beacon chain execution payload to types.Block.
|
||||
func convertPayload[T payloadType](payload T, parentRoot *zrntcommon.Root) (*types.Block, error) {
|
||||
var (
|
||||
header types.Header
|
||||
transactions []*types.Transaction
|
||||
withdrawals []*types.Withdrawal
|
||||
expectedHash [32]byte
|
||||
err error
|
||||
)
|
||||
switch p := any(payload).(type) {
|
||||
case *capella.ExecutionPayload:
|
||||
convertCapellaHeader(p, &header)
|
||||
transactions, err = convertTransactions(p.Transactions, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withdrawals = convertWithdrawals(p.Withdrawals, &header)
|
||||
expectedHash = p.BlockHash
|
||||
case *deneb.ExecutionPayload:
|
||||
convertDenebHeader(p, common.Hash(*parentRoot), &header)
|
||||
transactions, err = convertTransactions(p.Transactions, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
withdrawals = convertWithdrawals(p.Withdrawals, &header)
|
||||
expectedHash = p.BlockHash
|
||||
default:
|
||||
panic("unsupported block type")
|
||||
}
|
||||
|
||||
block := types.NewBlockWithHeader(&header)
|
||||
block = block.WithBody(transactions, nil)
|
||||
block = block.WithWithdrawals(withdrawals)
|
||||
hash := block.Hash()
|
||||
if hash != expectedHash {
|
||||
return block, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", expectedHash, hash)
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func convertCapellaHeader(payload *capella.ExecutionPayload, h *types.Header) {
|
||||
// note: h.TxHash is set in convertTransactions
|
||||
h.ParentHash = common.Hash(payload.ParentHash)
|
||||
h.UncleHash = types.EmptyUncleHash
|
||||
h.Coinbase = common.Address(payload.FeeRecipient)
|
||||
h.Root = common.Hash(payload.StateRoot)
|
||||
h.ReceiptHash = common.Hash(payload.ReceiptsRoot)
|
||||
h.Bloom = types.Bloom(payload.LogsBloom)
|
||||
h.Difficulty = common.Big0
|
||||
h.Number = new(big.Int).SetUint64(uint64(payload.BlockNumber))
|
||||
h.GasLimit = uint64(payload.GasLimit)
|
||||
h.GasUsed = uint64(payload.GasUsed)
|
||||
h.Time = uint64(payload.Timestamp)
|
||||
h.Extra = []byte(payload.ExtraData)
|
||||
h.MixDigest = common.Hash(payload.PrevRandao)
|
||||
h.Nonce = types.BlockNonce{}
|
||||
h.BaseFee = (*uint256.Int)(&payload.BaseFeePerGas).ToBig()
|
||||
}
|
||||
|
||||
func convertDenebHeader(payload *deneb.ExecutionPayload, parentRoot common.Hash, h *types.Header) {
|
||||
// note: h.TxHash is set in convertTransactions
|
||||
h.ParentHash = common.Hash(payload.ParentHash)
|
||||
h.UncleHash = types.EmptyUncleHash
|
||||
h.Coinbase = common.Address(payload.FeeRecipient)
|
||||
h.Root = common.Hash(payload.StateRoot)
|
||||
h.ReceiptHash = common.Hash(payload.ReceiptsRoot)
|
||||
h.Bloom = types.Bloom(payload.LogsBloom)
|
||||
h.Difficulty = common.Big0
|
||||
h.Number = new(big.Int).SetUint64(uint64(payload.BlockNumber))
|
||||
h.GasLimit = uint64(payload.GasLimit)
|
||||
h.GasUsed = uint64(payload.GasUsed)
|
||||
h.Time = uint64(payload.Timestamp)
|
||||
h.Extra = []byte(payload.ExtraData)
|
||||
h.MixDigest = common.Hash(payload.PrevRandao)
|
||||
h.Nonce = types.BlockNonce{}
|
||||
h.BaseFee = (*uint256.Int)(&payload.BaseFeePerGas).ToBig()
|
||||
// new in deneb
|
||||
h.BlobGasUsed = (*uint64)(&payload.BlobGasUsed)
|
||||
h.ExcessBlobGas = (*uint64)(&payload.ExcessBlobGas)
|
||||
h.ParentBeaconRoot = &parentRoot
|
||||
}
|
||||
|
||||
func convertTransactions(list zrntcommon.PayloadTransactions, execHeader *types.Header) ([]*types.Transaction, error) {
|
||||
txs := make([]*types.Transaction, len(list))
|
||||
for i, opaqueTx := range list {
|
||||
var tx types.Transaction
|
||||
if err := tx.UnmarshalBinary(opaqueTx); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse tx %d: %v", i, err)
|
||||
}
|
||||
txs[i] = &tx
|
||||
}
|
||||
execHeader.TxHash = types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil))
|
||||
return txs, nil
|
||||
}
|
||||
|
||||
func convertWithdrawals(list zrntcommon.Withdrawals, execHeader *types.Header) []*types.Withdrawal {
|
||||
withdrawals := make([]*types.Withdrawal, len(list))
|
||||
for i, w := range list {
|
||||
withdrawals[i] = &types.Withdrawal{
|
||||
Index: uint64(w.Index),
|
||||
Validator: uint64(w.ValidatorIndex),
|
||||
Address: common.Address(w.Address),
|
||||
Amount: uint64(w.Amount),
|
||||
}
|
||||
}
|
||||
wroot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil))
|
||||
execHeader.WithdrawalsHash = &wroot
|
||||
return withdrawals
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
zrntcommon "github.com/protolambda/zrnt/eth2/beacon/common"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go
|
||||
|
|
@ -57,6 +58,16 @@ type Header struct {
|
|||
BodyRoot common.Hash `gencodec:"required" json:"body_root"`
|
||||
}
|
||||
|
||||
func headerFromZRNT(zh *zrntcommon.BeaconBlockHeader) Header {
|
||||
return Header{
|
||||
Slot: uint64(zh.Slot),
|
||||
ProposerIndex: uint64(zh.ProposerIndex),
|
||||
ParentRoot: common.Hash(zh.ParentRoot),
|
||||
StateRoot: common.Hash(zh.StateRoot),
|
||||
BodyRoot: common.Hash(zh.BodyRoot),
|
||||
}
|
||||
}
|
||||
|
||||
// headerMarshaling is a field type overrides for gencodec.
|
||||
type headerMarshaling struct {
|
||||
Slot common.Decimal
|
||||
|
|
|
|||
|
|
@ -23,8 +23,15 @@ import (
|
|||
"github.com/ethereum/go-ethereum/beacon/merkle"
|
||||
"github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// HeadInfo represents an unvalidated new head announcement.
|
||||
type HeadInfo struct {
|
||||
Slot uint64
|
||||
BlockRoot common.Hash
|
||||
}
|
||||
|
||||
// BootstrapData contains a sync committee where light sync can be started,
|
||||
// together with a proof through a beacon header and corresponding state.
|
||||
// Note: BootstrapData is fetched from a server based on a known checkpoint hash.
|
||||
|
|
@ -134,3 +141,51 @@ func (u UpdateScore) BetterThan(w UpdateScore) bool {
|
|||
}
|
||||
return u.SignerCount > w.SignerCount
|
||||
}
|
||||
|
||||
type HeaderWithExecProof struct {
|
||||
Header
|
||||
PayloadHeader *ExecutionHeader
|
||||
PayloadBranch merkle.Values
|
||||
}
|
||||
|
||||
func (h *HeaderWithExecProof) Validate() error {
|
||||
payloadRoot := h.PayloadHeader.PayloadRoot()
|
||||
return merkle.VerifyProof(h.BodyRoot, params.BodyIndexExecPayload, h.PayloadBranch, payloadRoot)
|
||||
}
|
||||
|
||||
type FinalityUpdate struct {
|
||||
Attested, Finalized HeaderWithExecProof
|
||||
FinalityBranch merkle.Values
|
||||
// Sync committee BLS signature aggregate
|
||||
Signature SyncAggregate
|
||||
// Slot in which the signature has been created (newer than Header.Slot,
|
||||
// determines the signing sync committee)
|
||||
SignatureSlot uint64
|
||||
}
|
||||
|
||||
func (u *FinalityUpdate) SignedHeader() SignedHeader {
|
||||
return SignedHeader{
|
||||
Header: u.Attested.Header,
|
||||
Signature: u.Signature,
|
||||
SignatureSlot: u.SignatureSlot,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *FinalityUpdate) Validate() error {
|
||||
if err := u.Attested.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.Finalized.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return merkle.VerifyProof(u.Attested.StateRoot, params.StateIndexFinalBlock, u.FinalityBranch, merkle.Value(u.Finalized.Hash()))
|
||||
}
|
||||
|
||||
// ChainHeadEvent returns an authenticated execution payload associated with the
|
||||
// latest accepted head of the beacon chain, along with the hash of the latest
|
||||
// finalized execution block.
|
||||
type ChainHeadEvent struct {
|
||||
BeaconHead Header
|
||||
Block *types.Block
|
||||
Finalized common.Hash
|
||||
}
|
||||
|
|
|
|||
1703
beacon/types/testdata/block_capella.json
vendored
Normal file
1703
beacon/types/testdata/block_capella.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
2644
beacon/types/testdata/block_deneb.json
vendored
Normal file
2644
beacon/types/testdata/block_deneb.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -5,22 +5,22 @@
|
|||
# https://github.com/ethereum/execution-spec-tests/releases/download/v2.1.0/
|
||||
ca89c76851b0900bfcc3cbb9a26cbece1f3d7c64a3bed38723e914713290df6c fixtures_develop.tar.gz
|
||||
|
||||
# version:golang 1.21.6
|
||||
# version:golang 1.22.1
|
||||
# https://go.dev/dl/
|
||||
124926a62e45f78daabbaedb9c011d97633186a33c238ffc1e25320c02046248 go1.21.6.src.tar.gz
|
||||
31d6ecca09010ab351e51343a5af81d678902061fee871f912bdd5ef4d778850 go1.21.6.darwin-amd64.tar.gz
|
||||
0ff541fb37c38e5e5c5bcecc8f4f43c5ffd5e3a6c33a5d3e4003ded66fcfb331 go1.21.6.darwin-arm64.tar.gz
|
||||
a1d1a149b34bf0f53965a237682c6da1140acabb131bf0e597240e4a140b0e5e go1.21.6.freebsd-386.tar.gz
|
||||
de59e1217e4398b1522eed8dddabab2fa1b97aecbdca3af08e34832b4f0e3f81 go1.21.6.freebsd-amd64.tar.gz
|
||||
05d09041b5a1193c14e4b2db3f7fcc649b236c567f5eb93305c537851b72dd95 go1.21.6.linux-386.tar.gz
|
||||
3f934f40ac360b9c01f616a9aa1796d227d8b0328bf64cb045c7b8c4ee9caea4 go1.21.6.linux-amd64.tar.gz
|
||||
e2e8aa88e1b5170a0d495d7d9c766af2b2b6c6925a8f8956d834ad6b4cacbd9a go1.21.6.linux-arm64.tar.gz
|
||||
6a8eda6cc6a799ff25e74ce0c13fdc1a76c0983a0bb07c789a2a3454bf6ec9b2 go1.21.6.linux-armv6l.tar.gz
|
||||
e872b1e9a3f2f08fd4554615a32ca9123a4ba877ab6d19d36abc3424f86bc07f go1.21.6.linux-ppc64le.tar.gz
|
||||
92894d0f732d3379bc414ffdd617eaadad47e1d72610e10d69a1156db03fc052 go1.21.6.linux-s390x.tar.gz
|
||||
65b38857135cf45c80e1d267e0ce4f80fe149326c68835217da4f2da9b7943fe go1.21.6.windows-386.zip
|
||||
27ac9dd6e66fb3fd0acfa6792ff053c86e7d2c055b022f4b5d53bfddec9e3301 go1.21.6.windows-amd64.zip
|
||||
b93aff8f3c882c764c66a39b7a1483b0460e051e9992bf3435479129e5051bcd go1.21.6.windows-arm64.zip
|
||||
79c9b91d7f109515a25fc3ecdaad125d67e6bdb54f6d4d98580f46799caea321 go1.22.1.src.tar.gz
|
||||
3bc971772f4712fec0364f4bc3de06af22a00a12daab10b6f717fdcd13156cc0 go1.22.1.darwin-amd64.tar.gz
|
||||
f6a9cec6b8a002fcc9c0ee24ec04d67f430a52abc3cfd613836986bcc00d8383 go1.22.1.darwin-arm64.tar.gz
|
||||
99f81c10d5a3f8a886faf8fa86aaa2aaf929fbed54a972ae5eec3c5e0bdb961a go1.22.1.freebsd-386.tar.gz
|
||||
51c614ddd92ee4a9913a14c39bf80508d9cfba08561f24d2f075fd00f3cfb067 go1.22.1.freebsd-amd64.tar.gz
|
||||
8484df36d3d40139eaf0fe5e647b006435d826cc12f9ae72973bf7ec265e0ae4 go1.22.1.linux-386.tar.gz
|
||||
aab8e15785c997ae20f9c88422ee35d962c4562212bb0f879d052a35c8307c7f go1.22.1.linux-amd64.tar.gz
|
||||
e56685a245b6a0c592fc4a55f0b7803af5b3f827aaa29feab1f40e491acf35b8 go1.22.1.linux-arm64.tar.gz
|
||||
8cb7a90e48c20daed39a6ac8b8a40760030ba5e93c12274c42191d868687c281 go1.22.1.linux-armv6l.tar.gz
|
||||
ac775e19d93cc1668999b77cfe8c8964abfbc658718feccfe6e0eb87663cd668 go1.22.1.linux-ppc64le.tar.gz
|
||||
7bb7dd8e10f95c9a4cc4f6bef44c816a6e7c9e03f56ac6af6efbb082b19b379f go1.22.1.linux-s390x.tar.gz
|
||||
0c5ebb7eb39b7884ec99f92b425d4c03a96a72443562aafbf6e7d15c42a3108a go1.22.1.windows-386.zip
|
||||
cf9c66a208a106402a527f5b956269ca506cfe535fc388e828d249ea88ed28ba go1.22.1.windows-amd64.zip
|
||||
85b8511b298c9f4199ecae26afafcc3d46155bac934d43f2357b9224bcaa310f go1.22.1.windows-arm64.zip
|
||||
|
||||
# version:golangci 1.55.2
|
||||
# https://github.com/golangci/golangci-lint/releases/
|
||||
|
|
|
|||
122
cmd/blsync/main.go
Normal file
122
cmd/blsync/main.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright 2022 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/blsync"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/internal/flags"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
verbosityFlag = &cli.IntFlag{
|
||||
Name: "verbosity",
|
||||
Usage: "Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail",
|
||||
Value: 3,
|
||||
Category: flags.LoggingCategory,
|
||||
}
|
||||
vmoduleFlag = &cli.StringFlag{
|
||||
Name: "vmodule",
|
||||
Usage: "Per-module verbosity: comma-separated list of <pattern>=<level> (e.g. eth/*=5,p2p=4)",
|
||||
Value: "",
|
||||
Hidden: true,
|
||||
Category: flags.LoggingCategory,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := flags.NewApp("beacon light syncer tool")
|
||||
app.Flags = []cli.Flag{
|
||||
utils.BeaconApiFlag,
|
||||
utils.BeaconApiHeaderFlag,
|
||||
utils.BeaconThresholdFlag,
|
||||
utils.BeaconNoFilterFlag,
|
||||
utils.BeaconConfigFlag,
|
||||
utils.BeaconGenesisRootFlag,
|
||||
utils.BeaconGenesisTimeFlag,
|
||||
utils.BeaconCheckpointFlag,
|
||||
//TODO datadir for optional permanent database
|
||||
utils.MainnetFlag,
|
||||
utils.SepoliaFlag,
|
||||
utils.GoerliFlag,
|
||||
utils.BlsyncApiFlag,
|
||||
utils.BlsyncJWTSecretFlag,
|
||||
verbosityFlag,
|
||||
vmoduleFlag,
|
||||
}
|
||||
app.Action = sync
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func sync(ctx *cli.Context) error {
|
||||
usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
|
||||
output := io.Writer(os.Stderr)
|
||||
if usecolor {
|
||||
output = colorable.NewColorable(os.Stderr)
|
||||
}
|
||||
verbosity := log.FromLegacyLevel(ctx.Int(verbosityFlag.Name))
|
||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor)))
|
||||
|
||||
// set up blsync
|
||||
client := blsync.NewClient(ctx)
|
||||
client.SetEngineRPC(makeRPCClient(ctx))
|
||||
client.Start()
|
||||
|
||||
// run until stopped
|
||||
<-ctx.Done()
|
||||
client.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRPCClient(ctx *cli.Context) *rpc.Client {
|
||||
if !ctx.IsSet(utils.BlsyncApiFlag.Name) {
|
||||
log.Warn("No engine API target specified, performing a dry run")
|
||||
return nil
|
||||
}
|
||||
if !ctx.IsSet(utils.BlsyncJWTSecretFlag.Name) {
|
||||
utils.Fatalf("JWT secret parameter missing") //TODO use default if datadir is specified
|
||||
}
|
||||
|
||||
engineApiUrl, jwtFileName := ctx.String(utils.BlsyncApiFlag.Name), ctx.String(utils.BlsyncJWTSecretFlag.Name)
|
||||
var jwtSecret [32]byte
|
||||
if jwt, err := node.ObtainJWTSecret(jwtFileName); err == nil {
|
||||
copy(jwtSecret[:], jwt)
|
||||
} else {
|
||||
utils.Fatalf("Error loading or generating JWT secret: %v", err)
|
||||
}
|
||||
auth := node.NewJWTAuth(jwtSecret)
|
||||
cl, err := rpc.DialOptions(context.Background(), engineApiUrl, rpc.WithHTTPAuth(auth))
|
||||
if err != nil {
|
||||
utils.Fatalf("Could not create RPC client: %v", err)
|
||||
}
|
||||
return cl
|
||||
}
|
||||
|
|
@ -183,8 +183,8 @@ var attrFormatters = map[string]func(rlp.RawValue) (string, bool){
|
|||
}
|
||||
|
||||
func formatAttrRaw(v rlp.RawValue) (string, bool) {
|
||||
s := hex.EncodeToString(v)
|
||||
return s, true
|
||||
content, _, err := rlp.SplitString(v)
|
||||
return hex.EncodeToString(content), err == nil
|
||||
}
|
||||
|
||||
func formatAttrString(v rlp.RawValue) (string, bool) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
|
@ -56,21 +56,21 @@ type Chain struct {
|
|||
// NewChain takes the given chain.rlp file, and decodes and returns
|
||||
// the blocks from the file.
|
||||
func NewChain(dir string) (*Chain, error) {
|
||||
gen, err := loadGenesis(path.Join(dir, "genesis.json"))
|
||||
gen, err := loadGenesis(filepath.Join(dir, "genesis.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gblock := gen.ToBlock()
|
||||
|
||||
blocks, err := blocksFromFile(path.Join(dir, "chain.rlp"), gblock)
|
||||
blocks, err := blocksFromFile(filepath.Join(dir, "chain.rlp"), gblock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state, err := readState(path.Join(dir, "headstate.json"))
|
||||
state, err := readState(filepath.Join(dir, "headstate.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts, err := readAccounts(path.Join(dir, "accounts.json"))
|
||||
accounts, err := readAccounts(filepath.Join(dir, "accounts.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -38,7 +38,7 @@ type EngineClient struct {
|
|||
|
||||
// NewEngineClient creates a new engine client.
|
||||
func NewEngineClient(dir, url, jwt string) (*EngineClient, error) {
|
||||
headfcu, err := os.ReadFile(path.Join(dir, "headfcu.json"))
|
||||
headfcu, err := os.ReadFile(filepath.Join(dir, "headfcu.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read headfcu: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -648,7 +648,7 @@ The server should reject the request.`,
|
|||
0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8}},
|
||||
},
|
||||
nBytes: 5000,
|
||||
expHashes: []common.Hash{common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")},
|
||||
expHashes: []common.Hash{types.EmptyCodeHash},
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -754,8 +754,8 @@ func makeSidecar(data ...byte) *types.BlobTxSidecar {
|
|||
)
|
||||
for i := range blobs {
|
||||
blobs[i][0] = data[i]
|
||||
c, _ := kzg4844.BlobToCommitment(blobs[i])
|
||||
p, _ := kzg4844.ComputeBlobProof(blobs[i], c)
|
||||
c, _ := kzg4844.BlobToCommitment(&blobs[i])
|
||||
p, _ := kzg4844.ComputeBlobProof(&blobs[i], c)
|
||||
commitments = append(commitments, c)
|
||||
proofs = append(proofs, p)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
crand "crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ func makeJWTSecret() (string, [32]byte, error) {
|
|||
if _, err := crand.Read(secret[:]); err != nil {
|
||||
return "", secret, fmt.Errorf("failed to create jwt secret: %v", err)
|
||||
}
|
||||
jwtPath := path.Join(os.TempDir(), "jwt_secret")
|
||||
jwtPath := filepath.Join(os.TempDir(), "jwt_secret")
|
||||
if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(secret[:])), 0600); err != nil {
|
||||
return "", secret, fmt.Errorf("failed to prepare jwt secret file: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,9 +66,15 @@ func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool {
|
|||
for _, name := range names {
|
||||
set[name] = struct{}{}
|
||||
}
|
||||
for _, fn := range ctx.FlagNames() {
|
||||
if _, ok := set[fn]; ok {
|
||||
return true
|
||||
for _, ctx := range ctx.Lineage() {
|
||||
if ctx.Command != nil {
|
||||
for _, f := range ctx.Command.Flags {
|
||||
for _, name := range f.Names() {
|
||||
if _, ok := set[name]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -175,14 +176,14 @@ func open(ctx *cli.Context, epoch uint64) (*era.Era, error) {
|
|||
if epoch >= uint64(len(entries)) {
|
||||
return nil, fmt.Errorf("epoch out-of-bounds: last %d, want %d", len(entries)-1, epoch)
|
||||
}
|
||||
return era.Open(path.Join(dir, entries[epoch]))
|
||||
return era.Open(filepath.Join(dir, entries[epoch]))
|
||||
}
|
||||
|
||||
// verify checks each era1 file in a directory to ensure it is well-formed and
|
||||
// that the accumulator matches the expected value.
|
||||
func verify(ctx *cli.Context) error {
|
||||
if ctx.Args().Len() != 1 {
|
||||
return fmt.Errorf("missing accumulators file")
|
||||
return errors.New("missing accumulators file")
|
||||
}
|
||||
|
||||
roots, err := readHashes(ctx.Args().First())
|
||||
|
|
@ -203,7 +204,7 @@ func verify(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if len(entries) != len(roots) {
|
||||
return fmt.Errorf("number of era1 files should match the number of accumulator hashes")
|
||||
return errors.New("number of era1 files should match the number of accumulator hashes")
|
||||
}
|
||||
|
||||
// Verify each epoch matches the expected root.
|
||||
|
|
@ -211,7 +212,7 @@ func verify(ctx *cli.Context) error {
|
|||
// Wrap in function so defers don't stack.
|
||||
err := func() error {
|
||||
name := entries[i]
|
||||
e, err := era.Open(path.Join(dir, name))
|
||||
e, err := era.Open(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening era1 file %s: %w", name, err)
|
||||
}
|
||||
|
|
@ -308,7 +309,7 @@ func checkAccumulator(e *era.Era) error {
|
|||
func readHashes(f string) ([]common.Hash, error) {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open accumulators file")
|
||||
return nil, errors.New("unable to open accumulators file")
|
||||
}
|
||||
s := strings.Split(string(b), "\n")
|
||||
// Remove empty last element, if present.
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ func readInput(ctx *cli.Context) (*bbInput, error) {
|
|||
if headerStr == stdinSelector || ommersStr == stdinSelector || txsStr == stdinSelector || cliqueStr == stdinSelector {
|
||||
decoder := json.NewDecoder(os.Stdin)
|
||||
if err := decoder.Decode(inputData); err != nil {
|
||||
return nil, NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
|
||||
return nil, NewError(ErrorJson, fmt.Errorf("failed unmarshalling stdin: %v", err))
|
||||
}
|
||||
}
|
||||
if cliqueStr != stdinSelector && cliqueStr != "" {
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
|
|||
// Calculate the BlobBaseFee
|
||||
var excessBlobGas uint64
|
||||
if pre.Env.ExcessBlobGas != nil {
|
||||
excessBlobGas := *pre.Env.ExcessBlobGas
|
||||
excessBlobGas = *pre.Env.ExcessBlobGas
|
||||
vmContext.BlobBaseFee = eip4844.CalcBlobFee(excessBlobGas)
|
||||
} else {
|
||||
// If it is not explicitly defined, but we have the parent values, we try
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ func Transaction(ctx *cli.Context) error {
|
|||
if txStr == stdinSelector {
|
||||
decoder := json.NewDecoder(os.Stdin)
|
||||
if err := decoder.Decode(inputData); err != nil {
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshalling stdin: %v", err))
|
||||
}
|
||||
// Decode the body of already signed transactions
|
||||
body = common.FromHex(inputData.TxRlp)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
|
@ -96,7 +96,7 @@ func Transition(ctx *cli.Context) error {
|
|||
Debug: true,
|
||||
}
|
||||
getTracer = func(txIndex int, txHash common.Hash) (vm.EVMLogger, error) {
|
||||
traceFile, err := os.Create(path.Join(baseDir, fmt.Sprintf("trace-%d-%v.jsonl", txIndex, txHash.String())))
|
||||
traceFile, err := os.Create(filepath.Join(baseDir, fmt.Sprintf("trace-%d-%v.jsonl", txIndex, txHash.String())))
|
||||
if err != nil {
|
||||
return nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err))
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ func Transition(ctx *cli.Context) error {
|
|||
config = []byte(ctx.String(TraceTracerConfigFlag.Name))
|
||||
}
|
||||
getTracer = func(txIndex int, txHash common.Hash) (vm.EVMLogger, error) {
|
||||
traceFile, err := os.Create(path.Join(baseDir, fmt.Sprintf("trace-%d-%v.json", txIndex, txHash.String())))
|
||||
traceFile, err := os.Create(filepath.Join(baseDir, fmt.Sprintf("trace-%d-%v.json", txIndex, txHash.String())))
|
||||
if err != nil {
|
||||
return nil, NewError(ErrorIO, fmt.Errorf("failed creating trace-file: %v", err))
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ func Transition(ctx *cli.Context) error {
|
|||
if allocStr == stdinSelector || envStr == stdinSelector || txStr == stdinSelector {
|
||||
decoder := json.NewDecoder(os.Stdin)
|
||||
if err := decoder.Decode(inputData); err != nil {
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err))
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshalling stdin: %v", err))
|
||||
}
|
||||
}
|
||||
if allocStr != stdinSelector {
|
||||
|
|
@ -302,7 +302,7 @@ func saveFile(baseDir, filename string, data interface{}) error {
|
|||
if err != nil {
|
||||
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
|
||||
}
|
||||
location := path.Join(baseDir, filename)
|
||||
location := filepath.Join(baseDir, filename)
|
||||
if err = os.WriteFile(location, b, 0644); err != nil {
|
||||
return NewError(ErrorIO, fmt.Errorf("failed writing output: %v", err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func loadTransactions(txStr string, inputData *input, env stEnv, chainConfig *pa
|
|||
return newRlpTxIterator(body), nil
|
||||
}
|
||||
if err := json.Unmarshal(data, &txsWithKeys); err != nil {
|
||||
return nil, NewError(ErrorJson, fmt.Errorf("failed unmarshaling txs-file: %v", err))
|
||||
return nil, NewError(ErrorJson, fmt.Errorf("failed unmarshalling txs-file: %v", err))
|
||||
}
|
||||
} else {
|
||||
if len(inputData.TxRlp) > 0 {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func readFile(path, desc string, dest interface{}) error {
|
|||
defer inFile.Close()
|
||||
decoder := json.NewDecoder(inFile)
|
||||
if err := decoder.Decode(dest); err != nil {
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling %s file: %v", desc, err))
|
||||
return NewError(ErrorJson, fmt.Errorf("failed unmarshalling %s file: %v", desc, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func stateTestCmd(ctx *cli.Context) error {
|
|||
}
|
||||
// Load the test content from the input file
|
||||
if len(ctx.Args().First()) != 0 {
|
||||
return runStateTest(ctx.Args().First(), cfg, ctx.Bool(MachineFlag.Name), ctx.Bool(DumpFlag.Name))
|
||||
return runStateTest(ctx.Args().First(), cfg, ctx.Bool(DumpFlag.Name))
|
||||
}
|
||||
// Read filenames from stdin and execute back-to-back
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
|
@ -76,7 +76,7 @@ func stateTestCmd(ctx *cli.Context) error {
|
|||
if len(fname) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := runStateTest(fname, cfg, ctx.Bool(MachineFlag.Name), ctx.Bool(DumpFlag.Name)); err != nil {
|
||||
if err := runStateTest(fname, cfg, ctx.Bool(DumpFlag.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ func stateTestCmd(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
// runStateTest loads the state-test given by fname, and executes the test.
|
||||
func runStateTest(fname string, cfg vm.Config, jsonOut, dump bool) error {
|
||||
func runStateTest(fname string, cfg vm.Config, dump bool) error {
|
||||
src, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -105,9 +105,7 @@ func runStateTest(fname string, cfg vm.Config, jsonOut, dump bool) error {
|
|||
if tstate.StateDB != nil {
|
||||
root = tstate.StateDB.IntermediateRoot(false)
|
||||
result.Root = &root
|
||||
if jsonOut {
|
||||
fmt.Fprintf(os.Stderr, "{\"stateRoot\": \"%#x\"}\n", root)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "{\"stateRoot\": \"%#x\"}\n", root)
|
||||
if dump { // Dump any state to aid debugging
|
||||
cpy, _ := state.New(root, tstate.StateDB.Database(), nil)
|
||||
dump := cpy.RawDump(nil)
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ func importHistory(ctx *cli.Context) error {
|
|||
return fmt.Errorf("no era1 files found in %s", dir)
|
||||
}
|
||||
if len(networks) > 1 {
|
||||
return fmt.Errorf("multiple networks found, use a network flag to specify desired network")
|
||||
return errors.New("multiple networks found, use a network flag to specify desired network")
|
||||
}
|
||||
network = networks[0]
|
||||
}
|
||||
|
|
@ -514,13 +514,10 @@ func importPreimages(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseDumpConfig(ctx *cli.Context, stack *node.Node) (*state.DumpConfig, ethdb.Database, common.Hash, error) {
|
||||
db := utils.MakeChainDatabase(ctx, stack, true)
|
||||
defer db.Close()
|
||||
|
||||
func parseDumpConfig(ctx *cli.Context, stack *node.Node, db ethdb.Database) (*state.DumpConfig, common.Hash, error) {
|
||||
var header *types.Header
|
||||
if ctx.NArg() > 1 {
|
||||
return nil, nil, common.Hash{}, fmt.Errorf("expected 1 argument (number or hash), got %d", ctx.NArg())
|
||||
return nil, common.Hash{}, fmt.Errorf("expected 1 argument (number or hash), got %d", ctx.NArg())
|
||||
}
|
||||
if ctx.NArg() == 1 {
|
||||
arg := ctx.Args().First()
|
||||
|
|
@ -529,17 +526,17 @@ func parseDumpConfig(ctx *cli.Context, stack *node.Node) (*state.DumpConfig, eth
|
|||
if number := rawdb.ReadHeaderNumber(db, hash); number != nil {
|
||||
header = rawdb.ReadHeader(db, hash, *number)
|
||||
} else {
|
||||
return nil, nil, common.Hash{}, fmt.Errorf("block %x not found", hash)
|
||||
return nil, common.Hash{}, fmt.Errorf("block %x not found", hash)
|
||||
}
|
||||
} else {
|
||||
number, err := strconv.ParseUint(arg, 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, common.Hash{}, err
|
||||
return nil, common.Hash{}, err
|
||||
}
|
||||
if hash := rawdb.ReadCanonicalHash(db, number); hash != (common.Hash{}) {
|
||||
header = rawdb.ReadHeader(db, hash, number)
|
||||
} else {
|
||||
return nil, nil, common.Hash{}, fmt.Errorf("header for block %d not found", number)
|
||||
return nil, common.Hash{}, fmt.Errorf("header for block %d not found", number)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -547,7 +544,7 @@ func parseDumpConfig(ctx *cli.Context, stack *node.Node) (*state.DumpConfig, eth
|
|||
header = rawdb.ReadHeadHeader(db)
|
||||
}
|
||||
if header == nil {
|
||||
return nil, nil, common.Hash{}, errors.New("no head block found")
|
||||
return nil, common.Hash{}, errors.New("no head block found")
|
||||
}
|
||||
startArg := common.FromHex(ctx.String(utils.StartKeyFlag.Name))
|
||||
var start common.Hash
|
||||
|
|
@ -559,7 +556,7 @@ func parseDumpConfig(ctx *cli.Context, stack *node.Node) (*state.DumpConfig, eth
|
|||
start = crypto.Keccak256Hash(startArg)
|
||||
log.Info("Converting start-address to hash", "address", common.BytesToAddress(startArg), "hash", start.Hex())
|
||||
default:
|
||||
return nil, nil, common.Hash{}, fmt.Errorf("invalid start argument: %x. 20 or 32 hex-encoded bytes required", startArg)
|
||||
return nil, common.Hash{}, fmt.Errorf("invalid start argument: %x. 20 or 32 hex-encoded bytes required", startArg)
|
||||
}
|
||||
var conf = &state.DumpConfig{
|
||||
SkipCode: ctx.Bool(utils.ExcludeCodeFlag.Name),
|
||||
|
|
@ -571,14 +568,17 @@ func parseDumpConfig(ctx *cli.Context, stack *node.Node) (*state.DumpConfig, eth
|
|||
log.Info("State dump configured", "block", header.Number, "hash", header.Hash().Hex(),
|
||||
"skipcode", conf.SkipCode, "skipstorage", conf.SkipStorage,
|
||||
"start", hexutil.Encode(conf.Start), "limit", conf.Max)
|
||||
return conf, db, header.Root, nil
|
||||
return conf, header.Root, nil
|
||||
}
|
||||
|
||||
func dump(ctx *cli.Context) error {
|
||||
stack, _ := makeConfigNode(ctx)
|
||||
defer stack.Close()
|
||||
|
||||
conf, db, root, err := parseDumpConfig(ctx, stack)
|
||||
db := utils.MakeChainDatabase(ctx, stack, true)
|
||||
defer db.Close()
|
||||
|
||||
conf, root, err := parseDumpConfig(ctx, stack, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
||||
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
||||
"github.com/ethereum/go-ethereum/beacon/blsync"
|
||||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
|
@ -43,6 +44,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/naoina/toml"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
|
@ -212,16 +214,24 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
|
|||
}
|
||||
utils.RegisterFullSyncTester(stack, eth, common.BytesToHash(hex))
|
||||
}
|
||||
// Start the dev mode if requested, or launch the engine API for
|
||||
// interacting with external consensus client.
|
||||
|
||||
if ctx.IsSet(utils.DeveloperFlag.Name) {
|
||||
// Start dev mode.
|
||||
simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), eth)
|
||||
if err != nil {
|
||||
utils.Fatalf("failed to register dev mode catalyst service: %v", err)
|
||||
}
|
||||
catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon)
|
||||
stack.RegisterLifecycle(simBeacon)
|
||||
} else if ctx.IsSet(utils.BeaconApiFlag.Name) {
|
||||
// Start blsync mode.
|
||||
srv := rpc.NewServer()
|
||||
srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
|
||||
blsyncer := blsync.NewClient(ctx)
|
||||
blsyncer.SetEngineRPC(rpc.DialInProc(srv))
|
||||
stack.RegisterLifecycle(blsyncer)
|
||||
} else {
|
||||
// Launch the engine API for interacting with external consensus client.
|
||||
err := catalyst.Register(stack, eth)
|
||||
if err != nil {
|
||||
utils.Fatalf("failed to register catalyst service: %v", err)
|
||||
|
|
@ -264,7 +274,7 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) {
|
|||
cfg.Metrics.Enabled = ctx.Bool(utils.MetricsEnabledFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(utils.MetricsEnabledExpensiveFlag.Name) {
|
||||
cfg.Metrics.EnabledExpensive = ctx.Bool(utils.MetricsEnabledExpensiveFlag.Name)
|
||||
log.Warn("Expensive metrics are collected by default, please remove this flag", "flag", utils.MetricsEnabledExpensiveFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(utils.MetricsHTTPFlag.Name) {
|
||||
cfg.Metrics.HTTP = ctx.String(utils.MetricsHTTPFlag.Name)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ func TestConsoleWelcome(t *testing.T) {
|
|||
Welcome to the Geth JavaScript console!
|
||||
|
||||
instance: Geth/v{{gethver}}/{{goos}}-{{goarch}}/{{gover}}
|
||||
coinbase: {{.Etherbase}}
|
||||
at block: 0 ({{niltime}})
|
||||
datadir: {{.Datadir}}
|
||||
modules: {{apis}}
|
||||
|
|
@ -131,7 +130,6 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
|
|||
attach.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
|
||||
attach.SetTemplateFunc("gover", runtime.Version)
|
||||
attach.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
|
||||
attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
|
||||
attach.SetTemplateFunc("niltime", func() string {
|
||||
return time.Unix(1548854791, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
|
||||
})
|
||||
|
|
@ -144,7 +142,6 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
|
|||
Welcome to the Geth JavaScript console!
|
||||
|
||||
instance: Geth/v{{gethver}}/{{goos}}-{{goarch}}/{{gover}}
|
||||
coinbase: {{etherbase}}
|
||||
at block: 0 ({{niltime}}){{if ipc}}
|
||||
datadir: {{datadir}}{{end}}
|
||||
modules: {{apis}}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/cmd/utils"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/console/prompt"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/internal/debug"
|
||||
|
|
@ -116,13 +115,14 @@ var (
|
|||
utils.DiscoveryPortFlag,
|
||||
utils.MaxPeersFlag,
|
||||
utils.MaxPendingPeersFlag,
|
||||
utils.MiningEnabledFlag,
|
||||
utils.MiningEnabledFlag, // deprecated
|
||||
utils.MinerGasLimitFlag,
|
||||
utils.MinerGasPriceFlag,
|
||||
utils.MinerEtherbaseFlag,
|
||||
utils.MinerEtherbaseFlag, // deprecated
|
||||
utils.MinerExtraDataFlag,
|
||||
utils.MinerRecommitIntervalFlag,
|
||||
utils.MinerNewPayloadTimeout,
|
||||
utils.MinerPendingFeeRecipientFlag,
|
||||
utils.MinerNewPayloadTimeoutFlag, // deprecated
|
||||
utils.NATFlag,
|
||||
utils.NoDiscoverFlag,
|
||||
utils.DiscoveryV4Flag,
|
||||
|
|
@ -146,6 +146,14 @@ var (
|
|||
configFileFlag,
|
||||
utils.LogDebugFlag,
|
||||
utils.LogBacktraceAtFlag,
|
||||
utils.BeaconApiFlag,
|
||||
utils.BeaconApiHeaderFlag,
|
||||
utils.BeaconThresholdFlag,
|
||||
utils.BeaconNoFilterFlag,
|
||||
utils.BeaconConfigFlag,
|
||||
utils.BeaconGenesisRootFlag,
|
||||
utils.BeaconGenesisTimeFlag,
|
||||
utils.BeaconCheckpointFlag,
|
||||
}, utils.NetworkFlags, utils.DatabaseFlags)
|
||||
|
||||
rpcFlags = []cli.Flag{
|
||||
|
|
@ -421,24 +429,6 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isCon
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start auxiliary services if enabled
|
||||
if ctx.Bool(utils.MiningEnabledFlag.Name) {
|
||||
// Mining only makes sense if a full Ethereum node is running
|
||||
if ctx.String(utils.SyncModeFlag.Name) == "light" {
|
||||
utils.Fatalf("Light clients do not support mining")
|
||||
}
|
||||
ethBackend, ok := backend.(*eth.EthAPIBackend)
|
||||
if !ok {
|
||||
utils.Fatalf("Ethereum service not running")
|
||||
}
|
||||
// Set the gas price to the limits from the CLI and start mining
|
||||
gasprice := flags.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)
|
||||
ethBackend.TxPool().SetGasTip(gasprice)
|
||||
if err := ethBackend.StartMining(); err != nil {
|
||||
utils.Fatalf("Failed to start mining: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unlockAccounts unlocks any account specifically requested.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -541,7 +541,10 @@ func dumpState(ctx *cli.Context) error {
|
|||
stack, _ := makeConfigNode(ctx)
|
||||
defer stack.Close()
|
||||
|
||||
conf, db, root, err := parseDumpConfig(ctx, stack)
|
||||
db := utils.MakeChainDatabase(ctx, stack, true)
|
||||
defer db.Close()
|
||||
|
||||
conf, root, err := parseDumpConfig(ctx, stack, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
1
cmd/geth/testdata/clique.json
vendored
1
cmd/geth/testdata/clique.json
vendored
|
|
@ -8,6 +8,7 @@
|
|||
"byzantiumBlock": 0,
|
||||
"constantinopleBlock": 0,
|
||||
"petersburgBlock": 0,
|
||||
"terminalTotalDifficultyPassed": true,
|
||||
"clique": {
|
||||
"period": 5,
|
||||
"epoch": 30000
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/internal/flags"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/gballet/go-verkle"
|
||||
cli "github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
|
@ -245,13 +245,13 @@ func readList(filename string) ([]string, error) {
|
|||
// starting from genesis.
|
||||
func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, network string) error {
|
||||
if chain.CurrentSnapBlock().Number.BitLen() != 0 {
|
||||
return fmt.Errorf("history import only supported when starting from genesis")
|
||||
return errors.New("history import only supported when starting from genesis")
|
||||
}
|
||||
entries, err := era.ReadDir(dir, network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s: %w", dir, err)
|
||||
}
|
||||
checksums, err := readList(path.Join(dir, "checksums.txt"))
|
||||
checksums, err := readList(filepath.Join(dir, "checksums.txt"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read checksums.txt: %w", err)
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@ func ImportHistory(chain *core.BlockChain, db ethdb.Database, dir string, networ
|
|||
)
|
||||
for i, filename := range entries {
|
||||
err := func() error {
|
||||
f, err := os.Open(path.Join(dir, filename))
|
||||
f, err := os.Open(filepath.Join(dir, filename))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open era: %w", err)
|
||||
}
|
||||
|
|
@ -425,7 +425,7 @@ func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) er
|
|||
)
|
||||
for i := first; i <= last; i += step {
|
||||
err := func() error {
|
||||
filename := path.Join(dir, era.Filename(network, int(i/step), common.Hash{}))
|
||||
filename := filepath.Join(dir, era.Filename(network, int(i/step), common.Hash{}))
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create era file: %w", err)
|
||||
|
|
@ -458,7 +458,7 @@ func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) er
|
|||
return fmt.Errorf("export failed to finalize %d: %w", step/i, err)
|
||||
}
|
||||
// Set correct filename with root.
|
||||
os.Rename(filename, path.Join(dir, era.Filename(network, int(i/step), root)))
|
||||
os.Rename(filename, filepath.Join(dir, era.Filename(network, int(i/step), root)))
|
||||
|
||||
// Compute checksum of entire Era1.
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
|
|
@ -481,7 +481,7 @@ func ExportHistory(bc *core.BlockChain, dir string, first, last, step uint64) er
|
|||
}
|
||||
}
|
||||
|
||||
os.WriteFile(path.Join(dir, "checksums.txt"), []byte(strings.Join(checksums, "\n")), os.ModePerm)
|
||||
os.WriteFile(filepath.Join(dir, "checksums.txt"), []byte(strings.Join(checksums, "\n")), os.ModePerm)
|
||||
|
||||
log.Info("Exported blockchain to", "dir", dir)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
bparams "github.com/ethereum/go-ethereum/beacon/params"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/fdlimit"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
|
|
@ -281,6 +282,58 @@ var (
|
|||
Value: ethconfig.Defaults.TransactionHistory,
|
||||
Category: flags.StateCategory,
|
||||
}
|
||||
// Beacon client light sync settings
|
||||
BeaconApiFlag = &cli.StringSliceFlag{
|
||||
Name: "beacon.api",
|
||||
Usage: "Beacon node (CL) light client API URL. This flag can be given multiple times.",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconApiHeaderFlag = &cli.StringSliceFlag{
|
||||
Name: "beacon.api.header",
|
||||
Usage: "Pass custom HTTP header fields to the emote beacon node API in \"key:value\" format. This flag can be given multiple times.",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconThresholdFlag = &cli.IntFlag{
|
||||
Name: "beacon.threshold",
|
||||
Usage: "Beacon sync committee participation threshold",
|
||||
Value: bparams.SyncCommitteeSupermajority,
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconNoFilterFlag = &cli.BoolFlag{
|
||||
Name: "beacon.nofilter",
|
||||
Usage: "Disable future slot signature filter",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconConfigFlag = &cli.StringFlag{
|
||||
Name: "beacon.config",
|
||||
Usage: "Beacon chain config YAML file",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconGenesisRootFlag = &cli.StringFlag{
|
||||
Name: "beacon.genesis.gvroot",
|
||||
Usage: "Beacon chain genesis validators root",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconGenesisTimeFlag = &cli.Uint64Flag{
|
||||
Name: "beacon.genesis.time",
|
||||
Usage: "Beacon chain genesis time",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BeaconCheckpointFlag = &cli.StringFlag{
|
||||
Name: "beacon.checkpoint",
|
||||
Usage: "Beacon chain weak subjectivity checkpoint block hash",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BlsyncApiFlag = &cli.StringFlag{
|
||||
Name: "blsync.engine.api",
|
||||
Usage: "Target EL engine API URL",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
BlsyncJWTSecretFlag = &cli.StringFlag{
|
||||
Name: "blsync.jwtsecret",
|
||||
Usage: "Path to a JWT secret to use for target engine API endpoint",
|
||||
Category: flags.BeaconCategory,
|
||||
}
|
||||
// Transaction pool settings
|
||||
TxPoolLocalsFlag = &cli.StringFlag{
|
||||
Name: "txpool.locals",
|
||||
|
|
@ -425,11 +478,6 @@ var (
|
|||
}
|
||||
|
||||
// Miner settings
|
||||
MiningEnabledFlag = &cli.BoolFlag{
|
||||
Name: "mine",
|
||||
Usage: "Enable mining",
|
||||
Category: flags.MinerCategory,
|
||||
}
|
||||
MinerGasLimitFlag = &cli.Uint64Flag{
|
||||
Name: "miner.gaslimit",
|
||||
Usage: "Target gas ceiling for mined blocks",
|
||||
|
|
@ -442,11 +490,6 @@ var (
|
|||
Value: ethconfig.Defaults.Miner.GasPrice,
|
||||
Category: flags.MinerCategory,
|
||||
}
|
||||
MinerEtherbaseFlag = &cli.StringFlag{
|
||||
Name: "miner.etherbase",
|
||||
Usage: "0x prefixed public address for block mining rewards",
|
||||
Category: flags.MinerCategory,
|
||||
}
|
||||
MinerExtraDataFlag = &cli.StringFlag{
|
||||
Name: "miner.extradata",
|
||||
Usage: "Block extra data set by the miner (default = client version)",
|
||||
|
|
@ -458,10 +501,9 @@ var (
|
|||
Value: ethconfig.Defaults.Miner.Recommit,
|
||||
Category: flags.MinerCategory,
|
||||
}
|
||||
MinerNewPayloadTimeout = &cli.DurationFlag{
|
||||
Name: "miner.newpayload-timeout",
|
||||
Usage: "Specify the maximum time allowance for creating a new payload",
|
||||
Value: ethconfig.Defaults.Miner.NewPayloadTimeout,
|
||||
MinerPendingFeeRecipientFlag = &cli.StringFlag{
|
||||
Name: "miner.pending.feeRecipient",
|
||||
Usage: "0x prefixed public address for the pending block producer (not used for actual block production)",
|
||||
Category: flags.MinerCategory,
|
||||
}
|
||||
|
||||
|
|
@ -820,12 +862,6 @@ var (
|
|||
Usage: "Enable metrics collection and reporting",
|
||||
Category: flags.MetricsCategory,
|
||||
}
|
||||
MetricsEnabledExpensiveFlag = &cli.BoolFlag{
|
||||
Name: "metrics.expensive",
|
||||
Usage: "Enable expensive metrics collection and reporting",
|
||||
Category: flags.MetricsCategory,
|
||||
}
|
||||
|
||||
// MetricsHTTPFlag defines the endpoint for a stand-alone metrics HTTP endpoint.
|
||||
// Since the pprof service enables sensitive/vulnerable behavior, this allows a user
|
||||
// to enable a public-OK metrics endpoint without having to worry about ALSO exposing
|
||||
|
|
@ -1268,19 +1304,23 @@ func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error
|
|||
|
||||
// setEtherbase retrieves the etherbase from the directly specified command line flags.
|
||||
func setEtherbase(ctx *cli.Context, cfg *ethconfig.Config) {
|
||||
if !ctx.IsSet(MinerEtherbaseFlag.Name) {
|
||||
if ctx.IsSet(MinerEtherbaseFlag.Name) {
|
||||
log.Warn("Option --miner.etherbase is deprecated as the etherbase is set by the consensus client post-merge")
|
||||
return
|
||||
}
|
||||
addr := ctx.String(MinerEtherbaseFlag.Name)
|
||||
if !ctx.IsSet(MinerPendingFeeRecipientFlag.Name) {
|
||||
return
|
||||
}
|
||||
addr := ctx.String(MinerPendingFeeRecipientFlag.Name)
|
||||
if strings.HasPrefix(addr, "0x") || strings.HasPrefix(addr, "0X") {
|
||||
addr = addr[2:]
|
||||
}
|
||||
b, err := hex.DecodeString(addr)
|
||||
if err != nil || len(b) != common.AddressLength {
|
||||
Fatalf("-%s: invalid etherbase address %q", MinerEtherbaseFlag.Name, addr)
|
||||
Fatalf("-%s: invalid pending block producer address %q", MinerPendingFeeRecipientFlag.Name, addr)
|
||||
return
|
||||
}
|
||||
cfg.Miner.Etherbase = common.BytesToAddress(b)
|
||||
cfg.Miner.PendingFeeRecipient = common.BytesToAddress(b)
|
||||
}
|
||||
|
||||
// MakePasswordList reads password lines from the file specified by the global --password flag.
|
||||
|
|
@ -1496,6 +1536,9 @@ func setTxPool(ctx *cli.Context, cfg *legacypool.Config) {
|
|||
}
|
||||
|
||||
func setMiner(ctx *cli.Context, cfg *miner.Config) {
|
||||
if ctx.Bool(MiningEnabledFlag.Name) {
|
||||
log.Warn("The flag --mine is deprecated and will be removed")
|
||||
}
|
||||
if ctx.IsSet(MinerExtraDataFlag.Name) {
|
||||
cfg.ExtraData = []byte(ctx.String(MinerExtraDataFlag.Name))
|
||||
}
|
||||
|
|
@ -1508,8 +1551,9 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
|
|||
if ctx.IsSet(MinerRecommitIntervalFlag.Name) {
|
||||
cfg.Recommit = ctx.Duration(MinerRecommitIntervalFlag.Name)
|
||||
}
|
||||
if ctx.IsSet(MinerNewPayloadTimeout.Name) {
|
||||
cfg.NewPayloadTimeout = ctx.Duration(MinerNewPayloadTimeout.Name)
|
||||
if ctx.IsSet(MinerNewPayloadTimeoutFlag.Name) {
|
||||
log.Warn("The flag --miner.newpayload-timeout is deprecated and will be removed, please use --miner.recommit")
|
||||
cfg.Recommit = ctx.Duration(MinerNewPayloadTimeoutFlag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1668,6 +1712,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
|||
if ctx.String(GCModeFlag.Name) == "archive" && cfg.TransactionHistory != 0 {
|
||||
cfg.TransactionHistory = 0
|
||||
log.Warn("Disabled transaction unindexing for archive node")
|
||||
|
||||
cfg.StateScheme = rawdb.HashScheme
|
||||
log.Warn("Forcing hash state-scheme for archive mode")
|
||||
}
|
||||
if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheTrieFlag.Name) {
|
||||
cfg.TrieCleanCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheTrieFlag.Name) / 100
|
||||
|
|
@ -1783,8 +1830,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
|||
|
||||
// Figure out the dev account address.
|
||||
// setEtherbase has been called above, configuring the miner address from command line flags.
|
||||
if cfg.Miner.Etherbase != (common.Address{}) {
|
||||
developer = accounts.Account{Address: cfg.Miner.Etherbase}
|
||||
if cfg.Miner.PendingFeeRecipient != (common.Address{}) {
|
||||
developer = accounts.Account{Address: cfg.Miner.PendingFeeRecipient}
|
||||
} else if accs := ks.Accounts(); len(accs) > 0 {
|
||||
developer = ks.Accounts()[0]
|
||||
} else {
|
||||
|
|
@ -1795,7 +1842,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
|||
}
|
||||
// Make sure the address is configured as fee recipient, otherwise
|
||||
// the miner will fail to start.
|
||||
cfg.Miner.Etherbase = developer.Address
|
||||
cfg.Miner.PendingFeeRecipient = developer.Address
|
||||
|
||||
if err := ks.Unlock(developer, passphrase); err != nil {
|
||||
Fatalf("Failed to unlock developer account: %v", err)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ var DeprecatedFlags = []cli.Flag{
|
|||
LightNoSyncServeFlag,
|
||||
LogBacktraceAtFlag,
|
||||
LogDebugFlag,
|
||||
MinerNewPayloadTimeoutFlag,
|
||||
MinerEtherbaseFlag,
|
||||
MiningEnabledFlag,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -90,35 +93,35 @@ var (
|
|||
Name: "light.serve",
|
||||
Usage: "Maximum percentage of time allowed for serving LES requests (deprecated)",
|
||||
Value: ethconfig.Defaults.LightServ,
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
LightIngressFlag = &cli.IntFlag{
|
||||
Name: "light.ingress",
|
||||
Usage: "Incoming bandwidth limit for serving light clients (deprecated)",
|
||||
Value: ethconfig.Defaults.LightIngress,
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
LightEgressFlag = &cli.IntFlag{
|
||||
Name: "light.egress",
|
||||
Usage: "Outgoing bandwidth limit for serving light clients (deprecated)",
|
||||
Value: ethconfig.Defaults.LightEgress,
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
LightMaxPeersFlag = &cli.IntFlag{
|
||||
Name: "light.maxpeers",
|
||||
Usage: "Maximum number of light clients to serve, or light servers to attach to (deprecated)",
|
||||
Value: ethconfig.Defaults.LightPeers,
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
LightNoPruneFlag = &cli.BoolFlag{
|
||||
Name: "light.nopruning",
|
||||
Usage: "Disable ancient light chain data pruning (deprecated)",
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
LightNoSyncServeFlag = &cli.BoolFlag{
|
||||
Name: "light.nosyncserve",
|
||||
Usage: "Enables serving light clients before syncing (deprecated)",
|
||||
Category: flags.LightCategory,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
// Deprecated November 2023
|
||||
LogBacktraceAtFlag = &cli.StringFlag{
|
||||
|
|
@ -132,6 +135,28 @@ var (
|
|||
Usage: "Prepends log messages with call-site location (deprecated)",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
// Deprecated February 2024
|
||||
MinerNewPayloadTimeoutFlag = &cli.DurationFlag{
|
||||
Name: "miner.newpayload-timeout",
|
||||
Usage: "Specify the maximum time allowance for creating a new payload (deprecated)",
|
||||
Value: ethconfig.Defaults.Miner.Recommit,
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
MinerEtherbaseFlag = &cli.StringFlag{
|
||||
Name: "miner.etherbase",
|
||||
Usage: "0x prefixed public address for block mining rewards (deprecated)",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
MiningEnabledFlag = &cli.BoolFlag{
|
||||
Name: "mine",
|
||||
Usage: "Enable mining (deprecated)",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
MetricsEnabledExpensiveFlag = &cli.BoolFlag{
|
||||
Name: "metrics.expensive",
|
||||
Usage: "Enable expensive metrics collection and reporting (deprecated)",
|
||||
Category: flags.DeprecatedCategory,
|
||||
}
|
||||
)
|
||||
|
||||
// showDeprecated displays deprecated flags that will be soon removed from the codebase.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestHistoryImportAndExport(t *testing.T) {
|
|||
}
|
||||
|
||||
// Read checksums.
|
||||
b, err := os.ReadFile(path.Join(dir, "checksums.txt"))
|
||||
b, err := os.ReadFile(filepath.Join(dir, "checksums.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read checksums: %v", err)
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ func TestHistoryImportAndExport(t *testing.T) {
|
|||
entries, _ := era.ReadDir(dir, "mainnet")
|
||||
for i, filename := range entries {
|
||||
func() {
|
||||
f, err := os.Open(path.Join(dir, filename))
|
||||
f, err := os.Open(filepath.Join(dir, filename))
|
||||
if err != nil {
|
||||
t.Fatalf("error opening era file: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Adapted from: https://golang.org/src/crypto/cipher/xor.go
|
||||
// Adapted from: https://go.dev/src/crypto/subtle/xor_generic.go
|
||||
|
||||
// Package bitutil implements fast bitwise operations.
|
||||
package bitutil
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Adapted from: https://golang.org/src/crypto/cipher/xor_test.go
|
||||
// Adapted from: https://go.dev/src/crypto/subtle/xor_test.go
|
||||
|
||||
package bitutil
|
||||
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ func ReadBits(bigint *big.Int, buf []byte) {
|
|||
}
|
||||
}
|
||||
|
||||
// U256 encodes as a 256 bit two's complement number. This operation is destructive.
|
||||
// U256 encodes x as a 256 bit two's complement number. This operation is destructive.
|
||||
func U256(x *big.Int) *big.Int {
|
||||
return x.And(x, tt256m1)
|
||||
}
|
||||
|
|
@ -255,14 +255,15 @@ func S256(x *big.Int) *big.Int {
|
|||
//
|
||||
// Courtesy @karalabe and @chfast
|
||||
func Exp(base, exponent *big.Int) *big.Int {
|
||||
copyBase := new(big.Int).Set(base)
|
||||
result := big.NewInt(1)
|
||||
|
||||
for _, word := range exponent.Bits() {
|
||||
for i := 0; i < wordBits; i++ {
|
||||
if word&1 == 1 {
|
||||
U256(result.Mul(result, base))
|
||||
U256(result.Mul(result, copyBase))
|
||||
}
|
||||
U256(base.Mul(base, base))
|
||||
U256(copyBase.Mul(copyBase, copyBase))
|
||||
word >>= 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -475,3 +475,14 @@ func (d *Decimal) UnmarshalJSON(input []byte) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type PrettyBytes []byte
|
||||
|
||||
// TerminalString implements log.TerminalStringer, formatting a string for console
|
||||
// output during logging.
|
||||
func (b PrettyBytes) TerminalString() string {
|
||||
if len(b) < 7 {
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
return fmt.Sprintf("%#x...%x (%dB)", b[:3], b[len(b)-3:], len(b))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,13 +348,13 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H
|
|||
}
|
||||
|
||||
// Finalize implements consensus.Engine and processes withdrawals on top.
|
||||
func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) {
|
||||
func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) {
|
||||
if !beacon.IsPoSHeader(header) {
|
||||
beacon.ethone.Finalize(chain, header, state, txs, uncles, nil)
|
||||
beacon.ethone.Finalize(chain, header, state, body)
|
||||
return
|
||||
}
|
||||
// Withdrawals processing.
|
||||
for _, w := range withdrawals {
|
||||
for _, w := range body.Withdrawals {
|
||||
// Convert amount from gwei to wei.
|
||||
amount := new(uint256.Int).SetUint64(w.Amount)
|
||||
amount = amount.Mul(amount, uint256.NewInt(params.GWei))
|
||||
|
|
@ -365,29 +365,29 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
|
|||
|
||||
// FinalizeAndAssemble implements consensus.Engine, setting the final state and
|
||||
// assembling the block.
|
||||
func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) {
|
||||
func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
|
||||
if !beacon.IsPoSHeader(header) {
|
||||
return beacon.ethone.FinalizeAndAssemble(chain, header, state, txs, uncles, receipts, nil)
|
||||
return beacon.ethone.FinalizeAndAssemble(chain, header, state, body, receipts)
|
||||
}
|
||||
shanghai := chain.Config().IsShanghai(header.Number, header.Time)
|
||||
if shanghai {
|
||||
// All blocks after Shanghai must include a withdrawals root.
|
||||
if withdrawals == nil {
|
||||
withdrawals = make([]*types.Withdrawal, 0)
|
||||
if body.Withdrawals == nil {
|
||||
body.Withdrawals = make([]*types.Withdrawal, 0)
|
||||
}
|
||||
} else {
|
||||
if len(withdrawals) > 0 {
|
||||
if len(body.Withdrawals) > 0 {
|
||||
return nil, errors.New("withdrawals set before Shanghai activation")
|
||||
}
|
||||
}
|
||||
// Finalize and assemble the block.
|
||||
beacon.Finalize(chain, header, state, txs, uncles, withdrawals)
|
||||
beacon.Finalize(chain, header, state, body)
|
||||
|
||||
// Assign the final state root to header.
|
||||
header.Root = state.IntermediateRoot(true)
|
||||
|
||||
// Assemble and return the final block.
|
||||
return types.NewBlockWithWithdrawals(header, txs, uncles, receipts, withdrawals, trie.NewStackTrie(nil)), nil
|
||||
return types.NewBlockWithWithdrawals(header, body.Transactions, body.Uncles, receipts, body.Withdrawals, trie.NewStackTrie(nil)), nil
|
||||
}
|
||||
|
||||
// Seal generates a new sealing request for the given input block and pushes
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
lru "github.com/ethereum/go-ethereum/common/lru"
|
||||
"github.com/ethereum/go-ethereum/common/lru"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/misc"
|
||||
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
|
||||
|
|
@ -580,24 +580,24 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header
|
|||
|
||||
// Finalize implements consensus.Engine. There is no post-transaction
|
||||
// consensus rules in clique, do nothing here.
|
||||
func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) {
|
||||
func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) {
|
||||
// No block rewards in PoA, so the state remains as is
|
||||
}
|
||||
|
||||
// FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set,
|
||||
// nor block rewards given, and returns the final block.
|
||||
func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) {
|
||||
if len(withdrawals) > 0 {
|
||||
func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
|
||||
if len(body.Withdrawals) > 0 {
|
||||
return nil, errors.New("clique does not support withdrawals")
|
||||
}
|
||||
// Finalize block
|
||||
c.Finalize(chain, header, state, txs, uncles, nil)
|
||||
c.Finalize(chain, header, state, body)
|
||||
|
||||
// Assign the final state root to header.
|
||||
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
|
||||
|
||||
// Assemble and return the final block for sealing.
|
||||
return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)), nil
|
||||
return types.NewBlock(header, body.Transactions, nil, receipts, trie.NewStackTrie(nil)), nil
|
||||
}
|
||||
|
||||
// Authorize injects a private key into the consensus engine to mint new blocks
|
||||
|
|
|
|||
|
|
@ -88,16 +88,14 @@ type Engine interface {
|
|||
//
|
||||
// Note: The state database might be updated to reflect any consensus rules
|
||||
// that happen at finalization (e.g. block rewards).
|
||||
Finalize(chain ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
|
||||
uncles []*types.Header, withdrawals []*types.Withdrawal)
|
||||
Finalize(chain ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body)
|
||||
|
||||
// FinalizeAndAssemble runs any post-transaction state modifications (e.g. block
|
||||
// rewards or process withdrawals) and assembles the final block.
|
||||
//
|
||||
// Note: The block header and state database might be updated to reflect any
|
||||
// consensus rules that happen at finalization (e.g. block rewards).
|
||||
FinalizeAndAssemble(chain ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
|
||||
uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error)
|
||||
FinalizeAndAssemble(chain ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error)
|
||||
|
||||
// Seal generates a new sealing request for the given input block and pushes
|
||||
// the result into the given channel.
|
||||
|
|
@ -119,11 +117,3 @@ type Engine interface {
|
|||
// Close terminates any background threads maintained by the consensus engine.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PoW is a consensus engine based on proof-of-work.
|
||||
type PoW interface {
|
||||
Engine
|
||||
|
||||
// Hashrate returns the current mining hashrate of a PoW consensus engine.
|
||||
Hashrate() float64
|
||||
}
|
||||
|
|
|
|||
|
|
@ -501,25 +501,25 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H
|
|||
}
|
||||
|
||||
// Finalize implements consensus.Engine, accumulating the block and uncle rewards.
|
||||
func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) {
|
||||
func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) {
|
||||
// Accumulate any block and uncle rewards
|
||||
accumulateRewards(chain.Config(), state, header, uncles)
|
||||
accumulateRewards(chain.Config(), state, header, body.Uncles)
|
||||
}
|
||||
|
||||
// FinalizeAndAssemble implements consensus.Engine, accumulating the block and
|
||||
// uncle rewards, setting the final state and assembling the block.
|
||||
func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) {
|
||||
if len(withdrawals) > 0 {
|
||||
func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
|
||||
if len(body.Withdrawals) > 0 {
|
||||
return nil, errors.New("ethash does not support withdrawals")
|
||||
}
|
||||
// Finalize block
|
||||
ethash.Finalize(chain, header, state, txs, uncles, nil)
|
||||
ethash.Finalize(chain, header, state, body)
|
||||
|
||||
// Assign the final state root to header.
|
||||
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
|
||||
|
||||
// Header seems complete, assemble into a block and return
|
||||
return types.NewBlock(header, txs, uncles, receipts, trie.NewStackTrie(nil)), nil
|
||||
return types.NewBlock(header, body.Transactions, body.Uncles, receipts, trie.NewStackTrie(nil)), nil
|
||||
}
|
||||
|
||||
// SealHash returns the hash of a block prior to it being sealed.
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
// Copyright 2021 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package consensus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// transitionStatus describes the status of eth1/2 transition. This switch
|
||||
// between modes is a one-way action which is triggered by corresponding
|
||||
// consensus-layer message.
|
||||
type transitionStatus struct {
|
||||
LeftPoW bool // The flag is set when the first NewHead message received
|
||||
EnteredPoS bool // The flag is set when the first FinalisedBlock message received
|
||||
}
|
||||
|
||||
// Merger is an internal help structure used to track the eth1/2 transition status.
|
||||
// It's a common structure can be used in both full node and light client.
|
||||
type Merger struct {
|
||||
db ethdb.KeyValueStore
|
||||
status transitionStatus
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMerger creates a new Merger which stores its transition status in the provided db.
|
||||
func NewMerger(db ethdb.KeyValueStore) *Merger {
|
||||
var status transitionStatus
|
||||
blob := rawdb.ReadTransitionStatus(db)
|
||||
if len(blob) != 0 {
|
||||
if err := rlp.DecodeBytes(blob, &status); err != nil {
|
||||
log.Crit("Failed to decode the transition status", "err", err)
|
||||
}
|
||||
}
|
||||
return &Merger{
|
||||
db: db,
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// ReachTTD is called whenever the first NewHead message received
|
||||
// from the consensus-layer.
|
||||
func (m *Merger) ReachTTD() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.status.LeftPoW {
|
||||
return
|
||||
}
|
||||
m.status = transitionStatus{LeftPoW: true}
|
||||
blob, err := rlp.EncodeToBytes(m.status)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to encode the transition status: %v", err))
|
||||
}
|
||||
rawdb.WriteTransitionStatus(m.db, blob)
|
||||
log.Info("Left PoW stage")
|
||||
}
|
||||
|
||||
// FinalizePoS is called whenever the first FinalisedBlock message received
|
||||
// from the consensus-layer.
|
||||
func (m *Merger) FinalizePoS() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.status.EnteredPoS {
|
||||
return
|
||||
}
|
||||
m.status = transitionStatus{LeftPoW: true, EnteredPoS: true}
|
||||
blob, err := rlp.EncodeToBytes(m.status)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to encode the transition status: %v", err))
|
||||
}
|
||||
rawdb.WriteTransitionStatus(m.db, blob)
|
||||
log.Info("Entered PoS stage")
|
||||
}
|
||||
|
||||
// TDDReached reports whether the chain has left the PoW stage.
|
||||
func (m *Merger) TDDReached() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.status.LeftPoW
|
||||
}
|
||||
|
||||
// PoSFinalized reports whether the chain has entered the PoS stage.
|
||||
func (m *Merger) PoSFinalized() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.status.EnteredPoS
|
||||
}
|
||||
|
|
@ -325,9 +325,6 @@ func (c *Console) Welcome() {
|
|||
// Print some generic Geth metadata
|
||||
if res, err := c.jsre.Run(`
|
||||
var message = "instance: " + web3.version.node + "\n";
|
||||
try {
|
||||
message += "coinbase: " + eth.coinbase + "\n";
|
||||
} catch (err) {}
|
||||
message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
|
||||
try {
|
||||
message += " datadir: " + admin.datadir + "\n";
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
|
|||
ethConf := ðconfig.Config{
|
||||
Genesis: core.DeveloperGenesisBlock(11_500_000, nil),
|
||||
Miner: miner.Config{
|
||||
Etherbase: common.HexToAddress(testAddress),
|
||||
PendingFeeRecipient: common.HexToAddress(testAddress),
|
||||
},
|
||||
}
|
||||
if confOverride != nil {
|
||||
|
|
@ -152,8 +152,7 @@ func (env *tester) Close(t *testing.T) {
|
|||
}
|
||||
|
||||
// Tests that the node lists the correct welcome message, notably that it contains
|
||||
// the instance name, coinbase account, block number, data directory and supported
|
||||
// console modules.
|
||||
// the instance name, block number, data directory and supported console modules.
|
||||
func TestWelcome(t *testing.T) {
|
||||
tester := newTester(t, nil)
|
||||
defer tester.Close(t)
|
||||
|
|
@ -167,14 +166,14 @@ func TestWelcome(t *testing.T) {
|
|||
if want := fmt.Sprintf("instance: %s", testInstance); !strings.Contains(output, want) {
|
||||
t.Fatalf("console output missing instance: have\n%s\nwant also %s", output, want)
|
||||
}
|
||||
if want := fmt.Sprintf("coinbase: %s", testAddress); !strings.Contains(output, want) {
|
||||
t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
|
||||
}
|
||||
if want := "at block: 0"; !strings.Contains(output, want) {
|
||||
t.Fatalf("console output missing sync status: have\n%s\nwant also %s", output, want)
|
||||
}
|
||||
if want := fmt.Sprintf("datadir: %s", tester.workspace); !strings.Contains(output, want) {
|
||||
t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
|
||||
t.Fatalf("console output missing datadir: have\n%s\nwant also %s", output, want)
|
||||
}
|
||||
if want := "modules: "; !strings.Contains(output, want) {
|
||||
t.Fatalf("console output missing modules: have\n%s\nwant also %s", output, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func (it *instructionIterator) Next() bool {
|
|||
|
||||
it.op = vm.OpCode(it.code[it.pc])
|
||||
if it.op.IsPush() {
|
||||
a := uint64(it.op) - uint64(vm.PUSH1) + 1
|
||||
a := uint64(it.op) - uint64(vm.PUSH0)
|
||||
u := it.pc + 1 + a
|
||||
if uint64(len(it.code)) <= it.pc || uint64(len(it.code)) < u {
|
||||
it.error = fmt.Errorf("incomplete push instruction at %v", it.pc)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ func testHeaderVerificationForMerging(t *testing.T, isClique bool) {
|
|||
preBlocks []*types.Block
|
||||
postBlocks []*types.Block
|
||||
engine consensus.Engine
|
||||
merger = consensus.NewMerger(rawdb.NewMemoryDatabase())
|
||||
)
|
||||
if isClique {
|
||||
var (
|
||||
|
|
@ -186,11 +185,6 @@ func testHeaderVerificationForMerging(t *testing.T, isClique bool) {
|
|||
}
|
||||
chain.InsertChain(preBlocks[i : i+1])
|
||||
}
|
||||
|
||||
// Make the transition
|
||||
merger.ReachTTD()
|
||||
merger.FinalizePoS()
|
||||
|
||||
// Verify the blocks after the merging
|
||||
for i := 0; i < len(postBlocks); i++ {
|
||||
_, results := engine.VerifyHeaders(chain, []*types.Header{postHeaders[i]})
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/triedb"
|
||||
"github.com/ethereum/go-ethereum/triedb/hashdb"
|
||||
"github.com/ethereum/go-ethereum/triedb/pathdb"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -62,26 +61,26 @@ var (
|
|||
|
||||
chainInfoGauge = metrics.NewRegisteredGaugeInfo("chain/info", nil)
|
||||
|
||||
accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil)
|
||||
accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil)
|
||||
accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil)
|
||||
accountCommitTimer = metrics.NewRegisteredTimer("chain/account/commits", nil)
|
||||
accountReadTimer = metrics.NewRegisteredResettingTimer("chain/account/reads", nil)
|
||||
accountHashTimer = metrics.NewRegisteredResettingTimer("chain/account/hashes", nil)
|
||||
accountUpdateTimer = metrics.NewRegisteredResettingTimer("chain/account/updates", nil)
|
||||
accountCommitTimer = metrics.NewRegisteredResettingTimer("chain/account/commits", nil)
|
||||
|
||||
storageReadTimer = metrics.NewRegisteredTimer("chain/storage/reads", nil)
|
||||
storageHashTimer = metrics.NewRegisteredTimer("chain/storage/hashes", nil)
|
||||
storageUpdateTimer = metrics.NewRegisteredTimer("chain/storage/updates", nil)
|
||||
storageCommitTimer = metrics.NewRegisteredTimer("chain/storage/commits", nil)
|
||||
storageReadTimer = metrics.NewRegisteredResettingTimer("chain/storage/reads", nil)
|
||||
storageHashTimer = metrics.NewRegisteredResettingTimer("chain/storage/hashes", nil)
|
||||
storageUpdateTimer = metrics.NewRegisteredResettingTimer("chain/storage/updates", nil)
|
||||
storageCommitTimer = metrics.NewRegisteredResettingTimer("chain/storage/commits", nil)
|
||||
|
||||
snapshotAccountReadTimer = metrics.NewRegisteredTimer("chain/snapshot/account/reads", nil)
|
||||
snapshotStorageReadTimer = metrics.NewRegisteredTimer("chain/snapshot/storage/reads", nil)
|
||||
snapshotCommitTimer = metrics.NewRegisteredTimer("chain/snapshot/commits", nil)
|
||||
snapshotAccountReadTimer = metrics.NewRegisteredResettingTimer("chain/snapshot/account/reads", nil)
|
||||
snapshotStorageReadTimer = metrics.NewRegisteredResettingTimer("chain/snapshot/storage/reads", nil)
|
||||
snapshotCommitTimer = metrics.NewRegisteredResettingTimer("chain/snapshot/commits", nil)
|
||||
|
||||
triedbCommitTimer = metrics.NewRegisteredTimer("chain/triedb/commits", nil)
|
||||
triedbCommitTimer = metrics.NewRegisteredResettingTimer("chain/triedb/commits", nil)
|
||||
|
||||
blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil)
|
||||
blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil)
|
||||
blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil)
|
||||
blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil)
|
||||
blockInsertTimer = metrics.NewRegisteredResettingTimer("chain/inserts", nil)
|
||||
blockValidationTimer = metrics.NewRegisteredResettingTimer("chain/validation", nil)
|
||||
blockExecutionTimer = metrics.NewRegisteredResettingTimer("chain/execution", nil)
|
||||
blockWriteTimer = metrics.NewRegisteredResettingTimer("chain/write", nil)
|
||||
|
||||
blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil)
|
||||
blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil)
|
||||
|
|
@ -97,13 +96,11 @@ var (
|
|||
)
|
||||
|
||||
const (
|
||||
bodyCacheLimit = 256
|
||||
blockCacheLimit = 256
|
||||
receiptsCacheLimit = 32
|
||||
txLookupCacheLimit = 1024
|
||||
maxFutureBlocks = 256
|
||||
maxTimeFutureBlocks = 30
|
||||
TriesInMemory = 128
|
||||
bodyCacheLimit = 256
|
||||
blockCacheLimit = 256
|
||||
receiptsCacheLimit = 32
|
||||
txLookupCacheLimit = 1024
|
||||
TriesInMemory = 128
|
||||
|
||||
// BlockChainVersion ensures that an incompatible database forces a resync from scratch.
|
||||
//
|
||||
|
|
@ -245,9 +242,6 @@ type BlockChain struct {
|
|||
blockCache *lru.Cache[common.Hash, *types.Block]
|
||||
txLookupCache *lru.Cache[common.Hash, txLookup]
|
||||
|
||||
// future blocks are blocks added for later processing
|
||||
futureBlocks *lru.Cache[common.Hash, *types.Block]
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{} // shutdown signal, closed in Stop.
|
||||
stopping atomic.Bool // false if chain is running, true when stopped
|
||||
|
|
@ -299,7 +293,6 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
|
|||
receiptsCache: lru.NewCache[common.Hash, []*types.Receipt](receiptsCacheLimit),
|
||||
blockCache: lru.NewCache[common.Hash, *types.Block](blockCacheLimit),
|
||||
txLookupCache: lru.NewCache[common.Hash, txLookup](txLookupCacheLimit),
|
||||
futureBlocks: lru.NewCache[common.Hash, *types.Block](maxFutureBlocks),
|
||||
engine: engine,
|
||||
vmConfig: vmConfig,
|
||||
}
|
||||
|
|
@ -449,11 +442,6 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
|
|||
}
|
||||
bc.snaps, _ = snapshot.New(snapconfig, bc.db, bc.triedb, head.Root)
|
||||
}
|
||||
|
||||
// Start future block processor.
|
||||
bc.wg.Add(1)
|
||||
go bc.updateFutureBlocks()
|
||||
|
||||
// Rewind the chain in case of an incompatible config upgrade.
|
||||
if compat, ok := genesisErr.(*params.ConfigCompatError); ok {
|
||||
log.Warn("Rewinding chain to upgrade configuration", "err", compat)
|
||||
|
|
@ -628,6 +616,172 @@ func (bc *BlockChain) SetSafe(header *types.Header) {
|
|||
}
|
||||
}
|
||||
|
||||
// rewindPathHead implements the logic of rewindHead in the context of hash scheme.
|
||||
func (bc *BlockChain) rewindHashHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
|
||||
var (
|
||||
limit uint64 // The oldest block that will be searched for this rewinding
|
||||
beyondRoot = root == common.Hash{} // Flag whether we're beyond the requested root (no root, always true)
|
||||
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot point state
|
||||
rootNumber uint64 // Associated block number of requested root
|
||||
|
||||
start = time.Now() // Timestamp the rewinding is restarted
|
||||
logged = time.Now() // Timestamp last progress log was printed
|
||||
)
|
||||
// The oldest block to be searched is determined by the pivot block or a constant
|
||||
// searching threshold. The rationale behind this is as follows:
|
||||
//
|
||||
// - Snap sync is selected if the pivot block is available. The earliest available
|
||||
// state is the pivot block itself, so there is no sense in going further back.
|
||||
//
|
||||
// - Full sync is selected if the pivot block does not exist. The hash database
|
||||
// periodically flushes the state to disk, and the used searching threshold is
|
||||
// considered sufficient to find a persistent state, even for the testnet. It
|
||||
// might be not enough for a chain that is nearly empty. In the worst case,
|
||||
// the entire chain is reset to genesis, and snap sync is re-enabled on top,
|
||||
// which is still acceptable.
|
||||
if pivot != nil {
|
||||
limit = *pivot
|
||||
} else if head.Number.Uint64() > params.FullImmutabilityThreshold {
|
||||
limit = head.Number.Uint64() - params.FullImmutabilityThreshold
|
||||
}
|
||||
for {
|
||||
logger := log.Trace
|
||||
if time.Since(logged) > time.Second*8 {
|
||||
logged = time.Now()
|
||||
logger = log.Info
|
||||
}
|
||||
logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
|
||||
// If a root threshold was requested but not yet crossed, check
|
||||
if !beyondRoot && head.Root == root {
|
||||
beyondRoot, rootNumber = true, head.Number.Uint64()
|
||||
}
|
||||
// If search limit is reached, return the genesis block as the
|
||||
// new chain head.
|
||||
if head.Number.Uint64() < limit {
|
||||
log.Info("Rewinding limit reached, resetting to genesis", "number", head.Number, "hash", head.Hash(), "limit", limit)
|
||||
return bc.genesisBlock.Header(), rootNumber
|
||||
}
|
||||
// If the associated state is not reachable, continue searching
|
||||
// backwards until an available state is found.
|
||||
if !bc.HasState(head.Root) {
|
||||
// If the chain is gapped in the middle, return the genesis
|
||||
// block as the new chain head.
|
||||
parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1)
|
||||
if parent == nil {
|
||||
log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash)
|
||||
return bc.genesisBlock.Header(), rootNumber
|
||||
}
|
||||
head = parent
|
||||
|
||||
// If the genesis block is reached, stop searching.
|
||||
if head.Number.Uint64() == 0 {
|
||||
log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash())
|
||||
return head, rootNumber
|
||||
}
|
||||
continue // keep rewinding
|
||||
}
|
||||
// Once the available state is found, ensure that the requested root
|
||||
// has already been crossed. If not, continue rewinding.
|
||||
if beyondRoot || head.Number.Uint64() == 0 {
|
||||
log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash())
|
||||
return head, rootNumber
|
||||
}
|
||||
log.Debug("Skipping block with threshold state", "number", head.Number, "hash", head.Hash(), "root", head.Root)
|
||||
head = bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding
|
||||
}
|
||||
}
|
||||
|
||||
// rewindPathHead implements the logic of rewindHead in the context of path scheme.
|
||||
func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
|
||||
var (
|
||||
pivot = rawdb.ReadLastPivotNumber(bc.db) // Associated block number of pivot block
|
||||
rootNumber uint64 // Associated block number of requested root
|
||||
|
||||
// BeyondRoot represents whether the requested root is already
|
||||
// crossed. The flag value is set to true if the root is empty.
|
||||
beyondRoot = root == common.Hash{}
|
||||
|
||||
// noState represents if the target state requested for search
|
||||
// is unavailable and impossible to be recovered.
|
||||
noState = !bc.HasState(root) && !bc.stateRecoverable(root)
|
||||
|
||||
start = time.Now() // Timestamp the rewinding is restarted
|
||||
logged = time.Now() // Timestamp last progress log was printed
|
||||
)
|
||||
// Rewind the head block tag until an available state is found.
|
||||
for {
|
||||
logger := log.Trace
|
||||
if time.Since(logged) > time.Second*8 {
|
||||
logged = time.Now()
|
||||
logger = log.Info
|
||||
}
|
||||
logger("Block state missing, rewinding further", "number", head.Number, "hash", head.Hash(), "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
|
||||
// If a root threshold was requested but not yet crossed, check
|
||||
if !beyondRoot && head.Root == root {
|
||||
beyondRoot, rootNumber = true, head.Number.Uint64()
|
||||
}
|
||||
// If the root threshold hasn't been crossed but the available
|
||||
// state is reached, quickly determine if the target state is
|
||||
// possible to be reached or not.
|
||||
if !beyondRoot && noState && bc.HasState(head.Root) {
|
||||
beyondRoot = true
|
||||
log.Info("Disable the search for unattainable state", "root", root)
|
||||
}
|
||||
// Check if the associated state is available or recoverable if
|
||||
// the requested root has already been crossed.
|
||||
if beyondRoot && (bc.HasState(head.Root) || bc.stateRecoverable(head.Root)) {
|
||||
break
|
||||
}
|
||||
// If pivot block is reached, return the genesis block as the
|
||||
// new chain head. Theoretically there must be a persistent
|
||||
// state before or at the pivot block, prevent endless rewinding
|
||||
// towards the genesis just in case.
|
||||
if pivot != nil && *pivot >= head.Number.Uint64() {
|
||||
log.Info("Pivot block reached, resetting to genesis", "number", head.Number, "hash", head.Hash())
|
||||
return bc.genesisBlock.Header(), rootNumber
|
||||
}
|
||||
// If the chain is gapped in the middle, return the genesis
|
||||
// block as the new chain head
|
||||
parent := bc.GetHeader(head.ParentHash, head.Number.Uint64()-1) // Keep rewinding
|
||||
if parent == nil {
|
||||
log.Error("Missing block in the middle, resetting to genesis", "number", head.Number.Uint64()-1, "hash", head.ParentHash)
|
||||
return bc.genesisBlock.Header(), rootNumber
|
||||
}
|
||||
head = parent
|
||||
|
||||
// If the genesis block is reached, stop searching.
|
||||
if head.Number.Uint64() == 0 {
|
||||
log.Info("Genesis block reached", "number", head.Number, "hash", head.Hash())
|
||||
return head, rootNumber
|
||||
}
|
||||
}
|
||||
// Recover if the target state if it's not available yet.
|
||||
if !bc.HasState(head.Root) {
|
||||
if err := bc.triedb.Recover(head.Root); err != nil {
|
||||
log.Crit("Failed to rollback state", "err", err)
|
||||
}
|
||||
}
|
||||
log.Info("Rewound to block with state", "number", head.Number, "hash", head.Hash())
|
||||
return head, rootNumber
|
||||
}
|
||||
|
||||
// rewindHead searches the available states in the database and returns the associated
|
||||
// block as the new head block.
|
||||
//
|
||||
// If the given root is not empty, then the rewind should attempt to pass the specified
|
||||
// state root and return the associated block number as well. If the root, typically
|
||||
// representing the state corresponding to snapshot disk layer, is deemed impassable,
|
||||
// then block number zero is returned, indicating that snapshot recovery is disabled
|
||||
// and the whole snapshot should be auto-generated in case of head mismatch.
|
||||
func (bc *BlockChain) rewindHead(head *types.Header, root common.Hash) (*types.Header, uint64) {
|
||||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||||
return bc.rewindPathHead(head, root)
|
||||
}
|
||||
return bc.rewindHashHead(head, root)
|
||||
}
|
||||
|
||||
// setHeadBeyondRoot rewinds the local chain to a new head with the extra condition
|
||||
// that the rewind must pass the specified state root. This method is meant to be
|
||||
// used when rewinding with snapshots enabled to ensure that we go back further than
|
||||
|
|
@ -646,79 +800,40 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
|
|||
}
|
||||
defer bc.chainmu.Unlock()
|
||||
|
||||
// Track the block number of the requested root hash
|
||||
var rootNumber uint64 // (no root == always 0)
|
||||
|
||||
// Retrieve the last pivot block to short circuit rollbacks beyond it and the
|
||||
// current freezer limit to start nuking id underflown
|
||||
pivot := rawdb.ReadLastPivotNumber(bc.db)
|
||||
frozen, _ := bc.db.Ancients()
|
||||
var (
|
||||
// Track the block number of the requested root hash
|
||||
rootNumber uint64 // (no root == always 0)
|
||||
|
||||
// Retrieve the last pivot block to short circuit rollbacks beyond it
|
||||
// and the current freezer limit to start nuking it's underflown.
|
||||
pivot = rawdb.ReadLastPivotNumber(bc.db)
|
||||
)
|
||||
updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (*types.Header, bool) {
|
||||
// Rewind the blockchain, ensuring we don't end up with a stateless head
|
||||
// block. Note, depth equality is permitted to allow using SetHead as a
|
||||
// chain reparation mechanism without deleting any data!
|
||||
if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() <= currentBlock.Number.Uint64() {
|
||||
newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64())
|
||||
if newHeadBlock == nil {
|
||||
log.Error("Gap in the chain, rewinding to genesis", "number", header.Number, "hash", header.Hash())
|
||||
newHeadBlock = bc.genesisBlock
|
||||
} else {
|
||||
// Block exists. Keep rewinding until either we find one with state
|
||||
// or until we exceed the optional threshold root hash
|
||||
beyondRoot := (root == common.Hash{}) // Flag whether we're beyond the requested root (no root, always true)
|
||||
|
||||
for {
|
||||
// If a root threshold was requested but not yet crossed, check
|
||||
if root != (common.Hash{}) && !beyondRoot && newHeadBlock.Root() == root {
|
||||
beyondRoot, rootNumber = true, newHeadBlock.NumberU64()
|
||||
}
|
||||
if !bc.HasState(newHeadBlock.Root()) && !bc.stateRecoverable(newHeadBlock.Root()) {
|
||||
log.Trace("Block state missing, rewinding further", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash())
|
||||
if pivot == nil || newHeadBlock.NumberU64() > *pivot {
|
||||
parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1)
|
||||
if parent != nil {
|
||||
newHeadBlock = parent
|
||||
continue
|
||||
}
|
||||
log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash())
|
||||
newHeadBlock = bc.genesisBlock
|
||||
} else {
|
||||
log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot)
|
||||
newHeadBlock = bc.genesisBlock
|
||||
}
|
||||
}
|
||||
if beyondRoot || newHeadBlock.NumberU64() == 0 {
|
||||
if !bc.HasState(newHeadBlock.Root()) && bc.stateRecoverable(newHeadBlock.Root()) {
|
||||
// Rewind to a block with recoverable state. If the state is
|
||||
// missing, run the state recovery here.
|
||||
if err := bc.triedb.Recover(newHeadBlock.Root()); err != nil {
|
||||
log.Crit("Failed to rollback state", "err", err) // Shouldn't happen
|
||||
}
|
||||
log.Debug("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash())
|
||||
}
|
||||
break
|
||||
}
|
||||
log.Debug("Skipping block with threshold state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "root", newHeadBlock.Root())
|
||||
newHeadBlock = bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) // Keep rewinding
|
||||
}
|
||||
}
|
||||
var newHeadBlock *types.Header
|
||||
newHeadBlock, rootNumber = bc.rewindHead(header, root)
|
||||
rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash())
|
||||
|
||||
// Degrade the chain markers if they are explicitly reverted.
|
||||
// In theory we should update all in-memory markers in the
|
||||
// last step, however the direction of SetHead is from high
|
||||
// to low, so it's safe to update in-memory markers directly.
|
||||
bc.currentBlock.Store(newHeadBlock.Header())
|
||||
headBlockGauge.Update(int64(newHeadBlock.NumberU64()))
|
||||
bc.currentBlock.Store(newHeadBlock)
|
||||
headBlockGauge.Update(int64(newHeadBlock.Number.Uint64()))
|
||||
|
||||
// The head state is missing, which is only possible in the path-based
|
||||
// scheme. This situation occurs when the chain head is rewound below
|
||||
// the pivot point. In this scenario, there is no possible recovery
|
||||
// approach except for rerunning a snap sync. Do nothing here until the
|
||||
// state syncer picks it up.
|
||||
if !bc.HasState(newHeadBlock.Root()) {
|
||||
log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number(), "hash", newHeadBlock.Hash())
|
||||
if !bc.HasState(newHeadBlock.Root) {
|
||||
if newHeadBlock.Number.Uint64() != 0 {
|
||||
log.Crit("Chain is stateless at a non-genesis block")
|
||||
}
|
||||
log.Info("Chain is stateless, wait state sync", "number", newHeadBlock.Number, "hash", newHeadBlock.Hash())
|
||||
}
|
||||
}
|
||||
// Rewind the snap block in a simpleton way to the target head
|
||||
|
|
@ -745,6 +860,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
|
|||
// intent afterwards is full block importing, delete the chain segment
|
||||
// between the stateful-block and the sethead target.
|
||||
var wipe bool
|
||||
frozen, _ := bc.db.Ancients()
|
||||
if headNumber+1 < frozen {
|
||||
wipe = pivot == nil || headNumber >= *pivot
|
||||
}
|
||||
|
|
@ -775,7 +891,7 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
|
|||
// touching the header chain altogether, unless the freezer is broken
|
||||
if repair {
|
||||
if target, force := updateFn(bc.db, bc.CurrentBlock()); force {
|
||||
bc.hc.SetHead(target.Number.Uint64(), updateFn, delFn)
|
||||
bc.hc.SetHead(target.Number.Uint64(), nil, delFn)
|
||||
}
|
||||
} else {
|
||||
// Rewind the chain to the requested head and keep going backwards until a
|
||||
|
|
@ -794,7 +910,6 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha
|
|||
bc.receiptsCache.Purge()
|
||||
bc.blockCache.Purge()
|
||||
bc.txLookupCache.Purge()
|
||||
bc.futureBlocks.Purge()
|
||||
|
||||
// Clear safe block, finalized block if needed
|
||||
if safe := bc.CurrentSafeBlock(); safe != nil && head < safe.Number.Uint64() {
|
||||
|
|
@ -1048,24 +1163,6 @@ func (bc *BlockChain) insertStopped() bool {
|
|||
return bc.procInterrupt.Load()
|
||||
}
|
||||
|
||||
func (bc *BlockChain) procFutureBlocks() {
|
||||
blocks := make([]*types.Block, 0, bc.futureBlocks.Len())
|
||||
for _, hash := range bc.futureBlocks.Keys() {
|
||||
if block, exist := bc.futureBlocks.Peek(hash); exist {
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
}
|
||||
if len(blocks) > 0 {
|
||||
slices.SortFunc(blocks, func(a, b *types.Block) int {
|
||||
return a.Number().Cmp(b.Number())
|
||||
})
|
||||
// Insert one by one as chain insertion needs contiguous ancestry between blocks
|
||||
for i := range blocks {
|
||||
bc.InsertChain(blocks[i : i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteStatus status of write
|
||||
type WriteStatus byte
|
||||
|
||||
|
|
@ -1466,8 +1563,6 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types
|
|||
if status == CanonStatTy {
|
||||
bc.writeHeadBlock(block)
|
||||
}
|
||||
bc.futureBlocks.Remove(block.Hash())
|
||||
|
||||
if status == CanonStatTy {
|
||||
bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
|
||||
if len(logs) > 0 {
|
||||
|
|
@ -1487,25 +1582,6 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types
|
|||
return status, nil
|
||||
}
|
||||
|
||||
// addFutureBlock checks if the block is within the max allowed window to get
|
||||
// accepted for future processing, and returns an error if the block is too far
|
||||
// ahead and was not added.
|
||||
//
|
||||
// TODO after the transition, the future block shouldn't be kept. Because
|
||||
// it's not checked in the Geth side anymore.
|
||||
func (bc *BlockChain) addFutureBlock(block *types.Block) error {
|
||||
max := uint64(time.Now().Unix() + maxTimeFutureBlocks)
|
||||
if block.Time() > max {
|
||||
return fmt.Errorf("future block timestamp %v > allowed %v", block.Time(), max)
|
||||
}
|
||||
if block.Difficulty().Cmp(common.Big0) == 0 {
|
||||
// Never add PoS blocks into the future queue
|
||||
return nil
|
||||
}
|
||||
bc.futureBlocks.Add(block.Hash(), block)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertChain attempts to insert the given batch of blocks in to the canonical
|
||||
// chain or, otherwise, create a fork. If an error is returned it will return
|
||||
// the index number of the failing block as well an error describing what went
|
||||
|
|
@ -1643,26 +1719,10 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
|
|||
_, err := bc.recoverAncestors(block)
|
||||
return it.index, err
|
||||
}
|
||||
// First block is future, shove it (and all children) to the future queue (unknown ancestor)
|
||||
case errors.Is(err, consensus.ErrFutureBlock) || (errors.Is(err, consensus.ErrUnknownAncestor) && bc.futureBlocks.Contains(it.first().ParentHash())):
|
||||
for block != nil && (it.index == 0 || errors.Is(err, consensus.ErrUnknownAncestor)) {
|
||||
log.Debug("Future block, postponing import", "number", block.Number(), "hash", block.Hash())
|
||||
if err := bc.addFutureBlock(block); err != nil {
|
||||
return it.index, err
|
||||
}
|
||||
block, err = it.next()
|
||||
}
|
||||
stats.queued += it.processed()
|
||||
stats.ignored += it.remaining()
|
||||
|
||||
// If there are any still remaining, mark as ignored
|
||||
return it.index, err
|
||||
|
||||
// Some other error(except ErrKnownBlock) occurred, abort.
|
||||
// ErrKnownBlock is allowed here since some known blocks
|
||||
// still need re-execution to generate snapshots that are missing
|
||||
case err != nil && !errors.Is(err, ErrKnownBlock):
|
||||
bc.futureBlocks.Remove(block.Hash())
|
||||
stats.ignored += len(it.chain)
|
||||
bc.reportBlock(block, nil, err)
|
||||
return it.index, err
|
||||
|
|
@ -1867,23 +1927,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error)
|
|||
"root", block.Root())
|
||||
}
|
||||
}
|
||||
|
||||
// Any blocks remaining here? The only ones we care about are the future ones
|
||||
if block != nil && errors.Is(err, consensus.ErrFutureBlock) {
|
||||
if err := bc.addFutureBlock(block); err != nil {
|
||||
return it.index, err
|
||||
}
|
||||
block, err = it.next()
|
||||
|
||||
for ; block != nil && errors.Is(err, consensus.ErrUnknownAncestor); block, err = it.next() {
|
||||
if err := bc.addFutureBlock(block); err != nil {
|
||||
return it.index, err
|
||||
}
|
||||
stats.queued++
|
||||
}
|
||||
}
|
||||
stats.ignored += it.remaining()
|
||||
|
||||
return it.index, err
|
||||
}
|
||||
|
||||
|
|
@ -2334,20 +2378,6 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
|
|||
return head.Hash(), nil
|
||||
}
|
||||
|
||||
func (bc *BlockChain) updateFutureBlocks() {
|
||||
futureTimer := time.NewTicker(5 * time.Second)
|
||||
defer futureTimer.Stop()
|
||||
defer bc.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-futureTimer.C:
|
||||
bc.procFutureBlocks()
|
||||
case <-bc.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skipBlock returns 'true', if the block being imported can be skipped over, meaning
|
||||
// that the block does not need to be processed but can be considered already fully 'done'.
|
||||
func (bc *BlockChain) skipBlock(err error, it *insertIterator) bool {
|
||||
|
|
|
|||
|
|
@ -179,8 +179,3 @@ func (it *insertIterator) first() *types.Block {
|
|||
func (it *insertIterator) remaining() int {
|
||||
return len(it.chain) - it.index
|
||||
}
|
||||
|
||||
// processed returns the number of processed blocks.
|
||||
func (it *insertIterator) processed() int {
|
||||
return it.index + 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ package core
|
|||
|
||||
import (
|
||||
"math/big"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -1757,12 +1757,12 @@ func testRepair(t *testing.T, tt *rewindTest, snapshots bool) {
|
|||
|
||||
func testRepairWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme string) {
|
||||
// It's hard to follow the test case, visualize the input
|
||||
//log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
||||
// fmt.Println(tt.dump(true))
|
||||
|
||||
// Create a temporary persistent database
|
||||
datadir := t.TempDir()
|
||||
ancient := path.Join(datadir, "ancient")
|
||||
ancient := filepath.Join(datadir, "ancient")
|
||||
|
||||
db, err := rawdb.Open(rawdb.OpenOptions{
|
||||
Directory: datadir,
|
||||
|
|
@ -1830,10 +1830,14 @@ func testRepairWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme s
|
|||
}
|
||||
// Force run a freeze cycle
|
||||
type freezer interface {
|
||||
Freeze(threshold uint64) error
|
||||
Freeze() error
|
||||
Ancients() (uint64, error)
|
||||
}
|
||||
db.(freezer).Freeze(tt.freezeThreshold)
|
||||
if tt.freezeThreshold < uint64(tt.canonicalBlocks) {
|
||||
final := uint64(tt.canonicalBlocks) - tt.freezeThreshold
|
||||
chain.SetFinalized(canonblocks[int(final)-1].Header())
|
||||
}
|
||||
db.(freezer).Freeze()
|
||||
|
||||
// Set the simulated pivot block
|
||||
if tt.pivotBlock != nil {
|
||||
|
|
@ -1908,7 +1912,7 @@ func testIssue23496(t *testing.T, scheme string) {
|
|||
|
||||
// Create a temporary persistent database
|
||||
datadir := t.TempDir()
|
||||
ancient := path.Join(datadir, "ancient")
|
||||
ancient := filepath.Join(datadir, "ancient")
|
||||
|
||||
db, err := rawdb.Open(rawdb.OpenOptions{
|
||||
Directory: datadir,
|
||||
|
|
|
|||
|
|
@ -2044,10 +2044,14 @@ func testSetHeadWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme
|
|||
|
||||
// Force run a freeze cycle
|
||||
type freezer interface {
|
||||
Freeze(threshold uint64) error
|
||||
Freeze() error
|
||||
Ancients() (uint64, error)
|
||||
}
|
||||
db.(freezer).Freeze(tt.freezeThreshold)
|
||||
if tt.freezeThreshold < uint64(tt.canonicalBlocks) {
|
||||
final := uint64(tt.canonicalBlocks) - tt.freezeThreshold
|
||||
chain.SetFinalized(canonblocks[int(final)-1].Header())
|
||||
}
|
||||
db.(freezer).Freeze()
|
||||
|
||||
// Set the simulated pivot block
|
||||
if tt.pivotBlock != nil {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import (
|
|||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -63,7 +63,7 @@ type snapshotTestBasic struct {
|
|||
func (basic *snapshotTestBasic) prepare(t *testing.T) (*BlockChain, []*types.Block) {
|
||||
// Create a temporary persistent database
|
||||
datadir := t.TempDir()
|
||||
ancient := path.Join(datadir, "ancient")
|
||||
ancient := filepath.Join(datadir, "ancient")
|
||||
|
||||
db, err := rawdb.Open(rawdb.OpenOptions{
|
||||
Directory: datadir,
|
||||
|
|
|
|||
|
|
@ -2129,7 +2129,6 @@ func testSideImport(t *testing.T, numCanonBlocksInSidechain, blocksBetweenCommon
|
|||
// Generate a canonical chain to act as the main dataset
|
||||
chainConfig := *params.TestChainConfig
|
||||
var (
|
||||
merger = consensus.NewMerger(rawdb.NewMemoryDatabase())
|
||||
engine = beacon.New(ethash.NewFaker())
|
||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||
addr = crypto.PubkeyToAddress(key.PublicKey)
|
||||
|
|
@ -2153,8 +2152,6 @@ func testSideImport(t *testing.T, numCanonBlocksInSidechain, blocksBetweenCommon
|
|||
// Activate the transition since genesis if required
|
||||
if mergePoint == 0 {
|
||||
mergeBlock = 0
|
||||
merger.ReachTTD()
|
||||
merger.FinalizePoS()
|
||||
|
||||
// Set the terminal total difficulty in the config
|
||||
gspec.Config.TerminalTotalDifficulty = big.NewInt(0)
|
||||
|
|
@ -2189,8 +2186,6 @@ func testSideImport(t *testing.T, numCanonBlocksInSidechain, blocksBetweenCommon
|
|||
|
||||
// Activate the transition in the middle of the chain
|
||||
if mergePoint == 1 {
|
||||
merger.ReachTTD()
|
||||
merger.FinalizePoS()
|
||||
// Set the terminal total difficulty in the config
|
||||
ttd := big.NewInt(int64(len(blocks)))
|
||||
ttd.Mul(ttd, params.GenesisDifficulty)
|
||||
|
|
|
|||
|
|
@ -345,7 +345,8 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
|
|||
gen(i, b)
|
||||
}
|
||||
|
||||
block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, b.txs, b.uncles, b.receipts, b.withdrawals)
|
||||
body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals}
|
||||
block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, &body, b.receipts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ func TestGeneratePOSChain(t *testing.T) {
|
|||
gspec = &Genesis{
|
||||
Config: &config,
|
||||
Alloc: types.GenesisAlloc{
|
||||
address: {Balance: funds},
|
||||
params.BeaconRootsStorageAddress: {Balance: common.Big0, Code: asm4788},
|
||||
address: {Balance: funds},
|
||||
params.BeaconRootsAddress: {Balance: common.Big0, Code: asm4788},
|
||||
},
|
||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||
Difficulty: common.Big1,
|
||||
|
|
@ -180,7 +180,7 @@ func TestGeneratePOSChain(t *testing.T) {
|
|||
}
|
||||
state, _ := blockchain.State()
|
||||
idx := block.Time()%8191 + 8191
|
||||
got := state.GetState(params.BeaconRootsStorageAddress, common.BigToHash(new(big.Int).SetUint64(idx)))
|
||||
got := state.GetState(params.BeaconRootsAddress, common.BigToHash(new(big.Int).SetUint64(idx)))
|
||||
if got != want {
|
||||
t.Fatalf("block %d, wrong parent beacon root in state: got %s, want %s", i, got, want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import (
|
|||
|
||||
var errGenesisNoConfig = errors.New("genesis has no chain configuration")
|
||||
|
||||
// Deprecated: use types.GenesisAccount instead.
|
||||
// Deprecated: use types.Account instead.
|
||||
type GenesisAccount = types.Account
|
||||
|
||||
// Deprecated: use types.GenesisAlloc instead.
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ func ReadStateScheme(db ethdb.Reader) string {
|
|||
// the stored state.
|
||||
//
|
||||
// - If the provided scheme is none, use the scheme consistent with persistent
|
||||
// state, or fallback to hash-based scheme if state is empty.
|
||||
// state, or fallback to path-based scheme if state is empty.
|
||||
//
|
||||
// - If the provided scheme is hash, use hash-based scheme or error out if not
|
||||
// compatible with persistent state scheme.
|
||||
|
|
@ -329,10 +329,8 @@ func ParseStateScheme(provided string, disk ethdb.Database) (string, error) {
|
|||
stored := ReadStateScheme(disk)
|
||||
if provided == "" {
|
||||
if stored == "" {
|
||||
// use default scheme for empty database, flip it when
|
||||
// path mode is chosen as default
|
||||
log.Info("State schema set to default", "scheme", "hash")
|
||||
return HashScheme, nil
|
||||
log.Info("State schema set to default", "scheme", "path")
|
||||
return PathScheme, nil // use default scheme for empty database
|
||||
}
|
||||
log.Info("State scheme set to already existing", "scheme", stored)
|
||||
return stored, nil // reuse scheme of persistent scheme
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
package rawdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
|
@ -43,8 +43,6 @@ const (
|
|||
// The background thread will keep moving ancient chain segments from key-value
|
||||
// database to flat files for saving space on live database.
|
||||
type chainFreezer struct {
|
||||
threshold atomic.Uint64 // Number of recent blocks not to freeze (params.FullImmutabilityThreshold apart from tests)
|
||||
|
||||
*Freezer
|
||||
quit chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
|
@ -57,13 +55,11 @@ func newChainFreezer(datadir string, namespace string, readonly bool) (*chainFre
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf := chainFreezer{
|
||||
return &chainFreezer{
|
||||
Freezer: freezer,
|
||||
quit: make(chan struct{}),
|
||||
trigger: make(chan chan struct{}),
|
||||
}
|
||||
cf.threshold.Store(params.FullImmutabilityThreshold)
|
||||
return &cf, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the chain freezer instance and terminates the background thread.
|
||||
|
|
@ -77,6 +73,57 @@ func (f *chainFreezer) Close() error {
|
|||
return f.Freezer.Close()
|
||||
}
|
||||
|
||||
// readHeadNumber returns the number of chain head block. 0 is returned if the
|
||||
// block is unknown or not available yet.
|
||||
func (f *chainFreezer) readHeadNumber(db ethdb.KeyValueReader) uint64 {
|
||||
hash := ReadHeadBlockHash(db)
|
||||
if hash == (common.Hash{}) {
|
||||
log.Error("Head block is not reachable")
|
||||
return 0
|
||||
}
|
||||
number := ReadHeaderNumber(db, hash)
|
||||
if number == nil {
|
||||
log.Error("Number of head block is missing")
|
||||
return 0
|
||||
}
|
||||
return *number
|
||||
}
|
||||
|
||||
// readFinalizedNumber returns the number of finalized block. 0 is returned
|
||||
// if the block is unknown or not available yet.
|
||||
func (f *chainFreezer) readFinalizedNumber(db ethdb.KeyValueReader) uint64 {
|
||||
hash := ReadFinalizedBlockHash(db)
|
||||
if hash == (common.Hash{}) {
|
||||
return 0
|
||||
}
|
||||
number := ReadHeaderNumber(db, hash)
|
||||
if number == nil {
|
||||
log.Error("Number of finalized block is missing")
|
||||
return 0
|
||||
}
|
||||
return *number
|
||||
}
|
||||
|
||||
// freezeThreshold returns the threshold for chain freezing. It's determined
|
||||
// by formula: max(finality, HEAD-params.FullImmutabilityThreshold).
|
||||
func (f *chainFreezer) freezeThreshold(db ethdb.KeyValueReader) (uint64, error) {
|
||||
var (
|
||||
head = f.readHeadNumber(db)
|
||||
final = f.readFinalizedNumber(db)
|
||||
headLimit uint64
|
||||
)
|
||||
if head > params.FullImmutabilityThreshold {
|
||||
headLimit = head - params.FullImmutabilityThreshold
|
||||
}
|
||||
if final == 0 && headLimit == 0 {
|
||||
return 0, errors.New("freezing threshold is not available")
|
||||
}
|
||||
if final > headLimit {
|
||||
return final, nil
|
||||
}
|
||||
return headLimit, nil
|
||||
}
|
||||
|
||||
// freeze is a background thread that periodically checks the blockchain for any
|
||||
// import progress and moves ancient data from the fast database into the freezer.
|
||||
//
|
||||
|
|
@ -114,60 +161,39 @@ func (f *chainFreezer) freeze(db ethdb.KeyValueStore) {
|
|||
return
|
||||
}
|
||||
}
|
||||
// Retrieve the freezing threshold.
|
||||
hash := ReadHeadBlockHash(nfdb)
|
||||
if hash == (common.Hash{}) {
|
||||
log.Debug("Current full block hash unavailable") // new chain, empty database
|
||||
threshold, err := f.freezeThreshold(nfdb)
|
||||
if err != nil {
|
||||
backoff = true
|
||||
log.Debug("Current full block not old enough to freeze", "err", err)
|
||||
continue
|
||||
}
|
||||
number := ReadHeaderNumber(nfdb, hash)
|
||||
threshold := f.threshold.Load()
|
||||
frozen := f.frozen.Load()
|
||||
switch {
|
||||
case number == nil:
|
||||
log.Error("Current full block number unavailable", "hash", hash)
|
||||
backoff = true
|
||||
continue
|
||||
|
||||
case *number < threshold:
|
||||
log.Debug("Current full block not old enough to freeze", "number", *number, "hash", hash, "delay", threshold)
|
||||
backoff = true
|
||||
continue
|
||||
|
||||
case *number-threshold <= frozen:
|
||||
log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", frozen)
|
||||
// Short circuit if the blocks below threshold are already frozen.
|
||||
if frozen != 0 && frozen-1 >= threshold {
|
||||
backoff = true
|
||||
log.Debug("Ancient blocks frozen already", "threshold", threshold, "frozen", frozen)
|
||||
continue
|
||||
}
|
||||
head := ReadHeader(nfdb, hash, *number)
|
||||
if head == nil {
|
||||
log.Error("Current full block unavailable", "number", *number, "hash", hash)
|
||||
backoff = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Seems we have data ready to be frozen, process in usable batches
|
||||
var (
|
||||
start = time.Now()
|
||||
first, _ = f.Ancients()
|
||||
limit = *number - threshold
|
||||
start = time.Now()
|
||||
first = frozen // the first block to freeze
|
||||
last = threshold // the last block to freeze
|
||||
)
|
||||
if limit-first > freezerBatchLimit {
|
||||
limit = first + freezerBatchLimit
|
||||
if last-first+1 > freezerBatchLimit {
|
||||
last = freezerBatchLimit + first - 1
|
||||
}
|
||||
ancients, err := f.freezeRange(nfdb, first, limit)
|
||||
ancients, err := f.freezeRange(nfdb, first, last)
|
||||
if err != nil {
|
||||
log.Error("Error in block freeze operation", "err", err)
|
||||
backoff = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Batch of blocks have been frozen, flush them before wiping from leveldb
|
||||
if err := f.Sync(); err != nil {
|
||||
log.Crit("Failed to flush frozen tables", "err", err)
|
||||
}
|
||||
|
||||
// Wipe out all data from the active database
|
||||
batch := db.NewBatch()
|
||||
for i := 0; i < len(ancients); i++ {
|
||||
|
|
@ -250,8 +276,11 @@ func (f *chainFreezer) freeze(db ethdb.KeyValueStore) {
|
|||
}
|
||||
}
|
||||
|
||||
// freezeRange moves a batch of chain segments from the fast database to the freezer.
|
||||
// The parameters (number, limit) specify the relevant block range, both of which
|
||||
// are included.
|
||||
func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hashes []common.Hash, err error) {
|
||||
hashes = make([]common.Hash, 0, limit-number)
|
||||
hashes = make([]common.Hash, 0, limit-number+1)
|
||||
|
||||
_, err = f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
||||
for ; number <= limit; number++ {
|
||||
|
|
@ -293,11 +322,9 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash
|
|||
if err := op.AppendRaw(ChainFreezerDifficultyTable, number, td); err != nil {
|
||||
return fmt.Errorf("can't write td to Freezer: %v", err)
|
||||
}
|
||||
|
||||
hashes = append(hashes, hash)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return hashes, err
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue