Esempio n. 1
0
 public override async Task StopAsync(CancellationToken cancellationToken)
 {
     if (_logger.IsDebug())
     {
         LogDebug.BeaconNodeWorkerStopping(_logger, null);
     }
     _stopped = true;
     await base.StopAsync(cancellationToken);
 }
Esempio n. 2
0
        public override async Task StartAsync(CancellationToken cancellationToken)
        {
            if (_logger.IsDebug())
            {
                LogDebug.BeaconNodeWorkerStarting(_logger, null);
            }
            await base.StartAsync(cancellationToken);

            if (_logger.IsDebug())
            {
                LogDebug.BeaconNodeWorkerStarted(_logger, null);
            }
        }
Esempio n. 3
0
        public async Task <ApiResponse> PublishBlockAsync(SignedBeaconBlock signedBlock,
                                                          CancellationToken cancellationToken)
        {
            try
            {
                if (!_store.IsInitialized)
                {
                    return(new ApiResponse(StatusCode.CurrentlySyncing));
                }

                bool acceptedLocally = false;
                try
                {
                    await _forkChoice.OnBlockAsync(_store, signedBlock).ConfigureAwait(false);

                    // TODO: validate as per honest validator spec and return true/false
                    acceptedLocally = true;
                }
                catch (Exception ex)
                {
                    if (_logger.IsWarn())
                    {
                        Log.BlockNotAcceptedLocally(_logger, signedBlock.Message, ex);
                    }
                }

                if (_logger.IsDebug())
                {
                    LogDebug.PublishingBlockToNetwork(_logger, signedBlock.Message, null);
                }
                await _networkPeering.PublishBeaconBlockAsync(signedBlock).ConfigureAwait(false);

                if (acceptedLocally)
                {
                    return(new ApiResponse(StatusCode.Success));
                }
                else
                {
                    return(new ApiResponse(StatusCode.BroadcastButFailedValidation));
                }
            }
            catch (Exception ex)
            {
                if (_logger.IsWarn())
                {
                    Log.ApiErrorPublishBlock(_logger, ex);
                }
                throw;
            }
        }
        public async Task OnPeerDialOutConnected(string peerId)
        {
            Root headRoot = await _forkChoice.GetHeadAsync(_store).ConfigureAwait(false);

            BeaconState beaconState = await _store.GetBlockStateAsync(headRoot).ConfigureAwait(false);

            // Send request
            var status = BuildStatusFromHead(headRoot, beaconState);

            if (_logger.IsDebug())
            {
                LogDebug.SendingStatusToPeer(_logger, RpcDirection.Request, status, peerId, null);
            }
            await _networkPeering.SendStatusAsync(peerId, RpcDirection.Request, status).ConfigureAwait(false);
        }
        private async Task HandlePeerStatus(string peerId, PeeringStatus peerPeeringStatus, Root headRoot,
                                            BeaconState beaconState)
        {
            // if not valid, "immediately disconnect from one another following the handshake"
            bool isValidPeer = await IsValidPeerStatus(peerId, peerPeeringStatus, headRoot, beaconState)
                               .ConfigureAwait(false);

            if (!isValidPeer)
            {
                await _networkPeering.DisconnectPeerAsync(peerId).ConfigureAwait(false);

                return;
            }

            // check if we should request blocks
            var isPeerAhead = IsPeerAhead(peerPeeringStatus, beaconState);

            if (isPeerAhead)
            {
                // In theory, our chain since finalized checkpoint could be wrong
                // However it may be more efficient to check if our head is correct and sync from there,
                // or use the step option to sample blocks and find where we diverge.
                Slot finalizedSlot = _beaconChainUtility.ComputeStartSlotOfEpoch(beaconState.FinalizedCheckpoint.Epoch);

                if (_logger.IsInfo())
                {
                    Log.RequestingBlocksFromAheadPeer(_logger, peerId, finalizedSlot, peerPeeringStatus.HeadRoot,
                                                      peerPeeringStatus.HeadSlot, null);
                }

                // TODO: Need more sophistication, like Eth1; as peers are discovered, just put into a pool,
                // then, when need for sync determined, select the best peer(s) to use.

                await _networkPeering.RequestBlocksAsync(peerId, peerPeeringStatus.HeadRoot, finalizedSlot,
                                                         peerPeeringStatus.HeadSlot);
            }
            else
            {
                if (_logger.IsDebug())
                {
                    LogDebug.PeerBehind(_logger, peerId, peerPeeringStatus.FinalizedEpoch, peerPeeringStatus.HeadRoot,
                                        peerPeeringStatus.HeadSlot, null);
                }
            }
        }
        public async Task OnStatusRequestReceived(string peerId, PeeringStatus peerPeeringStatus)
        {
            Root headRoot = await _forkChoice.GetHeadAsync(_store).ConfigureAwait(false);

            BeaconState beaconState = await _store.GetBlockStateAsync(headRoot).ConfigureAwait(false);

            // Send response
            var status = BuildStatusFromHead(headRoot, beaconState);

            if (_logger.IsDebug())
            {
                LogDebug.SendingStatusToPeer(_logger, RpcDirection.Response, status, peerId, null);
            }
            await _networkPeering.SendStatusAsync(peerId, RpcDirection.Response, status).ConfigureAwait(false);

            // Determine if the peer is valid, and if we need to request blocks
            await HandlePeerStatus(peerId, peerPeeringStatus, headRoot, beaconState);
        }
