Skip to content

feat(request): deterministic proposer rotation with scheduled round timeout - #1051

Open
yvonneanne wants to merge 3 commits into
developfrom
feat/move1-deterministic-rotation
Open

feat(request): deterministic proposer rotation with scheduled round timeout#1051
yvonneanne wants to merge 3 commits into
developfrom
feat/move1-deterministic-rotation

Conversation

@yvonneanne

@yvonneanne yvonneanne commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #907. Realizes the design in #228; original intent of #182.

What

  • Election (organize.rs): drop the forward scan for a locally-active proposer, the entropy-seeded fallback, and the state.round = selected_round jump. proposer(r) is now a pure function of (round, participants, entropy); state.round advances only through bump_round.
  • Schedule (mod.rs, state.rs): T(0) keeps the generous inherited budget; T(r ≥ 1) starts at a short floor and grows by 23/20 up to a separate ceiling, so rotating past a dead proposer no longer costs a full round-0 wait.
  • wait_for_active_participants is untouched — it gates supply, not election.
  • proposer_per_round reduces both operands before adding: round derives from highest_seen_round, which a peer sets unbounded, so the bare sum overflows.
  • The deliberator's post-Accept wait carried an unexplained 500ms literal (ACCEPT_POSIT_TIMEOUT + 500ms); it is now 2 * ACCEPT_POSIT_TIMEOUT, and the schedule's floor is asserted to exceed it.

Why

  • Nodes with differing liveness views elected different proposers and advanced to different rounds, so they never co-located and the request could not settle.
  • This PR un-ignores the test from the stacked PR below: it stalls indefinitely without this change, settles with it.

Cost, deliberately paid

  • A dead proposer now burns T(r) before rotating, where the local filter used to skip it free. That is the price of peer-identical election; skipping needs the local knowledge being removed.
  • T(r ≥ 1) being short is what buys the cost down. Rounds 1..18 are shorter than the old uniform 20s, so an alive-but-contended proposer (permit or presignature wait) has less time than before.
  • 23/20 over doubling: most failed rounds are dead-or-unable proposers, where the cheapest early rounds waste least, and rotation changes proposer every round so doubling would penalise a healthy node for its predecessors. A round only doubles after five rotations. Multiplicative over additive, though: a fixed step clears a late node's indexing skew by one step however long it has been growing, leaving a window too short to complete a round in, and a ceiling below that skew leaves the two permanently offset since both advance at the same rate.

Open question: per-chain T(0)?

  • T(0) decides whether this change is free or a latency regression, and one global value serves budgets differing by three orders of magnitude.
  • Chain::expected_response_time_secs: NEAR/Solana 11s, Hydration 29, Canton 35, Ethereum 3605, Bitcoin ~4805. At T(0) = 20s a single dead round-0 proposer is over budget on NEAR and Solana; the regression fires with probability ≈ (nodes down)/n.
  • chain is on-chain request data, so a per-chain T(0) stays peer-identical
  • Options: one global T(0) sized for the tightest chain / derive from expected_response_time_secs / a per-chain constant beside expected_finality_time_secs. Flagging, not choosing.

Known exposure, not introduced here

entropy is requester-supplied, and entropy[0] alone fixes the rotation offset, so a requester can grind for a cycle that front-loads nodes it believes are down. Bounded (one round each) but repeatable per request. The local filter used to mask it.

Using an input fixed after submission works (block hash), which IndexedSignRequest does not carry. This is a separate issue, not addressed in this PR.

Tests

  • round_timeout_schedule — every round outlasts the post-Accept wait, rotation is cheaper than round 0, the cap is reached.
  • rotation_reaches_live_proposer_within_f_plus_1_rounds — exhaustive over down-sets and start rounds for n = 4, 5. n = 4 is load-bearing: a stride sharing a factor with n covers only half the participants, which an odd n masks.
  • The now-active integration test from the stacked PR.

@yvonneanne
yvonneanne force-pushed the test/divergent-active-views branch from 3951ff6 to 382bfe1 Compare July 24, 2026 07:39
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch from 8834349 to 950e9d6 Compare July 24, 2026 07:39
@yvonneanne
yvonneanne force-pushed the test/divergent-active-views branch from 382bfe1 to 0390ebb Compare July 24, 2026 07:40
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch from 950e9d6 to 41a5ffb Compare July 24, 2026 07:40
Comment thread chain-signatures/node/src/protocol/request/organize.rs Outdated
Comment thread chain-signatures/node/src/protocol/request/organize.rs
@yvonneanne
yvonneanne marked this pull request as ready for review July 27, 2026 09:50
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch 3 times, most recently from 6593179 to c728123 Compare July 27, 2026 10:37
@yvonneanne
yvonneanne requested review from jakmeier and volovyks July 27, 2026 11:34

