public async Task ProcessProposalDutiesAsync(Slot slot, CancellationToken cancellationToken) { // If proposer, get block, sign block, return to node // Retry if not successful; need to queue this up to send immediately if connection issue. (or broadcast?) BlsPublicKey?blsPublicKey = _validatorState.GetProposalDutyForSlot(slot); if (!(blsPublicKey is null)) { if (_logger.IsInfo()) { Log.ProposalDutyFor(_logger, slot, blsPublicKey, null); } BlsSignature randaoReveal = GetEpochSignature(slot, blsPublicKey); if (_logger.IsDebug()) { LogDebug.RequestingBlock(_logger, slot, blsPublicKey.ToShortString(), randaoReveal.ToString().Substring(0, 10), null); } BeaconBlock unsignedBlock = await _beaconNodeApi.NewBlockAsync(slot, randaoReveal, cancellationToken).ConfigureAwait(false); BeaconBlock signedBlock = SignBlock(unsignedBlock, blsPublicKey); if (_logger.IsDebug()) { LogDebug.PublishingSignedBlock(_logger, slot, blsPublicKey.ToShortString(), randaoReveal.ToString().Substring(0, 10), signedBlock, signedBlock.Signature.ToString().Substring(0, 10), null); } bool nodeAccepted = await _beaconNodeApi.PublishBlockAsync(signedBlock, cancellationToken).ConfigureAwait(false); _validatorState.ClearProposalDutyForSlot(slot); } }
public override async Task StopAsync(CancellationToken cancellationToken) { if (_logger.IsDebug()) { LogDebug.HonestValidatorWorkerStopping(_logger, null); } _stopped = true; await base.StopAsync(cancellationToken); }
public override async Task StartAsync(CancellationToken cancellationToken) { if (_logger.IsDebug()) { LogDebug.HonestValidatorWorkerStarting(_logger, null); } await base.StartAsync(cancellationToken); if (_logger.IsDebug()) { LogDebug.HonestValidatorWorkerStarted(_logger, null); } }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (_logger.IsInfo()) { Log.HonestValidatorWorkerExecuteStarted(_logger, _clientVersion.Description, _environment.EnvironmentName, Thread.CurrentThread.ManagedThreadId, null); } try { // Config // List of nodes // Validator private keys (or quickstart) // Seconds per slot string nodeVersion = string.Empty; while (nodeVersion == string.Empty) { try { nodeVersion = await _beaconNodeApi.GetNodeVersionAsync(stoppingToken).ConfigureAwait(false); } catch (Exception ex) { Log.WaitingForNodeVersion(_logger, ex); await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken).ConfigureAwait(false); } } ulong genesisTime = 0; while (genesisTime == 0) { try { genesisTime = await _beaconNodeApi.GetGenesisTimeAsync(stoppingToken).ConfigureAwait(false); } catch (Exception ex) { Log.WaitingForGenesisTime(_logger, ex); await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken).ConfigureAwait(false); } } Log.HonestValidatorWorkerConnected(_logger, nodeVersion, genesisTime, null); await _beaconChain.SetGenesisTimeAsync(genesisTime).ConfigureAwait(false); while (!stoppingToken.IsCancellationRequested && !_stopped) { try { DateTimeOffset clockTime = _clock.UtcNow(); ulong time = (ulong)clockTime.ToUnixTimeSeconds(); if (time > genesisTime) { await _validatorClient.OnTickAsync(_beaconChain, time, stoppingToken).ConfigureAwait(false); } // 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.HonestValidatorWorkerLoopError(_logger, ex); } } } } catch (Exception ex) { Log.HonestValidatorWorkerCriticalError(_logger, ex); throw; } if (_logger.IsDebug()) { LogDebug.HonestValidatorWorkerExecuteExiting(_logger, Thread.CurrentThread.ManagedThreadId, null); } }
public async Task UpdateDutiesActivityAsync(Epoch epoch, CancellationToken cancellationToken) { Activity activity = new Activity("update-duties"); activity.Start(); try { IList <BlsPublicKey> publicKeys = _validatorKeyProvider.GetPublicKeys(); ApiResponse <IList <ValidatorDuty> > validatorDutiesResponse = await _beaconNodeApi.ValidatorDutiesAsync(publicKeys, epoch, cancellationToken); if (validatorDutiesResponse.StatusCode != StatusCode.Success) { Log.ErrorGettingValidatorDuties(_logger, (int)validatorDutiesResponse.StatusCode, validatorDutiesResponse.StatusCode, null); return; } // Record proposal duties first, in case there is an error foreach (ValidatorDuty validatorDuty in validatorDutiesResponse.Content) { Slot?currentProposalSlot = _validatorState.ProposalSlot.GetValueOrDefault(validatorDuty.ValidatorPublicKey); if (validatorDuty.BlockProposalSlot != Slot.None && validatorDuty.BlockProposalSlot != currentProposalSlot) { _validatorState.SetProposalDuty(validatorDuty.ValidatorPublicKey, validatorDuty.BlockProposalSlot); if (_logger.IsInfo()) { Log.ValidatorDutyProposalChanged(_logger, validatorDuty.ValidatorPublicKey, epoch, validatorDuty.BlockProposalSlot, null); } } } foreach (ValidatorDuty validatorDuty in validatorDutiesResponse.Content) { Slot?currentAttestationSlot = _validatorState.AttestationSlot.GetValueOrDefault(validatorDuty.ValidatorPublicKey); Shard?currentAttestationShard = _validatorState.AttestationShard.GetValueOrDefault(validatorDuty.ValidatorPublicKey); if (validatorDuty.AttestationSlot != currentAttestationSlot || validatorDuty.AttestationShard != currentAttestationShard) { _validatorState.SetAttestationDuty(validatorDuty.ValidatorPublicKey, validatorDuty.AttestationSlot, validatorDuty.AttestationShard); if (_logger.IsDebug()) { LogDebug.ValidatorDutyAttestationChanged(_logger, validatorDuty.ValidatorPublicKey, epoch, validatorDuty.AttestationSlot, validatorDuty.AttestationShard, null); } } } } catch (Exception ex) { Log.ExceptionGettingValidatorDuties(_logger, ex.Message, ex); } finally { activity.Stop(); } }
public async Task ProcessProposalDutiesAsync(Slot slot, CancellationToken cancellationToken) { // If proposer, get block, sign block, return to node // Retry if not successful; need to queue this up to send immediately if connection issue. (or broadcast?) BlsPublicKey?blsPublicKey = _validatorState.GetProposalDutyForSlot(slot); if (!(blsPublicKey is null)) { Activity activity = new Activity("process-proposal-duty"); activity.Start(); try { if (_logger.IsInfo()) { Log.ProposalDutyFor(_logger, slot, _beaconChainInformation.Time, blsPublicKey, null); } BlsSignature randaoReveal = GetEpochSignature(slot, blsPublicKey); if (_logger.IsDebug()) { LogDebug.RequestingBlock(_logger, slot, blsPublicKey.ToShortString(), randaoReveal.ToString().Substring(0, 10), null); } ApiResponse <BeaconBlock> newBlockResponse = await _beaconNodeApi .NewBlockAsync(slot, randaoReveal, cancellationToken).ConfigureAwait(false); if (newBlockResponse.StatusCode == StatusCode.Success) { BeaconBlock unsignedBlock = newBlockResponse.Content; BlsSignature blockSignature = GetBlockSignature(unsignedBlock, blsPublicKey); SignedBeaconBlock signedBlock = new SignedBeaconBlock(unsignedBlock, blockSignature); if (_logger.IsDebug()) { LogDebug.PublishingSignedBlock(_logger, slot, blsPublicKey.ToShortString(), randaoReveal.ToString().Substring(0, 10), signedBlock.Message, signedBlock.Signature.ToString().Substring(0, 10), null); } ApiResponse publishBlockResponse = await _beaconNodeApi .PublishBlockAsync(signedBlock, cancellationToken) .ConfigureAwait(false); if (publishBlockResponse.StatusCode != StatusCode.Success && publishBlockResponse.StatusCode != StatusCode.BroadcastButFailedValidation) { throw new Exception( $"Error response from publish: {(int) publishBlockResponse.StatusCode} {publishBlockResponse.StatusCode}."); } bool nodeAccepted = publishBlockResponse.StatusCode == StatusCode.Success; // TODO: Log warning if not accepted? Not sure what else we could do. _validatorState.ClearProposalDutyForSlot(slot); } } catch (Exception ex) { Log.ExceptionProcessingProposalDuty(_logger, slot, blsPublicKey, ex.Message, ex); } finally { activity.Stop(); } } }
public async Task OnTickAsync(BeaconChainInformation beaconChainInformation, ulong time, CancellationToken cancellationToken) { // update time await beaconChainInformation.SetTimeAsync(time).ConfigureAwait(false); Slot currentSlot = GetCurrentSlot(beaconChainInformation); // TODO: attestation is done 1/3 way through slot // Not a new slot (clock is still the same as the slot we just checked), return bool shouldCheckSlot = currentSlot > beaconChainInformation.LastSlotChecked; if (!shouldCheckSlot) { return; } await UpdateForkVersionActivityAsync(cancellationToken).ConfigureAwait(false); // TODO: For beacon nodes that don't report SyncingStatus (which is nullable/optional), // then need a different strategy to determine the current head known by the beacon node. // The current code (2020-03-15) will simply start from slot 0 and process 1/second until // caught up with clock slot; at 6 seconds/slot, this is 6x faster, i.e. 1 day still takes 4 hours. // (okay for testing). // Alternative 1: see if the node supports /beacon/head, and use that slot // Alternative 2: try UpdateDuties, and if you get 406 DutiesNotAvailableForRequestedEpoch then use a // divide & conquer algorithm to determine block to check. (Could be a back off x1, x2, x4, x8, etc if 406) await UpdateSyncStatusActivityAsync(cancellationToken).ConfigureAwait(false); // Need to have an anchor block (will provide the genesis time) before can do anything // Absolutely no point in generating blocks < anchor slot // Irrespective of clock time, can't check duties >= 2 epochs ahead of current (head), as don't have data // - actually, validator only cares about what they need to sign this slot // - the beacon node takes care of subscribing to topics an epoch in advance, etc // While signing a block < highest (seen) slot may be a waste, there is no penalty for doing so // Generally no point in generating blocks <= current (head) slot Slot slotToCheck = Slot.Max(beaconChainInformation.LastSlotChecked, beaconChainInformation.SyncStatus.CurrentSlot) + Slot.One; LogDebug.ProcessingSlot(_logger, slotToCheck, currentSlot, beaconChainInformation.SyncStatus.CurrentSlot, null); // Slot is set before processing; if there is an error (in process; update duties has a try/catch), it will skip to the next slot // (maybe the error will be resolved; trade off of whether error can be fixed by retrying, e.g. network error, // but potentially getting stuck in a slot, vs missing a slot) // TODO: Maybe add a retry policy/retry count when to advance last slot checked regardless await beaconChainInformation.SetLastSlotChecked(slotToCheck); // TODO: Attestations should run checks one epoch ahead, for topic subscriptions, although this is more a beacon node thing to do. // Note that UpdateDuties will continue even if there is an error/issue (i.e. assume no change and process what we have) Epoch epochToCheck = ComputeEpochAtSlot(slotToCheck); await UpdateDutiesActivityAsync(epochToCheck, cancellationToken).ConfigureAwait(false); await ProcessProposalDutiesAsync(slotToCheck, cancellationToken).ConfigureAwait(false); // If upcoming attester, join (or change) topics // Subscribe to topics // Attest 1/3 way through slot }
public async Task UpdateDutiesActivityAsync(Epoch epoch, CancellationToken cancellationToken) { Activity activity = new Activity("update-duties"); activity.Start(); using IDisposable activityScope = _logger.BeginScope("[TraceId, {TraceId}], [SpanId, {SpanId}]", activity.TraceId, activity.SpanId); try { IList <BlsPublicKey> publicKeys = _validatorKeyProvider.GetPublicKeys(); ApiResponse <IList <ValidatorDuty> > validatorDutiesResponse = await _beaconNodeApi.ValidatorDutiesAsync(publicKeys, epoch, cancellationToken); if (validatorDutiesResponse.StatusCode != StatusCode.Success) { Log.ErrorGettingValidatorDuties(_logger, (int)validatorDutiesResponse.StatusCode, validatorDutiesResponse.StatusCode, null); return; } // Record proposal duties first, in case there is an error foreach (ValidatorDuty validatorDuty in validatorDutiesResponse.Content) { if (validatorDuty.BlockProposalSlot.HasValue) { Slot?currentProposalSlot = _validatorState.ProposalSlot.GetValueOrDefault(validatorDuty.ValidatorPublicKey); bool needsProposalUpdate = validatorDuty.BlockProposalSlot != currentProposalSlot; if (needsProposalUpdate) { _validatorState.SetProposalDuty(validatorDuty.ValidatorPublicKey, validatorDuty.BlockProposalSlot.Value); if (_logger.IsInfo()) { Log.ValidatorDutyProposalChanged(_logger, validatorDuty.ValidatorPublicKey, epoch, validatorDuty.BlockProposalSlot.Value, null); } } } } foreach (ValidatorDuty validatorDuty in validatorDutiesResponse.Content) { if (validatorDuty.AttestationSlot.HasValue) { bool needsAttestationUpdate; if (_validatorState.AttestationSlotAndIndex.TryGetValue(validatorDuty.ValidatorPublicKey, out (Slot, CommitteeIndex)currentValue)) { (Slot currentAttestationSlot, CommitteeIndex currentAttestationIndex) = currentValue; needsAttestationUpdate = validatorDuty.AttestationSlot != currentAttestationSlot || validatorDuty.AttestationIndex != currentAttestationIndex; } else { needsAttestationUpdate = true; } if (needsAttestationUpdate) { _validatorState.SetAttestationDuty(validatorDuty.ValidatorPublicKey, validatorDuty.AttestationSlot.Value, validatorDuty.AttestationIndex !.Value); if (_logger.IsDebug()) { LogDebug.ValidatorDutyAttestationChanged(_logger, validatorDuty.ValidatorPublicKey, epoch, validatorDuty.AttestationSlot.Value, validatorDuty.AttestationIndex.Value, null); } } } } } catch (Exception ex) { Log.ExceptionGettingValidatorDuties(_logger, ex.Message, ex); } finally { activity.Stop(); } }
public async Task ProcessAttestationDutiesAsync(Slot slot, CancellationToken cancellationToken) { // If attester, get attestation, sign attestation, return to node IList <(BlsPublicKey, CommitteeIndex)> attestationDutyList = _validatorState.GetAttestationDutyForSlot(slot); foreach ((BlsPublicKey validatorPublicKey, CommitteeIndex index) in attestationDutyList) { Activity activity = new Activity("process-attestation-duty"); activity.Start(); using IDisposable activityScope = _logger.BeginScope("[TraceId, {TraceId}], [SpanId, {SpanId}]", activity.TraceId, activity.SpanId); try { if (_logger.IsDebug()) { LogDebug.RequestingAttestationFor(_logger, slot, _beaconChainInformation.Time, validatorPublicKey, null); } ApiResponse <Attestation> newAttestationResponse = await _beaconNodeApi .NewAttestationAsync(validatorPublicKey, false, slot, index, cancellationToken) .ConfigureAwait(false); if (newAttestationResponse.StatusCode == StatusCode.Success) { Attestation unsignedAttestation = newAttestationResponse.Content; BlsSignature attestationSignature = await GetAttestationSignatureAsync(unsignedAttestation, validatorPublicKey) .ConfigureAwait(false); Attestation signedAttestation = new Attestation(unsignedAttestation.AggregationBits, unsignedAttestation.Data, attestationSignature); // TODO: Getting one attestation at a time probably isn't very scalable. // All validators are attesting the same data, just in different committees with different indexes // => Get the data once, group relevant validators by committee, sign and aggregate within each // committee (marking relevant aggregation bits), then publish one pre-aggregated value? if (_logger.IsDebug()) { LogDebug.PublishingSignedAttestation(_logger, slot, index, validatorPublicKey.ToShortString(), signedAttestation.Data, signedAttestation.Signature.ToString().Substring(0, 10), null); } ApiResponse publishAttestationResponse = await _beaconNodeApi .PublishAttestationAsync(signedAttestation, cancellationToken) .ConfigureAwait(false); if (publishAttestationResponse.StatusCode != StatusCode.Success && publishAttestationResponse.StatusCode != StatusCode.BroadcastButFailedValidation) { throw new Exception( $"Error response from publish: {(int) publishAttestationResponse.StatusCode} {publishAttestationResponse.StatusCode}."); } bool nodeAccepted = publishAttestationResponse.StatusCode == StatusCode.Success; // TODO: Log warning if not accepted? Not sure what else we could do. } } catch (Exception ex) { Log.ExceptionProcessingAttestationDuty(_logger, slot, validatorPublicKey, ex.Message, ex); } finally { activity.Stop(); } _validatorState.ClearAttestationDutyForSlot(slot); } }