Esempio n. 7
0
        /// <param name="eth1BlockHash"></param>
        /// <param name="eth1Timestamp"></param>
        /// <param name="deposits"></param>
        /// <returns></returns>
        public async Task <bool> TryGenesisAsync(Bytes32 eth1BlockHash, ulong eth1Timestamp)
        {
            if (_logger.IsDebug())
            {
                LogDebug.TryGenesis(_logger, eth1BlockHash, eth1Timestamp, (uint)_depositStore.Deposits.Count, null);
            }

            BeaconState candidateState = InitializeBeaconStateFromEth1(eth1BlockHash, eth1Timestamp);

            if (IsValidGenesisState(candidateState))
            {
                BeaconState genesisState = candidateState;
                await _forkChoice.InitializeForkChoiceStoreAsync(_store, genesisState);

                return(true);
            }

            return(false);
        }
Esempio n. 8
0
        public async Task <bool> TryGenesisAsync(Hash32 eth1BlockHash, ulong eth1Timestamp, IList <Deposit> deposits)
        {
            return(await Task.Run(() =>
            {
                if (_logger.IsDebug())
                {
                    LogDebug.TryGenesis(_logger, eth1BlockHash, eth1Timestamp, deposits.Count, null);
                }

                BeaconState candidateState = _genesis.InitializeBeaconStateFromEth1(eth1BlockHash, eth1Timestamp, deposits);
                if (_genesis.IsValidGenesisState(candidateState))
                {
                    BeaconState genesisState = candidateState;
                    _ = _forkChoice.GetGenesisStore(genesisState);
                    return true;
                }
                return false;
            }));
        }
