From 136ebd062ccd6bc5d3f6f9af6b91b897a9e81de8 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 26 Jun 2026 19:56:31 -0400 Subject: [PATCH] feat(frost/roast): attribute lost-sync triggers; document permissioned-set acceptance When a seat enters lost-sync from a transition bundle for an attempt it never observed, the triggering bundle is operator-authenticated -- log the sending seat and the claimed attempt-context hash once per lost-sync episode, so the operational runbook can attribute and remove/slash a member spamming bogus-attempt bundles. (This is an authenticated-insider liveness halt the blame bridge does NOT close, because a never-observed attempt yields no evidence to attribute it.) markLostSync now uses CompareAndSwap and returns whether it transitioned, so the attribution is logged exactly once even while the listener keeps receiving such bundles. No change to the fail-closed semantics. Also document that this residual is accepted under the PERMISSIONED operator set (attributable + liveness-only + governance-removable) and MUST be revisited before any move to a permissionless set, where the f+1 snapshot-corroboration / resync fix would be warranted instead of accepting it. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ition_exchange_frost_native_roast_retry.go | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/frost/signing/roast_transition_exchange_frost_native_roast_retry.go b/pkg/frost/signing/roast_transition_exchange_frost_native_roast_retry.go index 5b0ec79db4..2f02f2ec12 100644 --- a/pkg/frost/signing/roast_transition_exchange_frost_native_roast_retry.go +++ b/pkg/frost/signing/roast_transition_exchange_frost_native_roast_retry.go @@ -166,7 +166,22 @@ func (e *RoastTransitionExchange) onBundle(msg RunnerMessage) { // bridge addresses (PR2b-2) and that stays within 1b's accepted // fail-closed-terminate liveness regression (a single bad actor can already // kill a round by withholding a bundle). - e.markLostSync() + // + // Under the PERMISSIONED operator set this residual is accepted: the + // triggering seat is operator-authenticated (logged below for attribution), + // the action is liveness-only (fail-closed, never an unsafe/divergent + // signature), and a misbehaving operator is governance-removable. REVISIT + // before any move to a PERMISSIONLESS operator set, where an anonymous, + // costless, non-attributable DoS would warrant the f+1 snapshot-corroboration + // (or resync) fix rather than accepting it. + if e.markLostSync() { + e.logger.Warnf( + "roast transition exchange: seat %d entered lost-sync from an "+ + "unobserved-attempt bundle sent by seat %d (attempt context "+ + "hash %x); failing closed before next selection", + e.member, msg.Sender, hash, + ) + } return } e.verifyAndStore(bundle) @@ -174,8 +189,11 @@ func (e *RoastTransitionExchange) onBundle(msg RunnerMessage) { // markLostSync records that this seat received a transition for an attempt it // never observed -- it fell behind the group's committed ROAST attempt chain. -func (e *RoastTransitionExchange) markLostSync() { - e.lostSync.Store(true) +// It returns true only on the first transition into lost-sync, so the caller can +// attribute the triggering bundle exactly once (the listener may keep receiving +// such bundles -- including a spammer's -- while lost-sync stays latched). +func (e *RoastTransitionExchange) markLostSync() bool { + return e.lostSync.CompareAndSwap(false, true) } // HasLostSync reports whether this seat fell behind the group's committed ROAST