@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
)
)
var (
var (
@ -184,46 +185,56 @@ func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64
return updates , committees , nil
return updates , committees , nil
}
}
// GetOptimisticHead Update fetches a signed header based on the latest available
// GetOptimisticUpdate fetches the latest available optimistic update.
// optimistic update. Note that the signature should be verified by the caller
// Note that the signature should be verified by the caller as its validity
// as its validity depends on the update chain.
// depends on the update chain.
//
//
// See data structure definition here:
// See data structure definition here:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate
func ( api * BeaconLightApi ) GetOptimisticHead Update ( ) ( types . SignedHeader , error ) {
func ( api * BeaconLightApi ) GetOptimisticUpdate ( ) ( types . OptimisticUpdate , error ) {
resp , err := api . httpGet ( "/eth/v1/beacon/light_client/optimistic_update" )
resp , err := api . httpGet ( "/eth/v1/beacon/light_client/optimistic_update" )
if err != nil {
if err != nil {
return types . SignedHeader { } , err
return types . OptimisticUpdate { } , err
}
}
return decodeOptimisticHead Update ( resp )
return decodeOptimisticUpdate ( resp )
}
}
func decodeOptimisticHead Update ( enc [ ] byte ) ( types . SignedHeader , error ) {
func decodeOptimisticUpdate ( enc [ ] byte ) ( types . OptimisticUpdate , error ) {
var data struct {
var data struct {
Version string
Data struct {
Data struct {
Header jsonBeaconHeader ` json:"attested_header" `
Attested jsonHeaderWithExecProof ` json:"attested_header" `
Aggregate types . SyncAggregate ` json:"sync_aggregate" `
Aggregate types . SyncAggregate ` json:"sync_aggregate" `
SignatureSlot common . Decimal ` json:"signature_slot" `
SignatureSlot common . Decimal ` json:"signature_slot" `
} ` json:"data" `
} ` json:"data" `
}
}
if err := json . Unmarshal ( enc , & data ) ; err != nil {
if err := json . Unmarshal ( enc , & data ) ; err != nil {
return types . SignedHeader { } , err
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 . Header . Beacon . StateRoot == ( common . Hash { } ) {
if data . Data . Attested . Beacon . StateRoot == ( common . Hash { } ) {
// workaround for different event encoding format in Lodestar
// workaround for different event encoding format in Lodestar
if err := json . Unmarshal ( enc , & data . Data ) ; err != nil {
if err := json . Unmarshal ( enc , & data . Data ) ; err != nil {
return types . SignedHeader { } , err
return types . OptimisticUpdate { } , err
}
}
}
}
if len ( data . Data . Aggregate . Signers ) != params . SyncCommitteeBitmaskSize {
if len ( data . Data . Aggregate . Signers ) != params . SyncCommitteeBitmaskSize {
return types . SignedHeader { } , errors . New ( "invalid sync_committee_bits length" )
return types . OptimisticUpdate { } , errors . New ( "invalid sync_committee_bits length" )
}
}
if len ( data . Data . Aggregate . Signature ) != params . BLSSignatureSize {
if len ( data . Data . Aggregate . Signature ) != params . BLSSignatureSize {
return types . SignedHeader { } , errors . New ( "invalid sync_committee_signature length" )
return types . OptimisticUpdate { } , errors . New ( "invalid sync_committee_signature length" )
}
}
return types . SignedHeader {
return types . OptimisticUpdate {
Header : data . Data . Header . Beacon ,
Attested : types . HeaderWithExecProof {
Header : data . Data . Attested . Beacon ,
PayloadHeader : attestedExecHeader ,
PayloadBranch : data . Data . Attested . ExecutionBranch ,
} ,
Signature : data . Data . Aggregate ,
Signature : data . Data . Aggregate ,
SignatureSlot : uint64 ( data . Data . SignatureSlot ) ,
SignatureSlot : uint64 ( data . Data . SignatureSlot ) ,
} , nil
} , nil
@ -411,7 +422,7 @@ func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) {
type HeadEventListener struct {
type HeadEventListener struct {
OnNewHead func ( slot uint64 , blockRoot common . Hash )
OnNewHead func ( slot uint64 , blockRoot common . Hash )
OnSignedHead func ( head types . SignedHeader )
OnOptimistic func ( head types . OptimisticUpdate )
OnFinality func ( head types . FinalityUpdate )
OnFinality func ( head types . FinalityUpdate )
OnError func ( err error )
OnError func ( err error )
}
}
@ -449,21 +460,35 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
defer wg . Done ( )
defer wg . Done ( )
// Request initial data.
// Request initial data.
log . Trace ( "Requesting initial head header" )
if head , _ , _ , err := api . GetHeader ( common . Hash { } ) ; err == nil {
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 ( ) )
listener . OnNewHead ( head . Slot , head . Hash ( ) )
} else {
log . Debug ( "Failed to retrieve initial head header" , "error" , err )
}
}
if signedHead , err := api . GetOptimisticHeadUpdate ( ) ; err == nil {
log . Trace ( "Requesting initial optimistic update" )
listener . OnSignedHead ( signedHead )
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 {
if finalityUpdate , err := api . GetFinalityUpdate ( ) ; err == nil {
log . Trace ( "Retrieved initial finality update" , "slot" , finalityUpdate . Finalized . Slot , "hash" , finalityUpdate . Finalized . Hash ( ) )
listener . OnFinality ( finalityUpdate )
listener . OnFinality ( finalityUpdate )
} else {
log . Debug ( "Failed to retrieve initial finality update" , "error" , err )
}
}
log . Trace ( "Starting event stream processing loop" )
// Receive the stream.
// Receive the stream.
var stream * eventsource . Stream
var stream * eventsource . Stream
select {
select {
case stream = <- streamCh :
case stream = <- streamCh :
case <- ctx . Done ( ) :
case <- ctx . Done ( ) :
log . Trace ( "Stopping event stream processing loop" )
return
return
}
}
@ -474,8 +499,10 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
case event , ok := <- stream . Events :
case event , ok := <- stream . Events :
if ! ok {
if ! ok {
log . Trace ( "Event stream closed" )
return
return
}
}
log . Trace ( "New event received from event stream" , "type" , event . Event ( ) )
switch event . Event ( ) {
switch event . Event ( ) {
case "head" :
case "head" :
slot , blockRoot , err := decodeHeadEvent ( [ ] byte ( event . Data ( ) ) )
slot , blockRoot , err := decodeHeadEvent ( [ ] byte ( event . Data ( ) ) )
@ -485,9 +512,9 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
listener . OnError ( fmt . Errorf ( "error decoding head event: %v" , err ) )
listener . OnError ( fmt . Errorf ( "error decoding head event: %v" , err ) )
}
}
case "light_client_optimistic_update" :
case "light_client_optimistic_update" :
signedHead , err := decodeOptimisticHead Update ( [ ] byte ( event . Data ( ) ) )
optimisticUpdate , err := decodeOptimisticUpdate ( [ ] byte ( event . Data ( ) ) )
if err == nil {
if err == nil {
listener . OnSignedHead ( signedHead )
listener . OnOptimistic ( optimisticUpdate )
} else {
} else {
listener . OnError ( fmt . Errorf ( "error decoding optimistic update event: %v" , err ) )
listener . OnError ( fmt . Errorf ( "error decoding optimistic update event: %v" , err ) )
}
}
@ -521,7 +548,8 @@ func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func()
// established. It can only return nil when the context is canceled.
// established. It can only return nil when the context is canceled.
func ( api * BeaconLightApi ) startEventStream ( ctx context . Context , listener * HeadEventListener ) * eventsource . Stream {
func ( api * BeaconLightApi ) startEventStream ( ctx context . Context , listener * HeadEventListener ) * eventsource . Stream {
for retry := true ; retry ; retry = ctxSleep ( ctx , 5 * time . Second ) {
for retry := true ; retry ; retry = ctxSleep ( ctx , 5 * time . Second ) {
path := "/eth/v1/events?topics=head&topics=light_client_optimistic_update&topics=light_client_finality_update"
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 )
req , err := http . NewRequestWithContext ( ctx , "GET" , api . url + path , nil )
if err != nil {
if err != nil {
listener . OnError ( fmt . Errorf ( "error creating event subscription request: %v" , err ) )
listener . OnError ( fmt . Errorf ( "error creating event subscription request: %v" , err ) )
@ -535,6 +563,7 @@ func (api *BeaconLightApi) startEventStream(ctx context.Context, listener *HeadE
listener . OnError ( fmt . Errorf ( "error creating event subscription: %v" , err ) )
listener . OnError ( fmt . Errorf ( "error creating event subscription: %v" , err ) )
continue
continue
}
}
log . Trace ( "Successfully created event stream" )
return stream
return stream
}
}
return nil
return nil