Esempio n. 9
0
        public async Task <IList <ValidatorDuty> > GetValidatorDutiesAsync(
            IList <BlsPublicKey> validatorPublicKeys,
            Epoch?optionalEpoch)
        {
            Root head = await _forkChoice.GetHeadAsync(_store).ConfigureAwait(false);

            BeaconState headState = await _store.GetBlockStateAsync(head).ConfigureAwait(false);

            Epoch currentEpoch = _beaconStateAccessor.GetCurrentEpoch(headState);
            Epoch epoch        = optionalEpoch ?? currentEpoch;

            Epoch nextEpoch = currentEpoch + Epoch.One;

            if (epoch > nextEpoch)
            {
                throw new ArgumentOutOfRangeException(nameof(epoch), epoch,
                                                      $"Duties cannot look ahead more than the next epoch {nextEpoch}.");
            }

            Slot startSlot      = _beaconChainUtility.ComputeStartSlotOfEpoch(epoch);
            Root epochStartRoot = await _store.GetAncestorAsync(head, startSlot);

            TimeParameters timeParameters = _timeParameterOptions.CurrentValue;

            (Root epochStartRoot, Epoch epoch)cacheKey = (epochStartRoot, epoch);
            ConcurrentDictionary <BlsPublicKey, ValidatorDuty> dutiesForEpoch =
                await _validatorAssignmentsCache.Cache.GetOrCreateAsync(cacheKey, entry =>
            {
                entry.SlidingExpiration = TimeSpan.FromSeconds(2 * timeParameters.SecondsPerSlot);
                return(Task.FromResult(new ConcurrentDictionary <BlsPublicKey, ValidatorDuty>()));
            }).ConfigureAwait(false);

            IEnumerable <BlsPublicKey> missingValidators = validatorPublicKeys.Except(dutiesForEpoch.Keys);

            if (missingValidators.Any())
            {
                if (_logger.IsDebug())
                {
                    LogDebug.GettingMissingValidatorDutiesForCache(_logger, missingValidators.Count(), epoch,
                                                                   epochStartRoot,
                                                                   null);
                }

                BeaconState storedState = await _store.GetBlockStateAsync(epochStartRoot);

                // Clone, so that it can be safely mutated (transitioned forward)
                BeaconState state = BeaconState.Clone(storedState);

                // Transition to start slot, of target epoch (may have been a skip slot, i.e. stored state root may have been older)
                _beaconStateTransition.ProcessSlots(state, startSlot);

                // Check validators are valid (if not duties are empty).
                IList <DutyDetails> dutyDetailsList = new List <DutyDetails>();
                foreach (BlsPublicKey validatorPublicKey in missingValidators)
                {
                    ValidatorIndex?validatorIndex = FindValidatorIndexByPublicKey(state, validatorPublicKey);

                    if (validatorIndex.HasValue)
                    {
                        bool validatorActive = CheckIfValidatorActive(state, validatorIndex.Value);
                        if (validatorActive)
                        {
                            dutyDetailsList.Add(new DutyDetails(validatorPublicKey, validatorIndex.Value));
                        }
                        else
                        {
                            if (_logger.IsWarn())
                            {
                                Log.ValidatorNotActiveAtEpoch(_logger, epoch, validatorIndex.Value, validatorPublicKey,
                                                              null);
                            }
                            dutiesForEpoch[validatorPublicKey] =
                                new ValidatorDuty(validatorPublicKey, Slot.None, CommitteeIndex.None, Slot.None);
                        }
                    }
                    else
                    {
                        if (_logger.IsWarn())
                        {
                            Log.ValidatorNotFoundAtEpoch(_logger, epoch, validatorPublicKey, null);
                        }
                        dutiesForEpoch[validatorPublicKey] =
                            new ValidatorDuty(validatorPublicKey, Slot.None, CommitteeIndex.None, Slot.None);
                    }
                }

                if (dutyDetailsList.Any())
                {
                    // Check starting state
                    UpdateDutyDetailsForState(dutyDetailsList, state);

                    // Check other slots in epoch, if needed
                    Slot endSlotExclusive = startSlot + new Slot(timeParameters.SlotsPerEpoch);
                    Slot slotToCheck      = startSlot + Slot.One;
                    while (slotToCheck < endSlotExclusive)
                    {
                        _beaconStateTransition.ProcessSlots(state, slotToCheck);
                        UpdateDutyDetailsForState(dutyDetailsList, state);
                        slotToCheck += Slot.One;
                    }

                    // Active validators should always have attestation slots; warn if they don't
                    foreach (var dutyDetails in dutyDetailsList)
                    {
                        if (!dutyDetails.AttestationSlot.HasValue)
                        {
                            if (_logger.IsWarn())
                            {
                                Log.ValidatorDoesNotHaveAttestationSlot(_logger, epoch, dutyDetails.ValidatorPublicKey,
                                                                        null);
                            }
                        }
                    }

                    // Add to cached dictionary
                    foreach (var dutyDetails in dutyDetailsList)
                    {
                        ValidatorDuty validatorDuty =
                            new ValidatorDuty(dutyDetails.ValidatorPublicKey,
                                              dutyDetails.AttestationSlot,
                                              dutyDetails.AttestationCommitteeIndex,
                                              dutyDetails.BlockProposalSlot);
                        dutiesForEpoch[dutyDetails.ValidatorPublicKey] = validatorDuty;
                    }
                }
            }

            return(dutiesForEpoch
                   .Where(x => validatorPublicKeys.Contains(x.Key))
                   .Select(x => x.Value)
                   .ToList());
        }
