Skip to content

Commit aadcb88

Browse files
zsfelfoldifjl
andauthored
cmd/blsync, beacon/light: beacon chain light client (#28822)
Here we add a beacon chain light client for use by geth. Geth can now be configured to run against a beacon chain API endpoint, without pointing a CL to it. To set this up, use the `--beacon.api` flag. Information provided by the beacon chain is verified, i.e. geth does not blindly trust the beacon API endpoint in this mode. The root of trust are the beacon chain 'sync committees'. The configured beacon API endpoint must provide light client data. At this time, only Lodestar and Nimbus provide the necessary APIs. There is also a standalone tool, cmd/blsync, which uses the beacon chain light client to drive any EL implementation via its engine API. --------- Co-authored-by: Felix Lange <[email protected]>
1 parent d8e0807 commit aadcb88

31 files changed

+4049
-19
lines changed

beacon/blsync/block_sync.go

+203
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2023 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package blsync
18+
19+
import (
20+
"fmt"
21+
"math/big"
22+
23+
"github.com/ethereum/go-ethereum/beacon/engine"
24+
"github.com/ethereum/go-ethereum/beacon/light/request"
25+
"github.com/ethereum/go-ethereum/beacon/light/sync"
26+
"github.com/ethereum/go-ethereum/beacon/types"
27+
"github.com/ethereum/go-ethereum/common"
28+
"github.com/ethereum/go-ethereum/common/lru"
29+
ctypes "github.com/ethereum/go-ethereum/core/types"
30+
"github.com/ethereum/go-ethereum/event"
31+
"github.com/ethereum/go-ethereum/log"
32+
"github.com/ethereum/go-ethereum/trie"
33+
"github.com/holiman/uint256"
34+
"github.com/protolambda/zrnt/eth2/beacon/capella"
35+
"github.com/protolambda/zrnt/eth2/configs"
36+
"github.com/protolambda/ztyp/tree"
37+
)
38+
39+
// beaconBlockSync implements request.Module; it fetches the beacon blocks belonging
40+
// to the validated and prefetch heads.
41+
type beaconBlockSync struct {
42+
recentBlocks *lru.Cache[common.Hash, *capella.BeaconBlock]
43+
locked map[common.Hash]request.ServerAndID
44+
serverHeads map[request.Server]common.Hash
45+
headTracker headTracker
46+
47+
lastHeadInfo types.HeadInfo
48+
chainHeadFeed *event.Feed
49+
}
50+
51+
type headTracker interface {
52+
PrefetchHead() types.HeadInfo
53+
ValidatedHead() (types.SignedHeader, bool)
54+
ValidatedFinality() (types.FinalityUpdate, bool)
55+
}
56+
57+
// newBeaconBlockSync returns a new beaconBlockSync.
58+
func newBeaconBlockSync(headTracker headTracker, chainHeadFeed *event.Feed) *beaconBlockSync {
59+
return &beaconBlockSync{
60+
headTracker: headTracker,
61+
chainHeadFeed: chainHeadFeed,
62+
recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10),
63+
locked: make(map[common.Hash]request.ServerAndID),
64+
serverHeads: make(map[request.Server]common.Hash),
65+
}
66+
}
67+
68+
// Process implements request.Module.
69+
func (s *beaconBlockSync) Process(requester request.Requester, events []request.Event) {
70+
for _, event := range events {
71+
switch event.Type {
72+
case request.EvResponse, request.EvFail, request.EvTimeout:
73+
sid, req, resp := event.RequestInfo()
74+
blockRoot := common.Hash(req.(sync.ReqBeaconBlock))
75+
if resp != nil {
76+
s.recentBlocks.Add(blockRoot, resp.(*capella.BeaconBlock))
77+
}
78+
if s.locked[blockRoot] == sid {
79+
delete(s.locked, blockRoot)
80+
}
81+
case sync.EvNewHead:
82+
s.serverHeads[event.Server] = event.Data.(types.HeadInfo).BlockRoot
83+
case request.EvUnregistered:
84+
delete(s.serverHeads, event.Server)
85+
}
86+
}
87+
s.updateEventFeed()
88+
// request validated head block if unavailable and not yet requested
89+
if vh, ok := s.headTracker.ValidatedHead(); ok {
90+
s.tryRequestBlock(requester, vh.Header.Hash(), false)
91+
}
92+
// request prefetch head if the given server has announced it
93+
if prefetchHead := s.headTracker.PrefetchHead().BlockRoot; prefetchHead != (common.Hash{}) {
94+
s.tryRequestBlock(requester, prefetchHead, true)
95+
}
96+
}
97+
98+
func (s *beaconBlockSync) tryRequestBlock(requester request.Requester, blockRoot common.Hash, needSameHead bool) {
99+
if _, ok := s.recentBlocks.Get(blockRoot); ok {
100+
return
101+
}
102+
if _, ok := s.locked[blockRoot]; ok {
103+
return
104+
}
105+
for _, server := range requester.CanSendTo() {
106+
if needSameHead && (s.serverHeads[server] != blockRoot) {
107+
continue
108+
}
109+
id := requester.Send(server, sync.ReqBeaconBlock(blockRoot))
110+
s.locked[blockRoot] = request.ServerAndID{Server: server, ID: id}
111+
return
112+
}
113+
}
114+
115+
func blockHeadInfo(block *capella.BeaconBlock) types.HeadInfo {
116+
if block == nil {
117+
return types.HeadInfo{}
118+
}
119+
return types.HeadInfo{Slot: uint64(block.Slot), BlockRoot: beaconBlockHash(block)}
120+
}
121+
122+
// beaconBlockHash calculates the hash of a beacon block.
123+
func beaconBlockHash(beaconBlock *capella.BeaconBlock) common.Hash {
124+
return common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn()))
125+
}
126+
127+
// getExecBlock extracts the execution block from the beacon block's payload.
128+
func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) {
129+
payload := &beaconBlock.Body.ExecutionPayload
130+
txs := make([]*ctypes.Transaction, len(payload.Transactions))
131+
for i, opaqueTx := range payload.Transactions {
132+
var tx ctypes.Transaction
133+
if err := tx.UnmarshalBinary(opaqueTx); err != nil {
134+
return nil, fmt.Errorf("failed to parse tx %d: %v", i, err)
135+
}
136+
txs[i] = &tx
137+
}
138+
withdrawals := make([]*ctypes.Withdrawal, len(payload.Withdrawals))
139+
for i, w := range payload.Withdrawals {
140+
withdrawals[i] = &ctypes.Withdrawal{
141+
Index: uint64(w.Index),
142+
Validator: uint64(w.ValidatorIndex),
143+
Address: common.Address(w.Address),
144+
Amount: uint64(w.Amount),
145+
}
146+
}
147+
wroot := ctypes.DeriveSha(ctypes.Withdrawals(withdrawals), trie.NewStackTrie(nil))
148+
execHeader := &ctypes.Header{
149+
ParentHash: common.Hash(payload.ParentHash),
150+
UncleHash: ctypes.EmptyUncleHash,
151+
Coinbase: common.Address(payload.FeeRecipient),
152+
Root: common.Hash(payload.StateRoot),
153+
TxHash: ctypes.DeriveSha(ctypes.Transactions(txs), trie.NewStackTrie(nil)),
154+
ReceiptHash: common.Hash(payload.ReceiptsRoot),
155+
Bloom: ctypes.Bloom(payload.LogsBloom),
156+
Difficulty: common.Big0,
157+
Number: new(big.Int).SetUint64(uint64(payload.BlockNumber)),
158+
GasLimit: uint64(payload.GasLimit),
159+
GasUsed: uint64(payload.GasUsed),
160+
Time: uint64(payload.Timestamp),
161+
Extra: []byte(payload.ExtraData),
162+
MixDigest: common.Hash(payload.PrevRandao), // reused in merge
163+
Nonce: ctypes.BlockNonce{}, // zero
164+
BaseFee: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(),
165+
WithdrawalsHash: &wroot,
166+
}
167+
execBlock := ctypes.NewBlockWithHeader(execHeader).WithBody(txs, nil).WithWithdrawals(withdrawals)
168+
if execBlockHash := execBlock.Hash(); execBlockHash != common.Hash(payload.BlockHash) {
169+
return execBlock, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", common.Hash(payload.BlockHash), execBlockHash)
170+
}
171+
return execBlock, nil
172+
}
173+
174+
func (s *beaconBlockSync) updateEventFeed() {
175+
head, ok := s.headTracker.ValidatedHead()
176+
if !ok {
177+
return
178+
}
179+
finality, ok := s.headTracker.ValidatedFinality() //TODO fetch directly if subscription does not deliver
180+
if !ok || head.Header.Epoch() != finality.Attested.Header.Epoch() {
181+
return
182+
}
183+
validatedHead := head.Header.Hash()
184+
headBlock, ok := s.recentBlocks.Get(validatedHead)
185+
if !ok {
186+
return
187+
}
188+
headInfo := blockHeadInfo(headBlock)
189+
if headInfo == s.lastHeadInfo {
190+
return
191+
}
192+
s.lastHeadInfo = headInfo
193+
// new head block and finality info available; extract executable data and send event to feed
194+
execBlock, err := getExecBlock(headBlock)
195+
if err != nil {
196+
log.Error("Error extracting execution block from validated beacon block", "error", err)
197+
return
198+
}
199+
s.chainHeadFeed.Send(types.ChainHeadEvent{
200+
HeadBlock: engine.BlockToExecutableData(execBlock, nil, nil).ExecutionPayload,
201+
Finalized: common.Hash(finality.Finalized.PayloadHeader.BlockHash),
202+
})
203+
}

