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