Esempio n. 10
0
        public async Task OnBlockAsync(IStore store, SignedBeaconBlock signedBlock)
        {
            MiscellaneousParameters miscellaneousParameters = _miscellaneousParameterOptions.CurrentValue;
            MaxOperationsPerBlock   maxOperationsPerBlock   = _maxOperationsPerBlockOptions.CurrentValue;

            BeaconBlock block     = signedBlock.Message;
            Root        blockRoot = _cryptographyService.HashTreeRoot(block);

            if (_logger.IsInfo())
            {
                Log.OnBlock(_logger, blockRoot, block, signedBlock.Signature, null);
            }

            // Make a copy of the state to avoid mutability issues
            BeaconState parentState = await store.GetBlockStateAsync(block.ParentRoot).ConfigureAwait(false);

            BeaconState preState = BeaconState.Clone(parentState);

            // Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past.
            ulong storeTime        = store.Time;
            Slot  storeCurrentSlot = GetCurrentSlot(store);

            if (storeCurrentSlot < block.Slot)
            {
                throw new ArgumentOutOfRangeException(nameof(block), block.Slot,
                                                      $"Block slot time cannot be in the future, compared to store time {storeTime} (slot {storeCurrentSlot}, since genesis {store.GenesisTime}.");
            }

            // Add new block to the store
            await store.SetSignedBlockAsync(blockRoot, signedBlock).ConfigureAwait(false);

            // Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor)
            Slot finalizedSlot = _beaconChainUtility.ComputeStartSlotOfEpoch(store.FinalizedCheckpoint.Epoch);

            if (block.Slot <= finalizedSlot)
            {
                throw new ArgumentOutOfRangeException(nameof(block), block.Slot,
                                                      $"Block slot must be later than the finalized epoch start slot {finalizedSlot}.");
            }

            // Check block is a descendant of the finalized block at the checkpoint finalized slot
            Root ancestorOfBlockAtFinalizedSlot =
                await GetAncestorAsync(store, blockRoot, finalizedSlot).ConfigureAwait(false);

            if (!ancestorOfBlockAtFinalizedSlot.Equals(store.FinalizedCheckpoint.Root))
            {
                throw new Exception(
                          $"Block with hash tree root {blockRoot} is not a descendant of the finalized block {store.FinalizedCheckpoint.Root} at slot {finalizedSlot}.");
            }

            // Check the block is valid and compute the post-state
            BeaconState state = _beaconStateTransition.StateTransition(preState, signedBlock, validateResult: true);

            // Add new state for this block to the store
            await store.SetBlockStateAsync(blockRoot, state).ConfigureAwait(false);

            if (_logger.IsDebug())
            {
                LogDebug.AddedBlockToStore(_logger, block, state, blockRoot, null);
            }

            // Update justified checkpoint
            if (state.CurrentJustifiedCheckpoint.Epoch > store.JustifiedCheckpoint.Epoch)
            {
                if (state.CurrentJustifiedCheckpoint.Epoch > store.BestJustifiedCheckpoint.Epoch)
                {
                    await store.SetBestJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);
                }

                bool shouldUpdateJustifiedCheckpoint =
                    await ShouldUpdateJustifiedCheckpointAsync(store, state.CurrentJustifiedCheckpoint)
                    .ConfigureAwait(false);

                if (shouldUpdateJustifiedCheckpoint)
                {
                    await store.SetJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                    if (_logger.IsDebug())
                    {
                        LogDebug.UpdateJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                    }
                }
                else
                {
                    if (_logger.IsDebug())
                    {
                        LogDebug.UpdateBestJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                    }
                }
            }

            // Update finalized checkpoint
            if (state.FinalizedCheckpoint.Epoch > store.FinalizedCheckpoint.Epoch)
            {
                await store.SetFinalizedCheckpointAsync(state.FinalizedCheckpoint).ConfigureAwait(false);

                if (_logger.IsDebug())
                {
                    LogDebug.UpdateFinalizedCheckpoint(_logger, state.FinalizedCheckpoint, null);
                }

                // Update justified if new justified is later than store justified
                // or if store justified is not in chain with finalized checkpoint
                if (state.CurrentJustifiedCheckpoint.Epoch > store.JustifiedCheckpoint.Epoch)
                {
                    await store.SetJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                    if (_logger.IsDebug())
                    {
                        LogDebug.UpdateJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                    }
                }
                else
                {
                    Slot newFinalizedSlot =
                        _beaconChainUtility.ComputeStartSlotOfEpoch(store.FinalizedCheckpoint.Epoch);
                    Root ancestorOfJustifiedAtNewFinalizedSlot =
                        await GetAncestorAsync(store, store.JustifiedCheckpoint.Root, newFinalizedSlot)
                        .ConfigureAwait(false);

                    if (!ancestorOfJustifiedAtNewFinalizedSlot.Equals(store.FinalizedCheckpoint.Root))
                    {
                        await store.SetJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                        if (_logger.IsDebug())
                        {
                            LogDebug.UpdateJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                        }
                    }
                }
            }
        }