beacon/blsync/block_sync_test.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2023 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package blsync
18+
19+
import (
20+
"testing"
21+
22+
"github.com/ethereum/go-ethereum/beacon/light/request"
23+
"github.com/ethereum/go-ethereum/beacon/light/sync"
24+
"github.com/ethereum/go-ethereum/beacon/types"
25+
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/event"
27+
"github.com/protolambda/zrnt/eth2/beacon/capella"
28+
"github.com/protolambda/zrnt/eth2/configs"
29+
"github.com/protolambda/ztyp/tree"
30+
)
31+
32+
var (
33+
testServer1 = "testServer1"
34+
testServer2 = "testServer2"
35+
36+
testBlock1 = &capella.BeaconBlock{
37+
Slot: 123,
38+
Body: capella.BeaconBlockBody{
39+
ExecutionPayload: capella.ExecutionPayload{BlockNumber: 456},
40+
},
41+
}
42+
testBlock2 = &capella.BeaconBlock{
43+
Slot: 124,
44+
Body: capella.BeaconBlockBody{
45+
ExecutionPayload: capella.ExecutionPayload{BlockNumber: 457},
46+
},
47+
}
48+
)
49+
50+
func init() {
51+
eb1, _ := getExecBlock(testBlock1)
52+
testBlock1.Body.ExecutionPayload.BlockHash = tree.Root(eb1.Hash())
53+
eb2, _ := getExecBlock(testBlock2)
54+
testBlock2.Body.ExecutionPayload.BlockHash = tree.Root(eb2.Hash())
55+
}
56+
57+
func TestBlockSync(t *testing.T) {
58+
ht := &testHeadTracker{}
59+
eventFeed := new(event.Feed)
60+
blockSync := newBeaconBlockSync(ht, eventFeed)
61+
headCh := make(chan types.ChainHeadEvent, 16)
62+
eventFeed.Subscribe(headCh)
63+
ts := sync.NewTestScheduler(t, blockSync)
64+
ts.AddServer(testServer1, 1)
65+
ts.AddServer(testServer2, 1)
66+
67+
expHeadBlock := func(tci int, expHead *capella.BeaconBlock) {
68+
var expNumber, headNumber uint64
69+
if expHead != nil {
70+
expNumber = uint64(expHead.Body.ExecutionPayload.BlockNumber)
71+
}
72+
select {
73+
case event := <-headCh:
74+
headNumber = event.HeadBlock.Number
75+
default:
76+
}
77+
if headNumber != expNumber {
78+
t.Errorf("Wrong head block in test case #%d (expected block number %d, got %d)", tci, expNumber, headNumber)
79+
}
80+
}
81+
82+
// no block requests expected until head tracker knows about a head
83+
ts.Run(1)
84+
expHeadBlock(1, nil)
85+
86+
// set block 1 as prefetch head, announced by server 2
87+
head1 := blockHeadInfo(testBlock1)
88+
ht.prefetch = head1
89+
ts.ServerEvent(sync.EvNewHead, testServer2, head1)
90+
// expect request to server 2 which has announced the head
91+
ts.Run(2, testServer2, sync.ReqBeaconBlock(head1.BlockRoot))
92+
93+
// valid response
94+
ts.RequestEvent(request.EvResponse, ts.Request(2, 1), testBlock1)
95+
ts.AddAllowance(testServer2, 1)
96+
ts.Run(3)
97+
// head block still not expected as the fetched block is not the validated head yet
98+
expHeadBlock(3, nil)
99+
100+
// set as validated head, expect no further requests but block 1 set as head block
101+
ht.validated.Header = blockHeader(testBlock1)
102+
ts.Run(4)
103+
expHeadBlock(4, testBlock1)
104+
105+
// set block 2 as prefetch head, announced by server 1
106+
head2 := blockHeadInfo(testBlock2)
107+
ht.prefetch = head2
108+
ts.ServerEvent(sync.EvNewHead, testServer1, head2)
109+
// expect request to server 1
110+
ts.Run(5, testServer1, sync.ReqBeaconBlock(head2.BlockRoot))
111+
112+
// req2 fails, no further requests expected because server 2 has not announced it
113+
ts.RequestEvent(request.EvFail, ts.Request(5, 1), nil)
114+
ts.Run(6)
115+
116+
// set as validated head before retrieving block; now it's assumed to be available from server 2 too
117+
ht.validated.Header = blockHeader(testBlock2)
118+
// expect req2 retry to server 2
119+
ts.Run(7, testServer2, sync.ReqBeaconBlock(head2.BlockRoot))
120+
// now head block should be unavailable again
121+
expHeadBlock(4, nil)
122+
123+
// valid response, now head block should be block 2 immediately as it is already validated
124+
ts.RequestEvent(request.EvResponse, ts.Request(7, 1), testBlock2)
125+
ts.Run(8)
126+
expHeadBlock(5, testBlock2)
127+
}
128+
129+
func blockHeader(block *capella.BeaconBlock) types.Header {
130+
return types.Header{
131+
Slot: uint64(block.Slot),
132+
ProposerIndex: uint64(block.ProposerIndex),
133+
ParentRoot: common.Hash(block.ParentRoot),
134+
StateRoot: common.Hash(block.StateRoot),
135+
BodyRoot: common.Hash(block.Body.HashTreeRoot(configs.Mainnet, tree.GetHashFn())),
136+
}
137+
}
138+
139+
type testHeadTracker struct {
140+
prefetch types.HeadInfo
141+
validated types.SignedHeader
142+
}
143+
144+
func (h *testHeadTracker) PrefetchHead() types.HeadInfo {
145+
return h.prefetch
146+
}
147+
148+
func (h *testHeadTracker) ValidatedHead() (types.SignedHeader, bool) {
149+
return h.validated, h.validated.Header != (types.Header{})
150+
}
151+
152+
// TODO add test case for finality
153+
func (h *testHeadTracker) ValidatedFinality() (types.FinalityUpdate, bool) {
154+
return types.FinalityUpdate{
155+
Attested: types.HeaderWithExecProof{Header: h.validated.Header},
156+
Finalized: types.HeaderWithExecProof{PayloadHeader: &capella.ExecutionPayloadHeader{}},
157+
Signature: h.validated.Signature,
158+
SignatureSlot: h.validated.SignatureSlot,
159+
}, h.validated.Header != (types.Header{})
160+
}

0 commit comments

Comments
 (0)