Skip to content

Commit ca6ee3b

Browse files
authored
feat(tbtc/signer): member-key interactive signing state for multi-seat operators (#4098)
## What Keys a session's interactive signing state by `member_identifier` (`interactive_signing: Option<InteractiveSigningState>` → `BTreeMap<u16, InteractiveSigningState>`) so a **multi-seat operator can run interactive signing for all its seats in one process**. ## Why A multi-seat operator runs concurrent interactive signing per seat, all sharing one member-independent `SessionID`, in one process (the engine state is a process-global singleton). Today that breaks three ways: the 2nd seat's `InteractiveSessionOpen(same session, different member)` → `SessionConflict`; `round2` frees the whole state (wiping siblings); and the per-attempt consumed-nonce marker makes a sibling's `round2` falsely trip `ConsumedNonceReplay`. Surfaced by the keep-core real-cgo e2e (#4097). ## Design (reviewed: Gemini sound, Codex approve-with-refinements) Both reviewers rejected a session-wide live-attempt model — it would let one seat erase another's nonces, the exact bug being removed. - **Per-member live attempt**: `open(M, strictly-newer attempt)` replaces only M's entry (zeroizing M's old nonces); a stale/equal open for M is rejected, but a sibling on a different attempt is untouched. Seats are independent — exactly as separate processes would be. - **round2** removes only the member's entry (siblings stay live); the durable marker carries replay protection. - **Consumed markers** keyed per-`(attempt_id, member_id)` (composite); a **legacy bare `attempt_id` marker is honored fail-closed** on read. `aggregated_interactive_attempt_markers` stay per-attempt (one signature per attempt over public data). - **abort** is session-level over the map (removes all matching entries); **TTL sweep** is per-entry by `opened_at_unix` (siblings survive); **capacity** counts live member entries (a new member is a slot; a same-member replacement isn't). - `interactive_state_for_attempt_mut` is a member-keyed lookup. Live state still never persists (empty map on reload). Zeroization preserved via `InteractiveRound1State::Drop`. ## Tests New `interactive_multi_seat_two_members_one_process_aggregate_bip340`: two seats open the same attempt in one process, Round1 independently, member 1's Round2 frees only its entry and writes only its marker (member 2 stays live and unblocked), and the two interactive shares **aggregate to a valid BIP-340 signature**. Existing single-member tests updated for the map. **All 292 lib tests pass; `cargo fmt` clean.** Follow-ups (additional edge tests Codex itemized — per-member attempt advance, abort-by-attempt over multiple members, capacity new-vs-replacement) can land in review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 9fd9845 + d857577 commit ca6ee3b

4 files changed

Lines changed: 900 additions & 114 deletions

File tree

0 commit comments

Comments
 (0)