Esempio n. 11
0
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            if (_logger.IsInfo())
            {
                Log.BeaconNodeWorkerExecuteStarted(_logger, _clientVersion.Description,
                                                   _dataDirectory.ResolvedPath, _environment.EnvironmentName, Thread.CurrentThread.ManagedThreadId,
                                                   null);
            }

            try
            {
                await _nodeStart.InitializeNodeAsync();

                bool started = false;
                while (!stoppingToken.IsCancellationRequested && !_stopped)
                {
                    try
                    {
                        DateTimeOffset clockTime = _clock.UtcNow();
                        ulong          time      = (ulong)clockTime.ToUnixTimeSeconds();

                        if (_store.IsInitialized)
                        {
                            if (!started)
                            {
                                if (_logger.IsInfo())
                                {
                                    long slotValue = ((long)time - (long)_store.GenesisTime) /
                                                     _timeParameterOptions.CurrentValue.SecondsPerSlot;
                                    Log.WorkerStoreAvailableTickStarted(_logger, _store !.GenesisTime, time, slotValue,
                                                                        Thread.CurrentThread.ManagedThreadId, null);
                                }

                                started = true;
                            }

                            if (time >= _store.GenesisTime)
                            {
                                await _forkChoice.OnTickAsync(_store, time);
                            }
                            else
                            {
                                long timeToGenesis = (long)_store.GenesisTime - (long)time;
                                if (timeToGenesis < 10 || timeToGenesis % 10 == 0)
                                {
                                    if (_logger.IsInfo())
                                    {
                                        Log.GenesisCountdown(_logger, timeToGenesis, null);
                                    }
                                }
                            }
                        }

                        // Wait for remaining time, if any
                        // NOTE: To fast forward time during testing, have the second call to test _clock.Now() jump forward to avoid waiting.
                        DateTimeOffset nextClockTime = DateTimeOffset.FromUnixTimeSeconds((long)time + 1);
                        TimeSpan       remaining     = nextClockTime - _clock.UtcNow();
                        if (remaining > TimeSpan.Zero)
                        {
                            await Task.Delay(remaining, stoppingToken);
                        }
                    }
                    catch (TaskCanceledException)
                    {
                        // This is expected when exiting
                    }
                    catch (Exception ex)
                    {
                        if (_logger.IsError())
                        {
                            Log.BeaconNodeWorkerLoopError(_logger, ex);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.BeaconNodeWorkerCriticalError(_logger, ex);
                throw;
            }

            if (_logger.IsDebug())
            {
                LogDebug.BeaconNodeWorkerExecuteExiting(_logger, Thread.CurrentThread.ManagedThreadId, null);
            }
        }
Esempio n. 12
0
        public async Task OnBlockAsync(IStore store, BeaconBlock block)
        {
            MiscellaneousParameters miscellaneousParameters = _miscellaneousParameterOptions.CurrentValue;
            MaxOperationsPerBlock   maxOperationsPerBlock   = _maxOperationsPerBlockOptions.CurrentValue;

            Hash32 signingRoot = _cryptographyService.SigningRoot(block);

            if (_logger.IsInfo())
            {
                Log.OnBlock(_logger, signingRoot, block, null);
            }

            // Make a copy of the state to avoid mutability issues
            BeaconState parentState = await store.GetBlockStateAsync(block.ParentRoot).ConfigureAwait(false);

            BeaconState preState = BeaconState.Clone(parentState);

            // Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past.
            ulong blockTime = preState.GenesisTime + (ulong)block.Slot * _timeParameterOptions.CurrentValue.SecondsPerSlot;

            if (blockTime > store.Time)
            {
                throw new ArgumentOutOfRangeException(nameof(block), blockTime, $"Block slot time cannot be in the future, compared to store time {store.Time}.");
            }

            // Add new block to the store
            await store.SetBlockAsync(signingRoot, block).ConfigureAwait(false);

            // Check block is a descendant of the finalized block
            BeaconBlock finalizedCheckpointBlock = await store.GetBlockAsync(store.FinalizedCheckpoint.Root).ConfigureAwait(false);

            Hash32 ancestor = await GetAncestorAsync(store, signingRoot, finalizedCheckpointBlock !.Slot).ConfigureAwait(false);

            if (ancestor != store.FinalizedCheckpoint.Root)
            {
                throw new Exception($"Block with signing root {signingRoot} is not a descendant of the finalized block {store.FinalizedCheckpoint.Root} at slot {finalizedCheckpointBlock.Slot}.");
            }

            // Check that block is later than the finalized epoch slot
            Slot finalizedEpochStartSlot = _beaconChainUtility.ComputeStartSlotOfEpoch(store.FinalizedCheckpoint.Epoch);

            if (block.Slot <= finalizedEpochStartSlot)
            {
                throw new ArgumentOutOfRangeException(nameof(block), block.Slot, $"Block slot must be later than the finalized epoch start slot {finalizedEpochStartSlot}.");
            }

            // Check the block is valid and compute the post-state
            BeaconState state = _beaconStateTransition.StateTransition(preState, block, validateStateRoot: true);

            // Add new state for this block to the store
            await store.SetBlockStateAsync(signingRoot, state).ConfigureAwait(false);

            if (_logger.IsDebug())
            {
                LogDebug.AddedBlockToStore(_logger, block, state, signingRoot, null);
            }

            // Update justified checkpoint
            if (state.CurrentJustifiedCheckpoint.Epoch > store.JustifiedCheckpoint.Epoch)
            {
                await store.SetBestJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                bool shouldUpdateJustifiedCheckpoint = await ShouldUpdateJustifiedCheckpointAsync(store, state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                if (shouldUpdateJustifiedCheckpoint)
                {
                    await store.SetJustifiedCheckpointAsync(state.CurrentJustifiedCheckpoint).ConfigureAwait(false);

                    if (_logger.IsDebug())
                    {
                        LogDebug.UpdateJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                    }
                }
                else
                {
                    if (_logger.IsDebug())
                    {
                        LogDebug.UpdateBestJustifiedCheckpoint(_logger, state.CurrentJustifiedCheckpoint, null);
                    }
                }
            }

            // Update finalized checkpoint
            if (state.FinalizedCheckpoint.Epoch > store.FinalizedCheckpoint.Epoch)
            {
                await store.SetFinalizedCheckpointAsync(state.FinalizedCheckpoint).ConfigureAwait(false);

                if (_logger.IsDebug())
                {
                    LogDebug.UpdateFinalizedCheckpoint(_logger, state.FinalizedCheckpoint, null);
                }
            }
        }
Esempio n. 13
0
        public async Task <ApiResponse> PublishAttestationAsync(Attestation signedAttestation,
                                                                CancellationToken cancellationToken)
        {
            try
            {
                if (!_store.IsInitialized)
                {
                    return(new ApiResponse(StatusCode.CurrentlySyncing));
                }

                // NOTE: For an attestation generated 1/3 way during the slot, it will fail validation (as it is not in the past),
                // therefore it will never be accepted locally, but always broadcast.
                // i.e. broadcast, aggregated, added to operations pool, and pulled out to be included in subsequent blocks,
                // then applied (from the block)
                // But we want to include valid attestations in fork choice rules.
                // So, if might be valid in future, then hold until it is; i.e. check each slot
                // Once pending attestations are validated, add to latest message, but also to pool (for future blocks)
                // Need to exclude from pool if included in another block (but only if/when building on that chain)
                // A reorg could make previously invalid attestations now valid, or previously included attestations free, etc
                // i.e. don't remove, but need to keep/cache until finalisation (of target) has passed

                bool acceptedLocally = false;
                try
                {
                    await _forkChoice.OnAttestationAsync(_store, signedAttestation).ConfigureAwait(false);

                    acceptedLocally = true;
                }
                catch (Exception ex)
                {
                    if (_logger.IsWarn())
                    {
                        Log.AttestationNotAcceptedLocally(_logger, signedAttestation, ex);
                    }
                }

                // TODO: Network publishing
                if (_logger.IsDebug())
                {
                    LogDebug.PublishingAttestationToNetwork(_logger, signedAttestation, null);
                }
                await _networkPeering.PublishAttestationAsync(signedAttestation).ConfigureAwait(false);

                if (acceptedLocally)
                {
                    return(new ApiResponse(StatusCode.Success));
                }
                else
                {
                    return(new ApiResponse(StatusCode.BroadcastButFailedValidation));
                }
            }
            catch (Exception ex)
            {
                if (_logger.IsWarn())
                {
                    Log.ApiErrorPublishAttestation(_logger, ex);
                }
                throw;
            }
        }