feat(app): wire priority + infosync into the node - #574
Conversation
Mount the priority protocol behaviour and construct the infosync component in the node's P2P wiring, and trigger a cluster-wide priority exchange on the last slot of each epoch. The priority component rides the existing QBFT consensus and shares the node-wide P2P context; infosync advertises this node's supported versions, protocols, and proposal types. This is the participation half of #402 part B ("A"): it makes pluto take part in the per-epoch priority/QBFT info_sync round so mixed clusters with Charon reach quorum. Consuming the decided result (routing the duty path through ConsensusController and registering the protocol-switch subscriber) is a functional no-op while QBFTv2 is the only consensus protocol and is left as a TODO(#402 part B). Verified on a 2-charon + 2-pluto kurtosis cluster: all four nodes reach identical info_sync results, and Charon's "protocols not supported: [charon/priority/2.0.0]" and priority "consensus timeout" warnings stop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| let priority_deadline_calc = | ||
| pluto_core::deadline::DutyDeadlineCalculator::from_client(ð2_cl) | ||
| .await | ||
| .map_err(AppError::Deadline)?; |
There was a problem hiding this comment.
You can clone the existing dealine calculator and not create a new one:
| let priority_deadline_calc = | |
| pluto_core::deadline::DutyDeadlineCalculator::from_client(ð2_cl) | |
| .await | |
| .map_err(AppError::Deadline)?; | |
| let priority_deadline_calc = Arc::clone(&deadline_calc); |
There was a problem hiding this comment.
Done in 669fe0f — dropped the second DutyDeadlineCalculator and now pass Arc::clone(&deadline_calc) (the shared Arc<dyn DeadlineCalculator>); wire_p2p takes the Arc and new_component accepts it via the blanket impl DeadlineCalculator for Arc<T>.
| async move { | ||
| if slot.last_in_epoch() | ||
| && let Err(err) = infosync.trigger(ct.child_token(), slot.slot).await | ||
| { | ||
| tracing::warn!(%err, slot = ?slot.slot, "infosync trigger failed"); | ||
| } | ||
| Ok::<(), AppError>(()) | ||
| } |
There was a problem hiding this comment.
The subscribe_slot handles errors by logging already, so we can propagate errors:
| async move { | |
| if slot.last_in_epoch() | |
| && let Err(err) = infosync.trigger(ct.child_token(), slot.slot).await | |
| { | |
| tracing::warn!(%err, slot = ?slot.slot, "infosync trigger failed"); | |
| } | |
| Ok::<(), AppError>(()) | |
| } | |
| async move { | |
| if slot.last_in_epoch() { | |
| infosync.trigger(ct.child_token(), slot.slot).await?; | |
| } | |
| Ok::<(), AppError>(()) | |
| } |
There was a problem hiding this comment.
Done in 669fe0f — the trigger error now propagates via ? and subscribe_slot logs it; dropped the manual warn!.
| pub infosync: Arc<pluto_infosync::Component>, | ||
| } | ||
|
|
||
| /// Composes the core behaviours and builds the libp2p [`Node`]. |
There was a problem hiding this comment.
Probably better to revisit this with the new set of arguments.
There was a problem hiding this comment.
Done in 669fe0f — replaced the 14 positional args (and the too_many_arguments allow) with a WireP2PParams struct destructured at the top of wire_p2p.
… wiring tests - wire_p2p: replace the 14 positional args (and its too_many_arguments allow) with a WireP2PParams struct destructured at the top. - Reuse the shared Arc<dyn DeadlineCalculator> for priority instead of building a second DutyDeadlineCalculator; wire_p2p now takes the Arc. - infosync slot subscriber: propagate the trigger error via `?` (the scheduler's subscribe_slot already logs it) instead of catching/warning. - Extract local_protocols()/local_proposal_types() as pure helpers and unit-test the Charon-parity protocol precedence and builder-first proposal ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Condense the wiring comments to the rationale that isn't obvious from the code (Charon parity, 6s exchange timeout, move-only expired receiver, deferred consensus-switch), dropping restatements of the code itself. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Wires the
priorityprotocol behaviour andinfosynccomponent into the running node, and triggers a cluster-wide priority exchange on the last slot of each epoch.Previously both crates were implemented and tested but never mounted (
crates/app/src/node/mod.rscarried aTODO(#402 part B)), so a pluto node neither advertisedcharon/priority/2.0.0nor proposed into the info_sync QBFT round. In a mixed cluster this made Charon peers logprotocols not supported: [charon/priority/2.0.0]andPriority protocol consensus: consensus timeoutevery epoch.Scope — participation only ("A")
This is the participation half of #402 part B:
CoreBehaviourgains apriorityfield, constructed viapluto_priority::new_componenton the sharedp2p_contextand riding the existingArc<qbft::Consensus>(priority uses raw QBFT, matching Charon'swirePrioritise).infosync::Componentis built withversion::SUPPORTED, the concatenatedprotocols()of consensus/parsigex/peerinfo/priority, and[Builder?]+Fullproposal types.Slot::last_in_epoch()(6s exchange timeout,min_required= cluster threshold — both matching Charon).Deferred (
TODO(#402 part B)): consuming the decided result — routing the duty path throughConsensusControllerand registering the protocol-switch subscriber (set_current_consensus_for_protocol). That is a functional no-op while QBFTv2 is the only consensus protocol, so it is intentionally left out. This PR does not wireproposals()into the fetcher, because Charon does not either (itsProposals()/Protocols()accessors are dead code; full-vs-blinded is decided by--builder-api).Verification
cargo +nightly fmt --all --check,cargo clippy --workspace --all-targets --all-features -- -D warnings,cargo test --workspace --all-featuresall pass.protocol: qbft=4000, parsigex=3996, peerinfo=3992, priority=3988), proving pluto'scalculateResultis deterministic-equivalent to Charon's. Charon'sprotocols not supportedandconsensus timeoutwarnings stop; regular duties are unaffected.🤖 Generated with Claude Code