@volovyks volovyks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I believe this is the right strategy not to use active when choosing a proposer.
At the same time, we may try to understand why #1050 fails, since we rely on active in other places.

@yvonneanne

Copy link
Copy Markdown
Contributor Author

Overall, I believe this is the right strategy not to use active when choosing a proposer. At the same time, we may try to understand why #1050 fails, since we rely on active in other places.

Thanks! I don't think #1050 points at a bug in active: the test manufactures the divergence by construction (each node permanently hides its successor), so it's a fault model, not a symptom. The mechanism is accounted for: election filtered through a local-only set and wrote the scan's landing round into state.round, so views differing by one participant land on different rounds and never co-locate.

The fair concern underneath is that divergent views are normal in production. So the line here is: peer-identical decisions (proposer, round, deadline) derive only from (round, participants, entropy); local liveness is still fine for single-decider or local choices (mesh gate, presignature intersection, addressing).

Maybe we should make divergence observable (export active-set size + membership hash per node) so we can see how often views actually disagree?

@volovyks

Copy link
Copy Markdown
Contributor

@yvonneanne Agreed. Yes, we need to understand whether the active set actually differs in practice and, if it does, why.

We have an endpoint that shows the node's state, but a long that is triggered only on active set change is the simplest option.

Base automatically changed from test/divergent-active-views to develop July 29, 2026 06:02
@yvonneanne
yvonneanne requested a review from ppca July 29, 2026 09:02
Comment thread integration-tests/tests/cases/mpc_with_stream.rs
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch 2 times, most recently from 16b0be6 to 833f045 Compare July 29, 2026 18:33
Comment thread chain-signatures/node/src/protocol/request/mod.rs Outdated
Comment thread chain-signatures/node/src/protocol/request/mod.rs
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch 2 times, most recently from 098d2ae to 2086b5b Compare July 30, 2026 07:55
…imeout

Proposer election read the local active set to skip over peers it believed
were down. Nodes with divergent active views then elected different
proposers for the same round, so accepts never reached threshold and the
request never settled (#907).

Elect from inputs every node shares instead — round, membership, entropy —
and drop the search window. Rotation is a plain offset-plus-round modulo
`n`, which visits every participant before repeating, so a request still
reaches a live proposer within `f + 1` rounds.

Round deadlines now come from `round_timeout(r)`, a pure function of the
round: round 0 keeps the full `ORGANIZE_POSIT_TIMEOUT`, later rounds start
at a floor derived from `ACCEPT_POSIT_TIMEOUT` and grow back up to it.
Peers that agree on the round agree on the deadline.

Un-ignores test_divergent_active_views_still_converge.
Rounds grew by a fixed 1s step and stopped at ORGANIZE_POSIT_TIMEOUT.
Both parts break recovery from staggered indexing: nodes that observe a
request at different times enter the same round at different moments and
can only transact while both are inside it, so a round must outgrow that
skew. A ceiling below the skew leaves them permanently offset, since both
advance at the same rate; and an additive step clears the skew by one
step however long it has been growing, leaving a window too short to
complete a round.

Grow by 23/20 instead and cap at a separate, much higher
ROUND_TIMEOUT_CEILING. Any ratio above 1 eventually crosses the skew, so
the factor is set by what it costs the other case: rotating past dead
proposers wants rounds short. At 23/20 a round only doubles after five
rotations (2, 2.3, 2.6, 3, 3.5 against the previous 2, 3, 4, 5, 6), so
early rotation is cheaper than before, while a 60s skew is crossed at
round 26 with 5.8s of overlap rather than never.
… wait

min_viable was the deliberator's post-Accept obligation, not the cost of
completing a round — the assertion let a round through that cleared the
skew but couldn't fit a Propose broadcast plus accept gathering. Bar on
ROUND_TIMEOUT_FLOOR instead, and check that a larger skew gets a wider
settling window, which a fixed step can't do but a ratio can. Also tighten
the window-scaling guard to the ceiling minus a full round, the actual
bound settling_overlap needs to succeed, and document the tolerable-skew
bound on ROUND_TIMEOUT_CEILING itself.
@yvonneanne
yvonneanne force-pushed the feat/move1-deterministic-rotation branch from 1630f29 to 3ef8353 Compare July 30, 2026 08:41

@jakmeier jakmeier left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall great changes, as discussed in our call

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

signature generation: remove checking for active

4 participants