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
        }