mirror of https://github.com/ethereum/go-ethereum
commit
e7ebe92f27
@ -0,0 +1,163 @@ |
||||
// 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/params" |
||||
"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 |
||||
ValidatedOptimistic() (types.OptimisticUpdate, 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)) |
||||
log.Debug("Beacon block event", "type", event.Type.Name, "hash", blockRoot) |
||||
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.ValidatedOptimistic(); ok { |
||||
s.tryRequestBlock(requester, vh.Attested.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() { |
||||
optimistic, ok := s.headTracker.ValidatedOptimistic() |
||||
if !ok { |
||||
return |
||||
} |
||||
|
||||
validatedHead := optimistic.Attested.Hash() |
||||
headBlock, ok := s.recentBlocks.Get(validatedHead) |
||||
if !ok { |
||||
return |
||||
} |
||||
|
||||
var finalizedHash common.Hash |
||||
if finality, ok := s.headTracker.ValidatedFinality(); ok { |
||||
he := optimistic.Attested.Epoch() |
||||
fe := finality.Attested.Header.Epoch() |
||||
switch { |
||||
case he == fe: |
||||
finalizedHash = finality.Finalized.PayloadHeader.BlockHash() |
||||
case he < fe: |
||||
return |
||||
case he == fe+1: |
||||
parent, ok := s.recentBlocks.Get(optimistic.Attested.ParentRoot) |
||||
if !ok || parent.Slot()/params.EpochLength == fe { |
||||
return // head is at first slot of next epoch, wait for finality update
|
||||
} |
||||
} |
||||
} |
||||
|
||||
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: optimistic.Attested.Header, |
||||
Block: execBlock, |
||||
Finalized: finalizedHash, |
||||
}) |
||||
} |
@ -0,0 +1,160 @@ |
||||
// 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 = testServer("testServer1") |
||||
testServer2 = testServer("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")), |
||||
}, |
||||
}, |
||||
}) |
||||
) |
||||
|
||||
type testServer string |
||||
|
||||
func (t testServer) Name() string { |
||||
return string(t) |
||||
} |
||||
|
||||
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) ValidatedOptimistic() (types.OptimisticUpdate, bool) { |
||||
return types.OptimisticUpdate{ |
||||
Attested: types.HeaderWithExecProof{Header: h.validated.Header}, |
||||
Signature: h.validated.Signature, |
||||
SignatureSlot: h.validated.SignatureSlot, |
||||
}, 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{}) |
||||
} |
@ -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 |
||||
} |
@ -0,0 +1,129 @@ |
||||
// 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 { |
||||
var config lightClientConfig |
||||
customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name) |
||||
utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag, utils.BeaconConfigFlag) |
||||
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 |
||||
} |
||||
} |
||||
// Genesis root and time should always be specified together with custom chain config
|
||||
if customConfig { |
||||
if !ctx.IsSet(utils.BeaconGenesisRootFlag.Name) { |
||||
utils.Fatalf("Custom beacon chain config is specified but genesis root is missing") |
||||
} |
||||
if !ctx.IsSet(utils.BeaconGenesisTimeFlag.Name) { |
||||
utils.Fatalf("Custom beacon chain config is specified but genesis time is missing") |
||||
} |
||||
if !ctx.IsSet(utils.BeaconCheckpointFlag.Name) { |
||||
utils.Fatalf("Custom beacon chain config is specified but checkpoint is missing") |
||||
} |
||||
config.ChainConfig = &types.ChainConfig{ |
||||
GenesisTime: ctx.Uint64(utils.BeaconGenesisTimeFlag.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 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) |
||||
} |
||||
} else { |
||||
if ctx.IsSet(utils.BeaconGenesisRootFlag.Name) { |
||||
utils.Fatalf("Genesis root is specified but custom beacon chain config is missing") |
||||
} |
||||
if ctx.IsSet(utils.BeaconGenesisTimeFlag.Name) { |
||||
utils.Fatalf("Genesis time is specified but custom beacon chain config is missing") |
||||
} |
||||
} |
||||
// Checkpoint is required with custom chain config and is optional with pre-defined config
|
||||
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 |
||||
} |
@ -0,0 +1,150 @@ |
||||
// 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(): |
||||
log.Debug("Stopping engine API update loop") |
||||
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) |
||||
|
||||
log.Debug("Calling NewPayload", "number", event.Block.NumberU64(), "hash", event.Block.Hash()) |
||||
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) |
||||
} |
||||
|
||||
log.Debug("Calling ForkchoiceUpdated", "head", event.Block.Hash()) |
||||
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 |
||||
} |
@ -0,0 +1,114 @@ |
||||
// 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}}) |
||||
}, |
||||
OnOptimistic: func(update types.OptimisticUpdate) { |
||||
log.Debug("New optimistic update received", "slot", update.Attested.Slot, "blockRoot", update.Attested.Hash(), "signerCount", update.Signature.SignerCount()) |
||||
eventCallback(request.Event{Type: sync.EvNewOptimisticUpdate, Data: update}) |
||||
}, |
||||
OnFinality: func(update types.FinalityUpdate) { |
||||
log.Debug("New finality update received", "slot", update.Attested.Slot, "blockRoot", update.Attested.Hash(), "signerCount", update.Signature.SignerCount()) |
||||
eventCallback(request.Event{Type: sync.EvNewFinalityUpdate, Data: update}) |
||||
}, |
||||
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: |
||||
var r sync.RespHeader |
||||
log.Debug("Beacon API: requesting header", "reqid", id, "hash", common.Hash(data)) |
||||
r.Header, r.Canonical, r.Finalized, err = s.api.GetHeader(common.Hash(data)) |
||||
resp = r |
||||
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)) |
||||
case sync.ReqFinality: |
||||
log.Debug("Beacon API: requesting finality update") |
||||
resp, err = s.api.GetFinalityUpdate() |
||||
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 { |
||||
log.Debug("Beacon API request answered", "type", reflect.TypeOf(req), "reqid", id) |
||||
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 |
||||
} |
||||
} |
||||
|
||||
// Name implements request.Server
|
||||
func (s *ApiServer) Name() string { |
||||
return s.api.url |
||||
} |
@ -0,0 +1,578 @@ |
||||
// 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 ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"sync" |
||||
"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" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
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...)) |
||||
} |
||||
|
||||
// GetBestUpdatesAndCommittees 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 |
||||
} |
||||
|
||||
// GetOptimisticUpdate fetches 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) GetOptimisticUpdate() (types.OptimisticUpdate, error) { |
||||
resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update") |
||||
if err != nil { |
||||
return types.OptimisticUpdate{}, err |
||||
} |
||||
return decodeOptimisticUpdate(resp) |
||||
} |
||||
|
||||
func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) { |
||||
var data struct { |
||||
Version string |
||||
Data struct { |
||||
Attested jsonHeaderWithExecProof `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.OptimisticUpdate{}, err |
||||
} |
||||
// Decode the execution payload headers.
|
||||
attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution) |
||||
if err != nil { |
||||
return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err) |
||||
} |
||||
if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) { |
||||
// workaround for different event encoding format in Lodestar
|
||||
if err := json.Unmarshal(enc, &data.Data); err != nil { |
||||
return types.OptimisticUpdate{}, err |
||||
} |
||||
} |
||||
|
||||
if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize { |
||||
return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length") |
||||
} |
||||
if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize { |
||||
return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length") |
||||
} |
||||
return types.OptimisticUpdate{ |
||||
Attested: types.HeaderWithExecProof{ |
||||
Header: data.Data.Attested.Beacon, |
||||
PayloadHeader: attestedExecHeader, |
||||
PayloadBranch: data.Data.Attested.ExecutionBranch, |
||||
}, |
||||
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 |
||||
} |
||||
|
||||
// GetHeader fetches and validates the beacon header with the given blockRoot.
|
||||
// If blockRoot is null hash then the latest head header is fetched.
|
||||
// The values of the canonical and finalized flags are also returned. Note that
|
||||
// these flags are not validated.
|
||||
func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, bool, bool, 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{}, false, false, err |
||||
} |
||||
|
||||
var data struct { |
||||
Finalized bool `json:"finalized"` |
||||
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{}, false, false, err |
||||
} |
||||
header := data.Data.Header.Message |
||||
if blockRoot == (common.Hash{}) { |
||||
blockRoot = data.Data.Root |
||||
} |
||||
if header.Hash() != blockRoot { |
||||
return types.Header{}, false, false, errors.New("retrieved beacon header root does not match") |
||||
} |
||||
return header, data.Data.Canonical, data.Finalized, 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) |
||||
OnOptimistic func(head types.OptimisticUpdate) |
||||
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() { |
||||
var ( |
||||
ctx, closeCtx = context.WithCancel(context.Background()) |
||||
streamCh = make(chan *eventsource.Stream, 1) |
||||
wg sync.WaitGroup |
||||
) |
||||
|
||||
// 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.
|
||||
wg.Add(1) |
||||
go func() { |
||||
defer wg.Done() |
||||
stream := api.startEventStream(ctx, &listener) |
||||
if stream == nil { |
||||
// This case happens when the context was closed.
|
||||
return |
||||
} |
||||
// Stream was opened, wait for close signal.
|
||||
streamCh <- stream |
||||
<-ctx.Done() |
||||
stream.Close() |
||||
}() |
||||
|
||||
wg.Add(1) |
||||
go func() { |
||||
defer wg.Done() |
||||
|
||||
// Request initial data.
|
||||
log.Trace("Requesting initial head header") |
||||
if head, _, _, err := api.GetHeader(common.Hash{}); err == nil { |
||||
log.Trace("Retrieved initial head header", "slot", head.Slot, "hash", head.Hash()) |
||||
listener.OnNewHead(head.Slot, head.Hash()) |
||||
} else { |
||||
log.Debug("Failed to retrieve initial head header", "error", err) |
||||
} |
||||
log.Trace("Requesting initial optimistic update") |
||||
if optimisticUpdate, err := api.GetOptimisticUpdate(); err == nil { |
||||
log.Trace("Retrieved initial optimistic update", "slot", optimisticUpdate.Attested.Slot, "hash", optimisticUpdate.Attested.Hash()) |
||||
listener.OnOptimistic(optimisticUpdate) |
||||
} else { |
||||
log.Debug("Failed to retrieve initial optimistic update", "error", err) |
||||
} |
||||
log.Trace("Requesting initial finality update") |
||||
if finalityUpdate, err := api.GetFinalityUpdate(); err == nil { |
||||
log.Trace("Retrieved initial finality update", "slot", finalityUpdate.Finalized.Slot, "hash", finalityUpdate.Finalized.Hash()) |
||||
listener.OnFinality(finalityUpdate) |
||||
} else { |
||||
log.Debug("Failed to retrieve initial finality update", "error", err) |
||||
} |
||||
|
||||
log.Trace("Starting event stream processing loop") |
||||
// Receive the stream.
|
||||
var stream *eventsource.Stream |
||||
select { |
||||
case stream = <-streamCh: |
||||
case <-ctx.Done(): |
||||
log.Trace("Stopping event stream processing loop") |
||||
return |
||||
} |
||||
|
||||
for { |
||||
select { |
||||
case event, ok := <-stream.Events: |
||||
if !ok { |
||||
log.Trace("Event stream closed") |
||||
return |
||||
} |
||||
log.Trace("New event received from event stream", "type", event.Event()) |
||||
switch event.Event() { |
||||
case "head": |
||||
slot, blockRoot, err := decodeHeadEvent([]byte(event.Data())) |
||||
if err == nil { |
||||
listener.OnNewHead(slot, blockRoot) |
||||
} else { |
||||
listener.OnError(fmt.Errorf("error decoding head event: %v", err)) |
||||
} |
||||
case "light_client_optimistic_update": |
||||
optimisticUpdate, err := decodeOptimisticUpdate([]byte(event.Data())) |
||||
if err == nil { |
||||
listener.OnOptimistic(optimisticUpdate) |
||||
} else { |
||||
listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err)) |
||||
} |
||||
case "light_client_finality_update": |
||||
finalityUpdate, err := decodeFinalityUpdate([]byte(event.Data())) |
||||
if 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() { |
||||
closeCtx() |
||||
wg.Wait() |
||||
} |
||||
} |
||||
|
||||
// startEventStream establishes an event stream. This will keep retrying until the stream has been
|
||||
// established. It can only return nil when the context is canceled.
|
||||
func (api *BeaconLightApi) startEventStream(ctx context.Context, listener *HeadEventListener) *eventsource.Stream { |
||||
for retry := true; retry; retry = ctxSleep(ctx, 5*time.Second) { |
||||
path := "/eth/v1/events?topics=head&topics=light_client_finality_update&topics=light_client_optimistic_update" |
||||
log.Trace("Sending event subscription request") |
||||
req, err := http.NewRequestWithContext(ctx, "GET", api.url+path, nil) |
||||
if err != nil { |
||||
listener.OnError(fmt.Errorf("error creating event subscription request: %v", err)) |
||||
continue |
||||
} |
||||
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)) |
||||
continue |
||||
} |
||||
log.Trace("Successfully created event stream") |
||||
return stream |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func ctxSleep(ctx context.Context, timeout time.Duration) (ok bool) { |
||||
timer := time.NewTimer(timeout) |
||||
defer timer.Stop() |
||||
select { |
||||
case <-timer.C: |
||||
return true |
||||
case <-ctx.Done(): |
||||
return false |
||||
} |
||||
} |
@ -0,0 +1,161 @@ |
||||
// 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 |
||||
optimisticUpdate types.OptimisticUpdate |
||||
hasOptimisticUpdate 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, |
||||
} |
||||
} |
||||
|
||||
// ValidatedOptimistic returns the latest validated optimistic update.
|
||||
func (h *HeadTracker) ValidatedOptimistic() (types.OptimisticUpdate, bool) { |
||||
h.lock.RLock() |
||||
defer h.lock.RUnlock() |
||||
|
||||
return h.optimisticUpdate, h.hasOptimisticUpdate |
||||
} |
||||
|
||||
// ValidatedFinality returns the latest validated finality update.
|
||||
func (h *HeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) { |
||||
h.lock.RLock() |
||||
defer h.lock.RUnlock() |
||||
|
||||
return h.finalityUpdate, h.hasFinalityUpdate |
||||
} |
||||
|
||||
// ValidateOptimistic validates the given optimistic update. If the update is
|
||||
// successfully validated and it is better than the old validated update (higher
|
||||
// slot or same slot and more signers) then ValidatedOptimistic is updated.
|
||||
// The boolean return flag signals if ValidatedOptimistic has been changed.
|
||||
func (h *HeadTracker) ValidateOptimistic(update types.OptimisticUpdate) (bool, error) { |
||||
h.lock.Lock() |
||||
defer h.lock.Unlock() |
||||
|
||||
if err := update.Validate(); err != nil { |
||||
return false, err |
||||
} |
||||
replace, err := h.validate(update.SignedHeader(), h.optimisticUpdate.SignedHeader()) |
||||
if replace { |
||||
h.optimisticUpdate, h.hasOptimisticUpdate = update, true |
||||
h.changeCounter++ |
||||
} |
||||
return replace, err |
||||
} |
||||
|
||||
// ValidateFinality validates the given finality update. If the update is
|
||||
// successfully validated and it is better than the old validated update (higher
|
||||
// slot or same slot and more signers) then ValidatedFinality is updated.
|
||||
// The boolean return flag signals if ValidatedFinality has been changed.
|
||||
func (h *HeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) { |
||||
h.lock.Lock() |
||||
defer h.lock.Unlock() |
||||
|
||||
if err := update.Validate(); err != nil { |
||||
return false, err |
||||
} |
||||
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++ |
||||
} |
||||
|
||||
// ChangeCounter implements request.targetData
|
||||
func (h *HeadTracker) ChangeCounter() uint64 { |
||||
h.lock.RLock() |
||||
defer h.lock.RUnlock() |
||||
|
||||
return h.changeCounter |
||||
} |
@ -0,0 +1,403 @@ |
||||
// 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 the 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 interface { |
||||
Name() string |
||||
} |
||||
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) |
||||
} |
@ -0,0 +1,126 @@ |
||||
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) Name() string { |
||||
return "" |
||||
} |
||||
|
||||
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:] |
||||
} |
||||
} |
@ -0,0 +1,451 @@ |
||||
// 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 { |
||||
Name() string |
||||
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 { |
||||
Server |
||||
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 |
||||
} |
||||
|
||||
// Name implements request.Server
|
||||
func (s *serverWithTimeout) Name() string { |
||||
return s.parent.Name() |
||||
} |
||||
|
||||
// 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) |
||||
if s.childEventCb != nil { |
||||
s.childEventCb(event) |
||||
} |
||||
} |
||||
default: |
||||
if s.childEventCb != nil { |
||||
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() |
||||
if childEventCb != nil { |
||||
childEventCb(Event{Type: EvFail, Data: reqData}) |
||||
} |
||||
}) |
||||
childEventCb := s.childEventCb |
||||
s.lock.Unlock() |
||||
if childEventCb != nil { |
||||
childEventCb(Event{Type: EvTimeout, Data: reqData}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// unsubscribe stops all goroutines associated with the server.
|
||||
func (s *serverWithTimeout) unsubscribe() { |
||||
s.lock.Lock() |
||||
for _, timer := range s.timeouts { |
||||
if timer != nil { |
||||
timer.Stop() |
||||
} |
||||
} |
||||
s.lock.Unlock() |
||||
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 != nil { |
||||
childEventCb(event) |
||||
} |
||||
if sendCanRequestAgain && childEventCb != nil { |
||||
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) |
||||
} |
||||
|
||||
// unsubscribe stops all goroutines associated with the server.
|
||||
func (s *serverWithLimits) unsubscribe() { |
||||
s.lock.Lock() |
||||
if s.delayTimer != nil { |
||||
s.delayTimer.Stop() |
||||
s.delayTimer = nil |
||||
} |
||||
s.childEventCb = nil |
||||
s.lock.Unlock() |
||||
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 != nil { |
||||
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 != nil { |
||||
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)) |
||||
} |
@ -0,0 +1,182 @@ |
||||
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) |
||||
srv.unsubscribe() |
||||
} |
||||
|
||||
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) { |
||||
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) |
||||
srv.unsubscribe() |
||||
} |
||||
|
||||
func TestServerUnsubscribe(t *testing.T) { |
||||
rs := &testRequestServer{} |
||||
clock := &mclock.Simulated{} |
||||
srv := NewServer(rs, clock) |
||||
var eventCount int |
||||
srv.subscribe(func(event Event) { |
||||
eventCount++ |
||||
}) |
||||
eventCb := rs.eventCb |
||||
eventCb(Event{Type: testEventType}) |
||||
if eventCount != 1 { |
||||
t.Errorf("Server event callback not called before unsubscribe") |
||||
} |
||||
srv.unsubscribe() |
||||
if rs.eventCb != nil { |
||||
t.Errorf("Server event callback not removed after unsubscribe") |
||||
} |
||||
eventCb(Event{Type: testEventType}) |
||||
if eventCount != 1 { |
||||
t.Errorf("Server event callback called after unsubscribe") |
||||
} |
||||
} |
||||
|
||||
type testRequestServer struct { |
||||
eventCb func(Event) |
||||
} |
||||
|
||||
func (rs *testRequestServer) Name() string { return "" } |
||||
func (rs *testRequestServer) Subscribe(eventCb func(Event)) { rs.eventCb = eventCb } |
||||
func (rs *testRequestServer) SendRequest(ID, Request) {} |
||||
func (rs *testRequestServer) Unsubscribe() { rs.eventCb = nil } |
@ -0,0 +1,202 @@ |
||||
// 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/log" |
||||
) |
||||
|
||||
type headTracker interface { |
||||
ValidateOptimistic(update types.OptimisticUpdate) (bool, error) |
||||
ValidateFinality(head types.FinalityUpdate) (bool, error) |
||||
ValidatedFinality() (types.FinalityUpdate, bool) |
||||
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 |
||||
unvalidatedOptimistic map[request.Server]types.OptimisticUpdate |
||||
unvalidatedFinality map[request.Server]types.FinalityUpdate |
||||
serverHeads map[request.Server]types.HeadInfo |
||||
reqFinalityEpoch map[request.Server]uint64 // next epoch to request finality update
|
||||
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, |
||||
unvalidatedOptimistic: make(map[request.Server]types.OptimisticUpdate), |
||||
unvalidatedFinality: make(map[request.Server]types.FinalityUpdate), |
||||
serverHeads: make(map[request.Server]types.HeadInfo), |
||||
headServerCount: make(map[types.HeadInfo]headServerCount), |
||||
reqFinalityEpoch: make(map[request.Server]uint64), |
||||
} |
||||
return s |
||||
} |
||||
|
||||
// Process implements request.Module.
|
||||
func (s *HeadSync) Process(requester request.Requester, events []request.Event) { |
||||
nextPeriod, chainInit := s.chain.NextSyncPeriod() |
||||
if nextPeriod != s.nextSyncPeriod || chainInit != s.chainInit { |
||||
s.nextSyncPeriod, s.chainInit = nextPeriod, chainInit |
||||
s.processUnvalidatedUpdates() |
||||
} |
||||
|
||||
for _, event := range events { |
||||
switch event.Type { |
||||
case EvNewHead: |
||||
s.setServerHead(event.Server, event.Data.(types.HeadInfo)) |
||||
case EvNewOptimisticUpdate: |
||||
update := event.Data.(types.OptimisticUpdate) |
||||
s.newOptimisticUpdate(event.Server, update) |
||||
epoch := update.Attested.Epoch() |
||||
if epoch < s.reqFinalityEpoch[event.Server] { |
||||
continue |
||||
} |
||||
if finality, ok := s.headTracker.ValidatedFinality(); ok && finality.Attested.Header.Epoch() >= epoch { |
||||
continue |
||||
} |
||||
requester.Send(event.Server, ReqFinality{}) |
||||
s.reqFinalityEpoch[event.Server] = epoch + 1 |
||||
case EvNewFinalityUpdate: |
||||
s.newFinalityUpdate(event.Server, event.Data.(types.FinalityUpdate)) |
||||
case request.EvResponse: |
||||
_, _, resp := event.RequestInfo() |
||||
s.newFinalityUpdate(event.Server, resp.(types.FinalityUpdate)) |
||||
case request.EvUnregistered: |
||||
s.setServerHead(event.Server, types.HeadInfo{}) |
||||
delete(s.serverHeads, event.Server) |
||||
delete(s.unvalidatedOptimistic, event.Server) |
||||
delete(s.unvalidatedFinality, event.Server) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// newOptimisticUpdate handles received optimistic update; either validates it if
|
||||
// the chain is properly synced or stores it for further validation.
|
||||
func (s *HeadSync) newOptimisticUpdate(server request.Server, optimisticUpdate types.OptimisticUpdate) { |
||||
if !s.chainInit || types.SyncPeriod(optimisticUpdate.SignatureSlot) > s.nextSyncPeriod { |
||||
s.unvalidatedOptimistic[server] = optimisticUpdate |
||||
return |
||||
} |
||||
if _, err := s.headTracker.ValidateOptimistic(optimisticUpdate); err != nil { |
||||
log.Debug("Error validating optimistic update", "error", err) |
||||
} |
||||
} |
||||
|
||||
// newFinalityUpdate handles received finality update; 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 |
||||
} |
||||
if _, err := s.headTracker.ValidateFinality(finalityUpdate); err != nil { |
||||
log.Debug("Error validating finality update", "error", err) |
||||
} |
||||
} |
||||
|
||||
// processUnvalidatedUpdates iterates the list of unvalidated updates and validates
|
||||
// those which can be validated.
|
||||
func (s *HeadSync) processUnvalidatedUpdates() { |
||||
if !s.chainInit { |
||||
return |
||||
} |
||||
for server, optimisticUpdate := range s.unvalidatedOptimistic { |
||||
if types.SyncPeriod(optimisticUpdate.SignatureSlot) <= s.nextSyncPeriod { |
||||
if _, err := s.headTracker.ValidateOptimistic(optimisticUpdate); err != nil { |
||||
log.Debug("Error validating deferred optimistic update", "error", err) |
||||
} |
||||
delete(s.unvalidatedOptimistic, server) |
||||
} |
||||
} |
||||
for server, finalityUpdate := range s.unvalidatedFinality { |
||||
if types.SyncPeriod(finalityUpdate.SignatureSlot) <= s.nextSyncPeriod { |
||||
if _, err := s.headTracker.ValidateFinality(finalityUpdate); err != nil { |
||||
log.Debug("Error validating deferred finality update", "error", err) |
||||
} |
||||
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 |
||||
} |
@ -0,0 +1,183 @@ |
||||
// 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/light/request" |
||||
"github.com/ethereum/go-ethereum/beacon/types" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
) |
||||
|
||||
var ( |
||||
testServer1 = testServer("testServer1") |
||||
testServer2 = testServer("testServer2") |
||||
testServer3 = testServer("testServer3") |
||||
testServer4 = testServer("testServer4") |
||||
testServer5 = testServer("testServer5") |
||||
|
||||
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}} |
||||
|
||||
testOptUpdate1 = types.OptimisticUpdate{SignatureSlot: 0x0124, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x0123, StateRoot: common.Hash{1}}}} |
||||
testOptUpdate2 = types.OptimisticUpdate{SignatureSlot: 0x2010, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x200e, StateRoot: common.Hash{2}}}} |
||||
// testOptUpdate3 is at the end of period 1 but signed in period 2
|
||||
testOptUpdate3 = types.OptimisticUpdate{SignatureSlot: 0x4000, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x3fff, StateRoot: common.Hash{3}}}} |
||||
testOptUpdate4 = types.OptimisticUpdate{SignatureSlot: 0x6444, Attested: types.HeaderWithExecProof{Header: types.Header{Slot: 0x6443, StateRoot: common.Hash{4}}}} |
||||
) |
||||
|
||||
func finality(opt types.OptimisticUpdate) types.FinalityUpdate { |
||||
return types.FinalityUpdate{ |
||||
SignatureSlot: opt.SignatureSlot, |
||||
Attested: opt.Attested, |
||||
Finalized: types.HeaderWithExecProof{Header: types.Header{Slot: (opt.Attested.Header.Slot - 64) & uint64(0xffffffffffffffe0)}}, |
||||
} |
||||
} |
||||
|
||||
type testServer string |
||||
|
||||
func (t testServer) Name() string { |
||||
return string(t) |
||||
} |
||||
|
||||
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(EvNewOptimisticUpdate, testServer1, testOptUpdate1) |
||||
ts.Run(1, testServer1, ReqFinality{}) |
||||
// 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.OptimisticUpdate{testOptUpdate1}) |
||||
|
||||
chain.SetNextSyncPeriod(1) |
||||
ts.ServerEvent(EvNewFinalityUpdate, testServer1, finality(testOptUpdate2)) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer1, testOptUpdate2) |
||||
ts.AddServer(testServer2, 1) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer2, testOptUpdate2) |
||||
ts.Run(3) |
||||
// expect both head announcements to be validated instantly
|
||||
ht.ExpValidated(t, 3, []types.OptimisticUpdate{testOptUpdate2, testOptUpdate2}) |
||||
|
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer1, testOptUpdate3) |
||||
ts.AddServer(testServer3, 1) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer3, testOptUpdate4) |
||||
// finality should be requested from both servers
|
||||
ts.Run(4, testServer1, ReqFinality{}, testServer3, ReqFinality{}) |
||||
// future period announced heads should be queued
|
||||
ht.ExpValidated(t, 4, nil) |
||||
|
||||
chain.SetNextSyncPeriod(2) |
||||
ts.Run(5) |
||||
// testOptUpdate3 can be validated now but not testOptUpdate4
|
||||
ht.ExpValidated(t, 5, []types.OptimisticUpdate{testOptUpdate3}) |
||||
|
||||
ts.AddServer(testServer4, 1) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer4, testOptUpdate3) |
||||
// new server joined with recent optimistic update but still no finality; should be requested
|
||||
ts.Run(6, testServer4, ReqFinality{}) |
||||
ht.ExpValidated(t, 6, []types.OptimisticUpdate{testOptUpdate3}) |
||||
|
||||
ts.AddServer(testServer5, 1) |
||||
ts.RequestEvent(request.EvResponse, ts.Request(6, 1), finality(testOptUpdate3)) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer5, testOptUpdate3) |
||||
// finality update request answered; new server should not be requested
|
||||
ts.Run(7) |
||||
ht.ExpValidated(t, 7, []types.OptimisticUpdate{testOptUpdate3}) |
||||
|
||||
// server 3 disconnected without proving period 3, its announced head should be dropped
|
||||
ts.RemoveServer(testServer3) |
||||
ts.Run(8) |
||||
ht.ExpValidated(t, 8, nil) |
||||
|
||||
chain.SetNextSyncPeriod(3) |
||||
ts.Run(9) |
||||
// testOptUpdate4 could be validated now but it's not queued by any registered server
|
||||
ht.ExpValidated(t, 9, nil) |
||||
|
||||
ts.ServerEvent(EvNewFinalityUpdate, testServer2, finality(testOptUpdate4)) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer2, testOptUpdate4) |
||||
ts.Run(10) |
||||
// now testOptUpdate4 should be validated
|
||||
ht.ExpValidated(t, 10, []types.OptimisticUpdate{testOptUpdate4}) |
||||
} |
||||
|
||||
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
|
||||
} |
@ -0,0 +1,259 @@ |
||||
// 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.Name(), 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.Name(), 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 (tc *TestCommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error { |
||||
tc.fsp, tc.nsp, tc.init = bootstrap.Header.SyncPeriod(), bootstrap.Header.SyncPeriod()+2, true |
||||
return nil |
||||
} |
||||
|
||||
func (tc *TestCommitteeChain) InsertUpdate(update *types.LightClientUpdate, nextCommittee *types.SerializedSyncCommittee) error { |
||||
period := update.AttestedHeader.Header.SyncPeriod() |
||||
if period < tc.fsp || period > tc.nsp || !tc.init { |
||||
return light.ErrInvalidPeriod |
||||
} |
||||
if period == tc.nsp { |
||||
tc.nsp++ |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (tc *TestCommitteeChain) NextSyncPeriod() (uint64, bool) { |
||||
return tc.nsp, tc.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 (tc *TestCommitteeChain) SetNextSyncPeriod(nsp uint64) { |
||||
tc.init, tc.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.OptimisticUpdate |
||||
finality types.FinalityUpdate |
||||
} |
||||
|
||||
func (ht *TestHeadTracker) ValidateOptimistic(update types.OptimisticUpdate) (bool, error) { |
||||
ht.validated = append(ht.validated, update) |
||||
return true, nil |
||||
} |
||||
|
||||
func (ht *TestHeadTracker) ValidateFinality(update types.FinalityUpdate) (bool, error) { |
||||
ht.finality = update |
||||
return true, nil |
||||
} |
||||
|
||||
func (ht *TestHeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) { |
||||
return ht.finality, ht.finality.Attested.Header != (types.Header{}) |
||||
} |
||||
|
||||
func (ht *TestHeadTracker) ExpValidated(t *testing.T, tci int, expHeads []types.OptimisticUpdate) { |
||||
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.Attested.Header.Slot, expHead.Attested.Header.Hash()) |
||||
continue |
||||
} |
||||
if !reflect.DeepEqual(ht.validated[i], expHead) { |
||||
vhead := ht.validated[i].Attested.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.Attested.Header.Slot, expHead.Attested.Header.Hash(), vhead.Slot, vhead.Hash()) |
||||
} |
||||
} |
||||
for i := len(expHeads); i < len(ht.validated); i++ { |
||||
vhead := ht.validated[i].Attested.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) |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
// 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
|
||||
EvNewOptimisticUpdate = &request.EventType{Name: "newOptimisticUpdate"} // data: types.OptimisticUpdate
|
||||
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 |
||||
RespHeader struct { |
||||
Header types.Header |
||||
Canonical, Finalized bool |
||||
} |
||||
ReqCheckpointData common.Hash |
||||
ReqBeaconBlock common.Hash |
||||
ReqFinality struct{} |
||||
) |
@ -0,0 +1,398 @@ |
||||
// 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/params" |
||||
"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 |
||||
// per-server state is used to track the state of requesting checkpoint header
|
||||
// info. Part of this info (canonical and finalized state) is not validated
|
||||
// and therefore it is requested from each server separately after it has
|
||||
// reported a missing checkpoint (which is also not validated info).
|
||||
serverState map[request.Server]serverState |
||||
// the following fields are used to determine whether the checkpoint is on
|
||||
// epoch boundary. This information is validated and therefore stored globally.
|
||||
parentHash common.Hash |
||||
hasEpochInfo, epochBoundary bool |
||||
cpSlot, parentSlot uint64 |
||||
} |
||||
|
||||
const ( |
||||
ssDefault = iota // no action yet or checkpoint requested
|
||||
ssNeedHeader // checkpoint req failed, need cp header
|
||||
ssHeaderRequested // cp header requested
|
||||
ssNeedParent // cp header slot %32 != 0, need parent to check epoch boundary
|
||||
ssParentRequested // cp parent header requested
|
||||
ssPrintStatus // has all necessary info, print log message if init still not successful
|
||||
ssDone // log message printed, no more action required
|
||||
) |
||||
|
||||
type serverState struct { |
||||
state int |
||||
hasHeader, canonical, finalized bool // stored per server because not validated
|
||||
} |
||||
|
||||
// NewCheckpointInit creates a new CheckpointInit.
|
||||
func NewCheckpointInit(chain committeeChain, checkpointHash common.Hash) *CheckpointInit { |
||||
return &CheckpointInit{ |
||||
chain: chain, |
||||
checkpointHash: checkpointHash, |
||||
serverState: make(map[request.Server]serverState), |
||||
} |
||||
} |
||||
|
||||
// Process implements request.Module.
|
||||
func (s *CheckpointInit) Process(requester request.Requester, events []request.Event) { |
||||
if s.initialized { |
||||
return |
||||
} |
||||
|
||||
for _, event := range events { |
||||
switch event.Type { |
||||
case request.EvResponse, request.EvFail, request.EvTimeout: |
||||
sid, req, resp := event.RequestInfo() |
||||
if s.locked == sid { |
||||
s.locked = request.ServerAndID{} |
||||
} |
||||
if event.Type == request.EvTimeout { |
||||
continue |
||||
} |
||||
switch s.serverState[sid.Server].state { |
||||
case ssDefault: |
||||
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") |
||||
} |
||||
s.serverState[sid.Server] = serverState{state: ssNeedHeader} |
||||
case ssHeaderRequested: |
||||
if resp == nil { |
||||
s.serverState[sid.Server] = serverState{state: ssPrintStatus} |
||||
continue |
||||
} |
||||
newState := serverState{ |
||||
hasHeader: true, |
||||
canonical: resp.(RespHeader).Canonical, |
||||
finalized: resp.(RespHeader).Finalized, |
||||
} |
||||
s.cpSlot, s.parentHash = resp.(RespHeader).Header.Slot, resp.(RespHeader).Header.ParentRoot |
||||
if s.cpSlot%params.EpochLength == 0 { |
||||
s.hasEpochInfo, s.epochBoundary = true, true |
||||
} |
||||
if s.hasEpochInfo { |
||||
newState.state = ssPrintStatus |
||||
} else { |
||||
newState.state = ssNeedParent |
||||
} |
||||
s.serverState[sid.Server] = newState |
||||
case ssParentRequested: |
||||
s.parentSlot = resp.(RespHeader).Header.Slot |
||||
s.hasEpochInfo, s.epochBoundary = true, s.cpSlot/params.EpochLength > s.parentSlot/params.EpochLength |
||||
newState := s.serverState[sid.Server] |
||||
newState.state = ssPrintStatus |
||||
s.serverState[sid.Server] = newState |
||||
} |
||||
|
||||
case request.EvUnregistered: |
||||
delete(s.serverState, event.Server) |
||||
} |
||||
} |
||||
|
||||
// start a request if possible
|
||||
for _, server := range requester.CanSendTo() { |
||||
switch s.serverState[server].state { |
||||
case ssDefault: |
||||
if s.locked == (request.ServerAndID{}) { |
||||
id := requester.Send(server, ReqCheckpointData(s.checkpointHash)) |
||||
s.locked = request.ServerAndID{Server: server, ID: id} |
||||
} |
||||
case ssNeedHeader: |
||||
requester.Send(server, ReqHeader(s.checkpointHash)) |
||||
newState := s.serverState[server] |
||||
newState.state = ssHeaderRequested |
||||
s.serverState[server] = newState |
||||
case ssNeedParent: |
||||
requester.Send(server, ReqHeader(s.parentHash)) |
||||
newState := s.serverState[server] |
||||
newState.state = ssParentRequested |
||||
s.serverState[server] = newState |
||||
} |
||||
} |
||||
|
||||
// print log message if necessary
|
||||
for server, state := range s.serverState { |
||||
if state.state != ssPrintStatus { |
||||
continue |
||||
} |
||||
switch { |
||||
case !state.hasHeader: |
||||
log.Error("blsync: checkpoint block is not available, reported as unknown", "server", server.Name()) |
||||
case !state.canonical: |
||||
log.Error("blsync: checkpoint block is not available, reported as non-canonical", "server", server.Name()) |
||||
case !s.hasEpochInfo: |
||||
// should be available if hasHeader is true and state is ssPrintStatus
|
||||
panic("checkpoint epoch info not available when printing retrieval status") |
||||
case !s.epochBoundary: |
||||
log.Error("blsync: checkpoint block is not first of epoch", "slot", s.cpSlot, "parent", s.parentSlot, "server", server.Name()) |
||||
case !state.finalized: |
||||
log.Error("blsync: checkpoint block is reported as non-finalized", "server", server.Name()) |
||||
default: |
||||
log.Error("blsync: checkpoint not available, but reported as finalized; specified checkpoint hash might be too old", "server", server.Name()) |
||||
} |
||||
s.serverState[server] = serverState{state: ssDone} |
||||
} |
||||
} |
||||
|
||||
// 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 EvNewOptimisticUpdate: |
||||
update := event.Data.(types.OptimisticUpdate) |
||||
s.nextSyncPeriod[event.Server] = types.SyncPeriod(update.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 |
||||
} |
@ -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(EvNewOptimisticUpdate, testServer1, types.OptimisticUpdate{SignatureSlot: 0x2000*100 + 0x1000}) |
||||
ts.AddServer(testServer2, 3) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer2, types.OptimisticUpdate{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(EvNewOptimisticUpdate, testServer1, types.OptimisticUpdate{SignatureSlot: 0x2000*15 + 0x1000}) |
||||
ts.AddServer(testServer2, 1) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer2, types.OptimisticUpdate{SignatureSlot: 0x2000*16 + 0x1000}) |
||||
ts.AddServer(testServer3, 1) |
||||
ts.ServerEvent(EvNewOptimisticUpdate, testServer3, types.OptimisticUpdate{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(EvNewOptimisticUpdate, testServer4, types.OptimisticUpdate{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 |
||||
} |
@ -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())) |
||||
} |
@ -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()) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
|
||||
// ExecutionHeaderFromJSON 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)) |
||||
} |
||||
} |
@ -0,0 +1,141 @@ |
||||
// 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).WithBody(types.Body{Transactions: transactions, Withdrawals: withdrawals}) |
||||
if hash := block.Hash(); hash != expectedHash { |
||||
return nil, 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 |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,65 +1,101 @@ |
||||
# This file contains sha256 checksums of optional build dependencies. |
||||
|
||||
# version:spec-tests 1.0.6 |
||||
# version:spec-tests 2.1.0 |
||||
# https://github.com/ethereum/execution-spec-tests/releases |
||||
# https://github.com/ethereum/execution-spec-tests/releases/download/v1.0.6/ |
||||
485af7b66cf41eb3a8c1bd46632913b8eb95995df867cf665617bbc9b4beedd1 fixtures_develop.tar.gz |
||||
# https://github.com/ethereum/execution-spec-tests/releases/download/v2.1.0/ |
||||
ca89c76851b0900bfcc3cbb9a26cbece1f3d7c64a3bed38723e914713290df6c fixtures_develop.tar.gz |
||||
|
||||
# version:golang 1.21.5 |
||||
# version:golang 1.22.4 |
||||
# https://go.dev/dl/ |
||||
285cbbdf4b6e6e62ed58f370f3f6d8c30825d6e56c5853c66d3c23bcdb09db19 go1.21.5.src.tar.gz |
||||
a2e1d5743e896e5fe1e7d96479c0a769254aed18cf216cf8f4c3a2300a9b3923 go1.21.5.darwin-amd64.tar.gz |
||||
d0f8ac0c4fb3efc223a833010901d02954e3923cfe2c9a2ff0e4254a777cc9cc go1.21.5.darwin-arm64.tar.gz |
||||
2c05bbe0dc62456b90b7ddd354a54f373b7c377a98f8b22f52ab694b4f6cca58 go1.21.5.freebsd-386.tar.gz |
||||
30b6c64e9a77129605bc12f836422bf09eec577a8c899ee46130aeff81567003 go1.21.5.freebsd-amd64.tar.gz |
||||
8f4dba9cf5c61757bbd7e9ebdb93b6a30a1b03f4a636a1ba0cc2f27b907ab8e1 go1.21.5.linux-386.tar.gz |
||||
e2bc0b3e4b64111ec117295c088bde5f00eeed1567999ff77bc859d7df70078e go1.21.5.linux-amd64.tar.gz |
||||
841cced7ecda9b2014f139f5bab5ae31785f35399f236b8b3e75dff2a2978d96 go1.21.5.linux-arm64.tar.gz |
||||
837f4bf4e22fcdf920ffeaa4abf3d02d1314e03725431065f4d44c46a01b42fe go1.21.5.linux-armv6l.tar.gz |
||||
907b8c6ec4be9b184952e5d3493be66b1746442394a8bc78556c56834cd7c38b go1.21.5.linux-ppc64le.tar.gz |
||||
9c4a81b72ebe44368813cd03684e1080a818bf915d84163abae2ed325a1b2dc0 go1.21.5.linux-s390x.tar.gz |
||||
6da2418889dfb37763d0eb149c4a8d728c029e12f0cd54fbca0a31ae547e2d34 go1.21.5.windows-386.zip |
||||
bbe603cde7c9dee658f45164b4d06de1eff6e6e6b800100824e7c00d56a9a92f go1.21.5.windows-amd64.zip |
||||
9b7acca50e674294e43202df4fbc26d5af4d8bc3170a3342a1514f09a2dab5e9 go1.21.5.windows-arm64.zip |
||||
fed720678e728a7ca30ba8d1ded1caafe27d16028fab0232b8ba8e22008fb784 go1.22.4.src.tar.gz |
||||
b9647fa9fc83a0cc5d4f092a19eaeaecf45f063a5aa7d4962fde65aeb7ae6ce1 go1.22.4.aix-ppc64.tar.gz |
||||
7788f40f3a46f201df1dc46ca640403eb535d5513fc33449164a90dbd229b761 go1.22.4.darwin-amd64.pkg |
||||
c95967f50aa4ace34af0c236cbdb49a9a3e80ee2ad09d85775cb4462a5c19ed3 go1.22.4.darwin-amd64.tar.gz |
||||
4036c88faf57a6b096916f1827edcdbf5290a47cc5f59956e88cdd9b1b71088c go1.22.4.darwin-arm64.pkg |
||||
242b78dc4c8f3d5435d28a0d2cec9b4c1aa999b601fb8aa59fb4e5a1364bf827 go1.22.4.darwin-arm64.tar.gz |
||||
f2fbb51af4719d3616efb482d6ed2b96579b474156f85a7ddc6f126764feec4b go1.22.4.dragonfly-amd64.tar.gz |
||||
7c54884bb9f274884651d41e61d1bc12738863ad1497e97ea19ad0e9aa6bf7b5 go1.22.4.freebsd-386.tar.gz |
||||
88d44500e1701dd35797619774d6dd51bf60f45a8338b0a82ddc018e4e63fb78 go1.22.4.freebsd-amd64.tar.gz |
||||
3d9efe47db142a22679aba46b1772e3900b0d87ae13bd2b3bc80dbf2ac0b2cd6 go1.22.4.freebsd-arm.tar.gz |
||||
726dc093cf020277be45debf03c3b02b43c2efb3e2a5d4fba8f52579d65327dc go1.22.4.freebsd-arm64.tar.gz |
||||
5f6b67e5e32f1d6ccb2d4dcb44934a5e2e870a877ba7443d86ec43cfc28afa71 go1.22.4.freebsd-riscv64.tar.gz |
||||
d56ecc2f85b6418a21ef83879594d0c42ab4f65391a676bb12254870e6690d63 go1.22.4.illumos-amd64.tar.gz |
||||
47a2a8d249a91eb8605c33bceec63aedda0441a43eac47b4721e3975ff916cec go1.22.4.linux-386.tar.gz |
||||
ba79d4526102575196273416239cca418a651e049c2b099f3159db85e7bade7d go1.22.4.linux-amd64.tar.gz |
||||
a8e177c354d2e4a1b61020aca3562e27ea3e8f8247eca3170e3fa1e0c2f9e771 go1.22.4.linux-arm64.tar.gz |
||||
e2b143fbacbc9cbd448e9ef41ac3981f0488ce849af1cf37e2341d09670661de go1.22.4.linux-armv6l.tar.gz |
||||
e2ff9436e4b34bf6926b06d97916e26d67a909a2effec17967245900f0816f1d go1.22.4.linux-loong64.tar.gz |
||||
73f0dcc60458c4770593b05a7bc01cc0d31fc98f948c0c2334812c7a1f2fc3f1 go1.22.4.linux-mips.tar.gz |
||||
417af97fc2630a647052375768be4c38adcc5af946352ea5b28613ea81ca5d45 go1.22.4.linux-mips64.tar.gz |
||||
7486e2d7dd8c98eb44df815ace35a7fe7f30b7c02326e3741bd934077508139b go1.22.4.linux-mips64le.tar.gz |
||||
69479c8aad301e459a8365b40cad1074a0dbba5defb9291669f94809c4c4be6e go1.22.4.linux-mipsle.tar.gz |
||||
dd238847e65bc3e2745caca475a5db6522a2fcf85cf6c38fc36a06642b19efd7 go1.22.4.linux-ppc64.tar.gz |
||||
a3e5834657ef92523f570f798fed42f1f87bc18222a16815ec76b84169649ec4 go1.22.4.linux-ppc64le.tar.gz |
||||
56a827ff7dc6245bcd7a1e9288dffaa1d8b0fd7468562264c1523daf3b4f1b4a go1.22.4.linux-riscv64.tar.gz |
||||
7590c3e278e2dc6040aae0a39da3ca1eb2e3921673a7304cc34d588c45889eec go1.22.4.linux-s390x.tar.gz |
||||
ddd2eebe34471a2502de6c5dad04ab27c9fc80cbde7a9ad5b3c66ecec4504e1d go1.22.4.netbsd-386.tar.gz |
||||
33af79f6f935f6fbacc5d23876450b3567b79348fc065beef8e64081127dd234 go1.22.4.netbsd-amd64.tar.gz |
||||
fa3550ebd5375a70b3bcd342b5a71f4bd271dcbbfaf4eabefa2144ab5d8924b6 go1.22.4.netbsd-arm.tar.gz |
||||
c9a2971dec9f6d320c6f2b049b2353c6d0a2d35e87b8a4b2d78a2f0d62545f8e go1.22.4.netbsd-arm64.tar.gz |
||||
d21af022331bfdc2b5b161d616c3a1a4573d33cf7a30416ee509a8f3641deb47 go1.22.4.openbsd-386.tar.gz |
||||
72c0094c43f7e5722ec49c2a3e9dfa7a1123ac43a5f3a63eecf3e3795d3ff0ae go1.22.4.openbsd-amd64.tar.gz |
||||
1096831ea3c5ea3ca57d14251d9eda3786889531eb40d7d6775dcaa324d4b065 go1.22.4.openbsd-arm.tar.gz |
||||
a7ab8d4e0b02bf06ed144ba42c61c0e93ee00f2b433415dfd4ad4b6e79f31650 go1.22.4.openbsd-arm64.tar.gz |
||||
9716327c8a628358798898dc5148c49dbbeb5196bf2cbf088e550721a6e4f60b go1.22.4.openbsd-ppc64.tar.gz |
||||
a8dd4503c95c32a502a616ab78870a19889c9325fe9bd31eb16dd69346e4bfa8 go1.22.4.plan9-386.tar.gz |
||||
5423a25808d76fe5aca8607a2e5ac5673abf45446b168cb5e9d8519ee9fe39a1 go1.22.4.plan9-amd64.tar.gz |
||||
6af939ad583f5c85c09c53728ab7d38c3cc2b39167562d6c18a07c5c6608b370 go1.22.4.plan9-arm.tar.gz |
||||
e8cabe69c03085725afdb32a6f9998191a3e55a747b270d835fd05000d56abba go1.22.4.solaris-amd64.tar.gz |
||||
5c6446e2ea80bc6a971d2b34446f16e6517e638b0ff8d3ea229228d1931790b0 go1.22.4.windows-386.msi |
||||
aca4e2c37278a10f1c70dd0df142f7d66b50334fcee48978d409202d308d6d25 go1.22.4.windows-386.zip |
||||
3c21105d7b584759b6e266383b777caf6e87142d304a10b539dbc66ab482bb5f go1.22.4.windows-amd64.msi |
||||
26321c4d945a0035d8a5bc4a1965b0df401ff8ceac66ce2daadabf9030419a98 go1.22.4.windows-amd64.zip |
||||
c4303f02b864304eb83dd1db0b4ebf9d2ec9d216e7ef44a7657b166a52889c7f go1.22.4.windows-arm.msi |
||||
5fcd0671a49cecf39b41021621ee1b6e7aa1370f37122b72e80d4fd4185833b6 go1.22.4.windows-arm.zip |
||||
553cc6c460f4e3eb4fad5b897c0bb22cd8bbeb20929f0e3eeb939420320292ce go1.22.4.windows-arm64.msi |
||||
8a2daa9ea28cbdafddc6171aefed384f4e5b6e714fb52116fe9ed25a132f37ed go1.22.4.windows-arm64.zip |
||||
|
||||
# version:golangci 1.55.2 |
||||
# version:golangci 1.59.0 |
||||
# https://github.com/golangci/golangci-lint/releases/ |
||||
# https://github.com/golangci/golangci-lint/releases/download/v1.55.2/ |
||||
632e96e6d5294fbbe7b2c410a49c8fa01c60712a0af85a567de85bcc1623ea21 golangci-lint-1.55.2-darwin-amd64.tar.gz |
||||
234463f059249f82045824afdcdd5db5682d0593052f58f6a3039a0a1c3899f6 golangci-lint-1.55.2-darwin-arm64.tar.gz |
||||
2bdd105e2d4e003a9058c33a22bb191a1e0f30fa0790acca0d8fbffac1d6247c golangci-lint-1.55.2-freebsd-386.tar.gz |
||||
e75056e8b082386676ce23eba455cf893931a792c0d87e1e3743c0aec33c7fb5 golangci-lint-1.55.2-freebsd-amd64.tar.gz |
||||
5789b933facaf6136bd23f1d50add67b79bbcf8dfdfc9069a37f729395940a66 golangci-lint-1.55.2-freebsd-armv6.tar.gz |
||||
7f21ab1008d05f32c954f99470fc86a83a059e530fe2add1d0b7d8ed4d8992a7 golangci-lint-1.55.2-freebsd-armv7.tar.gz |
||||
33ab06139b9219a28251f10821da94423db30285cc2af97494cbb2a281927de9 golangci-lint-1.55.2-illumos-amd64.tar.gz |
||||
57ce6f8ce3ad6ee45d7cc3d9a047545a851c2547637834a3fcb086c7b40b1e6b golangci-lint-1.55.2-linux-386.tar.gz |
||||
ca21c961a33be3bc15e4292dc40c98c8dcc5463a7b6768a3afc123761630c09c golangci-lint-1.55.2-linux-amd64.tar.gz |
||||
8eb0cee9b1dbf0eaa49871798c7f8a5b35f2960c52d776a5f31eb7d886b92746 golangci-lint-1.55.2-linux-arm64.tar.gz |
||||
3195f3e0f37d353fd5bd415cabcd4e263f5c29d3d0ffb176c26ff3d2c75eb3bb golangci-lint-1.55.2-linux-armv6.tar.gz |
||||
c823ee36eb1a719e171de1f2f5ca3068033dce8d9817232fd10ed71fd6650406 golangci-lint-1.55.2-linux-armv7.tar.gz |
||||
758a5d2a356dc494bd13ed4c0d4bf5a54a4dc91267ea5ecdd87b86c7ca0624e7 golangci-lint-1.55.2-linux-loong64.tar.gz |
||||
2c7b9abdce7cae802a67d583cd7c6dca520bff6d0e17c8535a918e2f2b437aa0 golangci-lint-1.55.2-linux-mips64.tar.gz |
||||
024e0a15b85352cc27271285526e16a4ab66d3e67afbbe446c9808c06cb8dbed golangci-lint-1.55.2-linux-mips64le.tar.gz |
||||
6b00f89ba5506c1de1efdd9fa17c54093013a294fefd8b9b31534db626a672ee golangci-lint-1.55.2-linux-ppc64le.tar.gz |
||||
0faa0d047d9bf7b703ed3ea65b6117043c93504f9ca1de25ae929d3901c73d4a golangci-lint-1.55.2-linux-riscv64.tar.gz |
||||
30dec9b22e7d5bb4e9d5ccea96da20f71cd7db3c8cf30b8ddc7cb9174c4d742a golangci-lint-1.55.2-linux-s390x.tar.gz |
||||
5a0ede48f79ad707902fdb29be8cd2abd8302dc122b65ebae3fdfc86751c7698 golangci-lint-1.55.2-netbsd-386.tar.gz |
||||
95af20a2e617126dd5b08122ece7819101070e1582a961067ce8c41172f901ad golangci-lint-1.55.2-netbsd-amd64.tar.gz |
||||
94fb7dacb7527847cc95d7120904e19a2a0a81a0d50d61766c9e0251da72ab9d golangci-lint-1.55.2-netbsd-armv6.tar.gz |
||||
ca906bce5fee9619400e4a321c56476fe4a4efb6ac4fc989d340eb5563348873 golangci-lint-1.55.2-netbsd-armv7.tar.gz |
||||
45b442f69fc8915c4500201c0247b7f3f69544dbc9165403a61f9095f2c57355 golangci-lint-1.55.2-windows-386.zip |
||||
f57d434d231d43417dfa631587522f8c1991220b43c8ffadb9c7bd279508bf81 golangci-lint-1.55.2-windows-amd64.zip |
||||
fd7dc8f4c6829ee6fafb252a4d81d2155cd35da7833665cbb25d53ce7cecd990 golangci-lint-1.55.2-windows-arm64.zip |
||||
1892c3c24f9e7ef44b02f6750c703864b6dc350129f3ec39510300007b2376f1 golangci-lint-1.55.2-windows-armv6.zip |
||||
a5e68ae73d38748b5269fad36ac7575e3c162a5dc63ef58abdea03cc5da4522a golangci-lint-1.55.2-windows-armv7.zip |
||||
# https://github.com/golangci/golangci-lint/releases/download/v1.59.0/ |
||||
418acf7e255ddc0783e97129c9b03d9311b77826a5311d425a01c708a86417e7 golangci-lint-1.59.0-darwin-amd64.tar.gz |
||||
5f6a1d95a6dd69f6e328eb56dd311a38e04cfab79a1305fbf4957f4e203f47b6 golangci-lint-1.59.0-darwin-arm64.tar.gz |
||||
8899bf589185d49f747f3e5db9f0bde8a47245a100c64a3dd4d65e8e92cfc4f2 golangci-lint-1.59.0-freebsd-386.tar.gz |
||||
658212f138d9df2ac89427e22115af34bf387c0871d70f2a25101718946a014f golangci-lint-1.59.0-freebsd-amd64.tar.gz |
||||
4c6395ea40f314d3b6fa17d8997baab93464d5d1deeaab513155e625473bd03a golangci-lint-1.59.0-freebsd-armv6.tar.gz |
||||
ff37da4fbaacdb6bbae70fdbdbb1ba932a859956f788c82822fa06bef5b7c6b3 golangci-lint-1.59.0-freebsd-armv7.tar.gz |
||||
439739469ed2bda182b1ec276d40c40e02f195537f78e3672996741ad223d6b6 golangci-lint-1.59.0-illumos-amd64.tar.gz |
||||
940801d46790e40d0a097d8fee34e2606f0ef148cd039654029b0b8750a15ed6 golangci-lint-1.59.0-linux-386.tar.gz |
||||
3b14a439f33c4fff83dbe0349950d984042b9a1feb6c62f82787b598fc3ab5f4 golangci-lint-1.59.0-linux-amd64.tar.gz |
||||
c57e6c0b0fa03089a2611dceddd5bc5d206716cccdff8b149da8baac598719a1 golangci-lint-1.59.0-linux-arm64.tar.gz |
||||
93149e2d3b25ac754df9a23172403d8aa6d021a7e0d9c090a12f51897f68c9a0 golangci-lint-1.59.0-linux-armv6.tar.gz |
||||
d10ac38239d9efee3ee87b55c96cdf3fa09e1a525babe3ffdaaf65ccc48cf3dc golangci-lint-1.59.0-linux-armv7.tar.gz |
||||
047338114b4f0d5f08f0fb9a397b03cc171916ed0960be7dfb355c2320cd5e9c golangci-lint-1.59.0-linux-loong64.tar.gz |
||||
5632df0f7f8fc03a80a266130faef0b5902d280cf60621f1b2bdc1aef6d97ee9 golangci-lint-1.59.0-linux-mips64.tar.gz |
||||
71dd638c82fa4439171e7126d2c7a32b5d103bfdef282cea40c83632cb3d1f4b golangci-lint-1.59.0-linux-mips64le.tar.gz |
||||
6cf9ea0d34e91669948483f9ae7f07da319a879344373a1981099fbd890cde00 golangci-lint-1.59.0-linux-ppc64le.tar.gz |
||||
af0205fa6fbab197cee613c359947711231739095d21b5c837086233b36ad971 golangci-lint-1.59.0-linux-riscv64.tar.gz |
||||
a9d2fb93f3c688ebccef94f5dc96c0b07c4d20bf6556cddebd8442159b0c80f6 golangci-lint-1.59.0-linux-s390x.tar.gz |
||||
68ab4c57a847b8ace9679887f2f8b2b6760e57ee29dcde8c3f40dd8bb2654fa2 golangci-lint-1.59.0-netbsd-386.tar.gz |
||||
d277b8b435c19406d00de4d509eadf5a024a5782878332e9a1b7c02bb76e87a7 golangci-lint-1.59.0-netbsd-amd64.tar.gz |
||||
83211656be8dcfa1545af4f92894409f412d1f37566798cb9460a526593ad62c golangci-lint-1.59.0-netbsd-arm64.tar.gz |
||||
6c6866d28bf79fa9817a0f7d2b050890ed109cae80bdb4dfa39536a7226da237 golangci-lint-1.59.0-netbsd-armv6.tar.gz |
||||
11587566363bd03ca586b7df9776ccaed569fcd1f3489930ac02f9375b307503 golangci-lint-1.59.0-netbsd-armv7.tar.gz |
||||
466181a8967bafa495e41494f93a0bec829c2cf715de874583b0460b3b8ae2b8 golangci-lint-1.59.0-windows-386.zip |
||||
3317d8a87a99a49a0a1321d295c010790e6dbf43ee96b318f4b8bb23eae7a565 golangci-lint-1.59.0-windows-amd64.zip |
||||
b3af955c7fceac8220a36fc799e1b3f19d3b247d32f422caac5f9845df8f7316 golangci-lint-1.59.0-windows-arm64.zip |
||||
6f083c7d0c764e5a0e5bde46ee3e91ae357d80c194190fe1d9754392e9064c7e golangci-lint-1.59.0-windows-armv6.zip |
||||
3709b4dd425deadab27748778d08e03c0f804d7748f7dd5b6bb488d98aa031c7 golangci-lint-1.59.0-windows-armv7.zip |
||||
|
||||
# This is the builder on PPA that will build Go itself (inception-y), don't modify! |
||||
# |
||||
# This version is fine to be old and full of security holes, we just use it |
||||
# to build the latest Go. Don't change it. If it ever becomes insufficient, |
||||
# we need to switch over to a recursive builder to jump across supported |
||||
# versions. |
||||
# to build the latest Go. Don't change it. |
||||
# |
||||
# version:ppa-builder 1.19.6 |
||||
# version:ppa-builder-1 1.19.6 |
||||
# https://go.dev/dl/ |
||||
d7f0013f82e6d7f862cc6cb5c8cdb48eef5f2e239b35baa97e2f1a7466043767 go1.19.6.src.tar.gz |
||||
|
||||
# version:ppa-builder-2 1.21.9 |
||||
# https://go.dev/dl/ |
||||
58f0c5ced45a0012bce2ff7a9df03e128abcc8818ebabe5027bb92bafe20e421 go1.21.9.src.tar.gz |
||||
|
@ -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 |
||||
} |
@ -0,0 +1,325 @@ |
||||
// Copyright 2023 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"math/big" |
||||
"os" |
||||
"path/filepath" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/internal/era" |
||||
"github.com/ethereum/go-ethereum/internal/ethapi" |
||||
"github.com/ethereum/go-ethereum/internal/flags" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/ethereum/go-ethereum/trie" |
||||
"github.com/urfave/cli/v2" |
||||
) |
||||
|
||||
var app = flags.NewApp("go-ethereum era tool") |
||||
|
||||
var ( |
||||
dirFlag = &cli.StringFlag{ |
||||
Name: "dir", |
||||
Usage: "directory storing all relevant era1 files", |
||||
Value: "eras", |
||||
} |
||||
networkFlag = &cli.StringFlag{ |
||||
Name: "network", |
||||
Usage: "network name associated with era1 files", |
||||
Value: "mainnet", |
||||
} |
||||
eraSizeFlag = &cli.IntFlag{ |
||||
Name: "size", |
||||
Usage: "number of blocks per era", |
||||
Value: era.MaxEra1Size, |
||||
} |
||||
txsFlag = &cli.BoolFlag{ |
||||
Name: "txs", |
||||
Usage: "print full transaction values", |
||||
} |
||||
) |
||||
|
||||
var ( |
||||
blockCommand = &cli.Command{ |
||||
Name: "block", |
||||
Usage: "get block data", |
||||
ArgsUsage: "<number>", |
||||
Action: block, |
||||
Flags: []cli.Flag{ |
||||
txsFlag, |
||||
}, |
||||
} |
||||
infoCommand = &cli.Command{ |
||||
Name: "info", |
||||
ArgsUsage: "<epoch>", |
||||
Usage: "get epoch information", |
||||
Action: info, |
||||
} |
||||
verifyCommand = &cli.Command{ |
||||
Name: "verify", |
||||
ArgsUsage: "<expected>", |
||||
Usage: "verifies each era1 against expected accumulator root", |
||||
Action: verify, |
||||
} |
||||
) |
||||
|
||||
func init() { |
||||
app.Commands = []*cli.Command{ |
||||
blockCommand, |
||||
infoCommand, |
||||
verifyCommand, |
||||
} |
||||
app.Flags = []cli.Flag{ |
||||
dirFlag, |
||||
networkFlag, |
||||
eraSizeFlag, |
||||
} |
||||
} |
||||
|
||||
func main() { |
||||
if err := app.Run(os.Args); err != nil { |
||||
fmt.Fprintf(os.Stderr, "%v\n", err) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
|
||||
// block prints the specified block from an era1 store.
|
||||
func block(ctx *cli.Context) error { |
||||
num, err := strconv.ParseUint(ctx.Args().First(), 10, 64) |
||||
if err != nil { |
||||
return fmt.Errorf("invalid block number: %w", err) |
||||
} |
||||
e, err := open(ctx, num/uint64(ctx.Int(eraSizeFlag.Name))) |
||||
if err != nil { |
||||
return fmt.Errorf("error opening era1: %w", err) |
||||
} |
||||
defer e.Close() |
||||
// Read block with number.
|
||||
block, err := e.GetBlockByNumber(num) |
||||
if err != nil { |
||||
return fmt.Errorf("error reading block %d: %w", num, err) |
||||
} |
||||
// Convert block to JSON and print.
|
||||
val := ethapi.RPCMarshalBlock(block, ctx.Bool(txsFlag.Name), ctx.Bool(txsFlag.Name), params.MainnetChainConfig) |
||||
b, err := json.MarshalIndent(val, "", " ") |
||||
if err != nil { |
||||
return fmt.Errorf("error marshaling json: %w", err) |
||||
} |
||||
fmt.Println(string(b)) |
||||
return nil |
||||
} |
||||
|
||||
// info prints some high-level information about the era1 file.
|
||||
func info(ctx *cli.Context) error { |
||||
epoch, err := strconv.ParseUint(ctx.Args().First(), 10, 64) |
||||
if err != nil { |
||||
return fmt.Errorf("invalid epoch number: %w", err) |
||||
} |
||||
e, err := open(ctx, epoch) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer e.Close() |
||||
acc, err := e.Accumulator() |
||||
if err != nil { |
||||
return fmt.Errorf("error reading accumulator: %w", err) |
||||
} |
||||
td, err := e.InitialTD() |
||||
if err != nil { |
||||
return fmt.Errorf("error reading total difficulty: %w", err) |
||||
} |
||||
info := struct { |
||||
Accumulator common.Hash `json:"accumulator"` |
||||
TotalDifficulty *big.Int `json:"totalDifficulty"` |
||||
StartBlock uint64 `json:"startBlock"` |
||||
Count uint64 `json:"count"` |
||||
}{ |
||||
acc, td, e.Start(), e.Count(), |
||||
} |
||||
b, _ := json.MarshalIndent(info, "", " ") |
||||
fmt.Println(string(b)) |
||||
return nil |
||||
} |
||||
|
||||
// open opens an era1 file at a certain epoch.
|
||||
func open(ctx *cli.Context, epoch uint64) (*era.Era, error) { |
||||
var ( |
||||
dir = ctx.String(dirFlag.Name) |
||||
network = ctx.String(networkFlag.Name) |
||||
) |
||||
entries, err := era.ReadDir(dir, network) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error reading era dir: %w", err) |
||||
} |
||||
if epoch >= uint64(len(entries)) { |
||||
return nil, fmt.Errorf("epoch out-of-bounds: last %d, want %d", len(entries)-1, 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 errors.New("missing accumulators file") |
||||
} |
||||
|
||||
roots, err := readHashes(ctx.Args().First()) |
||||
if err != nil { |
||||
return fmt.Errorf("unable to read expected roots file: %w", err) |
||||
} |
||||
|
||||
var ( |
||||
dir = ctx.String(dirFlag.Name) |
||||
network = ctx.String(networkFlag.Name) |
||||
start = time.Now() |
||||
reported = time.Now() |
||||
) |
||||
|
||||
entries, err := era.ReadDir(dir, network) |
||||
if err != nil { |
||||
return fmt.Errorf("error reading %s: %w", dir, err) |
||||
} |
||||
|
||||
if len(entries) != len(roots) { |
||||
return errors.New("number of era1 files should match the number of accumulator hashes") |
||||
} |
||||
|
||||
// Verify each epoch matches the expected root.
|
||||
for i, want := range roots { |
||||
// Wrap in function so defers don't stack.
|
||||
err := func() error { |
||||
name := entries[i] |
||||
e, err := era.Open(filepath.Join(dir, name)) |
||||
if err != nil { |
||||
return fmt.Errorf("error opening era1 file %s: %w", name, err) |
||||
} |
||||
defer e.Close() |
||||
// Read accumulator and check against expected.
|
||||
if got, err := e.Accumulator(); err != nil { |
||||
return fmt.Errorf("error retrieving accumulator for %s: %w", name, err) |
||||
} else if got != want { |
||||
return fmt.Errorf("invalid root %s: got %s, want %s", name, got, want) |
||||
} |
||||
// Recompute accumulator.
|
||||
if err := checkAccumulator(e); err != nil { |
||||
return fmt.Errorf("error verify era1 file %s: %w", name, err) |
||||
} |
||||
// Give the user some feedback that something is happening.
|
||||
if time.Since(reported) >= 8*time.Second { |
||||
fmt.Printf("Verifying Era1 files \t\t verified=%d,\t elapsed=%s\n", i, common.PrettyDuration(time.Since(start))) |
||||
reported = time.Now() |
||||
} |
||||
return nil |
||||
}() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// checkAccumulator verifies the accumulator matches the data in the Era.
|
||||
func checkAccumulator(e *era.Era) error { |
||||
var ( |
||||
err error |
||||
want common.Hash |
||||
td *big.Int |
||||
tds = make([]*big.Int, 0) |
||||
hashes = make([]common.Hash, 0) |
||||
) |
||||
if want, err = e.Accumulator(); err != nil { |
||||
return fmt.Errorf("error reading accumulator: %w", err) |
||||
} |
||||
if td, err = e.InitialTD(); err != nil { |
||||
return fmt.Errorf("error reading total difficulty: %w", err) |
||||
} |
||||
it, err := era.NewIterator(e) |
||||
if err != nil { |
||||
return fmt.Errorf("error making era iterator: %w", err) |
||||
} |
||||
// To fully verify an era the following attributes must be checked:
|
||||
// 1) the block index is constructed correctly
|
||||
// 2) the tx root matches the value in the block
|
||||
// 3) the receipts root matches the value in the block
|
||||
// 4) the starting total difficulty value is correct
|
||||
// 5) the accumulator is correct by recomputing it locally, which verifies
|
||||
// the blocks are all correct (via hash)
|
||||
//
|
||||
// The attributes 1), 2), and 3) are checked for each block. 4) and 5) require
|
||||
// accumulation across the entire set and are verified at the end.
|
||||
for it.Next() { |
||||
// 1) next() walks the block index, so we're able to implicitly verify it.
|
||||
if it.Error() != nil { |
||||
return fmt.Errorf("error reading block %d: %w", it.Number(), err) |
||||
} |
||||
block, receipts, err := it.BlockAndReceipts() |
||||
if it.Error() != nil { |
||||
return fmt.Errorf("error reading block %d: %w", it.Number(), err) |
||||
} |
||||
// 2) recompute tx root and verify against header.
|
||||
tr := types.DeriveSha(block.Transactions(), trie.NewStackTrie(nil)) |
||||
if tr != block.TxHash() { |
||||
return fmt.Errorf("tx root in block %d mismatch: want %s, got %s", block.NumberU64(), block.TxHash(), tr) |
||||
} |
||||
// 3) recompute receipt root and check value against block.
|
||||
rr := types.DeriveSha(receipts, trie.NewStackTrie(nil)) |
||||
if rr != block.ReceiptHash() { |
||||
return fmt.Errorf("receipt root in block %d mismatch: want %s, got %s", block.NumberU64(), block.ReceiptHash(), rr) |
||||
} |
||||
hashes = append(hashes, block.Hash()) |
||||
td.Add(td, block.Difficulty()) |
||||
tds = append(tds, new(big.Int).Set(td)) |
||||
} |
||||
// 4+5) Verify accumulator and total difficulty.
|
||||
got, err := era.ComputeAccumulator(hashes, tds) |
||||
if err != nil { |
||||
return fmt.Errorf("error computing accumulator: %w", err) |
||||
} |
||||
if got != want { |
||||
return fmt.Errorf("expected accumulator root does not match calculated: got %s, want %s", got, want) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// readHashes reads a file of newline-delimited hashes.
|
||||
func readHashes(f string) ([]common.Hash, error) { |
||||
b, err := os.ReadFile(f) |
||||
if err != nil { |
||||
return nil, errors.New("unable to open accumulators file") |
||||
} |
||||
s := strings.Split(string(b), "\n") |
||||
// Remove empty last element, if present.
|
||||
if s[len(s)-1] == "" { |
||||
s = s[:len(s)-1] |
||||
} |
||||
// Convert to hashes.
|
||||
r := make([]common.Hash, len(s)) |
||||
for i := range s { |
||||
r[i] = common.HexToHash(s[i]) |
||||
} |
||||
return r, nil |
||||
} |
@ -1,81 +0,0 @@ |
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package t8ntool |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/vm" |
||||
"github.com/ethereum/go-ethereum/eth/tracers" |
||||
"github.com/ethereum/go-ethereum/log" |
||||
) |
||||
|
||||
// traceWriter is an vm.EVMLogger which also holds an inner logger/tracer.
|
||||
// When the TxEnd event happens, the inner tracer result is written to the file, and
|
||||
// the file is closed.
|
||||
type traceWriter struct { |
||||
inner vm.EVMLogger |
||||
f io.WriteCloser |
||||
} |
||||
|
||||
// Compile-time interface check
|
||||
var _ = vm.EVMLogger((*traceWriter)(nil)) |
||||
|
||||
func (t *traceWriter) CaptureTxEnd(restGas uint64) { |
||||
t.inner.CaptureTxEnd(restGas) |
||||
defer t.f.Close() |
||||
|
||||
if tracer, ok := t.inner.(tracers.Tracer); ok { |
||||
result, err := tracer.GetResult() |
||||
if err != nil { |
||||
log.Warn("Error in tracer", "err", err) |
||||
return |
||||
} |
||||
err = json.NewEncoder(t.f).Encode(result) |
||||
if err != nil { |
||||
log.Warn("Error writing tracer output", "err", err) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureTxStart(gasLimit uint64) { t.inner.CaptureTxStart(gasLimit) } |
||||
func (t *traceWriter) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { |
||||
t.inner.CaptureStart(env, from, to, create, input, gas, value) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureEnd(output []byte, gasUsed uint64, err error) { |
||||
t.inner.CaptureEnd(output, gasUsed, err) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { |
||||
t.inner.CaptureEnter(typ, from, to, input, gas, value) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureExit(output []byte, gasUsed uint64, err error) { |
||||
t.inner.CaptureExit(output, gasUsed, err) |
||||
} |
||||
|
||||
func (t *traceWriter) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { |
||||
t.inner.CaptureState(pc, op, gas, cost, scope, rData, depth, err) |
||||
} |
||||
func (t *traceWriter) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { |
||||
t.inner.CaptureFault(pc, op, gas, cost, scope, depth, err) |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue