feat(bifrost/sol): deterministic SOL keysign blockhash via finalized-slot search

Derive the SOL keysign blockhash deterministically from the consensus keysign height — search finalized slots by block time with a bracket-proof guard — instead of each node's scanner recentBlockhash, which fragmented TSS keysign parties (divergent msgID) → stalled SOL outbounds + a TssSyncFail leader slash. board 661.

Fix

  • Derive from the consensus height: GetBlockTimestamp(height) → target t − finalizationMargin → greatest finalized slot (getBlocks) with blockTime ≤ target, via a bracket-proof guard (a node that hasn't finalized past the target abstains rather than returning a divergent slot — the load-bearing invariant). The same anchor drives scheduled outbounds and the keep-alive, so every member signs identical bytes → one keysign party.
  • GetBlockhashAtSlot fetches only the blockhash (transactionDetails:none), not the full block; the derivation is memoized by height with singleflight coalescing.
  • Abstain handling: transient failures (no finalized block yet / IsBlockhashValid RPC error) return an error so the signer retains + retries; a permanently-expired hash returns the (nil,nil,nil,nil) no-op so the item drops and THORChain reschedules at a fresh height. IsBlockhashValid passes minContextSlot so a lagging RPC backend errors (→ retain) instead of falsely reporting a still-live hash expired (→ drop). New rpc.GetBlockTime (*int64, null-safe). A sol_keysign_abstain{reason} metric makes a fleet-wide stall observable.
  • Keep-alive: a fresh node (post-restart) seeds its baseline without signing the first, stale boundary, then signs the next fresh one.

Testing — unit tests (default CI) + stagenet The per-node logic is covered by unit tests that run in default CI: the rpc wrappers (incl. GetBlockTime null-safety and IsBlockhashValid minContextSlot / lagging-backend), the SignTx abstain branches (permanent-drop / transient-retain / lagging-retain), and TestConsensusBlockHashAcrossRPCViews — table-driven proof that every RPC view which brackets the consensus target selects the same slot and blockhash (galloping, snapshot-history bounds, insufficient finalization, lookback cap, tied timestamps, null block times). Cross-node convergence — inherently multi-process — is validated on stagenet at the RC cut on real SOL RPCs and real TSS, rather than an artificial single-process mocknet approximation.

Deferred residuals

  • Leader-abstention slash → go-tss poison/blame track.
  • SOL landing-rate regresses vs baseline until the Track-2 durable-nonce work lands.
  • RPC-search overall timeout bound (would thread a context through the rpc package) — follow-up.
Edited by Itzamna

Merge request reports

Loading
Loading