github.com/ethereum/go-ethereum@v1.16.1/beacon/light/api/light_api.go (about) 1 // Copyright 2022 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 detaiapi. 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 api 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "net/url" 27 "strconv" 28 "sync" 29 "time" 30 31 "github.com/donovanhide/eventsource" 32 "github.com/ethereum/go-ethereum/beacon/merkle" 33 "github.com/ethereum/go-ethereum/beacon/params" 34 "github.com/ethereum/go-ethereum/beacon/types" 35 "github.com/ethereum/go-ethereum/common" 36 "github.com/ethereum/go-ethereum/common/hexutil" 37 "github.com/ethereum/go-ethereum/log" 38 ) 39 40 var ( 41 ErrNotFound = errors.New("404 Not Found") 42 ErrInternal = errors.New("500 Internal Server Error") 43 ) 44 45 type CommitteeUpdate struct { 46 Update types.LightClientUpdate 47 NextSyncCommittee types.SerializedSyncCommittee 48 } 49 50 // See data structure definition here: 51 // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientupdate 52 type committeeUpdateJson struct { 53 Version string `json:"version"` 54 Data committeeUpdateData `json:"data"` 55 } 56 57 type committeeUpdateData struct { 58 Header jsonBeaconHeader `json:"attested_header"` 59 NextSyncCommittee types.SerializedSyncCommittee `json:"next_sync_committee"` 60 NextSyncCommitteeBranch merkle.Values `json:"next_sync_committee_branch"` 61 FinalizedHeader *jsonBeaconHeader `json:"finalized_header,omitempty"` 62 FinalityBranch merkle.Values `json:"finality_branch,omitempty"` 63 SyncAggregate types.SyncAggregate `json:"sync_aggregate"` 64 SignatureSlot common.Decimal `json:"signature_slot"` 65 } 66 67 type jsonBeaconHeader struct { 68 Beacon types.Header `json:"beacon"` 69 } 70 71 type jsonHeaderWithExecProof struct { 72 Beacon types.Header `json:"beacon"` 73 Execution json.RawMessage `json:"execution"` 74 ExecutionBranch merkle.Values `json:"execution_branch"` 75 } 76 77 // UnmarshalJSON unmarshals from JSON. 78 func (u *CommitteeUpdate) UnmarshalJSON(input []byte) error { 79 var dec committeeUpdateJson 80 if err := json.Unmarshal(input, &dec); err != nil { 81 return err 82 } 83 u.NextSyncCommittee = dec.Data.NextSyncCommittee 84 u.Update = types.LightClientUpdate{ 85 Version: dec.Version, 86 AttestedHeader: types.SignedHeader{ 87 Header: dec.Data.Header.Beacon, 88 Signature: dec.Data.SyncAggregate, 89 SignatureSlot: uint64(dec.Data.SignatureSlot), 90 }, 91 NextSyncCommitteeRoot: u.NextSyncCommittee.Root(), 92 NextSyncCommitteeBranch: dec.Data.NextSyncCommitteeBranch, 93 FinalityBranch: dec.Data.FinalityBranch, 94 } 95 if dec.Data.FinalizedHeader != nil { 96 u.Update.FinalizedHeader = &dec.Data.FinalizedHeader.Beacon 97 } 98 return nil 99 } 100 101 // fetcher is an interface useful for debug-harnessing the http api. 102 type fetcher interface { 103 Do(req *http.Request) (*http.Response, error) 104 } 105 106 // BeaconLightApi requests light client information from a beacon node REST API. 107 // Note: all required API endpoints are currently only implemented by Lodestar. 108 type BeaconLightApi struct { 109 url string 110 client fetcher 111 customHeaders map[string]string 112 } 113 114 func NewBeaconLightApi(url string, customHeaders map[string]string) *BeaconLightApi { 115 return &BeaconLightApi{ 116 url: url, 117 client: &http.Client{ 118 Timeout: time.Second * 10, 119 }, 120 customHeaders: customHeaders, 121 } 122 } 123 124 func (api *BeaconLightApi) httpGet(path string, params url.Values) ([]byte, error) { 125 uri, err := api.buildURL(path, params) 126 if err != nil { 127 return nil, err 128 } 129 req, err := http.NewRequest("GET", uri, nil) 130 if err != nil { 131 return nil, err 132 } 133 for k, v := range api.customHeaders { 134 req.Header.Set(k, v) 135 } 136 resp, err := api.client.Do(req) 137 if err != nil { 138 return nil, err 139 } 140 defer resp.Body.Close() 141 switch resp.StatusCode { 142 case 200: 143 return io.ReadAll(resp.Body) 144 case 404: 145 return nil, ErrNotFound 146 case 500: 147 return nil, ErrInternal 148 default: 149 return nil, fmt.Errorf("unexpected error from API endpoint \"%s\": status code %d", path, resp.StatusCode) 150 } 151 } 152 153 // GetBestUpdatesAndCommittees fetches and validates LightClientUpdate for given 154 // period and full serialized committee for the next period (committee root hash 155 // equals update.NextSyncCommitteeRoot). 156 // Note that the results are validated but the update signature should be verified 157 // by the caller as its validity depends on the update chain. 158 func (api *BeaconLightApi) GetBestUpdatesAndCommittees(firstPeriod, count uint64) ([]*types.LightClientUpdate, []*types.SerializedSyncCommittee, error) { 159 resp, err := api.httpGet("/eth/v1/beacon/light_client/updates", map[string][]string{ 160 "start_period": {strconv.FormatUint(firstPeriod, 10)}, 161 "count": {strconv.FormatUint(count, 10)}, 162 }) 163 if err != nil { 164 return nil, nil, err 165 } 166 167 var data []CommitteeUpdate 168 if err := json.Unmarshal(resp, &data); err != nil { 169 return nil, nil, err 170 } 171 if len(data) != int(count) { 172 return nil, nil, errors.New("invalid number of committee updates") 173 } 174 updates := make([]*types.LightClientUpdate, int(count)) 175 committees := make([]*types.SerializedSyncCommittee, int(count)) 176 for i, d := range data { 177 if d.Update.AttestedHeader.Header.SyncPeriod() != firstPeriod+uint64(i) { 178 return nil, nil, errors.New("wrong committee update header period") 179 } 180 if err := d.Update.Validate(); err != nil { 181 return nil, nil, err 182 } 183 if d.NextSyncCommittee.Root() != d.Update.NextSyncCommitteeRoot { 184 return nil, nil, errors.New("wrong sync committee root") 185 } 186 updates[i], committees[i] = new(types.LightClientUpdate), new(types.SerializedSyncCommittee) 187 *updates[i], *committees[i] = d.Update, d.NextSyncCommittee 188 } 189 return updates, committees, nil 190 } 191 192 // GetOptimisticUpdate fetches the latest available optimistic update. 193 // Note that the signature should be verified by the caller as its validity 194 // depends on the update chain. 195 // 196 // See data structure definition here: 197 // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientoptimisticupdate 198 func (api *BeaconLightApi) GetOptimisticUpdate() (types.OptimisticUpdate, error) { 199 resp, err := api.httpGet("/eth/v1/beacon/light_client/optimistic_update", nil) 200 if err != nil { 201 return types.OptimisticUpdate{}, err 202 } 203 return decodeOptimisticUpdate(resp) 204 } 205 206 func decodeOptimisticUpdate(enc []byte) (types.OptimisticUpdate, error) { 207 var data struct { 208 Version string `json:"version"` 209 Data struct { 210 Attested jsonHeaderWithExecProof `json:"attested_header"` 211 Aggregate types.SyncAggregate `json:"sync_aggregate"` 212 SignatureSlot common.Decimal `json:"signature_slot"` 213 } `json:"data"` 214 } 215 if err := json.Unmarshal(enc, &data); err != nil { 216 return types.OptimisticUpdate{}, err 217 } 218 // Decode the execution payload headers. 219 attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution) 220 if err != nil { 221 return types.OptimisticUpdate{}, fmt.Errorf("invalid attested header: %v", err) 222 } 223 if data.Data.Attested.Beacon.StateRoot == (common.Hash{}) { 224 // workaround for different event encoding format in Lodestar 225 if err := json.Unmarshal(enc, &data.Data); err != nil { 226 return types.OptimisticUpdate{}, err 227 } 228 } 229 230 if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize { 231 return types.OptimisticUpdate{}, errors.New("invalid sync_committee_bits length") 232 } 233 if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize { 234 return types.OptimisticUpdate{}, errors.New("invalid sync_committee_signature length") 235 } 236 return types.OptimisticUpdate{ 237 Attested: types.HeaderWithExecProof{ 238 Header: data.Data.Attested.Beacon, 239 PayloadHeader: attestedExecHeader, 240 PayloadBranch: data.Data.Attested.ExecutionBranch, 241 }, 242 Signature: data.Data.Aggregate, 243 SignatureSlot: uint64(data.Data.SignatureSlot), 244 }, nil 245 } 246 247 // GetFinalityUpdate fetches the latest available finality update. 248 // 249 // See data structure definition here: 250 // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientfinalityupdate 251 func (api *BeaconLightApi) GetFinalityUpdate() (types.FinalityUpdate, error) { 252 resp, err := api.httpGet("/eth/v1/beacon/light_client/finality_update", nil) 253 if err != nil { 254 return types.FinalityUpdate{}, err 255 } 256 return decodeFinalityUpdate(resp) 257 } 258 259 func decodeFinalityUpdate(enc []byte) (types.FinalityUpdate, error) { 260 var data struct { 261 Version string `json:"version"` 262 Data struct { 263 Attested jsonHeaderWithExecProof `json:"attested_header"` 264 Finalized jsonHeaderWithExecProof `json:"finalized_header"` 265 FinalityBranch merkle.Values `json:"finality_branch"` 266 Aggregate types.SyncAggregate `json:"sync_aggregate"` 267 SignatureSlot common.Decimal `json:"signature_slot"` 268 } 269 } 270 if err := json.Unmarshal(enc, &data); err != nil { 271 return types.FinalityUpdate{}, err 272 } 273 // Decode the execution payload headers. 274 attestedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Attested.Execution) 275 if err != nil { 276 return types.FinalityUpdate{}, fmt.Errorf("invalid attested header: %v", err) 277 } 278 finalizedExecHeader, err := types.ExecutionHeaderFromJSON(data.Version, data.Data.Finalized.Execution) 279 if err != nil { 280 return types.FinalityUpdate{}, fmt.Errorf("invalid finalized header: %v", err) 281 } 282 // Perform sanity checks. 283 if len(data.Data.Aggregate.Signers) != params.SyncCommitteeBitmaskSize { 284 return types.FinalityUpdate{}, errors.New("invalid sync_committee_bits length") 285 } 286 if len(data.Data.Aggregate.Signature) != params.BLSSignatureSize { 287 return types.FinalityUpdate{}, errors.New("invalid sync_committee_signature length") 288 } 289 290 return types.FinalityUpdate{ 291 Version: data.Version, 292 Attested: types.HeaderWithExecProof{ 293 Header: data.Data.Attested.Beacon, 294 PayloadHeader: attestedExecHeader, 295 PayloadBranch: data.Data.Attested.ExecutionBranch, 296 }, 297 Finalized: types.HeaderWithExecProof{ 298 Header: data.Data.Finalized.Beacon, 299 PayloadHeader: finalizedExecHeader, 300 PayloadBranch: data.Data.Finalized.ExecutionBranch, 301 }, 302 FinalityBranch: data.Data.FinalityBranch, 303 Signature: data.Data.Aggregate, 304 SignatureSlot: uint64(data.Data.SignatureSlot), 305 }, nil 306 } 307 308 // GetHeader fetches and validates the beacon header with the given blockRoot. 309 // If blockRoot is null hash then the latest head header is fetched. 310 // The values of the canonical and finalized flags are also returned. Note that 311 // these flags are not validated. 312 func (api *BeaconLightApi) GetHeader(blockRoot common.Hash) (types.Header, bool, bool, error) { 313 var blockId string 314 if blockRoot == (common.Hash{}) { 315 blockId = "head" 316 } else { 317 blockId = blockRoot.Hex() 318 } 319 resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/headers/%s", blockId), nil) 320 if err != nil { 321 return types.Header{}, false, false, err 322 } 323 324 var data struct { 325 Finalized bool `json:"finalized"` 326 Data struct { 327 Root common.Hash `json:"root"` 328 Canonical bool `json:"canonical"` 329 Header struct { 330 Message types.Header `json:"message"` 331 Signature hexutil.Bytes `json:"signature"` 332 } `json:"header"` 333 } `json:"data"` 334 } 335 if err := json.Unmarshal(resp, &data); err != nil { 336 return types.Header{}, false, false, err 337 } 338 header := data.Data.Header.Message 339 if blockRoot == (common.Hash{}) { 340 blockRoot = data.Data.Root 341 } 342 if header.Hash() != blockRoot { 343 return types.Header{}, false, false, errors.New("retrieved beacon header root does not match") 344 } 345 return header, data.Data.Canonical, data.Finalized, nil 346 } 347 348 // GetCheckpointData fetches and validates bootstrap data belonging to the given checkpoint. 349 func (api *BeaconLightApi) GetCheckpointData(checkpointHash common.Hash) (*types.BootstrapData, error) { 350 resp, err := api.httpGet(fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/0x%x", checkpointHash[:]), nil) 351 if err != nil { 352 return nil, err 353 } 354 355 // See data structure definition here: 356 // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md#lightclientbootstrap 357 type bootstrapData struct { 358 Version string `json:"version"` 359 Data struct { 360 Header jsonBeaconHeader `json:"header"` 361 Committee *types.SerializedSyncCommittee `json:"current_sync_committee"` 362 CommitteeBranch merkle.Values `json:"current_sync_committee_branch"` 363 } `json:"data"` 364 } 365 366 var data bootstrapData 367 if err := json.Unmarshal(resp, &data); err != nil { 368 return nil, err 369 } 370 if data.Data.Committee == nil { 371 return nil, errors.New("sync committee is missing") 372 } 373 header := data.Data.Header.Beacon 374 if header.Hash() != checkpointHash { 375 return nil, fmt.Errorf("invalid checkpoint block header, have %v want %v", header.Hash(), checkpointHash) 376 } 377 checkpoint := &types.BootstrapData{ 378 Version: data.Version, 379 Header: header, 380 CommitteeBranch: data.Data.CommitteeBranch, 381 CommitteeRoot: data.Data.Committee.Root(), 382 Committee: data.Data.Committee, 383 } 384 if err := checkpoint.Validate(); err != nil { 385 return nil, fmt.Errorf("invalid checkpoint: %w", err) 386 } 387 if checkpoint.Header.Hash() != checkpointHash { 388 return nil, errors.New("wrong checkpoint hash") 389 } 390 return checkpoint, nil 391 } 392 393 func (api *BeaconLightApi) GetBeaconBlock(blockRoot common.Hash) (*types.BeaconBlock, error) { 394 resp, err := api.httpGet(fmt.Sprintf("/eth/v2/beacon/blocks/0x%x", blockRoot), nil) 395 if err != nil { 396 return nil, err 397 } 398 399 var beaconBlockMessage struct { 400 Version string `json:"version"` 401 Data struct { 402 Message json.RawMessage `json:"message"` 403 } 404 } 405 if err := json.Unmarshal(resp, &beaconBlockMessage); err != nil { 406 return nil, fmt.Errorf("invalid block json data: %v", err) 407 } 408 block, err := types.BlockFromJSON(beaconBlockMessage.Version, beaconBlockMessage.Data.Message) 409 if err != nil { 410 return nil, err 411 } 412 computedRoot := block.Root() 413 if computedRoot != blockRoot { 414 return nil, fmt.Errorf("Beacon block root hash mismatch (expected: %x, got: %x)", blockRoot, computedRoot) 415 } 416 return block, nil 417 } 418 419 func decodeHeadEvent(enc []byte) (uint64, common.Hash, error) { 420 var data struct { 421 Slot common.Decimal `json:"slot"` 422 Block common.Hash `json:"block"` 423 } 424 if err := json.Unmarshal(enc, &data); err != nil { 425 return 0, common.Hash{}, err 426 } 427 return uint64(data.Slot), data.Block, nil 428 } 429 430 type HeadEventListener struct { 431 OnNewHead func(slot uint64, blockRoot common.Hash) 432 OnOptimistic func(head types.OptimisticUpdate) 433 OnFinality func(head types.FinalityUpdate) 434 OnError func(err error) 435 } 436 437 // StartHeadListener creates an event subscription for heads and signed (optimistic) 438 // head updates and calls the specified callback functions when they are received. 439 // The callbacks are also called for the current head and optimistic head at startup. 440 // They are never called concurrently. 441 func (api *BeaconLightApi) StartHeadListener(listener HeadEventListener) func() { 442 var ( 443 ctx, closeCtx = context.WithCancel(context.Background()) 444 streamCh = make(chan *eventsource.Stream, 1) 445 wg sync.WaitGroup 446 ) 447 448 // When connected to a Lodestar node the subscription blocks until the first actual 449 // event arrives; therefore we create the subscription in a separate goroutine while 450 // letting the main goroutine sync up to the current head. 451 wg.Add(1) 452 go func() { 453 defer wg.Done() 454 stream := api.startEventStream(ctx, &listener) 455 if stream == nil { 456 // This case happens when the context was closed. 457 return 458 } 459 // Stream was opened, wait for close signal. 460 streamCh <- stream 461 <-ctx.Done() 462 stream.Close() 463 }() 464 465 wg.Add(1) 466 go func() { 467 defer wg.Done() 468 469 // Request initial data. 470 log.Trace("Requesting initial head header") 471 if head, _, _, err := api.GetHeader(common.Hash{}); err == nil { 472 log.Trace("Retrieved initial head header", "slot", head.Slot, "hash", head.Hash()) 473 listener.OnNewHead(head.Slot, head.Hash()) 474 } else { 475 log.Debug("Failed to retrieve initial head header", "error", err) 476 } 477 log.Trace("Requesting initial optimistic update") 478 if optimisticUpdate, err := api.GetOptimisticUpdate(); err == nil { 479 log.Trace("Retrieved initial optimistic update", "slot", optimisticUpdate.Attested.Slot, "hash", optimisticUpdate.Attested.Hash()) 480 listener.OnOptimistic(optimisticUpdate) 481 } else { 482 log.Debug("Failed to retrieve initial optimistic update", "error", err) 483 } 484 log.Trace("Requesting initial finality update") 485 if finalityUpdate, err := api.GetFinalityUpdate(); err == nil { 486 log.Trace("Retrieved initial finality update", "slot", finalityUpdate.Finalized.Slot, "hash", finalityUpdate.Finalized.Hash()) 487 listener.OnFinality(finalityUpdate) 488 } else { 489 log.Debug("Failed to retrieve initial finality update", "error", err) 490 } 491 492 log.Trace("Starting event stream processing loop") 493 // Receive the stream. 494 var stream *eventsource.Stream 495 select { 496 case stream = <-streamCh: 497 case <-ctx.Done(): 498 log.Trace("Stopping event stream processing loop") 499 return 500 } 501 502 for { 503 select { 504 case event, ok := <-stream.Events: 505 if !ok { 506 log.Trace("Event stream closed") 507 return 508 } 509 log.Trace("New event received from event stream", "type", event.Event()) 510 switch event.Event() { 511 case "head": 512 slot, blockRoot, err := decodeHeadEvent([]byte(event.Data())) 513 if err == nil { 514 listener.OnNewHead(slot, blockRoot) 515 } else { 516 listener.OnError(fmt.Errorf("error decoding head event: %v", err)) 517 } 518 case "light_client_optimistic_update": 519 optimisticUpdate, err := decodeOptimisticUpdate([]byte(event.Data())) 520 if err == nil { 521 listener.OnOptimistic(optimisticUpdate) 522 } else { 523 listener.OnError(fmt.Errorf("error decoding optimistic update event: %v", err)) 524 } 525 case "light_client_finality_update": 526 finalityUpdate, err := decodeFinalityUpdate([]byte(event.Data())) 527 if err == nil { 528 listener.OnFinality(finalityUpdate) 529 } else { 530 listener.OnError(fmt.Errorf("error decoding finality update event: %v", err)) 531 } 532 default: 533 listener.OnError(fmt.Errorf("unexpected event: %s", event.Event())) 534 } 535 536 case err, ok := <-stream.Errors: 537 if !ok { 538 return 539 } 540 listener.OnError(err) 541 } 542 } 543 }() 544 545 return func() { 546 closeCtx() 547 wg.Wait() 548 } 549 } 550 551 // startEventStream establishes an event stream. This will keep retrying until the stream has been 552 // established. It can only return nil when the context is canceled. 553 func (api *BeaconLightApi) startEventStream(ctx context.Context, listener *HeadEventListener) *eventsource.Stream { 554 for retry := true; retry; retry = ctxSleep(ctx, 5*time.Second) { 555 log.Trace("Sending event subscription request") 556 uri, err := api.buildURL("/eth/v1/events", map[string][]string{"topics": {"head", "light_client_finality_update", "light_client_optimistic_update"}}) 557 if err != nil { 558 listener.OnError(fmt.Errorf("error creating event subscription URL: %v", err)) 559 continue 560 } 561 req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) 562 if err != nil { 563 listener.OnError(fmt.Errorf("error creating event subscription request: %v", err)) 564 continue 565 } 566 for k, v := range api.customHeaders { 567 req.Header.Set(k, v) 568 } 569 stream, err := eventsource.SubscribeWithRequest("", req) 570 if err != nil { 571 listener.OnError(fmt.Errorf("error creating event subscription: %v", err)) 572 continue 573 } 574 log.Trace("Successfully created event stream") 575 return stream 576 } 577 return nil 578 } 579 580 func ctxSleep(ctx context.Context, timeout time.Duration) (ok bool) { 581 timer := time.NewTimer(timeout) 582 defer timer.Stop() 583 select { 584 case <-timer.C: 585 return true 586 case <-ctx.Done(): 587 return false 588 } 589 } 590 591 func (api *BeaconLightApi) buildURL(path string, params url.Values) (string, error) { 592 uri, err := url.Parse(api.url) 593 if err != nil { 594 return "", err 595 } 596 uri = uri.JoinPath(path) 597 if params != nil { 598 uri.RawQuery = params.Encode() 599 } 600 return uri.String(), nil 601 }