Fix backup witness ordering and skipped rows on the schedule page

Three problems on /schedule, the first two verified against hived's own scheduling code in libraries/chain/witness_schedule.cpp.

  1. The backup witness schedule was not in scheduling order

hived fills the single backup ("timeshare") seat from its by_schedule_time index, which is ordered by (virtual_scheduled_time, witness id). Two separate things were wrong here.

Wrong sort key. The list was sorted by virtual_scheduled_time - virtual_last_update but update_witness_schedule4 sets, for every witness it processes, virtual_last_update = new_virtual_time virtual_scheduled_time = new_virtual_time + LAP2 / (votes + 1) so that difference is exactly LAP2 / (votes + 1) - a pure function of the vote weight that only changes when votes change. The panel was therefore really sorted by votes descending. Witnesses processed in the same round share one virtual_last_update, so the ordering happened to look right near the top and drifted further down, which is why the symptom was intermittent: a witness about to take the seat could sit far down the list because it had few votes.

Wrong pool, which mattered more. Candidates came from get_witnesses_by_vote(limit 100). The backup seat is regularly taken by a witness well outside the top 100 by votes - a live round was observed going to "witness4all", who is not in that list at all - so the next witness was not merely misplaced, it was missing from the panel entirely.

The queue now comes from database_api.list_witnesses(order: by_schedule_time), which is that index, over all witnesses rather than the best voted ones. Witnesses with an unset block signing key are dropped, because hived skips them when filling the seat (witness_object::is_disabled). Sorting is done by the chain, so no client side comparison of the 128 bit value is needed.

The current and next round are both excluded from the queue: taking a seat pushes a witness' virtual scheduled time to the back, so leaving them in showed whoever was about to produce as if they were last in line. Since HF26 the next round is already built, so its backup seat is read from future_shuffled_witnesses (get_witness_schedule now requests include_future) and shown first as fact rather than prediction.

Checked against the chain over five consecutive rounds: the panel's order matched the witnesses that actually took the seat, in order.

  1. The green bar skipped witnesses that had produced

"Produced" was derived from head block numbers accumulated while polling. getHafbeLastSyncedBlock can advance by more than one block between two polls, and any witness in the gap was never recorded, so it kept the unproduced styling and showed no block - the bar appeared to jump over it.

A witness' turn is decided by its slot in the round, not by whether we caught its block, so the row state is now derived from the position of the head block's producer in current_shuffled_witnesses. Block numbers come from the last blocks list, which cannot miss a block; that list trails the head block by one fetch, so the head block is folded in as well and observations are accumulated, otherwise the witness that just finished briefly lost its number.

Note the schedule array cannot be mapped onto block numbers arithmetically: get_scheduled_witness indexes it by aslot, so missed slots drift the alignment (a live round was observed offset by two). Only blocks within the current round are shown, bounded below by head - index, since a missed slot produces no block and can only make a round shorter.

Verified over 45 samples covering 17 consecutive producers: one highlighted row throughout, index and block number both advancing by one each time, and no produced row left without a block number.

  1. Block numbers and witness names were not links

Witness names now link to /@ and block numbers to /block/, in both the witness schedule and the backup schedule.

Side effects

  • useLastBlocks takes an optional limit; the home page keeps its previous default and query behaviour.
  • The backup query no longer invalidates itself inside an effect, which had it refetching in a loop; it derives from a memo and refreshes on an interval.

Merge request reports

Loading